Compare commits
688 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
4803be1987 | ||
|
|
1f415db44f | ||
|
|
0e316b1d55 | ||
|
|
eb545e75fb | ||
|
|
6edb5c30d5 | ||
|
|
597ed6eaa0 | ||
|
|
2b47d7494e | ||
|
|
213a88f62f | ||
|
|
07fd2e88a2 | ||
|
|
639afe881c | ||
|
|
2e718c0e9d | ||
|
|
b0a8fc165c | ||
|
|
ba6044e9e8 | ||
|
|
7f1c13a576 | ||
|
|
63c5e35e2b | ||
|
|
62e6a7d7fa | ||
|
|
e5a3dae332 | ||
|
|
b45a7663b3 | ||
|
|
6ef904f62b | ||
|
|
6d21cf3084 | ||
|
|
32bd96b6e2 | ||
|
|
fb5da76247 | ||
|
|
e588f51824 | ||
|
|
3e419c4955 | ||
|
|
606d2bafac | ||
|
|
8ac3c49286 | ||
|
|
534aa84ed0 | ||
|
|
04d17cb580 | ||
|
|
d039006eb4 | ||
|
|
fb04f62115 | ||
|
|
3bffccc48e | ||
|
|
eef9abf0bf | ||
|
|
de5ada30b7 | ||
|
|
12f7d0a516 | ||
|
|
0aa9c7c592 | ||
|
|
2216c8dc1c | ||
|
|
984270ebe1 | ||
|
|
2e2658ab6f | ||
|
|
1370f2a76b | ||
|
|
75dedf391a | ||
|
|
7b5c640d05 | ||
|
|
aa9a21b4d0 | ||
|
|
71de8014d5 | ||
|
|
80476d19f9 | ||
|
|
15103d18ef | ||
|
|
0dbd2004ad | ||
|
|
8c92566889 | ||
|
|
fb9449038b | ||
|
|
e06c4a873d | ||
|
|
c4c28c6c82 | ||
|
|
42ff9b803a | ||
|
|
3831e9739c | ||
|
|
f196e5cca2 | ||
|
|
d3af9105ee | ||
|
|
6d685ae4d6 | ||
|
|
8381d8246a | ||
|
|
b26322fc20 | ||
|
|
1c1e8127d8 | ||
|
|
1b3b4406ff | ||
|
|
cf0b77518a | ||
|
|
afdbf44e23 | ||
|
|
ec87781956 | ||
|
|
a6ae958be7 | ||
|
|
312103ef1b | ||
|
|
c2911bb2b7 | ||
|
|
8ca5e38121 | ||
|
|
4b8ad3a8a7 | ||
|
|
f219c2649d | ||
|
|
cfde54261b | ||
|
|
71a82b0a34 | ||
|
|
b7bd2d2664 | ||
|
|
cd26a0770d | ||
|
|
46893e84c3 | ||
|
|
567dcaf79d | ||
|
|
9368c7e05f | ||
|
|
654b3e9dbe | ||
|
|
f09db490f0 | ||
|
|
30d93cfde7 | ||
|
|
41b3db7d6b | ||
|
|
2a60debceb | ||
|
|
eb30642b6f | ||
|
|
ea85e2af6b | ||
|
|
ef979a0839 | ||
|
|
e0107b1dda | ||
|
|
ccc00f913d | ||
|
|
ad3c6bdc88 | ||
|
|
8fe3891ea9 | ||
|
|
63f21952f4 | ||
|
|
361d643ce7 | ||
|
|
abe1ffaab6 | ||
|
|
fc24c91dde | ||
|
|
53cabd5ee4 | ||
|
|
2b1e8cdbee | ||
|
|
9715146495 | ||
|
|
22b0b89949 | ||
|
|
2ebc23a777 | ||
|
|
0199285319 | ||
|
|
277ab2fe44 | ||
|
|
8a96dfdc8a | ||
|
|
66fbbb940a | ||
|
|
716ea1bb3c | ||
|
|
3d701d3daa | ||
|
|
598c74657c | ||
|
|
4bd53d5ab0 | ||
|
|
70f8d54a31 | ||
|
|
4ef25a33fc | ||
|
|
f5dd90a8dd | ||
|
|
a84defd689 | ||
|
|
1cf88d9540 | ||
|
|
644a0ee8c8 | ||
|
|
e9d5dc8fee | ||
|
|
8003202beb | ||
|
|
b46432b5b6 | ||
|
|
5e3f03df06 | ||
|
|
8ab8e6679a | ||
|
|
786b896018 | ||
|
|
40723f8705 | ||
|
|
2a0721bddf | ||
|
|
ff01e4a5e7 | ||
|
|
6794aff77c | ||
|
|
636f2a36b1 | ||
|
|
eee652cefe | ||
|
|
6d45cd45d1 | ||
|
|
f5fb135793 | ||
|
|
6bf32c978a | ||
|
|
8d3011fb9c | ||
|
|
9260066fa3 | ||
|
|
5e45c5805b | ||
|
|
db4de12767 | ||
|
|
d429795737 | ||
|
|
276219a691 | ||
|
|
03c1df98f4 | ||
|
|
79ba750dd5 | ||
|
|
1d0e187838 | ||
|
|
ad1e48aa2d | ||
|
|
7032eea045 | ||
|
|
bdb970203c | ||
|
|
fa4f5abc78 | ||
|
|
0c7b05b233 | ||
|
|
4ca98b5f17 | ||
|
|
4e00c78410 | ||
|
|
17adb19c0d | ||
|
|
1db936e253 | ||
|
|
7194ba7e0e | ||
|
|
59b9b6f091 | ||
|
|
c1ec8d15f3 | ||
|
|
24ba6abc6b | ||
|
|
f6c1bba3b6 | ||
|
|
a606961a22 | ||
|
|
cafe0e4ec2 | ||
|
|
e28c1266cf | ||
|
|
c1605a4f22 | ||
|
|
7aeb55de70 | ||
|
|
8ca65f9fda | ||
|
|
94524d1156 | ||
|
|
a1ed03478b | ||
|
|
402a6379b9 | ||
|
|
5d45bcd552 | ||
|
|
f1fa64c170 | ||
|
|
50fc78564c | ||
|
|
3e5863dc8a | ||
|
|
94b447a9c5 | ||
|
|
78d769797f | ||
|
|
672baae126 | ||
|
|
e942d71ed2 | ||
|
|
f5d24cf86c | ||
|
|
f63b1cd56d | ||
|
|
66719b3cda | ||
|
|
a5e9f6a6fc | ||
|
|
f821afdf3e | ||
|
|
2c61de83c6 | ||
|
|
6da6f75b88 | ||
|
|
a55807a708 | ||
|
|
fce86b0d08 | ||
|
|
d26b503dca | ||
|
|
5363839ac8 | ||
|
|
715a4bf393 | ||
|
|
8f83ecee65 | ||
|
|
2eed4bda42 | ||
|
|
f4e1e24ca7 | ||
|
|
05c540e6cc | ||
|
|
9656390c87 | ||
|
|
4b6470d1e1 | ||
|
|
56471c2fe4 | ||
|
|
9f56e4a582 | ||
|
|
12ea860eba | ||
|
|
b876c29862 | ||
|
|
6bbce039aa | ||
|
|
1584f20220 | ||
|
|
dcad5abc1c | ||
|
|
ab73261fd4 | ||
|
|
05b75c0a44 | ||
|
|
ba7ef0788e | ||
|
|
3aaa80974e | ||
|
|
995ca32eee | ||
|
|
bf5f48b85b | ||
|
|
d6e386a555 | ||
|
|
a0a71f683c | ||
|
|
7adf88b55b | ||
|
|
8a9d47fc4b | ||
|
|
2a0a69c917 | ||
|
|
aeab8f55bd | ||
|
|
9407050598 | ||
|
|
b99da63306 | ||
|
|
f0d6cfaae4 | ||
|
|
3120628d8a | ||
|
|
2654384461 | ||
|
|
eac3b25dc9 | ||
|
|
7788f91dd5 | ||
|
|
d0c9b7170c | ||
|
|
d84caa5528 | ||
|
|
2ab72bdf94 | ||
|
|
f6833fde29 | ||
|
|
fa8a50b525 | ||
|
|
d80c6bbf1d | ||
|
|
6f3ac4bf2a | ||
|
|
a6dc81a38e | ||
|
|
81c5ce40d4 | ||
|
|
c59f45a37b | ||
|
|
1b01f908e3 | ||
|
|
9720812a78 | ||
|
|
05b4066ba6 | ||
|
|
50c458b6cc | ||
|
|
2ab6d61a61 | ||
|
|
b77a39bdff | ||
|
|
d3f7432861 | ||
|
|
7f3ef5bf85 | ||
|
|
659fb3eb82 | ||
|
|
d1315bb092 | ||
|
|
b4ac0e2e7c | ||
|
|
bfe619272e | ||
|
|
963f025011 | ||
|
|
b8cdcaeb75 | ||
|
|
6b6dc75152 | ||
|
|
23647445d7 | ||
|
|
e60dda5027 | ||
|
|
f39551952f | ||
|
|
a9538052bf | ||
|
|
267d5179f5 | ||
|
|
10b8c93da4 | ||
|
|
c999f0c2cd | ||
|
|
54615dc03b | ||
|
|
9aea95ce85 | ||
|
|
80f48291f3 | ||
|
|
1a164cee3e | ||
|
|
da494cdc7c | ||
|
|
06635dfa75 | ||
|
|
a56fb3c8cd | ||
|
|
2dc3c62bbd | ||
|
|
0339d0caa8 | ||
|
|
3b5678dd91 | ||
|
|
82ff34234d | ||
|
|
f3d1369764 | ||
|
|
ed61444d82 | ||
|
|
ce0d68a8ba | ||
|
|
74aadbadb8 | ||
|
|
58f41eddd9 | ||
|
|
4726445ec4 | ||
|
|
3a85384377 | ||
|
|
d20b529508 | ||
|
|
7199f558e8 | ||
|
|
674cb24a1a | ||
|
|
02c7336315 | ||
|
|
cde052d819 | ||
|
|
989cc8d236 | ||
|
|
1186d63653 | ||
|
|
6e68d6dda0 | ||
|
|
7d876701b3 | ||
|
|
dbbb483853 | ||
|
|
85e9473d56 | ||
|
|
427d424707 | ||
|
|
f90c5fafa4 |
1
.dockerignore
Normal file
@@ -0,0 +1 @@
|
||||
docker
|
||||
25
.editorconfig
Normal file
@@ -0,0 +1,25 @@
|
||||
; This file is for unifying the coding style for different editors and IDEs.
|
||||
; Plugins are available for notepad++, emacs, vim, gedit,
|
||||
; textmate, visual studio, and more.
|
||||
;
|
||||
; See http://editorconfig.org for details.
|
||||
|
||||
# Top-most EditorConfig file.
|
||||
root = true
|
||||
|
||||
[*]
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.sh]
|
||||
indent_style = tab
|
||||
|
||||
[*.go]
|
||||
indent_style = tab
|
||||
|
||||
[Makefile]
|
||||
indent_style = tab
|
||||
|
||||
[*.mcl]
|
||||
indent_style = tab
|
||||
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
@@ -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
@@ -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
@@ -1,9 +1,16 @@
|
||||
.idea/
|
||||
.omv/
|
||||
.ssh/
|
||||
.vagrant/
|
||||
mgmt-documentation.pdf
|
||||
.envrc
|
||||
old/
|
||||
tmp/
|
||||
*_stringer.go
|
||||
bindata/*.go
|
||||
mgmt
|
||||
mgmt.static
|
||||
# crossbuild artifacts
|
||||
build/mgmt-*
|
||||
mgmt.iml
|
||||
rpmbuild/
|
||||
*.deb
|
||||
|
||||
24
.gitmodules
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
[submodule "vendor/github.com/coreos/etcd"]
|
||||
path = vendor/github.com/coreos/etcd
|
||||
url = https://github.com/coreos/etcd/
|
||||
[submodule "vendor/google.golang.org/grpc"]
|
||||
path = vendor/google.golang.org/grpc
|
||||
url = https://github.com/grpc/grpc-go
|
||||
[submodule "vendor/github.com/grpc-ecosystem/grpc-gateway"]
|
||||
path = vendor/github.com/grpc-ecosystem/grpc-gateway
|
||||
url = https://github.com/grpc-ecosystem/grpc-gateway
|
||||
[submodule "vendor/gopkg.in/fsnotify.v1"]
|
||||
path = vendor/gopkg.in/fsnotify.v1
|
||||
url = https://gopkg.in/fsnotify.v1
|
||||
[submodule "vendor/github.com/purpleidea/go-systemd"]
|
||||
path = vendor/github.com/purpleidea/go-systemd
|
||||
url = https://github.com/purpleidea/go-systemd
|
||||
[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
|
||||
34
.travis.yml
@@ -1,19 +1,40 @@
|
||||
language: go
|
||||
os:
|
||||
- linux
|
||||
go:
|
||||
- 1.4.3
|
||||
- 1.5.2
|
||||
- 1.9.x
|
||||
- 1.10.x
|
||||
- tip
|
||||
go_import_path: github.com/purpleidea/mgmt
|
||||
sudo: true
|
||||
dist: trusty
|
||||
sudo: required
|
||||
before_install:
|
||||
# as per a number of comments online, this might mitigate some flaky fails...
|
||||
- if [[ "$TRAVIS_OS_NAME" != "osx" ]]; then sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 0C49F3730359A14518585931BC711F9BA15703C6; fi
|
||||
# apt update tends to be flaky in travis, retry up to 3 times on failure
|
||||
# https://docs.travis-ci.com/user/common-build-problems/#travis_retry
|
||||
- if [[ "$TRAVIS_OS_NAME" != "osx" ]]; then travis_retry travis_retry sudo apt update; fi
|
||||
- git config remote.origin.fetch "+refs/heads/*:refs/remotes/origin/*"
|
||||
- git fetch --unshallow
|
||||
install: 'make deps'
|
||||
script: 'make test'
|
||||
matrix:
|
||||
fast_finish: false
|
||||
allow_failures:
|
||||
- go: 1.10.x
|
||||
- go: tip
|
||||
- os: osx
|
||||
# include only one build for osx for a quicker build as the nr. of these runners are sparse
|
||||
include:
|
||||
- os: osx
|
||||
go: 1.9.x
|
||||
|
||||
# the "secure" channel value is the result of running: ./misc/travis-encrypt.sh
|
||||
# with a value of: irc.freenode.net#mgmtconfig to eliminate noise from forks...
|
||||
notifications:
|
||||
irc:
|
||||
channels:
|
||||
- "irc.freenode.net#mgmtconfig"
|
||||
- secure: htcuWAczm3C1zKC9vUfdRzhIXM1vtF+q0cLlQFXK1IQQlk693/pM30Mmf2L/9V2DVDeps+GyLdip0ARXD1DZEJV0lK+Ca1qbHdFP1r4Xv6l5+jaDb5Y88YU5LI8K758QShiZJojuQ1aO2j8xmmt9V0/5y5QwlpPeHbKYBOFPBX3HvlT9DhvwZNKGhBb4qJOEaPVOwq9IkN3DyQ456MHcJ3q3vF9Lb440uTuLsJNof2AbYZH8ZIHCSG2N8tBj2qhJOpWQboYtQJzE2pRaGkGBL4kYcHZSZMXX8sl4cBM1vx/IRUkvBxJUpLJz2gn/eRI+/gr59juZE2K0+FOLlx9dLnX626Y9xSViopBI6JsIoHJDqNC7aGaF2qaYulGYN65VNKVqmghjgt6JLmmiKeH10hYrJMMvt2rms8l4+5iwmCwXvhH/WU9edzk2p5wqERMnostJFEJib0zI3yzLoF0sdJs+veKtagzfayY2d2l7hlmt951IpqqVWldVgWUcQKVvi8gmRarbwFlK+5D7BEnkUDcLNly/cqf7BgEeX6YfF+FiR4pgfOhYvGCD+2q91NgWQXHBCxbyN0be1TVdkXD94f0Lkn94VyEJJ+PkPlG+rPgFwGcjqN4oEGkJeJmES2If05q2Ms1dJLwYQDL3+Py4lNMSdSWj24TzlFVhtwHepuw=
|
||||
template:
|
||||
- "%{repository} (%{commit}: %{author}): %{message}"
|
||||
- "More info : %{build_url}"
|
||||
@@ -22,4 +43,7 @@ notifications:
|
||||
use_notice: false
|
||||
skip_join: false
|
||||
email:
|
||||
- travis-ci@shubin.ca
|
||||
recipients:
|
||||
- secure: qNkgP6QLl6VXpFQIxas2wggxvIiOmm1/hGRXm4BXsSFzHsJPvMamA3E1HEC7H+luiWTny1jtGSGgTJPV9CX1LtQV0g0S4ThaAvWuKvk3rXO8IVd++iA/Lh1s1H6JdKM0dJtLqFICawjeci4tOQzSvrM2eCBWqT0UYsrQsGHB6AF31GNAH0Acqd5cYeL+ZpbCN+hQEznAZQ7546N25TwqieI8Lg7nisA+lwYYwsaC2+f5RIeyvvKjQv3wzEdBAQ9CI9WQiTOUBnUnyYxMrdomQ/XGF66QnZy9vq5nEP83IFtuhPvSamL7ceT+yJW0jDyBi8sYEV7On7eXzjyHbiYpF4YHcJrFnf5RyV4kQGd6/SC8iZwK4Is4eyeAjDFTC+JafLajw9R9x9bK43BwlRAWOZxjFKe0cU/BVAjmlz87vHgUho2P41+0a5XfajfU6VhA5QFPK6rNH7W1CnA7D/0LmS0yaqJM1OCrm6LfoZEMhe0DxTJ9uWJbr0x1sYao6q8H4xYk+fyRgoBAr2TxYU7kXx8ThiRdzuQ8izdbojlzTYLe8liZMIsjL0axLsLK7YBWrjJUcDFDjR/DqmVxPrvbVFbCi9ChmBw0WmbJvDY0FV8T8dO8wCjg9JEmprAmWPyq0g/F87LFK4tAZqQFJGjP1qwsR9jdwdNTKeCdY656f/Y=
|
||||
on_failure: change
|
||||
on_success: change
|
||||
|
||||
7
AUTHORS
@@ -1,7 +1,12 @@
|
||||
This is a list of authors/contributors to the mgmt project.
|
||||
If you're a contributor, please send a patch with your name.
|
||||
If you're a core contributor, we might ask you to send a patch with your name.
|
||||
If you appreciate the work of one of the contributors, thank them a beverage!
|
||||
For a more exhaustive list please run: git log --format='%aN' | sort -u
|
||||
This list is sorted alphabetically by first name.
|
||||
|
||||
Felix Frank
|
||||
James Shubin
|
||||
Johan Bloemberg
|
||||
Jonathan Gold
|
||||
Julien Pivotto
|
||||
Paul Morgan
|
||||
|
||||
141
COPYING
@@ -1,5 +1,5 @@
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
@@ -7,15 +7,17 @@
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU Affero General Public License is a free, copyleft license for
|
||||
software and other kinds of works, specifically designed to ensure
|
||||
cooperation with the community in the case of network server software.
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
our General Public Licenses are intended to guarantee your freedom to
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users.
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
@@ -24,34 +26,44 @@ them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
Developers that use our General Public Licenses protect your rights
|
||||
with two steps: (1) assert copyright on the software, and (2) offer
|
||||
you this License which gives you legal permission to copy, distribute
|
||||
and/or modify the software.
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
|
||||
A secondary benefit of defending all users' freedom is that
|
||||
improvements made in alternate versions of the program, if they
|
||||
receive widespread use, become available for other developers to
|
||||
incorporate. Many developers of free software are heartened and
|
||||
encouraged by the resulting cooperation. However, in the case of
|
||||
software used on network servers, this result may fail to come about.
|
||||
The GNU General Public License permits making a modified version and
|
||||
letting the public access it on a server without ever releasing its
|
||||
source code to the public.
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
|
||||
The GNU Affero General Public License is designed specifically to
|
||||
ensure that, in such cases, the modified source code becomes available
|
||||
to the community. It requires the operator of a network server to
|
||||
provide the source code of the modified version running there to the
|
||||
users of that server. Therefore, public use of a modified version, on
|
||||
a publicly accessible server, gives the public access to the source
|
||||
code of the modified version.
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
|
||||
An older license, called the Affero General Public License and
|
||||
published by Affero, was designed to accomplish similar goals. This is
|
||||
a different license, not a version of the Affero GPL, but Affero has
|
||||
released a new version of the Affero GPL which permits relicensing under
|
||||
this license.
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
@@ -60,7 +72,7 @@ modification follow.
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
@@ -537,45 +549,35 @@ to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, if you modify the
|
||||
Program, your modified version must prominently offer all users
|
||||
interacting with it remotely through a computer network (if your version
|
||||
supports such interaction) an opportunity to receive the Corresponding
|
||||
Source of your version by providing access to the Corresponding Source
|
||||
from a network server at no charge, through some standard or customary
|
||||
means of facilitating copying of software. This Corresponding Source
|
||||
shall include the Corresponding Source for any work covered by version 3
|
||||
of the GNU General Public License that is incorporated pursuant to the
|
||||
following paragraph.
|
||||
13. Use with the GNU Affero General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU General Public License into a single
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the work with which it is combined will remain governed by version
|
||||
3 of the GNU General Public License.
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU Affero General Public License from time to time. Such new versions
|
||||
will be similar in spirit to the present version, but may differ in detail to
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU Affero General
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU Affero General Public License, you may choose any version ever published
|
||||
GNU General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU Affero General Public License can be used, that proxy's
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
@@ -633,29 +635,40 @@ the "copyright" line and a pointer to where the full notice is found.
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If your software can interact with users remotely through a computer
|
||||
network, you should also make sure that it provides a way for users to
|
||||
get its source. For example, if your program is a web application, its
|
||||
interface could display a "Source" link that leads users to an archive
|
||||
of the code. There are many ways you could offer source, and different
|
||||
solutions will be better for different programs; see section 13 for the
|
||||
specific requirements.
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<http://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<http://www.gnu.org/philosophy/why-not-lgpl.html>.
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
Mgmt
|
||||
Copyright (C) 2013-2016+ James Shubin and the project contributors
|
||||
Copyright (C) 2013-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 Affero General Public License as published by
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
143
DOCUMENTATION.md
@@ -1,143 +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. [Usage/FAQ - Notes on usage and frequently asked questions](#usage-and-frequently-asked-questions)
|
||||
5. [Reference - Detailed reference](#reference)
|
||||
* [graph.yaml](#graph.yaml)
|
||||
* [Command line](#command-line)
|
||||
6. [Examples - Example configurations](#examples)
|
||||
7. [Development - Background on module development and reporting bugs](#development)
|
||||
8. [Authors - Authors and contact information](#authors)
|
||||
|
||||
##Overview
|
||||
|
||||
The `mgmt` tool is a research prototype to demonstrate next generation config
|
||||
management techniques. Hopefully it will evolve into a useful, robust tool.
|
||||
|
||||
##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.
|
||||
|
||||
##Setup
|
||||
|
||||
During this prototype phase, the tool can be run out of the source directory.
|
||||
You'll probably want to use ```./run.sh run --file 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).
|
||||
|
||||
##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.
|
||||
|
||||
###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
|
||||
* [graph.yaml](#graph.yaml): Main graph definition file.
|
||||
* [Command line](#command-line): Command line parameters.
|
||||
|
||||
###graph.yaml
|
||||
This 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`.
|
||||
|
||||
####`--file <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.
|
||||
|
||||
##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)
|
||||
|
||||
##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/)
|
||||
212
Makefile
@@ -1,43 +1,91 @@
|
||||
# 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
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
SHELL = /bin/bash
|
||||
.PHONY: all version program path deps run race build clean test format docs rpmbuild rpm srpm spec tar upload upload-sources upload-srpms upload-rpms copr
|
||||
.SILENT: clean
|
||||
SHELL = /usr/bin/env bash
|
||||
.PHONY: all art cleanart version program lang path deps run race bindata generate build build-debug crossbuild clean test gofmt yamlfmt format docs rpmbuild mkdirs rpm srpm spec tar upload upload-sources upload-srpms upload-rpms copr
|
||||
.SILENT: clean bindata
|
||||
|
||||
SVERSION := $(shell git describe --match '[0-9]*\.[0-9]*\.[0-9]*' --tags --dirty --always)
|
||||
VERSION := $(shell git describe --match '[0-9]*\.[0-9]*\.[0-9]*' --tags --abbrev=0)
|
||||
PROGRAM := $(shell basename --suffix=-$(VERSION) $(notdir $(CURDIR)))
|
||||
# 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))
|
||||
VERSION := $(or $(VERSION),$(shell git describe --match '[0-9]*\.[0-9]*\.[0-9]*' --tags --abbrev=0))
|
||||
PROGRAM := $(shell echo $(notdir $(CURDIR)) | cut -f1 -d"-")
|
||||
ifeq ($(VERSION),$(SVERSION))
|
||||
RELEASE = 1
|
||||
else
|
||||
RELEASE = untagged
|
||||
endif
|
||||
ARCH = $(shell arch)
|
||||
SPEC = rpmbuild/SPECS/mgmt.spec
|
||||
SOURCE = rpmbuild/SOURCES/mgmt-$(VERSION).tar.bz2
|
||||
SRPM = rpmbuild/SRPMS/mgmt-$(VERSION)-$(RELEASE).src.rpm
|
||||
SRPM_BASE = mgmt-$(VERSION)-$(RELEASE).src.rpm
|
||||
RPM = rpmbuild/RPMS/mgmt-$(VERSION)-$(RELEASE).$(ARCH).rpm
|
||||
ARCH = $(uname -m)
|
||||
SPEC = rpmbuild/SPECS/$(PROGRAM).spec
|
||||
SOURCE = rpmbuild/SOURCES/$(PROGRAM)-$(VERSION).tar.bz2
|
||||
SRPM = rpmbuild/SRPMS/$(PROGRAM)-$(VERSION)-$(RELEASE).src.rpm
|
||||
SRPM_BASE = $(PROGRAM)-$(VERSION)-$(RELEASE).src.rpm
|
||||
RPM = rpmbuild/RPMS/$(PROGRAM)-$(VERSION)-$(RELEASE).$(ARCH).rpm
|
||||
USERNAME := $(shell cat ~/.config/copr 2>/dev/null | grep username | awk -F '=' '{print $$2}' | tr -d ' ')
|
||||
SERVER = 'dl.fedoraproject.org'
|
||||
REMOTE_PATH = 'pub/alt/$(USERNAME)/mgmt'
|
||||
REMOTE_PATH = 'pub/alt/$(USERNAME)/$(PROGRAM)'
|
||||
ifneq ($(GOTAGS),)
|
||||
BUILD_FLAGS = -tags '$(GOTAGS)'
|
||||
endif
|
||||
GOOSARCHES ?= linux/amd64 linux/ppc64 linux/ppc64le linux/arm64 darwin/amd64
|
||||
|
||||
all: docs
|
||||
GOHOSTOS = $(shell go env GOHOSTOS)
|
||||
GOHOSTARCH = $(shell go env GOHOSTARCH)
|
||||
|
||||
default: build
|
||||
|
||||
#
|
||||
# art
|
||||
#
|
||||
art: art/mgmt_logo_default_symbol.png art/mgmt_logo_default_tall.png art/mgmt_logo_default_wide.png art/mgmt_logo_reversed_symbol.png art/mgmt_logo_reversed_tall.png art/mgmt_logo_reversed_wide.png art/mgmt_logo_white_symbol.png art/mgmt_logo_white_tall.png art/mgmt_logo_white_wide.png
|
||||
|
||||
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
|
||||
|
||||
# NOTE: the widths are arbitrary
|
||||
art/mgmt_logo_default_symbol.png: art/mgmt_logo_default_symbol.svg
|
||||
inkscape --export-background='#ffffff' --without-gui --export-png "$@" --export-width 300 $(@:png=svg)
|
||||
|
||||
art/mgmt_logo_default_tall.png: art/mgmt_logo_default_tall.svg
|
||||
inkscape --export-background='#ffffff' --without-gui --export-png "$@" --export-width 400 $(@:png=svg)
|
||||
|
||||
art/mgmt_logo_default_wide.png: art/mgmt_logo_default_wide.svg
|
||||
inkscape --export-background='#ffffff' --without-gui --export-png "$@" --export-width 800 $(@:png=svg)
|
||||
|
||||
art/mgmt_logo_reversed_symbol.png: art/mgmt_logo_reversed_symbol.svg
|
||||
inkscape --export-background='#231f20' --without-gui --export-png "$@" --export-width 300 $(@:png=svg)
|
||||
|
||||
art/mgmt_logo_reversed_tall.png: art/mgmt_logo_reversed_tall.svg
|
||||
inkscape --export-background='#231f20' --without-gui --export-png "$@" --export-width 400 $(@:png=svg)
|
||||
|
||||
art/mgmt_logo_reversed_wide.png: art/mgmt_logo_reversed_wide.svg
|
||||
inkscape --export-background='#231f20' --without-gui --export-png "$@" --export-width 800 $(@:png=svg)
|
||||
|
||||
art/mgmt_logo_white_symbol.png: art/mgmt_logo_white_symbol.svg
|
||||
inkscape --export-background='#231f20' --without-gui --export-png "$@" --export-width 300 $(@:png=svg)
|
||||
|
||||
art/mgmt_logo_white_tall.png: art/mgmt_logo_white_tall.svg
|
||||
inkscape --export-background='#231f20' --without-gui --export-png "$@" --export-width 400 $(@:png=svg)
|
||||
|
||||
art/mgmt_logo_white_wide.png: art/mgmt_logo_white_wide.svg
|
||||
inkscape --export-background='#231f20' --without-gui --export-png "$@" --export-width 800 $(@:png=svg)
|
||||
|
||||
all: docs $(PROGRAM).static
|
||||
|
||||
# show the current version
|
||||
version:
|
||||
@@ -53,39 +101,90 @@ deps:
|
||||
./misc/make-deps.sh
|
||||
|
||||
run:
|
||||
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
|
||||
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)"
|
||||
|
||||
build: mgmt
|
||||
# generate go files from non-go source
|
||||
bindata:
|
||||
@echo "Generating: bindata..."
|
||||
$(MAKE) --quiet -C bindata
|
||||
|
||||
mgmt: main.go
|
||||
@echo "Building: $(PROGRAM), version: $(SVERSION)."
|
||||
generate:
|
||||
go generate
|
||||
# avoid equals sign in old golang versions eg in: -X foo=bar
|
||||
if go version | grep -qE 'go1.3|go1.4'; then \
|
||||
go build -ldflags "-X main.program $(PROGRAM) -X main.version $(SVERSION)" -o mgmt; \
|
||||
else \
|
||||
go build -ldflags "-X main.program=$(PROGRAM) -X main.version=$(SVERSION)" -o mgmt; \
|
||||
fi
|
||||
|
||||
lang:
|
||||
@# recursively run make in child dir named lang
|
||||
@echo "Generating: lang..."
|
||||
$(MAKE) --quiet -C lang
|
||||
|
||||
# build a `mgmt` binary for current host os/arch
|
||||
$(PROGRAM): build/mgmt-${GOHOSTOS}-${GOHOSTARCH}
|
||||
cp $< $@
|
||||
|
||||
$(PROGRAM).static: $(GO_FILES)
|
||||
@echo "Building: $(PROGRAM).static, version: $(SVERSION)..."
|
||||
go generate
|
||||
go build -a -installsuffix cgo -tags netgo -ldflags '-extldflags "-static" -X main.program=$(PROGRAM) -X main.version=$(SVERSION) -s -w' -o $(PROGRAM).static $(BUILD_FLAGS);
|
||||
|
||||
build: LDFLAGS=-s -w
|
||||
build: $(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
|
||||
time env GOOS=${GOOS} GOARCH=${GOARCH} go build -i -ldflags "-X main.program=$(PROGRAM) -X main.version=$(SVERSION) ${LDFLAGS}" -o $@ $(BUILD_FLAGS);
|
||||
|
||||
# create a list of binary file names to use as make targets
|
||||
crossbuild_targets = $(addprefix build/mgmt-,$(subst /,-,${GOOSARCHES}))
|
||||
crossbuild: ${crossbuild_targets}
|
||||
|
||||
clean:
|
||||
[ ! -e mgmt ] || rm mgmt
|
||||
$(MAKE) --quiet -C bindata clean
|
||||
$(MAKE) --quiet -C lang clean
|
||||
[ ! -e $(PROGRAM) ] || rm $(PROGRAM)
|
||||
rm -f *_stringer.go # generated by `go generate`
|
||||
rm -f *_mock.go # generated by `go generate`
|
||||
# crossbuild artifacts
|
||||
rm -f build/mgmt-*
|
||||
|
||||
test:
|
||||
test: build
|
||||
./test.sh
|
||||
|
||||
format:
|
||||
find -type f -name '*.go' -not -path './old/*' -not -path './tmp/*' -exec gofmt -w {} \;
|
||||
find -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" \;
|
||||
# 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 $*
|
||||
|
||||
docs: mgmt-documentation.pdf
|
||||
# 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"
|
||||
|
||||
mgmt-documentation.pdf: DOCUMENTATION.md
|
||||
pandoc DOCUMENTATION.md -o 'mgmt-documentation.pdf'
|
||||
gofmt:
|
||||
# TODO: remove gofmt once goimports has a -s option
|
||||
find . -maxdepth 6 -type f -name '*.go' -not -path './old/*' -not -path './tmp/*' -not -path './vendor/*' -exec gofmt -s -w {} \;
|
||||
find . -maxdepth 6 -type f -name '*.go' -not -path './old/*' -not -path './tmp/*' -not -path './vendor/*' -exec goimports -w {} \;
|
||||
|
||||
yamlfmt:
|
||||
find . -maxdepth 3 -type f -name '*.yaml' -not -path './old/*' -not -path './tmp/*' -not -path './omv.yaml' -exec ruby -e "require 'yaml'; x=YAML.load_file('{}').to_yaml.each_line.map(&:rstrip).join(10.chr)+10.chr; File.open('{}', 'w').write x" \;
|
||||
|
||||
format: gofmt yamlfmt
|
||||
|
||||
docs: $(PROGRAM)-documentation.pdf
|
||||
|
||||
$(PROGRAM)-documentation.pdf: docs/documentation.md
|
||||
pandoc docs/documentation.md -o docs/'$(PROGRAM)-documentation.pdf'
|
||||
|
||||
#
|
||||
# build aliases
|
||||
@@ -116,21 +215,21 @@ upload: upload-sources upload-srpms upload-rpms
|
||||
$(RPM): $(SPEC) $(SOURCE)
|
||||
@echo Running rpmbuild -bb...
|
||||
rpmbuild --define '_topdir $(shell pwd)/rpmbuild' -bb $(SPEC) && \
|
||||
mv rpmbuild/RPMS/$(ARCH)/mgmt-$(VERSION)-$(RELEASE).*.rpm $(RPM)
|
||||
mv rpmbuild/RPMS/$(ARCH)/$(PROGRAM)-$(VERSION)-$(RELEASE).*.rpm $(RPM)
|
||||
|
||||
$(SRPM): $(SPEC) $(SOURCE)
|
||||
@echo Running rpmbuild -bs...
|
||||
rpmbuild --define '_topdir $(shell pwd)/rpmbuild' -bs $(SPEC)
|
||||
# renaming is not needed because we aren't using the dist variable
|
||||
#mv rpmbuild/SRPMS/mgmt-$(VERSION)-$(RELEASE).*.src.rpm $(SRPM)
|
||||
#mv rpmbuild/SRPMS/$(PROGRAM)-$(VERSION)-$(RELEASE).*.src.rpm $(SRPM)
|
||||
|
||||
#
|
||||
# spec
|
||||
#
|
||||
$(SPEC): rpmbuild/ mgmt.spec.in
|
||||
$(SPEC): rpmbuild/ spec.in
|
||||
@echo Running templater...
|
||||
#cat mgmt.spec.in > $(SPEC)
|
||||
sed -e s/__VERSION__/$(VERSION)/ -e s/__RELEASE__/$(RELEASE)/ < mgmt.spec.in > $(SPEC)
|
||||
cat spec.in > $(SPEC)
|
||||
sed -e s/__PROGRAM__/$(PROGRAM)/g -e s/__VERSION__/$(VERSION)/g -e s/__RELEASE__/$(RELEASE)/g < spec.in > $(SPEC)
|
||||
# append a changelog to the .spec file
|
||||
git log --format="* %cd %aN <%aE>%n- (%h) %s%d%n" --date=local | sed -r 's/[0-9]+:[0-9]+:[0-9]+ //' >> $(SPEC)
|
||||
|
||||
@@ -140,7 +239,7 @@ $(SPEC): rpmbuild/ mgmt.spec.in
|
||||
$(SOURCE): rpmbuild/
|
||||
@echo Running git archive...
|
||||
# use HEAD if tag doesn't exist yet, so that development is easier...
|
||||
git archive --prefix=mgmt-$(VERSION)/ -o $(SOURCE) $(VERSION) 2> /dev/null || (echo 'Warning: $(VERSION) does not exist. Using HEAD instead.' && git archive --prefix=mgmt-$(VERSION)/ -o $(SOURCE) HEAD)
|
||||
git archive --prefix=$(PROGRAM)-$(VERSION)/ -o $(SOURCE) $(VERSION) 2> /dev/null || (echo 'Warning: $(VERSION) does not exist. Using HEAD instead.' && git archive --prefix=$(PROGRAM)-$(VERSION)/ -o $(SOURCE) HEAD)
|
||||
# TODO: if git archive had a --submodules flag this would easier!
|
||||
@echo Running git archive submodules...
|
||||
# i thought i would need --ignore-zeros, but it doesn't seem necessary!
|
||||
@@ -149,14 +248,14 @@ $(SOURCE): rpmbuild/
|
||||
temp="$${temp#\'}"; \
|
||||
path=$$temp; \
|
||||
[ "$$path" = "" ] && continue; \
|
||||
(cd $$path && git archive --prefix=mgmt-$(VERSION)/$$path/ HEAD > $$p/rpmbuild/tmp.tar && tar --concatenate --file=$$p/$(SOURCE) $$p/rpmbuild/tmp.tar && rm $$p/rpmbuild/tmp.tar); \
|
||||
(cd $$path && git archive --prefix=$(PROGRAM)-$(VERSION)/$$path/ HEAD > $$p/rpmbuild/tmp.tar && tar --concatenate --file=$$p/$(SOURCE) $$p/rpmbuild/tmp.tar && rm $$p/rpmbuild/tmp.tar); \
|
||||
done
|
||||
|
||||
# TODO: ensure that each sub directory exists
|
||||
rpmbuild/:
|
||||
mkdir -p rpmbuild/{BUILD,BUILDROOT,RPMS,SOURCES,SPECS,SRPMS}
|
||||
|
||||
rpmbuild:
|
||||
mkdirs:
|
||||
mkdir -p rpmbuild/{BUILD,BUILDROOT,RPMS,SOURCES,SPECS,SRPMS}
|
||||
|
||||
#
|
||||
@@ -218,4 +317,27 @@ upload-rpms: rpmbuild/RPMS/ rpmbuild/RPMS/SHA256SUMS rpmbuild/RPMS/SHA256SUMS.as
|
||||
copr: upload-srpms
|
||||
./misc/copr-build.py https://$(SERVER)/$(REMOTE_PATH)/SRPMS/$(SRPM_BASE)
|
||||
|
||||
#
|
||||
# deb build
|
||||
#
|
||||
|
||||
deb:
|
||||
./misc/gen-deb-changelog-from-git.sh
|
||||
dpkg-buildpackage
|
||||
# especially when building in Docker container, pull build artifact in project directory.
|
||||
cp ../mgmt_*_amd64.deb ./
|
||||
# cleanup
|
||||
rm -rf debian/mgmt/
|
||||
|
||||
build_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:
|
||||
docker rmi purpleidea/mgmt-build
|
||||
docker rmi purpleidea/mgmt
|
||||
|
||||
# vim: ts=8
|
||||
|
||||
105
README.md
@@ -1,66 +1,83 @@
|
||||
# *mgmt*: This is: mgmt!
|
||||
# *mgmt*: next generation config management!
|
||||
|
||||
[](http://travis-ci.org/purpleidea/mgmt)
|
||||
[](DOCUMENTATION.md)
|
||||
[](https://webchat.freenode.net/?channels=#mgmtconfig)
|
||||
[](https://ci.centos.org/job/purpleidea-mgmt/)
|
||||
[](https://copr.fedoraproject.org/coprs/purpleidea/mgmt/)
|
||||
[](art/)
|
||||
|
||||
[](https://goreportcard.com/report/github.com/purpleidea/mgmt)
|
||||
[](http://travis-ci.org/purpleidea/mgmt)
|
||||
[](https://godoc.org/github.com/purpleidea/mgmt)
|
||||
[](https://webchat.freenode.net/?channels=#mgmtconfig)
|
||||
[](https://ci.centos.org/job/purpleidea-mgmt/)
|
||||
|
||||
## 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).
|
||||
|
||||
## Questions:
|
||||
Please join the [#mgmtconfig](https://webchat.freenode.net/?channels=#mgmtconfig) IRC community!
|
||||
If you have a well phrased question that might benefit others, consider asking it by sending a patch to the documentation [FAQ](https://github.com/purpleidea/mgmt/blob/master/DOCUMENTATION.md#usage-and-frequently-asked-questions) section. I'll merge your question, and a patch with the answer!
|
||||
Come join us in the `mgmt` community!
|
||||
|
||||
## Quick start:
|
||||
* Either get the golang dependencies on your own, or run `make deps` if you're comfortable with how we install them.
|
||||
* Run `make build` to get a fresh built `mgmt` binary.
|
||||
* Run `cd $(mktemp --tmpdir -d tmp.XXX) && etcd` to get etcd running. The `mgmt` software will do this automatically for you in the future.
|
||||
* Run `time ./mgmt run --file examples/graph0.yaml --converged-timeout=1` 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!
|
||||
| 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) |
|
||||
|
||||
## Examples:
|
||||
Please look in the [examples/](examples/) folder for more examples!
|
||||
## Status:
|
||||
|
||||
Mgmt is a next generation automation tool. It has similarities to other tools in
|
||||
the configuration management space, but has a fast, modern, distributed systems
|
||||
approach. The project contains an engine and a language.
|
||||
[Please have a look at an introductory video or blog post.](docs/on-the-web.md)
|
||||
|
||||
Mgmt is a fairly new project. It is usable today, but not yet feature complete.
|
||||
With your help you'll be able to influence our design and get us to 1.0 sooner!
|
||||
Interested developers should read the [quick start guide](docs/quick-start-guide.md).
|
||||
|
||||
## Documentation:
|
||||
Please see: [DOCUMENTATION.md](DOCUMENTATION.md) or [PDF](https://pdfdoc-purpleidea.rhcloud.com/pdf/https://github.com/purpleidea/mgmt/blob/master/DOCUMENTATION.md).
|
||||
|
||||
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:
|
||||
|
||||
Feel free to grab one of the straightforward [#mgmtlove](https://github.com/purpleidea/mgmt/labels/mgmtlove)
|
||||
issues if you're a first time contributor to the project or if you're unsure
|
||||
about what to hack on!
|
||||
Please see: [TODO.md](TODO.md) for a list of upcoming work and TODO items.
|
||||
Please get involved by working on one of these items or by suggesting something else!
|
||||
Please get involved by working on one of these items or by suggesting something
|
||||
else!
|
||||
|
||||
## 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.
|
||||
|
||||
## Dependencies:
|
||||
* golang 1.4 or higher (required, available in most distros)
|
||||
* golang libraries (required, available with `go get`)
|
||||
|
||||
go get github.com/coreos/etcd/client
|
||||
go get gopkg.in/yaml.v2
|
||||
go get gopkg.in/fsnotify.v1
|
||||
go get github.com/codegangsta/cli
|
||||
go get github.com/coreos/go-systemd/dbus
|
||||
go get github.com/coreos/go-systemd/util
|
||||
|
||||
* stringer (required for building), available as a package on some platforms, otherwise via `go get`
|
||||
|
||||
go get golang.org/x/tools/cmd/stringer
|
||||
|
||||
* pandoc (optional, for building a pdf of the documentation)
|
||||
* graphviz (optional, for building a visual representation of the graph)
|
||||
Please set the `DEBUG` constant in [main.go](https://github.com/purpleidea/mgmt/blob/master/main.go)
|
||||
to `true`, and post the logs when you report the [issue](https://github.com/purpleidea/mgmt/issues).
|
||||
Bonus points if you provide a [shell](https://github.com/purpleidea/mgmt/tree/master/test/shell)
|
||||
or [OMV](https://github.com/purpleidea/mgmt/tree/master/test/omv) reproducible
|
||||
test case.
|
||||
Feel free to read my article on [debugging golang programs](https://purpleidea.com/blog/2016/02/15/debugging-golang-programs/).
|
||||
|
||||
## Patches:
|
||||
|
||||
We'd love to have your patches! Please send them by email, or as a pull request.
|
||||
|
||||
## On the web:
|
||||
* Introductory blog post: [https://ttboj.wordpress.com/2016/01/18/next-generation-configuration-mgmt/](https://ttboj.wordpress.com/2016/01/18/next-generation-configuration-mgmt/)
|
||||
* Julian Dunn at Cfgmgmtcamp 2016 [https://www.youtube.com/watch?v=kfF9IATUask&t=1949&html5=1](https://www.youtube.com/watch?v=kfF9IATUask&t=1949&html5=1)
|
||||
|
||||
##
|
||||
[Read what people are saying and publishing about mgmt!](docs/on-the-web.md)
|
||||
|
||||
Happy hacking!
|
||||
|
||||
6
THANKS
@@ -9,10 +9,16 @@ Chris Wright - For encouraging me to continue work on my prototype.
|
||||
|
||||
Daniel Riek - For supporting and sheltering this project from bureaucracy.
|
||||
|
||||
Diego Ongaro - For good chats, particularly around distributed systems.
|
||||
|
||||
Felix Frank - For taking a difficult problem and building an inspiring solution.
|
||||
|
||||
Ira Cooper - For having an algorithmic design discussion with me.
|
||||
|
||||
Jeff Darcy - For some algorithm recommendations, and NACKing my TopoSort idea!
|
||||
|
||||
Red Hat, inc. - For paying my salary, thus financially supporting my hacking.
|
||||
|
||||
Samuel Gélineau - For help with programming language theory and design.
|
||||
|
||||
And many others...
|
||||
|
||||
76
TODO.md
@@ -1,43 +1,85 @@
|
||||
# TODO
|
||||
|
||||
If you're looking for something to do, look here!
|
||||
Let us know if you're working on one of the items.
|
||||
If you'd like something to work on, ping @purpleidea and I'll create an issue
|
||||
tailored especially for you! Just let me know your approximate golang skill
|
||||
level and how many hours you'd like to spend on the patch.
|
||||
|
||||
## Package resource
|
||||
- [ ] base type [bug](https://github.com/purpleidea/mgmt/issues/11)
|
||||
- [ ] dnf blocker [bug](https://github.com/hughsie/PackageKit/issues/110)
|
||||
- [ ] install signal blocker [bug](https://github.com/hughsie/PackageKit/issues/109)
|
||||
|
||||
## File resource
|
||||
- [ ] ability to make/delete folders
|
||||
- [ ] recursive argument (can recursively watch/modify contents)
|
||||
- [ ] force argument (can cause switch from file <-> folder)
|
||||
- [ ] getfiles support on debian [bug](https://github.com/hughsie/PackageKit/issues/118)
|
||||
- [ ] directory info on fedora [bug](https://github.com/hughsie/PackageKit/issues/117)
|
||||
- [ ] dnf blocker [bug](https://github.com/hughsie/PackageKit/issues/110)
|
||||
|
||||
## File resource [bug](https://github.com/purpleidea/mgmt/issues/64) [:heart:](https://github.com/purpleidea/mgmt/labels/mgmtlove)
|
||||
|
||||
- [ ] recurse limit support [:heart:](https://github.com/purpleidea/mgmt/labels/mgmtlove)
|
||||
- [ ] fanotify support [bug](https://github.com/go-fsnotify/fsnotify/issues/114)
|
||||
|
||||
## Svc resource
|
||||
|
||||
- [ ] base resource improvements
|
||||
|
||||
## Exec resource
|
||||
|
||||
- [ ] base resource improvements
|
||||
|
||||
## Timer resource
|
||||
|
||||
- [ ] increment algorithm (linear, exponential, etc...) [:heart:](https://github.com/purpleidea/mgmt/labels/mgmtlove)
|
||||
|
||||
## User/Group resource
|
||||
|
||||
- [ ] automatic edges to file resource [:heart:](https://github.com/purpleidea/mgmt/labels/mgmtlove)
|
||||
|
||||
## Virt (libvirt) resource
|
||||
|
||||
- [ ] base resource improvements [:heart:](https://github.com/purpleidea/mgmt/labels/mgmtlove)
|
||||
|
||||
## Net (systemd-networkd) resource
|
||||
|
||||
- [ ] base resource
|
||||
- [ ] reset on recompile
|
||||
- [ ] increment algorithm (linear, exponential, etc...)
|
||||
|
||||
## Nspawn (systemd-nspawn) resource
|
||||
|
||||
- [ ] base resource [:heart:](https://github.com/purpleidea/mgmt/labels/mgmtlove)
|
||||
|
||||
## Mount (systemd-mount) resource
|
||||
|
||||
- [ ] base resource [:heart:](https://github.com/purpleidea/mgmt/labels/mgmtlove)
|
||||
|
||||
## Cron (systemd-timer) resource
|
||||
|
||||
- [ ] base resource [:heart:](https://github.com/purpleidea/mgmt/labels/mgmtlove)
|
||||
|
||||
## Http resource
|
||||
|
||||
- [ ] base resource [:heart:](https://github.com/purpleidea/mgmt/labels/mgmtlove)
|
||||
|
||||
## Etcd improvements
|
||||
- [ ] embedded etcd master
|
||||
- [ ] capnslog fixes [bug](https://github.com/coreos/etcd/issues/4115)
|
||||
|
||||
- [ ] fix embedded etcd master race
|
||||
|
||||
## Torrent/dht file transfer
|
||||
|
||||
- [ ] base plumbing
|
||||
|
||||
## GPG/Auth improvements
|
||||
|
||||
- [ ] base plumbing
|
||||
|
||||
## Language improvements
|
||||
- [ ] language design
|
||||
- [ ] lexer/parser
|
||||
|
||||
- [ ] more core functions
|
||||
- [ ] automatic language formatter, ala `gofmt`
|
||||
- [ ] gedit/gnome-builder/gtksourceview syntax highlighting
|
||||
- [ ] vim syntax highlighting
|
||||
- [ ] emacs syntax highlighting
|
||||
- [x] emacs syntax highlighting: see `misc/emacs/`
|
||||
|
||||
## Other
|
||||
|
||||
- [ ] better error/retry handling
|
||||
- [ ] resource grouping
|
||||
- [ ] automatic dependency adding (eg: packagekit file dependencies)
|
||||
- [ ] rpm package target in Makefile
|
||||
- [ ] deb package target in Makefile
|
||||
- [ ] reproducible builds
|
||||
- [ ] add your suggestions!
|
||||
|
||||
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/26-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"
|
||||
|
||||
# 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
|
||||
2
art/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
*.png
|
||||
misc/
|
||||
BIN
art/mgmt.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
94
art/mgmt_logo_default_symbol.svg
Normal file
@@ -0,0 +1,94 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 120 107.1" style="enable-background:new 0 0 120 107.1;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#FFFFFF;}
|
||||
.st1{fill:#E22434;}
|
||||
.st2{display:none;}
|
||||
.st3{fill:#1B3663;}
|
||||
.st4{fill:#00B1D1;}
|
||||
.st5{fill:#BFE6EF;}
|
||||
.st6{fill:#69CBE0;}
|
||||
.st7{fill:#0080BD;}
|
||||
.st8{fill:#005DAB;}
|
||||
.st9{fill:#183660;}
|
||||
.st10{fill:none;stroke:#183660;stroke-width:1.9441;stroke-miterlimit:10;}
|
||||
.st11{fill:none;stroke:#183660;stroke-width:1.6201;stroke-miterlimit:10;}
|
||||
.st12{fill:none;stroke:#183660;stroke-width:1.2961;stroke-miterlimit:10;}
|
||||
.st13{fill:none;stroke:#C0E6EF;stroke-width:1.9441;stroke-miterlimit:10;}
|
||||
.st14{fill:#C0E6EF;}
|
||||
.st15{fill:none;stroke:#C0E6EF;stroke-width:1.2961;stroke-miterlimit:10;}
|
||||
.st16{fill:none;stroke:#C0E6EF;stroke-width:1.6201;stroke-miterlimit:10;}
|
||||
.st17{fill:none;stroke:#FFFFFF;stroke-width:1.9441;stroke-miterlimit:10;}
|
||||
.st18{fill:none;stroke:#FFFFFF;stroke-width:1.2961;stroke-miterlimit:10;}
|
||||
.st19{fill:none;stroke:#FFFFFF;stroke-width:1.6201;stroke-miterlimit:10;}
|
||||
</style>
|
||||
<g id="Layer_2">
|
||||
</g>
|
||||
<g id="Layer_1">
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st10" x1="29.2" y1="24.1" x2="52.1" y2="42.8"/>
|
||||
<g>
|
||||
<polygon class="st9" points="27.7,27.8 24.9,20.5 32.6,21.8 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<circle class="st5" cx="16.1" cy="12.2" r="12.1"/>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st10" x1="52.1" y1="42.1" x2="74.1" y2="80"/>
|
||||
<g>
|
||||
<polygon class="st9" points="70.1,80.9 76.9,84.8 76.8,77.1 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st10" x1="69.4" y1="46.7" x2="95.8" y2="52.4"/>
|
||||
<g>
|
||||
<polygon class="st9" points="69.7,50.7 63.9,45.6 71.3,43.2 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st10" x1="52.1" y1="42.8" x2="71.9" y2="30.1"/>
|
||||
<g>
|
||||
<polygon class="st9" points="73.1,34 76.6,27.1 68.9,27.5 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st10" x1="16.8" y1="49.6" x2="34.8" y2="46.5"/>
|
||||
<g>
|
||||
<polygon class="st9" points="34.3,50.5 40.3,45.6 33,42.9 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st10" x1="92.3" y1="79.5" x2="107.8" y2="54.2"/>
|
||||
<g>
|
||||
<polygon class="st9" points="96.1,80.6 89.3,84.3 89.5,76.5 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st10" x1="97.3" y1="36.5" x2="107.8" y2="54.2"/>
|
||||
<g>
|
||||
<polygon class="st9" points="94.6,39.4 94.5,31.7 101.2,35.5 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<circle class="st3" cx="52.1" cy="42.8" r="12.1"/>
|
||||
<circle class="st4" cx="12.2" cy="50.8" r="12.1"/>
|
||||
<circle class="st7" cx="87.5" cy="21.7" r="12.1"/>
|
||||
<circle class="st8" cx="83.5" cy="95" r="12.1"/>
|
||||
<circle class="st6" cx="107.8" cy="54.2" r="12.1"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
132
art/mgmt_logo_default_tall.svg
Normal file
@@ -0,0 +1,132 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 168.3 131.6" style="enable-background:new 0 0 168.3 131.6;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#FFFFFF;}
|
||||
.st1{fill:#E22434;}
|
||||
.st2{display:none;}
|
||||
.st3{fill:#1B3663;}
|
||||
.st4{fill:#00B1D1;}
|
||||
.st5{fill:#BFE6EF;}
|
||||
.st6{fill:#69CBE0;}
|
||||
.st7{fill:#0080BD;}
|
||||
.st8{fill:#005DAB;}
|
||||
.st9{fill:#183660;}
|
||||
.st10{fill:none;stroke:#183660;stroke-width:1.9441;stroke-miterlimit:10;}
|
||||
.st11{fill:none;stroke:#183660;stroke-width:1.6201;stroke-miterlimit:10;}
|
||||
.st12{fill:none;stroke:#183660;stroke-width:1.2961;stroke-miterlimit:10;}
|
||||
.st13{fill:none;stroke:#C0E6EF;stroke-width:1.9441;stroke-miterlimit:10;}
|
||||
.st14{fill:#C0E6EF;}
|
||||
.st15{fill:none;stroke:#C0E6EF;stroke-width:1.2961;stroke-miterlimit:10;}
|
||||
.st16{fill:none;stroke:#C0E6EF;stroke-width:1.6201;stroke-miterlimit:10;}
|
||||
.st17{fill:none;stroke:#FFFFFF;stroke-width:1.9441;stroke-miterlimit:10;}
|
||||
.st18{fill:none;stroke:#FFFFFF;stroke-width:1.2961;stroke-miterlimit:10;}
|
||||
.st19{fill:none;stroke:#FFFFFF;stroke-width:1.6201;stroke-miterlimit:10;}
|
||||
</style>
|
||||
<g id="Layer_2">
|
||||
</g>
|
||||
<g id="Layer_1">
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<path class="st3" d="M4.7,105l0.1,1.8c1.1-1.4,2.6-2.1,4.4-2.1c1.9,0,3.2,0.9,4,2.6c1.1-1.7,2.6-2.6,4.7-2.6
|
||||
c3.3,0,5,2.3,5.1,6.9V124h-5v-12.1c0-1.1-0.2-1.9-0.5-2.4c-0.3-0.5-0.8-0.7-1.5-0.7c-0.9,0-1.6,0.6-2.1,1.7l0,0.6V124H9v-12.1
|
||||
c0-1.1-0.1-1.9-0.4-2.4c-0.3-0.5-0.8-0.7-1.6-0.7c-0.9,0-1.5,0.5-2,1.4V124H0v-19H4.7z"/>
|
||||
<path class="st3" d="M26.4,113.9c0-3.1,0.6-5.4,1.7-7s2.7-2.3,4.7-2.3c1.7,0,3.1,0.7,4,2L37,105h4.5v19c0,2.4-0.7,4.3-2,5.6
|
||||
c-1.4,1.3-3.3,1.9-5.9,1.9c-1,0-2.1-0.2-3.3-0.6s-2-0.9-2.6-1.6l1.7-3.4c0.5,0.5,1.1,0.9,1.8,1.2c0.7,0.3,1.5,0.5,2.1,0.5
|
||||
c1.1,0,1.9-0.3,2.4-0.8s0.7-1.4,0.7-2.6v-1.6c-0.9,1.2-2.2,1.9-3.7,1.9c-2,0-3.6-0.8-4.7-2.4s-1.7-3.8-1.7-6.7V113.9z
|
||||
M31.4,115.2c0,1.8,0.2,3,0.7,3.8s1.2,1.2,2.2,1.2c1,0,1.8-0.4,2.3-1.1V110c-0.5-0.8-1.3-1.2-2.2-1.2c-1,0-1.7,0.4-2.2,1.2
|
||||
s-0.7,2.1-0.7,3.9V115.2z"/>
|
||||
<path class="st3" d="M50.1,105l0.1,1.8c1.1-1.4,2.6-2.1,4.4-2.1c1.9,0,3.2,0.9,4,2.6c1.1-1.7,2.6-2.6,4.7-2.6
|
||||
c3.3,0,5,2.3,5.1,6.9V124h-5v-12.1c0-1.1-0.2-1.9-0.5-2.4s-0.8-0.7-1.5-0.7c-0.9,0-1.6,0.6-2.1,1.7l0,0.6V124h-5v-12.1
|
||||
c0-1.1-0.1-1.9-0.4-2.4s-0.8-0.7-1.6-0.7c-0.9,0-1.5,0.5-2,1.4V124h-5v-19H50.1z"/>
|
||||
<path class="st3" d="M78.2,100.3v4.7h2.5v3.7h-2.5v9.5c0,0.8,0.1,1.3,0.3,1.5c0.2,0.3,0.6,0.4,1.2,0.4c0.5,0,0.9,0,1.2-0.1
|
||||
l0,3.9c-0.8,0.3-1.8,0.5-2.7,0.5c-3.2,0-4.8-1.8-4.9-5.5v-10.1h-2.2V105h2.2v-4.7H78.2z"/>
|
||||
<path class="st4" d="M90.6,122.6c1.4,0,2.4-0.4,3.1-1.1c0.7-0.8,1.1-1.9,1.2-3.3h1.9c-0.1,1.9-0.7,3.5-1.9,4.6
|
||||
c-1.1,1.1-2.6,1.7-4.3,1.7c-2.3,0-4-0.7-5.1-2.2s-1.7-3.6-1.8-6.4v-2.3c0-2.9,0.6-5.1,1.7-6.6s2.9-2.2,5.1-2.2
|
||||
c1.9,0,3.4,0.6,4.5,1.8s1.7,2.9,1.7,5H95c-0.1-1.6-0.5-2.8-1.2-3.6s-1.8-1.3-3.1-1.3c-1.7,0-2.9,0.6-3.7,1.7s-1.2,2.9-1.2,5.2
|
||||
v2.2c0,2.4,0.4,4.2,1.2,5.3S89,122.6,90.6,122.6z"/>
|
||||
<path class="st4" d="M100.5,113.6c0-2.7,0.6-4.9,1.9-6.5s2.9-2.4,5.1-2.4c2.2,0,3.9,0.8,5.1,2.4s1.9,3.7,1.9,6.5v2
|
||||
c0,2.8-0.6,5-1.9,6.5s-2.9,2.3-5.1,2.3c-2.1,0-3.8-0.8-5-2.3s-1.9-3.6-1.9-6.3V113.6z M102.5,115.5c0,2.2,0.4,3.9,1.3,5.2
|
||||
c0.9,1.3,2.1,1.9,3.7,1.9c1.6,0,2.8-0.6,3.7-1.8s1.3-2.9,1.3-5.2v-2c0-2.2-0.4-3.9-1.3-5.2c-0.9-1.3-2.1-1.9-3.7-1.9
|
||||
c-1.5,0-2.7,0.6-3.6,1.8s-1.3,2.9-1.4,5.1V115.5z"/>
|
||||
<path class="st4" d="M121.1,105l0.1,3c0.6-1,1.3-1.9,2.2-2.5s1.9-0.9,3-0.9c3.3,0,5,2.2,5.1,6.6V124h-1.9v-12.5
|
||||
c0-1.7-0.3-3-0.9-3.8c-0.6-0.8-1.5-1.2-2.8-1.2c-1,0-1.9,0.4-2.8,1.2s-1.4,1.8-1.9,3.2V124h-2v-19H121.1z"/>
|
||||
<path class="st4" d="M138.2,124v-17.3h-2.6V105h2.6v-2.5c0-1.9,0.5-3.3,1.3-4.3s2-1.5,3.5-1.5c0.7,0,1.3,0.1,1.9,0.3l-0.1,1.8
|
||||
c-0.5-0.1-1-0.2-1.6-0.2c-0.9,0-1.7,0.4-2.2,1.1s-0.8,1.7-0.8,3v2.3h3.7v1.8h-3.7V124H138.2z"/>
|
||||
<path class="st4" d="M148,99.5c0-0.4,0.1-0.7,0.3-1s0.5-0.4,0.9-0.4s0.7,0.1,1,0.4s0.3,0.6,0.3,1s-0.1,0.7-0.3,1s-0.5,0.4-1,0.4
|
||||
s-0.7-0.1-0.9-0.4S148,99.9,148,99.5z M150.2,124h-2v-19h2V124z"/>
|
||||
<path class="st4" d="M155.3,113.6c0-3,0.5-5.2,1.5-6.7s2.6-2.2,4.6-2.2c2.2,0,3.8,1,4.9,3l0.1-2.7h1.8v19.6c0,2.3-0.6,4-1.6,5.2
|
||||
c-1.1,1.2-2.7,1.8-4.7,1.8c-1,0-2.1-0.3-3.2-0.8s-1.9-1.1-2.4-1.9l0.9-1.4c1.3,1.5,2.8,2.2,4.5,2.2c1.6,0,2.7-0.4,3.5-1.3
|
||||
c0.7-0.8,1.1-2.1,1.1-3.8v-3c-1.1,1.8-2.7,2.7-4.9,2.7c-2,0-3.5-0.7-4.5-2.2s-1.6-3.6-1.6-6.5V113.6z M157.2,115.4
|
||||
c0,2.4,0.4,4.2,1.1,5.4s1.9,1.7,3.5,1.7c2.1,0,3.6-1,4.5-3v-9.7c-0.9-2.2-2.4-3.3-4.4-3.3c-1.6,0-2.8,0.6-3.5,1.7
|
||||
s-1.1,2.9-1.1,5.3V115.4z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st11" x1="58.9" y1="20.1" x2="77.9" y2="35.6"/>
|
||||
<g>
|
||||
<polygon class="st9" points="57.6,23.2 55.3,17.1 61.7,18.2 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<circle class="st5" cx="48" cy="10.1" r="10.1"/>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st11" x1="77.9" y1="35.1" x2="96.2" y2="66.7"/>
|
||||
<g>
|
||||
<polygon class="st9" points="93,67.5 98.6,70.7 98.6,64.2 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st11" x1="92.3" y1="38.9" x2="114.4" y2="43.6"/>
|
||||
<g>
|
||||
<polygon class="st9" points="92.6,42.3 87.8,38 93.9,36 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st11" x1="77.9" y1="35.6" x2="94.5" y2="25.1"/>
|
||||
<g>
|
||||
<polygon class="st9" points="95.4,28.3 98.4,22.6 91.9,22.9 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st11" x1="48.5" y1="41.3" x2="63.5" y2="38.7"/>
|
||||
<g>
|
||||
<polygon class="st9" points="63.1,42.1 68.1,38 62,35.7 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st11" x1="111.4" y1="66.3" x2="124.4" y2="45.2"/>
|
||||
<g>
|
||||
<polygon class="st9" points="114.6,67.2 109,70.2 109.1,63.8 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st11" x1="115.6" y1="30.4" x2="124.4" y2="45.2"/>
|
||||
<g>
|
||||
<polygon class="st9" points="113.3,32.8 113.3,26.4 118.9,29.5 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<circle class="st3" cx="77.9" cy="35.6" r="10.1"/>
|
||||
<circle class="st4" cx="44.6" cy="42.4" r="10.1"/>
|
||||
<circle class="st7" cx="107.4" cy="18.1" r="10.1"/>
|
||||
<circle class="st8" cx="104.1" cy="79.1" r="10.1"/>
|
||||
<circle class="st6" cx="124.4" cy="45.2" r="10.1"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.2 KiB |
132
art/mgmt_logo_default_wide.svg
Normal file
@@ -0,0 +1,132 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 260.4 71.4" style="enable-background:new 0 0 260.4 71.4;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#FFFFFF;}
|
||||
.st1{fill:#E22434;}
|
||||
.st2{display:none;}
|
||||
.st3{fill:#1B3663;}
|
||||
.st4{fill:#00B1D1;}
|
||||
.st5{fill:#BFE6EF;}
|
||||
.st6{fill:#69CBE0;}
|
||||
.st7{fill:#0080BD;}
|
||||
.st8{fill:#005DAB;}
|
||||
.st9{fill:#183660;}
|
||||
.st10{fill:none;stroke:#183660;stroke-width:1.9441;stroke-miterlimit:10;}
|
||||
.st11{fill:none;stroke:#183660;stroke-width:1.6201;stroke-miterlimit:10;}
|
||||
.st12{fill:none;stroke:#183660;stroke-width:1.2961;stroke-miterlimit:10;}
|
||||
.st13{fill:none;stroke:#C0E6EF;stroke-width:1.9441;stroke-miterlimit:10;}
|
||||
.st14{fill:#C0E6EF;}
|
||||
.st15{fill:none;stroke:#C0E6EF;stroke-width:1.2961;stroke-miterlimit:10;}
|
||||
.st16{fill:none;stroke:#C0E6EF;stroke-width:1.6201;stroke-miterlimit:10;}
|
||||
.st17{fill:none;stroke:#FFFFFF;stroke-width:1.9441;stroke-miterlimit:10;}
|
||||
.st18{fill:none;stroke:#FFFFFF;stroke-width:1.2961;stroke-miterlimit:10;}
|
||||
.st19{fill:none;stroke:#FFFFFF;stroke-width:1.6201;stroke-miterlimit:10;}
|
||||
</style>
|
||||
<g id="Layer_2">
|
||||
</g>
|
||||
<g id="Layer_1">
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<path class="st3" d="M96.7,25.7l0.1,1.8c1.1-1.4,2.6-2.1,4.4-2.1c1.9,0,3.2,0.9,4,2.6c1.1-1.7,2.6-2.6,4.7-2.6
|
||||
c3.3,0,5,2.3,5.1,6.9v12.5h-5V32.6c0-1.1-0.2-1.9-0.5-2.4s-0.8-0.7-1.5-0.7c-0.9,0-1.6,0.6-2.1,1.7l0,0.6v12.9h-5V32.6
|
||||
c0-1.1-0.1-1.9-0.4-2.4s-0.8-0.7-1.6-0.7c-0.9,0-1.5,0.5-2,1.4v13.8h-5v-19H96.7z"/>
|
||||
<path class="st3" d="M118.5,34.6c0-3.1,0.6-5.4,1.7-7s2.7-2.3,4.7-2.3c1.7,0,3.1,0.7,4,2l0.2-1.7h4.5v19c0,2.4-0.7,4.3-2,5.6
|
||||
c-1.4,1.3-3.3,1.9-5.9,1.9c-1,0-2.1-0.2-3.3-0.6s-2-0.9-2.6-1.6l1.7-3.4c0.5,0.5,1.1,0.9,1.8,1.2c0.7,0.3,1.5,0.5,2.1,0.5
|
||||
c1.1,0,1.9-0.3,2.4-0.8s0.7-1.4,0.7-2.6v-1.6c-0.9,1.2-2.2,1.9-3.7,1.9c-2,0-3.6-0.8-4.7-2.4s-1.7-3.8-1.7-6.7V34.6z
|
||||
M123.5,35.9c0,1.8,0.2,3,0.7,3.8s1.2,1.2,2.2,1.2c1,0,1.8-0.4,2.3-1.1v-9.1c-0.5-0.8-1.3-1.2-2.2-1.2c-1,0-1.7,0.4-2.2,1.2
|
||||
s-0.7,2.1-0.7,3.9V35.9z"/>
|
||||
<path class="st3" d="M142.2,25.7l0.1,1.8c1.1-1.4,2.6-2.1,4.4-2.1c1.9,0,3.2,0.9,4,2.6c1.1-1.7,2.6-2.6,4.7-2.6
|
||||
c3.3,0,5,2.3,5.1,6.9v12.5h-5V32.6c0-1.1-0.2-1.9-0.5-2.4c-0.3-0.5-0.8-0.7-1.5-0.7c-0.9,0-1.6,0.6-2.1,1.7l0,0.6v12.9h-5V32.6
|
||||
c0-1.1-0.1-1.9-0.4-2.4c-0.3-0.5-0.8-0.7-1.6-0.7c-0.9,0-1.5,0.5-2,1.4v13.8h-5v-19H142.2z"/>
|
||||
<path class="st3" d="M170.3,21v4.7h2.5v3.7h-2.5v9.5c0,0.8,0.1,1.3,0.3,1.5c0.2,0.3,0.6,0.4,1.2,0.4c0.5,0,0.9,0,1.2-0.1l0,3.9
|
||||
c-0.8,0.3-1.8,0.5-2.7,0.5c-3.2,0-4.8-1.8-4.9-5.5V29.4h-2.2v-3.7h2.2V21H170.3z"/>
|
||||
<path class="st4" d="M182.7,43.2c1.4,0,2.4-0.4,3.1-1.1s1.1-1.8,1.2-3.3h1.9c-0.1,1.9-0.7,3.5-1.9,4.6s-2.6,1.7-4.3,1.7
|
||||
c-2.3,0-4-0.7-5.1-2.2s-1.7-3.6-1.8-6.4v-2.3c0-2.9,0.6-5.1,1.7-6.6s2.9-2.2,5.1-2.2c1.9,0,3.4,0.6,4.5,1.8s1.7,2.9,1.7,5H187
|
||||
c-0.1-1.6-0.5-2.8-1.2-3.6s-1.8-1.3-3.1-1.3c-1.7,0-2.9,0.6-3.7,1.7c-0.8,1.1-1.2,2.9-1.2,5.2v2.2c0,2.4,0.4,4.2,1.2,5.3
|
||||
S181,43.2,182.7,43.2z"/>
|
||||
<path class="st4" d="M192.6,34.2c0-2.7,0.6-4.9,1.9-6.5s2.9-2.4,5.1-2.4c2.2,0,3.9,0.8,5.1,2.4c1.2,1.6,1.9,3.7,1.9,6.5v2
|
||||
c0,2.8-0.6,5-1.9,6.5s-2.9,2.3-5.1,2.3s-3.8-0.8-5-2.3s-1.9-3.6-1.9-6.3V34.2z M194.6,36.2c0,2.2,0.4,3.9,1.3,5.2
|
||||
c0.9,1.3,2.1,1.9,3.7,1.9c1.6,0,2.8-0.6,3.7-1.8c0.8-1.2,1.3-2.9,1.3-5.2v-2c0-2.2-0.4-3.9-1.3-5.2c-0.9-1.3-2.1-1.9-3.7-1.9
|
||||
c-1.5,0-2.7,0.6-3.6,1.8c-0.9,1.2-1.3,2.9-1.4,5.1V36.2z"/>
|
||||
<path class="st4" d="M213.2,25.7l0.1,3c0.6-1,1.3-1.9,2.2-2.5s1.9-0.9,3-0.9c3.3,0,5,2.2,5.1,6.6v12.7h-1.9V32.2
|
||||
c0-1.7-0.3-3-0.9-3.8c-0.6-0.8-1.5-1.2-2.8-1.2c-1,0-1.9,0.4-2.8,1.2s-1.4,1.8-1.9,3.2v13.2h-2v-19H213.2z"/>
|
||||
<path class="st4" d="M230.3,44.7V27.4h-2.6v-1.8h2.6v-2.5c0-1.9,0.5-3.3,1.3-4.3s2-1.5,3.5-1.5c0.7,0,1.3,0.1,1.9,0.3l-0.1,1.8
|
||||
c-0.5-0.1-1-0.2-1.6-0.2c-0.9,0-1.7,0.4-2.2,1.1s-0.8,1.7-0.8,3v2.3h3.7v1.8h-3.7v17.3H230.3z"/>
|
||||
<path class="st4" d="M240.1,20.2c0-0.4,0.1-0.7,0.3-1s0.5-0.4,0.9-0.4s0.7,0.1,1,0.4s0.3,0.6,0.3,1s-0.1,0.7-0.3,1
|
||||
s-0.5,0.4-1,0.4s-0.7-0.1-0.9-0.4S240.1,20.6,240.1,20.2z M242.3,44.7h-2v-19h2V44.7z"/>
|
||||
<path class="st4" d="M247.4,34.3c0-3,0.5-5.2,1.5-6.7s2.6-2.2,4.6-2.2c2.2,0,3.8,1,4.9,3l0.1-2.7h1.8v19.6c0,2.3-0.6,4-1.6,5.2
|
||||
s-2.7,1.8-4.7,1.8c-1,0-2.1-0.3-3.2-0.8s-1.9-1.1-2.4-1.9l0.9-1.4c1.3,1.5,2.8,2.2,4.5,2.2c1.6,0,2.7-0.4,3.5-1.3
|
||||
c0.7-0.8,1.1-2.1,1.1-3.8v-3c-1.1,1.8-2.7,2.7-4.9,2.7c-2,0-3.5-0.7-4.5-2.2s-1.6-3.6-1.6-6.5V34.3z M249.3,36.1
|
||||
c0,2.4,0.4,4.2,1.1,5.4s1.9,1.7,3.5,1.7c2.1,0,3.6-1,4.5-3v-9.7c-0.9-2.2-2.4-3.3-4.4-3.3c-1.6,0-2.8,0.6-3.5,1.7
|
||||
s-1.1,2.9-1.1,5.3V36.1z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st12" x1="19.5" y1="16" x2="34.7" y2="28.5"/>
|
||||
<g>
|
||||
<polygon class="st9" points="18.4,18.5 16.6,13.7 21.7,14.5 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<circle class="st5" cx="10.8" cy="8.1" r="8.1"/>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st12" x1="34.7" y1="28" x2="49.4" y2="53.3"/>
|
||||
<g>
|
||||
<polygon class="st9" points="46.8,54 51.2,56.5 51.2,51.4 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st12" x1="46.2" y1="31.1" x2="63.9" y2="34.9"/>
|
||||
<g>
|
||||
<polygon class="st9" points="46.4,33.8 42.6,30.4 47.5,28.8 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st12" x1="34.7" y1="28.5" x2="47.9" y2="20.1"/>
|
||||
<g>
|
||||
<polygon class="st9" points="48.7,22.7 51.1,18.1 45.9,18.3 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st12" x1="11.2" y1="33.1" x2="23.2" y2="31"/>
|
||||
<g>
|
||||
<polygon class="st9" points="22.9,33.7 26.8,30.4 22,28.6 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st12" x1="61.5" y1="53" x2="71.9" y2="36.1"/>
|
||||
<g>
|
||||
<polygon class="st9" points="64.1,53.7 59.5,56.2 59.7,51 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st12" x1="64.2" y1="24.2" x2="70.7" y2="33.9"/>
|
||||
<g>
|
||||
<polygon class="st9" points="62.5,26.3 62.2,21.1 66.8,23.4 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<circle class="st3" cx="34.7" cy="28.5" r="8.1"/>
|
||||
<circle class="st4" cx="8.1" cy="33.9" r="8.1"/>
|
||||
<circle class="st7" cx="58.3" cy="14.5" r="8.1"/>
|
||||
<circle class="st8" cx="55.7" cy="63.3" r="8.1"/>
|
||||
<circle class="st6" cx="71.9" cy="36.1" r="8.1"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.2 KiB |
94
art/mgmt_logo_reversed_symbol.svg
Normal file
@@ -0,0 +1,94 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 120 107.1" style="enable-background:new 0 0 120 107.1;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#FFFFFF;}
|
||||
.st1{fill:#E22434;}
|
||||
.st2{display:none;}
|
||||
.st3{fill:#1B3663;}
|
||||
.st4{fill:#00B1D1;}
|
||||
.st5{fill:#BFE6EF;}
|
||||
.st6{fill:#69CBE0;}
|
||||
.st7{fill:#0080BD;}
|
||||
.st8{fill:#005DAB;}
|
||||
.st9{fill:#183660;}
|
||||
.st10{fill:none;stroke:#183660;stroke-width:1.9441;stroke-miterlimit:10;}
|
||||
.st11{fill:none;stroke:#183660;stroke-width:1.6201;stroke-miterlimit:10;}
|
||||
.st12{fill:none;stroke:#183660;stroke-width:1.2961;stroke-miterlimit:10;}
|
||||
.st13{fill:none;stroke:#C0E6EF;stroke-width:1.9441;stroke-miterlimit:10;}
|
||||
.st14{fill:#C0E6EF;}
|
||||
.st15{fill:none;stroke:#C0E6EF;stroke-width:1.2961;stroke-miterlimit:10;}
|
||||
.st16{fill:none;stroke:#C0E6EF;stroke-width:1.6201;stroke-miterlimit:10;}
|
||||
.st17{fill:none;stroke:#FFFFFF;stroke-width:1.9441;stroke-miterlimit:10;}
|
||||
.st18{fill:none;stroke:#FFFFFF;stroke-width:1.2961;stroke-miterlimit:10;}
|
||||
.st19{fill:none;stroke:#FFFFFF;stroke-width:1.6201;stroke-miterlimit:10;}
|
||||
</style>
|
||||
<g id="Layer_2">
|
||||
</g>
|
||||
<g id="Layer_1">
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st13" x1="29.2" y1="24.1" x2="52.1" y2="42.8"/>
|
||||
<g>
|
||||
<polygon class="st14" points="27.7,27.8 24.9,20.5 32.6,21.8 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<circle class="st3" cx="16.1" cy="12.2" r="12.1"/>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st13" x1="52.1" y1="42.1" x2="74.1" y2="80"/>
|
||||
<g>
|
||||
<polygon class="st14" points="70.1,80.9 76.9,84.8 76.8,77.1 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st13" x1="69.4" y1="46.7" x2="95.8" y2="52.4"/>
|
||||
<g>
|
||||
<polygon class="st14" points="69.7,50.7 63.9,45.6 71.3,43.2 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st13" x1="52.1" y1="42.8" x2="71.9" y2="30.1"/>
|
||||
<g>
|
||||
<polygon class="st14" points="73.1,34 76.6,27.1 68.9,27.5 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st13" x1="16.8" y1="49.6" x2="34.8" y2="46.5"/>
|
||||
<g>
|
||||
<polygon class="st14" points="34.3,50.5 40.3,45.6 33,42.9 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st13" x1="92.3" y1="79.5" x2="107.8" y2="54.2"/>
|
||||
<g>
|
||||
<polygon class="st14" points="96.1,80.6 89.3,84.3 89.5,76.5 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st13" x1="97.2" y1="36.3" x2="107.8" y2="54.2"/>
|
||||
<g>
|
||||
<polygon class="st14" points="94.4,39.3 94.3,31.5 101.1,35.3 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<circle class="st5" cx="52.1" cy="42.8" r="12.1"/>
|
||||
<circle class="st4" cx="12.2" cy="50.8" r="12.1"/>
|
||||
<circle class="st7" cx="87.5" cy="21.7" r="12.1"/>
|
||||
<circle class="st8" cx="83.5" cy="95" r="12.1"/>
|
||||
<circle class="st6" cx="107.8" cy="54.2" r="12.1"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
132
art/mgmt_logo_reversed_tall.svg
Normal file
@@ -0,0 +1,132 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 168.3 133" style="enable-background:new 0 0 168.3 133;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#FFFFFF;}
|
||||
.st1{fill:#E22434;}
|
||||
.st2{display:none;}
|
||||
.st3{fill:#1B3663;}
|
||||
.st4{fill:#00B1D1;}
|
||||
.st5{fill:#BFE6EF;}
|
||||
.st6{fill:#69CBE0;}
|
||||
.st7{fill:#0080BD;}
|
||||
.st8{fill:#005DAB;}
|
||||
.st9{fill:#183660;}
|
||||
.st10{fill:none;stroke:#183660;stroke-width:1.9441;stroke-miterlimit:10;}
|
||||
.st11{fill:none;stroke:#183660;stroke-width:1.6201;stroke-miterlimit:10;}
|
||||
.st12{fill:none;stroke:#183660;stroke-width:1.2961;stroke-miterlimit:10;}
|
||||
.st13{fill:none;stroke:#C0E6EF;stroke-width:1.9441;stroke-miterlimit:10;}
|
||||
.st14{fill:#C0E6EF;}
|
||||
.st15{fill:none;stroke:#C0E6EF;stroke-width:1.2961;stroke-miterlimit:10;}
|
||||
.st16{fill:none;stroke:#C0E6EF;stroke-width:1.6201;stroke-miterlimit:10;}
|
||||
.st17{fill:none;stroke:#FFFFFF;stroke-width:1.9441;stroke-miterlimit:10;}
|
||||
.st18{fill:none;stroke:#FFFFFF;stroke-width:1.2961;stroke-miterlimit:10;}
|
||||
.st19{fill:none;stroke:#FFFFFF;stroke-width:1.6201;stroke-miterlimit:10;}
|
||||
</style>
|
||||
<g id="Layer_2">
|
||||
</g>
|
||||
<g id="Layer_1">
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<path class="st0" d="M4.7,106.4l0.1,1.8c1.1-1.4,2.6-2.1,4.4-2.1c1.9,0,3.2,0.9,4,2.6c1.1-1.7,2.6-2.6,4.7-2.6
|
||||
c3.3,0,5,2.3,5.1,6.9v12.5h-5v-12.1c0-1.1-0.2-1.9-0.5-2.4c-0.3-0.5-0.8-0.7-1.5-0.7c-0.9,0-1.6,0.6-2.1,1.7l0,0.6v12.9H9v-12.1
|
||||
c0-1.1-0.1-1.9-0.4-2.4c-0.3-0.5-0.8-0.7-1.6-0.7c-0.9,0-1.5,0.5-2,1.4v13.8H0v-19H4.7z"/>
|
||||
<path class="st0" d="M26.4,115.3c0-3.1,0.6-5.4,1.7-7s2.7-2.3,4.7-2.3c1.7,0,3.1,0.7,4,2l0.2-1.7h4.5v19c0,2.4-0.7,4.3-2,5.6
|
||||
c-1.4,1.3-3.3,1.9-5.9,1.9c-1,0-2.1-0.2-3.3-0.6s-2-0.9-2.6-1.6l1.7-3.4c0.5,0.5,1.1,0.9,1.8,1.2c0.7,0.3,1.5,0.5,2.1,0.5
|
||||
c1.1,0,1.9-0.3,2.4-0.8c0.5-0.5,0.7-1.4,0.7-2.6v-1.6c-0.9,1.2-2.2,1.9-3.7,1.9c-2,0-3.6-0.8-4.7-2.4s-1.7-3.8-1.7-6.7V115.3z
|
||||
M31.4,116.6c0,1.8,0.2,3,0.7,3.8s1.2,1.2,2.2,1.2c1,0,1.8-0.4,2.3-1.1v-9.1c-0.5-0.8-1.3-1.2-2.2-1.2c-1,0-1.7,0.4-2.2,1.2
|
||||
s-0.7,2.1-0.7,3.9V116.6z"/>
|
||||
<path class="st0" d="M50.1,106.4l0.1,1.8c1.1-1.4,2.6-2.1,4.4-2.1c1.9,0,3.2,0.9,4,2.6c1.1-1.7,2.6-2.6,4.7-2.6
|
||||
c3.3,0,5,2.3,5.1,6.9v12.5h-5v-12.1c0-1.1-0.2-1.9-0.5-2.4s-0.8-0.7-1.5-0.7c-0.9,0-1.6,0.6-2.1,1.7l0,0.6v12.9h-5v-12.1
|
||||
c0-1.1-0.1-1.9-0.4-2.4s-0.8-0.7-1.6-0.7c-0.9,0-1.5,0.5-2,1.4v13.8h-5v-19H50.1z"/>
|
||||
<path class="st0" d="M78.2,101.7v4.7h2.5v3.7h-2.5v9.5c0,0.8,0.1,1.3,0.3,1.5c0.2,0.3,0.6,0.4,1.2,0.4c0.5,0,0.9,0,1.2-0.1
|
||||
l0,3.9c-0.8,0.3-1.8,0.5-2.7,0.5c-3.2,0-4.8-1.8-4.9-5.5v-10.1h-2.2v-3.7h2.2v-4.7H78.2z"/>
|
||||
<path class="st4" d="M90.6,124c1.4,0,2.4-0.4,3.1-1.1c0.7-0.8,1.1-1.9,1.2-3.3h1.9c-0.1,1.9-0.7,3.5-1.9,4.6
|
||||
c-1.1,1.1-2.6,1.7-4.3,1.7c-2.3,0-4-0.7-5.1-2.2s-1.7-3.6-1.8-6.4v-2.3c0-2.9,0.6-5.1,1.7-6.6s2.9-2.2,5.1-2.2
|
||||
c1.9,0,3.4,0.6,4.5,1.8s1.7,2.9,1.7,5H95c-0.1-1.6-0.5-2.8-1.2-3.6s-1.8-1.3-3.1-1.3c-1.7,0-2.9,0.6-3.7,1.7s-1.2,2.9-1.2,5.2
|
||||
v2.2c0,2.4,0.4,4.2,1.2,5.3S89,124,90.6,124z"/>
|
||||
<path class="st4" d="M100.5,115c0-2.7,0.6-4.9,1.9-6.5s2.9-2.4,5.1-2.4c2.2,0,3.9,0.8,5.1,2.4s1.9,3.7,1.9,6.5v2
|
||||
c0,2.8-0.6,5-1.9,6.5s-2.9,2.3-5.1,2.3c-2.1,0-3.8-0.8-5-2.3s-1.9-3.6-1.9-6.3V115z M102.5,116.9c0,2.2,0.4,3.9,1.3,5.2
|
||||
c0.9,1.3,2.1,1.9,3.7,1.9c1.6,0,2.8-0.6,3.7-1.8s1.3-2.9,1.3-5.2v-2c0-2.2-0.4-3.9-1.3-5.2c-0.9-1.3-2.1-1.9-3.7-1.9
|
||||
c-1.5,0-2.7,0.6-3.6,1.8s-1.3,2.9-1.4,5.1V116.9z"/>
|
||||
<path class="st4" d="M121.1,106.4l0.1,3c0.6-1,1.3-1.9,2.2-2.5s1.9-0.9,3-0.9c3.3,0,5,2.2,5.1,6.6v12.7h-1.9v-12.5
|
||||
c0-1.7-0.3-3-0.9-3.8c-0.6-0.8-1.5-1.2-2.8-1.2c-1,0-1.9,0.4-2.8,1.2s-1.4,1.8-1.9,3.2v13.2h-2v-19H121.1z"/>
|
||||
<path class="st4" d="M138.2,125.4v-17.3h-2.6v-1.8h2.6v-2.5c0-1.9,0.5-3.3,1.3-4.3s2-1.5,3.5-1.5c0.7,0,1.3,0.1,1.9,0.3
|
||||
l-0.1,1.8c-0.5-0.1-1-0.2-1.6-0.2c-0.9,0-1.7,0.4-2.2,1.1s-0.8,1.7-0.8,3v2.3h3.7v1.8h-3.7v17.3H138.2z"/>
|
||||
<path class="st4" d="M148,100.9c0-0.4,0.1-0.7,0.3-1s0.5-0.4,0.9-0.4s0.7,0.1,1,0.4s0.3,0.6,0.3,1s-0.1,0.7-0.3,1
|
||||
s-0.5,0.4-1,0.4s-0.7-0.1-0.9-0.4S148,101.3,148,100.9z M150.2,125.4h-2v-19h2V125.4z"/>
|
||||
<path class="st4" d="M155.3,115c0-3,0.5-5.2,1.5-6.7s2.6-2.2,4.6-2.2c2.2,0,3.8,1,4.9,3l0.1-2.7h1.8V126c0,2.3-0.6,4-1.6,5.2
|
||||
c-1.1,1.2-2.7,1.8-4.7,1.8c-1,0-2.1-0.3-3.2-0.8s-1.9-1.1-2.4-1.9l0.9-1.4c1.3,1.5,2.8,2.2,4.5,2.2c1.6,0,2.7-0.4,3.5-1.3
|
||||
c0.7-0.8,1.1-2.1,1.1-3.8v-3c-1.1,1.8-2.7,2.7-4.9,2.7c-2,0-3.5-0.7-4.5-2.2s-1.6-3.6-1.6-6.5V115z M157.2,116.8
|
||||
c0,2.4,0.4,4.2,1.1,5.4s1.9,1.7,3.5,1.7c2.1,0,3.6-1,4.5-3v-9.7c-0.9-2.2-2.4-3.3-4.4-3.3c-1.6,0-2.8,0.6-3.5,1.7
|
||||
s-1.1,2.9-1.1,5.3V116.8z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st16" x1="58.9" y1="20.1" x2="77.9" y2="35.6"/>
|
||||
<g>
|
||||
<polygon class="st14" points="57.6,23.2 55.3,17.1 61.7,18.2 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<circle class="st3" cx="48" cy="10.1" r="10.1"/>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st16" x1="77.9" y1="35.1" x2="96.2" y2="66.7"/>
|
||||
<g>
|
||||
<polygon class="st14" points="93,67.5 98.6,70.7 98.6,64.2 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st16" x1="92.3" y1="38.9" x2="114.4" y2="43.6"/>
|
||||
<g>
|
||||
<polygon class="st14" points="92.6,42.3 87.8,38 93.9,36 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st16" x1="77.9" y1="35.6" x2="94.5" y2="25.1"/>
|
||||
<g>
|
||||
<polygon class="st14" points="95.4,28.3 98.4,22.6 91.9,22.9 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st16" x1="48.5" y1="41.3" x2="63.5" y2="38.7"/>
|
||||
<g>
|
||||
<polygon class="st14" points="63.1,42.1 68.1,38 62,35.7 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st16" x1="111.4" y1="66.3" x2="124.4" y2="45.2"/>
|
||||
<g>
|
||||
<polygon class="st14" points="114.6,67.2 109,70.2 109.1,63.8 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st16" x1="115.5" y1="30.5" x2="124.4" y2="45.2"/>
|
||||
<g>
|
||||
<polygon class="st14" points="113.2,32.9 113.1,26.5 118.8,29.6 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<circle class="st5" cx="77.9" cy="35.6" r="10.1"/>
|
||||
<circle class="st4" cx="44.6" cy="42.4" r="10.1"/>
|
||||
<circle class="st7" cx="107.4" cy="18.1" r="10.1"/>
|
||||
<circle class="st8" cx="104.1" cy="79.1" r="10.1"/>
|
||||
<circle class="st6" cx="124.4" cy="45.2" r="10.1"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.2 KiB |
132
art/mgmt_logo_reversed_wide.svg
Normal file
@@ -0,0 +1,132 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 260.4 71.4" style="enable-background:new 0 0 260.4 71.4;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#FFFFFF;}
|
||||
.st1{fill:#E22434;}
|
||||
.st2{display:none;}
|
||||
.st3{fill:#1B3663;}
|
||||
.st4{fill:#00B1D1;}
|
||||
.st5{fill:#BFE6EF;}
|
||||
.st6{fill:#69CBE0;}
|
||||
.st7{fill:#0080BD;}
|
||||
.st8{fill:#005DAB;}
|
||||
.st9{fill:#183660;}
|
||||
.st10{fill:none;stroke:#183660;stroke-width:1.9441;stroke-miterlimit:10;}
|
||||
.st11{fill:none;stroke:#183660;stroke-width:1.6201;stroke-miterlimit:10;}
|
||||
.st12{fill:none;stroke:#183660;stroke-width:1.2961;stroke-miterlimit:10;}
|
||||
.st13{fill:none;stroke:#C0E6EF;stroke-width:1.9441;stroke-miterlimit:10;}
|
||||
.st14{fill:#C0E6EF;}
|
||||
.st15{fill:none;stroke:#C0E6EF;stroke-width:1.2961;stroke-miterlimit:10;}
|
||||
.st16{fill:none;stroke:#C0E6EF;stroke-width:1.6201;stroke-miterlimit:10;}
|
||||
.st17{fill:none;stroke:#FFFFFF;stroke-width:1.9441;stroke-miterlimit:10;}
|
||||
.st18{fill:none;stroke:#FFFFFF;stroke-width:1.2961;stroke-miterlimit:10;}
|
||||
.st19{fill:none;stroke:#FFFFFF;stroke-width:1.6201;stroke-miterlimit:10;}
|
||||
</style>
|
||||
<g id="Layer_2">
|
||||
</g>
|
||||
<g id="Layer_1">
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<path class="st0" d="M96.7,27.6l0.1,1.8c1.1-1.4,2.6-2.1,4.4-2.1c1.9,0,3.2,0.9,4,2.6c1.1-1.7,2.6-2.6,4.7-2.6
|
||||
c3.3,0,5,2.3,5.1,6.9v12.5h-5V34.4c0-1.1-0.2-1.9-0.5-2.4c-0.3-0.5-0.8-0.7-1.5-0.7c-0.9,0-1.6,0.6-2.1,1.7l0,0.6v12.9h-5V34.5
|
||||
c0-1.1-0.1-1.9-0.4-2.4c-0.3-0.5-0.8-0.7-1.6-0.7c-0.9,0-1.5,0.5-2,1.4v13.8h-5v-19H96.7z"/>
|
||||
<path class="st0" d="M118.5,36.5c0-3.1,0.6-5.4,1.7-7s2.7-2.3,4.7-2.3c1.7,0,3.1,0.7,4,2l0.2-1.7h4.5v19c0,2.4-0.7,4.3-2,5.6
|
||||
c-1.4,1.3-3.3,1.9-5.9,1.9c-1,0-2.1-0.2-3.3-0.6s-2-0.9-2.6-1.6l1.7-3.4c0.5,0.5,1.1,0.9,1.8,1.2c0.7,0.3,1.5,0.5,2.1,0.5
|
||||
c1.1,0,1.9-0.3,2.4-0.8s0.7-1.4,0.7-2.6v-1.6c-0.9,1.2-2.2,1.9-3.7,1.9c-2,0-3.6-0.8-4.7-2.4s-1.7-3.8-1.7-6.7V36.5z
|
||||
M123.5,37.7c0,1.8,0.2,3,0.7,3.8s1.2,1.2,2.2,1.2c1,0,1.8-0.4,2.3-1.1v-9.1c-0.5-0.8-1.3-1.2-2.2-1.2c-1,0-1.7,0.4-2.2,1.2
|
||||
c-0.5,0.8-0.7,2.1-0.7,3.9V37.7z"/>
|
||||
<path class="st0" d="M142.2,27.6l0.1,1.8c1.1-1.4,2.6-2.1,4.4-2.1c1.9,0,3.2,0.9,4,2.6c1.1-1.7,2.6-2.6,4.7-2.6
|
||||
c3.3,0,5,2.3,5.1,6.9v12.5h-5V34.4c0-1.1-0.2-1.9-0.5-2.4c-0.3-0.5-0.8-0.7-1.5-0.7c-0.9,0-1.6,0.6-2.1,1.7l0,0.6v12.9h-5V34.5
|
||||
c0-1.1-0.1-1.9-0.4-2.4c-0.3-0.5-0.8-0.7-1.6-0.7c-0.9,0-1.5,0.5-2,1.4v13.8h-5v-19H142.2z"/>
|
||||
<path class="st0" d="M170.3,22.9v4.7h2.5v3.7h-2.5v9.5c0,0.8,0.1,1.3,0.3,1.5c0.2,0.3,0.6,0.4,1.2,0.4c0.5,0,0.9,0,1.2-0.1
|
||||
l0,3.9c-0.8,0.3-1.8,0.5-2.7,0.5c-3.2,0-4.8-1.8-4.9-5.5V31.3h-2.2v-3.7h2.2v-4.7H170.3z"/>
|
||||
<path class="st4" d="M182.7,45.1c1.4,0,2.4-0.4,3.1-1.1c0.7-0.8,1.1-1.9,1.2-3.3h1.9c-0.1,1.9-0.7,3.5-1.9,4.6s-2.6,1.7-4.3,1.7
|
||||
c-2.3,0-4-0.7-5.1-2.2s-1.7-3.6-1.8-6.4V36c0-2.9,0.6-5.1,1.7-6.6s2.9-2.2,5.1-2.2c1.9,0,3.4,0.6,4.5,1.8s1.7,2.9,1.7,5H187
|
||||
c-0.1-1.6-0.5-2.8-1.2-3.6s-1.8-1.3-3.1-1.3c-1.7,0-2.9,0.6-3.7,1.7s-1.2,2.9-1.2,5.2v2.2c0,2.4,0.4,4.2,1.2,5.3
|
||||
S181,45.1,182.7,45.1z"/>
|
||||
<path class="st4" d="M192.6,36.1c0-2.7,0.6-4.9,1.9-6.5s2.9-2.4,5.1-2.4c2.2,0,3.9,0.8,5.1,2.4c1.2,1.6,1.9,3.7,1.9,6.5v2
|
||||
c0,2.8-0.6,5-1.9,6.5s-2.9,2.3-5.1,2.3s-3.8-0.8-5-2.3s-1.9-3.6-1.9-6.3V36.1z M194.6,38.1c0,2.2,0.4,3.9,1.3,5.2
|
||||
c0.9,1.3,2.1,1.9,3.7,1.9c1.6,0,2.8-0.6,3.7-1.8c0.8-1.2,1.3-2.9,1.3-5.2v-2c0-2.2-0.4-3.9-1.3-5.2c-0.9-1.3-2.1-1.9-3.7-1.9
|
||||
c-1.5,0-2.7,0.6-3.6,1.8c-0.9,1.2-1.3,2.9-1.4,5.1V38.1z"/>
|
||||
<path class="st4" d="M213.2,27.6l0.1,3c0.6-1,1.3-1.9,2.2-2.5s1.9-0.9,3-0.9c3.3,0,5,2.2,5.1,6.6v12.7h-1.9V34.1
|
||||
c0-1.7-0.3-3-0.9-3.8c-0.6-0.8-1.5-1.2-2.8-1.2c-1,0-1.9,0.4-2.8,1.2s-1.4,1.8-1.9,3.2v13.2h-2v-19H213.2z"/>
|
||||
<path class="st4" d="M230.3,46.6V29.3h-2.6v-1.8h2.6v-2.5c0-1.9,0.5-3.3,1.3-4.3s2-1.5,3.5-1.5c0.7,0,1.3,0.1,1.9,0.3l-0.1,1.8
|
||||
c-0.5-0.1-1-0.2-1.6-0.2c-0.9,0-1.7,0.4-2.2,1.1s-0.8,1.7-0.8,3v2.3h3.7v1.8h-3.7v17.3H230.3z"/>
|
||||
<path class="st4" d="M240.1,22.1c0-0.4,0.1-0.7,0.3-1s0.5-0.4,0.9-0.4s0.7,0.1,1,0.4s0.3,0.6,0.3,1s-0.1,0.7-0.3,1
|
||||
s-0.5,0.4-1,0.4s-0.7-0.1-0.9-0.4S240.1,22.5,240.1,22.1z M242.3,46.6h-2v-19h2V46.6z"/>
|
||||
<path class="st4" d="M247.4,36.2c0-3,0.5-5.2,1.5-6.7s2.6-2.2,4.6-2.2c2.2,0,3.8,1,4.9,3l0.1-2.7h1.8v19.6c0,2.3-0.6,4-1.6,5.2
|
||||
s-2.7,1.8-4.7,1.8c-1,0-2.1-0.3-3.2-0.8s-1.9-1.1-2.4-1.9l0.9-1.4c1.3,1.5,2.8,2.2,4.5,2.2c1.6,0,2.7-0.4,3.5-1.3
|
||||
c0.7-0.8,1.1-2.1,1.1-3.8v-3c-1.1,1.8-2.7,2.7-4.9,2.7c-2,0-3.5-0.7-4.5-2.2s-1.6-3.6-1.6-6.5V36.2z M249.3,38
|
||||
c0,2.4,0.4,4.2,1.1,5.4s1.9,1.7,3.5,1.7c2.1,0,3.6-1,4.5-3v-9.7c-0.9-2.2-2.4-3.3-4.4-3.3c-1.6,0-2.8,0.6-3.5,1.7
|
||||
s-1.1,2.9-1.1,5.3V38z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st15" x1="19.5" y1="16" x2="34.7" y2="28.5"/>
|
||||
<g>
|
||||
<polygon class="st14" points="18.4,18.5 16.6,13.7 21.7,14.5 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<circle class="st3" cx="10.8" cy="8.1" r="8.1"/>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st15" x1="34.7" y1="28" x2="49.4" y2="53.3"/>
|
||||
<g>
|
||||
<polygon class="st14" points="46.8,54 51.2,56.5 51.2,51.4 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st15" x1="46.2" y1="31.1" x2="63.9" y2="34.9"/>
|
||||
<g>
|
||||
<polygon class="st14" points="46.4,33.8 42.6,30.4 47.5,28.8 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st15" x1="34.7" y1="28.5" x2="47.9" y2="20.1"/>
|
||||
<g>
|
||||
<polygon class="st14" points="48.7,22.7 51.1,18.1 45.9,18.3 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st15" x1="11.2" y1="33.1" x2="23.2" y2="31"/>
|
||||
<g>
|
||||
<polygon class="st14" points="22.9,33.7 26.8,30.4 22,28.6 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st15" x1="61.5" y1="53" x2="71.9" y2="36.1"/>
|
||||
<g>
|
||||
<polygon class="st14" points="64.1,53.7 59.5,56.2 59.7,51 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st15" x1="64.8" y1="24.4" x2="71.9" y2="36.1"/>
|
||||
<g>
|
||||
<polygon class="st14" points="63,26.4 62.9,21.2 67.4,23.7 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<circle class="st5" cx="34.7" cy="28.5" r="8.1"/>
|
||||
<circle class="st4" cx="8.1" cy="33.9" r="8.1"/>
|
||||
<circle class="st7" cx="58.3" cy="14.5" r="8.1"/>
|
||||
<circle class="st8" cx="55.7" cy="63.3" r="8.1"/>
|
||||
<circle class="st6" cx="71.9" cy="36.1" r="8.1"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.2 KiB |
94
art/mgmt_logo_white_symbol.svg
Normal file
@@ -0,0 +1,94 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 120 107.1" style="enable-background:new 0 0 120 107.1;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#FFFFFF;}
|
||||
.st1{fill:#E22434;}
|
||||
.st2{display:none;}
|
||||
.st3{fill:#1B3663;}
|
||||
.st4{fill:#00B1D1;}
|
||||
.st5{fill:#BFE6EF;}
|
||||
.st6{fill:#69CBE0;}
|
||||
.st7{fill:#0080BD;}
|
||||
.st8{fill:#005DAB;}
|
||||
.st9{fill:#183660;}
|
||||
.st10{fill:none;stroke:#183660;stroke-width:1.9441;stroke-miterlimit:10;}
|
||||
.st11{fill:none;stroke:#183660;stroke-width:1.6201;stroke-miterlimit:10;}
|
||||
.st12{fill:none;stroke:#183660;stroke-width:1.2961;stroke-miterlimit:10;}
|
||||
.st13{fill:none;stroke:#C0E6EF;stroke-width:1.9441;stroke-miterlimit:10;}
|
||||
.st14{fill:#C0E6EF;}
|
||||
.st15{fill:none;stroke:#C0E6EF;stroke-width:1.2961;stroke-miterlimit:10;}
|
||||
.st16{fill:none;stroke:#C0E6EF;stroke-width:1.6201;stroke-miterlimit:10;}
|
||||
.st17{fill:none;stroke:#FFFFFF;stroke-width:1.9441;stroke-miterlimit:10;}
|
||||
.st18{fill:none;stroke:#FFFFFF;stroke-width:1.2961;stroke-miterlimit:10;}
|
||||
.st19{fill:none;stroke:#FFFFFF;stroke-width:1.6201;stroke-miterlimit:10;}
|
||||
</style>
|
||||
<g id="Layer_2">
|
||||
</g>
|
||||
<g id="Layer_1">
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st17" x1="29.2" y1="24.1" x2="52.1" y2="42.8"/>
|
||||
<g>
|
||||
<polygon class="st0" points="27.7,27.8 24.9,20.5 32.6,21.8 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<circle class="st0" cx="16.1" cy="12.2" r="12.1"/>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st17" x1="52.1" y1="42.1" x2="74.1" y2="80"/>
|
||||
<g>
|
||||
<polygon class="st0" points="70.1,80.9 76.9,84.8 76.8,77.1 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st17" x1="69.4" y1="46.7" x2="95.8" y2="52.4"/>
|
||||
<g>
|
||||
<polygon class="st0" points="69.7,50.7 63.9,45.6 71.3,43.2 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st17" x1="52.1" y1="42.8" x2="71.9" y2="30.1"/>
|
||||
<g>
|
||||
<polygon class="st0" points="73.1,34 76.6,27.1 68.9,27.5 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st17" x1="16.8" y1="49.6" x2="34.8" y2="46.5"/>
|
||||
<g>
|
||||
<polygon class="st0" points="34.3,50.5 40.3,45.6 33,42.9 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st17" x1="92.3" y1="79.5" x2="107.8" y2="54.2"/>
|
||||
<g>
|
||||
<polygon class="st0" points="96.1,80.6 89.3,84.3 89.5,76.5 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st17" x1="97.2" y1="36.6" x2="107.8" y2="54.2"/>
|
||||
<g>
|
||||
<polygon class="st0" points="94.4,39.5 94.3,31.8 101.1,35.5 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<circle class="st0" cx="52.1" cy="42.8" r="12.1"/>
|
||||
<circle class="st0" cx="12.2" cy="50.8" r="12.1"/>
|
||||
<circle class="st0" cx="87.5" cy="21.7" r="12.1"/>
|
||||
<circle class="st0" cx="83.5" cy="95" r="12.1"/>
|
||||
<circle class="st0" cx="107.8" cy="54.2" r="12.1"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
132
art/mgmt_logo_white_tall.svg
Normal file
@@ -0,0 +1,132 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 168.3 131.4" style="enable-background:new 0 0 168.3 131.4;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#FFFFFF;}
|
||||
.st1{fill:#E22434;}
|
||||
.st2{display:none;}
|
||||
.st3{fill:#1B3663;}
|
||||
.st4{fill:#00B1D1;}
|
||||
.st5{fill:#BFE6EF;}
|
||||
.st6{fill:#69CBE0;}
|
||||
.st7{fill:#0080BD;}
|
||||
.st8{fill:#005DAB;}
|
||||
.st9{fill:#183660;}
|
||||
.st10{fill:none;stroke:#183660;stroke-width:1.9441;stroke-miterlimit:10;}
|
||||
.st11{fill:none;stroke:#183660;stroke-width:1.6201;stroke-miterlimit:10;}
|
||||
.st12{fill:none;stroke:#183660;stroke-width:1.2961;stroke-miterlimit:10;}
|
||||
.st13{fill:none;stroke:#C0E6EF;stroke-width:1.9441;stroke-miterlimit:10;}
|
||||
.st14{fill:#C0E6EF;}
|
||||
.st15{fill:none;stroke:#C0E6EF;stroke-width:1.2961;stroke-miterlimit:10;}
|
||||
.st16{fill:none;stroke:#C0E6EF;stroke-width:1.6201;stroke-miterlimit:10;}
|
||||
.st17{fill:none;stroke:#FFFFFF;stroke-width:1.9441;stroke-miterlimit:10;}
|
||||
.st18{fill:none;stroke:#FFFFFF;stroke-width:1.2961;stroke-miterlimit:10;}
|
||||
.st19{fill:none;stroke:#FFFFFF;stroke-width:1.6201;stroke-miterlimit:10;}
|
||||
</style>
|
||||
<g id="Layer_2">
|
||||
</g>
|
||||
<g id="Layer_1">
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<path class="st0" d="M4.7,104.8l0.1,1.8c1.1-1.4,2.6-2.1,4.4-2.1c1.9,0,3.2,0.9,4,2.6c1.1-1.7,2.6-2.6,4.7-2.6
|
||||
c3.3,0,5,2.3,5.1,6.9v12.5h-5v-12.1c0-1.1-0.2-1.9-0.5-2.4c-0.3-0.5-0.8-0.7-1.5-0.7c-0.9,0-1.6,0.6-2.1,1.7l0,0.6v12.9H9v-12.1
|
||||
c0-1.1-0.1-1.9-0.4-2.4c-0.3-0.5-0.8-0.7-1.6-0.7c-0.9,0-1.5,0.5-2,1.4v13.8H0v-19H4.7z"/>
|
||||
<path class="st0" d="M26.4,113.8c0-3.1,0.6-5.4,1.7-7s2.7-2.3,4.7-2.3c1.7,0,3.1,0.7,4,2l0.2-1.7h4.5v19c0,2.4-0.7,4.3-2,5.6
|
||||
c-1.4,1.3-3.3,1.9-5.9,1.9c-1,0-2.1-0.2-3.3-0.6s-2-0.9-2.6-1.6l1.7-3.4c0.5,0.5,1.1,0.9,1.8,1.2c0.7,0.3,1.5,0.5,2.1,0.5
|
||||
c1.1,0,1.9-0.3,2.4-0.8s0.7-1.4,0.7-2.6v-1.6c-0.9,1.2-2.2,1.9-3.7,1.9c-2,0-3.6-0.8-4.7-2.4s-1.7-3.8-1.7-6.7V113.8z M31.4,115
|
||||
c0,1.8,0.2,3,0.7,3.8s1.2,1.2,2.2,1.2c1,0,1.8-0.4,2.3-1.1v-9.1c-0.5-0.8-1.3-1.2-2.2-1.2c-1,0-1.7,0.4-2.2,1.2
|
||||
s-0.7,2.1-0.7,3.9V115z"/>
|
||||
<path class="st0" d="M50.1,104.8l0.1,1.8c1.1-1.4,2.6-2.1,4.4-2.1c1.9,0,3.2,0.9,4,2.6c1.1-1.7,2.6-2.6,4.7-2.6
|
||||
c3.3,0,5,2.3,5.1,6.9v12.5h-5v-12.1c0-1.1-0.2-1.9-0.5-2.4s-0.8-0.7-1.5-0.7c-0.9,0-1.6,0.6-2.1,1.7l0,0.6v12.9h-5v-12.1
|
||||
c0-1.1-0.1-1.9-0.4-2.4s-0.8-0.7-1.6-0.7c-0.9,0-1.5,0.5-2,1.4v13.8h-5v-19H50.1z"/>
|
||||
<path class="st0" d="M78.2,100.2v4.7h2.5v3.7h-2.5v9.5c0,0.8,0.1,1.3,0.3,1.5c0.2,0.3,0.6,0.4,1.2,0.4c0.5,0,0.9,0,1.2-0.1
|
||||
l0,3.9c-0.8,0.3-1.8,0.5-2.7,0.5c-3.2,0-4.8-1.8-4.9-5.5v-10.1h-2.2v-3.7h2.2v-4.7H78.2z"/>
|
||||
<path class="st0" d="M90.6,122.4c1.4,0,2.4-0.4,3.1-1.1c0.7-0.8,1.1-1.8,1.2-3.3h1.9c-0.1,1.9-0.7,3.5-1.9,4.6
|
||||
c-1.1,1.1-2.6,1.7-4.3,1.7c-2.3,0-4-0.7-5.1-2.2s-1.7-3.6-1.8-6.4v-2.3c0-2.9,0.6-5.1,1.7-6.6s2.9-2.2,5.1-2.2
|
||||
c1.9,0,3.4,0.6,4.5,1.8s1.7,2.9,1.7,5H95c-0.1-1.6-0.5-2.8-1.2-3.6s-1.8-1.3-3.1-1.3c-1.7,0-2.9,0.6-3.7,1.7
|
||||
c-0.8,1.1-1.2,2.9-1.2,5.2v2.2c0,2.4,0.4,4.2,1.2,5.3S89,122.4,90.6,122.4z"/>
|
||||
<path class="st0" d="M100.5,113.4c0-2.7,0.6-4.9,1.9-6.5s2.9-2.4,5.1-2.4c2.2,0,3.9,0.8,5.1,2.4s1.9,3.7,1.9,6.5v2
|
||||
c0,2.8-0.6,5-1.9,6.5s-2.9,2.3-5.1,2.3c-2.1,0-3.8-0.8-5-2.3s-1.9-3.6-1.9-6.3V113.4z M102.5,115.3c0,2.2,0.4,3.9,1.3,5.2
|
||||
c0.9,1.3,2.1,1.9,3.7,1.9c1.6,0,2.8-0.6,3.7-1.8c0.8-1.2,1.3-2.9,1.3-5.2v-2c0-2.2-0.4-3.9-1.3-5.2c-0.9-1.3-2.1-1.9-3.7-1.9
|
||||
c-1.5,0-2.7,0.6-3.6,1.8c-0.9,1.2-1.3,2.9-1.4,5.1V115.3z"/>
|
||||
<path class="st0" d="M121.1,104.8l0.1,3c0.6-1,1.3-1.9,2.2-2.5s1.9-0.9,3-0.9c3.3,0,5,2.2,5.1,6.6v12.7h-1.9v-12.5
|
||||
c0-1.7-0.3-3-0.9-3.8c-0.6-0.8-1.5-1.2-2.8-1.2c-1,0-1.9,0.4-2.8,1.2s-1.4,1.8-1.9,3.2v13.2h-2v-19H121.1z"/>
|
||||
<path class="st0" d="M138.2,123.8v-17.3h-2.6v-1.8h2.6v-2.5c0-1.9,0.5-3.3,1.3-4.3s2-1.5,3.5-1.5c0.7,0,1.3,0.1,1.9,0.3
|
||||
l-0.1,1.8c-0.5-0.1-1-0.2-1.6-0.2c-0.9,0-1.7,0.4-2.2,1.1s-0.8,1.7-0.8,3v2.3h3.7v1.8h-3.7v17.3H138.2z"/>
|
||||
<path class="st0" d="M148,99.3c0-0.4,0.1-0.7,0.3-1s0.5-0.4,0.9-0.4s0.7,0.1,1,0.4s0.3,0.6,0.3,1s-0.1,0.7-0.3,1s-0.5,0.4-1,0.4
|
||||
s-0.7-0.1-0.9-0.4S148,99.7,148,99.3z M150.2,123.8h-2v-19h2V123.8z"/>
|
||||
<path class="st0" d="M155.3,113.5c0-3,0.5-5.2,1.5-6.7s2.6-2.2,4.6-2.2c2.2,0,3.8,1,4.9,3l0.1-2.7h1.8v19.6c0,2.3-0.6,4-1.6,5.2
|
||||
c-1.1,1.2-2.7,1.8-4.7,1.8c-1,0-2.1-0.3-3.2-0.8s-1.9-1.1-2.4-1.9l0.9-1.4c1.3,1.5,2.8,2.2,4.5,2.2c1.6,0,2.7-0.4,3.5-1.3
|
||||
c0.7-0.8,1.1-2.1,1.1-3.8v-3c-1.1,1.8-2.7,2.7-4.9,2.7c-2,0-3.5-0.7-4.5-2.2s-1.6-3.6-1.6-6.5V113.5z M157.2,115.2
|
||||
c0,2.4,0.4,4.2,1.1,5.4s1.9,1.7,3.5,1.7c2.1,0,3.6-1,4.5-3v-9.7c-0.9-2.2-2.4-3.3-4.4-3.3c-1.6,0-2.8,0.6-3.5,1.7
|
||||
s-1.1,2.9-1.1,5.3V115.2z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st19" x1="58.9" y1="20.1" x2="77.9" y2="35.6"/>
|
||||
<g>
|
||||
<polygon class="st0" points="57.6,23.2 55.3,17.1 61.7,18.2 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<circle class="st0" cx="48" cy="10.1" r="10.1"/>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st19" x1="77.9" y1="35.1" x2="96.2" y2="66.7"/>
|
||||
<g>
|
||||
<polygon class="st0" points="93,67.5 98.6,70.7 98.6,64.2 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st19" x1="92.3" y1="38.9" x2="114.4" y2="43.6"/>
|
||||
<g>
|
||||
<polygon class="st0" points="92.6,42.3 87.8,38 93.9,36 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st19" x1="77.9" y1="35.6" x2="94.5" y2="25.1"/>
|
||||
<g>
|
||||
<polygon class="st0" points="95.4,28.3 98.4,22.6 91.9,22.9 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st19" x1="48.5" y1="41.3" x2="63.5" y2="38.7"/>
|
||||
<g>
|
||||
<polygon class="st0" points="63.1,42.1 68.1,38 62,35.7 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st19" x1="111.4" y1="66.3" x2="124.4" y2="45.2"/>
|
||||
<g>
|
||||
<polygon class="st0" points="114.6,67.2 109,70.2 109.1,63.8 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st19" x1="115.4" y1="30.4" x2="124.4" y2="45.2"/>
|
||||
<g>
|
||||
<polygon class="st0" points="113.2,32.8 113,26.4 118.7,29.5 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<circle class="st0" cx="77.9" cy="35.6" r="10.1"/>
|
||||
<circle class="st0" cx="44.6" cy="42.4" r="10.1"/>
|
||||
<circle class="st0" cx="107.4" cy="18.1" r="10.1"/>
|
||||
<circle class="st0" cx="104.1" cy="79.1" r="10.1"/>
|
||||
<circle class="st0" cx="124.4" cy="45.2" r="10.1"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.2 KiB |
132
art/mgmt_logo_white_wide.svg
Normal file
@@ -0,0 +1,132 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 260.4 71.4" style="enable-background:new 0 0 260.4 71.4;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#FFFFFF;}
|
||||
.st1{fill:#E22434;}
|
||||
.st2{display:none;}
|
||||
.st3{fill:#1B3663;}
|
||||
.st4{fill:#00B1D1;}
|
||||
.st5{fill:#BFE6EF;}
|
||||
.st6{fill:#69CBE0;}
|
||||
.st7{fill:#0080BD;}
|
||||
.st8{fill:#005DAB;}
|
||||
.st9{fill:#183660;}
|
||||
.st10{fill:none;stroke:#183660;stroke-width:1.9441;stroke-miterlimit:10;}
|
||||
.st11{fill:none;stroke:#183660;stroke-width:1.6201;stroke-miterlimit:10;}
|
||||
.st12{fill:none;stroke:#183660;stroke-width:1.2961;stroke-miterlimit:10;}
|
||||
.st13{fill:none;stroke:#C0E6EF;stroke-width:1.9441;stroke-miterlimit:10;}
|
||||
.st14{fill:#C0E6EF;}
|
||||
.st15{fill:none;stroke:#C0E6EF;stroke-width:1.2961;stroke-miterlimit:10;}
|
||||
.st16{fill:none;stroke:#C0E6EF;stroke-width:1.6201;stroke-miterlimit:10;}
|
||||
.st17{fill:none;stroke:#FFFFFF;stroke-width:1.9441;stroke-miterlimit:10;}
|
||||
.st18{fill:none;stroke:#FFFFFF;stroke-width:1.2961;stroke-miterlimit:10;}
|
||||
.st19{fill:none;stroke:#FFFFFF;stroke-width:1.6201;stroke-miterlimit:10;}
|
||||
</style>
|
||||
<g id="Layer_2">
|
||||
</g>
|
||||
<g id="Layer_1">
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<path class="st0" d="M96.7,26l0.1,1.8c1.1-1.4,2.6-2.1,4.4-2.1c1.9,0,3.2,0.9,4,2.6c1.1-1.7,2.6-2.6,4.7-2.6
|
||||
c3.3,0,5,2.3,5.1,6.9V45h-5V32.9c0-1.1-0.2-1.9-0.5-2.4s-0.8-0.7-1.5-0.7c-0.9,0-1.6,0.6-2.1,1.7l0,0.6V45h-5V32.9
|
||||
c0-1.1-0.1-1.9-0.4-2.4s-0.8-0.7-1.6-0.7c-0.9,0-1.5,0.5-2,1.4V45h-5V26H96.7z"/>
|
||||
<path class="st0" d="M118.5,34.9c0-3.1,0.6-5.4,1.7-7s2.7-2.3,4.7-2.3c1.7,0,3.1,0.7,4,2l0.2-1.7h4.5v19c0,2.4-0.7,4.3-2,5.6
|
||||
c-1.4,1.3-3.3,1.9-5.9,1.9c-1,0-2.1-0.2-3.3-0.6s-2-0.9-2.6-1.6l1.7-3.4c0.5,0.5,1.1,0.9,1.8,1.2c0.7,0.3,1.5,0.5,2.1,0.5
|
||||
c1.1,0,1.9-0.3,2.4-0.8s0.7-1.4,0.7-2.6v-1.6c-0.9,1.2-2.2,1.9-3.7,1.9c-2,0-3.6-0.8-4.7-2.4s-1.7-3.8-1.7-6.7V34.9z
|
||||
M123.5,36.2c0,1.8,0.2,3,0.7,3.8s1.2,1.2,2.2,1.2c1,0,1.8-0.4,2.3-1.1V31c-0.5-0.8-1.3-1.2-2.2-1.2c-1,0-1.7,0.4-2.2,1.2
|
||||
s-0.7,2.1-0.7,3.9V36.2z"/>
|
||||
<path class="st0" d="M142.2,26l0.1,1.8c1.1-1.4,2.6-2.1,4.4-2.1c1.9,0,3.2,0.9,4,2.6c1.1-1.7,2.6-2.6,4.7-2.6
|
||||
c3.3,0,5,2.3,5.1,6.9V45h-5V32.9c0-1.1-0.2-1.9-0.5-2.4c-0.3-0.5-0.8-0.7-1.5-0.7c-0.9,0-1.6,0.6-2.1,1.7l0,0.6V45h-5V32.9
|
||||
c0-1.1-0.1-1.9-0.4-2.4c-0.3-0.5-0.8-0.7-1.6-0.7c-0.9,0-1.5,0.5-2,1.4V45h-5V26H142.2z"/>
|
||||
<path class="st0" d="M170.3,21.3V26h2.5v3.7h-2.5v9.5c0,0.8,0.1,1.3,0.3,1.5c0.2,0.3,0.6,0.4,1.2,0.4c0.5,0,0.9,0,1.2-0.1l0,3.9
|
||||
c-0.8,0.3-1.8,0.5-2.7,0.5c-3.2,0-4.8-1.8-4.9-5.5V29.7h-2.2V26h2.2v-4.7H170.3z"/>
|
||||
<path class="st0" d="M182.7,43.5c1.4,0,2.4-0.4,3.1-1.1c0.7-0.8,1.1-1.9,1.2-3.3h1.9c-0.1,1.9-0.7,3.5-1.9,4.6s-2.6,1.7-4.3,1.7
|
||||
c-2.3,0-4-0.7-5.1-2.2s-1.7-3.6-1.8-6.4v-2.3c0-2.9,0.6-5.1,1.7-6.6s2.9-2.2,5.1-2.2c1.9,0,3.4,0.6,4.5,1.8s1.7,2.8,1.7,5H187
|
||||
c-0.1-1.6-0.5-2.8-1.2-3.6s-1.8-1.3-3.1-1.3c-1.7,0-2.9,0.6-3.7,1.7c-0.8,1.1-1.2,2.9-1.2,5.2v2.2c0,2.4,0.4,4.2,1.2,5.3
|
||||
C179.8,43,181,43.5,182.7,43.5z"/>
|
||||
<path class="st0" d="M192.6,34.5c0-2.7,0.6-4.9,1.9-6.5s2.9-2.4,5.1-2.4c2.2,0,3.9,0.8,5.1,2.4c1.2,1.6,1.9,3.7,1.9,6.5v2
|
||||
c0,2.8-0.6,5-1.9,6.5s-2.9,2.3-5.1,2.3s-3.8-0.8-5-2.3s-1.9-3.6-1.9-6.3V34.5z M194.6,36.5c0,2.2,0.4,3.9,1.3,5.2
|
||||
c0.9,1.3,2.1,1.9,3.7,1.9c1.6,0,2.8-0.6,3.7-1.8c0.8-1.2,1.3-2.9,1.3-5.2v-2c0-2.2-0.4-3.9-1.3-5.2c-0.9-1.3-2.1-1.9-3.7-1.9
|
||||
c-1.5,0-2.7,0.6-3.6,1.8c-0.9,1.2-1.3,2.9-1.4,5.1V36.5z"/>
|
||||
<path class="st0" d="M213.2,26l0.1,3c0.6-1,1.3-1.9,2.2-2.5s1.9-0.9,3-0.9c3.3,0,5,2.2,5.1,6.6V45h-1.9V32.5
|
||||
c0-1.7-0.3-3-0.9-3.8c-0.6-0.8-1.5-1.2-2.8-1.2c-1,0-1.9,0.4-2.8,1.2s-1.4,1.8-1.9,3.2V45h-2V26H213.2z"/>
|
||||
<path class="st0" d="M230.3,45V27.7h-2.6V26h2.6v-2.5c0-1.9,0.5-3.3,1.3-4.3s2-1.5,3.5-1.5c0.7,0,1.3,0.1,1.9,0.3l-0.1,1.8
|
||||
c-0.5-0.1-1-0.2-1.6-0.2c-0.9,0-1.7,0.4-2.2,1.1s-0.8,1.7-0.8,3V26h3.7v1.8h-3.7V45H230.3z"/>
|
||||
<path class="st0" d="M240.1,20.5c0-0.4,0.1-0.7,0.3-1s0.5-0.4,0.9-0.4s0.7,0.1,1,0.4s0.3,0.6,0.3,1s-0.1,0.7-0.3,1
|
||||
s-0.5,0.4-1,0.4s-0.7-0.1-0.9-0.4S240.1,20.9,240.1,20.5z M242.3,45h-2V26h2V45z"/>
|
||||
<path class="st0" d="M247.4,34.6c0-3,0.5-5.2,1.5-6.7s2.6-2.2,4.6-2.2c2.2,0,3.8,1,4.9,3l0.1-2.7h1.8v19.6c0,2.3-0.6,4-1.6,5.2
|
||||
s-2.7,1.8-4.7,1.8c-1,0-2.1-0.3-3.2-0.8s-1.9-1.1-2.4-1.9l0.9-1.4c1.3,1.5,2.8,2.2,4.5,2.2c1.6,0,2.7-0.4,3.5-1.3
|
||||
c0.7-0.8,1.1-2.1,1.1-3.8v-3c-1.1,1.8-2.7,2.7-4.9,2.7c-2,0-3.5-0.7-4.5-2.2s-1.6-3.6-1.6-6.5V34.6z M249.3,36.4
|
||||
c0,2.4,0.4,4.2,1.1,5.4s1.9,1.7,3.5,1.7c2.1,0,3.6-1,4.5-3v-9.7c-0.9-2.2-2.4-3.3-4.4-3.3c-1.6,0-2.8,0.6-3.5,1.7
|
||||
s-1.1,2.9-1.1,5.3V36.4z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st18" x1="19.5" y1="16" x2="34.7" y2="28.5"/>
|
||||
<g>
|
||||
<polygon class="st0" points="18.4,18.5 16.6,13.7 21.7,14.5 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<circle class="st0" cx="10.8" cy="8.1" r="8.1"/>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st18" x1="34.7" y1="28" x2="49.4" y2="53.3"/>
|
||||
<g>
|
||||
<polygon class="st0" points="46.8,54 51.2,56.5 51.2,51.4 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st18" x1="46.2" y1="31.1" x2="63.9" y2="34.9"/>
|
||||
<g>
|
||||
<polygon class="st0" points="46.4,33.8 42.6,30.4 47.5,28.8 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st18" x1="34.7" y1="28.5" x2="47.9" y2="20.1"/>
|
||||
<g>
|
||||
<polygon class="st0" points="48.7,22.7 51.1,18.1 45.9,18.3 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st18" x1="11.2" y1="33.1" x2="23.2" y2="31"/>
|
||||
<g>
|
||||
<polygon class="st0" points="22.9,33.7 26.8,30.4 22,28.6 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st18" x1="61.5" y1="53" x2="71.9" y2="36.1"/>
|
||||
<g>
|
||||
<polygon class="st0" points="64.1,53.7 59.5,56.2 59.7,51 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st18" x1="64.7" y1="24.4" x2="71.9" y2="36.6"/>
|
||||
<g>
|
||||
<polygon class="st0" points="62.9,26.4 62.9,21.2 67.3,23.7 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<circle class="st0" cx="34.7" cy="28.5" r="8.1"/>
|
||||
<circle class="st0" cx="8.1" cy="33.9" r="8.1"/>
|
||||
<circle class="st0" cx="58.3" cy="14.5" r="8.1"/>
|
||||
<circle class="st0" cx="55.7" cy="63.3" r="8.1"/>
|
||||
<circle class="st0" cx="71.9" cy="36.1" r="8.1"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.2 KiB |
38
bindata/Makefile
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/>.
|
||||
|
||||
# The bindata target generates go files from any source defined below. To use
|
||||
# the files, import the "bindata" package and use:
|
||||
# `bytes, err := bindata.Asset("FILEPATH")`
|
||||
# where FILEPATH is the path of the original input file relative to `bindata/`.
|
||||
|
||||
.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 *.go
|
||||
229
config.go
@@ -1,229 +0,0 @@
|
||||
// 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/>.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"gopkg.in/yaml.v2"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type collectorTypeConfig struct {
|
||||
Type string `yaml:"type"`
|
||||
Pattern string `yaml:"pattern"` // XXX: Not Implemented
|
||||
}
|
||||
|
||||
type vertexConfig struct {
|
||||
Type string `yaml:"type"`
|
||||
Name string `yaml:"name"`
|
||||
}
|
||||
|
||||
type edgeConfig struct {
|
||||
Name string `yaml:"name"`
|
||||
From vertexConfig `yaml:"from"`
|
||||
To vertexConfig `yaml:"to"`
|
||||
}
|
||||
|
||||
type GraphConfig struct {
|
||||
Graph string `yaml:"graph"`
|
||||
Types struct {
|
||||
Noop []NoopType `yaml:"noop"`
|
||||
File []FileType `yaml:"file"`
|
||||
Service []ServiceType `yaml:"service"`
|
||||
Exec []ExecType `yaml:"exec"`
|
||||
} `yaml:"types"`
|
||||
Collector []collectorTypeConfig `yaml:"collect"`
|
||||
Edges []edgeConfig `yaml:"edges"`
|
||||
Comment string `yaml:"comment"`
|
||||
}
|
||||
|
||||
func (c *GraphConfig) Parse(data []byte) error {
|
||||
if err := yaml.Unmarshal(data, c); err != nil {
|
||||
return err
|
||||
}
|
||||
if c.Graph == "" {
|
||||
return errors.New("Graph config: invalid `graph`")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ParseConfigFromFile(filename string) *GraphConfig {
|
||||
data, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
log.Printf("Error: Config: ParseConfigFromFile: File: %v", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
var config GraphConfig
|
||||
if err := config.Parse(data); err != nil {
|
||||
log.Printf("Error: Config: ParseConfigFromFile: Parse: %v", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
return &config
|
||||
}
|
||||
|
||||
// XXX: we need to fix this function so that it either fails without modifying
|
||||
// the graph, passes successfully and modifies it, or basically panics i guess
|
||||
// this way an invalid compilation can leave the old graph running, and we we
|
||||
// don't modify a partial graph. so we really need to validate, and then perform
|
||||
// whatever actions are necessary
|
||||
// finding some way to do this on a copy of the graph, and then do a graph diff
|
||||
// and merge the new data into the old graph would be more appropriate, in
|
||||
// particular if we can ensure the graph merge can't fail. As for the putting
|
||||
// of stuff into etcd, we should probably store the operations to complete in
|
||||
// the new graph, and keep retrying until it succeeds, thus blocking any new
|
||||
// etcd operations until that time.
|
||||
func UpdateGraphFromConfig(config *GraphConfig, hostname string, g *Graph, etcdO *EtcdWObject) bool {
|
||||
|
||||
var NoopMap = make(map[string]*Vertex)
|
||||
var FileMap = make(map[string]*Vertex)
|
||||
var ServiceMap = make(map[string]*Vertex)
|
||||
var ExecMap = make(map[string]*Vertex)
|
||||
|
||||
var lookup = make(map[string]map[string]*Vertex)
|
||||
lookup["noop"] = NoopMap
|
||||
lookup["file"] = FileMap
|
||||
lookup["service"] = ServiceMap
|
||||
lookup["exec"] = ExecMap
|
||||
|
||||
//log.Printf("%+v", config) // debug
|
||||
|
||||
g.SetName(config.Graph) // set graph name
|
||||
|
||||
var keep []*Vertex // list of vertex which are the same in new graph
|
||||
|
||||
for _, t := range config.Types.Noop {
|
||||
obj := NewNoopType(t.Name)
|
||||
v := g.GetVertexMatch(obj)
|
||||
if v == nil { // no match found
|
||||
v = NewVertex(obj)
|
||||
g.AddVertex(v) // call standalone in case not part of an edge
|
||||
}
|
||||
NoopMap[obj.Name] = v // used for constructing edges
|
||||
keep = append(keep, v) // append
|
||||
}
|
||||
|
||||
for _, t := range config.Types.File {
|
||||
// XXX: should we export based on a @@ prefix, or a metaparam
|
||||
// like exported => true || exported => (host pattern)||(other pattern?)
|
||||
if strings.HasPrefix(t.Name, "@@") { // exported resource
|
||||
// add to etcd storage...
|
||||
t.Name = t.Name[2:] //slice off @@
|
||||
if !etcdO.EtcdPut(hostname, t.Name, "file", t) {
|
||||
log.Printf("Problem exporting file resource %v.", t.Name)
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
obj := NewFileType(t.Name, t.Path, t.Dirname, t.Basename, t.Content, t.State)
|
||||
v := g.GetVertexMatch(obj)
|
||||
if v == nil { // no match found
|
||||
v = NewVertex(obj)
|
||||
g.AddVertex(v) // call standalone in case not part of an edge
|
||||
}
|
||||
FileMap[obj.Name] = v // used for constructing edges
|
||||
keep = append(keep, v) // append
|
||||
}
|
||||
}
|
||||
|
||||
for _, t := range config.Types.Service {
|
||||
obj := NewServiceType(t.Name, t.State, t.Startup)
|
||||
v := g.GetVertexMatch(obj)
|
||||
if v == nil { // no match found
|
||||
v = NewVertex(obj)
|
||||
g.AddVertex(v) // call standalone in case not part of an edge
|
||||
}
|
||||
ServiceMap[obj.Name] = v // used for constructing edges
|
||||
keep = append(keep, v) // append
|
||||
}
|
||||
|
||||
for _, t := range config.Types.Exec {
|
||||
obj := NewExecType(t.Name, t.Cmd, t.Shell, t.Timeout, t.WatchCmd, t.WatchShell, t.IfCmd, t.IfShell, t.PollInt, t.State)
|
||||
v := g.GetVertexMatch(obj)
|
||||
if v == nil { // no match found
|
||||
v = NewVertex(obj)
|
||||
g.AddVertex(v) // call standalone in case not part of an edge
|
||||
}
|
||||
ExecMap[obj.Name] = v // used for constructing edges
|
||||
keep = append(keep, v) // append
|
||||
}
|
||||
|
||||
// lookup from etcd graph
|
||||
// do all the graph look ups in one single step, so that if the etcd
|
||||
// database changes, we don't have a partial state of affairs...
|
||||
nodes, ok := etcdO.EtcdGet()
|
||||
if ok {
|
||||
for _, t := range config.Collector {
|
||||
// XXX: use t.Type and optionally t.Pattern to collect from etcd storage
|
||||
log.Printf("Collect: %v; Pattern: %v", t.Type, t.Pattern)
|
||||
|
||||
for _, x := range etcdO.EtcdGetProcess(nodes, "file") {
|
||||
var obj *FileType
|
||||
if B64ToObj(x, &obj) != true {
|
||||
log.Printf("Collect: File: %v not collected!", x)
|
||||
continue
|
||||
}
|
||||
if t.Pattern != "" { // XXX: currently the pattern for files can only override the Dirname variable :P
|
||||
obj.Dirname = t.Pattern
|
||||
}
|
||||
|
||||
log.Printf("Collect: File: %v collected!", obj.GetName())
|
||||
|
||||
// XXX: similar to file add code:
|
||||
v := g.GetVertexMatch(obj)
|
||||
if v == nil { // no match found
|
||||
obj.Init() // initialize go channels or things won't work!!!
|
||||
v = NewVertex(obj)
|
||||
g.AddVertex(v) // call standalone in case not part of an edge
|
||||
}
|
||||
FileMap[obj.GetName()] = v // used for constructing edges
|
||||
keep = append(keep, v) // append
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// get rid of any vertices we shouldn't "keep" (that aren't in new graph)
|
||||
for _, v := range g.GetVertices() {
|
||||
if !HasVertex(v, keep) {
|
||||
// wait for exit before starting new graph!
|
||||
v.Type.SendEvent(eventExit, true, false)
|
||||
g.DeleteVertex(v)
|
||||
}
|
||||
}
|
||||
|
||||
for _, e := range config.Edges {
|
||||
if _, ok := lookup[e.From.Type]; !ok {
|
||||
return false
|
||||
}
|
||||
if _, ok := lookup[e.To.Type]; !ok {
|
||||
return false
|
||||
}
|
||||
if _, ok := lookup[e.From.Type][e.From.Name]; !ok {
|
||||
return false
|
||||
}
|
||||
if _, ok := lookup[e.To.Type][e.To.Name]; !ok {
|
||||
return false
|
||||
}
|
||||
g.AddEdge(lookup[e.From.Type][e.From.Name], lookup[e.To.Type][e.To.Name], NewEdge(e.Name))
|
||||
}
|
||||
return true
|
||||
}
|
||||
155
configwatch.go
@@ -1,155 +0,0 @@
|
||||
// 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/>.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"gopkg.in/fsnotify.v1"
|
||||
//"github.com/go-fsnotify/fsnotify" // git master of "gopkg.in/fsnotify.v1"
|
||||
"log"
|
||||
"math"
|
||||
"path"
|
||||
"strings"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// XXX: it would be great if we could reuse code between this and the file type
|
||||
// XXX: patch this to submit it as part of go-fsnotify if they're interested...
|
||||
func ConfigWatch(file string) chan bool {
|
||||
ch := make(chan bool)
|
||||
go func() {
|
||||
var safename = path.Clean(file) // no trailing slash
|
||||
|
||||
watcher, err := fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer watcher.Close()
|
||||
|
||||
patharray := PathSplit(safename) // tokenize the path
|
||||
var index = len(patharray) // starting index
|
||||
var current string // current "watcher" location
|
||||
var deltaDepth int // depth delta between watcher and event
|
||||
var send = false // send event?
|
||||
|
||||
for {
|
||||
current = strings.Join(patharray[0:index], "/")
|
||||
if current == "" { // the empty string top is the root dir ("/")
|
||||
current = "/"
|
||||
}
|
||||
log.Printf("Watching: %v", current) // attempting to watch...
|
||||
|
||||
// initialize in the loop so that we can reset on rm-ed handles
|
||||
err = watcher.Add(current)
|
||||
if err != nil {
|
||||
if err == syscall.ENOENT {
|
||||
index-- // usually not found, move up one dir
|
||||
} else if err == syscall.ENOSPC {
|
||||
// XXX: occasionally: no space left on device,
|
||||
// XXX: probably due to lack of inotify watches
|
||||
log.Printf("Lack of watches for config(%v) error: %+v", file, err.Error) // 0x408da0
|
||||
log.Fatal(err)
|
||||
} else {
|
||||
log.Printf("Unknown config(%v) error:", file)
|
||||
log.Fatal(err)
|
||||
}
|
||||
index = int(math.Max(1, float64(index)))
|
||||
continue
|
||||
}
|
||||
|
||||
select {
|
||||
case event := <-watcher.Events:
|
||||
// the deeper you go, the bigger the deltaDepth is...
|
||||
// this is the difference between what we're watching,
|
||||
// and the event... doesn't mean we can't watch deeper
|
||||
if current == event.Name {
|
||||
deltaDepth = 0 // i was watching what i was looking for
|
||||
|
||||
} else if HasPathPrefix(event.Name, current) {
|
||||
deltaDepth = len(PathSplit(current)) - len(PathSplit(event.Name)) // -1 or less
|
||||
|
||||
} else if HasPathPrefix(current, event.Name) {
|
||||
deltaDepth = len(PathSplit(event.Name)) - len(PathSplit(current)) // +1 or more
|
||||
|
||||
} else {
|
||||
// TODO different watchers get each others events!
|
||||
// https://github.com/go-fsnotify/fsnotify/issues/95
|
||||
// this happened with two values such as:
|
||||
// event.Name: /tmp/mgmt/f3 and current: /tmp/mgmt/f2
|
||||
continue
|
||||
}
|
||||
//log.Printf("The delta depth is: %v", deltaDepth)
|
||||
|
||||
// if we have what we wanted, awesome, send an event...
|
||||
if event.Name == safename {
|
||||
//log.Println("Event!")
|
||||
send = true
|
||||
|
||||
// file removed, move the watch upwards
|
||||
if deltaDepth >= 0 && (event.Op&fsnotify.Remove == fsnotify.Remove) {
|
||||
//log.Println("Removal!")
|
||||
watcher.Remove(current)
|
||||
index--
|
||||
}
|
||||
|
||||
// we must be a parent watcher, so descend in
|
||||
if deltaDepth < 0 {
|
||||
watcher.Remove(current)
|
||||
index++
|
||||
}
|
||||
|
||||
// if safename starts with event.Name, we're above, and no event should be sent
|
||||
} else if HasPathPrefix(safename, event.Name) {
|
||||
//log.Println("Above!")
|
||||
|
||||
if deltaDepth >= 0 && (event.Op&fsnotify.Remove == fsnotify.Remove) {
|
||||
log.Println("Removal!")
|
||||
watcher.Remove(current)
|
||||
index--
|
||||
}
|
||||
|
||||
if deltaDepth < 0 {
|
||||
log.Println("Parent!")
|
||||
if PathPrefixDelta(safename, event.Name) == 1 { // we're the parent dir
|
||||
//send = true
|
||||
}
|
||||
watcher.Remove(current)
|
||||
index++
|
||||
}
|
||||
|
||||
// if event.Name startswith safename, send event, we're already deeper
|
||||
} else if HasPathPrefix(event.Name, safename) {
|
||||
//log.Println("Event2!")
|
||||
//send = true
|
||||
}
|
||||
|
||||
case err := <-watcher.Errors:
|
||||
log.Println("error:", err)
|
||||
log.Fatal(err)
|
||||
|
||||
}
|
||||
|
||||
// do our event sending all together to avoid duplicate msgs
|
||||
if send {
|
||||
send = false
|
||||
ch <- true
|
||||
}
|
||||
}
|
||||
//close(ch)
|
||||
}()
|
||||
return ch
|
||||
}
|
||||
386
converger/converger.go
Normal file
@@ -0,0 +1,386 @@
|
||||
// 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 converger is a facility for reporting the converged state.
|
||||
package converger
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/purpleidea/mgmt/util"
|
||||
)
|
||||
|
||||
// TODO: we could make a new function that masks out the state of certain
|
||||
// UID's, but at the moment the new Timer code has obsoleted the need...
|
||||
|
||||
// Converger is the general interface for implementing a convergence watcher.
|
||||
type Converger interface { // TODO: need a better name
|
||||
Register() UID
|
||||
IsConverged(UID) bool // is the UID converged ?
|
||||
SetConverged(UID, bool) error // set the converged state of the UID
|
||||
Unregister(UID)
|
||||
Start()
|
||||
Pause()
|
||||
Loop(bool)
|
||||
ConvergedTimer(UID) <-chan time.Time
|
||||
Status() map[uint64]bool
|
||||
Timeout() int // returns the timeout that this was created with
|
||||
SetStateFn(func(bool) error) // sets the stateFn
|
||||
}
|
||||
|
||||
// UID is the interface resources can use to notify with if converged. You'll
|
||||
// need to use part of the Converger interface to Register initially too.
|
||||
type UID interface {
|
||||
ID() uint64 // get Id
|
||||
Name() string // get a friendly name
|
||||
SetName(string)
|
||||
IsValid() bool // has Id been initialized ?
|
||||
InvalidateID() // set Id to nil
|
||||
IsConverged() bool
|
||||
SetConverged(bool) error
|
||||
Unregister()
|
||||
ConvergedTimer() <-chan time.Time
|
||||
StartTimer() (func() error, error) // cancellable is the same as StopTimer()
|
||||
ResetTimer() error // resets counter to zero
|
||||
StopTimer() error
|
||||
}
|
||||
|
||||
// converger is an implementation of the Converger interface.
|
||||
type converger struct {
|
||||
timeout int // must be zero (instant) or greater seconds to run
|
||||
stateFn func(bool) error // run on converged state changes with state bool
|
||||
converged bool // did we converge (state changes of this run Fn)
|
||||
channel chan struct{} // signal here to run an isConverged check
|
||||
control chan bool // control channel for start/pause
|
||||
mutex sync.RWMutex // used for controlling access to status and lastid
|
||||
lastid uint64
|
||||
status map[uint64]bool
|
||||
}
|
||||
|
||||
// cuid is an implementation of the UID interface.
|
||||
type cuid struct {
|
||||
converger Converger
|
||||
id uint64
|
||||
name string // user defined, friendly name
|
||||
mutex sync.Mutex
|
||||
timer chan struct{}
|
||||
running bool // is the above timer running?
|
||||
wg sync.WaitGroup
|
||||
}
|
||||
|
||||
// NewConverger builds a new converger struct.
|
||||
func NewConverger(timeout int, stateFn func(bool) error) Converger {
|
||||
return &converger{
|
||||
timeout: timeout,
|
||||
stateFn: stateFn,
|
||||
channel: make(chan struct{}),
|
||||
control: make(chan bool),
|
||||
lastid: 0,
|
||||
status: make(map[uint64]bool),
|
||||
}
|
||||
}
|
||||
|
||||
// Register assigns a UID to the caller.
|
||||
func (obj *converger) Register() UID {
|
||||
obj.mutex.Lock()
|
||||
defer obj.mutex.Unlock()
|
||||
obj.lastid++
|
||||
obj.status[obj.lastid] = false // initialize as not converged
|
||||
return &cuid{
|
||||
converger: obj,
|
||||
id: obj.lastid,
|
||||
name: fmt.Sprintf("%d", obj.lastid), // some default
|
||||
timer: nil,
|
||||
running: false,
|
||||
}
|
||||
}
|
||||
|
||||
// IsConverged gets the converged status of a uid.
|
||||
func (obj *converger) IsConverged(uid UID) bool {
|
||||
if !uid.IsValid() {
|
||||
panic(fmt.Sprintf("the ID of UID(%s) is nil", uid.Name()))
|
||||
}
|
||||
obj.mutex.RLock()
|
||||
isConverged, found := obj.status[uid.ID()] // lookup
|
||||
obj.mutex.RUnlock()
|
||||
if !found {
|
||||
panic("the ID of UID is unregistered")
|
||||
}
|
||||
return isConverged
|
||||
}
|
||||
|
||||
// SetConverged updates the converger with the converged state of the UID.
|
||||
func (obj *converger) SetConverged(uid UID, isConverged bool) error {
|
||||
if !uid.IsValid() {
|
||||
return fmt.Errorf("the ID of UID(%s) is nil", uid.Name())
|
||||
}
|
||||
obj.mutex.Lock()
|
||||
if _, found := obj.status[uid.ID()]; !found {
|
||||
panic("the ID of UID is unregistered")
|
||||
}
|
||||
obj.status[uid.ID()] = isConverged // set
|
||||
obj.mutex.Unlock() // unlock *before* poke or deadlock!
|
||||
if isConverged != obj.converged { // only poke if it would be helpful
|
||||
// run in a go routine so that we never block... just queue up!
|
||||
// this allows us to send events, even if we haven't started...
|
||||
go func() { obj.channel <- struct{}{} }()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// isConverged returns true if *every* registered uid has converged.
|
||||
func (obj *converger) isConverged() bool {
|
||||
obj.mutex.RLock() // take a read lock
|
||||
defer obj.mutex.RUnlock()
|
||||
for _, v := range obj.status {
|
||||
if !v { // everyone must be converged for this to be true
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Unregister dissociates the ConvergedUID from the converged checking.
|
||||
func (obj *converger) Unregister(uid UID) {
|
||||
if !uid.IsValid() {
|
||||
panic(fmt.Sprintf("the ID of UID(%s) is nil", uid.Name()))
|
||||
}
|
||||
obj.mutex.Lock()
|
||||
uid.StopTimer() // ignore any errors
|
||||
delete(obj.status, uid.ID())
|
||||
obj.mutex.Unlock()
|
||||
uid.InvalidateID()
|
||||
}
|
||||
|
||||
// Start causes a Converger object to start or resume running.
|
||||
func (obj *converger) Start() {
|
||||
obj.control <- true
|
||||
}
|
||||
|
||||
// Pause causes a Converger object to stop running temporarily.
|
||||
func (obj *converger) Pause() { // FIXME: add a sync ACK on pause before return
|
||||
obj.control <- false
|
||||
}
|
||||
|
||||
// Loop is the main loop for a Converger object. It usually runs in a goroutine.
|
||||
// TODO: we could eventually have each resource tell us as soon as it converges,
|
||||
// and then keep track of the time delays here, to avoid callers needing select.
|
||||
// NOTE: when we have very short timeouts, if we start before all the resources
|
||||
// have joined the map, then it might appear as if we converged before we did!
|
||||
func (obj *converger) Loop(startPaused bool) {
|
||||
if obj.control == nil {
|
||||
panic("converger not initialized correctly")
|
||||
}
|
||||
if startPaused { // start paused without racing
|
||||
select {
|
||||
case e := <-obj.control:
|
||||
if !e {
|
||||
panic("converger expected true")
|
||||
}
|
||||
}
|
||||
}
|
||||
for {
|
||||
select {
|
||||
case e := <-obj.control: // expecting "false" which means pause!
|
||||
if e {
|
||||
panic("converger expected false")
|
||||
}
|
||||
// now i'm paused...
|
||||
select {
|
||||
case e := <-obj.control:
|
||||
if !e {
|
||||
panic("converger expected true")
|
||||
}
|
||||
// restart
|
||||
// kick once to refresh the check...
|
||||
go func() { obj.channel <- struct{}{} }()
|
||||
continue
|
||||
}
|
||||
|
||||
case <-obj.channel:
|
||||
if !obj.isConverged() {
|
||||
if obj.converged { // we're doing a state change
|
||||
if obj.stateFn != nil {
|
||||
// call an arbitrary function
|
||||
if err := obj.stateFn(false); err != nil {
|
||||
// FIXME: what to do on error ?
|
||||
}
|
||||
}
|
||||
}
|
||||
obj.converged = false
|
||||
continue
|
||||
}
|
||||
|
||||
// we have converged!
|
||||
if obj.timeout >= 0 { // only run if timeout is valid
|
||||
if !obj.converged { // we're doing a state change
|
||||
if obj.stateFn != nil {
|
||||
// call an arbitrary function
|
||||
if err := obj.stateFn(true); err != nil {
|
||||
// FIXME: what to do on error ?
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
obj.converged = true
|
||||
// loop and wait again...
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ConvergedTimer adds a timeout to a select call and blocks until then.
|
||||
// TODO: this means we could eventually have per resource converged timeouts
|
||||
func (obj *converger) ConvergedTimer(uid UID) <-chan time.Time {
|
||||
// be clever: if i'm already converged, this timeout should block which
|
||||
// avoids unnecessary new signals being sent! this avoids fast loops if
|
||||
// we have a low timeout, or in particular a timeout == 0
|
||||
if uid.IsConverged() {
|
||||
// blocks the case statement in select forever!
|
||||
return util.TimeAfterOrBlock(-1)
|
||||
}
|
||||
return util.TimeAfterOrBlock(obj.timeout)
|
||||
}
|
||||
|
||||
// Status returns a map of the converged status of each UID.
|
||||
func (obj *converger) Status() map[uint64]bool {
|
||||
status := make(map[uint64]bool)
|
||||
obj.mutex.RLock() // take a read lock
|
||||
defer obj.mutex.RUnlock()
|
||||
for k, v := range obj.status { // make a copy to avoid the mutex
|
||||
status[k] = v
|
||||
}
|
||||
return status
|
||||
}
|
||||
|
||||
// Timeout returns the timeout in seconds that converger was created with. This
|
||||
// is useful to avoid passing in the timeout value separately when you're
|
||||
// already passing in the Converger struct.
|
||||
func (obj *converger) Timeout() int {
|
||||
return obj.timeout
|
||||
}
|
||||
|
||||
// SetStateFn sets the state function to be run on change of converged state.
|
||||
func (obj *converger) SetStateFn(stateFn func(bool) error) {
|
||||
obj.stateFn = stateFn
|
||||
}
|
||||
|
||||
// ID returns the unique id of this UID object.
|
||||
func (obj *cuid) ID() uint64 {
|
||||
return obj.id
|
||||
}
|
||||
|
||||
// Name returns a user defined name for the specific cuid.
|
||||
func (obj *cuid) Name() string {
|
||||
return obj.name
|
||||
}
|
||||
|
||||
// SetName sets a user defined name for the specific cuid.
|
||||
func (obj *cuid) SetName(name string) {
|
||||
obj.name = name
|
||||
}
|
||||
|
||||
// IsValid tells us if the id is valid or has already been destroyed.
|
||||
func (obj *cuid) IsValid() bool {
|
||||
return obj.id != 0 // an id of 0 is invalid
|
||||
}
|
||||
|
||||
// InvalidateID marks the id as no longer valid.
|
||||
func (obj *cuid) InvalidateID() {
|
||||
obj.id = 0 // an id of 0 is invalid
|
||||
}
|
||||
|
||||
// IsConverged is a helper function to the regular IsConverged method.
|
||||
func (obj *cuid) IsConverged() bool {
|
||||
return obj.converger.IsConverged(obj)
|
||||
}
|
||||
|
||||
// SetConverged is a helper function to the regular SetConverged notification.
|
||||
func (obj *cuid) SetConverged(isConverged bool) error {
|
||||
return obj.converger.SetConverged(obj, isConverged)
|
||||
}
|
||||
|
||||
// Unregister is a helper function to unregister myself.
|
||||
func (obj *cuid) Unregister() {
|
||||
obj.converger.Unregister(obj)
|
||||
}
|
||||
|
||||
// ConvergedTimer is a helper around the regular ConvergedTimer method.
|
||||
func (obj *cuid) ConvergedTimer() <-chan time.Time {
|
||||
return obj.converger.ConvergedTimer(obj)
|
||||
}
|
||||
|
||||
// StartTimer runs an invisible timer that automatically converges on timeout.
|
||||
func (obj *cuid) StartTimer() (func() error, error) {
|
||||
obj.mutex.Lock()
|
||||
if !obj.running {
|
||||
obj.timer = make(chan struct{})
|
||||
obj.running = true
|
||||
} else {
|
||||
obj.mutex.Unlock()
|
||||
return obj.StopTimer, fmt.Errorf("timer already started")
|
||||
}
|
||||
obj.mutex.Unlock()
|
||||
obj.wg.Add(1)
|
||||
go func() {
|
||||
defer obj.wg.Done()
|
||||
for {
|
||||
select {
|
||||
case _, ok := <-obj.timer: // reset signal channel
|
||||
if !ok { // channel is closed
|
||||
return // false to exit
|
||||
}
|
||||
obj.SetConverged(false)
|
||||
|
||||
case <-obj.ConvergedTimer():
|
||||
obj.SetConverged(true) // converged!
|
||||
select {
|
||||
case _, ok := <-obj.timer: // reset signal channel
|
||||
if !ok { // channel is closed
|
||||
return // false to exit
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
return obj.StopTimer, nil
|
||||
}
|
||||
|
||||
// ResetTimer resets the counter to zero if using a StartTimer internally.
|
||||
func (obj *cuid) ResetTimer() error {
|
||||
obj.mutex.Lock()
|
||||
defer obj.mutex.Unlock()
|
||||
if obj.running {
|
||||
obj.timer <- struct{}{} // send the reset message
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("timer hasn't been started")
|
||||
}
|
||||
|
||||
// StopTimer stops the running timer permanently until a StartTimer is run.
|
||||
func (obj *cuid) StopTimer() error {
|
||||
obj.mutex.Lock()
|
||||
defer obj.mutex.Unlock()
|
||||
if !obj.running {
|
||||
return fmt.Errorf("timer isn't running")
|
||||
}
|
||||
close(obj.timer)
|
||||
obj.wg.Wait()
|
||||
obj.running = false
|
||||
return nil
|
||||
}
|
||||
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
@@ -0,0 +1 @@
|
||||
9
|
||||
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
@@ -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
@@ -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
@@ -0,0 +1,2 @@
|
||||
mgmt usr/bin
|
||||
misc/mgmt.service /lib/systemd/system
|
||||
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
|
||||
@@ -1,26 +1,19 @@
|
||||
// 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
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
// Package main provides the main entrypoint for using the `mgmt` software.
|
||||
package main
|
||||
|
||||
import (
|
||||
//"testing"
|
||||
)
|
||||
|
||||
//func TestT1(t *testing.T) {
|
||||
|
||||
//}
|
||||
22
docker/Dockerfile
Normal file
@@ -0,0 +1,22 @@
|
||||
FROM golang:1.9
|
||||
|
||||
MAINTAINER Michał Czeraszkiewicz <contact@czerasz.com>
|
||||
|
||||
# Set the reset cache variable
|
||||
# Read more here: http://czerasz.com/2014/11/13/docker-tip-and-tricks/#use-refreshedat-variable-for-better-cache-control
|
||||
ENV REFRESHED_AT 2017-11-16
|
||||
|
||||
# Update the package list to be able to use required packages
|
||||
RUN apt-get update
|
||||
|
||||
# Change the working directory
|
||||
WORKDIR /go/src/mgmt
|
||||
|
||||
# Copy all the files to the working directory
|
||||
COPY . /go/src/mgmt
|
||||
|
||||
# Install dependencies
|
||||
RUN make deps
|
||||
|
||||
# Build the binary
|
||||
RUN make build
|
||||
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"]
|
||||
34
docker/Dockerfile.development
Normal file
@@ -0,0 +1,34 @@
|
||||
FROM golang:1.9
|
||||
|
||||
MAINTAINER Michał Czeraszkiewicz <contact@czerasz.com>
|
||||
|
||||
# Set the reset cache variable
|
||||
# Read more here: http://czerasz.com/2014/11/13/docker-tip-and-tricks/#use-refreshedat-variable-for-better-cache-control
|
||||
ENV REFRESHED_AT 2017-11-16
|
||||
|
||||
RUN apt-get update
|
||||
|
||||
# Setup User to match Host User
|
||||
# Give the nre user superuser permissions
|
||||
ARG USER_ID=1000
|
||||
ARG GROUP_ID=1000
|
||||
ARG USER_NAME=mgmt
|
||||
ARG GROUP_NAME=$USER_NAME
|
||||
RUN groupadd --gid $GROUP_ID $GROUP_NAME && \
|
||||
useradd --create-home --home /home/$USER_NAME --uid ${USER_ID} --gid $GROUP_NAME --groups sudo $USER_NAME && \
|
||||
echo "$USER_NAME ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers
|
||||
|
||||
# Copy all the files to the working directory
|
||||
COPY . /home/$USER_NAME/mgmt
|
||||
|
||||
# Change working directory
|
||||
WORKDIR /home/$USER_NAME/mgmt
|
||||
|
||||
# Install dependencies
|
||||
RUN make deps
|
||||
|
||||
# Chown $GOPATH
|
||||
RUN chown -R ${USER_ID}:${GROUP_ID} /go
|
||||
|
||||
# Change user
|
||||
USER ${USER_NAME}
|
||||
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"]
|
||||
26
docker/scripts/build
Executable file
@@ -0,0 +1,26 @@
|
||||
#!/bin/bash
|
||||
|
||||
script_directory="$( cd "$( dirname "$0" )" && pwd )"
|
||||
project_directory=$script_directory/../..
|
||||
|
||||
# Specify the Docker image name
|
||||
image_name='purpleidea/mgmt'
|
||||
|
||||
# Build the image which contains the compiled binary
|
||||
docker build -t $image_name \
|
||||
--file=$project_directory/docker/Dockerfile $project_directory
|
||||
|
||||
# Remove the container if it already exists
|
||||
docker rm -f mgmt-export 2> /dev/null
|
||||
|
||||
# Start the container in background so we can "copy out" the binary
|
||||
docker run -d --name=mgmt-export $image_name bash -c 'while true; sleep 1000; done'
|
||||
|
||||
# Remove the current binary
|
||||
rm $project_directory/mgmt 2> /dev/null
|
||||
|
||||
# Get the binary from the container
|
||||
docker cp mgmt-export:/go/src/mgmt/mgmt $project_directory/mgmt
|
||||
|
||||
# Remove the container
|
||||
docker rm -f mgmt-export 2> /dev/null
|
||||
14
docker/scripts/build-development
Executable file
@@ -0,0 +1,14 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 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'
|
||||
|
||||
# Build the image
|
||||
docker build -t $image_name \
|
||||
--file=$project_directory/docker/Dockerfile.development $project_directory
|
||||
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 "$*"
|
||||
15
docker/scripts/run-development
Executable file
@@ -0,0 +1,15 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 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:/home/mgmt/mgmt \
|
||||
-it $image_name bash
|
||||
2
docs/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
mgmt-documentation.pdf
|
||||
_build
|
||||
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
@@ -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
@@ -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)
|
||||
418
docs/documentation.md
Normal file
@@ -0,0 +1,418 @@
|
||||
# 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 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/).
|
||||
|
||||
## 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.
|
||||
* [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.
|
||||
|
||||
### 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.
|
||||
|
||||
#### `--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
|
||||
```
|
||||
|
||||
#### Combining compile-time flags
|
||||
|
||||
You can combine multiple tags by using a space-separated list:
|
||||
|
||||
```
|
||||
GOTAGS="noaugeas novirt" 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/)
|
||||
267
docs/faq.md
Normal file
@@ -0,0 +1,267 @@
|
||||
## 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?
|
||||
|
||||
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 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 `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.
|
||||
|
||||
### 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!
|
||||
437
docs/function-guide.md
Normal file
@@ -0,0 +1,437 @@
|
||||
# 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
|
||||
package simplepoly
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/purpleidea/mgmt/lang/types"
|
||||
)
|
||||
|
||||
func init() {
|
||||
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
|
||||
func init() { // special golang method that runs once
|
||||
funcs.Register("foo", func() interfaces.Func { return &FooFunc{} })
|
||||
}
|
||||
```
|
||||
|
||||
### 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
@@ -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
|
||||
639
docs/language-guide.md
Normal file
@@ -0,0 +1,639 @@
|
||||
# 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
|
||||
- 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"]
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
### 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.
|
||||
|
||||
### 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!
|
||||
45
docs/on-the-web.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# 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](http://jonathangold.ca/awsec2-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) |
|
||||
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
|
||||
166
docs/puppet-guide.md
Normal file
@@ -0,0 +1,166 @@
|
||||
# Puppet guide
|
||||
|
||||
`mgmt` can use Puppet as its source for the configuration graph.
|
||||
This document goes into detail on how this works, and lists
|
||||
some pitfalls and limitations.
|
||||
|
||||
For basic instructions on how to use the Puppet support, see
|
||||
the [main documentation](documentation.md#puppet-support).
|
||||
|
||||
## Prerequisites
|
||||
|
||||
You need Puppet installed in your system. It is not important how you
|
||||
get it. On the most common Linux distributions, you can use packages
|
||||
from the OS maintainer, or upstream Puppet repositories. An alternative
|
||||
that will also work on OSX is the `puppet` Ruby gem. It also has the
|
||||
advantage that you can install any desired version in your home directory
|
||||
or any other location.
|
||||
|
||||
Any release of Puppet's 3.x and 4.x series should be suitable for use with
|
||||
`mgmt`. Most importantly, make sure to install the `ffrank-mgmtgraph` Puppet
|
||||
module (referred to below as "the translator module").
|
||||
|
||||
```
|
||||
puppet module install ffrank-mgmtgraph
|
||||
```
|
||||
|
||||
Please note that the module is not required on your Puppet master (if you
|
||||
use a master/agent setup). It's needed on the machine that runs `mgmt`.
|
||||
You can install the module on the master anyway, so that it gets distributed
|
||||
to your agents through Puppet's `pluginsync` mechanism.
|
||||
|
||||
### Testing the Puppet side
|
||||
|
||||
The following command should run successfully and print a YAML hash on your
|
||||
terminal:
|
||||
|
||||
```puppet
|
||||
puppet mgmtgraph print --code 'file { "/tmp/mgmt-test": ensure => present }'
|
||||
```
|
||||
|
||||
You can use this CLI to test any manifests before handing them straight
|
||||
to `mgmt`.
|
||||
|
||||
## Writing a suitable manifest
|
||||
|
||||
### Unsupported attributes
|
||||
|
||||
`mgmt` inherited its resource module from Puppet, so by and large, it's quite
|
||||
possible to express `mgmt` graphs in terms of Puppet manifests. However,
|
||||
there isn't (and likely never will be) full feature parity between the
|
||||
respective resource types. In consequence, a manifest can have semantics that
|
||||
cannot be transferred to `mgmt`.
|
||||
|
||||
For example, at the time of writing this, the `file` type in `mgmt` had no
|
||||
notion of permissions (the file `mode`) yet. This lead to the following
|
||||
warning (among others that will be discussed below):
|
||||
|
||||
```
|
||||
$ puppet mgmtgraph print --code 'file { "/tmp/foo": mode => "0600" }'
|
||||
Warning: cannot translate: File[/tmp/foo] { mode => "600" } (attribute is ignored)
|
||||
```
|
||||
|
||||
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
|
||||
`mgmt` will ignore this file's permissions. Including such attributes in
|
||||
manifests that are written expressly for `mgmt` is not sensible and should
|
||||
be avoided.
|
||||
|
||||
### Unsupported resources
|
||||
|
||||
Puppet has a fairly large number of
|
||||
[built-in types](https://docs.puppet.com/puppet/latest/reference/type.html),
|
||||
and countless more are available through
|
||||
[modules](https://forge.puppet.com/). It's unlikely that all of them will
|
||||
eventually receive native counterparts in `mgmt`.
|
||||
|
||||
When encountering an unknown resource, the translator module will replace
|
||||
it with an `exec` resource in its output. This resource will run the equivalent
|
||||
of a `puppet resource` command to make Puppet apply the original resource
|
||||
itself. This has quite abysmal performance, because processing such a
|
||||
resource requires the forking of at least one Puppet process (two if it
|
||||
is found to be out of sync). This comes with considerable overhead.
|
||||
On most systems, starting up any Puppet command takes several seconds.
|
||||
Compared to the split second that the actual work usually takes,
|
||||
this overhead can amount to several orders of magnitude.
|
||||
|
||||
Avoid Puppet types that `mgmt` does not implement (yet).
|
||||
|
||||
### Avoiding common warnings
|
||||
|
||||
Many resource parameters in Puppet take default values. For the most part,
|
||||
the translator module just ignores them. However, there are cases in which
|
||||
Puppet will default to convenient behavior that `mgmt` cannot quite replicate.
|
||||
For example, translating a plain `file` resource will lead to a warning message:
|
||||
|
||||
```
|
||||
$ puppet mgmtgraph print --code 'file { "/tmp/mgmt-test": }'
|
||||
Warning: File[/tmp/mgmt-test] uses the 'puppet' file bucket, which mgmt cannot do. There will be no backup copies!
|
||||
```
|
||||
|
||||
The reason is that per default, Puppet assumes the following parameter value
|
||||
(among others)
|
||||
|
||||
```puppet
|
||||
file { "/tmp/mgmt-test":
|
||||
backup => 'puppet',
|
||||
}
|
||||
```
|
||||
|
||||
To avoid this, specify the parameter explicitly:
|
||||
|
||||
```bash
|
||||
puppet mgmtgraph print --code 'file { "/tmp/mgmt-test": backup => false }'
|
||||
```
|
||||
|
||||
This is tedious in a more complex manifest. A good simplification is the
|
||||
following [resource default](https://docs.puppet.com/puppet/latest/reference/lang_defaults.html)
|
||||
anywhere on the top scope of your manifest:
|
||||
|
||||
```puppet
|
||||
File { backup => false }
|
||||
```
|
||||
|
||||
If you encounter similar warnings from other types and/or parameters,
|
||||
use the same approach to silence them if possible.
|
||||
|
||||
## Configuring Puppet
|
||||
|
||||
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
|
||||
do what you want. Reasons for this could be among the following:
|
||||
|
||||
* You use the `--puppet agent` variant and need to configure
|
||||
`servername`, `certname` and other master/agent-related options.
|
||||
* You don't want runtime information to end up in the `vardir`
|
||||
that is used by your regular `puppet agent`.
|
||||
* You install specific Puppet modules for `mgmt` in a non-standard
|
||||
location.
|
||||
|
||||
`mgmt` exposes only one Puppet option in order to allow you to
|
||||
control all of them, through its `--puppet-conf` option. It allows
|
||||
you to specify which `puppet.conf` file should be used during
|
||||
translation.
|
||||
|
||||
```
|
||||
mgmt run --puppet /opt/my-manifest.pp --puppet-conf /etc/mgmt/puppet.conf
|
||||
```
|
||||
|
||||
Within this file, you can just specify any needed options in the
|
||||
`[main]` section:
|
||||
|
||||
```
|
||||
[main]
|
||||
server=mgmt-master.example.net
|
||||
vardir=/var/lib/mgmt/puppet
|
||||
```
|
||||
|
||||
## Caveats
|
||||
|
||||
Please see the [README](https://github.com/ffrank/puppet-mgmtgraph/blob/master/README.md)
|
||||
of the translator module for the current state of supported and unsupported
|
||||
language features.
|
||||
|
||||
You should probably make sure to always use the latest release of
|
||||
both `ffrank-mgmtgraph` and `ffrank-yamlresource` (the latter is
|
||||
getting pulled in as a dependency of the former).
|
||||
182
docs/quick-start-guide.md
Normal file
@@ -0,0 +1,182 @@
|
||||
# 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.9 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 --lang examples/lang/hello0.mcl --tmp-prefix` 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.9 or higher (required, available in some distros and distributed
|
||||
as a binary officially by [golang.org](https://golang.org/dl/))
|
||||
|
||||
### Runtime
|
||||
|
||||
A relatively modern GNU/Linux system should be able to run `mgmt` without any
|
||||
problems. Since `mgmt` runs as a single statically compiled binary, all of the
|
||||
library dependencies are included. It is expected, that certain advanced
|
||||
resources require host specific facilities to work. These requirements are
|
||||
listed below:
|
||||
|
||||
| Resource | Dependency | Version | Check version with |
|
||||
|----------|-------------------|-----------------------------|-----------------------------------------------------------|
|
||||
| augeas | augeas-devel | `augeas 1.6` or greater | `dnf info augeas-devel` or `apt-cache show libaugeas-dev` |
|
||||
| file | inotify | `Linux 2.6.27` or greater | `uname -a` |
|
||||
| hostname | systemd-hostnamed | `systemd 25` or greater | `systemctl --version` |
|
||||
| nspawn | systemd-nspawn | `systemd ???` or greater | `systemctl --version` |
|
||||
| pkg | packagekitd | `packagekit 1.x` or greater | `pkcon --version` |
|
||||
| svc | systemd | `systemd ???` or greater | `systemctl --version` |
|
||||
| virt | libvirt-devel | `libvirt 1.2.0` or greater | `dnf info libvirt-devel` or `apt-cache show libvirt-dev` |
|
||||
| virt | libvirtd | `libvirt 1.2.0` or greater | `libvirtd --version` |
|
||||
|
||||
For building a visual representation of the graph, `graphviz` is required.
|
||||
|
||||
To build `mgmt` without augeas support please run:
|
||||
`GOTAGS='noaugeas' make build`
|
||||
|
||||
To build `mgmt` without libvirt support please run:
|
||||
`GOTAGS='novirt' make build`
|
||||
|
||||
To build `mgmt` without augeas or libvirt support please run:
|
||||
`GOTAGS='noaugeas novirt' make build`
|
||||
|
||||
## Binary Package Installation
|
||||
|
||||
Installation of `mgmt` from distribution packages currently needs improvement.
|
||||
They are not always up-to-date with git master and as such are not recommended.
|
||||
At the moment we have:
|
||||
* [COPR](https://copr.fedoraproject.org/coprs/purpleidea/mgmt/)
|
||||
* [Arch](https://aur.archlinux.org/packages/mgmt/)
|
||||
|
||||
Please contribute more! We'd especially like to see a Debian package!
|
||||
|
||||
## OSX/macOS/Darwin development
|
||||
|
||||
Developing and running `mgmt` on macOS is currently not supported (but not
|
||||
discouraged either). Meaning it might work but in the case it doesn't you would
|
||||
have to provide your own patches to fix problems (the project maintainer and
|
||||
community are glad to assist where needed).
|
||||
|
||||
There are currently some issues that make `mgmt` less suitable to run for provisioning
|
||||
macOS. But as a client to provision remote servers it should run fine.
|
||||
|
||||
Since the primary supported systems are Linux and these are the environments
|
||||
tested for it is wise to run these suites during macOS development as well. To
|
||||
ease this Docker can be leveraged ([Docker for Mac](https://docs.docker.com/docker-for-mac/)).
|
||||
|
||||
Before running any of the commands below create the development Docker image:
|
||||
|
||||
```
|
||||
docker/scripts/build-development
|
||||
```
|
||||
|
||||
This image requires updating every time dependencies (`make-deps.sh`) change.
|
||||
|
||||
Then to run the test suite:
|
||||
|
||||
```
|
||||
docker run --rm -ti \
|
||||
-v $PWD:/go/src/github.com/purpleidea/mgmt/ \
|
||||
-w /go/src/github.com/purpleidea/mgmt/ \
|
||||
purpleidea/mgmt:development \
|
||||
make test
|
||||
```
|
||||
|
||||
For convenience this command is wrapped in `docker/scripts/exec-development`.
|
||||
|
||||
Basically any command can be executed this way. Because the repository source is
|
||||
mounted into the Docker container invocation will be quick and allow rapid
|
||||
testing, example:
|
||||
|
||||
```
|
||||
docker/scripts/exec-development test/test-shell.sh load0.sh
|
||||
```
|
||||
|
||||
Other examples:
|
||||
|
||||
```
|
||||
docker/scripts/exec-development make build
|
||||
docker/scripts/exec-development ./mgmt run --tmp-prefix --lang examples/lang/load0.mcl
|
||||
```
|
||||
635
docs/resource-guide.md
Normal file
@@ -0,0 +1,635 @@
|
||||
# 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 API
|
||||
|
||||
To implement a resource in `mgmt` it must satisfy the
|
||||
[`Res`](https://github.com/purpleidea/mgmt/blob/master/resources/resources.go)
|
||||
interface. What follows are each of the method signatures and a description of
|
||||
each.
|
||||
|
||||
### Default
|
||||
|
||||
```golang
|
||||
Default() 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 generate an error. If you notice that this method is
|
||||
quite large, it might be an indication that you should reconsider the parameter
|
||||
list and interface to this resource. This method is called _before_ `Init`.
|
||||
|
||||
#### 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 obj.BaseRes.Validate() // remember to call the base method!
|
||||
}
|
||||
```
|
||||
|
||||
### 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, and finish by calling
|
||||
the `Init` method of the base resource.
|
||||
|
||||
#### Example
|
||||
|
||||
```golang
|
||||
// Init initializes the Foo resource.
|
||||
func (obj *FooRes) Init() error {
|
||||
// run the resource specific initialization, and error if anything fails
|
||||
if some_error {
|
||||
return err // something went wrong!
|
||||
}
|
||||
return obj.BaseRes.Init() // call the base resource init
|
||||
}
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
#### Example
|
||||
|
||||
```golang
|
||||
// Close runs some cleanup code for this resource.
|
||||
func (obj *FooRes) Close() error {
|
||||
err := obj.conn.Close() // close some internal connection
|
||||
|
||||
// call base close, b/c we're overriding
|
||||
if e := obj.BaseRes.Close(); err == nil {
|
||||
err = e
|
||||
} else if e != nil {
|
||||
err = multierr.Append(err, e) // list of errors
|
||||
}
|
||||
return err
|
||||
}
|
||||
```
|
||||
|
||||
You should probably check the return errors of your internal methods, and pass
|
||||
on an error if something went wrong. Remember to always call the base `Close`
|
||||
method! If you plan to return early if you hit an internal error, then at least
|
||||
call it with a defer!
|
||||
|
||||
### 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 operations_ should be executed!
|
||||
|
||||
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 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; !stateok, nil
|
||||
// do the apply!
|
||||
return false, nil // after success applying
|
||||
if any_error { return false, err } // anytime there's an err!
|
||||
}
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
#### Refresh notifications
|
||||
|
||||
Some resources may choose to support receiving refresh notifications. In general
|
||||
these should be avoided if possible, but nevertheless, they do make sense in
|
||||
certain situations. Resources that support these need to verify if one was sent
|
||||
during the CheckApply phase of execution. This is accomplished by calling the
|
||||
`Refresh() bool` method of the resource, and inspecting the return value. This
|
||||
is only necessary if you plan to perform a refresh action. Refresh actions
|
||||
should still respect the `apply` variable, and no system changes should be made
|
||||
if it is `false`. Refresh notifications are generated by any resource when an
|
||||
action is applied by that resource and are transmitted through graph edges which
|
||||
have enabled their propagation. Resources that currently perform some refresh
|
||||
action include `svc`, `timer`, and `password`.
|
||||
|
||||
#### Paired execution
|
||||
|
||||
For many resources it is not uncommon to see `CheckApply` run twice in rapid
|
||||
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 will cause a `Fatal`.
|
||||
|
||||
### 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 parameters.
|
||||
|
||||
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.Events()` call, and receive events for our
|
||||
resource itself!
|
||||
|
||||
#### Events
|
||||
|
||||
If we receive an internal event from the `<-obj.Events()` method, we can read it
|
||||
with the ReadEvent helper function. This function tells us if we should shutdown
|
||||
our resource, and if we should generate an event. When we want to send an event,
|
||||
we use the `Event` helper function. It is also important to mark the resource
|
||||
state as `dirty` if we believe it might have changed. We do this with the
|
||||
`StateOK(false)` function.
|
||||
|
||||
#### 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. It does this by calling the `Running` method.
|
||||
|
||||
#### Converged
|
||||
|
||||
The engine might be asked to shutdown when the entire state of the system has
|
||||
not seen any changes for some duration of time. The engine can determine this
|
||||
automatically, but each resource can block this if it is absolutely necessary.
|
||||
To do this, the `Watch` method should get the `ConvergedUID` handle that has
|
||||
been prepared for it by the engine. This is done by calling the `ConvergerUID`
|
||||
method on the resource object. The result can be used to set the converged
|
||||
status with `SetConverged`, and to notify when the particular timeout has been
|
||||
reached by waiting on `ConvergedTimer`.
|
||||
|
||||
Instead of interacting with the `ConvergedUID` with these two methods, we can
|
||||
instead use the `StartTimer` and `ResetTimer` methods which accomplish the same
|
||||
thing, but provide a `select`-free interface for different coding situations.
|
||||
|
||||
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
|
||||
|
||||
// notify engine that we're running
|
||||
if err := obj.Running(); err != nil {
|
||||
return err // bubble up a NACK...
|
||||
}
|
||||
|
||||
var send = false // send event?
|
||||
var exit *error
|
||||
for {
|
||||
select {
|
||||
case event := <-obj.Events():
|
||||
// we avoid sending events on unpause
|
||||
if exit, send = obj.ReadEvent(event); exit != nil {
|
||||
return *exit // exit
|
||||
}
|
||||
|
||||
// the actual events!
|
||||
case event := <-obj.foo.Events:
|
||||
if is_an_event {
|
||||
send = true // used below
|
||||
obj.StateOK(false) // 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
|
||||
obj.Event() // send the event!
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Summary
|
||||
|
||||
* Remember to call the appropriate `converger` methods throughout the resource.
|
||||
* Remember to call `Startup` when the `Watch` is running successfully.
|
||||
* Remember to process internal events and shutdown promptly if asked to.
|
||||
* Ensure the design of your resource is well thought out.
|
||||
* Have a look at the existing resources for a rough idea of how this all works.
|
||||
|
||||
### Compare
|
||||
|
||||
```golang
|
||||
Compare(Res) bool
|
||||
```
|
||||
|
||||
Each resource must have a `Compare` method. This takes as input another resource
|
||||
and must return whether they are identical or not. This is used for identifying
|
||||
if an existing resource can be used in place of a new one with a similar set of
|
||||
parameters. In particular, when switching from one graph to a new (possibly
|
||||
identical) graph, this avoids recomputing the state for resources which don't
|
||||
change or that are sufficiently similar that they don't need to be swapped out.
|
||||
|
||||
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.
|
||||
|
||||
#### Example
|
||||
|
||||
```golang
|
||||
// Compare two resources and return if they are equivalent.
|
||||
func (obj *FooRes) Compare(r Res) bool {
|
||||
// we can only compare FooRes to others of the same resource kind
|
||||
res, ok := r.(*FooRes)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if !obj.BaseRes.Compare(res) { // call base Compare
|
||||
return false
|
||||
}
|
||||
if obj.Name != res.Name {
|
||||
return false
|
||||
}
|
||||
|
||||
if obj.whatever != res.whatever {
|
||||
return false
|
||||
}
|
||||
if obj.Flag != res.Flag {
|
||||
return false
|
||||
}
|
||||
|
||||
return true // they must match!
|
||||
}
|
||||
```
|
||||
|
||||
### UIDs
|
||||
|
||||
```golang
|
||||
UIDs() []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() (AutoEdge, error)
|
||||
```
|
||||
|
||||
This returns a struct that implements the `AutoEdge` interface. This struct
|
||||
is used to match other resources that might be relevant dependencies for this
|
||||
resource.
|
||||
|
||||
### CollectPattern
|
||||
|
||||
```golang
|
||||
CollectPattern() string
|
||||
```
|
||||
|
||||
This is currently a stub and will be updated once the DSL is further along.
|
||||
|
||||
### UnmarshalYAML
|
||||
|
||||
```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
|
||||
}
|
||||
```
|
||||
|
||||
## Further considerations
|
||||
|
||||
There is some additional information that any resource writer will need to know.
|
||||
Each issue is listed separately below!
|
||||
|
||||
### Resource struct
|
||||
|
||||
Each resource will implement methods as pointer receivers on a resource struct.
|
||||
The resource struct must include an anonymous reference to the `BaseRes` struct.
|
||||
The naming convention for resources is that they end with a `Res` suffix. If
|
||||
you'd like your resource to be accessible by the `YAML` graph API (GAPI), then
|
||||
you'll need to include the appropriate YAML fields as shown below.
|
||||
|
||||
#### Example
|
||||
|
||||
```golang
|
||||
type FooRes struct {
|
||||
BaseRes `yaml:",inline"` // base properties
|
||||
|
||||
Whatever string `yaml:"whatever"` // you pick!
|
||||
Bar int // no yaml, used as public output value for send/recv
|
||||
Baz bool `yaml:"baz"` // something else
|
||||
|
||||
something string // some private field
|
||||
}
|
||||
```
|
||||
|
||||
### Resource registration
|
||||
|
||||
All resources must be registered with the engine so that they can be found. This
|
||||
also ensures they can be encoded and decoded. Make sure to include the following
|
||||
code snippet for this to work.
|
||||
|
||||
```golang
|
||||
func init() { // special golang method that runs once
|
||||
// set your resource kind and struct here (the kind must be lower case)
|
||||
RegisterResource("foo", func() Res { return &FooRes{} })
|
||||
}
|
||||
```
|
||||
|
||||
## Automatic edges
|
||||
|
||||
Automatic edges in `mgmt` are well described in [this article](https://purpleidea.com/blog/2016/03/14/automatic-edges-in-mgmt/).
|
||||
The best example of this technique can be seen in the `svc` resource.
|
||||
Unfortunately no further documentation about this subject has been written. To
|
||||
expand this section, please send a patch! Please contact us if you'd like to
|
||||
work on a resource that uses this feature, or to add it to an existing one!
|
||||
|
||||
## Automatic grouping
|
||||
|
||||
Automatic grouping in `mgmt` is well described in [this article](https://purpleidea.com/blog/2016/03/30/automatic-grouping-in-mgmt/).
|
||||
The best example of this technique can be seen in the `pkg` resource.
|
||||
Unfortunately no further documentation about this subject has been written. To
|
||||
expand this section, please send a patch! Please contact us if you'd like to
|
||||
work on a resource that uses this feature, or to add it to an existing one!
|
||||
|
||||
## Send/Recv
|
||||
|
||||
In `mgmt` there is a novel concept called _Send/Recv_. For some background,
|
||||
please [read the introductory article](https://purpleidea.com/blog/2016/12/07/sendrecv-in-mgmt/).
|
||||
When using this feature, the engine will automatically send the user specified
|
||||
value to the intended destination without requiring any resource specific code.
|
||||
Any time that one of the destination values is changed, the engine automatically
|
||||
marks the resource state as `dirty`. To detect if a particular value was
|
||||
received, and if it changed (during this invocation of CheckApply) from the
|
||||
previous value, you can query the Recv parameter. It will contain a `map` of all
|
||||
the keys which can be received on, and the value has a `Changed` property which
|
||||
will indicate whether the value was updated on this particular `CheckApply`
|
||||
invocation. The type of the sending key must match that of the receiving one.
|
||||
This can _only_ be done inside of the `CheckApply` function!
|
||||
|
||||
```golang
|
||||
// inside CheckApply, probably near the top
|
||||
if val, exists := obj.Recv["SomeKey"]; exists {
|
||||
log.Printf("SomeKey was sent to us from: %s.%s", val.Res, val.Key)
|
||||
if val.Changed {
|
||||
log.Printf("SomeKey was just updated!")
|
||||
// you may want to invalidate some local cache
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Astute readers will note that there isn't anything that prevents a user from
|
||||
sending an identically typed value to some arbitrary (public) key that the
|
||||
resource author hadn't considered! While this is true, resources should probably
|
||||
work within this problem space anyways. The rule of thumb is that any public
|
||||
parameter which is normally used in a resource can be used safely.
|
||||
|
||||
One subtle scenario is that if a resource creates a local cache or stores a
|
||||
computation that depends on the value of a public parameter and will require
|
||||
invalidation should that public parameter change, then you must detect that
|
||||
scenario and invalidate the cache when it occurs. This *must* be processed
|
||||
before there is a possibility of failure in CheckApply, because if we fail (and
|
||||
possibly run again) the subsequent send->recv transfer might not have a new
|
||||
value to copy, and therefore we won't see this notification of change.
|
||||
Therefore, it is important to process these promptly, if they must not be lost,
|
||||
such as for cache invalidation.
|
||||
|
||||
Remember, `Send/Recv` only changes your resource code if you cache state.
|
||||
|
||||
## 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!
|
||||
|
||||
### 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!
|
||||
177
docs/resources.md
Normal file
@@ -0,0 +1,177 @@
|
||||
# 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/resources)
|
||||
for more up-to-date information about these resources.
|
||||
|
||||
* [Augeas](#Augeas): Manipulate files using augeas.
|
||||
* [Exec](#Exec): Execute shell commands on the system.
|
||||
* [File](#File): Manage files and directories.
|
||||
* [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.
|
||||
* [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.
|
||||
|
||||
## Augeas
|
||||
|
||||
The augeas resource uses [augeas](http://augeas.net/) commands to manipulate
|
||||
files.
|
||||
|
||||
## 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`: 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.
|
||||
|
||||
## 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.
|
||||
|
||||
## 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.
|
||||
96
docs/style-guide.md
Normal file
@@ -0,0 +1,96 @@
|
||||
# Style guide
|
||||
|
||||
## Overview
|
||||
|
||||
This document aims to be a reference for the desired style for patches to mgmt.
|
||||
In particular it describes conventions which we use which are not officially
|
||||
enforced by the `gofmt` tool, and which might not be clearly defined elsewhere.
|
||||
Most of these are common sense to seasoned programmers, and we hope this will be
|
||||
a useful reference for new programmers.
|
||||
|
||||
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`.
|
||||
|
||||
## Suggestions
|
||||
|
||||
If you have any ideas for suggestions or other improvements to this guide,
|
||||
please let us know!
|
||||
300
etcd.go
@@ -1,300 +0,0 @@
|
||||
// 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/>.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
etcd_context "github.com/coreos/etcd/Godeps/_workspace/src/golang.org/x/net/context"
|
||||
etcd "github.com/coreos/etcd/client"
|
||||
"log"
|
||||
"math"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
//go:generate stringer -type=etcdMsg -output=etcdmsg_stringer.go
|
||||
type etcdMsg int
|
||||
|
||||
const (
|
||||
etcdStart etcdMsg = iota
|
||||
etcdEvent
|
||||
etcdFoo
|
||||
etcdBar
|
||||
)
|
||||
|
||||
//go:generate stringer -type=etcdConvergedState -output=etcdconvergedstate_stringer.go
|
||||
type etcdConvergedState int
|
||||
|
||||
const (
|
||||
etcdConvergedNil etcdConvergedState = iota
|
||||
//etcdConverged
|
||||
etcdConvergedTimeout
|
||||
)
|
||||
|
||||
type EtcdWObject struct { // etcd wrapper object
|
||||
seed string
|
||||
ctimeout int
|
||||
converged chan bool
|
||||
kapi etcd.KeysAPI
|
||||
convergedState etcdConvergedState
|
||||
}
|
||||
|
||||
func (etcdO *EtcdWObject) GetConvergedState() etcdConvergedState {
|
||||
return etcdO.convergedState
|
||||
}
|
||||
|
||||
func (etcdO *EtcdWObject) SetConvergedState(state etcdConvergedState) {
|
||||
etcdO.convergedState = state
|
||||
}
|
||||
|
||||
func (etcdO *EtcdWObject) GetKAPI() etcd.KeysAPI {
|
||||
if etcdO.kapi != nil { // memoize
|
||||
return etcdO.kapi
|
||||
}
|
||||
|
||||
cfg := etcd.Config{
|
||||
Endpoints: []string{etcdO.seed},
|
||||
Transport: etcd.DefaultTransport,
|
||||
// set timeout per request to fail fast when the target endpoint is unavailable
|
||||
HeaderTimeoutPerRequest: time.Second,
|
||||
}
|
||||
|
||||
var c etcd.Client
|
||||
var err error
|
||||
|
||||
c, err = etcd.New(cfg)
|
||||
if err != nil {
|
||||
// XXX: not sure if this ever errors
|
||||
if cerr, ok := err.(*etcd.ClusterError); ok {
|
||||
// XXX: not sure if this part ever matches
|
||||
// not running or disconnected
|
||||
if cerr == etcd.ErrClusterUnavailable {
|
||||
log.Fatal("XXX: etcd: ErrClusterUnavailable")
|
||||
} else {
|
||||
log.Fatal("XXX: etcd: Unknown")
|
||||
}
|
||||
}
|
||||
log.Fatal(err) // some unhandled error
|
||||
}
|
||||
etcdO.kapi = etcd.NewKeysAPI(c)
|
||||
return etcdO.kapi
|
||||
}
|
||||
|
||||
type EtcdChannelWatchResponse struct {
|
||||
resp *etcd.Response
|
||||
err error
|
||||
}
|
||||
|
||||
// wrap the etcd watcher.Next blocking function inside of a channel
|
||||
func (etcdO *EtcdWObject) EtcdChannelWatch(watcher etcd.Watcher, context etcd_context.Context) chan *EtcdChannelWatchResponse {
|
||||
ch := make(chan *EtcdChannelWatchResponse)
|
||||
go func() {
|
||||
for {
|
||||
resp, err := watcher.Next(context) // blocks here
|
||||
ch <- &EtcdChannelWatchResponse{resp, err}
|
||||
}
|
||||
}()
|
||||
return ch
|
||||
}
|
||||
|
||||
func (etcdO *EtcdWObject) EtcdWatch() chan etcdMsg {
|
||||
kapi := etcdO.GetKAPI()
|
||||
ctimeout := etcdO.ctimeout
|
||||
converged := etcdO.converged
|
||||
// XXX: i think we need this buffered so that when we're hanging on the
|
||||
// channel, which is inside the EtcdWatch main loop, we still want the
|
||||
// calls to Get/Set on etcd to succeed, so blocking them here would
|
||||
// kill the whole thing
|
||||
ch := make(chan etcdMsg, 1) // XXX: buffer of at least 1 is required
|
||||
go func(ch chan etcdMsg) {
|
||||
tmin := 500 // initial (min) delay in ms
|
||||
t := tmin // current time
|
||||
tmult := 2 // multiplier for exponential delay
|
||||
tmax := 16000 // max delay
|
||||
watcher := kapi.Watcher("/exported/", &etcd.WatcherOptions{Recursive: true})
|
||||
etcdch := etcdO.EtcdChannelWatch(watcher, etcd_context.Background())
|
||||
for {
|
||||
log.Printf("Etcd: Watching...")
|
||||
var resp *etcd.Response // = nil by default
|
||||
var err error
|
||||
select {
|
||||
case out := <-etcdch:
|
||||
etcdO.SetConvergedState(etcdConvergedNil)
|
||||
resp, err = out.resp, out.err
|
||||
|
||||
case _ = <-TimeAfterOrBlock(ctimeout):
|
||||
etcdO.SetConvergedState(etcdConvergedTimeout)
|
||||
converged <- true
|
||||
continue
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if err == etcd_context.Canceled {
|
||||
// ctx is canceled by another routine
|
||||
log.Fatal("Canceled")
|
||||
} else if err == etcd_context.DeadlineExceeded {
|
||||
// ctx is attached with a deadline and it exceeded
|
||||
log.Fatal("Deadline")
|
||||
} else if cerr, ok := err.(*etcd.ClusterError); ok {
|
||||
// not running or disconnected
|
||||
// TODO: is there a better way to parse errors?
|
||||
for _, e := range cerr.Errors {
|
||||
if strings.HasSuffix(e.Error(), "getsockopt: connection refused") {
|
||||
t = int(math.Min(float64(t*tmult), float64(tmax)))
|
||||
log.Printf("Etcd: Waiting %d ms for connection...", t)
|
||||
time.Sleep(time.Duration(t) * time.Millisecond) // sleep for t ms
|
||||
|
||||
} else if e.Error() == "unexpected EOF" {
|
||||
log.Printf("Etcd: Unexpected disconnect...")
|
||||
|
||||
} else if e.Error() == "EOF" {
|
||||
log.Printf("Etcd: Disconnected...")
|
||||
|
||||
} else if strings.HasPrefix(e.Error(), "unsupported protocol scheme") {
|
||||
// usually a bad peer endpoint value
|
||||
log.Fatal("Bad peer endpoint value?")
|
||||
|
||||
} else {
|
||||
log.Fatal("Woops: ", e.Error())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// bad cluster endpoints, which are not etcd servers
|
||||
log.Fatal("Woops: ", err)
|
||||
}
|
||||
} else {
|
||||
//log.Print(resp)
|
||||
//log.Printf("Watcher().Node.Value(%v): %+v", key, resp.Node.Value)
|
||||
// FIXME: we should actually reset when the server comes back, not here on msg!
|
||||
//XXX: can we fix this with one of these patterns?: https://blog.golang.org/go-concurrency-patterns-timing-out-and
|
||||
t = tmin // reset timer
|
||||
|
||||
// don't trigger event if nothing changed
|
||||
if n, p := resp.Node, resp.PrevNode; resp.Action == "set" && p != nil {
|
||||
if n.Key == p.Key && n.Value == p.Value {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: we get events on key/type/value changes for
|
||||
// each type directory... ignore the non final ones...
|
||||
// IOW, ignore everything except for the value or some
|
||||
// field which gets set last... this could be the max count field thing...
|
||||
|
||||
log.Printf("Etcd: Value: %v", resp.Node.Value) // event
|
||||
ch <- etcdEvent // event
|
||||
}
|
||||
|
||||
} // end for loop
|
||||
//close(ch)
|
||||
}(ch) // call go routine
|
||||
return ch
|
||||
}
|
||||
|
||||
// helper function to store our data in etcd
|
||||
func (etcdO *EtcdWObject) EtcdPut(hostname, key, typ string, obj interface{}) bool {
|
||||
kapi := etcdO.GetKAPI()
|
||||
output, ok := ObjToB64(obj)
|
||||
if !ok {
|
||||
log.Printf("Etcd: Could not encode %v key.", key)
|
||||
return false
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("/exported/%s/types/%s/type", hostname, key)
|
||||
_, err := kapi.Set(etcd_context.Background(), path, typ, nil)
|
||||
// XXX validate...
|
||||
|
||||
path = fmt.Sprintf("/exported/%s/types/%s/value", hostname, key)
|
||||
resp, err := kapi.Set(etcd_context.Background(), path, output, nil)
|
||||
if err != nil {
|
||||
if cerr, ok := err.(*etcd.ClusterError); ok {
|
||||
// not running or disconnected
|
||||
for _, e := range cerr.Errors {
|
||||
if strings.HasSuffix(e.Error(), "getsockopt: connection refused") {
|
||||
}
|
||||
//if e == etcd.ErrClusterUnavailable
|
||||
}
|
||||
}
|
||||
log.Printf("Etcd: Could not store %v key.", key)
|
||||
return false
|
||||
}
|
||||
log.Print("Etcd: ", resp) // w00t... bonus
|
||||
return true
|
||||
}
|
||||
|
||||
// lookup /exported/ node hierarchy
|
||||
func (etcdO *EtcdWObject) EtcdGet() (etcd.Nodes, bool) {
|
||||
kapi := etcdO.GetKAPI()
|
||||
// key structure is /exported/<hostname>/types/...
|
||||
resp, err := kapi.Get(etcd_context.Background(), "/exported/", &etcd.GetOptions{Recursive: true})
|
||||
if err != nil {
|
||||
return nil, false // not found
|
||||
}
|
||||
return resp.Node.Nodes, true
|
||||
}
|
||||
|
||||
func (etcdO *EtcdWObject) EtcdGetProcess(nodes etcd.Nodes, typ string) []string {
|
||||
//path := fmt.Sprintf("/exported/%s/types/", h)
|
||||
top := "/exported/"
|
||||
log.Printf("Etcd: Get: %+v", nodes) // Get().Nodes.Nodes
|
||||
var output []string
|
||||
|
||||
for _, x := range nodes { // loop through hosts
|
||||
if !strings.HasPrefix(x.Key, top) {
|
||||
log.Fatal("Error!")
|
||||
}
|
||||
host := x.Key[len(top):]
|
||||
//log.Printf("Get().Nodes[%v]: %+v ==> %+v", -1, host, x.Nodes)
|
||||
//log.Printf("Get().Nodes[%v]: %+v ==> %+v", i, x.Key, x.Nodes)
|
||||
types, ok := EtcdGetChildNodeByKey(x, "types")
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
for _, y := range types.Nodes { // loop through types
|
||||
//key := y.Key # UUID?
|
||||
//log.Printf("Get(%v): TYPE[%v]", host, y.Key)
|
||||
t, ok := EtcdGetChildNodeByKey(y, "type")
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if typ != "" && typ != t.Value {
|
||||
continue
|
||||
} // filter based on type
|
||||
|
||||
v, ok := EtcdGetChildNodeByKey(y, "value") // B64ToObj this
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
log.Printf("Etcd: Hostname: %v; Get: %v", host, t.Value)
|
||||
|
||||
output = append(output, v.Value)
|
||||
}
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
// TODO: wrap this somehow so it's a method of *etcd.Node
|
||||
// helper function that returns the node for a particular key under a node
|
||||
func EtcdGetChildNodeByKey(node *etcd.Node, key string) (*etcd.Node, bool) {
|
||||
for _, x := range node.Nodes {
|
||||
if x.Key == fmt.Sprintf("%s/%s", node.Key, key) {
|
||||
return x, true
|
||||
}
|
||||
}
|
||||
return nil, false // not found
|
||||
}
|
||||
94
etcd/client.go
Normal file
@@ -0,0 +1,94 @@
|
||||
// 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 etcd
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
etcd "github.com/coreos/etcd/clientv3" // "clientv3"
|
||||
errwrap "github.com/pkg/errors"
|
||||
context "golang.org/x/net/context"
|
||||
)
|
||||
|
||||
// ClientEtcd provides a simple etcd client for deploy and status operations.
|
||||
type ClientEtcd struct {
|
||||
Seeds []string // list of endpoints to try to connect
|
||||
|
||||
client *etcd.Client
|
||||
}
|
||||
|
||||
// GetClient returns a handle to the raw etcd client object.
|
||||
func (obj *ClientEtcd) GetClient() *etcd.Client {
|
||||
return obj.client
|
||||
}
|
||||
|
||||
// GetConfig returns the config struct to be used for the etcd client connect.
|
||||
func (obj *ClientEtcd) GetConfig() etcd.Config {
|
||||
cfg := etcd.Config{
|
||||
Endpoints: obj.Seeds,
|
||||
// RetryDialer chooses the next endpoint to use
|
||||
// it comes with a default dialer if unspecified
|
||||
DialTimeout: 5 * time.Second,
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
|
||||
// Connect connects the client to a server, and then builds the *API structs.
|
||||
// If reconnect is true, it will force a reconnect with new config endpoints.
|
||||
func (obj *ClientEtcd) Connect() error {
|
||||
if obj.client != nil { // memoize
|
||||
return nil
|
||||
}
|
||||
|
||||
var err error
|
||||
cfg := obj.GetConfig()
|
||||
obj.client, err = etcd.New(cfg) // connect!
|
||||
if err != nil {
|
||||
return errwrap.Wrapf(err, "client connect error")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Destroy cleans up the entire etcd client connection.
|
||||
func (obj *ClientEtcd) Destroy() error {
|
||||
err := obj.client.Close()
|
||||
//obj.wg.Wait()
|
||||
return err
|
||||
}
|
||||
|
||||
// Get runs a get on the client connection. This has the same signature as our
|
||||
// EmbdEtcd Get function.
|
||||
func (obj *ClientEtcd) Get(path string, opts ...etcd.OpOption) (map[string]string, error) {
|
||||
resp, err := obj.client.Get(context.TODO(), path, opts...)
|
||||
if err != nil || resp == nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// TODO: write a resp.ToMap() function on https://godoc.org/github.com/coreos/etcd/etcdserver/etcdserverpb#RangeResponse
|
||||
result := make(map[string]string)
|
||||
for _, x := range resp.Kvs {
|
||||
result[string(x.Key)] = string(x.Value)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Txn runs a transaction on the client connection. This has the same signature
|
||||
// as our EmbdEtcd Txn function.
|
||||
func (obj *ClientEtcd) Txn(ifcmps []etcd.Cmp, thenops, elseops []etcd.Op) (*etcd.TxnResponse, error) {
|
||||
return obj.client.KV.Txn(context.TODO()).If(ifcmps...).Then(thenops...).Else(elseops...).Commit()
|
||||
}
|
||||
171
etcd/deploy.go
Normal file
@@ -0,0 +1,171 @@
|
||||
// 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 etcd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
etcd "github.com/coreos/etcd/clientv3"
|
||||
errwrap "github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
deployPath = "deploy"
|
||||
payloadPath = "payload"
|
||||
hashPath = "hash"
|
||||
)
|
||||
|
||||
// WatchDeploy returns a channel which spits out events on new deploy activity.
|
||||
// FIXME: It should close the channel when it's done, and spit out errors when
|
||||
// something goes wrong.
|
||||
func WatchDeploy(obj *EmbdEtcd) chan error {
|
||||
// key structure is $NS/deploy/$id/payload = $data
|
||||
path := fmt.Sprintf("%s/%s/", NS, deployPath)
|
||||
ch := make(chan error, 1)
|
||||
// FIXME: fix our API so that we get a close event on shutdown.
|
||||
callback := func(re *RE) error {
|
||||
// TODO: is this even needed? it used to happen on conn errors
|
||||
//log.Printf("Etcd: Watch: Path: %v", path) // event
|
||||
if re == nil || re.response.Canceled {
|
||||
return fmt.Errorf("watch is empty") // will cause a CtxError+retry
|
||||
}
|
||||
if len(ch) == 0 { // send event only if one isn't pending
|
||||
ch <- nil // event
|
||||
}
|
||||
return nil
|
||||
}
|
||||
_, _ = obj.AddWatcher(path, callback, true, false, etcd.WithPrefix()) // no need to check errors
|
||||
return ch
|
||||
}
|
||||
|
||||
// GetDeploys gets all the available deploys.
|
||||
func GetDeploys(obj Client) (map[uint64]string, error) {
|
||||
// key structure is $NS/deploy/$id/payload = $data
|
||||
path := fmt.Sprintf("%s/%s/", NS, deployPath)
|
||||
keyMap, err := obj.Get(path, etcd.WithPrefix(), etcd.WithSort(etcd.SortByKey, etcd.SortAscend))
|
||||
if err != nil {
|
||||
return nil, errwrap.Wrapf(err, "could not get deploy")
|
||||
}
|
||||
result := make(map[uint64]string)
|
||||
for key, val := range keyMap {
|
||||
if !strings.HasPrefix(key, path) { // sanity check
|
||||
continue
|
||||
}
|
||||
|
||||
str := strings.Split(key[len(path):], "/")
|
||||
if len(str) != 2 {
|
||||
return nil, fmt.Errorf("unexpected chunk count of %d", len(str))
|
||||
}
|
||||
if s := str[1]; s != payloadPath {
|
||||
continue // skip, maybe there are other future additions
|
||||
}
|
||||
|
||||
var id uint64
|
||||
var err error
|
||||
x := str[0]
|
||||
if id, err = strconv.ParseUint(x, 10, 64); err != nil {
|
||||
return nil, fmt.Errorf("invalid id of `%s`", x)
|
||||
}
|
||||
|
||||
// TODO: do some sort of filtering here?
|
||||
//log.Printf("Etcd: GetDeploys(%s): Id => Data: %d => %s", key, id, val)
|
||||
result[id] = val
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetDeploy gets the latest deploy if id == 0, otherwise it returns the deploy
|
||||
// with the specified id if it exists.
|
||||
// FIXME: implement this more efficiently so that it doesn't have to download *all* the old deploys from etcd!
|
||||
func GetDeploy(obj Client, id uint64) (string, error) {
|
||||
result, err := GetDeploys(obj)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if id != 0 {
|
||||
str, exists := result[id]
|
||||
if !exists {
|
||||
return "", fmt.Errorf("can't find id `%d`", id)
|
||||
}
|
||||
return str, nil
|
||||
}
|
||||
// find the latest id
|
||||
var max uint64
|
||||
for i := range result {
|
||||
if i > max {
|
||||
max = i
|
||||
}
|
||||
}
|
||||
if max == 0 {
|
||||
return "", nil // no results yet
|
||||
}
|
||||
return result[max], nil
|
||||
}
|
||||
|
||||
// AddDeploy adds a new deploy. It takes an id and ensures it's sequential. If
|
||||
// hash is not empty, then it will check that the pHash matches what the
|
||||
// previous hash was, and also adds this new hash along side the id. This is
|
||||
// useful to make sure you get a linear chain of git patches, and to avoid two
|
||||
// contributors pushing conflicting deploys. This isn't git specific, and so any
|
||||
// arbitrary string hash can be used.
|
||||
// FIXME: prune old deploys from the store when they aren't needed anymore...
|
||||
func AddDeploy(obj Client, id uint64, hash, pHash string, data *string) error {
|
||||
// key structure is $NS/deploy/$id/payload = $data
|
||||
// key structure is $NS/deploy/$id/hash = $hash
|
||||
path := fmt.Sprintf("%s/%s/%d/%s", NS, deployPath, id, payloadPath)
|
||||
tPath := fmt.Sprintf("%s/%s/%d/%s", NS, deployPath, id, hashPath)
|
||||
ifs := []etcd.Cmp{} // list matching the desired state
|
||||
ops := []etcd.Op{} // list of ops in this transaction (then)
|
||||
|
||||
// TODO: use https://github.com/coreos/etcd/pull/7417 if merged
|
||||
// we're append only, so ensure this unique deploy id doesn't exist
|
||||
ifs = append(ifs, etcd.Compare(etcd.Version(path), "=", 0)) // KeyMissing
|
||||
//ifs = append(ifs, etcd.KeyMissing(path))
|
||||
|
||||
// don't look for previous deploy if this is the first deploy ever
|
||||
if id > 1 {
|
||||
// we append sequentially, so ensure previous key *does* exist
|
||||
prev := fmt.Sprintf("%s/%s/%d/%s", NS, deployPath, id-1, payloadPath)
|
||||
ifs = append(ifs, etcd.Compare(etcd.Version(prev), ">", 0)) // KeyExists
|
||||
//ifs = append(ifs, etcd.KeyExists(prev))
|
||||
|
||||
if hash != "" && pHash != "" {
|
||||
// does the previously stored hash match what we expect?
|
||||
prevHash := fmt.Sprintf("%s/%s/%d/%s", NS, deployPath, id-1, hashPath)
|
||||
ifs = append(ifs, etcd.Compare(etcd.Value(prevHash), "=", pHash))
|
||||
}
|
||||
}
|
||||
|
||||
ops = append(ops, etcd.OpPut(path, *data))
|
||||
if hash != "" {
|
||||
ops = append(ops, etcd.OpPut(tPath, hash)) // store new hash as well
|
||||
}
|
||||
|
||||
// it's important to do this in one transaction, and atomically, because
|
||||
// this way, we only generate one watch event, and only when it's needed
|
||||
result, err := obj.Txn(ifs, ops, nil)
|
||||
if err != nil {
|
||||
return errwrap.Wrapf(err, "error creating deploy id %d: %s", id)
|
||||
}
|
||||
if !result.Succeeded {
|
||||
return fmt.Errorf("could not create deploy id %d", id)
|
||||
}
|
||||
return nil // success
|
||||
}
|
||||
1837
etcd/etcd.go
Normal file
49
etcd/etcd_test.go
Normal file
@@ -0,0 +1,49 @@
|
||||
// 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 etcd
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
etcdtypes "github.com/coreos/etcd/pkg/types"
|
||||
)
|
||||
|
||||
func TestNewEmbdEtcd(t *testing.T) {
|
||||
// should return a new etcd object
|
||||
|
||||
noServer := false
|
||||
var flags Flags
|
||||
|
||||
obj := NewEmbdEtcd("", nil, nil, nil, nil, nil, noServer, 0, flags, "", nil)
|
||||
if obj == nil {
|
||||
t.Fatal("failed to create server object")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewEmbdEtcdConfigValidation(t *testing.T) {
|
||||
// running --no-server with no --seeds specified should fail early
|
||||
|
||||
seeds := make(etcdtypes.URLs, 0)
|
||||
noServer := true
|
||||
var flags Flags
|
||||
|
||||
obj := NewEmbdEtcd("", seeds, nil, nil, nil, nil, noServer, 0, flags, "", nil)
|
||||
if obj != nil {
|
||||
t.Fatal("server initialization should fail on invalid configuration")
|
||||
}
|
||||
}
|
||||
540
etcd/fs/file.go
Normal file
@@ -0,0 +1,540 @@
|
||||
// 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 fs
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
etcd "github.com/coreos/etcd/clientv3" // "clientv3"
|
||||
errwrap "github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func init() {
|
||||
gob.Register(&File{})
|
||||
}
|
||||
|
||||
// File represents a file node. This is the node of our tree structure. This is
|
||||
// not thread safe, and you can have at most one open file handle at a time.
|
||||
type File struct {
|
||||
// FIXME: add a rwmutex to make this thread safe
|
||||
fs *Fs // pointer to file system
|
||||
|
||||
Path string // relative path to file, trailing slash if it's a directory
|
||||
Mode os.FileMode
|
||||
ModTime time.Time
|
||||
//Size int64 // XXX: cache the size to avoid full file downloads for stat!
|
||||
|
||||
Children []*File // dir's use this
|
||||
Hash string // string not []byte so it's readable, matches data
|
||||
|
||||
data []byte // cache of the data. private so it doesn't get encoded
|
||||
cursor int64
|
||||
dirCursor int64
|
||||
|
||||
readOnly bool // is the file read-only?
|
||||
closed bool // is file closed?
|
||||
}
|
||||
|
||||
// path returns the expected path to the actual file in etcd.
|
||||
func (obj *File) path() string {
|
||||
// keys are prefixed with the hash-type eg: {sha256} to allow different
|
||||
// superblocks to share the same data prefix even with different hashes
|
||||
return fmt.Sprintf("%s/{%s}%s", obj.fs.sb.DataPrefix, obj.fs.Hash, obj.Hash)
|
||||
}
|
||||
|
||||
// cache downloads the file contents from etcd and stores them in our cache.
|
||||
func (obj *File) cache() error {
|
||||
if obj.Mode.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
h, err := obj.fs.hash(obj.data) // update hash
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if h == obj.Hash { // we already have the correct data cached
|
||||
return nil
|
||||
}
|
||||
|
||||
p := obj.path() // get file data from this path in etcd
|
||||
|
||||
result, err := obj.fs.get(p) // download the file...
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if result == nil || len(result) == 0 { // nothing found
|
||||
return err
|
||||
}
|
||||
data, exists := result[p]
|
||||
if !exists {
|
||||
return fmt.Errorf("could not find data") // programming error?
|
||||
}
|
||||
obj.data = data // save
|
||||
return nil
|
||||
}
|
||||
|
||||
// findNode is the "in array" equivalent for searching through a dir's children.
|
||||
// You must *not* specify an absolute path as the search string, but rather you
|
||||
// should specify the name. To search for something name "bar" inside a dir
|
||||
// named "/tmp/foo/", you just pass in "bar", not "/tmp/foo/bar".
|
||||
func (obj *File) findNode(name string) (*File, bool) {
|
||||
for _, node := range obj.Children {
|
||||
if name == node.Path {
|
||||
return node, true // found
|
||||
}
|
||||
}
|
||||
return nil, false // not found
|
||||
}
|
||||
|
||||
func fileCreate(fs *Fs, name string) (*File, error) {
|
||||
if name == "" {
|
||||
return nil, fmt.Errorf("invalid input path")
|
||||
}
|
||||
if !strings.HasPrefix(name, "/") {
|
||||
return nil, fmt.Errorf("invalid input path (not absolute)")
|
||||
}
|
||||
cleanPath := path.Clean(name) // remove possible trailing slashes
|
||||
|
||||
// try to add node to tree by first finding the parent node
|
||||
parentPath, filePath := path.Split(cleanPath) // looking for this
|
||||
|
||||
node, err := fs.find(parentPath)
|
||||
if err != nil { // might be ErrNotExist
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fi, err := node.Stat()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !fi.IsDir() { // is the parent a suitable home?
|
||||
return nil, &os.PathError{Op: "create", Path: name, Err: syscall.ENOTDIR}
|
||||
}
|
||||
|
||||
f, exists := node.findNode(filePath) // does file already exist inside?
|
||||
if exists { // already exists, overwrite!
|
||||
if err := f.Truncate(0); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
|
||||
data := []byte("") // empty file contents
|
||||
h, err := fs.hash(data) // TODO: use memoized value?
|
||||
if err != nil {
|
||||
return &File{}, err // TODO: nil instead?
|
||||
}
|
||||
|
||||
f = &File{
|
||||
fs: fs,
|
||||
Path: filePath, // the relative path chunk (not incl. dir name)
|
||||
Hash: h,
|
||||
data: data,
|
||||
}
|
||||
|
||||
// add to parent
|
||||
node.Children = append(node.Children, f)
|
||||
|
||||
// push new file up if not on server, and then push up the metadata
|
||||
if err := f.Sync(); err != nil {
|
||||
return f, err // TODO: ok to return the file so user can run sync?
|
||||
}
|
||||
|
||||
return f, nil
|
||||
}
|
||||
|
||||
func fileOpen(fs *Fs, name string) (*File, error) {
|
||||
if name == "" {
|
||||
return nil, fmt.Errorf("invalid input path")
|
||||
}
|
||||
if !strings.HasPrefix(name, "/") {
|
||||
return nil, fmt.Errorf("invalid input path (not absolute)")
|
||||
}
|
||||
cleanPath := path.Clean(name) // remove possible trailing slashes
|
||||
|
||||
node, err := fs.find(cleanPath)
|
||||
if err != nil { // might be ErrNotExist
|
||||
return &File{}, err // TODO: nil instead?
|
||||
}
|
||||
|
||||
// download file contents into obj.data
|
||||
if err := node.cache(); err != nil {
|
||||
return &File{}, err // TODO: nil instead?
|
||||
}
|
||||
|
||||
//fi, err := node.Stat()
|
||||
//if err != nil {
|
||||
// return nil, err
|
||||
//}
|
||||
//if fi.IsDir() { // can we open a directory? - yes we can apparently
|
||||
// return nil, fmt.Errorf("file is a directory")
|
||||
//}
|
||||
|
||||
node.readOnly = true // as per docs, fileOpen opens files as read-only
|
||||
node.closed = false // as per docs, fileOpen opens files as read-only
|
||||
|
||||
return node, nil
|
||||
}
|
||||
|
||||
// Close closes the file handle. This will try and run Sync automatically.
|
||||
func (obj *File) Close() error {
|
||||
if !obj.readOnly {
|
||||
obj.ModTime = time.Now()
|
||||
}
|
||||
|
||||
if err := obj.Sync(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// FIXME: there is a big implementation mistake between the metadata
|
||||
// node and the file handle, since they're currently sharing a struct!
|
||||
|
||||
// invalidate all of the fields
|
||||
//obj.fs = nil
|
||||
|
||||
//obj.Path = ""
|
||||
//obj.Mode = os.FileMode(0)
|
||||
//obj.ModTime = time.Time{}
|
||||
|
||||
//obj.Children = nil
|
||||
//obj.Hash = ""
|
||||
|
||||
//obj.data = nil
|
||||
obj.cursor = 0
|
||||
obj.readOnly = false
|
||||
|
||||
obj.closed = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// Name returns the path of the file.
|
||||
func (obj *File) Name() string {
|
||||
return obj.Path
|
||||
}
|
||||
|
||||
// Stat returns some information about the file.
|
||||
func (obj *File) Stat() (os.FileInfo, error) {
|
||||
// download file contents into obj.data
|
||||
if err := obj.cache(); err != nil { // needed so Size() works correctly
|
||||
return nil, err
|
||||
}
|
||||
return &FileInfo{ // everything is actually stored in the main file node
|
||||
file: obj,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Sync flushes the file contents to the server and calls the filesystem
|
||||
// metadata sync as well.
|
||||
// FIXME: instead of a txn, run a get and then a put in two separate stages. if
|
||||
// the get already found the data up there, then we don't need to push it all in
|
||||
// the put phase. with the txn it is always all sent up even if the put is never
|
||||
// needed. the get should just be a "key exists" test, and not a download of the
|
||||
// whole file. if we *do* do the download, we can byte-by-byte check for hash
|
||||
// collisions and panic if we find one :)
|
||||
func (obj *File) Sync() error {
|
||||
if obj.closed {
|
||||
return ErrFileClosed
|
||||
}
|
||||
|
||||
p := obj.path() // store file data at this path in etcd
|
||||
|
||||
// TODO: use https://github.com/coreos/etcd/pull/7417 if merged
|
||||
cmp := etcd.Compare(etcd.Version(p), "=", 0) // KeyMissing
|
||||
//cmp := etcd.KeyMissing(p))
|
||||
|
||||
op := etcd.OpPut(p, string(obj.data)) // this pushes contents to server
|
||||
|
||||
// it's important to do this in one transaction, and atomically, because
|
||||
// this way, we only generate one watch event, and only when it's needed
|
||||
result, err := obj.fs.txn([]etcd.Cmp{cmp}, []etcd.Op{op}, nil)
|
||||
if err != nil {
|
||||
return errwrap.Wrapf(err, "sync error with: %s (%s)", obj.Path, p)
|
||||
}
|
||||
if !result.Succeeded {
|
||||
if obj.fs.Debug {
|
||||
log.Printf("debug: data already exists in storage")
|
||||
}
|
||||
}
|
||||
|
||||
if err := obj.fs.sync(); err != nil { // push metadata up to server
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Truncate trims the file to the requested size. Since our file system can only
|
||||
// read and write data, but never edit existing data blocks, doing this will not
|
||||
// cause more space to be available.
|
||||
func (obj *File) Truncate(size int64) error {
|
||||
if obj.closed {
|
||||
return ErrFileClosed
|
||||
}
|
||||
if obj.readOnly {
|
||||
return &os.PathError{Op: "truncate", Path: obj.Path, Err: ErrFileReadOnly}
|
||||
}
|
||||
if size < 0 {
|
||||
return ErrOutOfRange
|
||||
}
|
||||
|
||||
if size > 0 { // if size == 0, we don't need to run cache!
|
||||
// download file contents into obj.data
|
||||
if err := obj.cache(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if size > int64(len(obj.data)) {
|
||||
diff := size - int64(len(obj.data))
|
||||
obj.data = append(obj.data, bytes.Repeat([]byte{00}, int(diff))...)
|
||||
} else {
|
||||
obj.data = obj.data[0:size]
|
||||
}
|
||||
|
||||
h, err := obj.fs.hash(obj.data) // update hash
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
obj.Hash = h
|
||||
obj.ModTime = time.Now()
|
||||
|
||||
// this pushes the new data and metadata up to etcd
|
||||
return obj.Sync()
|
||||
}
|
||||
|
||||
// Read reads up to len(b) bytes from the File. It returns the number of bytes
|
||||
// read and any error encountered. At end of file, Read returns 0, io.EOF.
|
||||
// NOTE: This reads into the byte input. It's a side effect!
|
||||
func (obj *File) Read(b []byte) (n int, err error) {
|
||||
if obj.closed {
|
||||
return 0, ErrFileClosed
|
||||
}
|
||||
if obj.Mode.IsDir() {
|
||||
return 0, fmt.Errorf("file is a directory")
|
||||
}
|
||||
|
||||
// download file contents into obj.data
|
||||
if err := obj.cache(); err != nil {
|
||||
return 0, err // TODO: -1 ?
|
||||
}
|
||||
|
||||
// TODO: can we optimize by reading just the length from etcd, and also
|
||||
// by only downloading the data range we're interested in?
|
||||
if len(b) > 0 && int(obj.cursor) == len(obj.data) {
|
||||
return 0, io.EOF
|
||||
}
|
||||
if len(obj.data)-int(obj.cursor) >= len(b) {
|
||||
n = len(b)
|
||||
} else {
|
||||
n = len(obj.data) - int(obj.cursor)
|
||||
}
|
||||
copy(b, obj.data[obj.cursor:obj.cursor+int64(n)]) // store into input b
|
||||
obj.cursor = obj.cursor + int64(n) // update cursor
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// ReadAt reads len(b) bytes from the File starting at byte offset off. It
|
||||
// returns the number of bytes read and the error, if any. ReadAt always returns
|
||||
// a non-nil error when n < len(b). At end of file, that error is io.EOF.
|
||||
func (obj *File) ReadAt(b []byte, off int64) (n int, err error) {
|
||||
obj.cursor = off
|
||||
return obj.Read(b)
|
||||
}
|
||||
|
||||
// Readdir lists the contents of the directory and returns a list of file info
|
||||
// objects for each entry.
|
||||
func (obj *File) Readdir(count int) ([]os.FileInfo, error) {
|
||||
if !obj.Mode.IsDir() {
|
||||
return nil, &os.PathError{Op: "readdir", Path: obj.Name(), Err: syscall.ENOTDIR}
|
||||
}
|
||||
|
||||
children := obj.Children[obj.dirCursor:] // available children to output
|
||||
var l = int64(len(children)) // initially assume to return them all
|
||||
var err error
|
||||
|
||||
// for count > 0, if we return the last entry, also return io.EOF
|
||||
if count > 0 {
|
||||
l = int64(count) // initial assumption
|
||||
if c := len(children); count >= c {
|
||||
l = int64(c)
|
||||
err = io.EOF // this result includes the last dir entry
|
||||
}
|
||||
}
|
||||
obj.dirCursor += l // store our progress
|
||||
|
||||
output := make([]os.FileInfo, l)
|
||||
// TODO: should this be sorted by "directory order" what does that mean?
|
||||
// from `man 3 readdir`: "unlikely that the names will be sorted"
|
||||
for i := range output {
|
||||
output[i] = &FileInfo{
|
||||
file: children[i],
|
||||
}
|
||||
}
|
||||
|
||||
// we're seen the whole directory, so reset the cursor
|
||||
if err == io.EOF || count <= 0 {
|
||||
obj.dirCursor = 0 // TODO: is it okay to reset the cursor?
|
||||
}
|
||||
|
||||
return output, err
|
||||
}
|
||||
|
||||
// Readdirnames returns a list of name is the current file handle's directory.
|
||||
// TODO: this implementation shares the dirCursor with Readdir, is this okay?
|
||||
// TODO: should Readdirnames even use a dirCursor at all?
|
||||
func (obj *File) Readdirnames(n int) (names []string, _ error) {
|
||||
fis, err := obj.Readdir(n)
|
||||
if fis != nil {
|
||||
for i, x := range fis {
|
||||
if x != nil {
|
||||
names = append(names, fis[i].Name())
|
||||
}
|
||||
}
|
||||
}
|
||||
return names, err
|
||||
}
|
||||
|
||||
// Seek sets the offset for the next Read or Write on file to offset,
|
||||
// interpreted according to whence: 0 means relative to the origin of the file,
|
||||
// 1 means relative to the current offset, and 2 means relative to the end. It
|
||||
// returns the new offset and an error, if any. The behavior of Seek on a file
|
||||
// opened with O_APPEND is not specified.
|
||||
func (obj *File) Seek(offset int64, whence int) (int64, error) {
|
||||
if obj.closed {
|
||||
return 0, ErrFileClosed
|
||||
}
|
||||
|
||||
switch whence {
|
||||
case io.SeekStart: // 0
|
||||
obj.cursor = offset
|
||||
case io.SeekCurrent: // 1
|
||||
obj.cursor += offset
|
||||
case io.SeekEnd: // 2
|
||||
// download file contents into obj.data
|
||||
if err := obj.cache(); err != nil {
|
||||
return 0, err // TODO: -1 ?
|
||||
}
|
||||
obj.cursor = int64(len(obj.data)) + offset
|
||||
}
|
||||
return obj.cursor, nil
|
||||
}
|
||||
|
||||
// Write writes to the given file.
|
||||
func (obj *File) Write(b []byte) (n int, err error) {
|
||||
if obj.closed {
|
||||
return 0, ErrFileClosed
|
||||
}
|
||||
if obj.readOnly {
|
||||
return 0, &os.PathError{Op: "write", Path: obj.Path, Err: ErrFileReadOnly}
|
||||
}
|
||||
|
||||
// download file contents into obj.data
|
||||
if err := obj.cache(); err != nil {
|
||||
return 0, err // TODO: -1 ?
|
||||
}
|
||||
|
||||
// calculate the write
|
||||
n = len(b)
|
||||
cur := obj.cursor
|
||||
diff := cur - int64(len(obj.data))
|
||||
|
||||
var tail []byte
|
||||
if n+int(cur) < len(obj.data) {
|
||||
tail = obj.data[n+int(cur):]
|
||||
}
|
||||
|
||||
if diff > 0 {
|
||||
obj.data = append(bytes.Repeat([]byte{00}, int(diff)), b...)
|
||||
obj.data = append(obj.data, tail...)
|
||||
} else {
|
||||
obj.data = append(obj.data[:cur], b...)
|
||||
obj.data = append(obj.data, tail...)
|
||||
}
|
||||
|
||||
h, err := obj.fs.hash(obj.data) // update hash
|
||||
if err != nil {
|
||||
return 0, err // TODO: -1 ?
|
||||
}
|
||||
obj.Hash = h
|
||||
obj.ModTime = time.Now()
|
||||
|
||||
// this pushes the new data and metadata up to etcd
|
||||
if err := obj.Sync(); err != nil {
|
||||
return 0, err // TODO: -1 ?
|
||||
}
|
||||
|
||||
obj.cursor = int64(len(obj.data))
|
||||
return
|
||||
}
|
||||
|
||||
// WriteAt writes into the given file at a certain offset.
|
||||
func (obj *File) WriteAt(b []byte, off int64) (n int, err error) {
|
||||
obj.cursor = off
|
||||
return obj.Write(b)
|
||||
}
|
||||
|
||||
// WriteString writes a string to the file.
|
||||
func (obj *File) WriteString(s string) (n int, err error) {
|
||||
return obj.Write([]byte(s))
|
||||
}
|
||||
|
||||
// FileInfo is a struct which provides some information about a file handle.
|
||||
type FileInfo struct {
|
||||
file *File // anonymous pointer to the actual file
|
||||
}
|
||||
|
||||
// Name returns the base name of the file.
|
||||
func (obj *FileInfo) Name() string {
|
||||
return obj.file.Name()
|
||||
}
|
||||
|
||||
// Size returns the length in bytes.
|
||||
func (obj *FileInfo) Size() int64 {
|
||||
return int64(len(obj.file.data))
|
||||
}
|
||||
|
||||
// Mode returns the file mode bits.
|
||||
func (obj *FileInfo) Mode() os.FileMode {
|
||||
return obj.file.Mode
|
||||
}
|
||||
|
||||
// ModTime returns the modification time.
|
||||
func (obj *FileInfo) ModTime() time.Time {
|
||||
return obj.file.ModTime
|
||||
}
|
||||
|
||||
// IsDir is an abbreviation for Mode().IsDir().
|
||||
func (obj *FileInfo) IsDir() bool {
|
||||
//return obj.file.Mode&os.ModeDir != 0
|
||||
return obj.file.Mode.IsDir()
|
||||
}
|
||||
|
||||
// Sys returns the underlying data source (can return nil).
|
||||
func (obj *FileInfo) Sys() interface{} {
|
||||
return nil // TODO: should we do something better?
|
||||
//return obj.file.fs // TODO: would this work?
|
||||
}
|
||||
821
etcd/fs/fs.go
Normal file
@@ -0,0 +1,821 @@
|
||||
// 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 fs implements a very simple and limited file system on top of etcd.
|
||||
package fs
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/gob"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
etcd "github.com/coreos/etcd/clientv3" // "clientv3"
|
||||
rpctypes "github.com/coreos/etcd/etcdserver/api/v3rpc/rpctypes"
|
||||
errwrap "github.com/pkg/errors"
|
||||
"github.com/spf13/afero"
|
||||
context "golang.org/x/net/context"
|
||||
)
|
||||
|
||||
func init() {
|
||||
gob.Register(&superBlock{})
|
||||
}
|
||||
|
||||
const (
|
||||
// EtcdTimeout is the timeout to wait before erroring.
|
||||
EtcdTimeout = 5 * time.Second // FIXME: chosen arbitrarily
|
||||
// DefaultDataPrefix is the default path for data storage in etcd.
|
||||
DefaultDataPrefix = "/_etcdfs/data"
|
||||
// DefaultHash is the default hashing algorithm to use.
|
||||
DefaultHash = "sha256"
|
||||
// PathSeparator is the path separator to use on this filesystem.
|
||||
PathSeparator = os.PathSeparator // usually the slash character
|
||||
)
|
||||
|
||||
// TODO: https://dave.cheney.net/2016/04/07/constant-errors
|
||||
var (
|
||||
IsPathSeparator = os.IsPathSeparator
|
||||
|
||||
// ErrNotImplemented is returned when something is not implemented by design.
|
||||
ErrNotImplemented = errors.New("not implemented")
|
||||
|
||||
// ErrExist is returned when requested path already exists.
|
||||
ErrExist = os.ErrExist
|
||||
|
||||
// ErrNotExist is returned when we can't find the requested path.
|
||||
ErrNotExist = os.ErrNotExist
|
||||
|
||||
ErrFileClosed = errors.New("File is closed")
|
||||
ErrFileReadOnly = errors.New("File handle is read only")
|
||||
ErrOutOfRange = errors.New("Out of range")
|
||||
)
|
||||
|
||||
// Fs is a specialized afero.Fs implementation for etcd. It implements a small
|
||||
// subset of the features, and has some special properties. In particular, file
|
||||
// data is stored with it's unique reference being a hash of the data. In this
|
||||
// way, you cannot actually edit a file, but rather you create a new one, and
|
||||
// update the metadata pointer to point to the new blob. This might seem slow,
|
||||
// but it has the unique advantage of being relatively straight forward to
|
||||
// implement, and repeated uploads of the same file cost almost nothing. Since
|
||||
// etcd isn't meant for large file systems, this fits the desired use case.
|
||||
// This implementation is designed to have a single writer for each superblock,
|
||||
// but as many readers as you like.
|
||||
// FIXME: this is not currently thread-safe, nor is it clear if it needs to be.
|
||||
// XXX: we probably aren't updating the modification time everywhere we should!
|
||||
// XXX: because we never delete data blocks, we need to occasionally "vacuum".
|
||||
// XXX: this is harder because we need to list of *all* metadata paths, if we
|
||||
// want them to be able to share storage backends. (we do)
|
||||
type Fs struct {
|
||||
Client *etcd.Client
|
||||
|
||||
Metadata string // location of "superblock" for this filesystem
|
||||
|
||||
DataPrefix string // prefix of data storage (no trailing slashes)
|
||||
Hash string // eg: sha256
|
||||
|
||||
Debug bool
|
||||
|
||||
sb *superBlock
|
||||
mounted bool
|
||||
}
|
||||
|
||||
// superBlock is the metadata structure of everything stored outside of the data
|
||||
// section in etcd. Its fields need to be exported or they won't get marshalled.
|
||||
type superBlock struct {
|
||||
DataPrefix string // prefix of data storage
|
||||
Hash string // hashing algorithm used
|
||||
|
||||
Tree *File // filesystem tree
|
||||
}
|
||||
|
||||
// NewEtcdFs creates a new filesystem handle on an etcd client connection. You
|
||||
// must specify the metadata string that you wish to use.
|
||||
func NewEtcdFs(client *etcd.Client, metadata string) afero.Fs {
|
||||
return &Fs{
|
||||
Client: client,
|
||||
Metadata: metadata,
|
||||
}
|
||||
}
|
||||
|
||||
// get a number of values from etcd.
|
||||
func (obj *Fs) get(path string, opts ...etcd.OpOption) (map[string][]byte, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), EtcdTimeout)
|
||||
resp, err := obj.Client.Get(ctx, path, opts...)
|
||||
cancel()
|
||||
if err != nil || resp == nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// TODO: write a resp.ToMap() function on https://godoc.org/github.com/coreos/etcd/etcdserver/etcdserverpb#RangeResponse
|
||||
result := make(map[string][]byte) // formerly: map[string][]byte
|
||||
for _, x := range resp.Kvs {
|
||||
result[string(x.Key)] = x.Value // formerly: bytes.NewBuffer(x.Value).String()
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// put a value into etcd.
|
||||
func (obj *Fs) put(path string, data []byte, opts ...etcd.OpOption) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), EtcdTimeout)
|
||||
_, err := obj.Client.Put(ctx, path, string(data), opts...) // TODO: obj.Client.KV ?
|
||||
cancel()
|
||||
if err != nil {
|
||||
switch err {
|
||||
case context.Canceled:
|
||||
return errwrap.Wrapf(err, "ctx canceled")
|
||||
case context.DeadlineExceeded:
|
||||
return errwrap.Wrapf(err, "ctx deadline exceeded")
|
||||
case rpctypes.ErrEmptyKey:
|
||||
return errwrap.Wrapf(err, "client-side error")
|
||||
default:
|
||||
return errwrap.Wrapf(err, "invalid endpoints")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// txn runs a txn in etcd.
|
||||
func (obj *Fs) txn(ifcmps []etcd.Cmp, thenops, elseops []etcd.Op) (*etcd.TxnResponse, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), EtcdTimeout)
|
||||
resp, err := obj.Client.Txn(ctx).If(ifcmps...).Then(thenops...).Else(elseops...).Commit()
|
||||
cancel()
|
||||
return resp, err
|
||||
}
|
||||
|
||||
// hash is a small helper that does the hashing for us.
|
||||
func (obj *Fs) hash(input []byte) (string, error) {
|
||||
var h hash.Hash
|
||||
switch obj.Hash {
|
||||
// TODO: add other hashes
|
||||
case "sha256":
|
||||
h = sha256.New()
|
||||
default:
|
||||
return "", fmt.Errorf("hash does not exist")
|
||||
}
|
||||
src := bytes.NewReader(input)
|
||||
if _, err := io.Copy(h, src); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return hex.EncodeToString(h.Sum(nil)), nil
|
||||
}
|
||||
|
||||
// sync overwrites the superblock with whatever version we have stored.
|
||||
func (obj *Fs) sync() error {
|
||||
b := bytes.Buffer{}
|
||||
e := gob.NewEncoder(&b)
|
||||
err := e.Encode(&obj.sb) // pass with &
|
||||
if err != nil {
|
||||
return errwrap.Wrapf(err, "gob failed to encode")
|
||||
}
|
||||
//base64.StdEncoding.EncodeToString(b.Bytes())
|
||||
return obj.put(obj.Metadata, b.Bytes())
|
||||
}
|
||||
|
||||
// mount downloads the initial cache of metadata, including the *file tree.
|
||||
// Since there's no explicit mount API in the afero.Fs interface, we hide this
|
||||
// method inside any operation that might do any real work, and make it
|
||||
// idempotent so that it can be called as much as we want. If there's no
|
||||
// metadata found (superblock) then we create one.
|
||||
func (obj *Fs) mount() error {
|
||||
if obj.mounted {
|
||||
return nil
|
||||
}
|
||||
|
||||
result, err := obj.get(obj.Metadata) // download the metadata...
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if result == nil || len(result) == 0 { // nothing found, create the fs
|
||||
if obj.Debug {
|
||||
log.Printf("debug: mount: creating new fs at: %s", obj.Metadata)
|
||||
}
|
||||
// trim any trailing slashes from DataPrefix
|
||||
for strings.HasSuffix(obj.DataPrefix, "/") {
|
||||
obj.DataPrefix = strings.TrimSuffix(obj.DataPrefix, "/")
|
||||
}
|
||||
if obj.DataPrefix == "" {
|
||||
obj.DataPrefix = DefaultDataPrefix
|
||||
}
|
||||
if obj.Hash == "" {
|
||||
obj.Hash = DefaultHash
|
||||
}
|
||||
// test run an empty string to see if our hash selection works!
|
||||
if _, err := obj.hash([]byte("")); err != nil {
|
||||
return fmt.Errorf("cannot hash with %s", obj.Hash)
|
||||
}
|
||||
|
||||
obj.sb = &superBlock{
|
||||
DataPrefix: obj.DataPrefix,
|
||||
Hash: obj.Hash,
|
||||
Tree: &File{ // include a root directory
|
||||
fs: obj,
|
||||
Path: "", // root dir is "" (empty string)
|
||||
Mode: os.ModeDir,
|
||||
},
|
||||
}
|
||||
if err := obj.sync(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
obj.mounted = true
|
||||
return nil
|
||||
}
|
||||
|
||||
if obj.Debug {
|
||||
log.Printf("debug: mount: opening old fs at: %s", obj.Metadata)
|
||||
}
|
||||
sb, exists := result[obj.Metadata]
|
||||
if !exists {
|
||||
return fmt.Errorf("could not find metadata") // programming error?
|
||||
}
|
||||
|
||||
// decode into obj.sb
|
||||
//bb, err := base64.StdEncoding.DecodeString(str)
|
||||
//if err != nil {
|
||||
// return errwrap.Wrapf(err, "base64 failed to decode")
|
||||
//}
|
||||
//b := bytes.NewBuffer(bb)
|
||||
b := bytes.NewBuffer(sb)
|
||||
d := gob.NewDecoder(b)
|
||||
if err := d.Decode(&obj.sb); err != nil { // pass with &
|
||||
return errwrap.Wrapf(err, "gob failed to decode")
|
||||
}
|
||||
|
||||
if obj.DataPrefix != "" && obj.DataPrefix != obj.sb.DataPrefix {
|
||||
return fmt.Errorf("the DataPrefix mount option `%s` does not match the remote value of `%s`", obj.DataPrefix, obj.sb.DataPrefix)
|
||||
}
|
||||
if obj.Hash != "" && obj.Hash != obj.sb.Hash {
|
||||
return fmt.Errorf("the Hash mount option `%s` does not match the remote value of `%s`", obj.Hash, obj.sb.Hash)
|
||||
}
|
||||
// if all checks passed, copy these values down locally
|
||||
obj.DataPrefix = obj.sb.DataPrefix
|
||||
obj.Hash = obj.sb.Hash
|
||||
|
||||
// hook up file system pointers to each element in the tree structure
|
||||
obj.traverse(obj.sb.Tree)
|
||||
|
||||
obj.mounted = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// traverse adds the file system pointer to each element in the tree structure.
|
||||
func (obj *Fs) traverse(node *File) {
|
||||
if node == nil {
|
||||
return
|
||||
}
|
||||
node.fs = obj
|
||||
for _, n := range node.Children {
|
||||
obj.traverse(n)
|
||||
}
|
||||
}
|
||||
|
||||
// find returns the file node corresponding to this absolute path if it exists.
|
||||
func (obj *Fs) find(absPath string) (*File, error) { // TODO: function naming?
|
||||
if absPath == "" {
|
||||
return nil, fmt.Errorf("empty path specified")
|
||||
}
|
||||
if !strings.HasPrefix(absPath, "/") {
|
||||
return nil, fmt.Errorf("invalid input path (not absolute)")
|
||||
}
|
||||
|
||||
node := obj.sb.Tree
|
||||
if node == nil {
|
||||
return nil, ErrNotExist // no nodes exist yet, not even root dir
|
||||
}
|
||||
|
||||
var x string // first value
|
||||
sp := PathSplit(absPath)
|
||||
if x, sp = sp[0], sp[1:]; x != node.Path {
|
||||
return nil, fmt.Errorf("root values do not match") // TODO: panic?
|
||||
}
|
||||
|
||||
for _, p := range sp {
|
||||
n, exists := node.findNode(p)
|
||||
if !exists {
|
||||
return nil, ErrNotExist
|
||||
}
|
||||
node = n // descend into this node
|
||||
}
|
||||
|
||||
return node, nil
|
||||
}
|
||||
|
||||
// Name returns the name of this filesystem.
|
||||
func (obj *Fs) Name() string { return "etcdfs" }
|
||||
|
||||
// URI returns a URI representing this particular filesystem.
|
||||
func (obj *Fs) URI() string {
|
||||
return fmt.Sprintf("%s://%s", obj.Name(), obj.Metadata)
|
||||
}
|
||||
|
||||
// Create creates a new file.
|
||||
func (obj *Fs) Create(name string) (afero.File, error) {
|
||||
if err := obj.mount(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return fileCreate(obj, name)
|
||||
}
|
||||
|
||||
// Mkdir makes a new directory.
|
||||
func (obj *Fs) Mkdir(name string, perm os.FileMode) error {
|
||||
if err := obj.mount(); err != nil {
|
||||
return err
|
||||
}
|
||||
if name == "" {
|
||||
return fmt.Errorf("invalid input path")
|
||||
}
|
||||
if !strings.HasPrefix(name, "/") {
|
||||
return fmt.Errorf("invalid input path (not absolute)")
|
||||
}
|
||||
|
||||
// remove possible trailing slashes
|
||||
cleanPath := path.Clean(name)
|
||||
|
||||
for strings.HasSuffix(cleanPath, "/") { // bonus clean for "/" as input
|
||||
cleanPath = strings.TrimSuffix(cleanPath, "/")
|
||||
}
|
||||
|
||||
if cleanPath == "" {
|
||||
if obj.sb.Tree == nil {
|
||||
return fmt.Errorf("woops, missing root directory")
|
||||
}
|
||||
return ErrExist // root directory already exists
|
||||
}
|
||||
|
||||
// try to add node to tree by first finding the parent node
|
||||
parentPath, dirPath := path.Split(cleanPath) // looking for this
|
||||
|
||||
f := &File{
|
||||
fs: obj,
|
||||
Path: dirPath,
|
||||
Mode: os.ModeDir,
|
||||
// TODO: add perm to struct or let chmod below do it
|
||||
}
|
||||
|
||||
node, err := obj.find(parentPath)
|
||||
if err != nil { // might be ErrNotExist
|
||||
return err
|
||||
}
|
||||
|
||||
fi, err := node.Stat()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !fi.IsDir() { // is the parent a suitable home?
|
||||
return &os.PathError{Op: "mkdir", Path: name, Err: syscall.ENOTDIR}
|
||||
}
|
||||
|
||||
_, exists := node.findNode(dirPath) // does file already exist inside?
|
||||
if exists {
|
||||
return ErrExist
|
||||
}
|
||||
|
||||
// add to parent
|
||||
node.Children = append(node.Children, f)
|
||||
|
||||
// push new file up if not on server, and then push up the metadata
|
||||
if err := f.Sync(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return obj.Chmod(name, perm)
|
||||
}
|
||||
|
||||
// MkdirAll creates a directory named path, along with any necessary parents,
|
||||
// and returns nil, or else returns an error. The permission bits perm are used
|
||||
// for all directories that MkdirAll creates. If path is already a directory,
|
||||
// MkdirAll does nothing and returns nil.
|
||||
func (obj *Fs) MkdirAll(path string, perm os.FileMode) error {
|
||||
if err := obj.mount(); err != nil {
|
||||
return err
|
||||
}
|
||||
// Copied mostly verbatim from golang stdlib.
|
||||
// Fast path: if we can tell whether path is a directory or file, stop
|
||||
// with success or error.
|
||||
dir, err := obj.Stat(path)
|
||||
if err == nil {
|
||||
if dir.IsDir() {
|
||||
return nil
|
||||
}
|
||||
return &os.PathError{Op: "mkdir", Path: path, Err: syscall.ENOTDIR}
|
||||
}
|
||||
|
||||
// Slow path: make sure parent exists and then call Mkdir for path.
|
||||
i := len(path)
|
||||
for i > 0 && IsPathSeparator(path[i-1]) { // Skip trailing path separator.
|
||||
i--
|
||||
}
|
||||
|
||||
j := i
|
||||
for j > 0 && !IsPathSeparator(path[j-1]) { // Scan backward over element.
|
||||
j--
|
||||
}
|
||||
|
||||
if j > 1 {
|
||||
// Create parent
|
||||
err = obj.MkdirAll(path[0:j-1], perm)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Parent now exists; invoke Mkdir and use its result.
|
||||
err = obj.Mkdir(path, perm)
|
||||
if err != nil {
|
||||
// Handle arguments like "foo/." by
|
||||
// double-checking that directory doesn't exist.
|
||||
dir, err1 := obj.Lstat(path)
|
||||
if err1 == nil && dir.IsDir() {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Open opens a path. It will be opened read-only.
|
||||
func (obj *Fs) Open(name string) (afero.File, error) {
|
||||
if err := obj.mount(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return fileOpen(obj, name) // this opens as read-only
|
||||
}
|
||||
|
||||
// OpenFile opens a path with a particular flag and permission.
|
||||
func (obj *Fs) OpenFile(name string, flag int, perm os.FileMode) (afero.File, error) {
|
||||
if err := obj.mount(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
chmod := false
|
||||
f, err := fileOpen(obj, name)
|
||||
if os.IsNotExist(err) && (flag&os.O_CREATE > 0) {
|
||||
f, err = fileCreate(obj, name)
|
||||
chmod = true
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
f.readOnly = (flag == os.O_RDONLY)
|
||||
|
||||
if flag&os.O_APPEND > 0 {
|
||||
if _, err := f.Seek(0, os.SEEK_END); err != nil {
|
||||
f.Close()
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if flag&os.O_TRUNC > 0 && flag&(os.O_RDWR|os.O_WRONLY) > 0 {
|
||||
if err := f.Truncate(0); err != nil {
|
||||
f.Close()
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if chmod {
|
||||
// TODO: the golang stdlib doesn't check this error, should we?
|
||||
if err := obj.Chmod(name, perm); err != nil {
|
||||
return f, err // TODO: should we return the file handle?
|
||||
}
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
|
||||
// Remove removes a path.
|
||||
func (obj *Fs) Remove(name string) error {
|
||||
if err := obj.mount(); err != nil {
|
||||
return err
|
||||
}
|
||||
if name == "" {
|
||||
return fmt.Errorf("invalid input path")
|
||||
}
|
||||
if !strings.HasPrefix(name, "/") {
|
||||
return fmt.Errorf("invalid input path (not absolute)")
|
||||
}
|
||||
|
||||
// remove possible trailing slashes
|
||||
cleanPath := path.Clean(name)
|
||||
|
||||
for strings.HasSuffix(cleanPath, "/") { // bonus clean for "/" as input
|
||||
cleanPath = strings.TrimSuffix(cleanPath, "/")
|
||||
}
|
||||
|
||||
if cleanPath == "" {
|
||||
return fmt.Errorf("can't remove root")
|
||||
}
|
||||
|
||||
f, err := obj.find(name) // get the file
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(f.Children) > 0 { // this file or dir has children, can't remove!
|
||||
return &os.PathError{Op: "remove", Path: name, Err: syscall.ENOTEMPTY}
|
||||
}
|
||||
|
||||
// find the parent node
|
||||
parentPath, filePath := path.Split(cleanPath) // looking for this
|
||||
|
||||
node, err := obj.find(parentPath)
|
||||
if err != nil { // might be ErrNotExist
|
||||
if os.IsNotExist(err) { // race! must have just disappeared
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
var index = -1 // int
|
||||
for i, n := range node.Children {
|
||||
if n.Path == filePath {
|
||||
index = i // found here!
|
||||
break
|
||||
}
|
||||
}
|
||||
if index == -1 {
|
||||
return fmt.Errorf("programming error")
|
||||
}
|
||||
// remove from list
|
||||
node.Children = append(node.Children[:index], node.Children[index+1:]...)
|
||||
return obj.sync()
|
||||
}
|
||||
|
||||
// RemoveAll removes path and any children it contains. It removes everything it
|
||||
// can but returns the first error it encounters. If the path does not exist,
|
||||
// RemoveAll returns nil (no error).
|
||||
func (obj *Fs) RemoveAll(path string) error {
|
||||
if err := obj.mount(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Simple case: if Remove works, we're done.
|
||||
err := obj.Remove(path)
|
||||
if err == nil || os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Otherwise, is this a directory we need to recurse into?
|
||||
dir, serr := obj.Lstat(path)
|
||||
if serr != nil {
|
||||
// TODO: I didn't check this logic thoroughly (edge cases?)
|
||||
if serr, ok := serr.(*os.PathError); ok && (os.IsNotExist(serr.Err) || serr.Err == syscall.ENOTDIR) {
|
||||
return nil
|
||||
}
|
||||
return serr
|
||||
}
|
||||
if !dir.IsDir() {
|
||||
// Not a directory; return the error from Remove.
|
||||
return err
|
||||
}
|
||||
|
||||
// Directory.
|
||||
fd, err := obj.Open(path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
// Race. It was deleted between the Lstat and Open.
|
||||
// Return nil per RemoveAll's docs.
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Remove contents & return first error.
|
||||
err = nil
|
||||
for {
|
||||
// TODO: why not do this in one shot? is there a syscall limit?
|
||||
names, err1 := fd.Readdirnames(100)
|
||||
for _, name := range names {
|
||||
err1 := obj.RemoveAll(path + string(PathSeparator) + name)
|
||||
if err == nil {
|
||||
err = err1
|
||||
}
|
||||
}
|
||||
if err1 == io.EOF {
|
||||
break
|
||||
}
|
||||
// If Readdirnames returned an error, use it.
|
||||
if err == nil {
|
||||
err = err1
|
||||
}
|
||||
if len(names) == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Close directory, because windows won't remove opened directory.
|
||||
fd.Close()
|
||||
|
||||
// Remove directory.
|
||||
err1 := obj.Remove(path)
|
||||
if err1 == nil || os.IsNotExist(err1) {
|
||||
return nil
|
||||
}
|
||||
if err == nil {
|
||||
err = err1
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Rename moves or renames a file or directory.
|
||||
// TODO: seems it's okay to move files or directories, but you can't clobber dirs
|
||||
// but you can clobber single files. a dir can't clobber a file and a file can't
|
||||
// clobber a dir. but a file can clobber another file but a dir can't clobber
|
||||
// another dir. you can also transplant dirs or files into other dirs.
|
||||
func (obj *Fs) Rename(oldname, newname string) error {
|
||||
// XXX: do we need to check if dest path is inside src path?
|
||||
// XXX: if dirs/files are next to each other, do we mess up the .Children list of the common parent?
|
||||
if err := obj.mount(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if oldname == newname {
|
||||
return nil
|
||||
}
|
||||
if oldname == "" || newname == "" {
|
||||
return fmt.Errorf("invalid input path")
|
||||
}
|
||||
if !strings.HasPrefix(oldname, "/") || !strings.HasPrefix(newname, "/") {
|
||||
return fmt.Errorf("invalid input path (not absolute)")
|
||||
}
|
||||
|
||||
// remove possible trailing slashes
|
||||
srcCleanPath := path.Clean(oldname)
|
||||
dstCleanPath := path.Clean(newname)
|
||||
|
||||
src, err := obj.find(srcCleanPath) // get the file
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
srcInfo, err := src.Stat()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
srcParentPath, srcName := path.Split(srcCleanPath) // looking for this
|
||||
parent, err := obj.find(srcParentPath)
|
||||
if err != nil { // might be ErrNotExist
|
||||
return err
|
||||
}
|
||||
var rmi = -1 // index of node to remove from parent
|
||||
// find the thing to be deleted
|
||||
for i, n := range parent.Children {
|
||||
if n.Path == srcName {
|
||||
rmi = i // found here!
|
||||
break
|
||||
}
|
||||
}
|
||||
if rmi == -1 {
|
||||
return fmt.Errorf("programming error")
|
||||
}
|
||||
|
||||
dst, err := obj.find(dstCleanPath) // does the destination already exist?
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
if err == nil { // dst exists!
|
||||
dstInfo, err := dst.Stat()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// dir's can clobber anything or be clobbered apparently
|
||||
if srcInfo.IsDir() || dstInfo.IsDir() {
|
||||
return ErrExist // dir's can't clobber anything
|
||||
}
|
||||
|
||||
// remove from list by index
|
||||
parent.Children = append(parent.Children[:rmi], parent.Children[rmi+1:]...)
|
||||
|
||||
// we're a file clobbering another file...
|
||||
// move file content from src -> dst and then delete src
|
||||
// TODO: run a dst.Close() for extra safety first?
|
||||
save := dst.Path // save the "name"
|
||||
*dst = *src // TODO: is this safe?
|
||||
dst.Path = save // "rename" it
|
||||
|
||||
} else { // dst does not exist
|
||||
|
||||
// check if the dst's parent exists and is a dir, if not, error
|
||||
// if it is a dir, add src as a child to it and then delete src
|
||||
dstParentPath, dstName := path.Split(dstCleanPath) // looking for this
|
||||
node, err := obj.find(dstParentPath)
|
||||
if err != nil { // might be ErrNotExist
|
||||
return err
|
||||
}
|
||||
fi, err := node.Stat()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !fi.IsDir() { // is the parent a suitable home?
|
||||
return &os.LinkError{Op: "rename", Old: oldname, New: newname, Err: syscall.ENOTDIR}
|
||||
}
|
||||
|
||||
// remove from list by index
|
||||
parent.Children = append(parent.Children[:rmi], parent.Children[rmi+1:]...)
|
||||
|
||||
src.Path = dstName // "rename" it
|
||||
node.Children = append(node.Children, src) // "copied"
|
||||
}
|
||||
|
||||
return obj.sync() // push up metadata changes
|
||||
}
|
||||
|
||||
// Stat returns some information about the particular path.
|
||||
func (obj *Fs) Stat(name string) (os.FileInfo, error) {
|
||||
if err := obj.mount(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !strings.HasPrefix(name, "/") {
|
||||
return nil, fmt.Errorf("invalid input path (not absolute)")
|
||||
}
|
||||
|
||||
f, err := obj.find(name) // get the file
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return f.Stat()
|
||||
}
|
||||
|
||||
// Lstat does exactly the same as Stat because we currently do not support
|
||||
// symbolic links.
|
||||
func (obj *Fs) Lstat(name string) (os.FileInfo, error) {
|
||||
if err := obj.mount(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// TODO: we don't have symbolic links in our fs, so we pass this to stat
|
||||
return obj.Stat(name)
|
||||
}
|
||||
|
||||
// Chmod changes the mode of a file.
|
||||
func (obj *Fs) Chmod(name string, mode os.FileMode) error {
|
||||
if err := obj.mount(); err != nil {
|
||||
return err
|
||||
}
|
||||
if !strings.HasPrefix(name, "/") {
|
||||
return fmt.Errorf("invalid input path (not absolute)")
|
||||
}
|
||||
|
||||
f, err := obj.find(name) // get the file
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
f.Mode = f.Mode | mode // XXX: what is the correct way to do this?
|
||||
return f.Sync() // push up the changed metadata
|
||||
}
|
||||
|
||||
// Chtimes changes the access and modification times of the named file, similar
|
||||
// to the Unix utime() or utimes() functions. The underlying filesystem may
|
||||
// truncate or round the values to a less precise time unit. If there is an
|
||||
// error, it will be of type *PathError.
|
||||
// FIXME: make sure everything we error is a *PathError
|
||||
// TODO: atime is not currently implement and so it is silently ignored.
|
||||
func (obj *Fs) Chtimes(name string, atime time.Time, mtime time.Time) error {
|
||||
if err := obj.mount(); err != nil {
|
||||
return err
|
||||
}
|
||||
if !strings.HasPrefix(name, "/") {
|
||||
return fmt.Errorf("invalid input path (not absolute)")
|
||||
}
|
||||
|
||||
f, err := obj.find(name) // get the file
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
f.ModTime = mtime
|
||||
// TODO: add atime
|
||||
return f.Sync() // push up the changed metadata
|
||||
}
|
||||
|
||||
// PathSplit splits a path into an array of tokens excluding any trailing empty
|
||||
// tokens.
|
||||
func PathSplit(p string) []string {
|
||||
if p == "/" { // TODO: can't this all be expressed nicely in one line?
|
||||
return []string{""}
|
||||
}
|
||||
return strings.Split(path.Clean(p), "/")
|
||||
}
|
||||
227
etcd/fs/fs_test.go
Normal file
@@ -0,0 +1,227 @@
|
||||
// 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 fs_test // named this way to make it easier for examples
|
||||
|
||||
import (
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/purpleidea/mgmt/etcd"
|
||||
etcdfs "github.com/purpleidea/mgmt/etcd/fs"
|
||||
"github.com/purpleidea/mgmt/util"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
// XXX: spawn etcd for this test, like `cdtmpmkdir && etcd` and then kill it...
|
||||
// XXX: write a bunch more tests to test this
|
||||
|
||||
// TODO: apparently using 0666 is equivalent to respecting the current umask
|
||||
const (
|
||||
umask = 0666
|
||||
superblock = "/some/superblock" // TODO: generate randomly per test?
|
||||
)
|
||||
|
||||
func TestFs1(t *testing.T) {
|
||||
etcdClient := &etcd.ClientEtcd{
|
||||
Seeds: []string{"localhost:2379"}, // endpoints
|
||||
}
|
||||
|
||||
if err := etcdClient.Connect(); err != nil {
|
||||
t.Logf("client connection error: %+v", err)
|
||||
return
|
||||
}
|
||||
defer etcdClient.Destroy()
|
||||
|
||||
etcdFs := &etcdfs.Fs{
|
||||
Client: etcdClient.GetClient(),
|
||||
Metadata: superblock,
|
||||
DataPrefix: etcdfs.DefaultDataPrefix,
|
||||
}
|
||||
//var etcdFs afero.Fs = NewEtcdFs()
|
||||
|
||||
if err := etcdFs.Mkdir("/", umask); err != nil {
|
||||
t.Logf("error: %+v", err)
|
||||
if err != etcdfs.ErrExist {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err := etcdFs.Mkdir("/tmp", umask); err != nil {
|
||||
t.Logf("error: %+v", err)
|
||||
if err != etcdfs.ErrExist {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
fi, err := etcdFs.Stat("/tmp")
|
||||
if err != nil {
|
||||
t.Logf("stat error: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
t.Logf("fi: %+v", fi)
|
||||
t.Logf("isdir: %t", fi.IsDir())
|
||||
|
||||
f, err := etcdFs.Create("/tmp/foo")
|
||||
if err != nil {
|
||||
t.Logf("error: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
t.Logf("handle: %+v", f)
|
||||
|
||||
i, err := f.WriteString("hello world!\n")
|
||||
if err != nil {
|
||||
t.Logf("error: %+v", err)
|
||||
return
|
||||
}
|
||||
t.Logf("wrote: %d", i)
|
||||
|
||||
if err := etcdFs.Mkdir("/tmp/d1", umask); err != nil {
|
||||
t.Logf("error: %+v", err)
|
||||
if err != etcdfs.ErrExist {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err := etcdFs.Rename("/tmp/foo", "/tmp/bar"); err != nil {
|
||||
t.Logf("rename error: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
//f2, err := etcdFs.Create("/tmp/bar")
|
||||
//if err != nil {
|
||||
// t.Logf("error: %+v", err)
|
||||
// return
|
||||
//}
|
||||
|
||||
//i2, err := f2.WriteString("hello bar!\n")
|
||||
//if err != nil {
|
||||
// t.Logf("error: %+v", err)
|
||||
// return
|
||||
//}
|
||||
//t.Logf("wrote: %d", i2)
|
||||
|
||||
dir, err := etcdFs.Open("/tmp")
|
||||
if err != nil {
|
||||
t.Logf("error: %+v", err)
|
||||
return
|
||||
}
|
||||
names, err := dir.Readdirnames(-1)
|
||||
if err != nil && err != io.EOF {
|
||||
t.Logf("error: %+v", err)
|
||||
return
|
||||
}
|
||||
for _, name := range names {
|
||||
t.Logf("name in /tmp: %+v", name)
|
||||
}
|
||||
|
||||
//dir, err := etcdFs.Open("/")
|
||||
//if err != nil {
|
||||
// t.Logf("error: %+v", err)
|
||||
// return
|
||||
//}
|
||||
//names, err := dir.Readdirnames(-1)
|
||||
//if err != nil && err != io.EOF {
|
||||
// t.Logf("error: %+v", err)
|
||||
// return
|
||||
//}
|
||||
//for _, name := range names {
|
||||
// t.Logf("name in /: %+v", name)
|
||||
//}
|
||||
}
|
||||
|
||||
func TestFs2(t *testing.T) {
|
||||
etcdClient := &etcd.ClientEtcd{
|
||||
Seeds: []string{"localhost:2379"}, // endpoints
|
||||
}
|
||||
|
||||
if err := etcdClient.Connect(); err != nil {
|
||||
t.Logf("client connection error: %+v", err)
|
||||
return
|
||||
}
|
||||
defer etcdClient.Destroy()
|
||||
|
||||
etcdFs := &etcdfs.Fs{
|
||||
Client: etcdClient.GetClient(),
|
||||
Metadata: superblock,
|
||||
DataPrefix: etcdfs.DefaultDataPrefix,
|
||||
}
|
||||
|
||||
tree, err := util.FsTree(etcdFs, "/")
|
||||
if err != nil {
|
||||
t.Errorf("tree error: %+v", err)
|
||||
return
|
||||
}
|
||||
t.Logf("tree: \n%s", tree)
|
||||
|
||||
tree2, err := util.FsTree(etcdFs, "/tmp")
|
||||
if err != nil {
|
||||
t.Errorf("tree2 error: %+v", err)
|
||||
return
|
||||
}
|
||||
t.Logf("tree2: \n%s", tree2)
|
||||
}
|
||||
|
||||
func TestFs3(t *testing.T) {
|
||||
etcdClient := &etcd.ClientEtcd{
|
||||
Seeds: []string{"localhost:2379"}, // endpoints
|
||||
}
|
||||
|
||||
if err := etcdClient.Connect(); err != nil {
|
||||
t.Logf("client connection error: %+v", err)
|
||||
return
|
||||
}
|
||||
defer etcdClient.Destroy()
|
||||
|
||||
etcdFs := &etcdfs.Fs{
|
||||
Client: etcdClient.GetClient(),
|
||||
Metadata: superblock,
|
||||
DataPrefix: etcdfs.DefaultDataPrefix,
|
||||
}
|
||||
|
||||
tree, err := util.FsTree(etcdFs, "/")
|
||||
if err != nil {
|
||||
t.Errorf("tree error: %+v", err)
|
||||
return
|
||||
}
|
||||
t.Logf("tree: \n%s", tree)
|
||||
|
||||
var memFs = afero.NewMemMapFs()
|
||||
|
||||
if err := util.CopyFs(etcdFs, memFs, "/", "/", false); err != nil {
|
||||
t.Errorf("CopyFs error: %+v", err)
|
||||
return
|
||||
}
|
||||
if err := util.CopyFs(etcdFs, memFs, "/", "/", true); err != nil {
|
||||
t.Errorf("CopyFs2 error: %+v", err)
|
||||
return
|
||||
}
|
||||
if err := util.CopyFs(etcdFs, memFs, "/", "/tmp/d1/", false); err != nil {
|
||||
t.Errorf("CopyFs3 error: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
tree2, err := util.FsTree(memFs, "/")
|
||||
if err != nil {
|
||||
t.Errorf("tree2 error: %+v", err)
|
||||
return
|
||||
}
|
||||
t.Logf("tree2: \n%s", tree2)
|
||||
}
|
||||
88
etcd/fs/util.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 fs
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
// ReadAll reads from r until an error or EOF and returns the data it read.
|
||||
// A successful call returns err == nil, not err == EOF. Because ReadAll is
|
||||
// defined to read from src until EOF, it does not treat an EOF from Read
|
||||
// as an error to be reported.
|
||||
//func ReadAll(r io.Reader) ([]byte, error) {
|
||||
// return afero.ReadAll(r)
|
||||
//}
|
||||
|
||||
// ReadDir reads the directory named by dirname and returns
|
||||
// a list of sorted directory entries.
|
||||
func (obj *Fs) ReadDir(dirname string) ([]os.FileInfo, error) {
|
||||
return afero.ReadDir(obj, dirname)
|
||||
}
|
||||
|
||||
// ReadFile reads the file named by filename and returns the contents.
|
||||
// A successful call returns err == nil, not err == EOF. Because ReadFile
|
||||
// reads the whole file, it does not treat an EOF from Read as an error
|
||||
// to be reported.
|
||||
func (obj *Fs) ReadFile(filename string) ([]byte, error) {
|
||||
return afero.ReadFile(obj, filename)
|
||||
}
|
||||
|
||||
// TempDir creates a new temporary directory in the directory dir
|
||||
// with a name beginning with prefix and returns the path of the
|
||||
// new directory. If dir is the empty string, TempDir uses the
|
||||
// default directory for temporary files (see os.TempDir).
|
||||
// Multiple programs calling TempDir simultaneously
|
||||
// will not choose the same directory. It is the caller's responsibility
|
||||
// to remove the directory when no longer needed.
|
||||
func (obj *Fs) TempDir(dir, prefix string) (name string, err error) {
|
||||
return afero.TempDir(obj, dir, prefix)
|
||||
}
|
||||
|
||||
// TempFile creates a new temporary file in the directory dir
|
||||
// with a name beginning with prefix, opens the file for reading
|
||||
// and writing, and returns the resulting *File.
|
||||
// If dir is the empty string, TempFile uses the default directory
|
||||
// for temporary files (see os.TempDir).
|
||||
// Multiple programs calling TempFile simultaneously
|
||||
// will not choose the same file. The caller can use f.Name()
|
||||
// to find the pathname of the file. It is the caller's responsibility
|
||||
// to remove the file when no longer needed.
|
||||
func (obj *Fs) TempFile(dir, prefix string) (f afero.File, err error) {
|
||||
return afero.TempFile(obj, dir, prefix)
|
||||
}
|
||||
|
||||
// WriteFile writes data to a file named by filename.
|
||||
// If the file does not exist, WriteFile creates it with permissions perm;
|
||||
// otherwise WriteFile truncates it before writing.
|
||||
func (obj *Fs) WriteFile(filename string, data []byte, perm os.FileMode) error {
|
||||
return afero.WriteFile(obj, filename, data, perm)
|
||||
}
|
||||
|
||||
// Walk walks the file tree rooted at root, calling walkFn for each file or
|
||||
// directory in the tree, including root. All errors that arise visiting files
|
||||
// and directories are filtered by walkFn. The files are walked in lexical
|
||||
// order, which makes the output deterministic but means that for very
|
||||
// large directories Walk can be inefficient.
|
||||
// Walk does not follow symbolic links.
|
||||
func (obj *Fs) Walk(root string, walkFn filepath.WalkFunc) error {
|
||||
return afero.Walk(obj, root, walkFn)
|
||||
}
|
||||
30
etcd/interfaces.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 etcd
|
||||
|
||||
import (
|
||||
etcd "github.com/coreos/etcd/clientv3" // "clientv3"
|
||||
)
|
||||
|
||||
// Client provides a simple interface specification for client requests. Both
|
||||
// EmbdEtcd and ClientEtcd implement this.
|
||||
type Client interface {
|
||||
// TODO: add more method signatures
|
||||
Get(path string, opts ...etcd.OpOption) (map[string]string, error)
|
||||
Txn(ifcmps []etcd.Cmp, thenops, elseops []etcd.Op) (*etcd.TxnResponse, error)
|
||||
}
|
||||
411
etcd/methods.go
Normal file
@@ -0,0 +1,411 @@
|
||||
// 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 etcd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
etcd "github.com/coreos/etcd/clientv3"
|
||||
rpctypes "github.com/coreos/etcd/etcdserver/api/v3rpc/rpctypes"
|
||||
etcdtypes "github.com/coreos/etcd/pkg/types"
|
||||
context "golang.org/x/net/context"
|
||||
)
|
||||
|
||||
// TODO: Could all these Etcd*(obj *EmbdEtcd, ...) functions which deal with the
|
||||
// interface between etcd paths and behaviour be grouped into a single struct ?
|
||||
|
||||
// Nominate nominates a particular client to be a server (peer).
|
||||
func Nominate(obj *EmbdEtcd, hostname string, urls etcdtypes.URLs) error {
|
||||
if obj.flags.Trace {
|
||||
log.Printf("Trace: Etcd: Nominate(%v): %v", hostname, urls.String())
|
||||
defer log.Printf("Trace: Etcd: Nominate(%v): Finished!", hostname)
|
||||
}
|
||||
// nominate someone to be a server
|
||||
nominate := fmt.Sprintf("%s/nominated/%s", NS, hostname)
|
||||
ops := []etcd.Op{} // list of ops in this txn
|
||||
if urls != nil {
|
||||
ops = append(ops, etcd.OpPut(nominate, urls.String())) // TODO: add a TTL? (etcd.WithLease)
|
||||
|
||||
} else { // delete message if set to erase
|
||||
ops = append(ops, etcd.OpDelete(nominate))
|
||||
}
|
||||
|
||||
if _, err := obj.Txn(nil, ops, nil); err != nil {
|
||||
return fmt.Errorf("nominate failed") // exit in progress?
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Nominated returns a urls map of nominated etcd server volunteers.
|
||||
// NOTE: I know 'nominees' might be more correct, but is less consistent here
|
||||
func Nominated(obj *EmbdEtcd) (etcdtypes.URLsMap, error) {
|
||||
path := fmt.Sprintf("%s/nominated/", NS)
|
||||
keyMap, err := obj.Get(path, etcd.WithPrefix()) // map[string]string, bool
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("nominated isn't available: %v", err)
|
||||
}
|
||||
nominated := make(etcdtypes.URLsMap)
|
||||
for key, val := range keyMap { // loop through directory of nominated
|
||||
if !strings.HasPrefix(key, path) {
|
||||
continue
|
||||
}
|
||||
name := key[len(path):] // get name of nominee
|
||||
if val == "" { // skip "erased" values
|
||||
continue
|
||||
}
|
||||
urls, err := etcdtypes.NewURLs(strings.Split(val, ","))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("nominated data format error: %v", err)
|
||||
}
|
||||
nominated[name] = urls // add to map
|
||||
if obj.flags.Debug {
|
||||
log.Printf("Etcd: Nominated(%v): %v", name, val)
|
||||
}
|
||||
}
|
||||
return nominated, nil
|
||||
}
|
||||
|
||||
// Volunteer offers yourself up to be a server if needed.
|
||||
func Volunteer(obj *EmbdEtcd, urls etcdtypes.URLs) error {
|
||||
if obj.flags.Trace {
|
||||
log.Printf("Trace: Etcd: Volunteer(%v): %v", obj.hostname, urls.String())
|
||||
defer log.Printf("Trace: Etcd: Volunteer(%v): Finished!", obj.hostname)
|
||||
}
|
||||
// volunteer to be a server
|
||||
volunteer := fmt.Sprintf("%s/volunteers/%s", NS, obj.hostname)
|
||||
ops := []etcd.Op{} // list of ops in this txn
|
||||
if urls != nil {
|
||||
// XXX: adding a TTL is crucial! (i think)
|
||||
ops = append(ops, etcd.OpPut(volunteer, urls.String())) // value is usually a peer "serverURL"
|
||||
|
||||
} else { // delete message if set to erase
|
||||
ops = append(ops, etcd.OpDelete(volunteer))
|
||||
}
|
||||
|
||||
if _, err := obj.Txn(nil, ops, nil); err != nil {
|
||||
return fmt.Errorf("volunteering failed") // exit in progress?
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Volunteers returns a urls map of available etcd server volunteers.
|
||||
func Volunteers(obj *EmbdEtcd) (etcdtypes.URLsMap, error) {
|
||||
if obj.flags.Trace {
|
||||
log.Printf("Trace: Etcd: Volunteers()")
|
||||
defer log.Printf("Trace: Etcd: Volunteers(): Finished!")
|
||||
}
|
||||
path := fmt.Sprintf("%s/volunteers/", NS)
|
||||
keyMap, err := obj.Get(path, etcd.WithPrefix())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("volunteers aren't available: %v", err)
|
||||
}
|
||||
volunteers := make(etcdtypes.URLsMap)
|
||||
for key, val := range keyMap { // loop through directory of volunteers
|
||||
if !strings.HasPrefix(key, path) {
|
||||
continue
|
||||
}
|
||||
name := key[len(path):] // get name of volunteer
|
||||
if val == "" { // skip "erased" values
|
||||
continue
|
||||
}
|
||||
urls, err := etcdtypes.NewURLs(strings.Split(val, ","))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("volunteers data format error: %v", err)
|
||||
}
|
||||
volunteers[name] = urls // add to map
|
||||
if obj.flags.Debug {
|
||||
log.Printf("Etcd: Volunteer(%v): %v", name, val)
|
||||
}
|
||||
}
|
||||
return volunteers, nil
|
||||
}
|
||||
|
||||
// AdvertiseEndpoints advertises the list of available client endpoints.
|
||||
func AdvertiseEndpoints(obj *EmbdEtcd, urls etcdtypes.URLs) error {
|
||||
if obj.flags.Trace {
|
||||
log.Printf("Trace: Etcd: AdvertiseEndpoints(%v): %v", obj.hostname, urls.String())
|
||||
defer log.Printf("Trace: Etcd: AdvertiseEndpoints(%v): Finished!", obj.hostname)
|
||||
}
|
||||
// advertise endpoints
|
||||
endpoints := fmt.Sprintf("%s/endpoints/%s", NS, obj.hostname)
|
||||
ops := []etcd.Op{} // list of ops in this txn
|
||||
if urls != nil {
|
||||
// TODO: add a TTL? (etcd.WithLease)
|
||||
ops = append(ops, etcd.OpPut(endpoints, urls.String())) // value is usually a "clientURL"
|
||||
|
||||
} else { // delete message if set to erase
|
||||
ops = append(ops, etcd.OpDelete(endpoints))
|
||||
}
|
||||
|
||||
if _, err := obj.Txn(nil, ops, nil); err != nil {
|
||||
return fmt.Errorf("endpoint advertising failed") // exit in progress?
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Endpoints returns a urls map of available etcd server endpoints.
|
||||
func Endpoints(obj *EmbdEtcd) (etcdtypes.URLsMap, error) {
|
||||
if obj.flags.Trace {
|
||||
log.Printf("Trace: Etcd: Endpoints()")
|
||||
defer log.Printf("Trace: Etcd: Endpoints(): Finished!")
|
||||
}
|
||||
path := fmt.Sprintf("%s/endpoints/", NS)
|
||||
keyMap, err := obj.Get(path, etcd.WithPrefix())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("endpoints aren't available: %v", err)
|
||||
}
|
||||
endpoints := make(etcdtypes.URLsMap)
|
||||
for key, val := range keyMap { // loop through directory of endpoints
|
||||
if !strings.HasPrefix(key, path) {
|
||||
continue
|
||||
}
|
||||
name := key[len(path):] // get name of volunteer
|
||||
if val == "" { // skip "erased" values
|
||||
continue
|
||||
}
|
||||
urls, err := etcdtypes.NewURLs(strings.Split(val, ","))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("endpoints data format error: %v", err)
|
||||
}
|
||||
endpoints[name] = urls // add to map
|
||||
if obj.flags.Debug {
|
||||
log.Printf("Etcd: Endpoint(%v): %v", name, val)
|
||||
}
|
||||
}
|
||||
return endpoints, nil
|
||||
}
|
||||
|
||||
// SetHostnameConverged sets whether a specific hostname is converged.
|
||||
func SetHostnameConverged(obj *EmbdEtcd, hostname string, isConverged bool) error {
|
||||
if obj.flags.Trace {
|
||||
log.Printf("Trace: Etcd: SetHostnameConverged(%s): %v", hostname, isConverged)
|
||||
defer log.Printf("Trace: Etcd: SetHostnameConverged(%v): Finished!", hostname)
|
||||
}
|
||||
converged := fmt.Sprintf("%s/converged/%s", NS, hostname)
|
||||
op := []etcd.Op{etcd.OpPut(converged, fmt.Sprintf("%t", isConverged))}
|
||||
if _, err := obj.Txn(nil, op, nil); err != nil { // TODO: do we need a skipConv flag here too?
|
||||
return fmt.Errorf("set converged failed") // exit in progress?
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// HostnameConverged returns a map of every hostname's converged state.
|
||||
func HostnameConverged(obj *EmbdEtcd) (map[string]bool, error) {
|
||||
if obj.flags.Trace {
|
||||
log.Printf("Trace: Etcd: HostnameConverged()")
|
||||
defer log.Printf("Trace: Etcd: HostnameConverged(): Finished!")
|
||||
}
|
||||
path := fmt.Sprintf("%s/converged/", NS)
|
||||
keyMap, err := obj.ComplexGet(path, true, etcd.WithPrefix()) // don't un-converge
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converged values aren't available: %v", err)
|
||||
}
|
||||
converged := make(map[string]bool)
|
||||
for key, val := range keyMap { // loop through directory...
|
||||
if !strings.HasPrefix(key, path) {
|
||||
continue
|
||||
}
|
||||
name := key[len(path):] // get name of key
|
||||
if val == "" { // skip "erased" values
|
||||
continue
|
||||
}
|
||||
b, err := strconv.ParseBool(val)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converged data format error: %v", err)
|
||||
}
|
||||
converged[name] = b // add to map
|
||||
}
|
||||
return converged, nil
|
||||
}
|
||||
|
||||
// AddHostnameConvergedWatcher adds a watcher with a callback that runs on
|
||||
// hostname state changes.
|
||||
func AddHostnameConvergedWatcher(obj *EmbdEtcd, callbackFn func(map[string]bool) error) (func(), error) {
|
||||
path := fmt.Sprintf("%s/converged/", NS)
|
||||
internalCbFn := func(re *RE) error {
|
||||
// TODO: get the value from the response, and apply delta...
|
||||
// for now, just run a get operation which is easier to code!
|
||||
m, err := HostnameConverged(obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return callbackFn(m) // call my function
|
||||
}
|
||||
return obj.AddWatcher(path, internalCbFn, true, true, etcd.WithPrefix()) // no block and no converger reset
|
||||
}
|
||||
|
||||
// SetClusterSize sets the ideal target cluster size of etcd peers.
|
||||
func SetClusterSize(obj *EmbdEtcd, value uint16) error {
|
||||
if obj.flags.Trace {
|
||||
log.Printf("Trace: Etcd: SetClusterSize(): %v", value)
|
||||
defer log.Printf("Trace: Etcd: SetClusterSize(): Finished!")
|
||||
}
|
||||
key := fmt.Sprintf("%s/idealClusterSize", NS)
|
||||
|
||||
if err := obj.Set(key, strconv.FormatUint(uint64(value), 10)); err != nil {
|
||||
return fmt.Errorf("function SetClusterSize failed: %v", err) // exit in progress?
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetClusterSize gets the ideal target cluster size of etcd peers.
|
||||
func GetClusterSize(obj *EmbdEtcd) (uint16, error) {
|
||||
key := fmt.Sprintf("%s/idealClusterSize", NS)
|
||||
keyMap, err := obj.Get(key)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("function GetClusterSize failed: %v", err)
|
||||
}
|
||||
|
||||
val, exists := keyMap[key]
|
||||
if !exists || val == "" {
|
||||
return 0, fmt.Errorf("function GetClusterSize failed: %v", err)
|
||||
}
|
||||
|
||||
v, err := strconv.ParseUint(val, 10, 16)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("function GetClusterSize failed: %v", err)
|
||||
}
|
||||
return uint16(v), nil
|
||||
}
|
||||
|
||||
// MemberAdd adds a member to the cluster.
|
||||
func MemberAdd(obj *EmbdEtcd, peerURLs etcdtypes.URLs) (*etcd.MemberAddResponse, error) {
|
||||
//obj.Connect(false) // TODO: ?
|
||||
ctx := context.Background()
|
||||
var response *etcd.MemberAddResponse
|
||||
var err error
|
||||
for {
|
||||
if obj.exiting { // the exit signal has been sent!
|
||||
return nil, fmt.Errorf("exiting etcd")
|
||||
}
|
||||
obj.rLock.RLock()
|
||||
response, err = obj.client.MemberAdd(ctx, peerURLs.StringSlice())
|
||||
obj.rLock.RUnlock()
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
if ctx, err = obj.CtxError(ctx, err); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// MemberRemove removes a member by mID and returns if it worked, and also
|
||||
// if there was an error. This is because it might have run without error, but
|
||||
// the member wasn't found, for example.
|
||||
func MemberRemove(obj *EmbdEtcd, mID uint64) (bool, error) {
|
||||
//obj.Connect(false) // TODO: ?
|
||||
ctx := context.Background()
|
||||
for {
|
||||
if obj.exiting { // the exit signal has been sent!
|
||||
return false, fmt.Errorf("exiting etcd")
|
||||
}
|
||||
obj.rLock.RLock()
|
||||
_, err := obj.client.MemberRemove(ctx, mID)
|
||||
obj.rLock.RUnlock()
|
||||
if err == nil {
|
||||
break
|
||||
} else if err == rpctypes.ErrMemberNotFound {
|
||||
// if we get this, member already shut itself down :)
|
||||
return false, nil
|
||||
}
|
||||
if ctx, err = obj.CtxError(ctx, err); err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Members returns information on cluster membership.
|
||||
// The member ID's are the keys, because an empty names means unstarted!
|
||||
// TODO: consider queueing this through the main loop with CtxError(ctx, err)
|
||||
func Members(obj *EmbdEtcd) (map[uint64]string, error) {
|
||||
//obj.Connect(false) // TODO: ?
|
||||
ctx := context.Background()
|
||||
var response *etcd.MemberListResponse
|
||||
var err error
|
||||
for {
|
||||
if obj.exiting { // the exit signal has been sent!
|
||||
return nil, fmt.Errorf("exiting etcd")
|
||||
}
|
||||
obj.rLock.RLock()
|
||||
if obj.flags.Trace {
|
||||
log.Printf("Trace: Etcd: Members(): Endpoints are: %v", obj.client.Endpoints())
|
||||
}
|
||||
response, err = obj.client.MemberList(ctx)
|
||||
obj.rLock.RUnlock()
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
if ctx, err = obj.CtxError(ctx, err); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
members := make(map[uint64]string)
|
||||
for _, x := range response.Members {
|
||||
members[x.ID] = x.Name // x.Name will be "" if unstarted!
|
||||
}
|
||||
return members, nil
|
||||
}
|
||||
|
||||
// Leader returns the current leader of the etcd server cluster.
|
||||
func Leader(obj *EmbdEtcd) (string, error) {
|
||||
//obj.Connect(false) // TODO: ?
|
||||
membersMap, err := Members(obj)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
addresses := obj.LocalhostClientURLs() // heuristic, but probably correct
|
||||
if len(addresses) == 0 {
|
||||
// probably a programming error...
|
||||
return "", fmt.Errorf("programming error")
|
||||
}
|
||||
endpoint := addresses[0].Host // FIXME: arbitrarily picked the first one
|
||||
|
||||
// part two
|
||||
ctx := context.Background()
|
||||
var response *etcd.StatusResponse
|
||||
for {
|
||||
if obj.exiting { // the exit signal has been sent!
|
||||
return "", fmt.Errorf("exiting etcd")
|
||||
}
|
||||
|
||||
obj.rLock.RLock()
|
||||
response, err = obj.client.Maintenance.Status(ctx, endpoint)
|
||||
obj.rLock.RUnlock()
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
if ctx, err = obj.CtxError(ctx, err); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
// isLeader: response.Header.MemberId == response.Leader
|
||||
for id, name := range membersMap {
|
||||
if id == response.Leader {
|
||||
return name, nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("members map is not current") // not found
|
||||
}
|
||||
181
etcd/resources.go
Normal file
@@ -0,0 +1,181 @@
|
||||
// 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 etcd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
"github.com/purpleidea/mgmt/resources"
|
||||
"github.com/purpleidea/mgmt/util"
|
||||
|
||||
etcd "github.com/coreos/etcd/clientv3"
|
||||
)
|
||||
|
||||
// WatchResources returns a channel that outputs events when exported resources
|
||||
// change.
|
||||
// TODO: Filter our watch (on the server side if possible) based on the
|
||||
// collection prefixes and filters that we care about...
|
||||
func WatchResources(obj *EmbdEtcd) chan error {
|
||||
ch := make(chan error, 1) // buffer it so we can measure it
|
||||
path := fmt.Sprintf("%s/exported/", NS)
|
||||
callback := func(re *RE) error {
|
||||
// TODO: is this even needed? it used to happen on conn errors
|
||||
log.Printf("Etcd: Watch: Path: %v", path) // event
|
||||
if re == nil || re.response.Canceled {
|
||||
return fmt.Errorf("watch is empty") // will cause a CtxError+retry
|
||||
}
|
||||
// we normally need to check if anything changed since the last
|
||||
// event, since a set (export) with no changes still causes the
|
||||
// watcher to trigger and this would cause an infinite loop. we
|
||||
// don't need to do this check anymore because we do the export
|
||||
// transactionally, and only if a change is needed. since it is
|
||||
// atomic, all the changes arrive together which avoids dupes!!
|
||||
if len(ch) == 0 { // send event only if one isn't pending
|
||||
// this check avoids multiple events all queueing up and then
|
||||
// being released continuously long after the changes stopped
|
||||
// do not block!
|
||||
ch <- nil // event
|
||||
}
|
||||
return nil
|
||||
}
|
||||
_, _ = obj.AddWatcher(path, callback, true, false, etcd.WithPrefix()) // no need to check errors
|
||||
return ch
|
||||
}
|
||||
|
||||
// SetResources exports all of the resources which we pass in to etcd.
|
||||
func SetResources(obj *EmbdEtcd, hostname string, resourceList []resources.Res) error {
|
||||
// key structure is $NS/exported/$hostname/resources/$uid = $data
|
||||
|
||||
var kindFilter []string // empty to get from everyone
|
||||
hostnameFilter := []string{hostname}
|
||||
// this is not a race because we should only be reading keys which we
|
||||
// set, and there should not be any contention with other hosts here!
|
||||
originals, err := GetResources(obj, hostnameFilter, kindFilter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(originals) == 0 && len(resourceList) == 0 { // special case of no add or del
|
||||
return nil
|
||||
}
|
||||
|
||||
ifs := []etcd.Cmp{} // list matching the desired state
|
||||
ops := []etcd.Op{} // list of ops in this transaction
|
||||
for _, res := range resourceList {
|
||||
if res.GetKind() == "" {
|
||||
log.Fatalf("Etcd: SetResources: Error: Empty kind: %v", res.GetName())
|
||||
}
|
||||
uid := fmt.Sprintf("%s/%s", res.GetKind(), res.GetName())
|
||||
path := fmt.Sprintf("%s/exported/%s/resources/%s", NS, hostname, uid)
|
||||
if data, err := resources.ResToB64(res); err == nil {
|
||||
ifs = append(ifs, etcd.Compare(etcd.Value(path), "=", data)) // desired state
|
||||
ops = append(ops, etcd.OpPut(path, data))
|
||||
} else {
|
||||
return fmt.Errorf("can't convert to B64: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
match := func(res resources.Res, resourceList []resources.Res) bool { // helper lambda
|
||||
for _, x := range resourceList {
|
||||
if res.GetKind() == x.GetKind() && res.GetName() == x.GetName() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
hasDeletes := false
|
||||
// delete old, now unused resources here...
|
||||
for _, res := range originals {
|
||||
if res.GetKind() == "" {
|
||||
log.Fatalf("Etcd: SetResources: Error: Empty kind: %v", res.GetName())
|
||||
}
|
||||
uid := fmt.Sprintf("%s/%s", res.GetKind(), res.GetName())
|
||||
path := fmt.Sprintf("%s/exported/%s/resources/%s", NS, hostname, uid)
|
||||
|
||||
if match(res, resourceList) { // if we match, no need to delete!
|
||||
continue
|
||||
}
|
||||
|
||||
ops = append(ops, etcd.OpDelete(path))
|
||||
|
||||
hasDeletes = true
|
||||
}
|
||||
|
||||
// if everything is already correct, do nothing, otherwise, run the ops!
|
||||
// it's important to do this in one transaction, and atomically, because
|
||||
// this way, we only generate one watch event, and only when it's needed
|
||||
if hasDeletes { // always run, ifs don't matter
|
||||
_, err = obj.Txn(nil, ops, nil) // TODO: does this run? it should!
|
||||
} else {
|
||||
_, err = obj.Txn(ifs, nil, ops) // TODO: do we need to look at response?
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// GetResources collects all of the resources which match a filter from etcd.
|
||||
// If the kindfilter or hostnameFilter is empty, then it assumes no filtering...
|
||||
// TODO: Expand this with a more powerful filter based on what we eventually
|
||||
// support in our collect DSL. Ideally a server side filter like WithFilter()
|
||||
// We could do this if the pattern was $NS/exported/$kind/$hostname/$uid = $data.
|
||||
func GetResources(obj *EmbdEtcd, hostnameFilter, kindFilter []string) ([]resources.Res, error) {
|
||||
// key structure is $NS/exported/$hostname/resources/$uid = $data
|
||||
path := fmt.Sprintf("%s/exported/", NS)
|
||||
resourceList := []resources.Res{}
|
||||
keyMap, err := obj.Get(path, etcd.WithPrefix(), etcd.WithSort(etcd.SortByKey, etcd.SortAscend))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not get resources: %v", err)
|
||||
}
|
||||
for key, val := range keyMap {
|
||||
if !strings.HasPrefix(key, path) { // sanity check
|
||||
continue
|
||||
}
|
||||
|
||||
str := strings.Split(key[len(path):], "/")
|
||||
if len(str) != 4 {
|
||||
return nil, fmt.Errorf("unexpected chunk count")
|
||||
}
|
||||
hostname, r, kind, name := str[0], str[1], str[2], str[3]
|
||||
if r != "resources" {
|
||||
return nil, fmt.Errorf("unexpected chunk pattern")
|
||||
}
|
||||
if kind == "" {
|
||||
return nil, fmt.Errorf("unexpected kind chunk")
|
||||
}
|
||||
|
||||
// FIXME: ideally this would be a server side filter instead!
|
||||
if len(hostnameFilter) > 0 && !util.StrInList(hostname, hostnameFilter) {
|
||||
continue
|
||||
}
|
||||
|
||||
// FIXME: ideally this would be a server side filter instead!
|
||||
if len(kindFilter) > 0 && !util.StrInList(kind, kindFilter) {
|
||||
continue
|
||||
}
|
||||
|
||||
if obj, err := resources.B64ToRes(val); err == nil {
|
||||
log.Printf("Etcd: Get: (Hostname, Kind, Name): (%s, %s, %s)", hostname, kind, name)
|
||||
resourceList = append(resourceList, obj)
|
||||
} else {
|
||||
return nil, fmt.Errorf("can't convert from B64: %v", err)
|
||||
}
|
||||
}
|
||||
return resourceList, nil
|
||||
}
|
||||
49
etcd/scheduler/alphastrategy.go
Normal file
@@ -0,0 +1,49 @@
|
||||
// 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 scheduler // TODO: i'd like this to be a separate package, but cycles!
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
)
|
||||
|
||||
func init() {
|
||||
Register("alpha", func() Strategy { return &alphaStrategy{} }) // must register the func and name
|
||||
}
|
||||
|
||||
type alphaStrategy struct {
|
||||
// no state to store
|
||||
}
|
||||
|
||||
// Schedule returns the first host out of a sorted group of available hostnames.
|
||||
func (obj *alphaStrategy) Schedule(hostnames map[string]string, opts *schedulerOptions) ([]string, error) {
|
||||
if len(hostnames) <= 0 {
|
||||
return nil, fmt.Errorf("strategy: cannot schedule from zero hosts")
|
||||
}
|
||||
if opts.maxCount <= 0 {
|
||||
return nil, fmt.Errorf("strategy: cannot schedule with a max of zero")
|
||||
}
|
||||
|
||||
sortedHosts := []string{}
|
||||
for key := range hostnames {
|
||||
sortedHosts = append(sortedHosts, key)
|
||||
}
|
||||
sort.Strings(sortedHosts)
|
||||
|
||||
return []string{sortedHosts[0]}, nil // pick first host
|
||||
}
|
||||
100
etcd/scheduler/options.go
Normal file
@@ -0,0 +1,100 @@
|
||||
// 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 scheduler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Option is a type that can be used to configure the scheduler.
|
||||
type Option func(*schedulerOptions)
|
||||
|
||||
// schedulerOptions represents the different possible configurable options. Not
|
||||
// all options necessarily work for each scheduler strategy algorithm.
|
||||
type schedulerOptions struct {
|
||||
debug bool
|
||||
logf func(format string, v ...interface{})
|
||||
strategy Strategy
|
||||
maxCount int // TODO: should this be *int to know when it's set?
|
||||
reuseLease bool
|
||||
sessionTTL int // TODO: should this be *int to know when it's set?
|
||||
hostsFilter []string
|
||||
// TODO: add more options
|
||||
}
|
||||
|
||||
// Debug specifies whether we should run in debug mode or not.
|
||||
func Debug(debug bool) Option {
|
||||
return func(so *schedulerOptions) {
|
||||
so.debug = debug
|
||||
}
|
||||
}
|
||||
|
||||
// Logf passes a logger function that we can use if so desired.
|
||||
func Logf(logf func(format string, v ...interface{})) Option {
|
||||
return func(so *schedulerOptions) {
|
||||
so.logf = logf
|
||||
}
|
||||
}
|
||||
|
||||
// StrategyKind sets the scheduler strategy used.
|
||||
func StrategyKind(strategy string) Option {
|
||||
return func(so *schedulerOptions) {
|
||||
f, exists := registeredStrategies[strategy]
|
||||
if !exists {
|
||||
panic(fmt.Sprintf("scheduler: undefined strategy: %s", strategy))
|
||||
}
|
||||
so.strategy = f()
|
||||
}
|
||||
}
|
||||
|
||||
// MaxCount is the maximum number of hosts that should get simultaneously
|
||||
// scheduled.
|
||||
func MaxCount(maxCount int) Option {
|
||||
return func(so *schedulerOptions) {
|
||||
if maxCount > 0 {
|
||||
so.maxCount = maxCount
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ReuseLease specifies whether we should try and re-use the lease between runs.
|
||||
// Ordinarily it would get discarded with each new version (deploy) of the code.
|
||||
func ReuseLease(reuseLease bool) Option {
|
||||
return func(so *schedulerOptions) {
|
||||
so.reuseLease = reuseLease
|
||||
}
|
||||
}
|
||||
|
||||
// SessionTTL is the amount of time to delay before expiring a key on abrupt
|
||||
// host disconnect of if ReuseLease is true.
|
||||
func SessionTTL(sessionTTL int) Option {
|
||||
return func(so *schedulerOptions) {
|
||||
if sessionTTL > 0 {
|
||||
so.sessionTTL = sessionTTL
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// HostsFilter specifies a manual list of hosts, to use as a subset of whatever
|
||||
// was auto-discovered.
|
||||
// XXX: think more about this idea...
|
||||
func HostsFilter(hosts []string) Option {
|
||||
return func(so *schedulerOptions) {
|
||||
so.hostsFilter = hosts
|
||||
}
|
||||
}
|
||||
84
etcd/scheduler/rrstrategy.go
Normal file
@@ -0,0 +1,84 @@
|
||||
// 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 scheduler // TODO: i'd like this to be a separate package, but cycles!
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"github.com/purpleidea/mgmt/util"
|
||||
)
|
||||
|
||||
func init() {
|
||||
Register("rr", func() Strategy { return &rrStrategy{} }) // must register the func and name
|
||||
}
|
||||
|
||||
type rrStrategy struct {
|
||||
// some stored state
|
||||
hosts []string
|
||||
}
|
||||
|
||||
// Schedule returns hosts in round robin style from the available hostnames.
|
||||
func (obj *rrStrategy) Schedule(hostnames map[string]string, opts *schedulerOptions) ([]string, error) {
|
||||
if len(hostnames) <= 0 {
|
||||
return nil, fmt.Errorf("strategy: cannot schedule from zero hosts")
|
||||
}
|
||||
if opts.maxCount <= 0 {
|
||||
return nil, fmt.Errorf("strategy: cannot schedule with a max of zero")
|
||||
}
|
||||
|
||||
// always get a deterministic list of current hosts first...
|
||||
sortedHosts := []string{}
|
||||
for key := range hostnames {
|
||||
sortedHosts = append(sortedHosts, key)
|
||||
}
|
||||
sort.Strings(sortedHosts)
|
||||
|
||||
if obj.hosts == nil {
|
||||
obj.hosts = []string{} // initialize if needed
|
||||
}
|
||||
|
||||
// add any new hosts we learned about, to the end of the list
|
||||
for _, x := range sortedHosts {
|
||||
if !util.StrInList(x, obj.hosts) {
|
||||
obj.hosts = append(obj.hosts, x)
|
||||
}
|
||||
}
|
||||
|
||||
// remove any hosts we previouly knew about from the list
|
||||
for ix := len(obj.hosts) - 1; ix >= 0; ix-- {
|
||||
if !util.StrInList(obj.hosts[ix], sortedHosts) {
|
||||
// delete entry at this index
|
||||
obj.hosts = append(obj.hosts[:ix], obj.hosts[ix+1:]...)
|
||||
}
|
||||
}
|
||||
|
||||
// get the maximum number of hosts to return
|
||||
max := len(obj.hosts) // can't return more than we have
|
||||
if opts.maxCount < max { // found a smaller limit
|
||||
max = opts.maxCount
|
||||
}
|
||||
|
||||
result := []string{}
|
||||
// now return the number of needed hosts from the list
|
||||
for i := 0; i < max; i++ {
|
||||
result = append(result, obj.hosts[i])
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
564
etcd/scheduler/scheduler.go
Normal file
@@ -0,0 +1,564 @@
|
||||
// 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 scheduler implements a distributed consensus scheduler with etcd.
|
||||
package scheduler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
etcd "github.com/coreos/etcd/clientv3"
|
||||
"github.com/coreos/etcd/clientv3/concurrency"
|
||||
errwrap "github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
// DefaultSessionTTL is the number of seconds to wait before a dead or
|
||||
// unresponsive host is removed from the scheduled pool.
|
||||
DefaultSessionTTL = 10 // seconds
|
||||
|
||||
// DefaultMaxCount is the maximum number of hosts to schedule on if not
|
||||
// specified.
|
||||
DefaultMaxCount = 1 // TODO: what is the logical value to choose? +Inf?
|
||||
|
||||
hostnameJoinChar = "," // char used to join and split lists of hostnames
|
||||
)
|
||||
|
||||
// ErrEndOfResults is a sentinel that represents no more results will be coming.
|
||||
var ErrEndOfResults = errors.New("scheduler: end of results")
|
||||
|
||||
var schedulerLeases = make(map[string]etcd.LeaseID) // process lifetime in-memory lease store
|
||||
|
||||
// schedulerResult represents output from the scheduler.
|
||||
type schedulerResult struct {
|
||||
hosts []string
|
||||
err error
|
||||
}
|
||||
|
||||
// Result is what is returned when you request a scheduler. You can call methods
|
||||
// on it, and it stores the necessary state while you're running. When one of
|
||||
// these is produced, the scheduler has already kicked off running for you
|
||||
// automatically.
|
||||
type Result struct {
|
||||
results chan *schedulerResult
|
||||
closeFunc func() // run this when you're done with the scheduler // TODO: replace with an input `context`
|
||||
}
|
||||
|
||||
// Next returns the next output from the scheduler when it changes. This blocks
|
||||
// until a new value is available, which is why you may wish to use a context to
|
||||
// cancel any read from this. It returns ErrEndOfResults if the scheduler shuts
|
||||
// down.
|
||||
func (obj *Result) Next(ctx context.Context) ([]string, error) {
|
||||
select {
|
||||
case val, ok := <-obj.results:
|
||||
if !ok {
|
||||
return nil, ErrEndOfResults
|
||||
}
|
||||
return val.hosts, val.err
|
||||
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
// Shutdown causes everything to clean up. We no longer need the scheduler.
|
||||
// TODO: should this be named Close() instead? Should it return an error?
|
||||
func (obj *Result) Shutdown() {
|
||||
obj.closeFunc()
|
||||
// XXX: should we have a waitgroup to wait for it all to close?
|
||||
}
|
||||
|
||||
// Schedule returns a scheduler result which can be queried with it's available
|
||||
// methods. This automatically causes different etcd clients sharing the same
|
||||
// path to discover each other and be part of the scheduled set. On close the
|
||||
// keys expire and will get removed from the scheduled set. Different options
|
||||
// can be passed in to customize the behaviour. Hostname represents the unique
|
||||
// identifier for the caller. The behaviour is undefined if this is run more
|
||||
// than once with the same path and hostname simultaneously.
|
||||
func Schedule(client *etcd.Client, path string, hostname string, opts ...Option) (*Result, error) {
|
||||
if strings.HasSuffix(path, "/") {
|
||||
return nil, fmt.Errorf("scheduler: path must not end with the slash char")
|
||||
}
|
||||
if !strings.HasPrefix(path, "/") {
|
||||
return nil, fmt.Errorf("scheduler: path must start with the slash char")
|
||||
}
|
||||
if hostname == "" {
|
||||
return nil, fmt.Errorf("scheduler: hostname must not be empty")
|
||||
}
|
||||
if strings.Contains(hostname, hostnameJoinChar) {
|
||||
return nil, fmt.Errorf("scheduler: hostname must not contain join char: %s", hostnameJoinChar)
|
||||
}
|
||||
|
||||
// key structure is $path/election = ???
|
||||
// key structure is $path/exchange/$hostname = ???
|
||||
// key structure is $path/scheduled = ???
|
||||
|
||||
options := &schedulerOptions{ // default scheduler options
|
||||
// If reuseLease is false, then on host disconnect, that hosts
|
||||
// entry will immediately expire, and the scheduler will react
|
||||
// instantly and remove that host entry from the list. If this
|
||||
// is true, or if the host closes without a clean shutdown, it
|
||||
// will take the TTL number of seconds to remove the key. This
|
||||
// can be set using the concurrency.WithTTL option to Session.
|
||||
reuseLease: false,
|
||||
sessionTTL: DefaultSessionTTL,
|
||||
maxCount: DefaultMaxCount,
|
||||
}
|
||||
for _, optionFunc := range opts { // apply the scheduler options
|
||||
optionFunc(options)
|
||||
}
|
||||
|
||||
if options.strategy == nil {
|
||||
return nil, fmt.Errorf("scheduler: strategy must be specified")
|
||||
}
|
||||
|
||||
sessionOptions := []concurrency.SessionOption{}
|
||||
|
||||
// here we try to re-use lease between multiple runs of the code
|
||||
// TODO: is it a good idea to try and re-use the lease b/w runs?
|
||||
if options.reuseLease {
|
||||
if leaseID, exists := schedulerLeases[path]; exists {
|
||||
sessionOptions = append(sessionOptions, concurrency.WithLease(leaseID))
|
||||
}
|
||||
}
|
||||
// ttl for key expiry on abrupt disconnection or if reuseLease is true!
|
||||
if options.sessionTTL > 0 {
|
||||
sessionOptions = append(sessionOptions, concurrency.WithTTL(options.sessionTTL))
|
||||
}
|
||||
|
||||
//options.debug = true // use this for local debugging
|
||||
session, err := concurrency.NewSession(client, sessionOptions...)
|
||||
if err != nil {
|
||||
return nil, errwrap.Wrapf(err, "scheduler: could not create session")
|
||||
}
|
||||
leaseID := session.Lease()
|
||||
if options.reuseLease {
|
||||
// save for next time, otherwise run session.Close() somewhere
|
||||
schedulerLeases[path] = leaseID
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background()) // cancel below
|
||||
//defer cancel() // do NOT do this, as it would cause an early cancel!
|
||||
|
||||
// stored scheduler results
|
||||
scheduledPath := fmt.Sprintf("%s/scheduled", path)
|
||||
scheduledChan := client.Watcher.Watch(ctx, scheduledPath)
|
||||
|
||||
// exchange hostname, and attach it to session (leaseID) so it expires
|
||||
// (gets deleted) when we disconnect...
|
||||
exchangePath := fmt.Sprintf("%s/exchange", path)
|
||||
exchangePathHost := fmt.Sprintf("%s/%s", exchangePath, hostname)
|
||||
exchangePathPrefix := fmt.Sprintf("%s/", exchangePath)
|
||||
|
||||
// open the watch *before* we set our key so that we can see the change!
|
||||
watchChan := client.Watcher.Watch(ctx, exchangePathPrefix, etcd.WithPrefix())
|
||||
|
||||
data := "TODO" // XXX: no data to exchange alongside hostnames yet
|
||||
ifops := []etcd.Cmp{
|
||||
etcd.Compare(etcd.Value(exchangePathHost), "=", data),
|
||||
etcd.Compare(etcd.LeaseValue(exchangePathHost), "=", leaseID),
|
||||
}
|
||||
elsop := etcd.OpPut(exchangePathHost, data, etcd.WithLease(leaseID))
|
||||
|
||||
// it's important to do this in one transaction, and atomically, because
|
||||
// this way, we only generate one watch event, and only when it's needed
|
||||
// updating leaseID, or key expiry (deletion) both generate watch events
|
||||
// XXX: context!!!
|
||||
if txn, err := client.KV.Txn(context.TODO()).If(ifops...).Then([]etcd.Op{}...).Else(elsop).Commit(); err != nil {
|
||||
defer cancel() // cancel to avoid leaks if we exit early...
|
||||
return nil, errwrap.Wrapf(err, "could not exchange in `%s`", path)
|
||||
} else if txn.Succeeded {
|
||||
options.logf("txn did nothing...") // then branch
|
||||
} else {
|
||||
options.logf("txn did an update...")
|
||||
}
|
||||
|
||||
// create an election object
|
||||
electionPath := fmt.Sprintf("%s/election", path)
|
||||
election := concurrency.NewElection(session, electionPath)
|
||||
electionChan := election.Observe(ctx)
|
||||
|
||||
elected := "" // who we "assume" is elected
|
||||
wg := &sync.WaitGroup{}
|
||||
ch := make(chan *schedulerResult)
|
||||
closeChan := make(chan struct{})
|
||||
send := func(hosts []string, err error) bool { // helper function for sending
|
||||
select {
|
||||
case ch <- &schedulerResult{ // send
|
||||
hosts: hosts,
|
||||
err: err,
|
||||
}:
|
||||
return true
|
||||
case <-closeChan: // unblock
|
||||
return false // not sent
|
||||
}
|
||||
}
|
||||
|
||||
once := &sync.Once{}
|
||||
onceBody := func() { // do not call directly, use closeFunc!
|
||||
//cancel() // TODO: is this needed here?
|
||||
// request a graceful shutdown, caller must call this to
|
||||
// shutdown when they are finished with the scheduler...
|
||||
// calling this will cause their hosts channels to close
|
||||
close(closeChan) // send a close signal
|
||||
}
|
||||
closeFunc := func() {
|
||||
once.Do(onceBody)
|
||||
}
|
||||
result := &Result{
|
||||
results: ch,
|
||||
// TODO: we could accept a context to watch for cancel instead?
|
||||
closeFunc: closeFunc,
|
||||
}
|
||||
|
||||
mutex := &sync.Mutex{}
|
||||
var campaignClose chan struct{}
|
||||
var campaignRunning bool
|
||||
// goroutine to vote for someone as scheduler! each participant must be
|
||||
// able to run this or nobody will be around to vote if others are down
|
||||
campaignFunc := func() {
|
||||
options.logf("starting campaign...")
|
||||
// the mutex ensures we don't fly past the wg.Wait() if someone
|
||||
// shuts down the scheduler right as we are about to start this
|
||||
// campaigning loop up. we do not want to fail unnecessarily...
|
||||
mutex.Lock()
|
||||
wg.Add(1)
|
||||
mutex.Unlock()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
go func() {
|
||||
defer cancel() // run cancel to stop campaigning...
|
||||
select {
|
||||
case <-campaignClose:
|
||||
return
|
||||
case <-closeChan:
|
||||
return
|
||||
}
|
||||
}()
|
||||
for {
|
||||
// TODO: previously, this looped infinitely fast
|
||||
// TODO: add some rate limiting here for initial
|
||||
// campaigning which occasionally loops a lot...
|
||||
if options.debug {
|
||||
//fmt.Printf(".") // debug
|
||||
options.logf("campaigning...")
|
||||
}
|
||||
|
||||
// "Campaign puts a value as eligible for the election.
|
||||
// It blocks until it is elected, an error occurs, or
|
||||
// the context is cancelled."
|
||||
|
||||
// vote for ourselves, as it's the only host we can
|
||||
// guarantee is alive, otherwise we wouldn't be voting!
|
||||
// it would be more sensible to vote for the last valid
|
||||
// hostname to keep things more stable, but if that
|
||||
// information was stale, and that host wasn't alive,
|
||||
// then this would defeat the point of picking them!
|
||||
if err := election.Campaign(ctx, hostname); err != nil {
|
||||
if err != context.Canceled {
|
||||
send(nil, errwrap.Wrapf(err, "scheduler: error campaigning"))
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
go func() {
|
||||
defer close(ch)
|
||||
if !options.reuseLease {
|
||||
defer session.Close() // this revokes the lease...
|
||||
}
|
||||
|
||||
defer func() {
|
||||
// XXX: should we ever resign? why would this block and thus need a context?
|
||||
if elected == hostname { // TODO: is it safe to just always do this?
|
||||
if err := election.Resign(context.TODO()); err != nil { // XXX: add a timeout?
|
||||
}
|
||||
}
|
||||
elected = "" // we don't care anymore!
|
||||
}()
|
||||
|
||||
// this "last" defer (first to run) should block until the other
|
||||
// goroutine has closed so we don't Close an in-use session, etc
|
||||
defer wg.Wait()
|
||||
|
||||
go func() {
|
||||
defer cancel() // run cancel to "free" Observe...
|
||||
|
||||
defer wg.Wait() // also wait here if parent exits first
|
||||
|
||||
select {
|
||||
case <-closeChan:
|
||||
// we want the above wg.Wait() to work if this
|
||||
// close happens. lock with the campaign start
|
||||
defer mutex.Unlock()
|
||||
mutex.Lock()
|
||||
return
|
||||
}
|
||||
}()
|
||||
hostnames := make(map[string]string)
|
||||
for {
|
||||
select {
|
||||
case val, ok := <-electionChan:
|
||||
if options.debug {
|
||||
options.logf("electionChan(%t): %+v", ok, val)
|
||||
}
|
||||
if !ok {
|
||||
if options.debug {
|
||||
options.logf("elections stream shutdown...")
|
||||
}
|
||||
electionChan = nil
|
||||
// done
|
||||
// TODO: do we need to send on error channel?
|
||||
// XXX: maybe if context was not called to exit us?
|
||||
|
||||
// ensure everyone waiting on closeChan
|
||||
// gets cleaned up so we free mem, etc!
|
||||
if watchChan == nil && scheduledChan == nil { // all now closed
|
||||
closeFunc()
|
||||
return
|
||||
}
|
||||
continue
|
||||
|
||||
}
|
||||
|
||||
elected = string(val.Kvs[0].Value)
|
||||
//if options.debug {
|
||||
options.logf("elected: %s", elected)
|
||||
//}
|
||||
if elected != hostname { // not me!
|
||||
// start up the campaign function
|
||||
if !campaignRunning {
|
||||
campaignClose = make(chan struct{})
|
||||
campaignFunc() // run
|
||||
campaignRunning = true
|
||||
}
|
||||
continue // someone else does the scheduling...
|
||||
} else { // campaigning while i am it loops fast
|
||||
// shutdown the campaign function
|
||||
if campaignRunning {
|
||||
close(campaignClose)
|
||||
wg.Wait()
|
||||
campaignRunning = false
|
||||
}
|
||||
}
|
||||
|
||||
// i was voted in to make the scheduling choice!
|
||||
|
||||
case watchResp, ok := <-watchChan:
|
||||
if options.debug {
|
||||
options.logf("watchChan(%t): %+v", ok, watchResp)
|
||||
}
|
||||
if !ok {
|
||||
if options.debug {
|
||||
options.logf("watch stream shutdown...")
|
||||
}
|
||||
watchChan = nil
|
||||
// done
|
||||
// TODO: do we need to send on error channel?
|
||||
// XXX: maybe if context was not called to exit us?
|
||||
|
||||
// ensure everyone waiting on closeChan
|
||||
// gets cleaned up so we free mem, etc!
|
||||
if electionChan == nil && scheduledChan == nil { // all now closed
|
||||
closeFunc()
|
||||
return
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
err := watchResp.Err()
|
||||
if watchResp.Canceled || err == context.Canceled {
|
||||
// channel get closed shortly...
|
||||
continue
|
||||
}
|
||||
if watchResp.Header.Revision == 0 { // by inspection
|
||||
// received empty message ?
|
||||
// switched client connection ?
|
||||
// FIXME: what should we do here ?
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
send(nil, errwrap.Wrapf(err, "scheduler: exchange watcher failed"))
|
||||
continue
|
||||
}
|
||||
if len(watchResp.Events) == 0 { // nothing interesting
|
||||
continue
|
||||
}
|
||||
|
||||
options.logf("running exchange values get...")
|
||||
resp, err := client.Get(ctx, exchangePathPrefix, etcd.WithPrefix(), etcd.WithSort(etcd.SortByKey, etcd.SortAscend))
|
||||
if err != nil || resp == nil {
|
||||
if err != nil {
|
||||
send(nil, errwrap.Wrapf(err, "scheduler: could not get exchange values in `%s`", path))
|
||||
} else { // if resp == nil
|
||||
send(nil, fmt.Errorf("scheduler: could not get exchange values in `%s`, resp is nil", path))
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// FIXME: the value key could instead be host
|
||||
// specific information which is used for some
|
||||
// purpose, eg: seconds active, and other data?
|
||||
hostnames = make(map[string]string) // reset
|
||||
for _, x := range resp.Kvs {
|
||||
k := string(x.Key)
|
||||
if !strings.HasPrefix(k, exchangePathPrefix) {
|
||||
continue
|
||||
}
|
||||
k = k[len(exchangePathPrefix):] // strip
|
||||
hostnames[k] = string(x.Value)
|
||||
}
|
||||
if options.debug {
|
||||
options.logf("available hostnames: %+v", hostnames)
|
||||
}
|
||||
|
||||
case scheduledResp, ok := <-scheduledChan:
|
||||
if options.debug {
|
||||
options.logf("scheduledChan(%t): %+v", ok, scheduledResp)
|
||||
}
|
||||
if !ok {
|
||||
if options.debug {
|
||||
options.logf("scheduled stream shutdown...")
|
||||
}
|
||||
scheduledChan = nil
|
||||
// done
|
||||
// TODO: do we need to send on error channel?
|
||||
// XXX: maybe if context was not called to exit us?
|
||||
|
||||
// ensure everyone waiting on closeChan
|
||||
// gets cleaned up so we free mem, etc!
|
||||
if electionChan == nil && watchChan == nil { // all now closed
|
||||
closeFunc()
|
||||
return
|
||||
}
|
||||
continue
|
||||
}
|
||||
// event! continue below and get new result...
|
||||
|
||||
// NOTE: not needed, exit this via Observe ctx cancel,
|
||||
// which will ultimately cause the chan to shutdown...
|
||||
//case <-closeChan:
|
||||
// return
|
||||
} // end select
|
||||
|
||||
if len(hostnames) == 0 {
|
||||
if options.debug {
|
||||
options.logf("zero hosts available")
|
||||
}
|
||||
continue // not enough hosts available
|
||||
}
|
||||
|
||||
// if we're currently elected, make a scheduling decision
|
||||
// if not, lookup the existing leader scheduling decision
|
||||
if elected != hostname {
|
||||
options.logf("i am not the leader, running scheduling result get...")
|
||||
resp, err := client.Get(ctx, scheduledPath)
|
||||
if err != nil || resp == nil || len(resp.Kvs) != 1 {
|
||||
if err != nil {
|
||||
send(nil, errwrap.Wrapf(err, "scheduler: could not get scheduling result in `%s`", path))
|
||||
} else if resp == nil {
|
||||
send(nil, fmt.Errorf("scheduler: could not get scheduling result in `%s`, resp is nil", path))
|
||||
} else if len(resp.Kvs) > 1 {
|
||||
send(nil, fmt.Errorf("scheduler: could not get scheduling result in `%s`, resp kvs: %+v", path, resp.Kvs))
|
||||
}
|
||||
// if len(resp.Kvs) == 0, we shouldn't error
|
||||
// in that situation it's just too early...
|
||||
continue
|
||||
}
|
||||
|
||||
result := string(resp.Kvs[0].Value)
|
||||
hosts := strings.Split(result, hostnameJoinChar)
|
||||
|
||||
if options.debug {
|
||||
options.logf("sending hosts: %+v", hosts)
|
||||
}
|
||||
// send that on channel!
|
||||
if !send(hosts, nil) {
|
||||
//return // pass instead, let channels clean up
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// i am the leader, run scheduler and store result
|
||||
options.logf("i am elected, running scheduler...")
|
||||
|
||||
// run actual scheduler and decide who should be chosen
|
||||
// TODO: is there any additional data that we can pass
|
||||
// to the scheduler so it can make a better decision ?
|
||||
hosts, err := options.strategy.Schedule(hostnames, options)
|
||||
if err != nil {
|
||||
send(nil, errwrap.Wrapf(err, "scheduler: strategy failed"))
|
||||
continue
|
||||
}
|
||||
sort.Strings(hosts) // for consistency
|
||||
|
||||
options.logf("storing scheduling result...")
|
||||
data := strings.Join(hosts, hostnameJoinChar)
|
||||
ifops := []etcd.Cmp{
|
||||
etcd.Compare(etcd.Value(scheduledPath), "=", data),
|
||||
}
|
||||
elsop := etcd.OpPut(scheduledPath, data)
|
||||
|
||||
// it's important to do this in one transaction, and atomically, because
|
||||
// this way, we only generate one watch event, and only when it's needed
|
||||
// updating leaseID, or key expiry (deletion) both generate watch events
|
||||
// XXX: context!!!
|
||||
if _, err := client.KV.Txn(context.TODO()).If(ifops...).Then([]etcd.Op{}...).Else(elsop).Commit(); err != nil {
|
||||
send(nil, errwrap.Wrapf(err, "scheduler: could not set scheduling result in `%s`", path))
|
||||
continue
|
||||
}
|
||||
|
||||
if options.debug {
|
||||
options.logf("sending hosts: %+v", hosts)
|
||||
}
|
||||
// send that on channel!
|
||||
if !send(hosts, nil) {
|
||||
//return // pass instead, let channels clean up
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// kick off an initial campaign if none exist already...
|
||||
options.logf("checking for existing leader...")
|
||||
leaderResult, err := election.Leader(ctx)
|
||||
if err == concurrency.ErrElectionNoLeader {
|
||||
// start up the campaign function
|
||||
if !campaignRunning {
|
||||
campaignClose = make(chan struct{})
|
||||
campaignFunc() // run
|
||||
campaignRunning = true
|
||||
}
|
||||
}
|
||||
if options.debug {
|
||||
if err != nil {
|
||||
options.logf("leader information error: %+v", err)
|
||||
} else {
|
||||
options.logf("leader information: %+v", leaderResult)
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
51
etcd/scheduler/strategy.go
Normal file
@@ -0,0 +1,51 @@
|
||||
// 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 scheduler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// registeredStrategies is a global map of all possible strategy implementations
|
||||
// which can be used. You should never touch this map directly. Use methods like
|
||||
// Register instead.
|
||||
var registeredStrategies = make(map[string]func() Strategy) // must initialize
|
||||
|
||||
// Strategy represents the methods a scheduler strategy must implement.
|
||||
type Strategy interface {
|
||||
Schedule(hostnames map[string]string, opts *schedulerOptions) ([]string, error)
|
||||
}
|
||||
|
||||
// Register takes a func and its name and makes it available for use. It is
|
||||
// commonly called in the init() method of the func at program startup. There is
|
||||
// no matching Unregister function.
|
||||
func Register(name string, fn func() Strategy) {
|
||||
if _, ok := registeredStrategies[name]; ok {
|
||||
panic(fmt.Sprintf("a strategy named %s is already registered", name))
|
||||
}
|
||||
//gob.Register(fn())
|
||||
registeredStrategies[name] = fn
|
||||
}
|
||||
|
||||
type nilStrategy struct {
|
||||
}
|
||||
|
||||
// Schedule returns an error for any scheduling request for this nil strategy.
|
||||
func (obj *nilStrategy) Schedule(hostnames map[string]string, opts *schedulerOptions) ([]string, error) {
|
||||
return nil, fmt.Errorf("scheduler: cannot schedule with nil scheduler")
|
||||
}
|
||||
109
etcd/str.go
Normal file
@@ -0,0 +1,109 @@
|
||||
// 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 etcd
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
etcd "github.com/coreos/etcd/clientv3"
|
||||
errwrap "github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// ErrNotExist is returned when GetStr can not find the requested key.
|
||||
// TODO: https://dave.cheney.net/2016/04/07/constant-errors
|
||||
var ErrNotExist = errors.New("errNotExist")
|
||||
|
||||
// WatchStr returns a channel which spits out events on key activity.
|
||||
// FIXME: It should close the channel when it's done, and spit out errors when
|
||||
// something goes wrong.
|
||||
// XXX: since the caller of this (via the World API) has no way to tell it it's
|
||||
// done, does that mean we leak go-routines since it might still be running, but
|
||||
// perhaps even blocked??? Could this cause a dead-lock? Should we instead return
|
||||
// some sort of struct which has a close method with it to ask for a shutdown?
|
||||
func WatchStr(obj *EmbdEtcd, key string) chan error {
|
||||
// new key structure is $NS/strings/$key = $data
|
||||
path := fmt.Sprintf("%s/strings/%s", NS, key)
|
||||
ch := make(chan error, 1)
|
||||
// FIXME: fix our API so that we get a close event on shutdown.
|
||||
callback := func(re *RE) error {
|
||||
// TODO: is this even needed? it used to happen on conn errors
|
||||
//log.Printf("Etcd: Watch: Path: %v", path) // event
|
||||
if re == nil || re.response.Canceled {
|
||||
return fmt.Errorf("watch is empty") // will cause a CtxError+retry
|
||||
}
|
||||
if len(ch) == 0 { // send event only if one isn't pending
|
||||
ch <- nil // event
|
||||
}
|
||||
return nil
|
||||
}
|
||||
_, _ = obj.AddWatcher(path, callback, true, false, etcd.WithPrefix()) // no need to check errors
|
||||
return ch
|
||||
}
|
||||
|
||||
// GetStr collects the string which matches a global namespace in etcd.
|
||||
func GetStr(obj *EmbdEtcd, key string) (string, error) {
|
||||
// new key structure is $NS/strings/$key = $data
|
||||
path := fmt.Sprintf("%s/strings/%s", NS, key)
|
||||
keyMap, err := obj.Get(path, etcd.WithPrefix())
|
||||
if err != nil {
|
||||
return "", errwrap.Wrapf(err, "could not get strings in: %s", key)
|
||||
}
|
||||
|
||||
if len(keyMap) == 0 {
|
||||
return "", ErrNotExist
|
||||
}
|
||||
|
||||
if count := len(keyMap); count != 1 {
|
||||
return "", fmt.Errorf("returned %d entries", count)
|
||||
}
|
||||
|
||||
val, exists := keyMap[path]
|
||||
if !exists {
|
||||
return "", fmt.Errorf("path `%s` is missing", path)
|
||||
}
|
||||
|
||||
//log.Printf("Etcd: GetStr(%s): %s", key, val)
|
||||
return val, nil
|
||||
}
|
||||
|
||||
// SetStr sets a key and hostname pair to a certain value. If the value is
|
||||
// nil, then it deletes the key. Otherwise the value should point to a string.
|
||||
// TODO: TTL or delete disconnect?
|
||||
func SetStr(obj *EmbdEtcd, key string, data *string) error {
|
||||
// key structure is $NS/strings/$key = $data
|
||||
path := fmt.Sprintf("%s/strings/%s", NS, key)
|
||||
ifs := []etcd.Cmp{} // list matching the desired state
|
||||
ops := []etcd.Op{} // list of ops in this transaction (then)
|
||||
els := []etcd.Op{} // list of ops in this transaction (else)
|
||||
if data == nil { // perform a delete
|
||||
// TODO: use https://github.com/coreos/etcd/pull/7417 if merged
|
||||
//ifs = append(ifs, etcd.KeyExists(path))
|
||||
ifs = append(ifs, etcd.Compare(etcd.Version(path), ">", 0))
|
||||
ops = append(ops, etcd.OpDelete(path))
|
||||
} else {
|
||||
data := *data // get the real value
|
||||
ifs = append(ifs, etcd.Compare(etcd.Value(path), "=", data)) // desired state
|
||||
els = append(els, etcd.OpPut(path, data))
|
||||
}
|
||||
|
||||
// it's important to do this in one transaction, and atomically, because
|
||||
// this way, we only generate one watch event, and only when it's needed
|
||||
_, err := obj.Txn(ifs, ops, els) // TODO: do we need to look at response?
|
||||
return errwrap.Wrapf(err, "could not set strings in: %s", key)
|
||||
}
|
||||
115
etcd/strmap.go
Normal file
@@ -0,0 +1,115 @@
|
||||
// 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 etcd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/purpleidea/mgmt/util"
|
||||
|
||||
etcd "github.com/coreos/etcd/clientv3"
|
||||
errwrap "github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// WatchStrMap returns a channel which spits out events on key activity.
|
||||
// FIXME: It should close the channel when it's done, and spit out errors when
|
||||
// something goes wrong.
|
||||
func WatchStrMap(obj *EmbdEtcd, key string) chan error {
|
||||
// new key structure is $NS/strings/$key/$hostname = $data
|
||||
path := fmt.Sprintf("%s/strings/%s", NS, key)
|
||||
ch := make(chan error, 1)
|
||||
// FIXME: fix our API so that we get a close event on shutdown.
|
||||
callback := func(re *RE) error {
|
||||
// TODO: is this even needed? it used to happen on conn errors
|
||||
//log.Printf("Etcd: Watch: Path: %v", path) // event
|
||||
if re == nil || re.response.Canceled {
|
||||
return fmt.Errorf("watch is empty") // will cause a CtxError+retry
|
||||
}
|
||||
if len(ch) == 0 { // send event only if one isn't pending
|
||||
ch <- nil // event
|
||||
}
|
||||
return nil
|
||||
}
|
||||
_, _ = obj.AddWatcher(path, callback, true, false, etcd.WithPrefix()) // no need to check errors
|
||||
return ch
|
||||
}
|
||||
|
||||
// GetStrMap collects all of the strings which match a namespace in etcd.
|
||||
func GetStrMap(obj *EmbdEtcd, hostnameFilter []string, key string) (map[string]string, error) {
|
||||
// old key structure is $NS/strings/$hostname/$key = $data
|
||||
// new key structure is $NS/strings/$key/$hostname = $data
|
||||
// FIXME: if we have the $key as the last token (old key structure), we
|
||||
// can allow the key to contain the slash char, otherwise we need to
|
||||
// verify that one isn't present in the input string.
|
||||
path := fmt.Sprintf("%s/strings/%s", NS, key)
|
||||
keyMap, err := obj.Get(path, etcd.WithPrefix(), etcd.WithSort(etcd.SortByKey, etcd.SortAscend))
|
||||
if err != nil {
|
||||
return nil, errwrap.Wrapf(err, "could not get strings in: %s", key)
|
||||
}
|
||||
result := make(map[string]string)
|
||||
for key, val := range keyMap {
|
||||
if !strings.HasPrefix(key, path) { // sanity check
|
||||
continue
|
||||
}
|
||||
|
||||
str := strings.Split(key[len(path):], "/")
|
||||
if len(str) != 2 {
|
||||
return nil, fmt.Errorf("unexpected chunk count of %d", len(str))
|
||||
}
|
||||
_, hostname := str[0], str[1]
|
||||
|
||||
if hostname == "" {
|
||||
return nil, fmt.Errorf("unexpected chunk length of %d", len(hostname))
|
||||
}
|
||||
|
||||
// FIXME: ideally this would be a server side filter instead!
|
||||
if len(hostnameFilter) > 0 && !util.StrInList(hostname, hostnameFilter) {
|
||||
continue
|
||||
}
|
||||
//log.Printf("Etcd: GetStr(%s): (Hostname, Data): (%s, %s)", key, hostname, val)
|
||||
result[hostname] = val
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// SetStrMap sets a key and hostname pair to a certain value. If the value is
|
||||
// nil, then it deletes the key. Otherwise the value should point to a string.
|
||||
// TODO: TTL or delete disconnect?
|
||||
func SetStrMap(obj *EmbdEtcd, hostname, key string, data *string) error {
|
||||
// key structure is $NS/strings/$key/$hostname = $data
|
||||
path := fmt.Sprintf("%s/strings/%s/%s", NS, key, hostname)
|
||||
ifs := []etcd.Cmp{} // list matching the desired state
|
||||
ops := []etcd.Op{} // list of ops in this transaction (then)
|
||||
els := []etcd.Op{} // list of ops in this transaction (else)
|
||||
if data == nil { // perform a delete
|
||||
// TODO: use https://github.com/coreos/etcd/pull/7417 if merged
|
||||
//ifs = append(ifs, etcd.KeyExists(path))
|
||||
ifs = append(ifs, etcd.Compare(etcd.Version(path), ">", 0))
|
||||
ops = append(ops, etcd.OpDelete(path))
|
||||
} else {
|
||||
data := *data // get the real value
|
||||
ifs = append(ifs, etcd.Compare(etcd.Value(path), "=", data)) // desired state
|
||||
els = append(els, etcd.OpPut(path, data))
|
||||
}
|
||||
|
||||
// it's important to do this in one transaction, and atomically, because
|
||||
// this way, we only generate one watch event, and only when it's needed
|
||||
_, err := obj.Txn(ifs, ops, els) // TODO: do we need to look at response?
|
||||
return errwrap.Wrapf(err, "could not set strings in: %s", key)
|
||||
}
|
||||
152
etcd/world.go
Normal file
@@ -0,0 +1,152 @@
|
||||
// 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 etcd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
etcdfs "github.com/purpleidea/mgmt/etcd/fs"
|
||||
"github.com/purpleidea/mgmt/etcd/scheduler"
|
||||
"github.com/purpleidea/mgmt/resources"
|
||||
)
|
||||
|
||||
// World is an etcd backed implementation of the World interface.
|
||||
type World struct {
|
||||
Hostname string // uuid for the consumer of these
|
||||
EmbdEtcd *EmbdEtcd
|
||||
MetadataPrefix string // expected metadata prefix
|
||||
StoragePrefix string // storage prefix for etcdfs storage
|
||||
StandaloneFs resources.Fs // store an fs here for local usage
|
||||
Debug bool
|
||||
Logf func(format string, v ...interface{})
|
||||
}
|
||||
|
||||
// ResWatch returns a channel which spits out events on possible exported
|
||||
// resource changes.
|
||||
func (obj *World) ResWatch() chan error {
|
||||
return WatchResources(obj.EmbdEtcd)
|
||||
}
|
||||
|
||||
// ResExport exports a list of resources under our hostname namespace.
|
||||
// Subsequent calls replace the previously set collection atomically.
|
||||
func (obj *World) ResExport(resourceList []resources.Res) error {
|
||||
return SetResources(obj.EmbdEtcd, obj.Hostname, resourceList)
|
||||
}
|
||||
|
||||
// ResCollect gets the collection of exported resources which match the filter.
|
||||
// It does this atomically so that a call always returns a complete collection.
|
||||
func (obj *World) ResCollect(hostnameFilter, kindFilter []string) ([]resources.Res, error) {
|
||||
// XXX: should we be restricted to retrieving resources that were
|
||||
// exported with a tag that allows or restricts our hostname? We could
|
||||
// enforce that here if the underlying API supported it... Add this?
|
||||
return GetResources(obj.EmbdEtcd, hostnameFilter, kindFilter)
|
||||
}
|
||||
|
||||
// StrWatch returns a channel which spits out events on possible string changes.
|
||||
func (obj *World) StrWatch(namespace string) chan error {
|
||||
return WatchStr(obj.EmbdEtcd, namespace)
|
||||
}
|
||||
|
||||
// StrIsNotExist returns whether the error from StrGet is a key missing error.
|
||||
func (obj *World) StrIsNotExist(err error) bool {
|
||||
return err == ErrNotExist
|
||||
}
|
||||
|
||||
// StrGet returns the value for the the given namespace.
|
||||
func (obj *World) StrGet(namespace string) (string, error) {
|
||||
return GetStr(obj.EmbdEtcd, namespace)
|
||||
}
|
||||
|
||||
// StrSet sets the namespace value to a particular string.
|
||||
func (obj *World) StrSet(namespace, value string) error {
|
||||
return SetStr(obj.EmbdEtcd, namespace, &value)
|
||||
}
|
||||
|
||||
// StrDel deletes the value in a particular namespace.
|
||||
func (obj *World) StrDel(namespace string) error {
|
||||
return SetStr(obj.EmbdEtcd, namespace, nil)
|
||||
}
|
||||
|
||||
// StrMapWatch returns a channel which spits out events on possible string changes.
|
||||
func (obj *World) StrMapWatch(namespace string) chan error {
|
||||
return WatchStrMap(obj.EmbdEtcd, namespace)
|
||||
}
|
||||
|
||||
// StrMapGet returns a map of hostnames to values in the given namespace.
|
||||
func (obj *World) StrMapGet(namespace string) (map[string]string, error) {
|
||||
return GetStrMap(obj.EmbdEtcd, []string{}, namespace)
|
||||
}
|
||||
|
||||
// StrMapSet sets the namespace value to a particular string under the identity
|
||||
// of its own hostname.
|
||||
func (obj *World) StrMapSet(namespace, value string) error {
|
||||
return SetStrMap(obj.EmbdEtcd, obj.Hostname, namespace, &value)
|
||||
}
|
||||
|
||||
// StrMapDel deletes the value in a particular namespace.
|
||||
func (obj *World) StrMapDel(namespace string) error {
|
||||
return SetStrMap(obj.EmbdEtcd, obj.Hostname, namespace, nil)
|
||||
}
|
||||
|
||||
// Scheduler returns a scheduling result of hosts in a particular namespace.
|
||||
func (obj *World) Scheduler(namespace string, opts ...scheduler.Option) (*scheduler.Result, error) {
|
||||
modifiedOpts := []scheduler.Option{}
|
||||
for _, o := range opts {
|
||||
modifiedOpts = append(modifiedOpts, o) // copy in
|
||||
}
|
||||
|
||||
modifiedOpts = append(modifiedOpts, scheduler.Debug(obj.Debug))
|
||||
modifiedOpts = append(modifiedOpts, scheduler.Logf(obj.Logf))
|
||||
|
||||
return scheduler.Schedule(obj.EmbdEtcd.GetClient(), fmt.Sprintf("%s/scheduler/%s", NS, namespace), obj.Hostname, modifiedOpts...)
|
||||
}
|
||||
|
||||
// Fs returns a distributed file system from a unique URI. For single host
|
||||
// execution that doesn't span more than a single host, this file system might
|
||||
// actually be a local or memory backed file system, so actually only
|
||||
// distributed within the boredom that is a single host cluster.
|
||||
func (obj *World) Fs(uri string) (resources.Fs, error) {
|
||||
u, err := url.Parse(uri)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// we're in standalone mode
|
||||
if u.Scheme == "memmapfs" && u.Path == "/" {
|
||||
return obj.StandaloneFs, nil
|
||||
}
|
||||
|
||||
if u.Scheme != "etcdfs" {
|
||||
return nil, fmt.Errorf("unknown scheme: `%s`", u.Scheme)
|
||||
}
|
||||
if u.Path == "" {
|
||||
return nil, fmt.Errorf("empty path: %s", u.Path)
|
||||
}
|
||||
if !strings.HasPrefix(u.Path, obj.MetadataPrefix) {
|
||||
return nil, fmt.Errorf("wrong path prefix: %s", u.Path)
|
||||
}
|
||||
|
||||
etcdFs := &etcdfs.Fs{
|
||||
Client: obj.EmbdEtcd.GetClient(),
|
||||
Metadata: u.Path,
|
||||
DataPrefix: obj.StoragePrefix,
|
||||
}
|
||||
return etcdFs, nil
|
||||
}
|
||||
55
event.go
@@ -1,55 +0,0 @@
|
||||
// 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/>.
|
||||
|
||||
package main
|
||||
|
||||
//go:generate stringer -type=eventName -output=eventname_stringer.go
|
||||
type eventName int
|
||||
|
||||
const (
|
||||
eventExit eventName = iota
|
||||
eventStart
|
||||
eventPause
|
||||
eventPoke
|
||||
eventBackPoke
|
||||
)
|
||||
|
||||
type Event struct {
|
||||
Name eventName
|
||||
Resp chan bool // channel to send an ack response on, nil to skip
|
||||
//Wg *sync.WaitGroup // receiver barrier to Wait() for everyone else on
|
||||
Msg string // some words for fun
|
||||
Activity bool // did something interesting happen?
|
||||
}
|
||||
|
||||
// send a single acknowledgement on the channel if one was requested
|
||||
func (event *Event) ACK() {
|
||||
if event.Resp != nil { // if they've requested an ACK
|
||||
event.Resp <- true // send ACK
|
||||
}
|
||||
}
|
||||
|
||||
func (event *Event) NACK() {
|
||||
if event.Resp != nil { // if they've requested an ACK
|
||||
event.Resp <- false // send NACK
|
||||
}
|
||||
}
|
||||
|
||||
// get the activity value
|
||||
func (event *Event) GetActivity() bool {
|
||||
return event.Activity
|
||||
}
|
||||
119
event/event.go
Normal file
@@ -0,0 +1,119 @@
|
||||
// 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
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
//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 (
|
||||
EventNil Kind = iota
|
||||
EventExit
|
||||
EventStart
|
||||
EventPause
|
||||
EventPoke
|
||||
EventBackPoke
|
||||
)
|
||||
|
||||
// Resp is a channel to be used for boolean responses. A nil represents an ACK,
|
||||
// and a non-nil represents a NACK (false). This also lets us use custom errors.
|
||||
type Resp chan error
|
||||
|
||||
// Event is the main struct that stores event information and responses.
|
||||
type Event struct {
|
||||
Kind Kind
|
||||
Resp Resp // channel to send an ack response on, nil to skip
|
||||
//Wg *sync.WaitGroup // receiver barrier to Wait() for everyone else on
|
||||
Err error // store an error in our event
|
||||
}
|
||||
|
||||
// ACK sends a single acknowledgement on the channel if one was requested.
|
||||
func (event *Event) ACK() {
|
||||
if event.Resp != nil { // if they've requested an ACK
|
||||
event.Resp.ACK()
|
||||
}
|
||||
}
|
||||
|
||||
// NACK sends a negative acknowledgement message on the channel if one was requested.
|
||||
func (event *Event) NACK() {
|
||||
if event.Resp != nil { // if they've requested a NACK
|
||||
event.Resp.NACK()
|
||||
}
|
||||
}
|
||||
|
||||
// ACKNACK sends a custom ACK or NACK message on the channel if one was requested.
|
||||
func (event *Event) ACKNACK(err error) {
|
||||
if event.Resp != nil { // if they've requested a NACK
|
||||
event.Resp.ACKNACK(err)
|
||||
}
|
||||
}
|
||||
|
||||
// NewResp is just a helper to return the right type of response channel.
|
||||
func NewResp() Resp {
|
||||
resp := make(chan error)
|
||||
return resp
|
||||
}
|
||||
|
||||
// ACK sends a true value to resp.
|
||||
func (resp Resp) ACK() {
|
||||
if resp != nil {
|
||||
resp <- nil // TODO: close instead?
|
||||
}
|
||||
}
|
||||
|
||||
// NACK sends a false value to resp.
|
||||
func (resp Resp) NACK() {
|
||||
if resp != nil {
|
||||
resp <- fmt.Errorf("NACK")
|
||||
}
|
||||
}
|
||||
|
||||
// ACKNACK sends a custom ACK or NACK. The ACK value is always nil, the NACK can
|
||||
// be any non-nil error value.
|
||||
func (resp Resp) ACKNACK(err error) {
|
||||
if resp != nil {
|
||||
resp <- err
|
||||
}
|
||||
}
|
||||
|
||||
// Wait waits for any response from a Resp channel and returns it.
|
||||
func (resp Resp) Wait() error {
|
||||
return <-resp
|
||||
}
|
||||
|
||||
// ACKWait waits for a +ive Ack from a Resp channel.
|
||||
func (resp Resp) ACKWait() {
|
||||
for {
|
||||
// wait until true value
|
||||
if resp.Wait() == nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Error returns the stored error value.
|
||||
func (event *Event) Error() error {
|
||||
return event.Err
|
||||
}
|
||||
14
examples/hcl/graph0.hcl
Normal file
@@ -0,0 +1,14 @@
|
||||
resource "file" "file1" {
|
||||
path = "/tmp/mgmt-hello-world"
|
||||
content = "hello, world"
|
||||
state = "exists"
|
||||
depends_on = ["noop.noop1", "exec.sleep"]
|
||||
}
|
||||
|
||||
resource "noop" "noop1" {
|
||||
test = "nil"
|
||||
}
|
||||
|
||||
resource "exec" "sleep" {
|
||||
cmd = "sleep 10s"
|
||||
}
|
||||
4
examples/hcl/graph1.hcl
Normal file
@@ -0,0 +1,4 @@
|
||||
resource "exec" "exec1" {
|
||||
cmd = "cat /tmp/mgmt-hello-world"
|
||||
state = "present"
|
||||
}
|
||||
9
examples/hcl/hil.hcl
Normal file
@@ -0,0 +1,9 @@
|
||||
resource "file" "file1" {
|
||||
path = "/tmp/mgmt-hello-world"
|
||||
content = "${exec.sleep.Output}"
|
||||
state = "exists"
|
||||
}
|
||||
|
||||
resource "exec" "sleep" {
|
||||
cmd = "echo hello"
|
||||
}
|
||||
4
examples/lang/answer.mcl
Normal file
@@ -0,0 +1,4 @@
|
||||
# it was a lovely surprise to me, when i realized that mgmt had the answer!
|
||||
print "answer" {
|
||||
msg => printf("the answer to life, the universe, and everything is: %d", answer()),
|
||||
}
|
||||
22
examples/lang/contains0.mcl
Normal file
@@ -0,0 +1,22 @@
|
||||
$set = ["a", "b", "c", "d",]
|
||||
|
||||
$c1 = "x1" in ["x1", "x2", "x3",]
|
||||
$c2 = 42 in [4, 13, 42,]
|
||||
$c3 = "x" in $set
|
||||
$c4 = "b" in $set
|
||||
|
||||
$s = printf("1: %t, 2: %t, 3: %t, 4: %t\n", $c1, $c2, $c3, $c4)
|
||||
|
||||
file "/tmp/mgmt/contains" {
|
||||
content => $s,
|
||||
}
|
||||
|
||||
$x = if hostname() in ["h1", "h3",] {
|
||||
printf("i (%s) am one of the chosen few!\n", hostname())
|
||||
} else {
|
||||
printf("i (%s) was not chosen :(\n", hostname())
|
||||
}
|
||||
|
||||
file "/tmp/mgmt/hello-${hostname()}" {
|
||||
content => $x,
|
||||
}
|
||||
4
examples/lang/datetime1.mcl
Normal file
@@ -0,0 +1,4 @@
|
||||
$d = datetime()
|
||||
file "/tmp/mgmt/datetime" {
|
||||
content => template("Hello! It is now: {{ datetime_print . }}\n", $d),
|
||||
}
|
||||
14
examples/lang/datetime2.mcl
Normal file
@@ -0,0 +1,14 @@
|
||||
$secplusone = datetime() + $ayear
|
||||
|
||||
# note the order of the assignment (year can come later in the code)
|
||||
$ayear = 60 * 60 * 24 * 365 # is a year in seconds (31536000)
|
||||
|
||||
$tmplvalues = struct{year => $secplusone, load => $theload,}
|
||||
|
||||
$theload = structlookup(load(), "x1")
|
||||
|
||||
if 5 > 3 {
|
||||
file "/tmp/mgmt/datetime" {
|
||||
content => template("Now + 1 year is: {{ .year }} seconds, aka: {{ datetime_print .year }}\n\nload average: {{ .load }}\n", $tmplvalues),
|
||||
}
|
||||
}
|
||||
14
examples/lang/datetime3.mcl
Normal file
@@ -0,0 +1,14 @@
|
||||
$secplusone = datetime() + $ayear
|
||||
|
||||
# note the order of the assignment (year can come later in the code)
|
||||
$ayear = 60 * 60 * 24 * 365 # is a year in seconds (31536000)
|
||||
|
||||
$tmplvalues = struct{year => $secplusone, load => $theload, vumeter => $vumeter,}
|
||||
|
||||
$theload = structlookup(load(), "x1")
|
||||
|
||||
$vumeter = vumeter("====", 10, 0.9)
|
||||
|
||||
file "/tmp/mgmt/datetime" {
|
||||
content => template("Now + 1 year is: {{ .year }} seconds, aka: {{ datetime_print .year }}\n\nload average: {{ .load }}\n\nvu: {{ .vumeter }}\n", $tmplvalues),
|
||||
}
|
||||
14
examples/lang/edges0.mcl
Normal file
@@ -0,0 +1,14 @@
|
||||
exec "exec0" {
|
||||
cmd => "sleep 10s",
|
||||
shell => "/bin/bash",
|
||||
}
|
||||
exec "exec1" {
|
||||
cmd => "sleep 10s",
|
||||
shell => "/bin/bash",
|
||||
}
|
||||
exec "exec2" {
|
||||
cmd => "sleep 10s",
|
||||
shell => "/bin/bash",
|
||||
}
|
||||
|
||||
Exec["exec0"] -> Exec["exec1"] -> Exec["exec2"]
|
||||
18
examples/lang/edges1.mcl
Normal file
@@ -0,0 +1,18 @@
|
||||
$b = true
|
||||
if $b {
|
||||
exec "exec0" {
|
||||
cmd => "sleep 10s",
|
||||
shell => "/bin/bash",
|
||||
}
|
||||
}
|
||||
exec "exec1" {
|
||||
cmd => "sleep 10s",
|
||||
shell => "/bin/bash",
|
||||
|
||||
Depend => $b ?: Exec["exec0"],
|
||||
Before => Exec["exec2"],
|
||||
}
|
||||
exec "exec2" {
|
||||
cmd => "sleep 10s",
|
||||
shell => "/bin/bash",
|
||||
}
|
||||
5
examples/lang/elvis0.mcl
Normal file
@@ -0,0 +1,5 @@
|
||||
$b = true # change me to false and then try editing the file manually
|
||||
file "/tmp/mgmt-elvis" {
|
||||
content => $b ?: "hello world\n",
|
||||
state => "exists",
|
||||
}
|
||||
20
examples/lang/env0.mcl
Normal file
@@ -0,0 +1,20 @@
|
||||
# read and print environment variable
|
||||
# env TEST=123 EMPTY= ./mgmt run --tmp-prefix --lang=examples/lang/env0.mcl --converged-timeout=5
|
||||
|
||||
$x = getenv("TEST", "321")
|
||||
|
||||
print "print1" {
|
||||
msg => printf("the value of the environment variable TEST is: %s", $x),
|
||||
}
|
||||
|
||||
$y = getenv("DOESNOTEXIT", "321")
|
||||
|
||||
print "print2" {
|
||||
msg => printf("environment variable DOESNOTEXIT does not exist, defaulting to: %s", $y),
|
||||
}
|
||||
|
||||
$z = getenv("EMPTY", "456")
|
||||
|
||||
print "print3" {
|
||||
msg => printf("same goes for epmty variables like EMPTY: %s", $z),
|
||||
}
|
||||
10
examples/lang/env1.mcl
Normal file
@@ -0,0 +1,10 @@
|
||||
$env = env()
|
||||
$m = maplookup($env, "GOPATH", "")
|
||||
|
||||
print "print0" {
|
||||
msg => if hasenv("GOPATH") {
|
||||
printf("GOPATH is: %s", $m)
|
||||
} else {
|
||||
"GOPATH is missing!"
|
||||
},
|
||||
}
|
||||