Compare commits
742 Commits
xo-server-
...
xo-server-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
68e37fff79 | ||
|
|
6958e71efd | ||
|
|
d0618182d1 | ||
|
|
e8891e27dd | ||
|
|
72baa1a786 | ||
|
|
4d72569030 | ||
|
|
532a368606 | ||
|
|
05a68030b6 | ||
|
|
db3728f9e4 | ||
|
|
ca2d3f5b48 | ||
|
|
f3a992a55f | ||
|
|
b5d1e7c459 | ||
|
|
eecd89772f | ||
|
|
5fd42bf216 | ||
|
|
3e55d8d9df | ||
|
|
d9f4a196d0 | ||
|
|
768ef8a316 | ||
|
|
a478d02e5d | ||
|
|
b98a7de3ae | ||
|
|
45dd2c6519 | ||
|
|
5176d2000e | ||
|
|
6f606761e4 | ||
|
|
cfabadffe4 | ||
|
|
7523cb3489 | ||
|
|
505a9d7c70 | ||
|
|
9581764cc8 | ||
|
|
f03493a252 | ||
|
|
22f2a05c8a | ||
|
|
a703ecc7e1 | ||
|
|
c90a687179 | ||
|
|
e08689ff0e | ||
|
|
80ffd811c9 | ||
|
|
56b583fc99 | ||
|
|
37a4e108be | ||
|
|
9ca531541d | ||
|
|
e304c554d0 | ||
|
|
a7dfa7a381 | ||
|
|
090d48b636 | ||
|
|
55567bf666 | ||
|
|
5867d84eaa | ||
|
|
4d8f1ab169 | ||
|
|
6a2963be41 | ||
|
|
b10f7b35ee | ||
|
|
dc1bb8992f | ||
|
|
5eb7119821 | ||
|
|
3fa8fc19dc | ||
|
|
341d98e00d | ||
|
|
7eee0f4341 | ||
|
|
0b71b7862a | ||
|
|
e2893a0eba | ||
|
|
0c39a4e193 | ||
|
|
066072b22d | ||
|
|
d548503590 | ||
|
|
3a02fc99a2 | ||
|
|
d8ae943d8a | ||
|
|
63dae9ed70 | ||
|
|
97f57f1f2b | ||
|
|
060ba6423e | ||
|
|
f31bbcdaab | ||
|
|
2a4bae54ab | ||
|
|
95df2f66a3 | ||
|
|
fdd33abe91 | ||
|
|
044c9bed4c | ||
|
|
81062638eb | ||
|
|
8c0028055a | ||
|
|
74f060b309 | ||
|
|
12de0ca463 | ||
|
|
72ed59b65b | ||
|
|
fe369bfa18 | ||
|
|
c708fd65d7 | ||
|
|
0c497900a2 | ||
|
|
0b92ceec90 | ||
|
|
7e5131c4d2 | ||
|
|
d585b47360 | ||
|
|
4530d95f48 | ||
|
|
013d4b9411 | ||
|
|
0a8fed1950 | ||
|
|
6dc4b4dc1b | ||
|
|
b25f418411 | ||
|
|
985a4780f5 | ||
|
|
92a1f2c6d5 | ||
|
|
81b9348e50 | ||
|
|
04e7d54620 | ||
|
|
729dbe16c0 | ||
|
|
974650bc56 | ||
|
|
8c9ed833c3 | ||
|
|
5e86e64e18 | ||
|
|
235c789c5e | ||
|
|
ab48d06967 | ||
|
|
18ca950cd2 | ||
|
|
82489b36c8 | ||
|
|
a67b6130f8 | ||
|
|
eab007db6e | ||
|
|
889eae276e | ||
|
|
2b2a72252b | ||
|
|
13e4568d3b | ||
|
|
92c4dda801 | ||
|
|
3e59ba4563 | ||
|
|
99c95626df | ||
|
|
20a9fc2497 | ||
|
|
fb32aeeeb6 | ||
|
|
76cb4037d4 | ||
|
|
af1530db36 | ||
|
|
9d6560aece | ||
|
|
caba246e0b | ||
|
|
731e2dc4c4 | ||
|
|
815bdf3454 | ||
|
|
b36ef9fdb1 | ||
|
|
fee3f7a716 | ||
|
|
dd00d6581f | ||
|
|
f4fc7acf4d | ||
|
|
5b8cdf06b3 | ||
|
|
9b3668423e | ||
|
|
7227af9aac | ||
|
|
7c8194307e | ||
|
|
9b5fac9e2b | ||
|
|
47991b7d1a | ||
|
|
a5ea24311a | ||
|
|
3c11f0acda | ||
|
|
4294dfd8fe | ||
|
|
e7b739bb3b | ||
|
|
baf5f7491d | ||
|
|
a2afe2fa1a | ||
|
|
8496a9bebd | ||
|
|
0d6b7d6f04 | ||
|
|
59d6a51635 | ||
|
|
b4693019f7 | ||
|
|
320d7a89ca | ||
|
|
1016f5f26f | ||
|
|
9284aee3fa | ||
|
|
32726efe88 | ||
|
|
87daf0271c | ||
|
|
70e73a5a65 | ||
|
|
588c369615 | ||
|
|
9aa9d4452c | ||
|
|
d7fe25c4fc | ||
|
|
990c5e9f08 | ||
|
|
7e2f2f6102 | ||
|
|
51def6535f | ||
|
|
14156b0911 | ||
|
|
e2ba1fa7f8 | ||
|
|
cdf1a5fe47 | ||
|
|
abf146707f | ||
|
|
c24a4009c8 | ||
|
|
7b928c4d41 | ||
|
|
4d50eae3c9 | ||
|
|
c3a5e0592d | ||
|
|
207aef7cb3 | ||
|
|
fbbd9ae249 | ||
|
|
7aa591ffbd | ||
|
|
1fd91de50d | ||
|
|
b87ad2df54 | ||
|
|
c9e2f94daf | ||
|
|
52774c7d6d | ||
|
|
01686b8e60 | ||
|
|
0317d6a862 | ||
|
|
2a316b1ffa | ||
|
|
b120146cdc | ||
|
|
7587458f99 | ||
|
|
f5fb066975 | ||
|
|
cd6a0fa678 | ||
|
|
e735420dd8 | ||
|
|
bc2f17c840 | ||
|
|
8c459cac0f | ||
|
|
878d2d9260 | ||
|
|
14bca4bbf7 | ||
|
|
ea55c10c4d | ||
|
|
75cde40b0e | ||
|
|
5434b4987f | ||
|
|
74fba76b85 | ||
|
|
2b8996e965 | ||
|
|
4673af6fd8 | ||
|
|
10fddc51bb | ||
|
|
2d14fde671 | ||
|
|
98cd2746ef | ||
|
|
ea18e4129c | ||
|
|
6980e2b959 | ||
|
|
7e7ec83c12 | ||
|
|
a8340c24c3 | ||
|
|
f49f3fb2a6 | ||
|
|
3c9ef8d199 | ||
|
|
16dde5c772 | ||
|
|
96635a98f5 | ||
|
|
54ef65ced9 | ||
|
|
4fb6bef04c | ||
|
|
224a79840d | ||
|
|
99ae3e0f7f | ||
|
|
02a4161ecb | ||
|
|
1a68c3947d | ||
|
|
d56590c6e6 | ||
|
|
f7b6fcf684 | ||
|
|
df3ec9a629 | ||
|
|
6bc4bf308b | ||
|
|
2ddb84f457 | ||
|
|
6b82cc7510 | ||
|
|
ac6d14113a | ||
|
|
398db72b44 | ||
|
|
a3da3299fa | ||
|
|
e763db7102 | ||
|
|
2b504ce5ab | ||
|
|
39a16f9a7f | ||
|
|
f3ea8d012f | ||
|
|
1b9aa63096 | ||
|
|
11bde53069 | ||
|
|
f115ee18c4 | ||
|
|
162a56232c | ||
|
|
de6f0ef8eb | ||
|
|
cd8a92c30b | ||
|
|
10030c4959 | ||
|
|
ffc155c341 | ||
|
|
42ea76eb2a | ||
|
|
be503f1341 | ||
|
|
6f4509c260 | ||
|
|
b2331084d1 | ||
|
|
ab3a594884 | ||
|
|
592adcf42e | ||
|
|
978c881ab7 | ||
|
|
99727447ef | ||
|
|
e02cb56ee0 | ||
|
|
02f75fb2e1 | ||
|
|
6cced719dc | ||
|
|
45ec0b9b01 | ||
|
|
2451ac3ade | ||
|
|
45a94fe73d | ||
|
|
e1173e6565 | ||
|
|
46f6911ef8 | ||
|
|
7629bf5be2 | ||
|
|
fb17de7988 | ||
|
|
32f4d42e59 | ||
|
|
0031c2a9b7 | ||
|
|
509e99913a | ||
|
|
c639cfcfd9 | ||
|
|
b140a2ca3e | ||
|
|
aa448e7a41 | ||
|
|
220750f887 | ||
|
|
808cc5d8d0 | ||
|
|
02e1a32fae | ||
|
|
d1690bda81 | ||
|
|
4d4a2897a5 | ||
|
|
cd73c8f82f | ||
|
|
934e67d146 | ||
|
|
4c96e44b9b | ||
|
|
189900549a | ||
|
|
6d18420a5d | ||
|
|
ab3d307393 | ||
|
|
89b2156f61 | ||
|
|
2f95da1892 | ||
|
|
306d5d8fc7 | ||
|
|
5553d5fefa | ||
|
|
9328518bbc | ||
|
|
75bb7d5a2d | ||
|
|
ceab4e37cd | ||
|
|
afb6974cc0 | ||
|
|
a4dc965c23 | ||
|
|
8b65c280a8 | ||
|
|
4ab24d2fe5 | ||
|
|
c70ca2ff64 | ||
|
|
649ab26da8 | ||
|
|
c921ea6eb7 | ||
|
|
58aed76aa3 | ||
|
|
e286c57ce4 | ||
|
|
840f0b6379 | ||
|
|
538025edd5 | ||
|
|
1e7d1b1628 | ||
|
|
defd42f74e | ||
|
|
aa54ab6e51 | ||
|
|
f0c28c74d8 | ||
|
|
3e285d6131 | ||
|
|
c96d94329e | ||
|
|
627227f2f9 | ||
|
|
42cef0da88 | ||
|
|
06f60b7d92 | ||
|
|
3ddb4d2b23 | ||
|
|
5a825bd459 | ||
|
|
ab1f08f687 | ||
|
|
2f89e3658a | ||
|
|
f08ab729bd | ||
|
|
052c974369 | ||
|
|
e760e868c1 | ||
|
|
5e3831a1a4 | ||
|
|
99e046ddea | ||
|
|
12e0759711 | ||
|
|
da0c1cec22 | ||
|
|
d23df2ab15 | ||
|
|
dbe828097c | ||
|
|
48a0623ded | ||
|
|
3527b86ec5 | ||
|
|
fe7a9104a8 | ||
|
|
cbfb94afcb | ||
|
|
74d8eff6d8 | ||
|
|
d7ed9ab64e | ||
|
|
3b6c5898fe | ||
|
|
ae22adc920 | ||
|
|
5dacf9c3f5 | ||
|
|
9129bfa284 | ||
|
|
96190c21d6 | ||
|
|
aa117a0ee3 | ||
|
|
39b0ea381b | ||
|
|
021cea0b34 | ||
|
|
eaad41fe55 | ||
|
|
e25d58d70a | ||
|
|
9c0967170a | ||
|
|
abd89df365 | ||
|
|
651e4bb775 | ||
|
|
7f06d6e68c | ||
|
|
e5146f7def | ||
|
|
d9bf7c7d12 | ||
|
|
b0bf18e235 | ||
|
|
f001b2c68f | ||
|
|
296141ad3d | ||
|
|
7abba0a69b | ||
|
|
dee7767427 | ||
|
|
087a71367d | ||
|
|
bec2e3b4a0 | ||
|
|
274884ef4d | ||
|
|
d3f52cdd1a | ||
|
|
cb97e37c15 | ||
|
|
3013fa86b6 | ||
|
|
2593743746 | ||
|
|
ab6bd56006 | ||
|
|
4095d6dcf6 | ||
|
|
cd4221e4f2 | ||
|
|
548754821e | ||
|
|
77f85579e3 | ||
|
|
05c325d686 | ||
|
|
adb71ad174 | ||
|
|
4a95f5cd9d | ||
|
|
f3b368fae4 | ||
|
|
20919a8a39 | ||
|
|
f2e86efc4d | ||
|
|
98b27d647e | ||
|
|
bf586d0837 | ||
|
|
ed45a9b156 | ||
|
|
f4ea39b602 | ||
|
|
f26fc9a0f0 | ||
|
|
3fd637b3c7 | ||
|
|
ea605383d5 | ||
|
|
a4fa670dc5 | ||
|
|
7d7cc56527 | ||
|
|
85d0271b86 | ||
|
|
749d5e22bb | ||
|
|
61c61adea1 | ||
|
|
c8a6fd19a7 | ||
|
|
0b143b580a | ||
|
|
ed69fedc0a | ||
|
|
ea0db57388 | ||
|
|
e6ef6ccccf | ||
|
|
4826e14cad | ||
|
|
5515f90147 | ||
|
|
2193c26acb | ||
|
|
1974a2c0e4 | ||
|
|
84fbe9ee06 | ||
|
|
b8e2cfc47f | ||
|
|
553fc6f5d9 | ||
|
|
f5d790b264 | ||
|
|
641e13496e | ||
|
|
a6e18819d4 | ||
|
|
faf5ff6aa4 | ||
|
|
ae20ca5558 | ||
|
|
22bd87c965 | ||
|
|
2129645f39 | ||
|
|
93a07b6207 | ||
|
|
7fa1923400 | ||
|
|
4c165bd620 | ||
|
|
7c04a455b4 | ||
|
|
06b6061518 | ||
|
|
3821ee3dcd | ||
|
|
03a33646d6 | ||
|
|
791183553e | ||
|
|
de6ef49043 | ||
|
|
28bf7ee90b | ||
|
|
4d1ca7ede4 | ||
|
|
f3b46515c5 | ||
|
|
0aa5e7ba63 | ||
|
|
8d8bf43b46 | ||
|
|
9983407c8b | ||
|
|
2471ad4215 | ||
|
|
f266982560 | ||
|
|
c059a416f7 | ||
|
|
099db6792a | ||
|
|
74a31f3301 | ||
|
|
f88c0b9b67 | ||
|
|
61ef313b1c | ||
|
|
2fa081a4ba | ||
|
|
d33af742dd | ||
|
|
823879e9f9 | ||
|
|
98eb285e14 | ||
|
|
37fd2e1103 | ||
|
|
56db5dc341 | ||
|
|
d48fa235b1 | ||
|
|
06a111495b | ||
|
|
f3fb0797bf | ||
|
|
561b8f4eed | ||
|
|
8cfc6f0b1d | ||
|
|
7e04f26f78 | ||
|
|
348c30b61e | ||
|
|
a4e9f1a683 | ||
|
|
f8c74daef5 | ||
|
|
b3a593afd7 | ||
|
|
58aa2b6a49 | ||
|
|
fb06905c86 | ||
|
|
4a2911557d | ||
|
|
99caa5dddc | ||
|
|
12c4680501 | ||
|
|
b7e05c236f | ||
|
|
0f03208aa1 | ||
|
|
d58add18fc | ||
|
|
3a0413d8bb | ||
|
|
9122f9b291 | ||
|
|
d279db2a0e | ||
|
|
c6657b9619 | ||
|
|
80d8388eb6 | ||
|
|
b1ee4bdc09 | ||
|
|
5b782993fd | ||
|
|
138e60e77c | ||
|
|
9771402c54 | ||
|
|
30dcb4d8d2 | ||
|
|
c418c766d8 | ||
|
|
334d843955 | ||
|
|
2e5169eb46 | ||
|
|
733c619b1f | ||
|
|
2021b644c0 | ||
|
|
f55ed13bd2 | ||
|
|
98395abc17 | ||
|
|
5db5c4e52c | ||
|
|
71e77ad45a | ||
|
|
25873e0e02 | ||
|
|
22638a8147 | ||
|
|
ce7bc9f438 | ||
|
|
43a362d0eb | ||
|
|
7d7e6e10b9 | ||
|
|
73821b0f12 | ||
|
|
2071a7d308 | ||
|
|
c439daadad | ||
|
|
083f325076 | ||
|
|
ee53433dcc | ||
|
|
ad10d13a75 | ||
|
|
4fd9639457 | ||
|
|
2f2ee1f431 | ||
|
|
a2f5f1cb0e | ||
|
|
1fbe7d92eb | ||
|
|
760974c7c7 | ||
|
|
e1587d11b1 | ||
|
|
0595360808 | ||
|
|
1a8149e456 | ||
|
|
fd6f92f6b5 | ||
|
|
ddf7226ba8 | ||
|
|
a1cd95752a | ||
|
|
d131a26a41 | ||
|
|
10a7c75001 | ||
|
|
1f454ababf | ||
|
|
84c9532456 | ||
|
|
aa7c9bca46 | ||
|
|
3d00b4ffbe | ||
|
|
0304d6079d | ||
|
|
ae41e64999 | ||
|
|
8c9f32c927 | ||
|
|
b4c612ff6d | ||
|
|
e708268067 | ||
|
|
e61873f335 | ||
|
|
6c3719b9b8 | ||
|
|
8c9ea7885a | ||
|
|
20cbf0c710 | ||
|
|
8de2066634 | ||
|
|
dfc312c092 | ||
|
|
ce15dbf31b | ||
|
|
ea1afb260a | ||
|
|
e9e0fdae37 | ||
|
|
124f7f43ab | ||
|
|
27df44bf44 | ||
|
|
b934a7de6a | ||
|
|
d521c75085 | ||
|
|
5e18b6b878 | ||
|
|
3183ca02b3 | ||
|
|
60a278490f | ||
|
|
b78e74cdf6 | ||
|
|
f61a16074b | ||
|
|
82766d1645 | ||
|
|
725f471a6a | ||
|
|
0b01a79d9d | ||
|
|
2653ff6536 | ||
|
|
0f30cc8e59 | ||
|
|
e3879cd4d1 | ||
|
|
7a4cdf8688 | ||
|
|
1839bf938a | ||
|
|
44d4096a79 | ||
|
|
41280c9d38 | ||
|
|
7c54adec9d | ||
|
|
68abd91fc2 | ||
|
|
4d2e42d244 | ||
|
|
5a87a6c502 | ||
|
|
d8ca15ceb3 | ||
|
|
a17f718517 | ||
|
|
3589dda8ee | ||
|
|
21f8e4d55b | ||
|
|
811e0123c9 | ||
|
|
47c4516060 | ||
|
|
13913334b6 | ||
|
|
7f60725c88 | ||
|
|
d55fb36182 | ||
|
|
41205aef20 | ||
|
|
aeadbc1d58 | ||
|
|
bd12ade426 | ||
|
|
f9c26089cd | ||
|
|
7ddb57078c | ||
|
|
3e7f1275d8 | ||
|
|
e963938016 | ||
|
|
312fcea5f1 | ||
|
|
9d05653f5b | ||
|
|
644ebd0a4f | ||
|
|
1033bfcfe5 | ||
|
|
a688310b95 | ||
|
|
e3ffc8784e | ||
|
|
bb35fc3801 | ||
|
|
c804630576 | ||
|
|
e5f3ca1623 | ||
|
|
0880787d68 | ||
|
|
cd582e2e3a | ||
|
|
aebd9319f5 | ||
|
|
de6cbb0f45 | ||
|
|
e14dcd0184 | ||
|
|
17ef653903 | ||
|
|
f5d5b5efc0 | ||
|
|
59dbee8f28 | ||
|
|
4db6971cc4 | ||
|
|
71482bd06c | ||
|
|
c3acf8341b | ||
|
|
1bc48fbf96 | ||
|
|
d45348c167 | ||
|
|
22caa0ee66 | ||
|
|
e6e8ccc855 | ||
|
|
d78522f5e1 | ||
|
|
3da2a618b9 | ||
|
|
047fa5b2db | ||
|
|
c763794ef3 | ||
|
|
7a4dcd52c4 | ||
|
|
e8e7a92131 | ||
|
|
99694161e1 | ||
|
|
00f944f3f4 | ||
|
|
1269411771 | ||
|
|
d4d8ea6cf2 | ||
|
|
160522c520 | ||
|
|
7024b5ec1b | ||
|
|
5b020035d6 | ||
|
|
fcea7fd4bf | ||
|
|
37e5bcad61 | ||
|
|
20679a62fd | ||
|
|
bb5a5bf2ed | ||
|
|
c1db993b92 | ||
|
|
c19916ff1c | ||
|
|
6fa2e79c1c | ||
|
|
6e7588e9fc | ||
|
|
03cc8248bc | ||
|
|
068df6f2b1 | ||
|
|
0966ba909b | ||
|
|
7941a24d51 | ||
|
|
e004ba63f8 | ||
|
|
1f30a19566 | ||
|
|
51f4578a41 | ||
|
|
bd3954a5f1 | ||
|
|
94967add7c | ||
|
|
783ab0b611 | ||
|
|
653a9526f5 | ||
|
|
34ac4b25af | ||
|
|
e072ff2d77 | ||
|
|
41dfbc2709 | ||
|
|
964e461597 | ||
|
|
ef2eec4c4a | ||
|
|
bf1d76d853 | ||
|
|
0682cbd554 | ||
|
|
f5191cdd42 | ||
|
|
b1c73208c5 | ||
|
|
ab221a465b | ||
|
|
4ecfa0477d | ||
|
|
bab2de36ad | ||
|
|
f479e914bb | ||
|
|
45441653f6 | ||
|
|
0303558ae1 | ||
|
|
b70e0e3e2b | ||
|
|
5e0c4d7b7a | ||
|
|
6c83308451 | ||
|
|
eeb898179e | ||
|
|
0ea662d8fe | ||
|
|
ea3219fa10 | ||
|
|
fb9203d396 | ||
|
|
23e7542871 | ||
|
|
a40832dffd | ||
|
|
5ba7493613 | ||
|
|
dd1d16f91c | ||
|
|
82e2a19749 | ||
|
|
b73126e6c1 | ||
|
|
cbd93f450e | ||
|
|
35c64be3d7 | ||
|
|
96ea70c027 | ||
|
|
ea277d0579 | ||
|
|
10d7cd1520 | ||
|
|
9051322338 | ||
|
|
d26b6103b5 | ||
|
|
f2e7963e1f | ||
|
|
a3d7e541d3 | ||
|
|
4617025bd4 | ||
|
|
0693e19605 | ||
|
|
62618acfed | ||
|
|
1feaa43d2e | ||
|
|
92d7d61926 | ||
|
|
265d77d776 | ||
|
|
48d9fde3b6 | ||
|
|
a9ea1a02ed | ||
|
|
19cd5c8881 | ||
|
|
cdb9c661bd | ||
|
|
c3a01c240b | ||
|
|
e2f748e63d | ||
|
|
52fa4f11ac | ||
|
|
bfcabd30c5 | ||
|
|
095ea470a1 | ||
|
|
c9b502c72b | ||
|
|
9390eacb7c | ||
|
|
f193ce87bf | ||
|
|
f6b3f898de | ||
|
|
de5ba5d0d3 | ||
|
|
c539dd5570 | ||
|
|
de76afea99 | ||
|
|
1d3616ae71 | ||
|
|
d76cb440f9 | ||
|
|
7c89d658f7 | ||
|
|
292c929117 | ||
|
|
daf42b63c8 | ||
|
|
07da03618f | ||
|
|
dda51f2801 | ||
|
|
25472bcfa6 | ||
|
|
6ff17d16f0 | ||
|
|
06b7116692 | ||
|
|
3c3ea0f3e1 | ||
|
|
db4d6511d6 | ||
|
|
6e42a67268 | ||
|
|
fd066e5eef | ||
|
|
3dd0c44410 | ||
|
|
12b42854e4 | ||
|
|
2fcb6d0c7c | ||
|
|
68e863723a | ||
|
|
d0b37d0f9a | ||
|
|
a0a1353445 | ||
|
|
6725cc6f61 | ||
|
|
7e9639052b | ||
|
|
21bd5ba376 | ||
|
|
28d5fb1822 | ||
|
|
cd3c031df1 | ||
|
|
a6db0f6fd9 | ||
|
|
c80f6e8285 | ||
|
|
c6d779853a | ||
|
|
1b720a504c | ||
|
|
72f8854a7a | ||
|
|
097d195f00 | ||
|
|
807da8f696 | ||
|
|
a8ca6b6fcb | ||
|
|
85e2e14c81 | ||
|
|
5ae45ddd55 | ||
|
|
4e0a3da01e | ||
|
|
541a99bbc5 | ||
|
|
f62008aba4 | ||
|
|
74f7415f84 | ||
|
|
1e1e079b65 | ||
|
|
0b4f808b2d | ||
|
|
ae4af99c59 | ||
|
|
0b17556fa4 | ||
|
|
e6ebc347e5 | ||
|
|
f2323a9d19 | ||
|
|
ce53fe5e31 | ||
|
|
f1d359b3e7 | ||
|
|
da99f3bc2a | ||
|
|
fec8dd74af | ||
|
|
f598e0d0d5 | ||
|
|
656d2494b0 | ||
|
|
6d07d58f37 | ||
|
|
dd5270f620 | ||
|
|
216f895953 | ||
|
|
6acb87b7ea | ||
|
|
acdccd697c | ||
|
|
cecc4b1f6d | ||
|
|
f93f115e13 | ||
|
|
debcf086b5 | ||
|
|
c6db974962 | ||
|
|
e9b0b0c42e | ||
|
|
ebee1a02fd | ||
|
|
de05139dfc | ||
|
|
e88d0579b0 | ||
|
|
5564e4daa2 | ||
|
|
4742cd4a03 | ||
|
|
99985f4fab | ||
|
|
42bb3b5aca | ||
|
|
70bedaf8dd | ||
|
|
38683a7fea | ||
|
|
ae1a68500c | ||
|
|
578f842eed | ||
|
|
a7dea95f90 | ||
|
|
1080562d96 | ||
|
|
b8cf5a2347 | ||
|
|
e9706d605a | ||
|
|
4b338c56b5 | ||
|
|
2974a96e96 | ||
|
|
ab7fd3d019 | ||
|
|
54c5659496 | ||
|
|
8c0811885f | ||
|
|
291570dfd7 | ||
|
|
f6b9b4cc19 | ||
|
|
a06403ab7c | ||
|
|
e64a95d1d7 | ||
|
|
6939e49643 | ||
|
|
bb18586484 | ||
|
|
09b39a4bf9 | ||
|
|
1d7d639654 | ||
|
|
42ec509574 | ||
|
|
dcef864c1c | ||
|
|
e8cb4f90f4 | ||
|
|
4e0b2e4f77 | ||
|
|
a2bcadeb7c | ||
|
|
84a4242e27 | ||
|
|
6373d667d9 | ||
|
|
7840d601e1 | ||
|
|
1893061c0d | ||
|
|
0289cc3ee5 | ||
|
|
a35c9d2d9e | ||
|
|
7f739a1371 | ||
|
|
62df78f329 | ||
|
|
be4511d95b | ||
|
|
b17e6058d1 | ||
|
|
99c28a184f | ||
|
|
7ed9adaf49 | ||
|
|
c3ef051657 | ||
|
|
ba149efa4a | ||
|
|
e30a7b3849 | ||
|
|
97ad0483ec | ||
|
|
3981c772a2 | ||
|
|
e762002560 | ||
|
|
061d8dc94f | ||
|
|
84e6228f90 | ||
|
|
301cc22985 | ||
|
|
29398b9869 | ||
|
|
8b65a75235 | ||
|
|
313f7e8173 | ||
|
|
d9e615e696 | ||
|
|
b526067eeb |
@@ -11,7 +11,7 @@ root = true
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespaces = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
# CoffeeScript
|
||||
#
|
||||
@@ -28,12 +28,12 @@ indent_style = space
|
||||
# Package.json
|
||||
#
|
||||
# This indentation style is the one used by npm.
|
||||
[/package.json]
|
||||
[package.json]
|
||||
indent_size = 2
|
||||
indent_style = space
|
||||
|
||||
# Jade
|
||||
[*.jade]
|
||||
# Pug (Jade)
|
||||
[*.{jade,pug}]
|
||||
indent_size = 2
|
||||
indent_style = space
|
||||
|
||||
@@ -41,7 +41,7 @@ indent_style = space
|
||||
#
|
||||
# Two spaces seems to be the standard most common style, at least in
|
||||
# Node.js (http://nodeguide.com/style.html#tabs-vs-spaces).
|
||||
[*.js]
|
||||
[*.{js,jsx,ts,tsx}]
|
||||
indent_size = 2
|
||||
indent_style = space
|
||||
|
||||
13
.gitignore
vendored
Normal file
13
.gitignore
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
/node_modules/
|
||||
/lerna-debug.log
|
||||
/lerna-debug.log.*
|
||||
|
||||
/packages/*/dist/
|
||||
/packages/*/node_modules/
|
||||
|
||||
npm-debug.log
|
||||
npm-debug.log.*
|
||||
pnpm-debug.log
|
||||
pnpm-debug.log.*
|
||||
yarn-error.log
|
||||
yarn-error.log.*
|
||||
@@ -1,9 +1,8 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- 'stable'
|
||||
- '4'
|
||||
- '0.12'
|
||||
- '0.10'
|
||||
- stable
|
||||
- 6
|
||||
- 4
|
||||
|
||||
# Use containers.
|
||||
# http://docs.travis-ci.com/user/workers/container-based-infrastructure/
|
||||
5
README.md
Normal file
5
README.md
Normal file
@@ -0,0 +1,5 @@
|
||||
Almost all dev for Xen Orchestra is happening in this repository.
|
||||
|
||||
Because transition is still underway, [xo-web](https://github.com/vatesfr/xo-web) and [xo-server](https://github.com/vatesfr/xo-server) are still developped in their own repositories.
|
||||
|
||||
For now, all issues are still to be reported in [xo-web's tracker](https://github.com/vatesfr/xo-web/issues).
|
||||
7
lerna.json
Normal file
7
lerna.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"lerna": "2.0.0-beta.34",
|
||||
"packages": [
|
||||
"packages/*"
|
||||
],
|
||||
"version": "0.0.0"
|
||||
}
|
||||
12
package.json
Normal file
12
package.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"devDependencies": {
|
||||
"husky": "^0.13.1",
|
||||
"lerna": "^2.0.0-beta.34",
|
||||
"promise-toolbox": "^0.8.0"
|
||||
},
|
||||
"scripts": {
|
||||
"commit-msg": "yarn test",
|
||||
"install": "lerna exec --concurrency 1 -- yarn",
|
||||
"test": "lerna exec -- yarn test"
|
||||
}
|
||||
}
|
||||
24
packages/vhd-cli/.npmignore
Normal file
24
packages/vhd-cli/.npmignore
Normal file
@@ -0,0 +1,24 @@
|
||||
/benchmark/
|
||||
/benchmarks/
|
||||
*.bench.js
|
||||
*.bench.js.map
|
||||
|
||||
/examples/
|
||||
example.js
|
||||
example.js.map
|
||||
*.example.js
|
||||
*.example.js.map
|
||||
|
||||
/fixture/
|
||||
/fixtures/
|
||||
*.fixture.js
|
||||
*.fixture.js.map
|
||||
*.fixtures.js
|
||||
*.fixtures.js.map
|
||||
|
||||
/test/
|
||||
/tests/
|
||||
*.spec.js
|
||||
*.spec.js.map
|
||||
|
||||
__snapshots__/
|
||||
51
packages/vhd-cli/README.md
Normal file
51
packages/vhd-cli/README.md
Normal file
@@ -0,0 +1,51 @@
|
||||
# vhd-cli [](https://travis-ci.org/vatesfr/xen-orchestra)
|
||||
|
||||
> ${pkg.description}
|
||||
|
||||
## Install
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/vhd-cli):
|
||||
|
||||
```
|
||||
> npm install --global vhd-cli
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
> vhd-cli <VHD file>
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
```
|
||||
# Install dependencies
|
||||
> npm install
|
||||
|
||||
# Run the tests
|
||||
> npm test
|
||||
|
||||
# Continuously compile
|
||||
> npm run dev
|
||||
|
||||
# Continuously run the tests
|
||||
> npm run dev-test
|
||||
|
||||
# Build for production (automatically called by npm install)
|
||||
> npm run build
|
||||
```
|
||||
|
||||
## Contributions
|
||||
|
||||
Contributions are *very* welcomed, either on the documentation or on
|
||||
the code.
|
||||
|
||||
You may:
|
||||
|
||||
- report any [issue](https://github.com/vatesfr/xo-web/issues)
|
||||
you've encountered;
|
||||
- fork and create a pull request.
|
||||
|
||||
## License
|
||||
|
||||
ISC © [Vates SAS](https://vates.fr)
|
||||
87
packages/vhd-cli/package.json
Normal file
87
packages/vhd-cli/package.json
Normal file
@@ -0,0 +1,87 @@
|
||||
{
|
||||
"name": "vhd-cli",
|
||||
"version": "0.0.0",
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
"keywords": [],
|
||||
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/packages/vhd-cli",
|
||||
"bugs": "https://github.com/vatesfr/xo-web/issues",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/vatesfr/xen-orchestra.git"
|
||||
},
|
||||
"author": {
|
||||
"name": "Julien Fontanet",
|
||||
"email": "julien.fontanet@isonoe.net"
|
||||
},
|
||||
"preferGlobal": false,
|
||||
"main": "dist/",
|
||||
"bin": {
|
||||
"vhd-cli": "dist/index.js"
|
||||
},
|
||||
"files": [
|
||||
"dist/"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nraynaud/struct-fu": "^1.0.1",
|
||||
"@nraynaud/xo-fs": "^0.0.5",
|
||||
"babel-runtime": "^6.22.0",
|
||||
"exec-promise": "^0.6.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-cli": "^6.22.2",
|
||||
"babel-eslint": "^7.1.1",
|
||||
"babel-plugin-lodash": "^3.2.11",
|
||||
"babel-plugin-transform-runtime": "^6.22.0",
|
||||
"babel-preset-env": "^1.1.8",
|
||||
"babel-preset-stage-3": "^6.22.0",
|
||||
"cross-env": "^3.1.4",
|
||||
"dependency-check": "^2.8.0",
|
||||
"jest": "^18.1.0",
|
||||
"rimraf": "^2.5.4",
|
||||
"standard": "^8.6.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
|
||||
"commitmsg": "npm test",
|
||||
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
|
||||
"dev-test": "jest --bail --watch",
|
||||
"posttest": "standard && dependency-check ./package.json",
|
||||
"prebuild": "rimraf dist/",
|
||||
"predev": "npm run prebuild",
|
||||
"prepublish": "npm run build",
|
||||
"test": "jest"
|
||||
},
|
||||
"babel": {
|
||||
"plugins": [
|
||||
"lodash",
|
||||
"transform-runtime"
|
||||
],
|
||||
"presets": [
|
||||
[
|
||||
"env",
|
||||
{
|
||||
"targets": {
|
||||
"node": 4
|
||||
}
|
||||
}
|
||||
],
|
||||
"stage-3"
|
||||
]
|
||||
},
|
||||
"jest": {
|
||||
"testPathDirs": [
|
||||
"<rootDir>/src"
|
||||
],
|
||||
"testRegex": "\\.spec\\.js$"
|
||||
},
|
||||
"standard": {
|
||||
"ignore": [
|
||||
"dist"
|
||||
],
|
||||
"parser": "babel-eslint"
|
||||
}
|
||||
}
|
||||
19
packages/vhd-cli/src/index.js
Executable file
19
packages/vhd-cli/src/index.js
Executable file
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import execPromise from 'exec-promise'
|
||||
import { RemoteHandlerLocal } from '@nraynaud/xo-fs'
|
||||
import { resolve } from 'path'
|
||||
|
||||
import Vhd from './vhd'
|
||||
|
||||
execPromise(async args => {
|
||||
const vhd = new Vhd(
|
||||
new RemoteHandlerLocal({ url: 'file:///' }),
|
||||
resolve(args[0])
|
||||
)
|
||||
|
||||
await vhd.readHeaderAndFooter()
|
||||
|
||||
console.log(vhd._header)
|
||||
console.log(vhd._footer)
|
||||
})
|
||||
441
packages/vhd-cli/src/vhd.js
Normal file
441
packages/vhd-cli/src/vhd.js
Normal file
@@ -0,0 +1,441 @@
|
||||
import assert from 'assert'
|
||||
import fu from '@nraynaud/struct-fu'
|
||||
import { dirname } from 'path'
|
||||
|
||||
// ===================================================================
|
||||
//
|
||||
// Spec:
|
||||
// https://www.microsoft.com/en-us/download/details.aspx?id=23850
|
||||
//
|
||||
// C implementation:
|
||||
// https://github.com/rubiojr/vhd-util-convert
|
||||
//
|
||||
// ===================================================================
|
||||
|
||||
/* eslint-disable no-unused-vars */
|
||||
|
||||
const HARD_DISK_TYPE_DIFFERENCING = 4
|
||||
const HARD_DISK_TYPE_DYNAMIC = 3
|
||||
const HARD_DISK_TYPE_FIXED = 2
|
||||
const PLATFORM_CODE_NONE = 0
|
||||
export const SECTOR_SIZE = 512
|
||||
|
||||
/* eslint-enable no-unused vars */
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const fuFooter = fu.struct([
|
||||
fu.char('cookie', 8), // 0
|
||||
fu.uint32('features'), // 8
|
||||
fu.uint32('fileFormatVersion'), // 12
|
||||
fu.struct('dataOffset', [
|
||||
fu.uint32('high'), // 16
|
||||
fu.uint32('low') // 20
|
||||
]),
|
||||
fu.uint32('timestamp'), // 24
|
||||
fu.char('creatorApplication', 4), // 28
|
||||
fu.uint32('creatorVersion'), // 32
|
||||
fu.uint32('creatorHostOs'), // 36
|
||||
fu.struct('originalSize', [ // At the creation, current size of the hard disk.
|
||||
fu.uint32('high'), // 40
|
||||
fu.uint32('low') // 44
|
||||
]),
|
||||
fu.struct('currentSize', [ // Current size of the virtual disk. At the creation: currentSize = originalSize.
|
||||
fu.uint32('high'), // 48
|
||||
fu.uint32('low') // 52
|
||||
]),
|
||||
fu.struct('diskGeometry', [
|
||||
fu.uint16('cylinders'), // 56
|
||||
fu.uint8('heads'), // 58
|
||||
fu.uint8('sectorsPerTrackCylinder') // 59
|
||||
]),
|
||||
fu.uint32('diskType'), // 60 Disk type, must be equal to HARD_DISK_TYPE_DYNAMIC/HARD_DISK_TYPE_DIFFERENCING.
|
||||
fu.uint32('checksum'), // 64
|
||||
fu.uint8('uuid', 16), // 68
|
||||
fu.char('saved'), // 84
|
||||
fu.char('hidden'), // 85
|
||||
fu.byte('reserved', 426) // 86
|
||||
])
|
||||
const FOOTER_SIZE = fuFooter.size
|
||||
|
||||
const fuHeader = fu.struct([
|
||||
fu.char('cookie', 8),
|
||||
fu.struct('dataOffset', [
|
||||
fu.uint32('high'),
|
||||
fu.uint32('low')
|
||||
]),
|
||||
fu.struct('tableOffset', [ // Absolute byte offset of the Block Allocation Table.
|
||||
fu.uint32('high'),
|
||||
fu.uint32('low')
|
||||
]),
|
||||
fu.uint32('headerVersion'),
|
||||
fu.uint32('maxTableEntries'), // Max entries in the Block Allocation Table.
|
||||
fu.uint32('blockSize'), // Block size (without bitmap) in bytes.
|
||||
fu.uint32('checksum'),
|
||||
fu.uint8('parentUuid', 16),
|
||||
fu.uint32('parentTimestamp'),
|
||||
fu.byte('reserved1', 4),
|
||||
fu.char16be('parentUnicodeName', 512),
|
||||
fu.struct('parentLocatorEntry', [
|
||||
fu.uint32('platformCode'),
|
||||
fu.uint32('platformDataSpace'),
|
||||
fu.uint32('platformDataLength'),
|
||||
fu.uint32('reserved'),
|
||||
fu.struct('platformDataOffset', [ // Absolute byte offset of the locator data.
|
||||
fu.uint32('high'),
|
||||
fu.uint32('low')
|
||||
])
|
||||
], 8),
|
||||
fu.byte('reserved2', 256)
|
||||
])
|
||||
const HEADER_SIZE = fuHeader.size
|
||||
|
||||
// ===================================================================
|
||||
// Helpers
|
||||
// ===================================================================
|
||||
|
||||
const SIZE_OF_32_BITS = Math.pow(2, 32)
|
||||
const uint32ToUint64 = fu => fu.high * SIZE_OF_32_BITS + fu.low
|
||||
|
||||
// Returns a 32 bits integer corresponding to a Vhd version.
|
||||
const getVhdVersion = (major, minor) => (major << 16) | (minor & 0x0000FFFF)
|
||||
|
||||
// bytes[] bit manipulation
|
||||
const testBit = (map, bit) => map[bit >> 3] & 1 << (bit & 7)
|
||||
const setBit = (map, bit) => {
|
||||
map[bit >> 3] |= 1 << (bit & 7)
|
||||
}
|
||||
const unsetBit = (map, bit) => {
|
||||
map[bit >> 3] &= ~(1 << (bit & 7))
|
||||
}
|
||||
|
||||
const addOffsets = (...offsets) => offsets.reduce(
|
||||
(a, b) => b == null
|
||||
? a
|
||||
: typeof b === 'object'
|
||||
? { bytes: a.bytes + b.bytes, bits: a.bits + b.bits }
|
||||
: { bytes: a.bytes + b, bits: a.bits },
|
||||
{ bytes: 0, bits: 0 }
|
||||
)
|
||||
|
||||
const pack = (field, value, buf, offset) => {
|
||||
field.pack(
|
||||
value,
|
||||
buf,
|
||||
addOffsets(field.offset, offset)
|
||||
)
|
||||
}
|
||||
|
||||
const unpack = (field, buf, offset) =>
|
||||
field.unpack(
|
||||
buf,
|
||||
addOffsets(field.offset, offset)
|
||||
)
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const streamToNewBuffer = stream => new Promise((resolve, reject) => {
|
||||
const chunks = []
|
||||
let length = 0
|
||||
|
||||
const onData = chunk => {
|
||||
chunks.push(chunk)
|
||||
length += chunk.length
|
||||
}
|
||||
stream.on('data', onData)
|
||||
|
||||
const clean = () => {
|
||||
stream.removeListener('data', onData)
|
||||
stream.removeListener('end', onEnd)
|
||||
stream.removeListener('error', onError)
|
||||
}
|
||||
const onEnd = () => {
|
||||
resolve(Buffer.concat(chunks, length))
|
||||
clean()
|
||||
}
|
||||
stream.on('end', onEnd)
|
||||
const onError = error => {
|
||||
reject(error)
|
||||
clean()
|
||||
}
|
||||
stream.on('error', onError)
|
||||
})
|
||||
|
||||
const streamToExistingBuffer = (
|
||||
stream,
|
||||
buffer,
|
||||
offset = 0,
|
||||
end = buffer.length
|
||||
) => new Promise((resolve, reject) => {
|
||||
assert(offset >= 0)
|
||||
assert(end > offset)
|
||||
assert(end <= buffer.length)
|
||||
|
||||
let i = offset
|
||||
|
||||
const onData = chunk => {
|
||||
const prev = i
|
||||
i += chunk.length
|
||||
|
||||
if (i > end) {
|
||||
return onError(new Error('too much data'))
|
||||
}
|
||||
|
||||
chunk.copy(buffer, prev)
|
||||
}
|
||||
stream.on('data', onData)
|
||||
|
||||
const clean = () => {
|
||||
stream.removeListener('data', onData)
|
||||
stream.removeListener('end', onEnd)
|
||||
stream.removeListener('error', onError)
|
||||
}
|
||||
const onEnd = () => {
|
||||
resolve(i - offset)
|
||||
clean()
|
||||
}
|
||||
stream.on('end', onEnd)
|
||||
const onError = error => {
|
||||
reject(error)
|
||||
clean()
|
||||
}
|
||||
stream.on('error', onError)
|
||||
})
|
||||
|
||||
// ===================================================================
|
||||
|
||||
// Returns the checksum of a raw struct.
|
||||
const computeChecksum = (struct, buf, offset = 0) => {
|
||||
let sum = 0
|
||||
|
||||
// Do not use the stored checksum to compute the new checksum.
|
||||
const checksumField = struct.fields.checksum
|
||||
const checksumOffset = offset + checksumField.offset
|
||||
for (let i = offset, n = checksumOffset; i < n; ++i) {
|
||||
sum += buf[i]
|
||||
}
|
||||
for (let i = checksumOffset + checksumField.size, n = offset + struct.size; i < n; ++i) {
|
||||
sum += buf[i]
|
||||
}
|
||||
|
||||
return ~sum >>> 0
|
||||
}
|
||||
|
||||
const verifyChecksum = (struct, buf, offset) =>
|
||||
unpack(struct.fields.checksum, buf, offset) === computeChecksum(struct, buf, offset)
|
||||
|
||||
const getParentLocatorSize = parentLocatorEntry => {
|
||||
const { platformDataSpace } = parentLocatorEntry
|
||||
|
||||
if (platformDataSpace < SECTOR_SIZE) {
|
||||
return platformDataSpace * SECTOR_SIZE
|
||||
}
|
||||
|
||||
return (platformDataSpace % SECTOR_SIZE === 0)
|
||||
? platformDataSpace
|
||||
: 0
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
// Euclidean division, returns the quotient and the remainder of a / b.
|
||||
const div = (a, b) => [ Math.floor(a / b), a % b ]
|
||||
|
||||
export default class Vhd {
|
||||
constructor (handler, path) {
|
||||
this._handler = handler
|
||||
this._path = path
|
||||
|
||||
this._blockAllocationTable = null
|
||||
this._blockBitmapSize = null
|
||||
this._footer = null
|
||||
this._header = null
|
||||
this._parent = null
|
||||
this._sectorsPerBlock = null
|
||||
}
|
||||
|
||||
// Read `length` bytes starting from `begin`.
|
||||
//
|
||||
// - if `buffer`: it is filled starting from `offset`, and the
|
||||
// number of written bytes is returned;
|
||||
// - otherwise: a new buffer is allocated and returned.
|
||||
_read (begin, length, buf, offset) {
|
||||
assert(begin >= 0)
|
||||
assert(length > 0)
|
||||
|
||||
return this._handler.createReadStream(this._path, {
|
||||
end: begin + length - 1,
|
||||
start: begin
|
||||
}).then(buf
|
||||
? stream => streamToExistingBuffer(stream, buf, offset, (offset || 0) + length)
|
||||
: streamToNewBuffer
|
||||
)
|
||||
}
|
||||
|
||||
// - if `buffer`: it is filled with 0 starting from `offset`, and
|
||||
// the number of written bytes is returned;
|
||||
// - otherwise: a new buffer is allocated and returned.
|
||||
_zeroes (length, buf, offset = 0) {
|
||||
if (buf) {
|
||||
assert(offset >= 0)
|
||||
assert(length > 0)
|
||||
|
||||
const end = offset + length
|
||||
assert(end <= buf.length)
|
||||
|
||||
buf.fill(0, offset, end)
|
||||
return Promise.resolve(length)
|
||||
}
|
||||
|
||||
return Promise.resolve(Buffer.alloc(length))
|
||||
}
|
||||
|
||||
// Return the position of a block in the VHD or undefined if not found.
|
||||
_getBlockAddress (block) {
|
||||
assert(block >= 0)
|
||||
assert(block < this._header.maxTableEntries)
|
||||
|
||||
const blockAddr = this._blockAllocationTable[block]
|
||||
if (blockAddr !== 0xFFFFFFFF) {
|
||||
return blockAddr * SECTOR_SIZE
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
async readHeaderAndFooter () {
|
||||
const buf = await this._read(0, FOOTER_SIZE + HEADER_SIZE)
|
||||
|
||||
if (!verifyChecksum(fuFooter, buf)) {
|
||||
throw new Error('footer checksum does not match')
|
||||
}
|
||||
|
||||
if (!verifyChecksum(fuHeader, buf, FOOTER_SIZE)) {
|
||||
throw new Error('header checksum does not match')
|
||||
}
|
||||
|
||||
return this._initMetadata(
|
||||
unpack(fuHeader, buf, FOOTER_SIZE),
|
||||
unpack(fuFooter, buf)
|
||||
)
|
||||
}
|
||||
|
||||
async _initMetadata (header, footer) {
|
||||
const sectorsPerBlock = header.blockSize / SECTOR_SIZE
|
||||
assert(sectorsPerBlock % 1 === 0)
|
||||
|
||||
// 1 bit per sector, rounded up to full sectors
|
||||
this._blockBitmapSize = Math.ceil(sectorsPerBlock / 8 / SECTOR_SIZE) * SECTOR_SIZE
|
||||
assert(this._blockBitmapSize === SECTOR_SIZE)
|
||||
|
||||
this._footer = footer
|
||||
this._header = header
|
||||
this.size = uint32ToUint64(this._footer.currentSize)
|
||||
|
||||
if (footer.diskType === HARD_DISK_TYPE_DIFFERENCING) {
|
||||
const parent = new Vhd(
|
||||
this._handler,
|
||||
`${dirname(this._path)}/${header.parentUnicodeName}`
|
||||
)
|
||||
await parent.readHeaderAndFooter()
|
||||
await parent.readBlockAllocationTable()
|
||||
|
||||
this._parent = parent
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
async readBlockAllocationTable () {
|
||||
const { maxTableEntries, tableOffset } = this._header
|
||||
const fuTable = fu.uint32(maxTableEntries)
|
||||
|
||||
this._blockAllocationTable = unpack(
|
||||
fuTable,
|
||||
await this._read(uint32ToUint64(tableOffset), fuTable.size)
|
||||
)
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
// read a single sector in a block
|
||||
async _readBlockSector (block, sector, begin, length, buf, offset) {
|
||||
assert(begin >= 0)
|
||||
assert(length > 0)
|
||||
assert(begin + length <= SECTOR_SIZE)
|
||||
|
||||
const blockAddr = this._getBlockAddress(block)
|
||||
const blockBitmapSize = this._blockBitmapSize
|
||||
const parent = this._parent
|
||||
|
||||
if (blockAddr && (
|
||||
!parent ||
|
||||
testBit(await this._read(blockAddr, blockBitmapSize), sector)
|
||||
)) {
|
||||
return this._read(
|
||||
blockAddr + blockBitmapSize + sector * SECTOR_SIZE + begin,
|
||||
length,
|
||||
buf,
|
||||
offset
|
||||
)
|
||||
}
|
||||
|
||||
return parent
|
||||
? parent._readBlockSector(block, sector, begin, length, buf, offset)
|
||||
: this._zeroes(length, buf, offset)
|
||||
}
|
||||
|
||||
_readBlock (block, begin, length, buf, offset) {
|
||||
assert(begin >= 0)
|
||||
assert(length > 0)
|
||||
|
||||
const { blockSize } = this._header
|
||||
assert(begin + length <= blockSize)
|
||||
|
||||
const blockAddr = this._getBlockAddress(block)
|
||||
const parent = this._parent
|
||||
|
||||
if (!blockAddr) {
|
||||
return parent
|
||||
? parent._readBlock(block, begin, length, buf, offset)
|
||||
: this._zeroes(length, buf, offset)
|
||||
}
|
||||
|
||||
if (!parent) {
|
||||
return this._read(blockAddr + this._blockBitmapSize + begin, length, buf, offset)
|
||||
}
|
||||
|
||||
// FIXME: we should read as many sectors in a single pass as
|
||||
// possible for maximum perf.
|
||||
const [ sector, beginInSector ] = div(begin, SECTOR_SIZE)
|
||||
return this._readBlockSector(
|
||||
block,
|
||||
sector,
|
||||
beginInSector,
|
||||
Math.min(length, SECTOR_SIZE - beginInSector),
|
||||
buf,
|
||||
offset
|
||||
)
|
||||
}
|
||||
|
||||
read (buf, begin, length = buf.length, offset) {
|
||||
assert(Buffer.isBuffer(buf))
|
||||
assert(begin >= 0)
|
||||
|
||||
const { size } = this
|
||||
if (begin >= size) {
|
||||
return Promise.resolve(0)
|
||||
}
|
||||
|
||||
const { blockSize } = this._header
|
||||
const [ block, beginInBlock ] = div(begin, blockSize)
|
||||
|
||||
return this._readBlock(
|
||||
block,
|
||||
beginInBlock,
|
||||
Math.min(length, blockSize - beginInBlock, size - begin),
|
||||
buf,
|
||||
offset
|
||||
)
|
||||
}
|
||||
}
|
||||
3592
packages/vhd-cli/yarn.lock
Normal file
3592
packages/vhd-cli/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
8
packages/xo-acl-resolver/.babelrc
Normal file
8
packages/xo-acl-resolver/.babelrc
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"comments": false,
|
||||
"compact": true,
|
||||
"presets": [
|
||||
"stage-0",
|
||||
"es2015"
|
||||
]
|
||||
}
|
||||
10
packages/xo-acl-resolver/.npmignore
Normal file
10
packages/xo-acl-resolver/.npmignore
Normal file
@@ -0,0 +1,10 @@
|
||||
/examples/
|
||||
example.js
|
||||
example.js.map
|
||||
*.example.js
|
||||
*.example.js.map
|
||||
|
||||
/test/
|
||||
/tests/
|
||||
*.spec.js
|
||||
*.spec.js.map
|
||||
74
packages/xo-acl-resolver/README.md
Normal file
74
packages/xo-acl-resolver/README.md
Normal file
@@ -0,0 +1,74 @@
|
||||
# xo-acl-resolver [](https://travis-ci.org/vatesfr/xen-orchestra)
|
||||
|
||||
> [Xen-Orchestra](http://xen-orchestra.com/) internal: do ACLs resolution.
|
||||
|
||||
## Install
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/xo-acl-resolver):
|
||||
|
||||
```
|
||||
> npm install --save xo-acl-resolver
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```js
|
||||
import check from 'xo-acl-resolver'
|
||||
|
||||
// This object contains a list of permissions returned from
|
||||
// xo-server's acl.getCurrentPermissions.
|
||||
const permissions = { /* ... */ }
|
||||
|
||||
// This function should returns synchronously an object from an id.
|
||||
const getObject = id => { /* ... */ }
|
||||
|
||||
// For a single object:
|
||||
if (check(permissions, getObject, objectId, permission)) {
|
||||
console.log(`${permission} set for object ${objectId}`)
|
||||
}
|
||||
|
||||
// For multiple objects/permissions:
|
||||
if (check(permissions, getObject, [
|
||||
[ object1Id, permission1 ],
|
||||
[ object12d, permission2 ],
|
||||
])) {
|
||||
console.log('all permissions checked')
|
||||
}
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
### Installing dependencies
|
||||
|
||||
```
|
||||
> npm install
|
||||
```
|
||||
|
||||
### Compilation
|
||||
|
||||
The sources files are watched and automatically recompiled on changes.
|
||||
|
||||
```
|
||||
> npm run dev
|
||||
```
|
||||
|
||||
### Tests
|
||||
|
||||
```
|
||||
> npm run test-dev
|
||||
```
|
||||
|
||||
## Contributions
|
||||
|
||||
Contributions are *very* welcomed, either on the documentation or on
|
||||
the code.
|
||||
|
||||
You may:
|
||||
|
||||
- report any [issue](https://github.com/vatesfr/xo-web/issues)
|
||||
you've encountered;
|
||||
- fork and create a pull request.
|
||||
|
||||
## License
|
||||
|
||||
ISC © [Vates SAS](https://vates.fr)
|
||||
48
packages/xo-acl-resolver/package.json
Normal file
48
packages/xo-acl-resolver/package.json
Normal file
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"name": "xo-acl-resolver",
|
||||
"version": "0.2.3",
|
||||
"license": "ISC",
|
||||
"description": "Xen-Orchestra internal: do ACLs resolution",
|
||||
"keywords": [],
|
||||
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/packages/xo-acl-resolver",
|
||||
"bugs": "https://github.com/vatesfr/xo-web/issues",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/vatesfr/xen-orchestra.git"
|
||||
},
|
||||
"author": {
|
||||
"name": "Julien Fontanet",
|
||||
"email": "julien.fontanet@vates.fr"
|
||||
},
|
||||
"preferGlobal": false,
|
||||
"main": "dist/",
|
||||
"bin": {},
|
||||
"files": [
|
||||
"dist/"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=0.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-cli": "^6.4.5",
|
||||
"babel-eslint": "^7.0.0",
|
||||
"babel-preset-es2015": "^6.3.13",
|
||||
"babel-preset-stage-0": "^6.3.13",
|
||||
"dependency-check": "^2.5.1",
|
||||
"standard": "^8.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "babel --source-maps --out-dir=dist/ src/",
|
||||
"depcheck": "dependency-check ./package.json",
|
||||
"dev": "babel --watch --source-maps --out-dir=dist/ src/",
|
||||
"lint": "standard",
|
||||
"posttest": "yarn run lint && yarn run depcheck",
|
||||
"prepublish": "yarn run build"
|
||||
},
|
||||
"standard": {
|
||||
"ignore": [
|
||||
"dist/**"
|
||||
],
|
||||
"parser": "babel-eslint"
|
||||
}
|
||||
}
|
||||
131
packages/xo-acl-resolver/src/index.js
Normal file
131
packages/xo-acl-resolver/src/index.js
Normal file
@@ -0,0 +1,131 @@
|
||||
// These global variables are not a problem because the algorithm is
|
||||
// synchronous.
|
||||
let permissionsByObject
|
||||
let getObject
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
const authorized = () => true // eslint-disable-line no-unused-vars
|
||||
const forbiddden = () => false // eslint-disable-line no-unused-vars
|
||||
|
||||
const and = (...checkers) => (object, permission) => { // eslint-disable-line no-unused-vars
|
||||
for (const checker of checkers) {
|
||||
if (!checker(object, permission)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
const or = (...checkers) => (object, permission) => { // eslint-disable-line no-unused-vars
|
||||
for (const checker of checkers) {
|
||||
if (checker(object, permission)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
const checkMember = (memberName) => (object, permission) => {
|
||||
const member = object[memberName]
|
||||
return member !== object.id && checkAuthorization(member, permission)
|
||||
}
|
||||
|
||||
const checkSelf = ({ id }, permission) => {
|
||||
const permissionsForObject = permissionsByObject[id]
|
||||
|
||||
return (
|
||||
permissionsForObject &&
|
||||
permissionsForObject[permission]
|
||||
)
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const checkAuthorizationByTypes = {
|
||||
host: or(checkSelf, checkMember('$pool')),
|
||||
|
||||
message: checkMember('$object'),
|
||||
|
||||
network: or(checkSelf, checkMember('$pool')),
|
||||
|
||||
SR: or(checkSelf, checkMember('$pool')),
|
||||
|
||||
task: checkMember('$host'),
|
||||
|
||||
VBD: checkMember('VDI'),
|
||||
|
||||
// Access to a VDI is granted if the user has access to the
|
||||
// containing SR or to a linked VM.
|
||||
VDI (vdi, permission) {
|
||||
// Check authorization for the containing SR.
|
||||
if (checkAuthorization(vdi.$SR, permission)) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check authorization for each of the connected VMs.
|
||||
for (const vbdId of vdi.$VBDs) {
|
||||
if (checkAuthorization(getObject(vbdId).VM, permission)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
},
|
||||
|
||||
'VDI-snapshot': checkMember('$snapshot_of'),
|
||||
|
||||
VIF: or(checkMember('$network'), checkMember('$VM')),
|
||||
|
||||
VM: or(checkSelf, checkMember('$container')),
|
||||
|
||||
'VM-controller': checkMember('$container'),
|
||||
|
||||
'VM-snapshot': checkMember('$snapshot_of'),
|
||||
|
||||
'VM-template': or(checkSelf, checkMember('$pool'))
|
||||
}
|
||||
|
||||
// Hoisting is important for this function.
|
||||
function checkAuthorization (objectId, permission) {
|
||||
const object = getObject(objectId)
|
||||
if (!object) {
|
||||
return false
|
||||
}
|
||||
|
||||
const checker = checkAuthorizationByTypes[object.type] || checkSelf
|
||||
|
||||
return checker(object, permission)
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export default (
|
||||
permissionsByObject_,
|
||||
getObject_,
|
||||
permissions,
|
||||
permission
|
||||
) => {
|
||||
// Assign global variables.
|
||||
permissionsByObject = permissionsByObject_
|
||||
getObject = getObject_
|
||||
|
||||
try {
|
||||
if (permission) {
|
||||
return checkAuthorization(permissions, permission)
|
||||
} else {
|
||||
for (const [objectId, permission] of permissions) {
|
||||
if (!checkAuthorization(objectId, permission)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
} finally {
|
||||
// Free the global variables.
|
||||
permissionsByObject = getObject = null
|
||||
}
|
||||
}
|
||||
2511
packages/xo-acl-resolver/yarn.lock
Normal file
2511
packages/xo-acl-resolver/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
90
packages/xo-cli/.jshintrc
Normal file
90
packages/xo-cli/.jshintrc
Normal file
@@ -0,0 +1,90 @@
|
||||
{
|
||||
// Julien Fontanet JSHint configuration
|
||||
//
|
||||
// Changes from defaults:
|
||||
// - all enforcing options (except `++` & `--`) enabled
|
||||
// - single quotes
|
||||
// - indentation set to 2 instead of 4
|
||||
// - almost all relaxing options disabled
|
||||
// - allow expression statements (necessary for chai.expect())
|
||||
// - allow global strict (most of my devs are in Node.js or Browserify)
|
||||
// - environments are set to Browserify, mocha & Node.js
|
||||
//
|
||||
// See http://jshint.com/docs/ for more details
|
||||
|
||||
"maxerr" : 50, // {int} Maximum error before stopping
|
||||
|
||||
// Enforcing
|
||||
"bitwise" : true, // true: Prohibit bitwise operators (&, |, ^, etc.)
|
||||
"camelcase" : true, // true: Identifiers must be in camelCase
|
||||
"curly" : true, // true: Require {} for every new block or scope
|
||||
"eqeqeq" : true, // true: Require triple equals (===) for comparison
|
||||
"forin" : true, // true: Require filtering for..in loops with obj.hasOwnProperty()
|
||||
"immed" : true, // true: Require immediate invocations to be wrapped in parens e.g. `(function () { } ());`
|
||||
"indent" : 2, // {int} Number of spaces to use for indentation
|
||||
"latedef" : true, // true: Require variables/functions to be defined before being used
|
||||
"newcap" : true, // true: Require capitalization of all constructor functions e.g. `new F()`
|
||||
"noarg" : true, // true: Prohibit use of `arguments.caller` and `arguments.callee`
|
||||
"noempty" : true, // true: Prohibit use of empty blocks
|
||||
"nonew" : true, // true: Prohibit use of constructors for side-effects (without assignment)
|
||||
"plusplus" : false, // true: Prohibit use of `++` & `--`
|
||||
"quotmark" : "single", // Quotation mark consistency:
|
||||
// false : do nothing (default)
|
||||
// true : ensure whatever is used is consistent
|
||||
// "single" : require single quotes
|
||||
// "double" : require double quotes
|
||||
"undef" : true, // true: Require all non-global variables to be declared (prevents global leaks)
|
||||
"unused" : true, // true: Require all defined variables be used
|
||||
"strict" : true, // true: Requires all functions run in ES5 Strict Mode
|
||||
"maxparams" : 4, // {int} Max number of formal params allowed per function
|
||||
"maxdepth" : 3, // {int} Max depth of nested blocks (within functions)
|
||||
"maxstatements" : 20, // {int} Max number statements per function
|
||||
"maxcomplexity" : 7, // {int} Max cyclomatic complexity per function
|
||||
"maxlen" : 80, // {int} Max number of characters per line
|
||||
|
||||
// Relaxing
|
||||
"asi" : false, // true: Tolerate Automatic Semicolon Insertion (no semicolons)
|
||||
"boss" : false, // true: Tolerate assignments where comparisons would be expected
|
||||
"debug" : false, // true: Allow debugger statements e.g. browser breakpoints.
|
||||
"eqnull" : false, // true: Tolerate use of `== null`
|
||||
"es5" : false, // true: Allow ES5 syntax (ex: getters and setters)
|
||||
"esnext" : false, // true: Allow ES.next (ES6) syntax (ex: `const`)
|
||||
"moz" : false, // true: Allow Mozilla specific syntax (extends and overrides esnext features)
|
||||
// (ex: `for each`, multiple try/catch, function expression…)
|
||||
"evil" : false, // true: Tolerate use of `eval` and `new Function()`
|
||||
"expr" : true, // true: Tolerate `ExpressionStatement` as Programs
|
||||
"funcscope" : false, // true: Tolerate defining variables inside control statements
|
||||
"globalstrict" : true, // true: Allow global "use strict" (also enables 'strict')
|
||||
"iterator" : false, // true: Tolerate using the `__iterator__` property
|
||||
"lastsemic" : false, // true: Tolerate omitting a semicolon for the last statement of a 1-line block
|
||||
"laxbreak" : false, // true: Tolerate possibly unsafe line breakings
|
||||
"laxcomma" : false, // true: Tolerate comma-first style coding
|
||||
"loopfunc" : false, // true: Tolerate functions being defined in loops
|
||||
"multistr" : false, // true: Tolerate multi-line strings
|
||||
"proto" : false, // true: Tolerate using the `__proto__` property
|
||||
"scripturl" : false, // true: Tolerate script-targeted URLs
|
||||
"shadow" : false, // true: Allows re-define variables later in code e.g. `var x=1; x=2;`
|
||||
"sub" : false, // true: Tolerate using `[]` notation when it can still be expressed in dot notation
|
||||
"supernew" : false, // true: Tolerate `new function () { ... };` and `new Object;`
|
||||
"validthis" : false, // true: Tolerate using this in a non-constructor function
|
||||
|
||||
// Environments
|
||||
"browser" : false, // Web Browser (window, document, etc)
|
||||
"browserify" : true, // Browserify (node.js code in the browser)
|
||||
"couch" : false, // CouchDB
|
||||
"devel" : true, // Development/debugging (alert, confirm, etc)
|
||||
"dojo" : false, // Dojo Toolkit
|
||||
"jquery" : false, // jQuery
|
||||
"mocha" : true, // mocha
|
||||
"mootools" : false, // MooTools
|
||||
"node" : true, // Node.js
|
||||
"nonstandard" : false, // Widely adopted globals (escape, unescape, etc)
|
||||
"prototypejs" : false, // Prototype and Scriptaculous
|
||||
"rhino" : false, // Rhino
|
||||
"worker" : false, // Web Workers
|
||||
"wsh" : false, // Windows Scripting Host
|
||||
"yui" : false, // Yahoo User Interface
|
||||
|
||||
// Custom Globals
|
||||
"globals" : {} // additional predefined global variables
|
||||
}
|
||||
130
packages/xo-cli/README.md
Normal file
130
packages/xo-cli/README.md
Normal file
@@ -0,0 +1,130 @@
|
||||
# XO-CLI
|
||||
[](http://travis-ci.org/vatesfr/xen-orchestra)
|
||||
[](https://david-dm.org/vatesfr/xo-cli)
|
||||
[](https://david-dm.org/vatesfr/xo-cli#info=devDependencies)
|
||||
|
||||
> Basic CLI for Xen-Orchestra
|
||||
|
||||
## Installation
|
||||
|
||||
#### [npm](https://npmjs.org/package/xo-cli)
|
||||
|
||||
```
|
||||
npm install -g xo-cli
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
> xo-cli --help
|
||||
Usage:
|
||||
|
||||
xo-cli --register [<XO-Server URL>] [<username>] [<password>]
|
||||
Registers the XO instance to use.
|
||||
|
||||
xo-cli --unregister
|
||||
Remove stored credentials.
|
||||
|
||||
xo-cli --list-commands [--json] [<pattern>]...
|
||||
Returns the list of available commands on the current XO instance.
|
||||
|
||||
The patterns can be used to filter on command names.
|
||||
|
||||
xo-cli --list-objects [--<property>]… [<property>=<value>]...
|
||||
Returns a list of XO objects.
|
||||
|
||||
--<property>
|
||||
Restricts displayed properties to those listed.
|
||||
|
||||
<property>=<value>
|
||||
Restricted displayed objects to those matching the patterns.
|
||||
|
||||
xo-cli <command> [<name>=<value>]...
|
||||
Executes a command on the current XO instance.
|
||||
```
|
||||
|
||||
#### Register your XO instance
|
||||
|
||||
```
|
||||
> xo-cli --register http://xo.my-company.net admin@admin.net admin
|
||||
Successfully logged with admin@admin.net
|
||||
```
|
||||
|
||||
Note: only a token will be saved in the configuration file.
|
||||
|
||||
#### List available objects
|
||||
|
||||
Prints all objects:
|
||||
|
||||
```
|
||||
> xo-cli --list-objects
|
||||
```
|
||||
|
||||
It is possible to filter on object properties, for instance to prints
|
||||
all VM templates:
|
||||
|
||||
```
|
||||
> xo-cli --list-objects type=VM-template
|
||||
```
|
||||
|
||||
#### List available commands
|
||||
|
||||
```
|
||||
> xo-cli --list-commands
|
||||
```
|
||||
|
||||
Commands can be filtered using patterns:
|
||||
|
||||
```
|
||||
> xo-cli --list-commands '{user,group}.*'
|
||||
```
|
||||
|
||||
#### Execute a command
|
||||
|
||||
The same syntax is used for all commands: `xo-cli <command> <param
|
||||
name>=<value>...`
|
||||
|
||||
E.g., adding a new server:
|
||||
|
||||
```
|
||||
> xo-cli server.add host=my.server.net username=root password=secret-password
|
||||
42
|
||||
```
|
||||
|
||||
The return value is the identifier of this new server in XO.
|
||||
|
||||
Parameters (except `true` and `false` which are correctly parsed as
|
||||
booleans) are assumed to be strings, for other types, you may use JSON
|
||||
encoding by prefixing with `json:`:
|
||||
|
||||
```
|
||||
> xo-cli foo.bar baz='json:[1, 2, 3]'
|
||||
```
|
||||
|
||||
##### VM export
|
||||
|
||||
```
|
||||
> xo-cli vm.export vm=a01667e0-8e29-49fc-a550-17be4226783c @=vm.xva
|
||||
```
|
||||
|
||||
##### VM import
|
||||
|
||||
```
|
||||
> xo-cli vm.import host=60a6939e-8b0a-4352-9954-5bde44bcdf7d @=vm.xva
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are *very* welcome, either on the documentation or on
|
||||
the code.
|
||||
|
||||
You may:
|
||||
|
||||
- report any [issue](https://github.com/vatesfr/xo-web/issues)
|
||||
you've encountered;
|
||||
- fork and create a pull request.
|
||||
|
||||
## License
|
||||
|
||||
XO-CLI is released under the [AGPL
|
||||
v3](http://www.gnu.org/licenses/agpl-3.0-standalone.html).
|
||||
54
packages/xo-cli/config.js
Normal file
54
packages/xo-cli/config.js
Normal file
@@ -0,0 +1,54 @@
|
||||
'use strict'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
var promisify = require('bluebird').promisify
|
||||
|
||||
var readFile = promisify(require('fs').readFile)
|
||||
var writeFile = promisify(require('fs').writeFile)
|
||||
|
||||
var assign = require('lodash/assign')
|
||||
var l33t = require('l33teral')
|
||||
var mkdirp = promisify(require('mkdirp'))
|
||||
var xdgBasedir = require('xdg-basedir')
|
||||
|
||||
// ===================================================================
|
||||
|
||||
var configPath = xdgBasedir.config + '/xo-cli'
|
||||
var configFile = configPath + '/config.json'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
var load = exports.load = function () {
|
||||
return readFile(configFile).then(JSON.parse).catch(function () {
|
||||
return {}
|
||||
})
|
||||
}
|
||||
|
||||
exports.get = function (path) {
|
||||
return load().then(function (config) {
|
||||
return l33t(config).tap(path)
|
||||
})
|
||||
}
|
||||
|
||||
var save = exports.save = function (config) {
|
||||
return mkdirp(configPath).then(function () {
|
||||
return writeFile(configFile, JSON.stringify(config))
|
||||
})
|
||||
}
|
||||
|
||||
exports.set = function (data) {
|
||||
return load().then(function (config) {
|
||||
return save(assign(config, data))
|
||||
})
|
||||
}
|
||||
|
||||
exports.unset = function (paths) {
|
||||
return load().then(function (config) {
|
||||
var l33tConfig = l33t(config)
|
||||
;[].concat(paths).forEach(function (path) {
|
||||
l33tConfig.purge(path, true)
|
||||
})
|
||||
return save(config)
|
||||
})
|
||||
}
|
||||
410
packages/xo-cli/index.js
Executable file
410
packages/xo-cli/index.js
Executable file
@@ -0,0 +1,410 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
'use strict'
|
||||
|
||||
var Bluebird = require('bluebird')
|
||||
Bluebird.longStackTraces()
|
||||
|
||||
var createReadStream = require('fs').createReadStream
|
||||
var createWriteStream = require('fs').createWriteStream
|
||||
var resolveUrl = require('url').resolve
|
||||
var stat = require('fs-promise').stat
|
||||
|
||||
var chalk = require('chalk')
|
||||
var eventToPromise = require('event-to-promise')
|
||||
var filter = require('lodash/filter')
|
||||
var forEach = require('lodash/forEach')
|
||||
var getKeys = require('lodash/keys')
|
||||
var got = require('got')
|
||||
var humanFormat = require('human-format')
|
||||
var identity = require('lodash/identity')
|
||||
var isArray = require('lodash/isArray')
|
||||
var isObject = require('lodash/isObject')
|
||||
var micromatch = require('micromatch')
|
||||
var multiline = require('multiline')
|
||||
var nicePipe = require('nice-pipe')
|
||||
var pairs = require('lodash/toPairs')
|
||||
var pick = require('lodash/pick')
|
||||
var prettyMs = require('pretty-ms')
|
||||
var progressStream = require('progress-stream')
|
||||
var Xo = require('xo-lib').default
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
var config = require('./config')
|
||||
|
||||
// ===================================================================
|
||||
|
||||
function connect () {
|
||||
return config.load().bind({}).then(function (config) {
|
||||
if (!config.server) {
|
||||
throw new Error('no server to connect to!')
|
||||
}
|
||||
|
||||
if (!config.token) {
|
||||
throw new Error('no token available')
|
||||
}
|
||||
|
||||
var xo = new Xo({ url: config.server })
|
||||
|
||||
return xo.open().then(function () {
|
||||
return xo.signIn({ token: config.token })
|
||||
}).then(function () {
|
||||
return xo
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function _startsWith (string, search) {
|
||||
return string.lastIndexOf(search, 0) === 0
|
||||
}
|
||||
|
||||
var FLAG_RE = /^--([^=]+)(?:=([^]*))?$/
|
||||
function extractFlags (args) {
|
||||
var flags = {}
|
||||
|
||||
var i = 0
|
||||
var n = args.length
|
||||
var matches
|
||||
while (i < n && (matches = args[i].match(FLAG_RE))) {
|
||||
var value = matches[2]
|
||||
|
||||
flags[matches[1]] = value === undefined ? true : value
|
||||
++i
|
||||
}
|
||||
args.splice(0, i)
|
||||
|
||||
return flags
|
||||
}
|
||||
|
||||
var PARAM_RE = /^([^=]+)=([^]*)$/
|
||||
function parseParameters (args) {
|
||||
var params = {}
|
||||
forEach(args, function (arg) {
|
||||
var matches
|
||||
if (!(matches = arg.match(PARAM_RE))) {
|
||||
throw new Error('invalid arg: ' + arg)
|
||||
}
|
||||
var name = matches[1]
|
||||
var value = matches[2]
|
||||
|
||||
if (_startsWith(value, 'json:')) {
|
||||
value = JSON.parse(value.slice(5))
|
||||
}
|
||||
|
||||
if (name === '@') {
|
||||
params['@'] = value
|
||||
return
|
||||
}
|
||||
|
||||
if (value === 'true') {
|
||||
value = true
|
||||
} else if (value === 'false') {
|
||||
value = false
|
||||
}
|
||||
|
||||
params[name] = value
|
||||
})
|
||||
|
||||
return params
|
||||
}
|
||||
|
||||
var humanFormatOpts = {
|
||||
unit: 'B',
|
||||
scale: 'binary'
|
||||
}
|
||||
|
||||
function printProgress (progress) {
|
||||
if (progress.length) {
|
||||
console.warn('%s% of %s @ %s/s - ETA %s',
|
||||
Math.round(progress.percentage),
|
||||
humanFormat(progress.length, humanFormatOpts),
|
||||
humanFormat(progress.speed, humanFormatOpts),
|
||||
prettyMs(progress.eta * 1e3)
|
||||
)
|
||||
} else {
|
||||
console.warn('%s @ %s/s',
|
||||
humanFormat(progress.transferred, humanFormatOpts),
|
||||
humanFormat(progress.speed, humanFormatOpts)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function wrap (val) {
|
||||
return function wrappedValue () {
|
||||
return val
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
var help = wrap((function (pkg) {
|
||||
return multiline.stripIndent(function () { /*
|
||||
Usage:
|
||||
|
||||
$name --register [<XO-Server URL>] [<username>] [<password>]
|
||||
Registers the XO instance to use.
|
||||
|
||||
$name --unregister
|
||||
Remove stored credentials.
|
||||
|
||||
$name --list-commands [--json] [<pattern>]...
|
||||
Returns the list of available commands on the current XO instance.
|
||||
|
||||
The patterns can be used to filter on command names.
|
||||
|
||||
$name --list-objects [--<property>]… [<property>=<value>]...
|
||||
Returns a list of XO objects.
|
||||
|
||||
--<property>
|
||||
Restricts displayed properties to those listed.
|
||||
|
||||
<property>=<value>
|
||||
Restricted displayed objects to those matching the patterns.
|
||||
|
||||
$name <command> [<name>=<value>]...
|
||||
Executes a command on the current XO instance.
|
||||
|
||||
$name v$version
|
||||
*/ }).replace(/<([^>]+)>|\$(\w+)/g, function (_, arg, key) {
|
||||
if (arg) {
|
||||
return '<' + chalk.yellow(arg) + '>'
|
||||
}
|
||||
|
||||
if (key === 'name') {
|
||||
return chalk.bold(pkg[key])
|
||||
}
|
||||
|
||||
return pkg[key]
|
||||
})
|
||||
})(require('./package')))
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
function main (args) {
|
||||
if (!args || !args.length || args[0] === '-h') {
|
||||
return help()
|
||||
}
|
||||
|
||||
var fnName = args[0].replace(/^--|-\w/g, function (match) {
|
||||
if (match === '--') {
|
||||
return ''
|
||||
}
|
||||
|
||||
return match[1].toUpperCase()
|
||||
})
|
||||
if (fnName in exports) {
|
||||
return exports[fnName](args.slice(1))
|
||||
}
|
||||
|
||||
return exports.call(args)
|
||||
}
|
||||
exports = module.exports = main
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
exports.help = help
|
||||
|
||||
function register (args) {
|
||||
var xo = new Xo({ url: args[0] })
|
||||
return xo.open().then(function () {
|
||||
return xo.signIn({
|
||||
email: args[1],
|
||||
password: args[2]
|
||||
})
|
||||
}).then(function () {
|
||||
console.log('Successfully logged with', xo.user.email)
|
||||
|
||||
return xo.call('token.create')
|
||||
}).then(function (token) {
|
||||
return config.set({
|
||||
server: args[0],
|
||||
token: token
|
||||
})
|
||||
})
|
||||
}
|
||||
exports.register = register
|
||||
|
||||
function unregister () {
|
||||
return config.unset([
|
||||
'server',
|
||||
'token'
|
||||
])
|
||||
}
|
||||
exports.unregister = unregister
|
||||
|
||||
function listCommands (args) {
|
||||
return connect().then(function getMethodsInfo (xo) {
|
||||
return xo.call('system.getMethodsInfo')
|
||||
}).then(function formatMethodsInfo (methods) {
|
||||
var json = false
|
||||
var patterns = []
|
||||
forEach(args, function (arg) {
|
||||
if (arg === '--json') {
|
||||
json = true
|
||||
} else {
|
||||
patterns.push(arg)
|
||||
}
|
||||
})
|
||||
|
||||
if (patterns.length) {
|
||||
methods = pick(methods, micromatch(Object.keys(methods), patterns))
|
||||
}
|
||||
|
||||
if (json) {
|
||||
return methods
|
||||
}
|
||||
|
||||
methods = pairs(methods)
|
||||
methods.sort(function (a, b) {
|
||||
a = a[0]
|
||||
b = b[0]
|
||||
if (a < b) {
|
||||
return -1
|
||||
}
|
||||
return +(a > b)
|
||||
})
|
||||
|
||||
var str = []
|
||||
forEach(methods, function (method) {
|
||||
var name = method[0]
|
||||
var info = method[1]
|
||||
str.push(chalk.bold.blue(name))
|
||||
forEach(info.params || [], function (info, name) {
|
||||
str.push(' ')
|
||||
if (info.optional) {
|
||||
str.push('[')
|
||||
}
|
||||
|
||||
var type = info.type
|
||||
str.push(
|
||||
name,
|
||||
'=<',
|
||||
type == null
|
||||
? 'unknown type'
|
||||
: isArray(type)
|
||||
? type.join('|')
|
||||
: type,
|
||||
'>'
|
||||
)
|
||||
|
||||
if (info.optional) {
|
||||
str.push(']')
|
||||
}
|
||||
})
|
||||
str.push('\n')
|
||||
if (info.description) {
|
||||
str.push(' ', info.description, '\n')
|
||||
}
|
||||
})
|
||||
return str.join('')
|
||||
})
|
||||
}
|
||||
exports.listCommands = listCommands
|
||||
|
||||
function listObjects (args) {
|
||||
var properties = getKeys(extractFlags(args))
|
||||
var filterProperties = properties.length
|
||||
? function (object) {
|
||||
return pick(object, properties)
|
||||
}
|
||||
: identity
|
||||
|
||||
var sieve = args.length ? parseParameters(args) : null
|
||||
|
||||
return connect().then(function getXoObjects (xo) {
|
||||
return xo.call('xo.getAllObjects')
|
||||
}).then(function filterObjects (objects) {
|
||||
objects = filter(objects, sieve)
|
||||
|
||||
const stdout = process.stdout
|
||||
stdout.write('[\n')
|
||||
for (var i = 0, n = objects.length; i < n;) {
|
||||
stdout.write(JSON.stringify(filterProperties(objects[i]), null, 2))
|
||||
stdout.write(++i < n ? ',\n' : '\n')
|
||||
}
|
||||
stdout.write(']')
|
||||
})
|
||||
}
|
||||
exports.listObjects = listObjects
|
||||
|
||||
function call (args) {
|
||||
if (!args.length) {
|
||||
throw new Error('missing command name')
|
||||
}
|
||||
|
||||
var method = args.shift()
|
||||
var params = parseParameters(args)
|
||||
|
||||
var file = params['@']
|
||||
delete params['@']
|
||||
|
||||
var baseUrl
|
||||
return connect().then(function (xo) {
|
||||
// FIXME: do not use private properties.
|
||||
baseUrl = xo._url.replace(/^ws/, 'http')
|
||||
|
||||
return xo.call(method, params)
|
||||
}).then(function handleResult (result) {
|
||||
var keys, key, url
|
||||
if (
|
||||
isObject(result) &&
|
||||
(keys = getKeys(result)).length === 1
|
||||
) {
|
||||
key = keys[0]
|
||||
|
||||
if (key === '$getFrom') {
|
||||
url = resolveUrl(baseUrl, result[key])
|
||||
var output = createWriteStream(file)
|
||||
|
||||
var progress = progressStream({ time: 1e3 }, printProgress)
|
||||
|
||||
return eventToPromise(nicePipe([
|
||||
got.stream(url).on('response', function (response) {
|
||||
var length = response.headers['content-length']
|
||||
if (length) {
|
||||
progress.length(length)
|
||||
}
|
||||
}),
|
||||
progress,
|
||||
output
|
||||
]), 'finish')
|
||||
}
|
||||
|
||||
if (key === '$sendTo') {
|
||||
url = resolveUrl(baseUrl, result[key])
|
||||
|
||||
return stat(file).then(function (stats) {
|
||||
var length = stats.size
|
||||
|
||||
var input = nicePipe([
|
||||
createReadStream(file),
|
||||
progressStream({
|
||||
length: length,
|
||||
time: 1e3
|
||||
}, printProgress)
|
||||
])
|
||||
|
||||
return got.post(url, {
|
||||
body: input,
|
||||
headers: {
|
||||
'content-length': length
|
||||
},
|
||||
method: 'POST'
|
||||
}).then(function (response) {
|
||||
return response.body
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
})
|
||||
}
|
||||
exports.call = call
|
||||
|
||||
// ===================================================================
|
||||
|
||||
if (!module.parent) {
|
||||
require('exec-promise')(exports)
|
||||
}
|
||||
57
packages/xo-cli/package.json
Normal file
57
packages/xo-cli/package.json
Normal file
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"name": "xo-cli",
|
||||
"version": "0.8.2",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "Basic CLI for Xen-Orchestra",
|
||||
"keywords": [
|
||||
"xo",
|
||||
"xen-orchestra",
|
||||
"xen",
|
||||
"orchestra"
|
||||
],
|
||||
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/packages/xo-cli",
|
||||
"bugs": "https://github.com/vatesfr/xo-web/issues",
|
||||
"author": "Julien Fontanet <julien.fontanet@vates.fr>",
|
||||
"preferGlobal": true,
|
||||
"bin": {
|
||||
"xo-cli": "index.js"
|
||||
},
|
||||
"files": [
|
||||
"*.js"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/vatesfr/xen-orchestra.git"
|
||||
},
|
||||
"dependencies": {
|
||||
"bluebird": "^3.4.6",
|
||||
"chalk": "^1.1.1",
|
||||
"event-to-promise": "^0.7.0",
|
||||
"exec-promise": "^0.6.1",
|
||||
"fs-promise": "^1.0.0",
|
||||
"got": "^6.5.0",
|
||||
"human-format": "^0.7.0",
|
||||
"l33teral": "^3.0.2",
|
||||
"lodash": "^4.16.4",
|
||||
"micromatch": "^2.2.0",
|
||||
"mkdirp": "^0.5.0",
|
||||
"multiline": "^1.0.2",
|
||||
"nice-pipe": "0.0.0",
|
||||
"pretty-ms": "^2.1.0",
|
||||
"progress-stream": "^1.1.1",
|
||||
"xdg-basedir": "^2.0.0",
|
||||
"xo-lib": "^0.8.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"standard": "^8.1.0"
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "standard",
|
||||
"posttest": "yarn run lint"
|
||||
},
|
||||
"greenkeeper": {
|
||||
"ignore": [
|
||||
"nice-pipe"
|
||||
]
|
||||
}
|
||||
}
|
||||
1413
packages/xo-cli/yarn.lock
Normal file
1413
packages/xo-cli/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
24
packages/xo-collection/.npmignore
Normal file
24
packages/xo-collection/.npmignore
Normal file
@@ -0,0 +1,24 @@
|
||||
/benchmark/
|
||||
/benchmarks/
|
||||
*.bench.js
|
||||
*.bench.js.map
|
||||
|
||||
/examples/
|
||||
example.js
|
||||
example.js.map
|
||||
*.example.js
|
||||
*.example.js.map
|
||||
|
||||
/fixture/
|
||||
/fixtures/
|
||||
*.fixture.js
|
||||
*.fixture.js.map
|
||||
*.fixtures.js
|
||||
*.fixtures.js.map
|
||||
|
||||
/test/
|
||||
/tests/
|
||||
*.spec.js
|
||||
*.spec.js.map
|
||||
|
||||
__snapshots__/
|
||||
662
packages/xo-collection/LICENSE
Normal file
662
packages/xo-collection/LICENSE
Normal file
@@ -0,0 +1,662 @@
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU Affero General Public License is a free, copyleft license for
|
||||
software and other kinds of works, specifically designed to ensure
|
||||
cooperation with the community in the case of network server software.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
our General Public Licenses are intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
Developers that use our General Public Licenses protect your rights
|
||||
with two steps: (1) assert copyright on the software, and (2) offer
|
||||
you this License which gives you legal permission to copy, distribute
|
||||
and/or modify the software.
|
||||
|
||||
A secondary benefit of defending all users' freedom is that
|
||||
improvements made in alternate versions of the program, if they
|
||||
receive widespread use, become available for other developers to
|
||||
incorporate. Many developers of free software are heartened and
|
||||
encouraged by the resulting cooperation. However, in the case of
|
||||
software used on network servers, this result may fail to come about.
|
||||
The GNU General Public License permits making a modified version and
|
||||
letting the public access it on a server without ever releasing its
|
||||
source code to the public.
|
||||
|
||||
The GNU Affero General Public License is designed specifically to
|
||||
ensure that, in such cases, the modified source code becomes available
|
||||
to the community. It requires the operator of a network server to
|
||||
provide the source code of the modified version running there to the
|
||||
users of that server. Therefore, public use of a modified version, on
|
||||
a publicly accessible server, gives the public access to the source
|
||||
code of the modified version.
|
||||
|
||||
An older license, called the Affero General Public License and
|
||||
published by Affero, was designed to accomplish similar goals. This is
|
||||
a different license, not a version of the Affero GPL, but Affero has
|
||||
released a new version of the Affero GPL which permits relicensing under
|
||||
this license.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, if you modify the
|
||||
Program, your modified version must prominently offer all users
|
||||
interacting with it remotely through a computer network (if your version
|
||||
supports such interaction) an opportunity to receive the Corresponding
|
||||
Source of your version by providing access to the Corresponding Source
|
||||
from a network server at no charge, through some standard or customary
|
||||
means of facilitating copying of software. This Corresponding Source
|
||||
shall include the Corresponding Source for any work covered by version 3
|
||||
of the GNU General Public License that is incorporated pursuant to the
|
||||
following paragraph.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the work with which it is combined will remain governed by version
|
||||
3 of the GNU General Public License.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU Affero General Public License from time to time. Such new versions
|
||||
will be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU Affero General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU Affero General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU Affero General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If your software can interact with users remotely through a computer
|
||||
network, you should also make sure that it provides a way for users to
|
||||
get its source. For example, if your program is a web application, its
|
||||
interface could display a "Source" link that leads users to an archive
|
||||
of the code. There are many ways you could offer source, and different
|
||||
solutions will be better for different programs; see section 13 for the
|
||||
specific requirements.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||
<http://www.gnu.org/licenses/>.
|
||||
|
||||
265
packages/xo-collection/README.md
Normal file
265
packages/xo-collection/README.md
Normal file
@@ -0,0 +1,265 @@
|
||||
# xo-collection [](https://travis-ci.org/vatesfr/xen-orchestra)
|
||||
|
||||
> Generic in-memory collection with events
|
||||
|
||||
## Install
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/xo-collection):
|
||||
|
||||
```
|
||||
> npm install --save xo-collection
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```javascript
|
||||
var Collection = require('xo-collection')
|
||||
```
|
||||
|
||||
### Creation
|
||||
|
||||
```javascript
|
||||
// Creates a new collection.
|
||||
var col = new Collection()
|
||||
```
|
||||
|
||||
### Manipulation
|
||||
|
||||
**Inserting a new item**
|
||||
|
||||
```javascript
|
||||
col.add('foo', true)
|
||||
```
|
||||
|
||||
- **Throws** `DuplicateItem` if the item is already in the collection.
|
||||
|
||||
**Updating an existing item**
|
||||
|
||||
```javascript
|
||||
col.update('foo', false)
|
||||
```
|
||||
|
||||
- **Throws** `NoSuchItem` if the item is not in the collection.
|
||||
|
||||
**Inserting or updating an item**
|
||||
|
||||
```javascript
|
||||
col.set('bar', true)
|
||||
```
|
||||
|
||||
**Notifying an external update**
|
||||
|
||||
> If an item is an object, it can be updated directly without using
|
||||
> the `set`/`update` methods.
|
||||
>
|
||||
> To make sure the collection stays in sync and the correct events are
|
||||
> sent, the `touch` method can be used to notify the change.
|
||||
|
||||
```javascript
|
||||
var baz = {}
|
||||
|
||||
col.add('baz', baz)
|
||||
|
||||
baz.prop = true
|
||||
col.touch('baz')
|
||||
```
|
||||
|
||||
> Because this is a much used pattern, `touch` returns the item to
|
||||
> allow its direct modification.
|
||||
|
||||
```javascript
|
||||
col.touch('baz').prop = false
|
||||
```
|
||||
|
||||
- **Throws** `NoSuchItem` if the item is not in the collection.
|
||||
- **Throws** `IllegalTouch` if the item is not an object.
|
||||
|
||||
**Removing an existing item**
|
||||
|
||||
```javascript
|
||||
col.remove('bar')
|
||||
```
|
||||
|
||||
- **Throws** `NoSuchItem` if the item is not in the collection.
|
||||
|
||||
**Removing an item without error**
|
||||
|
||||
This is the symmetric method of `set()`: it removes the item if it
|
||||
exists otherwise does nothing.
|
||||
|
||||
```javascript
|
||||
col.unset('bar')
|
||||
```
|
||||
|
||||
**Removing all items**
|
||||
|
||||
```javascript
|
||||
col.clear()
|
||||
```
|
||||
|
||||
### Query
|
||||
|
||||
**Checking the existence of an item**
|
||||
|
||||
```javascript
|
||||
var hasBar = col.has('bar')
|
||||
```
|
||||
|
||||
**Getting an existing item**
|
||||
|
||||
```javascript
|
||||
var foo = col.get('foo')
|
||||
|
||||
// The second parameter can be used to specify a fallback in case the
|
||||
// item does not exist.
|
||||
var bar = col.get('bar', 6.28)
|
||||
```
|
||||
|
||||
- **Throws** `NoSuchItem` if the item is not in the collection and no
|
||||
fallback has been passed.
|
||||
|
||||
**Getting a read-only view of the collection**
|
||||
|
||||
> This property is useful for example to iterate over the collection
|
||||
> or to make advanced queries with the help of utility libraries such
|
||||
> as lodash.
|
||||
|
||||
```javascript
|
||||
var _ = require('lodash')
|
||||
|
||||
// Prints all the items.
|
||||
_.forEach(col.all, function (value, key) {
|
||||
console.log('- %s: %j', key, value)
|
||||
})
|
||||
|
||||
// Finds all the items which are objects and have a property
|
||||
// `active` which equals `true`.
|
||||
var results = _.where(col.all, { active: true })
|
||||
```
|
||||
|
||||
**Getting the number of items**
|
||||
|
||||
```javascript
|
||||
var size = col.size
|
||||
```
|
||||
|
||||
### Events
|
||||
|
||||
> The events are emitted asynchronously (at the next turn/tick of the
|
||||
> event loop) and are deduplicated which means, for instance, that an
|
||||
> addition followed by an update will result only in a single
|
||||
> addition.
|
||||
|
||||
**New items**
|
||||
|
||||
```javascript
|
||||
col.on('add', (added) => {
|
||||
forEach(added, (value, key) => {
|
||||
console.log('+ %s: %j', key, value)
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
**Updated items**
|
||||
|
||||
```javascript
|
||||
col.on('update', (updated) => {
|
||||
forEach(updated, (value, key) => {
|
||||
console.log('± %s: %j', key, value)
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
**Removed items**
|
||||
|
||||
```javascript
|
||||
col.on('remove', (removed) => {
|
||||
// For consistency, `removed` is also a map but contrary to `added`
|
||||
// and `updated`, the values associated to the keys are not
|
||||
// significant since the items have already be removed.
|
||||
|
||||
forEach(removed, (value, key) => {
|
||||
console.log('- %s', key)
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
**End of update**
|
||||
|
||||
> Emitted when all the update process is finished and all the update
|
||||
> events has been emitted.
|
||||
|
||||
```javascript
|
||||
col.on('finish', () => {
|
||||
console.log('the collection has been updated')
|
||||
})
|
||||
```
|
||||
|
||||
### Iteration
|
||||
|
||||
```javascript
|
||||
for (const [key, value] of col) {
|
||||
console.log('- %s: %j', key, value)
|
||||
}
|
||||
|
||||
for (const key of col.keys()) {
|
||||
console.log('- %s', key)
|
||||
}
|
||||
|
||||
for (const value of col.values()) {
|
||||
console.log('- %j', value)
|
||||
}
|
||||
```
|
||||
|
||||
### Views
|
||||
|
||||
```javascript
|
||||
const View = require('xo-collection/view')
|
||||
```
|
||||
|
||||
> A view is a read-only collection which contains only the items of a
|
||||
> parent collection which satisfy a predicate.
|
||||
>
|
||||
> It is updated at most once per turn of the event loop and therefore
|
||||
> can be briefly invalid.
|
||||
|
||||
```javascript
|
||||
const myView = new View(parentCollection, function predicate (value, key) {
|
||||
// This function should return a boolean indicating whether the
|
||||
// current item should be in this view.
|
||||
})
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
```
|
||||
# Install dependencies
|
||||
> npm install
|
||||
|
||||
# Run the tests
|
||||
> npm test
|
||||
|
||||
# Continuously compile
|
||||
> npm run dev
|
||||
|
||||
# Continuously run the tests
|
||||
> npm run dev-test
|
||||
|
||||
# Build for production (automatically called by npm install)
|
||||
> npm run build
|
||||
```
|
||||
|
||||
## Contributions
|
||||
|
||||
Contributions are *very* welcomed, either on the documentation or on
|
||||
the code.
|
||||
|
||||
You may:
|
||||
|
||||
- report any [issue](https://github.com/vatesfr/xo-web/issues)
|
||||
you've encountered;
|
||||
- fork and create a pull request.
|
||||
|
||||
## License
|
||||
|
||||
ISC © [Vates SAS](http://vates.fr)
|
||||
1
packages/xo-collection/index.js
Normal file
1
packages/xo-collection/index.js
Normal file
@@ -0,0 +1 @@
|
||||
module.exports = require('./dist/index')
|
||||
86
packages/xo-collection/package.json
Normal file
86
packages/xo-collection/package.json
Normal file
@@ -0,0 +1,86 @@
|
||||
{
|
||||
"name": "xo-collection",
|
||||
"version": "0.4.1",
|
||||
"license": "ISC",
|
||||
"description": "Generic in-memory collection with events",
|
||||
"keywords": [],
|
||||
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/packages/xo-collection",
|
||||
"bugs": "https://github.com/vatesfr/xo-web/issues",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/vatesfr/xen-orchestra.git"
|
||||
},
|
||||
"author": {
|
||||
"name": "Fabrice Marsaud",
|
||||
"email": "fabrice.marsaud@vates.fr"
|
||||
},
|
||||
"preferGlobal": false,
|
||||
"main": "dist/collection",
|
||||
"bin": {},
|
||||
"files": [
|
||||
"dist/",
|
||||
"*.js"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
},
|
||||
"dependencies": {
|
||||
"babel-runtime": "^6.18.0",
|
||||
"kindof": "^2.0.0",
|
||||
"lodash": "^4.17.2",
|
||||
"make-error": "^1.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-cli": "^6.18.0",
|
||||
"babel-eslint": "^7.1.0",
|
||||
"babel-plugin-lodash": "^3.2.11",
|
||||
"babel-plugin-transform-runtime": "^6.15.0",
|
||||
"babel-preset-env": "^1.1.8",
|
||||
"babel-preset-stage-3": "^6.17.0",
|
||||
"cross-env": "^3.1.4",
|
||||
"dependency-check": "^2.7.0",
|
||||
"event-to-promise": "^0.7.0",
|
||||
"jest": "^18.1.0",
|
||||
"rimraf": "^2.5.4",
|
||||
"standard": "^8.6.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
|
||||
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
|
||||
"dev-test": "jest --bail --watch",
|
||||
"posttest": "standard && dependency-check ./package.json --entry dist/collection.js index.js unique-index.js view.js",
|
||||
"prebuild": "rimraf dist/",
|
||||
"predev": "yarn run prebuild",
|
||||
"prepublish": "yarn run build",
|
||||
"test": "jest"
|
||||
},
|
||||
"babel": {
|
||||
"plugins": [
|
||||
"lodash",
|
||||
"transform-runtime"
|
||||
],
|
||||
"presets": [
|
||||
[
|
||||
"env",
|
||||
{
|
||||
"targets": {
|
||||
"node": 4
|
||||
}
|
||||
}
|
||||
],
|
||||
"stage-3"
|
||||
]
|
||||
},
|
||||
"jest": {
|
||||
"testPathDirs": [
|
||||
"<rootDir>/src"
|
||||
],
|
||||
"testRegex": "\\.spec\\.js$"
|
||||
},
|
||||
"standard": {
|
||||
"ignore": [
|
||||
"dist"
|
||||
],
|
||||
"parser": "babel-eslint"
|
||||
}
|
||||
}
|
||||
5
packages/xo-collection/src/clear-object.js
Normal file
5
packages/xo-collection/src/clear-object.js
Normal file
@@ -0,0 +1,5 @@
|
||||
export default function clearObject (object) {
|
||||
for (const key in object) {
|
||||
delete object[key]
|
||||
}
|
||||
}
|
||||
363
packages/xo-collection/src/collection.js
Normal file
363
packages/xo-collection/src/collection.js
Normal file
@@ -0,0 +1,363 @@
|
||||
import kindOf from 'kindof'
|
||||
import {BaseError} from 'make-error'
|
||||
import {EventEmitter} from 'events'
|
||||
import {forEach} from 'lodash'
|
||||
|
||||
import isEmpty from './is-empty'
|
||||
import isObject from './is-object'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const {
|
||||
create: createObject,
|
||||
prototype: { hasOwnProperty }
|
||||
} = Object
|
||||
|
||||
export const ACTION_ADD = 'add'
|
||||
export const ACTION_UPDATE = 'update'
|
||||
export const ACTION_REMOVE = 'remove'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export class BufferAlreadyFlushed extends BaseError {
|
||||
constructor () {
|
||||
super('buffer flush already requested')
|
||||
}
|
||||
}
|
||||
|
||||
export class DuplicateIndex extends BaseError {
|
||||
constructor (name) {
|
||||
super('there is already an index with the name ' + name)
|
||||
}
|
||||
}
|
||||
|
||||
export class DuplicateItem extends BaseError {
|
||||
constructor (key) {
|
||||
super('there is already a item with the key ' + key)
|
||||
}
|
||||
}
|
||||
|
||||
export class IllegalTouch extends BaseError {
|
||||
constructor (value) {
|
||||
super('only an object value can be touched (found a ' + kindOf(value) + ')')
|
||||
}
|
||||
}
|
||||
|
||||
export class InvalidKey extends BaseError {
|
||||
constructor (key) {
|
||||
super('invalid key of type ' + kindOf(key))
|
||||
}
|
||||
}
|
||||
|
||||
export class NoSuchIndex extends BaseError {
|
||||
constructor (name) {
|
||||
super('there is no index with the name ' + name)
|
||||
}
|
||||
}
|
||||
|
||||
export class NoSuchItem extends BaseError {
|
||||
constructor (key) {
|
||||
super('there is no item with the key ' + key)
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export default class Collection extends EventEmitter {
|
||||
constructor () {
|
||||
super()
|
||||
|
||||
this._buffer = createObject(null)
|
||||
this._buffering = 0
|
||||
this._indexes = createObject(null)
|
||||
this._indexedItems = createObject(null)
|
||||
this._items = {} // createObject(null)
|
||||
this._size = 0
|
||||
}
|
||||
|
||||
// Overridable method used to compute the key of an item when
|
||||
// unspecified.
|
||||
//
|
||||
// Default implementation returns the `id` property.
|
||||
getKey (value) {
|
||||
return value && value.id
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// Properties
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
get all () {
|
||||
return this._items
|
||||
}
|
||||
|
||||
get indexes () {
|
||||
return this._indexedItems
|
||||
}
|
||||
|
||||
get size () {
|
||||
return this._size
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// Manipulation
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
add (keyOrObjectWithId, valueIfKey = undefined) {
|
||||
const [key, value] = this._resolveItem(keyOrObjectWithId, valueIfKey)
|
||||
this._assertHasNot(key)
|
||||
|
||||
this._items[key] = value
|
||||
this._size++
|
||||
this._touch(ACTION_ADD, key)
|
||||
}
|
||||
|
||||
clear () {
|
||||
forEach(this._items, (_, key) => this._remove(key))
|
||||
}
|
||||
|
||||
remove (keyOrObjectWithId) {
|
||||
const [key] = this._resolveItem(keyOrObjectWithId)
|
||||
this._assertHas(key)
|
||||
|
||||
this._remove(key)
|
||||
}
|
||||
|
||||
set (keyOrObjectWithId, valueIfKey = undefined) {
|
||||
const [key, value] = this._resolveItem(keyOrObjectWithId, valueIfKey)
|
||||
|
||||
const action = this.has(key) ? ACTION_UPDATE : ACTION_ADD
|
||||
this._items[key] = value
|
||||
if (action === ACTION_ADD) {
|
||||
this._size++
|
||||
}
|
||||
this._touch(action, key)
|
||||
}
|
||||
|
||||
touch (keyOrObjectWithId) {
|
||||
const [key] = this._resolveItem(keyOrObjectWithId)
|
||||
this._assertHas(key)
|
||||
const value = this.get(key)
|
||||
if (!isObject(value)) {
|
||||
throw new IllegalTouch(value)
|
||||
}
|
||||
|
||||
this._touch(ACTION_UPDATE, key)
|
||||
|
||||
return this.get(key)
|
||||
}
|
||||
|
||||
unset (keyOrObjectWithId) {
|
||||
const [key] = this._resolveItem(keyOrObjectWithId)
|
||||
|
||||
if (this.has(key)) {
|
||||
this._remove(key)
|
||||
}
|
||||
}
|
||||
|
||||
update (keyOrObjectWithId, valueIfKey = undefined) {
|
||||
const [key, value] = this._resolveItem(keyOrObjectWithId, valueIfKey)
|
||||
this._assertHas(key)
|
||||
|
||||
this._items[key] = value
|
||||
this._touch(ACTION_UPDATE, key)
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// Query
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
get (key, defaultValue) {
|
||||
if (this.has(key)) {
|
||||
return this._items[key]
|
||||
}
|
||||
|
||||
if (arguments.length > 1) {
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
// Throws a NoSuchItem.
|
||||
this._assertHas(key)
|
||||
}
|
||||
|
||||
has (key) {
|
||||
return hasOwnProperty.call(this._items, key)
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// Indexes
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
createIndex (name, index) {
|
||||
const {_indexes: indexes} = this
|
||||
if (hasOwnProperty.call(indexes, name)) {
|
||||
throw new DuplicateIndex(name)
|
||||
}
|
||||
|
||||
indexes[name] = index
|
||||
this._indexedItems[name] = index.items
|
||||
|
||||
index._attachCollection(this)
|
||||
}
|
||||
|
||||
deleteIndex (name) {
|
||||
const {_indexes: indexes} = this
|
||||
if (!hasOwnProperty.call(indexes, name)) {
|
||||
throw new NoSuchIndex(name)
|
||||
}
|
||||
|
||||
const index = indexes[name]
|
||||
delete indexes[name]
|
||||
delete this._indexedItems[name]
|
||||
|
||||
index._detachCollection(this)
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// Iteration
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
* [Symbol.iterator] () {
|
||||
const {_items: items} = this
|
||||
|
||||
for (const key in items) {
|
||||
yield [key, items[key]]
|
||||
}
|
||||
}
|
||||
|
||||
* keys () {
|
||||
const {_items: items} = this
|
||||
|
||||
for (const key in items) {
|
||||
yield key
|
||||
}
|
||||
}
|
||||
|
||||
* values () {
|
||||
const {_items: items} = this
|
||||
|
||||
for (const key in items) {
|
||||
yield items[key]
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// Events buffering
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
bufferEvents () {
|
||||
++this._buffering
|
||||
|
||||
let called = false
|
||||
return () => {
|
||||
if (called) {
|
||||
throw new BufferAlreadyFlushed()
|
||||
}
|
||||
called = true
|
||||
|
||||
if (--this._buffering) {
|
||||
return
|
||||
}
|
||||
|
||||
const {_buffer: buffer} = this
|
||||
|
||||
// Due to deduplication there could be nothing in the buffer.
|
||||
if (isEmpty(buffer)) {
|
||||
return
|
||||
}
|
||||
|
||||
const data = {
|
||||
add: createObject(null),
|
||||
remove: createObject(null),
|
||||
update: createObject(null)
|
||||
}
|
||||
|
||||
for (const key in this._buffer) {
|
||||
data[buffer[key]][key] = this._items[key]
|
||||
}
|
||||
|
||||
forEach(data, (items, action) => {
|
||||
if (!isEmpty(items)) {
|
||||
this.emit(action, items)
|
||||
}
|
||||
})
|
||||
|
||||
// Indicates the end of the update.
|
||||
//
|
||||
// This name has been chosen because it is used in Node writable
|
||||
// streams when the data has been successfully committed.
|
||||
this.emit('finish')
|
||||
|
||||
this._buffer = createObject(null)
|
||||
}
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
|
||||
_assertHas (key) {
|
||||
if (!this.has(key)) {
|
||||
throw new NoSuchItem(key)
|
||||
}
|
||||
}
|
||||
|
||||
_assertHasNot (key) {
|
||||
if (this.has(key)) {
|
||||
throw new DuplicateItem(key)
|
||||
}
|
||||
}
|
||||
|
||||
_assertValidKey (key) {
|
||||
if (!this._isValidKey(key)) {
|
||||
throw new InvalidKey(key)
|
||||
}
|
||||
}
|
||||
|
||||
_isValidKey (key) {
|
||||
return typeof key === 'number' || typeof key === 'string'
|
||||
}
|
||||
|
||||
_remove (key) {
|
||||
delete this._items[key]
|
||||
this._size--
|
||||
this._touch(ACTION_REMOVE, key)
|
||||
}
|
||||
|
||||
_resolveItem (keyOrObjectWithId, valueIfKey = undefined) {
|
||||
if (valueIfKey !== undefined) {
|
||||
this._assertValidKey(keyOrObjectWithId)
|
||||
|
||||
return [keyOrObjectWithId, valueIfKey]
|
||||
}
|
||||
|
||||
if (this._isValidKey(keyOrObjectWithId)) {
|
||||
return [keyOrObjectWithId]
|
||||
}
|
||||
|
||||
const key = this.getKey(keyOrObjectWithId)
|
||||
this._assertValidKey(key)
|
||||
|
||||
return [key, keyOrObjectWithId]
|
||||
}
|
||||
|
||||
_touch (action, key) {
|
||||
if (this._buffering === 0) {
|
||||
const flush = this.bufferEvents()
|
||||
|
||||
process.nextTick(flush)
|
||||
}
|
||||
|
||||
if (action === ACTION_ADD) {
|
||||
this._buffer[key] = this._buffer[key] ? ACTION_UPDATE : ACTION_ADD
|
||||
} else if (action === ACTION_REMOVE) {
|
||||
if (this._buffer[key] === ACTION_ADD) {
|
||||
delete this._buffer[key]
|
||||
} else {
|
||||
this._buffer[key] = ACTION_REMOVE
|
||||
}
|
||||
} else { // update
|
||||
if (!this._buffer[key]) {
|
||||
this._buffer[key] = ACTION_UPDATE
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
341
packages/xo-collection/src/collection.spec.js
Normal file
341
packages/xo-collection/src/collection.spec.js
Normal file
@@ -0,0 +1,341 @@
|
||||
/* eslint-env jest */
|
||||
|
||||
import eventToPromise from 'event-to-promise'
|
||||
import { forEach } from 'lodash'
|
||||
|
||||
import Collection, {DuplicateItem, NoSuchItem} from '..'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
function waitTicks (n = 2) {
|
||||
const {nextTick} = process
|
||||
|
||||
return new Promise(function (resolve) {
|
||||
(function waitNextTick () {
|
||||
// The first tick is handled by Promise#then()
|
||||
if (--n) {
|
||||
nextTick(waitNextTick)
|
||||
} else {
|
||||
resolve()
|
||||
}
|
||||
})()
|
||||
})
|
||||
}
|
||||
|
||||
describe('Collection', function () {
|
||||
beforeEach(function () {
|
||||
this.col = new Collection()
|
||||
this.col.add('bar', 0)
|
||||
|
||||
return waitTicks()
|
||||
})
|
||||
|
||||
it('is iterable', function () {
|
||||
const iterator = this.col[Symbol.iterator]()
|
||||
|
||||
expect(iterator.next()).toEqual({done: false, value: ['bar', 0]})
|
||||
expect(iterator.next()).toEqual({done: true, value: undefined})
|
||||
})
|
||||
|
||||
describe('#keys()', function () {
|
||||
it('returns an iterator over the keys', function () {
|
||||
const iterator = this.col.keys()
|
||||
|
||||
expect(iterator.next()).toEqual({done: false, value: 'bar'})
|
||||
expect(iterator.next()).toEqual({done: true, value: undefined})
|
||||
})
|
||||
})
|
||||
|
||||
describe('#values()', function () {
|
||||
it('returns an iterator over the values', function () {
|
||||
const iterator = this.col.values()
|
||||
|
||||
expect(iterator.next()).toEqual({done: false, value: 0})
|
||||
expect(iterator.next()).toEqual({done: true, value: undefined})
|
||||
})
|
||||
})
|
||||
|
||||
describe('#add()', function () {
|
||||
it('adds item to the collection', function () {
|
||||
const spy = jest.fn()
|
||||
this.col.on('add', spy)
|
||||
|
||||
this.col.add('foo', true)
|
||||
|
||||
expect(this.col.get('foo')).toBe(true)
|
||||
|
||||
// No sync events.
|
||||
expect(spy).not.toHaveBeenCalled()
|
||||
|
||||
// Async event.
|
||||
return eventToPromise(this.col, 'add').then(function (added) {
|
||||
expect(Object.keys(added)).toEqual([ 'foo' ])
|
||||
expect(added.foo).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
it('throws an exception if the item already exists', function () {
|
||||
expect(() => this.col.add('bar', true)).toThrowError(DuplicateItem)
|
||||
})
|
||||
|
||||
it('accepts an object with an id property', function () {
|
||||
const foo = { id: 'foo' }
|
||||
|
||||
this.col.add(foo)
|
||||
|
||||
expect(this.col.get(foo.id)).toBe(foo)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#update()', function () {
|
||||
it('updates an item of the collection', function () {
|
||||
const spy = jest.fn()
|
||||
this.col.on('update', spy)
|
||||
|
||||
this.col.update('bar', 1)
|
||||
expect(this.col.get('bar')).toBe(1) // Will be forgotten by de-duplication
|
||||
this.col.update('bar', 2)
|
||||
expect(this.col.get('bar')).toBe(2)
|
||||
|
||||
// No sync events.
|
||||
expect(spy).not.toHaveBeenCalled()
|
||||
|
||||
// Async event.
|
||||
return eventToPromise(this.col, 'update').then(function (updated) {
|
||||
expect(Object.keys(updated)).toEqual(['bar'])
|
||||
expect(updated.bar).toBe(2)
|
||||
})
|
||||
})
|
||||
|
||||
it('throws an exception if the item does not exist', function () {
|
||||
expect(() => this.col.update('baz', true)).toThrowError(NoSuchItem)
|
||||
})
|
||||
|
||||
it('accepts an object with an id property', function () {
|
||||
const bar = { id: 'bar' }
|
||||
|
||||
this.col.update(bar)
|
||||
|
||||
expect(this.col.get(bar.id)).toBe(bar)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#remove()', function () {
|
||||
it('removes an item of the collection', function () {
|
||||
const spy = jest.fn()
|
||||
this.col.on('remove', spy)
|
||||
|
||||
this.col.update('bar', 1)
|
||||
expect(this.col.get('bar')).toBe(1) // Will be forgotten by de-duplication
|
||||
this.col.remove('bar')
|
||||
|
||||
// No sync events.
|
||||
expect(spy).not.toHaveBeenCalled()
|
||||
|
||||
// Async event.
|
||||
return eventToPromise(this.col, 'remove').then(function (removed) {
|
||||
expect(Object.keys(removed)).toEqual(['bar'])
|
||||
expect(removed.bar).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
it('throws an exception if the item does not exist', function () {
|
||||
expect(() => this.col.remove('baz', true)).toThrowError(NoSuchItem)
|
||||
})
|
||||
|
||||
it('accepts an object with an id property', function () {
|
||||
const bar = { id: 'bar' }
|
||||
|
||||
this.col.remove(bar)
|
||||
|
||||
expect(this.col.has(bar.id)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#set()', function () {
|
||||
it('adds item if collection has not key', function () {
|
||||
const spy = jest.fn()
|
||||
this.col.on('add', spy)
|
||||
|
||||
this.col.set('foo', true)
|
||||
|
||||
expect(this.col.get('foo')).toBe(true)
|
||||
|
||||
// No sync events.
|
||||
expect(spy).not.toHaveBeenCalled()
|
||||
|
||||
// Async events.
|
||||
return eventToPromise(this.col, 'add').then(function (added) {
|
||||
expect(Object.keys(added)).toEqual(['foo'])
|
||||
expect(added.foo).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
it('updates item if collection has key', function () {
|
||||
const spy = jest.fn()
|
||||
this.col.on('udpate', spy)
|
||||
|
||||
this.col.set('bar', 1)
|
||||
|
||||
expect(this.col.get('bar')).toBe(1)
|
||||
|
||||
// No sync events.
|
||||
expect(spy).not.toHaveBeenCalled()
|
||||
|
||||
// Async events.
|
||||
return eventToPromise(this.col, 'update').then(function (updated) {
|
||||
expect(Object.keys(updated)).toEqual(['bar'])
|
||||
expect(updated.bar).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
it('accepts an object with an id property', function () {
|
||||
const foo = { id: 'foo' }
|
||||
|
||||
this.col.set(foo)
|
||||
|
||||
expect(this.col.get(foo.id)).toBe(foo)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#unset()', function () {
|
||||
it('removes an existing item', function () {
|
||||
this.col.unset('bar')
|
||||
|
||||
expect(this.col.has('bar')).toBe(false)
|
||||
|
||||
return eventToPromise(this.col, 'remove').then(function (removed) {
|
||||
expect(Object.keys(removed)).toEqual(['bar'])
|
||||
expect(removed.bar).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
it('does not throw if the item does not exists', function () {
|
||||
this.col.unset('foo')
|
||||
})
|
||||
|
||||
it('accepts an object with an id property', function () {
|
||||
this.col.unset({id: 'bar'})
|
||||
|
||||
expect(this.col.has('bar')).toBe(false)
|
||||
|
||||
return eventToPromise(this.col, 'remove').then(function (removed) {
|
||||
expect(Object.keys(removed)).toEqual(['bar'])
|
||||
expect(removed.bar).toBeUndefined()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('touch()', function () {
|
||||
it('can be used to signal an indirect update', function () {
|
||||
const foo = { id: 'foo' }
|
||||
this.col.add(foo)
|
||||
|
||||
return waitTicks().then(() => {
|
||||
this.col.touch(foo)
|
||||
|
||||
return eventToPromise(this.col, 'update', (items) => {
|
||||
expect(Object.keys(items)).toEqual(['foo'])
|
||||
expect(items.foo).toBe(foo)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('clear()', function () {
|
||||
it('removes all items from the collection', function () {
|
||||
this.col.clear()
|
||||
|
||||
expect(this.col.size).toBe(0)
|
||||
|
||||
return eventToPromise(this.col, 'remove').then((items) => {
|
||||
expect(Object.keys(items)).toEqual(['bar'])
|
||||
expect(items.bar).toBeUndefined()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('deduplicates events', function () {
|
||||
forEach({
|
||||
'add & update → add': [
|
||||
[
|
||||
['add', 'foo', 0],
|
||||
['update', 'foo', 1]
|
||||
],
|
||||
{
|
||||
add: {
|
||||
foo: 1
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
'add & remove → ∅': [
|
||||
[
|
||||
['add', 'foo', 0],
|
||||
['remove', 'foo']
|
||||
],
|
||||
{}
|
||||
],
|
||||
|
||||
'update & update → update': [
|
||||
[
|
||||
['update', 'bar', 1],
|
||||
['update', 'bar', 2]
|
||||
],
|
||||
{
|
||||
update: {
|
||||
bar: 2
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
'update & remove → remove': [
|
||||
[
|
||||
['update', 'bar', 1],
|
||||
['remove', 'bar']
|
||||
],
|
||||
{
|
||||
remove: {
|
||||
bar: undefined
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
'remove & add → update': [
|
||||
[
|
||||
['remove', 'bar'],
|
||||
['add', 'bar', 0]
|
||||
],
|
||||
{
|
||||
update: {
|
||||
bar: 0
|
||||
}
|
||||
}
|
||||
]
|
||||
}, ([operations, results], label) => {
|
||||
it(label, function () {
|
||||
const {col} = this
|
||||
|
||||
forEach(operations, ([method, ...args]) => {
|
||||
col[method](...args)
|
||||
})
|
||||
|
||||
const spies = Object.create(null)
|
||||
forEach(['add', 'update', 'remove'], event => {
|
||||
col.on(event, (spies[event] = jest.fn()))
|
||||
})
|
||||
|
||||
return waitTicks().then(() => {
|
||||
forEach(spies, (spy, event) => {
|
||||
const items = results[event]
|
||||
if (items) {
|
||||
expect(spy.mock.calls).toEqual([ [ items ] ])
|
||||
} else {
|
||||
expect(spy).not.toHaveBeenCalled()
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
149
packages/xo-collection/src/index.js
Normal file
149
packages/xo-collection/src/index.js
Normal file
@@ -0,0 +1,149 @@
|
||||
import { bind, iteratee } from 'lodash'
|
||||
|
||||
import clearObject from './clear-object'
|
||||
import isEmpty from './is-empty'
|
||||
import NotImplemented from './not-implemented'
|
||||
import {
|
||||
ACTION_ADD,
|
||||
ACTION_UPDATE,
|
||||
ACTION_REMOVE
|
||||
} from './collection'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export default class Index {
|
||||
constructor (computeHash) {
|
||||
if (computeHash) {
|
||||
this.computeHash = iteratee(computeHash)
|
||||
}
|
||||
|
||||
this._itemsByHash = Object.create(null)
|
||||
this._keysToHash = Object.create(null)
|
||||
|
||||
// Bound versions of listeners.
|
||||
this._onAdd = bind(this._onAdd, this)
|
||||
this._onUpdate = bind(this._onUpdate, this)
|
||||
this._onRemove = bind(this._onRemove, this)
|
||||
}
|
||||
|
||||
// This method is used to compute the hash under which an item must
|
||||
// be saved.
|
||||
computeHash (value, key) {
|
||||
throw new NotImplemented('this method must be overridden')
|
||||
}
|
||||
|
||||
// Remove empty items lists.
|
||||
sweep () {
|
||||
const {_itemsByHash: itemsByHash} = this
|
||||
for (const hash in itemsByHash) {
|
||||
if (isEmpty(itemsByHash[hash])) {
|
||||
delete itemsByHash[hash]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
get items () {
|
||||
return this._itemsByHash
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
_attachCollection (collection) {
|
||||
// Add existing entries.
|
||||
//
|
||||
// FIXME: I think there may be a race condition if the `add` event
|
||||
// has not been emitted yet.
|
||||
this._onAdd(collection.all)
|
||||
|
||||
collection.on(ACTION_ADD, this._onAdd)
|
||||
collection.on(ACTION_UPDATE, this._onUpdate)
|
||||
collection.on(ACTION_REMOVE, this._onRemove)
|
||||
}
|
||||
|
||||
_detachCollection (collection) {
|
||||
collection.removeListener(ACTION_ADD, this._onAdd)
|
||||
collection.removeListener(ACTION_UPDATE, this._onUpdate)
|
||||
collection.removeListener(ACTION_REMOVE, this._onRemove)
|
||||
|
||||
clearObject(this._itemsByHash)
|
||||
clearObject(this._keysToHash)
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
_onAdd (items) {
|
||||
const {
|
||||
computeHash,
|
||||
_itemsByHash: itemsByHash,
|
||||
_keysToHash: keysToHash
|
||||
} = this
|
||||
|
||||
for (const key in items) {
|
||||
const value = items[key]
|
||||
|
||||
const hash = computeHash(value, key)
|
||||
|
||||
if (hash != null) {
|
||||
(
|
||||
itemsByHash[hash] ||
|
||||
|
||||
// FIXME: We do not use objects without prototype for now
|
||||
// because it breaks Angular in xo-web, change it back when
|
||||
// this is fixed.
|
||||
(itemsByHash[hash] = {})
|
||||
)[key] = value
|
||||
|
||||
keysToHash[key] = hash
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_onUpdate (items) {
|
||||
const {
|
||||
computeHash,
|
||||
_itemsByHash: itemsByHash,
|
||||
_keysToHash: keysToHash
|
||||
} = this
|
||||
|
||||
for (const key in items) {
|
||||
const value = items[key]
|
||||
|
||||
const prev = keysToHash[key]
|
||||
const hash = computeHash(value, key)
|
||||
|
||||
// Removes item from the previous hash's list if any.
|
||||
if (prev != null) delete itemsByHash[prev][key]
|
||||
|
||||
// Inserts item into the new hash's list if any.
|
||||
if (hash != null) {
|
||||
(
|
||||
itemsByHash[hash] ||
|
||||
|
||||
// FIXME: idem: change back to Object.create(null)
|
||||
(itemsByHash[hash] = {})
|
||||
)[key] = value
|
||||
|
||||
keysToHash[key] = hash
|
||||
} else {
|
||||
delete keysToHash[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_onRemove (items) {
|
||||
const {
|
||||
_itemsByHash: itemsByHash,
|
||||
_keysToHash: keysToHash
|
||||
} = this
|
||||
|
||||
for (const key in items) {
|
||||
const prev = keysToHash[key]
|
||||
if (prev != null) {
|
||||
delete itemsByHash[prev][key]
|
||||
delete keysToHash[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
180
packages/xo-collection/src/index.spec.js
Normal file
180
packages/xo-collection/src/index.spec.js
Normal file
@@ -0,0 +1,180 @@
|
||||
/* eslint-env jest */
|
||||
|
||||
import eventToPromise from 'event-to-promise'
|
||||
import { forEach } from 'lodash'
|
||||
|
||||
import Collection from '..'
|
||||
import Index from '../index'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const waitTicks = (n = 2) => {
|
||||
const {nextTick} = process
|
||||
|
||||
return new Promise(resolve => {
|
||||
(function waitNextTick () {
|
||||
// The first tick is handled by Promise#then()
|
||||
if (--n) {
|
||||
nextTick(waitNextTick)
|
||||
} else {
|
||||
resolve()
|
||||
}
|
||||
})()
|
||||
})
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
describe('Index', function () {
|
||||
let col, byGroup
|
||||
const item1 = {
|
||||
id: '2ccb8a72-dc65-48e4-88fe-45ef541f2cba',
|
||||
group: 'foo'
|
||||
}
|
||||
const item2 = {
|
||||
id: '7d21dc51-4da8-4538-a2e9-dd6f4784eb76',
|
||||
group: 'bar'
|
||||
}
|
||||
const item3 = {
|
||||
id: '668c1274-4442-44a6-b99a-512188e0bb09',
|
||||
group: 'foo'
|
||||
}
|
||||
const item4 = {
|
||||
id: 'd90b7335-e540-4a44-ad22-c4baae9cd0a9'
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
col = new Collection()
|
||||
forEach([item1, item2, item3, item4], item => {
|
||||
col.add(item)
|
||||
})
|
||||
|
||||
byGroup = new Index('group')
|
||||
|
||||
col.createIndex('byGroup', byGroup)
|
||||
|
||||
return waitTicks()
|
||||
})
|
||||
|
||||
it('works with existing items', function () {
|
||||
expect(col.indexes).toEqual({
|
||||
byGroup: {
|
||||
foo: {
|
||||
[item1.id]: item1,
|
||||
[item3.id]: item3
|
||||
},
|
||||
bar: {
|
||||
[item2.id]: item2
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('works with added items', function () {
|
||||
const item5 = {
|
||||
id: '823b56c4-4b96-4f3a-9533-5d08177167ac',
|
||||
group: 'baz'
|
||||
}
|
||||
|
||||
col.add(item5)
|
||||
|
||||
return waitTicks().then(() => {
|
||||
expect(col.indexes).toEqual({
|
||||
byGroup: {
|
||||
foo: {
|
||||
[item1.id]: item1,
|
||||
[item3.id]: item3
|
||||
},
|
||||
bar: {
|
||||
[item2.id]: item2
|
||||
},
|
||||
baz: {
|
||||
[item5.id]: item5
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('works with updated items', function () {
|
||||
const item1bis = {
|
||||
id: item1.id,
|
||||
group: 'bar'
|
||||
}
|
||||
|
||||
col.update(item1bis)
|
||||
|
||||
return waitTicks().then(() => {
|
||||
expect(col.indexes).toEqual({
|
||||
byGroup: {
|
||||
foo: {
|
||||
[item3.id]: item3
|
||||
},
|
||||
bar: {
|
||||
[item1.id]: item1bis,
|
||||
[item2.id]: item2
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('works with removed items', function () {
|
||||
col.remove(item2)
|
||||
|
||||
return waitTicks().then(() => {
|
||||
expect(col.indexes).toEqual({
|
||||
byGroup: {
|
||||
foo: {
|
||||
[item1.id]: item1,
|
||||
[item3.id]: item3
|
||||
},
|
||||
bar: {}
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('correctly updates the value even the same object has the same hash', function () {
|
||||
const item1bis = {
|
||||
id: item1.id,
|
||||
group: item1.group,
|
||||
newProp: true
|
||||
}
|
||||
|
||||
col.update(item1bis)
|
||||
|
||||
return eventToPromise(col, 'finish').then(() => {
|
||||
expect(col.indexes).toEqual({
|
||||
byGroup: {
|
||||
foo: {
|
||||
[item1.id]: item1bis,
|
||||
[item3.id]: item3
|
||||
},
|
||||
bar: {
|
||||
[item2.id]: item2
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('#sweep()', function () {
|
||||
it('removes empty items lists', function () {
|
||||
col.remove(item2)
|
||||
|
||||
return waitTicks().then(() => {
|
||||
byGroup.sweep()
|
||||
|
||||
expect(col.indexes).toEqual({
|
||||
byGroup: {
|
||||
foo: {
|
||||
[item1.id]: item1,
|
||||
[item3.id]: item3
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
7
packages/xo-collection/src/is-empty.js
Normal file
7
packages/xo-collection/src/is-empty.js
Normal file
@@ -0,0 +1,7 @@
|
||||
export default function isEmpty (object) {
|
||||
/* eslint no-unused-vars: 0 */
|
||||
for (const key in object) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
3
packages/xo-collection/src/is-object.js
Normal file
3
packages/xo-collection/src/is-object.js
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function isObject (value) {
|
||||
return (value !== null) && (typeof value === 'object')
|
||||
}
|
||||
7
packages/xo-collection/src/not-implemented.js
Normal file
7
packages/xo-collection/src/not-implemented.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import {BaseError} from 'make-error'
|
||||
|
||||
export default class NotImplemented extends BaseError {
|
||||
constructor (message) {
|
||||
super(message || 'this method is not implemented')
|
||||
}
|
||||
}
|
||||
124
packages/xo-collection/src/unique-index.js
Normal file
124
packages/xo-collection/src/unique-index.js
Normal file
@@ -0,0 +1,124 @@
|
||||
import { bind, iteratee } from 'lodash'
|
||||
|
||||
import clearObject from './clear-object'
|
||||
import NotImplemented from './not-implemented'
|
||||
import {
|
||||
ACTION_ADD,
|
||||
ACTION_UPDATE,
|
||||
ACTION_REMOVE
|
||||
} from './collection'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export default class UniqueIndex {
|
||||
constructor (computeHash) {
|
||||
if (computeHash) {
|
||||
this.computeHash = iteratee(computeHash)
|
||||
}
|
||||
|
||||
this._itemByHash = Object.create(null)
|
||||
this._keysToHash = Object.create(null)
|
||||
|
||||
// Bound versions of listeners.
|
||||
this._onAdd = bind(this._onAdd, this)
|
||||
this._onUpdate = bind(this._onUpdate, this)
|
||||
this._onRemove = bind(this._onRemove, this)
|
||||
}
|
||||
|
||||
// This method is used to compute the hash under which an item must
|
||||
// be saved.
|
||||
computeHash (value, key) {
|
||||
throw new NotImplemented('this method must be overridden')
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
get items () {
|
||||
return this._itemByHash
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
_attachCollection (collection) {
|
||||
// Add existing entries.
|
||||
//
|
||||
// FIXME: I think there may be a race condition if the `add` event
|
||||
// has not been emitted yet.
|
||||
this._onAdd(collection.all)
|
||||
|
||||
collection.on(ACTION_ADD, this._onAdd)
|
||||
collection.on(ACTION_UPDATE, this._onUpdate)
|
||||
collection.on(ACTION_REMOVE, this._onRemove)
|
||||
}
|
||||
|
||||
_detachCollection (collection) {
|
||||
collection.removeListener(ACTION_ADD, this._onAdd)
|
||||
collection.removeListener(ACTION_UPDATE, this._onUpdate)
|
||||
collection.removeListener(ACTION_REMOVE, this._onRemove)
|
||||
|
||||
clearObject(this._itemByHash)
|
||||
clearObject(this._keysToHash)
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
_onAdd (items) {
|
||||
const {
|
||||
computeHash,
|
||||
_itemByHash: itemByHash,
|
||||
_keysToHash: keysToHash
|
||||
} = this
|
||||
|
||||
for (const key in items) {
|
||||
const value = items[key]
|
||||
|
||||
const hash = computeHash(value, key)
|
||||
|
||||
if (hash != null) {
|
||||
itemByHash[hash] = value
|
||||
keysToHash[key] = hash
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_onUpdate (items) {
|
||||
const {
|
||||
computeHash,
|
||||
_itemByHash: itemByHash,
|
||||
_keysToHash: keysToHash
|
||||
} = this
|
||||
|
||||
for (const key in items) {
|
||||
const value = items[key]
|
||||
|
||||
const prev = keysToHash[key]
|
||||
const hash = computeHash(value, key)
|
||||
|
||||
// Removes item from the previous hash's list if any.
|
||||
if (prev != null) delete itemByHash[prev]
|
||||
|
||||
// Inserts item into the new hash's list if any.
|
||||
if (hash != null) {
|
||||
itemByHash[hash] = value
|
||||
keysToHash[key] = hash
|
||||
} else {
|
||||
delete keysToHash[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_onRemove (items) {
|
||||
const {
|
||||
_itemByHash: itemByHash,
|
||||
_keysToHash: keysToHash
|
||||
} = this
|
||||
|
||||
for (const key in items) {
|
||||
const prev = keysToHash[key]
|
||||
if (prev != null) {
|
||||
delete itemByHash[prev]
|
||||
delete keysToHash[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
131
packages/xo-collection/src/unique-index.spec.js
Normal file
131
packages/xo-collection/src/unique-index.spec.js
Normal file
@@ -0,0 +1,131 @@
|
||||
/* eslint-env jest */
|
||||
|
||||
import eventToPromise from 'event-to-promise'
|
||||
import { forEach } from 'lodash'
|
||||
|
||||
import Collection from '..'
|
||||
import Index from '../unique-index'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const waitTicks = (n = 2) => {
|
||||
const {nextTick} = process
|
||||
|
||||
return new Promise(resolve => {
|
||||
(function waitNextTick () {
|
||||
// The first tick is handled by Promise#then()
|
||||
if (--n) {
|
||||
nextTick(waitNextTick)
|
||||
} else {
|
||||
resolve()
|
||||
}
|
||||
})()
|
||||
})
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
describe('UniqueIndex', function () {
|
||||
let col, byKey
|
||||
const item1 = {
|
||||
id: '2ccb8a72-dc65-48e4-88fe-45ef541f2cba',
|
||||
key: '036dee1b-9a3b-4fb5-be8a-4f535b355581'
|
||||
}
|
||||
const item2 = {
|
||||
id: '7d21dc51-4da8-4538-a2e9-dd6f4784eb76',
|
||||
key: '103cd893-d2cc-4d37-96fd-c259ad04c0d4'
|
||||
}
|
||||
const item3 = {
|
||||
id: '668c1274-4442-44a6-b99a-512188e0bb09'
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
col = new Collection()
|
||||
forEach([item1, item2, item3], item => {
|
||||
col.add(item)
|
||||
})
|
||||
|
||||
byKey = new Index('key')
|
||||
|
||||
col.createIndex('byKey', byKey)
|
||||
|
||||
return waitTicks()
|
||||
})
|
||||
|
||||
it('works with existing items', function () {
|
||||
expect(col.indexes).toEqual({
|
||||
byKey: {
|
||||
[item1.key]: item1,
|
||||
[item2.key]: item2
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('works with added items', function () {
|
||||
const item4 = {
|
||||
id: '823b56c4-4b96-4f3a-9533-5d08177167ac',
|
||||
key: '1437af14-429a-40db-8a51-8a2f5ed03201'
|
||||
}
|
||||
|
||||
col.add(item4)
|
||||
|
||||
return waitTicks().then(() => {
|
||||
expect(col.indexes).toEqual({
|
||||
byKey: {
|
||||
[item1.key]: item1,
|
||||
[item2.key]: item2,
|
||||
[item4.key]: item4
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('works with updated items', function () {
|
||||
const item1bis = {
|
||||
id: item1.id,
|
||||
key: 'e03d4a3a-0331-4aca-97a2-016bbd43a29b'
|
||||
}
|
||||
|
||||
col.update(item1bis)
|
||||
|
||||
return waitTicks().then(() => {
|
||||
expect(col.indexes).toEqual({
|
||||
byKey: {
|
||||
[item1bis.key]: item1bis,
|
||||
[item2.key]: item2
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('works with removed items', function () {
|
||||
col.remove(item2)
|
||||
|
||||
return waitTicks().then(() => {
|
||||
expect(col.indexes).toEqual({
|
||||
byKey: {
|
||||
[item1.key]: item1
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('correctly updates the value even the same object has the same hash', function () {
|
||||
const item1bis = {
|
||||
id: item1.id,
|
||||
key: item1.key,
|
||||
newProp: true
|
||||
}
|
||||
|
||||
col.update(item1bis)
|
||||
|
||||
return eventToPromise(col, 'finish').then(() => {
|
||||
expect(col.indexes).toEqual({
|
||||
byKey: {
|
||||
[item1.key]: item1bis,
|
||||
[item2.key]: item2
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
56
packages/xo-collection/src/view.example.js
Normal file
56
packages/xo-collection/src/view.example.js
Normal file
@@ -0,0 +1,56 @@
|
||||
import { forEach } from 'lodash'
|
||||
|
||||
import Collection from '..'
|
||||
import View from '../view'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
// Create the collection.
|
||||
const users = new Collection()
|
||||
users.getKey = (user) => user.name
|
||||
|
||||
// Inserts some data.
|
||||
users.add({
|
||||
name: 'bob'
|
||||
})
|
||||
users.add({
|
||||
name: 'clara',
|
||||
active: true
|
||||
})
|
||||
users.add({
|
||||
name: 'ophelia'
|
||||
})
|
||||
users.add({
|
||||
name: 'Steve',
|
||||
active: true
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
// Create the view.
|
||||
const activeUsers = new View(users, 'active')
|
||||
|
||||
// Register some event listeners to see the changes.
|
||||
activeUsers.on('add', users => {
|
||||
forEach(users, (_, id) => {
|
||||
console.log('+ active user:', id)
|
||||
})
|
||||
})
|
||||
activeUsers.on('remove', users => {
|
||||
forEach(users, (_, id) => {
|
||||
console.log('- active user:', id)
|
||||
})
|
||||
})
|
||||
|
||||
// Make some changes in the future.
|
||||
setTimeout(function () {
|
||||
console.log('-----')
|
||||
|
||||
users.set({
|
||||
name: 'ophelia',
|
||||
active: true
|
||||
})
|
||||
users.set({
|
||||
name: 'Steve'
|
||||
})
|
||||
}, 10)
|
||||
88
packages/xo-collection/src/view.js
Normal file
88
packages/xo-collection/src/view.js
Normal file
@@ -0,0 +1,88 @@
|
||||
import { bind, forEach, iteratee as createCallback } from 'lodash'
|
||||
|
||||
import Collection, {
|
||||
ACTION_ADD,
|
||||
ACTION_UPDATE,
|
||||
ACTION_REMOVE
|
||||
} from './collection'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export default class View extends Collection {
|
||||
constructor (collection, predicate) {
|
||||
super()
|
||||
|
||||
this._collection = collection
|
||||
this._predicate = createCallback(predicate)
|
||||
|
||||
// Handles initial items.
|
||||
this._onAdd(this._collection.all)
|
||||
|
||||
// Bound versions of listeners.
|
||||
this._onAdd = bind(this._onAdd, this)
|
||||
this._onUpdate = bind(this._onUpdate, this)
|
||||
this._onRemove = bind(this._onRemove, this)
|
||||
|
||||
// Register listeners.
|
||||
this._collection.on(ACTION_ADD, this._onAdd)
|
||||
this._collection.on(ACTION_UPDATE, this._onUpdate)
|
||||
this._collection.on(ACTION_REMOVE, this._onRemove)
|
||||
}
|
||||
|
||||
// This method is necessary to free the memory of the view if its
|
||||
// life span is shorter than the collection.
|
||||
destroy () {
|
||||
this._collection.removeListener(ACTION_ADD, this._onAdd)
|
||||
this._collection.removeListener(ACTION_UPDATE, this._onUpdate)
|
||||
this._collection.removeListener(ACTION_REMOVE, this._onRemove)
|
||||
}
|
||||
|
||||
add () {
|
||||
throw new Error('a view is read only')
|
||||
}
|
||||
|
||||
clear () {
|
||||
throw new Error('a view is read only')
|
||||
}
|
||||
|
||||
set () {
|
||||
throw new Error('a view is read only')
|
||||
}
|
||||
|
||||
update () {
|
||||
throw new Error('a view is read only')
|
||||
}
|
||||
|
||||
_onAdd (items) {
|
||||
const {_predicate: predicate} = this
|
||||
|
||||
forEach(items, (value, key) => {
|
||||
if (predicate(value, key, this)) {
|
||||
// super.add() cannot be used because the item may already be
|
||||
// in the view if it was already present at the creation of
|
||||
// the view and its event not already emitted.
|
||||
super.set(key, value)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
_onUpdate (items) {
|
||||
const {_predicate: predicate} = this
|
||||
|
||||
forEach(items, (value, key) => {
|
||||
if (predicate(value, key, this)) {
|
||||
super.set(key, value)
|
||||
} else if (super.has(key)) {
|
||||
super.remove(key)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
_onRemove (items) {
|
||||
forEach(items, (value, key) => {
|
||||
if (super.has(key)) {
|
||||
super.remove(key)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
1
packages/xo-collection/unique-index.js
Normal file
1
packages/xo-collection/unique-index.js
Normal file
@@ -0,0 +1 @@
|
||||
module.exports = require('./dist/unique-index')
|
||||
1
packages/xo-collection/view.js
Normal file
1
packages/xo-collection/view.js
Normal file
@@ -0,0 +1 @@
|
||||
module.exports = require('./dist/view')
|
||||
3440
packages/xo-collection/yarn.lock
Normal file
3440
packages/xo-collection/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
10
packages/xo-common/.npmignore
Normal file
10
packages/xo-common/.npmignore
Normal file
@@ -0,0 +1,10 @@
|
||||
/examples/
|
||||
example.js
|
||||
example.js.map
|
||||
*.example.js
|
||||
*.example.js.map
|
||||
|
||||
/test/
|
||||
/tests/
|
||||
*.spec.js
|
||||
*.spec.js.map
|
||||
49
packages/xo-common/README.md
Normal file
49
packages/xo-common/README.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# xo-common [](https://travis-ci.org/vatesfr/xen-orchestra)
|
||||
|
||||
> Code shared between [XO](https://xen-orchestra.com) server and clients
|
||||
|
||||
## Install
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/xo-common):
|
||||
|
||||
```
|
||||
> npm install --save xo-common
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
**TODO**
|
||||
|
||||
## Development
|
||||
|
||||
```
|
||||
# Install dependencies
|
||||
> npm install
|
||||
|
||||
# Run the tests
|
||||
> npm test
|
||||
|
||||
# Continuously compile
|
||||
> npm run dev
|
||||
|
||||
# Continuously run the tests
|
||||
> npm run dev-test
|
||||
|
||||
# Build for production (automatically called by npm install)
|
||||
> npm run build
|
||||
```
|
||||
|
||||
## Contributions
|
||||
|
||||
Contributions are *very* welcomed, either on the documentation or on
|
||||
the code.
|
||||
|
||||
You may:
|
||||
|
||||
- report any [issue](https://github.com/vatesfr/xo-web/issues)
|
||||
you've encountered;
|
||||
- fork and create a pull request.
|
||||
|
||||
## License
|
||||
|
||||
AGPL3 © [Vates SAS](https://vates.fr)
|
||||
1
packages/xo-common/api-errors.js
Normal file
1
packages/xo-common/api-errors.js
Normal file
@@ -0,0 +1 @@
|
||||
module.exports = require('./dist/api-errors')
|
||||
77
packages/xo-common/package.json
Normal file
77
packages/xo-common/package.json
Normal file
@@ -0,0 +1,77 @@
|
||||
{
|
||||
"name": "xo-common",
|
||||
"version": "0.1.0",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "Code shared between [XO](https://xen-orchestra.com) server and clients",
|
||||
"keywords": [],
|
||||
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/packages/xo-common",
|
||||
"bugs": "https://github.com/vatesfr/xo-web/issues",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/vatesfr/xen-orchestra.git"
|
||||
},
|
||||
"author": {
|
||||
"name": "Julien Fontanet",
|
||||
"email": "julien.fontanet@isonoe.net"
|
||||
},
|
||||
"preferGlobal": false,
|
||||
"bin": {},
|
||||
"files": [
|
||||
"dist/",
|
||||
"*.js"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
},
|
||||
"dependencies": {
|
||||
"babel-runtime": "^6.18.0",
|
||||
"lodash": "^4.16.6",
|
||||
"make-error": "^1.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-cli": "^6.18.0",
|
||||
"babel-eslint": "^7.1.0",
|
||||
"babel-plugin-lodash": "^3.2.9",
|
||||
"babel-plugin-transform-runtime": "^6.15.0",
|
||||
"babel-preset-env": "^1.0.0",
|
||||
"babel-preset-stage-0": "^6.16.0",
|
||||
"cross-env": "^3.1.3",
|
||||
"dependency-check": "^2.6.0",
|
||||
"rimraf": "^2.5.4",
|
||||
"standard": "^8.5.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
|
||||
"clean": "rimraf dist/",
|
||||
"depcheck": "dependency-check ./package.json --entry api-errors.js",
|
||||
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
|
||||
"lint": "standard",
|
||||
"posttest": "yarn run lint && yarn run depcheck",
|
||||
"prebuild": "yarn run clean",
|
||||
"predev": "yarn run clean",
|
||||
"prepublish": "yarn run build"
|
||||
},
|
||||
"babel": {
|
||||
"plugins": [
|
||||
"lodash"
|
||||
],
|
||||
"presets": [
|
||||
[
|
||||
"env",
|
||||
{
|
||||
"targets": {
|
||||
"browsers": "> 1%",
|
||||
"node": 4
|
||||
}
|
||||
}
|
||||
],
|
||||
"stage-0"
|
||||
]
|
||||
},
|
||||
"standard": {
|
||||
"ignore": [
|
||||
"dist"
|
||||
],
|
||||
"parser": "babel-eslint"
|
||||
}
|
||||
}
|
||||
166
packages/xo-common/src/api-errors.js
Normal file
166
packages/xo-common/src/api-errors.js
Normal file
@@ -0,0 +1,166 @@
|
||||
import { BaseError } from 'make-error'
|
||||
import { isArray, iteratee } from 'lodash'
|
||||
|
||||
class XoError extends BaseError {
|
||||
constructor ({ code, message, data }) {
|
||||
super(message)
|
||||
this.code = code
|
||||
this.data = data
|
||||
}
|
||||
|
||||
toJsonRpcError () {
|
||||
return {
|
||||
message: this.message,
|
||||
code: this.code,
|
||||
data: this.data
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const create = (code, getProps) => {
|
||||
const factory = args => new XoError({ ...getProps(args), code })
|
||||
factory.is = (error, predicate) =>
|
||||
error.code === code && iteratee(predicate)(error)
|
||||
|
||||
return factory
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
|
||||
export const notImplemented = create(0, () => ({
|
||||
message: 'not implemented'
|
||||
}))
|
||||
|
||||
export const noSuchObject = create(1, (id, type) => ({
|
||||
data: { id, type },
|
||||
message: 'no such object'
|
||||
}))
|
||||
|
||||
export const unauthorized = create(2, () => ({
|
||||
message: 'not authenticated or not enough permissions'
|
||||
}))
|
||||
|
||||
export const invalidCredentials = create(3, () => ({
|
||||
message: 'invalid credentials'
|
||||
}))
|
||||
|
||||
// Deprecated alreadyAuthenticated (4)
|
||||
|
||||
export const forbiddenOperation = create(5, (operation, reason) => ({
|
||||
data: { operation, reason },
|
||||
message: `forbidden operation: ${operation}`
|
||||
}))
|
||||
|
||||
// Deprecated GenericError (6)
|
||||
|
||||
export const noHostsAvailable = create(7, () => ({
|
||||
message: 'no hosts available'
|
||||
}))
|
||||
|
||||
export const authenticationFailed = create(8, () => ({
|
||||
message: 'authentication failed'
|
||||
}))
|
||||
|
||||
export const serverUnreachable = create(9, objectId => ({
|
||||
data: {
|
||||
objectId
|
||||
},
|
||||
message: 'server unreachable'
|
||||
}))
|
||||
|
||||
export const invalidParameters = create(10, (message, errors) => {
|
||||
if (isArray(message)) {
|
||||
errors = message
|
||||
message = undefined
|
||||
}
|
||||
|
||||
return {
|
||||
data: { errors },
|
||||
message: message || 'invalid parameters'
|
||||
}
|
||||
})
|
||||
|
||||
export const vmMissingPvDrivers = create(11, ({ vm }) => ({
|
||||
data: {
|
||||
objectId: vm
|
||||
},
|
||||
message: 'missing PV drivers'
|
||||
}))
|
||||
|
||||
export const vmIsTemplate = create(12, ({ vm }) => ({
|
||||
data: {
|
||||
objectId: vm
|
||||
},
|
||||
message: 'VM is a template'
|
||||
}))
|
||||
|
||||
// TODO: We should probably create a more generic error which gathers all incorrect state errors.
|
||||
// e.g.:
|
||||
// incorrectState {
|
||||
// data: {
|
||||
// objectId: 'af43e227-3deb-4822-a79b-968825de72eb',
|
||||
// property: 'power_state',
|
||||
// actual: 'Running',
|
||||
// expected: 'Halted'
|
||||
// },
|
||||
// message: 'incorrect state'
|
||||
// }
|
||||
export const vmBadPowerState = create(13, ({ vm, expected, actual }) => ({
|
||||
data: {
|
||||
objectId: vm,
|
||||
expected,
|
||||
actual
|
||||
},
|
||||
message: `VM state is ${actual} but should be ${expected}`
|
||||
}))
|
||||
|
||||
export const vmLacksFeature = create(14, ({ vm, feature }) => ({
|
||||
data: {
|
||||
objectId: vm,
|
||||
feature
|
||||
},
|
||||
message: `VM lacks feature ${feature || ''}`
|
||||
}))
|
||||
|
||||
export const notSupportedDuringUpgrade = create(15, () => ({
|
||||
message: 'not supported during upgrade'
|
||||
}))
|
||||
|
||||
export const objectAlreadyExists = create(16, ({ objectId, objectType }) => ({
|
||||
data: {
|
||||
objectId,
|
||||
objectType
|
||||
},
|
||||
message: `${objectType || 'object'} already exists`
|
||||
}))
|
||||
|
||||
export const vdiInUse = create(17, ({ vdi, operation }) => ({
|
||||
data: {
|
||||
objectId: vdi,
|
||||
operation
|
||||
},
|
||||
message: 'VDI in use'
|
||||
}))
|
||||
|
||||
export const hostOffline = create(18, ({ host }) => ({
|
||||
data: {
|
||||
objectId: host
|
||||
},
|
||||
message: 'host offline'
|
||||
}))
|
||||
|
||||
export const operationBlocked = create(19, ({ objectId, code }) => ({
|
||||
data: {
|
||||
objectId,
|
||||
code
|
||||
},
|
||||
message: 'operation blocked'
|
||||
}))
|
||||
|
||||
export const patchPrecheckFailed = create(20, ({ errorType, patch }) => ({
|
||||
data: {
|
||||
objectId: patch,
|
||||
errorType
|
||||
},
|
||||
message: `patch precheck failed: ${errorType}`
|
||||
}))
|
||||
2580
packages/xo-common/yarn.lock
Normal file
2580
packages/xo-common/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
10
packages/xo-lib/.npmignore
Normal file
10
packages/xo-lib/.npmignore
Normal file
@@ -0,0 +1,10 @@
|
||||
/examples/
|
||||
example.js
|
||||
example.js.map
|
||||
*.example.js
|
||||
*.example.js.map
|
||||
|
||||
/test/
|
||||
/tests/
|
||||
*.spec.js
|
||||
*.spec.js.map
|
||||
168
packages/xo-lib/README.md
Normal file
168
packages/xo-lib/README.md
Normal file
@@ -0,0 +1,168 @@
|
||||
# xo-lib [](https://travis-ci.org/vatesfr/xen-orchestra)
|
||||
|
||||
> Library to connect to XO-Server.
|
||||
|
||||
## Install
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/xo-lib):
|
||||
|
||||
```
|
||||
npm install --save xo-lib
|
||||
```
|
||||
|
||||
Then require the package:
|
||||
|
||||
```javascript
|
||||
import Xo from 'xo-lib'
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
> If the URL is not provided and the current environment is a web
|
||||
> browser, the location of the current page will be used.
|
||||
|
||||
```javascript
|
||||
// Connect to XO.
|
||||
const xo = new Xo({ url: 'https://xo.company.tld' })
|
||||
|
||||
// Let's start by opening the connection.
|
||||
await xo.open()
|
||||
|
||||
// Must sign in before being able to call any methods (all calls will
|
||||
// be buffered until signed in).
|
||||
await xo.signIn({
|
||||
email: 'admin@admin.net',
|
||||
password: 'admin'
|
||||
})
|
||||
|
||||
console('signed as', xo.user)
|
||||
```
|
||||
|
||||
The credentials can also be passed directly to the constructor:
|
||||
|
||||
```javascript
|
||||
const xo = Xo({
|
||||
url: 'https://xo.company.tld',
|
||||
credentials: {
|
||||
email: 'admin@admin.net',
|
||||
password: 'admin',
|
||||
}
|
||||
})
|
||||
|
||||
xo.open()
|
||||
|
||||
xo.on('authenticated', () => {
|
||||
console.log(xo.user)
|
||||
})
|
||||
```
|
||||
|
||||
> If the URL is not provided and the current environment is a web
|
||||
> browser, the location of the current page will be used.
|
||||
|
||||
### Connection
|
||||
|
||||
```javascript
|
||||
await xo.open()
|
||||
|
||||
console.log('connected')
|
||||
```
|
||||
|
||||
### Disconnection
|
||||
|
||||
```javascript
|
||||
xo.close()
|
||||
|
||||
console.log('disconnected')
|
||||
```
|
||||
|
||||
### Method call
|
||||
|
||||
```javascript
|
||||
const token = await xo.call('token.create')
|
||||
|
||||
console.log('Token created', token)
|
||||
```
|
||||
|
||||
### Status
|
||||
|
||||
The connection status is available through the status property which
|
||||
is *open*, *connecting* or *closed*.
|
||||
|
||||
```javascript
|
||||
console.log('%s to xo-server', xo.status)
|
||||
```
|
||||
|
||||
### Current user
|
||||
|
||||
Information about the user account used to sign in is available
|
||||
through the `user` property.
|
||||
|
||||
```javascript
|
||||
console.log('Current user is', xo.user)
|
||||
```
|
||||
|
||||
> This property is null when the status is not connected.
|
||||
|
||||
### Events
|
||||
|
||||
```javascript
|
||||
xo.on('open', () => {
|
||||
console.log('connected')
|
||||
})
|
||||
```
|
||||
|
||||
```javascript
|
||||
xo.on('closed', () => {
|
||||
console.log('disconnected')
|
||||
})
|
||||
```
|
||||
|
||||
```javascript
|
||||
xo.on('notification', function (notif) {
|
||||
console.log('notification:', notif.method, notif.params)
|
||||
})
|
||||
```
|
||||
|
||||
```javascript
|
||||
xo.on('authenticated', () => {
|
||||
console.log('authenticated as', xo.user)
|
||||
})
|
||||
|
||||
xo.on('authenticationFailure', () => {
|
||||
console.log('failed to authenticate')
|
||||
})
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
```
|
||||
# Install dependencies
|
||||
> npm install
|
||||
|
||||
# Run the tests
|
||||
> npm test
|
||||
|
||||
# Continuously compile
|
||||
> npm run dev
|
||||
|
||||
# Continuously run the tests
|
||||
> npm run dev-test
|
||||
|
||||
# Build for production (automatically called by npm install)
|
||||
> npm run build
|
||||
```
|
||||
|
||||
## Contributions
|
||||
|
||||
Contributions are *very* welcomed, either on the documentation or on
|
||||
the code.
|
||||
|
||||
You may:
|
||||
|
||||
- report any [issue](https://github.com/vatesfr/xo-web/issues)
|
||||
you've encountered;
|
||||
- fork and create a pull request.
|
||||
|
||||
## License
|
||||
|
||||
ISC © [Vates SAS](http://vates.fr)
|
||||
45
packages/xo-lib/example.js
Normal file
45
packages/xo-lib/example.js
Normal file
@@ -0,0 +1,45 @@
|
||||
'use strict'
|
||||
|
||||
process.on('unhandledRejection', function (error) {
|
||||
console.log(error)
|
||||
})
|
||||
|
||||
var Xo = require('./').default
|
||||
|
||||
var xo = new Xo({
|
||||
url: 'localhost:9000'
|
||||
})
|
||||
|
||||
xo.open().then(function () {
|
||||
return xo.call('acl.get', {}).then(function (result) {
|
||||
console.log('success:', result)
|
||||
}).catch(function (error) {
|
||||
console.log('failure:', error)
|
||||
})
|
||||
}).then(function () {
|
||||
return xo.signIn({
|
||||
email: 'admin@admin.net',
|
||||
password: 'admin'
|
||||
}).then(function () {
|
||||
console.log('connected as ', xo.user)
|
||||
}).catch(function (error) {
|
||||
console.log('failure:', error)
|
||||
})
|
||||
}).then(function () {
|
||||
return xo.signIn({
|
||||
email: 'tom',
|
||||
password: 'tom'
|
||||
}).then(function () {
|
||||
console.log('connected as', xo.user)
|
||||
|
||||
return xo.call('acl.get', {}).then(function (result) {
|
||||
console.log('success:', result)
|
||||
}).catch(function (error) {
|
||||
console.log('failure:', error)
|
||||
})
|
||||
}).catch(function (error) {
|
||||
console.log('failure', error)
|
||||
})
|
||||
}).then(function () {
|
||||
return xo.close()
|
||||
})
|
||||
80
packages/xo-lib/package.json
Normal file
80
packages/xo-lib/package.json
Normal file
@@ -0,0 +1,80 @@
|
||||
{
|
||||
"name": "xo-lib",
|
||||
"version": "0.8.2",
|
||||
"license": "ISC",
|
||||
"description": "Library to connect to XO-Server",
|
||||
"keywords": [
|
||||
"xen",
|
||||
"orchestra",
|
||||
"xen-orchestra"
|
||||
],
|
||||
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/packages/xo-lib",
|
||||
"bugs": "https://github.com/vatesfr/xo-web/issues",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/vatesfr/xen-orchestra.git"
|
||||
},
|
||||
"author": {
|
||||
"name": "Julien Fontanet",
|
||||
"email": "julien.fontanet@vates.fr"
|
||||
},
|
||||
"preferGlobal": false,
|
||||
"main": "dist/",
|
||||
"bin": {},
|
||||
"files": [
|
||||
"dist/"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
},
|
||||
"dependencies": {
|
||||
"jsonrpc-websocket-client": "^0.1.2",
|
||||
"lodash": "^4.17.2",
|
||||
"make-error": "^1.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-cli": "^6.18.0",
|
||||
"babel-eslint": "^7.1.1",
|
||||
"babel-plugin-lodash": "^3.2.9",
|
||||
"babel-preset-env": "^1.0.1",
|
||||
"babel-preset-stage-0": "^6.16.0",
|
||||
"cross-env": "^3.1.3",
|
||||
"dependency-check": "^2.6.0",
|
||||
"rimraf": "^2.5.4",
|
||||
"standard": "^8.5.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
|
||||
"clean": "rimraf dist/",
|
||||
"depcheck": "dependency-check ./package.json",
|
||||
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
|
||||
"lint": "standard",
|
||||
"posttest": "yarn run lint && yarn run depcheck",
|
||||
"prebuild": "yarn run clean",
|
||||
"predev": "yarn run clean",
|
||||
"prepublish": "yarn run build"
|
||||
},
|
||||
"babel": {
|
||||
"plugins": [
|
||||
"lodash"
|
||||
],
|
||||
"presets": [
|
||||
[
|
||||
"env",
|
||||
{
|
||||
"targets": {
|
||||
"browsers": "> 2%",
|
||||
"node": 4
|
||||
}
|
||||
}
|
||||
],
|
||||
"stage-0"
|
||||
]
|
||||
},
|
||||
"standard": {
|
||||
"ignore": [
|
||||
"dist"
|
||||
],
|
||||
"parser": "babel-eslint"
|
||||
}
|
||||
}
|
||||
83
packages/xo-lib/src/index.js
Normal file
83
packages/xo-lib/src/index.js
Normal file
@@ -0,0 +1,83 @@
|
||||
import JsonRpcWebSocketClient, {
|
||||
OPEN,
|
||||
CLOSED
|
||||
} from 'jsonrpc-websocket-client'
|
||||
import { BaseError } from 'make-error'
|
||||
import { startsWith } from 'lodash'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const noop = () => {}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export class XoError extends BaseError {}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export default class Xo extends JsonRpcWebSocketClient {
|
||||
constructor (opts) {
|
||||
const url = opts && opts.url || '.'
|
||||
super(`${url === '/' ? '' : url}/api/`)
|
||||
|
||||
this._credentials = opts && opts.credentials || null
|
||||
this._user = null
|
||||
|
||||
this.on(OPEN, () => {
|
||||
if (this._credentials) {
|
||||
this._signIn(this._credentials).catch(noop)
|
||||
}
|
||||
})
|
||||
this.on(CLOSED, () => {
|
||||
this._user = null
|
||||
})
|
||||
}
|
||||
|
||||
get user () {
|
||||
return this._user
|
||||
}
|
||||
|
||||
call (method, args, i) {
|
||||
if (startsWith(method, 'session.')) {
|
||||
return Promise.reject(
|
||||
new XoError('session.*() methods are disabled from this interface')
|
||||
)
|
||||
}
|
||||
|
||||
const promise = super.call(method, args)
|
||||
promise.retry = (predicate) => promise.catch((error) => {
|
||||
i = (i || 0) + 1
|
||||
if (predicate(error, i)) {
|
||||
return this.call(method, args, i)
|
||||
}
|
||||
})
|
||||
|
||||
return promise
|
||||
}
|
||||
|
||||
refreshUser () {
|
||||
return super.call('session.getUser').then(user => {
|
||||
return (this._user = user)
|
||||
})
|
||||
}
|
||||
|
||||
signIn (credentials) {
|
||||
// Register this credentials for future use.
|
||||
this._credentials = credentials
|
||||
|
||||
return this._signIn(credentials)
|
||||
}
|
||||
|
||||
_signIn (credentials) {
|
||||
return super.call('session.signIn', credentials).then(
|
||||
user => {
|
||||
this._user = user
|
||||
this.emit('authenticated')
|
||||
},
|
||||
error => {
|
||||
this.emit('authenticationFailure', error)
|
||||
throw error
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
2619
packages/xo-lib/yarn.lock
Normal file
2619
packages/xo-lib/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
24
packages/xo-remote-parser/.npmignore
Normal file
24
packages/xo-remote-parser/.npmignore
Normal file
@@ -0,0 +1,24 @@
|
||||
/benchmark/
|
||||
/benchmarks/
|
||||
*.bench.js
|
||||
*.bench.js.map
|
||||
|
||||
/examples/
|
||||
example.js
|
||||
example.js.map
|
||||
*.example.js
|
||||
*.example.js.map
|
||||
|
||||
/fixture/
|
||||
/fixtures/
|
||||
*.fixture.js
|
||||
*.fixture.js.map
|
||||
*.fixtures.js
|
||||
*.fixtures.js.map
|
||||
|
||||
/test/
|
||||
/tests/
|
||||
*.spec.js
|
||||
*.spec.js.map
|
||||
|
||||
__snapshots__/
|
||||
49
packages/xo-remote-parser/README.md
Normal file
49
packages/xo-remote-parser/README.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# ${pkg.name} [](https://travis-ci.org/${pkg.shortGitHubPath})
|
||||
|
||||
> ${pkg.description}
|
||||
|
||||
## Install
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/${pkg.name}):
|
||||
|
||||
```
|
||||
> npm install --save ${pkg.name}
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
**TODO**
|
||||
|
||||
## Development
|
||||
|
||||
```
|
||||
# Install dependencies
|
||||
> npm install
|
||||
|
||||
# Run the tests
|
||||
> npm test
|
||||
|
||||
# Continuously compile
|
||||
> npm run dev
|
||||
|
||||
# Continuously run the tests
|
||||
> npm run dev-test
|
||||
|
||||
# Build for production (automatically called by npm install)
|
||||
> npm run build
|
||||
```
|
||||
|
||||
## Contributions
|
||||
|
||||
Contributions are *very* welcomed, either on the documentation or on
|
||||
the code.
|
||||
|
||||
You may:
|
||||
|
||||
- report any [issue](${pkg.bugs})
|
||||
you've encountered;
|
||||
- fork and create a pull request.
|
||||
|
||||
## License
|
||||
|
||||
${pkg.license} © [${pkg.author.name}](${pkg.author.url})
|
||||
81
packages/xo-remote-parser/package.json
Normal file
81
packages/xo-remote-parser/package.json
Normal file
@@ -0,0 +1,81 @@
|
||||
{
|
||||
"name": "xo-remote-parser",
|
||||
"version": "0.3.0",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "",
|
||||
"keywords": [],
|
||||
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/packages/xo-remote-parser",
|
||||
"bugs": "https://github.com/vatesfr/xo-web/issues",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/vatesfr/xen-orchestra.git"
|
||||
},
|
||||
"author": {
|
||||
"name": "Fabrice Marsaud",
|
||||
"email": "fabrice.marsaud@vates.fr"
|
||||
},
|
||||
"preferGlobal": false,
|
||||
"main": "dist/",
|
||||
"bin": {},
|
||||
"files": [
|
||||
"dist/"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
},
|
||||
"dependencies": {
|
||||
"lodash": "^4.13.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-cli": "^6.18.0",
|
||||
"babel-eslint": "^7.1.1",
|
||||
"babel-plugin-lodash": "^3.2.11",
|
||||
"babel-preset-env": "^1.1.7",
|
||||
"babel-preset-stage-3": "^6.17.0",
|
||||
"cross-env": "^3.1.4",
|
||||
"deep-freeze": "^0.0.1",
|
||||
"dependency-check": "^2.7.0",
|
||||
"jest": "^18.1.0",
|
||||
"rimraf": "^2.5.4",
|
||||
"standard": "^8.6.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
|
||||
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
|
||||
"dev-test": "jest --bail --watch",
|
||||
"posttest": "standard && dependency-check ./package.json",
|
||||
"prebuild": "rimraf dist/",
|
||||
"predev": "yarn run prebuild",
|
||||
"prepublish": "yarn run build",
|
||||
"test": "jest"
|
||||
},
|
||||
"babel": {
|
||||
"plugins": [
|
||||
"lodash"
|
||||
],
|
||||
"presets": [
|
||||
[
|
||||
"env",
|
||||
{
|
||||
"targets": {
|
||||
"browsers": "> 5%",
|
||||
"node": 4
|
||||
}
|
||||
}
|
||||
],
|
||||
"stage-3"
|
||||
]
|
||||
},
|
||||
"jest": {
|
||||
"testPathDirs": [
|
||||
"<rootDir>/src"
|
||||
],
|
||||
"testRegex": "\\.spec\\.js$"
|
||||
},
|
||||
"standard": {
|
||||
"ignore": [
|
||||
"dist"
|
||||
],
|
||||
"parser": "babel-eslint"
|
||||
}
|
||||
}
|
||||
57
packages/xo-remote-parser/src/index.js
Normal file
57
packages/xo-remote-parser/src/index.js
Normal file
@@ -0,0 +1,57 @@
|
||||
import filter from 'lodash/filter'
|
||||
import map from 'lodash/map'
|
||||
import trim from 'lodash/trim'
|
||||
import trimStart from 'lodash/trimStart'
|
||||
|
||||
const sanitizePath = (...paths) => filter(map(paths, s => s && filter(map(s.split('/'), trim)).join('/'))).join('/')
|
||||
|
||||
export const parse = string => {
|
||||
const object = { }
|
||||
|
||||
const [type, rest] = string.split('://')
|
||||
if (type === 'file') {
|
||||
object.type = 'file'
|
||||
object.path = `/${trimStart(rest, '/')}` // the leading slash has been forgotten on client side first implementation
|
||||
} else if (type === 'nfs') {
|
||||
object.type = 'nfs'
|
||||
const [host, path] = rest.split(':')
|
||||
object.host = host
|
||||
object.path = `/${trimStart(path, '/')}` // takes care of a missing leading slash coming from previous version format
|
||||
} else if (type === 'smb') {
|
||||
object.type = 'smb'
|
||||
const lastAtSign = rest.lastIndexOf('@')
|
||||
const smb = rest.slice(lastAtSign + 1)
|
||||
const auth = rest.slice(0, lastAtSign)
|
||||
const firstColon = auth.indexOf(':')
|
||||
const username = auth.slice(0, firstColon)
|
||||
const password = auth.slice(firstColon + 1)
|
||||
const [domain, sh] = smb.split('\\\\')
|
||||
const [host, path] = sh.split('\0')
|
||||
object.host = host
|
||||
object.path = path
|
||||
object.domain = domain
|
||||
object.username = username
|
||||
object.password = password
|
||||
}
|
||||
return object
|
||||
}
|
||||
|
||||
export const format = ({type, host, path, username, password, domain}) => {
|
||||
type === 'local' && (type = 'file')
|
||||
let string = `${type}://`
|
||||
if (type === 'nfs') {
|
||||
string += `${host}:`
|
||||
}
|
||||
if (type === 'smb') {
|
||||
string += `${username}:${password}@${domain}\\\\${host}`
|
||||
}
|
||||
path = sanitizePath(path)
|
||||
if (type === 'smb') {
|
||||
path = path.split('/')
|
||||
path = '\0' + path.join('\\') // FIXME saving with the windows fashion \ was a bad idea :,(
|
||||
} else {
|
||||
path = `/${path}`
|
||||
}
|
||||
string += path
|
||||
return string
|
||||
}
|
||||
89
packages/xo-remote-parser/src/index.spec.js
Normal file
89
packages/xo-remote-parser/src/index.spec.js
Normal file
@@ -0,0 +1,89 @@
|
||||
/* eslint-env jest */
|
||||
|
||||
import deepFreeze from 'deep-freeze'
|
||||
|
||||
import { parse, format } from './'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
// Data used for both parse and format (i.e. correctly formatted).
|
||||
const data = deepFreeze({
|
||||
file: {
|
||||
string: 'file:///var/lib/xoa/backup',
|
||||
object: {
|
||||
type: 'file',
|
||||
path: '/var/lib/xoa/backup'
|
||||
}
|
||||
},
|
||||
SMB: {
|
||||
string: 'smb://Administrator:pas:sw@ord@toto\\\\192.168.100.225\\smb\0',
|
||||
object: {
|
||||
type: 'smb',
|
||||
host: '192.168.100.225\\smb',
|
||||
path: '',
|
||||
domain: 'toto',
|
||||
username: 'Administrator',
|
||||
password: 'pas:sw@ord'
|
||||
}
|
||||
},
|
||||
NFS: {
|
||||
string: 'nfs://192.168.100.225:/media/nfs',
|
||||
object: {
|
||||
type: 'nfs',
|
||||
host: '192.168.100.225',
|
||||
path: '/media/nfs'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const parseData = deepFreeze({
|
||||
...data,
|
||||
|
||||
'file with missing leading slash (#7)': {
|
||||
string: 'file://var/lib/xoa/backup',
|
||||
object: {
|
||||
type: 'file',
|
||||
path: '/var/lib/xoa/backup'
|
||||
}
|
||||
},
|
||||
'nfs with missing leading slash': {
|
||||
string: 'nfs://192.168.100.225:media/nfs',
|
||||
object: {
|
||||
type: 'nfs',
|
||||
host: '192.168.100.225',
|
||||
path: '/media/nfs'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const formatData = deepFreeze({
|
||||
...data,
|
||||
|
||||
'file with local type': {
|
||||
string: 'file:///var/lib/xoa/backup',
|
||||
object: {
|
||||
type: 'local',
|
||||
path: '/var/lib/xoa/backup'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
describe('format', () => {
|
||||
for (const name in formatData) {
|
||||
const datum = formatData[name]
|
||||
it(name, () => {
|
||||
expect(format(datum.object)).toBe(datum.string)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
describe('parse', () => {
|
||||
for (const name in parseData) {
|
||||
const datum = parseData[name]
|
||||
it(name, () => {
|
||||
expect(parse(datum.string)).toEqual(datum.object)
|
||||
})
|
||||
}
|
||||
})
|
||||
3426
packages/xo-remote-parser/yarn.lock
Normal file
3426
packages/xo-remote-parser/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
10
packages/xo-server-auth-github/.npmignore
Normal file
10
packages/xo-server-auth-github/.npmignore
Normal file
@@ -0,0 +1,10 @@
|
||||
/examples/
|
||||
example.js
|
||||
example.js.map
|
||||
*.example.js
|
||||
*.example.js.map
|
||||
|
||||
/test/
|
||||
/tests/
|
||||
*.spec.js
|
||||
*.spec.js.map
|
||||
64
packages/xo-server-auth-github/README.md
Normal file
64
packages/xo-server-auth-github/README.md
Normal file
@@ -0,0 +1,64 @@
|
||||
# xo-server-auth-github [](https://travis-ci.org/vatesfr/xen-orchestra)
|
||||
|
||||
> GitHub authentication plugin for XO-Server
|
||||
|
||||
This plugin allows GitHub users to authenticate to Xen-Orchestra.
|
||||
|
||||
The first time a user signs in, XO will create a new XO user with the
|
||||
same identifier.
|
||||
|
||||
## Install
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/xo-server-auth-github):
|
||||
|
||||
```
|
||||
> npm install --global xo-server-auth-github
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
> This plugin is based on [passport-github](https://github.com/jaredhanson/passport-github),
|
||||
> see [its documentation](https://github.com/jaredhanson/passport-github#configure-strategy)
|
||||
> for more information about the configuration.
|
||||
|
||||
Like all other xo-server plugins, it can be configured directly via
|
||||
the web interface, see [the plugin documentation](https://xen-orchestra.com/docs/plugins.html).
|
||||
|
||||

|
||||
|
||||
## Development
|
||||
|
||||
### Installing dependencies
|
||||
|
||||
```
|
||||
> npm install
|
||||
```
|
||||
|
||||
### Compilation
|
||||
|
||||
The sources files are watched and automatically recompiled on changes.
|
||||
|
||||
```
|
||||
> npm run dev
|
||||
```
|
||||
|
||||
### Tests
|
||||
|
||||
```
|
||||
> npm run test-dev
|
||||
```
|
||||
|
||||
## Contributions
|
||||
|
||||
Contributions are *very* welcomed, either on the documentation or on
|
||||
the code.
|
||||
|
||||
You may:
|
||||
|
||||
- report any [issue](https://github.com/vatesfr/xo-web/issues)
|
||||
you've encountered;
|
||||
- fork and create a pull request.
|
||||
|
||||
## License
|
||||
|
||||
AGPL3 © [Vates SAS](http://vates.fr)
|
||||
BIN
packages/xo-server-auth-github/github.png
Normal file
BIN
packages/xo-server-auth-github/github.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 45 KiB |
74
packages/xo-server-auth-github/package.json
Normal file
74
packages/xo-server-auth-github/package.json
Normal file
@@ -0,0 +1,74 @@
|
||||
{
|
||||
"name": "xo-server-auth-github",
|
||||
"version": "0.2.1",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "GitHub authentication plugin for XO-Server",
|
||||
"keywords": [
|
||||
"xo-server",
|
||||
"xo-server",
|
||||
"authentication",
|
||||
"github"
|
||||
],
|
||||
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/packages/xo-server-auth-github",
|
||||
"bugs": "https://github.com/vatesfr/xo-web/issues",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/vatesfr/xen-orchestra.git"
|
||||
},
|
||||
"author": {
|
||||
"name": "Julien Fontanet",
|
||||
"email": "julien.fontanet@isonoe.net"
|
||||
},
|
||||
"preferGlobal": false,
|
||||
"main": "dist/",
|
||||
"bin": {},
|
||||
"files": [
|
||||
"dist/"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
},
|
||||
"dependencies": {
|
||||
"babel-runtime": "^6.11.6",
|
||||
"passport-github": "^1.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-cli": "^6.16.0",
|
||||
"babel-eslint": "^7.0.0",
|
||||
"babel-plugin-transform-runtime": "^6.15.0",
|
||||
"babel-preset-latest": "^6.16.0",
|
||||
"babel-preset-stage-0": "^6.16.0",
|
||||
"clarify": "^2.0.0",
|
||||
"dependency-check": "^2.6.0",
|
||||
"mocha": "^3.1.0",
|
||||
"must": "^0.13.2",
|
||||
"source-map-support": "^0.4.3",
|
||||
"standard": "^8.2.0",
|
||||
"trace": "^2.3.3"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
|
||||
"depcheck": "dependency-check ./package.json",
|
||||
"dev": "NODE_DEV=development babel --watch --source-maps --out-dir=dist/ src/",
|
||||
"dev-test": "mocha --opts .mocha.opts --watch --reporter=min \"dist/**/*.spec.js\"",
|
||||
"lint": "standard",
|
||||
"posttest": "yarn run lint && yarn run depcheck",
|
||||
"prepublish": "yarn run build",
|
||||
"test": "mocha --opts .mocha.opts \"dist/**/*.spec.js\""
|
||||
},
|
||||
"babel": {
|
||||
"plugins": [
|
||||
"transform-runtime"
|
||||
],
|
||||
"presets": [
|
||||
"latest",
|
||||
"stage-0"
|
||||
]
|
||||
},
|
||||
"standard": {
|
||||
"ignore": [
|
||||
"dist"
|
||||
],
|
||||
"parser": "babel-eslint"
|
||||
}
|
||||
}
|
||||
44
packages/xo-server-auth-github/src/index.js
Normal file
44
packages/xo-server-auth-github/src/index.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import {Strategy} from 'passport-github'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export const configurationSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
clientID: {
|
||||
type: 'string'
|
||||
},
|
||||
clientSecret: {
|
||||
type: 'string'
|
||||
}
|
||||
},
|
||||
required: ['clientID', 'clientSecret']
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
class AuthGitHubXoPlugin {
|
||||
constructor (xo) {
|
||||
this._xo = xo
|
||||
}
|
||||
|
||||
configure (conf) {
|
||||
this._conf = conf
|
||||
}
|
||||
|
||||
load () {
|
||||
const {_xo: xo} = this
|
||||
|
||||
xo.registerPassportStrategy(new Strategy(this._conf, async (accessToken, refreshToken, profile, done) => {
|
||||
try {
|
||||
done(null, await xo.registerUser('github', profile.username))
|
||||
} catch (error) {
|
||||
done(error.message)
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export default ({xo}) => new AuthGitHubXoPlugin(xo)
|
||||
17
packages/xo-server-auth-github/src/index.spec.js
Normal file
17
packages/xo-server-auth-github/src/index.spec.js
Normal file
@@ -0,0 +1,17 @@
|
||||
/* eslint-env mocha */
|
||||
|
||||
import expect from 'must'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
import myLib from './'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
describe.skip('myLib', () => {
|
||||
it('does something', () => {
|
||||
// TODO: some real tests.
|
||||
|
||||
expect(myLib).to.exists()
|
||||
})
|
||||
})
|
||||
2721
packages/xo-server-auth-github/yarn.lock
Normal file
2721
packages/xo-server-auth-github/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
1
packages/xo-server-auth-ldap/.gitignore
vendored
Normal file
1
packages/xo-server-auth-ldap/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/ldap.cache.conf
|
||||
24
packages/xo-server-auth-ldap/.npmignore
Normal file
24
packages/xo-server-auth-ldap/.npmignore
Normal file
@@ -0,0 +1,24 @@
|
||||
/benchmark/
|
||||
/benchmarks/
|
||||
*.bench.js
|
||||
*.bench.js.map
|
||||
|
||||
/examples/
|
||||
example.js
|
||||
example.js.map
|
||||
*.example.js
|
||||
*.example.js.map
|
||||
|
||||
/fixture/
|
||||
/fixtures/
|
||||
*.fixture.js
|
||||
*.fixture.js.map
|
||||
*.fixtures.js
|
||||
*.fixtures.js.map
|
||||
|
||||
/test/
|
||||
/tests/
|
||||
*.spec.js
|
||||
*.spec.js.map
|
||||
|
||||
__snapshots__/
|
||||
82
packages/xo-server-auth-ldap/README.md
Normal file
82
packages/xo-server-auth-ldap/README.md
Normal file
@@ -0,0 +1,82 @@
|
||||
# xo-server-auth-ldap [](https://travis-ci.org/vatesfr/xen-orchestra)
|
||||
|
||||
> LDAP authentication plugin for XO-Server
|
||||
|
||||
This plugin allows LDAP users to authenticate to Xen-Orchestra.
|
||||
|
||||
The first time a user signs in, XO will create a new XO user with the
|
||||
same identifier.
|
||||
|
||||
## Install
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/xo-server-auth-ldap):
|
||||
|
||||
```
|
||||
> npm install --global xo-server-auth-ldap
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Like all other xo-server plugins, it can be configured directly via
|
||||
the web interface, see [the plugin documentation](https://xen-orchestra.com/docs/plugins.html).
|
||||
|
||||
If you have issues, you can use the provided CLI to gather more
|
||||
information:
|
||||
|
||||
```
|
||||
> xo-server-auth-ldap
|
||||
? uri ldap://ldap.company.net
|
||||
? fill optional certificateAuthorities? No
|
||||
? fill optional checkCertificate? No
|
||||
? fill optional bind? No
|
||||
? base ou=people,dc=company,dc=net
|
||||
? fill optional filter? No
|
||||
configuration saved in ./ldap.cache.conf
|
||||
? Username john.smith
|
||||
? Password *****
|
||||
searching for entries...
|
||||
0 entries found
|
||||
could not authenticate john.smith
|
||||
```
|
||||
|
||||
## Algorithm
|
||||
|
||||
1. If `bind` is defined, attempt to bind using this user.
|
||||
2. Searches for the user in the directory starting from the `base`
|
||||
with the defined `filter`.
|
||||
3. If found, a bind is attempted using the distinguished name of this
|
||||
user and the provided password.
|
||||
|
||||
## Development
|
||||
|
||||
```
|
||||
# Install dependencies
|
||||
> npm install
|
||||
|
||||
# Run the tests
|
||||
> npm test
|
||||
|
||||
# Continuously compile
|
||||
> npm run dev
|
||||
|
||||
# Continuously run the tests
|
||||
> npm run dev-test
|
||||
|
||||
# Build for production (automatically called by npm install)
|
||||
> npm run build
|
||||
```
|
||||
|
||||
## Contributions
|
||||
|
||||
Contributions are *very* welcomed, either on the documentation or on
|
||||
the code.
|
||||
|
||||
You may:
|
||||
|
||||
- report any [issue](https://github.com/vatesfr/xo-web/issues)
|
||||
you've encountered;
|
||||
- fork and create a pull request.
|
||||
|
||||
## License
|
||||
|
||||
AGPL3 © [Vates SAS](http://vates.fr)
|
||||
97
packages/xo-server-auth-ldap/package.json
Normal file
97
packages/xo-server-auth-ldap/package.json
Normal file
@@ -0,0 +1,97 @@
|
||||
{
|
||||
"name": "xo-server-auth-ldap",
|
||||
"version": "0.6.2",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "LDAP authentication plugin for XO-Server",
|
||||
"keywords": [
|
||||
"ldap",
|
||||
"orchestra",
|
||||
"plugin",
|
||||
"xen",
|
||||
"xen-orchestra",
|
||||
"xo-server"
|
||||
],
|
||||
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/packages/xo-server-auth-ldap",
|
||||
"bugs": "https://github.com/vatesfr/xo-web/issues",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/vatesfr/xen-orchestra.git"
|
||||
},
|
||||
"author": {
|
||||
"name": "Julien Fontanet",
|
||||
"email": "julien.fontanet@vates.fr"
|
||||
},
|
||||
"preferGlobal": false,
|
||||
"main": "dist/",
|
||||
"bin": {
|
||||
"xo-server-auth-ldap": "dist/test-cli.js"
|
||||
},
|
||||
"files": [
|
||||
"dist/"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
},
|
||||
"dependencies": {
|
||||
"babel-runtime": "^6.22.0",
|
||||
"event-to-promise": "^0.7.0",
|
||||
"exec-promise": "^0.6.1",
|
||||
"inquirer": "^3.0.1",
|
||||
"ldapjs": "^1.0.1",
|
||||
"lodash": "^4.17.4",
|
||||
"promise-toolbox": "^0.8.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-cli": "^6.22.2",
|
||||
"babel-eslint": "^7.1.1",
|
||||
"babel-plugin-lodash": "^3.2.11",
|
||||
"babel-plugin-transform-runtime": "^6.22.0",
|
||||
"babel-preset-env": "^1.1.8",
|
||||
"babel-preset-stage-3": "^6.22.0",
|
||||
"cross-env": "^3.1.4",
|
||||
"dependency-check": "^2.8.0",
|
||||
"jest": "^18.1.0",
|
||||
"rimraf": "^2.5.4",
|
||||
"standard": "^8.6.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
|
||||
"commitmsg": "npm test",
|
||||
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
|
||||
"dev-test": "jest --bail --watch",
|
||||
"posttest": "standard && dependency-check ./package.json",
|
||||
"prebuild": "rimraf dist/",
|
||||
"predev": "npm run prebuild",
|
||||
"prepublish": "npm run build",
|
||||
"test": "jest"
|
||||
},
|
||||
"babel": {
|
||||
"plugins": [
|
||||
"lodash",
|
||||
"transform-runtime"
|
||||
],
|
||||
"presets": [
|
||||
[
|
||||
"env",
|
||||
{
|
||||
"targets": {
|
||||
"node": 4
|
||||
}
|
||||
}
|
||||
],
|
||||
"stage-3"
|
||||
]
|
||||
},
|
||||
"jest": {
|
||||
"testPathDirs": [
|
||||
"<rootDir>/src"
|
||||
],
|
||||
"testRegex": "\\.spec\\.js$"
|
||||
},
|
||||
"standard": {
|
||||
"ignore": [
|
||||
"dist"
|
||||
],
|
||||
"parser": "babel-eslint"
|
||||
}
|
||||
}
|
||||
244
packages/xo-server-auth-ldap/src/index.js
Normal file
244
packages/xo-server-auth-ldap/src/index.js
Normal file
@@ -0,0 +1,244 @@
|
||||
/* eslint no-throw-literal: 0 */
|
||||
|
||||
import eventToPromise from 'event-to-promise'
|
||||
import { bind, noop } from 'lodash'
|
||||
import { createClient } from 'ldapjs'
|
||||
import { escape } from 'ldapjs/lib/filters/escape'
|
||||
import { promisify } from 'promise-toolbox'
|
||||
import { readFile } from 'fs'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const VAR_RE = /\{\{([^}]+)\}\}/g
|
||||
const evalFilter = (filter, vars) => filter.replace(VAR_RE, (_, name) => {
|
||||
const value = vars[name]
|
||||
|
||||
if (value === undefined) {
|
||||
throw new Error('invalid variable: ' + name)
|
||||
}
|
||||
|
||||
return escape(value)
|
||||
})
|
||||
|
||||
export const configurationSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
uri: {
|
||||
description: 'URI of the LDAP server.',
|
||||
type: 'string'
|
||||
},
|
||||
certificateAuthorities: {
|
||||
description: `
|
||||
Paths to CA certificates to use when connecting to SSL-secured LDAP servers.
|
||||
|
||||
If not specified, it will use a default set of well-known CAs.
|
||||
`.trim(),
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string'
|
||||
}
|
||||
},
|
||||
checkCertificate: {
|
||||
description: 'Enforce the validity of the server\'s certificates. You can disable it when connecting to servers that use a self-signed certificate.',
|
||||
type: 'boolean',
|
||||
default: true
|
||||
},
|
||||
bind: {
|
||||
description: 'Credentials to use before looking for the user record.',
|
||||
type: 'object',
|
||||
properties: {
|
||||
dn: {
|
||||
description: `
|
||||
Full distinguished name of the user permitted to search the LDAP directory for the user to authenticate.
|
||||
|
||||
Example: uid=xoa-auth,ou=people,dc=company,dc=net
|
||||
|
||||
For Microsoft Active Directory, it can also be \`<user>@<domain>\`.
|
||||
`.trim(),
|
||||
type: 'string'
|
||||
},
|
||||
password: {
|
||||
description: 'Password of the user permitted of search the LDAP directory.',
|
||||
type: 'string'
|
||||
}
|
||||
},
|
||||
required: ['dn', 'password']
|
||||
},
|
||||
base: {
|
||||
description: 'The base is the part of the description tree where the users are looked for.',
|
||||
type: 'string'
|
||||
},
|
||||
filter: {
|
||||
description: `
|
||||
Filter used to find the user.
|
||||
|
||||
For Microsoft Active Directory, you can try one of the following filters:
|
||||
|
||||
- \`(cn={{name}})\`
|
||||
- \`(sAMAccountName={{name}})\`
|
||||
- \`(sAMAccountName={{name}}@<domain>)\` (replace \`<domain>\` by your own domain)
|
||||
- \`(userPrincipalName={{name}})\`
|
||||
`.trim(),
|
||||
type: 'string',
|
||||
default: '(uid={{name}})'
|
||||
}
|
||||
},
|
||||
required: ['uri', 'base']
|
||||
}
|
||||
|
||||
export const testSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
username: {
|
||||
description: 'LDAP username',
|
||||
type: 'string'
|
||||
},
|
||||
password: {
|
||||
description: 'LDAP password',
|
||||
type: 'string'
|
||||
}
|
||||
},
|
||||
required: ['username', 'password']
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
class AuthLdap {
|
||||
constructor (xo) {
|
||||
this._xo = xo
|
||||
|
||||
this._authenticate = bind(this._authenticate, this)
|
||||
}
|
||||
|
||||
async configure (conf) {
|
||||
const clientOpts = this._clientOpts = {
|
||||
url: conf.uri,
|
||||
maxConnections: 5,
|
||||
tlsOptions: {}
|
||||
}
|
||||
|
||||
{
|
||||
const {
|
||||
bind,
|
||||
checkCertificate = true,
|
||||
certificateAuthorities
|
||||
} = conf
|
||||
|
||||
if (bind) {
|
||||
clientOpts.bindDN = bind.dn
|
||||
clientOpts.bindCredentials = bind.password
|
||||
}
|
||||
|
||||
const {tlsOptions} = clientOpts
|
||||
|
||||
tlsOptions.rejectUnauthorized = checkCertificate
|
||||
if (certificateAuthorities) {
|
||||
tlsOptions.ca = await Promise.all(
|
||||
certificateAuthorities.map(path => readFile(path))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const {
|
||||
bind: credentials,
|
||||
base: searchBase,
|
||||
filter: searchFilter = '(uid={{name}})'
|
||||
} = conf
|
||||
|
||||
this._credentials = credentials
|
||||
this._searchBase = searchBase
|
||||
this._searchFilter = searchFilter
|
||||
}
|
||||
|
||||
load () {
|
||||
this._xo.registerAuthenticationProvider(this._authenticate)
|
||||
}
|
||||
|
||||
unload () {
|
||||
this._xo.unregisterAuthenticationProvider(this._authenticate)
|
||||
}
|
||||
|
||||
test ({ username, password }) {
|
||||
return this._authenticate({
|
||||
username,
|
||||
password
|
||||
}).then(result => {
|
||||
if (result === null) {
|
||||
throw new Error('could not authenticate user')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async _authenticate ({ username, password }, logger = noop) {
|
||||
if (username === undefined || password === undefined) {
|
||||
logger('require `username` and `password` to authenticate!')
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
const client = createClient(this._clientOpts)
|
||||
|
||||
try {
|
||||
// Promisify some methods.
|
||||
const bind = promisify(client.bind, client)
|
||||
const search = promisify(client.search, client)
|
||||
|
||||
await eventToPromise(client, 'connect')
|
||||
|
||||
// Bind if necessary.
|
||||
{
|
||||
const {_credentials: credentials} = this
|
||||
if (credentials) {
|
||||
logger(`attempting to bind with as ${credentials.dn}...`)
|
||||
await bind(credentials.dn, credentials.password)
|
||||
logger(`successfully bound as ${credentials.dn}`)
|
||||
}
|
||||
}
|
||||
|
||||
// Search for the user.
|
||||
const entries = []
|
||||
{
|
||||
logger('searching for entries...')
|
||||
const response = await search(this._searchBase, {
|
||||
scope: 'sub',
|
||||
filter: evalFilter(this._searchFilter, {
|
||||
name: username
|
||||
})
|
||||
})
|
||||
|
||||
response.on('searchEntry', entry => {
|
||||
logger('.')
|
||||
entries.push(entry.json)
|
||||
})
|
||||
|
||||
const {status} = await eventToPromise(response, 'end')
|
||||
if (status) {
|
||||
throw new Error('unexpected search response status: ' + status)
|
||||
}
|
||||
|
||||
logger(`${entries.length} entries found`)
|
||||
}
|
||||
|
||||
// Try to find an entry which can be bind with the given password.
|
||||
for (const entry of entries) {
|
||||
try {
|
||||
logger(`attempting to bind as ${entry.objectName}`)
|
||||
await bind(entry.objectName, password)
|
||||
logger(`successfully bound as ${entry.objectName} => ${username} authenticated`)
|
||||
return { username }
|
||||
} catch (error) {
|
||||
logger(`failed to bind as ${entry.objectName}: ${error.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
logger(`could not authenticate ${username}`)
|
||||
return null
|
||||
} finally {
|
||||
client.unbind()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export default ({xo}) => new AuthLdap(xo)
|
||||
166
packages/xo-server-auth-ldap/src/prompt-schema.js
Normal file
166
packages/xo-server-auth-ldap/src/prompt-schema.js
Normal file
@@ -0,0 +1,166 @@
|
||||
import { forEach, isFinite, isInteger } from 'lodash'
|
||||
import { forOwn as forOwnAsync } from 'promise-toolbox'
|
||||
import { prompt } from 'inquirer'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const EMPTY_OBJECT = Object.freeze({ __proto__: null })
|
||||
|
||||
const _extractValue = ({ value }) => value
|
||||
|
||||
export const confirm = (message, {
|
||||
default: defaultValue = null
|
||||
} = EMPTY_OBJECT) => prompt({
|
||||
default: defaultValue,
|
||||
message,
|
||||
name: 'value',
|
||||
type: 'confirm'
|
||||
}).then(_extractValue)
|
||||
|
||||
export const input = (message, {
|
||||
default: defaultValue = null,
|
||||
filter = undefined,
|
||||
validate = undefined
|
||||
} = EMPTY_OBJECT) => prompt({
|
||||
default: defaultValue,
|
||||
message,
|
||||
name: 'value',
|
||||
type: 'input',
|
||||
validate
|
||||
}).then(_extractValue)
|
||||
|
||||
export const list = (message, choices, {
|
||||
default: defaultValue = null
|
||||
} = EMPTY_OBJECT) => prompt({
|
||||
default: defaultValue,
|
||||
choices,
|
||||
message,
|
||||
name: 'value',
|
||||
type: 'list'
|
||||
}).then(_extractValue)
|
||||
|
||||
export const password = (message, {
|
||||
default: defaultValue = null,
|
||||
filter = undefined,
|
||||
validate = undefined
|
||||
} = EMPTY_OBJECT) => prompt({
|
||||
default: defaultValue,
|
||||
message,
|
||||
name: 'value',
|
||||
type: 'password',
|
||||
validate
|
||||
}).then(_extractValue)
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const promptByType = {
|
||||
__proto__: null,
|
||||
|
||||
array: async (schema, defaultValue, path) => {
|
||||
const items = []
|
||||
if (defaultValue == null) {
|
||||
defaultValue = items
|
||||
}
|
||||
|
||||
let i = 0
|
||||
|
||||
const itemSchema = schema.items
|
||||
const promptItem = async () => {
|
||||
items[i] = await promptGeneric(
|
||||
itemSchema,
|
||||
defaultValue[i],
|
||||
path
|
||||
? `${path} [${i}]`
|
||||
: `[${i}]`
|
||||
)
|
||||
|
||||
++i
|
||||
}
|
||||
|
||||
let n = schema.minItems || 0
|
||||
while (i < n) { // eslint-disable-line no-unmodified-loop-condition
|
||||
await promptItem()
|
||||
}
|
||||
|
||||
n = schema.maxItems || Infinity
|
||||
while (
|
||||
i < n && // eslint-disable-line no-unmodified-loop-condition
|
||||
await confirm('additional item?', {
|
||||
default: false
|
||||
})
|
||||
) {
|
||||
await promptItem()
|
||||
}
|
||||
|
||||
return items
|
||||
},
|
||||
|
||||
boolean: (schema, defaultValue, path) => confirm(path, {
|
||||
default: defaultValue != null ? defaultValue : schema.default
|
||||
}),
|
||||
|
||||
enum: (schema, defaultValue, path) => list(path, schema.enum, {
|
||||
defaultValue: defaultValue || schema.defaultValue
|
||||
}),
|
||||
|
||||
integer: (schema, defaultValue, path) => input(path, {
|
||||
default: defaultValue || schema.default,
|
||||
filter: input => +input,
|
||||
validate: input => isInteger(+input)
|
||||
}),
|
||||
|
||||
number: (schema, defaultValue, path) => input(path, {
|
||||
default: defaultValue || schema.default,
|
||||
filter: input => +input,
|
||||
validate: input => isFinite(+input)
|
||||
}),
|
||||
|
||||
object: async (schema, defaultValue, path) => {
|
||||
const value = {}
|
||||
|
||||
const required = {}
|
||||
schema.required && forEach(schema.required, name => {
|
||||
required[name] = true
|
||||
})
|
||||
|
||||
const promptProperty = async (schema, name) => {
|
||||
const subpath = path
|
||||
? `${path} > ${schema.title || name}`
|
||||
: schema.title || name
|
||||
|
||||
if (
|
||||
required[name] ||
|
||||
await confirm(`fill optional ${subpath}?`, {
|
||||
default: Boolean(defaultValue && name in defaultValue)
|
||||
})
|
||||
) {
|
||||
value[name] = await promptGeneric(
|
||||
schema,
|
||||
defaultValue && defaultValue[name],
|
||||
subpath
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
await forOwnAsync.call(schema.properties || {}, promptProperty)
|
||||
|
||||
return value
|
||||
},
|
||||
|
||||
string: (schema, defaultValue, path) => input(path, {
|
||||
default: defaultValue || schema.default
|
||||
})
|
||||
}
|
||||
|
||||
export default function promptGeneric (schema, defaultValue, path) {
|
||||
const type = schema.enum
|
||||
? 'enum'
|
||||
: schema.type
|
||||
|
||||
const prompt = promptByType[type.toLowerCase()]
|
||||
if (!prompt) {
|
||||
throw new Error(`unsupported type: ${type}`)
|
||||
}
|
||||
|
||||
return prompt(schema, defaultValue, path)
|
||||
}
|
||||
49
packages/xo-server-auth-ldap/src/test-cli.js
Executable file
49
packages/xo-server-auth-ldap/src/test-cli.js
Executable file
@@ -0,0 +1,49 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import execPromise from 'exec-promise'
|
||||
import { bind } from 'lodash'
|
||||
import { fromCallback } from 'promise-toolbox'
|
||||
import { readFile, writeFile } from 'fs'
|
||||
|
||||
import promptSchema, {
|
||||
input,
|
||||
password
|
||||
} from './prompt-schema'
|
||||
import createPlugin, {
|
||||
configurationSchema
|
||||
} from './'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const CACHE_FILE = './ldap.cache.conf'
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
execPromise(async args => {
|
||||
const config = await promptSchema(
|
||||
configurationSchema,
|
||||
await fromCallback(cb => readFile(CACHE_FILE, 'utf-8', cb)).then(
|
||||
JSON.parse,
|
||||
() => ({})
|
||||
)
|
||||
)
|
||||
await fromCallback(cb => writeFile(CACHE_FILE, JSON.stringify(config, null, 2), cb)).then(
|
||||
() => {
|
||||
console.log('configuration saved in %s', CACHE_FILE)
|
||||
},
|
||||
error => {
|
||||
console.warn('failed to save configuration in %s', CACHE_FILE)
|
||||
console.warn(error.message)
|
||||
}
|
||||
)
|
||||
|
||||
const plugin = createPlugin({})
|
||||
await plugin.configure(config)
|
||||
|
||||
await plugin._authenticate({
|
||||
username: await input('Username', {
|
||||
validate: input => !!input.length
|
||||
}),
|
||||
password: await password('Password')
|
||||
}, bind(console.log, console))
|
||||
})
|
||||
3644
packages/xo-server-auth-ldap/yarn.lock
Normal file
3644
packages/xo-server-auth-ldap/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
10
packages/xo-server-auth-saml/.npmignore
Normal file
10
packages/xo-server-auth-saml/.npmignore
Normal file
@@ -0,0 +1,10 @@
|
||||
/examples/
|
||||
example.js
|
||||
example.js.map
|
||||
*.example.js
|
||||
*.example.js.map
|
||||
|
||||
/test/
|
||||
/tests/
|
||||
*.spec.js
|
||||
*.spec.js.map
|
||||
63
packages/xo-server-auth-saml/README.md
Normal file
63
packages/xo-server-auth-saml/README.md
Normal file
@@ -0,0 +1,63 @@
|
||||
# xo-server-auth-saml [](https://travis-ci.org/vatesfr/xen-orchestra)
|
||||
|
||||
> SAML authentication plugin for XO-Server
|
||||
|
||||
This plugin allows SAML users to authenticate to Xen-Orchestra.
|
||||
|
||||
The first time a user signs in, XO will create a new XO user with the
|
||||
same identifier.
|
||||
|
||||
## Install
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/xo-server-auth-saml):
|
||||
|
||||
```
|
||||
> npm install --global xo-server-auth-saml
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
> This plugin is based on [passport-saml](https://github.com/bergie/passport-saml),
|
||||
> see [its documentation](https://github.com/bergie/passport-saml#configure-strategy)
|
||||
> for more information about the configuration.
|
||||
|
||||
Like all other xo-server plugins, it can be configured directly via
|
||||
the web interface, see [the plugin documentation](https://xen-orchestra.com/docs/plugins.html).
|
||||
|
||||
> Important: When registering your instance to your identity provider,
|
||||
> you must configure its callback URL to
|
||||
> `http://xo.company.net/signin/saml/callback`!
|
||||
|
||||
## Development
|
||||
|
||||
```
|
||||
# Install dependencies
|
||||
> npm install
|
||||
|
||||
# Run the tests
|
||||
> npm test
|
||||
|
||||
# Continuously compile
|
||||
> npm run dev
|
||||
|
||||
# Continuously run the tests
|
||||
> npm run dev-test
|
||||
|
||||
# Build for production (automatically called by npm install)
|
||||
> npm run build
|
||||
```
|
||||
|
||||
## Contributions
|
||||
|
||||
Contributions are *very* welcomed, either on the documentation or on
|
||||
the code.
|
||||
|
||||
You may:
|
||||
|
||||
- report any [issue](https://github.com/vatesfr/xo-web/issues)
|
||||
you've encountered;
|
||||
- fork and create a pull request.
|
||||
|
||||
## License
|
||||
|
||||
AGPL3 © [Vates SAS](http://vates.fr)
|
||||
75
packages/xo-server-auth-saml/package.json
Normal file
75
packages/xo-server-auth-saml/package.json
Normal file
@@ -0,0 +1,75 @@
|
||||
{
|
||||
"name": "xo-server-auth-saml",
|
||||
"version": "0.4.1",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "SAML authentication plugin for XO-Server",
|
||||
"keywords": [
|
||||
"authentication",
|
||||
"orchestra",
|
||||
"plugin",
|
||||
"saml",
|
||||
"xen",
|
||||
"xen-orchestra",
|
||||
"xo-server"
|
||||
],
|
||||
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/packages/xo-server-auth-saml",
|
||||
"bugs": "https://github.com/vatesfr/xo-web/issues",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/vatesfr/xen-orchestra.git"
|
||||
},
|
||||
"author": {
|
||||
"name": "Julien Fontanet",
|
||||
"email": "julien.fontanet@isonoe.net"
|
||||
},
|
||||
"preferGlobal": false,
|
||||
"main": "dist/",
|
||||
"bin": {},
|
||||
"files": [
|
||||
"dist/"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
},
|
||||
"dependencies": {
|
||||
"babel-runtime": "^6.11.6",
|
||||
"passport-saml": "^0.15.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-cli": "^6.14.0",
|
||||
"babel-eslint": "^7.1.0",
|
||||
"babel-plugin-transform-runtime": "^6.15.0",
|
||||
"babel-preset-latest": "^6.16.0",
|
||||
"babel-preset-stage-0": "^6.5.0",
|
||||
"cross-env": "^3.1.3",
|
||||
"dependency-check": "^2.6.0",
|
||||
"rimraf": "^2.5.4",
|
||||
"standard": "^8.2.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
|
||||
"clean": "rimraf dist/",
|
||||
"depcheck": "dependency-check ./package.json",
|
||||
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
|
||||
"lint": "standard",
|
||||
"posttest": "yarn run lint && yarn run depcheck",
|
||||
"prebuild": "yarn run clean",
|
||||
"predev": "yarn run clean",
|
||||
"prepublish": "yarn run build"
|
||||
},
|
||||
"babel": {
|
||||
"plugins": [
|
||||
"transform-runtime"
|
||||
],
|
||||
"presets": [
|
||||
"latest",
|
||||
"stage-0"
|
||||
]
|
||||
},
|
||||
"standard": {
|
||||
"ignore": [
|
||||
"dist"
|
||||
],
|
||||
"parser": "babel-eslint"
|
||||
}
|
||||
}
|
||||
59
packages/xo-server-auth-saml/src/index.js
Normal file
59
packages/xo-server-auth-saml/src/index.js
Normal file
@@ -0,0 +1,59 @@
|
||||
import {Strategy} from 'passport-saml'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export const configurationSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
cert: {
|
||||
type: 'string'
|
||||
},
|
||||
entryPoint: {
|
||||
type: 'string'
|
||||
},
|
||||
issuer: {
|
||||
type: 'string'
|
||||
},
|
||||
usernameField: {
|
||||
type: 'string'
|
||||
}
|
||||
},
|
||||
required: ['cert', 'entryPoint', 'issuer']
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
class AuthSamlXoPlugin {
|
||||
constructor ({ xo }) {
|
||||
this._conf = null
|
||||
this._usernameField = null
|
||||
this._xo = xo
|
||||
}
|
||||
|
||||
configure ({ usernameField, ...conf }) {
|
||||
this._usernameField = usernameField
|
||||
this._conf = conf
|
||||
}
|
||||
|
||||
load () {
|
||||
const xo = this._xo
|
||||
|
||||
xo.registerPassportStrategy(new Strategy(this._conf, async (profile, done) => {
|
||||
const name = profile[this._usernameField]
|
||||
if (!name) {
|
||||
done('no name found for this user')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
done(null, await xo.registerUser('saml', name))
|
||||
} catch (error) {
|
||||
done(error.message)
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export default opts => new AuthSamlXoPlugin(opts)
|
||||
2668
packages/xo-server-auth-saml/yarn.lock
Normal file
2668
packages/xo-server-auth-saml/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
10
packages/xo-server-backup-reports/.npmignore
Normal file
10
packages/xo-server-backup-reports/.npmignore
Normal file
@@ -0,0 +1,10 @@
|
||||
/examples/
|
||||
example.js
|
||||
example.js.map
|
||||
*.example.js
|
||||
*.example.js.map
|
||||
|
||||
/test/
|
||||
/tests/
|
||||
*.spec.js
|
||||
*.spec.js.map
|
||||
52
packages/xo-server-backup-reports/README.md
Normal file
52
packages/xo-server-backup-reports/README.md
Normal file
@@ -0,0 +1,52 @@
|
||||
# xo-server-backup-reports [](https://travis-ci.org/vatesfr/xen-orchestra)
|
||||
|
||||
> Backup reports plugin for XO-Server
|
||||
|
||||
XO-Server plugin which sends email reports and Xmpp messages when backup jobs are done.
|
||||
|
||||
## Install
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/xo-server-backup-reports):
|
||||
|
||||
```
|
||||
> npm install --global xo-server-backup-reports
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Like all other xo-server plugins, it can be configured directly via
|
||||
the web interface, see [the plugin documentation](https://xen-orchestra.com/docs/plugins.html).
|
||||
|
||||
## Development
|
||||
|
||||
```
|
||||
# Install dependencies
|
||||
> npm install
|
||||
|
||||
# Run the tests
|
||||
> npm test
|
||||
|
||||
# Continuously compile
|
||||
> npm run dev
|
||||
|
||||
# Continuously run the tests
|
||||
> npm run dev-test
|
||||
|
||||
# Build for production (automatically called by npm install)
|
||||
> npm run build
|
||||
```
|
||||
|
||||
## Contributions
|
||||
|
||||
Contributions are *very* welcomed, either on the documentation or on
|
||||
the code.
|
||||
|
||||
You may:
|
||||
|
||||
- report any [issue](https://github.com/vatesfr/xo-web/issues)
|
||||
you've encountered;
|
||||
- fork and create a pull request.
|
||||
|
||||
## License
|
||||
|
||||
AGPL3 © [Vates SAS](http://vates.fr)
|
||||
85
packages/xo-server-backup-reports/package.json
Normal file
85
packages/xo-server-backup-reports/package.json
Normal file
@@ -0,0 +1,85 @@
|
||||
{
|
||||
"name": "xo-server-backup-reports",
|
||||
"version": "0.6.0",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "Backup reports plugin for XO-Server",
|
||||
"keywords": [
|
||||
"backup",
|
||||
"email",
|
||||
"mail",
|
||||
"orchestra",
|
||||
"plugin",
|
||||
"report",
|
||||
"reports",
|
||||
"xen",
|
||||
"xen-orchestra",
|
||||
"xo-server"
|
||||
],
|
||||
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/packages/xo-server-backup-reports",
|
||||
"bugs": "https://github.com/vatesfr/xo-web/issues",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/vatesfr/xen-orchestra.git"
|
||||
},
|
||||
"author": {
|
||||
"name": "Julien Fontanet",
|
||||
"email": "julien.fontanet@isonoe.net"
|
||||
},
|
||||
"preferGlobal": false,
|
||||
"main": "dist/",
|
||||
"bin": {},
|
||||
"files": [
|
||||
"dist/"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
},
|
||||
"dependencies": {
|
||||
"lodash": "^4.13.1",
|
||||
"moment": "^2.13.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-cli": "^6.18.0",
|
||||
"babel-eslint": "^7.1.1",
|
||||
"babel-plugin-lodash": "^3.2.10",
|
||||
"babel-preset-env": "^1.0.0",
|
||||
"babel-preset-stage-0": "^6.16.0",
|
||||
"cross-env": "^3.1.3",
|
||||
"dependency-check": "^2.6.0",
|
||||
"rimraf": "^2.5.4",
|
||||
"standard": "^8.5.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
|
||||
"clean": "rimraf dist/",
|
||||
"depcheck": "dependency-check ./package.json",
|
||||
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
|
||||
"lint": "standard",
|
||||
"posttest": "yarn run lint && yarn run depcheck",
|
||||
"prebuild": "yarn run clean",
|
||||
"predev": "yarn run clean",
|
||||
"prepublish": "yarn run build"
|
||||
},
|
||||
"babel": {
|
||||
"plugins": [
|
||||
"lodash"
|
||||
],
|
||||
"presets": [
|
||||
[
|
||||
"env",
|
||||
{
|
||||
"targets": {
|
||||
"node": 4
|
||||
}
|
||||
}
|
||||
],
|
||||
"stage-0"
|
||||
]
|
||||
},
|
||||
"standard": {
|
||||
"ignore": [
|
||||
"dist"
|
||||
],
|
||||
"parser": "babel-eslint"
|
||||
}
|
||||
}
|
||||
178
packages/xo-server-backup-reports/src/index.js
Normal file
178
packages/xo-server-backup-reports/src/index.js
Normal file
@@ -0,0 +1,178 @@
|
||||
import moment from 'moment'
|
||||
import { forEach } from 'lodash'
|
||||
|
||||
export const configurationSchema = {
|
||||
type: 'object',
|
||||
|
||||
properties: {
|
||||
toMails: {
|
||||
type: 'array',
|
||||
title: 'mails',
|
||||
description: 'an array of recipients (mails)',
|
||||
|
||||
items: {
|
||||
type: 'string'
|
||||
},
|
||||
minItems: 1
|
||||
},
|
||||
toXmpp: {
|
||||
type: 'array',
|
||||
title: 'xmpp address',
|
||||
description: 'an array of recipients (xmpp)',
|
||||
|
||||
items: {
|
||||
type: 'string'
|
||||
},
|
||||
minItems: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const logError = e => {
|
||||
console.error('backup report error:', e)
|
||||
}
|
||||
|
||||
class BackupReportsXoPlugin {
|
||||
constructor (xo) {
|
||||
this._xo = xo
|
||||
this._report = ::this._wrapper
|
||||
}
|
||||
|
||||
configure ({ toMails, toXmpp }) {
|
||||
this._mailsReceivers = toMails
|
||||
this._xmppReceivers = toXmpp
|
||||
}
|
||||
|
||||
load () {
|
||||
this._xo.on('job:terminated', this._report)
|
||||
}
|
||||
|
||||
unload () {
|
||||
this._xo.removeListener('job:terminated', this._report)
|
||||
}
|
||||
|
||||
_wrapper (status) {
|
||||
return new Promise(resolve => resolve(this._listener(status))).catch(logError)
|
||||
}
|
||||
|
||||
_listener (status) {
|
||||
let nSuccess = 0
|
||||
let nCalls = 0
|
||||
let reportWhen
|
||||
|
||||
const text = []
|
||||
const nagiosText = []
|
||||
|
||||
forEach(status.calls, call => {
|
||||
// Ignore call if it's not a Backup a Snapshot or a Disaster Recovery.
|
||||
if (call.method !== 'vm.deltaCopy' &&
|
||||
call.method !== 'vm.rollingDeltaBackup' &&
|
||||
call.method !== 'vm.rollingDrCopy' &&
|
||||
call.method !== 'vm.rollingSnapshot' &&
|
||||
call.method !== 'vm.rollingBackup') {
|
||||
return
|
||||
}
|
||||
|
||||
reportWhen = call.params._reportWhen
|
||||
|
||||
if (reportWhen === 'never') {
|
||||
return
|
||||
}
|
||||
|
||||
nCalls++
|
||||
if (!call.error) {
|
||||
nSuccess++
|
||||
}
|
||||
|
||||
let vm
|
||||
|
||||
try {
|
||||
vm = this._xo.getObject(call.params.id || call.params.vm)
|
||||
} catch (e) {}
|
||||
|
||||
const start = moment(call.start)
|
||||
const end = moment(call.end)
|
||||
const duration = moment.duration(end - start).humanize()
|
||||
|
||||
text.push([
|
||||
`### VM : ${vm ? vm.name_label : 'undefined'}`,
|
||||
` - UUID: ${vm ? vm.uuid : 'undefined'}`,
|
||||
call.error
|
||||
? ` - Status: Failure\n - Error: ${call.error.message}`
|
||||
: ' - Status: Success',
|
||||
` - Start time: ${String(start)}`,
|
||||
` - End time: ${String(end)}`,
|
||||
` - Duration: ${duration}`
|
||||
].join('\n'))
|
||||
|
||||
if (call.error) {
|
||||
nagiosText.push(
|
||||
`[ ${vm ? vm.name_label : 'undefined'} : ${call.error.message} ]`
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
// No backup calls.
|
||||
if (nCalls === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const globalSuccess = nSuccess === nCalls
|
||||
if (globalSuccess && (
|
||||
reportWhen === 'fail' || // xo-web < 5
|
||||
reportWhen === 'failure' // xo-web >= 5
|
||||
)) {
|
||||
return
|
||||
}
|
||||
|
||||
const start = moment(status.start)
|
||||
const end = moment(status.end)
|
||||
const duration = moment.duration(end - start).humanize()
|
||||
let method = status.calls[Object.keys(status.calls)[0]].method
|
||||
method = method.slice(method.indexOf('.') + 1)
|
||||
.replace(/([A-Z])/g, ' $1').replace(/^./, letter => letter.toUpperCase()) // humanize
|
||||
const tag = status.calls[Object.keys(status.calls)[0]].params.tag
|
||||
|
||||
// Global status.
|
||||
text.unshift([
|
||||
`## Global status for "${tag}" (${method}): ${globalSuccess ? 'Success' : 'Fail'}`,
|
||||
` - Start time: ${String(start)}`,
|
||||
` - End time: ${String(end)}`,
|
||||
` - Duration: ${duration}`,
|
||||
` - Successful backed up VM number: ${nSuccess}`,
|
||||
` - Failed backed up VM: ${nCalls - nSuccess}`,
|
||||
''
|
||||
].join('\n'))
|
||||
|
||||
const markdown = text.join('\n')
|
||||
const markdownNagios = nagiosText.join(' ')
|
||||
|
||||
// TODO : Handle errors when `sendEmail` isn't present. (Plugin dependencies)
|
||||
|
||||
const xo = this._xo
|
||||
return Promise.all([
|
||||
xo.sendEmail && xo.sendEmail({
|
||||
to: this._mailsReceivers,
|
||||
subject: `[Xen Orchestra][${globalSuccess ? 'Success' : 'Failure'}] Backup report for ${tag}`,
|
||||
markdown
|
||||
}),
|
||||
xo.sendToXmppClient && xo.sendToXmppClient({
|
||||
to: this._xmppReceivers,
|
||||
message: markdown
|
||||
}),
|
||||
xo.sendSlackMessage && xo.sendSlackMessage({
|
||||
message: markdown
|
||||
}),
|
||||
xo.sendPassiveCheck && xo.sendPassiveCheck({
|
||||
status: globalSuccess ? 0 : 2,
|
||||
message: globalSuccess ? `[Xen Orchestra] [Success] Backup report for ${tag}` : `[Xen Orchestra] [Failure] Backup report for ${tag} - VMs : ${markdownNagios}`
|
||||
})
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export default ({ xo }) => new BackupReportsXoPlugin(xo)
|
||||
2574
packages/xo-server-backup-reports/yarn.lock
Normal file
2574
packages/xo-server-backup-reports/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
10
packages/xo-server-cloud/.npmignore
Normal file
10
packages/xo-server-cloud/.npmignore
Normal file
@@ -0,0 +1,10 @@
|
||||
/examples/
|
||||
example.js
|
||||
example.js.map
|
||||
*.example.js
|
||||
*.example.js.map
|
||||
|
||||
/test/
|
||||
/tests/
|
||||
*.spec.js
|
||||
*.spec.js.map
|
||||
48
packages/xo-server-cloud/README.md
Normal file
48
packages/xo-server-cloud/README.md
Normal file
@@ -0,0 +1,48 @@
|
||||
# xo-server-cloud [](https://travis-ci.org/vatesfr/xen-orchestra)
|
||||
|
||||
## Install
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/xo-server-cloud):
|
||||
|
||||
```
|
||||
> npm install --global xo-server-cloud
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Like all other xo-server plugins, it can be configured directly via
|
||||
the web interface, see [the plugin documentation](https://xen-orchestra.com/docs/plugins.html).
|
||||
|
||||
## Development
|
||||
|
||||
```
|
||||
# Install dependencies
|
||||
> npm install
|
||||
|
||||
# Run the tests
|
||||
> npm test
|
||||
|
||||
# Continuously compile
|
||||
> npm run dev
|
||||
|
||||
# Continuously run the tests
|
||||
> npm run dev-test
|
||||
|
||||
# Build for production (automatically called by npm install)
|
||||
> npm run build
|
||||
```
|
||||
|
||||
## Contributions
|
||||
|
||||
Contributions are *very* welcomed, either on the documentation or on
|
||||
the code.
|
||||
|
||||
You may:
|
||||
|
||||
- report any [issue](https://github.com/vatesfr/xo-web/issues)
|
||||
you've encountered;
|
||||
- fork and create a pull request.
|
||||
|
||||
## License
|
||||
|
||||
AGPL3 © [Vates SAS](https://vates.fr)
|
||||
83
packages/xo-server-cloud/package.json
Normal file
83
packages/xo-server-cloud/package.json
Normal file
@@ -0,0 +1,83 @@
|
||||
{
|
||||
"name": "xo-server-cloud",
|
||||
"version": "0.2.0",
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
"keywords": [
|
||||
"cloud",
|
||||
"orchestra",
|
||||
"plugin",
|
||||
"xen",
|
||||
"xen-orchestra",
|
||||
"xo-server"
|
||||
],
|
||||
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/packages/xo-server-transport-cloud",
|
||||
"bugs": "https://github.com/vatesfr/xo-web/issues",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/vatesfr/xen-orchestra.git"
|
||||
},
|
||||
"author": {
|
||||
"name": "Pierre Donias",
|
||||
"email": "pierre.donias@gmail.com"
|
||||
},
|
||||
"preferGlobal": false,
|
||||
"main": "dist/",
|
||||
"bin": {},
|
||||
"files": [
|
||||
"dist/"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
},
|
||||
"dependencies": {
|
||||
"babel-runtime": "^6.20.0",
|
||||
"event-to-promise": "^0.7.0",
|
||||
"jsonrpc-websocket-client": "^0.1.2",
|
||||
"superagent": "^3.4.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-cli": "^6.18.0",
|
||||
"babel-eslint": "^7.1.1",
|
||||
"babel-plugin-transform-runtime": "^6.15.0",
|
||||
"babel-preset-env": "^1.1.4",
|
||||
"babel-preset-stage-3": "^6.16.0",
|
||||
"cross-env": "^3.1.3",
|
||||
"dependency-check": "^2.6.0",
|
||||
"rimraf": "^2.5.4",
|
||||
"standard": "^8.6.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
|
||||
"clean": "rimraf dist/",
|
||||
"depcheck": "dependency-check ./package.json",
|
||||
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
|
||||
"lint": "standard",
|
||||
"posttest": "yarn run lint && yarn run depcheck",
|
||||
"prebuild": "yarn run clean",
|
||||
"predev": "yarn run clean",
|
||||
"prepublish": "yarn run build"
|
||||
},
|
||||
"babel": {
|
||||
"plugins": [
|
||||
"transform-runtime"
|
||||
],
|
||||
"presets": [
|
||||
[
|
||||
"env",
|
||||
{
|
||||
"targets": {
|
||||
"node": 4
|
||||
}
|
||||
}
|
||||
],
|
||||
"stage-3"
|
||||
]
|
||||
},
|
||||
"standard": {
|
||||
"ignore": [
|
||||
"dist"
|
||||
],
|
||||
"parser": "babel-eslint"
|
||||
}
|
||||
}
|
||||
149
packages/xo-server-cloud/src/index.js
Normal file
149
packages/xo-server-cloud/src/index.js
Normal file
@@ -0,0 +1,149 @@
|
||||
import Client, { createBackoff } from 'jsonrpc-websocket-client'
|
||||
import eventToPromise from 'event-to-promise'
|
||||
import request from 'superagent'
|
||||
import { PassThrough } from 'stream'
|
||||
|
||||
const UPDATER_URL = 'localhost'
|
||||
const WS_PORT = 9001
|
||||
const HTTP_PORT = 9002
|
||||
|
||||
// ===================================================================
|
||||
|
||||
class XoServerCloud {
|
||||
constructor ({ xo }) {
|
||||
this._xo = xo
|
||||
|
||||
// Defined in configure().
|
||||
this._conf = null
|
||||
this._key = null
|
||||
}
|
||||
|
||||
configure (configuration) {
|
||||
this._conf = configuration
|
||||
}
|
||||
|
||||
async load () {
|
||||
const getResourceCatalog = () => this._getCatalog()
|
||||
getResourceCatalog.description = 'Get the list of all available resources'
|
||||
getResourceCatalog.permission = 'admin'
|
||||
|
||||
const registerResource = ({ namespace }) => this._registerResource(namespace)
|
||||
registerResource.description = 'Register a resource via cloud plugin'
|
||||
registerResource.params = {
|
||||
namespace: {
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
registerResource.permission = 'admin'
|
||||
|
||||
this._unsetApiMethods = this._xo.addApiMethods({
|
||||
cloud: {
|
||||
getResourceCatalog,
|
||||
registerResource
|
||||
}
|
||||
})
|
||||
this._unsetRequestResource = this._xo.defineProperty('requestResource', this._requestResource, this)
|
||||
|
||||
const updater = this._updater = new Client(`${UPDATER_URL}:${WS_PORT}`)
|
||||
const connect = () => updater.open(createBackoff()).catch(
|
||||
error => {
|
||||
console.error('xo-server-cloud: fail to connect to updater', error)
|
||||
|
||||
return connect()
|
||||
}
|
||||
)
|
||||
updater
|
||||
.on('close', connect)
|
||||
.on('scheduledAttempt', ({ delay }) => {
|
||||
console.warn('xo-server-cloud: next attempt in %s ms', delay)
|
||||
})
|
||||
connect()
|
||||
}
|
||||
|
||||
unload () {
|
||||
this._unsetApiMethods()
|
||||
this._unsetRequestResource()
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
async _getCatalog () {
|
||||
const catalog = await this._updater.call('getResourceCatalog')
|
||||
|
||||
if (!catalog) {
|
||||
throw new Error('cannot get catalog')
|
||||
}
|
||||
|
||||
return catalog
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
async _getNamespaces () {
|
||||
const catalog = await this._getCatalog()
|
||||
|
||||
if (!catalog._namespaces) {
|
||||
throw new Error('cannot get namespaces')
|
||||
}
|
||||
|
||||
return catalog._namespaces
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
async _registerResource (namespace) {
|
||||
const _namespace = (await this._getNamespaces())[namespace]
|
||||
|
||||
if (_namespace.registered || _namespace.pending) {
|
||||
return new Error(`already registered for ${namespace}`)
|
||||
}
|
||||
|
||||
return this._updater.call('registerResource', { namespace })
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
async _getNamespaceCatalog (namespace) {
|
||||
const namespaceCatalog = (await this._getCatalog())[namespace]
|
||||
|
||||
if (!namespaceCatalog) {
|
||||
throw new Error(`cannot get catalog: ${namespace} not registered`)
|
||||
}
|
||||
|
||||
return namespaceCatalog
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
async _requestResource (namespace, id, version) {
|
||||
const _namespace = (await this._getNamespaces())[namespace]
|
||||
|
||||
if (!_namespace || !_namespace.registered) {
|
||||
throw new Error(`cannot get resource: ${namespace} not registered`)
|
||||
}
|
||||
|
||||
const namespaceCatalog = await this._getNamespaceCatalog(namespace)
|
||||
|
||||
const downloadToken = await this._updater.call('getResourceDownloadToken', {
|
||||
token: namespaceCatalog._token,
|
||||
id,
|
||||
version
|
||||
})
|
||||
|
||||
if (!downloadToken) {
|
||||
throw new Error('cannot get download token')
|
||||
}
|
||||
|
||||
const req = request.get(`${UPDATER_URL}:${HTTP_PORT}/`)
|
||||
.set('Authorization', `Bearer ${downloadToken}`)
|
||||
|
||||
// Impossible to pipe the response directly: https://github.com/visionmedia/superagent/issues/1187
|
||||
const pt = new PassThrough()
|
||||
req.pipe(pt)
|
||||
pt.length = (await eventToPromise(req, 'response')).headers['content-length']
|
||||
|
||||
return pt
|
||||
}
|
||||
}
|
||||
|
||||
export default opts => new XoServerCloud(opts)
|
||||
10
packages/xo-server-load-balancer/.npmignore
Normal file
10
packages/xo-server-load-balancer/.npmignore
Normal file
@@ -0,0 +1,10 @@
|
||||
/examples/
|
||||
example.js
|
||||
example.js.map
|
||||
*.example.js
|
||||
*.example.js.map
|
||||
|
||||
/test/
|
||||
/tests/
|
||||
*.spec.js
|
||||
*.spec.js.map
|
||||
53
packages/xo-server-load-balancer/README.md
Normal file
53
packages/xo-server-load-balancer/README.md
Normal file
@@ -0,0 +1,53 @@
|
||||
# xo-server-load-balancer [](https://travis-ci.org/vatesfr/xen-orchestra)
|
||||
|
||||
XO-Server plugin that allows load balancing.
|
||||
|
||||
## Install
|
||||
|
||||
Go inside your `xo-server` folder and install it:
|
||||
|
||||
```
|
||||
> npm install --global xo-server-load-balancer
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Like all other xo-server plugins, it can be configured directly via
|
||||
the web interface, see [the plugin documentation](https://xen-orchestra.com/docs/plugins.html).
|
||||
|
||||
## Development
|
||||
|
||||
### Installing dependencies
|
||||
|
||||
```
|
||||
> npm install
|
||||
```
|
||||
|
||||
### Compilation
|
||||
|
||||
The sources files are watched and automatically recompiled on changes.
|
||||
|
||||
```
|
||||
> npm run dev
|
||||
```
|
||||
|
||||
### Tests
|
||||
|
||||
```
|
||||
> npm run test-dev
|
||||
```
|
||||
|
||||
## Contributions
|
||||
|
||||
Contributions are *very* welcomed, either on the documentation or on
|
||||
the code.
|
||||
|
||||
You may:
|
||||
|
||||
- report any [issue](https://github.com/vatesfr/xo-web/issues)
|
||||
you've encountered;
|
||||
- fork and create a pull request.
|
||||
|
||||
## License
|
||||
|
||||
AGPL3 © [Vates SAS](http://vates.fr)
|
||||
72
packages/xo-server-load-balancer/package.json
Normal file
72
packages/xo-server-load-balancer/package.json
Normal file
@@ -0,0 +1,72 @@
|
||||
{
|
||||
"name": "xo-server-load-balancer",
|
||||
"version": "0.3.1",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "Load balancer for XO-Server",
|
||||
"keywords": [
|
||||
"load",
|
||||
"balancer",
|
||||
"server",
|
||||
"pool",
|
||||
"host"
|
||||
],
|
||||
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/packages/xo-server-load-balancer",
|
||||
"bugs": "https://github.com/vatesfr/xo-web/issues",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/vatesfr/xen-orchestra.git"
|
||||
},
|
||||
"author": {
|
||||
"name": "Julien Fontanet",
|
||||
"email": "julien.fontanet@isonoe.net"
|
||||
},
|
||||
"preferGlobal": false,
|
||||
"main": "dist/",
|
||||
"bin": {},
|
||||
"files": [
|
||||
"dist/"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
},
|
||||
"dependencies": {
|
||||
"babel-runtime": "^6.11.6",
|
||||
"cron": "^1.1.0",
|
||||
"event-to-promise": "^0.7.0",
|
||||
"lodash": "^4.16.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-cli": "^6.16.0",
|
||||
"babel-eslint": "^7.0.0",
|
||||
"babel-plugin-lodash": "^3.2.9",
|
||||
"babel-plugin-transform-runtime": "^6.15.0",
|
||||
"babel-preset-es2015": "^6.16.0",
|
||||
"babel-preset-stage-0": "^6.16.0",
|
||||
"dependency-check": "^2.5.1",
|
||||
"standard": "^8.2.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
|
||||
"depcheck": "dependency-check ./package.json",
|
||||
"dev": "NODE_DEV=development babel --watch --source-maps --out-dir=dist/ src/",
|
||||
"lint": "standard",
|
||||
"posttest": "yarn run lint && yarn run depcheck",
|
||||
"prepublish": "yarn run build"
|
||||
},
|
||||
"babel": {
|
||||
"plugins": [
|
||||
"transform-runtime",
|
||||
"lodash"
|
||||
],
|
||||
"presets": [
|
||||
"es2015",
|
||||
"stage-0"
|
||||
]
|
||||
},
|
||||
"standard": {
|
||||
"ignore": [
|
||||
"dist"
|
||||
],
|
||||
"parser": "babel-eslint"
|
||||
}
|
||||
}
|
||||
225
packages/xo-server-load-balancer/src/density-plan.js
Normal file
225
packages/xo-server-load-balancer/src/density-plan.js
Normal file
@@ -0,0 +1,225 @@
|
||||
import { clone, filter, map as mapToArray } from 'lodash'
|
||||
|
||||
import Plan from './plan'
|
||||
import { debug } from './utils'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export default class DensityPlan extends Plan {
|
||||
_checkRessourcesThresholds (objects, averages) {
|
||||
return filter(objects, object =>
|
||||
averages[object.id].memoryFree > this._thresholds.memoryFree.low
|
||||
)
|
||||
}
|
||||
|
||||
async execute () {
|
||||
const results = await this._findHostsToOptimize()
|
||||
|
||||
if (!results) {
|
||||
return
|
||||
}
|
||||
|
||||
const {
|
||||
hosts,
|
||||
toOptimize
|
||||
} = results
|
||||
|
||||
let {
|
||||
averages: hostsAverages
|
||||
} = results
|
||||
|
||||
const pools = await this._getPlanPools()
|
||||
let optimizationsCount = 0
|
||||
|
||||
for (const hostToOptimize of toOptimize) {
|
||||
const {
|
||||
id: hostId,
|
||||
$poolId: poolId
|
||||
} = hostToOptimize
|
||||
|
||||
const {
|
||||
master: masterId
|
||||
} = pools[poolId]
|
||||
|
||||
// Avoid master optimization.
|
||||
if (masterId === hostId) {
|
||||
continue
|
||||
}
|
||||
|
||||
// A host to optimize needs the ability to be restarted.
|
||||
if (hostToOptimize.powerOnMode === '') {
|
||||
debug(`Host (${hostId}) does not have a power on mode.`)
|
||||
continue
|
||||
}
|
||||
|
||||
let poolMaster // Pool master.
|
||||
const poolHosts = [] // Without master.
|
||||
const masters = [] // Without the master of this loop.
|
||||
const otherHosts = []
|
||||
|
||||
for (const dest of hosts) {
|
||||
const {
|
||||
id: destId,
|
||||
$poolId: destPoolId
|
||||
} = dest
|
||||
|
||||
// Destination host != Host to optimize!
|
||||
if (destId === hostId) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (destPoolId === poolId) {
|
||||
if (destId === masterId) {
|
||||
poolMaster = dest
|
||||
} else {
|
||||
poolHosts.push(dest)
|
||||
}
|
||||
} else if (destId === pools[destPoolId].master) {
|
||||
masters.push(dest)
|
||||
} else {
|
||||
otherHosts.push(dest)
|
||||
}
|
||||
}
|
||||
|
||||
const simulResults = await this._simulate({
|
||||
host: hostToOptimize,
|
||||
destinations: [
|
||||
[ poolMaster ],
|
||||
poolHosts,
|
||||
masters,
|
||||
otherHosts
|
||||
],
|
||||
hostsAverages: clone(hostsAverages)
|
||||
})
|
||||
|
||||
if (simulResults) {
|
||||
// Update stats.
|
||||
hostsAverages = simulResults.hostsAverages
|
||||
|
||||
// Migrate.
|
||||
await this._migrate(hostId, simulResults.moves)
|
||||
optimizationsCount++
|
||||
}
|
||||
}
|
||||
|
||||
debug(`Density mode: ${optimizationsCount} optimizations.`)
|
||||
}
|
||||
|
||||
async _simulate ({ host, destinations, hostsAverages }) {
|
||||
const { id: hostId } = host
|
||||
|
||||
debug(`Try to optimize Host (${hostId}).`)
|
||||
|
||||
const vms = await this._getVms(hostId)
|
||||
const vmsAverages = await this._getVmsAverages(vms, host)
|
||||
|
||||
for (const vm of vms) {
|
||||
if (!vm.xenTools) {
|
||||
debug(`VM (${vm.id}) of Host (${hostId}) does not support pool migration.`)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Sort vms by amount of memory. (+ -> -)
|
||||
vms.sort((a, b) =>
|
||||
vmsAverages[b.id].memory - vmsAverages[a.id].memory
|
||||
)
|
||||
|
||||
const simulResults = {
|
||||
hostsAverages,
|
||||
moves: []
|
||||
}
|
||||
|
||||
// Try to find a destination for each VM.
|
||||
for (const vm of vms) {
|
||||
let move
|
||||
|
||||
// Simulate the VM move on a destinations set.
|
||||
for (const subDestinations of destinations) {
|
||||
move = this._testMigration({
|
||||
vm,
|
||||
destinations: subDestinations,
|
||||
hostsAverages,
|
||||
vmsAverages
|
||||
})
|
||||
|
||||
// Destination found.
|
||||
if (move) {
|
||||
simulResults.moves.push(move)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Unable to move a VM.
|
||||
if (!move) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Done.
|
||||
return simulResults
|
||||
}
|
||||
|
||||
// Test if a VM migration on a destination (of a destinations set) is possible.
|
||||
_testMigration ({ vm, destinations, hostsAverages, vmsAverages }) {
|
||||
const {
|
||||
_thresholds: {
|
||||
critical: criticalThreshold
|
||||
}
|
||||
} = this
|
||||
|
||||
// Sort the destinations by available memory. (- -> +)
|
||||
destinations.sort((a, b) =>
|
||||
hostsAverages[a.id].memoryFree - hostsAverages[b.id].memoryFree
|
||||
)
|
||||
|
||||
for (const destination of destinations) {
|
||||
const destinationAverages = hostsAverages[destination.id]
|
||||
const vmAverages = vmsAverages[vm.id]
|
||||
|
||||
// Unable to move the VM.
|
||||
if (
|
||||
destinationAverages.cpu + vmAverages.cpu >= criticalThreshold ||
|
||||
destinationAverages.memoryFree - vmAverages.memory <= criticalThreshold
|
||||
) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Move ok. Update stats.
|
||||
destinationAverages.cpu += vmAverages.cpu
|
||||
destinationAverages.memoryFree -= vmAverages.memory
|
||||
|
||||
// Available movement.
|
||||
return {
|
||||
vm,
|
||||
destination
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Migrate the VMs of one host.
|
||||
// Try to shutdown the VMs host.
|
||||
async _migrate (hostId, moves) {
|
||||
const xapiSrc = this.xo.getXapi(hostId)
|
||||
|
||||
await Promise.all(
|
||||
mapToArray(moves, move => {
|
||||
const {
|
||||
vm,
|
||||
destination
|
||||
} = move
|
||||
const xapiDest = this.xo.getXapi(destination)
|
||||
debug(`Migrate VM (${vm.id}) to Host (${destination.id}) from Host (${vm.$container}).`)
|
||||
return xapiDest.migrateVm(vm._xapiId, this.xo.getXapi(destination), destination._xapiId)
|
||||
})
|
||||
)
|
||||
|
||||
debug(`Shutdown Host (${hostId}).`)
|
||||
|
||||
try {
|
||||
await xapiSrc.shutdownHost(hostId)
|
||||
} catch (error) {
|
||||
debug(`Unable to shutdown Host (${hostId}).`, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
203
packages/xo-server-load-balancer/src/index.js
Normal file
203
packages/xo-server-load-balancer/src/index.js
Normal file
@@ -0,0 +1,203 @@
|
||||
import EventEmitter from 'events'
|
||||
import eventToPromise from 'event-to-promise'
|
||||
import { CronJob } from 'cron'
|
||||
import { intersection, map as mapToArray, uniq } from 'lodash'
|
||||
|
||||
import DensityPlan from './density-plan'
|
||||
import PerformancePlan from './performance-plan'
|
||||
import {
|
||||
DEFAULT_CRITICAL_THRESHOLD_CPU,
|
||||
DEFAULT_CRITICAL_THRESHOLD_MEMORY_FREE
|
||||
} from './plan'
|
||||
import {
|
||||
EXECUTION_DELAY,
|
||||
debug
|
||||
} from './utils'
|
||||
|
||||
class Emitter extends EventEmitter {}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const PERFORMANCE_MODE = 0
|
||||
const DENSITY_MODE = 1
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export const configurationSchema = {
|
||||
type: 'object',
|
||||
|
||||
properties: {
|
||||
plans: {
|
||||
type: 'array',
|
||||
description: 'an array of plans',
|
||||
title: 'Plans',
|
||||
|
||||
items: {
|
||||
type: 'object',
|
||||
title: 'Plan',
|
||||
|
||||
properties: {
|
||||
name: {
|
||||
type: 'string',
|
||||
title: 'Name'
|
||||
},
|
||||
|
||||
mode: {
|
||||
enum: [ 'Performance mode', 'Density mode' ],
|
||||
title: 'Mode'
|
||||
},
|
||||
|
||||
pools: {
|
||||
type: 'array',
|
||||
description: 'list of pools where to apply the policy',
|
||||
|
||||
items: {
|
||||
type: 'string',
|
||||
$type: 'Pool'
|
||||
}
|
||||
},
|
||||
|
||||
thresholds: {
|
||||
type: 'object',
|
||||
title: 'Critical thresholds',
|
||||
|
||||
properties: {
|
||||
cpu: {
|
||||
type: 'integer',
|
||||
title: 'CPU (%)',
|
||||
default: DEFAULT_CRITICAL_THRESHOLD_CPU
|
||||
},
|
||||
memoryFree: {
|
||||
type: 'integer',
|
||||
title: 'RAM, Free memory (MB)',
|
||||
default: DEFAULT_CRITICAL_THRESHOLD_MEMORY_FREE
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
excludedHosts: {
|
||||
type: 'array',
|
||||
title: 'Excluded hosts',
|
||||
description: 'list of hosts that are not affected by the plan',
|
||||
|
||||
items: {
|
||||
type: 'string',
|
||||
$type: 'Host'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
required: [ 'name', 'mode', 'pools' ]
|
||||
},
|
||||
|
||||
minItems: 1
|
||||
}
|
||||
},
|
||||
|
||||
additionalProperties: false
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
// Create a job not enabled by default.
|
||||
// A job is a cron task, a running and enabled state.
|
||||
const makeJob = (cronPattern, fn) => {
|
||||
const job = {
|
||||
running: false,
|
||||
emitter: new Emitter()
|
||||
}
|
||||
|
||||
job.cron = new CronJob(cronPattern, async () => {
|
||||
if (job.running) {
|
||||
return
|
||||
}
|
||||
|
||||
job.running = true
|
||||
|
||||
try {
|
||||
await fn()
|
||||
} catch (error) {
|
||||
console.error('[WARN] scheduled function:', error && error.stack || error)
|
||||
} finally {
|
||||
job.running = false
|
||||
job.emitter.emit('finish')
|
||||
}
|
||||
})
|
||||
|
||||
job.isEnabled = () => job.cron.running
|
||||
|
||||
return job
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// ===================================================================
|
||||
|
||||
class LoadBalancerPlugin {
|
||||
constructor (xo) {
|
||||
this.xo = xo
|
||||
this._job = makeJob(`*/${EXECUTION_DELAY} * * * *`, ::this._executePlans)
|
||||
this._emitter
|
||||
}
|
||||
|
||||
async configure ({ plans }) {
|
||||
const job = this._job
|
||||
const enabled = job.isEnabled()
|
||||
|
||||
if (enabled) {
|
||||
job.cron.stop()
|
||||
}
|
||||
|
||||
// Wait until all old plans stopped running.
|
||||
if (job.running) {
|
||||
await eventToPromise(job.emitter, 'finish')
|
||||
}
|
||||
|
||||
this._plans = []
|
||||
this._poolIds = [] // Used pools.
|
||||
|
||||
if (plans) {
|
||||
for (const plan of plans) {
|
||||
this._addPlan(plan.mode === 'Performance mode' ? PERFORMANCE_MODE : DENSITY_MODE, plan)
|
||||
}
|
||||
}
|
||||
|
||||
if (enabled) {
|
||||
job.cron.start()
|
||||
}
|
||||
}
|
||||
|
||||
load () {
|
||||
this._job.cron.start()
|
||||
}
|
||||
|
||||
unload () {
|
||||
this._job.cron.stop()
|
||||
}
|
||||
|
||||
_addPlan (mode, { name, pools, ...options }) {
|
||||
pools = uniq(pools)
|
||||
|
||||
// Check already used pools.
|
||||
if (intersection(pools, this._poolIds).length > 0) {
|
||||
throw new Error(`Pool(s) already included in an other plan: ${pools}`)
|
||||
}
|
||||
|
||||
this._poolIds = this._poolIds.concat(pools)
|
||||
this._plans.push(mode === PERFORMANCE_MODE
|
||||
? new PerformancePlan(this.xo, name, pools, options)
|
||||
: new DensityPlan(this.xo, name, pools, options)
|
||||
)
|
||||
}
|
||||
|
||||
_executePlans () {
|
||||
debug('Execute plans!')
|
||||
|
||||
return Promise.all(
|
||||
mapToArray(this._plans, plan => plan.execute())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export default ({ xo }) => new LoadBalancerPlugin(xo)
|
||||
138
packages/xo-server-load-balancer/src/performance-plan.js
Normal file
138
packages/xo-server-load-balancer/src/performance-plan.js
Normal file
@@ -0,0 +1,138 @@
|
||||
import { filter, find, map as mapToArray } from 'lodash'
|
||||
|
||||
import Plan from './plan'
|
||||
import { debug } from './utils'
|
||||
|
||||
// Compare a list of objects and give the best.
|
||||
function searchBestObject (objects, fun) {
|
||||
let object = objects[0]
|
||||
|
||||
for (let i = 1; i < objects.length; i++) {
|
||||
if (fun(object, objects[i]) > 0) {
|
||||
object = objects[i]
|
||||
}
|
||||
}
|
||||
|
||||
return object
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export default class PerformancePlan extends Plan {
|
||||
_checkRessourcesThresholds (objects, averages) {
|
||||
return filter(objects, object => {
|
||||
const objectAverages = averages[object.id]
|
||||
|
||||
return (
|
||||
objectAverages.cpu >= this._thresholds.cpu.high ||
|
||||
objectAverages.memoryFree <= this._thresholds.memoryFree.high
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
async execute () {
|
||||
// Try to power on a hosts set.
|
||||
try {
|
||||
await Promise.all(
|
||||
mapToArray(
|
||||
filter(this._getHosts({ powerState: 'Halted' }), host => host.powerOnMode !== ''),
|
||||
host => {
|
||||
const { id } = host
|
||||
return this.xo.getXapi(id).powerOnHost(id)
|
||||
}
|
||||
)
|
||||
)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
|
||||
const results = await this._findHostsToOptimize()
|
||||
|
||||
if (!results) {
|
||||
return
|
||||
}
|
||||
|
||||
const {
|
||||
averages,
|
||||
toOptimize
|
||||
} = results
|
||||
let { hosts } = results
|
||||
|
||||
toOptimize.sort((a, b) => {
|
||||
a = averages[a.id]
|
||||
b = averages[b.id]
|
||||
|
||||
return (b.cpu - a.cpu) || (a.memoryFree - b.memoryFree)
|
||||
})
|
||||
|
||||
for (const exceededHost of toOptimize) {
|
||||
const { id } = exceededHost
|
||||
|
||||
debug(`Try to optimize Host (${exceededHost.id}).`)
|
||||
hosts = filter(hosts, host => host.id !== id)
|
||||
|
||||
// Search bests combinations for the worst host.
|
||||
await this._optimize({
|
||||
exceededHost,
|
||||
hosts,
|
||||
hostsAverages: averages
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async _optimize ({ exceededHost, hosts, hostsAverages }) {
|
||||
const vms = await this._getVms(exceededHost.id)
|
||||
const vmsAverages = await this._getVmsAverages(vms, exceededHost)
|
||||
|
||||
// Sort vms by cpu usage. (lower to higher)
|
||||
vms.sort((a, b) =>
|
||||
vmsAverages[b.id].cpu - vmsAverages[a.id].cpu
|
||||
)
|
||||
|
||||
const exceededAverages = hostsAverages[exceededHost.id]
|
||||
const promises = []
|
||||
|
||||
const xapiSrc = this.xo.getXapi(exceededHost)
|
||||
let optimizationsCount = 0
|
||||
|
||||
const searchFunction = (a, b) => hostsAverages[b.id].cpu - hostsAverages[a.id].cpu
|
||||
|
||||
for (const vm of vms) {
|
||||
// Search host with lower cpu usage in the same pool first. In other pool if necessary.
|
||||
let destination = searchBestObject(find(hosts, host => host.$poolId === vm.$poolId), searchFunction)
|
||||
|
||||
if (!destination) {
|
||||
destination = searchBestObject(hosts, searchFunction)
|
||||
}
|
||||
|
||||
const destinationAverages = hostsAverages[destination.id]
|
||||
const vmAverages = vmsAverages[vm.id]
|
||||
|
||||
// Unable to move the vm.
|
||||
if (
|
||||
exceededAverages.cpu - vmAverages.cpu < destinationAverages.cpu + vmAverages.cpu ||
|
||||
destinationAverages.memoryFree > vmAverages.memory
|
||||
) {
|
||||
continue
|
||||
}
|
||||
|
||||
exceededAverages.cpu -= vmAverages.cpu
|
||||
destinationAverages.cpu += vmAverages.cpu
|
||||
|
||||
exceededAverages.memoryFree += vmAverages.memory
|
||||
destinationAverages.memoryFree -= vmAverages.memory
|
||||
|
||||
debug(`Migrate VM (${vm.id}) to Host (${destination.id}) from Host (${exceededHost.id}).`)
|
||||
optimizationsCount++
|
||||
|
||||
promises.push(
|
||||
xapiSrc.migrateVm(vm._xapiId, this.xo.getXapi(destination), destination._xapiId)
|
||||
)
|
||||
}
|
||||
|
||||
await Promise.all(promises)
|
||||
debug(`Performance mode: ${optimizationsCount} optimizations for Host (${exceededHost.id}).`)
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
253
packages/xo-server-load-balancer/src/plan.js
Normal file
253
packages/xo-server-load-balancer/src/plan.js
Normal file
@@ -0,0 +1,253 @@
|
||||
import { filter, includes, map as mapToArray } from 'lodash'
|
||||
|
||||
import {
|
||||
EXECUTION_DELAY,
|
||||
debug
|
||||
} from './utils'
|
||||
|
||||
const MINUTES_OF_HISTORICAL_DATA = 30
|
||||
|
||||
// CPU threshold in percent.
|
||||
export const DEFAULT_CRITICAL_THRESHOLD_CPU = 90.0
|
||||
|
||||
// Memory threshold in MB.
|
||||
export const DEFAULT_CRITICAL_THRESHOLD_MEMORY_FREE = 64.0
|
||||
|
||||
// Thresholds factors.
|
||||
const HIGH_THRESHOLD_FACTOR = 0.85
|
||||
const LOW_THRESHOLD_FACTOR = 0.25
|
||||
|
||||
const HIGH_THRESHOLD_MEMORY_FREE_FACTOR = 1.25
|
||||
const LOW_THRESHOLD_MEMORY_FREE_FACTOR = 20.0
|
||||
|
||||
const numberOrDefault = (value, def) => (value >= 0) ? value : def
|
||||
|
||||
// ===================================================================
|
||||
// Averages.
|
||||
// ===================================================================
|
||||
|
||||
function computeAverage (values, nPoints = values.length) {
|
||||
let sum = 0
|
||||
let tot = 0
|
||||
|
||||
const { length } = values
|
||||
|
||||
for (let i = length - nPoints; i < length; i++) {
|
||||
const value = values[i]
|
||||
|
||||
sum += value || 0
|
||||
|
||||
if (value) {
|
||||
tot += 1
|
||||
}
|
||||
}
|
||||
|
||||
return sum / tot
|
||||
}
|
||||
|
||||
function computeRessourcesAverage (objects, objectsStats, nPoints) {
|
||||
const averages = {}
|
||||
|
||||
for (const object of objects) {
|
||||
const { id } = object
|
||||
const { stats } = objectsStats[id]
|
||||
|
||||
averages[id] = {
|
||||
cpu: computeAverage(
|
||||
mapToArray(stats.cpus, cpu => computeAverage(cpu, nPoints))
|
||||
),
|
||||
nCpus: stats.cpus.length,
|
||||
memoryFree: computeAverage(stats.memoryFree, nPoints),
|
||||
memory: computeAverage(stats.memory, nPoints)
|
||||
}
|
||||
}
|
||||
|
||||
return averages
|
||||
}
|
||||
|
||||
function computeRessourcesAverageWithWeight (averages1, averages2, ratio) {
|
||||
const averages = {}
|
||||
|
||||
for (const id in averages1) {
|
||||
const objectAverages = averages[id] = {}
|
||||
|
||||
for (const averageName in averages1[id]) {
|
||||
objectAverages[averageName] = averages1[id][averageName] * ratio + averages2[id][averageName] * (1 - ratio)
|
||||
}
|
||||
}
|
||||
|
||||
return averages
|
||||
}
|
||||
|
||||
function setRealCpuAverageOfVms (vms, vmsAverages, nCpus) {
|
||||
for (const vm of vms) {
|
||||
const averages = vmsAverages[vm.id]
|
||||
averages.cpu *= averages.nCpus / nCpus
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export default class Plan {
|
||||
constructor (xo, name, poolIds, {
|
||||
excludedHosts,
|
||||
thresholds
|
||||
} = {}) {
|
||||
this.xo = xo
|
||||
this._name = name
|
||||
this._poolIds = poolIds
|
||||
this._excludedHosts = excludedHosts
|
||||
this._thresholds = {
|
||||
cpu: {
|
||||
critical: numberOrDefault(thresholds && thresholds.cpu, DEFAULT_CRITICAL_THRESHOLD_CPU)
|
||||
},
|
||||
memoryFree: {
|
||||
critical: numberOrDefault(thresholds && thresholds.memoryFree, DEFAULT_CRITICAL_THRESHOLD_MEMORY_FREE) * 1024
|
||||
}
|
||||
}
|
||||
|
||||
for (const key in this._thresholds) {
|
||||
const attr = this._thresholds[key]
|
||||
const { critical } = attr
|
||||
|
||||
if (key === 'memoryFree') {
|
||||
attr.high = critical * HIGH_THRESHOLD_MEMORY_FREE_FACTOR
|
||||
attr.low = critical * LOW_THRESHOLD_MEMORY_FREE_FACTOR
|
||||
} else {
|
||||
attr.high = critical * HIGH_THRESHOLD_FACTOR
|
||||
attr.low = critical * LOW_THRESHOLD_FACTOR
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
execute () {
|
||||
throw new Error('Not implemented')
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// Get hosts to optimize.
|
||||
// ===================================================================
|
||||
|
||||
async _findHostsToOptimize () {
|
||||
const hosts = this._getHosts()
|
||||
const hostsStats = await this._getHostsStats(hosts, 'minutes')
|
||||
|
||||
// Check if a ressource's utilization exceeds threshold.
|
||||
const avgNow = computeRessourcesAverage(hosts, hostsStats, EXECUTION_DELAY)
|
||||
let toOptimize = this._checkRessourcesThresholds(hosts, avgNow)
|
||||
|
||||
// No ressource's utilization problem.
|
||||
if (toOptimize.length === 0) {
|
||||
debug('No hosts to optimize.')
|
||||
return
|
||||
}
|
||||
|
||||
// Check in the last 30 min interval with ratio.
|
||||
const avgBefore = computeRessourcesAverage(hosts, hostsStats, MINUTES_OF_HISTORICAL_DATA)
|
||||
const avgWithRatio = computeRessourcesAverageWithWeight(avgNow, avgBefore, 0.75)
|
||||
|
||||
toOptimize = this._checkRessourcesThresholds(toOptimize, avgWithRatio)
|
||||
|
||||
// No ressource's utilization problem.
|
||||
if (toOptimize.length === 0) {
|
||||
debug('No hosts to optimize.')
|
||||
return
|
||||
}
|
||||
|
||||
return {
|
||||
toOptimize,
|
||||
averages: avgWithRatio,
|
||||
hosts
|
||||
}
|
||||
}
|
||||
|
||||
_checkRessourcesThresholds () {
|
||||
throw new Error('Not implemented')
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// Get objects.
|
||||
// ===================================================================
|
||||
|
||||
_getPlanPools () {
|
||||
const pools = {}
|
||||
|
||||
try {
|
||||
for (const poolId of this._poolIds) {
|
||||
pools[poolId] = this.xo.getObject(poolId)
|
||||
}
|
||||
} catch (_) {
|
||||
return {}
|
||||
}
|
||||
|
||||
return pools
|
||||
}
|
||||
|
||||
// Compute hosts for each pool. They can change over time.
|
||||
_getHosts ({ powerState = 'Running' } = {}) {
|
||||
return filter(this.xo.getObjects(), object => (
|
||||
object.type === 'host' &&
|
||||
includes(this._poolIds, object.$poolId) &&
|
||||
object.power_state === powerState &&
|
||||
!includes(this._excludedHosts, object.id)
|
||||
))
|
||||
}
|
||||
|
||||
async _getVms (hostId) {
|
||||
return filter(this.xo.getObjects(), object =>
|
||||
object.type === 'VM' &&
|
||||
object.power_state === 'Running' &&
|
||||
object.$container === hostId
|
||||
)
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// Get stats.
|
||||
// ===================================================================
|
||||
|
||||
async _getHostsStats (hosts, granularity) {
|
||||
const hostsStats = {}
|
||||
|
||||
await Promise.all(mapToArray(hosts, host =>
|
||||
this.xo.getXapiHostStats(host, granularity).then(hostStats => {
|
||||
hostsStats[host.id] = {
|
||||
nPoints: hostStats.stats.cpus[0].length,
|
||||
stats: hostStats.stats,
|
||||
averages: {}
|
||||
}
|
||||
})
|
||||
))
|
||||
|
||||
return hostsStats
|
||||
}
|
||||
|
||||
async _getVmsStats (vms, granularity) {
|
||||
const vmsStats = {}
|
||||
|
||||
await Promise.all(mapToArray(vms, vm =>
|
||||
this.xo.getXapiVmStats(vm, granularity).then(vmStats => {
|
||||
vmsStats[vm.id] = {
|
||||
nPoints: vmStats.stats.cpus[0].length,
|
||||
stats: vmStats.stats,
|
||||
averages: {}
|
||||
}
|
||||
})
|
||||
))
|
||||
|
||||
return vmsStats
|
||||
}
|
||||
|
||||
async _getVmsAverages (vms, host) {
|
||||
const vmsStats = await this._getVmsStats(vms, 'minutes')
|
||||
const vmsAverages = computeRessourcesAverageWithWeight(
|
||||
computeRessourcesAverage(vms, vmsStats, EXECUTION_DELAY),
|
||||
computeRessourcesAverage(vms, vmsStats, MINUTES_OF_HISTORICAL_DATA),
|
||||
0.75
|
||||
)
|
||||
|
||||
// Compute real CPU usage. Virtuals cpus to reals cpus.
|
||||
setRealCpuAverageOfVms(vms, vmsAverages, host.CPUs.cpu_count)
|
||||
|
||||
return vmsAverages
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user