Compare commits
966 Commits
v0.7.0-rc3
...
v1.4.0-rc1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2df71b654e | ||
|
|
c126ebe8da | ||
|
|
4611e07d90 | ||
|
|
aa14311c8c | ||
|
|
15fc665978 | ||
|
|
013e77fe9d | ||
|
|
3669df9599 | ||
|
|
9fd78909f3 | ||
|
|
0981bbf0d3 | ||
|
|
3b4153f538 | ||
|
|
5de9b00fc8 | ||
|
|
a9eb57759c | ||
|
|
8a132649e0 | ||
|
|
41a3330d89 | ||
|
|
21f18fd121 | ||
|
|
20c3650c09 | ||
|
|
738847dcf9 | ||
|
|
de5a1991f4 | ||
|
|
08fbe26c67 | ||
|
|
ba388b6bc6 | ||
|
|
da8d5ed027 | ||
|
|
b4cd6af468 | ||
|
|
592709a25c | ||
|
|
ccc330a43e | ||
|
|
5c07c08105 | ||
|
|
b8e442fa69 | ||
|
|
a999a5b8db | ||
|
|
80c99642b2 | ||
|
|
c73954fab0 | ||
|
|
fc8ef7d98d | ||
|
|
89ff2e2298 | ||
|
|
0ac085b273 | ||
|
|
9389687557 | ||
|
|
00956038f9 | ||
|
|
2df6bdcd66 | ||
|
|
54b17af985 | ||
|
|
fd544f1f9e | ||
|
|
b1ed273b64 | ||
|
|
ee998fc259 | ||
|
|
fc17e89393 | ||
|
|
4876087067 | ||
|
|
19a8103eb7 | ||
|
|
c320736b7a | ||
|
|
a16c56f7be | ||
|
|
7d86fa5f8b | ||
|
|
f6a76a10ae | ||
|
|
d263be4bae | ||
|
|
9c9c3ce141 | ||
|
|
67961c9349 | ||
|
|
204011e401 | ||
|
|
9ced2ba666 | ||
|
|
fb2acdb1f0 | ||
|
|
34b7ab73c7 | ||
|
|
c5f1b28af8 | ||
|
|
43f483a5ef | ||
|
|
a7ba5d045b | ||
|
|
48e9314d0c | ||
|
|
231ece3a9e | ||
|
|
b5ef0f1c4e | ||
|
|
947049cc3c | ||
|
|
4cb3e0fcb7 | ||
|
|
8cda43a68a | ||
|
|
22cac7abed | ||
|
|
a29eee070b | ||
|
|
d9d48a1905 | ||
|
|
a268907302 | ||
|
|
1c2e55ed17 | ||
|
|
82aaa413f5 | ||
|
|
a08ad16a01 | ||
|
|
6bd6f0c43c | ||
|
|
b512a9336a | ||
|
|
d520ef1a1b | ||
|
|
992142b8ea | ||
|
|
41543d533f | ||
|
|
3cfd6a63a0 | ||
|
|
b5dd1d3f1c | ||
|
|
7250ee8ab4 | ||
|
|
e58456ead9 | ||
|
|
51f18089c4 | ||
|
|
c6c57a8aaf | ||
|
|
1ac26d6094 | ||
|
|
f40468c8b1 | ||
|
|
44552d55d0 | ||
|
|
480e45cd01 | ||
|
|
8b196aaf35 | ||
|
|
099aa4f9c0 | ||
|
|
9ab2a58e4f | ||
|
|
32fe594212 | ||
|
|
f7a49abdeb | ||
|
|
a9bbe5046a | ||
|
|
5494e76041 | ||
|
|
493e12b9a3 | ||
|
|
3b92e04065 | ||
|
|
b08d29cc2c | ||
|
|
8385ce5b00 | ||
|
|
c89a1f01b6 | ||
|
|
f458e0408d | ||
|
|
0a26e696ee | ||
|
|
ec9f6b3031 | ||
|
|
285a82dc96 | ||
|
|
c9173bbda1 | ||
|
|
87091ba83d | ||
|
|
55368a3897 | ||
|
|
dab1c4ffb3 | ||
|
|
a5af88224a | ||
|
|
868a4b01ce | ||
|
|
4de1396a46 | ||
|
|
0e586b7996 | ||
|
|
b9fea2e8d3 | ||
|
|
c3838be166 | ||
|
|
958bbef1f1 | ||
|
|
bd845cca31 | ||
|
|
543f106689 | ||
|
|
60909e435f | ||
|
|
7e912e12e8 | ||
|
|
2dda87e2c4 | ||
|
|
f87dc8078a | ||
|
|
47195af80a | ||
|
|
bbe86be115 | ||
|
|
c47e8c5bf7 | ||
|
|
d79e5e8753 | ||
|
|
c5748f31d5 | ||
|
|
75c4b3e020 | ||
|
|
65df13e0be | ||
|
|
349203b058 | ||
|
|
5ec984177d | ||
|
|
6889096ff4 | ||
|
|
7b474cd905 | ||
|
|
2f8eaa3314 | ||
|
|
d8bba34a87 | ||
|
|
fda647eec3 | ||
|
|
f7327d764f | ||
|
|
d1971fbd0e | ||
|
|
5d82fcb029 | ||
|
|
0b8b0dc1e1 | ||
|
|
2751745a59 | ||
|
|
4769f143cf | ||
|
|
baee5d18ea | ||
|
|
28a487018e | ||
|
|
1b3395523c | ||
|
|
a608098c39 | ||
|
|
4b6ffe752e | ||
|
|
af000395b4 | ||
|
|
b92a7752f4 | ||
|
|
42ae262c3f | ||
|
|
ab9f22a954 | ||
|
|
a97854e089 | ||
|
|
449633abaa | ||
|
|
fa14b6960b | ||
|
|
2d65164e57 | ||
|
|
21714bb8d3 | ||
|
|
5e57bc0c99 | ||
|
|
7feee617a7 | ||
|
|
6214ab5c19 | ||
|
|
70bcc448b7 | ||
|
|
86574b8b61 | ||
|
|
6a87cc6556 | ||
|
|
f34acf272c | ||
|
|
bd8e8d051b | ||
|
|
bb0cc11e93 | ||
|
|
8b5efd8d47 | ||
|
|
48b2dee10e | ||
|
|
35612e3221 | ||
|
|
7e7c134a80 | ||
|
|
27be3cd7b3 | ||
|
|
3b918c4cc9 | ||
|
|
6433676991 | ||
|
|
abc00e7ca9 | ||
|
|
cccaaeac86 | ||
|
|
40148d227f | ||
|
|
48d602140c | ||
|
|
5dfc818303 | ||
|
|
0e053faec0 | ||
|
|
3650b75377 | ||
|
|
f27af999d9 | ||
|
|
2e60e54ab0 | ||
|
|
76e2f24bd8 | ||
|
|
15f152ce73 | ||
|
|
25ffc9b694 | ||
|
|
c79ca2e16e | ||
|
|
4d242bd5c4 | ||
|
|
96257a5e18 | ||
|
|
38ac3b05fb | ||
|
|
f793518aa6 | ||
|
|
69b54017a9 | ||
|
|
a49344e70b | ||
|
|
3a7cde6d76 | ||
|
|
e35449ba91 | ||
|
|
5dd11c3ddc | ||
|
|
19ccdc0456 | ||
|
|
7a0ebc409b | ||
|
|
cb36d1c400 | ||
|
|
b5e885251b | ||
|
|
b30d6db3b0 | ||
|
|
b35d682cca | ||
|
|
79e7388d65 | ||
|
|
5b812cf815 | ||
|
|
ba4cbcd1d9 | ||
|
|
5b00d8ee7a | ||
|
|
1e2e950709 | ||
|
|
f4912b3ff9 | ||
|
|
6a09845340 | ||
|
|
c8915d646d | ||
|
|
67d932d9f8 | ||
|
|
5716be7a34 | ||
|
|
c6510571c5 | ||
|
|
7b0d400693 | ||
|
|
9c3321d4a3 | ||
|
|
c291763251 | ||
|
|
cc58b8c6b2 | ||
|
|
2719d8a8e4 | ||
|
|
d289b153a4 | ||
|
|
b630bc836b | ||
|
|
170df073e6 | ||
|
|
47320f9350 | ||
|
|
f9dd8945e6 | ||
|
|
63c75c68cb | ||
|
|
a3f942f03b | ||
|
|
d244043ce7 | ||
|
|
3de5a836c9 | ||
|
|
0ecdfff839 | ||
|
|
12409e7e20 | ||
|
|
732f1924e1 | ||
|
|
6dc7faf9b2 | ||
|
|
5ba2c76c71 | ||
|
|
a51d688f5f | ||
|
|
24f45e4eba | ||
|
|
4e6fff8ed1 | ||
|
|
d36d4555aa | ||
|
|
ad64c50cc1 | ||
|
|
cbfe50c5ee | ||
|
|
2cd3cb2442 | ||
|
|
32061238aa | ||
|
|
a40ac4ea03 | ||
|
|
75d384bbe8 | ||
|
|
4fcc3a880c | ||
|
|
424b7e5b9b | ||
|
|
c7b764693d | ||
|
|
1ef301c631 | ||
|
|
ce9ae25741 | ||
|
|
7cb46e1919 | ||
|
|
cbcdb2628c | ||
|
|
7fb9afe39c | ||
|
|
6f33622a12 | ||
|
|
765a7c3ed4 | ||
|
|
a04c0f3740 | ||
|
|
6fcc1e3967 | ||
|
|
d9d3c2b0d8 | ||
|
|
d859052453 | ||
|
|
180fe241d8 | ||
|
|
553fed3eea | ||
|
|
c94a683b87 | ||
|
|
45e422d01e | ||
|
|
fecbb9df2e | ||
|
|
53225f6e9b | ||
|
|
09569f68bd | ||
|
|
fd5c81978a | ||
|
|
2a7da35139 | ||
|
|
a0fcec674f | ||
|
|
a4e7036086 | ||
|
|
03f90fd748 | ||
|
|
4fc82b69ef | ||
|
|
a366336895 | ||
|
|
af965e9446 | ||
|
|
8327006f61 | ||
|
|
242844f084 | ||
|
|
420d17ad27 | ||
|
|
f36eb70e74 | ||
|
|
1048a4eead | ||
|
|
92f5dd3752 | ||
|
|
fca70ede1b | ||
|
|
8dc7fb9494 | ||
|
|
f45243eae9 | ||
|
|
0ccc5ad735 | ||
|
|
bf98a1ae3f | ||
|
|
bb20e96a98 | ||
|
|
43c620c4d8 | ||
|
|
47261eab01 | ||
|
|
3b4d73e106 | ||
|
|
33a60488cd | ||
|
|
7615c26f44 | ||
|
|
402af04b44 | ||
|
|
cbe24ca06b | ||
|
|
de8faafb72 | ||
|
|
c67eba4dbb | ||
|
|
671a78ac08 | ||
|
|
b733bde9cd | ||
|
|
204facc395 | ||
|
|
8289d4e1bb | ||
|
|
85436f675b | ||
|
|
1f045bc696 | ||
|
|
b4584a616a | ||
|
|
fd8a4df4a6 | ||
|
|
340bb42160 | ||
|
|
0d9fd52c42 | ||
|
|
c5d4cb91c3 | ||
|
|
5e4b5975a9 | ||
|
|
bc04aa99ed | ||
|
|
a8da4c7e78 | ||
|
|
adb2d5d697 | ||
|
|
0fb89736e4 | ||
|
|
f673138932 | ||
|
|
8a741c5d32 | ||
|
|
b1d9732f65 | ||
|
|
5078c80c36 | ||
|
|
305155f92a | ||
|
|
959cdd5ddd | ||
|
|
a281a84cca | ||
|
|
3b731ade43 | ||
|
|
cdb26fac99 | ||
|
|
45155f4e6a | ||
|
|
487610f6c5 | ||
|
|
b328ecaec3 | ||
|
|
e466ea667a | ||
|
|
260379d2b7 | ||
|
|
fb96c470a9 | ||
|
|
19e2c91f1b | ||
|
|
28c3181518 | ||
|
|
39e1339fb0 | ||
|
|
6c6d23b649 | ||
|
|
a2e3c9aa50 | ||
|
|
09bd518cd0 | ||
|
|
5dd92a610b | ||
|
|
2bbd3b375a | ||
|
|
ecd2853bdb | ||
|
|
af60ac5798 | ||
|
|
49e4315251 | ||
|
|
3c34f77616 | ||
|
|
9106a97f34 | ||
|
|
68b005bc50 | ||
|
|
94a4fe7778 | ||
|
|
96b8a83c35 | ||
|
|
59bd47a0e6 | ||
|
|
4c49c8fef5 | ||
|
|
75168c6d11 | ||
|
|
3978d93fca | ||
|
|
034073b8ab | ||
|
|
ed3f08f0f9 | ||
|
|
8d894ba396 | ||
|
|
cc133372a7 | ||
|
|
39922220b0 | ||
|
|
d789c9fba9 | ||
|
|
b5ca78c269 | ||
|
|
c539270c2a | ||
|
|
ff3db59776 | ||
|
|
922b23eb78 | ||
|
|
1e3cd14af7 | ||
|
|
3e68d3c92c | ||
|
|
853d27dffc | ||
|
|
284f029b19 | ||
|
|
437034cb48 | ||
|
|
06743261b9 | ||
|
|
c467aedcda | ||
|
|
f9cbc5ce34 | ||
|
|
172a4782df | ||
|
|
4ec338a9e1 | ||
|
|
ad1fb97378 | ||
|
|
51aff79c7e | ||
|
|
e37b7c5331 | ||
|
|
4551278b99 | ||
|
|
60ac78b816 | ||
|
|
e3268e2b62 | ||
|
|
26939ebb7e | ||
|
|
1eb6991798 | ||
|
|
d0bfdb444e | ||
|
|
6bbde90e0a | ||
|
|
d52b995450 | ||
|
|
f0b9928541 | ||
|
|
d11a92418d | ||
|
|
411810bb2d | ||
|
|
0f0c9b6149 | ||
|
|
7805c91f98 | ||
|
|
ef7c16d4d4 | ||
|
|
ff87df4231 | ||
|
|
cfd9e80ac6 | ||
|
|
7d4685dcdd | ||
|
|
272f3942f1 | ||
|
|
abaecf4e44 | ||
|
|
e054331f11 | ||
|
|
53fced8122 | ||
|
|
b033444d20 | ||
|
|
242d42fbb1 | ||
|
|
62c9096164 | ||
|
|
9962e4c232 | ||
|
|
d2544357dc | ||
|
|
b27fe9422c | ||
|
|
bd857716a3 | ||
|
|
85c8916f4e | ||
|
|
b466ac9d45 | ||
|
|
3aac1ad148 | ||
|
|
a692402798 | ||
|
|
6d5fc4c499 | ||
|
|
42e366a821 | ||
|
|
b217ad5732 | ||
|
|
4ae80319dd | ||
|
|
096d990b4e | ||
|
|
b395b93571 | ||
|
|
720b54f1fd | ||
|
|
bed064419b | ||
|
|
f0f990f08d | ||
|
|
b22bf7476c | ||
|
|
6520d8a1b9 | ||
|
|
021c3a0d8d | ||
|
|
1655c1963e | ||
|
|
bbb904b149 | ||
|
|
1889d95199 | ||
|
|
69f876b73e | ||
|
|
f7605990a5 | ||
|
|
4602ccc2cb | ||
|
|
4e16b1c9bd | ||
|
|
1e26b0b687 | ||
|
|
cf89c124a4 | ||
|
|
923e4629a5 | ||
|
|
6e5433df0d | ||
|
|
cca8fe280d | ||
|
|
e7ff8e65e9 | ||
|
|
070ba1a023 | ||
|
|
15eab4a4b0 | ||
|
|
d038f8fd7d | ||
|
|
b6847b40d0 | ||
|
|
aef937609e | ||
|
|
70ea28669a | ||
|
|
063f12dabe | ||
|
|
6249708038 | ||
|
|
1048c939ac | ||
|
|
fd9a9d2ad2 | ||
|
|
7530cb4374 | ||
|
|
e51a391cc5 | ||
|
|
187f5d27d0 | ||
|
|
6911b4c01d | ||
|
|
f242815b6d | ||
|
|
d6d891ced9 | ||
|
|
922fa42205 | ||
|
|
6605c3bbd4 | ||
|
|
73f12a9004 | ||
|
|
cdc3a189ad | ||
|
|
db096d6e76 | ||
|
|
189df7e911 | ||
|
|
a4bfa75289 | ||
|
|
bc12860e7e | ||
|
|
b573e0f378 | ||
|
|
1332d3d5e1 | ||
|
|
fc2d4f5c71 | ||
|
|
2cd67d9bbe | ||
|
|
32714dbde2 | ||
|
|
5941a6ac4b | ||
|
|
5ceb1dd9ee | ||
|
|
6a07450503 | ||
|
|
ff2d8e0613 | ||
|
|
a21c414ce1 | ||
|
|
56ac7f1f87 | ||
|
|
d86975ba57 | ||
|
|
acc60f63e2 | ||
|
|
6ae1a92da7 | ||
|
|
4eee06e578 | ||
|
|
e05f30c4fc | ||
|
|
9e0302fd85 | ||
|
|
7b2b3a3fb8 | ||
|
|
2a6e06fdc6 | ||
|
|
f38a4eadda | ||
|
|
3f0e76e866 | ||
|
|
2c1e20662c | ||
|
|
2e9b86757b | ||
|
|
3e331c8ac5 | ||
|
|
73617b8a5a | ||
|
|
73ff97e465 | ||
|
|
79719e74c8 | ||
|
|
8491a7cedd | ||
|
|
a0061beedf | ||
|
|
8ecd6a45a1 | ||
|
|
a90bce0d23 | ||
|
|
4291cbfc86 | ||
|
|
544695d670 | ||
|
|
f50e9fc8a5 | ||
|
|
75f8c5c4ff | ||
|
|
738cfefdbd | ||
|
|
4a518ebfc9 | ||
|
|
096c281ded | ||
|
|
7b421d02b0 | ||
|
|
7a51d3695c | ||
|
|
c04357c32d | ||
|
|
3186728de5 | ||
|
|
6a3624aea6 | ||
|
|
1067ffec78 | ||
|
|
4dae68c51c | ||
|
|
7e30bb9983 | ||
|
|
3900bc385e | ||
|
|
8afeca5a00 | ||
|
|
34a0f29b7f | ||
|
|
223a7b49eb | ||
|
|
f28d658cda | ||
|
|
0816893d97 | ||
|
|
2cbd384229 | ||
|
|
a677b753bc | ||
|
|
5de796c05a | ||
|
|
4997104f70 | ||
|
|
19a595773b | ||
|
|
c98844ec45 | ||
|
|
fde6789d4a | ||
|
|
3fefb5f888 | ||
|
|
5a961b8887 | ||
|
|
79a7e59adb | ||
|
|
84926cb463 | ||
|
|
158517eab5 | ||
|
|
2516850976 | ||
|
|
ac5cb304d6 | ||
|
|
d35e0e05d8 | ||
|
|
fdc16672d5 | ||
|
|
1356e609b3 | ||
|
|
4410480fd6 | ||
|
|
bc3f2a195d | ||
|
|
261be61cc0 | ||
|
|
62d8aaa58e | ||
|
|
e871741ec3 | ||
|
|
088249d751 | ||
|
|
babf6ddb48 | ||
|
|
8b0be9cd2b | ||
|
|
748be0ad66 | ||
|
|
41e02e6f64 | ||
|
|
d4ae014f76 | ||
|
|
ecdd081c27 | ||
|
|
8f69c1faff | ||
|
|
02a47b2edc | ||
|
|
2f28a00e02 | ||
|
|
da5cab621a | ||
|
|
74136bf8e6 | ||
|
|
53c88bc505 | ||
|
|
5dfcd31b54 | ||
|
|
59a752c306 | ||
|
|
77759afcaa | ||
|
|
ca0d475c83 | ||
|
|
5ea76f704a | ||
|
|
b2cbd62a8d | ||
|
|
8f4b2bc458 | ||
|
|
571597dde3 | ||
|
|
63f8277ecb | ||
|
|
15699a253c | ||
|
|
060390c160 | ||
|
|
fb7a5745c2 | ||
|
|
553e21f919 | ||
|
|
af5935d3f2 | ||
|
|
2a8d8fa891 | ||
|
|
19fcea6264 | ||
|
|
ab3c508a39 | ||
|
|
6a18025fe5 | ||
|
|
2fb3c6fe3e | ||
|
|
693ca3179b | ||
|
|
c3a501d33d | ||
|
|
daed587841 | ||
|
|
ff4b315d0c | ||
|
|
e5f90c5ac5 | ||
|
|
ff2d445039 | ||
|
|
26c2f3cc69 | ||
|
|
5b7bb8c81f | ||
|
|
7e71e4c876 | ||
|
|
76054f1152 | ||
|
|
19157702b3 | ||
|
|
18e0ea81d9 | ||
|
|
a66463285b | ||
|
|
eb0c4b2982 | ||
|
|
1aa8521cf8 | ||
|
|
bcc1aed724 | ||
|
|
2de5daffe9 | ||
|
|
235857d021 | ||
|
|
1e5baa57da | ||
|
|
c62c05773c | ||
|
|
af6888020d | ||
|
|
4c2d21275a | ||
|
|
73980f9c73 | ||
|
|
368a13ed13 | ||
|
|
f6ce1f0685 | ||
|
|
4981e76755 | ||
|
|
8babf66dc4 | ||
|
|
8ee82f263d | ||
|
|
2d92956c82 | ||
|
|
4cd73c111e | ||
|
|
355859e707 | ||
|
|
2cd6ec4db6 | ||
|
|
dc540a0cf0 | ||
|
|
93cd0877dd | ||
|
|
8d941162d8 | ||
|
|
23e51e3b8d | ||
|
|
27f11ec6c2 | ||
|
|
63c3d57993 | ||
|
|
8080d01ac9 | ||
|
|
d1d0c30924 | ||
|
|
a8ade0f873 | ||
|
|
6611eb1134 | ||
|
|
e80342d369 | ||
|
|
23edbd05e8 | ||
|
|
299d59b5fc | ||
|
|
90963f8f45 | ||
|
|
9afc3da083 | ||
|
|
be9874d2f4 | ||
|
|
8a4fa93202 | ||
|
|
78c08c4dd9 | ||
|
|
d65f9518df | ||
|
|
64949bb888 | ||
|
|
17b3589782 | ||
|
|
00af8545d6 | ||
|
|
4126cdbba7 | ||
|
|
e4c2271c6b | ||
|
|
b5fdd63a85 | ||
|
|
0779e13d46 | ||
|
|
5dbb0f2a28 | ||
|
|
8dc2050fd8 | ||
|
|
cdd682429e | ||
|
|
51de09e16e | ||
|
|
42248daf60 | ||
|
|
6d606cc52b | ||
|
|
b2e0510697 | ||
|
|
d26d20d730 | ||
|
|
ff98f27407 | ||
|
|
df32dfdc70 | ||
|
|
10a4c59385 | ||
|
|
78051c2814 | ||
|
|
391082fa50 | ||
|
|
23a4d8ec76 | ||
|
|
8fa2d80325 | ||
|
|
be2c4044ce | ||
|
|
4f177ee605 | ||
|
|
e2ed97648a | ||
|
|
9b793b5d7c | ||
|
|
d9ad645f6d | ||
|
|
f096f552d1 | ||
|
|
f94704a803 | ||
|
|
8a7c8d7758 | ||
|
|
8e7181e690 | ||
|
|
30534a617b | ||
|
|
69be7de1c4 | ||
|
|
2fb7651b60 | ||
|
|
da74a931e6 | ||
|
|
acc72e634e | ||
|
|
f93f360ab2 | ||
|
|
bb7b22a739 | ||
|
|
fd75fd3dc7 | ||
|
|
283c4b7193 | ||
|
|
1e6d38cbf0 | ||
|
|
f041ceefc5 | ||
|
|
62cef9bbd2 | ||
|
|
34d75115db | ||
|
|
ff35f9a5aa | ||
|
|
3133664a11 | ||
|
|
03532960f3 | ||
|
|
77cd10e840 | ||
|
|
493ba98292 | ||
|
|
2f8aa22925 | ||
|
|
14257bdde8 | ||
|
|
db5da1ebfe | ||
|
|
6baa1ee7bd | ||
|
|
752bec258d | ||
|
|
885e809019 | ||
|
|
ebb03a8c80 | ||
|
|
270456adfd | ||
|
|
36251dd177 | ||
|
|
3c5b38e225 | ||
|
|
bdd1d745fd | ||
|
|
7135ae2cdc | ||
|
|
9a75d2d5b4 | ||
|
|
3929481b5b | ||
|
|
4a4f76c669 | ||
|
|
892a17bdbf | ||
|
|
34603057a5 | ||
|
|
2ab211d831 | ||
|
|
ee27d7e582 | ||
|
|
6bf91a2c55 | ||
|
|
39801ee005 | ||
|
|
cd5f5d6090 | ||
|
|
015deae38c | ||
|
|
47491c7e41 | ||
|
|
99267f0cce | ||
|
|
1813c0a0d2 | ||
|
|
7d3630d981 | ||
|
|
4e29df1b8a | ||
|
|
6444fdc414 | ||
|
|
5471359991 | ||
|
|
5cd881b791 | ||
|
|
1f6cad6171 | ||
|
|
ff448b0b96 | ||
|
|
8bbb578b74 | ||
|
|
b530a3cd95 | ||
|
|
98bccd9eca | ||
|
|
06bb106199 | ||
|
|
fc96f75c35 | ||
|
|
83e6696fd3 | ||
|
|
dd56abf725 | ||
|
|
35afccefdf | ||
|
|
0d889ef9a8 | ||
|
|
e51ea84439 | ||
|
|
26ec406a04 | ||
|
|
b3a9893fcf | ||
|
|
e2992d1626 | ||
|
|
b75e418f71 | ||
|
|
7add8369f9 | ||
|
|
7ee3c0e12f | ||
|
|
569811d1f0 | ||
|
|
8d3dc7bede | ||
|
|
61191056a7 | ||
|
|
65c794ea67 | ||
|
|
1b8d869199 | ||
|
|
27b07dab5a | ||
|
|
4e433c272f | ||
|
|
529c1ec83b | ||
|
|
a4e77692c4 | ||
|
|
1c5d41a56e | ||
|
|
48acef5336 | ||
|
|
cf1ddf2ff8 | ||
|
|
b79173c272 | ||
|
|
3a7096f19b | ||
|
|
353fa604ab | ||
|
|
9670aa8505 | ||
|
|
0ec9c2c0e0 | ||
|
|
f906142534 | ||
|
|
a9f19b7d1d | ||
|
|
1be5c6c527 | ||
|
|
ac761a3973 | ||
|
|
ff32d2d34c | ||
|
|
fc1e4e060d | ||
|
|
7765a3308d | ||
|
|
8e5eba0ac5 | ||
|
|
ad5fad46ee | ||
|
|
2980684ae0 | ||
|
|
1761d770c9 | ||
|
|
e854d0f374 | ||
|
|
7e32fcc7d3 | ||
|
|
9a5df48213 | ||
|
|
70d5495c7e | ||
|
|
e59ede0a08 | ||
|
|
613e740d0a | ||
|
|
92277088f2 | ||
|
|
35a018df42 | ||
|
|
cbac292e4d | ||
|
|
d2bee16058 | ||
|
|
f4975d7361 | ||
|
|
b929c1e70b | ||
|
|
29fc46370c | ||
|
|
4f1418d3ba | ||
|
|
edb8022336 | ||
|
|
e5a7889ce9 | ||
|
|
6bbed5fd4c | ||
|
|
2a575837b2 | ||
|
|
bdb0d32235 | ||
|
|
7519325162 | ||
|
|
f5230f1299 | ||
|
|
11e78892c1 | ||
|
|
6503928fbf | ||
|
|
d90217ab06 | ||
|
|
1a0e818328 | ||
|
|
85bdaa6145 | ||
|
|
47eaf2bda4 | ||
|
|
f5193d065a | ||
|
|
cc78ff7bd1 | ||
|
|
087bc6fd2d | ||
|
|
80820e610a | ||
|
|
4155dee5fa | ||
|
|
19e8c841f8 | ||
|
|
62e1fdfbb6 | ||
|
|
1ca021fc6f | ||
|
|
6a1b3697fe | ||
|
|
1dd1f19ca5 | ||
|
|
e7d9d71442 | ||
|
|
190749f6c1 | ||
|
|
95a8aa69db | ||
|
|
98803c2345 | ||
|
|
6e6f83e417 | ||
|
|
f5c8cc1b53 | ||
|
|
8443aa43da | ||
|
|
a6153c37e6 | ||
|
|
c6232e67aa | ||
|
|
68f3b9cfda | ||
|
|
81b35c69e1 | ||
|
|
247d3f5822 | ||
|
|
15e08a2f8e | ||
|
|
ccce5e52c1 | ||
|
|
b1058f3be9 | ||
|
|
1a7227aff6 | ||
|
|
c9dd9772b4 | ||
|
|
0cdfbc76b5 | ||
|
|
05b8587849 | ||
|
|
340c0d27b6 | ||
|
|
69d1077169 | ||
|
|
7dc719148e | ||
|
|
528c39f238 | ||
|
|
ee51e854ba | ||
|
|
d444edd82f | ||
|
|
38faa7073b | ||
|
|
2d955f50de | ||
|
|
86d810038a | ||
|
|
62fc20ae25 | ||
|
|
b2059859df | ||
|
|
ff1ab8cfe9 | ||
|
|
0a83a4d6d0 | ||
|
|
328bd5bd2e | ||
|
|
cc3c78663c | ||
|
|
f1f41dffa2 | ||
|
|
6bd52cea7f | ||
|
|
64ec90fe3d | ||
|
|
7fa9666be9 | ||
|
|
b431254cc4 | ||
|
|
b54b2425bb | ||
|
|
de1bbbea38 | ||
|
|
3cc41b0a98 | ||
|
|
e43fb097c8 | ||
|
|
3346c6bc38 | ||
|
|
ba5934c559 | ||
|
|
40100ffa7c | ||
|
|
d53452a7eb | ||
|
|
2bcc69b59a | ||
|
|
be5d207d5d | ||
|
|
a425c4cc1b | ||
|
|
4df962d4b6 | ||
|
|
2e03f1b706 | ||
|
|
14e548bf40 | ||
|
|
dcb0456e18 | ||
|
|
584c4f4686 | ||
|
|
20856a4171 | ||
|
|
6790e050de | ||
|
|
0ac5a2d40b | ||
|
|
4cd20d2163 | ||
|
|
30f44f7b98 | ||
|
|
a2e6a78c83 | ||
|
|
56d465e7bf | ||
|
|
93745adc2d | ||
|
|
feaba53aa8 | ||
|
|
a7c34b9855 | ||
|
|
fa1dc760f2 | ||
|
|
f3b9c72262 | ||
|
|
f6ba07bf8b | ||
|
|
47a447cb67 | ||
|
|
833420def2 | ||
|
|
a4cf5b47b2 | ||
|
|
0d7810120f | ||
|
|
f4948a6ae4 | ||
|
|
540545ff74 | ||
|
|
67ab66f95d | ||
|
|
84ed484196 | ||
|
|
e7071ea390 | ||
|
|
13c6c15587 | ||
|
|
ed645bd758 | ||
|
|
1d617f1b92 | ||
|
|
55d7d9e555 | ||
|
|
4546f19bab | ||
|
|
a063252e06 | ||
|
|
aa781b4a51 | ||
|
|
c6eba498c5 | ||
|
|
debc6a2f8c | ||
|
|
b6106af3ec | ||
|
|
4abc6dd4d1 | ||
|
|
4d9b2595b8 | ||
|
|
56aac57035 | ||
|
|
f7de33c1a5 | ||
|
|
9fb6654d0e | ||
|
|
9a34545aa9 | ||
|
|
432fa5636e | ||
|
|
05ca9416e7 | ||
|
|
0a7d679866 | ||
|
|
90640882b4 | ||
|
|
67c7ba0fed | ||
|
|
98f7f9e207 | ||
|
|
4d89e3f5b7 | ||
|
|
4479ff5cdb | ||
|
|
0a8c4fa471 | ||
|
|
bba93792d5 | ||
|
|
2b0638c95f | ||
|
|
ac3cd213dc | ||
|
|
b3d62c26f7 | ||
|
|
8cc4850fe0 | ||
|
|
0187b4ff92 | ||
|
|
ff51ea0ac8 | ||
|
|
9f4ffef125 | ||
|
|
64d442c2a3 | ||
|
|
b35727ee95 | ||
|
|
e584bfb47d | ||
|
|
8031ef551a | ||
|
|
3964f5be3b | ||
|
|
d59735fc79 | ||
|
|
0c6ce15f45 | ||
|
|
9f909ae54d | ||
|
|
e286b4f20b | ||
|
|
4ca24cffc6 | ||
|
|
28075b2a78 | ||
|
|
90c9257154 | ||
|
|
1d289143fe | ||
|
|
5fe26f3ab5 | ||
|
|
2867d31a88 | ||
|
|
13b34a6668 | ||
|
|
1f1c56d0ec | ||
|
|
ceaec960c4 | ||
|
|
17e9ac1292 | ||
|
|
c1abc67fa8 | ||
|
|
25e5ca5e4c | ||
|
|
ee16cd4311 | ||
|
|
343824c742 | ||
|
|
2bfef7d7a8 | ||
|
|
72f7f9a572 | ||
|
|
284fb06636 | ||
|
|
307a966e73 | ||
|
|
6431034285 | ||
|
|
db6475c811 | ||
|
|
7d34039738 | ||
|
|
bb4ad618e1 | ||
|
|
33b6145162 | ||
|
|
eb3f7fc9aa | ||
|
|
0fa7112e75 | ||
|
|
6b466251ef | ||
|
|
b344657d0f | ||
|
|
84eb54e8db | ||
|
|
4d4c633f7e | ||
|
|
a6468ddcff | ||
|
|
980ee23130 | ||
|
|
ffb0735aba | ||
|
|
35ce5cd8b5 | ||
|
|
d65f376ed9 | ||
|
|
100d85149f | ||
|
|
21fb3ebfa9 | ||
|
|
3ccba631c3 | ||
|
|
6cb3fe08e8 | ||
|
|
57018249a6 | ||
|
|
7dce4d54c1 | ||
|
|
bbf822e430 | ||
|
|
a2c0ffffdf | ||
|
|
7d40c641bc | ||
|
|
b8b4ddc2ae | ||
|
|
2ae22a66fe | ||
|
|
3685461692 | ||
|
|
b767e5e1a7 | ||
|
|
f6576815c9 | ||
|
|
ba2c65ec19 | ||
|
|
4057275605 | ||
|
|
c9d2957d7a | ||
|
|
8ad8fbb877 | ||
|
|
36ad824dd8 | ||
|
|
ed4ad24f62 | ||
|
|
8665960ff2 | ||
|
|
1a0a6f1c0e | ||
|
|
ecc832d060 | ||
|
|
d7368a1a50 | ||
|
|
19d4be62c6 | ||
|
|
5ecbf19aa8 | ||
|
|
404e21f937 | ||
|
|
11d3bd69af | ||
|
|
691f7cb42c | ||
|
|
1f4d23bf50 | ||
|
|
0ae29130cd | ||
|
|
5278dd5015 | ||
|
|
892b8d4bff | ||
|
|
6ce8226cf4 | ||
|
|
a7d405991c | ||
|
|
9755a37fe6 | ||
|
|
e3df9911bc | ||
|
|
0c32665255 | ||
|
|
6f9bab57e3 | ||
|
|
e608377088 | ||
|
|
200052dd56 | ||
|
|
0107f361bb | ||
|
|
ea86e4e087 | ||
|
|
a90d93691f | ||
|
|
ddbedb9b31 | ||
|
|
94fd38c358 | ||
|
|
fb6442f9bb | ||
|
|
e8ffaedb08 | ||
|
|
d4fbc039a7 | ||
|
|
61f591f56e | ||
|
|
cc538ad523 | ||
|
|
cdd3dcc99e | ||
|
|
4b121ec3b1 |
@@ -12,6 +12,7 @@ tests/integration/.tox
|
||||
*/*/*/*.pyc
|
||||
*/*/*/__pycache__
|
||||
.trash-cache
|
||||
.dapper
|
||||
#.dapper
|
||||
vendor/*/*/*/.git
|
||||
tmp
|
||||
docs/_site
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -6,6 +6,7 @@
|
||||
/dist
|
||||
/gopath
|
||||
/images/*/build
|
||||
/scripts/images/vmware/assets
|
||||
.dockerfile
|
||||
*.swp
|
||||
/tests/integration/MANIFEST
|
||||
@@ -16,3 +17,5 @@
|
||||
__pycache__
|
||||
/.dapper
|
||||
/.trash-cache
|
||||
.idea
|
||||
.trash-conf
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
FROM ubuntu:16.04
|
||||
# FROM arm64=aarch64/ubuntu:16.04 arm=armhf/ubuntu:16.04
|
||||
# FROM arm64=aarch64/ubuntu:16.04
|
||||
|
||||
ENV DAPPER_ENV VERSION DEV_BUILD
|
||||
ENV DAPPER_DOCKER_SOCKET true
|
||||
ENV DAPPER_SOURCE /go/src/github.com/rancher/os
|
||||
ENV DAPPER_OUTPUT ./bin ./dist ./build/initrd
|
||||
ENV DAPPER_RUN_ARGS --privileged
|
||||
ENV TRASH_CACHE ${DAPPER_SOURCE}/.trash-cache
|
||||
ENV SHELL /bin/bash
|
||||
WORKDIR ${DAPPER_SOURCE}
|
||||
# get the apt-cacher proxy set
|
||||
ARG APTPROXY=
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
RUN echo "Acquire::http { Proxy \"$APTPROXY\"; };" >> /etc/apt/apt.conf.d/01proxy \
|
||||
&& cat /etc/apt/apt.conf.d/01proxy \
|
||||
&& apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
build-essential \
|
||||
ca-certificates \
|
||||
cpio \
|
||||
@@ -19,6 +15,7 @@ RUN apt-get update && \
|
||||
dosfstools \
|
||||
gccgo \
|
||||
genisoimage \
|
||||
gettext \
|
||||
git \
|
||||
isolinux \
|
||||
less \
|
||||
@@ -27,6 +24,7 @@ RUN apt-get update && \
|
||||
libselinux1-dev \
|
||||
locales \
|
||||
module-init-tools \
|
||||
mtools \
|
||||
openssh-client \
|
||||
pkg-config \
|
||||
qemu \
|
||||
@@ -36,7 +34,20 @@ RUN apt-get update && \
|
||||
syslinux-common \
|
||||
vim \
|
||||
wget \
|
||||
xorriso
|
||||
xorriso \
|
||||
xz-utils \
|
||||
telnet
|
||||
|
||||
########## Dapper Configuration #####################
|
||||
|
||||
ENV DAPPER_ENV VERSION DEV_BUILD RUNTEST DEBUG APTPROXY ENGINE_REGISTRY_MIRROR INTEGRATION_TESTS
|
||||
ENV DAPPER_DOCKER_SOCKET true
|
||||
ENV DAPPER_SOURCE /go/src/github.com/rancher/os
|
||||
ENV DAPPER_OUTPUT ./bin ./dist ./build/initrd ./build/kernel
|
||||
ENV DAPPER_RUN_ARGS --privileged
|
||||
ENV TRASH_CACHE ${DAPPER_SOURCE}/.trash-cache
|
||||
ENV SHELL /bin/bash
|
||||
WORKDIR ${DAPPER_SOURCE}
|
||||
|
||||
########## General Configuration #####################
|
||||
ARG DAPPER_HOST_ARCH=amd64
|
||||
@@ -53,15 +64,13 @@ ARG DOCKER_BUILD_VERSION=1.10.3
|
||||
ARG DOCKER_BUILD_PATCH_VERSION=v${DOCKER_BUILD_VERSION}-ros1
|
||||
ARG SELINUX_POLICY_URL=https://github.com/rancher/refpolicy/releases/download/v0.0.3/policy.29
|
||||
|
||||
ARG KERNEL_URL_amd64=https://github.com/rancher/os-kernel/releases/download/Ubuntu-4.4.0-37.56-rancher1/linux-4.4.19-rancher-x86.tar.gz
|
||||
ARG KERNEL_URL_arm64=https://github.com/imikushin/os-kernel/releases/download/Estuary-4.4.0-arm64.8/linux-4.4.0-rancher-arm64.tar.gz
|
||||
ARG KERNEL_VERSION_amd64=4.14.32-rancher2
|
||||
ARG KERNEL_URL_amd64=https://github.com/rancher/os-kernel/releases/download/v${KERNEL_VERSION_amd64}/linux-${KERNEL_VERSION_amd64}-x86.tar.gz
|
||||
|
||||
ARG DOCKER_URL_amd64=https://get.docker.com/builds/Linux/x86_64/docker-${DOCKER_VERSION}.tgz
|
||||
ARG DOCKER_URL_arm=https://github.com/rancher/docker/releases/download/${DOCKER_PATCH_VERSION}/docker-${DOCKER_VERSION}_arm.tgz
|
||||
ARG DOCKER_URL_arm64=https://github.com/rancher/docker/releases/download/${DOCKER_PATCH_VERSION}/docker-${DOCKER_VERSION}_arm64.tgz
|
||||
|
||||
ARG BUILD_DOCKER_URL_amd64=https://get.docker.com/builds/Linux/x86_64/docker-${DOCKER_BUILD_VERSION}
|
||||
ARG BUILD_DOCKER_URL_arm=https://github.com/rancher/docker/releases/download/${DOCKER_BUILD_PATCH_VERSION}/docker-${DOCKER_BUILD_VERSION}_arm
|
||||
ARG BUILD_DOCKER_URL_arm64=https://github.com/rancher/docker/releases/download/${DOCKER_BUILD_PATCH_VERSION}/docker-${DOCKER_BUILD_VERSION}_arm64
|
||||
|
||||
ARG OS_RELEASES_YML=https://releases.rancher.com/os
|
||||
@@ -70,9 +79,15 @@ ARG OS_SERVICES_REPO=https://raw.githubusercontent.com/${OS_REPO}/os-services
|
||||
ARG IMAGE_NAME=${OS_REPO}/os
|
||||
ARG DFS_IMAGE=${OS_REPO}/docker:v${DOCKER_VERSION}-2
|
||||
|
||||
ARG OS_BASE_URL_amd64=https://github.com/rancher/os-base/releases/download/v2016.08.1-1/os-base_amd64.tar.xz
|
||||
ARG OS_BASE_URL_arm64=https://github.com/rancher/os-base/releases/download/v2016.08.1-1/os-base_arm64.tar.xz
|
||||
ARG OS_BASE_URL_arm=https://github.com/rancher/os-base/releases/download/v2016.08.1-1/os-base_arm.tar.xz
|
||||
ARG OS_BASE_URL_amd64=https://github.com/rancher/os-base/releases/download/v2018.02-3/os-base_amd64.tar.xz
|
||||
ARG OS_BASE_URL_arm64=https://github.com/rancher/os-base/releases/download/v2018.02-3/os-base_arm64.tar.xz
|
||||
|
||||
ARG SYSTEM_DOCKER_VERSION=17.06-ros4
|
||||
ARG SYSTEM_DOCKER_URL_amd64=https://github.com/niusmallnan/os-system-docker/releases/download/${SYSTEM_DOCKER_VERSION}/docker-amd64-${SYSTEM_DOCKER_VERSION}.tgz
|
||||
ARG SYSTEM_DOCKER_URL_arm64=https://github.com/niusmallnan/os-system-docker/releases/download/${SYSTEM_DOCKER_VERSION}/docker-arm64-${SYSTEM_DOCKER_VERSION}.tgz
|
||||
|
||||
ARG VMWARE_AUTOFORMAT=1
|
||||
ARG OPEN_VMTOOLS_VERSION=10.2.5-1
|
||||
######################################################
|
||||
|
||||
# Set up environment and export all ARGS as ENV
|
||||
@@ -81,7 +96,6 @@ ENV ARCH=${ARCH} \
|
||||
|
||||
ENV BUILD_DOCKER_URL=BUILD_DOCKER_URL_${ARCH} \
|
||||
BUILD_DOCKER_URL_amd64=${BUILD_DOCKER_URL_amd64} \
|
||||
BUILD_DOCKER_URL_arm=${BUILD_DOCKER_URL_arm} \
|
||||
BUILD_DOCKER_URL_arm64=${BUILD_DOCKER_URL_arm64} \
|
||||
DAPPER_HOST_ARCH=${DAPPER_HOST_ARCH} \
|
||||
DFS_IMAGE=${DFS_IMAGE} \
|
||||
@@ -89,35 +103,40 @@ ENV BUILD_DOCKER_URL=BUILD_DOCKER_URL_${ARCH} \
|
||||
DOCKER_PATCH_VERSION=${DOCKER_PATCH_VERSION} \
|
||||
DOCKER_URL=DOCKER_URL_${ARCH} \
|
||||
DOCKER_URL_amd64=${DOCKER_URL_amd64} \
|
||||
DOCKER_URL_arm=${DOCKER_URL_arm} \
|
||||
DOCKER_URL_arm64=${DOCKER_URL_arm64} \
|
||||
DOCKER_VERSION=${DOCKER_VERSION} \
|
||||
DOWNLOADS=/usr/src/downloads \
|
||||
GOPATH=/go \
|
||||
GO_VERSION=1.7.1 \
|
||||
GO_VERSION=1.8.5 \
|
||||
GOARCH=$ARCH \
|
||||
HOSTNAME_DEFAULT=${HOSTNAME_DEFAULT} \
|
||||
IMAGE_NAME=${IMAGE_NAME} \
|
||||
KERNEL_VERSION=${KERNEL_VERSION_amd64} \
|
||||
KERNEL_URL=KERNEL_URL_${ARCH} \
|
||||
KERNEL_URL_amd64=${KERNEL_URL_amd64} \
|
||||
KERNEL_URL_arm64=${KERNEL_URL_arm64} \
|
||||
OS_BASE_SHA1=OS_BASE_SHA1_${ARCH} \
|
||||
OS_BASE_URL=OS_BASE_URL_${ARCH} \
|
||||
OS_BASE_URL_amd64=${OS_BASE_URL_amd64} \
|
||||
OS_BASE_URL_arm=${OS_BASE_URL_arm} \
|
||||
OS_BASE_URL_arm64=${OS_BASE_URL_arm64} \
|
||||
OS_RELEASES_YML=${OS_RELEASES_YML} \
|
||||
OS_REPO=${OS_REPO} \
|
||||
OS_SERVICES_REPO=${OS_SERVICES_REPO} \
|
||||
REPO_VERSION=master \
|
||||
SELINUX_POLICY_URL=${SELINUX_POLICY_URL}
|
||||
SELINUX_POLICY_URL=${SELINUX_POLICY_URL} \
|
||||
SYSTEM_DOCKER_URL=SYSTEM_DOCKER_URL_${ARCH} \
|
||||
SYSTEM_DOCKER_URL_amd64=${SYSTEM_DOCKER_URL_amd64} \
|
||||
SYSTEM_DOCKER_URL_arm64=${SYSTEM_DOCKER_URL_arm64} \
|
||||
VMWARE_AUTOFORMAT=${VMWARE_AUTOFORMAT} \
|
||||
OPEN_VMTOOLS_VERSION=${OPEN_VMTOOLS_VERSION}
|
||||
ENV PATH=${GOPATH}/bin:/usr/local/go/bin:$PATH
|
||||
|
||||
RUN mkdir -p ${DOWNLOADS}
|
||||
|
||||
# Download kernel
|
||||
RUN rm /bin/sh && ln -s /bin/bash /bin/sh
|
||||
RUN if [ -n "${!KERNEL_URL}" ]; then \
|
||||
RUN echo "... Downloading ${!KERNEL_URL}"; \
|
||||
if [ -n "${!KERNEL_URL}" ]; then \
|
||||
curl -fL ${!KERNEL_URL} > ${DOWNLOADS}/kernel.tar.gz \
|
||||
;fi
|
||||
|
||||
@@ -125,23 +144,13 @@ RUN if [ -n "${!KERNEL_URL}" ]; then \
|
||||
RUN curl -pfL ${SELINUX_POLICY_URL} > ${DOWNLOADS}/$(basename ${SELINUX_POLICY_URL})
|
||||
|
||||
# Install Go
|
||||
COPY assets/go-dnsclient.patch ${DAPPER_SOURCE}
|
||||
RUN ln -sf go-6 /usr/bin/go && \
|
||||
curl -sfL https://storage.googleapis.com/golang/go${GO_VERSION}.src.tar.gz | tar -xzf - -C /usr/local && \
|
||||
patch /usr/local/go/src/net/dnsclient_unix.go ${DAPPER_SOURCE}/go-dnsclient.patch && \
|
||||
cd /usr/local/go/src && \
|
||||
GOROOT_BOOTSTRAP=/usr GOARCH=${HOST_ARCH} GOHOSTARCH=${HOST_ARCH} ./make.bash && \
|
||||
rm /usr/bin/go
|
||||
RUN wget -O - https://storage.googleapis.com/golang/go${GO_VERSION}.linux-${GOARCH}.tar.gz | tar -xzf - -C /usr/local && \
|
||||
go get github.com/rancher/trash && go get github.com/golang/lint/golint
|
||||
|
||||
# Install Host Docker
|
||||
RUN curl -fL ${!BUILD_DOCKER_URL} > /usr/bin/docker && \
|
||||
chmod +x /usr/bin/docker
|
||||
|
||||
# Install Trash
|
||||
RUN go get github.com/rancher/trash
|
||||
|
||||
RUN go get gopkg.in/check.v1
|
||||
|
||||
# Install dapper
|
||||
RUN curl -sL https://releases.rancher.com/dapper/latest/dapper-`uname -s`-`uname -m | sed 's/arm.*/arm/'` > /usr/bin/dapper && \
|
||||
chmod +x /usr/bin/dapper
|
||||
|
||||
41
Makefile
Normal file → Executable file
41
Makefile
Normal file → Executable file
@@ -1,8 +1,8 @@
|
||||
TARGETS := $(shell ls scripts | grep -vE 'clean|run|help')
|
||||
TARGETS := $(shell ls scripts | grep -vE 'clean|run|help|release|build-moby|run-moby')
|
||||
|
||||
.dapper:
|
||||
@echo Downloading dapper
|
||||
@curl -sL https://releases.rancher.com/dapper/latest/dapper-`uname -s`-`uname -m` > .dapper.tmp
|
||||
@curl -sL https://releases.rancher.com/dapper/latest/dapper-`uname -s`-`uname -m|sed 's/v7l//'` > .dapper.tmp
|
||||
@@chmod +x .dapper.tmp
|
||||
@./.dapper.tmp -v
|
||||
@mv .dapper.tmp .dapper
|
||||
@@ -25,12 +25,49 @@ run: build/initrd/.id .dapper
|
||||
./.dapper -m bind build-target
|
||||
./scripts/run
|
||||
|
||||
build-moby:
|
||||
./scripts/build-moby
|
||||
|
||||
run-moby:
|
||||
./scripts/run-moby
|
||||
|
||||
shell-bind: .dapper
|
||||
./.dapper -m bind -s
|
||||
|
||||
clean:
|
||||
@./scripts/clean
|
||||
|
||||
release: .dapper release-build qcows
|
||||
|
||||
release-build:
|
||||
mkdir -p dist
|
||||
./.dapper release 2>&1 | tee dist/release.log
|
||||
|
||||
itest:
|
||||
mkdir -p dist
|
||||
./.dapper integration-test 2>&1 | tee dist/itest.log
|
||||
grep --binary-files=text FAIL dist/itest.log || true
|
||||
|
||||
qcows:
|
||||
cp dist/artifacts/rancheros.iso scripts/images/openstack/
|
||||
cd scripts/images/openstack && \
|
||||
APPEND="console=tty1 console=ttyS0,115200n8 printk.devkmsg=on rancher.autologin=ttyS0 panic=10" \
|
||||
NAME=openstack ../../../.dapper
|
||||
cd scripts/images/openstack && \
|
||||
APPEND="console=tty1 printk.devkmsg=on notsc clocksource=kvm-clock rancher.network.interfaces.eth0.ipv4ll rancher.cloud_init.datasources=[digitalocean] rancher.autologin=tty1 rancher.autologin=ttyS0 panic=10 rancher.resize_device=/dev/vda" \
|
||||
NAME=digitalocean ../../../.dapper
|
||||
cp ./scripts/images/openstack/dist/*.img dist/artifacts/
|
||||
|
||||
rpi64:
|
||||
# scripts/images/raspberry-pi-hypriot64/dist/rancheros-raspberry-pi.zip
|
||||
cp dist/artifacts/rootfs_arm64.tar.gz scripts/images/raspberry-pi-hypriot64/
|
||||
cd scripts/images/raspberry-pi-hypriot64/ \
|
||||
&& ../../../.dapper
|
||||
|
||||
vmware: .dapper
|
||||
mkdir -p dist
|
||||
INTEGRATION_TESTS=0 ./.dapper vmware-release 2>&1 | tee dist/release.log
|
||||
|
||||
help:
|
||||
@./scripts/help
|
||||
|
||||
|
||||
99
README.md
99
README.md
@@ -10,46 +10,52 @@ a container that runs the user Docker. The user Docker is then the instance tha
|
||||
used to create containers. We created this separation because it seemed logical and also
|
||||
it would really be bad if somebody did `docker rm -f $(docker ps -qa)` and deleted the entire OS.
|
||||
|
||||

|
||||

|
||||
|
||||
## Latest Release
|
||||
## Release
|
||||
|
||||
**v0.6.1 - Docker 1.12.1 - Linux 4.4.19**
|
||||
- **Latest: v1.3.0 - Docker 17.09.1-ce - Linux 4.9.80**
|
||||
- **Stable: v1.2.0 - Docker 17.09.1-ce - Linux 4.9.78**
|
||||
|
||||
### ISO
|
||||
|
||||
https://releases.rancher.com/os/latest/rancheros.iso
|
||||
https://releases.rancher.com/os/v0.6.1/rancheros.iso
|
||||
- https://releases.rancher.com/os/latest/rancheros.iso
|
||||
- https://releases.rancher.com/os/v1.2.0/rancheros.iso
|
||||
|
||||
### Additional Downloads
|
||||
|
||||
#### Latest
|
||||
#### Latest Links
|
||||
|
||||
* https://releases.rancher.com/os/latest/initrd
|
||||
* https://releases.rancher.com/os/latest/iso-checksums.txt
|
||||
* https://releases.rancher.com/os/latest/rancheros-openstack.img
|
||||
* https://releases.rancher.com/os/latest/rancheros-raspberry-pi.zip
|
||||
* https://releases.rancher.com/os/latest/rancheros-v0.6.1.tar.gz
|
||||
* https://releases.rancher.com/os/latest/rancheros.iso
|
||||
* https://releases.rancher.com/os/latest/rootfs_arm.tar.gz
|
||||
* https://releases.rancher.com/os/latest/rootfs_arm64.tar.gz
|
||||
* https://releases.rancher.com/os/latest/rancheros-digitalocean.img
|
||||
* https://releases.rancher.com/os/latest/rancheros-aliyun.vhd
|
||||
* https://releases.rancher.com/os/latest/rancheros.ipxe
|
||||
* https://releases.rancher.com/os/latest/rancheros-gce.tar.gz
|
||||
* https://releases.rancher.com/os/latest/rootfs.tar.gz
|
||||
* https://releases.rancher.com/os/latest/vmlinuz
|
||||
|
||||
#### v0.6.1
|
||||
#### v1.2.0 Links
|
||||
|
||||
* https://releases.rancher.com/os/v0.6.1/initrd
|
||||
* https://releases.rancher.com/os/v0.6.1/iso-checksums.txt
|
||||
* https://releases.rancher.com/os/v0.6.1/rancheros-openstack.img
|
||||
* https://releases.rancher.com/os/v0.6.1/rancheros-raspberry-pi.zip
|
||||
* https://releases.rancher.com/os/v0.6.1/rancheros-v0.6.1.tar.gz
|
||||
* https://releases.rancher.com/os/v0.6.1/rancheros.iso
|
||||
* https://releases.rancher.com/os/v0.6.1/rootfs_arm.tar.gz
|
||||
* https://releases.rancher.com/os/v0.6.1/rootfs_arm64.tar.gz
|
||||
* https://releases.rancher.com/os/v0.6.1/rootfs.tar.gz
|
||||
* https://releases.rancher.com/os/v0.6.1/vmlinuz
|
||||
* https://releases.rancher.com/os/v1.2.0/initrd
|
||||
* https://releases.rancher.com/os/v1.2.0/iso-checksums.txt
|
||||
* https://releases.rancher.com/os/v1.2.0/rancheros-openstack.img
|
||||
* https://releases.rancher.com/os/v1.2.0/rancheros-digitalocean.img
|
||||
* https://releases.rancher.com/os/v1.2.0/rancheros-aliyun.vhd
|
||||
* https://releases.rancher.com/os/v1.2.0/rancheros.ipxe
|
||||
* https://releases.rancher.com/os/v1.2.0/rancheros-gce.tar.gz
|
||||
* https://releases.rancher.com/os/v1.2.0/rootfs.tar.gz
|
||||
* https://releases.rancher.com/os/v1.2.0/vmlinuz
|
||||
|
||||
**Note**: you can use `http` instead of `https` in the above URLs, e.g. for iPXE.
|
||||
#### ARM Links
|
||||
|
||||
* https://releases.rancher.com/os/latest/rootfs_arm64.tar.gz
|
||||
* https://releases.rancher.com/os/latest/rancheros-raspberry-pi64.zip
|
||||
* https://releases.rancher.com/os/v1.2.0/rootfs_arm64.tar.gz
|
||||
* https://releases.rancher.com/os/v1.2.0/rancheros-raspberry-pi64.zip
|
||||
|
||||
**Note**: you can use `http` instead of `https` in the above URLs, e.g. for iPXE.
|
||||
|
||||
### Amazon
|
||||
|
||||
@@ -57,24 +63,35 @@ SSH keys are added to the **`rancher`** user, so you must log in using the **ran
|
||||
|
||||
**HVM**
|
||||
|
||||
Region | Type | AMI |
|
||||
-------|------|------
|
||||
ap-northeast-1 | HVM | [ami-75954214](https://console.aws.amazon.com/ec2/home?region=ap-northeast-1#launchInstanceWizard:ami=ami-75954214)
|
||||
ap-northeast-2 | HVM | [ami-690dd807](https://console.aws.amazon.com/ec2/home?region=ap-northeast-2#launchInstanceWizard:ami=ami-690dd807)
|
||||
ap-south-1 | HVM | [ami-ed8cf982](https://console.aws.amazon.com/ec2/home?region=ap-south-1#launchInstanceWizard:ami=ami-ed8cf982)
|
||||
ap-southeast-1 | HVM | [ami-27bc6644](https://console.aws.amazon.com/ec2/home?region=ap-southeast-1#launchInstanceWizard:ami=ami-27bc6644)
|
||||
ap-southeast-2 | HVM | [ami-67172604](https://console.aws.amazon.com/ec2/home?region=ap-southeast-2#launchInstanceWizard:ami=ami-67172604)
|
||||
eu-central-1 | HVM | [ami-e88d7f87](https://console.aws.amazon.com/ec2/home?region=eu-central-1#launchInstanceWizard:ami=ami-e88d7f87)
|
||||
eu-west-1 | HVM | [ami-934837e0](https://console.aws.amazon.com/ec2/home?region=eu-west-1#launchInstanceWizard:ami=ami-934837e0)
|
||||
sa-east-1 | HVM | [ami-6949d905](https://console.aws.amazon.com/ec2/home?region=sa-east-1#launchInstanceWizard:ami=ami-6949d905)
|
||||
us-east-1 | HVM | [ami-a8d2a4bf](https://console.aws.amazon.com/ec2/home?region=us-east-1#launchInstanceWizard:ami=ami-a8d2a4bf)
|
||||
us-west-1 | HVM | [ami-fccb879c](https://console.aws.amazon.com/ec2/home?region=us-west-1#launchInstanceWizard:ami=ami-fccb879c)
|
||||
us-west-2 | HVM | [ami-1ed3007e](https://console.aws.amazon.com/ec2/home?region=us-west-2#launchInstanceWizard:ami=ami-1ed3007e)
|
||||
Region | Type | AMI(Stable) | AMI(Latest)
|
||||
-------|------|------|------
|
||||
ap-south-1 | HVM | [ami-12db887d](https://ap-south-1.console.aws.amazon.com/ec2/home?region=ap-south-1#launchInstanceWizard:ami=ami-12db887d) | [ami-7e227811](https://ap-south-1.console.aws.amazon.com/ec2/home?region=ap-south-1#launchInstanceWizard:ami=ami-7e227811)
|
||||
eu-west-3 | HVM | [ami-d5a315a8](https://eu-west-3.console.aws.amazon.com/ec2/home?region=eu-west-3#launchInstanceWizard:ami=ami-d5a315a8) | [ami-662d9b1b](https://eu-west-3.console.aws.amazon.com/ec2/home?region=eu-west-3#launchInstanceWizard:ami=ami-662d9b1b)
|
||||
eu-west-2 | HVM | [ami-80bd58e7](https://eu-west-2.console.aws.amazon.com/ec2/home?region=eu-west-2#launchInstanceWizard:ami=ami-80bd58e7) | [ami-89bb5aee](https://eu-west-2.console.aws.amazon.com/ec2/home?region=eu-west-2#launchInstanceWizard:ami=ami-89bb5aee)
|
||||
eu-west-1 | HVM | [ami-69187010](https://eu-west-1.console.aws.amazon.com/ec2/home?region=eu-west-1#launchInstanceWizard:ami=ami-69187010) | [ami-386b3841](https://eu-west-1.console.aws.amazon.com/ec2/home?region=eu-west-1#launchInstanceWizard:ami=ami-386b3841)
|
||||
ap-northeast-2 | HVM | [ami-57dd7f39](https://ap-northeast-2.console.aws.amazon.com/ec2/home?region=ap-northeast-2#launchInstanceWizard:ami=ami-57dd7f39) | [ami-7da90613](https://ap-northeast-2.console.aws.amazon.com/ec2/home?region=ap-northeast-2#launchInstanceWizard:ami=ami-7da90613)
|
||||
ap-northeast-1 | HVM | [ami-a3c2b5c5](https://ap-northeast-1.console.aws.amazon.com/ec2/home?region=ap-northeast-1#launchInstanceWizard:ami=ami-a3c2b5c5) | [ami-14070f68](https://ap-northeast-1.console.aws.amazon.com/ec2/home?region=ap-northeast-1#launchInstanceWizard:ami=ami-14070f68)
|
||||
sa-east-1 | HVM | [ami-6c2f6100](https://sa-east-1.console.aws.amazon.com/ec2/home?region=sa-east-1#launchInstanceWizard:ami=ami-6c2f6100) | [ami-7345121f](https://sa-east-1.console.aws.amazon.com/ec2/home?region=sa-east-1#launchInstanceWizard:ami=ami-7345121f)
|
||||
ca-central-1 | HVM | [ami-b8a622dc](https://ca-central-1.console.aws.amazon.com/ec2/home?region=ca-central-1#launchInstanceWizard:ami=ami-b8a622dc) | [ami-1ea3257a](https://ca-central-1.console.aws.amazon.com/ec2/home?region=ca-central-1#launchInstanceWizard:ami=ami-1ea3257a)
|
||||
ap-southeast-1 | HVM | [ami-0f5a1b73](https://ap-southeast-1.console.aws.amazon.com/ec2/home?region=ap-southeast-1#launchInstanceWizard:ami=ami-0f5a1b73) | [ami-4b4d1437](https://ap-southeast-1.console.aws.amazon.com/ec2/home?region=ap-southeast-1#launchInstanceWizard:ami=ami-4b4d1437)
|
||||
ap-southeast-2 | HVM | [ami-edc73c8f](https://ap-southeast-2.console.aws.amazon.com/ec2/home?region=ap-southeast-2#launchInstanceWizard:ami=ami-edc73c8f) | [ami-e4498586](https://ap-southeast-2.console.aws.amazon.com/ec2/home?region=ap-southeast-2#launchInstanceWizard:ami=ami-e4498586)
|
||||
eu-central-1 | HVM | [ami-28422647](https://eu-central-1.console.aws.amazon.com/ec2/home?region=eu-central-1#launchInstanceWizard:ami=ami-28422647) | [ami-bc386557](https://eu-central-1.console.aws.amazon.com/ec2/home?region=eu-central-1#launchInstanceWizard:ami=ami-bc386557)
|
||||
us-east-1 | HVM | [ami-a7151cdd](https://us-east-1.console.aws.amazon.com/ec2/home?region=us-east-1#launchInstanceWizard:ami=ami-a7151cdd) | [ami-38964e45](https://us-east-1.console.aws.amazon.com/ec2/home?region=us-east-1#launchInstanceWizard:ami=ami-38964e45)
|
||||
us-east-2 | HVM | [ami-a383b6c6](https://us-east-2.console.aws.amazon.com/ec2/home?region=us-east-2#launchInstanceWizard:ami=ami-a383b6c6) | [ami-a0d7e6c5](https://us-east-2.console.aws.amazon.com/ec2/home?region=us-east-2#launchInstanceWizard:ami=ami-a0d7e6c5)
|
||||
us-west-1 | HVM | [ami-c4b3bca4](https://us-west-1.console.aws.amazon.com/ec2/home?region=us-west-1#launchInstanceWizard:ami=ami-c4b3bca4) | [ami-a4a0b6c4](https://us-west-1.console.aws.amazon.com/ec2/home?region=us-west-1#launchInstanceWizard:ami=ami-a4a0b6c4)
|
||||
us-west-2 | HVM | [ami-6e1a9e16](https://us-west-2.console.aws.amazon.com/ec2/home?region=us-west-2#launchInstanceWizard:ami=ami-6e1a9e16) | [ami-c60c94be](https://us-west-2.console.aws.amazon.com/ec2/home?region=us-west-2#launchInstanceWizard:ami=ami-c60c94be)
|
||||
cn-north-1 | HVM | [ami-18c11e75](https://cn-north-1.console.amazonaws.cn/ec2/home?region=cn-north-1#launchInstanceWizard:ami=ami-18c11e75) | [ami-07a37d6a](https://cn-north-1.console.amazonaws.cn/ec2/home?region=cn-north-1#launchInstanceWizard:ami=ami-07a37d6a)
|
||||
cn-northwest-1 | HVM | [ami-bcedf9de](https://cn-northwest-1.console.amazonaws.cn/ec2/home?region=cn-northwest-1#launchInstanceWizard:ami=ami-bcedf9de) | [ami-8f8094ed](https://cn-northwest-1.console.amazonaws.cn/ec2/home?region=cn-northwest-1#launchInstanceWizard:ami=ami-8f8094ed)
|
||||
|
||||
Additionally, images are available with support for Amazon EC2 Container Service (ECS) [here](https://docs.rancher.com/os/amazon-ecs/#amazon-ecs-enabled-amis).
|
||||
|
||||
### Google Compute Engine
|
||||
|
||||
We are providing a disk image that users can download and import for use in Google Compute Engine. The image can be obtained from the release artifacts for RancherOS.
|
||||
|
||||
[Download Image](https://github.com/rancher/os/releases/download/v0.6.1/rancheros-v0.6.1.tar.gz)
|
||||
[Download Latest Image](https://releases.rancher.com/os/latest/rancheros-gce.tar.gz)
|
||||
|
||||
[Download Stable Image](https://releases.rancher.com/os/v1.2.0/rancheros-gce.tar.gz)
|
||||
|
||||
Please follow the directions at our [docs to launch in GCE](http://docs.rancher.com/os/running-rancheros/cloud/gce/).
|
||||
|
||||
@@ -85,12 +102,16 @@ Please refer to our [RancherOS Documentation](http://docs.rancher.com/os/) websi
|
||||
## Support, Discussion, and Community
|
||||
If you need any help with RancherOS or Rancher, please join us at either our [Rancher forums](http://forums.rancher.com) or [#rancher IRC channel](http://webchat.freenode.net/?channels=rancher) where most of our team hangs out at.
|
||||
|
||||
For security issues, please email security@rancher.com instead of posting a public issue in GitHub. You may (but are not required to) use the GPG key located on [Keybase](https://keybase.io/rancher).
|
||||
|
||||
|
||||
Please submit any **RancherOS** bugs, issues, and feature requests to [rancher/os](//github.com/rancher/os/issues).
|
||||
|
||||
Please submit any **Rancher** bugs, issues, and feature requests to [rancher/rancher](//github.com/rancher/rancher/issues).
|
||||
|
||||
#License
|
||||
Copyright (c) 2014-2016 [Rancher Labs, Inc.](http://rancher.com)
|
||||
## License
|
||||
|
||||
Copyright (c) 2014-2018 [Rancher Labs, Inc.](http://rancher.com)
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"name": "bridge",
|
||||
"type": "bridge",
|
||||
"bridge": "docker-sys",
|
||||
"isDefaultGateway": true,
|
||||
"ipMasq": true,
|
||||
"hairpinMode": true,
|
||||
"ipam": {
|
||||
"type": "host-local",
|
||||
"subnet": "172.18.42.1/16"
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
bridge.d/
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"path": "/usr/bin/ros",
|
||||
"args": [
|
||||
"cni-glue",
|
||||
"poststop"
|
||||
]
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"path": "/usr/bin/ros",
|
||||
"args": [
|
||||
"cni-glue"
|
||||
]
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
296a297,300
|
||||
> conf.update(name)
|
||||
> }
|
||||
>
|
||||
> func (conf *resolverConfig) update(name string) {
|
||||
300a305,316
|
||||
> }
|
||||
>
|
||||
> func UpdateDnsConf() {
|
||||
> resolvConf.initOnce.Do(resolvConf.init)
|
||||
>
|
||||
> // Ensure only one update at a time checks resolv.conf.
|
||||
> if !resolvConf.tryAcquireSema() {
|
||||
> return
|
||||
> }
|
||||
> defer resolvConf.releaseSema()
|
||||
>
|
||||
> resolvConf.update("/etc/resolv.conf")
|
||||
@@ -1,20 +1,108 @@
|
||||
package cloudinitexecute
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
log "github.com/Sirupsen/logrus"
|
||||
"github.com/rancher/os/log"
|
||||
"github.com/rancher/os/util"
|
||||
)
|
||||
|
||||
func authorizeSSHKeys(user string, authorizedKeys []string, name string) {
|
||||
for _, authorizedKey := range authorizedKeys {
|
||||
cmd := exec.Command("update-ssh-keys", user, authorizedKey)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{"err": err, "user": user, "auth_key": authorizedKey}).Error("Error updating SSH authorized_keys")
|
||||
const (
|
||||
sshDirName = ".ssh"
|
||||
authorizedKeysFileName = "authorized_keys"
|
||||
)
|
||||
|
||||
func authorizeSSHKeys(username string, authorizedKeys []string, name string) error {
|
||||
var uid int
|
||||
var gid int
|
||||
var homeDir string
|
||||
|
||||
bytes, err := ioutil.ReadFile("/etc/passwd")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, line := range strings.Split(string(bytes), "\n") {
|
||||
if strings.HasPrefix(line, username) {
|
||||
split := strings.Split(line, ":")
|
||||
if len(split) < 6 {
|
||||
break
|
||||
}
|
||||
uid, err = strconv.Atoi(split[2])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
gid, err = strconv.Atoi(split[3])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
homeDir = split[5]
|
||||
}
|
||||
}
|
||||
|
||||
sshDir := path.Join(homeDir, sshDirName)
|
||||
authorizedKeysFile := path.Join(sshDir, authorizedKeysFileName)
|
||||
|
||||
if _, err := os.Stat(sshDir); os.IsNotExist(err) {
|
||||
if err = os.Mkdir(sshDir, 0700); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = os.Chown(sshDir, uid, gid); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, authorizedKey := range authorizedKeys {
|
||||
if err = authorizeSSHKey(authorizedKey, authorizedKeysFile, uid, gid); err != nil {
|
||||
log.Errorf("Failed to authorize SSH key %s: %v", authorizedKey, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func authorizeSSHKey(authorizedKey, authorizedKeysFile string, uid, gid int) error {
|
||||
authorizedKeysFileInfo, err := os.Stat(authorizedKeysFile)
|
||||
if os.IsNotExist(err) {
|
||||
keysFile, err := os.Create(authorizedKeysFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = keysFile.Chmod(0600); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = keysFile.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
authorizedKeysFileInfo, err = os.Stat(authorizedKeysFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
bytes, err := ioutil.ReadFile(authorizedKeysFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !strings.Contains(string(bytes), authorizedKey) {
|
||||
bytes = append(bytes, []byte(authorizedKey)...)
|
||||
bytes = append(bytes, '\n')
|
||||
}
|
||||
|
||||
perm := authorizedKeysFileInfo.Mode().Perm()
|
||||
if err = util.WriteFileAtomic(authorizedKeysFile, bytes, perm); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.Chown(authorizedKeysFile, uid, gid)
|
||||
}
|
||||
|
||||
@@ -9,10 +9,11 @@ import (
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
log "github.com/Sirupsen/logrus"
|
||||
"github.com/coreos/coreos-cloudinit/system"
|
||||
rancherConfig "github.com/rancher/os/config"
|
||||
"github.com/rancher/os/config/cloudinit/config"
|
||||
"github.com/rancher/os/config/cloudinit/system"
|
||||
"github.com/rancher/os/docker"
|
||||
"github.com/rancher/os/log"
|
||||
"github.com/rancher/os/util"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
@@ -37,6 +38,7 @@ func init() {
|
||||
func Main() {
|
||||
flags.Parse(os.Args[1:])
|
||||
|
||||
log.InitLogger()
|
||||
log.Infof("Running cloud-init-execute: pre-console=%v, console=%v", preConsole, console)
|
||||
|
||||
cfg := rancherConfig.LoadConfig()
|
||||
@@ -56,8 +58,12 @@ func Main() {
|
||||
|
||||
func ApplyConsole(cfg *rancherConfig.CloudConfig) {
|
||||
if len(cfg.SSHAuthorizedKeys) > 0 {
|
||||
authorizeSSHKeys("rancher", cfg.SSHAuthorizedKeys, sshKeyName)
|
||||
authorizeSSHKeys("docker", cfg.SSHAuthorizedKeys, sshKeyName)
|
||||
if err := authorizeSSHKeys("rancher", cfg.SSHAuthorizedKeys, sshKeyName); err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
if err := authorizeSSHKeys("docker", cfg.SSHAuthorizedKeys, sshKeyName); err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
WriteFiles(cfg, "console")
|
||||
@@ -66,6 +72,25 @@ func ApplyConsole(cfg *rancherConfig.CloudConfig) {
|
||||
if len(mount) != 4 {
|
||||
log.Errorf("Unable to mount %s: must specify exactly four arguments", mount[1])
|
||||
}
|
||||
|
||||
if mount[2] == "nfs" || mount[2] == "nfs4" {
|
||||
if err := os.MkdirAll(mount[1], 0755); err != nil {
|
||||
log.Errorf("Unable to create mount point %s: %v", mount[1], err)
|
||||
continue
|
||||
}
|
||||
cmdArgs := []string{mount[0], mount[1], "-t", mount[2]}
|
||||
if mount[3] != "" {
|
||||
cmdArgs = append(cmdArgs, "-o", mount[3])
|
||||
}
|
||||
cmd := exec.Command("mount", cmdArgs...)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
log.Errorf("Failed to mount %s: %v", mount[1], err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
device := util.ResolveDevice(mount[0])
|
||||
|
||||
if mount[2] == "swap" {
|
||||
@@ -79,32 +104,14 @@ func ApplyConsole(cfg *rancherConfig.CloudConfig) {
|
||||
continue
|
||||
}
|
||||
|
||||
cmdArgs := []string{device, mount[1]}
|
||||
if mount[2] != "" {
|
||||
cmdArgs = append(cmdArgs, "-t", mount[2])
|
||||
}
|
||||
if mount[3] != "" {
|
||||
cmdArgs = append(cmdArgs, "-o", mount[3])
|
||||
}
|
||||
cmd := exec.Command("mount", cmdArgs...)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
if err := util.Mount(device, mount[1], mount[2], mount[3]); err != nil {
|
||||
log.Errorf("Failed to mount %s: %v", mount[1], err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, runcmd := range cfg.Runcmd {
|
||||
if len(runcmd) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
cmd := exec.Command(runcmd[0], runcmd[1:]...)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
log.Errorf("Failed to run %s: %v", runcmd, err)
|
||||
}
|
||||
err := util.RunCommandSequence(cfg.Runcmd)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,6 +125,14 @@ func WriteFiles(cfg *rancherConfig.CloudConfig, container string) {
|
||||
continue
|
||||
}
|
||||
|
||||
content, err := config.DecodeContent(file.File.Content, file.File.Encoding)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
file.File.Content = string(content)
|
||||
file.File.Encoding = ""
|
||||
|
||||
f := system.File{
|
||||
File: file.File,
|
||||
}
|
||||
@@ -131,11 +146,15 @@ func WriteFiles(cfg *rancherConfig.CloudConfig, container string) {
|
||||
}
|
||||
|
||||
func applyPreConsole(cfg *rancherConfig.CloudConfig) {
|
||||
if _, err := os.Stat(resizeStamp); os.IsNotExist(err) && cfg.Rancher.ResizeDevice != "" {
|
||||
if err := resizeDevice(cfg); err == nil {
|
||||
os.Create(resizeStamp)
|
||||
if cfg.Rancher.ResizeDevice != "" {
|
||||
if _, err := os.Stat(resizeStamp); os.IsNotExist(err) {
|
||||
if err := resizeDevice(cfg); err == nil {
|
||||
os.Create(resizeStamp)
|
||||
} else {
|
||||
log.Errorf("Failed to resize %s: %s", cfg.Rancher.ResizeDevice, err)
|
||||
}
|
||||
} else {
|
||||
log.Errorf("Failed to resize %s: %s", cfg.Rancher.ResizeDevice, err)
|
||||
log.Infof("Skipped resizing %s because %s exists", cfg.Rancher.ResizeDevice, resizeStamp)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,7 +193,11 @@ func resizeDevice(cfg *rancherConfig.CloudConfig) error {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd = exec.Command("resize2fs", fmt.Sprintf("%s1", cfg.Rancher.ResizeDevice))
|
||||
targetPartition := fmt.Sprintf("%s1", cfg.Rancher.ResizeDevice)
|
||||
if strings.Contains(cfg.Rancher.ResizeDevice, "nvme") {
|
||||
targetPartition = fmt.Sprintf("%sp1", cfg.Rancher.ResizeDevice)
|
||||
}
|
||||
cmd = exec.Command("resize2fs", targetPartition)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
err = cmd.Run()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Copyright 2015 CoreOS, Inc.
|
||||
// Copyright 2015 Rancher Labs, Inc.
|
||||
// Copyright 2015-2017 Rancher Labs, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -16,29 +16,35 @@
|
||||
package cloudinitsave
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"flag"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
yaml "github.com/cloudfoundry-incubator/candiedyaml"
|
||||
|
||||
log "github.com/Sirupsen/logrus"
|
||||
"github.com/coreos/coreos-cloudinit/config"
|
||||
"github.com/coreos/coreos-cloudinit/datasource"
|
||||
"github.com/coreos/coreos-cloudinit/datasource/configdrive"
|
||||
"github.com/coreos/coreos-cloudinit/datasource/file"
|
||||
"github.com/coreos/coreos-cloudinit/datasource/metadata/digitalocean"
|
||||
"github.com/coreos/coreos-cloudinit/datasource/metadata/ec2"
|
||||
"github.com/coreos/coreos-cloudinit/datasource/metadata/packet"
|
||||
"github.com/coreos/coreos-cloudinit/datasource/proc_cmdline"
|
||||
"github.com/coreos/coreos-cloudinit/datasource/url"
|
||||
"github.com/coreos/coreos-cloudinit/pkg"
|
||||
"github.com/rancher/netconf"
|
||||
"github.com/rancher/os/cmd/cloudinitsave/gce"
|
||||
"github.com/rancher/os/cmd/control"
|
||||
"github.com/rancher/os/cmd/network"
|
||||
rancherConfig "github.com/rancher/os/config"
|
||||
"github.com/rancher/os/config/cloudinit/config"
|
||||
"github.com/rancher/os/config/cloudinit/datasource"
|
||||
"github.com/rancher/os/config/cloudinit/datasource/configdrive"
|
||||
"github.com/rancher/os/config/cloudinit/datasource/file"
|
||||
"github.com/rancher/os/config/cloudinit/datasource/metadata/aliyun"
|
||||
"github.com/rancher/os/config/cloudinit/datasource/metadata/cloudstack"
|
||||
"github.com/rancher/os/config/cloudinit/datasource/metadata/digitalocean"
|
||||
"github.com/rancher/os/config/cloudinit/datasource/metadata/ec2"
|
||||
"github.com/rancher/os/config/cloudinit/datasource/metadata/gce"
|
||||
"github.com/rancher/os/config/cloudinit/datasource/metadata/packet"
|
||||
"github.com/rancher/os/config/cloudinit/datasource/proccmdline"
|
||||
"github.com/rancher/os/config/cloudinit/datasource/url"
|
||||
"github.com/rancher/os/config/cloudinit/datasource/vmware"
|
||||
"github.com/rancher/os/config/cloudinit/pkg"
|
||||
"github.com/rancher/os/log"
|
||||
"github.com/rancher/os/netconf"
|
||||
"github.com/rancher/os/util"
|
||||
)
|
||||
|
||||
@@ -48,26 +54,61 @@ const (
|
||||
datasourceTimeout = 5 * time.Minute
|
||||
)
|
||||
|
||||
var (
|
||||
network bool
|
||||
flags *flag.FlagSet
|
||||
)
|
||||
|
||||
func init() {
|
||||
flags = flag.NewFlagSet(os.Args[0], flag.ContinueOnError)
|
||||
flags.BoolVar(&network, "network", true, "use network based datasources")
|
||||
}
|
||||
|
||||
func Main() {
|
||||
flags.Parse(os.Args[1:])
|
||||
log.InitLogger()
|
||||
log.Info("Running cloud-init-save")
|
||||
|
||||
log.Infof("Running cloud-init-save: network=%v", network)
|
||||
if err := control.UdevSettle(); err != nil {
|
||||
log.Errorf("Failed to run udev settle: %v", err)
|
||||
}
|
||||
|
||||
if err := saveCloudConfig(); err != nil {
|
||||
log.Errorf("Failed to save cloud-config: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func saveCloudConfig() error {
|
||||
log.Infof("SaveCloudConfig")
|
||||
|
||||
cfg := rancherConfig.LoadConfig()
|
||||
log.Debugf("init: SaveCloudConfig(pre ApplyNetworkConfig): %#v", cfg.Rancher.Network)
|
||||
network.ApplyNetworkConfig(cfg)
|
||||
|
||||
log.Infof("datasources that will be consided: %#v", cfg.Rancher.CloudInit.Datasources)
|
||||
dss := getDatasources(cfg.Rancher.CloudInit.Datasources)
|
||||
if len(dss) == 0 {
|
||||
log.Errorf("currentDatasource - none found")
|
||||
return nil
|
||||
}
|
||||
|
||||
foundDs := selectDatasource(dss)
|
||||
log.Infof("Cloud-init datasource that was used: %s", foundDs)
|
||||
|
||||
// Apply any newly detected network config.
|
||||
cfg = rancherConfig.LoadConfig()
|
||||
log.Debugf("init: SaveCloudConfig(post ApplyNetworkConfig): %#v", cfg.Rancher.Network)
|
||||
network.ApplyNetworkConfig(cfg)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func RequiresNetwork(datasource string) bool {
|
||||
// TODO: move into the datasources (and metadatasources)
|
||||
// and then we can enable that platforms defaults..
|
||||
parts := strings.SplitN(datasource, ":", 2)
|
||||
requiresNetwork, ok := map[string]bool{
|
||||
"ec2": true,
|
||||
"file": false,
|
||||
"url": true,
|
||||
"cmdline": true,
|
||||
"configdrive": false,
|
||||
"digitalocean": true,
|
||||
"gce": true,
|
||||
"packet": true,
|
||||
}[parts[0]]
|
||||
return ok && requiresNetwork
|
||||
}
|
||||
|
||||
func saveFiles(cloudConfigBytes, scriptBytes []byte, metadata datasource.Metadata) error {
|
||||
os.MkdirAll(rancherConfig.CloudConfigDir, os.ModeDir|0600)
|
||||
|
||||
@@ -83,7 +124,7 @@ func saveFiles(cloudConfigBytes, scriptBytes []byte, metadata datasource.Metadat
|
||||
if err := util.WriteFileAtomic(rancherConfig.CloudConfigBootFile, cloudConfigBytes, 400); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Infof("Written to %s:\n%s", rancherConfig.CloudConfigBootFile, string(cloudConfigBytes))
|
||||
log.Infof("Wrote to %s", rancherConfig.CloudConfigBootFile)
|
||||
}
|
||||
|
||||
metaDataBytes, err := yaml.Marshal(metadata)
|
||||
@@ -94,26 +135,57 @@ func saveFiles(cloudConfigBytes, scriptBytes []byte, metadata datasource.Metadat
|
||||
if err = util.WriteFileAtomic(rancherConfig.MetaDataFile, metaDataBytes, 400); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Infof("Written to %s:\n%s", rancherConfig.MetaDataFile, string(metaDataBytes))
|
||||
log.Infof("Wrote to %s", rancherConfig.MetaDataFile)
|
||||
|
||||
// if we write the empty meta yml, the merge fails.
|
||||
// TODO: the problem is that a partially filled one will still have merge issues, so that needs fixing - presumably by making merge more clever, and making more fields optional
|
||||
emptyMeta, err := yaml.Marshal(datasource.Metadata{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if bytes.Compare(metaDataBytes, emptyMeta) == 0 {
|
||||
log.Infof("not writing %s: its all defaults.", rancherConfig.CloudConfigNetworkFile)
|
||||
return nil
|
||||
}
|
||||
|
||||
type nonRancherCfg struct {
|
||||
Network netconf.NetworkConfig `yaml:"network,omitempty"`
|
||||
}
|
||||
type nonCfg struct {
|
||||
Rancher nonRancherCfg `yaml:"rancher,omitempty"`
|
||||
}
|
||||
// write the network.yml file from metadata
|
||||
cc := nonCfg{
|
||||
Rancher: nonRancherCfg{
|
||||
Network: metadata.NetworkConfig,
|
||||
},
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(path.Dir(rancherConfig.CloudConfigNetworkFile), 0700); err != nil {
|
||||
log.Errorf("Failed to create directory for file %s: %v", rancherConfig.CloudConfigNetworkFile, err)
|
||||
}
|
||||
|
||||
if err := rancherConfig.WriteToFile(cc, rancherConfig.CloudConfigNetworkFile); err != nil {
|
||||
log.Errorf("Failed to save config file %s: %v", rancherConfig.CloudConfigNetworkFile, err)
|
||||
}
|
||||
log.Infof("Wrote to %s", rancherConfig.CloudConfigNetworkFile)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func currentDatasource() (datasource.Datasource, error) {
|
||||
cfg := rancherConfig.LoadConfig()
|
||||
func fetchAndSave(ds datasource.Datasource) error {
|
||||
var metadata datasource.Metadata
|
||||
|
||||
dss := getDatasources(cfg)
|
||||
if len(dss) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
ds := selectDatasource(dss)
|
||||
return ds, nil
|
||||
}
|
||||
|
||||
func saveCloudConfig() error {
|
||||
userDataBytes, metadata, err := fetchUserData()
|
||||
log.Infof("Fetching user-data from datasource %s", ds)
|
||||
userDataBytes, err := ds.FetchUserdata()
|
||||
if err != nil {
|
||||
log.Errorf("Failed fetching user-data from datasource: %v", err)
|
||||
return err
|
||||
}
|
||||
log.Infof("Fetching meta-data from datasource of type %v", ds.Type())
|
||||
metadata, err = ds.FetchMetadata()
|
||||
if err != nil {
|
||||
log.Errorf("Failed fetching meta-data from datasource: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -134,7 +206,7 @@ func saveCloudConfig() error {
|
||||
userDataBytes = []byte{}
|
||||
}
|
||||
} else {
|
||||
log.Errorf("Unrecognized user-data\n%s", userData)
|
||||
log.Errorf("Unrecognized user-data\n(%s)", userData)
|
||||
userDataBytes = []byte{}
|
||||
}
|
||||
|
||||
@@ -146,84 +218,60 @@ func saveCloudConfig() error {
|
||||
return saveFiles(userDataBytes, scriptBytes, metadata)
|
||||
}
|
||||
|
||||
func fetchUserData() ([]byte, datasource.Metadata, error) {
|
||||
var metadata datasource.Metadata
|
||||
ds, err := currentDatasource()
|
||||
if err != nil || ds == nil {
|
||||
log.Errorf("Failed to select datasource: %v", err)
|
||||
return nil, metadata, err
|
||||
}
|
||||
log.Infof("Fetching user-data from datasource %v", ds.Type())
|
||||
userDataBytes, err := ds.FetchUserdata()
|
||||
if err != nil {
|
||||
log.Errorf("Failed fetching user-data from datasource: %v", err)
|
||||
return nil, metadata, err
|
||||
}
|
||||
log.Infof("Fetching meta-data from datasource of type %v", ds.Type())
|
||||
metadata, err = ds.FetchMetadata()
|
||||
if err != nil {
|
||||
log.Errorf("Failed fetching meta-data from datasource: %v", err)
|
||||
return nil, metadata, err
|
||||
}
|
||||
return userDataBytes, metadata, nil
|
||||
}
|
||||
|
||||
// getDatasources creates a slice of possible Datasources for cloudinit based
|
||||
// on the different source command-line flags.
|
||||
func getDatasources(cfg *rancherConfig.CloudConfig) []datasource.Datasource {
|
||||
func getDatasources(datasources []string) []datasource.Datasource {
|
||||
dss := make([]datasource.Datasource, 0, 5)
|
||||
|
||||
for _, ds := range cfg.Rancher.CloudInit.Datasources {
|
||||
for _, ds := range datasources {
|
||||
parts := strings.SplitN(ds, ":", 2)
|
||||
|
||||
root := ""
|
||||
if len(parts) > 1 {
|
||||
root = parts[1]
|
||||
}
|
||||
|
||||
switch parts[0] {
|
||||
case "ec2":
|
||||
if network {
|
||||
if len(parts) == 1 {
|
||||
dss = append(dss, ec2.NewDatasource(ec2.DefaultAddress))
|
||||
} else {
|
||||
dss = append(dss, ec2.NewDatasource(parts[1]))
|
||||
}
|
||||
case "*":
|
||||
dss = append(dss, getDatasources([]string{"configdrive", "vmware", "ec2", "digitalocean", "packet", "gce", "cloudstack"})...)
|
||||
case "cloudstack":
|
||||
for _, source := range cloudstack.NewDatasource(root) {
|
||||
dss = append(dss, source)
|
||||
}
|
||||
case "ec2":
|
||||
dss = append(dss, ec2.NewDatasource(root))
|
||||
case "file":
|
||||
if len(parts) == 2 {
|
||||
dss = append(dss, file.NewDatasource(parts[1]))
|
||||
if root != "" {
|
||||
dss = append(dss, file.NewDatasource(root))
|
||||
}
|
||||
case "url":
|
||||
if network {
|
||||
if len(parts) == 2 {
|
||||
dss = append(dss, url.NewDatasource(parts[1]))
|
||||
}
|
||||
if root != "" {
|
||||
dss = append(dss, url.NewDatasource(root))
|
||||
}
|
||||
case "cmdline":
|
||||
if network {
|
||||
if len(parts) == 1 {
|
||||
dss = append(dss, proc_cmdline.NewDatasource())
|
||||
}
|
||||
if len(parts) == 1 {
|
||||
dss = append(dss, proccmdline.NewDatasource())
|
||||
}
|
||||
case "configdrive":
|
||||
if len(parts) == 2 {
|
||||
dss = append(dss, configdrive.NewDatasource(parts[1]))
|
||||
if root == "" {
|
||||
root = "/media/config-2"
|
||||
}
|
||||
dss = append(dss, configdrive.NewDatasource(root))
|
||||
case "digitalocean":
|
||||
if network {
|
||||
if len(parts) == 1 {
|
||||
dss = append(dss, digitalocean.NewDatasource(digitalocean.DefaultAddress))
|
||||
} else {
|
||||
dss = append(dss, digitalocean.NewDatasource(parts[1]))
|
||||
}
|
||||
} else {
|
||||
enableDoLinkLocal()
|
||||
}
|
||||
// TODO: should we enableDoLinkLocal() - to avoid the need for the other kernel/oem options?
|
||||
dss = append(dss, digitalocean.NewDatasource(root))
|
||||
case "gce":
|
||||
if network {
|
||||
dss = append(dss, gce.NewDatasource("http://metadata.google.internal/"))
|
||||
}
|
||||
dss = append(dss, gce.NewDatasource(root))
|
||||
case "packet":
|
||||
if !network {
|
||||
enablePacketNetwork(&cfg.Rancher)
|
||||
dss = append(dss, packet.NewDatasource(root))
|
||||
case "vmware":
|
||||
// made vmware datasource dependent on detecting vmware independently, as it crashes things otherwise
|
||||
v := vmware.NewDatasource(root)
|
||||
if v != nil {
|
||||
dss = append(dss, v)
|
||||
}
|
||||
dss = append(dss, packet.NewDatasource("https://metadata.packet.net/"))
|
||||
case "aliyun":
|
||||
dss = append(dss, aliyun.NewDatasource(root))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -231,13 +279,13 @@ func getDatasources(cfg *rancherConfig.CloudConfig) []datasource.Datasource {
|
||||
}
|
||||
|
||||
func enableDoLinkLocal() {
|
||||
err := netconf.ApplyNetworkConfigs(&netconf.NetworkConfig{
|
||||
_, err := netconf.ApplyNetworkConfigs(&netconf.NetworkConfig{
|
||||
Interfaces: map[string]netconf.InterfaceConfig{
|
||||
"eth0": {
|
||||
IPV4LL: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
}, false, false)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to apply link local on eth0: %v", err)
|
||||
}
|
||||
@@ -260,13 +308,17 @@ func selectDatasource(sources []datasource.Datasource) datasource.Datasource {
|
||||
|
||||
duration := datasourceInterval
|
||||
for {
|
||||
log.Infof("Checking availability of %q\n", s.Type())
|
||||
log.Infof("cloud-init: Checking availability of %q", s.Type())
|
||||
if s.IsAvailable() {
|
||||
log.Infof("cloud-init: Datasource available: %s", s)
|
||||
ds <- s
|
||||
return
|
||||
} else if !s.AvailabilityChanges() {
|
||||
}
|
||||
if !s.AvailabilityChanges() {
|
||||
log.Infof("cloud-init: Datasource unavailable, skipping: %s", s)
|
||||
return
|
||||
}
|
||||
log.Errorf("cloud-init: Datasource not ready, will retry: %s", s)
|
||||
select {
|
||||
case <-stop:
|
||||
return
|
||||
@@ -286,6 +338,10 @@ func selectDatasource(sources []datasource.Datasource) datasource.Datasource {
|
||||
var s datasource.Datasource
|
||||
select {
|
||||
case s = <-ds:
|
||||
err := fetchAndSave(s)
|
||||
if err != nil {
|
||||
log.Errorf("Error fetching cloud-init datasource(%s): %s", s, err)
|
||||
}
|
||||
case <-done:
|
||||
case <-time.After(datasourceTimeout):
|
||||
}
|
||||
|
||||
@@ -1,130 +0,0 @@
|
||||
// Copyright 2016 CoreOS, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package gce
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/coreos/coreos-cloudinit/datasource"
|
||||
"github.com/coreos/coreos-cloudinit/datasource/metadata"
|
||||
)
|
||||
|
||||
const (
|
||||
apiVersion = "computeMetadata/v1/"
|
||||
metadataPath = apiVersion
|
||||
userdataPath = apiVersion + "instance/attributes/user-data"
|
||||
)
|
||||
|
||||
type metadataService struct {
|
||||
metadata.MetadataService
|
||||
}
|
||||
|
||||
func NewDatasource(root string) *metadataService {
|
||||
return &metadataService{metadata.NewDatasource(root, apiVersion, userdataPath, metadataPath, http.Header{"Metadata-Flavor": {"Google"}})}
|
||||
}
|
||||
|
||||
func (ms metadataService) FetchMetadata() (datasource.Metadata, error) {
|
||||
public, err := ms.fetchIP("instance/network-interfaces/0/access-configs/0/external-ip")
|
||||
if err != nil {
|
||||
return datasource.Metadata{}, err
|
||||
}
|
||||
local, err := ms.fetchIP("instance/network-interfaces/0/ip")
|
||||
if err != nil {
|
||||
return datasource.Metadata{}, err
|
||||
}
|
||||
hostname, err := ms.fetchString("instance/hostname")
|
||||
if err != nil {
|
||||
return datasource.Metadata{}, err
|
||||
}
|
||||
|
||||
projectSshKeys, err := ms.fetchString("project/attributes/sshKeys")
|
||||
if err != nil {
|
||||
return datasource.Metadata{}, err
|
||||
}
|
||||
instanceSshKeys, err := ms.fetchString("instance/attributes/sshKeys")
|
||||
if err != nil {
|
||||
return datasource.Metadata{}, err
|
||||
}
|
||||
|
||||
keyStrings := strings.Split(projectSshKeys+"\n"+instanceSshKeys, "\n")
|
||||
|
||||
sshPublicKeys := map[string]string{}
|
||||
i := 0
|
||||
for _, keyString := range keyStrings {
|
||||
keySlice := strings.SplitN(keyString, ":", 2)
|
||||
if len(keySlice) == 2 {
|
||||
key := strings.TrimSpace(keySlice[1])
|
||||
if key != "" {
|
||||
sshPublicKeys[strconv.Itoa(i)] = strings.TrimSpace(keySlice[1])
|
||||
i++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return datasource.Metadata{
|
||||
PublicIPv4: public,
|
||||
PrivateIPv4: local,
|
||||
Hostname: hostname,
|
||||
SSHPublicKeys: sshPublicKeys,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (ms metadataService) Type() string {
|
||||
return "gce-metadata-service"
|
||||
}
|
||||
|
||||
func (ms metadataService) fetchString(key string) (string, error) {
|
||||
data, err := ms.FetchData(ms.MetadataUrl() + key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
func (ms metadataService) fetchIP(key string) (net.IP, error) {
|
||||
str, err := ms.fetchString(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if str == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if ip := net.ParseIP(str); ip != nil {
|
||||
return ip, nil
|
||||
} else {
|
||||
return nil, fmt.Errorf("couldn't parse %q as IP address", str)
|
||||
}
|
||||
}
|
||||
|
||||
func (ms metadataService) FetchUserdata() ([]byte, error) {
|
||||
data, err := ms.FetchData(ms.UserdataUrl())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(data) == 0 {
|
||||
data, err = ms.FetchData(ms.MetadataUrl() + "instance/attributes/startup-script")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
package cloudinitsave
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
yaml "github.com/cloudfoundry-incubator/candiedyaml"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/packethost/packngo/metadata"
|
||||
"github.com/rancher/netconf"
|
||||
rancherConfig "github.com/rancher/os/config"
|
||||
)
|
||||
|
||||
func enablePacketNetwork(cfg *rancherConfig.RancherConfig) {
|
||||
bootStrapped := false
|
||||
for _, v := range cfg.Network.Interfaces {
|
||||
if v.Address != "" {
|
||||
if err := netconf.ApplyNetworkConfigs(&cfg.Network); err != nil {
|
||||
logrus.Errorf("Failed to bootstrap network: %v", err)
|
||||
return
|
||||
}
|
||||
bootStrapped = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !bootStrapped {
|
||||
return
|
||||
}
|
||||
|
||||
c := metadata.NewClient(http.DefaultClient)
|
||||
m, err := c.Metadata.Get()
|
||||
if err != nil {
|
||||
logrus.Errorf("Failed to get Packet metadata: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
bondCfg := netconf.InterfaceConfig{
|
||||
Addresses: []string{},
|
||||
BondOpts: map[string]string{
|
||||
"lacp_rate": "1",
|
||||
"xmit_hash_policy": "layer3+4",
|
||||
"downdelay": "200",
|
||||
"updelay": "200",
|
||||
"miimon": "100",
|
||||
"mode": "4",
|
||||
},
|
||||
}
|
||||
netCfg := netconf.NetworkConfig{
|
||||
Interfaces: map[string]netconf.InterfaceConfig{},
|
||||
}
|
||||
for _, iface := range m.Network.Interfaces {
|
||||
netCfg.Interfaces["mac="+iface.Mac] = netconf.InterfaceConfig{
|
||||
Bond: "bond0",
|
||||
}
|
||||
}
|
||||
for _, addr := range m.Network.Addresses {
|
||||
bondCfg.Addresses = append(bondCfg.Addresses, fmt.Sprintf("%s/%d", addr.Address, addr.Cidr))
|
||||
if addr.Gateway != "" {
|
||||
if addr.AddressFamily == 4 {
|
||||
if addr.Public {
|
||||
bondCfg.Gateway = addr.Gateway
|
||||
}
|
||||
} else {
|
||||
bondCfg.GatewayIpv6 = addr.Gateway
|
||||
}
|
||||
}
|
||||
|
||||
if addr.AddressFamily == 4 && strings.HasPrefix(addr.Gateway, "10.") {
|
||||
bondCfg.PostUp = append(bondCfg.PostUp, "ip route add 10.0.0.0/8 via "+addr.Gateway)
|
||||
}
|
||||
}
|
||||
|
||||
netCfg.Interfaces["bond0"] = bondCfg
|
||||
b, _ := yaml.Marshal(netCfg)
|
||||
logrus.Debugf("Generated network config: %s", string(b))
|
||||
|
||||
cc := rancherConfig.CloudConfig{
|
||||
Rancher: rancherConfig.RancherConfig{
|
||||
Network: netCfg,
|
||||
},
|
||||
}
|
||||
|
||||
// Post to phone home URL on first boot
|
||||
if _, err = os.Stat(rancherConfig.CloudConfigNetworkFile); err != nil {
|
||||
if _, err = http.Post(m.PhoneHomeURL, "application/json", bytes.NewReader([]byte{})); err != nil {
|
||||
logrus.Errorf("Failed to post to Packet phone home URL: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(path.Dir(rancherConfig.CloudConfigNetworkFile), 0700); err != nil {
|
||||
logrus.Errorf("Failed to create directory for file %s: %v", rancherConfig.CloudConfigNetworkFile, err)
|
||||
}
|
||||
|
||||
if err := rancherConfig.WriteToFile(cc, rancherConfig.CloudConfigNetworkFile); err != nil {
|
||||
logrus.Errorf("Failed to save config file %s: %v", rancherConfig.CloudConfigNetworkFile, err)
|
||||
}
|
||||
}
|
||||
104
cmd/control/autologin.go
Normal file
104
cmd/control/autologin.go
Normal file
@@ -0,0 +1,104 @@
|
||||
package control
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/codegangsta/cli"
|
||||
"github.com/rancher/os/config"
|
||||
"github.com/rancher/os/log"
|
||||
)
|
||||
|
||||
func AutologinMain() {
|
||||
log.InitLogger()
|
||||
app := cli.NewApp()
|
||||
|
||||
app.Name = os.Args[0]
|
||||
app.Usage = "autologin console"
|
||||
app.Version = config.Version
|
||||
app.Author = "Rancher Labs, Inc."
|
||||
app.Email = "sven@rancher.com"
|
||||
app.EnableBashCompletion = true
|
||||
app.Action = autologinAction
|
||||
app.HideHelp = true
|
||||
app.Run(os.Args)
|
||||
}
|
||||
|
||||
func autologinAction(c *cli.Context) error {
|
||||
cmd := exec.Command("/bin/stty", "sane")
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stdin = os.Stdin
|
||||
if err := cmd.Run(); err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
|
||||
usertty := ""
|
||||
user := "root"
|
||||
tty := ""
|
||||
if c.NArg() > 0 {
|
||||
usertty = c.Args().Get(0)
|
||||
s := strings.SplitN(usertty, ":", 2)
|
||||
user = s[0]
|
||||
if len(s) > 1 {
|
||||
tty = s[1]
|
||||
}
|
||||
}
|
||||
|
||||
mode := filepath.Base(os.Args[0])
|
||||
console := CurrentConsole()
|
||||
|
||||
cfg := config.LoadConfig()
|
||||
// replace \n and \l
|
||||
banner := config.Banner
|
||||
banner = strings.Replace(banner, "\\v", config.Version, -1)
|
||||
banner = strings.Replace(banner, "\\s", "RancherOS "+runtime.GOARCH, -1)
|
||||
banner = strings.Replace(banner, "\\r", config.GetKernelVersion(), -1)
|
||||
banner = strings.Replace(banner, "\\n", cfg.Hostname, -1)
|
||||
banner = strings.Replace(banner, "\\l", tty, -1)
|
||||
banner = strings.Replace(banner, "\\\\", "\\", -1)
|
||||
banner = banner + "\n"
|
||||
banner = banner + "Autologin " + console + "\n"
|
||||
fmt.Printf(banner)
|
||||
|
||||
loginBin := ""
|
||||
args := []string{}
|
||||
if console == "centos" || console == "fedora" ||
|
||||
mode == "recovery" {
|
||||
// For some reason, centos and fedora ttyS0 and tty1 don't work with `login -f rancher`
|
||||
// until I make time to read their source, lets just give us a way to get work done
|
||||
loginBin = "bash"
|
||||
args = append(args, "--login")
|
||||
if mode == "recovery" {
|
||||
os.Setenv("PROMPT_COMMAND", `echo "[`+fmt.Sprintf("Recovery console %s@%s:${PWD}", user, cfg.Hostname)+`]"`)
|
||||
}
|
||||
} else {
|
||||
loginBin = "login"
|
||||
args = append(args, "-f", user)
|
||||
// TODO: add a PROMPT_COMMAND if we haven't switch-rooted
|
||||
}
|
||||
|
||||
loginBinPath, err := exec.LookPath(loginBin)
|
||||
if err != nil {
|
||||
fmt.Printf("error finding %s in path: %s", cmd.Args[0], err)
|
||||
return err
|
||||
}
|
||||
os.Setenv("TERM", "linux")
|
||||
|
||||
// Causes all sorts of issues
|
||||
//return syscall.Exec(loginBinPath, args, os.Environ())
|
||||
cmd = exec.Command(loginBinPath, args...)
|
||||
cmd.Env = os.Environ()
|
||||
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stdin = os.Stdin
|
||||
if err := cmd.Run(); err != nil {
|
||||
log.Errorf("\nError starting %s: %s", cmd.Args[0], err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
116
cmd/control/bootstrap.go
Normal file
116
cmd/control/bootstrap.go
Normal file
@@ -0,0 +1,116 @@
|
||||
package control
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/rancher/os/config"
|
||||
"github.com/rancher/os/log"
|
||||
"github.com/rancher/os/util"
|
||||
)
|
||||
|
||||
func BootstrapMain() {
|
||||
log.InitLogger()
|
||||
|
||||
log.Debugf("bootstrapAction")
|
||||
if err := UdevSettle(); err != nil {
|
||||
log.Errorf("Failed to run udev settle: %v", err)
|
||||
}
|
||||
|
||||
log.Debugf("bootstrapAction: loadingConfig")
|
||||
cfg := config.LoadConfig()
|
||||
|
||||
log.Debugf("bootstrapAction: MdadmScan(%v)", cfg.Rancher.State.MdadmScan)
|
||||
if cfg.Rancher.State.MdadmScan {
|
||||
if err := mdadmScan(); err != nil {
|
||||
log.Errorf("Failed to run mdadm scan: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
stateScript := cfg.Rancher.State.Script
|
||||
log.Debugf("bootstrapAction: stateScript(%v)", stateScript)
|
||||
if stateScript != "" {
|
||||
if err := runStateScript(stateScript); err != nil {
|
||||
log.Errorf("Failed to run state script: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
log.Debugf("bootstrapAction: RunCommandSequence(%v)", cfg.Bootcmd)
|
||||
err := util.RunCommandSequence(cfg.Bootcmd)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
|
||||
if cfg.Rancher.State.Dev != "" && cfg.Rancher.State.Wait {
|
||||
waitForRoot(cfg)
|
||||
}
|
||||
|
||||
if len(cfg.Rancher.State.Autoformat) > 0 {
|
||||
log.Infof("bootstrap container: Autoformat(%v) as %s", cfg.Rancher.State.Autoformat, "ext4")
|
||||
if err := autoformat(cfg.Rancher.State.Autoformat); err != nil {
|
||||
log.Errorf("Failed to run autoformat: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
log.Debugf("bootstrapAction: udev settle2")
|
||||
if err := UdevSettle(); err != nil {
|
||||
log.Errorf("Failed to run udev settle: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func mdadmScan() error {
|
||||
cmd := exec.Command("mdadm", "--assemble", "--scan")
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
func runStateScript(script string) error {
|
||||
f, err := ioutil.TempFile("", "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := f.WriteString(script); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := f.Chmod(os.ModePerm); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := f.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
return util.RunScript(f.Name())
|
||||
}
|
||||
|
||||
func waitForRoot(cfg *config.CloudConfig) {
|
||||
var dev string
|
||||
for i := 0; i < 30; i++ {
|
||||
dev = util.ResolveDevice(cfg.Rancher.State.Dev)
|
||||
if dev != "" {
|
||||
break
|
||||
}
|
||||
time.Sleep(time.Millisecond * 1000)
|
||||
}
|
||||
if dev == "" {
|
||||
return
|
||||
}
|
||||
for i := 0; i < 30; i++ {
|
||||
if _, err := os.Stat(dev); err == nil {
|
||||
break
|
||||
}
|
||||
time.Sleep(time.Millisecond * 1000)
|
||||
}
|
||||
}
|
||||
|
||||
func autoformat(autoformatDevices []string) error {
|
||||
cmd := exec.Command("/usr/sbin/auto-format.sh")
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Env = []string{
|
||||
"AUTOFORMAT=" + strings.Join(autoformatDevices, " "),
|
||||
}
|
||||
return cmd.Run()
|
||||
}
|
||||
@@ -1,19 +1,27 @@
|
||||
package control
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
log "github.com/Sirupsen/logrus"
|
||||
"github.com/codegangsta/cli"
|
||||
"github.com/rancher/os/cmd/control/service"
|
||||
"github.com/rancher/os/config"
|
||||
"github.com/rancher/os/log"
|
||||
)
|
||||
|
||||
func Main() {
|
||||
log.InitLogger()
|
||||
cli.VersionPrinter = func(c *cli.Context) {
|
||||
cfg := config.LoadConfig()
|
||||
runningName := cfg.Rancher.Upgrade.Image + ":" + config.Version
|
||||
fmt.Fprintf(c.App.Writer, "version %s from os image %s\n", c.App.Version, runningName)
|
||||
}
|
||||
app := cli.NewApp()
|
||||
|
||||
app.Name = os.Args[0]
|
||||
app.Usage = "Control and configure RancherOS"
|
||||
app.Version = config.VERSION
|
||||
app.Usage = fmt.Sprintf("Control and configure RancherOS\nbuilt: %s", config.BuildDate)
|
||||
app.Version = config.Version
|
||||
app.Author = "Rancher Labs, Inc."
|
||||
app.EnableBashCompletion = true
|
||||
app.Before = func(c *cli.Context) error {
|
||||
@@ -37,14 +45,27 @@ func Main() {
|
||||
HideHelp: true,
|
||||
Subcommands: consoleSubcommands(),
|
||||
},
|
||||
{
|
||||
Name: "console-init",
|
||||
Hidden: true,
|
||||
HideHelp: true,
|
||||
SkipFlagParsing: true,
|
||||
Action: consoleInitAction,
|
||||
},
|
||||
{
|
||||
Name: "dev",
|
||||
ShortName: "d",
|
||||
Usage: "dev spec",
|
||||
Hidden: true,
|
||||
HideHelp: true,
|
||||
SkipFlagParsing: true,
|
||||
Action: devAction,
|
||||
},
|
||||
{
|
||||
Name: "docker-init",
|
||||
Hidden: true,
|
||||
HideHelp: true,
|
||||
SkipFlagParsing: true,
|
||||
Action: dockerInitAction,
|
||||
},
|
||||
{
|
||||
Name: "engine",
|
||||
Usage: "manage which Docker engine is used",
|
||||
@@ -53,31 +74,66 @@ func Main() {
|
||||
},
|
||||
{
|
||||
Name: "entrypoint",
|
||||
Hidden: true,
|
||||
HideHelp: true,
|
||||
SkipFlagParsing: true,
|
||||
Action: entrypointAction,
|
||||
},
|
||||
{
|
||||
Name: "env",
|
||||
ShortName: "e",
|
||||
Usage: "env command",
|
||||
Hidden: true,
|
||||
HideHelp: true,
|
||||
SkipFlagParsing: true,
|
||||
Action: envAction,
|
||||
},
|
||||
serviceCommand(),
|
||||
service.Commands(),
|
||||
{
|
||||
Name: "os",
|
||||
Usage: "operating system upgrade/downgrade",
|
||||
HideHelp: true,
|
||||
Subcommands: osSubcommands(),
|
||||
},
|
||||
{
|
||||
Name: "preload-images",
|
||||
Hidden: true,
|
||||
HideHelp: true,
|
||||
SkipFlagParsing: true,
|
||||
Action: preloadImagesAction,
|
||||
},
|
||||
{
|
||||
Name: "recovery-init",
|
||||
Hidden: true,
|
||||
HideHelp: true,
|
||||
SkipFlagParsing: true,
|
||||
Action: recoveryInitAction,
|
||||
},
|
||||
{
|
||||
Name: "switch-console",
|
||||
Hidden: true,
|
||||
HideHelp: true,
|
||||
SkipFlagParsing: true,
|
||||
Action: switchConsoleAction,
|
||||
},
|
||||
{
|
||||
Name: "tls",
|
||||
Usage: "setup tls configuration",
|
||||
HideHelp: true,
|
||||
Subcommands: tlsConfCommands(),
|
||||
},
|
||||
{
|
||||
Name: "udev-settle",
|
||||
Hidden: true,
|
||||
HideHelp: true,
|
||||
SkipFlagParsing: true,
|
||||
Action: udevSettleAction,
|
||||
},
|
||||
{
|
||||
Name: "user-docker",
|
||||
Hidden: true,
|
||||
HideHelp: true,
|
||||
SkipFlagParsing: true,
|
||||
Action: userDockerAction,
|
||||
},
|
||||
installCommand,
|
||||
selinuxCommand(),
|
||||
}
|
||||
|
||||
@@ -5,12 +5,13 @@ import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"sort"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
log "github.com/Sirupsen/logrus"
|
||||
yaml "github.com/cloudfoundry-incubator/candiedyaml"
|
||||
"github.com/rancher/os/log"
|
||||
|
||||
"github.com/codegangsta/cli"
|
||||
"github.com/rancher/os/config"
|
||||
@@ -76,6 +77,22 @@ func configSubcommands() []cli.Command {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "syslinux",
|
||||
Usage: "edit Syslinux boot global.cfg",
|
||||
Action: editSyslinux,
|
||||
},
|
||||
{
|
||||
Name: "validate",
|
||||
Usage: "validate configuration from stdin",
|
||||
Action: validate,
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "input, i",
|
||||
Usage: "File from which to read",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,9 +102,6 @@ func imagesFromConfig(cfg *config.CloudConfig) []string {
|
||||
for _, service := range cfg.Rancher.BootstrapContainers {
|
||||
imagesMap[service.Image] = 1
|
||||
}
|
||||
for _, service := range cfg.Rancher.Autoformat {
|
||||
imagesMap[service.Image] = 1
|
||||
}
|
||||
for _, service := range cfg.Rancher.Services {
|
||||
imagesMap[service.Image] = 1
|
||||
}
|
||||
@@ -96,7 +110,7 @@ func imagesFromConfig(cfg *config.CloudConfig) []string {
|
||||
i := 0
|
||||
for image := range imagesMap {
|
||||
images[i] = image
|
||||
i += 1
|
||||
i++
|
||||
}
|
||||
sort.Strings(images)
|
||||
return images
|
||||
@@ -138,7 +152,22 @@ func env2map(env []string) map[string]string {
|
||||
return m
|
||||
}
|
||||
|
||||
func editSyslinux(c *cli.Context) error {
|
||||
cmd := exec.Command("system-docker", "run", "--rm", "-it",
|
||||
"-v", "/:/host",
|
||||
"-w", "/host",
|
||||
"--entrypoint=vi",
|
||||
"rancher/os-console:"+config.Version,
|
||||
"boot/global.cfg")
|
||||
cmd.Stdout, cmd.Stderr, cmd.Stdin = os.Stdout, os.Stderr, os.Stdin
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
func configSet(c *cli.Context) error {
|
||||
if c.NArg() < 2 {
|
||||
return nil
|
||||
}
|
||||
|
||||
key := c.Args().Get(0)
|
||||
value := c.Args().Get(1)
|
||||
if key == "" {
|
||||
@@ -186,24 +215,21 @@ func configGet(c *cli.Context) error {
|
||||
}
|
||||
|
||||
func merge(c *cli.Context) error {
|
||||
input := os.Stdin
|
||||
inputFile := c.String("input")
|
||||
if inputFile != "" {
|
||||
var err error
|
||||
input, err = os.Open(inputFile)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer input.Close()
|
||||
}
|
||||
|
||||
bytes, err := ioutil.ReadAll(input)
|
||||
bytes, err := inputBytes(c)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if err = config.Merge(bytes); err != nil {
|
||||
log.Fatal(err)
|
||||
log.Error(err)
|
||||
validationErrors, err := config.ValidateBytes(bytes)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
for _, validationError := range validationErrors.Errors() {
|
||||
log.Error(validationError)
|
||||
}
|
||||
log.Fatal("EXITING: Failed to parse configuration")
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -227,3 +253,32 @@ func export(c *cli.Context) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validate(c *cli.Context) error {
|
||||
bytes, err := inputBytes(c)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
validationErrors, err := config.ValidateBytes(bytes)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
for _, validationError := range validationErrors.Errors() {
|
||||
log.Error(validationError)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func inputBytes(c *cli.Context) ([]byte, error) {
|
||||
input := os.Stdin
|
||||
inputFile := c.String("input")
|
||||
if inputFile != "" {
|
||||
var err error
|
||||
input, err = os.Open(inputFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer input.Close()
|
||||
}
|
||||
return ioutil.ReadAll(input)
|
||||
}
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
package control
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
|
||||
log "github.com/Sirupsen/logrus"
|
||||
"github.com/codegangsta/cli"
|
||||
"github.com/docker/docker/reference"
|
||||
composeConfig "github.com/docker/libcompose/config"
|
||||
"github.com/docker/libcompose/project/options"
|
||||
"github.com/rancher/os/cmd/control/service"
|
||||
"github.com/rancher/os/compose"
|
||||
"github.com/rancher/os/config"
|
||||
"github.com/rancher/os/docker"
|
||||
"github.com/rancher/os/log"
|
||||
"github.com/rancher/os/util"
|
||||
"github.com/rancher/os/util/network"
|
||||
)
|
||||
|
||||
@@ -56,17 +57,17 @@ func consoleSwitch(c *cli.Context) error {
|
||||
newConsole := c.Args()[0]
|
||||
|
||||
cfg := config.LoadConfig()
|
||||
if newConsole == currentConsole() {
|
||||
validateConsole(newConsole, cfg)
|
||||
if newConsole == CurrentConsole() {
|
||||
log.Warnf("Console is already set to %s", newConsole)
|
||||
}
|
||||
|
||||
if !c.Bool("force") {
|
||||
in := bufio.NewReader(os.Stdin)
|
||||
fmt.Println(`Switching consoles will
|
||||
1. destroy the current console container
|
||||
2. log you out
|
||||
3. restart Docker`)
|
||||
if !yes(in, "Continue") {
|
||||
if !yes("Continue") {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -82,11 +83,11 @@ func consoleSwitch(c *cli.Context) error {
|
||||
Privileged: true,
|
||||
Net: "host",
|
||||
Pid: "host",
|
||||
Image: config.OS_BASE,
|
||||
Image: config.OsBase,
|
||||
Labels: map[string]string{
|
||||
config.SCOPE: config.SYSTEM,
|
||||
config.ScopeLabel: config.System,
|
||||
},
|
||||
Command: []string{"/usr/bin/switch-console", newConsole},
|
||||
Command: []string{"/usr/bin/ros", "switch-console", newConsole},
|
||||
VolumesFrom: []string{"all-volumes"},
|
||||
})
|
||||
if err != nil {
|
||||
@@ -109,6 +110,7 @@ func consoleEnable(c *cli.Context) error {
|
||||
newConsole := c.Args()[0]
|
||||
|
||||
cfg := config.LoadConfig()
|
||||
validateConsole(newConsole, cfg)
|
||||
|
||||
if newConsole != "default" {
|
||||
if err := compose.StageServices(cfg, newConsole); err != nil {
|
||||
@@ -125,18 +127,11 @@ func consoleEnable(c *cli.Context) error {
|
||||
|
||||
func consoleList(c *cli.Context) error {
|
||||
cfg := config.LoadConfig()
|
||||
|
||||
consoles, err := network.GetConsoles(cfg.Rancher.Repositories.ToArray())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
consoles = append(consoles, "default")
|
||||
sort.Strings(consoles)
|
||||
|
||||
currentConsole := currentConsole()
|
||||
consoles := availableConsoles(cfg)
|
||||
CurrentConsole := CurrentConsole()
|
||||
|
||||
for _, console := range consoles {
|
||||
if console == currentConsole {
|
||||
if console == CurrentConsole {
|
||||
fmt.Printf("current %s\n", console)
|
||||
} else if console == cfg.Rancher.Console {
|
||||
fmt.Printf("enabled %s\n", console)
|
||||
@@ -148,12 +143,49 @@ func consoleList(c *cli.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func currentConsole() (console string) {
|
||||
consoleBytes, err := ioutil.ReadFile("/run/console-done")
|
||||
if err == nil {
|
||||
console = strings.TrimSpace(string(consoleBytes))
|
||||
} else {
|
||||
log.Warnf("Failed to detect current console: %v", err)
|
||||
func validateConsole(console string, cfg *config.CloudConfig) {
|
||||
consoles := availableConsoles(cfg)
|
||||
if !service.IsLocalOrURL(console) && !util.Contains(consoles, console) {
|
||||
log.Fatalf("%s is not a valid console", console)
|
||||
}
|
||||
}
|
||||
|
||||
func availableConsoles(cfg *config.CloudConfig) []string {
|
||||
consoles, err := network.GetConsoles(cfg.Rancher.Repositories.ToArray())
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
consoles = append(consoles, "default")
|
||||
sort.Strings(consoles)
|
||||
return consoles
|
||||
}
|
||||
|
||||
// CurrentConsole gets the name of the console that's running
|
||||
func CurrentConsole() (console string) {
|
||||
// TODO: replace this docker container look up with a libcompose service lookup?
|
||||
|
||||
// sudo system-docker inspect --format "{{.Config.Image}}" console
|
||||
client, err := docker.NewSystemClient()
|
||||
if err != nil {
|
||||
log.Warnf("Failed to detect current console: %v", err)
|
||||
return
|
||||
}
|
||||
info, err := client.ContainerInspect(context.Background(), "console")
|
||||
if err != nil {
|
||||
log.Warnf("Failed to detect current console: %v", err)
|
||||
return
|
||||
}
|
||||
// parse image name, then remove os- prefix and the console suffix
|
||||
image, err := reference.ParseNamed(info.Config.Image)
|
||||
if err != nil {
|
||||
log.Warnf("Failed to detect current console(%s): %v", info.Config.Image, err)
|
||||
return
|
||||
}
|
||||
|
||||
if strings.Contains(image.Name(), "os-console") {
|
||||
console = "default"
|
||||
return
|
||||
}
|
||||
console = strings.TrimPrefix(strings.TrimSuffix(image.Name(), "console"), "rancher/os-")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package console
|
||||
package control
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -11,9 +11,11 @@ import (
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
log "github.com/Sirupsen/logrus"
|
||||
"github.com/codegangsta/cli"
|
||||
"github.com/rancher/os/cmd/cloudinitexecute"
|
||||
"github.com/rancher/os/config"
|
||||
"github.com/rancher/os/config/cmdline"
|
||||
"github.com/rancher/os/log"
|
||||
"github.com/rancher/os/util"
|
||||
)
|
||||
|
||||
@@ -29,63 +31,76 @@ type symlink struct {
|
||||
oldname, newname string
|
||||
}
|
||||
|
||||
func Main() {
|
||||
func ConsoleInitMain() {
|
||||
if err := consoleInitFunc(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func consoleInitAction(c *cli.Context) error {
|
||||
return consoleInitFunc()
|
||||
}
|
||||
|
||||
func createHomeDir(homedir string, uid, gid int) {
|
||||
if _, err := os.Stat(homedir); os.IsNotExist(err) {
|
||||
if err := os.MkdirAll(homedir, 0755); err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
if err := os.Chown(homedir, uid, gid); err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func consoleInitFunc() error {
|
||||
cfg := config.LoadConfig()
|
||||
|
||||
if _, err := os.Stat(rancherHome); os.IsNotExist(err) {
|
||||
if err := os.MkdirAll(rancherHome, 0755); err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
if err := os.Chown(rancherHome, 1100, 1100); err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := os.Stat(dockerHome); os.IsNotExist(err) {
|
||||
if err := os.MkdirAll(dockerHome, 0755); err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
if err := os.Chown(dockerHome, 1101, 1101); err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
password := config.GetCmdline("rancher.password")
|
||||
cmd := exec.Command("chpasswd")
|
||||
cmd.Stdin = strings.NewReader(fmt.Sprint("rancher:", password))
|
||||
// Now that we're booted, stop writing debug messages to the console
|
||||
cmd := exec.Command("sudo", "dmesg", "--console-off")
|
||||
if err := cmd.Run(); err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
|
||||
cmd = exec.Command("bash", "-c", `sed -E -i 's/(rancher:.*:).*(:.*:.*:.*:.*:.*:.*)$/\1\2/' /etc/shadow`)
|
||||
if err := cmd.Run(); err != nil {
|
||||
log.Error(err)
|
||||
createHomeDir(rancherHome, 1100, 1100)
|
||||
createHomeDir(dockerHome, 1101, 1101)
|
||||
|
||||
password := cmdline.GetCmdline("rancher.password")
|
||||
if password != "" {
|
||||
cmd := exec.Command("chpasswd")
|
||||
cmd.Stdin = strings.NewReader(fmt.Sprint("rancher:", password))
|
||||
if err := cmd.Run(); err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
|
||||
cmd = exec.Command("bash", "-c", `sed -E -i 's/(rancher:.*:).*(:.*:.*:.*:.*:.*:.*)$/\1\2/' /etc/shadow`)
|
||||
if err := cmd.Run(); err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := setupSSH(cfg); err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
|
||||
if err := writeRespawn(); err != nil {
|
||||
if err := writeRespawn("rancher", cfg.Rancher.SSH.Daemon, false); err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
|
||||
if err := modifySshdConfig(); err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
|
||||
if err := writeOsRelease(); err != nil {
|
||||
if err := modifySshdConfig(cfg); err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
|
||||
for _, link := range []symlink{
|
||||
{"/var/lib/rancher/engine/docker", "/usr/bin/docker"},
|
||||
{"/var/lib/rancher/engine/docker-init", "/usr/bin/docker-init"},
|
||||
{"/var/lib/rancher/engine/docker-containerd", "/usr/bin/docker-containerd"},
|
||||
{"/var/lib/rancher/engine/docker-containerd-ctr", "/usr/bin/docker-containerd-ctr"},
|
||||
{"/var/lib/rancher/engine/docker-containerd-shim", "/usr/bin/docker-containerd-shim"},
|
||||
{"/var/lib/rancher/engine/dockerd", "/usr/bin/dockerd"},
|
||||
{"/var/lib/rancher/engine/docker-proxy", "/usr/bin/docker-proxy"},
|
||||
{"/var/lib/rancher/engine/docker-runc", "/usr/bin/docker-runc"},
|
||||
{"/usr/share/ros/os-release", "/usr/lib/os-release"},
|
||||
{"/usr/share/ros/os-release", "/etc/os-release"},
|
||||
} {
|
||||
syscall.Unlink(link.newname)
|
||||
if err := os.Symlink(link.oldname, link.newname); err != nil {
|
||||
@@ -93,11 +108,45 @@ func Main() {
|
||||
}
|
||||
}
|
||||
|
||||
cmd = exec.Command("bash", "-c", `echo 'RancherOS \n \l' > /etc/issue`)
|
||||
if err := cmd.Run(); err != nil {
|
||||
// font backslashes need to be escaped for when issue is output! (but not the others..)
|
||||
if err := ioutil.WriteFile("/etc/issue", []byte(config.Banner), 0644); err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
|
||||
// write out a profile.d file for the proxy settings.
|
||||
// maybe write these on the host and bindmount into everywhere?
|
||||
proxyLines := []string{}
|
||||
for _, k := range []string{"http_proxy", "HTTP_PROXY", "https_proxy", "HTTPS_PROXY", "no_proxy", "NO_PROXY"} {
|
||||
if v, ok := cfg.Rancher.Environment[k]; ok {
|
||||
proxyLines = append(proxyLines, fmt.Sprintf("export %s=%s", k, v))
|
||||
}
|
||||
}
|
||||
|
||||
if len(proxyLines) > 0 {
|
||||
proxyString := strings.Join(proxyLines, "\n")
|
||||
proxyString = fmt.Sprintf("#!/bin/sh\n%s\n", proxyString)
|
||||
if err := ioutil.WriteFile("/etc/profile.d/proxy.sh", []byte(proxyString), 0755); err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
// write out a profile.d file for the PATH settings.
|
||||
pathLines := []string{}
|
||||
for _, k := range []string{"PATH", "path"} {
|
||||
if v, ok := cfg.Rancher.Environment[k]; ok {
|
||||
for _, p := range strings.Split(v, ",") {
|
||||
pathLines = append(pathLines, fmt.Sprintf("export PATH=$PATH:%s", strings.TrimSpace(p)))
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(pathLines) > 0 {
|
||||
pathString := strings.Join(pathLines, "\n")
|
||||
pathString = fmt.Sprintf("#!/bin/sh\n%s\n", pathString)
|
||||
if err := ioutil.WriteFile("/etc/profile.d/path.sh", []byte(pathString), 0755); err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
cmd = exec.Command("bash", "-c", `echo $(/sbin/ifconfig | grep -B1 "inet addr" |awk '{ if ( $1 == "inet" ) { print $2 } else if ( $2 == "Link" ) { printf "%s:" ,$1 } }' |awk -F: '{ print $1 ": " $3}') >> /etc/issue`)
|
||||
if err := cmd.Run(); err != nil {
|
||||
log.Error(err)
|
||||
@@ -105,13 +154,18 @@ func Main() {
|
||||
|
||||
cloudinitexecute.ApplyConsole(cfg)
|
||||
|
||||
if err := runScript(config.CloudConfigScriptFile); err != nil {
|
||||
if err := util.RunScript(config.CloudConfigScriptFile); err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
if err := runScript(startScript); err != nil {
|
||||
if err := util.RunScript(startScript); err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
if err := runScript("/etc/rc.local"); err != nil {
|
||||
|
||||
if err := ioutil.WriteFile(consoleDone, []byte(CurrentConsole()), 0644); err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
|
||||
if err := util.RunScript("/etc/rc.local"); err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
|
||||
@@ -119,27 +173,28 @@ func Main() {
|
||||
|
||||
respawnBinPath, err := exec.LookPath("respawn")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
return err
|
||||
}
|
||||
|
||||
if err := ioutil.WriteFile(consoleDone, []byte(cfg.Rancher.Console), 0644); err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
|
||||
log.Fatal(syscall.Exec(respawnBinPath, []string{"respawn", "-f", "/etc/respawn.conf"}, os.Environ()))
|
||||
return syscall.Exec(respawnBinPath, []string{"respawn", "-f", "/etc/respawn.conf"}, os.Environ())
|
||||
}
|
||||
|
||||
func generateRespawnConf(cmdline string) string {
|
||||
func generateRespawnConf(cmdline, user string, sshd, recovery bool) string {
|
||||
var respawnConf bytes.Buffer
|
||||
|
||||
autologinBin := "/usr/bin/autologin"
|
||||
if recovery {
|
||||
autologinBin = "/usr/bin/recovery"
|
||||
}
|
||||
|
||||
for i := 1; i < 7; i++ {
|
||||
tty := fmt.Sprintf("tty%d", i)
|
||||
|
||||
respawnConf.WriteString(gettyCmd)
|
||||
if strings.Contains(cmdline, fmt.Sprintf("rancher.autologin=%s", tty)) {
|
||||
respawnConf.WriteString(" --autologin rancher")
|
||||
respawnConf.WriteString(fmt.Sprintf(" -n -l %s -o %s:tty%d", autologinBin, user, i))
|
||||
}
|
||||
respawnConf.WriteString(fmt.Sprintf(" 115200 %s\n", tty))
|
||||
respawnConf.WriteString(fmt.Sprintf(" --noclear %s linux\n", tty))
|
||||
}
|
||||
|
||||
for _, tty := range []string{"ttyS0", "ttyS1", "ttyS2", "ttyS3", "ttyAMA0"} {
|
||||
@@ -149,23 +204,25 @@ func generateRespawnConf(cmdline string) string {
|
||||
|
||||
respawnConf.WriteString(gettyCmd)
|
||||
if strings.Contains(cmdline, fmt.Sprintf("rancher.autologin=%s", tty)) {
|
||||
respawnConf.WriteString(" --autologin rancher")
|
||||
respawnConf.WriteString(fmt.Sprintf(" -n -l %s -o %s:%s", autologinBin, user, tty))
|
||||
}
|
||||
respawnConf.WriteString(fmt.Sprintf(" 115200 %s\n", tty))
|
||||
respawnConf.WriteString(fmt.Sprintf(" %s\n", tty))
|
||||
}
|
||||
|
||||
respawnConf.WriteString("/usr/sbin/sshd -D")
|
||||
if sshd {
|
||||
respawnConf.WriteString("/usr/sbin/sshd -D")
|
||||
}
|
||||
|
||||
return respawnConf.String()
|
||||
}
|
||||
|
||||
func writeRespawn() error {
|
||||
func writeRespawn(user string, sshd, recovery bool) error {
|
||||
cmdline, err := ioutil.ReadFile("/proc/cmdline")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
respawn := generateRespawnConf(string(cmdline))
|
||||
respawn := generateRespawnConf(string(cmdline), user, sshd, recovery)
|
||||
|
||||
files, err := ioutil.ReadDir("/etc/respawn.conf.d")
|
||||
if err == nil {
|
||||
@@ -185,19 +242,28 @@ func writeRespawn() error {
|
||||
return ioutil.WriteFile("/etc/respawn.conf", []byte(respawn), 0644)
|
||||
}
|
||||
|
||||
func modifySshdConfig() error {
|
||||
func modifySshdConfig(cfg *config.CloudConfig) error {
|
||||
sshdConfig, err := ioutil.ReadFile("/etc/ssh/sshd_config")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sshdConfigString := string(sshdConfig)
|
||||
|
||||
for _, item := range []string{
|
||||
modifiedLines := []string{
|
||||
"UseDNS no",
|
||||
"PermitRootLogin no",
|
||||
"ServerKeyBits 2048",
|
||||
"AllowGroups docker",
|
||||
} {
|
||||
}
|
||||
|
||||
if cfg.Rancher.SSH.Port > 0 && cfg.Rancher.SSH.Port < 65355 {
|
||||
modifiedLines = append(modifiedLines, fmt.Sprintf("Port %d", cfg.Rancher.SSH.Port))
|
||||
}
|
||||
if cfg.Rancher.SSH.ListenAddress != "" {
|
||||
modifiedLines = append(modifiedLines, fmt.Sprintf("ListenAddress %s", cfg.Rancher.SSH.ListenAddress))
|
||||
}
|
||||
|
||||
for _, item := range modifiedLines {
|
||||
match, err := regexp.Match("^"+item, sshdConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -210,33 +276,6 @@ func modifySshdConfig() error {
|
||||
return ioutil.WriteFile("/etc/ssh/sshd_config", []byte(sshdConfigString), 0644)
|
||||
}
|
||||
|
||||
func writeOsRelease() error {
|
||||
idLike := "busybox"
|
||||
if osRelease, err := ioutil.ReadFile("/etc/os-release"); err == nil {
|
||||
for _, line := range strings.Split(string(osRelease), "\n") {
|
||||
if strings.HasPrefix(line, "ID_LIKE") {
|
||||
split := strings.Split(line, "ID_LIKE")
|
||||
if len(split) > 1 {
|
||||
idLike = split[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ioutil.WriteFile("/etc/os-release", []byte(fmt.Sprintf(`
|
||||
NAME="RancherOS"
|
||||
VERSION=%s
|
||||
ID=rancheros
|
||||
ID_LIKE=%s
|
||||
VERSION_ID=%s
|
||||
PRETTY_NAME="RancherOS %s"
|
||||
HOME_URL=
|
||||
SUPPORT_URL=
|
||||
BUG_REPORT_URL=
|
||||
BUILD_ID=
|
||||
`, config.VERSION, idLike, config.VERSION, config.VERSION)), 0644)
|
||||
}
|
||||
|
||||
func setupSSH(cfg *config.CloudConfig) error {
|
||||
for _, keyType := range []string{"rsa", "dsa", "ecdsa", "ed25519"} {
|
||||
outputFile := fmt.Sprintf("/etc/ssh/ssh_host_%s_key", keyType)
|
||||
@@ -246,8 +285,8 @@ func setupSSH(cfg *config.CloudConfig) error {
|
||||
continue
|
||||
}
|
||||
|
||||
saved, savedExists := cfg.Rancher.Ssh.Keys[keyType]
|
||||
pub, pubExists := cfg.Rancher.Ssh.Keys[keyType+"-pub"]
|
||||
saved, savedExists := cfg.Rancher.SSH.Keys[keyType]
|
||||
pub, pubExists := cfg.Rancher.SSH.Keys[keyType+"-pub"]
|
||||
|
||||
if savedExists && pubExists {
|
||||
// TODO check permissions
|
||||
@@ -281,29 +320,3 @@ func setupSSH(cfg *config.CloudConfig) error {
|
||||
|
||||
return os.MkdirAll("/var/run/sshd", 0644)
|
||||
}
|
||||
|
||||
func runScript(path string) error {
|
||||
if !util.ExistsAndExecutable(path) {
|
||||
return nil
|
||||
}
|
||||
|
||||
script, err := os.Open(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
magic := make([]byte, 2)
|
||||
if _, err = script.Read(magic); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd := exec.Command("/bin/sh", path)
|
||||
if string(magic) == "#!" {
|
||||
cmd = exec.Command(path)
|
||||
}
|
||||
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
return cmd.Run()
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package dockerinit
|
||||
package control
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -9,19 +9,19 @@ import (
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
log "github.com/Sirupsen/logrus"
|
||||
"github.com/rancher/os/config"
|
||||
"github.com/codegangsta/cli"
|
||||
"github.com/rancher/os/log"
|
||||
"github.com/rancher/os/util"
|
||||
)
|
||||
|
||||
const (
|
||||
consoleDone = "/run/console-done"
|
||||
dockerConf = "/var/lib/rancher/conf/docker"
|
||||
dockerDone = "/run/docker-done"
|
||||
dockerLog = "/var/log/docker.log"
|
||||
dockerConf = "/var/lib/rancher/conf/docker"
|
||||
dockerDone = "/run/docker-done"
|
||||
dockerLog = "/var/log/docker.log"
|
||||
)
|
||||
|
||||
func Main() {
|
||||
func dockerInitAction(c *cli.Context) error {
|
||||
// TODO: this should be replaced by a "Console ready event watcher"
|
||||
for {
|
||||
if _, err := os.Stat(consoleDone); err == nil {
|
||||
break
|
||||
@@ -29,21 +29,33 @@ func Main() {
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
}
|
||||
|
||||
dockerBin := "/usr/bin/docker"
|
||||
for _, binPath := range []string{
|
||||
dockerBin := ""
|
||||
dockerPaths := []string{
|
||||
"/usr/bin",
|
||||
"/opt/bin",
|
||||
"/usr/local/bin",
|
||||
"/var/lib/rancher/docker",
|
||||
} {
|
||||
}
|
||||
for _, binPath := range dockerPaths {
|
||||
if util.ExistsAndExecutable(path.Join(binPath, "dockerd")) {
|
||||
dockerBin = path.Join(binPath, "dockerd")
|
||||
break
|
||||
}
|
||||
if util.ExistsAndExecutable(path.Join(binPath, "docker")) {
|
||||
dockerBin = path.Join(binPath, "docker")
|
||||
break
|
||||
}
|
||||
if dockerBin == "" {
|
||||
for _, binPath := range dockerPaths {
|
||||
if util.ExistsAndExecutable(path.Join(binPath, "docker")) {
|
||||
dockerBin = path.Join(binPath, "docker")
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if dockerBin == "" {
|
||||
err := fmt.Errorf("Failed to find either dockerd or docker binaries")
|
||||
log.Error(err)
|
||||
return err
|
||||
}
|
||||
log.Infof("Found %s", dockerBin)
|
||||
|
||||
if err := syscall.Mount("", "/", "", syscall.MS_SHARED|syscall.MS_REC, ""); err != nil {
|
||||
log.Error(err)
|
||||
@@ -54,7 +66,7 @@ func Main() {
|
||||
|
||||
mountInfo, err := ioutil.ReadFile("/proc/self/mountinfo")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
return err
|
||||
}
|
||||
|
||||
for _, mount := range strings.Split(string(mountInfo), "\n") {
|
||||
@@ -66,14 +78,13 @@ func Main() {
|
||||
args := []string{
|
||||
"bash",
|
||||
"-c",
|
||||
fmt.Sprintf(`[ -e %s ] && source %s; exec /usr/bin/dockerlaunch %s %s $DOCKER_OPTS >> %s 2>&1`, dockerConf, dockerConf, dockerBin, strings.Join(os.Args[1:], " "), dockerLog),
|
||||
fmt.Sprintf(`[ -e %s ] && source %s; exec /usr/bin/dockerlaunch %s %s $DOCKER_OPTS >> %s 2>&1`, dockerConf, dockerConf, dockerBin, strings.Join(c.Args(), " "), dockerLog),
|
||||
}
|
||||
|
||||
cfg := config.LoadConfig()
|
||||
|
||||
if err := ioutil.WriteFile(dockerDone, []byte(cfg.Rancher.Docker.Engine), 0644); err != nil {
|
||||
// TODO: this should be replaced by a "Docker ready event watcher"
|
||||
if err := ioutil.WriteFile(dockerDone, []byte(CurrentEngine()), 0644); err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
|
||||
log.Fatal(syscall.Exec("/bin/bash", args, os.Environ()))
|
||||
return syscall.Exec("/bin/bash", args, os.Environ())
|
||||
}
|
||||
@@ -2,24 +2,23 @@ package control
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
|
||||
log "github.com/Sirupsen/logrus"
|
||||
"github.com/codegangsta/cli"
|
||||
"github.com/docker/docker/reference"
|
||||
"github.com/docker/libcompose/project/options"
|
||||
"github.com/rancher/os/cmd/control/service"
|
||||
"github.com/rancher/os/compose"
|
||||
"github.com/rancher/os/config"
|
||||
"github.com/rancher/os/docker"
|
||||
"github.com/rancher/os/log"
|
||||
"github.com/rancher/os/util"
|
||||
"github.com/rancher/os/util/network"
|
||||
)
|
||||
|
||||
const (
|
||||
dockerDone = "/run/docker-done"
|
||||
)
|
||||
|
||||
func engineSubcommands() []cli.Command {
|
||||
return []cli.Command{
|
||||
{
|
||||
@@ -57,6 +56,7 @@ func engineSwitch(c *cli.Context) error {
|
||||
newEngine := c.Args()[0]
|
||||
|
||||
cfg := config.LoadConfig()
|
||||
validateEngine(newEngine, cfg)
|
||||
|
||||
project, err := compose.GetProject(cfg, true, false)
|
||||
if err != nil {
|
||||
@@ -89,6 +89,7 @@ func engineEnable(c *cli.Context) error {
|
||||
newEngine := c.Args()[0]
|
||||
|
||||
cfg := config.LoadConfig()
|
||||
validateEngine(newEngine, cfg)
|
||||
|
||||
if err := compose.StageServices(cfg, newEngine); err != nil {
|
||||
return err
|
||||
@@ -103,14 +104,8 @@ func engineEnable(c *cli.Context) error {
|
||||
|
||||
func engineList(c *cli.Context) error {
|
||||
cfg := config.LoadConfig()
|
||||
|
||||
engines, err := network.GetEngines(cfg.Rancher.Repositories.ToArray())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sort.Strings(engines)
|
||||
|
||||
currentEngine := currentEngine()
|
||||
engines := availableEngines(cfg)
|
||||
currentEngine := CurrentEngine()
|
||||
|
||||
for _, engine := range engines {
|
||||
if engine == currentEngine {
|
||||
@@ -125,12 +120,49 @@ func engineList(c *cli.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func currentEngine() (engine string) {
|
||||
engineBytes, err := ioutil.ReadFile(dockerDone)
|
||||
if err == nil {
|
||||
engine = strings.TrimSpace(string(engineBytes))
|
||||
} else {
|
||||
log.Warnf("Failed to detect current Docker engine: %v", err)
|
||||
func validateEngine(engine string, cfg *config.CloudConfig) {
|
||||
engines := availableEngines(cfg)
|
||||
if !service.IsLocalOrURL(engine) && !util.Contains(engines, engine) {
|
||||
log.Fatalf("%s is not a valid engine", engine)
|
||||
}
|
||||
}
|
||||
|
||||
func availableEngines(cfg *config.CloudConfig) []string {
|
||||
engines, err := network.GetEngines(cfg.Rancher.Repositories.ToArray())
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
sort.Strings(engines)
|
||||
return engines
|
||||
}
|
||||
|
||||
// CurrentEngine gets the name of the docker that's running
|
||||
func CurrentEngine() (engine string) {
|
||||
// sudo system-docker inspect --format "{{.Config.Image}}" docker
|
||||
client, err := docker.NewSystemClient()
|
||||
if err != nil {
|
||||
log.Warnf("Failed to detect current docker: %v", err)
|
||||
return
|
||||
}
|
||||
info, err := client.ContainerInspect(context.Background(), "docker")
|
||||
if err != nil {
|
||||
log.Warnf("Failed to detect current docker: %v", err)
|
||||
return
|
||||
}
|
||||
// parse image name, then remove os- prefix and the engine suffix
|
||||
image, err := reference.ParseNamed(info.Config.Image)
|
||||
if err != nil {
|
||||
log.Warnf("Failed to detect current docker(%s): %v", info.Config.Image, err)
|
||||
return
|
||||
}
|
||||
if t, ok := image.(reference.NamedTagged); ok {
|
||||
tag := t.Tag()
|
||||
if !strings.HasPrefix(tag, "1.") {
|
||||
// TODO: this assumes we only do Docker ce :/
|
||||
tag = tag + "-ce"
|
||||
}
|
||||
return "docker-" + tag
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@ import (
|
||||
"os/exec"
|
||||
"syscall"
|
||||
|
||||
log "github.com/Sirupsen/logrus"
|
||||
"github.com/codegangsta/cli"
|
||||
"github.com/rancher/os/log"
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"github.com/rancher/os/cmd/cloudinitexecute"
|
||||
@@ -45,6 +45,8 @@ func entrypointAction(c *cli.Context) error {
|
||||
writeFiles(cfg)
|
||||
}
|
||||
|
||||
setupCommandSymlinks()
|
||||
|
||||
if len(os.Args) < 3 {
|
||||
return nil
|
||||
}
|
||||
@@ -58,7 +60,7 @@ func entrypointAction(c *cli.Context) error {
|
||||
}
|
||||
|
||||
func writeFiles(cfg *config.CloudConfig) error {
|
||||
id, err := util.GetCurrentContainerId()
|
||||
id, err := util.GetCurrentContainerID()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -74,3 +76,29 @@ func writeFiles(cfg *config.CloudConfig) error {
|
||||
cloudinitexecute.WriteFiles(cfg, info.Name[1:])
|
||||
return nil
|
||||
}
|
||||
|
||||
func setupCommandSymlinks() {
|
||||
for _, link := range []symlink{
|
||||
{config.RosBin, "/usr/bin/autologin"},
|
||||
{config.RosBin, "/usr/bin/recovery"},
|
||||
{config.RosBin, "/usr/bin/cloud-init-execute"},
|
||||
{config.RosBin, "/usr/bin/cloud-init-save"},
|
||||
{config.RosBin, "/usr/bin/dockerlaunch"},
|
||||
{config.RosBin, "/usr/bin/respawn"},
|
||||
{config.RosBin, "/usr/sbin/netconf"},
|
||||
{config.RosBin, "/usr/sbin/wait-for-docker"},
|
||||
{config.RosBin, "/usr/sbin/poweroff"},
|
||||
{config.RosBin, "/usr/sbin/reboot"},
|
||||
{config.RosBin, "/usr/sbin/halt"},
|
||||
{config.RosBin, "/usr/sbin/shutdown"},
|
||||
{config.RosBin, "/sbin/poweroff"},
|
||||
{config.RosBin, "/sbin/reboot"},
|
||||
{config.RosBin, "/sbin/halt"},
|
||||
{config.RosBin, "/sbin/shutdown"},
|
||||
} {
|
||||
os.Remove(link.newname)
|
||||
if err := os.Symlink(link.oldname, link.newname); err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
102
cmd/control/install/grub.go
Normal file
102
cmd/control/install/grub.go
Normal file
@@ -0,0 +1,102 @@
|
||||
package install
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/rancher/os/log"
|
||||
)
|
||||
|
||||
func RunGrub(baseName, device string) error {
|
||||
log.Debugf("installGrub")
|
||||
|
||||
//grub-install --boot-directory=${baseName}/boot ${device}
|
||||
cmd := exec.Command("grub-install", "--boot-directory="+baseName+"/boot", device)
|
||||
if err := cmd.Run(); err != nil {
|
||||
log.Errorf("%s", err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func grubConfig(menu BootVars) error {
|
||||
log.Debugf("grubConfig")
|
||||
|
||||
filetmpl, err := template.New("grub2config").Parse(`{{define "grub2menu"}}menuentry "{{.Name}}" {
|
||||
set root=(hd0,msdos1)
|
||||
linux /{{.bootDir}}vmlinuz-{{.Version}}-rancheros {{.KernelArgs}} {{.Append}}
|
||||
initrd /{{.bootDir}}initrd-{{.Version}}-rancheros
|
||||
}
|
||||
|
||||
{{end}}
|
||||
set default="0"
|
||||
set timeout="{{.Timeout}}"
|
||||
{{if .Fallback}}set fallback={{.Fallback}}{{end}}
|
||||
|
||||
{{- range .Entries}}
|
||||
{{template "grub2menu" .}}
|
||||
{{- end}}
|
||||
|
||||
`)
|
||||
if err != nil {
|
||||
log.Errorf("grub2config %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
cfgFile := filepath.Join(menu.BaseName, menu.BootDir+"grub/grub.cfg")
|
||||
log.Debugf("grubConfig written to %s", cfgFile)
|
||||
|
||||
f, err := os.Create(cfgFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = filetmpl.Execute(f, menu)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func PvGrubConfig(menu BootVars) error {
|
||||
log.Debugf("pvGrubConfig")
|
||||
|
||||
filetmpl, err := template.New("grublst").Parse(`{{define "grubmenu"}}
|
||||
title RancherOS {{.Version}}-({{.Name}})
|
||||
root (hd0)
|
||||
kernel /${bootDir}vmlinuz-{{.Version}}-rancheros {{.KernelArgs}} {{.Append}}
|
||||
initrd /${bootDir}initrd-{{.Version}}-rancheros
|
||||
|
||||
{{end}}
|
||||
default 0
|
||||
timeout {{.Timeout}}
|
||||
{{if .Fallback}}fallback {{.Fallback}}{{end}}
|
||||
hiddenmenu
|
||||
|
||||
{{- range .Entries}}
|
||||
{{template "grubmenu" .}}
|
||||
{{- end}}
|
||||
|
||||
`)
|
||||
if err != nil {
|
||||
log.Errorf("pv grublst: %s", err)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
cfgFile := filepath.Join(menu.BaseName, menu.BootDir+"grub/menu.lst")
|
||||
log.Debugf("grubMenu written to %s", cfgFile)
|
||||
f, err := os.Create(cfgFile)
|
||||
if err != nil {
|
||||
log.Errorf("Create(%s) %s", cfgFile, err)
|
||||
|
||||
return err
|
||||
}
|
||||
err = filetmpl.Execute(f, menu)
|
||||
if err != nil {
|
||||
log.Errorf("execute %s", err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
84
cmd/control/install/install.go
Normal file
84
cmd/control/install/install.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package install
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/rancher/os/config"
|
||||
"github.com/rancher/os/log"
|
||||
"github.com/rancher/os/util"
|
||||
)
|
||||
|
||||
const BootDir = "boot/"
|
||||
|
||||
type MenuEntry struct {
|
||||
Name, BootDir, Version, KernelArgs, Append string
|
||||
}
|
||||
type BootVars struct {
|
||||
BaseName, BootDir string
|
||||
Timeout uint
|
||||
Fallback int
|
||||
Entries []MenuEntry
|
||||
}
|
||||
|
||||
func MountDevice(baseName, device, partition string, raw bool) (string, string, error) {
|
||||
log.Debugf("mountdevice %s, raw %v", partition, raw)
|
||||
|
||||
if partition == "" {
|
||||
if raw {
|
||||
log.Debugf("util.Mount (raw) %s, %s", partition, baseName)
|
||||
|
||||
cmd := exec.Command("lsblk", "-no", "pkname", partition)
|
||||
log.Debugf("Run(%v)", cmd)
|
||||
cmd.Stderr = os.Stderr
|
||||
device := ""
|
||||
// TODO: out can == "" - this is used to "detect software RAID" which is terrible
|
||||
if out, err := cmd.Output(); err == nil {
|
||||
device = "/dev/" + strings.TrimSpace(string(out))
|
||||
}
|
||||
|
||||
log.Debugf("mountdevice return -> d: %s, p: %s", device, partition)
|
||||
return device, partition, util.Mount(partition, baseName, "", "")
|
||||
}
|
||||
|
||||
//rootfs := partition
|
||||
// Don't use ResolveDevice - it can fail, whereas `blkid -L LABEL` works more often
|
||||
|
||||
cfg := config.LoadConfig()
|
||||
d, _, err := util.Blkid("RANCHER_BOOT")
|
||||
if err != nil {
|
||||
log.Errorf("Failed to run blkid: %s", err)
|
||||
}
|
||||
if d != "" {
|
||||
partition = d
|
||||
baseName = filepath.Join(baseName, BootDir)
|
||||
} else {
|
||||
if dev := util.ResolveDevice(cfg.Rancher.State.Dev); dev != "" {
|
||||
// try the rancher.state.dev setting
|
||||
partition = dev
|
||||
} else {
|
||||
d, _, err := util.Blkid("RANCHER_STATE")
|
||||
if err != nil {
|
||||
log.Errorf("Failed to run blkid: %s", err)
|
||||
}
|
||||
if d != "" {
|
||||
partition = d
|
||||
}
|
||||
}
|
||||
}
|
||||
cmd := exec.Command("lsblk", "-no", "pkname", partition)
|
||||
log.Debugf("Run(%v)", cmd)
|
||||
cmd.Stderr = os.Stderr
|
||||
// TODO: out can == "" - this is used to "detect software RAID" which is terrible
|
||||
if out, err := cmd.Output(); err == nil {
|
||||
device = "/dev/" + strings.TrimSpace(string(out))
|
||||
}
|
||||
}
|
||||
os.MkdirAll(baseName, 0755)
|
||||
cmd := exec.Command("mount", partition, baseName)
|
||||
//cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr
|
||||
log.Debugf("mountdevice return2 -> d: %s, p: %s", device, partition)
|
||||
return device, partition, cmd.Run()
|
||||
}
|
||||
94
cmd/control/install/syslinux.go
Normal file
94
cmd/control/install/syslinux.go
Normal file
@@ -0,0 +1,94 @@
|
||||
package install
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"html/template"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/rancher/os/log"
|
||||
)
|
||||
|
||||
func syslinuxConfig(menu BootVars) error {
|
||||
log.Debugf("syslinuxConfig")
|
||||
|
||||
filetmpl, err := template.New("syslinuxconfig").Parse(`{{define "syslinuxmenu"}}
|
||||
LABEL {{.Name}}
|
||||
LINUX ../vmlinuz-{{.Version}}-rancheros
|
||||
APPEND {{.KernelArgs}} {{.Append}}
|
||||
INITRD ../initrd-{{.Version}}-rancheros
|
||||
{{end}}
|
||||
TIMEOUT 20 #2 seconds
|
||||
DEFAULT RancherOS-current
|
||||
|
||||
{{- range .Entries}}
|
||||
{{template "syslinuxmenu" .}}
|
||||
{{- end}}
|
||||
|
||||
`)
|
||||
if err != nil {
|
||||
log.Errorf("syslinuxconfig %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
cfgFile := filepath.Join(menu.BaseName, menu.BootDir+"syslinux/syslinux.cfg")
|
||||
log.Debugf("syslinuxConfig written to %s", cfgFile)
|
||||
f, err := os.Create(cfgFile)
|
||||
if err != nil {
|
||||
log.Errorf("Create(%s) %s", cfgFile, err)
|
||||
return err
|
||||
}
|
||||
err = filetmpl.Execute(f, menu)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ReadGlobalCfg(globalCfg string) (string, error) {
|
||||
append := ""
|
||||
buf, err := ioutil.ReadFile(globalCfg)
|
||||
if err != nil {
|
||||
return append, err
|
||||
}
|
||||
|
||||
s := bufio.NewScanner(bytes.NewReader(buf))
|
||||
for s.Scan() {
|
||||
line := strings.TrimSpace(s.Text())
|
||||
if strings.HasPrefix(line, "APPEND") {
|
||||
append = strings.TrimSpace(strings.TrimPrefix(line, "APPEND"))
|
||||
}
|
||||
}
|
||||
return append, nil
|
||||
}
|
||||
|
||||
func ReadSyslinuxCfg(currentCfg string) (string, string, error) {
|
||||
vmlinuzFile := ""
|
||||
initrdFile := ""
|
||||
// Need to parse currentCfg for the lines:
|
||||
// KERNEL ../vmlinuz-4.9.18-rancher^M
|
||||
// INITRD ../initrd-41e02e6-dirty^M
|
||||
buf, err := ioutil.ReadFile(currentCfg)
|
||||
if err != nil {
|
||||
return vmlinuzFile, initrdFile, err
|
||||
}
|
||||
|
||||
DIST := filepath.Dir(currentCfg)
|
||||
|
||||
s := bufio.NewScanner(bytes.NewReader(buf))
|
||||
for s.Scan() {
|
||||
line := strings.TrimSpace(s.Text())
|
||||
if strings.HasPrefix(line, "KERNEL") {
|
||||
vmlinuzFile = strings.TrimSpace(strings.TrimPrefix(line, "KERNEL"))
|
||||
vmlinuzFile = filepath.Join(DIST, filepath.Base(vmlinuzFile))
|
||||
}
|
||||
if strings.HasPrefix(line, "INITRD") {
|
||||
initrdFile = strings.TrimSpace(strings.TrimPrefix(line, "INITRD"))
|
||||
initrdFile = filepath.Join(DIST, filepath.Base(initrdFile))
|
||||
}
|
||||
}
|
||||
return vmlinuzFile, initrdFile, err
|
||||
}
|
||||
@@ -1,18 +1,17 @@
|
||||
package control
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
|
||||
log "github.com/Sirupsen/logrus"
|
||||
yaml "github.com/cloudfoundry-incubator/candiedyaml"
|
||||
"github.com/rancher/os/log"
|
||||
|
||||
"github.com/codegangsta/cli"
|
||||
dockerClient "github.com/docker/engine-api/client"
|
||||
@@ -22,6 +21,8 @@ import (
|
||||
"github.com/rancher/os/compose"
|
||||
"github.com/rancher/os/config"
|
||||
"github.com/rancher/os/docker"
|
||||
"github.com/rancher/os/util"
|
||||
"github.com/rancher/os/util/network"
|
||||
)
|
||||
|
||||
type Images struct {
|
||||
@@ -53,7 +54,7 @@ func osSubcommands() []cli.Command {
|
||||
Usage: "do not reboot after upgrade",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "kexec",
|
||||
Name: "kexec, k",
|
||||
Usage: "reboot using kexec",
|
||||
},
|
||||
cli.StringFlag{
|
||||
@@ -64,6 +65,10 @@ func osSubcommands() []cli.Command {
|
||||
Name: "upgrade-console",
|
||||
Usage: "upgrade console even if persistent",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "debug",
|
||||
Usage: "Run installer with debug output",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -80,41 +85,50 @@ func osSubcommands() []cli.Command {
|
||||
}
|
||||
|
||||
func getImages() (*Images, error) {
|
||||
upgradeUrl, err := getUpgradeUrl()
|
||||
upgradeURL, err := getUpgradeURL()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var body []byte
|
||||
|
||||
if strings.HasPrefix(upgradeUrl, "/") {
|
||||
body, err = ioutil.ReadFile(upgradeUrl)
|
||||
if strings.HasPrefix(upgradeURL, "/") {
|
||||
body, err = ioutil.ReadFile(upgradeURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
u, err := url.Parse(upgradeUrl)
|
||||
u, err := url.Parse(upgradeURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
q := u.Query()
|
||||
q.Set("current", config.VERSION)
|
||||
u.RawQuery = q.Encode()
|
||||
upgradeUrl = u.String()
|
||||
|
||||
resp, err := http.Get(upgradeUrl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
q.Set("current", config.Version)
|
||||
if hypervisor := util.GetHypervisor(); hypervisor == "" {
|
||||
q.Set("hypervisor", hypervisor)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, err = ioutil.ReadAll(resp.Body)
|
||||
u.RawQuery = q.Encode()
|
||||
upgradeURL = u.String()
|
||||
|
||||
body, err = network.LoadFromNetwork(upgradeURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return parseBody(body)
|
||||
images, err := parseBody(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cfg := config.LoadConfig()
|
||||
images.Current = formatImage(images.Current, cfg)
|
||||
for i := len(images.Available) - 1; i >= 0; i-- {
|
||||
images.Available[i] = formatImage(images.Available[i], cfg)
|
||||
}
|
||||
|
||||
return images, nil
|
||||
}
|
||||
|
||||
func osMetaDataGet(c *cli.Context) error {
|
||||
@@ -128,13 +142,31 @@ func osMetaDataGet(c *cli.Context) error {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
for _, image := range images.Available {
|
||||
cfg := config.LoadConfig()
|
||||
runningName := cfg.Rancher.Upgrade.Image + ":" + config.Version
|
||||
runningName = formatImage(runningName, cfg)
|
||||
|
||||
foundRunning := false
|
||||
for i := len(images.Available) - 1; i >= 0; i-- {
|
||||
image := images.Available[i]
|
||||
_, _, err := client.ImageInspectWithRaw(context.Background(), image, false)
|
||||
local := "local"
|
||||
if dockerClient.IsErrImageNotFound(err) {
|
||||
fmt.Println(image, "remote")
|
||||
} else {
|
||||
fmt.Println(image, "local")
|
||||
local = "remote"
|
||||
}
|
||||
available := "available"
|
||||
if image == images.Current {
|
||||
available = "latest"
|
||||
}
|
||||
var running string
|
||||
if image == runningName {
|
||||
foundRunning = true
|
||||
running = "running"
|
||||
}
|
||||
fmt.Println(image, local, available, running)
|
||||
}
|
||||
if !foundRunning {
|
||||
fmt.Println(config.Version, "running")
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -150,6 +182,10 @@ func getLatestImage() (string, error) {
|
||||
}
|
||||
|
||||
func osUpgrade(c *cli.Context) error {
|
||||
if runtime.GOARCH != "amd64" {
|
||||
log.Fatalf("ros install / upgrade only supported on 'amd64', not '%s'", runtime.GOARCH)
|
||||
}
|
||||
|
||||
image := c.String("image")
|
||||
|
||||
if image == "" {
|
||||
@@ -165,7 +201,16 @@ func osUpgrade(c *cli.Context) error {
|
||||
if c.Args().Present() {
|
||||
log.Fatalf("invalid arguments %v", c.Args())
|
||||
}
|
||||
if err := startUpgradeContainer(image, c.Bool("stage"), c.Bool("force"), !c.Bool("no-reboot"), c.Bool("kexec"), c.Bool("upgrade-console"), c.String("append")); err != nil {
|
||||
if err := startUpgradeContainer(
|
||||
image,
|
||||
c.Bool("stage"),
|
||||
c.Bool("force"),
|
||||
!c.Bool("no-reboot"),
|
||||
c.Bool("kexec"),
|
||||
c.Bool("upgrade-console"),
|
||||
c.Bool("debug"),
|
||||
c.String("append"),
|
||||
); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -173,20 +218,21 @@ func osUpgrade(c *cli.Context) error {
|
||||
}
|
||||
|
||||
func osVersion(c *cli.Context) error {
|
||||
fmt.Println(config.VERSION)
|
||||
fmt.Println(config.Version)
|
||||
return nil
|
||||
}
|
||||
|
||||
func startUpgradeContainer(image string, stage, force, reboot, kexec bool, upgradeConsole bool, kernelArgs string) error {
|
||||
in := bufio.NewReader(os.Stdin)
|
||||
|
||||
func startUpgradeContainer(image string, stage, force, reboot, kexec, upgradeConsole, debug bool, kernelArgs string) error {
|
||||
command := []string{
|
||||
"-t", "rancher-upgrade",
|
||||
"-r", config.VERSION,
|
||||
"-r", config.Version,
|
||||
}
|
||||
|
||||
if kexec {
|
||||
command = append(command, "-k")
|
||||
command = append(command, "--kexec")
|
||||
}
|
||||
if debug {
|
||||
command = append(command, "--debug")
|
||||
}
|
||||
|
||||
kernelArgs = strings.TrimSpace(kernelArgs)
|
||||
@@ -203,10 +249,10 @@ func startUpgradeContainer(image string, stage, force, reboot, kexec bool, upgra
|
||||
fmt.Printf("Upgrading to %s\n", image)
|
||||
confirmation := "Continue"
|
||||
imageSplit := strings.Split(image, ":")
|
||||
if len(imageSplit) > 1 && imageSplit[1] == config.VERSION+config.SUFFIX {
|
||||
if len(imageSplit) > 1 && imageSplit[1] == config.Version+config.Suffix {
|
||||
confirmation = fmt.Sprintf("Already at version %s. Continue anyway", imageSplit[1])
|
||||
}
|
||||
if !force && !yes(in, confirmation) {
|
||||
if !force && !yes(confirmation) {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
@@ -217,7 +263,7 @@ func startUpgradeContainer(image string, stage, force, reboot, kexec bool, upgra
|
||||
Pid: "host",
|
||||
Image: image,
|
||||
Labels: map[string]string{
|
||||
config.SCOPE: config.SYSTEM,
|
||||
config.ScopeLabel: config.System,
|
||||
},
|
||||
Command: command,
|
||||
})
|
||||
@@ -256,7 +302,7 @@ func startUpgradeContainer(image string, stage, force, reboot, kexec bool, upgra
|
||||
return err
|
||||
}
|
||||
|
||||
if reboot && (force || yes(in, "Continue with reboot")) {
|
||||
if reboot && (force || yes("Continue with reboot")) {
|
||||
log.Info("Rebooting")
|
||||
power.Reboot()
|
||||
}
|
||||
@@ -275,7 +321,7 @@ func parseBody(body []byte) (*Images, error) {
|
||||
return update, nil
|
||||
}
|
||||
|
||||
func getUpgradeUrl() (string, error) {
|
||||
func getUpgradeURL() (string, error) {
|
||||
cfg := config.LoadConfig()
|
||||
return cfg.Rancher.Upgrade.Url, nil
|
||||
return cfg.Rancher.Upgrade.URL, nil
|
||||
}
|
||||
|
||||
118
cmd/control/preload.go
Normal file
118
cmd/control/preload.go
Normal file
@@ -0,0 +1,118 @@
|
||||
package control
|
||||
|
||||
import (
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/codegangsta/cli"
|
||||
|
||||
dockerClient "github.com/docker/engine-api/client"
|
||||
"github.com/docker/engine-api/types"
|
||||
"github.com/rancher/os/config"
|
||||
"github.com/rancher/os/docker"
|
||||
"github.com/rancher/os/log"
|
||||
)
|
||||
|
||||
const (
|
||||
userImagesPreloadDirectory = "/var/lib/rancher/preload/docker"
|
||||
)
|
||||
|
||||
func preloadImagesAction(c *cli.Context) error {
|
||||
err := PreloadImages(docker.NewDefaultClient, userImagesPreloadDirectory)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to preload user images: %v", err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func shouldLoad(file string) bool {
|
||||
if strings.HasSuffix(file, ".done") {
|
||||
return false
|
||||
}
|
||||
if _, err := os.Stat(fmt.Sprintf("%s.done", file)); err == nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func PreloadImages(clientFactory func() (dockerClient.APIClient, error), imagesDir string) error {
|
||||
var client dockerClient.APIClient
|
||||
clientInitialized := false
|
||||
|
||||
if _, err := os.Stat(imagesDir); os.IsNotExist(err) {
|
||||
if err = os.MkdirAll(imagesDir, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
files, err := ioutil.ReadDir(imagesDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
filename := path.Join(imagesDir, file.Name())
|
||||
if !shouldLoad(filename) {
|
||||
log.Infof("Skipping to preload the file: %s", filename)
|
||||
continue
|
||||
}
|
||||
|
||||
image, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer image.Close()
|
||||
var imageReader io.Reader
|
||||
imageReader = image
|
||||
match, err := regexp.MatchString(".t?gz$", file.Name())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if match {
|
||||
imageReader, err = gzip.NewReader(image)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if !clientInitialized {
|
||||
client, err = clientFactory()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
clientInitialized = true
|
||||
}
|
||||
|
||||
var imageLoadResponse types.ImageLoadResponse
|
||||
if imageLoadResponse, err = client.ImageLoad(context.Background(), imageReader, false); err != nil {
|
||||
return err
|
||||
}
|
||||
cfg := config.LoadConfig()
|
||||
if cfg.Rancher.PreloadWait {
|
||||
if _, err := ioutil.ReadAll(imageLoadResponse.Body); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
log.Infof("Finished to load image %s", filename)
|
||||
|
||||
log.Infof("Creating done stamp file for image %s", filename)
|
||||
doneStamp, err := os.Create(fmt.Sprintf("%s.done", filename))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer doneStamp.Close()
|
||||
log.Infof("Finished to created the done stamp file for image %s", filename)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
23
cmd/control/recovery_init.go
Normal file
23
cmd/control/recovery_init.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package control
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"syscall"
|
||||
|
||||
"github.com/codegangsta/cli"
|
||||
"github.com/rancher/os/log"
|
||||
)
|
||||
|
||||
func recoveryInitAction(c *cli.Context) error {
|
||||
if err := writeRespawn("root", false, true); err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
|
||||
respawnBinPath, err := exec.LookPath("respawn")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return syscall.Exec(respawnBinPath, []string{"respawn", "-f", "/etc/respawn.conf"}, os.Environ())
|
||||
}
|
||||
@@ -11,13 +11,11 @@ import (
|
||||
func selinuxCommand() cli.Command {
|
||||
app := cli.Command{}
|
||||
app.Name = "selinux"
|
||||
app.Usage = "Launch SELinux tools container."
|
||||
app.Action = func(c *cli.Context) error {
|
||||
argv := []string{"system-docker", "run", "-it", "--privileged", "--rm",
|
||||
"--net", "host", "--pid", "host", "--ipc", "host",
|
||||
"-v", "/usr/bin/docker:/usr/bin/docker.dist:ro",
|
||||
"-v", "/usr/bin/ros:/usr/bin/dockerlaunch:ro",
|
||||
"-v", "/usr/bin/ros:/usr/bin/user-docker:ro",
|
||||
"-v", "/usr/bin/ros:/usr/bin/system-docker:ro",
|
||||
"-v", "/usr/bin/ros:/sbin/poweroff:ro",
|
||||
"-v", "/usr/bin/ros:/sbin/reboot:ro",
|
||||
@@ -50,7 +48,7 @@ func selinuxCommand() cli.Command {
|
||||
"-v", "/etc/selinux:/etc/selinux",
|
||||
"-v", "/var/lib/selinux:/var/lib/selinux",
|
||||
"-v", "/usr/share/selinux:/usr/share/selinux",
|
||||
fmt.Sprintf("%s/os-selinuxtools:%s%s", config.OS_REPO, config.VERSION, config.SUFFIX), "bash"}
|
||||
fmt.Sprintf("%s/os-selinuxtools:%s%s", config.OsRepo, config.Version, config.Suffix), "bash"}
|
||||
syscall.Exec("/bin/system-docker", argv, []string{})
|
||||
return nil
|
||||
}
|
||||
|
||||
180
cmd/control/service/app/app.go
Normal file
180
cmd/control/service/app/app.go
Normal file
@@ -0,0 +1,180 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/rancher/os/log"
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"github.com/codegangsta/cli"
|
||||
"github.com/docker/libcompose/project"
|
||||
"github.com/docker/libcompose/project/options"
|
||||
)
|
||||
|
||||
func ProjectPs(p project.APIProject, c *cli.Context) error {
|
||||
qFlag := c.Bool("q")
|
||||
allInfo, err := p.Ps(context.Background(), qFlag, c.Args()...)
|
||||
if err != nil {
|
||||
return cli.NewExitError(err.Error(), 1)
|
||||
}
|
||||
os.Stdout.WriteString(allInfo.String(!qFlag))
|
||||
return nil
|
||||
}
|
||||
|
||||
func ProjectStop(p project.APIProject, c *cli.Context) error {
|
||||
err := p.Stop(context.Background(), c.Int("timeout"), c.Args()...)
|
||||
if err != nil {
|
||||
return cli.NewExitError(err.Error(), 1)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ProjectDown(p project.APIProject, c *cli.Context) error {
|
||||
options := options.Down{
|
||||
RemoveVolume: c.Bool("volumes"),
|
||||
RemoveImages: options.ImageType(c.String("rmi")),
|
||||
RemoveOrphans: c.Bool("remove-orphans"),
|
||||
}
|
||||
err := p.Down(context.Background(), options, c.Args()...)
|
||||
if err != nil {
|
||||
return cli.NewExitError(err.Error(), 1)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ProjectBuild(p project.APIProject, c *cli.Context) error {
|
||||
config := options.Build{
|
||||
NoCache: c.Bool("no-cache"),
|
||||
ForceRemove: c.Bool("force-rm"),
|
||||
Pull: c.Bool("pull"),
|
||||
}
|
||||
err := p.Build(context.Background(), config, c.Args()...)
|
||||
if err != nil {
|
||||
return cli.NewExitError(err.Error(), 1)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ProjectCreate(p project.APIProject, c *cli.Context) error {
|
||||
options := options.Create{
|
||||
NoRecreate: c.Bool("no-recreate"),
|
||||
ForceRecreate: c.Bool("force-recreate"),
|
||||
NoBuild: c.Bool("no-build"),
|
||||
}
|
||||
err := p.Create(context.Background(), options, c.Args()...)
|
||||
if err != nil {
|
||||
return cli.NewExitError(err.Error(), 1)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ProjectUp(p project.APIProject, c *cli.Context) error {
|
||||
options := options.Up{
|
||||
Create: options.Create{
|
||||
NoRecreate: c.Bool("no-recreate"),
|
||||
ForceRecreate: c.Bool("force-recreate"),
|
||||
NoBuild: c.Bool("no-build"),
|
||||
},
|
||||
}
|
||||
ctx, cancelFun := context.WithCancel(context.Background())
|
||||
err := p.Up(ctx, options, c.Args()...)
|
||||
if err != nil {
|
||||
return cli.NewExitError(err.Error(), 1)
|
||||
}
|
||||
if c.Bool("foreground") {
|
||||
signalChan := make(chan os.Signal, 1)
|
||||
cleanupDone := make(chan bool)
|
||||
signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM)
|
||||
errChan := make(chan error)
|
||||
go func() {
|
||||
errChan <- p.Log(ctx, true, c.Args()...)
|
||||
}()
|
||||
go func() {
|
||||
select {
|
||||
case <-signalChan:
|
||||
fmt.Printf("\nGracefully stopping...\n")
|
||||
cancelFun()
|
||||
ProjectStop(p, c)
|
||||
cleanupDone <- true
|
||||
case err := <-errChan:
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
cleanupDone <- true
|
||||
}
|
||||
}()
|
||||
<-cleanupDone
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ProjectStart(p project.APIProject, c *cli.Context) error {
|
||||
err := p.Start(context.Background(), c.Args()...)
|
||||
if err != nil {
|
||||
return cli.NewExitError(err.Error(), 1)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ProjectRestart(p project.APIProject, c *cli.Context) error {
|
||||
err := p.Restart(context.Background(), c.Int("timeout"), c.Args()...)
|
||||
if err != nil {
|
||||
return cli.NewExitError(err.Error(), 1)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ProjectLog(p project.APIProject, c *cli.Context) error {
|
||||
err := p.Log(context.Background(), c.Bool("follow"), c.Args()...)
|
||||
if err != nil {
|
||||
return cli.NewExitError(err.Error(), 1)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ProjectPull(p project.APIProject, c *cli.Context) error {
|
||||
err := p.Pull(context.Background(), c.Args()...)
|
||||
if err != nil && !c.Bool("ignore-pull-failures") {
|
||||
return cli.NewExitError(err.Error(), 1)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ProjectDelete(p project.APIProject, c *cli.Context) error {
|
||||
options := options.Delete{
|
||||
RemoveVolume: c.Bool("v"),
|
||||
}
|
||||
if !c.Bool("force") {
|
||||
options.BeforeDeleteCallback = func(stoppedContainers []string) bool {
|
||||
fmt.Printf("Going to remove %v\nAre you sure? [yN]\n", strings.Join(stoppedContainers, ", "))
|
||||
var answer string
|
||||
_, err := fmt.Scanln(&answer)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return false
|
||||
}
|
||||
if answer != "y" && answer != "Y" {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
err := p.Delete(context.Background(), options, c.Args()...)
|
||||
if err != nil {
|
||||
return cli.NewExitError(err.Error(), 1)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ProjectKill(p project.APIProject, c *cli.Context) error {
|
||||
err := p.Kill(context.Background(), c.String("signal"), c.Args()...)
|
||||
if err != nil {
|
||||
return cli.NewExitError(err.Error(), 1)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
243
cmd/control/service/command/command.go
Normal file
243
cmd/control/service/command/command.go
Normal file
@@ -0,0 +1,243 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/codegangsta/cli"
|
||||
composeApp "github.com/docker/libcompose/cli/app"
|
||||
"github.com/rancher/os/cmd/control/service/app"
|
||||
)
|
||||
|
||||
func verifyOneOrMoreServices(c *cli.Context) error {
|
||||
if len(c.Args()) == 0 {
|
||||
return errors.New("Must specify one or more services")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func CreateCommand(factory composeApp.ProjectFactory) cli.Command {
|
||||
return cli.Command{
|
||||
Name: "create",
|
||||
Usage: "Create services",
|
||||
Before: verifyOneOrMoreServices,
|
||||
Action: composeApp.WithProject(factory, app.ProjectCreate),
|
||||
Flags: []cli.Flag{
|
||||
cli.BoolFlag{
|
||||
Name: "no-recreate",
|
||||
Usage: "If containers already exist, don't recreate them. Incompatible with --force-recreate.",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "force-recreate",
|
||||
Usage: "Recreate containers even if their configuration and image haven't changed. Incompatible with --no-recreate.",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "no-build",
|
||||
Usage: "Don't build an image, even if it's missing.",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func BuildCommand(factory composeApp.ProjectFactory) cli.Command {
|
||||
return cli.Command{
|
||||
Name: "build",
|
||||
Usage: "Build or rebuild services",
|
||||
Before: verifyOneOrMoreServices,
|
||||
Action: composeApp.WithProject(factory, app.ProjectBuild),
|
||||
Flags: []cli.Flag{
|
||||
cli.BoolFlag{
|
||||
Name: "no-cache",
|
||||
Usage: "Do not use cache when building the image",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "force-rm",
|
||||
Usage: "Always remove intermediate containers",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "pull",
|
||||
Usage: "Always attempt to pull a newer version of the image",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func PsCommand(factory composeApp.ProjectFactory) cli.Command {
|
||||
return cli.Command{
|
||||
Name: "ps",
|
||||
Usage: "List containers",
|
||||
Action: composeApp.WithProject(factory, app.ProjectPs),
|
||||
Flags: []cli.Flag{
|
||||
cli.BoolFlag{
|
||||
Name: "q",
|
||||
Usage: "Only display IDs",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func UpCommand(factory composeApp.ProjectFactory) cli.Command {
|
||||
return cli.Command{
|
||||
Name: "up",
|
||||
Usage: "Create and start containers",
|
||||
Before: verifyOneOrMoreServices,
|
||||
Action: composeApp.WithProject(factory, app.ProjectUp),
|
||||
Flags: []cli.Flag{
|
||||
cli.BoolFlag{
|
||||
Name: "foreground",
|
||||
Usage: "Run in foreground and log",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "no-build",
|
||||
Usage: "Don't build an image, even if it's missing.",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "no-recreate",
|
||||
Usage: "If containers already exist, don't recreate them. Incompatible with --force-recreate.",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "force-recreate",
|
||||
Usage: "Recreate containers even if their configuration and image haven't changed. Incompatible with --no-recreate.",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func StartCommand(factory composeApp.ProjectFactory) cli.Command {
|
||||
return cli.Command{
|
||||
Name: "start",
|
||||
Usage: "Start services",
|
||||
Before: verifyOneOrMoreServices,
|
||||
Action: composeApp.WithProject(factory, app.ProjectStart),
|
||||
Flags: []cli.Flag{
|
||||
cli.BoolTFlag{
|
||||
Name: "foreground",
|
||||
Usage: "Run in foreground and log",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func PullCommand(factory composeApp.ProjectFactory) cli.Command {
|
||||
return cli.Command{
|
||||
Name: "pull",
|
||||
Usage: "Pulls service images",
|
||||
Before: verifyOneOrMoreServices,
|
||||
Action: composeApp.WithProject(factory, app.ProjectPull),
|
||||
Flags: []cli.Flag{
|
||||
cli.BoolFlag{
|
||||
Name: "ignore-pull-failures",
|
||||
Usage: "Pull what it can and ignores images with pull failures.",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func LogsCommand(factory composeApp.ProjectFactory) cli.Command {
|
||||
return cli.Command{
|
||||
Name: "logs",
|
||||
Usage: "View output from containers",
|
||||
Before: verifyOneOrMoreServices,
|
||||
Action: composeApp.WithProject(factory, app.ProjectLog),
|
||||
Flags: []cli.Flag{
|
||||
cli.IntFlag{
|
||||
Name: "lines",
|
||||
Usage: "number of lines to tail",
|
||||
Value: 100,
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "follow, f",
|
||||
Usage: "Follow log output.",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func RestartCommand(factory composeApp.ProjectFactory) cli.Command {
|
||||
return cli.Command{
|
||||
Name: "restart",
|
||||
Usage: "Restart services",
|
||||
Before: verifyOneOrMoreServices,
|
||||
Action: composeApp.WithProject(factory, app.ProjectRestart),
|
||||
Flags: []cli.Flag{
|
||||
cli.IntFlag{
|
||||
Name: "timeout,t",
|
||||
Usage: "Specify a shutdown timeout in seconds.",
|
||||
Value: 10,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func StopCommand(factory composeApp.ProjectFactory) cli.Command {
|
||||
return cli.Command{
|
||||
Name: "stop",
|
||||
Usage: "Stop services",
|
||||
Before: verifyOneOrMoreServices,
|
||||
Action: composeApp.WithProject(factory, app.ProjectStop),
|
||||
Flags: []cli.Flag{
|
||||
cli.IntFlag{
|
||||
Name: "timeout,t",
|
||||
Usage: "Specify a shutdown timeout in seconds.",
|
||||
Value: 10,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func DownCommand(factory composeApp.ProjectFactory) cli.Command {
|
||||
return cli.Command{
|
||||
Name: "down",
|
||||
Usage: "Stop and remove containers, networks, images, and volumes",
|
||||
Before: verifyOneOrMoreServices,
|
||||
Action: composeApp.WithProject(factory, app.ProjectDown),
|
||||
Flags: []cli.Flag{
|
||||
cli.BoolFlag{
|
||||
Name: "volumes,v",
|
||||
Usage: "Remove data volumes",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "rmi",
|
||||
Usage: "Remove images, type may be one of: 'all' to remove all images, or 'local' to remove only images that don't have an custom name set by the `image` field",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "remove-orphans",
|
||||
Usage: "Remove containers for services not defined in the Compose file",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func RmCommand(factory composeApp.ProjectFactory) cli.Command {
|
||||
return cli.Command{
|
||||
Name: "rm",
|
||||
Usage: "Delete services",
|
||||
Before: verifyOneOrMoreServices,
|
||||
Action: composeApp.WithProject(factory, app.ProjectDelete),
|
||||
Flags: []cli.Flag{
|
||||
cli.BoolFlag{
|
||||
Name: "force,f",
|
||||
Usage: "Allow deletion of all services",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "v",
|
||||
Usage: "Remove volumes associated with containers",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func KillCommand(factory composeApp.ProjectFactory) cli.Command {
|
||||
return cli.Command{
|
||||
Name: "kill",
|
||||
Usage: "Kill containers",
|
||||
Before: verifyOneOrMoreServices,
|
||||
Action: composeApp.WithProject(factory, app.ProjectKill),
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "signal,s",
|
||||
Usage: "SIGNAL to send to the container",
|
||||
Value: "SIGKILL",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,17 @@
|
||||
package control
|
||||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/codegangsta/cli"
|
||||
"github.com/docker/libcompose/cli/command"
|
||||
dockerApp "github.com/docker/libcompose/cli/docker/app"
|
||||
"github.com/docker/libcompose/project"
|
||||
"github.com/rancher/os/cmd/control/service/command"
|
||||
"github.com/rancher/os/compose"
|
||||
"github.com/rancher/os/config"
|
||||
"github.com/rancher/os/log"
|
||||
"github.com/rancher/os/util"
|
||||
"github.com/rancher/os/util/network"
|
||||
)
|
||||
|
||||
@@ -24,20 +25,21 @@ func (p *projectFactory) Create(c *cli.Context) (project.APIProject, error) {
|
||||
|
||||
func beforeApp(c *cli.Context) error {
|
||||
if c.GlobalBool("verbose") {
|
||||
logrus.SetLevel(logrus.DebugLevel)
|
||||
log.SetLevel(log.DebugLevel)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func serviceCommand() cli.Command {
|
||||
func Commands() cli.Command {
|
||||
factory := &projectFactory{}
|
||||
|
||||
app := cli.Command{}
|
||||
app.Name = "service"
|
||||
app.ShortName = "s"
|
||||
app.Usage = "Command line interface for services and compose."
|
||||
app.Before = beforeApp
|
||||
app.Flags = append(command.CommonFlags(), dockerApp.DockerClientFlags()...)
|
||||
app.Flags = append(dockerApp.DockerClientFlags(), cli.BoolFlag{
|
||||
Name: "verbose,debug",
|
||||
})
|
||||
app.Subcommands = append(serviceSubCommands(),
|
||||
command.BuildCommand(factory),
|
||||
command.CreateCommand(factory),
|
||||
@@ -46,11 +48,9 @@ func serviceCommand() cli.Command {
|
||||
command.LogsCommand(factory),
|
||||
command.RestartCommand(factory),
|
||||
command.StopCommand(factory),
|
||||
command.ScaleCommand(factory),
|
||||
command.RmCommand(factory),
|
||||
command.PullCommand(factory),
|
||||
command.KillCommand(factory),
|
||||
command.PortCommand(factory),
|
||||
command.PsCommand(factory),
|
||||
)
|
||||
|
||||
@@ -91,6 +91,8 @@ func disable(c *cli.Context) error {
|
||||
cfg := config.LoadConfig()
|
||||
|
||||
for _, service := range c.Args() {
|
||||
validateService(service, cfg)
|
||||
|
||||
if _, ok := cfg.Rancher.ServicesInclude[service]; !ok {
|
||||
continue
|
||||
}
|
||||
@@ -101,7 +103,7 @@ func disable(c *cli.Context) error {
|
||||
|
||||
if changed {
|
||||
if err := updateIncludedServices(cfg); err != nil {
|
||||
logrus.Fatal(err)
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,16 +115,19 @@ func del(c *cli.Context) error {
|
||||
cfg := config.LoadConfig()
|
||||
|
||||
for _, service := range c.Args() {
|
||||
validateService(service, cfg)
|
||||
|
||||
if _, ok := cfg.Rancher.ServicesInclude[service]; !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
delete(cfg.Rancher.ServicesInclude, service)
|
||||
changed = true
|
||||
}
|
||||
|
||||
if changed {
|
||||
if err := updateIncludedServices(cfg); err != nil {
|
||||
logrus.Fatal(err)
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,9 +140,11 @@ func enable(c *cli.Context) error {
|
||||
var enabledServices []string
|
||||
|
||||
for _, service := range c.Args() {
|
||||
validateService(service, cfg)
|
||||
|
||||
if val, ok := cfg.Rancher.ServicesInclude[service]; !ok || !val {
|
||||
if strings.HasPrefix(service, "/") && !strings.HasPrefix(service, "/var/lib/rancher/conf") {
|
||||
logrus.Fatalf("ERROR: Service should be in path /var/lib/rancher/conf")
|
||||
if isLocal(service) && !strings.HasPrefix(service, "/var/lib/rancher/conf") {
|
||||
log.Fatalf("ERROR: Service should be in path /var/lib/rancher/conf")
|
||||
}
|
||||
|
||||
cfg.Rancher.ServicesInclude[service] = true
|
||||
@@ -147,11 +154,11 @@ func enable(c *cli.Context) error {
|
||||
|
||||
if len(enabledServices) > 0 {
|
||||
if err := compose.StageServices(cfg, enabledServices...); err != nil {
|
||||
logrus.Fatal(err)
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if err := updateIncludedServices(cfg); err != nil {
|
||||
logrus.Fatal(err)
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,10 +173,7 @@ func list(c *cli.Context) error {
|
||||
clone[service] = enabled
|
||||
}
|
||||
|
||||
services, err := network.GetServices(cfg.Rancher.Repositories.ToArray())
|
||||
if err != nil {
|
||||
logrus.Fatalf("Failed to get services: %v", err)
|
||||
}
|
||||
services := availableService(cfg)
|
||||
|
||||
for _, service := range services {
|
||||
if enabled, ok := clone[service]; ok {
|
||||
@@ -194,3 +198,34 @@ func list(c *cli.Context) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func isLocal(service string) bool {
|
||||
return strings.HasPrefix(service, "/")
|
||||
}
|
||||
|
||||
func IsLocalOrURL(service string) bool {
|
||||
return isLocal(service) || strings.HasPrefix(service, "http:/") || strings.HasPrefix(service, "https:/")
|
||||
}
|
||||
|
||||
// ValidService checks to see if the service definition exists
|
||||
func ValidService(service string, cfg *config.CloudConfig) bool {
|
||||
services := availableService(cfg)
|
||||
if !IsLocalOrURL(service) && !util.Contains(services, service) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func validateService(service string, cfg *config.CloudConfig) {
|
||||
if !ValidService(service, cfg) {
|
||||
log.Fatalf("%s is not a valid service", service)
|
||||
}
|
||||
}
|
||||
|
||||
func availableService(cfg *config.CloudConfig) []string {
|
||||
services, err := network.GetServices(cfg.Rancher.Repositories.ToArray())
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to get services: %v", err)
|
||||
}
|
||||
return services
|
||||
}
|
||||
@@ -1,31 +1,32 @@
|
||||
package switchconsole
|
||||
package control
|
||||
|
||||
import (
|
||||
"os"
|
||||
"errors"
|
||||
|
||||
log "github.com/Sirupsen/logrus"
|
||||
"github.com/codegangsta/cli"
|
||||
"github.com/docker/libcompose/project/options"
|
||||
"github.com/rancher/os/compose"
|
||||
"github.com/rancher/os/config"
|
||||
"github.com/rancher/os/log"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
func Main() {
|
||||
if len(os.Args) != 2 {
|
||||
log.Fatal("Must specify exactly one existing container")
|
||||
func switchConsoleAction(c *cli.Context) error {
|
||||
if len(c.Args()) != 1 {
|
||||
return errors.New("Must specify exactly one existing container")
|
||||
}
|
||||
newConsole := os.Args[1]
|
||||
newConsole := c.Args()[0]
|
||||
|
||||
cfg := config.LoadConfig()
|
||||
|
||||
project, err := compose.GetProject(cfg, true, false)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
return err
|
||||
}
|
||||
|
||||
if newConsole != "default" {
|
||||
if err = compose.LoadSpecialService(project, cfg, "console", newConsole); err != nil {
|
||||
log.Fatal(err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,10 +37,12 @@ func Main() {
|
||||
if err = project.Up(context.Background(), options.Up{
|
||||
Log: true,
|
||||
}, "console"); err != nil {
|
||||
log.Fatal(err)
|
||||
return err
|
||||
}
|
||||
|
||||
if err = project.Restart(context.Background(), 10, "docker"); err != nil {
|
||||
log.Errorf("Failed to restart Docker: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
log "github.com/Sirupsen/logrus"
|
||||
"github.com/rancher/os/log"
|
||||
|
||||
"github.com/codegangsta/cli"
|
||||
machineUtil "github.com/docker/machine/utils"
|
||||
@@ -16,8 +16,8 @@ import (
|
||||
const (
|
||||
NAME string = "rancher"
|
||||
BITS int = 2048
|
||||
ServerTlsPath string = "/etc/docker/tls"
|
||||
ClientTlsPath string = "/home/rancher/.docker"
|
||||
ServerTLSPath string = "/etc/docker/tls"
|
||||
ClientTLSPath string = "/home/rancher/.docker"
|
||||
Cert string = "cert.pem"
|
||||
Key string = "key.pem"
|
||||
ServerCert string = "server-cert.pem"
|
||||
@@ -76,11 +76,7 @@ func writeCerts(generateServer bool, hostname []string, certPath, keyPath, caCer
|
||||
if err := config.Set("rancher.docker.server_cert", string(cert)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := config.Set("rancher.docker.server_key", string(key)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return config.Set("rancher.docker.server_key", string(key))
|
||||
}
|
||||
|
||||
func writeCaCerts(cfg *config.CloudConfig, caCertPath, caKeyPath string) error {
|
||||
@@ -141,9 +137,9 @@ func generate(c *cli.Context) error {
|
||||
func Generate(generateServer bool, outDir string, hostnames []string) error {
|
||||
if outDir == "" {
|
||||
if generateServer {
|
||||
outDir = ServerTlsPath
|
||||
outDir = ServerTLSPath
|
||||
} else {
|
||||
outDir = ClientTlsPath
|
||||
outDir = ClientTLSPath
|
||||
}
|
||||
log.Infof("Out directory (-d, --dir) not specified, using default: %s", outDir)
|
||||
}
|
||||
|
||||
36
cmd/control/udevsettle.go
Normal file
36
cmd/control/udevsettle.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package control
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
"github.com/codegangsta/cli"
|
||||
"github.com/rancher/os/log"
|
||||
)
|
||||
|
||||
func udevSettleAction(c *cli.Context) {
|
||||
if err := UdevSettle(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func UdevSettle() error {
|
||||
cmd := exec.Command("udevd", "--daemon")
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd = exec.Command("udevadm", "trigger", "--action=add")
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd = exec.Command("udevadm", "settle")
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
return cmd.Run()
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package userdocker
|
||||
package control
|
||||
|
||||
import (
|
||||
"io"
|
||||
@@ -12,37 +12,42 @@ import (
|
||||
|
||||
"path/filepath"
|
||||
|
||||
log "github.com/Sirupsen/logrus"
|
||||
"github.com/codegangsta/cli"
|
||||
composeClient "github.com/docker/libcompose/docker/client"
|
||||
"github.com/docker/libcompose/project"
|
||||
"github.com/rancher/os/cmd/control"
|
||||
"github.com/rancher/os/compose"
|
||||
"github.com/rancher/os/config"
|
||||
rosDocker "github.com/rancher/os/docker"
|
||||
"github.com/rancher/os/log"
|
||||
"github.com/rancher/os/util"
|
||||
)
|
||||
|
||||
const (
|
||||
DEFAULT_STORAGE_CONTEXT = "console"
|
||||
DOCKER_PID_FILE = "/var/run/docker.pid"
|
||||
DOCKER_COMMAND = "docker-init"
|
||||
userDocker = "user-docker"
|
||||
sourceDirectory = "/engine"
|
||||
destDirectory = "/var/lib/rancher/engine"
|
||||
defaultStorageContext = "console"
|
||||
dockerPidFile = "/var/run/docker.pid"
|
||||
sourceDirectory = "/engine"
|
||||
destDirectory = "/var/lib/rancher/engine"
|
||||
)
|
||||
|
||||
func Main() {
|
||||
var (
|
||||
dockerCommand = []string{
|
||||
"ros",
|
||||
"docker-init",
|
||||
}
|
||||
)
|
||||
|
||||
func userDockerAction(c *cli.Context) error {
|
||||
if err := copyBinaries(sourceDirectory, destDirectory); err != nil {
|
||||
log.Fatal(err)
|
||||
return err
|
||||
}
|
||||
|
||||
if err := syscall.Mount("/host/sys", "/sys", "", syscall.MS_BIND|syscall.MS_REC, ""); err != nil {
|
||||
log.Fatal(err)
|
||||
return err
|
||||
}
|
||||
|
||||
cfg := config.LoadConfig()
|
||||
|
||||
log.Fatal(startDocker(cfg))
|
||||
return startDocker(cfg)
|
||||
}
|
||||
|
||||
func copyBinaries(source, dest string) error {
|
||||
@@ -98,15 +103,15 @@ func copyBinaries(source, dest string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeCerts(cfg *config.CloudConfig) error {
|
||||
outDir := control.ServerTlsPath
|
||||
func writeConfigCerts(cfg *config.CloudConfig) error {
|
||||
outDir := ServerTLSPath
|
||||
if err := os.MkdirAll(outDir, 0700); err != nil {
|
||||
return err
|
||||
}
|
||||
caCertPath := filepath.Join(outDir, control.CaCert)
|
||||
caKeyPath := filepath.Join(outDir, control.CaKey)
|
||||
serverCertPath := filepath.Join(outDir, control.ServerCert)
|
||||
serverKeyPath := filepath.Join(outDir, control.ServerKey)
|
||||
caCertPath := filepath.Join(outDir, CaCert)
|
||||
caKeyPath := filepath.Join(outDir, CaKey)
|
||||
serverCertPath := filepath.Join(outDir, ServerCert)
|
||||
serverKeyPath := filepath.Join(outDir, ServerKey)
|
||||
if cfg.Rancher.Docker.CACert != "" {
|
||||
if err := util.WriteFileAtomic(caCertPath, []byte(cfg.Rancher.Docker.CACert), 0400); err != nil {
|
||||
return err
|
||||
@@ -131,7 +136,7 @@ func writeCerts(cfg *config.CloudConfig) error {
|
||||
func startDocker(cfg *config.CloudConfig) error {
|
||||
storageContext := cfg.Rancher.Docker.StorageContext
|
||||
if storageContext == "" {
|
||||
storageContext = DEFAULT_STORAGE_CONTEXT
|
||||
storageContext = defaultStorageContext
|
||||
}
|
||||
|
||||
log.Infof("Starting Docker in context: %s", storageContext)
|
||||
@@ -160,7 +165,7 @@ func startDocker(cfg *config.CloudConfig) error {
|
||||
log.Debugf("User Docker args: %v", args)
|
||||
|
||||
if dockerCfg.TLS {
|
||||
if err := writeCerts(cfg); err != nil {
|
||||
if err := writeConfigCerts(cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -170,14 +175,14 @@ func startDocker(cfg *config.CloudConfig) error {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd := []string{"docker-runc", "exec", "--", info.ID, "env"}
|
||||
cmd := []string{"system-docker-runc", "exec", "--", info.ID, "env"}
|
||||
log.Info(dockerCfg.AppendEnv())
|
||||
cmd = append(cmd, dockerCfg.AppendEnv()...)
|
||||
cmd = append(cmd, DOCKER_COMMAND)
|
||||
cmd = append(cmd, dockerCommand...)
|
||||
cmd = append(cmd, args...)
|
||||
log.Infof("Running %v", cmd)
|
||||
|
||||
return syscall.Exec("/usr/bin/ros", cmd, os.Environ())
|
||||
return syscall.Exec("/usr/bin/system-docker-runc", cmd, os.Environ())
|
||||
}
|
||||
|
||||
func waitForPid(service string, project *project.Project) (int, error) {
|
||||
@@ -208,7 +213,7 @@ func getPid(service string, project *project.Project) (int, error) {
|
||||
}
|
||||
|
||||
client, err := composeClient.Create(composeClient.Options{
|
||||
Host: config.DOCKER_SYSTEM_HOST,
|
||||
Host: config.SystemDockerHost,
|
||||
})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
@@ -3,13 +3,16 @@ package control
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
log "github.com/Sirupsen/logrus"
|
||||
"github.com/rancher/os/config"
|
||||
"github.com/rancher/os/log"
|
||||
)
|
||||
|
||||
func yes(in *bufio.Reader, question string) bool {
|
||||
func yes(question string) bool {
|
||||
fmt.Printf("%s [y/N]: ", question)
|
||||
in := bufio.NewReader(os.Stdin)
|
||||
line, err := in.ReadString('\n')
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
@@ -17,3 +20,11 @@ func yes(in *bufio.Reader, question string) bool {
|
||||
|
||||
return strings.ToLower(line[0:1]) == "y"
|
||||
}
|
||||
|
||||
func formatImage(image string, cfg *config.CloudConfig) string {
|
||||
domainRegistry := cfg.Rancher.Environment["REGISTRY_DOMAIN"]
|
||||
if domainRegistry != "docker.io" && domainRegistry != "" {
|
||||
return fmt.Sprintf("%s/%s", domainRegistry, image)
|
||||
}
|
||||
return image
|
||||
}
|
||||
|
||||
@@ -1,82 +1,79 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"os"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
|
||||
log "github.com/Sirupsen/logrus"
|
||||
"github.com/rancher/os/docker"
|
||||
"github.com/rancher/os/log"
|
||||
|
||||
"io/ioutil"
|
||||
|
||||
"github.com/docker/libnetwork/resolvconf"
|
||||
"github.com/rancher/netconf"
|
||||
"github.com/rancher/os/config"
|
||||
"github.com/rancher/os/docker"
|
||||
"github.com/rancher/os/hostname"
|
||||
"github.com/rancher/os/netconf"
|
||||
)
|
||||
|
||||
var (
|
||||
stopNetworkPre bool
|
||||
flags *flag.FlagSet
|
||||
)
|
||||
|
||||
func init() {
|
||||
flags = flag.NewFlagSet(os.Args[0], flag.ContinueOnError)
|
||||
flags.BoolVar(&stopNetworkPre, "stop-network-pre", false, "")
|
||||
}
|
||||
|
||||
func Main() {
|
||||
flags.Parse(os.Args[1:])
|
||||
|
||||
log.Infof("Running network: stop-network-pre=%v", stopNetworkPre)
|
||||
|
||||
if stopNetworkPre {
|
||||
client, err := docker.NewSystemClient()
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
|
||||
err = client.ContainerStop(context.Background(), "network-pre", 10)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
|
||||
_, err = client.ContainerWait(context.Background(), "network-pre")
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
}
|
||||
log.InitLogger()
|
||||
|
||||
cfg := config.LoadConfig()
|
||||
ApplyNetworkConfig(cfg)
|
||||
|
||||
nameservers := cfg.Rancher.Network.Dns.Nameservers
|
||||
search := cfg.Rancher.Network.Dns.Search
|
||||
userSetDns := len(nameservers) > 0 || len(search) > 0
|
||||
if !userSetDns {
|
||||
nameservers = cfg.Rancher.Defaults.Network.Dns.Nameservers
|
||||
search = cfg.Rancher.Defaults.Network.Dns.Search
|
||||
}
|
||||
|
||||
if _, err := resolvconf.Build("/etc/resolv.conf", nameservers, search, nil); err != nil {
|
||||
log.Infof("Restart syslog")
|
||||
client, err := docker.NewSystemClient()
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
|
||||
if err := hostname.SetHostnameFromCloudConfig(cfg); err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
|
||||
if err := netconf.ApplyNetworkConfigs(&cfg.Rancher.Network); err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
|
||||
userSetHostname := cfg.Hostname != ""
|
||||
if err := netconf.RunDhcp(&cfg.Rancher.Network, !userSetHostname, !userSetDns); err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
|
||||
if err := hostname.SyncHostname(); err != nil {
|
||||
if err := client.ContainerRestart(context.Background(), "syslog", 10); err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
|
||||
select {}
|
||||
}
|
||||
|
||||
func ApplyNetworkConfig(cfg *config.CloudConfig) {
|
||||
log.Infof("Apply Network Config")
|
||||
userSetDNS := len(cfg.Rancher.Network.DNS.Nameservers) > 0 || len(cfg.Rancher.Network.DNS.Search) > 0
|
||||
|
||||
if err := hostname.SetHostnameFromCloudConfig(cfg); err != nil {
|
||||
log.Errorf("Failed to set hostname from cloud config: %v", err)
|
||||
}
|
||||
|
||||
userSetHostname := cfg.Hostname != ""
|
||||
dhcpSetDNS, err := netconf.ApplyNetworkConfigs(&cfg.Rancher.Network, userSetHostname, userSetDNS)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to apply network configs(by netconf): %v", err)
|
||||
}
|
||||
|
||||
if dhcpSetDNS {
|
||||
log.Infof("DNS set by DHCP")
|
||||
}
|
||||
|
||||
if !userSetDNS && !dhcpSetDNS {
|
||||
// only write 8.8.8.8,8.8.4.4 as a last resort
|
||||
log.Infof("Writing default resolv.conf - no user setting, and no DHCP setting")
|
||||
if _, err := resolvconf.Build("/etc/resolv.conf",
|
||||
cfg.Rancher.Defaults.Network.DNS.Nameservers,
|
||||
cfg.Rancher.Defaults.Network.DNS.Search,
|
||||
nil); err != nil {
|
||||
log.Errorf("Failed to write resolv.conf (!userSetDNS and !dhcpSetDNS): %v", err)
|
||||
}
|
||||
}
|
||||
if userSetDNS {
|
||||
if _, err := resolvconf.Build("/etc/resolv.conf", cfg.Rancher.Network.DNS.Nameservers, cfg.Rancher.Network.DNS.Search, nil); err != nil {
|
||||
log.Errorf("Failed to write resolv.conf (userSetDNS): %v", err)
|
||||
} else {
|
||||
log.Infof("writing to /etc/resolv.conf: nameservers: %v, search: %v", cfg.Rancher.Network.DNS.Nameservers, cfg.Rancher.Network.DNS.Search)
|
||||
}
|
||||
}
|
||||
|
||||
resolve, err := ioutil.ReadFile("/etc/resolv.conf")
|
||||
log.Debugf("Resolve.conf == [%s], %v", resolve, err)
|
||||
|
||||
log.Infof("Apply Network Config SyncHostname")
|
||||
if err := hostname.SyncHostname(); err != nil {
|
||||
log.Errorf("Failed to sync hostname: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,23 +2,32 @@ package power
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
|
||||
log "github.com/Sirupsen/logrus"
|
||||
"github.com/docker/engine-api/types"
|
||||
"github.com/docker/engine-api/types/container"
|
||||
"github.com/docker/engine-api/types/filters"
|
||||
"github.com/rancher/os/cmd/control/install"
|
||||
"github.com/rancher/os/config"
|
||||
"github.com/rancher/os/log"
|
||||
|
||||
"github.com/rancher/os/docker"
|
||||
"github.com/rancher/os/util"
|
||||
)
|
||||
|
||||
// You can't shutdown the system from a process in console because we want to stop the console container.
|
||||
// If you do that you kill yourself. So we spawn a separate container to do power operations
|
||||
// This can up because on shutdown we want ssh to gracefully die, terminating ssh connections and not just hanging tcp session
|
||||
//
|
||||
// Be careful of container name. only [a-zA-Z0-9][a-zA-Z0-9_.-] are allowed
|
||||
func runDocker(name string) error {
|
||||
if os.ExpandEnv("${IN_DOCKER}") == "true" {
|
||||
return nil
|
||||
@@ -29,14 +38,16 @@ func runDocker(name string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd := []string{name}
|
||||
cmd := os.Args
|
||||
log.Debugf("runDocker cmd: %s", cmd)
|
||||
|
||||
if name == "" {
|
||||
name = filepath.Base(os.Args[0])
|
||||
cmd = os.Args
|
||||
}
|
||||
|
||||
existing, err := client.ContainerInspect(context.Background(), name)
|
||||
containerName := strings.TrimPrefix(strings.Join(strings.Split(name, "/"), "-"), "-")
|
||||
|
||||
existing, err := client.ContainerInspect(context.Background(), containerName)
|
||||
if err == nil && existing.ID != "" {
|
||||
err := client.ContainerRemove(context.Background(), types.ContainerRemoveOptions{
|
||||
ContainerID: existing.ID,
|
||||
@@ -47,12 +58,12 @@ func runDocker(name string) error {
|
||||
}
|
||||
}
|
||||
|
||||
currentContainerId, err := util.GetCurrentContainerId()
|
||||
currentContainerID, err := util.GetCurrentContainerID()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
currentContainer, err := client.ContainerInspect(context.Background(), currentContainerId)
|
||||
currentContainer, err := client.ContainerInspect(context.Background(), currentContainerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -71,26 +82,40 @@ func runDocker(name string) error {
|
||||
currentContainer.ID,
|
||||
},
|
||||
Privileged: true,
|
||||
}, nil, name)
|
||||
}, nil, containerName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
go func() {
|
||||
client.ContainerAttach(context.Background(), types.ContainerAttachOptions{
|
||||
ContainerID: powerContainer.ID,
|
||||
Stream: true,
|
||||
Stderr: true,
|
||||
Stdout: true,
|
||||
})
|
||||
}()
|
||||
|
||||
err = client.ContainerStart(context.Background(), powerContainer.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = client.ContainerWait(context.Background(), powerContainer.ID)
|
||||
reader, err := client.ContainerLogs(context.Background(), types.ContainerLogsOptions{
|
||||
ContainerID: powerContainer.ID,
|
||||
ShowStderr: true,
|
||||
ShowStdout: true,
|
||||
Follow: true,
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
for {
|
||||
p := make([]byte, 4096)
|
||||
n, err := reader.Read(p)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
if n == 0 {
|
||||
reader.Close()
|
||||
break
|
||||
}
|
||||
}
|
||||
if n > 0 {
|
||||
fmt.Print(string(p))
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
@@ -100,40 +125,72 @@ func runDocker(name string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func common(name string) {
|
||||
func reboot(name string, force bool, code uint) {
|
||||
if os.Geteuid() != 0 {
|
||||
log.Fatalf("%s: Need to be root", os.Args[0])
|
||||
}
|
||||
|
||||
if err := runDocker(name); err != nil {
|
||||
log.Fatal(err)
|
||||
// Add shutdown timeout
|
||||
cfg := config.LoadConfig()
|
||||
timeoutValue := cfg.Rancher.ShutdownTimeout
|
||||
if timeoutValue == 0 {
|
||||
timeoutValue = 60
|
||||
}
|
||||
}
|
||||
if timeoutValue < 5 {
|
||||
timeoutValue = 5
|
||||
}
|
||||
log.Infof("Setting %s timeout to %d (rancher.shutdown_timeout set to %d)", os.Args[0], timeoutValue, cfg.Rancher.ShutdownTimeout)
|
||||
|
||||
func PowerOff() {
|
||||
common("poweroff")
|
||||
reboot(syscall.LINUX_REBOOT_CMD_POWER_OFF)
|
||||
}
|
||||
go func() {
|
||||
timeout := time.After(time.Duration(timeoutValue) * time.Second)
|
||||
tick := time.Tick(100 * time.Millisecond)
|
||||
// Keep trying until we're timed out or got a result or got an error
|
||||
for {
|
||||
select {
|
||||
// Got a timeout! fail with a timeout error
|
||||
case <-timeout:
|
||||
log.Errorf("Container shutdown taking too long, forcing %s.", os.Args[0])
|
||||
syscall.Sync()
|
||||
syscall.Reboot(int(code))
|
||||
case <-tick:
|
||||
fmt.Printf(".")
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
func Reboot() {
|
||||
common("reboot")
|
||||
reboot(syscall.LINUX_REBOOT_CMD_RESTART)
|
||||
}
|
||||
// reboot -f should work even when system-docker is having problems
|
||||
if !force {
|
||||
if kexecFlag || previouskexecFlag || kexecAppendFlag != "" {
|
||||
// pass through the cmdline args
|
||||
name = ""
|
||||
}
|
||||
if err := runDocker(name); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func Halt() {
|
||||
common("halt")
|
||||
reboot(syscall.LINUX_REBOOT_CMD_HALT)
|
||||
}
|
||||
if kexecFlag || previouskexecFlag || kexecAppendFlag != "" {
|
||||
// need to mount boot dir, or `system-docker run -v /:/host -w /host/boot` ?
|
||||
baseName := "/mnt/new_img"
|
||||
_, _, err := install.MountDevice(baseName, "", "", false)
|
||||
if err != nil {
|
||||
log.Errorf("ERROR: can't Kexec: %s", err)
|
||||
return
|
||||
}
|
||||
defer util.Unmount(baseName)
|
||||
Kexec(previouskexecFlag, filepath.Join(baseName, install.BootDir), kexecAppendFlag)
|
||||
return
|
||||
}
|
||||
|
||||
func reboot(code uint) {
|
||||
err := shutDownContainers()
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
if !force {
|
||||
err := shutDownContainers()
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
syscall.Sync()
|
||||
|
||||
err = syscall.Reboot(int(code))
|
||||
err := syscall.Reboot(int(code))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
@@ -181,33 +238,69 @@ func shutDownContainers() error {
|
||||
return err
|
||||
}
|
||||
|
||||
currentContainerId, err := util.GetCurrentContainerId()
|
||||
currentContainerID, err := util.GetCurrentContainerID()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var stopErrorStrings []string
|
||||
consoleContainerIdx := -1
|
||||
|
||||
for _, container := range containers {
|
||||
if container.ID == currentContainerId {
|
||||
for idx, container := range containers {
|
||||
if container.ID == currentContainerID {
|
||||
continue
|
||||
}
|
||||
if container.Names[0] == "/console" {
|
||||
consoleContainerIdx = idx
|
||||
continue
|
||||
}
|
||||
|
||||
log.Infof("Stopping %s : %v", container.ID[:12], container.Names)
|
||||
log.Infof("Stopping %s : %s", container.Names[0], container.ID[:12])
|
||||
stopErr := client.ContainerStop(context.Background(), container.ID, timeout)
|
||||
if stopErr != nil {
|
||||
log.Errorf("------- Error Stopping %s : %s", container.Names[0], stopErr.Error())
|
||||
stopErrorStrings = append(stopErrorStrings, " ["+container.ID+"] "+stopErr.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// lets see what containers are still running and only wait on those
|
||||
containers, err = client.ContainerList(context.Background(), opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var waitErrorStrings []string
|
||||
|
||||
for _, container := range containers {
|
||||
if container.ID == currentContainerId {
|
||||
for idx, container := range containers {
|
||||
if container.ID == currentContainerID {
|
||||
continue
|
||||
}
|
||||
if container.Names[0] == "/console" {
|
||||
consoleContainerIdx = idx
|
||||
continue
|
||||
}
|
||||
log.Infof("Waiting %s : %s", container.Names[0], container.ID[:12])
|
||||
_, waitErr := client.ContainerWait(context.Background(), container.ID)
|
||||
if waitErr != nil {
|
||||
log.Errorf("------- Error Waiting %s : %s", container.Names[0], waitErr.Error())
|
||||
waitErrorStrings = append(waitErrorStrings, " ["+container.ID+"] "+waitErr.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// and now stop the console
|
||||
if consoleContainerIdx != -1 {
|
||||
container := containers[consoleContainerIdx]
|
||||
log.Infof("Console Stopping %v : %s", container.Names, container.ID[:12])
|
||||
stopErr := client.ContainerStop(context.Background(), container.ID, timeout)
|
||||
if stopErr != nil {
|
||||
log.Errorf("------- Error Stopping %v : %s", container.Names, stopErr.Error())
|
||||
stopErrorStrings = append(stopErrorStrings, " ["+container.ID+"] "+stopErr.Error())
|
||||
}
|
||||
|
||||
log.Infof("Console Waiting %v : %s", container.Names, container.ID[:12])
|
||||
_, waitErr := client.ContainerWait(context.Background(), container.ID)
|
||||
if waitErr != nil {
|
||||
log.Errorf("------- Error Waiting %v : %s", container.Names, waitErr.Error())
|
||||
waitErrorStrings = append(waitErrorStrings, " ["+container.ID+"] "+waitErr.Error())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,46 +1,227 @@
|
||||
package power
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
|
||||
"github.com/codegangsta/cli"
|
||||
"github.com/rancher/os/cmd/control/install"
|
||||
"github.com/rancher/os/config"
|
||||
"github.com/rancher/os/log"
|
||||
)
|
||||
|
||||
func Main() {
|
||||
var (
|
||||
haltFlag bool
|
||||
poweroffFlag bool
|
||||
rebootFlag bool
|
||||
forceFlag bool
|
||||
kexecFlag bool
|
||||
previouskexecFlag bool
|
||||
kexecAppendFlag string
|
||||
)
|
||||
|
||||
func Shutdown() {
|
||||
log.InitLogger()
|
||||
app := cli.NewApp()
|
||||
|
||||
app.Name = os.Args[0]
|
||||
app.Usage = "Control and configure RancherOS"
|
||||
app.Version = config.VERSION
|
||||
app.Name = filepath.Base(os.Args[0])
|
||||
app.Usage = fmt.Sprintf("%s RancherOS\nbuilt: %s", app.Name, config.BuildDate)
|
||||
app.Version = config.Version
|
||||
app.Author = "Rancher Labs, Inc."
|
||||
app.Email = "sid@rancher.com"
|
||||
app.EnableBashCompletion = true
|
||||
app.Action = shutdown
|
||||
app.Flags = []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "r, R",
|
||||
Usage: "reboot after shutdown",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "h",
|
||||
Usage: "halt the system",
|
||||
// --no-wall
|
||||
// Do not send wall message before halt, power-off,
|
||||
// reboot.
|
||||
|
||||
// halt, poweroff, reboot ONLY
|
||||
// -f, --force
|
||||
// Force immediate halt, power-off, reboot. Do not
|
||||
// contact the init system.
|
||||
cli.BoolFlag{
|
||||
Name: "f, force",
|
||||
Usage: "Force immediate halt, power-off, reboot. Do not contact the init system.",
|
||||
Destination: &forceFlag,
|
||||
},
|
||||
|
||||
// -w, --wtmp-only
|
||||
// Only write wtmp shutdown entry, do not actually
|
||||
// halt, power-off, reboot.
|
||||
|
||||
// -d, --no-wtmp
|
||||
// Do not write wtmp shutdown entry.
|
||||
|
||||
// -n, --no-sync
|
||||
// Don't sync hard disks/storage media before halt,
|
||||
// power-off, reboot.
|
||||
|
||||
// shutdown ONLY
|
||||
// -h
|
||||
// Equivalent to --poweroff, unless --halt is
|
||||
// specified.
|
||||
|
||||
// -k
|
||||
// Do not halt, power-off, reboot, just write wall
|
||||
// message.
|
||||
|
||||
// -c
|
||||
// Cancel a pending shutdown. This may be used
|
||||
// cancel the effect of an invocation of shutdown
|
||||
// with a time argument that is not "+0" or "now".
|
||||
|
||||
}
|
||||
// -H, --halt
|
||||
// Halt the machine.
|
||||
if app.Name == "halt" {
|
||||
app.Flags = append(app.Flags, cli.BoolTFlag{
|
||||
Name: "H, halt",
|
||||
Usage: "halt the machine",
|
||||
Destination: &haltFlag,
|
||||
})
|
||||
} else {
|
||||
app.Flags = append(app.Flags, cli.BoolFlag{
|
||||
Name: "H, halt",
|
||||
Usage: "halt the machine",
|
||||
Destination: &haltFlag,
|
||||
})
|
||||
}
|
||||
// -P, --poweroff
|
||||
// Power-off the machine (the default for shutdown cmd).
|
||||
if app.Name == "poweroff" {
|
||||
app.Flags = append(app.Flags, cli.BoolTFlag{
|
||||
Name: "P, poweroff",
|
||||
Usage: "poweroff the machine",
|
||||
Destination: &poweroffFlag,
|
||||
})
|
||||
} else {
|
||||
// shutdown -h
|
||||
// Equivalent to --poweroff
|
||||
if app.Name == "shutdown" {
|
||||
app.Flags = append(app.Flags, cli.BoolFlag{
|
||||
Name: "h",
|
||||
Usage: "poweroff the machine",
|
||||
Destination: &poweroffFlag,
|
||||
})
|
||||
}
|
||||
app.Flags = append(app.Flags, cli.BoolFlag{
|
||||
Name: "P, poweroff",
|
||||
Usage: "poweroff the machine",
|
||||
Destination: &poweroffFlag,
|
||||
})
|
||||
}
|
||||
// -r, --reboot
|
||||
// Reboot the machine.
|
||||
if app.Name == "reboot" {
|
||||
app.Flags = append(app.Flags, cli.BoolTFlag{
|
||||
Name: "r, reboot",
|
||||
Usage: "reboot after shutdown",
|
||||
Destination: &rebootFlag,
|
||||
})
|
||||
// OR? maybe implement it as a `kexec` cli tool?
|
||||
app.Flags = append(app.Flags, cli.BoolFlag{
|
||||
Name: "kexec",
|
||||
Usage: "kexec the default RancherOS cfg",
|
||||
Destination: &kexecFlag,
|
||||
})
|
||||
app.Flags = append(app.Flags, cli.BoolFlag{
|
||||
Name: "kexec-previous",
|
||||
Usage: "kexec the previous RancherOS cfg",
|
||||
Destination: &previouskexecFlag,
|
||||
})
|
||||
app.Flags = append(app.Flags, cli.StringFlag{
|
||||
Name: "kexec-append",
|
||||
Usage: "kexec using the specified kernel boot params (ignores global.cfg)",
|
||||
Destination: &kexecAppendFlag,
|
||||
})
|
||||
} else {
|
||||
app.Flags = append(app.Flags, cli.BoolFlag{
|
||||
Name: "r, reboot",
|
||||
Usage: "reboot after shutdown",
|
||||
Destination: &rebootFlag,
|
||||
})
|
||||
}
|
||||
//TODO: add the time and msg flags...
|
||||
app.HideHelp = true
|
||||
|
||||
app.Run(os.Args)
|
||||
}
|
||||
|
||||
func shutdown(c *cli.Context) error {
|
||||
common("")
|
||||
reboot := c.String("r")
|
||||
poweroff := c.String("h")
|
||||
|
||||
if reboot == "now" {
|
||||
Reboot()
|
||||
} else if poweroff == "now" {
|
||||
PowerOff()
|
||||
func Kexec(previous bool, bootDir, append string) error {
|
||||
cfg := "linux-current.cfg"
|
||||
if previous {
|
||||
cfg = "linux-previous.cfg"
|
||||
}
|
||||
cfgFile := filepath.Join(bootDir, cfg)
|
||||
vmlinuzFile, initrdFile, err := install.ReadSyslinuxCfg(cfgFile)
|
||||
if err != nil {
|
||||
log.Errorf("%s", err)
|
||||
return err
|
||||
}
|
||||
globalCfgFile := filepath.Join(bootDir, "global.cfg")
|
||||
if append == "" {
|
||||
append, err = install.ReadGlobalCfg(globalCfgFile)
|
||||
if err != nil {
|
||||
log.Errorf("%s", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
// TODO: read global.cfg if append == ""
|
||||
// kexec -l ${DIST}/vmlinuz --initrd=${DIST}/initrd --append="${kernelArgs} ${APPEND}" -f
|
||||
cmd := exec.Command(
|
||||
"kexec",
|
||||
"-l", vmlinuzFile,
|
||||
"--initrd", initrdFile,
|
||||
"--append", append,
|
||||
"-f")
|
||||
log.Debugf("Run(%#v)", cmd)
|
||||
cmd.Stderr = os.Stderr
|
||||
if _, err := cmd.Output(); err != nil {
|
||||
log.Errorf("Failed to kexec: %s", err)
|
||||
return err
|
||||
}
|
||||
log.Infof("kexec'd to new install")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Reboot is used by installation / upgrade
|
||||
// TODO: add kexec option
|
||||
func Reboot() {
|
||||
os.Args = []string{"reboot"}
|
||||
reboot("reboot", false, syscall.LINUX_REBOOT_CMD_RESTART)
|
||||
}
|
||||
|
||||
func shutdown(c *cli.Context) error {
|
||||
// the shutdown command's default is poweroff
|
||||
var powerCmd uint
|
||||
powerCmd = syscall.LINUX_REBOOT_CMD_POWER_OFF
|
||||
if rebootFlag {
|
||||
powerCmd = syscall.LINUX_REBOOT_CMD_RESTART
|
||||
} else if poweroffFlag {
|
||||
powerCmd = syscall.LINUX_REBOOT_CMD_POWER_OFF
|
||||
} else if haltFlag {
|
||||
powerCmd = syscall.LINUX_REBOOT_CMD_HALT
|
||||
}
|
||||
|
||||
timeArg := c.Args().Get(0)
|
||||
// We may be called via an absolute path, so check that now and make sure we
|
||||
// don't pass the wrong app name down. Aside from the logic in the immediate
|
||||
// context here, the container name is derived from how we were called and
|
||||
// cannot contain slashes.
|
||||
appName := filepath.Base(c.App.Name)
|
||||
if appName == "shutdown" && timeArg != "" {
|
||||
if timeArg != "now" {
|
||||
err := fmt.Errorf("Sorry, can't parse '%s' as time value (only 'now' supported)", timeArg)
|
||||
log.Error(err)
|
||||
return err
|
||||
}
|
||||
// TODO: if there are more params, LOG them
|
||||
}
|
||||
|
||||
reboot(appName, forceFlag, powerCmd)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package respawn
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
@@ -12,21 +13,28 @@ import (
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
log "github.com/Sirupsen/logrus"
|
||||
"github.com/codegangsta/cli"
|
||||
"github.com/rancher/os/config"
|
||||
"github.com/rancher/os/log"
|
||||
)
|
||||
|
||||
var (
|
||||
running bool = true
|
||||
processes map[int]*os.Process = map[int]*os.Process{}
|
||||
processLock = sync.Mutex{}
|
||||
running = true
|
||||
processes = map[int]*os.Process{}
|
||||
processLock = sync.Mutex{}
|
||||
)
|
||||
|
||||
func Main() {
|
||||
log.InitLogger()
|
||||
runtime.GOMAXPROCS(1)
|
||||
runtime.LockOSThread()
|
||||
app := cli.NewApp()
|
||||
|
||||
app.Name = os.Args[0]
|
||||
app.Usage = fmt.Sprintf("%s RancherOS\nbuilt: %s", app.Name, config.BuildDate)
|
||||
app.Version = config.Version
|
||||
app.Author = "Rancher Labs, Inc."
|
||||
|
||||
app.Flags = []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "file, f",
|
||||
@@ -35,6 +43,9 @@ func Main() {
|
||||
}
|
||||
app.Action = run
|
||||
|
||||
log.Infof("%s, %s", app.Usage, app.Version)
|
||||
fmt.Printf("%s, %s", app.Usage, app.Version)
|
||||
|
||||
app.Run(os.Args)
|
||||
}
|
||||
|
||||
@@ -68,17 +79,21 @@ func run(c *cli.Context) error {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
lines := strings.Split(string(input), "\n")
|
||||
doneChannel := make(chan string, len(lines))
|
||||
|
||||
for _, line := range strings.Split(string(input), "\n") {
|
||||
for _, line := range lines {
|
||||
if strings.TrimSpace(line) == "" || strings.Index(strings.TrimSpace(line), "#") == 0 {
|
||||
continue
|
||||
}
|
||||
wg.Add(1)
|
||||
go execute(line, &wg)
|
||||
go execute(line, doneChannel)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
for i := 0; i < len(lines); i++ {
|
||||
line := <-doneChannel
|
||||
log.Infof("FINISHED: %s", line)
|
||||
fmt.Printf("FINISHED: %s", line)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -100,19 +115,20 @@ func termPids() {
|
||||
defer processLock.Unlock()
|
||||
|
||||
for _, process := range processes {
|
||||
log.Infof("sending SIGTERM to %d", process.Pid)
|
||||
process.Signal(syscall.SIGTERM)
|
||||
}
|
||||
}
|
||||
|
||||
func execute(line string, wg *sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
func execute(line string, doneChannel chan string) {
|
||||
defer func() { doneChannel <- line }()
|
||||
|
||||
start := time.Now()
|
||||
count := 0
|
||||
|
||||
for {
|
||||
args := strings.Split(line, " ")
|
||||
args := strings.Split(line, " ")
|
||||
|
||||
for {
|
||||
cmd := exec.Command(args[0], args[1:]...)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
@@ -143,7 +159,7 @@ func execute(line string, wg *sync.WaitGroup) {
|
||||
count++
|
||||
|
||||
if count > 10 {
|
||||
if start.Sub(time.Now()) <= (1 * time.Second) {
|
||||
if time.Now().Sub(start) <= (1 * time.Second) {
|
||||
log.Errorf("%s : restarted too fast, not executing", line)
|
||||
break
|
||||
}
|
||||
|
||||
@@ -1,11 +1,20 @@
|
||||
package sysinit
|
||||
|
||||
import (
|
||||
log "github.com/Sirupsen/logrus"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
|
||||
initPkg "github.com/rancher/os/init"
|
||||
"github.com/rancher/os/log"
|
||||
)
|
||||
|
||||
func Main() {
|
||||
log.InitLogger()
|
||||
|
||||
resolve, err := ioutil.ReadFile("/etc/resolv.conf")
|
||||
log.Infof("Resolv.conf == [%s], %v", resolve, err)
|
||||
log.Infof("Exec %v", os.Args)
|
||||
|
||||
if err := initPkg.SysInit(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
package systemdocker
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/docker/docker/docker"
|
||||
"github.com/rancher/os/config"
|
||||
)
|
||||
|
||||
func Main() {
|
||||
if os.Geteuid() != 0 {
|
||||
log.Fatalf("%s: Need to be root", os.Args[0])
|
||||
}
|
||||
|
||||
if os.Getenv("DOCKER_HOST") == "" {
|
||||
os.Setenv("DOCKER_HOST", config.DOCKER_SYSTEM_HOST)
|
||||
}
|
||||
|
||||
docker.Main()
|
||||
}
|
||||
@@ -3,17 +3,18 @@ package wait
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/rancher/os/config"
|
||||
"github.com/rancher/os/docker"
|
||||
"github.com/rancher/os/log"
|
||||
)
|
||||
|
||||
func Main() {
|
||||
_, err := docker.NewClient(config.DOCKER_HOST)
|
||||
log.InitLogger()
|
||||
_, err := docker.NewClient(config.DockerHost)
|
||||
if err != nil {
|
||||
logrus.Errorf("Failed to conect to Docker")
|
||||
log.Errorf("Failed to connect to Docker")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
logrus.Infof("Docker is ready")
|
||||
log.Infof("Docker is ready")
|
||||
}
|
||||
|
||||
@@ -5,17 +5,19 @@ import (
|
||||
|
||||
"golang.org/x/net/context"
|
||||
|
||||
log "github.com/Sirupsen/logrus"
|
||||
yaml "github.com/cloudfoundry-incubator/candiedyaml"
|
||||
dockerClient "github.com/docker/engine-api/client"
|
||||
"github.com/docker/libcompose/cli/logger"
|
||||
composeConfig "github.com/docker/libcompose/config"
|
||||
"github.com/docker/libcompose/docker"
|
||||
composeClient "github.com/docker/libcompose/docker/client"
|
||||
|
||||
"github.com/docker/libcompose/project"
|
||||
"github.com/docker/libcompose/project/events"
|
||||
"github.com/docker/libcompose/project/options"
|
||||
"github.com/rancher/os/config"
|
||||
rosDocker "github.com/rancher/os/docker"
|
||||
"github.com/rancher/os/log"
|
||||
"github.com/rancher/os/util"
|
||||
"github.com/rancher/os/util/network"
|
||||
)
|
||||
@@ -201,7 +203,7 @@ func newCoreServiceProject(cfg *config.CloudConfig, useNetwork, loadConsole bool
|
||||
|
||||
go func() {
|
||||
for event := range projectEvents {
|
||||
if event.EventType == events.ContainerStarted && event.ServiceName == "ntp" {
|
||||
if event.EventType == events.ContainerStarted && event.ServiceName == "network" {
|
||||
useNetwork = true
|
||||
}
|
||||
}
|
||||
@@ -245,13 +247,47 @@ func StageServices(cfg *config.CloudConfig, services ...string) error {
|
||||
}
|
||||
|
||||
// Reduce service configurations to just image and labels
|
||||
needToPull := false
|
||||
var client, userClient, systemClient dockerClient.APIClient
|
||||
for _, serviceName := range p.ServiceConfigs.Keys() {
|
||||
serviceConfig, _ := p.ServiceConfigs.Get(serviceName)
|
||||
|
||||
// test to see if we need to Pull
|
||||
if serviceConfig.Labels[config.ScopeLabel] != config.System {
|
||||
if userClient == nil {
|
||||
userClient, err = rosDocker.NewDefaultClient()
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
|
||||
}
|
||||
client = userClient
|
||||
} else {
|
||||
if systemClient == nil {
|
||||
systemClient, err = rosDocker.NewSystemClient()
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
client = systemClient
|
||||
}
|
||||
}
|
||||
if client != nil {
|
||||
_, _, err := client.ImageInspectWithRaw(context.Background(), serviceConfig.Image, false)
|
||||
if err == nil {
|
||||
log.Infof("Service %s using local image %s", serviceName, serviceConfig.Image)
|
||||
continue
|
||||
}
|
||||
}
|
||||
needToPull = true
|
||||
|
||||
p.ServiceConfigs.Add(serviceName, &composeConfig.ServiceConfig{
|
||||
Image: serviceConfig.Image,
|
||||
Labels: serviceConfig.Labels,
|
||||
})
|
||||
}
|
||||
|
||||
return p.Pull(context.Background())
|
||||
if needToPull {
|
||||
return p.Pull(context.Background())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -3,35 +3,42 @@ package compose
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
log "github.com/Sirupsen/logrus"
|
||||
yaml "github.com/cloudfoundry-incubator/candiedyaml"
|
||||
composeConfig "github.com/docker/libcompose/config"
|
||||
"github.com/docker/libcompose/project"
|
||||
"github.com/rancher/os/config"
|
||||
"github.com/rancher/os/docker"
|
||||
"github.com/rancher/os/log"
|
||||
"github.com/rancher/os/util/network"
|
||||
)
|
||||
|
||||
func LoadService(p *project.Project, cfg *config.CloudConfig, useNetwork bool, service string) error {
|
||||
bytes, err := network.LoadServiceResource(service, useNetwork, cfg)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return err
|
||||
}
|
||||
|
||||
m := map[interface{}]interface{}{}
|
||||
if err = yaml.Unmarshal(bytes, &m); err != nil {
|
||||
return fmt.Errorf("Failed to parse YAML configuration for %s: %v", service, err)
|
||||
e := fmt.Errorf("Failed to parse YAML configuration for %s: %v", service, err)
|
||||
log.Error(e)
|
||||
return e
|
||||
}
|
||||
|
||||
m = adjustContainerNames(m)
|
||||
|
||||
bytes, err = yaml.Marshal(m)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to marshal YAML configuration for %s: %v", service, err)
|
||||
e := fmt.Errorf("Failed to marshal YAML configuration for %s: %v", service, err)
|
||||
log.Error(e)
|
||||
return e
|
||||
}
|
||||
|
||||
if err = p.Load(bytes); err != nil {
|
||||
return fmt.Errorf("Failed to load %s: %v", service, err)
|
||||
e := fmt.Errorf("Failed to load %s: %v", service, err)
|
||||
log.Error(e)
|
||||
return e
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -85,7 +92,7 @@ func projectReload(p *project.Project, useNetwork *bool, loadConsole bool, envir
|
||||
|
||||
if err := LoadService(p, cfg, *useNetwork, service); err != nil {
|
||||
if err != network.ErrNoNetwork {
|
||||
log.Error(err)
|
||||
log.Errorf("Failed to load service(%s): %v", service, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
@@ -99,12 +106,12 @@ func projectReload(p *project.Project, useNetwork *bool, loadConsole bool, envir
|
||||
|
||||
if loadConsole {
|
||||
if err := loadConsoleService(cfg, p); err != nil {
|
||||
log.Errorf("Failed to load console: %v", err)
|
||||
log.Errorf("Failed to load rancher.console=(%s): %v", cfg.Rancher.Console, err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := loadEngineService(cfg, p); err != nil {
|
||||
log.Errorf("Failed to load engine: %v", err)
|
||||
log.Errorf("Failed to load rancher.docker.engine=(%s): %v", cfg.Rancher.Docker.Engine, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
38
config/cloudinit/Documentation/cloud-config-deprecated.md
Normal file
38
config/cloudinit/Documentation/cloud-config-deprecated.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# Deprecated Cloud-Config Features
|
||||
|
||||
## Retrieving SSH Authorized Keys
|
||||
|
||||
### From a GitHub User
|
||||
|
||||
Using the `coreos-ssh-import-github` field, we can import public SSH keys from a GitHub user to use as authorized keys to a server.
|
||||
|
||||
```yaml
|
||||
#cloud-config
|
||||
|
||||
users:
|
||||
- name: elroy
|
||||
coreos-ssh-import-github: elroy
|
||||
```
|
||||
|
||||
### From an HTTP Endpoint
|
||||
|
||||
We can also pull public SSH keys from any HTTP endpoint which matches [GitHub's API response format](https://developer.github.com/v3/users/keys/#list-public-keys-for-a-user).
|
||||
For example, if you have an installation of GitHub Enterprise, you can provide a complete URL with an authentication token:
|
||||
|
||||
```yaml
|
||||
#cloud-config
|
||||
|
||||
users:
|
||||
- name: elroy
|
||||
coreos-ssh-import-url: https://github-enterprise.example.com/api/v3/users/elroy/keys?access_token=<TOKEN>
|
||||
```
|
||||
|
||||
You can also specify any URL whose response matches the JSON format for public keys:
|
||||
|
||||
```yaml
|
||||
#cloud-config
|
||||
|
||||
users:
|
||||
- name: elroy
|
||||
coreos-ssh-import-url: https://example.com/public-keys
|
||||
```
|
||||
26
config/cloudinit/Documentation/cloud-config-locations.md
Normal file
26
config/cloudinit/Documentation/cloud-config-locations.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# Cloud-Config Locations
|
||||
|
||||
On every boot, coreos-cloudinit looks for a config file to configure your host. Here is a list of locations which are used by the Cloud-Config utility, depending on your CoreOS platform:
|
||||
|
||||
| Location | Description |
|
||||
| --- | --- |
|
||||
| `/media/configvirtfs/openstack/latest/user_data` | `/media/configvirtfs` mount point with [config-2](/os/docs/latest/config-drive.html#contents-and-format) label. It should contain a `openstack/latest/user_data` relative path. Usually used by cloud providers or in VM installations. |
|
||||
| `/media/configdrive/openstack/latest/user_data` | FAT or ISO9660 filesystem with [config-2](/os/docs/latest/config-drive.html#qemu-virtfs) label and `/media/configdrive/` mount point. It should also contain a `openstack/latest/user_data` relative path. Usually used in installations which are configured by USB Flash sticks or CDROM media. |
|
||||
| Kernel command line: `cloud-config-url=http://example.com/user_data`. | You can find this string using this command `cat /proc/cmdline`. Usually used in [PXE](/os/docs/latest/booting-with-pxe.html) or [iPXE](/os/docs/latest/booting-with-ipxe.html) boots. |
|
||||
| `/var/lib/coreos-install/user_data` | When you install CoreOS manually using the [coreos-install](/os/docs/latest/installing-to-disk.html) tool. Usually used in bare metal installations. |
|
||||
| `/usr/share/oem/cloud-config.yml` | Path for OEM images. |
|
||||
| `/var/lib/coreos-vagrant/vagrantfile-user-data`| Vagrant OEM scripts automatically store Cloud-Config into this path. |
|
||||
| `/var/lib/waagent/CustomData`| Azure platform uses OEM path for first Cloud-Config initialization and then `/var/lib/waagent/CustomData` to apply your settings. |
|
||||
| `http://169.254.169.254/metadata/v1/user-data` `http://169.254.169.254/2009-04-04/user-data` `https://metadata.packet.net/userdata`|DigitalOcean, EC2 and Packet cloud providers correspondingly use these URLs to download Cloud-Config.|
|
||||
| `/usr/share/oem/bin/vmtoolsd --cmd "info-get guestinfo.coreos.config.data"` | Cloud-Config provided by [VMware Guestinfo][VMware Guestinfo] |
|
||||
| `/usr/share/oem/bin/vmtoolsd --cmd "info-get guestinfo.coreos.config.url"` | Cloud-Config URL provided by [VMware Guestinfo][VMware Guestinfo] |
|
||||
|
||||
[VMware Guestinfo]: vmware-guestinfo.md
|
||||
|
||||
You can also run the `coreos-cloudinit` tool manually and provide a path to your custom Cloud-Config file:
|
||||
|
||||
```sh
|
||||
sudo coreos-cloudinit --from-file=/home/core/cloud-config.yaml
|
||||
```
|
||||
|
||||
This command will apply your custom cloud-config.
|
||||
37
config/cloudinit/Documentation/cloud-config-oem.md
Normal file
37
config/cloudinit/Documentation/cloud-config-oem.md
Normal file
@@ -0,0 +1,37 @@
|
||||
## OEM configuration
|
||||
|
||||
The `coreos.oem.*` parameters follow the [os-release spec][os-release], but have been repurposed as a way for coreos-cloudinit to know about the OEM partition on this machine. Customizing this section is only needed when generating a new OEM of CoreOS from the SDK. The fields include:
|
||||
|
||||
- **id**: Lowercase string identifying the OEM
|
||||
- **name**: Human-friendly string representing the OEM
|
||||
- **version-id**: Lowercase string identifying the version of the OEM
|
||||
- **home-url**: Link to the homepage of the provider or OEM
|
||||
- **bug-report-url**: Link to a place to file bug reports about this OEM
|
||||
|
||||
coreos-cloudinit renders these fields to `/etc/oem-release`.
|
||||
If no **id** field is provided, coreos-cloudinit will ignore this section.
|
||||
|
||||
For example, the following cloud-config document...
|
||||
|
||||
```yaml
|
||||
#cloud-config
|
||||
coreos:
|
||||
oem:
|
||||
id: "rackspace"
|
||||
name: "Rackspace Cloud Servers"
|
||||
version-id: "168.0.0"
|
||||
home-url: "https://www.rackspace.com/cloud/servers/"
|
||||
bug-report-url: "https://github.com/coreos/coreos-overlay"
|
||||
```
|
||||
|
||||
...would be rendered to the following `/etc/oem-release`:
|
||||
|
||||
```yaml
|
||||
ID=rackspace
|
||||
NAME="Rackspace Cloud Servers"
|
||||
VERSION_ID=168.0.0
|
||||
HOME_URL="https://www.rackspace.com/cloud/servers/"
|
||||
BUG_REPORT_URL="https://github.com/coreos/coreos-overlay"
|
||||
```
|
||||
|
||||
[os-release]: http://www.freedesktop.org/software/systemd/man/os-release.html
|
||||
485
config/cloudinit/Documentation/cloud-config.md
Normal file
485
config/cloudinit/Documentation/cloud-config.md
Normal file
@@ -0,0 +1,485 @@
|
||||
# Using Cloud-Config
|
||||
|
||||
CoreOS allows you to declaratively customize various OS-level items, such as network configuration, user accounts, and systemd units. This document describes the full list of items we can configure. The `coreos-cloudinit` program uses these files as it configures the OS after startup or during runtime.
|
||||
|
||||
Your cloud-config is processed during each boot. Invalid cloud-config won't be processed but will be logged in the journal. You can validate your cloud-config with the [CoreOS online validator](https://coreos.com/validate/) or by running `coreos-cloudinit -validate`. In addition to these two validation methods you can debug `coreos-cloudinit` system output through the `journalctl` tool:
|
||||
|
||||
```sh
|
||||
journalctl --identifier=coreos-cloudinit
|
||||
```
|
||||
|
||||
It will show `coreos-cloudinit` run output which was triggered by system boot.
|
||||
|
||||
## Configuration File
|
||||
|
||||
The file used by this system initialization program is called a "cloud-config" file. It is inspired by the [cloud-init][cloud-init] project's [cloud-config][cloud-config] file, which is "the defacto multi-distribution package that handles early initialization of a cloud instance" ([cloud-init docs][cloud-init-docs]). Because the cloud-init project includes tools which aren't used by CoreOS, only the relevant subset of its configuration items will be implemented in our cloud-config file. In addition to those, we added a few CoreOS-specific items, such as etcd configuration, OEM definition, and systemd units.
|
||||
|
||||
We've designed our implementation to allow the same cloud-config file to work across all of our supported platforms.
|
||||
|
||||
[cloud-init]: https://launchpad.net/cloud-init
|
||||
[cloud-init-docs]: http://cloudinit.readthedocs.org/en/latest/index.html
|
||||
[cloud-config]: http://cloudinit.readthedocs.org/en/latest/topics/format.html#cloud-config-data
|
||||
|
||||
### File Format
|
||||
|
||||
The cloud-config file uses the [YAML][yaml] file format, which uses whitespace and new-lines to delimit lists, associative arrays, and values.
|
||||
|
||||
A cloud-config file must contain a header: either `#cloud-config` for processing as cloud-config (suggested) or `#!` for processing as a shell script (advanced). If cloud-config has the `#cloud-config` header, it should followed by an associative array which has zero or more of the following keys:
|
||||
|
||||
- `coreos`
|
||||
- `ssh_authorized_keys`
|
||||
- `hostname`
|
||||
- `users`
|
||||
- `write_files`
|
||||
- `manage_etc_hosts`
|
||||
|
||||
The expected values for these keys are defined in the rest of this document.
|
||||
|
||||
If cloud-config header starts on `#!` then coreos-cloudinit will recognize it as shell script which is interpreted by bash and run it as transient systemd service.
|
||||
|
||||
[yaml]: https://en.wikipedia.org/wiki/YAML
|
||||
|
||||
### Providing Cloud-Config with Config-Drive
|
||||
|
||||
CoreOS tries to conform to each platform's native method to provide user data. Each cloud provider tends to be unique, but this complexity has been abstracted by CoreOS. You can view each platform's instructions on their documentation pages. The most universal way to provide cloud-config is [via config-drive](https://github.com/rancher/os/config/cloudinit/blob/master/Documentation/config-drive.md), which attaches a read-only device to the machine, that contains your cloud-config file.
|
||||
|
||||
## Configuration Parameters
|
||||
|
||||
### coreos
|
||||
|
||||
#### etcd (deprecated. see etcd2)
|
||||
|
||||
The `coreos.etcd.*` parameters will be translated to a partial systemd unit acting as an etcd configuration file.
|
||||
If the platform environment supports the templating feature of coreos-cloudinit it is possible to automate etcd configuration with the `$private_ipv4` and `$public_ipv4` fields. For example, the following cloud-config document...
|
||||
|
||||
```yaml
|
||||
#cloud-config
|
||||
|
||||
coreos:
|
||||
etcd:
|
||||
name: "node001"
|
||||
# generate a new token for each unique cluster from https://discovery.etcd.io/new
|
||||
discovery: "https://discovery.etcd.io/<token>"
|
||||
# multi-region and multi-cloud deployments need to use $public_ipv4
|
||||
addr: "$public_ipv4:4001"
|
||||
peer-addr: "$private_ipv4:7001"
|
||||
```
|
||||
|
||||
...will generate a systemd unit drop-in for etcd.service with the following contents:
|
||||
|
||||
```yaml
|
||||
[Service]
|
||||
Environment="ETCD_NAME=node001"
|
||||
Environment="ETCD_DISCOVERY=https://discovery.etcd.io/<token>"
|
||||
Environment="ETCD_ADDR=203.0.113.29:4001"
|
||||
Environment="ETCD_PEER_ADDR=192.0.2.13:7001"
|
||||
```
|
||||
|
||||
For more information about the available configuration parameters, see the [etcd documentation][etcd-config].
|
||||
|
||||
_Note: The `$private_ipv4` and `$public_ipv4` substitution variables referenced in other documents are only supported on Amazon EC2, Google Compute Engine, OpenStack, Rackspace, DigitalOcean, and Vagrant._
|
||||
|
||||
[etcd-config]: https://github.com/coreos/etcd/blob/release-0.4/Documentation/configuration.md
|
||||
|
||||
#### etcd2
|
||||
|
||||
The `coreos.etcd2.*` parameters will be translated to a partial systemd unit acting as an etcd configuration file.
|
||||
If the platform environment supports the templating feature of coreos-cloudinit it is possible to automate etcd configuration with the `$private_ipv4` and `$public_ipv4` fields. When generating a [discovery token](https://discovery.etcd.io/new?size=3), set the `size` parameter, since etcd uses this to determine if all members have joined the cluster. After the cluster is bootstrapped, it can grow or shrink from this configured size.
|
||||
|
||||
For example, the following cloud-config document...
|
||||
|
||||
```yaml
|
||||
#cloud-config
|
||||
|
||||
coreos:
|
||||
etcd2:
|
||||
# generate a new token for each unique cluster from https://discovery.etcd.io/new?size=3
|
||||
discovery: "https://discovery.etcd.io/<token>"
|
||||
# multi-region and multi-cloud deployments need to use $public_ipv4
|
||||
advertise-client-urls: "http://$public_ipv4:2379"
|
||||
initial-advertise-peer-urls: "http://$private_ipv4:2380"
|
||||
# listen on both the official ports and the legacy ports
|
||||
# legacy ports can be omitted if your application doesn't depend on them
|
||||
listen-client-urls: "http://0.0.0.0:2379,http://0.0.0.0:4001"
|
||||
listen-peer-urls: "http://$private_ipv4:2380,http://$private_ipv4:7001"
|
||||
```
|
||||
|
||||
...will generate a systemd unit drop-in for etcd2.service with the following contents:
|
||||
|
||||
```yaml
|
||||
[Service]
|
||||
Environment="ETCD_DISCOVERY=https://discovery.etcd.io/<token>"
|
||||
Environment="ETCD_ADVERTISE_CLIENT_URLS=http://203.0.113.29:2379"
|
||||
Environment="ETCD_INITIAL_ADVERTISE_PEER_URLS=http://192.0.2.13:2380"
|
||||
Environment="ETCD_LISTEN_CLIENT_URLS=http://0.0.0.0:2379,http://0.0.0.0:4001"
|
||||
Environment="ETCD_LISTEN_PEER_URLS=http://192.0.2.13:2380,http://192.0.2.13:7001"
|
||||
```
|
||||
|
||||
For more information about the available configuration parameters, see the [etcd2 documentation][etcd2-config].
|
||||
|
||||
_Note: The `$private_ipv4` and `$public_ipv4` substitution variables referenced in other documents are only supported on Amazon EC2, Google Compute Engine, OpenStack, Rackspace, DigitalOcean, and Vagrant._
|
||||
|
||||
[etcd2-config]: https://github.com/coreos/etcd/blob/v2.3.2/Documentation/configuration.md
|
||||
|
||||
#### fleet
|
||||
|
||||
The `coreos.fleet.*` parameters work very similarly to `coreos.etcd2.*`, and allow for the configuration of fleet through environment variables. For example, the following cloud-config document...
|
||||
|
||||
```yaml
|
||||
#cloud-config
|
||||
|
||||
coreos:
|
||||
fleet:
|
||||
public-ip: "$public_ipv4"
|
||||
metadata: "region=us-west"
|
||||
```
|
||||
|
||||
...will generate a systemd unit drop-in like this:
|
||||
|
||||
```yaml
|
||||
[Service]
|
||||
Environment="FLEET_PUBLIC_IP=203.0.113.29"
|
||||
Environment="FLEET_METADATA=region=us-west"
|
||||
```
|
||||
|
||||
List of fleet configuration parameters:
|
||||
|
||||
- **agent_ttl**: An Agent will be considered dead if it exceeds this amount of time to communicate with the Registry
|
||||
- **engine_reconcile_interval**: Interval in seconds at which the engine should reconcile the cluster schedule in etcd
|
||||
- **etcd_cafile**: Path to CA file used for TLS communication with etcd
|
||||
- **etcd_certfile**: Provide TLS configuration when SSL certificate authentication is enabled in etcd endpoints
|
||||
- **etcd_keyfile**: Path to private key file used for TLS communication with etcd
|
||||
- **etcd_key_prefix**: etcd prefix path to be used for fleet keys
|
||||
- **etcd_request_timeout**: Amount of time in seconds to allow a single etcd request before considering it failed
|
||||
- **etcd_servers**: Comma separated list of etcd endpoints
|
||||
- **etcd_username**: Username for Basic Authentication to etcd endpoints
|
||||
- **etcd_password**: Password for Basic Authentication to etcd endpoints
|
||||
- **metadata**: Comma separated key/value pairs that are published with the local to the fleet registry
|
||||
- **public_ip**: IP accessible by other nodes for inter-host communication
|
||||
- **verbosity**: Enable debug logging by setting this to an integer value greater than zero
|
||||
|
||||
For more information on fleet configuration, see the [fleet documentation][fleet-config].
|
||||
|
||||
[fleet-config]: https://github.com/coreos/fleet/blob/master/Documentation/deployment-and-configuration.md#configuration
|
||||
|
||||
#### flannel
|
||||
|
||||
The `coreos.flannel.*` parameters also work very similarly to `coreos.etcd2.*`
|
||||
and `coreos.fleet.*`. They can be used to set environment variables for
|
||||
flanneld. For example, the following cloud-config...
|
||||
|
||||
```yaml
|
||||
#cloud-config
|
||||
|
||||
coreos:
|
||||
flannel:
|
||||
etcd_prefix: "/coreos.com/network2"
|
||||
```
|
||||
|
||||
...will generate a systemd unit drop-in like so:
|
||||
|
||||
```
|
||||
[Service]
|
||||
Environment="FLANNELD_ETCD_PREFIX=/coreos.com/network2"
|
||||
```
|
||||
|
||||
List of flannel configuration parameters:
|
||||
|
||||
- **etcd_endpoints**: Comma separated list of etcd endpoints
|
||||
- **etcd_cafile**: Path to CA file used for TLS communication with etcd
|
||||
- **etcd_certfile**: Path to certificate file used for TLS communication with etcd
|
||||
- **etcd_keyfile**: Path to private key file used for TLS communication with etcd
|
||||
- **etcd_prefix**: etcd prefix path to be used for flannel keys
|
||||
- **etcd_username**: Username for Basic Authentication to etcd endpoints
|
||||
- **etcd_password**: Password for Basic Authentication to etcd endpoints
|
||||
- **ip_masq**: Install IP masquerade rules for traffic outside of flannel subnet
|
||||
- **subnet_file**: Path to flannel subnet file to write out
|
||||
- **interface**: Interface (name or IP) that should be used for inter-host communication
|
||||
- **public_ip**: IP accessible by other nodes for inter-host communication
|
||||
|
||||
For more information on flannel configuration, see the [flannel documentation][flannel-readme].
|
||||
|
||||
[flannel-readme]: https://github.com/coreos/flannel/blob/master/README.md
|
||||
|
||||
#### locksmith
|
||||
|
||||
The `coreos.locksmith.*` parameters can be used to set environment variables
|
||||
for locksmith. For example, the following cloud-config...
|
||||
|
||||
```yaml
|
||||
#cloud-config
|
||||
|
||||
coreos:
|
||||
locksmith:
|
||||
endpoint: "http://example.com:2379"
|
||||
```
|
||||
|
||||
...will generate a systemd unit drop-in like so:
|
||||
|
||||
```
|
||||
[Service]
|
||||
Environment="LOCKSMITHD_ENDPOINT=http://example.com:2379"
|
||||
```
|
||||
|
||||
List of locksmith configuration parameters:
|
||||
|
||||
- **endpoint**: Comma separated list of etcd endpoints
|
||||
- **etcd_cafile**: Path to CA file used for TLS communication with etcd
|
||||
- **etcd_certfile**: Path to certificate file used for TLS communication with etcd
|
||||
- **etcd_keyfile**: Path to private key file used for TLS communication with etcd
|
||||
- **group**: Name of the reboot group in which this instance belongs
|
||||
- **window_start**: Start time of the reboot window
|
||||
- **window_length**: Duration of the reboot window
|
||||
- **etcd_username**: Username for Basic Authentication to etcd endpoints
|
||||
- **etcd_password**: Password for Basic Authentication to etcd endpoints
|
||||
|
||||
For the complete list of locksmith configuration parameters, see the [locksmith documentation][locksmith-readme].
|
||||
|
||||
[locksmith-readme]: https://github.com/coreos/locksmith/blob/master/README.md
|
||||
|
||||
#### update
|
||||
|
||||
The `coreos.update.*` parameters manipulate settings related to how CoreOS instances are updated.
|
||||
|
||||
These fields will be written out to and replace `/etc/coreos/update.conf`. If only one of the parameters is given it will only overwrite the given field.
|
||||
The `reboot-strategy` parameter also affects the behaviour of [locksmith](https://github.com/coreos/locksmith).
|
||||
|
||||
- **reboot-strategy**: One of "reboot", "etcd-lock", "best-effort" or "off" for controlling when reboots are issued after an update is performed.
|
||||
- _reboot_: Reboot immediately after an update is applied.
|
||||
- _etcd-lock_: Reboot after first taking a distributed lock in etcd, this guarantees that only one host will reboot concurrently and that the cluster will remain available during the update.
|
||||
- _best-effort_ - If etcd is running, "etcd-lock", otherwise simply "reboot".
|
||||
- _off_ - Disable rebooting after updates are applied (not recommended).
|
||||
- **server**: The location of the [CoreUpdate][coreupdate] server which will be queried for updates. Also known as the [omaha][omaha-docs] server endpoint.
|
||||
- **group**: signifies the channel which should be used for automatic updates. This value defaults to the version of the image initially downloaded. (one of "master", "alpha", "beta", "stable")
|
||||
|
||||
[coreupdate]: https://coreos.com/products/coreupdate
|
||||
[omaha-docs]: https://coreos.com/docs/coreupdate/custom-apps/coreupdate-protocol/
|
||||
|
||||
*Note: cloudinit will only manipulate the locksmith unit file in the systemd runtime directory (`/run/systemd/system/locksmithd.service`). If any manual modifications are made to an overriding unit configuration file (e.g. `/etc/systemd/system/locksmithd.service`), cloudinit will no longer be able to control the locksmith service unit.*
|
||||
|
||||
##### Example
|
||||
|
||||
```yaml
|
||||
#cloud-config
|
||||
coreos:
|
||||
update:
|
||||
reboot-strategy: "etcd-lock"
|
||||
```
|
||||
|
||||
#### units
|
||||
|
||||
The `coreos.units.*` parameters define a list of arbitrary systemd units to start after booting. This feature is intended to help you start essential services required to mount storage and configure networking in order to join the CoreOS cluster. It is not intended to be a Chef/Puppet replacement.
|
||||
|
||||
Each item is an object with the following fields:
|
||||
|
||||
- **name**: String representing unit's name. Required.
|
||||
- **runtime**: Boolean indicating whether or not to persist the unit across reboots. This is analogous to the `--runtime` argument to `systemctl enable`. The default value is false.
|
||||
- **enable**: Boolean indicating whether or not to handle the [Install] section of the unit file. This is similar to running `systemctl enable <name>`. The default value is false.
|
||||
- **content**: Plaintext string representing entire unit file. If no value is provided, the unit is assumed to exist already.
|
||||
- **command**: Command to execute on unit: start, stop, reload, restart, try-restart, reload-or-restart, reload-or-try-restart. The default behavior is to not execute any commands.
|
||||
- **mask**: Whether to mask the unit file by symlinking it to `/dev/null` (analogous to `systemctl mask <name>`). Note that unlike `systemctl mask`, **this will destructively remove any existing unit file** located at `/etc/systemd/system/<unit>`, to ensure that the mask succeeds. The default value is false.
|
||||
- **drop-ins**: A list of unit drop-ins with the following fields:
|
||||
- **name**: String representing unit's name. Required.
|
||||
- **content**: Plaintext string representing entire file. Required.
|
||||
|
||||
|
||||
**NOTE:** The command field is ignored for all network, netdev, and link units. The systemd-networkd.service unit will be restarted in their place.
|
||||
|
||||
##### Examples
|
||||
|
||||
Write a unit to disk, automatically starting it.
|
||||
|
||||
```yaml
|
||||
#cloud-config
|
||||
|
||||
coreos:
|
||||
units:
|
||||
- name: "docker-redis.service"
|
||||
command: "start"
|
||||
content: |
|
||||
[Unit]
|
||||
Description=Redis container
|
||||
Author=Me
|
||||
After=docker.service
|
||||
|
||||
[Service]
|
||||
Restart=always
|
||||
ExecStart=/usr/bin/docker start -a redis_server
|
||||
ExecStop=/usr/bin/docker stop -t 2 redis_server
|
||||
```
|
||||
|
||||
Add the DOCKER_OPTS environment variable to docker.service.
|
||||
|
||||
```yaml
|
||||
#cloud-config
|
||||
|
||||
coreos:
|
||||
units:
|
||||
- name: "docker.service"
|
||||
drop-ins:
|
||||
- name: "50-insecure-registry.conf"
|
||||
content: |
|
||||
[Service]
|
||||
Environment=DOCKER_OPTS='--insecure-registry="10.0.1.0/24"'
|
||||
```
|
||||
|
||||
Start the built-in `etcd2` and `fleet` services:
|
||||
|
||||
```yaml
|
||||
#cloud-config
|
||||
|
||||
coreos:
|
||||
units:
|
||||
- name: "etcd2.service"
|
||||
command: "start"
|
||||
- name: "fleet.service"
|
||||
command: "start"
|
||||
```
|
||||
|
||||
### ssh_authorized_keys
|
||||
|
||||
The `ssh_authorized_keys` parameter adds public SSH keys which will be authorized for the `core` user.
|
||||
|
||||
The keys will be named "coreos-cloudinit" by default.
|
||||
Override this by using the `--ssh-key-name` flag when calling `coreos-cloudinit`.
|
||||
|
||||
```yaml
|
||||
#cloud-config
|
||||
|
||||
ssh_authorized_keys:
|
||||
- "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC0g+ZTxC7weoIJLUafOgrm+h..."
|
||||
```
|
||||
|
||||
### hostname
|
||||
|
||||
The `hostname` parameter defines the system's hostname.
|
||||
This is the local part of a fully-qualified domain name (i.e. `foo` in `foo.example.com`).
|
||||
|
||||
```yaml
|
||||
#cloud-config
|
||||
|
||||
hostname: "coreos1"
|
||||
```
|
||||
|
||||
### users
|
||||
|
||||
The `users` parameter adds or modifies the specified list of users. Each user is an object which consists of the following fields. Each field is optional and of type string unless otherwise noted.
|
||||
All but the `passwd` and `ssh-authorized-keys` fields will be ignored if the user already exists.
|
||||
|
||||
- **name**: Required. Login name of user
|
||||
- **gecos**: GECOS comment of user
|
||||
- **passwd**: Hash of the password to use for this user
|
||||
- **homedir**: User's home directory. Defaults to /home/\<name\>
|
||||
- **no-create-home**: Boolean. Skip home directory creation.
|
||||
- **primary-group**: Default group for the user. Defaults to a new group created named after the user.
|
||||
- **groups**: Add user to these additional groups
|
||||
- **no-user-group**: Boolean. Skip default group creation.
|
||||
- **ssh-authorized-keys**: List of public SSH keys to authorize for this user
|
||||
- **coreos-ssh-import-github** [DEPRECATED]: Authorize SSH keys from GitHub user
|
||||
- **coreos-ssh-import-github-users** [DEPRECATED]: Authorize SSH keys from a list of GitHub users
|
||||
- **coreos-ssh-import-url** [DEPRECATED]: Authorize SSH keys imported from a url endpoint.
|
||||
- **system**: Create the user as a system user. No home directory will be created.
|
||||
- **no-log-init**: Boolean. Skip initialization of lastlog and faillog databases.
|
||||
- **shell**: User's login shell.
|
||||
|
||||
The following fields are not yet implemented:
|
||||
|
||||
- **inactive**: Deactivate the user upon creation
|
||||
- **lock-passwd**: Boolean. Disable password login for user
|
||||
- **sudo**: Entry to add to /etc/sudoers for user. By default, no sudo access is authorized.
|
||||
- **selinux-user**: Corresponding SELinux user
|
||||
- **ssh-import-id**: Import SSH keys by ID from Launchpad.
|
||||
|
||||
```yaml
|
||||
#cloud-config
|
||||
|
||||
users:
|
||||
- name: "elroy"
|
||||
passwd: "$6$5s2u6/jR$un0AvWnqilcgaNB3Mkxd5yYv6mTlWfOoCYHZmfi3LDKVltj.E8XNKEcwWm..."
|
||||
groups:
|
||||
- "sudo"
|
||||
- "docker"
|
||||
ssh-authorized-keys:
|
||||
- "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC0g+ZTxC7weoIJLUafOgrm+h..."
|
||||
```
|
||||
|
||||
#### Generating a password hash
|
||||
|
||||
If you choose to use a password instead of an SSH key, generating a safe hash is extremely important to the security of your system. Simplified hashes like md5crypt are trivial to crack on modern GPU hardware. Here are a few ways to generate secure hashes:
|
||||
|
||||
```
|
||||
# On Debian/Ubuntu (via the package "whois")
|
||||
mkpasswd --method=SHA-512 --rounds=4096
|
||||
|
||||
# OpenSSL (note: this will only make md5crypt. While better than plantext it should not be considered fully secure)
|
||||
openssl passwd -1
|
||||
|
||||
# Python (change password and salt values)
|
||||
python -c "import crypt, getpass, pwd; print crypt.crypt('password', '\$6\$SALT\$')"
|
||||
|
||||
# Perl (change password and salt values)
|
||||
perl -e 'print crypt("password","\$6\$SALT\$") . "\n"'
|
||||
```
|
||||
|
||||
Using a higher number of rounds will help create more secure passwords, but given enough time, password hashes can be reversed. On most RPM based distributions there is a tool called mkpasswd available in the `expect` package, but this does not handle "rounds" nor advanced hashing algorithms.
|
||||
|
||||
### write_files
|
||||
|
||||
The `write_files` directive defines a set of files to create on the local filesystem.
|
||||
Each item in the list may have the following keys:
|
||||
|
||||
- **path**: Absolute location on disk where contents should be written
|
||||
- **content**: Data to write at the provided `path`
|
||||
- **permissions**: Integer representing file permissions, typically in octal notation (i.e. 0644)
|
||||
- **owner**: User and group that should own the file written to disk. This is equivalent to the `<user>:<group>` argument to `chown <user>:<group> <path>`.
|
||||
- **encoding**: Optional. The encoding of the data in content. If not specified this defaults to the yaml document encoding (usually utf-8). Supported encoding types are:
|
||||
- **b64, base64**: Base64 encoded content
|
||||
- **gz, gzip**: gzip encoded content, for use with the !!binary tag
|
||||
- **gz+b64, gz+base64, gzip+b64, gzip+base64**: Base64 encoded gzip content
|
||||
|
||||
|
||||
```yaml
|
||||
#cloud-config
|
||||
write_files:
|
||||
- path: "/etc/resolv.conf"
|
||||
permissions: "0644"
|
||||
owner: "root"
|
||||
content: |
|
||||
nameserver 8.8.8.8
|
||||
- path: "/etc/motd"
|
||||
permissions: "0644"
|
||||
owner: "root"
|
||||
content: |
|
||||
Good news, everyone!
|
||||
- path: "/tmp/like_this"
|
||||
permissions: "0644"
|
||||
owner: "root"
|
||||
encoding: "gzip"
|
||||
content: !!binary |
|
||||
H4sIAKgdh1QAAwtITM5WyK1USMqvUCjPLMlQSMssS1VIya9KzVPIySwszS9SyCpNLwYARQFQ5CcAAAA=
|
||||
- path: "/tmp/or_like_this"
|
||||
permissions: "0644"
|
||||
owner: "root"
|
||||
encoding: "gzip+base64"
|
||||
content: |
|
||||
H4sIAKgdh1QAAwtITM5WyK1USMqvUCjPLMlQSMssS1VIya9KzVPIySwszS9SyCpNLwYARQFQ5CcAAAA=
|
||||
- path: "/tmp/todolist"
|
||||
permissions: "0644"
|
||||
owner: "root"
|
||||
encoding: "base64"
|
||||
content: |
|
||||
UGFjayBteSBib3ggd2l0aCBmaXZlIGRvemVuIGxpcXVvciBqdWdz
|
||||
```
|
||||
|
||||
### manage_etc_hosts
|
||||
|
||||
The `manage_etc_hosts` parameter configures the contents of the `/etc/hosts` file, which is used for local name resolution.
|
||||
Currently, the only supported value is "localhost" which will cause your system's hostname
|
||||
to resolve to "127.0.0.1". This is helpful when the host does not have DNS
|
||||
infrastructure in place to resolve its own hostname, for example, when using Vagrant.
|
||||
|
||||
```yaml
|
||||
#cloud-config
|
||||
|
||||
manage_etc_hosts: "localhost"
|
||||
```
|
||||
40
config/cloudinit/Documentation/config-drive.md
Normal file
40
config/cloudinit/Documentation/config-drive.md
Normal file
@@ -0,0 +1,40 @@
|
||||
# Distribution via Config Drive
|
||||
|
||||
CoreOS supports providing configuration data via [config drive][config-drive]
|
||||
disk images. Currently only providing a single script or cloud config file is
|
||||
supported.
|
||||
|
||||
[config-drive]: http://docs.openstack.org/user-guide/cli_config_drive.html
|
||||
|
||||
## Contents and Format
|
||||
|
||||
The image should be a single FAT or ISO9660 file system with the label
|
||||
`config-2` and the configuration data should be located at
|
||||
`openstack/latest/user_data`.
|
||||
|
||||
For example, to wrap up a config named `user_data` in a config drive image:
|
||||
|
||||
```sh
|
||||
mkdir -p /tmp/new-drive/openstack/latest
|
||||
cp user_data /tmp/new-drive/openstack/latest/user_data
|
||||
mkisofs -R -V config-2 -o configdrive.iso /tmp/new-drive
|
||||
rm -r /tmp/new-drive
|
||||
```
|
||||
|
||||
If on OS X, replace the `mkisofs` invocation with:
|
||||
|
||||
```sh
|
||||
hdiutil makehybrid -iso -joliet -default-volume-name config-2 -o configdrive.iso /tmp/new-drive
|
||||
```
|
||||
|
||||
## QEMU virtfs
|
||||
|
||||
One exception to the above, when using QEMU it is possible to skip creating an
|
||||
image and use a plain directory containing the same contents:
|
||||
|
||||
```sh
|
||||
qemu-system-x86_64 \
|
||||
-fsdev local,id=conf,security_model=none,readonly,path=/tmp/new-drive \
|
||||
-device virtio-9p-pci,fsdev=conf,mount_tag=config-2 \
|
||||
[usual qemu options here...]
|
||||
```
|
||||
27
config/cloudinit/Documentation/debian-interfaces.md
Normal file
27
config/cloudinit/Documentation/debian-interfaces.md
Normal file
@@ -0,0 +1,27 @@
|
||||
#Debian Interfaces#
|
||||
**WARNING**: This option is EXPERIMENTAL and may change or be removed at any
|
||||
point.
|
||||
There is basic support for converting from a Debian network configuration to
|
||||
networkd unit files. The -convert-netconf=debian option is used to activate
|
||||
this feature.
|
||||
|
||||
#convert-netconf#
|
||||
Default: ""
|
||||
Read the network config provided in cloud-drive and translate it from the
|
||||
specified format into networkd unit files (requires the -from-configdrive
|
||||
flag). Currently only supports "debian" which provides support for a small
|
||||
subset of the [Debian network configuration]
|
||||
(https://wiki.debian.org/NetworkConfiguration). These options include:
|
||||
|
||||
- interface config methods
|
||||
- static
|
||||
- address/netmask
|
||||
- gateway
|
||||
- hwaddress
|
||||
- dns-nameservers
|
||||
- dhcp
|
||||
- hwaddress
|
||||
- manual
|
||||
- loopback
|
||||
- vlan_raw_device
|
||||
- bond-slaves
|
||||
36
config/cloudinit/Documentation/vmware-guestinfo.md
Normal file
36
config/cloudinit/Documentation/vmware-guestinfo.md
Normal file
@@ -0,0 +1,36 @@
|
||||
# VMWare Guestinfo Interface
|
||||
|
||||
## Cloud-Config VMWare Guestinfo Variables
|
||||
|
||||
coreos-cloudinit accepts configuration from the VMware RPC API's *guestinfo*
|
||||
facility. This datasource can be enabled with the `--from-vmware-guestinfo`
|
||||
flag to coreos-cloudinit.
|
||||
|
||||
The following guestinfo variables are recognized and processed by cloudinit
|
||||
when passed from the hypervisor to the virtual machine at boot time. Note that
|
||||
property names are prefixed with `guestinfo.` in the VMX, e.g., `guestinfo.hostname`.
|
||||
|
||||
| guestinfo variable | type |
|
||||
|:--------------------------------------|:--------------------------------|
|
||||
| `hostname` | `hostname` |
|
||||
| `interface.<n>.name` | `string` |
|
||||
| `interface.<n>.mac` | `MAC address` |
|
||||
| `interface.<n>.dhcp` | `{"yes", "no"}` |
|
||||
| `interface.<n>.role` | `{"public", "private"}` |
|
||||
| `interface.<n>.ip.<m>.address` | `CIDR IP address` |
|
||||
| `interface.<n>.route.<l>.gateway` | `IP address` |
|
||||
| `interface.<n>.route.<l>.destination` | `CIDR IP address` |
|
||||
| `dns.server.<x>` | `IP address` |
|
||||
| `dns.domain.<y>` | `DNS search domain` |
|
||||
| `coreos.config.data` | `string` |
|
||||
| `coreos.config.data.encoding` | `{"", "base64", "gzip+base64"}` |
|
||||
| `coreos.config.url` | `URL` |
|
||||
|
||||
Note: "n", "m", "l", "x" and "y" are 0-indexed, incrementing integers. The
|
||||
identifier for an `interface` does not correspond to anything outside of this
|
||||
configuration; it serves only to distinguish between multiple `interface`s.
|
||||
|
||||
The guide to [booting on VMWare][bootvmware] is the starting point for more
|
||||
information about configuring and running CoreOS on VMWare.
|
||||
|
||||
[bootvmware]: https://github.com/coreos/docs/blob/master/os/booting-on-vmware.md
|
||||
@@ -1,3 +1,7 @@
|
||||
**NOTE**: This project has been superseded by [Ignition][ignition] and is no longer under active development. Please direct all development efforts to Ignition.
|
||||
|
||||
[ignition]: https://github.com/coreos/ignition
|
||||
|
||||
# coreos-cloudinit [](https://travis-ci.org/coreos/coreos-cloudinit)
|
||||
|
||||
coreos-cloudinit enables a user to customize CoreOS machines by providing either a cloud-config document or an executable script through user-data.
|
||||
@@ -9,8 +13,8 @@ Additionally, several [CoreOS-specific options][custom-cloud-config] have been i
|
||||
All supported cloud-config parameters are [documented here][all-cloud-config].
|
||||
|
||||
[official-cloud-config]: http://cloudinit.readthedocs.org/en/latest/topics/format.html#cloud-config-data
|
||||
[custom-cloud-config]: https://github.com/coreos/coreos-cloudinit/blob/master/Documentation/cloud-config.md#coreos-parameters
|
||||
[all-cloud-config]: https://github.com/coreos/coreos-cloudinit/tree/master/Documentation/cloud-config.md
|
||||
[custom-cloud-config]: https://github.com/rancher/os/config/cloudinit/blob/master/Documentation/cloud-config.md#coreos-parameters
|
||||
[all-cloud-config]: https://github.com/rancher/os/config/cloudinit/tree/master/Documentation/cloud-config.md
|
||||
|
||||
The following is an example cloud-config document:
|
||||
|
||||
@@ -4,7 +4,7 @@ NAME="coreos-cloudinit"
|
||||
ORG_PATH="github.com/coreos"
|
||||
REPO_PATH="${ORG_PATH}/${NAME}"
|
||||
VERSION=$(git describe --dirty --tags)
|
||||
GLDFLAGS="-X main.version \"${VERSION}\""
|
||||
GLDFLAGS="-X main.version=\"${VERSION}\""
|
||||
|
||||
if [ ! -h gopath/src/${REPO_PATH} ]; then
|
||||
mkdir -p gopath/src/${ORG_PATH}
|
||||
547
config/cloudinit/config/config_test.go
Normal file
547
config/cloudinit/config/config_test.go
Normal file
@@ -0,0 +1,547 @@
|
||||
// Copyright 2015 CoreOS, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewCloudConfig(t *testing.T) {
|
||||
tests := []struct {
|
||||
contents string
|
||||
|
||||
config CloudConfig
|
||||
}{
|
||||
{},
|
||||
{
|
||||
contents: "#cloud-config\nwrite_files:\n - path: underscore",
|
||||
config: CloudConfig{WriteFiles: []File{{Path: "underscore"}}},
|
||||
},
|
||||
{
|
||||
contents: "#cloud-config\nwrite-files:\n - path: hyphen",
|
||||
config: CloudConfig{WriteFiles: []File{{Path: "hyphen"}}},
|
||||
},
|
||||
{
|
||||
contents: "#cloud-config\ncoreos:\n update:\n reboot-strategy: off",
|
||||
config: CloudConfig{CoreOS: CoreOS{Update: Update{RebootStrategy: "off"}}},
|
||||
},
|
||||
{
|
||||
contents: "#cloud-config\ncoreos:\n update:\n reboot-strategy: false",
|
||||
config: CloudConfig{CoreOS: CoreOS{Update: Update{RebootStrategy: "false"}}},
|
||||
},
|
||||
{
|
||||
contents: "#cloud-config\nwrite_files:\n - permissions: 0744",
|
||||
config: CloudConfig{WriteFiles: []File{{RawFilePermissions: "0744"}}},
|
||||
},
|
||||
{
|
||||
contents: "#cloud-config\nwrite_files:\n - permissions: 744",
|
||||
config: CloudConfig{WriteFiles: []File{{RawFilePermissions: "744"}}},
|
||||
},
|
||||
{
|
||||
contents: "#cloud-config\nwrite_files:\n - permissions: '0744'",
|
||||
config: CloudConfig{WriteFiles: []File{{RawFilePermissions: "0744"}}},
|
||||
},
|
||||
{
|
||||
contents: "#cloud-config\nwrite_files:\n - permissions: '744'",
|
||||
config: CloudConfig{WriteFiles: []File{{RawFilePermissions: "744"}}},
|
||||
},
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
config, err := NewCloudConfig(tt.contents)
|
||||
if err != nil {
|
||||
t.Errorf("bad error (test case #%d): want %v, got %s", i, nil, err)
|
||||
}
|
||||
if !reflect.DeepEqual(&tt.config, config) {
|
||||
t.Errorf("bad config (test case #%d): want %#v, got %#v", i, tt.config, config)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewCloudConfigDecode(t *testing.T) {
|
||||
// //all of these decode to "bar"
|
||||
contentTests := map[string]string{
|
||||
"base64": "YmFy",
|
||||
"b64": "YmFy",
|
||||
// theoretically gz+gzip are supported but they break yaml
|
||||
// "gz": "\x1f\x8b\x08\x08w\x14\x87T\x02\xffok\x00KJ,\x02\x00\xaa\x8c\xffv\x03\x00\x00\x00",
|
||||
// "gzip": "\x1f\x8b\x08\x08w\x14\x87T\x02\xffok\x00KJ,\x02\x00\xaa\x8c\xffv\x03\x00\x00\x00",
|
||||
"gz+base64": "H4sIABMVh1QAA0tKLAIAqoz/dgMAAAA=",
|
||||
"gzip+base64": "H4sIABMVh1QAA0tKLAIAqoz/dgMAAAA=",
|
||||
"gz+b64": "H4sIABMVh1QAA0tKLAIAqoz/dgMAAAA=",
|
||||
"gzip+b64": "H4sIABMVh1QAA0tKLAIAqoz/dgMAAAA=",
|
||||
}
|
||||
|
||||
type testCase struct {
|
||||
contents string
|
||||
config CloudConfig
|
||||
}
|
||||
|
||||
var decodingTests []testCase
|
||||
for name, content := range contentTests {
|
||||
decodingTests = append(decodingTests, testCase{
|
||||
contents: fmt.Sprintf("#cloud-config\nwrite_files:\n - encoding: %q\n content: |\n %s", name, content),
|
||||
config: CloudConfig{WriteFiles: []File{{Content: "bar"}}},
|
||||
})
|
||||
}
|
||||
|
||||
for i, tt := range decodingTests {
|
||||
config, err := NewCloudConfig(tt.contents)
|
||||
if err != nil {
|
||||
t.Errorf("bad error (test case #%d): want %v, got %s", i, nil, err)
|
||||
}
|
||||
|
||||
if err := config.Decode(); err != nil {
|
||||
t.Errorf("bad error (test case #%d): want %v, got %s", i, nil, err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(&tt.config, config) {
|
||||
t.Errorf("bad config (test case #%d): want %#v, got %#v", i, tt.config, config)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestIsZero(t *testing.T) {
|
||||
tests := []struct {
|
||||
c interface{}
|
||||
|
||||
empty bool
|
||||
}{
|
||||
{struct{}{}, true},
|
||||
{struct{ a, b string }{}, true},
|
||||
{struct{ A, b string }{}, true},
|
||||
{struct{ A, B string }{}, true},
|
||||
{struct{ A string }{A: "hello"}, false},
|
||||
{struct{ A int }{}, true},
|
||||
{struct{ A int }{A: 1}, false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
if empty := IsZero(tt.c); tt.empty != empty {
|
||||
t.Errorf("bad result (%q): want %t, got %t", tt.c, tt.empty, empty)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAssertStructValid(t *testing.T) {
|
||||
tests := []struct {
|
||||
c interface{}
|
||||
|
||||
err error
|
||||
}{
|
||||
{struct{}{}, nil},
|
||||
{struct {
|
||||
A, b string `valid:"^1|2$"`
|
||||
}{}, nil},
|
||||
{struct {
|
||||
A, b string `valid:"^1|2$"`
|
||||
}{A: "1", b: "2"}, nil},
|
||||
{struct {
|
||||
A, b string `valid:"^1|2$"`
|
||||
}{A: "1", b: "hello"}, nil},
|
||||
{struct {
|
||||
A, b string `valid:"^1|2$"`
|
||||
}{A: "hello", b: "2"}, &ErrorValid{Value: "hello", Field: "A", Valid: "^1|2$"}},
|
||||
{struct {
|
||||
A, b int `valid:"^1|2$"`
|
||||
}{}, nil},
|
||||
{struct {
|
||||
A, b int `valid:"^1|2$"`
|
||||
}{A: 1, b: 2}, nil},
|
||||
{struct {
|
||||
A, b int `valid:"^1|2$"`
|
||||
}{A: 1, b: 9}, nil},
|
||||
{struct {
|
||||
A, b int `valid:"^1|2$"`
|
||||
}{A: 9, b: 2}, &ErrorValid{Value: "9", Field: "A", Valid: "^1|2$"}},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
if err := AssertStructValid(tt.c); !reflect.DeepEqual(tt.err, err) {
|
||||
t.Errorf("bad result (%q): want %q, got %q", tt.c, tt.err, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigCompile(t *testing.T) {
|
||||
tests := []interface{}{
|
||||
Etcd{},
|
||||
File{},
|
||||
Flannel{},
|
||||
Fleet{},
|
||||
Locksmith{},
|
||||
OEM{},
|
||||
Unit{},
|
||||
Update{},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
ttt := reflect.TypeOf(tt)
|
||||
for i := 0; i < ttt.NumField(); i++ {
|
||||
ft := ttt.Field(i)
|
||||
if !isFieldExported(ft) {
|
||||
continue
|
||||
}
|
||||
|
||||
if _, err := regexp.Compile(ft.Tag.Get("valid")); err != nil {
|
||||
t.Errorf("bad regexp(%s.%s): want %v, got %s", ttt.Name(), ft.Name, nil, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCloudConfigUnknownKeys(t *testing.T) {
|
||||
contents := `
|
||||
coreos:
|
||||
etcd:
|
||||
discovery: "https://discovery.etcd.io/827c73219eeb2fa5530027c37bf18877"
|
||||
coreos_unknown:
|
||||
foo: "bar"
|
||||
section_unknown:
|
||||
dunno:
|
||||
something
|
||||
bare_unknown:
|
||||
bar
|
||||
write_files:
|
||||
- content: fun
|
||||
path: /var/party
|
||||
file_unknown: nofun
|
||||
users:
|
||||
- name: fry
|
||||
passwd: somehash
|
||||
user_unknown: philip
|
||||
hostname:
|
||||
foo
|
||||
`
|
||||
cfg, err := NewCloudConfig(contents)
|
||||
if err != nil {
|
||||
t.Fatalf("error instantiating CloudConfig with unknown keys: %v", err)
|
||||
}
|
||||
if cfg.Hostname != "foo" {
|
||||
t.Fatalf("hostname not correctly set when invalid keys are present")
|
||||
}
|
||||
if cfg.CoreOS.Etcd.Discovery != "https://discovery.etcd.io/827c73219eeb2fa5530027c37bf18877" {
|
||||
t.Fatalf("etcd section not correctly set when invalid keys are present")
|
||||
}
|
||||
if len(cfg.WriteFiles) < 1 || cfg.WriteFiles[0].Content != "fun" || cfg.WriteFiles[0].Path != "/var/party" {
|
||||
t.Fatalf("write_files section not correctly set when invalid keys are present")
|
||||
}
|
||||
if len(cfg.Users) < 1 || cfg.Users[0].Name != "fry" || cfg.Users[0].PasswordHash != "somehash" {
|
||||
t.Fatalf("users section not correctly set when invalid keys are present")
|
||||
}
|
||||
}
|
||||
|
||||
// Assert that the parsing of a cloud config file "generally works"
|
||||
func TestCloudConfigEmpty(t *testing.T) {
|
||||
cfg, err := NewCloudConfig("")
|
||||
if err != nil {
|
||||
t.Fatalf("Encountered unexpected error :%v", err)
|
||||
}
|
||||
|
||||
keys := cfg.SSHAuthorizedKeys
|
||||
if len(keys) != 0 {
|
||||
t.Error("Parsed incorrect number of SSH keys")
|
||||
}
|
||||
|
||||
if len(cfg.WriteFiles) != 0 {
|
||||
t.Error("Expected zero WriteFiles")
|
||||
}
|
||||
|
||||
if cfg.Hostname != "" {
|
||||
t.Errorf("Expected hostname to be empty, got '%s'", cfg.Hostname)
|
||||
}
|
||||
}
|
||||
|
||||
// Assert that the parsing of a cloud config file "generally works"
|
||||
func TestCloudConfig(t *testing.T) {
|
||||
contents := `
|
||||
coreos:
|
||||
etcd:
|
||||
discovery: "https://discovery.etcd.io/827c73219eeb2fa5530027c37bf18877"
|
||||
update:
|
||||
reboot_strategy: reboot
|
||||
units:
|
||||
- name: 50-eth0.network
|
||||
runtime: yes
|
||||
content: '[Match]
|
||||
|
||||
Name=eth47
|
||||
|
||||
|
||||
[Network]
|
||||
|
||||
Address=10.209.171.177/19
|
||||
|
||||
'
|
||||
oem:
|
||||
id: rackspace
|
||||
name: Rackspace Cloud Servers
|
||||
version_id: 168.0.0
|
||||
home_url: https://www.rackspace.com/cloud/servers/
|
||||
bug_report_url: https://github.com/coreos/coreos-overlay
|
||||
ssh_authorized_keys:
|
||||
- foobar
|
||||
- foobaz
|
||||
write_files:
|
||||
- content: |
|
||||
penny
|
||||
elroy
|
||||
path: /etc/dogepack.conf
|
||||
permissions: '0644'
|
||||
owner: root:dogepack
|
||||
hostname: trontastic
|
||||
`
|
||||
cfg, err := NewCloudConfig(contents)
|
||||
if err != nil {
|
||||
t.Fatalf("Encountered unexpected error :%v", err)
|
||||
}
|
||||
|
||||
keys := cfg.SSHAuthorizedKeys
|
||||
if len(keys) != 2 {
|
||||
t.Error("Parsed incorrect number of SSH keys")
|
||||
} else if keys[0] != "foobar" {
|
||||
t.Error("Expected first SSH key to be 'foobar'")
|
||||
} else if keys[1] != "foobaz" {
|
||||
t.Error("Expected first SSH key to be 'foobaz'")
|
||||
}
|
||||
|
||||
if len(cfg.WriteFiles) != 1 {
|
||||
t.Error("Failed to parse correct number of write_files")
|
||||
} else {
|
||||
wf := cfg.WriteFiles[0]
|
||||
if wf.Content != "penny\nelroy\n" {
|
||||
t.Errorf("WriteFile has incorrect contents '%s'", wf.Content)
|
||||
}
|
||||
if wf.Encoding != "" {
|
||||
t.Errorf("WriteFile has incorrect encoding %s", wf.Encoding)
|
||||
}
|
||||
if wf.RawFilePermissions != "0644" {
|
||||
t.Errorf("WriteFile has incorrect permissions %s", wf.RawFilePermissions)
|
||||
}
|
||||
if wf.Path != "/etc/dogepack.conf" {
|
||||
t.Errorf("WriteFile has incorrect path %s", wf.Path)
|
||||
}
|
||||
if wf.Owner != "root:dogepack" {
|
||||
t.Errorf("WriteFile has incorrect owner %s", wf.Owner)
|
||||
}
|
||||
}
|
||||
|
||||
if len(cfg.CoreOS.Units) != 1 {
|
||||
t.Error("Failed to parse correct number of units")
|
||||
} else {
|
||||
u := cfg.CoreOS.Units[0]
|
||||
expect := `[Match]
|
||||
Name=eth47
|
||||
|
||||
[Network]
|
||||
Address=10.209.171.177/19
|
||||
`
|
||||
if u.Content != expect {
|
||||
t.Errorf("Unit has incorrect contents '%s'.\nExpected '%s'.", u.Content, expect)
|
||||
}
|
||||
if u.Runtime != true {
|
||||
t.Errorf("Unit has incorrect runtime value")
|
||||
}
|
||||
if u.Name != "50-eth0.network" {
|
||||
t.Errorf("Unit has incorrect name %s", u.Name)
|
||||
}
|
||||
}
|
||||
|
||||
if cfg.CoreOS.OEM.ID != "rackspace" {
|
||||
t.Errorf("Failed parsing coreos.oem. Expected ID 'rackspace', got %q.", cfg.CoreOS.OEM.ID)
|
||||
}
|
||||
|
||||
if cfg.Hostname != "trontastic" {
|
||||
t.Errorf("Failed to parse hostname")
|
||||
}
|
||||
if cfg.CoreOS.Update.RebootStrategy != "reboot" {
|
||||
t.Errorf("Failed to parse locksmith strategy")
|
||||
}
|
||||
}
|
||||
|
||||
// Assert that our interface conversion doesn't panic
|
||||
func TestCloudConfigKeysNotList(t *testing.T) {
|
||||
contents := `
|
||||
ssh_authorized_keys:
|
||||
- foo: bar
|
||||
`
|
||||
cfg, err := NewCloudConfig(contents)
|
||||
if err != nil {
|
||||
t.Fatalf("Encountered unexpected error: %v", err)
|
||||
}
|
||||
|
||||
keys := cfg.SSHAuthorizedKeys
|
||||
if len(keys) != 0 {
|
||||
t.Error("Parsed incorrect number of SSH keys")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCloudConfigSerializationHeader(t *testing.T) {
|
||||
cfg, _ := NewCloudConfig("")
|
||||
contents := cfg.String()
|
||||
header := strings.SplitN(contents, "\n", 2)[0]
|
||||
if header != "#cloud-config" {
|
||||
t.Fatalf("Serialized config did not have expected header")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCloudConfigUsers(t *testing.T) {
|
||||
contents := `
|
||||
users:
|
||||
- name: elroy
|
||||
passwd: somehash
|
||||
ssh_authorized_keys:
|
||||
- somekey
|
||||
gecos: arbitrary comment
|
||||
homedir: /home/place
|
||||
no_create_home: yes
|
||||
primary_group: things
|
||||
groups:
|
||||
- ping
|
||||
- pong
|
||||
no_user_group: true
|
||||
system: y
|
||||
no_log_init: True
|
||||
shell: /bin/sh
|
||||
`
|
||||
cfg, err := NewCloudConfig(contents)
|
||||
if err != nil {
|
||||
t.Fatalf("Encountered unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if len(cfg.Users) != 1 {
|
||||
t.Fatalf("Parsed %d users, expected 1", len(cfg.Users))
|
||||
}
|
||||
|
||||
user := cfg.Users[0]
|
||||
|
||||
if user.Name != "elroy" {
|
||||
t.Errorf("User name is %q, expected 'elroy'", user.Name)
|
||||
}
|
||||
|
||||
if user.PasswordHash != "somehash" {
|
||||
t.Errorf("User passwd is %q, expected 'somehash'", user.PasswordHash)
|
||||
}
|
||||
|
||||
if keys := user.SSHAuthorizedKeys; len(keys) != 1 {
|
||||
t.Errorf("Parsed %d ssh keys, expected 1", len(keys))
|
||||
} else {
|
||||
key := user.SSHAuthorizedKeys[0]
|
||||
if key != "somekey" {
|
||||
t.Errorf("User SSH key is %q, expected 'somekey'", key)
|
||||
}
|
||||
}
|
||||
|
||||
if user.GECOS != "arbitrary comment" {
|
||||
t.Errorf("Failed to parse gecos field, got %q", user.GECOS)
|
||||
}
|
||||
|
||||
if user.Homedir != "/home/place" {
|
||||
t.Errorf("Failed to parse homedir field, got %q", user.Homedir)
|
||||
}
|
||||
|
||||
if !user.NoCreateHome {
|
||||
t.Errorf("Failed to parse no_create_home field")
|
||||
}
|
||||
|
||||
if user.PrimaryGroup != "things" {
|
||||
t.Errorf("Failed to parse primary_group field, got %q", user.PrimaryGroup)
|
||||
}
|
||||
|
||||
if len(user.Groups) != 2 {
|
||||
t.Errorf("Failed to parse 2 goups, got %d", len(user.Groups))
|
||||
} else {
|
||||
if user.Groups[0] != "ping" {
|
||||
t.Errorf("First group was %q, not expected value 'ping'", user.Groups[0])
|
||||
}
|
||||
if user.Groups[1] != "pong" {
|
||||
t.Errorf("First group was %q, not expected value 'pong'", user.Groups[1])
|
||||
}
|
||||
}
|
||||
|
||||
if !user.NoUserGroup {
|
||||
t.Errorf("Failed to parse no_user_group field")
|
||||
}
|
||||
|
||||
if !user.System {
|
||||
t.Errorf("Failed to parse system field")
|
||||
}
|
||||
|
||||
if !user.NoLogInit {
|
||||
t.Errorf("Failed to parse no_log_init field")
|
||||
}
|
||||
|
||||
if user.Shell != "/bin/sh" {
|
||||
t.Errorf("Failed to parse shell field, got %q", user.Shell)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCloudConfigUsersGithubUser(t *testing.T) {
|
||||
|
||||
contents := `
|
||||
users:
|
||||
- name: elroy
|
||||
coreos_ssh_import_github: bcwaldon
|
||||
`
|
||||
cfg, err := NewCloudConfig(contents)
|
||||
if err != nil {
|
||||
t.Fatalf("Encountered unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if len(cfg.Users) != 1 {
|
||||
t.Fatalf("Parsed %d users, expected 1", len(cfg.Users))
|
||||
}
|
||||
|
||||
user := cfg.Users[0]
|
||||
|
||||
if user.Name != "elroy" {
|
||||
t.Errorf("User name is %q, expected 'elroy'", user.Name)
|
||||
}
|
||||
|
||||
if user.SSHImportGithubUser != "bcwaldon" {
|
||||
t.Errorf("github user is %q, expected 'bcwaldon'", user.SSHImportGithubUser)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCloudConfigUsersSSHImportURL(t *testing.T) {
|
||||
contents := `
|
||||
users:
|
||||
- name: elroy
|
||||
coreos_ssh_import_url: https://token:x-auth-token@github.enterprise.com/api/v3/polvi/keys
|
||||
`
|
||||
cfg, err := NewCloudConfig(contents)
|
||||
if err != nil {
|
||||
t.Fatalf("Encountered unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if len(cfg.Users) != 1 {
|
||||
t.Fatalf("Parsed %d users, expected 1", len(cfg.Users))
|
||||
}
|
||||
|
||||
user := cfg.Users[0]
|
||||
|
||||
if user.Name != "elroy" {
|
||||
t.Errorf("User name is %q, expected 'elroy'", user.Name)
|
||||
}
|
||||
|
||||
if user.SSHImportURL != "https://token:x-auth-token@github.enterprise.com/api/v3/polvi/keys" {
|
||||
t.Errorf("ssh import url is %q, expected 'https://token:x-auth-token@github.enterprise.com/api/v3/polvi/keys'", user.SSHImportURL)
|
||||
}
|
||||
}
|
||||
69
config/cloudinit/config/file_test.go
Normal file
69
config/cloudinit/config/file_test.go
Normal file
@@ -0,0 +1,69 @@
|
||||
// Copyright 2015 CoreOS, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestEncodingValid(t *testing.T) {
|
||||
tests := []struct {
|
||||
value string
|
||||
|
||||
isValid bool
|
||||
}{
|
||||
{value: "base64", isValid: true},
|
||||
{value: "b64", isValid: true},
|
||||
{value: "gz", isValid: true},
|
||||
{value: "gzip", isValid: true},
|
||||
{value: "gz+base64", isValid: true},
|
||||
{value: "gzip+base64", isValid: true},
|
||||
{value: "gz+b64", isValid: true},
|
||||
{value: "gzip+b64", isValid: true},
|
||||
{value: "gzzzzbase64", isValid: false},
|
||||
{value: "gzipppbase64", isValid: false},
|
||||
{value: "unknown", isValid: false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
isValid := (nil == AssertStructValid(File{Encoding: tt.value}))
|
||||
if tt.isValid != isValid {
|
||||
t.Errorf("bad assert (%s): want %t, got %t", tt.value, tt.isValid, isValid)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRawFilePermissionsValid(t *testing.T) {
|
||||
tests := []struct {
|
||||
value string
|
||||
|
||||
isValid bool
|
||||
}{
|
||||
{value: "744", isValid: true},
|
||||
{value: "0744", isValid: true},
|
||||
{value: "1744", isValid: true},
|
||||
{value: "01744", isValid: true},
|
||||
{value: "11744", isValid: false},
|
||||
{value: "rwxr--r--", isValid: false},
|
||||
{value: "800", isValid: false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
isValid := (nil == AssertStructValid(File{RawFilePermissions: tt.value}))
|
||||
if tt.isValid != isValid {
|
||||
t.Errorf("bad assert (%s): want %t, got %t", tt.value, tt.isValid, isValid)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,8 @@ type Flannel struct {
|
||||
EtcdCertFile string `yaml:"etcd_certfile" env:"FLANNELD_ETCD_CERTFILE"`
|
||||
EtcdKeyFile string `yaml:"etcd_keyfile" env:"FLANNELD_ETCD_KEYFILE"`
|
||||
EtcdPrefix string `yaml:"etcd_prefix" env:"FLANNELD_ETCD_PREFIX"`
|
||||
EtcdUsername string `yaml:"etcd_username" env:"FLANNELD_ETCD_USERNAME"`
|
||||
EtcdPassword string `yaml:"etcd_password" env:"FLANNELD_ETCD_PASSWORD"`
|
||||
IPMasq string `yaml:"ip_masq" env:"FLANNELD_IP_MASQ"`
|
||||
SubnetFile string `yaml:"subnet_file" env:"FLANNELD_SUBNET_FILE"`
|
||||
Iface string `yaml:"interface" env:"FLANNELD_IFACE"`
|
||||
@@ -25,6 +25,8 @@ type Fleet struct {
|
||||
EtcdKeyPrefix string `yaml:"etcd_key_prefix" env:"FLEET_ETCD_KEY_PREFIX"`
|
||||
EtcdRequestTimeout float64 `yaml:"etcd_request_timeout" env:"FLEET_ETCD_REQUEST_TIMEOUT"`
|
||||
EtcdServers string `yaml:"etcd_servers" env:"FLEET_ETCD_SERVERS"`
|
||||
EtcdUsername string `yaml:"etcd_username" env:"FLEET_ETCD_USERNAME"`
|
||||
EtcdPassword string `yaml:"etcd_password" env:"FLEET_ETCD_PASSWORD"`
|
||||
Metadata string `yaml:"metadata" env:"FLEET_METADATA"`
|
||||
PublicIP string `yaml:"public_ip" env:"FLEET_PUBLIC_IP"`
|
||||
TokenLimit int `yaml:"token_limit" env:"FLEET_TOKEN_LIMIT"`
|
||||
@@ -19,6 +19,8 @@ type Locksmith struct {
|
||||
EtcdCAFile string `yaml:"etcd_cafile" env:"LOCKSMITHD_ETCD_CAFILE"`
|
||||
EtcdCertFile string `yaml:"etcd_certfile" env:"LOCKSMITHD_ETCD_CERTFILE"`
|
||||
EtcdKeyFile string `yaml:"etcd_keyfile" env:"LOCKSMITHD_ETCD_KEYFILE"`
|
||||
EtcdUsername string `yaml:"etcd_username" env:"LOCKSMITHD_ETCD_USERNAME"`
|
||||
EtcdPassword string `yaml:"etcd_password" env:"LOCKSMITHD_ETCD_PASSWORD"`
|
||||
Group string `yaml:"group" env:"LOCKSMITHD_GROUP"`
|
||||
RebootWindowStart string `yaml:"window_start" env:"REBOOT_WINDOW_START" valid:"^((?i:sun|mon|tue|wed|thu|fri|sat|sun) )?0*([0-9]|1[0-9]|2[0-3]):0*([0-9]|[1-5][0-9])$"`
|
||||
RebootWindowLength string `yaml:"window_length" env:"REBOOT_WINDOW_LENGTH" valid:"^[-+]?([0-9]*(\\.[0-9]*)?[a-z]+)+$"`
|
||||
76
config/cloudinit/config/locksmith_test.go
Normal file
76
config/cloudinit/config/locksmith_test.go
Normal file
@@ -0,0 +1,76 @@
|
||||
// Copyright 2015 CoreOS, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRebootWindowStart(t *testing.T) {
|
||||
tests := []struct {
|
||||
value string
|
||||
|
||||
isValid bool
|
||||
}{
|
||||
{value: "Sun 0:0", isValid: true},
|
||||
{value: "Sun 00:00", isValid: true},
|
||||
{value: "sUn 23:59", isValid: true},
|
||||
{value: "mon 0:0", isValid: true},
|
||||
{value: "tue 0:0", isValid: true},
|
||||
{value: "tues 0:0", isValid: false},
|
||||
{value: "wed 0:0", isValid: true},
|
||||
{value: "thu 0:0", isValid: true},
|
||||
{value: "thur 0:0", isValid: false},
|
||||
{value: "fri 0:0", isValid: true},
|
||||
{value: "sat 0:0", isValid: true},
|
||||
{value: "sat00:00", isValid: false},
|
||||
{value: "00:00", isValid: true},
|
||||
{value: "10:10", isValid: true},
|
||||
{value: "20:20", isValid: true},
|
||||
{value: "20:30", isValid: true},
|
||||
{value: "20:40", isValid: true},
|
||||
{value: "20:50", isValid: true},
|
||||
{value: "20:60", isValid: false},
|
||||
{value: "24:00", isValid: false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
isValid := (nil == AssertStructValid(Locksmith{RebootWindowStart: tt.value}))
|
||||
if tt.isValid != isValid {
|
||||
t.Errorf("bad assert (%s): want %t, got %t", tt.value, tt.isValid, isValid)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRebootWindowLength(t *testing.T) {
|
||||
tests := []struct {
|
||||
value string
|
||||
|
||||
isValid bool
|
||||
}{
|
||||
{value: "1h", isValid: true},
|
||||
{value: "1d", isValid: true},
|
||||
{value: "0d", isValid: true},
|
||||
{value: "0.5h", isValid: true},
|
||||
{value: "0.5.0h", isValid: false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
isValid := (nil == AssertStructValid(Locksmith{RebootWindowLength: tt.value}))
|
||||
if tt.isValid != isValid {
|
||||
t.Errorf("bad assert (%s): want %t, got %t", tt.value, tt.isValid, isValid)
|
||||
}
|
||||
}
|
||||
}
|
||||
46
config/cloudinit/config/unit_test.go
Normal file
46
config/cloudinit/config/unit_test.go
Normal file
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
Copyright 2014 CoreOS, Inc.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCommandValid(t *testing.T) {
|
||||
tests := []struct {
|
||||
value string
|
||||
|
||||
isValid bool
|
||||
}{
|
||||
{value: "start", isValid: true},
|
||||
{value: "stop", isValid: true},
|
||||
{value: "restart", isValid: true},
|
||||
{value: "reload", isValid: true},
|
||||
{value: "try-restart", isValid: true},
|
||||
{value: "reload-or-restart", isValid: true},
|
||||
{value: "reload-or-try-restart", isValid: true},
|
||||
{value: "tryrestart", isValid: false},
|
||||
{value: "unknown", isValid: false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
isValid := (nil == AssertStructValid(Unit{Command: tt.value}))
|
||||
if tt.isValid != isValid {
|
||||
t.Errorf("bad assert (%s): want %t, got %t", tt.value, tt.isValid, isValid)
|
||||
}
|
||||
}
|
||||
}
|
||||
43
config/cloudinit/config/update_test.go
Normal file
43
config/cloudinit/config/update_test.go
Normal file
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
Copyright 2014 CoreOS, Inc.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRebootStrategyValid(t *testing.T) {
|
||||
tests := []struct {
|
||||
value string
|
||||
|
||||
isValid bool
|
||||
}{
|
||||
{value: "best-effort", isValid: true},
|
||||
{value: "etcd-lock", isValid: true},
|
||||
{value: "reboot", isValid: true},
|
||||
{value: "off", isValid: true},
|
||||
{value: "besteffort", isValid: false},
|
||||
{value: "unknown", isValid: false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
isValid := (nil == AssertStructValid(Update{RebootStrategy: tt.value}))
|
||||
if tt.isValid != isValid {
|
||||
t.Errorf("bad assert (%s): want %t, got %t", tt.value, tt.isValid, isValid)
|
||||
}
|
||||
}
|
||||
}
|
||||
52
config/cloudinit/config/validate/context.go
Normal file
52
config/cloudinit/config/validate/context.go
Normal file
@@ -0,0 +1,52 @@
|
||||
// Copyright 2015 CoreOS, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package validate
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Context represents the current position within a newline-delimited string.
|
||||
// Each line is loaded, one by one, into currentLine (newline omitted) and
|
||||
// lineNumber keeps track of its position within the original string.
|
||||
type Context struct {
|
||||
currentLine string
|
||||
remainingLines string
|
||||
lineNumber int
|
||||
}
|
||||
|
||||
// Increment moves the Context to the next line (if available).
|
||||
func (c *Context) Increment() {
|
||||
if c.currentLine == "" && c.remainingLines == "" {
|
||||
return
|
||||
}
|
||||
|
||||
lines := strings.SplitN(c.remainingLines, "\n", 2)
|
||||
c.currentLine = lines[0]
|
||||
if len(lines) == 2 {
|
||||
c.remainingLines = lines[1]
|
||||
} else {
|
||||
c.remainingLines = ""
|
||||
}
|
||||
c.lineNumber++
|
||||
}
|
||||
|
||||
// NewContext creates a Context from the provided data. It strips out all
|
||||
// carriage returns and moves to the first line (if available).
|
||||
func NewContext(content []byte) Context {
|
||||
c := Context{remainingLines: strings.Replace(string(content), "\r", "", -1)}
|
||||
c.Increment()
|
||||
return c
|
||||
}
|
||||
131
config/cloudinit/config/validate/context_test.go
Normal file
131
config/cloudinit/config/validate/context_test.go
Normal file
@@ -0,0 +1,131 @@
|
||||
// Copyright 2015 CoreOS, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package validate
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewContext(t *testing.T) {
|
||||
tests := []struct {
|
||||
in string
|
||||
|
||||
out Context
|
||||
}{
|
||||
{
|
||||
out: Context{
|
||||
currentLine: "",
|
||||
remainingLines: "",
|
||||
lineNumber: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
in: "this\r\nis\r\na\r\ntest",
|
||||
out: Context{
|
||||
currentLine: "this",
|
||||
remainingLines: "is\na\ntest",
|
||||
lineNumber: 1,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
if out := NewContext([]byte(tt.in)); !reflect.DeepEqual(tt.out, out) {
|
||||
t.Errorf("bad context (%q): want %#v, got %#v", tt.in, tt.out, out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIncrement(t *testing.T) {
|
||||
tests := []struct {
|
||||
init Context
|
||||
op func(c *Context)
|
||||
|
||||
res Context
|
||||
}{
|
||||
{
|
||||
init: Context{
|
||||
currentLine: "",
|
||||
remainingLines: "",
|
||||
lineNumber: 0,
|
||||
},
|
||||
res: Context{
|
||||
currentLine: "",
|
||||
remainingLines: "",
|
||||
lineNumber: 0,
|
||||
},
|
||||
op: func(c *Context) {
|
||||
c.Increment()
|
||||
},
|
||||
},
|
||||
{
|
||||
init: Context{
|
||||
currentLine: "test",
|
||||
remainingLines: "",
|
||||
lineNumber: 1,
|
||||
},
|
||||
res: Context{
|
||||
currentLine: "",
|
||||
remainingLines: "",
|
||||
lineNumber: 2,
|
||||
},
|
||||
op: func(c *Context) {
|
||||
c.Increment()
|
||||
c.Increment()
|
||||
c.Increment()
|
||||
},
|
||||
},
|
||||
{
|
||||
init: Context{
|
||||
currentLine: "this",
|
||||
remainingLines: "is\na\ntest",
|
||||
lineNumber: 1,
|
||||
},
|
||||
res: Context{
|
||||
currentLine: "is",
|
||||
remainingLines: "a\ntest",
|
||||
lineNumber: 2,
|
||||
},
|
||||
op: func(c *Context) {
|
||||
c.Increment()
|
||||
},
|
||||
},
|
||||
{
|
||||
init: Context{
|
||||
currentLine: "this",
|
||||
remainingLines: "is\na\ntest",
|
||||
lineNumber: 1,
|
||||
},
|
||||
res: Context{
|
||||
currentLine: "test",
|
||||
remainingLines: "",
|
||||
lineNumber: 4,
|
||||
},
|
||||
op: func(c *Context) {
|
||||
c.Increment()
|
||||
c.Increment()
|
||||
c.Increment()
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
res := tt.init
|
||||
if tt.op(&res); !reflect.DeepEqual(tt.res, res) {
|
||||
t.Errorf("bad context (%d, %#v): want %#v, got %#v", i, tt.init, tt.res, res)
|
||||
}
|
||||
}
|
||||
}
|
||||
157
config/cloudinit/config/validate/node.go
Normal file
157
config/cloudinit/config/validate/node.go
Normal file
@@ -0,0 +1,157 @@
|
||||
// Copyright 2015 CoreOS, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package validate
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
var (
|
||||
yamlKey = regexp.MustCompile(`^ *-? ?(?P<key>.*?):`)
|
||||
yamlElem = regexp.MustCompile(`^ *-`)
|
||||
)
|
||||
|
||||
type Node struct {
|
||||
name string
|
||||
line int
|
||||
children []Node
|
||||
field reflect.StructField
|
||||
reflect.Value
|
||||
}
|
||||
|
||||
// Child attempts to find the child with the given name in the Node's list of
|
||||
// children. If no such child is found, an invalid Node is returned.
|
||||
func (n Node) Child(name string) Node {
|
||||
for _, c := range n.children {
|
||||
if c.name == name {
|
||||
return c
|
||||
}
|
||||
}
|
||||
return Node{}
|
||||
}
|
||||
|
||||
// HumanType returns the human-consumable string representation of the type of
|
||||
// the Node.
|
||||
func (n Node) HumanType() string {
|
||||
switch k := n.Kind(); k {
|
||||
case reflect.Slice:
|
||||
c := n.Type().Elem()
|
||||
return "[]" + Node{Value: reflect.New(c).Elem()}.HumanType()
|
||||
default:
|
||||
return k.String()
|
||||
}
|
||||
}
|
||||
|
||||
// NewNode returns the Node representation of the given value. The context
|
||||
// will be used in an attempt to determine line numbers for the given value.
|
||||
func NewNode(value interface{}, context Context) Node {
|
||||
var n Node
|
||||
toNode(value, context, &n)
|
||||
return n
|
||||
}
|
||||
|
||||
// toNode converts the given value into a Node and then recursively processes
|
||||
// each of the Nodes components (e.g. fields, array elements, keys).
|
||||
func toNode(v interface{}, c Context, n *Node) {
|
||||
vv := reflect.ValueOf(v)
|
||||
if !vv.IsValid() {
|
||||
return
|
||||
}
|
||||
|
||||
n.Value = vv
|
||||
switch vv.Kind() {
|
||||
case reflect.Struct:
|
||||
// Walk over each field in the structure, skipping unexported fields,
|
||||
// and create a Node for it.
|
||||
for i := 0; i < vv.Type().NumField(); i++ {
|
||||
ft := vv.Type().Field(i)
|
||||
k := ft.Tag.Get("yaml")
|
||||
if k == "-" || k == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
cn := Node{name: k, field: ft}
|
||||
c, ok := findKey(cn.name, c)
|
||||
if ok {
|
||||
cn.line = c.lineNumber
|
||||
}
|
||||
toNode(vv.Field(i).Interface(), c, &cn)
|
||||
n.children = append(n.children, cn)
|
||||
}
|
||||
case reflect.Map:
|
||||
// Walk over each key in the map and create a Node for it.
|
||||
v := v.(map[interface{}]interface{})
|
||||
for k, cv := range v {
|
||||
cn := Node{name: fmt.Sprintf("%s", k)}
|
||||
c, ok := findKey(cn.name, c)
|
||||
if ok {
|
||||
cn.line = c.lineNumber
|
||||
}
|
||||
toNode(cv, c, &cn)
|
||||
n.children = append(n.children, cn)
|
||||
}
|
||||
case reflect.Slice:
|
||||
// Walk over each element in the slice and create a Node for it.
|
||||
// While iterating over the slice, preserve the context after it
|
||||
// is modified. This allows the line numbers to reflect the current
|
||||
// element instead of the first.
|
||||
for i := 0; i < vv.Len(); i++ {
|
||||
cn := Node{
|
||||
name: fmt.Sprintf("%s[%d]", n.name, i),
|
||||
field: n.field,
|
||||
}
|
||||
var ok bool
|
||||
c, ok = findElem(c)
|
||||
if ok {
|
||||
cn.line = c.lineNumber
|
||||
}
|
||||
toNode(vv.Index(i).Interface(), c, &cn)
|
||||
n.children = append(n.children, cn)
|
||||
c.Increment()
|
||||
}
|
||||
case reflect.String, reflect.Int, reflect.Bool, reflect.Float64:
|
||||
default:
|
||||
panic(fmt.Sprintf("toNode(): unhandled kind %s", vv.Kind()))
|
||||
}
|
||||
}
|
||||
|
||||
// findKey attempts to find the requested key within the provided context.
|
||||
// A modified copy of the context is returned with every line up to the key
|
||||
// incremented past. A boolean, true if the key was found, is also returned.
|
||||
func findKey(key string, context Context) (Context, bool) {
|
||||
return find(yamlKey, key, context)
|
||||
}
|
||||
|
||||
// findElem attempts to find an array element within the provided context.
|
||||
// A modified copy of the context is returned with every line up to the array
|
||||
// element incremented past. A boolean, true if the key was found, is also
|
||||
// returned.
|
||||
func findElem(context Context) (Context, bool) {
|
||||
return find(yamlElem, "", context)
|
||||
}
|
||||
|
||||
func find(exp *regexp.Regexp, key string, context Context) (Context, bool) {
|
||||
for len(context.currentLine) > 0 || len(context.remainingLines) > 0 {
|
||||
matches := exp.FindStringSubmatch(context.currentLine)
|
||||
if len(matches) > 0 && (key == "" || matches[1] == key) {
|
||||
return context, true
|
||||
}
|
||||
|
||||
context.Increment()
|
||||
}
|
||||
return context, false
|
||||
}
|
||||
284
config/cloudinit/config/validate/node_test.go
Normal file
284
config/cloudinit/config/validate/node_test.go
Normal file
@@ -0,0 +1,284 @@
|
||||
// Copyright 2015 CoreOS, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package validate
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestChild(t *testing.T) {
|
||||
tests := []struct {
|
||||
parent Node
|
||||
name string
|
||||
|
||||
child Node
|
||||
}{
|
||||
{},
|
||||
{
|
||||
name: "c1",
|
||||
},
|
||||
{
|
||||
parent: Node{
|
||||
children: []Node{
|
||||
{name: "c1"},
|
||||
{name: "c2"},
|
||||
{name: "c3"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
parent: Node{
|
||||
children: []Node{
|
||||
{name: "c1"},
|
||||
{name: "c2"},
|
||||
{name: "c3"},
|
||||
},
|
||||
},
|
||||
name: "c2",
|
||||
child: Node{name: "c2"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
if child := tt.parent.Child(tt.name); !reflect.DeepEqual(tt.child, child) {
|
||||
t.Errorf("bad child (%q): want %#v, got %#v", tt.name, tt.child, child)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestHumanType(t *testing.T) {
|
||||
tests := []struct {
|
||||
node Node
|
||||
|
||||
humanType string
|
||||
}{
|
||||
{
|
||||
humanType: "invalid",
|
||||
},
|
||||
{
|
||||
node: Node{Value: reflect.ValueOf("hello")},
|
||||
humanType: "string",
|
||||
},
|
||||
{
|
||||
node: Node{
|
||||
Value: reflect.ValueOf([]int{1, 2}),
|
||||
children: []Node{
|
||||
{Value: reflect.ValueOf(1)},
|
||||
{Value: reflect.ValueOf(2)},
|
||||
}},
|
||||
humanType: "[]int",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
if humanType := tt.node.HumanType(); tt.humanType != humanType {
|
||||
t.Errorf("bad type (%q): want %q, got %q", tt.node, tt.humanType, humanType)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestToNode(t *testing.T) {
|
||||
tests := []struct {
|
||||
value interface{}
|
||||
context Context
|
||||
|
||||
node Node
|
||||
}{
|
||||
{},
|
||||
{
|
||||
value: struct{}{},
|
||||
node: Node{Value: reflect.ValueOf(struct{}{})},
|
||||
},
|
||||
{
|
||||
value: struct {
|
||||
A int `yaml:"a"`
|
||||
}{},
|
||||
node: Node{
|
||||
children: []Node{
|
||||
{
|
||||
name: "a",
|
||||
field: reflect.TypeOf(struct {
|
||||
A int `yaml:"a"`
|
||||
}{}).Field(0),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
value: struct {
|
||||
A []int `yaml:"a"`
|
||||
}{},
|
||||
node: Node{
|
||||
children: []Node{
|
||||
{
|
||||
name: "a",
|
||||
field: reflect.TypeOf(struct {
|
||||
A []int `yaml:"a"`
|
||||
}{}).Field(0),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
value: map[interface{}]interface{}{
|
||||
"a": map[interface{}]interface{}{
|
||||
"b": 2,
|
||||
},
|
||||
},
|
||||
context: NewContext([]byte("a:\n b: 2")),
|
||||
node: Node{
|
||||
children: []Node{
|
||||
{
|
||||
line: 1,
|
||||
name: "a",
|
||||
children: []Node{
|
||||
{name: "b", line: 2},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
value: struct {
|
||||
A struct {
|
||||
Jon bool `yaml:"b"`
|
||||
} `yaml:"a"`
|
||||
}{},
|
||||
node: Node{
|
||||
children: []Node{
|
||||
{
|
||||
name: "a",
|
||||
children: []Node{
|
||||
{
|
||||
name: "b",
|
||||
field: reflect.TypeOf(struct {
|
||||
Jon bool `yaml:"b"`
|
||||
}{}).Field(0),
|
||||
Value: reflect.ValueOf(false),
|
||||
},
|
||||
},
|
||||
field: reflect.TypeOf(struct {
|
||||
A struct {
|
||||
Jon bool `yaml:"b"`
|
||||
} `yaml:"a"`
|
||||
}{}).Field(0),
|
||||
Value: reflect.ValueOf(struct {
|
||||
Jon bool `yaml:"b"`
|
||||
}{}),
|
||||
},
|
||||
},
|
||||
Value: reflect.ValueOf(struct {
|
||||
A struct {
|
||||
Jon bool `yaml:"b"`
|
||||
} `yaml:"a"`
|
||||
}{}),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
var node Node
|
||||
toNode(tt.value, tt.context, &node)
|
||||
if !nodesEqual(tt.node, node) {
|
||||
t.Errorf("bad node (%#v): want %#v, got %#v", tt.value, tt.node, node)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindKey(t *testing.T) {
|
||||
tests := []struct {
|
||||
key string
|
||||
context Context
|
||||
|
||||
found bool
|
||||
}{
|
||||
{},
|
||||
{
|
||||
key: "key1",
|
||||
context: NewContext([]byte("key1: hi")),
|
||||
found: true,
|
||||
},
|
||||
{
|
||||
key: "key2",
|
||||
context: NewContext([]byte("key1: hi")),
|
||||
found: false,
|
||||
},
|
||||
{
|
||||
key: "key3",
|
||||
context: NewContext([]byte("key1:\n key2:\n key3: hi")),
|
||||
found: true,
|
||||
},
|
||||
{
|
||||
key: "key4",
|
||||
context: NewContext([]byte("key1:\n - key4: hi")),
|
||||
found: true,
|
||||
},
|
||||
{
|
||||
key: "key5",
|
||||
context: NewContext([]byte("#key5")),
|
||||
found: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
if _, found := findKey(tt.key, tt.context); tt.found != found {
|
||||
t.Errorf("bad find (%q): want %t, got %t", tt.key, tt.found, found)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindElem(t *testing.T) {
|
||||
tests := []struct {
|
||||
context Context
|
||||
|
||||
found bool
|
||||
}{
|
||||
{},
|
||||
{
|
||||
context: NewContext([]byte("test: hi")),
|
||||
found: false,
|
||||
},
|
||||
{
|
||||
context: NewContext([]byte("test:\n - a\n -b")),
|
||||
found: true,
|
||||
},
|
||||
{
|
||||
context: NewContext([]byte("test:\n -\n a")),
|
||||
found: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
if _, found := findElem(tt.context); tt.found != found {
|
||||
t.Errorf("bad find (%q): want %t, got %t", tt.context, tt.found, found)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func nodesEqual(a, b Node) bool {
|
||||
if a.name != b.name ||
|
||||
a.line != b.line ||
|
||||
!reflect.DeepEqual(a.field, b.field) ||
|
||||
len(a.children) != len(b.children) {
|
||||
return false
|
||||
}
|
||||
for i := 0; i < len(a.children); i++ {
|
||||
if !nodesEqual(a.children[i], b.children[i]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
88
config/cloudinit/config/validate/report.go
Normal file
88
config/cloudinit/config/validate/report.go
Normal file
@@ -0,0 +1,88 @@
|
||||
// Copyright 2015 CoreOS, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package validate
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Report represents the list of entries resulting from validation.
|
||||
type Report struct {
|
||||
entries []Entry
|
||||
}
|
||||
|
||||
// Error adds an error entry to the report.
|
||||
func (r *Report) Error(line int, message string) {
|
||||
r.entries = append(r.entries, Entry{entryError, message, line})
|
||||
}
|
||||
|
||||
// Warning adds a warning entry to the report.
|
||||
func (r *Report) Warning(line int, message string) {
|
||||
r.entries = append(r.entries, Entry{entryWarning, message, line})
|
||||
}
|
||||
|
||||
// Info adds an info entry to the report.
|
||||
func (r *Report) Info(line int, message string) {
|
||||
r.entries = append(r.entries, Entry{entryInfo, message, line})
|
||||
}
|
||||
|
||||
// Entries returns the list of entries in the report.
|
||||
func (r *Report) Entries() []Entry {
|
||||
return r.entries
|
||||
}
|
||||
|
||||
// Entry represents a single generic item in the report.
|
||||
type Entry struct {
|
||||
kind entryKind
|
||||
message string
|
||||
line int
|
||||
}
|
||||
|
||||
// String returns a human-readable representation of the entry.
|
||||
func (e Entry) String() string {
|
||||
return fmt.Sprintf("line %d: %s: %s", e.line, e.kind, e.message)
|
||||
}
|
||||
|
||||
// MarshalJSON satisfies the json.Marshaler interface, returning the entry
|
||||
// encoded as a JSON object.
|
||||
func (e Entry) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(map[string]interface{}{
|
||||
"kind": e.kind.String(),
|
||||
"message": e.message,
|
||||
"line": e.line,
|
||||
})
|
||||
}
|
||||
|
||||
type entryKind int
|
||||
|
||||
const (
|
||||
entryError entryKind = iota
|
||||
entryWarning
|
||||
entryInfo
|
||||
)
|
||||
|
||||
func (k entryKind) String() string {
|
||||
switch k {
|
||||
case entryError:
|
||||
return "error"
|
||||
case entryWarning:
|
||||
return "warning"
|
||||
case entryInfo:
|
||||
return "info"
|
||||
default:
|
||||
panic(fmt.Sprintf("invalid kind %d", k))
|
||||
}
|
||||
}
|
||||
96
config/cloudinit/config/validate/report_test.go
Normal file
96
config/cloudinit/config/validate/report_test.go
Normal file
@@ -0,0 +1,96 @@
|
||||
// Copyright 2015 CoreOS, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package validate
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestEntry(t *testing.T) {
|
||||
tests := []struct {
|
||||
entry Entry
|
||||
|
||||
str string
|
||||
json []byte
|
||||
}{
|
||||
{
|
||||
Entry{entryInfo, "test info", 1},
|
||||
"line 1: info: test info",
|
||||
[]byte(`{"kind":"info","line":1,"message":"test info"}`),
|
||||
},
|
||||
{
|
||||
Entry{entryWarning, "test warning", 1},
|
||||
"line 1: warning: test warning",
|
||||
[]byte(`{"kind":"warning","line":1,"message":"test warning"}`),
|
||||
},
|
||||
{
|
||||
Entry{entryError, "test error", 2},
|
||||
"line 2: error: test error",
|
||||
[]byte(`{"kind":"error","line":2,"message":"test error"}`),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
if str := tt.entry.String(); tt.str != str {
|
||||
t.Errorf("bad string (%q): want %q, got %q", tt.entry, tt.str, str)
|
||||
}
|
||||
json, err := tt.entry.MarshalJSON()
|
||||
if err != nil {
|
||||
t.Errorf("bad error (%q): want %v, got %q", tt.entry, nil, err)
|
||||
}
|
||||
if !bytes.Equal(tt.json, json) {
|
||||
t.Errorf("bad JSON (%q): want %q, got %q", tt.entry, tt.json, json)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestReport(t *testing.T) {
|
||||
type reportFunc struct {
|
||||
fn func(*Report, int, string)
|
||||
line int
|
||||
message string
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
fs []reportFunc
|
||||
|
||||
es []Entry
|
||||
}{
|
||||
{
|
||||
[]reportFunc{
|
||||
{(*Report).Warning, 1, "test warning 1"},
|
||||
{(*Report).Error, 2, "test error 2"},
|
||||
{(*Report).Info, 10, "test info 10"},
|
||||
},
|
||||
[]Entry{
|
||||
{entryWarning, "test warning 1", 1},
|
||||
{entryError, "test error 2", 2},
|
||||
{entryInfo, "test info 10", 10},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
r := Report{}
|
||||
for _, f := range tt.fs {
|
||||
f.fn(&r, f.line, f.message)
|
||||
}
|
||||
if es := r.Entries(); !reflect.DeepEqual(tt.es, es) {
|
||||
t.Errorf("bad entries (%v): want %#v, got %#v", tt.fs, tt.es, es)
|
||||
}
|
||||
}
|
||||
}
|
||||
180
config/cloudinit/config/validate/rules.go
Normal file
180
config/cloudinit/config/validate/rules.go
Normal file
@@ -0,0 +1,180 @@
|
||||
// Copyright 2015 CoreOS, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package validate
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"path"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/rancher/os/config/cloudinit/config"
|
||||
)
|
||||
|
||||
type rule func(config Node, report *Report)
|
||||
|
||||
// Rules contains all of the validation rules.
|
||||
var Rules = []rule{
|
||||
checkDiscoveryURL,
|
||||
checkEncoding,
|
||||
checkStructure,
|
||||
checkValidity,
|
||||
checkWriteFiles,
|
||||
checkWriteFilesUnderCoreos,
|
||||
}
|
||||
|
||||
// checkDiscoveryURL verifies that the string is a valid url.
|
||||
func checkDiscoveryURL(cfg Node, report *Report) {
|
||||
c := cfg.Child("coreos").Child("etcd").Child("discovery")
|
||||
if !c.IsValid() {
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := url.ParseRequestURI(c.String()); err != nil {
|
||||
report.Warning(c.line, "discovery URL is not valid")
|
||||
}
|
||||
}
|
||||
|
||||
// checkEncoding validates that, for each file under 'write_files', the
|
||||
// content can be decoded given the specified encoding.
|
||||
func checkEncoding(cfg Node, report *Report) {
|
||||
for _, f := range cfg.Child("write_files").children {
|
||||
e := f.Child("encoding")
|
||||
if !e.IsValid() {
|
||||
continue
|
||||
}
|
||||
|
||||
c := f.Child("content")
|
||||
if _, err := config.DecodeContent(c.String(), e.String()); err != nil {
|
||||
report.Error(c.line, fmt.Sprintf("content cannot be decoded as %q", e.String()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// checkStructure compares the provided config to the empty config.CloudConfig
|
||||
// structure. Each node is checked to make sure that it exists in the known
|
||||
// structure and that its type is compatible.
|
||||
func checkStructure(cfg Node, report *Report) {
|
||||
g := NewNode(config.CloudConfig{}, NewContext([]byte{}))
|
||||
checkNodeStructure(cfg, g, report)
|
||||
}
|
||||
|
||||
func checkNodeStructure(n, g Node, r *Report) {
|
||||
if !isCompatible(n.Kind(), g.Kind()) {
|
||||
r.Warning(n.line, fmt.Sprintf("incorrect type for %q (want %s)", n.name, g.HumanType()))
|
||||
return
|
||||
}
|
||||
|
||||
switch g.Kind() {
|
||||
case reflect.Struct:
|
||||
for _, cn := range n.children {
|
||||
if cg := g.Child(cn.name); cg.IsValid() {
|
||||
if msg := cg.field.Tag.Get("deprecated"); msg != "" {
|
||||
r.Warning(cn.line, fmt.Sprintf("deprecated key %q (%s)", cn.name, msg))
|
||||
}
|
||||
checkNodeStructure(cn, cg, r)
|
||||
} else {
|
||||
r.Warning(cn.line, fmt.Sprintf("unrecognized key %q", cn.name))
|
||||
}
|
||||
}
|
||||
case reflect.Slice:
|
||||
for _, cn := range n.children {
|
||||
var cg Node
|
||||
c := g.Type().Elem()
|
||||
toNode(reflect.New(c).Elem().Interface(), Context{}, &cg)
|
||||
checkNodeStructure(cn, cg, r)
|
||||
}
|
||||
case reflect.String, reflect.Int, reflect.Float64, reflect.Bool:
|
||||
default:
|
||||
panic(fmt.Sprintf("checkNodeStructure(): unhandled kind %s", g.Kind()))
|
||||
}
|
||||
}
|
||||
|
||||
// isCompatible determines if the type of kind n can be converted to the type
|
||||
// of kind g in the context of YAML. This is not an exhaustive list, but its
|
||||
// enough for the purposes of cloud-config validation.
|
||||
func isCompatible(n, g reflect.Kind) bool {
|
||||
switch g {
|
||||
case reflect.String:
|
||||
return n == reflect.String || n == reflect.Int || n == reflect.Float64 || n == reflect.Bool
|
||||
case reflect.Struct:
|
||||
return n == reflect.Struct || n == reflect.Map
|
||||
case reflect.Float64:
|
||||
return n == reflect.Float64 || n == reflect.Int
|
||||
case reflect.Bool, reflect.Slice, reflect.Int:
|
||||
return n == g
|
||||
default:
|
||||
panic(fmt.Sprintf("isCompatible(): unhandled kind %s", g))
|
||||
}
|
||||
}
|
||||
|
||||
// checkValidity checks the value of every node in the provided config by
|
||||
// running config.AssertValid() on it.
|
||||
func checkValidity(cfg Node, report *Report) {
|
||||
g := NewNode(config.CloudConfig{}, NewContext([]byte{}))
|
||||
checkNodeValidity(cfg, g, report)
|
||||
}
|
||||
|
||||
func checkNodeValidity(n, g Node, r *Report) {
|
||||
if err := config.AssertValid(n.Value, g.field.Tag.Get("valid")); err != nil {
|
||||
r.Error(n.line, fmt.Sprintf("invalid value %v", n.Value.Interface()))
|
||||
}
|
||||
switch g.Kind() {
|
||||
case reflect.Struct:
|
||||
for _, cn := range n.children {
|
||||
if cg := g.Child(cn.name); cg.IsValid() {
|
||||
checkNodeValidity(cn, cg, r)
|
||||
}
|
||||
}
|
||||
case reflect.Slice:
|
||||
for _, cn := range n.children {
|
||||
var cg Node
|
||||
c := g.Type().Elem()
|
||||
toNode(reflect.New(c).Elem().Interface(), Context{}, &cg)
|
||||
checkNodeValidity(cn, cg, r)
|
||||
}
|
||||
case reflect.String, reflect.Int, reflect.Float64, reflect.Bool:
|
||||
default:
|
||||
panic(fmt.Sprintf("checkNodeValidity(): unhandled kind %s", g.Kind()))
|
||||
}
|
||||
}
|
||||
|
||||
// checkWriteFiles checks to make sure that the target file can actually be
|
||||
// written. Note that this check is approximate (it only checks to see if the file
|
||||
// is under /usr).
|
||||
func checkWriteFiles(cfg Node, report *Report) {
|
||||
for _, f := range cfg.Child("write_files").children {
|
||||
c := f.Child("path")
|
||||
if !c.IsValid() {
|
||||
continue
|
||||
}
|
||||
|
||||
d := path.Dir(c.String())
|
||||
switch {
|
||||
case strings.HasPrefix(d, "/usr"):
|
||||
report.Error(c.line, "file cannot be written to a read-only filesystem")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// checkWriteFilesUnderCoreos checks to see if the 'write_files' node is a
|
||||
// child of 'coreos' (it shouldn't be).
|
||||
func checkWriteFilesUnderCoreos(cfg Node, report *Report) {
|
||||
c := cfg.Child("coreos").Child("write_files")
|
||||
if c.IsValid() {
|
||||
report.Info(c.line, "write_files doesn't belong under coreos")
|
||||
}
|
||||
}
|
||||
408
config/cloudinit/config/validate/rules_test.go
Normal file
408
config/cloudinit/config/validate/rules_test.go
Normal file
@@ -0,0 +1,408 @@
|
||||
// Copyright 2015 CoreOS, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package validate
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCheckDiscoveryURL(t *testing.T) {
|
||||
tests := []struct {
|
||||
config string
|
||||
|
||||
entries []Entry
|
||||
}{
|
||||
{},
|
||||
{
|
||||
config: "coreos:\n etcd:\n discovery: https://discovery.etcd.io/00000000000000000000000000000000",
|
||||
},
|
||||
{
|
||||
config: "coreos:\n etcd:\n discovery: http://custom.domain/mytoken",
|
||||
},
|
||||
{
|
||||
config: "coreos:\n etcd:\n discovery: disco",
|
||||
entries: []Entry{{entryWarning, "discovery URL is not valid", 3}},
|
||||
},
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
r := Report{}
|
||||
n, err := parseCloudConfig([]byte(tt.config), &r)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
checkDiscoveryURL(n, &r)
|
||||
|
||||
if e := r.Entries(); !reflect.DeepEqual(tt.entries, e) {
|
||||
t.Errorf("bad report (%d, %q): want %#v, got %#v", i, tt.config, tt.entries, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckEncoding(t *testing.T) {
|
||||
tests := []struct {
|
||||
config string
|
||||
|
||||
entries []Entry
|
||||
}{
|
||||
{},
|
||||
{
|
||||
config: "write_files:\n - encoding: base64\n content: aGVsbG8K",
|
||||
},
|
||||
{
|
||||
config: "write_files:\n - content: !!binary aGVsbG8K",
|
||||
},
|
||||
{
|
||||
config: "write_files:\n - encoding: base64\n content: !!binary aGVsbG8K",
|
||||
entries: []Entry{{entryError, `content cannot be decoded as "base64"`, 3}},
|
||||
},
|
||||
{
|
||||
config: "write_files:\n - encoding: base64\n content: !!binary YUdWc2JHOEsK",
|
||||
},
|
||||
{
|
||||
config: "write_files:\n - encoding: gzip\n content: !!binary H4sIAOC3tVQAA8tIzcnJ5wIAIDA6NgYAAAA=",
|
||||
},
|
||||
{
|
||||
config: "write_files:\n - encoding: gzip+base64\n content: H4sIAOC3tVQAA8tIzcnJ5wIAIDA6NgYAAAA=",
|
||||
},
|
||||
{
|
||||
config: "write_files:\n - encoding: custom\n content: hello",
|
||||
entries: []Entry{{entryError, `content cannot be decoded as "custom"`, 3}},
|
||||
},
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
r := Report{}
|
||||
n, err := parseCloudConfig([]byte(tt.config), &r)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
checkEncoding(n, &r)
|
||||
|
||||
if e := r.Entries(); !reflect.DeepEqual(tt.entries, e) {
|
||||
t.Errorf("bad report (%d, %q): want %#v, got %#v", i, tt.config, tt.entries, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckStructure(t *testing.T) {
|
||||
tests := []struct {
|
||||
config string
|
||||
|
||||
entries []Entry
|
||||
}{
|
||||
{},
|
||||
|
||||
// Test for unrecognized keys
|
||||
{
|
||||
config: "test:",
|
||||
entries: []Entry{{entryWarning, "unrecognized key \"test\"", 1}},
|
||||
},
|
||||
{
|
||||
config: "coreos:\n etcd:\n bad:",
|
||||
entries: []Entry{{entryWarning, "unrecognized key \"bad\"", 3}},
|
||||
},
|
||||
{
|
||||
config: "coreos:\n etcd:\n discovery: good",
|
||||
},
|
||||
|
||||
// Test for deprecated keys
|
||||
{
|
||||
config: "coreos:\n etcd:\n addr: hi",
|
||||
},
|
||||
{
|
||||
config: "coreos:\n etcd:\n proxy: hi",
|
||||
entries: []Entry{{entryWarning, "deprecated key \"proxy\" (etcd2 options no longer work for etcd)", 3}},
|
||||
},
|
||||
|
||||
// Test for error on list of nodes
|
||||
{
|
||||
config: "coreos:\n units:\n - hello\n - goodbye",
|
||||
entries: []Entry{
|
||||
{entryWarning, "incorrect type for \"units[0]\" (want struct)", 3},
|
||||
{entryWarning, "incorrect type for \"units[1]\" (want struct)", 4},
|
||||
},
|
||||
},
|
||||
|
||||
// Test for incorrect types
|
||||
// Want boolean
|
||||
{
|
||||
config: "coreos:\n units:\n - enable: true",
|
||||
},
|
||||
{
|
||||
config: "coreos:\n units:\n - enable: 4",
|
||||
entries: []Entry{{entryWarning, "incorrect type for \"enable\" (want bool)", 3}},
|
||||
},
|
||||
{
|
||||
config: "coreos:\n units:\n - enable: bad",
|
||||
entries: []Entry{{entryWarning, "incorrect type for \"enable\" (want bool)", 3}},
|
||||
},
|
||||
{
|
||||
config: "coreos:\n units:\n - enable:\n bad:",
|
||||
entries: []Entry{{entryWarning, "incorrect type for \"enable\" (want bool)", 3}},
|
||||
},
|
||||
{
|
||||
config: "coreos:\n units:\n - enable:\n - bad",
|
||||
entries: []Entry{{entryWarning, "incorrect type for \"enable\" (want bool)", 3}},
|
||||
},
|
||||
// Want string
|
||||
{
|
||||
config: "hostname: true",
|
||||
},
|
||||
{
|
||||
config: "hostname: 4",
|
||||
},
|
||||
{
|
||||
config: "hostname: host",
|
||||
},
|
||||
{
|
||||
config: "hostname:\n name:",
|
||||
entries: []Entry{{entryWarning, "incorrect type for \"hostname\" (want string)", 1}},
|
||||
},
|
||||
{
|
||||
config: "hostname:\n - name",
|
||||
entries: []Entry{{entryWarning, "incorrect type for \"hostname\" (want string)", 1}},
|
||||
},
|
||||
// Want struct
|
||||
{
|
||||
config: "coreos: true",
|
||||
entries: []Entry{{entryWarning, "incorrect type for \"coreos\" (want struct)", 1}},
|
||||
},
|
||||
{
|
||||
config: "coreos: 4",
|
||||
entries: []Entry{{entryWarning, "incorrect type for \"coreos\" (want struct)", 1}},
|
||||
},
|
||||
{
|
||||
config: "coreos: hello",
|
||||
entries: []Entry{{entryWarning, "incorrect type for \"coreos\" (want struct)", 1}},
|
||||
},
|
||||
{
|
||||
config: "coreos:\n etcd:\n discovery: fire in the disco",
|
||||
},
|
||||
{
|
||||
config: "coreos:\n - hello",
|
||||
entries: []Entry{{entryWarning, "incorrect type for \"coreos\" (want struct)", 1}},
|
||||
},
|
||||
// Want []string
|
||||
{
|
||||
config: "ssh_authorized_keys: true",
|
||||
entries: []Entry{{entryWarning, "incorrect type for \"ssh_authorized_keys\" (want []string)", 1}},
|
||||
},
|
||||
{
|
||||
config: "ssh_authorized_keys: 4",
|
||||
entries: []Entry{{entryWarning, "incorrect type for \"ssh_authorized_keys\" (want []string)", 1}},
|
||||
},
|
||||
{
|
||||
config: "ssh_authorized_keys: key",
|
||||
entries: []Entry{{entryWarning, "incorrect type for \"ssh_authorized_keys\" (want []string)", 1}},
|
||||
},
|
||||
{
|
||||
config: "ssh_authorized_keys:\n key: value",
|
||||
entries: []Entry{{entryWarning, "incorrect type for \"ssh_authorized_keys\" (want []string)", 1}},
|
||||
},
|
||||
{
|
||||
config: "ssh_authorized_keys:\n - key",
|
||||
},
|
||||
{
|
||||
config: "ssh_authorized_keys:\n - key: value",
|
||||
entries: []Entry{{entryWarning, "incorrect type for \"ssh_authorized_keys[0]\" (want string)", 2}},
|
||||
},
|
||||
// Want []struct
|
||||
{
|
||||
config: "users:\n true",
|
||||
entries: []Entry{{entryWarning, "incorrect type for \"users\" (want []struct)", 1}},
|
||||
},
|
||||
{
|
||||
config: "users:\n 4",
|
||||
entries: []Entry{{entryWarning, "incorrect type for \"users\" (want []struct)", 1}},
|
||||
},
|
||||
{
|
||||
config: "users:\n bad",
|
||||
entries: []Entry{{entryWarning, "incorrect type for \"users\" (want []struct)", 1}},
|
||||
},
|
||||
{
|
||||
config: "users:\n bad:",
|
||||
entries: []Entry{{entryWarning, "incorrect type for \"users\" (want []struct)", 1}},
|
||||
},
|
||||
{
|
||||
config: "users:\n - name: good",
|
||||
},
|
||||
// Want struct within array
|
||||
{
|
||||
config: "users:\n - true",
|
||||
entries: []Entry{{entryWarning, "incorrect type for \"users[0]\" (want struct)", 2}},
|
||||
},
|
||||
{
|
||||
config: "users:\n - name: hi\n - true",
|
||||
entries: []Entry{{entryWarning, "incorrect type for \"users[1]\" (want struct)", 3}},
|
||||
},
|
||||
{
|
||||
config: "users:\n - 4",
|
||||
entries: []Entry{{entryWarning, "incorrect type for \"users[0]\" (want struct)", 2}},
|
||||
},
|
||||
{
|
||||
config: "users:\n - bad",
|
||||
entries: []Entry{{entryWarning, "incorrect type for \"users[0]\" (want struct)", 2}},
|
||||
},
|
||||
{
|
||||
config: "users:\n - - bad",
|
||||
entries: []Entry{{entryWarning, "incorrect type for \"users[0]\" (want struct)", 2}},
|
||||
},
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
r := Report{}
|
||||
n, err := parseCloudConfig([]byte(tt.config), &r)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
checkStructure(n, &r)
|
||||
|
||||
if e := r.Entries(); !reflect.DeepEqual(tt.entries, e) {
|
||||
t.Errorf("bad report (%d, %q): want %#v, got %#v", i, tt.config, tt.entries, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckValidity(t *testing.T) {
|
||||
tests := []struct {
|
||||
config string
|
||||
|
||||
entries []Entry
|
||||
}{
|
||||
// string
|
||||
{
|
||||
config: "hostname: test",
|
||||
},
|
||||
|
||||
// int
|
||||
{
|
||||
config: "coreos:\n fleet:\n verbosity: 2",
|
||||
},
|
||||
|
||||
// bool
|
||||
{
|
||||
config: "coreos:\n units:\n - enable: true",
|
||||
},
|
||||
|
||||
// slice
|
||||
{
|
||||
config: "coreos:\n units:\n - command: start\n - name: stop",
|
||||
},
|
||||
{
|
||||
config: "coreos:\n units:\n - command: lol",
|
||||
entries: []Entry{{entryError, "invalid value lol", 3}},
|
||||
},
|
||||
|
||||
// struct
|
||||
{
|
||||
config: "coreos:\n update:\n reboot_strategy: off",
|
||||
},
|
||||
{
|
||||
config: "coreos:\n update:\n reboot_strategy: always",
|
||||
entries: []Entry{{entryError, "invalid value always", 3}},
|
||||
},
|
||||
|
||||
// unknown
|
||||
{
|
||||
config: "unknown: hi",
|
||||
},
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
r := Report{}
|
||||
n, err := parseCloudConfig([]byte(tt.config), &r)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
checkValidity(n, &r)
|
||||
|
||||
if e := r.Entries(); !reflect.DeepEqual(tt.entries, e) {
|
||||
t.Errorf("bad report (%d, %q): want %#v, got %#v", i, tt.config, tt.entries, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckWriteFiles(t *testing.T) {
|
||||
tests := []struct {
|
||||
config string
|
||||
|
||||
entries []Entry
|
||||
}{
|
||||
{},
|
||||
{
|
||||
config: "write_files:\n - path: /valid",
|
||||
},
|
||||
{
|
||||
config: "write_files:\n - path: /tmp/usr/valid",
|
||||
},
|
||||
{
|
||||
config: "write_files:\n - path: /usr/invalid",
|
||||
entries: []Entry{{entryError, "file cannot be written to a read-only filesystem", 2}},
|
||||
},
|
||||
{
|
||||
config: "write-files:\n - path: /tmp/../usr/invalid",
|
||||
entries: []Entry{{entryError, "file cannot be written to a read-only filesystem", 2}},
|
||||
},
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
r := Report{}
|
||||
n, err := parseCloudConfig([]byte(tt.config), &r)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
checkWriteFiles(n, &r)
|
||||
|
||||
if e := r.Entries(); !reflect.DeepEqual(tt.entries, e) {
|
||||
t.Errorf("bad report (%d, %q): want %#v, got %#v", i, tt.config, tt.entries, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckWriteFilesUnderCoreos(t *testing.T) {
|
||||
tests := []struct {
|
||||
config string
|
||||
|
||||
entries []Entry
|
||||
}{
|
||||
{},
|
||||
{
|
||||
config: "write_files:\n - path: /hi",
|
||||
},
|
||||
{
|
||||
config: "coreos:\n write_files:\n - path: /hi",
|
||||
entries: []Entry{{entryInfo, "write_files doesn't belong under coreos", 2}},
|
||||
},
|
||||
{
|
||||
config: "coreos:\n write-files:\n - path: /hyphen",
|
||||
entries: []Entry{{entryInfo, "write_files doesn't belong under coreos", 2}},
|
||||
},
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
r := Report{}
|
||||
n, err := parseCloudConfig([]byte(tt.config), &r)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
checkWriteFilesUnderCoreos(n, &r)
|
||||
|
||||
if e := r.Entries(); !reflect.DeepEqual(tt.entries, e) {
|
||||
t.Errorf("bad report (%d, %q): want %#v, got %#v", i, tt.config, tt.entries, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
164
config/cloudinit/config/validate/validate.go
Normal file
164
config/cloudinit/config/validate/validate.go
Normal file
@@ -0,0 +1,164 @@
|
||||
// Copyright 2015 CoreOS, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package validate
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/rancher/os/config/cloudinit/config"
|
||||
|
||||
"github.com/coreos/yaml"
|
||||
)
|
||||
|
||||
var (
|
||||
yamlLineError = regexp.MustCompile(`^YAML error: line (?P<line>[[:digit:]]+): (?P<msg>.*)$`)
|
||||
yamlError = regexp.MustCompile(`^YAML error: (?P<msg>.*)$`)
|
||||
)
|
||||
|
||||
// Validate runs a series of validation tests against the given userdata and
|
||||
// returns a report detailing all of the issues. Presently, only cloud-configs
|
||||
// can be validated.
|
||||
func Validate(userdataBytes []byte) (Report, error) {
|
||||
switch {
|
||||
case len(userdataBytes) == 0:
|
||||
return Report{}, nil
|
||||
case config.IsScript(string(userdataBytes)):
|
||||
return Report{}, nil
|
||||
case config.IsIgnitionConfig(string(userdataBytes)):
|
||||
return Report{}, nil
|
||||
case config.IsCloudConfig(string(userdataBytes)):
|
||||
return validateCloudConfig(userdataBytes, Rules)
|
||||
default:
|
||||
return Report{entries: []Entry{
|
||||
{kind: entryError, message: `must be "#cloud-config" or begin with "#!"`, line: 1},
|
||||
}}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// validateCloudConfig runs all of the validation rules in Rules and returns
|
||||
// the resulting report and any errors encountered.
|
||||
func validateCloudConfig(config []byte, rules []rule) (report Report, err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err = fmt.Errorf("%v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
c, err := parseCloudConfig(config, &report)
|
||||
if err != nil {
|
||||
return report, err
|
||||
}
|
||||
|
||||
for _, r := range rules {
|
||||
r(c, &report)
|
||||
}
|
||||
return report, nil
|
||||
}
|
||||
|
||||
// parseCloudConfig parses the provided config into a node structure and logs
|
||||
// any parsing issues into the provided report. Unrecoverable errors are
|
||||
// returned as an error.
|
||||
func parseCloudConfig(cfg []byte, report *Report) (Node, error) {
|
||||
yaml.UnmarshalMappingKeyTransform = func(nameIn string) (nameOut string) {
|
||||
return nameIn
|
||||
}
|
||||
// unmarshal the config into an implicitly-typed form. The yaml library
|
||||
// will implicitly convert types into their normalized form
|
||||
// (e.g. 0744 -> 484, off -> false).
|
||||
var weak map[interface{}]interface{}
|
||||
if err := yaml.Unmarshal(cfg, &weak); err != nil {
|
||||
matches := yamlLineError.FindStringSubmatch(err.Error())
|
||||
if len(matches) == 3 {
|
||||
line, err := strconv.Atoi(matches[1])
|
||||
if err != nil {
|
||||
return Node{}, err
|
||||
}
|
||||
msg := matches[2]
|
||||
report.Error(line, msg)
|
||||
return Node{}, nil
|
||||
}
|
||||
|
||||
matches = yamlError.FindStringSubmatch(err.Error())
|
||||
if len(matches) == 2 {
|
||||
report.Error(1, matches[1])
|
||||
return Node{}, nil
|
||||
}
|
||||
|
||||
return Node{}, errors.New("couldn't parse yaml error")
|
||||
}
|
||||
w := NewNode(weak, NewContext(cfg))
|
||||
w = normalizeNodeNames(w, report)
|
||||
|
||||
// unmarshal the config into the explicitly-typed form.
|
||||
yaml.UnmarshalMappingKeyTransform = func(nameIn string) (nameOut string) {
|
||||
return strings.Replace(nameIn, "-", "_", -1)
|
||||
}
|
||||
var strong config.CloudConfig
|
||||
if err := yaml.Unmarshal([]byte(cfg), &strong); err != nil {
|
||||
return Node{}, err
|
||||
}
|
||||
s := NewNode(strong, NewContext(cfg))
|
||||
|
||||
// coerceNodes weak nodes and strong nodes. strong nodes replace weak nodes
|
||||
// if they are compatible types (this happens when the yaml library
|
||||
// converts the input).
|
||||
// (e.g. weak 484 is replaced by strong 0744, weak 4 is not replaced by
|
||||
// strong false)
|
||||
return coerceNodes(w, s), nil
|
||||
}
|
||||
|
||||
// coerceNodes recursively evaluates two nodes, returning a new node containing
|
||||
// either the weak or strong node's value and its recursively processed
|
||||
// children. The strong node's value is used if the two nodes are leafs, are
|
||||
// both valid, and are compatible types (defined by isCompatible()). The weak
|
||||
// node is returned in all other cases. coerceNodes is used to counteract the
|
||||
// effects of yaml's automatic type conversion. The weak node is the one
|
||||
// resulting from unmarshalling into an empty interface{} (the type is
|
||||
// inferred). The strong node is the one resulting from unmarshalling into a
|
||||
// struct. If the two nodes are of compatible types, the yaml library correctly
|
||||
// parsed the value into the strongly typed unmarshalling. In this case, we
|
||||
// prefer the strong node because its actually the type we are expecting.
|
||||
func coerceNodes(w, s Node) Node {
|
||||
n := w
|
||||
n.children = nil
|
||||
if len(w.children) == 0 && len(s.children) == 0 &&
|
||||
w.IsValid() && s.IsValid() &&
|
||||
isCompatible(w.Kind(), s.Kind()) {
|
||||
n.Value = s.Value
|
||||
}
|
||||
|
||||
for _, cw := range w.children {
|
||||
n.children = append(n.children, coerceNodes(cw, s.Child(cw.name)))
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// normalizeNodeNames replaces all occurences of '-' with '_' within key names
|
||||
// and makes a note of each replacement in the report.
|
||||
func normalizeNodeNames(node Node, report *Report) Node {
|
||||
if strings.Contains(node.name, "-") {
|
||||
// TODO(crawford): Enable this message once the new validator hits stable.
|
||||
//report.Info(node.line, fmt.Sprintf("%q uses '-' instead of '_'", node.name))
|
||||
node.name = strings.Replace(node.name, "-", "_", -1)
|
||||
}
|
||||
for i := range node.children {
|
||||
node.children[i] = normalizeNodeNames(node.children[i], report)
|
||||
}
|
||||
return node
|
||||
}
|
||||
177
config/cloudinit/config/validate/validate_test.go
Normal file
177
config/cloudinit/config/validate/validate_test.go
Normal file
@@ -0,0 +1,177 @@
|
||||
// Copyright 2015 CoreOS, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package validate
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseCloudConfig(t *testing.T) {
|
||||
tests := []struct {
|
||||
config string
|
||||
|
||||
entries []Entry
|
||||
}{
|
||||
{},
|
||||
{
|
||||
config: " ",
|
||||
entries: []Entry{{entryError, "found character that cannot start any token", 1}},
|
||||
},
|
||||
{
|
||||
config: "a:\na",
|
||||
entries: []Entry{{entryError, "could not find expected ':'", 2}},
|
||||
},
|
||||
{
|
||||
config: "#hello\na:\na",
|
||||
entries: []Entry{{entryError, "could not find expected ':'", 3}},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
r := Report{}
|
||||
parseCloudConfig([]byte(tt.config), &r)
|
||||
|
||||
if e := r.Entries(); !reflect.DeepEqual(tt.entries, e) {
|
||||
t.Errorf("bad report (%s): want %#v, got %#v", tt.config, tt.entries, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateCloudConfig(t *testing.T) {
|
||||
tests := []struct {
|
||||
config string
|
||||
rules []rule
|
||||
|
||||
report Report
|
||||
err error
|
||||
}{
|
||||
{
|
||||
rules: []rule{func(_ Node, _ *Report) { panic("something happened") }},
|
||||
err: errors.New("something happened"),
|
||||
},
|
||||
{
|
||||
config: "write_files:\n - permissions: 0744",
|
||||
rules: Rules,
|
||||
},
|
||||
{
|
||||
config: "write_files:\n - permissions: '0744'",
|
||||
rules: Rules,
|
||||
},
|
||||
{
|
||||
config: "write_files:\n - permissions: 744",
|
||||
rules: Rules,
|
||||
},
|
||||
{
|
||||
config: "write_files:\n - permissions: '744'",
|
||||
rules: Rules,
|
||||
},
|
||||
{
|
||||
config: "coreos:\n update:\n reboot-strategy: off",
|
||||
rules: Rules,
|
||||
},
|
||||
{
|
||||
config: "coreos:\n update:\n reboot-strategy: false",
|
||||
rules: Rules,
|
||||
report: Report{entries: []Entry{{entryError, "invalid value false", 3}}},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
r, err := validateCloudConfig([]byte(tt.config), tt.rules)
|
||||
if !reflect.DeepEqual(tt.err, err) {
|
||||
t.Errorf("bad error (%s): want %v, got %v", tt.config, tt.err, err)
|
||||
}
|
||||
if !reflect.DeepEqual(tt.report, r) {
|
||||
t.Errorf("bad report (%s): want %+v, got %+v", tt.config, tt.report, r)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidate(t *testing.T) {
|
||||
tests := []struct {
|
||||
config string
|
||||
|
||||
report Report
|
||||
}{
|
||||
{},
|
||||
{
|
||||
config: "#!/bin/bash\necho hey",
|
||||
},
|
||||
{
|
||||
config: "{}",
|
||||
report: Report{entries: []Entry{{entryError, `must be "#cloud-config" or begin with "#!"`, 1}}},
|
||||
},
|
||||
{
|
||||
config: `{"ignitionVersion":0}`,
|
||||
},
|
||||
{
|
||||
config: `{"ignitionVersion":1}`,
|
||||
},
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
r, err := Validate([]byte(tt.config))
|
||||
if err != nil {
|
||||
t.Errorf("bad error (case #%d): want %v, got %v", i, nil, err)
|
||||
}
|
||||
if !reflect.DeepEqual(tt.report, r) {
|
||||
t.Errorf("bad report (case #%d): want %+v, got %+v", i, tt.report, r)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkValidate(b *testing.B) {
|
||||
config := `#cloud-config
|
||||
hostname: test
|
||||
|
||||
coreos:
|
||||
etcd:
|
||||
name: node001
|
||||
discovery: https://discovery.etcd.io/disco
|
||||
addr: $public_ipv4:4001
|
||||
peer-addr: $private_ipv4:7001
|
||||
fleet:
|
||||
verbosity: 2
|
||||
metadata: "hi"
|
||||
update:
|
||||
reboot-strategy: off
|
||||
units:
|
||||
- name: hi.service
|
||||
command: start
|
||||
enable: true
|
||||
- name: bye.service
|
||||
command: stop
|
||||
|
||||
ssh_authorized_keys:
|
||||
- ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC0g+ZTxC7weoIJLUafOgrm+h...
|
||||
- ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC0g+ZTxC7weoIJLUafOgrm+h...
|
||||
|
||||
users:
|
||||
- name: me
|
||||
|
||||
write_files:
|
||||
- path: /etc/yes
|
||||
content: "Hi"
|
||||
|
||||
manage_etc_hosts: localhost`
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
if _, err := Validate([]byte(config)); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
169
config/cloudinit/datasource/configdrive/configdrive.go
Normal file
169
config/cloudinit/datasource/configdrive/configdrive.go
Normal file
@@ -0,0 +1,169 @@
|
||||
// Copyright 2015 CoreOS, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package configdrive
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"syscall"
|
||||
|
||||
"github.com/rancher/os/log"
|
||||
|
||||
"github.com/docker/docker/pkg/mount"
|
||||
"github.com/rancher/os/config/cloudinit/datasource"
|
||||
"github.com/rancher/os/util"
|
||||
)
|
||||
|
||||
const (
|
||||
configDevName = "config-2"
|
||||
configDev = "LABEL=" + configDevName
|
||||
configDevMountPoint = "/media/config-2"
|
||||
openstackAPIVersion = "latest"
|
||||
)
|
||||
|
||||
type ConfigDrive struct {
|
||||
root string
|
||||
readFile func(filename string) ([]byte, error)
|
||||
lastError error
|
||||
availabilityChanges bool
|
||||
}
|
||||
|
||||
func NewDatasource(root string) *ConfigDrive {
|
||||
return &ConfigDrive{root, ioutil.ReadFile, nil, true}
|
||||
}
|
||||
|
||||
func (cd *ConfigDrive) IsAvailable() bool {
|
||||
if cd.root == configDevMountPoint {
|
||||
cd.lastError = MountConfigDrive()
|
||||
if cd.lastError != nil {
|
||||
log.Error(cd.lastError)
|
||||
// Don't keep retrying if we can't mount
|
||||
cd.availabilityChanges = false
|
||||
return false
|
||||
}
|
||||
defer cd.Finish()
|
||||
}
|
||||
|
||||
_, cd.lastError = os.Stat(cd.root)
|
||||
return !os.IsNotExist(cd.lastError)
|
||||
// TODO: consider changing IsNotExists to not-available _and_ does not change
|
||||
}
|
||||
|
||||
func (cd *ConfigDrive) Finish() error {
|
||||
return UnmountConfigDrive()
|
||||
}
|
||||
|
||||
func (cd *ConfigDrive) String() string {
|
||||
if cd.lastError != nil {
|
||||
return fmt.Sprintf("%s: %s (lastError: %s)", cd.Type(), cd.root, cd.lastError)
|
||||
}
|
||||
return fmt.Sprintf("%s: %s", cd.Type(), cd.root)
|
||||
}
|
||||
|
||||
func (cd *ConfigDrive) AvailabilityChanges() bool {
|
||||
return cd.availabilityChanges
|
||||
}
|
||||
|
||||
func (cd *ConfigDrive) ConfigRoot() string {
|
||||
return cd.openstackRoot()
|
||||
}
|
||||
|
||||
func (cd *ConfigDrive) FetchMetadata() (metadata datasource.Metadata, err error) {
|
||||
var data []byte
|
||||
var m struct {
|
||||
SSHAuthorizedKeyMap map[string]string `json:"public_keys"`
|
||||
Hostname string `json:"hostname"`
|
||||
NetworkConfig struct {
|
||||
ContentPath string `json:"content_path"`
|
||||
} `json:"network_config"`
|
||||
}
|
||||
|
||||
if data, err = cd.tryReadFile(path.Join(cd.openstackVersionRoot(), "meta_data.json")); err != nil || len(data) == 0 {
|
||||
return
|
||||
}
|
||||
if err = json.Unmarshal([]byte(data), &m); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
metadata.SSHPublicKeys = m.SSHAuthorizedKeyMap
|
||||
metadata.Hostname = m.Hostname
|
||||
// TODO: I don't think we've used this for anything
|
||||
/* if m.NetworkConfig.ContentPath != "" {
|
||||
metadata.NetworkConfig, err = cd.tryReadFile(path.Join(cd.openstackRoot(), m.NetworkConfig.ContentPath))
|
||||
}
|
||||
*/
|
||||
return
|
||||
}
|
||||
|
||||
func (cd *ConfigDrive) FetchUserdata() ([]byte, error) {
|
||||
return cd.tryReadFile(path.Join(cd.openstackVersionRoot(), "user_data"))
|
||||
}
|
||||
|
||||
func (cd *ConfigDrive) Type() string {
|
||||
return "cloud-drive"
|
||||
}
|
||||
|
||||
func (cd *ConfigDrive) openstackRoot() string {
|
||||
return path.Join(cd.root, "openstack")
|
||||
}
|
||||
|
||||
func (cd *ConfigDrive) openstackVersionRoot() string {
|
||||
return path.Join(cd.openstackRoot(), openstackAPIVersion)
|
||||
}
|
||||
|
||||
func (cd *ConfigDrive) tryReadFile(filename string) ([]byte, error) {
|
||||
if cd.root == configDevMountPoint {
|
||||
cd.lastError = MountConfigDrive()
|
||||
if cd.lastError != nil {
|
||||
log.Error(cd.lastError)
|
||||
return nil, cd.lastError
|
||||
}
|
||||
defer cd.Finish()
|
||||
}
|
||||
log.Debugf("Attempting to read from %q\n", filename)
|
||||
data, err := cd.readFile(filename)
|
||||
if os.IsNotExist(err) {
|
||||
err = nil
|
||||
}
|
||||
if err != nil {
|
||||
log.Errorf("ERROR read cloud-config file(%s) - err: %q", filename, err)
|
||||
}
|
||||
return data, err
|
||||
}
|
||||
|
||||
func MountConfigDrive() error {
|
||||
if err := os.MkdirAll(configDevMountPoint, 700); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
configDev := util.ResolveDevice(configDev)
|
||||
|
||||
if configDev == "" {
|
||||
return mount.Mount(configDevName, configDevMountPoint, "9p", "trans=virtio,version=9p2000.L")
|
||||
}
|
||||
|
||||
fsType, err := util.GetFsType(configDev)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return mount.Mount(configDev, configDevMountPoint, fsType, "ro")
|
||||
}
|
||||
|
||||
func UnmountConfigDrive() error {
|
||||
return syscall.Unmount(configDevMountPoint, 0)
|
||||
}
|
||||
144
config/cloudinit/datasource/configdrive/configdrive_test.go
Normal file
144
config/cloudinit/datasource/configdrive/configdrive_test.go
Normal file
@@ -0,0 +1,144 @@
|
||||
// Copyright 2015 CoreOS, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package configdrive
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/rancher/os/config/cloudinit/datasource"
|
||||
"github.com/rancher/os/config/cloudinit/datasource/test"
|
||||
)
|
||||
|
||||
func TestFetchMetadata(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
root string
|
||||
files test.MockFilesystem
|
||||
|
||||
metadata datasource.Metadata
|
||||
}{
|
||||
{
|
||||
root: "/",
|
||||
files: test.NewMockFilesystem(test.File{Path: "/openstack/latest/meta_data.json", Contents: ""}),
|
||||
},
|
||||
{
|
||||
root: "/",
|
||||
files: test.NewMockFilesystem(test.File{Path: "/openstack/latest/meta_data.json", Contents: `{"ignore": "me"}`}),
|
||||
},
|
||||
{
|
||||
root: "/",
|
||||
files: test.NewMockFilesystem(test.File{Path: "/openstack/latest/meta_data.json", Contents: `{"hostname": "host"}`}),
|
||||
metadata: datasource.Metadata{Hostname: "host"},
|
||||
},
|
||||
{
|
||||
root: "/media/configdrive",
|
||||
files: test.NewMockFilesystem(test.File{Path: "/media/configdrive/openstack/latest/meta_data.json", Contents: `{"hostname": "host", "network_config": {"content_path": "config_file.json"}, "public_keys":{"1": "key1", "2": "key2"}}`},
|
||||
test.File{Path: "/media/configdrive/openstack/config_file.json", Contents: "make it work"},
|
||||
),
|
||||
metadata: datasource.Metadata{
|
||||
Hostname: "host",
|
||||
SSHPublicKeys: map[string]string{
|
||||
"1": "key1",
|
||||
"2": "key2",
|
||||
},
|
||||
},
|
||||
},
|
||||
} {
|
||||
cd := ConfigDrive{tt.root, tt.files.ReadFile, nil, true}
|
||||
metadata, err := cd.FetchMetadata()
|
||||
if err != nil {
|
||||
t.Fatalf("bad error for %+v: want %v, got %q", tt, nil, err)
|
||||
}
|
||||
if !reflect.DeepEqual(tt.metadata, metadata) {
|
||||
t.Fatalf("bad metadata for %+v: want %#v, got %#v", tt, tt.metadata, metadata)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetchUserdata(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
root string
|
||||
files test.MockFilesystem
|
||||
|
||||
userdata string
|
||||
}{
|
||||
{
|
||||
"/",
|
||||
test.NewMockFilesystem(),
|
||||
"",
|
||||
},
|
||||
{
|
||||
"/",
|
||||
test.NewMockFilesystem(test.File{Path: "/openstack/latest/user_data", Contents: "userdata"}),
|
||||
"userdata",
|
||||
},
|
||||
{
|
||||
"/media/configdrive",
|
||||
test.NewMockFilesystem(test.File{Path: "/media/configdrive/openstack/latest/user_data", Contents: "userdata"}),
|
||||
"userdata",
|
||||
},
|
||||
} {
|
||||
cd := ConfigDrive{tt.root, tt.files.ReadFile, nil, true}
|
||||
userdata, err := cd.FetchUserdata()
|
||||
if err != nil {
|
||||
t.Fatalf("bad error for %+v: want %v, got %q", tt, nil, err)
|
||||
}
|
||||
if string(userdata) != tt.userdata {
|
||||
t.Fatalf("bad userdata for %+v: want %q, got %q", tt, tt.userdata, userdata)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigRoot(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
root string
|
||||
configRoot string
|
||||
}{
|
||||
{
|
||||
"/",
|
||||
"/openstack",
|
||||
},
|
||||
{
|
||||
"/media/configdrive",
|
||||
"/media/configdrive/openstack",
|
||||
},
|
||||
} {
|
||||
cd := ConfigDrive{tt.root, nil, nil, true}
|
||||
if configRoot := cd.ConfigRoot(); configRoot != tt.configRoot {
|
||||
t.Fatalf("bad config root for %q: want %q, got %q", tt, tt.configRoot, configRoot)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewDatasource(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
root string
|
||||
expectRoot string
|
||||
}{
|
||||
{
|
||||
root: "",
|
||||
expectRoot: "",
|
||||
},
|
||||
{
|
||||
root: "/media/configdrive",
|
||||
expectRoot: "/media/configdrive",
|
||||
},
|
||||
} {
|
||||
service := NewDatasource(tt.root)
|
||||
if service.root != tt.expectRoot {
|
||||
t.Fatalf("bad root (%q): want %q, got %q", tt.root, tt.expectRoot, service.root)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,8 @@ package datasource
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"github.com/rancher/os/netconf"
|
||||
)
|
||||
|
||||
type Datasource interface {
|
||||
@@ -25,14 +27,21 @@ type Datasource interface {
|
||||
FetchMetadata() (Metadata, error)
|
||||
FetchUserdata() ([]byte, error)
|
||||
Type() string
|
||||
String() string
|
||||
// Finish gives the datasource the oportunity to clean up, unmount or release any open / cache resources
|
||||
Finish() error
|
||||
}
|
||||
|
||||
type Metadata struct {
|
||||
PublicIPv4 net.IP
|
||||
PublicIPv6 net.IP
|
||||
PrivateIPv4 net.IP
|
||||
PrivateIPv6 net.IP
|
||||
// TODO: move to netconf/types.go ?
|
||||
// see https://ahmetalpbalkan.com/blog/comparison-of-instance-metadata-services/
|
||||
Hostname string
|
||||
SSHPublicKeys map[string]string
|
||||
NetworkConfig interface{}
|
||||
NetworkConfig netconf.NetworkConfig
|
||||
RootDisk string
|
||||
|
||||
PublicIPv4 net.IP
|
||||
PublicIPv6 net.IP
|
||||
PrivateIPv4 net.IP
|
||||
PrivateIPv6 net.IP
|
||||
}
|
||||
@@ -15,41 +15,51 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
|
||||
"github.com/coreos/coreos-cloudinit/datasource"
|
||||
"github.com/rancher/os/config/cloudinit/datasource"
|
||||
)
|
||||
|
||||
type localFile struct {
|
||||
path string
|
||||
type LocalFile struct {
|
||||
path string
|
||||
lastError error
|
||||
}
|
||||
|
||||
func NewDatasource(path string) *localFile {
|
||||
return &localFile{path}
|
||||
func NewDatasource(path string) *LocalFile {
|
||||
return &LocalFile{path, nil}
|
||||
}
|
||||
|
||||
func (f *localFile) IsAvailable() bool {
|
||||
_, err := os.Stat(f.path)
|
||||
return !os.IsNotExist(err)
|
||||
func (f *LocalFile) IsAvailable() bool {
|
||||
_, f.lastError = os.Stat(f.path)
|
||||
return !os.IsNotExist(f.lastError)
|
||||
}
|
||||
|
||||
func (f *localFile) AvailabilityChanges() bool {
|
||||
func (f *LocalFile) Finish() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *LocalFile) String() string {
|
||||
return fmt.Sprintf("%s: %s (lastError: %s)", f.Type(), f.path, f.lastError)
|
||||
}
|
||||
|
||||
func (f *LocalFile) AvailabilityChanges() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (f *localFile) ConfigRoot() string {
|
||||
func (f *LocalFile) ConfigRoot() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (f *localFile) FetchMetadata() (datasource.Metadata, error) {
|
||||
func (f *LocalFile) FetchMetadata() (datasource.Metadata, error) {
|
||||
return datasource.Metadata{}, nil
|
||||
}
|
||||
|
||||
func (f *localFile) FetchUserdata() ([]byte, error) {
|
||||
func (f *LocalFile) FetchUserdata() ([]byte, error) {
|
||||
return ioutil.ReadFile(f.path)
|
||||
}
|
||||
|
||||
func (f *localFile) Type() string {
|
||||
func (f *LocalFile) Type() string {
|
||||
return "local-file"
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user