Compare commits
1552 Commits
xo-server/
...
xo-server/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
522d6eed92 | ||
|
|
9d1d6ea4c5 | ||
|
|
0afd506a41 | ||
|
|
9dfb837e3f | ||
|
|
4ab63b569f | ||
|
|
8d390d256d | ||
|
|
4eec5e06fc | ||
|
|
e4063b1ba8 | ||
|
|
0c3227cf8e | ||
|
|
7bed200bf5 | ||
|
|
4f763e2109 | ||
|
|
75167fb65b | ||
|
|
675588f780 | ||
|
|
2d6f94edd8 | ||
|
|
247c66ef4b | ||
|
|
1076fac40f | ||
|
|
14a4a415a2 | ||
|
|
524355b59c | ||
|
|
36fe49f3f5 | ||
|
|
c0c0af9b14 | ||
|
|
d1e472d482 | ||
|
|
c80e43ad0d | ||
|
|
fdd395e2b6 | ||
|
|
e094437168 | ||
|
|
2ee0be7466 | ||
|
|
2784a7cc92 | ||
|
|
b09f998d6c | ||
|
|
bdeb5895f6 | ||
|
|
3944b8aaee | ||
|
|
6e66cffb92 | ||
|
|
57092ee788 | ||
|
|
70e9e1c706 | ||
|
|
9662b8fbee | ||
|
|
9f66421ae7 | ||
|
|
50584c2e50 | ||
|
|
7be4e1901a | ||
|
|
b47146de45 | ||
|
|
97b229b2c7 | ||
|
|
6bb5bb9403 | ||
|
|
8c4b8271d8 | ||
|
|
69291c0574 | ||
|
|
2dc073dcd6 | ||
|
|
1894cb35d2 | ||
|
|
cd37420b07 | ||
|
|
55cb6b39db | ||
|
|
89d13b2285 | ||
|
|
1b64b0468a | ||
|
|
085fb83294 | ||
|
|
edd606563f | ||
|
|
fb804e99f0 | ||
|
|
1707cbcb54 | ||
|
|
6d6a630c31 | ||
|
|
ff2990e8e5 | ||
|
|
d679aff0fb | ||
|
|
603a444905 | ||
|
|
a002958448 | ||
|
|
cb4bc37424 | ||
|
|
0fc6f917e6 | ||
|
|
ec0d012b24 | ||
|
|
2cd4b171a1 | ||
|
|
0cb6906c4d | ||
|
|
4c19b93c30 | ||
|
|
6165f1b405 | ||
|
|
37a4221e43 | ||
|
|
9831b222b5 | ||
|
|
7b6f44fb74 | ||
|
|
399f4d0ea3 | ||
|
|
26a668a875 | ||
|
|
bf96262b6e | ||
|
|
1155fa1fe9 | ||
|
|
1875d31731 | ||
|
|
6f855fd14e | ||
|
|
08e392bb46 | ||
|
|
66d63e0546 | ||
|
|
7ee56fe8bc | ||
|
|
669d04ee48 | ||
|
|
cb1b37326e | ||
|
|
7bb73bee67 | ||
|
|
7286ddc338 | ||
|
|
7d1f9e33fe | ||
|
|
63c676ebfe | ||
|
|
fcaf6b7923 | ||
|
|
9f347a170a | ||
|
|
2f7cd4426d | ||
|
|
854f256470 | ||
|
|
5d0b40f752 | ||
|
|
27a2853ee8 | ||
|
|
67f6b80312 | ||
|
|
016037adc1 | ||
|
|
70d5c1034d | ||
|
|
ed6fb8754f | ||
|
|
6d08a9b11c | ||
|
|
cf6aa7cf79 | ||
|
|
6c4e57aae0 | ||
|
|
d08a04959c | ||
|
|
2762f74ce5 | ||
|
|
6ebcf6eec5 | ||
|
|
25b78fb7e1 | ||
|
|
670dd2dd96 | ||
|
|
1baf04f786 | ||
|
|
ce05b7a041 | ||
|
|
290cc146c8 | ||
|
|
db4d46a584 | ||
|
|
8ed2e51dde | ||
|
|
33702c09a6 | ||
|
|
45aeca3753 | ||
|
|
deae7dfb4d | ||
|
|
2af043ebdd | ||
|
|
e121295735 | ||
|
|
7c1c405a64 | ||
|
|
5d7c95a34d | ||
|
|
504c934fc9 | ||
|
|
81b0223f73 | ||
|
|
6d1e410bfd | ||
|
|
26c5c6152d | ||
|
|
d83bf0ebaf | ||
|
|
5adfe9a552 | ||
|
|
883f461dc7 | ||
|
|
8595ebc258 | ||
|
|
2bd31f4560 | ||
|
|
6df85ecadd | ||
|
|
07829918e4 | ||
|
|
b0d400b6eb | ||
|
|
706cb895ad | ||
|
|
45bf539b3c | ||
|
|
0923981f8d | ||
|
|
b0ac14363d | ||
|
|
5d346aba37 | ||
|
|
124cb15ebe | ||
|
|
a244ab898d | ||
|
|
3c551590eb | ||
|
|
10e30cccbc | ||
|
|
806a6b86a2 | ||
|
|
9719fdf5cc | ||
|
|
6d8764f8cb | ||
|
|
d9fd9cb408 | ||
|
|
7710ec0aba | ||
|
|
c97bd78cd0 | ||
|
|
728c5aa86e | ||
|
|
83d68ca293 | ||
|
|
47d7561db4 | ||
|
|
7d993e8319 | ||
|
|
1d1a597b22 | ||
|
|
23082f9300 | ||
|
|
ea1a7f9376 | ||
|
|
1796c7bab8 | ||
|
|
65ad76479a | ||
|
|
422db04ec8 | ||
|
|
d12f60fe37 | ||
|
|
194c1c991c | ||
|
|
3e8e2222c1 | ||
|
|
1620327a33 | ||
|
|
b1131e3667 | ||
|
|
db0250ac08 | ||
|
|
0a6b605760 | ||
|
|
81ac2375e5 | ||
|
|
6bcaca6cd7 | ||
|
|
ec8375252e | ||
|
|
766aa1762f | ||
|
|
5165e0a54c | ||
|
|
a2f7ad627e | ||
|
|
1176c162d4 | ||
|
|
a4880cd017 | ||
|
|
383bdce416 | ||
|
|
7cc300dd83 | ||
|
|
687809db9d | ||
|
|
1127ec3a90 | ||
|
|
a797edfae9 | ||
|
|
938e106252 | ||
|
|
a0eb9caaa2 | ||
|
|
442f53d45e | ||
|
|
68de1ca248 | ||
|
|
e16061141e | ||
|
|
64cbe3d209 | ||
|
|
ebdc6376d8 | ||
|
|
68335123a1 | ||
|
|
25b18f4ef8 | ||
|
|
9ad615b0ff | ||
|
|
12eaceb032 | ||
|
|
3263511b72 | ||
|
|
75cae8c647 | ||
|
|
9991ef624c | ||
|
|
489e9fce27 | ||
|
|
0655628073 | ||
|
|
9460822529 | ||
|
|
d02358ac0d | ||
|
|
366237a625 | ||
|
|
2f2da18994 | ||
|
|
ecd30db215 | ||
|
|
1980854f6f | ||
|
|
7d4f006c25 | ||
|
|
b697be2383 | ||
|
|
143e53c43f | ||
|
|
6dde1ade01 | ||
|
|
d4de391ac5 | ||
|
|
af15f4bc6a | ||
|
|
d4ace24caa | ||
|
|
c5ab47fa66 | ||
|
|
d60051b629 | ||
|
|
22ff330ee7 | ||
|
|
dd62bef66d | ||
|
|
e7feb99f8d | ||
|
|
6358accece | ||
|
|
9ce8a24eea | ||
|
|
4d0673f489 | ||
|
|
fbe1e6a7d5 | ||
|
|
4ed02ca501 | ||
|
|
af245ed9fe | ||
|
|
fc86a3e882 | ||
|
|
f9109edcf1 | ||
|
|
ec100e1a91 | ||
|
|
746c5f4a79 | ||
|
|
b2611728a1 | ||
|
|
fc6cc4234d | ||
|
|
7706c1cb63 | ||
|
|
4d7a07220c | ||
|
|
436875f7dc | ||
|
|
21c6f53ecc | ||
|
|
5472be8b72 | ||
|
|
d22542fcf3 | ||
|
|
1d8341eb27 | ||
|
|
1897a7ada3 | ||
|
|
a048698c66 | ||
|
|
f891e57f4a | ||
|
|
fcc590e48a | ||
|
|
9a02a2a65b | ||
|
|
536a6c5c60 | ||
|
|
86a6871ee8 | ||
|
|
6046045151 | ||
|
|
9c3ddd4ba4 | ||
|
|
6c9f55c1d7 | ||
|
|
5bec3d7dcd | ||
|
|
a4c309efe8 | ||
|
|
4e22a208dd | ||
|
|
ff9e77118e | ||
|
|
6c6dfa9ac4 | ||
|
|
d60d5207d8 | ||
|
|
8c0ae892f5 | ||
|
|
f570492a11 | ||
|
|
cc447304f5 | ||
|
|
8f8c6366e3 | ||
|
|
3b13bcb098 | ||
|
|
df60784b51 | ||
|
|
bae3122bb5 | ||
|
|
0770aef4bf | ||
|
|
c198350bfa | ||
|
|
a2ed388777 | ||
|
|
f6670c699a | ||
|
|
5fa4c95480 | ||
|
|
5b8608c186 | ||
|
|
bb75d42ede | ||
|
|
b4b6def07a | ||
|
|
b305700987 | ||
|
|
40232b7eb1 | ||
|
|
67ff666db4 | ||
|
|
5960fd4fe0 | ||
|
|
f8b28c519c | ||
|
|
ee1105b6dd | ||
|
|
4778274c97 | ||
|
|
d7ecb32238 | ||
|
|
744306fc50 | ||
|
|
11bbb8ed4d | ||
|
|
b5092a4444 | ||
|
|
e2442c07a9 | ||
|
|
6f924d4e83 | ||
|
|
faf1508914 | ||
|
|
7eb8152835 | ||
|
|
8f45905831 | ||
|
|
4ba2ffce5b | ||
|
|
ffb3659ef5 | ||
|
|
6dec07d562 | ||
|
|
afb22f3279 | ||
|
|
f2f369db64 | ||
|
|
635c76db93 | ||
|
|
5f50f1928d | ||
|
|
32c9ed1dc2 | ||
|
|
0536926a1f | ||
|
|
3959c98479 | ||
|
|
2ce5735676 | ||
|
|
71741e144e | ||
|
|
f2e64cdd5e | ||
|
|
afaa5d5e9e | ||
|
|
d82861727d | ||
|
|
90f0795416 | ||
|
|
9efbe7771c | ||
|
|
a75caac13d | ||
|
|
279d0d20ea | ||
|
|
332ba96d34 | ||
|
|
3f6e5b7606 | ||
|
|
94703492fd | ||
|
|
df78117617 | ||
|
|
909b9480e4 | ||
|
|
21762ac1aa | ||
|
|
412bc175b4 | ||
|
|
dc0eb76e88 | ||
|
|
2695941a3c | ||
|
|
3506be1a70 | ||
|
|
cbf4786b39 | ||
|
|
8dbf334208 | ||
|
|
60ba5fbc72 | ||
|
|
c3ace0c44f | ||
|
|
8eceb90e63 | ||
|
|
4754e19e83 | ||
|
|
a0559d0dc9 | ||
|
|
8d03ce19b0 | ||
|
|
2470d851e9 | ||
|
|
df99f5c0a5 | ||
|
|
36f5084c52 | ||
|
|
b77d3f123d | ||
|
|
3c14405155 | ||
|
|
c10b0afaa8 | ||
|
|
3f7a2d6bfb | ||
|
|
f2a0d56e01 | ||
|
|
0736cc8414 | ||
|
|
53240d40a0 | ||
|
|
4137dd7cc8 | ||
|
|
8907290d27 | ||
|
|
401dc1cb10 | ||
|
|
a6b5d26f56 | ||
|
|
eb55cba34a | ||
|
|
b0b41d984e | ||
|
|
947f64e32d | ||
|
|
24ccbfa9b6 | ||
|
|
8110acb795 | ||
|
|
7473aede60 | ||
|
|
6f204f721b | ||
|
|
7b0e08094a | ||
|
|
322e1a75b9 | ||
|
|
a0806d98a1 | ||
|
|
182897d971 | ||
|
|
f90a639fcc | ||
|
|
d95d7208a2 | ||
|
|
bbac8ffe64 | ||
|
|
801a649fb1 | ||
|
|
7c09ceecfd | ||
|
|
8c4954fb9b | ||
|
|
fbe892105b | ||
|
|
584e1bb847 | ||
|
|
c437ab282e | ||
|
|
42a100d138 | ||
|
|
65807bf35d | ||
|
|
2995f48ede | ||
|
|
d452702aef | ||
|
|
f8ed9c7357 | ||
|
|
9143120177 | ||
|
|
fd3b1bee92 | ||
|
|
bff42954d1 | ||
|
|
6b74fd6a02 | ||
|
|
0547cebfe2 | ||
|
|
caefdf4300 | ||
|
|
a59df15994 | ||
|
|
33304eb8d9 | ||
|
|
eb21a1bfb3 | ||
|
|
ce0333b0a7 | ||
|
|
25a1b53a91 | ||
|
|
6aba73f970 | ||
|
|
6406bb7fb6 | ||
|
|
2458107903 | ||
|
|
628f9bd9b5 | ||
|
|
2d791571d5 | ||
|
|
ed57127a79 | ||
|
|
6d9bcff8e1 | ||
|
|
8126cd1879 | ||
|
|
ab34c2261c | ||
|
|
6953f65970 | ||
|
|
52073e79fa | ||
|
|
8e3484bb17 | ||
|
|
7110da8a36 | ||
|
|
7ffd6ded51 | ||
|
|
5e04547ecf | ||
|
|
7cbe5f64ce | ||
|
|
47ed78031a | ||
|
|
fd3d24b834 | ||
|
|
c2f607b452 | ||
|
|
b1328bb6e2 | ||
|
|
2a02583e27 | ||
|
|
cfb49f9136 | ||
|
|
5f20091f24 | ||
|
|
a37b8e35a1 | ||
|
|
84c980c3ea | ||
|
|
5823057b41 | ||
|
|
024a9b1763 | ||
|
|
0425780cd3 | ||
|
|
20734dc7f3 | ||
|
|
0574c58f16 | ||
|
|
31e3117190 | ||
|
|
f780ba2c5a | ||
|
|
f125b593bf | ||
|
|
baee4e185d | ||
|
|
ca8476d466 | ||
|
|
757bf82a78 | ||
|
|
644887f727 | ||
|
|
563b643461 | ||
|
|
0e4a6fd2e1 | ||
|
|
d452bf1f1c | ||
|
|
126828a813 | ||
|
|
03dc6fb73a | ||
|
|
3653e89714 | ||
|
|
318dd14e42 | ||
|
|
2d13844b5d | ||
|
|
b777b7432a | ||
|
|
6f91c225c2 | ||
|
|
c355e9ca4a | ||
|
|
4514ea8123 | ||
|
|
a9a1472cb7 | ||
|
|
250b0eee28 | ||
|
|
5cd7527937 | ||
|
|
57ebd5bb7a | ||
|
|
c18a697d6b | ||
|
|
ad40b72508 | ||
|
|
3a72e5910d | ||
|
|
8f3eb65a05 | ||
|
|
700cd83ff5 | ||
|
|
0c27881eaf | ||
|
|
f7fdc6acd2 | ||
|
|
2c5f844edc | ||
|
|
a253de43c5 | ||
|
|
dbaf67a986 | ||
|
|
5175d06e37 | ||
|
|
651a27b558 | ||
|
|
fd41f8def6 | ||
|
|
208ea04fd5 | ||
|
|
5ee83a1af9 | ||
|
|
901c7704f4 | ||
|
|
c6f7290f92 | ||
|
|
5368eda98b | ||
|
|
7b9be209c8 | ||
|
|
cee05fea7c | ||
|
|
b87acb47e2 | ||
|
|
cb192bf9ea | ||
|
|
16351ba7f3 | ||
|
|
96ba128942 | ||
|
|
76c8d4af25 | ||
|
|
3ea2b3cc00 | ||
|
|
0df0936022 | ||
|
|
4fc11a7fd3 | ||
|
|
8c509271a6 | ||
|
|
67d5b63ef9 | ||
|
|
4f999511a6 | ||
|
|
cfbf239175 | ||
|
|
1aedf9bb07 | ||
|
|
c2d4423720 | ||
|
|
c2f7a2620c | ||
|
|
6f0cda34b4 | ||
|
|
1a472fdf1f | ||
|
|
0551f61228 | ||
|
|
b900adfddd | ||
|
|
0e339daef5 | ||
|
|
5f5733e8b9 | ||
|
|
1372050a7b | ||
|
|
1960951c5e | ||
|
|
bc070407c7 | ||
|
|
0172ee0b6b | ||
|
|
2953bc6bb8 | ||
|
|
c0ed3a9e3c | ||
|
|
5456e4fe75 | ||
|
|
867a1e960e | ||
|
|
48dc68c3fe | ||
|
|
2c719f326b | ||
|
|
201f92eb93 | ||
|
|
46f055b216 | ||
|
|
08305e679b | ||
|
|
e9e0b70199 | ||
|
|
441d784027 | ||
|
|
558956bf55 | ||
|
|
0d8250a3ac | ||
|
|
dc1f5826f8 | ||
|
|
06fb06829b | ||
|
|
bbf52d2611 | ||
|
|
f55a6617e9 | ||
|
|
3bd273fbdd | ||
|
|
1b64a543f1 | ||
|
|
97b07f7d42 | ||
|
|
ebb472b8f6 | ||
|
|
1a2ef6479e | ||
|
|
876c63fe80 | ||
|
|
32236962f5 | ||
|
|
ba66af922f | ||
|
|
28b9bbe54f | ||
|
|
bf6bd7cbdc | ||
|
|
ddcb2468a6 | ||
|
|
f048b58935 | ||
|
|
09f6200c2e | ||
|
|
354692fb06 | ||
|
|
2c5858c2e0 | ||
|
|
1f41fd0436 | ||
|
|
e0bbefdfae | ||
|
|
bc6fbb2797 | ||
|
|
b579cf8128 | ||
|
|
a94ed014b7 | ||
|
|
0db991b668 | ||
|
|
347ced6942 | ||
|
|
5d7a775b2b | ||
|
|
df732ab4bf | ||
|
|
31cd3953d6 | ||
|
|
4666b13892 | ||
|
|
37d7ddb4b0 | ||
|
|
3abbaeb44b | ||
|
|
847ea49042 | ||
|
|
779068c2ee | ||
|
|
140cd6882d | ||
|
|
2e295c2391 | ||
|
|
596b0995f4 | ||
|
|
b61fe97893 | ||
|
|
209aa2ebe6 | ||
|
|
c03a0e857e | ||
|
|
2854d698e6 | ||
|
|
944163be0e | ||
|
|
269a9eaff0 | ||
|
|
7f9c49cbc4 | ||
|
|
2b6bfeeb15 | ||
|
|
fa9742bc92 | ||
|
|
472e419abc | ||
|
|
169d11387b | ||
|
|
e59ac6d947 | ||
|
|
e193b45562 | ||
|
|
1ac34f810e | ||
|
|
e65e5c6e5f | ||
|
|
af6365c76a | ||
|
|
8c672b23b5 | ||
|
|
3b53f5ac11 | ||
|
|
ccdc744748 | ||
|
|
261f0b4bf0 | ||
|
|
495b59c2e5 | ||
|
|
d6e1c13c39 | ||
|
|
f7f13b9e07 | ||
|
|
62564d747f | ||
|
|
1d5d59c4c0 | ||
|
|
e8380b8a12 | ||
|
|
c304d9cc62 | ||
|
|
aad4ebf287 | ||
|
|
6c2f48181c | ||
|
|
480b6ff7d6 | ||
|
|
4bdd6f972c | ||
|
|
6674d8456a | ||
|
|
d1478ff694 | ||
|
|
cb20d46b74 | ||
|
|
9dd2538043 | ||
|
|
f25136a512 | ||
|
|
03eb56ad2a | ||
|
|
2508840701 | ||
|
|
6e098f5a4f | ||
|
|
31b33406fd | ||
|
|
7ab7c763ed | ||
|
|
06258e757a | ||
|
|
5919b43a21 | ||
|
|
7d4b9521e7 | ||
|
|
f9d2fd7997 | ||
|
|
bdbc20c3c6 | ||
|
|
69d6d03714 | ||
|
|
f40e1e55b0 | ||
|
|
b9082ed838 | ||
|
|
4edfefa9a2 | ||
|
|
0f98ee5407 | ||
|
|
7fdf119873 | ||
|
|
3c054e6ea1 | ||
|
|
98899ece72 | ||
|
|
2061a006d0 | ||
|
|
5496c2d7fd | ||
|
|
d6b862a4a9 | ||
|
|
d581f8a852 | ||
|
|
3a593ee35a | ||
|
|
415d34fdaa | ||
|
|
7d28191bb5 | ||
|
|
e2c7693370 | ||
|
|
f17ff02f4d | ||
|
|
225043e01d | ||
|
|
56f78349f8 | ||
|
|
8839d4f55a | ||
|
|
2562aec1d2 | ||
|
|
db2361be84 | ||
|
|
d08fcbfef3 | ||
|
|
7601b93e65 | ||
|
|
1103ec40e0 | ||
|
|
af32c7e3db | ||
|
|
170918eb3b | ||
|
|
a91e615a8d | ||
|
|
cc92c26fe3 | ||
|
|
937135db32 | ||
|
|
01366558b4 | ||
|
|
b0dbd54ea4 | ||
|
|
f113915307 | ||
|
|
0a3c3d9bb1 | ||
|
|
ba2e005c3e | ||
|
|
b9ea52d65f | ||
|
|
f1e328d333 | ||
|
|
23f1965398 | ||
|
|
fc82f185cb | ||
|
|
56b25f373f | ||
|
|
1ac6add122 | ||
|
|
91b1a903f9 | ||
|
|
a8d6654ef5 | ||
|
|
63093b1be6 | ||
|
|
60abe8f37e | ||
|
|
7ba3909aa1 | ||
|
|
eecdba2d05 | ||
|
|
7bdc005aa7 | ||
|
|
d46703fdc4 | ||
|
|
e4aa85f603 | ||
|
|
233124ef50 | ||
|
|
36a3012de2 | ||
|
|
2b4ee96ed7 | ||
|
|
85a2afd55c | ||
|
|
6cd0d8456a | ||
|
|
7750a0a773 | ||
|
|
a5364b9257 | ||
|
|
e0e7b1406d | ||
|
|
38b67a0002 | ||
|
|
18dd4f8a52 | ||
|
|
879f9b4ea9 | ||
|
|
3db0dda67a | ||
|
|
ed9ee15b90 | ||
|
|
44ff85e8e9 | ||
|
|
cb07e9ba11 | ||
|
|
bfe05ce5fc | ||
|
|
64ee23cec0 | ||
|
|
c022d3c4a4 | ||
|
|
69c764301f | ||
|
|
2f777daef6 | ||
|
|
a10bf7330e | ||
|
|
782bb5967d | ||
|
|
aeb2f55f0d | ||
|
|
ae68749b1b | ||
|
|
a3c25d56a0 | ||
|
|
d2b9cc8df9 | ||
|
|
2027daa75c | ||
|
|
f3493a08bd | ||
|
|
f3963269ae | ||
|
|
ae2212c245 | ||
|
|
3a19ac4c93 | ||
|
|
666f546cf0 | ||
|
|
464f57d7da | ||
|
|
2a192f33a1 | ||
|
|
9ca2674261 | ||
|
|
24bc91dc0c | ||
|
|
cf2d5b502f | ||
|
|
61450ef602 | ||
|
|
78f1d1738e | ||
|
|
9f595cf5f7 | ||
|
|
25b8e49975 | ||
|
|
d40086cd13 | ||
|
|
8f9d8d93b9 | ||
|
|
1080c10004 | ||
|
|
866aeca220 | ||
|
|
121b3afc61 | ||
|
|
e8406b04b4 | ||
|
|
8e7fe81806 | ||
|
|
852807b5d7 | ||
|
|
9928d47fa2 | ||
|
|
412a1bd62a | ||
|
|
b290520951 | ||
|
|
dde677b6d3 | ||
|
|
75030847bd | ||
|
|
e7b9cb76bc | ||
|
|
e96c4c0dd3 | ||
|
|
b553b3fa50 | ||
|
|
c6fb924b8f | ||
|
|
b13844c4a6 | ||
|
|
ab6c83a3fc | ||
|
|
7e0a97973f | ||
|
|
6a8a79bba5 | ||
|
|
4a0c58c50a | ||
|
|
eb0c963332 | ||
|
|
023fe82932 | ||
|
|
2e1a06c7bf | ||
|
|
8b6961d40c | ||
|
|
53351877da | ||
|
|
522445894e | ||
|
|
550351bb16 | ||
|
|
328adbb56f | ||
|
|
44a36bbba3 | ||
|
|
4cc4adeda6 | ||
|
|
c14e6f2a63 | ||
|
|
cfcb2d54d8 | ||
|
|
010d60e504 | ||
|
|
eabde07ff6 | ||
|
|
be19ad5f2a | ||
|
|
d1d0816961 | ||
|
|
7be7170504 | ||
|
|
478272f515 | ||
|
|
09af6958c8 | ||
|
|
adb3a2b64e | ||
|
|
1ee7e842dc | ||
|
|
b080a57406 | ||
|
|
7c017e345a | ||
|
|
4b91343155 | ||
|
|
02a3df8ad0 | ||
|
|
6a7080f4ee | ||
|
|
4547042577 | ||
|
|
0e39eea7f8 | ||
|
|
1e5aefea63 | ||
|
|
02c4f333b0 | ||
|
|
1e8fc4020b | ||
|
|
f969701ac1 | ||
|
|
b236243857 | ||
|
|
39edc64922 | ||
|
|
f22ece403f | ||
|
|
f5423bb314 | ||
|
|
b1e5945ebe | ||
|
|
76b5be8171 | ||
|
|
804bca2041 | ||
|
|
10602b47b4 | ||
|
|
8d7c522596 | ||
|
|
3ac455c5a7 | ||
|
|
2b19a459df | ||
|
|
41ba2d9bf6 | ||
|
|
a7b5eb69d3 | ||
|
|
67c209bb5e | ||
|
|
a6d436d9ea | ||
|
|
652c784e13 | ||
|
|
a0a3b7a158 | ||
|
|
789f51bd2a | ||
|
|
c2f1a74f96 | ||
|
|
a9ed7a3f3b | ||
|
|
b348e88a5f | ||
|
|
1615395866 | ||
|
|
e483abcad0 | ||
|
|
12b6760f6e | ||
|
|
6fde6d7eac | ||
|
|
a7ef891217 | ||
|
|
8f22dfe87b | ||
|
|
2dc7fab39a | ||
|
|
74cb2e3c63 | ||
|
|
6e763a58f1 | ||
|
|
a8e72ed410 | ||
|
|
fcdfd5f936 | ||
|
|
f1faa463c1 | ||
|
|
a0f4952b54 | ||
|
|
bd82ded07d | ||
|
|
016e17dedb | ||
|
|
5cd3e1b368 | ||
|
|
b2b39458da | ||
|
|
556bbe394d | ||
|
|
07288b3f26 | ||
|
|
90f79b7708 | ||
|
|
e220786a20 | ||
|
|
f16b993294 | ||
|
|
c241bea3bf | ||
|
|
084654cd3c | ||
|
|
d21742afb6 | ||
|
|
b5259384e8 | ||
|
|
bf78ad9fbe | ||
|
|
ab3577c369 | ||
|
|
6efb90c94e | ||
|
|
cbcc400eb4 | ||
|
|
15aec7da7e | ||
|
|
46535e4f56 | ||
|
|
e3f945c079 | ||
|
|
04239c57fe | ||
|
|
ad4439ed55 | ||
|
|
9fe3ef430f | ||
|
|
ff30773097 | ||
|
|
f7531d1e18 | ||
|
|
658008ab64 | ||
|
|
b089d63112 | ||
|
|
ee9b1b7f57 | ||
|
|
cd0fc8176f | ||
|
|
8e291e3e46 | ||
|
|
e3024076cd | ||
|
|
6105874abc | ||
|
|
1855f7829d | ||
|
|
456e8bd9c0 | ||
|
|
d5f2efac26 | ||
|
|
21e692623c | ||
|
|
80e9589af5 | ||
|
|
b2b9ae0677 | ||
|
|
63122905e6 | ||
|
|
f99b6f4646 | ||
|
|
39090c2a22 | ||
|
|
76baa8c791 | ||
|
|
74e4b9d6d2 | ||
|
|
bbfc5039f7 | ||
|
|
b2fd694483 | ||
|
|
b03f38ff22 | ||
|
|
fe48811047 | ||
|
|
bd9396b031 | ||
|
|
f0497ec16d | ||
|
|
7e9e179fa7 | ||
|
|
de62464ad8 | ||
|
|
f6911ca195 | ||
|
|
aec09ed8d2 | ||
|
|
51a983e460 | ||
|
|
0eb46e29c7 | ||
|
|
5ee11c7b6b | ||
|
|
b55accd76f | ||
|
|
fef2be1bc7 | ||
|
|
0b3858f91d | ||
|
|
d07ea1b337 | ||
|
|
7e2dbc7358 | ||
|
|
c676f08a7c | ||
|
|
92f24b5728 | ||
|
|
0254e71435 | ||
|
|
2972fc5814 | ||
|
|
975c96217c | ||
|
|
c30c1848bc | ||
|
|
94615d3b36 | ||
|
|
37a00f0d16 | ||
|
|
0dbe70f5af | ||
|
|
7584374b0b | ||
|
|
71ca51dc1a | ||
|
|
aa81e72e45 | ||
|
|
9954bb9c15 | ||
|
|
e5c0250423 | ||
|
|
135799ed5e | ||
|
|
22c3b57960 | ||
|
|
7054dd74a4 | ||
|
|
d4f1e52ef6 | ||
|
|
76a44459cf | ||
|
|
a5590b090c | ||
|
|
74c9a57070 | ||
|
|
06e283e070 | ||
|
|
8ab2ca3f24 | ||
|
|
0eb949ba39 | ||
|
|
be35693814 | ||
|
|
1e5f13795c | ||
|
|
cca2265633 | ||
|
|
0f0d1ac370 | ||
|
|
d52d4ac183 | ||
|
|
841220fd01 | ||
|
|
ca5e10784b | ||
|
|
712319974b | ||
|
|
067a6d01bc | ||
|
|
27825f9e2e | ||
|
|
425eb115dc | ||
|
|
0a5ce55e2b | ||
|
|
dbc8ed9d4c | ||
|
|
e31e990684 | ||
|
|
8618f56481 | ||
|
|
a39fc4667e | ||
|
|
4c369e240b | ||
|
|
4e291d01d4 | ||
|
|
b32fce46cb | ||
|
|
d1fcd45aac | ||
|
|
ebdb92c708 | ||
|
|
112909d35b | ||
|
|
c6e2c559f1 | ||
|
|
cf8613886a | ||
|
|
39c25c2001 | ||
|
|
9c9721ade5 | ||
|
|
4b8abe4ce8 | ||
|
|
33dfaba276 | ||
|
|
dd8bbcf358 | ||
|
|
cc3e4369ed | ||
|
|
548355fce6 | ||
|
|
a4e0e6544b | ||
|
|
62067e0801 | ||
|
|
5eb40d2299 | ||
|
|
53990a531b | ||
|
|
d799aea3c4 | ||
|
|
9af86cbba2 | ||
|
|
6fbfece4ff | ||
|
|
d56cca7873 | ||
|
|
fa1096a6ba | ||
|
|
16ff721331 | ||
|
|
798ee9dc46 | ||
|
|
bc17c60305 | ||
|
|
432688d577 | ||
|
|
da8d4b47f1 | ||
|
|
ed1e2e2449 | ||
|
|
b9744b4688 | ||
|
|
2f328f8f37 | ||
|
|
ffefd7e50b | ||
|
|
c2445f8a7c | ||
|
|
54d44079cc | ||
|
|
87f089a12c | ||
|
|
aa2172e4db | ||
|
|
0a33e94e79 | ||
|
|
fada54abae | ||
|
|
802641b719 | ||
|
|
1da93829d4 | ||
|
|
9e7acbc49a | ||
|
|
318765d40b | ||
|
|
94ba20dfa1 | ||
|
|
2ad3dc4a32 | ||
|
|
eef940dd7c | ||
|
|
1b5fc12ac1 | ||
|
|
c1c7b8dfcd | ||
|
|
d4510c2afe | ||
|
|
f241f073a3 | ||
|
|
26a6c72611 | ||
|
|
51cee7804b | ||
|
|
52228430f1 | ||
|
|
4fcd45d8a4 | ||
|
|
ea5736947d | ||
|
|
6b5f36fb7e | ||
|
|
b328d6d95f | ||
|
|
2e06921bf8 | ||
|
|
fd95e2d711 | ||
|
|
490e253b79 | ||
|
|
8a10f5cd52 | ||
|
|
5cebcc2424 | ||
|
|
7663b89289 | ||
|
|
d0d1f9e3c0 | ||
|
|
4433760e37 | ||
|
|
b63fcd2254 | ||
|
|
eb2aa352ef | ||
|
|
ec95f198ec | ||
|
|
f166e480f1 | ||
|
|
2051f0486c | ||
|
|
f33fc5d730 | ||
|
|
1603df3503 | ||
|
|
6055ac182b | ||
|
|
4678f60adf | ||
|
|
b48521950e | ||
|
|
45da6fb39f | ||
|
|
1604d327da | ||
|
|
98b2b325a1 | ||
|
|
55b3def630 | ||
|
|
1b5f6bd3eb | ||
|
|
3472b85345 | ||
|
|
1c6ae53656 | ||
|
|
a84a56f611 | ||
|
|
69751aa415 | ||
|
|
9ef0337807 | ||
|
|
a3fdd274c3 | ||
|
|
f069c02153 | ||
|
|
ad0d14c517 | ||
|
|
a72da79743 | ||
|
|
6bc35217d9 | ||
|
|
c526e9ac8f | ||
|
|
3dfebcadc1 | ||
|
|
693af532a2 | ||
|
|
4bac57fcd1 | ||
|
|
59291fcac2 | ||
|
|
1f21b202a5 | ||
|
|
b3ca02a166 | ||
|
|
2462f8f87e | ||
|
|
18268acc1f | ||
|
|
44f4423a9d | ||
|
|
37250a6f48 | ||
|
|
fd97541417 | ||
|
|
dba3e30d02 | ||
|
|
5962283ad3 | ||
|
|
cdd705831b | ||
|
|
a1a7c5e4bb | ||
|
|
c1e9061568 | ||
|
|
90ee04de57 | ||
|
|
cd24cfbe5c | ||
|
|
5b05668a30 | ||
|
|
a2eca9589f | ||
|
|
a2adbb19bd | ||
|
|
012e5c09ed | ||
|
|
5f33aa7fd4 | ||
|
|
7ea2204dfd | ||
|
|
be8cadf5ac | ||
|
|
fc7192c748 | ||
|
|
5c6b6b3919 | ||
|
|
87553ac685 | ||
|
|
384f396bbb | ||
|
|
d40560bd49 | ||
|
|
67952ca246 | ||
|
|
4ddbefb147 | ||
|
|
edabc17ac9 | ||
|
|
c7e8211fae | ||
|
|
dc3f07dd52 | ||
|
|
fae6a5cfb4 | ||
|
|
cafd225797 | ||
|
|
bf77d2973e | ||
|
|
dfd8133431 | ||
|
|
e9d78b8990 | ||
|
|
13532cdffb | ||
|
|
c17620efb7 | ||
|
|
d3b5b1080f | ||
|
|
e4b0c23bb9 | ||
|
|
6c24fb6d79 | ||
|
|
bd93551c13 | ||
|
|
dc0799a56b | ||
|
|
e1fca1520b | ||
|
|
c72f3b8efb | ||
|
|
afcd172a94 | ||
|
|
5957700699 | ||
|
|
451d023e81 | ||
|
|
64d166cebf | ||
|
|
51b067def7 | ||
|
|
de25e8dd6f | ||
|
|
39ac291ba2 | ||
|
|
722d26c5b9 | ||
|
|
0346760a75 | ||
|
|
a2015143f8 | ||
|
|
071d99a7b4 | ||
|
|
e6d8e063c9 | ||
|
|
29526144c9 | ||
|
|
b88bcafddd | ||
|
|
78d844d693 | ||
|
|
49310b53e9 | ||
|
|
4684487ff8 | ||
|
|
89250205dc | ||
|
|
9039919b03 | ||
|
|
df16803f11 | ||
|
|
48327cf5f8 | ||
|
|
2ead125b9d | ||
|
|
23807f11f9 | ||
|
|
cad666d07f | ||
|
|
a0b11051e1 | ||
|
|
b6e6fb9de2 | ||
|
|
4172fd8aff | ||
|
|
d60df6ca69 | ||
|
|
f9ba7377eb | ||
|
|
5d3c47b8ae | ||
|
|
ef9cc860a9 | ||
|
|
5a1ec8aa74 | ||
|
|
ac435f3699 | ||
|
|
0374dec79c | ||
|
|
41b6bf38c4 | ||
|
|
ddba251449 | ||
|
|
bf76e47648 | ||
|
|
5a3ad35bdd | ||
|
|
7da18274ad | ||
|
|
9b66ae05eb | ||
|
|
d407964625 | ||
|
|
bc254d5a8d | ||
|
|
1d9128a650 | ||
|
|
a1f2e9e4e2 | ||
|
|
8c8188c24f | ||
|
|
e42eaf73b8 | ||
|
|
fbd8552c0b | ||
|
|
daa26b2e1e | ||
|
|
18473e6819 | ||
|
|
a51d6b9a74 | ||
|
|
706871c12a | ||
|
|
16c49e965c | ||
|
|
f29e867441 | ||
|
|
4a5f6d8393 | ||
|
|
39e3025077 | ||
|
|
9ee293816d | ||
|
|
b3c1397753 | ||
|
|
93d63538bb | ||
|
|
09c28d2311 | ||
|
|
5b8ed496db | ||
|
|
27b4cf743e | ||
|
|
95938a65b6 | ||
|
|
70780fc4ef | ||
|
|
21d265d12f | ||
|
|
788526d6eb | ||
|
|
a7203e9a17 | ||
|
|
eff50daae4 | ||
|
|
7612db15e0 | ||
|
|
0731ee473d | ||
|
|
3a6a76fe19 | ||
|
|
edb0509194 | ||
|
|
1806be1fd7 | ||
|
|
7769f25869 | ||
|
|
f6e5743f20 | ||
|
|
1ef6196de6 | ||
|
|
75ceee23ec | ||
|
|
38f2372ee8 | ||
|
|
b58826da6e | ||
|
|
2c05e606c7 | ||
|
|
92f1c080c4 | ||
|
|
8f79699e84 | ||
|
|
e7b3e28f76 | ||
|
|
123677b6c6 | ||
|
|
f393c7173c | ||
|
|
0abfb9f1e4 | ||
|
|
379771a5c1 | ||
|
|
2f4a76b3fa | ||
|
|
7d82e199b7 | ||
|
|
de693a08ad | ||
|
|
923091ca62 | ||
|
|
cb65ddedb6 | ||
|
|
7aa75539c9 | ||
|
|
a1a7cf59b3 | ||
|
|
f572cb5f3e | ||
|
|
8f1a31ad13 | ||
|
|
dcf63e3da2 | ||
|
|
153dca8137 | ||
|
|
db97787b15 | ||
|
|
7148fec6e1 | ||
|
|
db6d48f8ca | ||
|
|
6d87a1a604 | ||
|
|
5c414fc7b4 | ||
|
|
127f4446ae | ||
|
|
49a49e2a2c | ||
|
|
8b9389b468 | ||
|
|
ee17336d64 | ||
|
|
2b768d2fb5 | ||
|
|
4fc63010d4 | ||
|
|
3311ded843 | ||
|
|
135c2e7a71 | ||
|
|
a3a8f600b6 | ||
|
|
23643b99e2 | ||
|
|
764a334952 | ||
|
|
6f163f2d01 | ||
|
|
aae65f8235 | ||
|
|
764ea571eb | ||
|
|
aef719b574 | ||
|
|
73e52e625b | ||
|
|
a967588fe1 | ||
|
|
72e530f062 | ||
|
|
a0b422f99e | ||
|
|
6b92f30324 | ||
|
|
464cfe701c | ||
|
|
070fa03108 | ||
|
|
51abeabb33 | ||
|
|
74b0697da1 | ||
|
|
0ca409bb22 | ||
|
|
527cf25d1b | ||
|
|
a49594e6a5 | ||
|
|
5cd22b41d6 | ||
|
|
934949c514 | ||
|
|
cc61e8d334 | ||
|
|
81be499c49 | ||
|
|
082aa55566 | ||
|
|
c783039557 | ||
|
|
206dfeb879 | ||
|
|
f839e76f4b | ||
|
|
c67151f922 | ||
|
|
aedec5de18 | ||
|
|
3df973a1ea | ||
|
|
0253031d7f | ||
|
|
f4445d4681 | ||
|
|
e399dfa7e6 | ||
|
|
787f95ac3a | ||
|
|
d2f35d46d2 | ||
|
|
b21d078c5d | ||
|
|
032c3fb856 | ||
|
|
bbd79379ce | ||
|
|
d01d544a0a | ||
|
|
01ecd76976 | ||
|
|
26e8ae4bf3 | ||
|
|
3befdbc93d | ||
|
|
c91a890d42 | ||
|
|
3369ab601a | ||
|
|
bfe5b71f19 | ||
|
|
eb25cf65dd | ||
|
|
aa1ca3be64 | ||
|
|
a4e03daeee | ||
|
|
cbd0b9db1d | ||
|
|
621e8e89a5 | ||
|
|
c9eca5ec7e | ||
|
|
05063b76eb | ||
|
|
7d1d34d1eb | ||
|
|
40423a0547 | ||
|
|
682804b1ad | ||
|
|
790e67866c | ||
|
|
8399edb4dc | ||
|
|
55a1c27d6d | ||
|
|
35bf7dc484 | ||
|
|
4d3dfa1dca | ||
|
|
ac4686125f | ||
|
|
fb2ca3bb19 | ||
|
|
c0122aace7 | ||
|
|
c141e92cc4 | ||
|
|
5236441be0 | ||
|
|
94620748ab | ||
|
|
da70c03845 | ||
|
|
337eb0f27b | ||
|
|
26a63c4baf | ||
|
|
8b03890f2a | ||
|
|
61731e2c2e | ||
|
|
5c1611c484 | ||
|
|
a502965d19 | ||
|
|
b55764db56 | ||
|
|
510897f672 | ||
|
|
2689fd17d0 | ||
|
|
75e3949cec | ||
|
|
a7bb4b7104 | ||
|
|
9bcb2ac094 | ||
|
|
9ab110277a | ||
|
|
d1506bcdae | ||
|
|
00c38c96cd | ||
|
|
9798d4ff6a | ||
|
|
5801b29ede | ||
|
|
22ed022787 | ||
|
|
f7e7ecf5ae | ||
|
|
c116d3f453 | ||
|
|
d9b3d263ae | ||
|
|
9a265a0437 | ||
|
|
16450e2133 | ||
|
|
5678742810 | ||
|
|
76d551a238 | ||
|
|
5467c4b1b8 | ||
|
|
e48d277440 | ||
|
|
9d1da81557 | ||
|
|
91f557ac9e | ||
|
|
452826bd61 | ||
|
|
84564bb7fb | ||
|
|
bf7647c737 | ||
|
|
1f98d7e5ec | ||
|
|
e4486f4c17 | ||
|
|
65daa23a74 | ||
|
|
523a30afb4 | ||
|
|
1d0de4584e | ||
|
|
7aac124407 | ||
|
|
03e8b664ac | ||
|
|
66883ae37c | ||
|
|
2a075d929a | ||
|
|
41147483d8 | ||
|
|
ca517784ed | ||
|
|
4dd3be1568 | ||
|
|
7412d97bf3 | ||
|
|
ab7b2da83b | ||
|
|
19e26729a8 | ||
|
|
0101365ebc | ||
|
|
1f56e63f9c | ||
|
|
883a30c7ad | ||
|
|
6e151a9f8b | ||
|
|
321bb299b1 | ||
|
|
2ca18340c7 | ||
|
|
74d4237913 | ||
|
|
a4f9b9208d | ||
|
|
8fd65b7365 | ||
|
|
4f4d0bf6aa | ||
|
|
873e2aed94 | ||
|
|
89d485e188 | ||
|
|
2e9870014f | ||
|
|
528529c0d1 | ||
|
|
9bddec2dfd | ||
|
|
f986487df9 | ||
|
|
99461a70e6 | ||
|
|
adbbb15a92 | ||
|
|
d85a4c9ad4 | ||
|
|
41baea780a | ||
|
|
a165884bcb | ||
|
|
456adc5d0b | ||
|
|
cfc42906b9 | ||
|
|
738d657c8e | ||
|
|
a51452ee7c | ||
|
|
5d2a41082a | ||
|
|
f9dd00b79b | ||
|
|
898244d04d | ||
|
|
33334830cc | ||
|
|
8503350bfd | ||
|
|
a4bb2aaf12 | ||
|
|
da443045bf | ||
|
|
b9927cd48d | ||
|
|
7af3f7e881 | ||
|
|
ee81febc89 | ||
|
|
8146bee846 | ||
|
|
53e94378ae | ||
|
|
8592ead0e3 | ||
|
|
67699372f2 | ||
|
|
95a8ced558 | ||
|
|
ae437be6e7 | ||
|
|
1441d9f4ee | ||
|
|
adeb5c2344 | ||
|
|
f0b0277b9d | ||
|
|
6fb5fb63e7 | ||
|
|
5330cc5ae9 | ||
|
|
bcc2244fdb | ||
|
|
ad2de95f32 | ||
|
|
453dee33ba | ||
|
|
13f36b3f79 | ||
|
|
719b63ee02 | ||
|
|
38a5698f90 | ||
|
|
a05b60f48e | ||
|
|
8694ecd417 | ||
|
|
100d007271 | ||
|
|
a1a764d807 | ||
|
|
6cb30adf5d | ||
|
|
9eb939e38f | ||
|
|
3e26060979 | ||
|
|
cc60aa7b84 | ||
|
|
bdfdafaec0 | ||
|
|
7737dc6b6c | ||
|
|
1a89465201 | ||
|
|
fe3ce45b8e | ||
|
|
7af0883f08 | ||
|
|
f48c21b124 | ||
|
|
abc2e74f2c | ||
|
|
6130c49b83 | ||
|
|
433b58511c | ||
|
|
b04111c79b | ||
|
|
06ca0079b3 | ||
|
|
ff53c6b49d | ||
|
|
da58458fb7 | ||
|
|
1a21989ad1 | ||
|
|
76d54b8914 | ||
|
|
d22d64f68c | ||
|
|
580ae005f4 | ||
|
|
75ab9d2e8c | ||
|
|
6c246768e9 | ||
|
|
bc75bc199b | ||
|
|
f234b6a540 | ||
|
|
bff8bfea7b | ||
|
|
48bf0d1942 | ||
|
|
04bbb84845 | ||
|
|
311f8cd00f | ||
|
|
ed0ab78048 | ||
|
|
0eec1c1f61 | ||
|
|
b4a3b832dc | ||
|
|
8e7830dd7d | ||
|
|
d32a18d965 | ||
|
|
7d00d47cb6 | ||
|
|
b0853eb119 | ||
|
|
25d29fb389 | ||
|
|
ed241ede9d | ||
|
|
04a27d5778 | ||
|
|
70a2067a06 | ||
|
|
4a13c01817 | ||
|
|
151c2b573c | ||
|
|
4cdb3f4c6a | ||
|
|
3cf0384bc5 | ||
|
|
ad2f6ebe93 | ||
|
|
178a429f26 | ||
|
|
71194d5b4e | ||
|
|
771c530b85 | ||
|
|
78a6b622fb | ||
|
|
0177bbebe0 | ||
|
|
8deed4a9cd | ||
|
|
60b2576ce8 | ||
|
|
90cc58a8fe | ||
|
|
d79f750e30 | ||
|
|
9f9ab01508 | ||
|
|
4dc89c9082 | ||
|
|
500349a8bd | ||
|
|
e299f3e510 | ||
|
|
6114f4644f | ||
|
|
eb664404e1 | ||
|
|
370f645cf0 | ||
|
|
bd5b18a163 | ||
|
|
c51b0c6a41 | ||
|
|
d56f6e75f9 | ||
|
|
c743348872 | ||
|
|
578049bfb6 | ||
|
|
564cd37628 | ||
|
|
60ecf91935 | ||
|
|
7b452e93b2 | ||
|
|
0bc1f7bf8c | ||
|
|
62c2421d85 | ||
|
|
f2edf56d02 | ||
|
|
376e5aeb45 | ||
|
|
3d1c7e0bc1 | ||
|
|
a2603f882d | ||
|
|
0c1dcafc35 | ||
|
|
d5108f8007 | ||
|
|
ebeca9aa04 | ||
|
|
7307d9f7f1 | ||
|
|
eac7cdae1c | ||
|
|
7e548cb133 | ||
|
|
0a61512fc7 | ||
|
|
479f2010a9 | ||
|
|
55796932c4 | ||
|
|
9267adf79a | ||
|
|
cc2f86cb06 | ||
|
|
14c6895135 | ||
|
|
83f6647352 | ||
|
|
792ecee399 | ||
|
|
93a1ef6bdb | ||
|
|
62607b16f8 | ||
|
|
ca60376447 | ||
|
|
3cc4b5db79 | ||
|
|
f8179c83e7 | ||
|
|
b2233f61e4 | ||
|
|
a7e2f776e4 | ||
|
|
50d672892c | ||
|
|
e6154db6e5 | ||
|
|
34e8f57f7d | ||
|
|
4a8c089fa9 | ||
|
|
b3aa5ee247 | ||
|
|
64bf98a7d3 | ||
|
|
e8e38beeb8 | ||
|
|
de96a96ac6 | ||
|
|
ee3ad17163 | ||
|
|
fafd8a5d51 | ||
|
|
c5879f17f8 | ||
|
|
999cbd314c | ||
|
|
a9d34a223a | ||
|
|
3381030ed5 | ||
|
|
2bacc6cfe8 | ||
|
|
973c936514 | ||
|
|
c5121a7fc5 | ||
|
|
37bf0f6b53 | ||
|
|
203d51cdbf | ||
|
|
9669d8eb8b | ||
|
|
1b7571be5b | ||
|
|
614ff2a30e | ||
|
|
5b11671997 | ||
|
|
5158e08901 | ||
|
|
2cb9c7211e | ||
|
|
24e26c95ff | ||
|
|
b8286af8fa | ||
|
|
735279c27c | ||
|
|
d75be22d1f | ||
|
|
40f1b1c665 | ||
|
|
d076c875ed | ||
|
|
771c7fe863 | ||
|
|
369454c12a | ||
|
|
1784eacf58 | ||
|
|
e86f5b3b7c | ||
|
|
80ff6cda04 | ||
|
|
dff96cfd95 | ||
|
|
31d244ef78 | ||
|
|
8325a84ab2 | ||
|
|
793839c7d5 | ||
|
|
23cf87dbc0 | ||
|
|
7171de336d | ||
|
|
e206cfe6d6 | ||
|
|
1a71cc9223 | ||
|
|
ed6fcf5ae7 | ||
|
|
bb31693a4d | ||
|
|
d15c8b16f3 | ||
|
|
8b9c932b80 | ||
|
|
c10e8f5f9a | ||
|
|
cd27e43994 | ||
|
|
20614cf64b | ||
|
|
c69c02bcb3 | ||
|
|
21b177cbb4 | ||
|
|
31a7e48768 | ||
|
|
fd3d60ed7d | ||
|
|
e2e369a463 | ||
|
|
0c3304f041 | ||
|
|
0d4b9b4bce | ||
|
|
1192bf6a87 | ||
|
|
2366a91e8d | ||
|
|
2c4e46c630 | ||
|
|
a989e296b0 | ||
|
|
26648dbcd2 | ||
|
|
147d759d35 | ||
|
|
f6b07c5609 | ||
|
|
29fa7a053f | ||
|
|
f380f245c6 | ||
|
|
1824a30cde | ||
|
|
8ab86fd6bb | ||
|
|
2c7bdc54c1 | ||
|
|
39b57da42b | ||
|
|
a98a9fd97a | ||
|
|
093a5c1019 | ||
|
|
ba25acaab9 | ||
|
|
9783802370 | ||
|
|
47bb02ac08 | ||
|
|
1f952d81aa | ||
|
|
0632019e44 | ||
|
|
bc14d0f580 | ||
|
|
4bc3998010 | ||
|
|
cd9c2d1988 | ||
|
|
5d597c22bf | ||
|
|
574144f9b1 | ||
|
|
b26bee3524 | ||
|
|
c835cf7829 | ||
|
|
a1d819dcb6 | ||
|
|
2234cc9334 | ||
|
|
cabd1506b7 | ||
|
|
a7c2e321bf | ||
|
|
31b75179bd | ||
|
|
06152f3131 | ||
|
|
c82f8c997f | ||
|
|
f06840f4b8 | ||
|
|
11c3d6d056 | ||
|
|
814a566845 | ||
|
|
f3bcaf2710 | ||
|
|
f9dba9266f | ||
|
|
00f7df3982 | ||
|
|
b873a77409 | ||
|
|
cd5a26398a | ||
|
|
5e8a614d82 | ||
|
|
bad601edb1 | ||
|
|
58297219a8 | ||
|
|
087b191e5b | ||
|
|
de4468a15a | ||
|
|
b73de087d2 | ||
|
|
39fd092055 | ||
|
|
dec88bd601 | ||
|
|
bc9975baa1 | ||
|
|
d2d401883e | ||
|
|
3b3ac1688a | ||
|
|
6bb0ca22d0 | ||
|
|
e157bc7b97 | ||
|
|
578da0f1a7 | ||
|
|
8f0dd0b0c6 | ||
|
|
9e05bc4fad | ||
|
|
4cee341ce5 | ||
|
|
c40192aa46 | ||
|
|
651748cd4e | ||
|
|
9e8f8357b1 | ||
|
|
3cc4c07fa1 | ||
|
|
8ed52af203 | ||
|
|
8e9a941b5d | ||
|
|
4538a4d33a | ||
|
|
a8469456ce | ||
|
|
6ca25f913a | ||
|
|
da74c7df8a | ||
|
|
278aa87753 | ||
|
|
0f77294718 | ||
|
|
38df160a5e | ||
|
|
dc61a3307c | ||
|
|
2724f8d3c5 | ||
|
|
36e1c1eff0 | ||
|
|
81f830fe23 | ||
|
|
6a42482b92 | ||
|
|
8be83c278b | ||
|
|
eb82980cbc | ||
|
|
08a6d28868 | ||
|
|
37afbc8e9d | ||
|
|
12546b3f17 | ||
|
|
70821c5d26 | ||
|
|
2f7725c5a9 | ||
|
|
a96d26c3bd | ||
|
|
a9f37c9238 | ||
|
|
239ebcdcb8 | ||
|
|
9e6e62c5e8 | ||
|
|
3a0e9f422e | ||
|
|
ef8beb9310 | ||
|
|
260094a666 | ||
|
|
659f9a8f18 | ||
|
|
cf2e3c8018 | ||
|
|
a05901e792 | ||
|
|
0928ec5c4c | ||
|
|
ef843d02c0 | ||
|
|
07cdc6bf2f | ||
|
|
832c1ef83c | ||
|
|
f5d0fc8672 | ||
|
|
4c0c917fb5 | ||
|
|
9e5ac261e2 | ||
|
|
06abfc4337 | ||
|
|
c9c54200aa | ||
|
|
c5f948099d | ||
|
|
a78cb59bc3 | ||
|
|
fb5ff24747 | ||
|
|
acb2ede658 | ||
|
|
05b1bffeef | ||
|
|
b330b55054 | ||
|
|
9a67e63e9b | ||
|
|
0d629a5385 | ||
|
|
4e6a214581 | ||
|
|
333c591771 | ||
|
|
e6645101ad | ||
|
|
48788986cb | ||
|
|
d400beffb6 | ||
|
|
7762135cb1 | ||
|
|
d0d594df69 | ||
|
|
ed97f2d786 | ||
|
|
f3a724237f | ||
|
|
aeb3a308a8 | ||
|
|
62cc97bc44 | ||
|
|
20928c7a7f | ||
|
|
6f4a865604 | ||
|
|
e7919416d5 | ||
|
|
cf8a512af4 | ||
|
|
3efef7bbbb | ||
|
|
65d8e01529 | ||
|
|
e0dd7a9b4b | ||
|
|
3a32d220dd | ||
|
|
5efaf7b23e | ||
|
|
91375a5447 | ||
|
|
f6574d346c | ||
|
|
b9b29b11d4 | ||
|
|
54d062156d | ||
|
|
4dc24e48d8 | ||
|
|
3a24464c90 | ||
|
|
f2ad9395fc | ||
|
|
61539416e5 | ||
|
|
ecf7c1f3f3 | ||
|
|
d18886ed7c | ||
|
|
07cc529f6e | ||
|
|
01b87bfbfd | ||
|
|
b126f0cb79 | ||
|
|
10c6901a04 | ||
|
|
c26efb111e | ||
|
|
f07cbe0087 | ||
|
|
7d72e196e0 | ||
|
|
9bc935da96 |
@@ -1,30 +1,65 @@
|
||||
# EditorConfig is awesome: http://EditorConfig.org
|
||||
# http://EditorConfig.org
|
||||
#
|
||||
# Julien Fontanet's configuration
|
||||
# https://gist.github.com/julien-f/8096213
|
||||
|
||||
# top-most EditorConfig file
|
||||
# Top-most EditorConfig file.
|
||||
root = true
|
||||
|
||||
# Unix-style newlines with a newline ending every file
|
||||
#
|
||||
# Tab indentation (size of 4 spaces)
|
||||
# Common config.
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_size = 2
|
||||
indent_style = tab
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespaces = true
|
||||
|
||||
# YAML only allows spaces.
|
||||
[*.yaml]
|
||||
# CoffeeScript
|
||||
#
|
||||
# https://github.com/polarmobile/coffeescript-style-guide/blob/master/README.md
|
||||
[*.{,lit}coffee]
|
||||
indent_size = 2
|
||||
indent_style = space
|
||||
|
||||
# Special settings for NPM file.
|
||||
# Markdown
|
||||
[*.{md,mdwn,mdown,markdown}]
|
||||
indent_size = 4
|
||||
indent_style = space
|
||||
|
||||
# Package.json
|
||||
#
|
||||
# This indentation style is the one used by npm.
|
||||
[/package.json]
|
||||
indent_size = 2
|
||||
indent_style = space
|
||||
|
||||
# For CoffeeScript files, we follow this Polarmobile style guide (https://github.com/polarmobile/coffeescript-style-guide/blob/master/README.md).
|
||||
[*{,.spec}.{,lit}coffee]
|
||||
# Jade
|
||||
[*.jade]
|
||||
indent_size = 2
|
||||
indent_style = space
|
||||
|
||||
# JavaScript
|
||||
#
|
||||
# Two spaces seems to be the standard most common style, at least in
|
||||
# Node.js (http://nodeguide.com/style.html#tabs-vs-spaces).
|
||||
[*.js]
|
||||
indent_size = 2
|
||||
indent_style = space
|
||||
|
||||
# Less
|
||||
[*.less]
|
||||
indent_size = 2
|
||||
indent_style = space
|
||||
|
||||
# Sass
|
||||
#
|
||||
# Style used for http://libsass.com
|
||||
[*.s[ac]ss]
|
||||
indent_size = 2
|
||||
indent_style = space
|
||||
|
||||
# YAML
|
||||
#
|
||||
# Only spaces are allowed.
|
||||
[*.yaml]
|
||||
indent_size = 2
|
||||
indent_style = space
|
||||
|
||||
9
.gitignore
vendored
9
.gitignore
vendored
@@ -1,2 +1,11 @@
|
||||
/.nyc_output/
|
||||
/dist/
|
||||
/node_modules/
|
||||
/src/api/index.js
|
||||
/src/xapi/mixins/index.js
|
||||
/src/xo-mixins/index.js
|
||||
|
||||
npm-debug.log
|
||||
npm-debug.log.*
|
||||
|
||||
.xo-server.*
|
||||
|
||||
126
.jshintrc
126
.jshintrc
@@ -1,126 +0,0 @@
|
||||
{
|
||||
// --------------------------------------------------------------------
|
||||
// JSHint Configuration, Node.js Edition
|
||||
// --------------------------------------------------------------------
|
||||
//
|
||||
// This is an options template for [JSHint][1], forked from
|
||||
// haschek's [JSHint template][2]:
|
||||
//
|
||||
// * the environment has been changed to `node`;
|
||||
// * recent options were added;
|
||||
// * coding style has been adapted to node (e.g. 2 spaces
|
||||
// indenting, global use strict).
|
||||
//
|
||||
// [1]: http://www.jshint.com/
|
||||
// [2]: https://gist.github.com/haschek/2595796
|
||||
//
|
||||
// @author Julien Fontanet <julien.fontanet@isonoe.net>
|
||||
// @license http://unlicense.org/
|
||||
|
||||
// == Enforcing Options ===============================================
|
||||
//
|
||||
// These options tell JSHint to be more strict towards your code. Use
|
||||
// them if you want to allow only a safe subset of JavaScript, very
|
||||
// useful when your codebase is shared with a big number of developers
|
||||
// with different skill levels.
|
||||
|
||||
"bitwise" : true, // Prohibit bitwise operators (&, |, ^, etc.).
|
||||
"camelcase" : true, // Require variable names to use either camelCase or UPPER_CASE styles.
|
||||
"curly" : true, // Require {} for every new block or scope.
|
||||
"eqeqeq" : true, // Require triple equals i.e. `===`.
|
||||
"forin" : true, // Tolerate `for in` loops without `hasOwnPrototype`.
|
||||
"freeze" : true, // Prohibit modification of native objects' prototypes.
|
||||
"immed" : true, // Require immediate invocations to be wrapped in parens e.g. `( function(){}() );`
|
||||
"indent" : 2, // Specify indentation spacing
|
||||
"latedef" : true, // Prohibit variable use before definition.
|
||||
"newcap" : true, // Require capitalization of all constructor functions e.g. `new F()`.
|
||||
"noarg" : true, // Prohibit use of `arguments.caller` and `arguments.callee`.
|
||||
"noempty" : true, // Prohibit use of empty blocks.
|
||||
"nonew" : true, // Prohibit use of constructors for side-effects.
|
||||
"plusplus" : false, // Prohibit use of `++` & `--`.
|
||||
"quotmark" : "'", // Require single quotes.
|
||||
"undef" : true, // Require all non-global variables be declared before they are used.
|
||||
"unused" : true, // Prohibit unused variables.
|
||||
"strict" : true, // Require `use strict` pragma in every function.
|
||||
"trailing" : true, // Prohibit trailing whitespaces.
|
||||
"maxparams" : 4, // Prohibit more than 4 parameters per function definition.
|
||||
"maxdepth" : 3, // Prohibit nesting more than 3 control blocks.
|
||||
"maxstatements" : 20, // Prohibit more than 20 statements per function.
|
||||
"maxcomplexity" : 7, // Prohibit having to much branches in your code.
|
||||
"maxlen" : 80, // Prohibit line with more than 80 characters.
|
||||
|
||||
// == Relaxing Options ================================================
|
||||
//
|
||||
// These options allow you to suppress certain types of warnings. Use
|
||||
// them only if you are absolutely positive that you know what you are
|
||||
// doing.
|
||||
|
||||
"asi" : false, // Tolerate Automatic Semicolon Insertion (no semicolons).
|
||||
"boss" : false, // Tolerate assignments inside if, for & while. Usually conditions & loops are for comparison, not assignments.
|
||||
"debug" : false, // Allow debugger statements e.g. browser breakpoints.
|
||||
"eqnull" : false, // Tolerate use of `== null`.
|
||||
"es5" : false, // Allow EcmaScript 5 syntax.
|
||||
"esnext" : true, // Allow ES.next specific features such as `const` and `let`.
|
||||
"evil" : false, // Tolerate use of `eval`.
|
||||
"expr" : true, // Tolerate `ExpressionStatement` as Programs. (Allowed for Mocha.)
|
||||
"funcscope" : false, // Tolerate declarations of variables inside of control structures while accessing them later from the outside.
|
||||
"gcl" : false, // Makes JSHint compatible with Google Closure Compiler.
|
||||
"globalstrict" : true, // Allow global "use strict" (also enables 'strict').
|
||||
"iterator" : false, // Allow usage of __iterator__ property.
|
||||
"lastsemic" : false, // Tolerat missing semicolons when the it is omitted for the last statement in a one-line block.
|
||||
"laxbreak" : false, // Tolerate unsafe line breaks e.g. `return [\n] x` without semicolons.
|
||||
"laxcomma" : false, // Suppress warnings about comma-first coding style.
|
||||
"loopfunc" : false, // Allow functions to be defined within loops.
|
||||
"maxerr" : 50, // Maximum errors before stopping.
|
||||
"moz" : false, // Tolerate Mozilla JavaScript extensions.
|
||||
"notypeof" : false, // Tolerate invalid typeof values.
|
||||
"multistr" : false, // Tolerate multi-line strings.
|
||||
"proto" : false, // Tolerate __proto__ property. This property is deprecated.
|
||||
"scripturl" : false, // Tolerate script-targeted URLs.
|
||||
"smarttabs" : false, // Tolerate mixed tabs and spaces when the latter are used for alignmnent only.
|
||||
"shadow" : false, // Allows re-define variables later in code e.g. `var x=1; x=2;`.
|
||||
"sub" : false, // Tolerate all forms of subscript notation besides dot notation e.g. `dict['key']` instead of `dict.key`.
|
||||
"supernew" : false, // Tolerate `new function () { ... };` and `new Object;`.
|
||||
"validthis" : false, // Tolerate strict violations when the code is running in strict mode and you use this in a non-constructor function.
|
||||
|
||||
// == Environments ====================================================
|
||||
//
|
||||
// These options pre-define global variables that are exposed by
|
||||
// popular JavaScript libraries and runtime environments—such as
|
||||
// browser or node.js.
|
||||
|
||||
"browser" : false, // Standard browser globals e.g. `window`, `document`.
|
||||
"couch" : false, // Enable globals exposed by CouchDB.
|
||||
"devel" : false, // Allow development statements e.g. `console.log();`.
|
||||
"dojo" : false, // Enable globals exposed by Dojo Toolkit.
|
||||
"jquery" : false, // Enable globals exposed by jQuery JavaScript library.
|
||||
"mootools" : false, // Enable globals exposed by MooTools JavaScript framework.
|
||||
"node" : true, // Enable globals available when code is running inside of the NodeJS runtime environment.
|
||||
"nonstandard" : false, // Define non-standard but widely adopted globals such as escape and unescape.
|
||||
"phantom" : false, // Enable globals exposed by PhantomJS.
|
||||
"prototypejs" : false, // Enable globals exposed by Prototype JavaScript framework.
|
||||
"rhino" : false, // Enable globals available when your code is running inside of the Rhino runtime environment.
|
||||
"worker" : false, // Enable globals exposed when running inside a Web Worker.
|
||||
"wsh" : false, // Enable globals available when your code is running as a script for the Windows Script Host.
|
||||
"yui" : false, // Enable globals exposed by YUI.
|
||||
|
||||
// == JSLint Legacy ===================================================
|
||||
//
|
||||
// These options are legacy from JSLint. Aside from bug fixes they will
|
||||
// not be improved in any way and might be removed at any point.
|
||||
|
||||
"nomen" : false, // Prohibit use of initial or trailing underbars in names.
|
||||
"onevar" : false, // Allow only one `var` statement per function.
|
||||
"passfail" : false, // Stop on first error.
|
||||
"white" : false, // Check against strict whitespace and indentation rules.
|
||||
|
||||
"globals": {
|
||||
// Mocha.
|
||||
"after" : false,
|
||||
"afterEach" : false,
|
||||
"before" : false,
|
||||
"beforeEach" : false,
|
||||
"describe" : false,
|
||||
"it" : false
|
||||
}
|
||||
}
|
||||
1
.mocha.opts
Normal file
1
.mocha.opts
Normal file
@@ -0,0 +1 @@
|
||||
--require ./better-stacks.js
|
||||
10
.npmignore
Normal file
10
.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
|
||||
8
.travis.yml
Normal file
8
.travis.yml
Normal file
@@ -0,0 +1,8 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- '6'
|
||||
- '4'
|
||||
|
||||
# Use containers.
|
||||
# http://docs.travis-ci.com/user/workers/container-based-infrastructure/
|
||||
sudo: false
|
||||
3
ISSUE_TEMPLATE.md
Normal file
3
ISSUE_TEMPLATE.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# ALL ISSUES SHOULD BE CREATED IN XO-WEB'S TRACKER!
|
||||
|
||||
https://github.com/vatesfr/xo-web/issues
|
||||
23
README.md
23
README.md
@@ -1,5 +1,7 @@
|
||||
# Xen Orchestra Server
|
||||
|
||||

|
||||
|
||||
XO-Server is part of [Xen Orchestra](https://github.com/vatesfr/xo), a web interface for XenServer or XAPI enabled hosts.
|
||||
|
||||
It contains all the logic of XO and handles:
|
||||
@@ -9,6 +11,7 @@ It contains all the logic of XO and handles:
|
||||
- users authentication and authorizations (work in progress);
|
||||
- a JSON-RPC based interface for XO clients (i.e. [XO-Web](https://github.com/vatesfr/xo-web)).
|
||||
|
||||
[](https://travis-ci.org/vatesfr/xo-server)
|
||||
[](https://david-dm.org/vatesfr/xo-server)
|
||||
[](https://david-dm.org/vatesfr/xo-server#info=devDependencies)
|
||||
|
||||
@@ -16,10 +19,22 @@ ___
|
||||
|
||||
## Installation
|
||||
|
||||
Manual install procedure is [available here](https://github.com/vatesfr/xo/blob/master/installation.md)
|
||||
Manual install procedure is [available here](https://xen-orchestra.com/docs/from_the_sources.html).
|
||||
|
||||
## Compilation
|
||||
|
||||
Production build:
|
||||
|
||||
```
|
||||
$ npm run build
|
||||
```
|
||||
|
||||
Development build:
|
||||
|
||||
```
|
||||
$ npm run dev
|
||||
```
|
||||
|
||||
## How to report a bug?
|
||||
|
||||
If you are certain the bug is exclusively related to XO-Server, you may use the [bugtracker of this repository](https://github.com/vatesfr/xo-server/issues).
|
||||
|
||||
Otherwise, please consider using the [bugtracker of the general repository](https://github.com/vatesfr/xo/issues).
|
||||
All bug reports should go into the [bugtracker of xo-web](https://github.com/vatesfr/xo-web/issues).
|
||||
|
||||
37
better-stacks.js
Normal file
37
better-stacks.js
Normal file
@@ -0,0 +1,37 @@
|
||||
Error.stackTraceLimit = 100
|
||||
|
||||
// Async stacks.
|
||||
//
|
||||
// Disabled for now as it cause a huge memory usage with
|
||||
// fs.createReadStream().
|
||||
// TODO: find a way to reenable.
|
||||
//
|
||||
// try { require('trace') } catch (_) {}
|
||||
|
||||
// Removes internal modules.
|
||||
try {
|
||||
var sep = require('path').sep
|
||||
|
||||
require('stack-chain').filter.attach(function (_, frames) {
|
||||
var filtered = frames.filter(function (frame) {
|
||||
var name = frame && frame.getFileName()
|
||||
|
||||
return (
|
||||
// has a filename
|
||||
name &&
|
||||
|
||||
// contains a separator (no internal modules)
|
||||
name.indexOf(sep) !== -1
|
||||
)
|
||||
})
|
||||
|
||||
// depd (used amongst other by express requires at least 3 frames
|
||||
// in the stack.
|
||||
return filtered.length > 2
|
||||
? filtered
|
||||
: frames
|
||||
})
|
||||
} catch (_) {}
|
||||
|
||||
// Source maps.
|
||||
try { require('julien-f-source-map-support/register') } catch (_) {}
|
||||
@@ -1,7 +1,31 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
'use strict';
|
||||
'use strict'
|
||||
|
||||
//====================================================================
|
||||
// ===================================================================
|
||||
|
||||
require('exec-promise')(require('../'));
|
||||
// Better stack traces if possible.
|
||||
require('../better-stacks')
|
||||
|
||||
// Use Bluebird for all promises as it provides better performance and
|
||||
// less memory usage.
|
||||
global.Promise = require('bluebird')
|
||||
|
||||
// Make unhandled rejected promises visible.
|
||||
process.on('unhandledRejection', function (reason) {
|
||||
console.warn('[Warn] Possibly unhandled rejection:', reason && reason.stack || reason)
|
||||
})
|
||||
|
||||
;(function (EE) {
|
||||
var proto = EE.prototype
|
||||
var emit = proto.emit
|
||||
proto.emit = function patchedError (event, error) {
|
||||
if (event === 'error' && !this.listenerCount(event)) {
|
||||
return console.warn('[Warn] Unhandled error event:', error && error.stack || error)
|
||||
}
|
||||
|
||||
return emit.apply(this, arguments)
|
||||
}
|
||||
})(require('events').EventEmitter)
|
||||
|
||||
require('exec-promise')(require('../'))
|
||||
|
||||
10
bin/xo-server-logs
Executable file
10
bin/xo-server-logs
Executable file
@@ -0,0 +1,10 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
'use strict'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
// Better stack traces if possible.
|
||||
require('../better-stacks')
|
||||
|
||||
require('exec-promise')(require('../dist/logs-cli').default)
|
||||
40
config.json
Normal file
40
config.json
Normal file
@@ -0,0 +1,40 @@
|
||||
// Vendor config: DO NOT TOUCH!
|
||||
//
|
||||
// See sample.config.yaml to override.
|
||||
{
|
||||
"http": {
|
||||
"listen": [
|
||||
{
|
||||
"port": 80
|
||||
}
|
||||
],
|
||||
"mounts": {},
|
||||
|
||||
// Ciphers to use.
|
||||
//
|
||||
// These are the default ciphers in Node 4.2.6, we are setting
|
||||
// them explicitly for older Node versions.
|
||||
"ciphers": "ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384:DHE-RSA-AES256-SHA384:ECDHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA256:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!SRP:!CAMELLIA",
|
||||
|
||||
// Tell Node to respect the cipher order.
|
||||
"honorCipherOrder": true,
|
||||
|
||||
// Specify to use at least TLSv1.1.
|
||||
// See: https://github.com/certsimple/minimum-tls-version
|
||||
"secureOptions": 117440512
|
||||
},
|
||||
"datadir": "/var/lib/xo-server/data",
|
||||
|
||||
// Should users be created on first sign in?
|
||||
//
|
||||
// Necessary for external authentication providers.
|
||||
"createUserOnFirstSignin": true,
|
||||
|
||||
// Whether API logs should contains the full request/response on
|
||||
// errors.
|
||||
//
|
||||
// This is disabled by default for performance (lots of data) and
|
||||
// security concerns (avoiding sensitive data in the logs) but can
|
||||
// be turned for investigation by the administrator.
|
||||
"verboseApiLogsOnErrors": false
|
||||
}
|
||||
1
config/.gitignore
vendored
1
config/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
/local.yaml
|
||||
70
gulpfile.js
Normal file
70
gulpfile.js
Normal file
@@ -0,0 +1,70 @@
|
||||
'use strict'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
var gulp = require('gulp')
|
||||
|
||||
var babel = require('gulp-babel')
|
||||
var coffee = require('gulp-coffee')
|
||||
var plumber = require('gulp-plumber')
|
||||
var rimraf = require('rimraf')
|
||||
var sourceMaps = require('gulp-sourcemaps')
|
||||
var watch = require('gulp-watch')
|
||||
|
||||
var join = require('path').join
|
||||
|
||||
// ===================================================================
|
||||
|
||||
var SRC_DIR = join(__dirname, 'src')
|
||||
var DIST_DIR = join(__dirname, 'dist')
|
||||
|
||||
var PRODUCTION = process.argv.indexOf('--production') !== -1
|
||||
|
||||
// ===================================================================
|
||||
|
||||
function src (patterns) {
|
||||
return PRODUCTION
|
||||
? gulp.src(patterns, {
|
||||
base: SRC_DIR,
|
||||
cwd: SRC_DIR
|
||||
})
|
||||
: watch(patterns, {
|
||||
base: SRC_DIR,
|
||||
cwd: SRC_DIR,
|
||||
ignoreInitial: false,
|
||||
verbose: true
|
||||
})
|
||||
.pipe(plumber())
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
gulp.task(function clean (cb) {
|
||||
rimraf(DIST_DIR, cb)
|
||||
})
|
||||
|
||||
gulp.task(function buildCoffee () {
|
||||
return src('**/*.coffee')
|
||||
.pipe(sourceMaps.init())
|
||||
.pipe(coffee({
|
||||
bare: true
|
||||
}))
|
||||
|
||||
// Necessary to correctly compile generators.
|
||||
.pipe(babel())
|
||||
|
||||
.pipe(sourceMaps.write('.'))
|
||||
.pipe(gulp.dest(DIST_DIR))
|
||||
})
|
||||
|
||||
gulp.task(function buildEs6 () {
|
||||
return src('**/*.js')
|
||||
.pipe(sourceMaps.init())
|
||||
.pipe(babel())
|
||||
.pipe(sourceMaps.write('.'))
|
||||
.pipe(gulp.dest(DIST_DIR))
|
||||
})
|
||||
|
||||
// ===================================================================
|
||||
|
||||
gulp.task('build', gulp.series('clean', gulp.parallel('buildCoffee', 'buildEs6')))
|
||||
13
index.js
13
index.js
@@ -1,6 +1,11 @@
|
||||
'use strict';
|
||||
'use strict'
|
||||
|
||||
//====================================================================
|
||||
// ===================================================================
|
||||
|
||||
require('coffee-script/register');
|
||||
module.exports = require('./src/main');
|
||||
// Enable xo logs by default.
|
||||
if (process.env.DEBUG === undefined) {
|
||||
process.env.DEBUG = 'app-conf,xen-api,xo:*'
|
||||
}
|
||||
|
||||
// Import the real main module.
|
||||
module.exports = require('./dist').default
|
||||
|
||||
184
package.json
184
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "xo-server",
|
||||
"version": "3.5.0-alpha1",
|
||||
"license": "AGPL3",
|
||||
"version": "5.3.2",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "Server part of Xen-Orchestra",
|
||||
"keywords": [
|
||||
"xen",
|
||||
@@ -11,50 +11,162 @@
|
||||
],
|
||||
"homepage": "http://github.com/vatesfr/xo-server/",
|
||||
"bugs": {
|
||||
"url": "https://github.com/vatesfr/xo-server/issues"
|
||||
},
|
||||
"author": "Julien Fontanet <julien.fontanet@vates.fr>",
|
||||
"preferGlobal": true,
|
||||
"directories": {
|
||||
"bin": "bin"
|
||||
"url": "https://github.com/vatesfr/xo-web/issues"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/vatesfr/xo-server.git"
|
||||
},
|
||||
"author": "Julien Fontanet <julien.fontanet@vates.fr>",
|
||||
"preferGlobal": true,
|
||||
"files": [
|
||||
"better-stacks.js",
|
||||
"bin/",
|
||||
"dist/",
|
||||
"config.json",
|
||||
"index.js",
|
||||
"signin.pug"
|
||||
],
|
||||
"directories": {
|
||||
"bin": "bin"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.12"
|
||||
},
|
||||
"dependencies": {
|
||||
"backoff": "~2.4.0",
|
||||
"bluebird": "^2.2.2",
|
||||
"coffee-script": "~1.7.1",
|
||||
"connect": "^3.1.0",
|
||||
"event-to-promise": "^0.3.0",
|
||||
"exec-promise": "^0.3.0",
|
||||
"extendable": "~0.0.6",
|
||||
"fibers": "~1.0.1",
|
||||
"hashy": "~0.3.6",
|
||||
"hiredis": "~0.1.17",
|
||||
"http-server-plus": "^0.2.3",
|
||||
"js-yaml": "~3.1.0",
|
||||
"nconf": "~0.6.9",
|
||||
"redis": "~0.11.0",
|
||||
"require-tree": "~0.3.3",
|
||||
"schema-inspector": "^1.4.5",
|
||||
"serve-static": "^1.4.0",
|
||||
"then-redis": "~0.3.12",
|
||||
"underscore": "~1.6.0",
|
||||
"ws": "~0.4.31",
|
||||
"xml2js": "~0.4.4",
|
||||
"xmlrpc": "~1.2.0"
|
||||
"@marsaud/smb2-promise": "^0.2.1",
|
||||
"@nraynaud/struct-fu": "^1.0.1",
|
||||
"app-conf": "^0.4.0",
|
||||
"babel-runtime": "^6.5.0",
|
||||
"base64url": "^2.0.0",
|
||||
"blocked": "^1.1.0",
|
||||
"bluebird": "^3.1.1",
|
||||
"body-parser": "^1.13.3",
|
||||
"connect-flash": "^0.1.1",
|
||||
"cookie": "^0.3.0",
|
||||
"cookie-parser": "^1.3.5",
|
||||
"cron": "^1.0.9",
|
||||
"d3-time-format": "^2.0.0",
|
||||
"debug": "^2.1.3",
|
||||
"escape-string-regexp": "^1.0.3",
|
||||
"event-to-promise": "^0.7.0",
|
||||
"exec-promise": "^0.6.1",
|
||||
"execa": "^0.5.0",
|
||||
"express": "^4.13.3",
|
||||
"express-session": "^1.11.3",
|
||||
"fatfs": "^0.10.3",
|
||||
"fs-extra": "^0.30.0",
|
||||
"fs-promise": "^0.5.0",
|
||||
"get-stream": "^2.1.0",
|
||||
"hashy": "~0.4.2",
|
||||
"helmet": "^2.0.0",
|
||||
"highland": "^2.5.1",
|
||||
"http-proxy": "^1.13.2",
|
||||
"http-server-plus": "^0.7.0",
|
||||
"human-format": "^0.7.0",
|
||||
"is-my-json-valid": "^2.13.1",
|
||||
"is-redirect": "^1.0.0",
|
||||
"js-yaml": "^3.2.7",
|
||||
"json-rpc-peer": "^0.12.0",
|
||||
"json5": "^0.5.0",
|
||||
"julien-f-source-map-support": "0.0.0",
|
||||
"julien-f-unzip": "^0.2.1",
|
||||
"kindof": "^2.0.0",
|
||||
"level": "^1.3.0",
|
||||
"level-party": "^3.0.4",
|
||||
"level-sublevel": "^6.5.2",
|
||||
"leveldown": "^1.4.2",
|
||||
"lodash": "^4.13.1",
|
||||
"make-error": "^1",
|
||||
"micromatch": "^2.3.2",
|
||||
"minimist": "^1.2.0",
|
||||
"moment-timezone": "^0.5.4",
|
||||
"ms": "^0.7.1",
|
||||
"multikey-hash": "^1.0.1",
|
||||
"ndjson": "^1.4.3",
|
||||
"partial-stream": "0.0.0",
|
||||
"passport": "^0.3.0",
|
||||
"passport-local": "^1.0.0",
|
||||
"promise-toolbox": "^0.7.0",
|
||||
"proxy-agent": "^2.0.0",
|
||||
"pug": "^2.0.0-alpha6",
|
||||
"redis": "^2.0.1",
|
||||
"schema-inspector": "^1.5.1",
|
||||
"semver": "^5.1.0",
|
||||
"serve-static": "^1.9.2",
|
||||
"stack-chain": "^1.3.3",
|
||||
"tar-stream": "^1.5.2",
|
||||
"through2": "^2.0.0",
|
||||
"trace": "^2.0.1",
|
||||
"uuid": "^2.0.3",
|
||||
"ws": "^1.1.1",
|
||||
"xen-api": "^0.9.4",
|
||||
"xml2js": "~0.4.6",
|
||||
"xo-acl-resolver": "^0.2.2",
|
||||
"xo-collection": "^0.4.0",
|
||||
"xo-remote-parser": "^0.3",
|
||||
"xo-vmdk-to-vhd": "0.0.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"chai": "~1.9.1",
|
||||
"glob": "~4.0.4",
|
||||
"mocha": "^1.21.0",
|
||||
"node-inspector": "^0.7.4",
|
||||
"sinon": "^1.10.3"
|
||||
"babel-eslint": "^7.0.0",
|
||||
"babel-plugin-lodash": "^3.2.9",
|
||||
"babel-plugin-transform-decorators-legacy": "^1.3.4",
|
||||
"babel-plugin-transform-runtime": "^6.5.2",
|
||||
"babel-preset-es2015": "^6.5.0",
|
||||
"babel-preset-stage-0": "^6.5.0",
|
||||
"chai": "^3.0.0",
|
||||
"dependency-check": "^2.4.0",
|
||||
"ghooks": "^1.0.3",
|
||||
"gulp": "git://github.com/gulpjs/gulp#4.0",
|
||||
"gulp-babel": "^6",
|
||||
"gulp-coffee": "^2.3.1",
|
||||
"gulp-plumber": "^1.0.0",
|
||||
"gulp-sourcemaps": "^2.1.1",
|
||||
"gulp-watch": "^4.2.2",
|
||||
"index-modules": "0.0.0",
|
||||
"leche": "^2.1.1",
|
||||
"mocha": "^3.0.2",
|
||||
"must": "^0.13.1",
|
||||
"nyc": "^8.1.0",
|
||||
"rimraf": "^2.5.2",
|
||||
"sinon": "^1.14.1",
|
||||
"standard": "^8.1.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "npm run build-indexes && gulp build --production",
|
||||
"depcheck": "dependency-check ./package.json",
|
||||
"build-indexes": "index-modules src/api src/xapi/mixins src/xo-mixins",
|
||||
"dev": "npm run build-indexes && gulp build",
|
||||
"dev-test": "mocha --opts .mocha.opts --watch --reporter=min \"dist/**/*.spec.js\"",
|
||||
"lint": "standard",
|
||||
"postrelease": "git checkout master && git merge --ff-only stable && git checkout next-release && git merge --ff-only stable",
|
||||
"posttest": "npm run lint && npm run depcheck",
|
||||
"prepublish": "npm run build",
|
||||
"prerelease": "git checkout next-release && git pull --ff-only && git checkout stable && git pull --ff-only && git merge next-release",
|
||||
"release": "npm version",
|
||||
"start": "node bin/xo-server",
|
||||
"test": "coffee run-tests"
|
||||
"test": "nyc mocha --opts .mocha.opts \"dist/**/*.spec.js\""
|
||||
},
|
||||
"babel": {
|
||||
"plugins": [
|
||||
"lodash",
|
||||
"transform-decorators-legacy",
|
||||
"transform-runtime"
|
||||
],
|
||||
"presets": [
|
||||
"stage-0",
|
||||
"es2015"
|
||||
]
|
||||
},
|
||||
"config": {
|
||||
"ghooks": {
|
||||
"commit-msg": "npm test"
|
||||
}
|
||||
},
|
||||
"standard": {
|
||||
"ignore": [
|
||||
"dist"
|
||||
],
|
||||
"parser": "babel-eslint"
|
||||
}
|
||||
}
|
||||
|
||||
29
run-tests
29
run-tests
@@ -1,29 +0,0 @@
|
||||
#!/usr/bin/env coffee
|
||||
|
||||
# Tests runner.
|
||||
$mocha = require 'mocha'
|
||||
|
||||
# Used to find the specification files.
|
||||
$glob = require 'glob'
|
||||
|
||||
#=====================================================================
|
||||
|
||||
do ->
|
||||
# Instantiates the tests runner.
|
||||
mocha = new $mocha {
|
||||
reporter: 'spec'
|
||||
}
|
||||
|
||||
# Processes arguments.
|
||||
do ->
|
||||
{argv} = process
|
||||
i = 2
|
||||
n = argv.length
|
||||
mocha.grep argv[i++] while i < n
|
||||
|
||||
$glob 'src/**/*.spec.{coffee,js}', (error, files) ->
|
||||
console.error(error) if error
|
||||
|
||||
mocha.addFile file for file in files
|
||||
|
||||
mocha.run()
|
||||
@@ -1,4 +1,18 @@
|
||||
# Note: Relative paths will be resolved from XO-Server's directory.
|
||||
# BE *VERY* CAREFUL WHEN EDITING!
|
||||
# YAML FILES ARE SUPER SUPER SENSITIVE TO MISTAKES IN WHITESPACE OR ALIGNMENT!
|
||||
# visit http://www.yamllint.com/ to validate this file as needed
|
||||
|
||||
#=====================================================================
|
||||
|
||||
# Example XO-Server configuration.
|
||||
#
|
||||
# This file is automatically looking for at the following places:
|
||||
# - `$HOME/.config/xo-server/config.yaml`
|
||||
# - `/etc/xo-server/config.yaml`
|
||||
#
|
||||
# The first entries have priority.
|
||||
#
|
||||
# Note: paths are relative to the configuration file.
|
||||
|
||||
#=====================================================================
|
||||
|
||||
@@ -44,7 +58,7 @@ http:
|
||||
# Sets it to '127.0.0.1' to listen only on the local host.
|
||||
#
|
||||
# Default: '0.0.0.0' (all addresses)
|
||||
#host: '127.0.0.1'
|
||||
#hostname: '127.0.0.1'
|
||||
|
||||
# Port on which the server is listening on.
|
||||
#
|
||||
@@ -58,17 +72,26 @@ http:
|
||||
#socket: './http.sock'
|
||||
|
||||
# Basic HTTPS.
|
||||
#
|
||||
# You can find the list of possible options there https://nodejs.org/docs/latest/api/tls.html#tls.createServer
|
||||
# -
|
||||
# # The only difference is the presence of the certificate and the
|
||||
# # key.
|
||||
|
||||
# #host: '127.0.0.1'
|
||||
# #
|
||||
# #hostname: '127.0.0.1'
|
||||
# port: 443
|
||||
|
||||
# # File containing the certificate (PEM format).
|
||||
#
|
||||
# # If a chain of certificates authorities is needed, you may bundle
|
||||
# # them directly in the certificate.
|
||||
# #
|
||||
# # Note: the order of certificates does matter, your certificate
|
||||
# # should come first followed by the certificate of the above
|
||||
# # certificate authority up to the root.
|
||||
# #
|
||||
# # Default: undefined
|
||||
# certificate: './certificate.pem'
|
||||
# cert: './certificate.pem'
|
||||
|
||||
# # File containing the private key (PEM format).
|
||||
# #
|
||||
@@ -78,15 +101,35 @@ http:
|
||||
# # Default: undefined
|
||||
# key: './key.pem'
|
||||
|
||||
# If set to true, all HTTP traffic will be redirected to the first
|
||||
# HTTPs configuration.
|
||||
#redirectToHttps: true
|
||||
|
||||
# List of files/directories which will be served.
|
||||
mounts:
|
||||
#'/': '/path/to/xo-web/dist/'
|
||||
|
||||
# List of proxied URLs (HTTP & WebSockets).
|
||||
proxies:
|
||||
# '/any/url': 'http://localhost:54722'
|
||||
|
||||
# HTTP proxy configuration used by xo-server to fetch resources on the
|
||||
# Internet.
|
||||
#
|
||||
# See: https://github.com/TooTallNate/node-proxy-agent#maps-proxy-protocols-to-httpagent-implementations
|
||||
#httpProxy: 'http://jsmith:qwerty@proxy.lan:3128'
|
||||
|
||||
#=====================================================================
|
||||
|
||||
# Connection to the Redis server.
|
||||
redis:
|
||||
# Syntax: tcp://[db[:password]@]hostname[:port]
|
||||
# Syntax: redis://[db[:password]@]hostname[:port]
|
||||
#
|
||||
# Default: tcp://localhost:6379
|
||||
# Default: redis://localhost:6379
|
||||
#uri: ''
|
||||
|
||||
# Directory containing the database of XO.
|
||||
# Currently used for logs.
|
||||
#
|
||||
# Default: '/var/lib/xo-server/data'
|
||||
#datadir: '/var/lib/xo-server/data'
|
||||
50
signin.pug
Normal file
50
signin.pug
Normal file
@@ -0,0 +1,50 @@
|
||||
doctype html
|
||||
html
|
||||
head
|
||||
meta(charset = 'utf-8')
|
||||
meta(http-equiv = 'X-UA-Compatible' content = 'IE=edge,chrome=1')
|
||||
meta(name = 'viewport' content = 'width=device-width, initial-scale=1.0')
|
||||
title Xen Orchestra
|
||||
meta(name = 'author' content = 'Vates SAS')
|
||||
link(rel = 'stylesheet' href = 'index.css')
|
||||
body(style = 'display: flex; height: 100vh;')
|
||||
div(style = 'margin: auto; width: 20em;')
|
||||
div.m-b-2(style = 'display: flex;')
|
||||
img(src = 'assets/logo.png' style = 'margin: auto;')
|
||||
h2.text-xs-center.m-b-2 Xen Orchestra
|
||||
form(action = 'signin/local' method = 'post')
|
||||
fieldset
|
||||
if error
|
||||
p.text-danger #{error}
|
||||
.input-group.m-b-1
|
||||
span.input-group-addon
|
||||
i.xo-icon-user.fa-fw
|
||||
input.form-control(
|
||||
name = 'username'
|
||||
type = 'text'
|
||||
placeholder = 'Username'
|
||||
required
|
||||
)
|
||||
.input-group.m-b-1
|
||||
span.input-group-addon
|
||||
i.fa.fa-key.fa-fw
|
||||
input.form-control(
|
||||
name = 'password'
|
||||
type = 'password'
|
||||
placeholder = 'Password'
|
||||
required
|
||||
)
|
||||
.checkbox
|
||||
label
|
||||
input(
|
||||
name = 'remember-me'
|
||||
type = 'checkbox'
|
||||
)
|
||||
|
|
||||
| Remember me
|
||||
div
|
||||
button.btn.btn-block.btn-info
|
||||
i.fa.fa-sign-in
|
||||
| Sign in
|
||||
each label, id in strategies
|
||||
div: a(href = 'signin/' + id) Sign in with #{label}
|
||||
@@ -1,439 +0,0 @@
|
||||
{EventEmitter: $EventEmitter} = require 'events'
|
||||
|
||||
#----------------------------------------------------------------------
|
||||
|
||||
$_ = require 'underscore'
|
||||
|
||||
#=====================================================================
|
||||
|
||||
{$each, $makeFunction, $mapInPlace} = require './utils'
|
||||
|
||||
#=====================================================================
|
||||
|
||||
class $MappedCollection extends $EventEmitter
|
||||
|
||||
# The dispatch function is called whenever a new item has to be
|
||||
# processed and returns the name of the rule to use.
|
||||
#
|
||||
# To change the way it is dispatched, just override this it.
|
||||
dispatch: ->
|
||||
(@genval and (@genval.rule ? @genval.type)) ? 'unknown'
|
||||
|
||||
# This function is called when an item has been dispatched to a
|
||||
# missing rule.
|
||||
#
|
||||
# The default behavior is to throw an error but you may instead
|
||||
# choose to create a rule:
|
||||
#
|
||||
# collection.missingRule = collection.rule
|
||||
missingRule: (name) ->
|
||||
throw new Error "undefined rule “#{name}”"
|
||||
|
||||
# This function is called when the new generator of an existing item has been
|
||||
# matched to a different rule.
|
||||
#
|
||||
# The default behavior is to throw an error as it usually indicates a bug but
|
||||
# you can ignore it.
|
||||
ruleConflict: (rule, item) ->
|
||||
throw new Error "the item “#{item.key}” was of rule “#{item.rule}” "+
|
||||
"but matches to “#{rule}”"
|
||||
|
||||
constructor: ->
|
||||
# Items are stored here indexed by key.
|
||||
#
|
||||
# The prototype of this object is set to `null` to avoid pollution
|
||||
# from enumerable properties of `Object.prototype` and the
|
||||
# performance hit of `hasOwnProperty o`.
|
||||
@_byKey = Object.create null
|
||||
|
||||
# Hooks are stored here indexed by moment.
|
||||
@_hooks = {
|
||||
beforeDispatch: []
|
||||
beforeUpdate: []
|
||||
beforeSave: []
|
||||
afterRule: []
|
||||
}
|
||||
|
||||
# Rules are stored here indexed by name.
|
||||
#
|
||||
# The prototype of this object is set to `null` to avoid pollution
|
||||
# from enumerable properties of `Object.prototype` and to be able
|
||||
# to use the `name of @_rules` syntax.
|
||||
@_rules = Object.create null
|
||||
|
||||
# Register a hook to run at a given point.
|
||||
#
|
||||
# A hook receives as parameter an event object with the following
|
||||
# properties:
|
||||
# - `preventDefault()`: prevents the next default action from
|
||||
# happening;
|
||||
# - `stopPropagation()`: prevents other hooks from being run.
|
||||
#
|
||||
# Note: if a hook throws an exception, `event.stopPropagation()`
|
||||
# then `event.preventDefault()` will be called and the exception
|
||||
# will be forwarded.
|
||||
#
|
||||
# # Item hook
|
||||
#
|
||||
# Valid items related moments are:
|
||||
# - beforeDispatch: even before the item has been dispatched;
|
||||
# - beforeUpdate: after the item has been dispatched but before
|
||||
# updating its value.
|
||||
# - beforeSave: after the item has been updated.
|
||||
#
|
||||
# An item hook is run in the context of the current item.
|
||||
#
|
||||
# # Rule hook
|
||||
#
|
||||
# Valid rules related moments are:
|
||||
# - afterRule: just after a new rule has been defined (even
|
||||
# singleton).
|
||||
#
|
||||
# An item hook is run in the context of the current rule.
|
||||
hook: (name, hook) ->
|
||||
# Allows a nicer syntax for CoffeeScript.
|
||||
if $_.isObject name
|
||||
# Extracts the name and the value from the first property of the
|
||||
# object.
|
||||
do ->
|
||||
object = name
|
||||
return for own name, hook of object
|
||||
|
||||
hooks = @_hooks[name]
|
||||
|
||||
@_assert(
|
||||
hooks?
|
||||
"invalid hook moment “#{name}”"
|
||||
)
|
||||
|
||||
hooks.push hook
|
||||
|
||||
# Register a new singleton rule.
|
||||
#
|
||||
# See the `rule()` method for more information.
|
||||
item: (name, definition) ->
|
||||
# Creates the corresponding rule.
|
||||
rule = @rule name, definition, true
|
||||
|
||||
# Creates the singleton.
|
||||
item = {
|
||||
rule: rule.name
|
||||
key: rule.key() # No context because there is not generator.
|
||||
val: undefined
|
||||
}
|
||||
@_updateItems [item], true
|
||||
|
||||
# Register a new rule.
|
||||
#
|
||||
# If the definition is a function, it will be run in the context of
|
||||
# an item-like object with the following properties:
|
||||
# - `key`: the definition for the key of this item;
|
||||
# - `val`: the definition for the value of this item.
|
||||
#
|
||||
# Warning: The definition function is run only once!
|
||||
rule: (name, definition, singleton = false) ->
|
||||
# Allows a nicer syntax for CoffeeScript.
|
||||
if $_.isObject name
|
||||
# Extracts the name and the definition from the first property
|
||||
# of the object.
|
||||
do ->
|
||||
object = name
|
||||
return for own name, definition of object
|
||||
|
||||
@_assert(
|
||||
name not of @_rules
|
||||
"the rule “#{name}” is already defined"
|
||||
)
|
||||
|
||||
# Extracts the rule definition.
|
||||
if $_.isFunction definition
|
||||
ctx = {
|
||||
name
|
||||
key: undefined
|
||||
data: undefined
|
||||
val: undefined
|
||||
singleton
|
||||
}
|
||||
definition.call ctx
|
||||
else
|
||||
ctx = {
|
||||
name
|
||||
key: definition?.key
|
||||
data: definition?.data
|
||||
val: definition?.val
|
||||
singleton
|
||||
}
|
||||
|
||||
# Runs the `afterRule` hook and returns if the registration has
|
||||
# been prevented.
|
||||
return unless @_runHook 'afterRule', ctx
|
||||
|
||||
{key, data, val} = ctx
|
||||
|
||||
# The default key.
|
||||
key ?= if singleton then -> name else -> @genkey
|
||||
|
||||
# The default value.
|
||||
val ?= -> @genval
|
||||
|
||||
# Makes sure `key` is a function for uniformity.
|
||||
key = $makeFunction key unless $_.isFunction key
|
||||
|
||||
# Register the new rule.
|
||||
@_rules[name] = {
|
||||
name
|
||||
key
|
||||
data
|
||||
val
|
||||
singleton
|
||||
}
|
||||
|
||||
#--------------------------------
|
||||
|
||||
get: (keys, ignoreMissingItems = false) ->
|
||||
if keys is undefined
|
||||
items = $_.map @_byKey, (item) -> item.val
|
||||
else
|
||||
items = @_fetchItems keys, ignoreMissingItems
|
||||
$mapInPlace items, (item) -> item.val
|
||||
|
||||
if $_.isString keys then items[0] else items
|
||||
|
||||
getRaw: (keys, ignoreMissingItems = false) ->
|
||||
if keys is undefined
|
||||
item for _, item of @_byKey
|
||||
else
|
||||
items = @_fetchItems keys, ignoreMissingItems
|
||||
|
||||
if $_.isString keys then items[0] else items
|
||||
|
||||
remove: (keys, ignoreMissingItems = false) ->
|
||||
@_removeItems (@_fetchItems keys, ignoreMissingItems)
|
||||
|
||||
set: (items, {add, update, remove} = {}) ->
|
||||
add = true unless add?
|
||||
update = true unless update?
|
||||
remove = false unless remove?
|
||||
|
||||
itemsToAdd = {}
|
||||
itemsToUpdate = {}
|
||||
|
||||
itemsToRemove = {}
|
||||
$_.extend itemsToRemove, @_byKey if remove
|
||||
|
||||
$each items, (genval, genkey) =>
|
||||
item = {
|
||||
rule: undefined
|
||||
key: undefined
|
||||
data: undefined
|
||||
val: undefined
|
||||
genkey
|
||||
genval
|
||||
}
|
||||
|
||||
return unless @_runHook 'beforeDispatch', item
|
||||
|
||||
# Searches for a rule to handle it.
|
||||
ruleName = @dispatch.call item
|
||||
rule = @_rules[ruleName]
|
||||
|
||||
unless rule?
|
||||
@missingRule ruleName
|
||||
|
||||
# If `missingRule()` has not created the rule, just keep this
|
||||
# item.
|
||||
rule = @_rules[ruleName]
|
||||
return unless rule?
|
||||
|
||||
# Checks if this is a singleton.
|
||||
@_assert(
|
||||
not rule.singleton
|
||||
"cannot add items to singleton rule “#{rule.name}”"
|
||||
)
|
||||
|
||||
# Computes its key.
|
||||
key = rule.key.call item
|
||||
|
||||
@_assert(
|
||||
$_.isString key
|
||||
"the key “#{key}” is not a string"
|
||||
)
|
||||
|
||||
# Updates known values.
|
||||
item.rule = rule.name
|
||||
item.key = key
|
||||
|
||||
if key of @_byKey
|
||||
# Marks this item as not to be removed.
|
||||
delete itemsToRemove[key]
|
||||
|
||||
if update
|
||||
# Fetches the existing entry.
|
||||
prev = @_byKey[key]
|
||||
|
||||
# Checks if there is a conflict in rules.
|
||||
unless item.rule is prev.rule
|
||||
@ruleConflict item.rule, prev
|
||||
item.prevRule = prev.rule
|
||||
else
|
||||
delete item.prevRule
|
||||
|
||||
# Gets its previous data/value.
|
||||
item.data = prev.data
|
||||
item.val = prev.val
|
||||
|
||||
# Registers the item to be updated.
|
||||
itemsToUpdate[key] = item
|
||||
|
||||
# Note: an item will be updated only once per `set()` and
|
||||
# only the last generator will be used.
|
||||
else
|
||||
if add
|
||||
|
||||
# Registers the item to be added.
|
||||
itemsToAdd[key] = item
|
||||
|
||||
# Adds items.
|
||||
@_updateItems itemsToAdd, true
|
||||
|
||||
# Updates items.
|
||||
@_updateItems itemsToUpdate
|
||||
|
||||
# Removes any items not seen (iff `remove` is true).
|
||||
@_removeItems itemsToRemove
|
||||
|
||||
# Forces items to update their value.
|
||||
touch: (keys) ->
|
||||
@_updateItems (@_fetchItems keys, true)
|
||||
|
||||
#--------------------------------
|
||||
|
||||
_assert: (cond, message) ->
|
||||
throw new Error message unless cond
|
||||
|
||||
# Emits item related event.
|
||||
_emitEvent: (event, items) ->
|
||||
getRule = if event is 'exit'
|
||||
(item) -> item.prevRule or item.rule
|
||||
else
|
||||
(item) -> item.rule
|
||||
|
||||
byRule = Object.create null
|
||||
|
||||
# One per item.
|
||||
$each items, (item) =>
|
||||
@emit "key=#{item.key}", event, item
|
||||
|
||||
(byRule[getRule item] ?= []).push item
|
||||
|
||||
# One per rule.
|
||||
@emit "rule=#{rule}", event, byRule[rule] for rule of byRule
|
||||
|
||||
# One for everything.
|
||||
@emit 'any', event, items
|
||||
|
||||
_fetchItems: (keys, ignoreMissingItems = false) ->
|
||||
unless $_.isArray keys
|
||||
keys = if $_.isObject keys then $_.keys keys else [keys]
|
||||
|
||||
items = []
|
||||
for key in keys
|
||||
item = @_byKey[key]
|
||||
if item?
|
||||
items.push item
|
||||
else
|
||||
@_assert(
|
||||
ignoreMissingItems
|
||||
"no item with key “#{key}”"
|
||||
)
|
||||
items
|
||||
|
||||
_removeItems: (items) ->
|
||||
return if $_.isEmpty items
|
||||
|
||||
$each items, (item) => delete @_byKey[item.key]
|
||||
|
||||
@_emitEvent 'exit', items
|
||||
|
||||
|
||||
# Runs hooks for the moment `name` with the given context and
|
||||
# returns false if the default action has been prevented.
|
||||
_runHook: (name, ctx) ->
|
||||
hooks = @_hooks[name]
|
||||
|
||||
# If no hooks, nothing to do.
|
||||
return true unless hooks? and (n = hooks.length) isnt 0
|
||||
|
||||
# Flags controlling the run.
|
||||
notStopped = true
|
||||
actionNotPrevented = true
|
||||
|
||||
# Creates the event object.
|
||||
event = {
|
||||
stopPropagation: -> notStopped = false
|
||||
|
||||
# TODO: Should `preventDefault()` imply `stopPropagation()`?
|
||||
preventDefault: -> actionNotPrevented = false
|
||||
}
|
||||
|
||||
i = 0
|
||||
while notStopped and i < n
|
||||
hooks[i++].call ctx, event
|
||||
|
||||
# TODO: Is exception handling necessary to have the wanted
|
||||
# behavior?
|
||||
|
||||
return actionNotPrevented
|
||||
|
||||
_updateItems: (items, areNew) ->
|
||||
return if $_.isEmpty items
|
||||
|
||||
# An update is similar to an exit followed by an enter.
|
||||
@_removeItems items unless areNew
|
||||
|
||||
$each items, (item) =>
|
||||
return unless @_runHook 'beforeUpdate', item
|
||||
|
||||
{rule: ruleName} = item
|
||||
|
||||
# Computes its value.
|
||||
do =>
|
||||
# Item is not passed directly to function to avoid direct
|
||||
# modification.
|
||||
#
|
||||
# This is not a true security but better than nothing.
|
||||
proxy = Object.create item
|
||||
|
||||
updateValue = (parent, prop, def) ->
|
||||
if not $_.isObject def
|
||||
parent[prop] = def
|
||||
else if $_.isFunction def
|
||||
parent[prop] = def.call proxy, parent[prop]
|
||||
else if $_.isArray def
|
||||
i = 0
|
||||
n = def.length
|
||||
|
||||
current = parent[prop] ?= new Array n
|
||||
while i < n
|
||||
updateValue current, i, def[i]
|
||||
++i
|
||||
else
|
||||
# It's a plain object.
|
||||
current = parent[prop] ?= {}
|
||||
for i of def
|
||||
updateValue current, i, def[i]
|
||||
|
||||
updateValue item, 'data', @_rules[ruleName].data
|
||||
updateValue item, 'val', @_rules[ruleName].val
|
||||
|
||||
unless @_runHook 'beforeSave', item
|
||||
# FIXME: should not be removed, only not saved.
|
||||
delete @_byKey[item.key]
|
||||
|
||||
# Really inserts the items and trigger events.
|
||||
$each items, (item) => @_byKey[item.key] = item
|
||||
@_emitEvent 'enter', items
|
||||
|
||||
#=====================================================================
|
||||
|
||||
module.exports = {$MappedCollection}
|
||||
@@ -1,121 +0,0 @@
|
||||
{expect: $expect} = require 'chai'
|
||||
|
||||
$sinon = require 'sinon'
|
||||
|
||||
#---------------------------------------------------------------------
|
||||
|
||||
{$MappedCollection} = require './MappedCollection.coffee'
|
||||
|
||||
#=====================================================================
|
||||
|
||||
describe '$MappedCollection', ->
|
||||
|
||||
# Shared variables.
|
||||
collection = null
|
||||
|
||||
beforeEach ->
|
||||
collection = new $MappedCollection()
|
||||
|
||||
#-------------------------------------------------------------------
|
||||
|
||||
describe '#dispatch()', ->
|
||||
|
||||
# Test data.
|
||||
beforeEach ->
|
||||
collection.rule test: {}
|
||||
|
||||
#------------------------------
|
||||
|
||||
it 'should have genkey and genval', ->
|
||||
collection.dispatch = ->
|
||||
$expect(@genkey).to.equal 'a key'
|
||||
$expect(@genval).to.equal 'a value'
|
||||
|
||||
'test'
|
||||
|
||||
collection.set {
|
||||
'a key': 'a value'
|
||||
}
|
||||
|
||||
#------------------------------
|
||||
|
||||
it 'should be used to dispatch an item', ->
|
||||
collection.dispatch = -> 'test'
|
||||
|
||||
collection.set [
|
||||
'any value'
|
||||
]
|
||||
|
||||
$expect(collection.getRaw('0').rule).to.equal 'test'
|
||||
|
||||
#-------------------------------------------------------------------
|
||||
|
||||
describe 'item hooks', ->
|
||||
|
||||
# Test data.
|
||||
beforeEach ->
|
||||
collection.rule test: {}
|
||||
|
||||
#------------------------------
|
||||
|
||||
it 'should be called in the correct order', ->
|
||||
|
||||
beforeDispatch = $sinon.spy()
|
||||
collection.hook {beforeDispatch}
|
||||
|
||||
dispatcher = $sinon.spy ->
|
||||
$expect(beforeDispatch.called).to.true
|
||||
|
||||
# It still is a dispatcher.
|
||||
'test'
|
||||
collection.dispatch = dispatcher
|
||||
|
||||
beforeUpdate = $sinon.spy ->
|
||||
$expect(dispatcher.called).to.true
|
||||
collection.hook {beforeUpdate}
|
||||
|
||||
beforeSave = $sinon.spy ->
|
||||
$expect(beforeUpdate.called).to.true
|
||||
collection.hook {beforeSave}
|
||||
|
||||
collection.set [
|
||||
'any value'
|
||||
]
|
||||
|
||||
$expect(beforeSave.called).to.be.true
|
||||
|
||||
#-------------------------------------------------------------------
|
||||
|
||||
describe 'adding new items', ->
|
||||
|
||||
beforeEach ->
|
||||
collection.rule test: {}
|
||||
collection.dispatch = -> 'test'
|
||||
|
||||
#------------------------------
|
||||
|
||||
it 'should trigger three `enter` events', ->
|
||||
keySpy = $sinon.spy()
|
||||
ruleSpy = $sinon.spy()
|
||||
anySpy = $sinon.spy()
|
||||
|
||||
collection.on 'key=a key', keySpy
|
||||
collection.on 'rule=test', ruleSpy
|
||||
collection.on 'any', anySpy
|
||||
|
||||
collection.set {
|
||||
'a key': 'a value'
|
||||
}
|
||||
|
||||
item = collection.getRaw 'a key'
|
||||
|
||||
# TODO: items can be an array or a object (it is not defined).
|
||||
$expect(keySpy.args).to.deep.equal [
|
||||
['enter', item]
|
||||
]
|
||||
$expect(ruleSpy.args).to.deep.equal [
|
||||
['enter', [item]]
|
||||
]
|
||||
$expect(anySpy.args).to.deep.equal [
|
||||
['enter', {'a key': item}]
|
||||
]
|
||||
70
src/api-errors.js
Normal file
70
src/api-errors.js
Normal file
@@ -0,0 +1,70 @@
|
||||
import {JsonRpcError} from 'json-rpc-peer'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
// Export standard JSON-RPC errors.
|
||||
export { // eslint-disable-line no-duplicate-imports
|
||||
InvalidJson,
|
||||
InvalidParameters,
|
||||
InvalidRequest,
|
||||
JsonRpcError,
|
||||
MethodNotFound
|
||||
} from 'json-rpc-peer'
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export class NotImplemented extends JsonRpcError {
|
||||
constructor () {
|
||||
super('not implemented', 0)
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export class NoSuchObject extends JsonRpcError {
|
||||
constructor (id, type) {
|
||||
super('no such object', 1, {id, type})
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export class Unauthorized extends JsonRpcError {
|
||||
constructor () {
|
||||
super('not authenticated or not enough permissions', 2)
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export class InvalidCredential extends JsonRpcError {
|
||||
constructor () {
|
||||
super('invalid credential', 3)
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export class AlreadyAuthenticated extends JsonRpcError {
|
||||
constructor () {
|
||||
super('already authenticated', 4)
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export class ForbiddenOperation extends JsonRpcError {
|
||||
constructor (operation, reason) {
|
||||
super(`forbidden operation: ${operation}`, 5, reason)
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
// To be used with a user-readable message.
|
||||
// The message can be destined to be displayed to the front-end user.
|
||||
export class GenericError extends JsonRpcError {
|
||||
constructor (message) {
|
||||
super(message, 6)
|
||||
}
|
||||
}
|
||||
337
src/api.js
337
src/api.js
@@ -1,337 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
|
||||
var $_ = require('underscore');
|
||||
|
||||
var $requireTree = require('require-tree');
|
||||
|
||||
var $schemaInspector = require('schema-inspector');
|
||||
|
||||
//--------------------------------------------------------------------
|
||||
|
||||
var $wait = require('./fibers-utils').$wait;
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
|
||||
function $deprecated(fn)
|
||||
{
|
||||
return function (session, req) {
|
||||
console.warn(req.method +' is deprecated!');
|
||||
|
||||
return fn.apply(this, arguments);
|
||||
};
|
||||
}
|
||||
|
||||
var wrap = function (val) {
|
||||
return function () {
|
||||
return val;
|
||||
};
|
||||
};
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
|
||||
// TODO: Helper functions that could be written:
|
||||
// - checkParams(req.params, param1, ..., paramN)
|
||||
|
||||
var helpers = {};
|
||||
|
||||
helpers.checkPermission = function (permission)
|
||||
{
|
||||
// TODO: Handle token permission.
|
||||
|
||||
var userId = this.session.get('user_id', undefined);
|
||||
|
||||
if (undefined === userId)
|
||||
{
|
||||
throw Api.err.UNAUTHORIZED;
|
||||
}
|
||||
|
||||
if (!permission)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var user = $wait(this.users.first(userId));
|
||||
// The user MUST exist at this time.
|
||||
|
||||
if (!user.hasPermission(permission))
|
||||
{
|
||||
throw Api.err.UNAUTHORIZED;
|
||||
}
|
||||
};
|
||||
|
||||
// Checks and returns parameters.
|
||||
helpers.getParams = function (schema) {
|
||||
var params = this.request.params;
|
||||
|
||||
schema = {
|
||||
type: 'object',
|
||||
properties: schema,
|
||||
};
|
||||
|
||||
var result = $schemaInspector.validate(schema, params);
|
||||
|
||||
if (!result.valid)
|
||||
{
|
||||
this.throw('INVALID_PARAMS', result.error);
|
||||
}
|
||||
|
||||
return params;
|
||||
};
|
||||
|
||||
helpers.getUserPublicProperties = function (user) {
|
||||
// Handles both properties and wrapped models.
|
||||
var properties = user.properties || user;
|
||||
|
||||
return $_.pick(properties, 'id', 'email', 'permission');
|
||||
};
|
||||
|
||||
helpers.getServerPublicProperties = function (server) {
|
||||
// Handles both properties and wrapped models.
|
||||
var properties = server.properties || server;
|
||||
|
||||
return $_.pick(properties, 'id', 'host', 'username');
|
||||
};
|
||||
|
||||
helpers.throw = function (errorId, data) {
|
||||
var error = Api.err[errorId];
|
||||
|
||||
if (!error)
|
||||
{
|
||||
console.error('Invalid error:', errorId);
|
||||
throw Api.err.SERVER_ERROR;
|
||||
}
|
||||
|
||||
if (data)
|
||||
{
|
||||
error = $_.extend({}, error, {data: data});
|
||||
}
|
||||
|
||||
throw error;
|
||||
};
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
|
||||
function Api(xo)
|
||||
{
|
||||
if ( !(this instanceof Api) )
|
||||
{
|
||||
return new Api(xo);
|
||||
}
|
||||
|
||||
this.xo = xo;
|
||||
}
|
||||
|
||||
Api.prototype.exec = function (session, request) {
|
||||
var ctx = Object.create(this.xo);
|
||||
$_.extend(ctx, helpers, {
|
||||
session: session,
|
||||
request: request,
|
||||
});
|
||||
|
||||
var method = this.getMethod(request.method);
|
||||
|
||||
if (!method)
|
||||
{
|
||||
console.warn('Invalid method: '+ request.method);
|
||||
throw Api.err.INVALID_METHOD;
|
||||
}
|
||||
|
||||
if ('permission' in method)
|
||||
{
|
||||
helpers.checkPermission.call(ctx, method.permission)
|
||||
}
|
||||
|
||||
if (method.params)
|
||||
{
|
||||
helpers.getParams.call(ctx, method.params);
|
||||
}
|
||||
|
||||
return method.call(ctx, request.params);
|
||||
};
|
||||
|
||||
Api.prototype.getMethod = function (name) {
|
||||
var parts = name.split('.');
|
||||
|
||||
var current = Api.fn;
|
||||
for (
|
||||
var i = 0, n = parts.length;
|
||||
(i < n) && (current = current[parts[i]]);
|
||||
++i
|
||||
)
|
||||
{
|
||||
/* jshint noempty:false */
|
||||
}
|
||||
|
||||
// Method found.
|
||||
if ($_.isFunction(current))
|
||||
{
|
||||
return current;
|
||||
}
|
||||
|
||||
// It's a (deprecated) alias.
|
||||
if ($_.isString(current))
|
||||
{
|
||||
return $deprecated(this.getMethod(current));
|
||||
}
|
||||
|
||||
// No entry found, looking for a catch-all method.
|
||||
current = Api.fn;
|
||||
var catchAll;
|
||||
for (i = 0; (i < n) && (current = current[parts[i]]); ++i)
|
||||
{
|
||||
catchAll = current.__catchAll || catchAll;
|
||||
}
|
||||
|
||||
return catchAll;
|
||||
};
|
||||
|
||||
module.exports = Api;
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
|
||||
function err(code, message)
|
||||
{
|
||||
return {
|
||||
'code': code,
|
||||
'message': message
|
||||
};
|
||||
}
|
||||
|
||||
Api.err = {
|
||||
|
||||
//////////////////////////////////////////////////////////////////
|
||||
// JSON-RPC errors.
|
||||
//////////////////////////////////////////////////////////////////
|
||||
|
||||
'INVALID_JSON': err(-32700, 'invalid JSON'),
|
||||
|
||||
'INVALID_REQUEST': err(-32600, 'invalid JSON-RPC request'),
|
||||
|
||||
'INVALID_METHOD': err(-32601, 'method not found'),
|
||||
|
||||
'INVALID_PARAMS': err(-32602, 'invalid parameter(s)'),
|
||||
|
||||
'SERVER_ERROR': err(-32603, 'unknown error from the server'),
|
||||
|
||||
//////////////////////////////////////////////////////////////////
|
||||
// XO errors.
|
||||
//////////////////////////////////////////////////////////////////
|
||||
|
||||
'NOT_IMPLEMENTED': err(0, 'not implemented'),
|
||||
|
||||
'NO_SUCH_OBJECT': err(1, 'no such object'),
|
||||
|
||||
// Not authenticated or not enough permissions.
|
||||
'UNAUTHORIZED': err(2, 'not authenticated or not enough permissions'),
|
||||
|
||||
// Invalid email & passwords or token.
|
||||
'INVALID_CREDENTIAL': err(3, 'invalid credential'),
|
||||
|
||||
'ALREADY_AUTHENTICATED': err(4, 'already authenticated'),
|
||||
};
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
|
||||
var $register = function (path, fn, params) {
|
||||
var component, current;
|
||||
|
||||
if (params)
|
||||
{
|
||||
fn.params = params;
|
||||
}
|
||||
|
||||
if (!$_.isArray(path))
|
||||
{
|
||||
path = path.split('.');
|
||||
}
|
||||
|
||||
current = Api.fn;
|
||||
for (var i = 0, n = path.length - 1; i < n; ++i)
|
||||
{
|
||||
component = path[i];
|
||||
current = (current[component] || (current[component] = {}));
|
||||
}
|
||||
|
||||
if ($_.isFunction(fn))
|
||||
{
|
||||
current[path[n]] = fn;
|
||||
}
|
||||
else if ($_.isObject(fn) && !$_.isArray(fn))
|
||||
{
|
||||
// If it is not an function but an object, copies its
|
||||
// properties.
|
||||
|
||||
component = path[n];
|
||||
current = (current[component] || (current[component] = {}));
|
||||
|
||||
for (var prop in fn)
|
||||
{
|
||||
current[prop] = fn[prop];
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
current[path[n]] = wrap(fn);
|
||||
}
|
||||
};
|
||||
|
||||
Api.fn = $requireTree('./api');
|
||||
|
||||
//--------------------------------------------------------------------
|
||||
|
||||
$register('system.getVersion', wrap('0.1'));
|
||||
|
||||
$register('xo.getAllObjects', function () {
|
||||
return this.getObjects();
|
||||
});
|
||||
|
||||
// Returns the list of available methods similar to XML-RPC
|
||||
// introspection (http://xmlrpc-c.sourceforge.net/introspection.html).
|
||||
(function () {
|
||||
var methods = {};
|
||||
|
||||
(function browse(container, path) {
|
||||
var n = path.length;
|
||||
$_.each(container, function (content, key) {
|
||||
path[n] = key;
|
||||
if ($_.isFunction(content))
|
||||
{
|
||||
methods[path.join('.')] = {
|
||||
description: content.description,
|
||||
params: content.params || {},
|
||||
permission: content.permission,
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
browse(content, path);
|
||||
}
|
||||
});
|
||||
path.pop();
|
||||
})(Api.fn, []);
|
||||
|
||||
$register('system.listMethods', wrap($_.keys(methods)));
|
||||
$register('system.methodSignature', function (params) {
|
||||
var method = methods[params.name];
|
||||
|
||||
if (!method)
|
||||
{
|
||||
this.throw('NO_SUCH_OBJECT');
|
||||
}
|
||||
|
||||
// XML-RPC can have multiple signatures per method.
|
||||
return [
|
||||
// XML-RPC requires the method name.
|
||||
$_.extend({name: name}, method)
|
||||
];
|
||||
}, {
|
||||
name: {
|
||||
description: 'method to describe',
|
||||
type: 'string',
|
||||
},
|
||||
});
|
||||
|
||||
$register('system.getMethodsInfo', wrap(methods));
|
||||
})();
|
||||
0
src/api/.index-modules
Normal file
0
src/api/.index-modules
Normal file
49
src/api/acl.js
Normal file
49
src/api/acl.js
Normal file
@@ -0,0 +1,49 @@
|
||||
export async function get () {
|
||||
return /* await */ this.getAllAcls()
|
||||
}
|
||||
|
||||
get.permission = 'admin'
|
||||
|
||||
get.description = 'get existing ACLs'
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export async function getCurrentPermissions () {
|
||||
return /* await */ this.getPermissionsForUser(this.session.get('user_id'))
|
||||
}
|
||||
|
||||
getCurrentPermissions.permission = ''
|
||||
|
||||
getCurrentPermissions.description = 'get (explicit) permissions by object for the current user'
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export async function add ({subject, object, action}) {
|
||||
await this.addAcl(subject, object, action)
|
||||
}
|
||||
|
||||
add.permission = 'admin'
|
||||
|
||||
add.params = {
|
||||
subject: { type: 'string' },
|
||||
object: { type: 'string' },
|
||||
action: { type: 'string' }
|
||||
}
|
||||
|
||||
add.description = 'add a new ACL entry'
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export async function remove ({subject, object, action}) {
|
||||
await this.removeAcl(subject, object, action)
|
||||
}
|
||||
|
||||
remove.permission = 'admin'
|
||||
|
||||
remove.params = {
|
||||
subject: { type: 'string' },
|
||||
object: { type: 'string' },
|
||||
action: { type: 'string' }
|
||||
}
|
||||
|
||||
remove.description = 'remove an existing ACL entry'
|
||||
40
src/api/disk.js
Normal file
40
src/api/disk.js
Normal file
@@ -0,0 +1,40 @@
|
||||
import {parseSize} from '../utils'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export async function create ({name, size, sr}) {
|
||||
const vdi = await this.getXapi(sr).createVdi(parseSize(size), {
|
||||
name_label: name,
|
||||
sr: sr._xapiId
|
||||
})
|
||||
return vdi.$id
|
||||
}
|
||||
|
||||
create.description = 'create a new disk on a SR'
|
||||
|
||||
create.params = {
|
||||
name: { type: 'string' },
|
||||
size: { type: ['integer', 'string'] },
|
||||
sr: { type: 'string' }
|
||||
}
|
||||
|
||||
create.resolve = {
|
||||
sr: ['sr', 'SR', 'administrate']
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export async function resize ({ vdi, size }) {
|
||||
await this.getXapi(vdi).resizeVdi(vdi._xapiId, parseSize(size))
|
||||
}
|
||||
|
||||
resize.description = 'resize an existing VDI'
|
||||
|
||||
resize.params = {
|
||||
id: { type: 'string' },
|
||||
size: { type: ['integer', 'string'] }
|
||||
}
|
||||
|
||||
resize.resolve = {
|
||||
vdi: ['id', ['VDI', 'VDI-snapshot'], 'administrate']
|
||||
}
|
||||
60
src/api/docker.js
Normal file
60
src/api/docker.js
Normal file
@@ -0,0 +1,60 @@
|
||||
export async function register ({vm}) {
|
||||
await this.getXapi(vm).registerDockerContainer(vm._xapiId)
|
||||
}
|
||||
register.description = 'Register the VM for Docker management'
|
||||
|
||||
register.params = {
|
||||
vm: { type: 'string' }
|
||||
}
|
||||
|
||||
register.resolve = {
|
||||
vm: ['vm', 'VM', 'administrate']
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
export async function deregister ({vm}) {
|
||||
await this.getXapi(vm).unregisterDockerContainer(vm._xapiId)
|
||||
}
|
||||
deregister.description = 'Deregister the VM for Docker management'
|
||||
|
||||
deregister.params = {
|
||||
vm: { type: 'string' }
|
||||
}
|
||||
|
||||
deregister.resolve = {
|
||||
vm: ['vm', 'VM', 'administrate']
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
export async function start ({vm, container}) {
|
||||
await this.getXapi(vm).startDockerContainer(vm._xapiId, container)
|
||||
}
|
||||
|
||||
export async function stop ({vm, container}) {
|
||||
await this.getXapi(vm).stopDockerContainer(vm._xapiId, container)
|
||||
}
|
||||
|
||||
export async function restart ({vm, container}) {
|
||||
await this.getXapi(vm).restartDockerContainer(vm._xapiId, container)
|
||||
}
|
||||
|
||||
export async function pause ({vm, container}) {
|
||||
await this.getXapi(vm).pauseDockerContainer(vm._xapiId, container)
|
||||
}
|
||||
|
||||
export async function unpause ({vm, container}) {
|
||||
await this.getXapi(vm).unpauseDockerContainer(vm._xapiId, container)
|
||||
}
|
||||
|
||||
for (let fn of [start, stop, restart, pause, unpause]) {
|
||||
fn.params = {
|
||||
vm: { type: 'string' },
|
||||
container: { type: 'string' }
|
||||
}
|
||||
|
||||
fn.resolve = {
|
||||
vm: ['vm', 'VM', 'operate']
|
||||
}
|
||||
}
|
||||
91
src/api/group.js
Normal file
91
src/api/group.js
Normal file
@@ -0,0 +1,91 @@
|
||||
export async function create ({name}) {
|
||||
return (await this.createGroup({name})).id
|
||||
}
|
||||
|
||||
create.description = 'creates a new group'
|
||||
create.permission = 'admin'
|
||||
create.params = {
|
||||
name: {type: 'string'}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
// Deletes an existing group.
|
||||
async function delete_ ({id}) {
|
||||
await this.deleteGroup(id)
|
||||
}
|
||||
|
||||
// delete is not a valid identifier.
|
||||
export {delete_ as delete}
|
||||
|
||||
delete_.description = 'deletes an existing group'
|
||||
delete_.permission = 'admin'
|
||||
delete_.params = {
|
||||
id: {type: 'string'}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export async function getAll () {
|
||||
return /* await */ this.getAllGroups()
|
||||
}
|
||||
|
||||
getAll.description = 'returns all the existing group'
|
||||
getAll.permission = 'admin'
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
// sets group.users with an array of user ids
|
||||
export async function setUsers ({id, userIds}) {
|
||||
await this.setGroupUsers(id, userIds)
|
||||
}
|
||||
|
||||
setUsers.description = 'sets the users belonging to a group'
|
||||
setUsers.permission = 'admin'
|
||||
setUsers.params = {
|
||||
id: {type: 'string'},
|
||||
userIds: {}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
// adds the user id to group.users
|
||||
export async function addUser ({id, userId}) {
|
||||
await this.addUserToGroup(userId, id)
|
||||
}
|
||||
|
||||
addUser.description = 'adds a user to a group'
|
||||
addUser.permission = 'admin'
|
||||
addUser.params = {
|
||||
id: {type: 'string'},
|
||||
userId: {type: 'string'}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
// remove the user id from group.users
|
||||
export async function removeUser ({id, userId}) {
|
||||
await this.removeUserFromGroup(userId, id)
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
removeUser.description = 'removes a user from a group'
|
||||
removeUser.permission = 'admin'
|
||||
removeUser.params = {
|
||||
id: {type: 'string'},
|
||||
userId: {type: 'string'}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export async function set ({id, name}) {
|
||||
await this.updateGroup(id, {name})
|
||||
}
|
||||
|
||||
set.description = 'changes the properties of an existing group'
|
||||
set.permission = 'admin'
|
||||
set.params = {
|
||||
id: { type: 'string' },
|
||||
name: { type: 'string', optional: true }
|
||||
}
|
||||
@@ -1,27 +1,32 @@
|
||||
{$wait} = require '../fibers-utils'
|
||||
$debug = (require 'debug') 'xo:api:vm'
|
||||
$find = require 'lodash/find'
|
||||
$findIndex = require 'lodash/findIndex'
|
||||
$forEach = require 'lodash/forEach'
|
||||
endsWith = require 'lodash/endsWith'
|
||||
startsWith = require 'lodash/startsWith'
|
||||
{coroutine: $coroutine} = require 'bluebird'
|
||||
{
|
||||
extractProperty,
|
||||
parseXml
|
||||
} = require '../utils'
|
||||
|
||||
#=====================================================================
|
||||
|
||||
exports.set = (params) ->
|
||||
try
|
||||
host = @getObject params.id
|
||||
catch
|
||||
@throw 'NO_SUCH_OBJECT'
|
||||
set = ({
|
||||
host,
|
||||
|
||||
xapi = @getXAPI host
|
||||
# TODO: use camel case.
|
||||
name_label: nameLabel,
|
||||
name_description: nameDescription
|
||||
}) ->
|
||||
return @getXapi(host).setHostProperties(host._xapiId, {
|
||||
nameLabel,
|
||||
nameDescription
|
||||
})
|
||||
|
||||
for param, field of {
|
||||
'name_label'
|
||||
'name_description'
|
||||
'enabled'
|
||||
}
|
||||
continue unless param of params
|
||||
set.description = 'changes the properties of an host'
|
||||
|
||||
$wait xapi.call "host.set_#{field}", host.ref, params[param]
|
||||
|
||||
return true
|
||||
exports.set.permission = 'admin'
|
||||
exports.set.params =
|
||||
set.params =
|
||||
id: type: 'string'
|
||||
name_label:
|
||||
type: 'string'
|
||||
@@ -29,106 +34,234 @@ exports.set.params =
|
||||
name_description:
|
||||
type: 'string'
|
||||
optional: true
|
||||
enabled:
|
||||
type: 'boolean'
|
||||
|
||||
set.resolve = {
|
||||
host: ['id', 'host', 'administrate'],
|
||||
}
|
||||
|
||||
exports.set = set
|
||||
|
||||
#---------------------------------------------------------------------
|
||||
|
||||
# FIXME: set force to false per default when correctly implemented in
|
||||
# UI.
|
||||
restart = ({host, force = true}) ->
|
||||
return @getXapi(host).rebootHost(host._xapiId, force)
|
||||
|
||||
restart.description = 'restart the host'
|
||||
|
||||
restart.params = {
|
||||
id: { type: 'string' },
|
||||
force: {
|
||||
type: 'boolean',
|
||||
optional: true
|
||||
}
|
||||
}
|
||||
|
||||
exports.restart = ({id}) ->
|
||||
@checkPermission 'admin'
|
||||
restart.resolve = {
|
||||
host: ['id', 'host', 'operate'],
|
||||
}
|
||||
|
||||
try
|
||||
host = @getObject id
|
||||
catch
|
||||
@throw 'NO_SUCH_OBJECT'
|
||||
exports.restart = restart
|
||||
|
||||
xapi = @getXAPI host
|
||||
#---------------------------------------------------------------------
|
||||
|
||||
$wait xapi.call 'host.disable', host.ref
|
||||
$wait xapi.call 'host.reboot', host.ref
|
||||
restartAgent = ({host}) ->
|
||||
return @getXapi(host).restartHostAgent(host._xapiId)
|
||||
|
||||
return true
|
||||
exports.restart.permission = 'admin'
|
||||
exports.restart.params = {
|
||||
restartAgent.description = 'restart the Xen agent on the host'
|
||||
|
||||
restartAgent.params = {
|
||||
id: { type: 'string' }
|
||||
}
|
||||
|
||||
exports.restart_agent = ({id}) ->
|
||||
try
|
||||
host = @getObject id
|
||||
catch
|
||||
@throw 'NO_SUCH_OBJECT'
|
||||
restartAgent.resolve = {
|
||||
host: ['id', 'host', 'administrate'],
|
||||
}
|
||||
|
||||
xapi = @getXAPI host
|
||||
# TODO camel case
|
||||
exports.restart_agent = restartAgent
|
||||
|
||||
$wait xapi.call 'host.restart_agent', host.ref
|
||||
#---------------------------------------------------------------------
|
||||
|
||||
return true
|
||||
exports.restart_agent.permission = 'admin'
|
||||
exports.restart_agent.params = {
|
||||
start = ({host}) ->
|
||||
return @getXapi(host).powerOnHost(host._xapiId)
|
||||
|
||||
start.description = 'start the host'
|
||||
|
||||
start.params = {
|
||||
id: { type: 'string' }
|
||||
}
|
||||
|
||||
exports.stop = ({id}) ->
|
||||
try
|
||||
host = @getObject id
|
||||
catch
|
||||
@throw 'NO_SUCH_OBJECT'
|
||||
start.resolve = {
|
||||
host: ['id', 'host', 'operate'],
|
||||
}
|
||||
|
||||
xapi = @getXAPI host
|
||||
exports.start = start
|
||||
|
||||
$wait xapi.call 'host.disable', host.ref
|
||||
$wait xapi.call 'host.shutdown', host.ref
|
||||
#---------------------------------------------------------------------
|
||||
|
||||
return true
|
||||
exports.stop.permission = 'admin'
|
||||
exports.stop.params = {
|
||||
stop = ({host}) ->
|
||||
return @getXapi(host).shutdownHost(host._xapiId)
|
||||
|
||||
stop.description = 'stop the host'
|
||||
|
||||
stop.params = {
|
||||
id: { type: 'string' }
|
||||
}
|
||||
|
||||
exports.detach = ({id}) ->
|
||||
try
|
||||
host = @getObject id
|
||||
catch
|
||||
@throw 'NO_SUCH_OBJECT'
|
||||
stop.resolve = {
|
||||
host: ['id', 'host', 'operate'],
|
||||
}
|
||||
|
||||
xapi = @getXAPI host
|
||||
exports.stop = stop
|
||||
|
||||
$wait xapi.call 'pool.eject', host.ref
|
||||
#---------------------------------------------------------------------
|
||||
|
||||
return true
|
||||
exports.detach.permission = 'admin'
|
||||
exports.detach.params = {
|
||||
detach = ({host}) ->
|
||||
return @getXapi(host).ejectHostFromPool(host._xapiId)
|
||||
|
||||
detach.description = 'eject the host of a pool'
|
||||
|
||||
detach.params = {
|
||||
id: { type: 'string' }
|
||||
}
|
||||
|
||||
exports.enable = ({id}) ->
|
||||
try
|
||||
host = @getObject id
|
||||
catch
|
||||
@throw 'NO_SUCH_OBJECT'
|
||||
detach.resolve = {
|
||||
host: ['id', 'host', 'administrate'],
|
||||
}
|
||||
|
||||
xapi = @getXAPI host
|
||||
exports.detach = detach
|
||||
|
||||
$wait xapi.call 'host.enable', host.ref
|
||||
#---------------------------------------------------------------------
|
||||
|
||||
return true
|
||||
exports.stop.permission = 'admin'
|
||||
exports.stop.params = {
|
||||
enable = ({host}) ->
|
||||
return @getXapi(host).enableHost(host._xapiId)
|
||||
|
||||
enable.description = 'enable to create VM on the host'
|
||||
|
||||
enable.params = {
|
||||
id: { type: 'string' }
|
||||
}
|
||||
|
||||
exports.disable = ({id}) ->
|
||||
try
|
||||
host = @getObject id
|
||||
catch
|
||||
@throw 'NO_SUCH_OBJECT'
|
||||
enable.resolve = {
|
||||
host: ['id', 'host', 'administrate'],
|
||||
}
|
||||
|
||||
xapi = @getXAPI host
|
||||
exports.enable = enable
|
||||
|
||||
$wait xapi.call 'host.disable', host.ref
|
||||
#---------------------------------------------------------------------
|
||||
|
||||
return true
|
||||
exports.stop.permission = 'admin'
|
||||
exports.stop.params = {
|
||||
disable = ({host}) ->
|
||||
return @getXapi(host).disableHost(host._xapiId)
|
||||
|
||||
disable.description = 'disable to create VM on the hsot'
|
||||
|
||||
disable.params = {
|
||||
id: { type: 'string' }
|
||||
}
|
||||
}
|
||||
|
||||
disable.resolve = {
|
||||
host: ['id', 'host', 'administrate'],
|
||||
}
|
||||
|
||||
exports.disable = disable
|
||||
|
||||
#---------------------------------------------------------------------
|
||||
# Returns an array of missing new patches in the host
|
||||
# Returns an empty array if up-to-date
|
||||
# Throws an error if the host is not running the latest XS version
|
||||
|
||||
listMissingPatches = ({host}) ->
|
||||
return @getXapi(host).listMissingPoolPatchesOnHost(host._xapiId)
|
||||
|
||||
listMissingPatches.params = {
|
||||
host: { type: 'string' }
|
||||
}
|
||||
|
||||
listMissingPatches.resolve = {
|
||||
host: ['host', 'host', 'view'],
|
||||
}
|
||||
|
||||
exports.listMissingPatches = listMissingPatches
|
||||
|
||||
listMissingPatches.description = 'return an array of missing new patches in the host'
|
||||
|
||||
#---------------------------------------------------------------------
|
||||
|
||||
installPatch = ({host, patch: patchUuid}) ->
|
||||
return @getXapi(host).installPoolPatchOnHost(patchUuid, host._xapiId)
|
||||
|
||||
installPatch.description = 'install a patch on an host'
|
||||
|
||||
installPatch.params = {
|
||||
host: { type: 'string' }
|
||||
patch: { type: 'string' }
|
||||
}
|
||||
|
||||
installPatch.resolve = {
|
||||
host: ['host', 'host', 'administrate']
|
||||
}
|
||||
|
||||
exports.installPatch = installPatch
|
||||
|
||||
#---------------------------------------------------------------------
|
||||
|
||||
installAllPatches = ({host}) ->
|
||||
return @getXapi(host).installAllPoolPatchesOnHost(host._xapiId)
|
||||
|
||||
installAllPatches.description = 'install all the missing patches on a host'
|
||||
|
||||
installAllPatches.params = {
|
||||
host: { type: 'string' }
|
||||
}
|
||||
|
||||
installAllPatches.resolve = {
|
||||
host: ['host', 'host', 'administrate']
|
||||
}
|
||||
|
||||
exports.installAllPatches = installAllPatches
|
||||
|
||||
#---------------------------------------------------------------------
|
||||
|
||||
emergencyShutdownHost = ({host}) ->
|
||||
return @getXapi(host).emergencyShutdownHost(host._xapiId)
|
||||
|
||||
emergencyShutdownHost.description = 'suspend all VMs and shutdown host'
|
||||
|
||||
emergencyShutdownHost.params = {
|
||||
host: { type: 'string' }
|
||||
}
|
||||
|
||||
emergencyShutdownHost.resolve = {
|
||||
host: ['host', 'host', 'administrate']
|
||||
}
|
||||
|
||||
exports.emergencyShutdownHost = emergencyShutdownHost
|
||||
|
||||
#---------------------------------------------------------------------
|
||||
|
||||
stats = ({host, granularity}) ->
|
||||
return @getXapiHostStats(host, granularity)
|
||||
|
||||
stats.description = 'returns statistic of the host'
|
||||
|
||||
stats.params = {
|
||||
host: { type: 'string' },
|
||||
granularity: {
|
||||
type: 'string',
|
||||
optional: true
|
||||
}
|
||||
}
|
||||
|
||||
stats.resolve = {
|
||||
host: ['host', 'host', 'view']
|
||||
}
|
||||
|
||||
exports.stats = stats;
|
||||
|
||||
#=====================================================================
|
||||
|
||||
Object.defineProperty(exports, '__esModule', {
|
||||
value: true
|
||||
})
|
||||
|
||||
39
src/api/ip-pool.js
Normal file
39
src/api/ip-pool.js
Normal file
@@ -0,0 +1,39 @@
|
||||
import { Unauthorized } from '../api-errors'
|
||||
|
||||
export function create (props) {
|
||||
return this.createIpPool(props)
|
||||
}
|
||||
|
||||
create.permission = 'admin'
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
function delete_ ({ id }) {
|
||||
return this.deleteIpPool(id)
|
||||
}
|
||||
export { delete_ as delete }
|
||||
|
||||
delete_.permission = 'admin'
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export function getAll (params) {
|
||||
const { user } = this
|
||||
|
||||
if (!user) {
|
||||
throw new Unauthorized()
|
||||
}
|
||||
|
||||
return this.getAllIpPools(user.permission === 'admin'
|
||||
? params && params.userId
|
||||
: user.id
|
||||
)
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export function set ({ id, ...props }) {
|
||||
return this.updateIpPool(id, props)
|
||||
}
|
||||
|
||||
set.permission = 'admin'
|
||||
105
src/api/job.js
Normal file
105
src/api/job.js
Normal file
@@ -0,0 +1,105 @@
|
||||
// FIXME so far, no acls for jobs
|
||||
|
||||
export async function getAll () {
|
||||
return /* await */ this.getAllJobs()
|
||||
}
|
||||
|
||||
getAll.permission = 'admin'
|
||||
getAll.description = 'Gets all available jobs'
|
||||
|
||||
export async function get (id) {
|
||||
return /* await */ this.getJob(id)
|
||||
}
|
||||
|
||||
get.permission = 'admin'
|
||||
get.description = 'Gets an existing job'
|
||||
get.params = {
|
||||
id: {type: 'string'}
|
||||
}
|
||||
|
||||
export async function create ({job}) {
|
||||
job.userId = this.session.get('user_id')
|
||||
|
||||
return (await this.createJob(job)).id
|
||||
}
|
||||
|
||||
create.permission = 'admin'
|
||||
create.description = 'Creates a new job from description object'
|
||||
create.params = {
|
||||
job: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: {type: 'string', optional: true},
|
||||
type: {type: 'string'},
|
||||
key: {type: 'string'},
|
||||
method: {type: 'string'},
|
||||
paramsVector: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
type: {type: 'string'},
|
||||
items: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object'
|
||||
}
|
||||
}
|
||||
},
|
||||
optional: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function set ({job}) {
|
||||
await this.updateJob(job)
|
||||
}
|
||||
|
||||
set.permission = 'admin'
|
||||
set.description = 'Modifies an existing job from a description object'
|
||||
set.params = {
|
||||
job: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {type: 'string'},
|
||||
name: {type: 'string', optional: true},
|
||||
type: {type: 'string'},
|
||||
key: {type: 'string'},
|
||||
method: {type: 'string'},
|
||||
paramsVector: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
type: {type: 'string'},
|
||||
items: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object'
|
||||
}
|
||||
}
|
||||
},
|
||||
optional: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function delete_ ({id}) {
|
||||
await this.removeJob(id)
|
||||
}
|
||||
|
||||
delete_.permission = 'admin'
|
||||
delete_.description = 'Deletes an existing job'
|
||||
delete_.params = {
|
||||
id: {type: 'string'}
|
||||
}
|
||||
|
||||
export {delete_ as delete}
|
||||
|
||||
export async function runSequence ({idSequence}) {
|
||||
await this.runJobSequence(idSequence)
|
||||
}
|
||||
|
||||
runSequence.permission = 'admin'
|
||||
runSequence.description = 'Runs jobs sequentially, in the provided order'
|
||||
runSequence.params = {
|
||||
idSequence: {type: 'array', items: {type: 'string'}}
|
||||
}
|
||||
38
src/api/log.js
Normal file
38
src/api/log.js
Normal file
@@ -0,0 +1,38 @@
|
||||
export async function get ({namespace}) {
|
||||
const logger = await this.getLogger(namespace)
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const logs = {}
|
||||
|
||||
logger.createReadStream()
|
||||
.on('data', (data) => {
|
||||
logs[data.key] = data.value
|
||||
})
|
||||
.on('end', () => {
|
||||
resolve(logs)
|
||||
})
|
||||
.on('error', reject)
|
||||
})
|
||||
}
|
||||
|
||||
get.description = 'returns logs list for one namespace'
|
||||
get.params = {
|
||||
namespace: { type: 'string' }
|
||||
}
|
||||
get.permission = 'admin'
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
async function delete_ ({namespace, id}) {
|
||||
const logger = await this.getLogger(namespace)
|
||||
logger.del(id)
|
||||
}
|
||||
|
||||
delete_.description = 'deletes one or several logs from a namespace'
|
||||
delete_.params = {
|
||||
id: { type: [ 'array', 'string' ] },
|
||||
namespace: { type: 'string' }
|
||||
}
|
||||
delete_.permission = 'admin'
|
||||
|
||||
export {delete_ as delete}
|
||||
@@ -1,19 +0,0 @@
|
||||
{$wait} = require '../fibers-utils'
|
||||
|
||||
#=====================================================================
|
||||
|
||||
exports.delete = ({id}) ->
|
||||
try
|
||||
message = @getObject id
|
||||
catch
|
||||
@throw 'NO_SUCH_OBJECT'
|
||||
|
||||
xapi = @getXAPI message
|
||||
|
||||
$wait xapi.call 'message.destroy', message.ref
|
||||
|
||||
return true
|
||||
exports.delete.permission = 'admin'
|
||||
exports.delete.params =
|
||||
id:
|
||||
type: 'string'
|
||||
12
src/api/message.js
Normal file
12
src/api/message.js
Normal file
@@ -0,0 +1,12 @@
|
||||
async function delete_ ({ message }) {
|
||||
await this.getXapi(message).call('message.destroy', message._xapiRef)
|
||||
}
|
||||
export {delete_ as delete}
|
||||
|
||||
delete_.params = {
|
||||
id: { type: 'string' }
|
||||
}
|
||||
|
||||
delete_.resolve = {
|
||||
message: ['id', 'message', 'administrate']
|
||||
}
|
||||
118
src/api/network.js
Normal file
118
src/api/network.js
Normal file
@@ -0,0 +1,118 @@
|
||||
import { mapToArray } from '../utils'
|
||||
|
||||
export function getBondModes () {
|
||||
return ['balance-slb', 'active-backup', 'lacp']
|
||||
}
|
||||
|
||||
export async function create ({ pool, name, description, pif, mtu = 1500, vlan = 0 }) {
|
||||
return this.getXapi(pool).createNetwork({
|
||||
name,
|
||||
description,
|
||||
pifId: pif && this.getObject(pif, 'PIF')._xapiId,
|
||||
mtu: +mtu,
|
||||
vlan: +vlan
|
||||
})
|
||||
}
|
||||
|
||||
create.params = {
|
||||
pool: { type: 'string' },
|
||||
name: { type: 'string' },
|
||||
description: { type: 'string', optional: true },
|
||||
pif: { type: 'string', optional: true },
|
||||
mtu: { type: ['integer', 'string'], optional: true },
|
||||
vlan: { type: ['integer', 'string'], optional: true }
|
||||
}
|
||||
|
||||
create.resolve = {
|
||||
pool: ['pool', 'pool', 'administrate']
|
||||
}
|
||||
create.permission = 'admin'
|
||||
|
||||
// =================================================================
|
||||
|
||||
export async function createBonded ({ pool, name, description, pifs, mtu = 1500, mac, bondMode }) {
|
||||
return this.getXapi(pool).createBondedNetwork({
|
||||
name,
|
||||
description,
|
||||
pifIds: mapToArray(pifs, pif =>
|
||||
this.getObject(pif, 'PIF')._xapiId
|
||||
),
|
||||
mtu: +mtu,
|
||||
mac,
|
||||
bondMode
|
||||
})
|
||||
}
|
||||
|
||||
createBonded.params = {
|
||||
pool: { type: 'string' },
|
||||
name: { type: 'string' },
|
||||
description: { type: 'string', optional: true },
|
||||
pifs: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string'
|
||||
}
|
||||
},
|
||||
mtu: { type: ['integer', 'string'], optional: true },
|
||||
// RegExp since schema-inspector does not provide a param check based on an enumeration
|
||||
bondMode: { type: 'string', pattern: new RegExp(`^(${getBondModes().join('|')})$`) }
|
||||
}
|
||||
|
||||
createBonded.resolve = {
|
||||
pool: ['pool', 'pool', 'administrate']
|
||||
}
|
||||
createBonded.permission = 'admin'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export async function set ({
|
||||
network,
|
||||
|
||||
name_description: nameDescription,
|
||||
name_label: nameLabel,
|
||||
defaultIsLocked,
|
||||
id
|
||||
}) {
|
||||
await this.getXapi(network).setNetworkProperties(network._xapiId, {
|
||||
nameDescription,
|
||||
nameLabel,
|
||||
defaultIsLocked
|
||||
})
|
||||
}
|
||||
|
||||
set.params = {
|
||||
id: {
|
||||
type: 'string'
|
||||
},
|
||||
name_label: {
|
||||
type: 'string',
|
||||
optional: true
|
||||
},
|
||||
name_description: {
|
||||
type: 'string',
|
||||
optional: true
|
||||
},
|
||||
defaultIsLocked: {
|
||||
type: 'boolean',
|
||||
optional: true
|
||||
}
|
||||
}
|
||||
|
||||
set.resolve = {
|
||||
network: ['id', 'network', 'administrate']
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
|
||||
export async function delete_ ({ network }) {
|
||||
return this.getXapi(network).deleteNetwork(network._xapiId)
|
||||
}
|
||||
export {delete_ as delete}
|
||||
|
||||
delete_.params = {
|
||||
id: { type: 'string' }
|
||||
}
|
||||
|
||||
delete_.resolve = {
|
||||
network: ['id', 'network', 'administrate']
|
||||
}
|
||||
49
src/api/pbd.js
Normal file
49
src/api/pbd.js
Normal file
@@ -0,0 +1,49 @@
|
||||
// FIXME: too low level, should be removed.
|
||||
|
||||
// ===================================================================
|
||||
// Delete
|
||||
|
||||
async function delete_ ({PBD}) {
|
||||
// TODO: check if PBD is attached before
|
||||
await this.getXapi(PBD).call('PBD.destroy', PBD._xapiRef)
|
||||
}
|
||||
export {delete_ as delete}
|
||||
|
||||
delete_.params = {
|
||||
id: { type: 'string' }
|
||||
}
|
||||
|
||||
delete_.resolve = {
|
||||
PBD: ['id', 'PBD', 'administrate']
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// Disconnect
|
||||
|
||||
export async function disconnect ({ pbd }) {
|
||||
return this.getXapi(pbd).unplugPbd(pbd._xapiId)
|
||||
}
|
||||
|
||||
disconnect.params = {
|
||||
id: { type: 'string' }
|
||||
}
|
||||
|
||||
disconnect.resolve = {
|
||||
pbd: ['id', 'PBD', 'administrate']
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// Connect
|
||||
|
||||
export async function connect ({PBD}) {
|
||||
// TODO: check if PBD is attached before
|
||||
await this.getXapi(PBD).call('PBD.plug', PBD._xapiRef)
|
||||
}
|
||||
|
||||
connect.params = {
|
||||
id: { type: 'string' }
|
||||
}
|
||||
|
||||
connect.resolve = {
|
||||
PBD: ['id', 'PBD', 'administrate']
|
||||
}
|
||||
93
src/api/pif.js
Normal file
93
src/api/pif.js
Normal file
@@ -0,0 +1,93 @@
|
||||
// TODO: too low level, move into host.
|
||||
|
||||
import { IPV4_CONFIG_MODES, IPV6_CONFIG_MODES } from '../xapi'
|
||||
|
||||
export function getIpv4ConfigurationModes () {
|
||||
return IPV4_CONFIG_MODES
|
||||
}
|
||||
|
||||
export function getIpv6ConfigurationModes () {
|
||||
return IPV6_CONFIG_MODES
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// Delete
|
||||
|
||||
async function delete_ ({pif}) {
|
||||
// TODO: check if PIF is attached before
|
||||
await this.getXapi(pif).call('PIF.destroy', pif._xapiRef)
|
||||
}
|
||||
export {delete_ as delete}
|
||||
|
||||
delete_.params = {
|
||||
id: { type: 'string' }
|
||||
}
|
||||
|
||||
delete_.resolve = {
|
||||
pif: ['id', 'PIF', 'administrate']
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// Disconnect
|
||||
|
||||
export async function disconnect ({pif}) {
|
||||
// TODO: check if PIF is attached before
|
||||
await this.getXapi(pif).call('PIF.unplug', pif._xapiRef)
|
||||
}
|
||||
|
||||
disconnect.params = {
|
||||
id: { type: 'string' }
|
||||
}
|
||||
|
||||
disconnect.resolve = {
|
||||
pif: ['id', 'PIF', 'administrate']
|
||||
}
|
||||
// ===================================================================
|
||||
// Connect
|
||||
|
||||
export async function connect ({pif}) {
|
||||
// TODO: check if PIF is attached before
|
||||
await this.getXapi(pif).call('PIF.plug', pif._xapiRef)
|
||||
}
|
||||
|
||||
connect.params = {
|
||||
id: { type: 'string' }
|
||||
}
|
||||
|
||||
connect.resolve = {
|
||||
pif: ['id', 'PIF', 'administrate']
|
||||
}
|
||||
// ===================================================================
|
||||
// Reconfigure IP
|
||||
|
||||
export async function reconfigureIp ({ pif, mode = 'DHCP', ip, netmask, gateway, dns }) {
|
||||
await this.getXapi(pif).call('PIF.reconfigure_ip', pif._xapiRef, mode, ip, netmask, gateway, dns)
|
||||
}
|
||||
|
||||
reconfigureIp.params = {
|
||||
id: { type: 'string', optional: true },
|
||||
mode: { type: 'string', optional: true },
|
||||
ip: { type: 'string', optional: true },
|
||||
netmask: { type: 'string', optional: true },
|
||||
gateway: { type: 'string', optional: true },
|
||||
dns: { type: 'string', optional: true }
|
||||
}
|
||||
|
||||
reconfigureIp.resolve = {
|
||||
pif: ['id', 'PIF', 'administrate']
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export async function editPif ({ pif, vlan }) {
|
||||
await this.getXapi(pif).editPif(pif._xapiId, { vlan })
|
||||
}
|
||||
|
||||
editPif.params = {
|
||||
id: { type: 'string' },
|
||||
vlan: { type: ['integer', 'string'] }
|
||||
}
|
||||
|
||||
editPif.resolve = {
|
||||
pif: ['id', 'PIF', 'administrate']
|
||||
}
|
||||
104
src/api/plugin.js
Normal file
104
src/api/plugin.js
Normal file
@@ -0,0 +1,104 @@
|
||||
export async function get () {
|
||||
return /* await */ this.getPlugins()
|
||||
}
|
||||
|
||||
get.description = 'returns a list of all installed plugins'
|
||||
|
||||
get.permission = 'admin'
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export async function configure ({ id, configuration }) {
|
||||
await this.configurePlugin(id, configuration)
|
||||
}
|
||||
|
||||
configure.description = 'sets the configuration of a plugin'
|
||||
|
||||
configure.params = {
|
||||
id: {
|
||||
type: 'string'
|
||||
},
|
||||
configuration: {}
|
||||
}
|
||||
|
||||
configure.permission = 'admin'
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export async function disableAutoload ({ id }) {
|
||||
await this.disablePluginAutoload(id)
|
||||
}
|
||||
|
||||
disableAutoload.description = ''
|
||||
|
||||
disableAutoload.params = {
|
||||
id: {
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
|
||||
disableAutoload.permission = 'admin'
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export async function enableAutoload ({ id }) {
|
||||
await this.enablePluginAutoload(id)
|
||||
}
|
||||
|
||||
enableAutoload.description = 'enables a plugin, allowing it to be loaded'
|
||||
|
||||
enableAutoload.params = {
|
||||
id: {
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
|
||||
enableAutoload.permission = 'admin'
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export async function load ({ id }) {
|
||||
await this.loadPlugin(id)
|
||||
}
|
||||
|
||||
load.description = 'loads a plugin'
|
||||
|
||||
load.params = {
|
||||
id: {
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
|
||||
load.permission = 'admin'
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export async function unload ({ id }) {
|
||||
await this.unloadPlugin(id)
|
||||
}
|
||||
|
||||
unload.description = 'unloads a plugin'
|
||||
|
||||
unload.params = {
|
||||
id: {
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
|
||||
unload.permission = 'admin'
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export async function purgeConfiguration ({ id }) {
|
||||
await this.purgePluginConfiguration(id)
|
||||
}
|
||||
|
||||
purgeConfiguration.description = 'removes a plugin configuration'
|
||||
|
||||
purgeConfiguration.params = {
|
||||
id: {
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
|
||||
purgeConfiguration.permission = 'admin'
|
||||
@@ -1,31 +0,0 @@
|
||||
{$wait} = require '../fibers-utils'
|
||||
|
||||
#=====================================================================
|
||||
|
||||
exports.set = ->
|
||||
try
|
||||
pool = @getObject params.id
|
||||
catch
|
||||
@throw 'NO_SUCH_OBJECT'
|
||||
|
||||
xapi = @getXAPI pool
|
||||
|
||||
for param, field of {
|
||||
'name_label'
|
||||
'name_description'
|
||||
}
|
||||
continue unless param of params
|
||||
|
||||
$wait xapi.call "pool.set_#{field}", pool.ref, params[param]
|
||||
|
||||
return true
|
||||
exports.set.permission = 'admin'
|
||||
exports.set.params =
|
||||
id:
|
||||
type: 'string'
|
||||
name_label:
|
||||
type: 'string'
|
||||
optional: true
|
||||
name_description:
|
||||
type: 'string'
|
||||
optional: true
|
||||
160
src/api/pool.js
Normal file
160
src/api/pool.js
Normal file
@@ -0,0 +1,160 @@
|
||||
import {GenericError} from '../api-errors'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export async function set ({
|
||||
pool,
|
||||
|
||||
// TODO: use camel case.
|
||||
name_description: nameDescription,
|
||||
name_label: nameLabel
|
||||
}) {
|
||||
await this.getXapi(pool).setPoolProperties({
|
||||
nameDescription,
|
||||
nameLabel
|
||||
})
|
||||
}
|
||||
|
||||
set.params = {
|
||||
id: {
|
||||
type: 'string'
|
||||
},
|
||||
name_label: {
|
||||
type: 'string',
|
||||
optional: true
|
||||
},
|
||||
name_description: {
|
||||
type: 'string',
|
||||
optional: true
|
||||
}
|
||||
}
|
||||
|
||||
set.resolve = {
|
||||
pool: ['id', 'pool', 'administrate']
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export async function setDefaultSr ({ sr }) {
|
||||
await this.hasPermissions(this.user.id, [ [ sr.$pool, 'administrate' ] ])
|
||||
|
||||
await this.getXapi(sr).setDefaultSr(sr._xapiId)
|
||||
}
|
||||
|
||||
setDefaultSr.permission = '' // signed in
|
||||
|
||||
setDefaultSr.params = {
|
||||
sr: {
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
|
||||
setDefaultSr.resolve = {
|
||||
sr: ['sr', 'SR']
|
||||
}
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export async function installPatch ({pool, patch: patchUuid}) {
|
||||
await this.getXapi(pool).installPoolPatchOnAllHosts(patchUuid)
|
||||
}
|
||||
|
||||
installPatch.params = {
|
||||
pool: {
|
||||
type: 'string'
|
||||
},
|
||||
patch: {
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
|
||||
installPatch.resolve = {
|
||||
pool: ['pool', 'pool', 'administrate']
|
||||
}
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export async function installAllPatches ({ pool }) {
|
||||
await this.getXapi(pool).installAllPoolPatchesOnAllHosts()
|
||||
}
|
||||
|
||||
installPatch.params = {
|
||||
pool: {
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
|
||||
installPatch.resolve = {
|
||||
pool: ['pool', 'pool', 'administrate']
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
async function handlePatchUpload (req, res, {pool}) {
|
||||
const contentLength = req.headers['content-length']
|
||||
if (!contentLength) {
|
||||
res.writeHead(411)
|
||||
res.end('Content length is mandatory')
|
||||
return
|
||||
}
|
||||
|
||||
await this.getXapi(pool).uploadPoolPatch(req, contentLength)
|
||||
}
|
||||
|
||||
export async function uploadPatch ({pool}) {
|
||||
return {
|
||||
$sendTo: await this.registerHttpRequest(handlePatchUpload, {pool})
|
||||
}
|
||||
}
|
||||
|
||||
uploadPatch.params = {
|
||||
pool: { type: 'string' }
|
||||
}
|
||||
|
||||
uploadPatch.resolve = {
|
||||
pool: ['pool', 'pool', 'administrate']
|
||||
}
|
||||
|
||||
// Compatibility
|
||||
//
|
||||
// TODO: remove when no longer used in xo-web
|
||||
export {uploadPatch as patch}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export async function mergeInto ({ source, target, force }) {
|
||||
try {
|
||||
await this.mergeXenPools(source._xapiId, target._xapiId, force)
|
||||
} catch (e) {
|
||||
// FIXME: should we expose plain XAPI error messages?
|
||||
throw new GenericError(e.message)
|
||||
}
|
||||
}
|
||||
|
||||
mergeInto.params = {
|
||||
force: { type: 'boolean', optional: true },
|
||||
source: { type: 'string' },
|
||||
target: { type: 'string' }
|
||||
}
|
||||
|
||||
mergeInto.resolve = {
|
||||
source: ['source', 'pool', 'administrate'],
|
||||
target: ['target', 'pool', 'administrate']
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export async function getLicenseState ({pool}) {
|
||||
return this.getXapi(pool).call(
|
||||
'pool.get_license_state',
|
||||
pool._xapiId.$ref,
|
||||
)
|
||||
}
|
||||
|
||||
getLicenseState.params = {
|
||||
pool: {
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
|
||||
getLicenseState.resolve = {
|
||||
pool: ['pool', 'pool', 'administrate']
|
||||
}
|
||||
72
src/api/remote.js
Normal file
72
src/api/remote.js
Normal file
@@ -0,0 +1,72 @@
|
||||
export async function getAll () {
|
||||
return this.getAllRemotes()
|
||||
}
|
||||
|
||||
getAll.permission = 'admin'
|
||||
getAll.description = 'Gets all existing fs remote points'
|
||||
|
||||
export async function get ({id}) {
|
||||
return this.getRemote(id)
|
||||
}
|
||||
|
||||
get.permission = 'admin'
|
||||
get.description = 'Gets an existing fs remote point'
|
||||
get.params = {
|
||||
id: {type: 'string'}
|
||||
}
|
||||
|
||||
export async function test ({id}) {
|
||||
return this.testRemote(id)
|
||||
}
|
||||
|
||||
test.permission = 'admin'
|
||||
test.description = 'Performs a read/write matching test on a remote point'
|
||||
test.params = {
|
||||
id: {type: 'string'}
|
||||
}
|
||||
|
||||
export async function list ({id}) {
|
||||
return this.listRemoteBackups(id)
|
||||
}
|
||||
|
||||
list.permission = 'admin'
|
||||
list.description = 'Lists the files found in a remote point'
|
||||
list.params = {
|
||||
id: {type: 'string'}
|
||||
}
|
||||
|
||||
export async function create ({name, url}) {
|
||||
return this.createRemote({name, url})
|
||||
}
|
||||
|
||||
create.permission = 'admin'
|
||||
create.description = 'Creates a new fs remote point'
|
||||
create.params = {
|
||||
name: {type: 'string'},
|
||||
url: {type: 'string'}
|
||||
}
|
||||
|
||||
export async function set ({id, name, url, enabled}) {
|
||||
await this.updateRemote(id, {name, url, enabled})
|
||||
}
|
||||
|
||||
set.permission = 'admin'
|
||||
set.description = 'Modifies an existing fs remote point'
|
||||
set.params = {
|
||||
id: {type: 'string'},
|
||||
name: {type: 'string', optional: true},
|
||||
url: {type: 'string', optional: true},
|
||||
enabled: {type: 'boolean', optional: true}
|
||||
}
|
||||
|
||||
async function delete_ ({id}) {
|
||||
await this.removeRemote(id)
|
||||
}
|
||||
|
||||
delete_.permission = 'admin'
|
||||
delete_.description = 'Deletes an existing fs remote point'
|
||||
delete_.params = {
|
||||
id: {type: 'string'}
|
||||
}
|
||||
|
||||
export {delete_ as delete}
|
||||
237
src/api/resource-set.js
Normal file
237
src/api/resource-set.js
Normal file
@@ -0,0 +1,237 @@
|
||||
import {
|
||||
Unauthorized
|
||||
} from '../api-errors'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export function create ({ name, subjects, objects, limits }) {
|
||||
return this.createResourceSet(name, subjects, objects, limits)
|
||||
}
|
||||
|
||||
create.permission = 'admin'
|
||||
|
||||
create.params = {
|
||||
name: {
|
||||
type: 'string'
|
||||
},
|
||||
subjects: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string'
|
||||
},
|
||||
optional: true
|
||||
},
|
||||
objects: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string'
|
||||
},
|
||||
optional: true
|
||||
},
|
||||
limits: {
|
||||
type: 'object',
|
||||
optional: true
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
function delete_ ({ id }) {
|
||||
return this.deleteResourceSet(id)
|
||||
}
|
||||
export { delete_ as delete }
|
||||
|
||||
delete_.permission = 'admin'
|
||||
|
||||
delete_.params = {
|
||||
id: {
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export function set ({ id, name, subjects, objects, ipPools, limits }) {
|
||||
return this.updateResourceSet(id, {
|
||||
limits,
|
||||
name,
|
||||
objects,
|
||||
ipPools,
|
||||
subjects
|
||||
})
|
||||
}
|
||||
|
||||
set.permission = 'admin'
|
||||
|
||||
set.params = {
|
||||
id: {
|
||||
type: 'string'
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
optional: true
|
||||
},
|
||||
subjects: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string'
|
||||
},
|
||||
optional: true
|
||||
},
|
||||
objects: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string'
|
||||
},
|
||||
optional: true
|
||||
},
|
||||
ipPools: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string'
|
||||
},
|
||||
optional: true
|
||||
},
|
||||
limits: {
|
||||
type: 'object',
|
||||
optional: true
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export function get ({ id }) {
|
||||
return this.getResourceSet(id)
|
||||
}
|
||||
|
||||
get.permission = 'admin'
|
||||
|
||||
get.params = {
|
||||
id: {
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export async function getAll () {
|
||||
const { user } = this
|
||||
if (!user) {
|
||||
throw new Unauthorized()
|
||||
}
|
||||
|
||||
return this.getAllResourceSets(user.id)
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export function addObject ({ id, object }) {
|
||||
return this.addObjectToResourceSet(object, id)
|
||||
}
|
||||
|
||||
addObject.permission = 'admin'
|
||||
|
||||
addObject.params = {
|
||||
id: {
|
||||
type: 'string'
|
||||
},
|
||||
object: {
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export function removeObject ({ id, object }) {
|
||||
return this.removeObjectFromResourceSet(object, id)
|
||||
}
|
||||
|
||||
removeObject.permission = 'admin'
|
||||
|
||||
removeObject.params = {
|
||||
id: {
|
||||
type: 'string'
|
||||
},
|
||||
object: {
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export function addSubject ({ id, subject }) {
|
||||
return this.addSubjectToResourceSet(subject, id)
|
||||
}
|
||||
|
||||
addSubject.permission = 'admin'
|
||||
|
||||
addSubject.params = {
|
||||
id: {
|
||||
type: 'string'
|
||||
},
|
||||
subject: {
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export function removeSubject ({ id, subject }) {
|
||||
return this.removeSubjectFromResourceSet(subject, id)
|
||||
}
|
||||
|
||||
removeSubject.permission = 'admin'
|
||||
|
||||
removeSubject.params = {
|
||||
id: {
|
||||
type: 'string'
|
||||
},
|
||||
subject: {
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export function addLimit ({ id, limitId, quantity }) {
|
||||
return this.addLimitToResourceSet(limitId, quantity, id)
|
||||
}
|
||||
|
||||
addLimit.permission = 'admin'
|
||||
|
||||
addLimit.params = {
|
||||
id: {
|
||||
type: 'string'
|
||||
},
|
||||
limitId: {
|
||||
type: 'string'
|
||||
},
|
||||
quantity: {
|
||||
type: 'integer'
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export function removeLimit ({ id, limitId }) {
|
||||
return this.removeLimitFromResourceSet(limitId, id)
|
||||
}
|
||||
|
||||
removeLimit.permission = 'admin'
|
||||
|
||||
removeLimit.params = {
|
||||
id: {
|
||||
type: 'string'
|
||||
},
|
||||
limitId: {
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export function recomputeAllLimits () {
|
||||
return this.recomputeResourceSetsLimits()
|
||||
}
|
||||
|
||||
recomputeAllLimits.permission = 'admin'
|
||||
3
src/api/role.js
Normal file
3
src/api/role.js
Normal file
@@ -0,0 +1,3 @@
|
||||
export async function getAll () {
|
||||
return /* await */ this.getRoles()
|
||||
}
|
||||
57
src/api/schedule.js
Normal file
57
src/api/schedule.js
Normal file
@@ -0,0 +1,57 @@
|
||||
// FIXME so far, no acls for schedules
|
||||
|
||||
export async function getAll () {
|
||||
return /* await */ this.getAllSchedules()
|
||||
}
|
||||
|
||||
getAll.permission = 'admin'
|
||||
getAll.description = 'Gets all existing schedules'
|
||||
|
||||
export async function get (id) {
|
||||
return /* await */ this.getSchedule(id)
|
||||
}
|
||||
|
||||
get.permission = 'admin'
|
||||
get.description = 'Gets an existing schedule'
|
||||
get.params = {
|
||||
id: {type: 'string'}
|
||||
}
|
||||
|
||||
export async function create ({ jobId, cron, enabled, name, timezone }) {
|
||||
return /* await */ this.createSchedule(this.session.get('user_id'), { job: jobId, cron, enabled, name, timezone })
|
||||
}
|
||||
|
||||
create.permission = 'admin'
|
||||
create.description = 'Creates a new schedule'
|
||||
create.params = {
|
||||
jobId: {type: 'string'},
|
||||
cron: {type: 'string'},
|
||||
enabled: {type: 'boolean', optional: true},
|
||||
name: {type: 'string', optional: true}
|
||||
}
|
||||
|
||||
export async function set ({ id, jobId, cron, enabled, name, timezone }) {
|
||||
await this.updateSchedule(id, { job: jobId, cron, enabled, name, timezone })
|
||||
}
|
||||
|
||||
set.permission = 'admin'
|
||||
set.description = 'Modifies an existing schedule'
|
||||
set.params = {
|
||||
id: {type: 'string'},
|
||||
jobId: {type: 'string', optional: true},
|
||||
cron: {type: 'string', optional: true},
|
||||
enabled: {type: 'boolean', optional: true},
|
||||
name: {type: 'string', optional: true}
|
||||
}
|
||||
|
||||
async function delete_ ({id}) {
|
||||
await this.removeSchedule(id)
|
||||
}
|
||||
|
||||
delete_.permission = 'admin'
|
||||
delete_.description = 'Deletes an existing schedule'
|
||||
delete_.params = {
|
||||
id: {type: 'string'}
|
||||
}
|
||||
|
||||
export {delete_ as delete}
|
||||
30
src/api/scheduler.js
Normal file
30
src/api/scheduler.js
Normal file
@@ -0,0 +1,30 @@
|
||||
export async function enable ({id}) {
|
||||
const schedule = await this.getSchedule(id)
|
||||
schedule.enabled = true
|
||||
await this.updateSchedule(id, schedule)
|
||||
}
|
||||
|
||||
enable.permission = 'admin'
|
||||
enable.description = 'Enables a schedule to run it\'s job as scheduled'
|
||||
enable.params = {
|
||||
id: {type: 'string'}
|
||||
}
|
||||
|
||||
export async function disable ({id}) {
|
||||
const schedule = await this.getSchedule(id)
|
||||
schedule.enabled = false
|
||||
await this.updateSchedule(id, schedule)
|
||||
}
|
||||
|
||||
disable.permission = 'admin'
|
||||
disable.description = 'Disables a schedule'
|
||||
disable.params = {
|
||||
id: {type: 'string'}
|
||||
}
|
||||
|
||||
export function getScheduleTable () {
|
||||
return this.scheduleTable
|
||||
}
|
||||
|
||||
disable.permission = 'admin'
|
||||
disable.description = 'Get a map of existing schedules enabled/disabled state'
|
||||
@@ -1,88 +0,0 @@
|
||||
{$wait} = require '../fibers-utils'
|
||||
|
||||
#=====================================================================
|
||||
|
||||
# FIXME: We are storing passwords which is bad!
|
||||
# Could we use tokens instead?
|
||||
|
||||
# Adds a new server.
|
||||
exports.add = ({host, username, password}) ->
|
||||
server = $wait @servers.add {
|
||||
host
|
||||
username
|
||||
password
|
||||
}
|
||||
|
||||
return server.id
|
||||
exports.add.description = 'Add a new Xen server to XO'
|
||||
exports.add.permission = 'admin'
|
||||
exports.add.params =
|
||||
host:
|
||||
type: 'string'
|
||||
username:
|
||||
type: 'string'
|
||||
password:
|
||||
type: 'string'
|
||||
|
||||
# Removes an existing server.
|
||||
exports.remove = ({id}) ->
|
||||
# Throws an error if the server did not exist.
|
||||
@throw 'NO_SUCH_OBJECT' unless $wait @servers.remove id
|
||||
|
||||
return true
|
||||
exports.remove.permission = 'admin'
|
||||
exports.remove.params =
|
||||
id:
|
||||
type: 'string'
|
||||
|
||||
# Returns all servers.
|
||||
exports.getAll = ->
|
||||
# Retrieves the servers.
|
||||
servers = $wait @servers.get()
|
||||
|
||||
# Filters out private properties.
|
||||
for server, i in servers
|
||||
servers[i] = @getServerPublicProperties server
|
||||
|
||||
return servers
|
||||
exports.getAll.permission = 'admin'
|
||||
|
||||
# Changes the properties of an existing server.
|
||||
exports.set = ({id, host, username, password}) ->
|
||||
# Retrieves the server.
|
||||
server = $wait @servers.first id
|
||||
|
||||
# Throws an error if it did not exist.
|
||||
@throw 'NO_SUCH_OBJECT' unless server
|
||||
|
||||
# Updates the provided properties.
|
||||
server.set {host} if host?
|
||||
server.set {username} if username?
|
||||
server.set {password} if password?
|
||||
|
||||
# Updates the server.
|
||||
$wait @servers.update server
|
||||
|
||||
return true
|
||||
exports.set.permission = 'admin'
|
||||
exports.set.params =
|
||||
id:
|
||||
type: 'string'
|
||||
host:
|
||||
type: 'string'
|
||||
optional: true
|
||||
username:
|
||||
type: 'string'
|
||||
optional: true
|
||||
password:
|
||||
type: 'string'
|
||||
optional: true
|
||||
|
||||
|
||||
# Connects to an existing server.
|
||||
exports.connect = ->
|
||||
@throw 'NOT_IMPLEMENTED'
|
||||
|
||||
# Disconnects from an existing server.
|
||||
exports.disconnect = ->
|
||||
@throw 'NOT_IMPLEMENTED'
|
||||
131
src/api/server.js
Normal file
131
src/api/server.js
Normal file
@@ -0,0 +1,131 @@
|
||||
import {
|
||||
noop,
|
||||
pCatch
|
||||
} from '../utils'
|
||||
|
||||
export async function add ({
|
||||
host,
|
||||
username,
|
||||
password,
|
||||
readOnly,
|
||||
autoConnect = true
|
||||
}) {
|
||||
const server = await this.registerXenServer({host, username, password, readOnly})
|
||||
|
||||
if (autoConnect) {
|
||||
// Connect asynchronously, ignore any errors.
|
||||
this.connectXenServer(server.id)::pCatch(noop)
|
||||
}
|
||||
|
||||
return server.id
|
||||
}
|
||||
|
||||
add.description = 'register a new Xen server'
|
||||
|
||||
add.permission = 'admin'
|
||||
|
||||
add.params = {
|
||||
host: {
|
||||
type: 'string'
|
||||
},
|
||||
username: {
|
||||
type: 'string'
|
||||
},
|
||||
password: {
|
||||
type: 'string'
|
||||
},
|
||||
autoConnect: {
|
||||
optional: true,
|
||||
type: 'boolean'
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export async function remove ({id}) {
|
||||
await this.unregisterXenServer(id)
|
||||
}
|
||||
|
||||
remove.description = 'unregister a Xen server'
|
||||
|
||||
remove.permission = 'admin'
|
||||
|
||||
remove.params = {
|
||||
id: {
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
// TODO: remove this function when users are integrated to the main
|
||||
// collection.
|
||||
export function getAll () {
|
||||
return this.getAllXenServers()
|
||||
}
|
||||
|
||||
getAll.description = 'returns all the registered Xen server'
|
||||
|
||||
getAll.permission = 'admin'
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export async function set ({id, host, username, password, readOnly}) {
|
||||
await this.updateXenServer(id, {host, username, password, readOnly})
|
||||
}
|
||||
|
||||
set.description = 'changes the properties of a Xen server'
|
||||
|
||||
set.permission = 'admin'
|
||||
|
||||
set.params = {
|
||||
id: {
|
||||
type: 'string'
|
||||
},
|
||||
host: {
|
||||
type: 'string',
|
||||
optional: true
|
||||
},
|
||||
username: {
|
||||
type: 'string',
|
||||
optional: true
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
optional: true
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export async function connect ({id}) {
|
||||
this.updateXenServer(id, {enabled: true})::pCatch(noop)
|
||||
await this.connectXenServer(id)
|
||||
}
|
||||
|
||||
connect.description = 'connect a Xen server'
|
||||
|
||||
connect.permission = 'admin'
|
||||
|
||||
connect.params = {
|
||||
id: {
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export async function disconnect ({id}) {
|
||||
this.updateXenServer(id, {enabled: false})::pCatch(noop)
|
||||
await this.disconnectXenServer(id)
|
||||
}
|
||||
|
||||
disconnect.description = 'disconnect a Xen server'
|
||||
|
||||
disconnect.permission = 'admin'
|
||||
|
||||
disconnect.params = {
|
||||
id: {
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
{$wait} = require '../fibers-utils'
|
||||
|
||||
#=====================================================================
|
||||
|
||||
# Signs a user in with its email/password.
|
||||
exports.signInWithPassword = ({email, password}) ->
|
||||
@throw 'ALREADY_AUTHENTICATED' if @session.has 'user_id'
|
||||
|
||||
# Gets the user.
|
||||
user = $wait @users.first {email}
|
||||
|
||||
# Invalid credentials if the user does not exists or if the password
|
||||
# does not check.
|
||||
@throw 'INVALID_CREDENTIAL' unless user and user.checkPassword password
|
||||
|
||||
# Stores the user identifier in the session.
|
||||
@session.set 'user_id', user.get 'id'
|
||||
|
||||
# Returns the user.
|
||||
return @getUserPublicProperties user
|
||||
exports.signInWithPassword.params = {
|
||||
email: { type: 'string' }
|
||||
password: { type: 'string' }
|
||||
}
|
||||
|
||||
# Signs a user in with a token.
|
||||
exports.signInWithToken = ({token}) ->
|
||||
@throw 'ALREADY_AUTHENTICATED' if @session.has 'user_id'
|
||||
|
||||
# Gets the token.
|
||||
token = $wait @tokens.first token
|
||||
@throw 'INVALID_CREDENTIAL' unless token?
|
||||
|
||||
# Stores the user and the token identifiers in the session.
|
||||
user_id = token.get('user_id')
|
||||
@session.set 'token_id', token.get 'id'
|
||||
@session.set 'user_id', user_id
|
||||
|
||||
# Returns the user.
|
||||
user = $wait @users.first user_id
|
||||
return @getUserPublicProperties user
|
||||
exports.signInWithToken.params = {
|
||||
token: { type: 'string' }
|
||||
}
|
||||
|
||||
exports.signOut = ->
|
||||
@session.unset 'token_id'
|
||||
@session.unset 'user_id'
|
||||
|
||||
return true
|
||||
|
||||
# Gets the the currently signed in user.
|
||||
exports.getUser = ->
|
||||
id = @session.get 'user_id', null
|
||||
|
||||
# If the user is not signed in, returns null.
|
||||
return null unless id?
|
||||
|
||||
# Returns the user.
|
||||
user = $wait @users.first id
|
||||
return @getUserPublicProperties user
|
||||
62
src/api/session.js
Normal file
62
src/api/session.js
Normal file
@@ -0,0 +1,62 @@
|
||||
import {deprecate} from 'util'
|
||||
|
||||
import { getUserPublicProperties } from '../utils'
|
||||
import {InvalidCredential, AlreadyAuthenticated} from '../api-errors'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export async function signIn (credentials) {
|
||||
if (this.session.has('user_id')) {
|
||||
throw new AlreadyAuthenticated()
|
||||
}
|
||||
|
||||
const user = await this.authenticateUser(credentials)
|
||||
if (!user) {
|
||||
throw new InvalidCredential()
|
||||
}
|
||||
this.session.set('user_id', user.id)
|
||||
|
||||
return getUserPublicProperties(user)
|
||||
}
|
||||
|
||||
signIn.description = 'sign in'
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export const signInWithPassword = deprecate(signIn, 'use session.signIn() instead')
|
||||
|
||||
signInWithPassword.params = {
|
||||
email: { type: 'string' },
|
||||
password: { type: 'string' }
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export const signInWithToken = deprecate(signIn, 'use session.signIn() instead')
|
||||
|
||||
signInWithToken.params = {
|
||||
token: { type: 'string' }
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export function signOut () {
|
||||
this.session.unset('user_id')
|
||||
}
|
||||
|
||||
signOut.description = 'sign out the user from the current session'
|
||||
|
||||
// This method requires the user to be signed in.
|
||||
signOut.permission = ''
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export async function getUser () {
|
||||
const userId = this.session.get('user_id')
|
||||
|
||||
return userId === undefined
|
||||
? null
|
||||
: getUserPublicProperties(await this.getUser(userId))
|
||||
}
|
||||
|
||||
getUser.description = 'return the currently connected user'
|
||||
@@ -1,46 +0,0 @@
|
||||
{$wait} = require '../fibers-utils'
|
||||
|
||||
#=====================================================================
|
||||
|
||||
exports.set = (params) ->
|
||||
try
|
||||
SR = @getObject params.id
|
||||
catch
|
||||
@throw 'NO_SUCH_OBJECT'
|
||||
|
||||
xapi = @getXAPI SR
|
||||
|
||||
for param, field of {
|
||||
'name_label'
|
||||
'name_description'
|
||||
}
|
||||
continue unless param of params
|
||||
|
||||
$wait xapi.call "SR.set_#{field}", SR.ref, params[param]
|
||||
|
||||
return true
|
||||
exports.set.permission = 'admin'
|
||||
exports.set.params = {
|
||||
id: { type: 'string' }
|
||||
|
||||
name_label: { type: 'string', optional: true }
|
||||
|
||||
name_description: { type: 'string', optional: true }
|
||||
}
|
||||
|
||||
|
||||
exports.scan = ({id}) ->
|
||||
try
|
||||
SR = @getObject id
|
||||
catch
|
||||
@throw 'NO_SUCH_OBJECT'
|
||||
|
||||
xapi = @getXAPI SR
|
||||
|
||||
$wait xapi.call 'SR.scan', SR.ref
|
||||
|
||||
return true
|
||||
exports.scan.permission = 'admin'
|
||||
exports.scan.params = {
|
||||
id: { type: 'string' }
|
||||
}
|
||||
716
src/api/sr.js
Normal file
716
src/api/sr.js
Normal file
@@ -0,0 +1,716 @@
|
||||
import { asInteger } from '../xapi/utils'
|
||||
import {
|
||||
ensureArray,
|
||||
forEach,
|
||||
parseXml
|
||||
} from '../utils'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export async function set ({
|
||||
sr,
|
||||
|
||||
// TODO: use camel case.
|
||||
name_description: nameDescription,
|
||||
name_label: nameLabel
|
||||
}) {
|
||||
await this.getXapi(sr).setSrProperties(sr._xapiId, {
|
||||
nameDescription,
|
||||
nameLabel
|
||||
})
|
||||
}
|
||||
|
||||
set.params = {
|
||||
id: { type: 'string' },
|
||||
|
||||
name_label: { type: 'string', optional: true },
|
||||
|
||||
name_description: { type: 'string', optional: true }
|
||||
}
|
||||
|
||||
set.resolve = {
|
||||
sr: ['id', 'SR', 'operate']
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export async function scan ({SR}) {
|
||||
await this.getXapi(SR).call('SR.scan', SR._xapiRef)
|
||||
}
|
||||
|
||||
scan.params = {
|
||||
id: { type: 'string' }
|
||||
}
|
||||
|
||||
scan.resolve = {
|
||||
SR: ['id', 'SR', 'operate']
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
// TODO: find a way to call this "delete" and not destroy
|
||||
export async function destroy ({ sr }) {
|
||||
await this.getXapi(sr).destroySr(sr._xapiId)
|
||||
}
|
||||
|
||||
destroy.params = {
|
||||
id: { type: 'string' }
|
||||
}
|
||||
|
||||
destroy.resolve = {
|
||||
sr: ['id', 'SR', 'administrate']
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export async function forget ({SR}) {
|
||||
await this.getXapi(SR).forgetSr(SR._xapiId)
|
||||
}
|
||||
|
||||
forget.params = {
|
||||
id: { type: 'string' }
|
||||
}
|
||||
|
||||
forget.resolve = {
|
||||
SR: ['id', 'SR', 'administrate']
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export async function connectAllPbds ({SR}) {
|
||||
await this.getXapi(SR).connectAllSrPbds(SR._xapiId)
|
||||
}
|
||||
|
||||
connectAllPbds.params = {
|
||||
id: { type: 'string' }
|
||||
}
|
||||
|
||||
connectAllPbds.resolve = {
|
||||
SR: ['id', 'SR', 'administrate']
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export async function disconnectAllPbds ({SR}) {
|
||||
await this.getXapi(SR).disconnectAllSrPbds(SR._xapiId)
|
||||
}
|
||||
|
||||
disconnectAllPbds.params = {
|
||||
id: { type: 'string' }
|
||||
}
|
||||
|
||||
disconnectAllPbds.resolve = {
|
||||
SR: ['id', 'SR', 'administrate']
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export async function createIso ({
|
||||
host,
|
||||
nameLabel,
|
||||
nameDescription,
|
||||
path,
|
||||
type,
|
||||
user,
|
||||
password
|
||||
}) {
|
||||
const xapi = this.getXapi(host)
|
||||
|
||||
const deviceConfig = {}
|
||||
if (type === 'local') {
|
||||
deviceConfig.legacy_mode = 'true'
|
||||
} else if (type === 'smb') {
|
||||
path = path.replace(/\\/g, '/')
|
||||
deviceConfig.username = user
|
||||
deviceConfig.cifspassword = password
|
||||
}
|
||||
|
||||
deviceConfig.location = path
|
||||
|
||||
const srRef = await xapi.call(
|
||||
'SR.create',
|
||||
host._xapiRef,
|
||||
deviceConfig,
|
||||
'0', // SR size 0 because ISO
|
||||
nameLabel,
|
||||
nameDescription,
|
||||
'iso', // SR type ISO
|
||||
'iso', // SR content type ISO
|
||||
type !== 'local',
|
||||
{}
|
||||
)
|
||||
|
||||
const sr = await xapi.call('SR.get_record', srRef)
|
||||
return sr.uuid
|
||||
}
|
||||
|
||||
createIso.params = {
|
||||
host: { type: 'string' },
|
||||
nameLabel: { type: 'string' },
|
||||
nameDescription: { type: 'string' },
|
||||
path: { type: 'string' },
|
||||
type: { type: 'string' },
|
||||
user: { type: 'string', optional: true },
|
||||
password: { type: 'string', optional: true }
|
||||
}
|
||||
|
||||
createIso.resolve = {
|
||||
host: ['host', 'host', 'administrate']
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// NFS SR
|
||||
|
||||
// This functions creates a NFS SR
|
||||
|
||||
export async function createNfs ({
|
||||
host,
|
||||
nameLabel,
|
||||
nameDescription,
|
||||
server,
|
||||
serverPath,
|
||||
nfsVersion
|
||||
}) {
|
||||
const xapi = this.getXapi(host)
|
||||
|
||||
const deviceConfig = {
|
||||
server,
|
||||
serverpath: serverPath
|
||||
}
|
||||
|
||||
// if NFS version given
|
||||
if (nfsVersion) {
|
||||
deviceConfig.nfsversion = nfsVersion
|
||||
}
|
||||
|
||||
const srRef = await xapi.call(
|
||||
'SR.create',
|
||||
host._xapiRef,
|
||||
deviceConfig,
|
||||
'0',
|
||||
nameLabel,
|
||||
nameDescription,
|
||||
'nfs', // SR LVM over iSCSI
|
||||
'user', // recommended by Citrix
|
||||
true,
|
||||
{}
|
||||
)
|
||||
|
||||
const sr = await xapi.call('SR.get_record', srRef)
|
||||
return sr.uuid
|
||||
}
|
||||
|
||||
createNfs.params = {
|
||||
host: { type: 'string' },
|
||||
nameLabel: { type: 'string' },
|
||||
nameDescription: { type: 'string' },
|
||||
server: { type: 'string' },
|
||||
serverPath: { type: 'string' },
|
||||
nfsVersion: { type: 'string', optional: true }
|
||||
}
|
||||
|
||||
createNfs.resolve = {
|
||||
host: ['host', 'host', 'administrate']
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// Local LVM SR
|
||||
|
||||
// This functions creates a local LVM SR
|
||||
|
||||
export async function createLvm ({
|
||||
host,
|
||||
nameLabel,
|
||||
nameDescription,
|
||||
device
|
||||
}) {
|
||||
const xapi = this.getXapi(host)
|
||||
|
||||
const deviceConfig = {
|
||||
device
|
||||
}
|
||||
|
||||
const srRef = await xapi.call(
|
||||
'SR.create',
|
||||
host._xapiRef,
|
||||
deviceConfig,
|
||||
'0',
|
||||
nameLabel,
|
||||
nameDescription,
|
||||
'lvm', // SR LVM
|
||||
'user', // recommended by Citrix
|
||||
false,
|
||||
{}
|
||||
)
|
||||
|
||||
const sr = await xapi.call('SR.get_record', srRef)
|
||||
return sr.uuid
|
||||
}
|
||||
|
||||
createLvm.params = {
|
||||
host: { type: 'string' },
|
||||
nameLabel: { type: 'string' },
|
||||
nameDescription: { type: 'string' },
|
||||
device: { type: 'string' }
|
||||
}
|
||||
|
||||
createLvm.resolve = {
|
||||
host: ['host', 'host', 'administrate']
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// This function helps to detect all NFS shares (exports) on a NFS server
|
||||
// Return a table of exports with their paths and ACLs
|
||||
|
||||
export async function probeNfs ({
|
||||
host,
|
||||
server
|
||||
}) {
|
||||
const xapi = this.getXapi(host)
|
||||
|
||||
const deviceConfig = {
|
||||
server
|
||||
}
|
||||
|
||||
let xml
|
||||
|
||||
try {
|
||||
await xapi.call(
|
||||
'SR.probe',
|
||||
host._xapiRef,
|
||||
deviceConfig,
|
||||
'nfs',
|
||||
{}
|
||||
)
|
||||
|
||||
throw new Error('the call above should have thrown an error')
|
||||
} catch (error) {
|
||||
if (error.code !== 'SR_BACKEND_FAILURE_101') {
|
||||
throw error
|
||||
}
|
||||
|
||||
xml = parseXml(error.params[2])
|
||||
}
|
||||
|
||||
const nfsExports = []
|
||||
forEach(ensureArray(xml['nfs-exports'].Export), nfsExport => {
|
||||
nfsExports.push({
|
||||
path: nfsExport.Path.trim(),
|
||||
acl: nfsExport.Accesslist.trim()
|
||||
})
|
||||
})
|
||||
|
||||
return nfsExports
|
||||
}
|
||||
|
||||
probeNfs.params = {
|
||||
host: { type: 'string' },
|
||||
server: { type: 'string' }
|
||||
}
|
||||
|
||||
probeNfs.resolve = {
|
||||
host: ['host', 'host', 'administrate']
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// ISCSI SR
|
||||
|
||||
// This functions creates a iSCSI SR
|
||||
|
||||
export async function createIscsi ({
|
||||
host,
|
||||
nameLabel,
|
||||
nameDescription,
|
||||
size,
|
||||
target,
|
||||
port,
|
||||
targetIqn,
|
||||
scsiId,
|
||||
chapUser,
|
||||
chapPassword
|
||||
}) {
|
||||
const xapi = this.getXapi(host)
|
||||
|
||||
const deviceConfig = {
|
||||
target,
|
||||
targetIQN: targetIqn,
|
||||
SCSIid: scsiId
|
||||
}
|
||||
|
||||
// if we give user and password
|
||||
if (chapUser && chapPassword) {
|
||||
deviceConfig.chapUser = chapUser
|
||||
deviceConfig.chapPassword = chapPassword
|
||||
}
|
||||
|
||||
// if we give another port than default iSCSI
|
||||
if (port) {
|
||||
deviceConfig.port = asInteger(port)
|
||||
}
|
||||
|
||||
const srRef = await xapi.call(
|
||||
'SR.create',
|
||||
host._xapiRef,
|
||||
deviceConfig,
|
||||
'0',
|
||||
nameLabel,
|
||||
nameDescription,
|
||||
'lvmoiscsi', // SR LVM over iSCSI
|
||||
'user', // recommended by Citrix
|
||||
true,
|
||||
{}
|
||||
)
|
||||
|
||||
const sr = await xapi.call('SR.get_record', srRef)
|
||||
return sr.uuid
|
||||
}
|
||||
|
||||
createIscsi.params = {
|
||||
host: { type: 'string' },
|
||||
nameLabel: { type: 'string' },
|
||||
nameDescription: { type: 'string' },
|
||||
target: { type: 'string' },
|
||||
port: { type: 'integer', optional: true },
|
||||
targetIqn: { type: 'string' },
|
||||
scsiId: { type: 'string' },
|
||||
chapUser: { type: 'string', optional: true },
|
||||
chapPassword: { type: 'string', optional: true }
|
||||
}
|
||||
|
||||
createIscsi.resolve = {
|
||||
host: ['host', 'host', 'administrate']
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// This function helps to detect all iSCSI IQN on a Target (iSCSI "server")
|
||||
// Return a table of IQN or empty table if no iSCSI connection to the target
|
||||
|
||||
export async function probeIscsiIqns ({
|
||||
host,
|
||||
target: targetIp,
|
||||
port,
|
||||
chapUser,
|
||||
chapPassword
|
||||
}) {
|
||||
const xapi = this.getXapi(host)
|
||||
|
||||
const deviceConfig = {
|
||||
target: targetIp
|
||||
}
|
||||
|
||||
// if we give user and password
|
||||
if (chapUser && chapPassword) {
|
||||
deviceConfig.chapUser = chapUser
|
||||
deviceConfig.chapPassword = chapPassword
|
||||
}
|
||||
|
||||
// if we give another port than default iSCSI
|
||||
if (port) {
|
||||
deviceConfig.port = asInteger(port)
|
||||
}
|
||||
|
||||
let xml
|
||||
|
||||
try {
|
||||
await xapi.call(
|
||||
'SR.probe',
|
||||
host._xapiRef,
|
||||
deviceConfig,
|
||||
'lvmoiscsi',
|
||||
{}
|
||||
)
|
||||
|
||||
throw new Error('the call above should have thrown an error')
|
||||
} catch (error) {
|
||||
if (error.code === 'SR_BACKEND_FAILURE_141') {
|
||||
return []
|
||||
}
|
||||
if (error.code !== 'SR_BACKEND_FAILURE_96') {
|
||||
throw error
|
||||
}
|
||||
|
||||
xml = parseXml(error.params[2])
|
||||
}
|
||||
|
||||
const targets = []
|
||||
forEach(ensureArray(xml['iscsi-target-iqns'].TGT), target => {
|
||||
// if the target is on another IP adress, do not display it
|
||||
if (target.IPAddress.trim() === targetIp) {
|
||||
targets.push({
|
||||
iqn: target.TargetIQN.trim(),
|
||||
ip: target.IPAddress.trim()
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return targets
|
||||
}
|
||||
|
||||
probeIscsiIqns.params = {
|
||||
host: { type: 'string' },
|
||||
target: { type: 'string' },
|
||||
port: { type: 'integer', optional: true },
|
||||
chapUser: { type: 'string', optional: true },
|
||||
chapPassword: { type: 'string', optional: true }
|
||||
}
|
||||
probeIscsiIqns.resolve = {
|
||||
host: ['host', 'host', 'administrate']
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// This function helps to detect all iSCSI ID and LUNs on a Target
|
||||
// It will return a LUN table
|
||||
|
||||
export async function probeIscsiLuns ({
|
||||
host,
|
||||
target: targetIp,
|
||||
port,
|
||||
targetIqn,
|
||||
chapUser,
|
||||
chapPassword
|
||||
}) {
|
||||
const xapi = this.getXapi(host)
|
||||
|
||||
const deviceConfig = {
|
||||
target: targetIp,
|
||||
targetIQN: targetIqn
|
||||
}
|
||||
|
||||
// if we give user and password
|
||||
if (chapUser && chapPassword) {
|
||||
deviceConfig.chapUser = chapUser
|
||||
deviceConfig.chapPassword = chapPassword
|
||||
}
|
||||
|
||||
// if we give another port than default iSCSI
|
||||
if (port) {
|
||||
deviceConfig.port = asInteger(port)
|
||||
}
|
||||
|
||||
let xml
|
||||
|
||||
try {
|
||||
await xapi.call(
|
||||
'SR.probe',
|
||||
host._xapiRef,
|
||||
deviceConfig,
|
||||
'lvmoiscsi',
|
||||
{}
|
||||
)
|
||||
|
||||
throw new Error('the call above should have thrown an error')
|
||||
} catch (error) {
|
||||
if (error.code !== 'SR_BACKEND_FAILURE_107') {
|
||||
throw error
|
||||
}
|
||||
|
||||
xml = parseXml(error.params[2])
|
||||
}
|
||||
|
||||
const luns = []
|
||||
forEach(ensureArray(xml['iscsi-target'].LUN), lun => {
|
||||
luns.push({
|
||||
id: lun.LUNid.trim(),
|
||||
vendor: lun.vendor.trim(),
|
||||
serial: lun.serial.trim(),
|
||||
size: lun.size.trim(),
|
||||
scsiId: lun.SCSIid.trim()
|
||||
})
|
||||
})
|
||||
|
||||
return luns
|
||||
}
|
||||
|
||||
probeIscsiLuns.params = {
|
||||
host: { type: 'string' },
|
||||
target: { type: 'string' },
|
||||
port: { type: 'integer', optional: true },
|
||||
targetIqn: { type: 'string' },
|
||||
chapUser: { type: 'string', optional: true },
|
||||
chapPassword: { type: 'string', optional: true }
|
||||
}
|
||||
|
||||
probeIscsiLuns.resolve = {
|
||||
host: ['host', 'host', 'administrate']
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// This function helps to detect if this target already exists in XAPI
|
||||
// It returns a table of SR UUID, empty if no existing connections
|
||||
|
||||
export async function probeIscsiExists ({
|
||||
host,
|
||||
target: targetIp,
|
||||
port,
|
||||
targetIqn,
|
||||
scsiId,
|
||||
chapUser,
|
||||
chapPassword
|
||||
}) {
|
||||
const xapi = this.getXapi(host)
|
||||
|
||||
const deviceConfig = {
|
||||
target: targetIp,
|
||||
targetIQN: targetIqn,
|
||||
SCSIid: scsiId
|
||||
}
|
||||
|
||||
// if we give user and password
|
||||
if (chapUser && chapPassword) {
|
||||
deviceConfig.chapUser = chapUser
|
||||
deviceConfig.chapPassword = chapPassword
|
||||
}
|
||||
|
||||
// if we give another port than default iSCSI
|
||||
if (port) {
|
||||
deviceConfig.port = asInteger(port)
|
||||
}
|
||||
|
||||
const xml = parseXml(await xapi.call('SR.probe', host._xapiRef, deviceConfig, 'lvmoiscsi', {}))
|
||||
|
||||
const srs = []
|
||||
forEach(ensureArray(xml['SRlist'].SR), sr => {
|
||||
// get the UUID of SR connected to this LUN
|
||||
srs.push({uuid: sr.UUID.trim()})
|
||||
})
|
||||
|
||||
return srs
|
||||
}
|
||||
|
||||
probeIscsiExists.params = {
|
||||
host: { type: 'string' },
|
||||
target: { type: 'string' },
|
||||
port: { type: 'integer', optional: true },
|
||||
targetIqn: { type: 'string' },
|
||||
scsiId: { type: 'string' },
|
||||
chapUser: { type: 'string', optional: true },
|
||||
chapPassword: { type: 'string', optional: true }
|
||||
}
|
||||
|
||||
probeIscsiExists.resolve = {
|
||||
host: ['host', 'host', 'administrate']
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// This function helps to detect if this NFS SR already exists in XAPI
|
||||
// It returns a table of SR UUID, empty if no existing connections
|
||||
|
||||
export async function probeNfsExists ({
|
||||
host,
|
||||
server,
|
||||
serverPath
|
||||
}) {
|
||||
const xapi = this.getXapi(host)
|
||||
|
||||
const deviceConfig = {
|
||||
server,
|
||||
serverpath: serverPath
|
||||
}
|
||||
|
||||
const xml = parseXml(await xapi.call('SR.probe', host._xapiRef, deviceConfig, 'nfs', {}))
|
||||
|
||||
const srs = []
|
||||
|
||||
forEach(ensureArray(xml['SRlist'].SR), sr => {
|
||||
// get the UUID of SR connected to this LUN
|
||||
srs.push({uuid: sr.UUID.trim()})
|
||||
})
|
||||
|
||||
return srs
|
||||
}
|
||||
|
||||
probeNfsExists.params = {
|
||||
host: { type: 'string' },
|
||||
server: { type: 'string' },
|
||||
serverPath: { type: 'string' }
|
||||
}
|
||||
|
||||
probeNfsExists.resolve = {
|
||||
host: ['host', 'host', 'administrate']
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// This function helps to reattach a forgotten NFS/iSCSI SR
|
||||
|
||||
export async function reattach ({
|
||||
host,
|
||||
uuid,
|
||||
nameLabel,
|
||||
nameDescription,
|
||||
type
|
||||
}) {
|
||||
const xapi = this.getXapi(host)
|
||||
|
||||
if (type === 'iscsi') {
|
||||
type = 'lvmoiscsi' // the internal XAPI name
|
||||
}
|
||||
|
||||
const srRef = await xapi.call(
|
||||
'SR.introduce',
|
||||
uuid,
|
||||
nameLabel,
|
||||
nameDescription,
|
||||
type,
|
||||
'user',
|
||||
true,
|
||||
{}
|
||||
)
|
||||
|
||||
const sr = await xapi.call('SR.get_record', srRef)
|
||||
return sr.uuid
|
||||
}
|
||||
|
||||
reattach.params = {
|
||||
host: { type: 'string' },
|
||||
uuid: { type: 'string' },
|
||||
nameLabel: { type: 'string' },
|
||||
nameDescription: { type: 'string' },
|
||||
type: { type: 'string' }
|
||||
}
|
||||
|
||||
reattach.resolve = {
|
||||
host: ['host', 'host', 'administrate']
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// This function helps to reattach a forgotten ISO SR
|
||||
|
||||
export async function reattachIso ({
|
||||
host,
|
||||
uuid,
|
||||
nameLabel,
|
||||
nameDescription,
|
||||
type
|
||||
}) {
|
||||
const xapi = this.getXapi(host)
|
||||
|
||||
if (type === 'iscsi') {
|
||||
type = 'lvmoiscsi' // the internal XAPI name
|
||||
}
|
||||
|
||||
const srRef = await xapi.call(
|
||||
'SR.introduce',
|
||||
uuid,
|
||||
nameLabel,
|
||||
nameDescription,
|
||||
type,
|
||||
'iso',
|
||||
true,
|
||||
{}
|
||||
)
|
||||
|
||||
const sr = await xapi.call('SR.get_record', srRef)
|
||||
return sr.uuid
|
||||
}
|
||||
|
||||
reattachIso.params = {
|
||||
host: { type: 'string' },
|
||||
uuid: { type: 'string' },
|
||||
nameLabel: { type: 'string' },
|
||||
nameDescription: { type: 'string' },
|
||||
type: { type: 'string' }
|
||||
}
|
||||
|
||||
reattachIso.resolve = {
|
||||
host: ['host', 'host', 'administrate']
|
||||
}
|
||||
67
src/api/system.js
Normal file
67
src/api/system.js
Normal file
@@ -0,0 +1,67 @@
|
||||
import forEach from 'lodash/forEach'
|
||||
import getKeys from 'lodash/keys'
|
||||
import moment from 'moment-timezone'
|
||||
|
||||
import { NoSuchObject } from '../api-errors'
|
||||
import { version as xoServerVersion } from '../../package.json'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export function getMethodsInfo () {
|
||||
const methods = {}
|
||||
|
||||
forEach(this.apiMethods, (method, name) => {
|
||||
methods[name] = {
|
||||
description: method.description,
|
||||
params: method.params || {},
|
||||
permission: method.permission
|
||||
}
|
||||
})
|
||||
|
||||
return methods
|
||||
}
|
||||
getMethodsInfo.description = 'returns the signatures of all available API methods'
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export const getServerTimezone = (tz => () => tz)(moment.tz.guess())
|
||||
getServerTimezone.description = 'return the timezone server'
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export const getServerVersion = () => xoServerVersion
|
||||
getServerVersion.description = 'return the version of xo-server'
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export const getVersion = () => '0.1'
|
||||
getVersion.description = 'API version (unstable)'
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export function listMethods () {
|
||||
return getKeys(this.apiMethods)
|
||||
}
|
||||
listMethods.description = 'returns the name of all available API methods'
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export function methodSignature ({method: name}) {
|
||||
const method = this.apiMethods[name]
|
||||
|
||||
if (!method) {
|
||||
throw new NoSuchObject()
|
||||
}
|
||||
|
||||
// Return an array for compatibility with XML-RPC.
|
||||
return [
|
||||
// XML-RPC require the name of the method.
|
||||
{
|
||||
name,
|
||||
description: method.description,
|
||||
params: method.params || {},
|
||||
permission: method.permission
|
||||
}
|
||||
]
|
||||
}
|
||||
methodSignature.description = 'returns the signature of an API method'
|
||||
31
src/api/tag.js
Normal file
31
src/api/tag.js
Normal file
@@ -0,0 +1,31 @@
|
||||
export async function add ({tag, object}) {
|
||||
await this.getXapi(object).addTag(object._xapiId, tag)
|
||||
}
|
||||
|
||||
add.description = 'add a new tag to an object'
|
||||
|
||||
add.resolve = {
|
||||
object: ['id', null, 'administrate']
|
||||
}
|
||||
|
||||
add.params = {
|
||||
tag: { type: 'string' },
|
||||
id: { type: 'string' }
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export async function remove ({tag, object}) {
|
||||
await this.getXapi(object).removeTag(object._xapiId, tag)
|
||||
}
|
||||
|
||||
remove.description = 'remove an existing tag from an object'
|
||||
|
||||
remove.resolve = {
|
||||
object: ['id', null, 'administrate']
|
||||
}
|
||||
|
||||
remove.params = {
|
||||
tag: { type: 'string' },
|
||||
id: { type: 'string' }
|
||||
}
|
||||
25
src/api/task.js
Normal file
25
src/api/task.js
Normal file
@@ -0,0 +1,25 @@
|
||||
export async function cancel ({task}) {
|
||||
await this.getXapi(task).call('task.cancel', task._xapiRef)
|
||||
}
|
||||
|
||||
cancel.params = {
|
||||
id: { type: 'string' }
|
||||
}
|
||||
|
||||
cancel.resolve = {
|
||||
task: ['id', 'task', 'administrate']
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export async function destroy ({task}) {
|
||||
await this.getXapi(task).call('task.destroy', task._xapiRef)
|
||||
}
|
||||
|
||||
destroy.params = {
|
||||
id: { type: 'string' }
|
||||
}
|
||||
|
||||
destroy.resolve = {
|
||||
task: ['id', 'task', 'administrate']
|
||||
}
|
||||
49
src/api/test.js
Normal file
49
src/api/test.js
Normal file
@@ -0,0 +1,49 @@
|
||||
export function getPermissionsForUser ({ userId }) {
|
||||
return this.getPermissionsForUser(userId)
|
||||
}
|
||||
|
||||
getPermissionsForUser.permission = 'admin'
|
||||
|
||||
getPermissionsForUser.params = {
|
||||
userId: {
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export function hasPermission ({ userId, objectId, permission }) {
|
||||
return this.hasPermissions(userId, [
|
||||
[ objectId, permission ]
|
||||
])
|
||||
}
|
||||
|
||||
hasPermission.permission = 'admin'
|
||||
|
||||
hasPermission.params = {
|
||||
userId: {
|
||||
type: 'string'
|
||||
},
|
||||
objectId: {
|
||||
type: 'string'
|
||||
},
|
||||
permission: {
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export function wait ({duration, returnValue}) {
|
||||
return new Promise(resolve => {
|
||||
setTimeout(() => {
|
||||
resolve(returnValue)
|
||||
}, +duration)
|
||||
})
|
||||
}
|
||||
|
||||
wait.params = {
|
||||
duration: {
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
{$wait} = require '../fibers-utils'
|
||||
|
||||
#=====================================================================
|
||||
|
||||
# Creates a new token.
|
||||
#
|
||||
# TODO: Token permission.
|
||||
exports.create = ->
|
||||
userId = @session.get 'user_id'
|
||||
|
||||
# The user MUST be signed in and not with a token
|
||||
@throw 'UNAUTHORIZED' if not userId? or @session.has 'token_id'
|
||||
|
||||
# Creates the token.
|
||||
token = $wait @tokens.generate userId
|
||||
|
||||
return token.id
|
||||
|
||||
# Deletes a token.
|
||||
exports.delete = ({token: tokenId}) ->
|
||||
# Gets the token.
|
||||
token = $wait @tokens.first tokenId
|
||||
@throw 'NO_SUCH_OBJECT' unless token?
|
||||
|
||||
# Deletes the token.
|
||||
$wait @tokens.remove tokenId
|
||||
|
||||
return true
|
||||
exports.delete.params = {
|
||||
token: { type: 'string' }
|
||||
}
|
||||
27
src/api/token.js
Normal file
27
src/api/token.js
Normal file
@@ -0,0 +1,27 @@
|
||||
// TODO: Prevent token connections from creating tokens.
|
||||
// TODO: Token permission.
|
||||
export async function create () {
|
||||
const userId = this.session.get('user_id')
|
||||
return (await this.createAuthenticationToken({userId})).id
|
||||
}
|
||||
|
||||
create.description = 'create a new authentication token'
|
||||
|
||||
create.permission = '' // sign in
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
// TODO: an user should be able to delete its own tokens.
|
||||
async function delete_ ({token: id}) {
|
||||
await this.deleteAuthenticationToken(id)
|
||||
}
|
||||
|
||||
export {delete_ as delete}
|
||||
|
||||
delete_.description = 'delete an existing authentication token'
|
||||
|
||||
delete_.permission = 'admin'
|
||||
|
||||
delete_.params = {
|
||||
token: { type: 'string' }
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
{$wait} = require '../fibers-utils'
|
||||
|
||||
#=====================================================================
|
||||
|
||||
# Creates a new user.
|
||||
exports.create = ({email, password, permission}) ->
|
||||
# Creates the user.
|
||||
user = $wait @users.create email, password, permission
|
||||
|
||||
return user.id
|
||||
exports.create.permission = 'admin'
|
||||
exports.create.params = {
|
||||
email: { type: 'string' }
|
||||
password: { type: 'string' }
|
||||
permission: { type: 'string', optional: true}
|
||||
}
|
||||
|
||||
# Deletes an existing user.
|
||||
#
|
||||
# FIXME: a user should not be able to delete itself.
|
||||
exports.delete = ({id}) ->
|
||||
# The user cannot delete himself.
|
||||
@throw 'INVALID_PARAMS' if id is @session.get 'user_id'
|
||||
|
||||
# Throws an error if the user did not exist.
|
||||
@throw 'NO_SUCH_OBJECT' unless $wait @users.remove id
|
||||
|
||||
return true
|
||||
exports.delete.permission = 'admin'
|
||||
exports.delete.params = {
|
||||
id: { type: 'string' }
|
||||
}
|
||||
|
||||
# Changes the password of the current user.
|
||||
exports.changePassword = ({old, new: newP}) ->
|
||||
# Gets the current user (which MUST exist).
|
||||
user = $wait @users.first @session.get 'user_id'
|
||||
|
||||
# Checks its old password.
|
||||
@throw 'INVALID_CREDENTIAL' unless user.checkPassword old
|
||||
|
||||
# Sets the new password.
|
||||
user.setPassword newP
|
||||
|
||||
# Updates the user.
|
||||
$wait @users.update user
|
||||
|
||||
return true
|
||||
exports.changePassword.permission = '' # Signed in.
|
||||
exports.changePassword.params = {
|
||||
old: { type: 'string' }
|
||||
new: { type: 'string' }
|
||||
}
|
||||
|
||||
# Returns the user with a given identifier.
|
||||
exports.get = ({id}) ->
|
||||
# Only an administrator can see another user.
|
||||
@checkPermission 'admin' unless @session.get 'user_id' is id
|
||||
|
||||
# Retrieves the user.
|
||||
user = $wait @users.first id
|
||||
|
||||
# Throws an error if it did not exist.
|
||||
@throw 'NO_SUCH_OBJECT' unless user
|
||||
|
||||
return @getUserPublicProperties user
|
||||
exports.get.params = {
|
||||
id: { type: 'string' }
|
||||
}
|
||||
|
||||
# Returns all users.
|
||||
exports.getAll = ->
|
||||
# Retrieves the users.
|
||||
users = $wait @users.get()
|
||||
|
||||
# Filters out private properties.
|
||||
for user, i in users
|
||||
users[i] = @getUserPublicProperties user
|
||||
|
||||
return users
|
||||
exports.getAll.permission = 'admin'
|
||||
|
||||
# Changes the properties of an existing user.
|
||||
exports.set = ({id, email, password, permission}) ->
|
||||
# Retrieves the user.
|
||||
user = $wait @users.first id
|
||||
|
||||
# Throws an error if it did not exist.
|
||||
@throw 'NO_SUCH_OBJECT' unless user
|
||||
|
||||
# Updates the provided properties.
|
||||
user.set {email} if email?
|
||||
user.set {permission} if permission?
|
||||
user.setPassword password if password?
|
||||
|
||||
# Updates the user.
|
||||
$wait @users.update user
|
||||
|
||||
return true
|
||||
exports.set.permission = 'admin'
|
||||
exports.set.params = {
|
||||
id: { type: 'string' }
|
||||
email: { type: 'string', optional: true }
|
||||
password: { type: 'string', optional: true }
|
||||
permission: { type: 'string', optional: true }
|
||||
}
|
||||
99
src/api/user.js
Normal file
99
src/api/user.js
Normal file
@@ -0,0 +1,99 @@
|
||||
import {InvalidParameters} from '../api-errors'
|
||||
import { getUserPublicProperties, mapToArray } from '../utils'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export async function create ({email, password, permission}) {
|
||||
return (await this.createUser({email, password, permission})).id
|
||||
}
|
||||
|
||||
create.description = 'creates a new user'
|
||||
|
||||
create.permission = 'admin'
|
||||
|
||||
create.params = {
|
||||
email: { type: 'string' },
|
||||
password: { type: 'string' },
|
||||
permission: { type: 'string', optional: true }
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
// Deletes an existing user.
|
||||
async function delete_ ({id}) {
|
||||
if (id === this.session.get('user_id')) {
|
||||
throw new InvalidParameters('a user cannot delete itself')
|
||||
}
|
||||
|
||||
await this.deleteUser(id)
|
||||
}
|
||||
|
||||
// delete is not a valid identifier.
|
||||
export {delete_ as delete}
|
||||
|
||||
delete_.description = 'deletes an existing user'
|
||||
|
||||
delete_.permission = 'admin'
|
||||
|
||||
delete_.params = {
|
||||
id: { type: 'string' }
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
// TODO: remove this function when users are integrated to the main
|
||||
// collection.
|
||||
export async function getAll () {
|
||||
// Retrieves the users.
|
||||
const users = await this.getAllUsers()
|
||||
|
||||
// Filters out private properties.
|
||||
return mapToArray(users, getUserPublicProperties)
|
||||
}
|
||||
|
||||
getAll.description = 'returns all the existing users'
|
||||
|
||||
getAll.permission = 'admin'
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export async function set ({id, email, password, permission, preferences}) {
|
||||
const isAdmin = this.user && this.user.permission === 'admin'
|
||||
if (isAdmin) {
|
||||
if (permission && id === this.session.get('user_id')) {
|
||||
throw new InvalidParameters('a user cannot change its own permission')
|
||||
}
|
||||
} else if (email || password || permission) {
|
||||
throw new InvalidParameters('this properties can only changed by an administrator')
|
||||
}
|
||||
|
||||
await this.updateUser(id, {email, password, permission, preferences})
|
||||
}
|
||||
|
||||
set.description = 'changes the properties of an existing user'
|
||||
|
||||
set.permission = ''
|
||||
|
||||
set.params = {
|
||||
id: { type: 'string' },
|
||||
email: { type: 'string', optional: true },
|
||||
password: { type: 'string', optional: true },
|
||||
permission: { type: 'string', optional: true },
|
||||
preferences: { type: 'object', optional: true }
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export async function changePassword ({oldPassword, newPassword}) {
|
||||
const id = this.session.get('user_id')
|
||||
await this.changeUserPassword(id, oldPassword, newPassword)
|
||||
}
|
||||
|
||||
changePassword.description = 'change password after checking old password (user function)'
|
||||
|
||||
changePassword.permission = ''
|
||||
|
||||
changePassword.params = {
|
||||
oldPassword: {type: 'string'},
|
||||
newPassword: {type: 'string'}
|
||||
}
|
||||
@@ -1,37 +1,109 @@
|
||||
{$wait} = require '../fibers-utils'
|
||||
# FIXME: too low level, should be removed.
|
||||
|
||||
{coroutine: $coroutine} = require 'bluebird'
|
||||
|
||||
#=====================================================================
|
||||
|
||||
exports.delete = ({id}) ->
|
||||
try
|
||||
VBD = @getObject params.id
|
||||
catch
|
||||
@throw 'NO_SUCH_OBJECT'
|
||||
|
||||
xapi = @getXAPI VBD
|
||||
delete_ = $coroutine ({vbd}) ->
|
||||
xapi = @getXapi vbd
|
||||
|
||||
# TODO: check if VBD is attached before
|
||||
$wait xapi.call 'VBD.destroy', VBD.ref
|
||||
yield xapi.call 'VBD.destroy', vbd._xapiRef
|
||||
|
||||
return true
|
||||
exports.delete.permission = 'admin'
|
||||
exports.delete.params = {
|
||||
|
||||
delete_.params = {
|
||||
id: { type: 'string' }
|
||||
}
|
||||
|
||||
exports.disconnect = ({id}) ->
|
||||
try
|
||||
VBD = @getObject params.id
|
||||
catch
|
||||
@throw 'NO_SUCH_OBJECT'
|
||||
delete_.resolve = {
|
||||
vbd: ['id', 'VBD', 'administrate'],
|
||||
}
|
||||
|
||||
xapi = @getXAPI VBD
|
||||
exports.delete = delete_
|
||||
|
||||
# TODO: check if VBD is attached before
|
||||
$wait xapi.call 'VBD.unplug_force', VBD.ref
|
||||
#---------------------------------------------------------------------
|
||||
|
||||
return true
|
||||
exports.disconnect.permission = 'admin'
|
||||
exports.disconnect.params = {
|
||||
disconnect = $coroutine ({vbd}) ->
|
||||
xapi = @getXapi vbd
|
||||
yield xapi.disconnectVbd(vbd._xapiRef)
|
||||
return
|
||||
|
||||
disconnect.params = {
|
||||
id: { type: 'string' }
|
||||
}
|
||||
|
||||
disconnect.resolve = {
|
||||
vbd: ['id', 'VBD', 'administrate'],
|
||||
}
|
||||
|
||||
exports.disconnect = disconnect
|
||||
|
||||
#---------------------------------------------------------------------
|
||||
|
||||
connect = $coroutine ({vbd}) ->
|
||||
xapi = @getXapi vbd
|
||||
yield xapi.connectVbd(vbd._xapiRef)
|
||||
return
|
||||
|
||||
connect.params = {
|
||||
id: { type: 'string' }
|
||||
}
|
||||
|
||||
connect.resolve = {
|
||||
vbd: ['id', 'VBD', 'administrate'],
|
||||
}
|
||||
|
||||
exports.connect = connect
|
||||
|
||||
#---------------------------------------------------------------------
|
||||
|
||||
set = $coroutine (params) ->
|
||||
{vbd} = params
|
||||
xapi = @getXapi vbd
|
||||
|
||||
{ _xapiRef: ref } = vbd
|
||||
|
||||
# VBD position
|
||||
if 'position' of params
|
||||
yield xapi.call 'VBD.set_userdevice', ref, String(params.position)
|
||||
|
||||
set.params = {
|
||||
# Identifier of the VBD to update.
|
||||
id: { type: 'string' }
|
||||
|
||||
position: { type: ['string', 'number'], optional: true }
|
||||
|
||||
}
|
||||
|
||||
set.resolve = {
|
||||
vbd: ['id', 'VBD', 'administrate'],
|
||||
}
|
||||
|
||||
exports.set = set
|
||||
|
||||
#---------------------------------------------------------------------
|
||||
|
||||
setBootable = $coroutine ({vbd, bootable}) ->
|
||||
xapi = @getXapi vbd
|
||||
{ _xapiRef: ref } = vbd
|
||||
|
||||
yield xapi.call 'VBD.set_bootable', ref, bootable
|
||||
return
|
||||
|
||||
setBootable.params = {
|
||||
vbd: { type: 'string' }
|
||||
bootable: { type: 'boolean' }
|
||||
}
|
||||
|
||||
setBootable.resolve = {
|
||||
vbd: ['vbd', 'VBD', 'administrate'],
|
||||
}
|
||||
|
||||
exports.setBootable = setBootable
|
||||
|
||||
#=====================================================================
|
||||
|
||||
Object.defineProperty(exports, '__esModule', {
|
||||
value: true
|
||||
})
|
||||
|
||||
@@ -1,49 +1,47 @@
|
||||
{isArray: $isArray} = require 'underscore'
|
||||
# FIXME: rename to disk.*
|
||||
|
||||
#---------------------------------------------------------------------
|
||||
{coroutine: $coroutine} = require 'bluebird'
|
||||
|
||||
{$wait} = require '../fibers-utils'
|
||||
{format} = require 'json-rpc-peer'
|
||||
{InvalidParameters} = require '../api-errors'
|
||||
{isArray: $isArray, parseSize} = require '../utils'
|
||||
{JsonRpcError} = require '../api-errors'
|
||||
|
||||
#=====================================================================
|
||||
|
||||
exports.delete = ({id}) ->
|
||||
try
|
||||
VDI = @getObject id
|
||||
catch
|
||||
@throw 'NO_SUCH_OBJECT'
|
||||
delete_ = $coroutine ({vdi}) ->
|
||||
yield @getXapi(vdi).deleteVdi(vdi._xapiId)
|
||||
|
||||
xapi = @getXAPI VDI
|
||||
return
|
||||
|
||||
# TODO: check if VDI is attached before
|
||||
$wait xapi.call 'VDI.destroy', VDI.ref
|
||||
delete_.params = {
|
||||
id: { type: 'string' },
|
||||
}
|
||||
|
||||
return true
|
||||
exports.delete.permission = 'admin'
|
||||
exports.delete.params =
|
||||
id:
|
||||
type: 'string'
|
||||
delete_.resolve = {
|
||||
vdi: ['id', ['VDI', 'VDI-snapshot'], 'administrate'],
|
||||
}
|
||||
|
||||
exports.set = (params) ->
|
||||
try
|
||||
VDI = @getObject params.id
|
||||
catch
|
||||
@throw 'NO_SUCH_OBJECT'
|
||||
exports.delete = delete_
|
||||
|
||||
xapi = @getXAPI VDI
|
||||
#---------------------------------------------------------------------
|
||||
|
||||
{ref} = VDI
|
||||
# FIXME: human readable strings should be handled.
|
||||
set = $coroutine (params) ->
|
||||
{vdi} = params
|
||||
xapi = @getXapi vdi
|
||||
|
||||
{_xapiRef: ref} = vdi
|
||||
|
||||
# Size.
|
||||
if 'size' of params
|
||||
{size} = params
|
||||
size = parseSize(params.size)
|
||||
|
||||
if size < VDI.size
|
||||
@throw(
|
||||
'INVALID_SIZE'
|
||||
"cannot set new size below the current size (#{VDI.size})"
|
||||
if size < vdi.size
|
||||
throw new InvalidParameters(
|
||||
"cannot set new size (#{size}) below the current size (#{vdi.size})"
|
||||
)
|
||||
|
||||
$wait xapi.call 'VDI.resize_online', ref, "#{size}"
|
||||
yield xapi.resizeVdi(ref, size)
|
||||
|
||||
# Other fields.
|
||||
for param, fields of {
|
||||
@@ -53,11 +51,11 @@ exports.set = (params) ->
|
||||
continue unless param of params
|
||||
|
||||
for field in (if $isArray fields then fields else [fields])
|
||||
$wait xapi.call "VDI.set_#{field}", ref, "#{params[param]}"
|
||||
yield xapi.call "VDI.set_#{field}", ref, "#{params[param]}"
|
||||
|
||||
return true
|
||||
exports.set.permission = 'admin'
|
||||
exports.set.params = {
|
||||
|
||||
set.params = {
|
||||
# Identifier of the VDI to update.
|
||||
id: { type: 'string' }
|
||||
|
||||
@@ -66,5 +64,38 @@ exports.set.params = {
|
||||
name_description: { type: 'string', optional: true }
|
||||
|
||||
# size of VDI
|
||||
size: { type: 'integer', optional: true }
|
||||
size: { type: ['integer', 'string'], optional: true }
|
||||
}
|
||||
|
||||
set.resolve = {
|
||||
vdi: ['id', ['VDI', 'VDI-snapshot'], 'administrate'],
|
||||
}
|
||||
|
||||
exports.set = set
|
||||
|
||||
#---------------------------------------------------------------------
|
||||
|
||||
migrate = $coroutine ({vdi, sr}) ->
|
||||
xapi = @getXapi vdi
|
||||
|
||||
yield xapi.moveVdi(vdi._xapiRef, sr._xapiRef)
|
||||
|
||||
return true
|
||||
|
||||
migrate.params = {
|
||||
id: { type: 'string' }
|
||||
sr_id: { type: 'string' }
|
||||
}
|
||||
|
||||
migrate.resolve = {
|
||||
vdi: ['id', ['VDI', 'VDI-snapshot'], 'administrate'],
|
||||
sr: ['sr_id', 'SR', 'administrate'],
|
||||
}
|
||||
|
||||
exports.migrate = migrate
|
||||
|
||||
#=====================================================================
|
||||
|
||||
Object.defineProperty(exports, '__esModule', {
|
||||
value: true
|
||||
})
|
||||
|
||||
142
src/api/vif.js
Normal file
142
src/api/vif.js
Normal file
@@ -0,0 +1,142 @@
|
||||
import {
|
||||
diffItems,
|
||||
noop,
|
||||
pCatch
|
||||
} from '../utils'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
// TODO: move into vm and rename to removeInterface
|
||||
async function delete_ ({vif}) {
|
||||
this.allocIpAddresses(
|
||||
vif.id,
|
||||
vif.$network,
|
||||
null,
|
||||
vif.allowedIpv4Addresses.concat(vif.allowedIpv6Addresses)
|
||||
)::pCatch(noop)
|
||||
|
||||
await this.getXapi(vif).deleteVif(vif._xapiId)
|
||||
}
|
||||
export {delete_ as delete}
|
||||
|
||||
delete_.params = {
|
||||
id: { type: 'string' }
|
||||
}
|
||||
|
||||
delete_.resolve = {
|
||||
vif: ['id', 'VIF', 'administrate']
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
// TODO: move into vm and rename to disconnectInterface
|
||||
export async function disconnect ({vif}) {
|
||||
// TODO: check if VIF is attached before
|
||||
await this.getXapi(vif).disconnectVif(vif._xapiId)
|
||||
}
|
||||
|
||||
disconnect.params = {
|
||||
id: { type: 'string' }
|
||||
}
|
||||
|
||||
disconnect.resolve = {
|
||||
vif: ['id', 'VIF', 'operate']
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// TODO: move into vm and rename to connectInterface
|
||||
export async function connect ({vif}) {
|
||||
// TODO: check if VIF is attached before
|
||||
await this.getXapi(vif).connectVif(vif._xapiId)
|
||||
}
|
||||
|
||||
connect.params = {
|
||||
id: { type: 'string' }
|
||||
}
|
||||
|
||||
connect.resolve = {
|
||||
vif: ['id', 'VIF', 'operate']
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export async function set ({
|
||||
vif,
|
||||
network,
|
||||
mac,
|
||||
allowedIpv4Addresses,
|
||||
allowedIpv6Addresses,
|
||||
attached
|
||||
}) {
|
||||
const oldIpAddresses = vif.allowedIpv4Addresses.concat(vif.allowedIpv6Addresses)
|
||||
const newIpAddresses = []
|
||||
{
|
||||
const { push } = newIpAddresses
|
||||
push.apply(newIpAddresses, allowedIpv4Addresses || vif.allowedIpv4Addresses)
|
||||
push.apply(newIpAddresses, allowedIpv6Addresses || vif.allowedIpv6Addresses)
|
||||
}
|
||||
|
||||
if (network || mac) {
|
||||
const xapi = this.getXapi(vif)
|
||||
|
||||
const vm = xapi.getObject(vif.$VM)
|
||||
mac == null && (mac = vif.MAC)
|
||||
network = xapi.getObject(network && network.id || vif.$network)
|
||||
attached == null && (attached = vif.attached)
|
||||
|
||||
await this.allocIpAddresses(vif.id, null, oldIpAddresses)
|
||||
|
||||
// create new VIF with new parameters
|
||||
await xapi.createVif(vm.$id, network.$id, {
|
||||
mac,
|
||||
currently_attached: attached,
|
||||
ipv4Allowed: allowedIpv4Addresses,
|
||||
ipv6Allowed: allowedIpv6Addresses
|
||||
})
|
||||
|
||||
await this.allocIpAddresses(vif.id, newIpAddresses)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const [ addAddresses, removeAddresses ] = diffItems(
|
||||
newIpAddresses,
|
||||
oldIpAddresses
|
||||
)
|
||||
await this.allocIpAddresses(
|
||||
vif.id,
|
||||
addAddresses,
|
||||
removeAddresses
|
||||
)
|
||||
|
||||
return this.getXapi(vif).editVif(vif._xapiId, {
|
||||
ipv4Allowed: allowedIpv4Addresses,
|
||||
ipv6Allowed: allowedIpv6Addresses
|
||||
})
|
||||
}
|
||||
|
||||
set.params = {
|
||||
id: { type: 'string' },
|
||||
network: { type: 'string', optional: true },
|
||||
mac: { type: 'string', optional: true },
|
||||
allowedIpv4Addresses: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string'
|
||||
},
|
||||
optional: true
|
||||
},
|
||||
allowedIpv6Addresses: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string'
|
||||
},
|
||||
optional: true
|
||||
},
|
||||
attached: { type: 'boolean', optional: true }
|
||||
}
|
||||
|
||||
set.resolve = {
|
||||
vif: ['id', 'VIF', 'operate'],
|
||||
network: ['network', 'network', 'operate']
|
||||
}
|
||||
1519
src/api/vm.coffee
1519
src/api/vm.coffee
File diff suppressed because it is too large
Load Diff
49
src/api/xo.js
Normal file
49
src/api/xo.js
Normal file
@@ -0,0 +1,49 @@
|
||||
import { streamToBuffer } from '../utils'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export function clean () {
|
||||
return this.clean()
|
||||
}
|
||||
|
||||
clean.permission = 'admin'
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export async function exportConfig () {
|
||||
return {
|
||||
$getFrom: await this.registerHttpRequest((req, res) => {
|
||||
res.writeHead(200, 'OK', {
|
||||
'content-disposition': 'attachment'
|
||||
})
|
||||
|
||||
return this.exportConfig()
|
||||
},
|
||||
undefined,
|
||||
{ suffix: '/config.json' })
|
||||
}
|
||||
}
|
||||
|
||||
exportConfig.permission = 'admin'
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export function getAllObjects () {
|
||||
return this.getObjects()
|
||||
}
|
||||
|
||||
getAllObjects.permission = ''
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export async function importConfig () {
|
||||
return {
|
||||
$sendTo: await this.registerHttpRequest(async (req, res) => {
|
||||
await this.importConfig(JSON.parse(await streamToBuffer(req)))
|
||||
|
||||
res.end('config successfully imported')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
importConfig.permission = 'admin'
|
||||
@@ -1,255 +1,172 @@
|
||||
'use strict';
|
||||
import Model from './model'
|
||||
import {BaseError} from 'make-error'
|
||||
import {EventEmitter} from 'events'
|
||||
import {
|
||||
isArray,
|
||||
isObject,
|
||||
map
|
||||
} from './utils'
|
||||
|
||||
//====================================================================
|
||||
// ===================================================================
|
||||
|
||||
var _ = require('underscore');
|
||||
var Promise = require('bluebird');
|
||||
|
||||
//====================================================================
|
||||
|
||||
function Collection()
|
||||
{
|
||||
// Parent constructor.
|
||||
Collection.super_.call(this);
|
||||
export class ModelAlreadyExists extends BaseError {
|
||||
constructor (id) {
|
||||
super('this model already exists: ' + id)
|
||||
}
|
||||
}
|
||||
require('util').inherits(Collection, require('events').EventEmitter);
|
||||
|
||||
Collection.prototype.model = require('./model');
|
||||
// ===================================================================
|
||||
|
||||
/**
|
||||
* Adds new models to this collection.
|
||||
*/
|
||||
Collection.prototype.add = function (models, options) {
|
||||
var array = true;
|
||||
if (!_.isArray(models))
|
||||
{
|
||||
models = [models];
|
||||
array = false;
|
||||
}
|
||||
export default class Collection extends EventEmitter {
|
||||
// Default value for Model.
|
||||
get Model () {
|
||||
return Model
|
||||
}
|
||||
|
||||
for (var i = 0, n = models.length; i < n; ++i)
|
||||
{
|
||||
var model = models[i];
|
||||
// Make this property writable.
|
||||
set Model (Model) {
|
||||
Object.defineProperty(this, 'Model', {
|
||||
configurable: true,
|
||||
enumerale: true,
|
||||
value: Model,
|
||||
writable: true
|
||||
})
|
||||
}
|
||||
|
||||
if ( !(model instanceof this.model) )
|
||||
{
|
||||
model = new this.model(model);
|
||||
}
|
||||
async add (models, opts) {
|
||||
const array = isArray(models)
|
||||
if (!array) {
|
||||
models = [models]
|
||||
}
|
||||
|
||||
var error = model.validate();
|
||||
if (undefined !== error)
|
||||
{
|
||||
// TODO: Better system inspired by Backbone.js.
|
||||
throw error;
|
||||
}
|
||||
const {Model} = this
|
||||
map(models, model => {
|
||||
if (!(model instanceof Model)) {
|
||||
model = new Model(model)
|
||||
}
|
||||
|
||||
models[i] = model.properties;
|
||||
}
|
||||
const error = model.validate()
|
||||
if (error) {
|
||||
// TODO: Better system inspired by Backbone.js
|
||||
throw error
|
||||
}
|
||||
|
||||
var self = this;
|
||||
return Promise.cast(this._add(models, options)).then(function (models) {
|
||||
self.emit('add', models);
|
||||
return model.properties
|
||||
}, models)
|
||||
|
||||
if (!array)
|
||||
{
|
||||
return models[0];
|
||||
}
|
||||
return models;
|
||||
});
|
||||
};
|
||||
models = await this._add(models, opts)
|
||||
this.emit('add', models)
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
Collection.prototype.first = function (properties) {
|
||||
if (!_.isObject(properties))
|
||||
{
|
||||
properties = (undefined !== properties)
|
||||
? { 'id': properties }
|
||||
: {}
|
||||
;
|
||||
}
|
||||
return array
|
||||
? models
|
||||
: new this.Model(models[0])
|
||||
}
|
||||
|
||||
var self = this;
|
||||
return Promise.cast(this._first(properties)).then(function (model) {
|
||||
if (!model)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
async first (properties) {
|
||||
if (!isObject(properties)) {
|
||||
properties = (properties !== undefined)
|
||||
? { id: properties }
|
||||
: {}
|
||||
}
|
||||
|
||||
return new self.model(model);
|
||||
});
|
||||
};
|
||||
const model = await this._first(properties)
|
||||
return model && new this.Model(model)
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all models which have a given set of properties.
|
||||
*
|
||||
* /!\: Does not return instances of this.model.
|
||||
*/
|
||||
Collection.prototype.get = function (properties) {
|
||||
// For coherence with other methods.
|
||||
if (!_.isObject(properties))
|
||||
{
|
||||
properties = (undefined !== properties)
|
||||
? { 'id': properties }
|
||||
: {}
|
||||
;
|
||||
}
|
||||
async get (properties) {
|
||||
if (!isObject(properties)) {
|
||||
properties = (properties !== undefined)
|
||||
? { id: properties }
|
||||
: {}
|
||||
}
|
||||
|
||||
/* jshint newcap: false */
|
||||
return Promise.cast(this._get(properties));
|
||||
};
|
||||
return /* await */ this._get(properties)
|
||||
}
|
||||
|
||||
async remove (ids) {
|
||||
if (!isArray(ids)) {
|
||||
ids = [ids]
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes models from this collection.
|
||||
*/
|
||||
Collection.prototype.remove = function (ids) {
|
||||
if (!_.isArray(ids))
|
||||
{
|
||||
ids = [ids];
|
||||
}
|
||||
await this._remove(ids)
|
||||
|
||||
var self = this;
|
||||
return Promise.cast(this._remove(ids)).then(function () {
|
||||
self.emit('remove', ids);
|
||||
return true;
|
||||
});
|
||||
};
|
||||
this.emit('remove', ids)
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Smartly updates the collection.
|
||||
*
|
||||
* - Adds new models.
|
||||
* - Updates existing models.
|
||||
* - Removes missing models.
|
||||
*/
|
||||
// Collection.prototype.set = function (/*models*/) {
|
||||
// // TODO:
|
||||
// };
|
||||
async update (models) {
|
||||
const array = isArray(models)
|
||||
if (!isArray(models)) {
|
||||
models = [models]
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates existing models.
|
||||
*/
|
||||
Collection.prototype.update = function (models) {
|
||||
var array = true;
|
||||
if (!_.isArray(models))
|
||||
{
|
||||
models = [models];
|
||||
array = false;
|
||||
}
|
||||
const {Model} = this
|
||||
map(models, model => {
|
||||
if (!(model instanceof Model)) {
|
||||
// TODO: Problems, we may be mixing in some default
|
||||
// properties which will overwrite existing ones.
|
||||
model = new Model(model)
|
||||
}
|
||||
|
||||
for (var i = 0, n = models.length; i < n; i++)
|
||||
{
|
||||
var model = models[i];
|
||||
const id = model.get('id')
|
||||
|
||||
if ( !(model instanceof this.model) )
|
||||
{
|
||||
// TODO: Problems, we may be mixing in some default
|
||||
// properties which will overwrite existing ones.
|
||||
model = new this.model(model);
|
||||
}
|
||||
// Missing models should be added not updated.
|
||||
if (id === undefined) {
|
||||
// FIXME: should not throw an exception but return a rejected promise.
|
||||
throw new Error('a model without an id cannot be updated')
|
||||
}
|
||||
|
||||
var id = model.get('id');
|
||||
const error = model.validate()
|
||||
if (error !== undefined) {
|
||||
// TODO: Better system inspired by Backbone.js.
|
||||
throw error
|
||||
}
|
||||
|
||||
// Missing models should be added not updated.
|
||||
if (!id)
|
||||
{
|
||||
return Promise.reject('a model without an id cannot be updated');
|
||||
}
|
||||
return model.properties
|
||||
}, models)
|
||||
|
||||
var error = model.validate();
|
||||
if (undefined !== error)
|
||||
{
|
||||
// TODO: Better system inspired by Backbone.js.
|
||||
throw error;
|
||||
}
|
||||
models = await this._update(models)
|
||||
this.emit('update', models)
|
||||
|
||||
models[i] = model.properties;
|
||||
}
|
||||
return array
|
||||
? models
|
||||
: new this.Model(models[0])
|
||||
}
|
||||
|
||||
var self = this;
|
||||
return Promise.cast(this._update(models)).then(function (models) {
|
||||
self.emit('update', models);
|
||||
// Methods to override in implementations.
|
||||
|
||||
if (!array)
|
||||
{
|
||||
return models[0];
|
||||
}
|
||||
return models;
|
||||
});
|
||||
};
|
||||
_add () {
|
||||
throw new Error('not implemented')
|
||||
}
|
||||
|
||||
//Collection.extend = require('extendable');
|
||||
_get () {
|
||||
throw new Error('not implemented')
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// Methods to override in implementations.
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
_remove () {
|
||||
throw new Error('not implemented')
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
Collection.prototype._add = function (models, options) {
|
||||
throw 'not implemented';
|
||||
};
|
||||
_update () {
|
||||
throw new Error('not implemented')
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
Collection.prototype._get = function (properties) {
|
||||
throw 'not implemented';
|
||||
};
|
||||
// Methods which may be overridden in implementations.
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
Collection.prototype._remove = function (ids) {
|
||||
throw 'not implemented';
|
||||
};
|
||||
count (properties) {
|
||||
return this.get(properties).get('count')
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
Collection.prototype._update = function (models) {
|
||||
throw 'not implemented';
|
||||
};
|
||||
exists (properties) {
|
||||
/* jshint eqnull: true */
|
||||
return this.first(properties).then(model => model != null)
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// Methods which may be overriden in implementations.
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
async _first (properties) {
|
||||
const models = await this.get(properties)
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
Collection.prototype.count = function (properties) {
|
||||
return this.get(properties).then(function (models) {
|
||||
return models.length;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
Collection.prototype.exists = function (properties) {
|
||||
return this.first(properties).then(function (model) {
|
||||
return (null !== model);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
Collection.prototype._first = function (properties) {
|
||||
return Promise.cast(this.get(properties)).then(function (models) {
|
||||
if (0 === models.length)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return models[0];
|
||||
});
|
||||
};
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
|
||||
module.exports = Collection;
|
||||
return models.length
|
||||
? models[0]
|
||||
: null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
//====================================================================
|
||||
|
||||
var _ = require('underscore');
|
||||
var Promise = require('bluebird');
|
||||
|
||||
//====================================================================
|
||||
|
||||
function Memory(models)
|
||||
{
|
||||
Memory.super_.call(this);
|
||||
|
||||
this.models = {};
|
||||
this.next_id = 0;
|
||||
|
||||
if (models)
|
||||
{
|
||||
this.add(models);
|
||||
}
|
||||
}
|
||||
require('util').inherits(Memory, require('../collection'));
|
||||
|
||||
Memory.prototype._add = function (models, options) {
|
||||
// TODO: Temporary mesure, implement “set()” instead.
|
||||
var replace = !!(options && options.replace);
|
||||
|
||||
for (var i = 0, n = models.length; i < n; ++i)
|
||||
{
|
||||
var model = models[i];
|
||||
|
||||
var id = model.id;
|
||||
|
||||
if (undefined === id)
|
||||
{
|
||||
model.id = id = ''+ this.next_id++;
|
||||
}
|
||||
else if (!replace && this.models[id])
|
||||
{
|
||||
// Existing models are ignored.
|
||||
return Promise.reject('cannot add existing models!');
|
||||
}
|
||||
|
||||
this.models[id] = model;
|
||||
}
|
||||
|
||||
return models;
|
||||
};
|
||||
|
||||
Memory.prototype._first = function (properties) {
|
||||
if (_.isEmpty(properties))
|
||||
{
|
||||
// Return the first model if any.
|
||||
for (var id in this.models)
|
||||
{
|
||||
return this.models[id];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return _.findWhere(this.models, properties);
|
||||
};
|
||||
|
||||
Memory.prototype._get = function (properties) {
|
||||
if (_.isEmpty(properties))
|
||||
{
|
||||
return _.values(this.models);
|
||||
}
|
||||
|
||||
return _.where(this.models, properties);
|
||||
};
|
||||
|
||||
Memory.prototype._remove = function (ids) {
|
||||
for (var i = 0, n = ids.length; i < n; ++i)
|
||||
{
|
||||
delete this.models[ids[i]];
|
||||
}
|
||||
};
|
||||
|
||||
Memory.prototype._update = function (models) {
|
||||
for (var i = 0, n = models.length; i < n; i++)
|
||||
{
|
||||
var model = models[i];
|
||||
|
||||
var id = model.id;
|
||||
|
||||
// Missing models should be added not updated.
|
||||
if (!this.models[id])
|
||||
{
|
||||
return Promise.reject('missing model');
|
||||
}
|
||||
|
||||
_.extend(this.models[id], model);
|
||||
}
|
||||
return models;
|
||||
};
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
|
||||
Memory.extend = require('extendable');
|
||||
module.exports = Memory;
|
||||
|
||||
@@ -1,22 +1,27 @@
|
||||
'use strict';
|
||||
import Collection, {ModelAlreadyExists} from '../collection'
|
||||
import difference from 'lodash/difference'
|
||||
import filter from 'lodash/filter'
|
||||
import getKey from 'lodash/keys'
|
||||
import {createClient as createRedisClient} from 'redis'
|
||||
import {v4 as generateUuid} from 'uuid'
|
||||
|
||||
//====================================================================
|
||||
import {
|
||||
forEach,
|
||||
isEmpty,
|
||||
mapToArray,
|
||||
promisifyAll
|
||||
} from '../utils'
|
||||
|
||||
var _ = require('underscore');
|
||||
var Promise = require('bluebird');
|
||||
// ===================================================================
|
||||
|
||||
var thenRedis = require('then-redis');
|
||||
|
||||
//====================================================================
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// ///////////////////////////////////////////////////////////////////
|
||||
// Data model:
|
||||
// - prefix +'_id': value of the last generated identifier;
|
||||
// - prefix +'_ids': set containing identifier of all models;
|
||||
// - prefix +'_'+ index +':' + value: set of identifiers which have
|
||||
// value for the given index.
|
||||
// - prefix +':'+ id: hash containing the properties of a model;
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// ///////////////////////////////////////////////////////////////////
|
||||
|
||||
// TODO: then-redis sends commands in order, we should use this
|
||||
// semantic to simplify the code.
|
||||
@@ -26,208 +31,139 @@ var thenRedis = require('then-redis');
|
||||
|
||||
// TODO: Remote events.
|
||||
|
||||
function Redis(options, models)
|
||||
{
|
||||
if (!options)
|
||||
{
|
||||
options = {};
|
||||
}
|
||||
export default class Redis extends Collection {
|
||||
constructor ({
|
||||
connection,
|
||||
indexes = [],
|
||||
prefix,
|
||||
uri = 'tcp://localhost:6379'
|
||||
}) {
|
||||
super()
|
||||
|
||||
_.defaults(options, {
|
||||
'uri': 'tcp://localhost:6379',
|
||||
'indexes': [],
|
||||
});
|
||||
this.indexes = indexes
|
||||
this.prefix = prefix
|
||||
this.redis = promisifyAll(connection || createRedisClient(uri))
|
||||
}
|
||||
|
||||
if (!options.prefix)
|
||||
{
|
||||
throw 'missing option: prefix';
|
||||
}
|
||||
_extract (ids) {
|
||||
const prefix = this.prefix + ':'
|
||||
const {redis} = this
|
||||
|
||||
Redis.super_.call(this, models);
|
||||
const models = []
|
||||
return Promise.all(mapToArray(ids, id => {
|
||||
return redis.hgetall(prefix + id).then(model => {
|
||||
// If empty, consider it a no match.
|
||||
if (isEmpty(model)) {
|
||||
return
|
||||
}
|
||||
|
||||
this.redis = options.connection || thenRedis.createClient(options.uri);
|
||||
this.prefix = options.prefix;
|
||||
this.indexes = options.indexes;
|
||||
// Mix the identifier in.
|
||||
model.id = id
|
||||
|
||||
models.push(model)
|
||||
})
|
||||
})).then(() => models)
|
||||
}
|
||||
|
||||
_add (models, {replace = false} = {}) {
|
||||
// TODO: remove “replace” which is a temporary measure, implement
|
||||
// “set()” instead.
|
||||
|
||||
const {indexes, prefix, redis} = this
|
||||
|
||||
return Promise.all(mapToArray(models, async model => {
|
||||
// Generate a new identifier if necessary.
|
||||
if (model.id === undefined) {
|
||||
model.id = generateUuid()
|
||||
}
|
||||
|
||||
const success = await redis.sadd(prefix + '_ids', model.id)
|
||||
|
||||
// The entry already exists an we are not in replace mode.
|
||||
if (!success && !replace) {
|
||||
throw new ModelAlreadyExists(model.id)
|
||||
}
|
||||
|
||||
// TODO: Remove existing fields.
|
||||
|
||||
const params = []
|
||||
forEach(model, (value, name) => {
|
||||
// No need to store the identifier (already in the key).
|
||||
if (name === 'id') {
|
||||
return
|
||||
}
|
||||
|
||||
params.push(name, value)
|
||||
})
|
||||
|
||||
const key = `${prefix}:${model.id}`
|
||||
const promises = [
|
||||
redis.del(key),
|
||||
redis.hmset(key, ...params)
|
||||
]
|
||||
|
||||
// Update indexes.
|
||||
forEach(indexes, (index) => {
|
||||
const value = model[index]
|
||||
if (value === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
const key = prefix + '_' + index + ':' + value
|
||||
promises.push(redis.sadd(key, model.id))
|
||||
})
|
||||
|
||||
await Promise.all(promises)
|
||||
|
||||
return model
|
||||
}))
|
||||
}
|
||||
|
||||
_get (properties) {
|
||||
const {prefix, redis} = this
|
||||
|
||||
if (isEmpty(properties)) {
|
||||
return redis.smembers(prefix + '_ids').then(ids => this._extract(ids))
|
||||
}
|
||||
|
||||
// Special treatment for the identifier.
|
||||
const id = properties.id
|
||||
if (id !== undefined) {
|
||||
delete properties.id
|
||||
return this._extract([id]).then(models => {
|
||||
return (models.length && !isEmpty(properties))
|
||||
? filter(models)
|
||||
: models
|
||||
})
|
||||
}
|
||||
|
||||
const {indexes} = this
|
||||
|
||||
// Check for non indexed fields.
|
||||
const unfit = difference(getKey(properties), indexes)
|
||||
if (unfit.length) {
|
||||
throw new Error('fields not indexed: ' + unfit.join())
|
||||
}
|
||||
|
||||
const keys = mapToArray(properties, (value, index) => `${prefix}_${index}:${value}`)
|
||||
return redis.sinter(...keys).then(ids => this._extract(ids))
|
||||
}
|
||||
|
||||
_remove (ids) {
|
||||
const {prefix, redis} = this
|
||||
|
||||
// TODO: handle indexes.
|
||||
|
||||
return Promise.all([
|
||||
// Remove the identifiers from the main index.
|
||||
redis.srem(prefix + '_ids', ...ids),
|
||||
|
||||
// Remove the models.
|
||||
redis.del(mapToArray(ids, id => `${prefix}:${id}`))
|
||||
])
|
||||
}
|
||||
|
||||
_update (models) {
|
||||
return this._add(models, { replace: true })
|
||||
}
|
||||
}
|
||||
require('util').inherits(Redis, require('../collection'));
|
||||
|
||||
// Private method.
|
||||
Redis.prototype._extract = function (ids) {
|
||||
var redis = this.redis;
|
||||
var prefix = this.prefix +':';
|
||||
|
||||
var promises = [];
|
||||
|
||||
_.each(ids, function (id) {
|
||||
promises.push(redis.hgetall(prefix + id).then(function (model) {
|
||||
// If empty, considers it a no match and returns null.
|
||||
if (_.isEmpty(model))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Mix the identifier in.
|
||||
model.id = id;
|
||||
return model;
|
||||
}));
|
||||
});
|
||||
|
||||
return Promise.all(promises).then(function (models) {
|
||||
return _.filter(models, function (model) {
|
||||
return (null !== model);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
Redis.prototype._add = function (models, options) {
|
||||
// TODO: Temporary mesure, implement “set()” instead.
|
||||
var replace = !!(options && options.replace);
|
||||
|
||||
var redis = this.redis;
|
||||
var prefix = this.prefix;
|
||||
var indexes = this.indexes;
|
||||
|
||||
var promises = [];
|
||||
|
||||
_.each(models, function (model) {
|
||||
var promise;
|
||||
|
||||
// Generates a new identifier if necessary.
|
||||
if (undefined === model.id)
|
||||
{
|
||||
promise = redis.incr(prefix +'_id').then(function (id) {
|
||||
model.id = id;
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
// Ensures the promise chain is correctly initialized.
|
||||
promise = Promise.cast();
|
||||
}
|
||||
|
||||
promise = promise.then(function () {
|
||||
// Adds the identifier to the models' ids set.
|
||||
return redis.sadd(prefix +'_ids', model.id);
|
||||
}).then(function (success) {
|
||||
// The entry already existed an we are not in replace mode.
|
||||
if (!success && !replace)
|
||||
{
|
||||
throw 'cannot add existing model: '+ model.id;
|
||||
}
|
||||
|
||||
// TODO: Remove existing fields.
|
||||
|
||||
var params = [prefix +':'+ model.id];
|
||||
_.each(model, function (value, prop) {
|
||||
// No need to store the id (already in the key.)
|
||||
if ('id' === prop)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
params.push(prop, value);
|
||||
});
|
||||
|
||||
var promises = [
|
||||
redis.send('hmset', params),
|
||||
];
|
||||
|
||||
// Adds indexes.
|
||||
_.each(indexes, function (index) {
|
||||
var value = model[index];
|
||||
if (undefined === value)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var key = prefix +'_'+ index +':'+ value;
|
||||
promises.push(redis.sadd(key, model.id));
|
||||
});
|
||||
|
||||
return Promise.all(promises);
|
||||
|
||||
}).then(function () { return model; });
|
||||
|
||||
promises.push(promise);
|
||||
});
|
||||
|
||||
return Promise.all(promises);
|
||||
};
|
||||
|
||||
Redis.prototype._get = function (properties) {
|
||||
var prefix = this.prefix;
|
||||
var redis = this.redis;
|
||||
var self = this;
|
||||
|
||||
if (_.isEmpty(properties))
|
||||
{
|
||||
return redis.smembers(prefix +'_ids').then(function (ids) {
|
||||
return self._extract(ids);
|
||||
});
|
||||
}
|
||||
|
||||
// Special treatment for 'id'.
|
||||
var id = properties.id;
|
||||
delete properties.id;
|
||||
|
||||
// Special case where we only match against id.
|
||||
if (_.isEmpty(properties))
|
||||
{
|
||||
return this._extract([id]);
|
||||
}
|
||||
|
||||
var indexes = this.indexes;
|
||||
var unfit = _.difference(_.keys(properties), indexes);
|
||||
if (0 !== unfit.length)
|
||||
{
|
||||
throw 'not indexed fields: '+ unfit.join();
|
||||
}
|
||||
|
||||
var keys = _.map(properties, function (value, index) {
|
||||
return (prefix +'_'+ index +':'+ value);
|
||||
});
|
||||
return redis.send('sinter', keys).then(function (ids) {
|
||||
if (undefined !== id)
|
||||
{
|
||||
if (!_.contains(ids, id))
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
ids = [id];
|
||||
}
|
||||
|
||||
return self._extract(ids);
|
||||
});
|
||||
};
|
||||
|
||||
Redis.prototype._remove = function (ids) {
|
||||
var redis = this.redis;
|
||||
var prefix = this.prefix;
|
||||
|
||||
var promises = [];
|
||||
|
||||
var keys = [];
|
||||
for (var i = 0, n = ids.length; i < n; ++i)
|
||||
{
|
||||
keys.push(prefix +':'+ ids[i]);
|
||||
}
|
||||
|
||||
// TODO: Handle indexes.
|
||||
promises.push(
|
||||
redis.send('srem', [prefix +'_ids'].concat(ids)),
|
||||
redis.send('del', keys)
|
||||
);
|
||||
|
||||
return Promise.all(promises);
|
||||
};
|
||||
|
||||
Redis.prototype._update = function (models) {
|
||||
// TODO:
|
||||
return this._add(models, { 'replace': true });
|
||||
};
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
|
||||
Redis.extend = require('extendable');
|
||||
module.exports = Redis;
|
||||
|
||||
@@ -1,81 +1,50 @@
|
||||
'use strict';
|
||||
import {EventEmitter} from 'events'
|
||||
|
||||
//====================================================================
|
||||
import {createRawObject, noop} from './utils'
|
||||
|
||||
var EventEmitter = require('events').EventEmitter;
|
||||
var inherits = require('util').inherits;
|
||||
// ===================================================================
|
||||
|
||||
//--------------------------------------------------------------------
|
||||
export default class Connection extends EventEmitter {
|
||||
constructor () {
|
||||
super()
|
||||
|
||||
var extend = require('underscore').extend;
|
||||
this._data = createRawObject()
|
||||
}
|
||||
|
||||
//====================================================================
|
||||
// Close the connection.
|
||||
close () {
|
||||
// Prevent errors when the connection is closed more than once.
|
||||
this.close = noop
|
||||
|
||||
var has = Object.prototype.hasOwnProperty;
|
||||
has = has.call.bind(has);
|
||||
this.emit('close')
|
||||
}
|
||||
|
||||
//====================================================================
|
||||
// Gets the value for this key.
|
||||
get (key, defaultValue) {
|
||||
const {_data: data} = this
|
||||
|
||||
var Connection = function Connection(adapter) {
|
||||
this.data = Object.create(null);
|
||||
if (key in data) {
|
||||
return data[key]
|
||||
}
|
||||
|
||||
this._adapter = adapter;
|
||||
};
|
||||
inherits(Connection, EventEmitter);
|
||||
if (arguments.length >= 2) {
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
extend(Connection.prototype, {
|
||||
// Close the connection.
|
||||
close: function () {
|
||||
this._adapter.close();
|
||||
this.emit('close');
|
||||
throw new Error('no value for `' + key + '`')
|
||||
}
|
||||
|
||||
// Releases values AMAP to ease the garbage collecting.
|
||||
for (var key in this)
|
||||
{
|
||||
if (has(this, key))
|
||||
{
|
||||
delete this[key];
|
||||
}
|
||||
}
|
||||
},
|
||||
// Checks whether there is a value for this key.
|
||||
has (key) {
|
||||
return key in this._data
|
||||
}
|
||||
|
||||
// Gets the value for this key.
|
||||
get: function (key, defaultValue) {
|
||||
var data = this.data;
|
||||
// Sets the value for this key.
|
||||
set (key, value) {
|
||||
this._data[key] = value
|
||||
}
|
||||
|
||||
if (key in data)
|
||||
{
|
||||
return data[key];
|
||||
}
|
||||
|
||||
if (arguments.length >= 2)
|
||||
{
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
throw new Error('no value for `'+ key +'`');
|
||||
},
|
||||
|
||||
// Checks whether there is a value for this key.
|
||||
has: function (key) {
|
||||
return key in this.data;
|
||||
},
|
||||
|
||||
// Sets the value for this key.
|
||||
set: function (key, value) {
|
||||
this.data[key] = value;
|
||||
},
|
||||
|
||||
// Sends a message.
|
||||
send: function (name, data) {
|
||||
this._adapter.send(name, data);
|
||||
},
|
||||
|
||||
unset: function (key) {
|
||||
delete this.data[key];
|
||||
},
|
||||
});
|
||||
|
||||
//====================================================================
|
||||
|
||||
module.exports = Connection;
|
||||
unset (key) {
|
||||
delete this._data[key]
|
||||
}
|
||||
}
|
||||
|
||||
347
src/decorators.js
Normal file
347
src/decorators.js
Normal file
@@ -0,0 +1,347 @@
|
||||
import bind from 'lodash/bind'
|
||||
|
||||
import {
|
||||
isArray,
|
||||
isPromise,
|
||||
isFunction,
|
||||
noop,
|
||||
pFinally
|
||||
} from './utils'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const {
|
||||
defineProperties,
|
||||
defineProperty,
|
||||
getOwnPropertyDescriptor
|
||||
} = Object
|
||||
|
||||
// ===================================================================
|
||||
|
||||
// See: https://github.com/jayphelps/core-decorators.js#autobind
|
||||
//
|
||||
// TODO: make it work for all class methods.
|
||||
export const autobind = (target, key, {
|
||||
configurable,
|
||||
enumerable,
|
||||
value: fn,
|
||||
writable
|
||||
}) => ({
|
||||
configurable,
|
||||
enumerable,
|
||||
|
||||
get () {
|
||||
if (this === target) {
|
||||
return fn
|
||||
}
|
||||
|
||||
const bound = bind(fn, this)
|
||||
|
||||
defineProperty(this, key, {
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
value: bound,
|
||||
writable: true
|
||||
})
|
||||
|
||||
return bound
|
||||
},
|
||||
set (newValue) {
|
||||
// Cannot use assignment because it will call the setter on
|
||||
// the prototype.
|
||||
defineProperty(this, key, {
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
value: newValue,
|
||||
writable: true
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
// Debounce decorator for methods.
|
||||
//
|
||||
// See: https://github.com/wycats/javascript-decorators
|
||||
//
|
||||
// TODO: make it work for single functions.
|
||||
export const debounce = duration => (target, name, descriptor) => {
|
||||
const fn = descriptor.value
|
||||
|
||||
// This symbol is used to store the related data directly on the
|
||||
// current object.
|
||||
const s = Symbol()
|
||||
|
||||
function debounced () {
|
||||
const data = this[s] || (this[s] = {
|
||||
lastCall: 0,
|
||||
wrapper: null
|
||||
})
|
||||
|
||||
const now = Date.now()
|
||||
if (now > data.lastCall + duration) {
|
||||
data.lastCall = now
|
||||
try {
|
||||
const result = fn.apply(this, arguments)
|
||||
data.wrapper = () => result
|
||||
} catch (error) {
|
||||
data.wrapper = () => { throw error }
|
||||
}
|
||||
}
|
||||
return data.wrapper()
|
||||
}
|
||||
debounced.reset = obj => { delete obj[s] }
|
||||
|
||||
descriptor.value = debounced
|
||||
return descriptor
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
const _push = Array.prototype.push
|
||||
|
||||
export const deferrable = (target, name, descriptor) => {
|
||||
let fn
|
||||
function newFn () {
|
||||
const deferreds = []
|
||||
const defer = fn => {
|
||||
deferreds.push(fn)
|
||||
}
|
||||
defer.clear = () => {
|
||||
deferreds.length = 0
|
||||
}
|
||||
|
||||
const args = [ defer ]
|
||||
_push.apply(args, arguments)
|
||||
|
||||
let executeDeferreds = () => {
|
||||
let i = deferreds.length
|
||||
while (i) {
|
||||
deferreds[--i]()
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const result = fn.apply(this, args)
|
||||
|
||||
if (isPromise(result)) {
|
||||
result::pFinally(executeDeferreds)
|
||||
|
||||
// Do not execute the deferreds in the finally block.
|
||||
executeDeferreds = noop
|
||||
}
|
||||
|
||||
return result
|
||||
} finally {
|
||||
executeDeferreds()
|
||||
}
|
||||
}
|
||||
|
||||
if (descriptor) {
|
||||
fn = descriptor.value
|
||||
descriptor.value = newFn
|
||||
|
||||
return descriptor
|
||||
}
|
||||
|
||||
fn = target
|
||||
return newFn
|
||||
}
|
||||
|
||||
// Deferred functions are only executed on failures.
|
||||
//
|
||||
// i.e.: defer.clear() is automatically called in case of success.
|
||||
deferrable.onFailure = (target, name, descriptor) => {
|
||||
let fn
|
||||
function newFn (defer) {
|
||||
const result = fn.apply(this, arguments)
|
||||
|
||||
return isPromise(result)
|
||||
? result.then(result => {
|
||||
defer.clear()
|
||||
return result
|
||||
})
|
||||
: (defer.clear(), result)
|
||||
}
|
||||
|
||||
if (descriptor) {
|
||||
fn = descriptor.value
|
||||
descriptor.value = newFn
|
||||
} else {
|
||||
fn = target
|
||||
target = newFn
|
||||
}
|
||||
|
||||
return deferrable(target, name, descriptor)
|
||||
}
|
||||
|
||||
// Deferred functions are only executed on success.
|
||||
//
|
||||
// i.e.: defer.clear() is automatically called in case of failure.
|
||||
deferrable.onSuccess = (target, name, descriptor) => {
|
||||
let fn
|
||||
function newFn (defer) {
|
||||
try {
|
||||
const result = fn.apply(this, arguments)
|
||||
|
||||
return isPromise(result)
|
||||
? result.then(null, error => {
|
||||
defer.clear()
|
||||
throw error
|
||||
})
|
||||
: result
|
||||
} catch (error) {
|
||||
defer.clear()
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
if (descriptor) {
|
||||
fn = descriptor.value
|
||||
descriptor.value = newFn
|
||||
} else {
|
||||
fn = target
|
||||
target = newFn
|
||||
}
|
||||
|
||||
return deferrable(target, name, descriptor)
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
const _ownKeys = (
|
||||
typeof Reflect !== 'undefined' && Reflect.ownKeys ||
|
||||
(({
|
||||
getOwnPropertyNames: names,
|
||||
getOwnPropertySymbols: symbols
|
||||
}) => symbols
|
||||
? obj => names(obj).concat(symbols(obj))
|
||||
: names
|
||||
)(Object)
|
||||
)
|
||||
|
||||
const _bindPropertyDescriptor = (descriptor, thisArg) => {
|
||||
const { get, set, value } = descriptor
|
||||
if (get) {
|
||||
descriptor.get = bind(get, thisArg)
|
||||
}
|
||||
if (set) {
|
||||
descriptor.set = bind(set, thisArg)
|
||||
}
|
||||
|
||||
if (isFunction(value)) {
|
||||
descriptor.value = bind(value, thisArg)
|
||||
}
|
||||
|
||||
return descriptor
|
||||
}
|
||||
|
||||
const _isIgnoredProperty = name => (
|
||||
name[0] === '_' ||
|
||||
name === 'constructor'
|
||||
)
|
||||
|
||||
const _IGNORED_STATIC_PROPERTIES = {
|
||||
__proto__: null,
|
||||
|
||||
arguments: true,
|
||||
caller: true,
|
||||
length: true,
|
||||
name: true,
|
||||
prototype: true
|
||||
}
|
||||
const _isIgnoredStaticProperty = name => _IGNORED_STATIC_PROPERTIES[name]
|
||||
|
||||
export const mixin = MixIns => Class => {
|
||||
if (!isArray(MixIns)) {
|
||||
MixIns = [ MixIns ]
|
||||
}
|
||||
|
||||
const { name } = Class
|
||||
|
||||
// Copy properties of plain object mix-ins to the prototype.
|
||||
{
|
||||
const allMixIns = MixIns
|
||||
MixIns = []
|
||||
const { prototype } = Class
|
||||
const descriptors = { __proto__: null }
|
||||
for (const MixIn of allMixIns) {
|
||||
if (isFunction(MixIn)) {
|
||||
MixIns.push(MixIn)
|
||||
continue
|
||||
}
|
||||
|
||||
for (const prop of _ownKeys(MixIn)) {
|
||||
if (prop in prototype) {
|
||||
throw new Error(`${name}#${prop} is already defined`)
|
||||
}
|
||||
|
||||
(
|
||||
descriptors[prop] = getOwnPropertyDescriptor(MixIn, prop)
|
||||
).enumerable = false // Object methods are enumerable but class methods are not.
|
||||
}
|
||||
}
|
||||
defineProperties(prototype, descriptors)
|
||||
}
|
||||
|
||||
const Decorator = (...args) => {
|
||||
const instance = new Class(...args)
|
||||
|
||||
for (const MixIn of MixIns) {
|
||||
const { prototype } = MixIn
|
||||
const mixinInstance = new MixIn(instance)
|
||||
const descriptors = { __proto__: null }
|
||||
for (const prop of _ownKeys(prototype)) {
|
||||
if (_isIgnoredProperty(prop)) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (prop in instance) {
|
||||
throw new Error(`${name}#${prop} is already defined`)
|
||||
}
|
||||
|
||||
descriptors[prop] = _bindPropertyDescriptor(
|
||||
getOwnPropertyDescriptor(prototype, prop),
|
||||
mixinInstance
|
||||
)
|
||||
}
|
||||
defineProperties(instance, descriptors)
|
||||
}
|
||||
|
||||
return instance
|
||||
}
|
||||
|
||||
// Copy original and mixed-in static properties on Decorator class.
|
||||
const descriptors = { __proto__: null }
|
||||
for (const prop of _ownKeys(Class)) {
|
||||
let descriptor
|
||||
if (!(
|
||||
// Special properties are not defined...
|
||||
_isIgnoredStaticProperty(prop) &&
|
||||
|
||||
// if they already exist...
|
||||
(descriptor = getOwnPropertyDescriptor(Decorator, prop)) &&
|
||||
|
||||
// and are not configurable.
|
||||
!descriptor.configurable
|
||||
)) {
|
||||
descriptors[prop] = getOwnPropertyDescriptor(Class, prop)
|
||||
}
|
||||
}
|
||||
for (const MixIn of MixIns) {
|
||||
for (const prop of _ownKeys(MixIn)) {
|
||||
if (_isIgnoredStaticProperty(prop)) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (prop in descriptors) {
|
||||
throw new Error(`${name}.${prop} is already defined`)
|
||||
}
|
||||
|
||||
descriptors[prop] = getOwnPropertyDescriptor(MixIn, prop)
|
||||
}
|
||||
}
|
||||
defineProperties(Decorator, descriptors)
|
||||
|
||||
return Decorator
|
||||
}
|
||||
173
src/decorators.spec.js
Normal file
173
src/decorators.spec.js
Normal file
@@ -0,0 +1,173 @@
|
||||
/* eslint-env mocha */
|
||||
|
||||
import expect from 'must'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
import {autobind, debounce, deferrable} from './decorators'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
describe('autobind()', () => {
|
||||
class Foo {
|
||||
@autobind
|
||||
getFoo () {
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
it('returns a bound instance for a method', () => {
|
||||
const foo = new Foo()
|
||||
const { getFoo } = foo
|
||||
|
||||
expect(getFoo()).to.equal(foo)
|
||||
})
|
||||
|
||||
it('returns the same bound instance each time', () => {
|
||||
const foo = new Foo()
|
||||
|
||||
expect(foo.getFoo).to.equal(foo.getFoo)
|
||||
})
|
||||
|
||||
it('works with multiple instances of the same class', () => {
|
||||
const foo1 = new Foo()
|
||||
const foo2 = new Foo()
|
||||
|
||||
const getFoo1 = foo1.getFoo
|
||||
const getFoo2 = foo2.getFoo
|
||||
|
||||
expect(getFoo1()).to.equal(foo1)
|
||||
expect(getFoo2()).to.equal(foo2)
|
||||
})
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
describe('debounce()', () => {
|
||||
let i
|
||||
|
||||
class Foo {
|
||||
@debounce(1e1)
|
||||
foo () {
|
||||
++i
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
i = 0
|
||||
})
|
||||
|
||||
it('works', done => {
|
||||
const foo = new Foo()
|
||||
|
||||
expect(i).to.equal(0)
|
||||
|
||||
foo.foo()
|
||||
expect(i).to.equal(1)
|
||||
|
||||
foo.foo()
|
||||
expect(i).to.equal(1)
|
||||
|
||||
setTimeout(() => {
|
||||
foo.foo()
|
||||
expect(i).to.equal(2)
|
||||
|
||||
done()
|
||||
}, 2e1)
|
||||
})
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
describe('deferrable()', () => {
|
||||
it('works with normal termination', () => {
|
||||
let i = 0
|
||||
const fn = deferrable(defer => {
|
||||
i += 2
|
||||
defer(() => { i -= 2 })
|
||||
|
||||
i *= 2
|
||||
defer(() => { i /= 2 })
|
||||
|
||||
return i
|
||||
})
|
||||
|
||||
expect(fn()).to.equal(4)
|
||||
expect(i).to.equal(0)
|
||||
})
|
||||
|
||||
it('defer.clear() removes previous deferreds', () => {
|
||||
let i = 0
|
||||
const fn = deferrable(defer => {
|
||||
i += 2
|
||||
defer(() => { i -= 2 })
|
||||
|
||||
defer.clear()
|
||||
|
||||
i *= 2
|
||||
defer(() => { i /= 2 })
|
||||
|
||||
return i
|
||||
})
|
||||
|
||||
expect(fn()).to.equal(4)
|
||||
expect(i).to.equal(2)
|
||||
})
|
||||
|
||||
it('works with exception', () => {
|
||||
let i = 0
|
||||
const fn = deferrable(defer => {
|
||||
i += 2
|
||||
defer(() => { i -= 2 })
|
||||
|
||||
i *= 2
|
||||
defer(() => { i /= 2 })
|
||||
|
||||
throw i
|
||||
})
|
||||
|
||||
expect(() => fn()).to.throw(4)
|
||||
expect(i).to.equal(0)
|
||||
})
|
||||
|
||||
it('works with promise resolution', async () => {
|
||||
let i = 0
|
||||
const fn = deferrable(async defer => {
|
||||
i += 2
|
||||
defer(() => { i -= 2 })
|
||||
|
||||
i *= 2
|
||||
defer(() => { i /= 2 })
|
||||
|
||||
// Wait a turn of the events loop.
|
||||
await Promise.resolve()
|
||||
|
||||
return i
|
||||
})
|
||||
|
||||
await expect(fn()).to.eventually.equal(4)
|
||||
expect(i).to.equal(0)
|
||||
})
|
||||
|
||||
it('works with promise rejection', async () => {
|
||||
let i = 0
|
||||
const fn = deferrable(async defer => {
|
||||
// Wait a turn of the events loop.
|
||||
await Promise.resolve()
|
||||
|
||||
i += 2
|
||||
defer(() => { i -= 2 })
|
||||
|
||||
i *= 2
|
||||
defer(() => { i /= 2 })
|
||||
|
||||
// Wait a turn of the events loop.
|
||||
await Promise.resolve()
|
||||
|
||||
throw i
|
||||
})
|
||||
|
||||
await expect(fn()).to.reject.to.equal(4)
|
||||
expect(i).to.equal(0)
|
||||
})
|
||||
})
|
||||
84
src/fatfs-buffer.js
Normal file
84
src/fatfs-buffer.js
Normal file
@@ -0,0 +1,84 @@
|
||||
// Buffer driver for [fatfs](https://github.com/natevw/fatfs).
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// ```js
|
||||
// import fatfs from 'fatfs'
|
||||
// import fatfsBuffer, { init as fatfsBufferInit } from './fatfs-buffer'
|
||||
//
|
||||
// const buffer = fatfsBufferinit()
|
||||
//
|
||||
// const fs = fatfs.createFileSystem(fatfsBuffer(buffer))
|
||||
//
|
||||
// fs.writeFile('/foo', 'content of foo', function (err, content) {
|
||||
// if (err) {
|
||||
// console.error(err)
|
||||
// }
|
||||
// })
|
||||
|
||||
import { boot16 as fat16 } from 'fatfs/structs'
|
||||
|
||||
const SECTOR_SIZE = 512
|
||||
|
||||
// Creates a 10MB buffer and initializes it as a FAT 16 volume.
|
||||
export function init () {
|
||||
const buf = new Buffer(10 * 1024 * 1024) // 10MB
|
||||
buf.fill(0)
|
||||
|
||||
// https://github.com/natevw/fatfs/blob/master/structs.js
|
||||
fat16.pack({
|
||||
jmpBoot: new Buffer('eb3c90', 'hex'),
|
||||
OEMName: 'mkfs.fat',
|
||||
BytsPerSec: SECTOR_SIZE,
|
||||
SecPerClus: 4,
|
||||
ResvdSecCnt: 1,
|
||||
NumFATs: 2,
|
||||
RootEntCnt: 512,
|
||||
TotSec16: 20480,
|
||||
Media: 248,
|
||||
FATSz16: 20,
|
||||
SecPerTrk: 32,
|
||||
NumHeads: 64,
|
||||
HiddSec: 0,
|
||||
TotSec32: 0,
|
||||
DrvNum: 128,
|
||||
Reserved1: 0,
|
||||
BootSig: 41,
|
||||
VolID: 895111106,
|
||||
VolLab: 'NO NAME ',
|
||||
FilSysType: 'FAT16 '
|
||||
}, buf)
|
||||
|
||||
// End of sector.
|
||||
buf[0x1fe] = 0x55
|
||||
buf[0x1ff] = 0xaa
|
||||
|
||||
// Mark sector as reserved.
|
||||
buf[0x200] = 0xf8
|
||||
buf[0x201] = 0xff
|
||||
buf[0x202] = 0xff
|
||||
buf[0x203] = 0xff
|
||||
|
||||
// Mark sector as reserved.
|
||||
buf[0x2a00] = 0xf8
|
||||
buf[0x2a01] = 0xff
|
||||
buf[0x2a02] = 0xff
|
||||
buf[0x2a03] = 0xff
|
||||
|
||||
return buf
|
||||
}
|
||||
|
||||
export default buffer => {
|
||||
return {
|
||||
sectorSize: SECTOR_SIZE,
|
||||
numSectors: Math.floor(buffer.length / SECTOR_SIZE),
|
||||
readSectors: (i, target, cb) => {
|
||||
buffer.copy(target, 0, i * SECTOR_SIZE)
|
||||
cb()
|
||||
},
|
||||
writeSectors: (i, source, cb) => {
|
||||
source.copy(buffer, i * SECTOR_SIZE, 0)
|
||||
cb()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,141 +0,0 @@
|
||||
# Low level tools.
|
||||
$_ = require 'underscore'
|
||||
|
||||
# Async code is easier with fibers (light threads)!
|
||||
$fiber = require 'fibers'
|
||||
|
||||
$Promise = require 'bluebird'
|
||||
|
||||
#=====================================================================
|
||||
|
||||
$isPromise = (obj) -> obj? and $_.isFunction obj.then
|
||||
|
||||
# The value is guarantee to resolve asynchronously.
|
||||
$runAsync = (value, resolve, reject) ->
|
||||
if $isPromise value
|
||||
return value.then resolve, reject
|
||||
|
||||
if $_.isFunction value # Continuable
|
||||
async = false
|
||||
handler = (error, result) ->
|
||||
unless async
|
||||
return process.nextTick handler.bind null, error, result
|
||||
if error?
|
||||
return reject error
|
||||
resolve result
|
||||
value handler
|
||||
async = true
|
||||
return
|
||||
|
||||
unless $_.isObject value
|
||||
return process.nextTick -> resolve value
|
||||
|
||||
left = 0
|
||||
results = if $_.isArray value
|
||||
new Array value.length
|
||||
else
|
||||
Object.create null
|
||||
|
||||
$_.each value, (value, index) ->
|
||||
++left
|
||||
$runAsync(
|
||||
value
|
||||
(result) ->
|
||||
# Returns if already rejected.
|
||||
return unless results
|
||||
|
||||
results[index] = result
|
||||
resolve results unless --left
|
||||
(error) ->
|
||||
# Returns if already rejected.
|
||||
return unless results
|
||||
|
||||
# Frees the reference ASAP.
|
||||
results = null
|
||||
|
||||
reject error
|
||||
)
|
||||
|
||||
if left is 0
|
||||
process.nextTick -> resolve value
|
||||
|
||||
#=====================================================================
|
||||
|
||||
# Makes a function running in its own fiber.
|
||||
$fiberize = (fn) ->
|
||||
(args...) ->
|
||||
$fiber(=>
|
||||
try
|
||||
fn.apply this, args
|
||||
catch error
|
||||
process.nextTick ->
|
||||
throw error
|
||||
).run()
|
||||
|
||||
# Makes a function run in its own fiber and returns a promise.
|
||||
$promisify = (fn) ->
|
||||
(args...) ->
|
||||
new $Promise (resolve, reject) ->
|
||||
$fiber(=>
|
||||
try
|
||||
resolve fn.apply this, args
|
||||
catch error
|
||||
reject error
|
||||
).run()
|
||||
|
||||
# Waits for an event.
|
||||
#
|
||||
# Note: if the *error* event is emitted, this function will throw.
|
||||
$waitEvent = (emitter, event) ->
|
||||
fiber = $fiber.current
|
||||
throw new Error 'not running in a fiber' unless fiber?
|
||||
|
||||
errorHandler = null
|
||||
handler = (args...) ->
|
||||
emitter.removeListener 'error', errorHandler
|
||||
fiber.run args
|
||||
errorHandler = (error) ->
|
||||
emitter.removeListener event, handler
|
||||
fiber.throwInto error
|
||||
|
||||
emitter.once event, handler
|
||||
emitter.once 'error', errorHandler
|
||||
|
||||
$fiber.yield()
|
||||
|
||||
# Waits for a promise or a continuable to end.
|
||||
#
|
||||
# If value is composed (array or map), every asynchronous value is
|
||||
# resolved before returning (parallelization).
|
||||
$wait = (value) ->
|
||||
fiber = $fiber.current
|
||||
throw new Error 'not running in a fiber' unless fiber?
|
||||
|
||||
if $wait._stash
|
||||
value = $wait._stash
|
||||
delete $wait._stash
|
||||
|
||||
$runAsync(
|
||||
value
|
||||
fiber.run.bind fiber
|
||||
fiber.throwInto.bind fiber
|
||||
)
|
||||
|
||||
$fiber.yield()
|
||||
|
||||
$wait.register = ->
|
||||
throw new Error 'something has already been registered' if $wait._stash
|
||||
|
||||
deferred = $Promise.defer()
|
||||
$wait._stash = deferred.promise
|
||||
|
||||
deferred.callback
|
||||
|
||||
#=====================================================================
|
||||
|
||||
module.exports = {
|
||||
$fiberize
|
||||
$promisify
|
||||
$waitEvent
|
||||
$wait
|
||||
}
|
||||
@@ -1,247 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
//====================================================================
|
||||
|
||||
var expect = require('chai').expect;
|
||||
|
||||
//--------------------------------------------------------------------
|
||||
|
||||
var Promise = require('bluebird');
|
||||
|
||||
//--------------------------------------------------------------------
|
||||
|
||||
var utils = require('./fibers-utils');
|
||||
var $fiberize = utils.$fiberize;
|
||||
|
||||
//====================================================================
|
||||
|
||||
describe('$fiberize', function () {
|
||||
it('creates a function which runs in a new fiber', function () {
|
||||
var previous = require('fibers').current;
|
||||
|
||||
var fn = $fiberize(function () {
|
||||
var current = require('fibers').current;
|
||||
|
||||
expect(current).to.exists;
|
||||
expect(current).to.not.equal(previous);
|
||||
});
|
||||
|
||||
fn();
|
||||
});
|
||||
|
||||
it('forwards all arguments (even this)', function () {
|
||||
var self = {};
|
||||
var arg1 = {};
|
||||
var arg2 = {};
|
||||
|
||||
$fiberize(function (arg1, arg2) {
|
||||
expect(this).to.equal(self);
|
||||
expect(arg1).to.equal(arg1);
|
||||
expect(arg2).to.equal(arg2);
|
||||
}).call(self, arg1, arg2);
|
||||
});
|
||||
});
|
||||
|
||||
//--------------------------------------------------------------------
|
||||
|
||||
describe('$wait', function () {
|
||||
var $wait = utils.$wait;
|
||||
|
||||
it('waits for a promise', function (done) {
|
||||
$fiberize(function () {
|
||||
var value = {};
|
||||
var promise = Promise.cast(value);
|
||||
|
||||
expect($wait(promise)).to.equal(value);
|
||||
|
||||
done();
|
||||
})();
|
||||
});
|
||||
|
||||
it('handles promise rejection', function (done) {
|
||||
$fiberize(function () {
|
||||
var promise = Promise.reject('an exception');
|
||||
|
||||
expect(function () {
|
||||
$wait(promise);
|
||||
}).to.throw('an exception');
|
||||
|
||||
done();
|
||||
})();
|
||||
});
|
||||
|
||||
it('waits for a continuable', function (done) {
|
||||
$fiberize(function () {
|
||||
var value = {};
|
||||
var continuable = function (callback) {
|
||||
callback(null, value);
|
||||
};
|
||||
|
||||
expect($wait(continuable)).to.equal(value);
|
||||
|
||||
done();
|
||||
})();
|
||||
});
|
||||
|
||||
it('handles continuable error', function (done) {
|
||||
$fiberize(function () {
|
||||
var continuable = function (callback) {
|
||||
callback('an exception');
|
||||
};
|
||||
|
||||
expect(function () {
|
||||
$wait(continuable);
|
||||
}).to.throw('an exception');
|
||||
|
||||
done();
|
||||
})();
|
||||
});
|
||||
|
||||
it('forwards scalar values', function (done) {
|
||||
$fiberize(function () {
|
||||
var value = 'a scalar value';
|
||||
expect($wait(value)).to.equal(value);
|
||||
|
||||
value = [
|
||||
'foo',
|
||||
'bar',
|
||||
'baz',
|
||||
];
|
||||
expect($wait(value)).to.deep.equal(value);
|
||||
|
||||
value = [];
|
||||
expect($wait(value)).to.deep.equal(value);
|
||||
|
||||
value = {
|
||||
foo: 'foo',
|
||||
bar: 'bar',
|
||||
baz: 'baz',
|
||||
};
|
||||
expect($wait(value)).to.deep.equal(value);
|
||||
|
||||
value = {};
|
||||
expect($wait(value)).to.deep.equal(value);
|
||||
|
||||
done();
|
||||
})();
|
||||
});
|
||||
|
||||
it('handles arrays of promises/continuables', function (done) {
|
||||
$fiberize(function () {
|
||||
var value1 = {};
|
||||
var value2 = {};
|
||||
|
||||
var promise = Promise.cast(value1);
|
||||
var continuable = function (callback) {
|
||||
callback(null, value2);
|
||||
};
|
||||
|
||||
var results = $wait([promise, continuable]);
|
||||
expect(results[0]).to.equal(value1);
|
||||
expect(results[1]).to.equal(value2);
|
||||
|
||||
done();
|
||||
})();
|
||||
});
|
||||
|
||||
it('handles maps of promises/continuable', function (done) {
|
||||
$fiberize(function () {
|
||||
var value1 = {};
|
||||
var value2 = {};
|
||||
|
||||
var promise = Promise.cast(value1);
|
||||
var continuable = function (callback) {
|
||||
callback(null, value2);
|
||||
};
|
||||
|
||||
var results = $wait({
|
||||
foo: promise,
|
||||
bar: continuable
|
||||
});
|
||||
expect(results.foo).to.equal(value1);
|
||||
expect(results.bar).to.equal(value2);
|
||||
|
||||
done();
|
||||
})();
|
||||
});
|
||||
|
||||
it('handles nested arrays/maps', function (done) {
|
||||
var promise = Promise.cast('a promise');
|
||||
var continuable = function (callback) {
|
||||
callback(null, 'a continuable');
|
||||
};
|
||||
|
||||
$fiberize(function () {
|
||||
expect($wait({
|
||||
foo: promise,
|
||||
bar: [
|
||||
continuable,
|
||||
'a scalar'
|
||||
]
|
||||
})).to.deep.equal({
|
||||
foo: 'a promise',
|
||||
bar: [
|
||||
'a continuable',
|
||||
'a scalar'
|
||||
]
|
||||
});
|
||||
|
||||
done();
|
||||
})();
|
||||
});
|
||||
|
||||
describe('#register()', function () {
|
||||
it('registers a callback-based function to be waited', function (done) {
|
||||
$fiberize(function () {
|
||||
var fn = function (value, callback) {
|
||||
callback(null, value);
|
||||
};
|
||||
|
||||
var value = {};
|
||||
expect($wait(fn(value, $wait.register()))).to.equal(value);
|
||||
|
||||
value = {};
|
||||
expect($wait(fn(value, $wait.register()))).to.equal(value);
|
||||
|
||||
done();
|
||||
})();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
//--------------------------------------------------------------------
|
||||
|
||||
describe('$waitEvent', function () {
|
||||
var $waitEvent = utils.$waitEvent;
|
||||
|
||||
it('waits for an event', function (done) {
|
||||
$fiberize(function () {
|
||||
var emitter = new (require('events').EventEmitter)();
|
||||
|
||||
var value = {};
|
||||
process.nextTick(function () {
|
||||
emitter.emit('foo', value);
|
||||
});
|
||||
|
||||
expect($waitEvent(emitter, 'foo')[0]).to.equal(value);
|
||||
|
||||
done();
|
||||
})();
|
||||
});
|
||||
|
||||
it('handles the error event', function (done) {
|
||||
$fiberize(function () {
|
||||
var emitter = new (require('events').EventEmitter)();
|
||||
|
||||
process.nextTick(function () {
|
||||
emitter.emit('error', 'an error');
|
||||
});
|
||||
|
||||
expect(function () {
|
||||
$waitEvent(emitter, 'foo');
|
||||
}).to.throw('an error');
|
||||
|
||||
done();
|
||||
})();
|
||||
});
|
||||
});
|
||||
54
src/glob-matcher.js
Normal file
54
src/glob-matcher.js
Normal file
@@ -0,0 +1,54 @@
|
||||
// See: https://gist.github.com/julien-f/5b9a3537eb82a34b04e2
|
||||
|
||||
var matcher = require('micromatch').matcher
|
||||
|
||||
module.exports = function globMatcher (patterns, opts) {
|
||||
if (!Array.isArray(patterns)) {
|
||||
if (patterns[0] === '!') {
|
||||
var m = matcher(patterns.slice(1), opts)
|
||||
return function (string) {
|
||||
return !m(string)
|
||||
}
|
||||
} else {
|
||||
return matcher(patterns, opts)
|
||||
}
|
||||
}
|
||||
|
||||
var noneMustMatch = []
|
||||
var anyMustMatch = []
|
||||
|
||||
// TODO: could probably be optimized by combining all positive patterns (and all negative patterns) as a single matcher.
|
||||
for (var i = 0, n = patterns.length; i < n; ++i) {
|
||||
var pattern = patterns[i]
|
||||
if (pattern[0] === '!') {
|
||||
noneMustMatch.push(matcher(pattern.slice(1), opts))
|
||||
} else {
|
||||
anyMustMatch.push(matcher(pattern, opts))
|
||||
}
|
||||
}
|
||||
|
||||
var nNone = noneMustMatch.length
|
||||
var nAny = anyMustMatch.length
|
||||
|
||||
return function (string) {
|
||||
var i
|
||||
|
||||
for (i = 0; i < nNone; ++i) {
|
||||
if (noneMustMatch[i](string)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if (nAny === 0) {
|
||||
return true
|
||||
}
|
||||
|
||||
for (i = 0; i < nAny; ++i) {
|
||||
if (anyMustMatch[i](string)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -1,330 +0,0 @@
|
||||
$_ = require 'underscore'
|
||||
|
||||
# FIXME: This file name should reflect what's inside!
|
||||
|
||||
#=====================================================================
|
||||
|
||||
$asArray = (val) -> if $_.isArray val then val else [val]
|
||||
$asFunction = (val) -> if $_.isFunction val then val else -> val
|
||||
|
||||
$each = $_.each
|
||||
|
||||
$first = (collection, def) ->
|
||||
if (n = collection.length)?
|
||||
return collection[0] unless n is 0
|
||||
else
|
||||
return value for own _, value of collection
|
||||
|
||||
# Nothing was found, returns the `def` value.
|
||||
def
|
||||
|
||||
$removeValue = (array, value) ->
|
||||
index = array.indexOf value
|
||||
return false if index is -1
|
||||
array.splice index, 1
|
||||
true
|
||||
|
||||
#---------------------------------------------------------------------
|
||||
|
||||
# TODO: currently the watch can be updated multiple times per
|
||||
# “$MappedCollection.set()” which is inefficient: it should be
|
||||
# possible to address that.
|
||||
|
||||
$watch = (collection, {
|
||||
# Key(s) of the “remote” objects watched.
|
||||
#
|
||||
# If it is a function, it is evaluated in the scope of the “current”
|
||||
# object. (TODO)
|
||||
#
|
||||
# Default: undefined
|
||||
keys
|
||||
|
||||
# Alias for `keys`.
|
||||
key
|
||||
|
||||
# Rule(s) of the “remote” objects watched.
|
||||
#
|
||||
# If it is a function, it is evaluated in the scope of the “current”
|
||||
# object. (TODO)
|
||||
#
|
||||
# Note: `key`/`keys` and `rule`/`rules` cannot be used both.
|
||||
#
|
||||
# Default: undefined
|
||||
rules
|
||||
|
||||
# Alias for `rules`.
|
||||
rule
|
||||
|
||||
# Value to add to the set.
|
||||
#
|
||||
# If it is a function, it is evaluated in the scope of the “remote”
|
||||
# object.
|
||||
#
|
||||
# Default: -> @val
|
||||
val
|
||||
|
||||
# Predicates the “remote” object must fulfill to be used.
|
||||
#
|
||||
# Default: -> true
|
||||
if: cond
|
||||
|
||||
# Function evaluated in the scope of the “remote” object which
|
||||
# returns the key of the object to update (usually the current one).
|
||||
#
|
||||
# TODO: Does it make sense to return an array?
|
||||
#
|
||||
# Default: undefined
|
||||
bind
|
||||
|
||||
# Initial value.
|
||||
init
|
||||
|
||||
# Function called when a loop is detected.
|
||||
#
|
||||
# Usually it is used to either throw an exception or do nothing to
|
||||
# stop the loop.
|
||||
#
|
||||
# Note: The function may also returns `true` to force the processing
|
||||
# to continue.
|
||||
#
|
||||
# Default: (number_of_loops) -> throw new Error 'loop detected'
|
||||
loopDetected
|
||||
}, fn) ->
|
||||
val = if val is undefined
|
||||
# The default value is simply the value of the item.
|
||||
-> @val
|
||||
else
|
||||
$asFunction val
|
||||
|
||||
loopDetected ?= -> throw new Error 'loop detected'
|
||||
|
||||
# Method allowing the cleanup when the helper is no longer used.
|
||||
#cleanUp = -> # TODO: noop for now.
|
||||
|
||||
# Keys of items using the current helper.
|
||||
consumers = Object.create null
|
||||
|
||||
# Current values.
|
||||
values = Object.create null
|
||||
values.common = init
|
||||
|
||||
# The number of nested processing for this watcher is counted to
|
||||
# avoid an infinite loop.
|
||||
loops = 0
|
||||
|
||||
updating = false
|
||||
|
||||
process = (event, items) ->
|
||||
return if updating
|
||||
|
||||
# Values are grouped by namespace.
|
||||
valuesByNamespace = Object.create null
|
||||
|
||||
$each items, (item, key) -> # `key` is a local variable.
|
||||
return unless not cond? or cond.call item
|
||||
|
||||
if bind?
|
||||
key = bind.call item
|
||||
|
||||
# If bind did not return a key, ignores this value.
|
||||
return unless key?
|
||||
|
||||
namespace = "$#{key}"
|
||||
else
|
||||
namespace = 'common'
|
||||
|
||||
# Computes the current value.
|
||||
value = val.call item
|
||||
|
||||
(valuesByNamespace[namespace] ?= []).push value
|
||||
|
||||
# Stops here if no values were computed.
|
||||
return if do ->
|
||||
return false for _ of valuesByNamespace
|
||||
true
|
||||
|
||||
if loops
|
||||
return unless (loopDetected loops) is true
|
||||
previousLoops = loops++
|
||||
|
||||
# For each namespace.
|
||||
for namespace, values_ of valuesByNamespace
|
||||
|
||||
# Updates the value.
|
||||
value = values[namespace]
|
||||
ctx = {
|
||||
# TODO: test the $_.clone
|
||||
value: if value is undefined then $_.clone init else value
|
||||
}
|
||||
changed = if event is 'enter'
|
||||
fn.call ctx, values_, {}
|
||||
else
|
||||
fn.call ctx, {}, values_
|
||||
|
||||
# Notifies watchers unless it is known the value has not
|
||||
# changed.
|
||||
unless changed is false
|
||||
values[namespace] = ctx.value
|
||||
updating = true
|
||||
if namespace is 'common'
|
||||
collection.touch consumers
|
||||
else
|
||||
collection.touch (namespace.substr 1)
|
||||
updating = false
|
||||
|
||||
loops = previousLoops
|
||||
|
||||
processOne = (event, item) ->
|
||||
process event, [item]
|
||||
|
||||
# Sets up the watch based on the provided criteria.
|
||||
#
|
||||
# TODO: provides a way to clean this when no longer used.
|
||||
keys = $asArray (keys ? key ? [])
|
||||
rules = $asArray (rules ? rule ? [])
|
||||
if not $_.isEmpty keys
|
||||
# Matching is done on the keys.
|
||||
|
||||
throw new Error 'cannot use keys and rules' unless $_.isEmpty rules
|
||||
|
||||
$each keys, (key) -> collection.on "key=#{key}", processOne
|
||||
|
||||
# Handles existing items.
|
||||
process 'enter', (collection.getRaw keys, true)
|
||||
else if not $_.isEmpty rules
|
||||
# Matching is done the rules.
|
||||
|
||||
$each rules, (rule) -> collection.on "rule=#{rule}", process
|
||||
|
||||
# TODO: Inefficient, is there another way?
|
||||
rules = do -> # Minor optimization.
|
||||
tmp = Object.create null
|
||||
tmp[rule] = true for rule in rules
|
||||
tmp
|
||||
$each collection.getRaw(), (item) ->
|
||||
processOne 'enter', item if item.rule of rules
|
||||
else
|
||||
# No matching done.
|
||||
|
||||
collection.on 'any', process
|
||||
|
||||
# Handles existing items.
|
||||
process 'enter', collection.getRaw()
|
||||
|
||||
# Creates the generator: the function which items will used to
|
||||
# register to this watcher and to get the current value.
|
||||
generator = do (key) -> # Declare a local variable.
|
||||
->
|
||||
{key} = this
|
||||
|
||||
# Register this item has a consumer.
|
||||
consumers[key] = true
|
||||
|
||||
# Returns the value for this item if any or the common value.
|
||||
namespace = "$#{key}"
|
||||
if namespace of values
|
||||
values[namespace]
|
||||
else
|
||||
values.common
|
||||
|
||||
# Creates a helper to unregister an item from this watcher.
|
||||
generator.unregister = do (key) -> # Declare a local variable.
|
||||
->
|
||||
{key} = this
|
||||
delete consumers[key]
|
||||
delete values["$#{key}"]
|
||||
|
||||
# Creates a helper to get the value without using an item.
|
||||
generator.raw = (key) ->
|
||||
values[if key? then "$#{key}" else 'common']
|
||||
|
||||
# Returns the generator.
|
||||
generator
|
||||
|
||||
#=====================================================================
|
||||
|
||||
$map = (options) ->
|
||||
options.init = Object.create null
|
||||
|
||||
$watch this, options, (entered, exited) ->
|
||||
changed = false
|
||||
|
||||
$each entered, ([key, value]) =>
|
||||
unless @value[key] is value
|
||||
@value[key] = value
|
||||
changed = true
|
||||
$each exited, ([key, value]) =>
|
||||
if key of @value
|
||||
delete @value[key]
|
||||
changed = true
|
||||
|
||||
changed
|
||||
|
||||
#---------------------------------------------------------------------
|
||||
|
||||
# Creates a set of value from various items.
|
||||
$set = (options) ->
|
||||
# Contrary to other helpers, the default value is the key.
|
||||
options.val ?= -> @key
|
||||
|
||||
options.init = []
|
||||
|
||||
$watch this, options, (entered, exited) ->
|
||||
changed = false
|
||||
|
||||
$each entered, (value) =>
|
||||
if (@value.indexOf value) is -1
|
||||
@value.push value
|
||||
changed = true
|
||||
|
||||
$each exited, (value) =>
|
||||
changed = true if $removeValue @value, value
|
||||
|
||||
changed
|
||||
|
||||
#---------------------------------------------------------------------
|
||||
|
||||
$sum = (options) ->
|
||||
options.init ?= 0
|
||||
|
||||
$watch this, options, (entered, exited) ->
|
||||
prev = @value
|
||||
|
||||
$each entered, (value) => @value += value
|
||||
$each exited, (value) => @value -= value
|
||||
|
||||
@value isnt prev
|
||||
|
||||
#---------------------------------------------------------------------
|
||||
|
||||
# Uses a value from another item.
|
||||
#
|
||||
# Important note: Behavior is not specified when binding to multiple
|
||||
# items.
|
||||
$val = (options) ->
|
||||
# The default value.
|
||||
def = options.default
|
||||
delete options.default
|
||||
|
||||
options.init ?= def
|
||||
|
||||
# Should the last value be kept instead of returning to the default
|
||||
# value when no items are available!
|
||||
keepLast = !!options.keepLast
|
||||
delete options.keepLast
|
||||
|
||||
$watch this, options, (entered, exited) ->
|
||||
prev = @value
|
||||
|
||||
@value = $first entered, (if keepLast then @value else def)
|
||||
|
||||
@value isnt prev
|
||||
|
||||
#=====================================================================
|
||||
|
||||
module.exports = {
|
||||
$map
|
||||
$set
|
||||
$sum
|
||||
$val
|
||||
}
|
||||
@@ -1,244 +0,0 @@
|
||||
{expect: $expect} = require 'chai'
|
||||
|
||||
$sinon = require 'sinon'
|
||||
|
||||
#---------------------------------------------------------------------
|
||||
|
||||
{$MappedCollection} = require './MappedCollection.coffee'
|
||||
|
||||
$nonBindedHelpers = require './helpers'
|
||||
|
||||
#=====================================================================
|
||||
|
||||
describe 'Helper', ->
|
||||
|
||||
# Shared variables.
|
||||
collection = $set = $sum = $val = null
|
||||
beforeEach ->
|
||||
# Creates the collection.
|
||||
collection = new $MappedCollection()
|
||||
|
||||
# Dispatcher used for tests.
|
||||
collection.dispatch = -> (@genkey.split '.')[0]
|
||||
|
||||
# Missing rules should be automatically created.
|
||||
collection.missingRule = collection.rule
|
||||
|
||||
# # Monkey patch the collection to see all emitted events.
|
||||
# emit = collection.emit
|
||||
# collection.emit = (args...) ->
|
||||
# console.log args...
|
||||
# emit.call collection, args...
|
||||
|
||||
# Binds helpers to this collection.
|
||||
{$set, $sum, $val} = do ->
|
||||
helpers = {}
|
||||
helpers[name] = fn.bind collection for name, fn of $nonBindedHelpers
|
||||
helpers
|
||||
|
||||
#-------------------------------------------------------------------
|
||||
|
||||
# All helpers share the same logical code, we need only to test one
|
||||
# extensively and test the others basically.
|
||||
#
|
||||
# $sum was chosen because it is the simplest helper to test.
|
||||
describe '$sum', ->
|
||||
|
||||
it 'with single key', ->
|
||||
collection.set foo: 1
|
||||
|
||||
collection.item sum: ->
|
||||
@val = $sum {
|
||||
key: 'foo'
|
||||
}
|
||||
|
||||
$expect(collection.get 'sum').to.equal 1
|
||||
|
||||
collection.set foo:2
|
||||
|
||||
$expect(collection.get 'sum').to.equal 2
|
||||
|
||||
collection.remove 'foo'
|
||||
|
||||
$expect(collection.get 'sum').to.equal 0
|
||||
|
||||
it 'with multiple keys', ->
|
||||
collection.set {
|
||||
foo: 1
|
||||
bar: 2
|
||||
}
|
||||
|
||||
collection.item sum: ->
|
||||
@val = $sum {
|
||||
keys: ['foo', 'bar']
|
||||
}
|
||||
|
||||
$expect(collection.get 'sum').to.equal 3
|
||||
|
||||
collection.set bar:3
|
||||
|
||||
$expect(collection.get 'sum').to.equal 4
|
||||
|
||||
collection.remove 'foo'
|
||||
|
||||
$expect(collection.get 'sum').to.equal 3
|
||||
|
||||
# FIXME: This test fails but this feature is not used.
|
||||
it.skip 'with dynamic keys', ->
|
||||
collection.set {
|
||||
foo: 1
|
||||
bar: 2
|
||||
}
|
||||
|
||||
collection.rule sum: ->
|
||||
@val = $sum {
|
||||
key: -> (@key.split '.')[1]
|
||||
}
|
||||
collection.set {
|
||||
'sum.foo': null
|
||||
'sum.bar': null
|
||||
}
|
||||
|
||||
$expect(collection.get 'sum.foo').to.equal 1
|
||||
$expect(collection.get 'sum.bar').to.equal 2
|
||||
|
||||
collection.remove 'bar'
|
||||
|
||||
$expect(collection.get 'sum.foo').to.equal 1
|
||||
$expect(collection.get 'sum.bar').to.equal 0
|
||||
|
||||
it 'with single rule', ->
|
||||
collection.set {
|
||||
'foo.1': 1
|
||||
'foo.2': 2
|
||||
}
|
||||
|
||||
collection.item sum: ->
|
||||
@val = $sum {
|
||||
rule: 'foo'
|
||||
}
|
||||
|
||||
$expect(collection.get 'sum').to.equal 3
|
||||
|
||||
collection.set 'foo.2':3
|
||||
|
||||
$expect(collection.get 'sum').to.equal 4
|
||||
|
||||
collection.remove 'foo.1'
|
||||
|
||||
$expect(collection.get 'sum').to.equal 3
|
||||
|
||||
it 'with multiple rules', ->
|
||||
collection.set {
|
||||
'foo': 1
|
||||
'bar.1': 2
|
||||
'bar.2': 3
|
||||
}
|
||||
|
||||
collection.item sum: ->
|
||||
@val = $sum {
|
||||
rules: ['foo', 'bar']
|
||||
}
|
||||
|
||||
$expect(collection.get 'sum').to.equal 6
|
||||
|
||||
collection.set 'bar.1':3
|
||||
|
||||
$expect(collection.get 'sum').to.equal 7
|
||||
|
||||
collection.remove 'bar.2'
|
||||
|
||||
$expect(collection.get 'sum').to.equal 4
|
||||
|
||||
it 'with bind', ->
|
||||
collection.set {
|
||||
'foo': {
|
||||
sum: 2 # This item will participate to `sum.2`.
|
||||
val: 1
|
||||
}
|
||||
'bar': {
|
||||
sum: 1 # This item will participate to `sum.1`.
|
||||
val: 2
|
||||
}
|
||||
}
|
||||
|
||||
collection.rule sum: ->
|
||||
@val = $sum {
|
||||
bind: ->
|
||||
id = @val.sum
|
||||
return unless id?
|
||||
"sum.#{id}"
|
||||
val: -> @val.val
|
||||
}
|
||||
collection.set {
|
||||
'sum.1': null
|
||||
'sum.2': null
|
||||
}
|
||||
|
||||
$expect(collection.get 'sum.1').equal 2
|
||||
$expect(collection.get 'sum.2').equal 1
|
||||
|
||||
collection.set {
|
||||
'foo': {
|
||||
sum: 1
|
||||
val: 3
|
||||
}
|
||||
}
|
||||
|
||||
$expect(collection.get 'sum.1').equal 5
|
||||
$expect(collection.get 'sum.2').equal 0
|
||||
|
||||
collection.remove 'bar'
|
||||
|
||||
$expect(collection.get 'sum.1').equal 3
|
||||
$expect(collection.get 'sum.2').equal 0
|
||||
|
||||
|
||||
it 'with predicate', ->
|
||||
collection.set {
|
||||
foo: 1
|
||||
bar: 2
|
||||
baz: 3
|
||||
}
|
||||
|
||||
collection.item sum: ->
|
||||
@val = $sum {
|
||||
if: -> /^b/.test @rule
|
||||
}
|
||||
|
||||
$expect(collection.get 'sum').equal 5
|
||||
|
||||
collection.set foo:4
|
||||
|
||||
$expect(collection.get 'sum').equal 5
|
||||
|
||||
collection.set bar:5
|
||||
|
||||
$expect(collection.get 'sum').equal 8
|
||||
|
||||
collection.remove 'baz'
|
||||
|
||||
$expect(collection.get 'sum').equal 5
|
||||
|
||||
it 'with initial value', ->
|
||||
collection.set foo: 1
|
||||
|
||||
collection.item sum: ->
|
||||
@val = $sum {
|
||||
key: 'foo'
|
||||
init: 2
|
||||
}
|
||||
|
||||
$expect(collection.get 'sum').to.equal 3
|
||||
|
||||
collection.set foo:2
|
||||
|
||||
$expect(collection.get 'sum').to.equal 4
|
||||
|
||||
collection.remove 'foo'
|
||||
|
||||
$expect(collection.get 'sum').to.equal 2
|
||||
|
||||
# TODO:
|
||||
# - dynamic keys
|
||||
# - dynamic rules
|
||||
13
src/http-proxy.js
Normal file
13
src/http-proxy.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import ProxyAgent from 'proxy-agent'
|
||||
|
||||
let agent
|
||||
export { agent as default }
|
||||
|
||||
export function setup (uri) {
|
||||
agent = uri != null
|
||||
? new ProxyAgent(uri)
|
||||
: undefined
|
||||
}
|
||||
|
||||
const { env } = process
|
||||
setup(env.http_proxy || env.HTTP_PROXY)
|
||||
143
src/http-request.js
Normal file
143
src/http-request.js
Normal file
@@ -0,0 +1,143 @@
|
||||
import isRedirect from 'is-redirect'
|
||||
import { assign, isString, startsWith } from 'lodash'
|
||||
import { request as httpRequest } from 'http'
|
||||
import { request as httpsRequest } from 'https'
|
||||
import { stringify as formatQueryString } from 'querystring'
|
||||
import {
|
||||
format as formatUrl,
|
||||
parse as parseUrl,
|
||||
resolve as resolveUrl
|
||||
} from 'url'
|
||||
|
||||
import { streamToBuffer } from './utils'
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
const raw = opts => {
|
||||
let req
|
||||
|
||||
const pResponse = new Promise((resolve, reject) => {
|
||||
const {
|
||||
body,
|
||||
headers: { ...headers } = {},
|
||||
protocol,
|
||||
query,
|
||||
...rest
|
||||
} = opts
|
||||
|
||||
if (headers['content-length'] == null && body != null) {
|
||||
let tmp
|
||||
if (isString(body)) {
|
||||
headers['content-length'] = Buffer.byteLength(body)
|
||||
} else if (
|
||||
(
|
||||
(tmp = body.headers) &&
|
||||
(tmp = tmp['content-length']) != null
|
||||
) ||
|
||||
(tmp = body.length) != null
|
||||
) {
|
||||
headers['content-length'] = tmp
|
||||
}
|
||||
}
|
||||
|
||||
if (query) {
|
||||
rest.path = `${rest.pathname || rest.path || '/'}?${
|
||||
isString(query)
|
||||
? query
|
||||
: formatQueryString(query)
|
||||
}`
|
||||
}
|
||||
|
||||
// Some headers can be explicitly removed by setting them to null.
|
||||
const headersToRemove = []
|
||||
for (const header in headers) {
|
||||
if (headers[header] === null) {
|
||||
delete headers[header]
|
||||
headersToRemove.push(header)
|
||||
}
|
||||
}
|
||||
|
||||
const secure = protocol && startsWith(protocol.toLowerCase(), 'https')
|
||||
let requestFn
|
||||
if (secure) {
|
||||
requestFn = httpsRequest
|
||||
} else {
|
||||
requestFn = httpRequest
|
||||
delete rest.rejectUnauthorized
|
||||
}
|
||||
|
||||
req = requestFn({
|
||||
...rest,
|
||||
headers
|
||||
})
|
||||
|
||||
for (let i = 0, length = headersToRemove.length; i < length; ++i) {
|
||||
req.removeHeader(headersToRemove[i])
|
||||
}
|
||||
|
||||
if (body) {
|
||||
if (typeof body.pipe === 'function') {
|
||||
body.pipe(req)
|
||||
} else {
|
||||
req.end(body)
|
||||
}
|
||||
} else {
|
||||
req.end()
|
||||
}
|
||||
req.on('error', reject)
|
||||
req.once('response', resolve)
|
||||
}).then(response => {
|
||||
response.cancel = () => {
|
||||
req.abort()
|
||||
}
|
||||
response.readAll = () => streamToBuffer(response)
|
||||
|
||||
const length = response.headers['content-length']
|
||||
if (length) {
|
||||
response.length = length
|
||||
}
|
||||
|
||||
const code = response.statusCode
|
||||
const { location } = response.headers
|
||||
if (isRedirect(code) && location) {
|
||||
assign(opts, parseUrl(resolveUrl(formatUrl(opts), location)))
|
||||
return raw(opts)
|
||||
}
|
||||
if (code < 200 || code >= 300) {
|
||||
const error = new Error(response.statusMessage)
|
||||
error.code = code
|
||||
Object.defineProperty(error, 'response', {
|
||||
configurable: true,
|
||||
value: response,
|
||||
writable: true
|
||||
})
|
||||
|
||||
throw error
|
||||
}
|
||||
|
||||
return response
|
||||
})
|
||||
pResponse.request = req
|
||||
|
||||
return pResponse
|
||||
}
|
||||
|
||||
const httpRequestPlus = (...args) => {
|
||||
const opts = {}
|
||||
for (let i = 0, length = args.length; i < length; ++i) {
|
||||
const arg = args[i]
|
||||
assign(opts, isString(arg) ? parseUrl(arg) : arg)
|
||||
}
|
||||
|
||||
const pResponse = raw(opts)
|
||||
|
||||
pResponse.cancel = () => {
|
||||
const { request } = pResponse
|
||||
request.emit('error', new Error('HTTP request canceled!'))
|
||||
request.abort()
|
||||
}
|
||||
pResponse.readAll = () => pResponse.then(response => response.readAll())
|
||||
|
||||
return pResponse
|
||||
}
|
||||
export { httpRequestPlus as default }
|
||||
633
src/index.js
Normal file
633
src/index.js
Normal file
@@ -0,0 +1,633 @@
|
||||
import createLogger from 'debug'
|
||||
const debug = createLogger('xo:main')
|
||||
|
||||
import appConf from 'app-conf'
|
||||
import bind from 'lodash/bind'
|
||||
import blocked from 'blocked'
|
||||
import createExpress from 'express'
|
||||
import eventToPromise from 'event-to-promise'
|
||||
import has from 'lodash/has'
|
||||
import helmet from 'helmet'
|
||||
import includes from 'lodash/includes'
|
||||
import proxyConsole from './proxy-console'
|
||||
import serveStatic from 'serve-static'
|
||||
import startsWith from 'lodash/startsWith'
|
||||
import WebSocket from 'ws'
|
||||
import { compile as compilePug } from 'pug'
|
||||
import { createServer as createProxyServer } from 'http-proxy'
|
||||
import { join as joinPath } from 'path'
|
||||
|
||||
import JsonRpcPeer from 'json-rpc-peer'
|
||||
import { InvalidCredential } from './api-errors'
|
||||
import {
|
||||
readFile,
|
||||
readdir
|
||||
} from 'fs-promise'
|
||||
|
||||
import WebServer from 'http-server-plus'
|
||||
import Xo from './xo'
|
||||
import {
|
||||
setup as setupHttpProxy
|
||||
} from './http-proxy'
|
||||
import {
|
||||
createRawObject,
|
||||
forEach,
|
||||
isArray,
|
||||
isFunction,
|
||||
mapToArray,
|
||||
pFromCallback
|
||||
} from './utils'
|
||||
|
||||
import bodyParser from 'body-parser'
|
||||
import connectFlash from 'connect-flash'
|
||||
import cookieParser from 'cookie-parser'
|
||||
import expressSession from 'express-session'
|
||||
import passport from 'passport'
|
||||
import { parse as parseCookies } from 'cookie'
|
||||
import { Strategy as LocalStrategy } from 'passport-local'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const warn = (...args) => {
|
||||
console.warn('[Warn]', ...args)
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const DEPRECATED_ENTRIES = [
|
||||
'users',
|
||||
'servers'
|
||||
]
|
||||
|
||||
async function loadConfiguration () {
|
||||
const config = await appConf.load('xo-server', {
|
||||
ignoreUnknownFormats: true
|
||||
})
|
||||
|
||||
debug('Configuration loaded.')
|
||||
|
||||
// Print a message if deprecated entries are specified.
|
||||
forEach(DEPRECATED_ENTRIES, entry => {
|
||||
if (has(config, entry)) {
|
||||
warn(`${entry} configuration is deprecated.`)
|
||||
}
|
||||
})
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
function createExpressApp () {
|
||||
const app = createExpress()
|
||||
|
||||
app.use(helmet())
|
||||
|
||||
// Registers the cookie-parser and express-session middlewares,
|
||||
// necessary for connect-flash.
|
||||
app.use(cookieParser())
|
||||
app.use(expressSession({
|
||||
resave: false,
|
||||
saveUninitialized: false,
|
||||
|
||||
// TODO: should be in the config file.
|
||||
secret: 'CLWguhRZAZIXZcbrMzHCYmefxgweItKnS'
|
||||
}))
|
||||
|
||||
// Registers the connect-flash middleware, necessary for Passport to
|
||||
// display error messages.
|
||||
app.use(connectFlash())
|
||||
|
||||
// Registers the body-parser middleware, necessary for Passport to
|
||||
// access the username and password from the sign in form.
|
||||
app.use(bodyParser.urlencoded({ extended: false }))
|
||||
|
||||
// Registers Passport's middlewares.
|
||||
app.use(passport.initialize())
|
||||
|
||||
return app
|
||||
}
|
||||
|
||||
async function setUpPassport (express, xo) {
|
||||
const strategies = createRawObject()
|
||||
xo.registerPassportStrategy = strategy => {
|
||||
passport.use(strategy)
|
||||
|
||||
const {name} = strategy
|
||||
if (name !== 'local') {
|
||||
strategies[name] = strategy.label || name
|
||||
}
|
||||
}
|
||||
|
||||
// Registers the sign in form.
|
||||
const signInPage = compilePug(
|
||||
await readFile(joinPath(__dirname, '..', 'signin.pug'))
|
||||
)
|
||||
express.get('/signin', (req, res, next) => {
|
||||
res.send(signInPage({
|
||||
error: req.flash('error')[0],
|
||||
strategies
|
||||
}))
|
||||
})
|
||||
|
||||
const SIGNIN_STRATEGY_RE = /^\/signin\/([^/]+)(\/callback)?(:?\?.*)?$/
|
||||
express.use(async (req, res, next) => {
|
||||
const { url } = req
|
||||
const matches = url.match(SIGNIN_STRATEGY_RE)
|
||||
|
||||
if (matches) {
|
||||
return passport.authenticate(matches[1], async (err, user, info) => {
|
||||
if (err) {
|
||||
return next(err)
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
req.flash('error', info ? info.message : 'Invalid credentials')
|
||||
return res.redirect('/signin')
|
||||
}
|
||||
|
||||
// The cookie will be set in via the next request because some
|
||||
// browsers do not save cookies on redirect.
|
||||
req.flash(
|
||||
'token',
|
||||
(await xo.createAuthenticationToken({userId: user.id})).id
|
||||
)
|
||||
|
||||
// The session is only persistent for internal provider and if 'Remember me' box is checked
|
||||
req.flash(
|
||||
'session-is-persistent',
|
||||
matches[1] === 'local' && req.body['remember-me'] === 'on'
|
||||
)
|
||||
|
||||
res.redirect(req.flash('return-url')[0] || '/')
|
||||
})(req, res, next)
|
||||
}
|
||||
|
||||
const token = req.flash('token')[0]
|
||||
|
||||
if (token) {
|
||||
const isPersistent = req.flash('session-is-persistent')[0]
|
||||
|
||||
if (isPersistent) {
|
||||
// Persistent cookie ? => 1 year
|
||||
res.cookie('token', token, { maxAge: 1000 * 60 * 60 * 24 * 365 })
|
||||
} else {
|
||||
// Non-persistent : external provider as Github, Twitter...
|
||||
res.cookie('token', token)
|
||||
}
|
||||
|
||||
next()
|
||||
} else if (req.cookies.token) {
|
||||
next()
|
||||
} else if (/favicon|fontawesome|images|styles|\.(?:css|jpg|png)$/.test(url)) {
|
||||
next()
|
||||
} else {
|
||||
req.flash('return-url', url)
|
||||
return res.redirect('/signin')
|
||||
}
|
||||
})
|
||||
|
||||
// Install the local strategy.
|
||||
xo.registerPassportStrategy(new LocalStrategy(
|
||||
async (username, password, done) => {
|
||||
try {
|
||||
const user = await xo.authenticateUser({username, password})
|
||||
done(null, user)
|
||||
} catch (error) {
|
||||
done(null, false, { message: error.message })
|
||||
}
|
||||
}
|
||||
))
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
async function registerPlugin (pluginPath, pluginName) {
|
||||
const plugin = require(pluginPath)
|
||||
const { version = 'unknown' } = (() => {
|
||||
try {
|
||||
return require(pluginPath + '/package.json')
|
||||
} catch (_) {
|
||||
return {}
|
||||
}
|
||||
})()
|
||||
|
||||
// Supports both “normal” CommonJS and Babel's ES2015 modules.
|
||||
const {
|
||||
default: factory = plugin,
|
||||
configurationSchema,
|
||||
configurationPresets
|
||||
} = plugin
|
||||
|
||||
// The default export can be either a factory or directly a plugin
|
||||
// instance.
|
||||
const instance = isFunction(factory)
|
||||
? factory({ xo: this })
|
||||
: factory
|
||||
|
||||
await this.registerPlugin(
|
||||
pluginName,
|
||||
instance,
|
||||
configurationSchema,
|
||||
configurationPresets,
|
||||
version
|
||||
)
|
||||
}
|
||||
|
||||
const debugPlugin = createLogger('xo:plugin')
|
||||
|
||||
function registerPluginWrapper (pluginPath, pluginName) {
|
||||
debugPlugin('register %s', pluginName)
|
||||
|
||||
return registerPlugin.call(this, pluginPath, pluginName).then(
|
||||
() => {
|
||||
debugPlugin(`successfully register ${pluginName}`)
|
||||
},
|
||||
error => {
|
||||
debugPlugin(`failed register ${pluginName}`)
|
||||
debugPlugin(error)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const PLUGIN_PREFIX = 'xo-server-'
|
||||
const PLUGIN_PREFIX_LENGTH = PLUGIN_PREFIX.length
|
||||
|
||||
async function registerPluginsInPath (path) {
|
||||
const files = await readdir(path).catch(error => {
|
||||
if (error.code === 'ENOENT') {
|
||||
return []
|
||||
}
|
||||
throw error
|
||||
})
|
||||
|
||||
await Promise.all(mapToArray(files, name => {
|
||||
if (startsWith(name, PLUGIN_PREFIX)) {
|
||||
return registerPluginWrapper.call(
|
||||
this,
|
||||
`${path}/${name}`,
|
||||
name.slice(PLUGIN_PREFIX_LENGTH)
|
||||
)
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
async function registerPlugins (xo) {
|
||||
await Promise.all(mapToArray([
|
||||
`${__dirname}/../node_modules/`,
|
||||
'/usr/local/lib/node_modules/'
|
||||
], xo::registerPluginsInPath))
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
async function makeWebServerListen ({
|
||||
certificate,
|
||||
|
||||
// The properties was called `certificate` before.
|
||||
cert = certificate,
|
||||
|
||||
key,
|
||||
...opts
|
||||
}) {
|
||||
if (cert && key) {
|
||||
[opts.cert, opts.key] = await Promise.all([
|
||||
readFile(cert),
|
||||
readFile(key)
|
||||
])
|
||||
}
|
||||
|
||||
try {
|
||||
const niceAddress = await this.listen(opts)
|
||||
debug(`Web server listening on ${niceAddress}`)
|
||||
} catch (error) {
|
||||
if (error.niceAddress) {
|
||||
warn(`Web server could not listen on ${error.niceAddress}`)
|
||||
|
||||
const {code} = error
|
||||
if (code === 'EACCES') {
|
||||
warn(' Access denied.')
|
||||
warn(' Ports < 1024 are often reserved to privileges users.')
|
||||
} else if (code === 'EADDRINUSE') {
|
||||
warn(' Address already in use.')
|
||||
}
|
||||
} else {
|
||||
warn('Web server could not listen:', error.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function createWebServer (opts) {
|
||||
const webServer = new WebServer()
|
||||
|
||||
await Promise.all(mapToArray(opts, webServer::makeWebServerListen))
|
||||
|
||||
return webServer
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const setUpProxies = (express, opts, xo) => {
|
||||
if (!opts) {
|
||||
return
|
||||
}
|
||||
|
||||
const proxy = createProxyServer({
|
||||
ignorePath: true
|
||||
}).on('error', (error) => console.error(error))
|
||||
|
||||
// TODO: sort proxies by descending prefix length.
|
||||
|
||||
// HTTP request proxy.
|
||||
express.use((req, res, next) => {
|
||||
const { url } = req
|
||||
|
||||
for (const prefix in opts) {
|
||||
if (startsWith(url, prefix)) {
|
||||
const target = opts[prefix]
|
||||
|
||||
proxy.web(req, res, {
|
||||
target: target + url.slice(prefix.length)
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
next()
|
||||
})
|
||||
|
||||
// WebSocket proxy.
|
||||
const webSocketServer = new WebSocket.Server({
|
||||
noServer: true
|
||||
})
|
||||
xo.on('stop', () => pFromCallback(cb => webSocketServer.close(cb)))
|
||||
|
||||
express.on('upgrade', (req, socket, head) => {
|
||||
const { url } = req
|
||||
|
||||
for (const prefix in opts) {
|
||||
if (startsWith(url, prefix)) {
|
||||
const target = opts[prefix]
|
||||
|
||||
proxy.ws(req, socket, head, {
|
||||
target: target + url.slice(prefix.length)
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const setUpStaticFiles = (express, opts) => {
|
||||
forEach(opts, (paths, url) => {
|
||||
if (!isArray(paths)) {
|
||||
paths = [paths]
|
||||
}
|
||||
|
||||
forEach(paths, path => {
|
||||
debug('Setting up %s → %s', url, path)
|
||||
|
||||
express.use(url, serveStatic(path))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const setUpApi = (webServer, xo, verboseLogsOnErrors) => {
|
||||
const webSocketServer = new WebSocket.Server({
|
||||
server: webServer,
|
||||
path: '/api/'
|
||||
})
|
||||
xo.on('stop', () => pFromCallback(cb => webSocketServer.close(cb)))
|
||||
|
||||
webSocketServer.on('connection', socket => {
|
||||
const { remoteAddress } = socket.upgradeReq.socket
|
||||
|
||||
debug('+ WebSocket connection (%s)', remoteAddress)
|
||||
|
||||
// Create the abstract XO object for this connection.
|
||||
const connection = xo.createUserConnection()
|
||||
connection.once('close', () => {
|
||||
socket.close()
|
||||
})
|
||||
|
||||
// Create the JSON-RPC server for this connection.
|
||||
const jsonRpc = new JsonRpcPeer(message => {
|
||||
if (message.type === 'request') {
|
||||
return xo.callApiMethod(connection, message.method, message.params)
|
||||
}
|
||||
})
|
||||
connection.notify = bind(jsonRpc.notify, jsonRpc)
|
||||
|
||||
// Close the XO connection with this WebSocket.
|
||||
socket.once('close', () => {
|
||||
debug('- WebSocket connection (%s)', remoteAddress)
|
||||
|
||||
connection.close()
|
||||
})
|
||||
|
||||
// Connect the WebSocket to the JSON-RPC server.
|
||||
socket.on('message', message => {
|
||||
jsonRpc.write(message)
|
||||
})
|
||||
|
||||
const onSend = error => {
|
||||
if (error) {
|
||||
warn('WebSocket send:', error.stack)
|
||||
}
|
||||
}
|
||||
jsonRpc.on('data', data => {
|
||||
// The socket may have been closed during the API method
|
||||
// execution.
|
||||
if (socket.readyState === WebSocket.OPEN) {
|
||||
socket.send(data, onSend)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const CONSOLE_PROXY_PATH_RE = /^\/api\/consoles\/(.*)$/
|
||||
|
||||
const setUpConsoleProxy = (webServer, xo) => {
|
||||
const webSocketServer = new WebSocket.Server({
|
||||
noServer: true
|
||||
})
|
||||
xo.on('stop', () => pFromCallback(cb => webSocketServer.close(cb)))
|
||||
|
||||
webServer.on('upgrade', async (req, socket, head) => {
|
||||
const matches = CONSOLE_PROXY_PATH_RE.exec(req.url)
|
||||
if (!matches) {
|
||||
return
|
||||
}
|
||||
|
||||
const [, id] = matches
|
||||
try {
|
||||
// TODO: factorize permissions checking in an Express middleware.
|
||||
{
|
||||
const { token } = parseCookies(req.headers.cookie)
|
||||
|
||||
const user = await xo.authenticateUser({ token })
|
||||
if (!await xo.hasPermissions(user.id, [ [ id, 'operate' ] ])) {
|
||||
throw new InvalidCredential()
|
||||
}
|
||||
|
||||
const { remoteAddress } = socket
|
||||
debug('+ Console proxy (%s - %s)', user.name, remoteAddress)
|
||||
socket.on('close', () => {
|
||||
debug('- Console proxy (%s - %s)', user.name, remoteAddress)
|
||||
})
|
||||
}
|
||||
|
||||
const xapi = xo.getXapi(id, ['VM', 'VM-controller'])
|
||||
const vmConsole = xapi.getVmConsole(id)
|
||||
|
||||
// FIXME: lost connection due to VM restart is not detected.
|
||||
webSocketServer.handleUpgrade(req, socket, head, connection => {
|
||||
proxyConsole(connection, vmConsole, xapi.sessionId)
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(error && error.stack || error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const USAGE = (({
|
||||
name,
|
||||
version
|
||||
}) => `Usage: ${name} [--safe-mode]
|
||||
|
||||
${name} v${version}`)(require('../package.json'))
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export default async function main (args) {
|
||||
if (includes(args, '--help') || includes(args, '-h')) {
|
||||
return USAGE
|
||||
}
|
||||
|
||||
{
|
||||
const debug = createLogger('xo:perf')
|
||||
blocked(ms => {
|
||||
debug('blocked for %sms', ms | 0)
|
||||
})
|
||||
}
|
||||
|
||||
const config = await loadConfiguration()
|
||||
|
||||
const webServer = await createWebServer(config.http.listen)
|
||||
|
||||
// Now the web server is listening, drop privileges.
|
||||
try {
|
||||
const {user, group} = config
|
||||
if (group) {
|
||||
process.setgid(group)
|
||||
debug('Group changed to', group)
|
||||
}
|
||||
if (user) {
|
||||
process.setuid(user)
|
||||
debug('User changed to', user)
|
||||
}
|
||||
} catch (error) {
|
||||
warn('Failed to change user/group:', error)
|
||||
}
|
||||
|
||||
if (config.httpProxy) {
|
||||
setupHttpProxy(config.httpProxy)
|
||||
}
|
||||
|
||||
// Creates main object.
|
||||
const xo = new Xo(config)
|
||||
|
||||
// Register web server close on XO stop.
|
||||
xo.on('stop', () => pFromCallback(cb => webServer.close(cb)))
|
||||
|
||||
// Connects to all registered servers.
|
||||
await xo.start()
|
||||
|
||||
// Express is used to manage non WebSocket connections.
|
||||
const express = createExpressApp()
|
||||
|
||||
if (config.http.redirectToHttps) {
|
||||
let port
|
||||
forEach(config.http.listen, listen => {
|
||||
if (
|
||||
listen.port &&
|
||||
(listen.cert || listen.certificate)
|
||||
) {
|
||||
port = listen.port
|
||||
return false
|
||||
}
|
||||
})
|
||||
|
||||
if (port === undefined) {
|
||||
warn('Could not setup HTTPs redirection: no HTTPs port found')
|
||||
} else {
|
||||
express.use((req, res, next) => {
|
||||
if (req.secure) {
|
||||
return next()
|
||||
}
|
||||
|
||||
res.redirect(`https://${req.hostname}:${port}${req.originalUrl}`)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Must be set up before the API.
|
||||
setUpConsoleProxy(webServer, xo)
|
||||
|
||||
// Must be set up before the API.
|
||||
express.use(bind(xo._handleHttpRequest, xo))
|
||||
|
||||
// Everything above is not protected by the sign in, allowing xo-cli
|
||||
// to work properly.
|
||||
await setUpPassport(express, xo)
|
||||
|
||||
// Attaches express to the web server.
|
||||
webServer.on('request', express)
|
||||
webServer.on('upgrade', (req, socket, head) => {
|
||||
express.emit('upgrade', req, socket, head)
|
||||
})
|
||||
|
||||
// Must be set up before the static files.
|
||||
setUpApi(webServer, xo, config.verboseApiLogsOnErrors)
|
||||
|
||||
setUpProxies(express, config.http.proxies, xo)
|
||||
|
||||
setUpStaticFiles(express, config.http.mounts)
|
||||
|
||||
if (!includes(args, '--safe-mode')) {
|
||||
await registerPlugins(xo)
|
||||
}
|
||||
|
||||
// Gracefully shutdown on signals.
|
||||
//
|
||||
// TODO: implements a timeout? (or maybe it is the services launcher
|
||||
// responsibility?)
|
||||
forEach([ 'SIGINT', 'SIGTERM' ], signal => {
|
||||
let alreadyCalled = false
|
||||
|
||||
process.on(signal, () => {
|
||||
if (alreadyCalled) {
|
||||
warn('forced exit')
|
||||
process.exit(1)
|
||||
}
|
||||
alreadyCalled = true
|
||||
|
||||
debug('%s caught, closing…', signal)
|
||||
xo.stop()
|
||||
})
|
||||
})
|
||||
|
||||
await eventToPromise(xo, 'stopped')
|
||||
|
||||
debug('bye :-)')
|
||||
}
|
||||
195
src/job-executor.js
Normal file
195
src/job-executor.js
Normal file
@@ -0,0 +1,195 @@
|
||||
import assign from 'lodash/assign'
|
||||
import Bluebird from 'bluebird'
|
||||
import every from 'lodash/every'
|
||||
import filter from 'lodash/filter'
|
||||
import isArray from 'lodash/isArray'
|
||||
import isPlainObject from 'lodash/isPlainObject'
|
||||
import map from 'lodash/map'
|
||||
import mapValues from 'lodash/mapValues'
|
||||
import size from 'lodash/size'
|
||||
import some from 'lodash/some'
|
||||
import { BaseError } from 'make-error'
|
||||
|
||||
import { crossProduct } from './math'
|
||||
import {
|
||||
serializeError,
|
||||
thunkToArray
|
||||
} from './utils'
|
||||
|
||||
export class JobExecutorError extends BaseError {}
|
||||
export class UnsupportedJobType extends JobExecutorError {
|
||||
constructor (job) {
|
||||
super('Unknown job type: ' + job.type)
|
||||
}
|
||||
}
|
||||
export class UnsupportedVectorType extends JobExecutorError {
|
||||
constructor (vector) {
|
||||
super('Unknown vector type: ' + vector.type)
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const match = (pattern, value) => {
|
||||
if (isPlainObject(pattern)) {
|
||||
if (pattern.__or && size(pattern) === 1) {
|
||||
return some(pattern.__or, subpattern => match(subpattern, value))
|
||||
}
|
||||
|
||||
return isPlainObject(value) && every(pattern, (subpattern, key) => (
|
||||
value[key] !== undefined && match(subpattern, value[key])
|
||||
))
|
||||
}
|
||||
|
||||
if (isArray(pattern)) {
|
||||
return isArray(value) && every(pattern, subpattern =>
|
||||
some(value, subvalue => match(subpattern, subvalue))
|
||||
)
|
||||
}
|
||||
|
||||
return pattern === value
|
||||
}
|
||||
|
||||
const paramsVectorActionsMap = {
|
||||
extractProperties ({ mapping, value }) {
|
||||
return mapValues(mapping, key => value[key])
|
||||
},
|
||||
crossProduct ({ items }) {
|
||||
return thunkToArray(crossProduct(
|
||||
map(items, value => resolveParamsVector.call(this, value))
|
||||
))
|
||||
},
|
||||
fetchObjects ({ pattern }) {
|
||||
return filter(this.xo.getObjects(), object => match(pattern, object))
|
||||
},
|
||||
map ({ collection, iteratee, paramName = 'value' }) {
|
||||
return map(resolveParamsVector.call(this, collection), value => {
|
||||
return resolveParamsVector.call(this, {
|
||||
...iteratee,
|
||||
[paramName]: value
|
||||
})
|
||||
})
|
||||
},
|
||||
set: ({ values }) => values
|
||||
}
|
||||
|
||||
export function resolveParamsVector (paramsVector) {
|
||||
const visitor = paramsVectorActionsMap[paramsVector.type]
|
||||
if (!visitor) {
|
||||
throw new Error(`Unsupported function '${paramsVector.type}'.`)
|
||||
}
|
||||
|
||||
return visitor.call(this, paramsVector)
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export default class JobExecutor {
|
||||
constructor (xo) {
|
||||
this.xo = xo
|
||||
this._extractValueCb = {
|
||||
'set': items => items.values
|
||||
}
|
||||
|
||||
// The logger is not available until Xo has started.
|
||||
xo.on('start', () => xo.getLogger('jobs').then(logger => {
|
||||
this._logger = logger
|
||||
}))
|
||||
}
|
||||
|
||||
async exec (job) {
|
||||
const runJobId = this._logger.notice(`Starting execution of ${job.id}.`, {
|
||||
event: 'job.start',
|
||||
userId: job.userId,
|
||||
jobId: job.id,
|
||||
key: job.key
|
||||
})
|
||||
|
||||
try {
|
||||
if (job.type === 'call') {
|
||||
const execStatus = await this._execCall(job, runJobId)
|
||||
|
||||
this.xo.emit('job:terminated', execStatus)
|
||||
} else {
|
||||
throw new UnsupportedJobType(job)
|
||||
}
|
||||
|
||||
this._logger.notice(`Execution terminated for ${job.id}.`, {
|
||||
event: 'job.end',
|
||||
runJobId
|
||||
})
|
||||
} catch (error) {
|
||||
this._logger.error(`The execution of ${job.id} has failed.`, {
|
||||
event: 'job.end',
|
||||
runJobId,
|
||||
error: serializeError(error)
|
||||
})
|
||||
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async _execCall (job, runJobId) {
|
||||
const { paramsVector } = job
|
||||
const paramsFlatVector = paramsVector
|
||||
? resolveParamsVector.call(this, paramsVector)
|
||||
: [{}] // One call with no parameters
|
||||
|
||||
const connection = this.xo.createUserConnection()
|
||||
|
||||
connection.set('user_id', job.userId)
|
||||
|
||||
const execStatus = {
|
||||
runJobId,
|
||||
start: Date.now(),
|
||||
calls: {}
|
||||
}
|
||||
|
||||
await Bluebird.map(paramsFlatVector, params => {
|
||||
const runCallId = this._logger.notice(`Starting ${job.method} call. (${job.id})`, {
|
||||
event: 'jobCall.start',
|
||||
runJobId,
|
||||
method: job.method,
|
||||
params
|
||||
})
|
||||
|
||||
const call = execStatus.calls[runCallId] = {
|
||||
method: job.method,
|
||||
params,
|
||||
start: Date.now()
|
||||
}
|
||||
|
||||
return this.xo.callApiMethod(connection, job.method, assign({}, params)).then(
|
||||
value => {
|
||||
this._logger.notice(`Call ${job.method} (${runCallId}) is a success. (${job.id})`, {
|
||||
event: 'jobCall.end',
|
||||
runJobId,
|
||||
runCallId,
|
||||
returnedValue: value
|
||||
})
|
||||
|
||||
call.returnedValue = value
|
||||
call.end = Date.now()
|
||||
},
|
||||
reason => {
|
||||
this._logger.notice(`Call ${job.method} (${runCallId}) has failed. (${job.id})`, {
|
||||
event: 'jobCall.end',
|
||||
runJobId,
|
||||
runCallId,
|
||||
error: serializeError(reason)
|
||||
})
|
||||
|
||||
call.error = reason
|
||||
call.end = Date.now()
|
||||
}
|
||||
)
|
||||
}, {
|
||||
concurrency: 2
|
||||
})
|
||||
|
||||
connection.close()
|
||||
execStatus.end = Date.now()
|
||||
|
||||
return execStatus
|
||||
}
|
||||
}
|
||||
100
src/job-executor.spec.js
Normal file
100
src/job-executor.spec.js
Normal file
@@ -0,0 +1,100 @@
|
||||
/* eslint-env mocha */
|
||||
|
||||
import leche from 'leche'
|
||||
import { expect } from 'chai'
|
||||
|
||||
import { resolveParamsVector } from './job-executor'
|
||||
|
||||
describe('resolveParamsVector', function () {
|
||||
leche.withData({
|
||||
'cross product with three sets': [
|
||||
// Expected result.
|
||||
[ { id: 3, value: 'foo', remote: 'local' },
|
||||
{ id: 7, value: 'foo', remote: 'local' },
|
||||
{ id: 10, value: 'foo', remote: 'local' },
|
||||
{ id: 3, value: 'bar', remote: 'local' },
|
||||
{ id: 7, value: 'bar', remote: 'local' },
|
||||
{ id: 10, value: 'bar', remote: 'local' } ],
|
||||
// Entry.
|
||||
{
|
||||
type: 'crossProduct',
|
||||
items: [{
|
||||
type: 'set',
|
||||
values: [ { id: 3 }, { id: 7 }, { id: 10 } ]
|
||||
}, {
|
||||
type: 'set',
|
||||
values: [ { value: 'foo' }, { value: 'bar' } ]
|
||||
}, {
|
||||
type: 'set',
|
||||
values: [ { remote: 'local' } ]
|
||||
}]
|
||||
}
|
||||
],
|
||||
'cross product with `set` and `map`': [
|
||||
// Expected result.
|
||||
[
|
||||
{ remote: 'local', id: 'vm:2' },
|
||||
{ remote: 'smb', id: 'vm:2' }
|
||||
],
|
||||
|
||||
// Entry.
|
||||
{
|
||||
type: 'crossProduct',
|
||||
items: [{
|
||||
type: 'set',
|
||||
values: [ { remote: 'local' }, { remote: 'smb' } ]
|
||||
}, {
|
||||
type: 'map',
|
||||
collection: {
|
||||
type: 'fetchObjects',
|
||||
pattern: {
|
||||
$pool: { __or: [ 'pool:1', 'pool:8', 'pool:12' ] },
|
||||
power_state: 'Running',
|
||||
tags: [ 'foo' ],
|
||||
type: 'VM'
|
||||
}
|
||||
},
|
||||
iteratee: {
|
||||
type: 'extractProperties',
|
||||
mapping: { id: 'id' }
|
||||
}
|
||||
}]
|
||||
},
|
||||
|
||||
// Context.
|
||||
{
|
||||
xo: {
|
||||
getObjects: function () {
|
||||
return [{
|
||||
id: 'vm:1',
|
||||
$pool: 'pool:1',
|
||||
tags: [],
|
||||
type: 'VM',
|
||||
power_state: 'Halted'
|
||||
}, {
|
||||
id: 'vm:2',
|
||||
$pool: 'pool:1',
|
||||
tags: [ 'foo' ],
|
||||
type: 'VM',
|
||||
power_state: 'Running'
|
||||
}, {
|
||||
id: 'host:1',
|
||||
type: 'host',
|
||||
power_state: 'Running'
|
||||
}, {
|
||||
id: 'vm:3',
|
||||
$pool: 'pool:8',
|
||||
tags: [ 'foo' ],
|
||||
type: 'VM',
|
||||
power_state: 'Halted'
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}, function (expectedResult, entry, context) {
|
||||
it('Resolves params vector', function () {
|
||||
expect(resolveParamsVector.call(context, entry)).to.deep.have.members(expectedResult)
|
||||
})
|
||||
})
|
||||
})
|
||||
22
src/loggers/abstract.js
Normal file
22
src/loggers/abstract.js
Normal file
@@ -0,0 +1,22 @@
|
||||
export default class AbstractLogger {}
|
||||
|
||||
// See: https://en.wikipedia.org/wiki/Syslog#Severity_level
|
||||
const LEVELS = [
|
||||
'emergency',
|
||||
'alert',
|
||||
'critical',
|
||||
'error',
|
||||
'warning',
|
||||
'notice',
|
||||
'informational',
|
||||
'debug'
|
||||
]
|
||||
|
||||
// Create high level log methods.
|
||||
for (const level of LEVELS) {
|
||||
Object.defineProperty(AbstractLogger.prototype, level, {
|
||||
value (message, data) {
|
||||
return this._add(level, message, data)
|
||||
}
|
||||
})
|
||||
}
|
||||
59
src/loggers/leveldb.js
Normal file
59
src/loggers/leveldb.js
Normal file
@@ -0,0 +1,59 @@
|
||||
import highland from 'highland'
|
||||
|
||||
import AbstractLogger from './abstract'
|
||||
import { forEach, noop } from '../utils'
|
||||
|
||||
let lastDate = 0
|
||||
let increment = 0
|
||||
|
||||
function generateUniqueKey (date) {
|
||||
if (date === lastDate) {
|
||||
return `${date}:${increment++}`
|
||||
}
|
||||
|
||||
increment = 0
|
||||
return String(lastDate = date)
|
||||
}
|
||||
|
||||
export default class LevelDbLogger extends AbstractLogger {
|
||||
constructor (db, namespace) {
|
||||
super()
|
||||
|
||||
this._db = db
|
||||
this._namespace = namespace
|
||||
}
|
||||
|
||||
_add (level, message, data) {
|
||||
const time = Date.now()
|
||||
|
||||
const log = {
|
||||
level,
|
||||
message,
|
||||
data,
|
||||
namespace: this._namespace,
|
||||
time
|
||||
}
|
||||
|
||||
const key = generateUniqueKey(time)
|
||||
this._db.putSync(key, log)
|
||||
return key
|
||||
}
|
||||
|
||||
createReadStream () {
|
||||
return highland(this._db.createReadStream())
|
||||
.filter(({value}) => value.namespace === this._namespace)
|
||||
}
|
||||
|
||||
del (id) {
|
||||
if (!Array.isArray(id)) {
|
||||
id = [id]
|
||||
}
|
||||
forEach(id, id => {
|
||||
this._db.get(id).then(value => {
|
||||
if (value.namespace === this._namespace) {
|
||||
this._db.delSync(id, noop)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
202
src/logs-cli.js
Normal file
202
src/logs-cli.js
Normal file
@@ -0,0 +1,202 @@
|
||||
import appConf from 'app-conf'
|
||||
import get from 'lodash/get'
|
||||
import highland from 'highland'
|
||||
import levelup from 'level-party'
|
||||
import ndjson from 'ndjson'
|
||||
import parseArgs from 'minimist'
|
||||
import sublevel from 'level-sublevel'
|
||||
import util from 'util'
|
||||
import { repair as repairDb } from 'leveldown'
|
||||
|
||||
import {forEach} from './utils'
|
||||
import globMatcher from './glob-matcher'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
async function printLogs (db, args) {
|
||||
let stream = highland(db.createReadStream({reverse: true}))
|
||||
|
||||
if (args.since) {
|
||||
stream = stream.filter(({value}) => (value.time >= args.since))
|
||||
}
|
||||
|
||||
if (args.until) {
|
||||
stream = stream.filter(({value}) => (value.time <= args.until))
|
||||
}
|
||||
|
||||
const fields = Object.keys(args.matchers)
|
||||
|
||||
if (fields.length > 0) {
|
||||
stream = stream.filter(({value}) => {
|
||||
for (const field of fields) {
|
||||
const fieldValue = get(value, field)
|
||||
if (fieldValue === undefined || !args.matchers[field](fieldValue)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
stream = stream.take(args.limit)
|
||||
|
||||
if (args.json) {
|
||||
stream = highland(stream.pipe(ndjson.serialize()))
|
||||
.each(value => {
|
||||
process.stdout.write(value)
|
||||
})
|
||||
} else {
|
||||
stream = stream.each(value => {
|
||||
console.log(util.inspect(value, { depth: null }))
|
||||
})
|
||||
}
|
||||
|
||||
return new Promise(resolve => {
|
||||
stream.done(resolve)
|
||||
})
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
function helper () {
|
||||
console.error(`
|
||||
xo-server-logs --help, -h
|
||||
|
||||
Display this help message.
|
||||
|
||||
xo-server-logs [--json] [--limit=<limit>] [--since=<date>] [--until=<date>] [<pattern>...]
|
||||
|
||||
Prints the logs.
|
||||
|
||||
--json
|
||||
Display the results as new line delimited JSON for consumption
|
||||
by another program.
|
||||
|
||||
--limit=<limit>, -n <limit>
|
||||
Limit the number of results to be displayed (default 100)
|
||||
|
||||
--since=<date>, --until=<date>
|
||||
Start showing entries on or newer than the specified date, or on
|
||||
or older than the specified date.
|
||||
|
||||
<date> should use the format \`YYYY-MM-DD\`.
|
||||
|
||||
<pattern>
|
||||
Patterns can be used to filter the entries.
|
||||
|
||||
Patterns have the following format \`<field>=<value>\`/\`<field>\`.
|
||||
|
||||
xo-server-logs --repair
|
||||
|
||||
Repair/compact the database.
|
||||
|
||||
This is an advanced operation and should be used only when necessary and offline (xo-server should be stopped).
|
||||
`)
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
function getArgs () {
|
||||
const stringArgs = ['since', 'until', 'limit']
|
||||
const args = parseArgs(process.argv.slice(2), {
|
||||
string: stringArgs,
|
||||
boolean: ['help', 'json', 'repair'],
|
||||
default: {
|
||||
limit: 100,
|
||||
json: false,
|
||||
help: false
|
||||
},
|
||||
alias: {
|
||||
limit: 'n',
|
||||
help: 'h'
|
||||
}
|
||||
})
|
||||
|
||||
const patterns = {}
|
||||
|
||||
for (let value of args._) {
|
||||
value = String(value)
|
||||
|
||||
const i = value.indexOf('=')
|
||||
|
||||
if (i !== -1) {
|
||||
const field = value.slice(0, i)
|
||||
const pattern = value.slice(i + 1)
|
||||
|
||||
patterns[pattern]
|
||||
? patterns[field].push(pattern)
|
||||
: patterns[field] = [ pattern ]
|
||||
} else if (!patterns[value]) {
|
||||
patterns[value] = null
|
||||
}
|
||||
}
|
||||
|
||||
const trueFunction = () => true
|
||||
args.matchers = {}
|
||||
|
||||
for (const field in patterns) {
|
||||
const values = patterns[field]
|
||||
args.matchers[field] = (values === null) ? trueFunction : globMatcher(values)
|
||||
}
|
||||
|
||||
// Warning: minimist makes one array of values if the same option is used many times.
|
||||
// (But only for strings args, not boolean)
|
||||
forEach(stringArgs, arg => {
|
||||
if (args[arg] instanceof Array) {
|
||||
throw new Error(`error: too many values for ${arg} argument`)
|
||||
}
|
||||
})
|
||||
|
||||
;['since', 'until'].forEach(arg => {
|
||||
if (args[arg] !== undefined) {
|
||||
args[arg] = Date.parse(args[arg])
|
||||
|
||||
if (isNaN(args[arg])) {
|
||||
throw new Error(`error: bad ${arg} timestamp format`)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (isNaN(args.limit = +args.limit)) {
|
||||
throw new Error('error: limit is not a valid number')
|
||||
}
|
||||
|
||||
return args
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export default async function main () {
|
||||
const args = getArgs()
|
||||
|
||||
if (args.help) {
|
||||
helper()
|
||||
return
|
||||
}
|
||||
|
||||
const config = await appConf.load('xo-server', {
|
||||
ignoreUnknownFormats: true
|
||||
})
|
||||
|
||||
if (args.repair) {
|
||||
await new Promise((resolve, reject) => {
|
||||
repairDb(`${config.datadir}/leveldb`, error => {
|
||||
if (error) {
|
||||
reject(error)
|
||||
} else {
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const db = sublevel(levelup(
|
||||
`${config.datadir}/leveldb`,
|
||||
{ valueEncoding: 'json' }
|
||||
)).sublevel('logs')
|
||||
|
||||
return printLogs(db, args)
|
||||
}
|
||||
230
src/main.coffee
230
src/main.coffee
@@ -1,230 +0,0 @@
|
||||
# File system handling.
|
||||
$fs = require 'fs'
|
||||
|
||||
#---------------------------------------------------------------------
|
||||
|
||||
# Low level tools.
|
||||
$_ = require 'underscore'
|
||||
|
||||
# HTTP(s) middleware framework.
|
||||
$connect = require 'connect'
|
||||
$serveStatic = require 'serve-static'
|
||||
|
||||
$eventToPromise = require 'event-to-promise'
|
||||
|
||||
# Configuration handling.
|
||||
$nconf = require 'nconf'
|
||||
|
||||
$Promise = require 'bluebird'
|
||||
$Promise.longStackTraces()
|
||||
|
||||
# WebSocket server.
|
||||
{Server: $WSServer} = require 'ws'
|
||||
|
||||
# YAML formatting and parsing.
|
||||
$YAML = require 'js-yaml'
|
||||
|
||||
#---------------------------------------------------------------------
|
||||
|
||||
$API = require './api'
|
||||
$Connection = require './connection'
|
||||
$XO = require './xo'
|
||||
|
||||
# Helpers for dealing with fibers.
|
||||
{$fiberize, $promisify, $waitEvent, $wait} = require './fibers-utils'
|
||||
|
||||
# HTTP/HTTPS server which can listen on multiple ports.
|
||||
$WebServer = require 'http-server-plus'
|
||||
|
||||
#=====================================================================
|
||||
|
||||
$readFile = $Promise.promisify $fs.readFile
|
||||
|
||||
$handleJsonRpcCall = (api, session, encodedRequest) ->
|
||||
request = {
|
||||
id: null
|
||||
}
|
||||
|
||||
formatError = (error) -> JSON.stringify {
|
||||
jsonrpc: '2.0'
|
||||
error: error
|
||||
id: request.id
|
||||
}
|
||||
|
||||
# Parses the JSON.
|
||||
try
|
||||
request = JSON.parse encodedRequest.toString()
|
||||
catch error
|
||||
return formatError (
|
||||
if error instanceof SyntaxError
|
||||
$API.err.INVALID_JSON
|
||||
else
|
||||
$API.err.SERVER_ERROR
|
||||
)
|
||||
|
||||
# Checks it is a compliant JSON-RPC 2.0 request.
|
||||
if (
|
||||
not request.method? or
|
||||
not request.params? or
|
||||
not request.id? or
|
||||
request.jsonrpc isnt '2.0'
|
||||
)
|
||||
return formatError $API.err.INVALID_REQUEST
|
||||
|
||||
# Executes the requested method on the API.
|
||||
try
|
||||
JSON.stringify {
|
||||
jsonrpc: '2.0'
|
||||
result: $wait api.exec session, request
|
||||
id: request.id
|
||||
}
|
||||
catch error
|
||||
# If it is not a valid API error, hides it with a generic server error.
|
||||
unless (error not instanceof Error) and error.code? and error.message?
|
||||
console.error error.stack ? error
|
||||
error = $API.err.SERVER_ERROR
|
||||
|
||||
formatError error
|
||||
|
||||
#=====================================================================
|
||||
|
||||
# Main.
|
||||
module.exports = $promisify (args) ->
|
||||
|
||||
# Relative paths in the configuration are relative to this
|
||||
# directory's parent.
|
||||
process.chdir "#{__dirname}/.."
|
||||
|
||||
# Loads the environment.
|
||||
$nconf.env()
|
||||
|
||||
# Parses process' arguments.
|
||||
$nconf.argv()
|
||||
|
||||
# Loads the configuration files.
|
||||
format =
|
||||
stringify: $YAML.safeDump
|
||||
parse: $YAML.safeLoad
|
||||
$nconf.use 'file', {
|
||||
file: "#{__dirname}/../config/local.yaml"
|
||||
format
|
||||
}
|
||||
|
||||
# Defines defaults configuration.
|
||||
$nconf.defaults {
|
||||
http: {
|
||||
listen: [
|
||||
port: 80
|
||||
]
|
||||
mounts: []
|
||||
}
|
||||
redis: {
|
||||
# Default values are handled by `redis`.
|
||||
}
|
||||
}
|
||||
|
||||
# Prints a message if deprecated entries are specified.
|
||||
for entry in ['users', 'servers']
|
||||
if $nconf.get entry
|
||||
console.warn "[Warn] `#{entry}` configuration is deprecated."
|
||||
|
||||
# Creates the web server according to the configuration.
|
||||
webServer = new $WebServer()
|
||||
$wait $Promise.map ($nconf.get 'http:listen'), (options) ->
|
||||
# Reads certificate and key if necessary.
|
||||
if options.certificate? and options.key?
|
||||
options.certificate = $wait $readFile options.certificate
|
||||
options.key = $wait $readFile options.key
|
||||
|
||||
# Starts listening
|
||||
webServer.listen options
|
||||
.then ->
|
||||
console.log "WebServer listening on #{@niceAddress()}"
|
||||
.catch (error) ->
|
||||
console.warn "[WARN] WebServer could not listen on #{@niceAddress()}"
|
||||
switch error.code
|
||||
when 'EACCES'
|
||||
console.warn ' Access denied.'
|
||||
console.warn ' Ports < 1024 are often reserved to privileges users.'
|
||||
when 'EADDRINUSE'
|
||||
console.warn ' Address already in use.'
|
||||
|
||||
# Now the web server is listening, drop privileges.
|
||||
try
|
||||
if (group = $nconf.get 'group')?
|
||||
process.setgid group
|
||||
if (user = $nconf.get 'user')?
|
||||
process.setuid user
|
||||
catch error
|
||||
console.warn "[WARN] Failed to change the user or group: #{error.message}"
|
||||
|
||||
# Handles error as gracefully as possible.
|
||||
webServer.on 'error', (error) ->
|
||||
console.error '[ERR] Web server', error
|
||||
webServer.close()
|
||||
|
||||
# Creates the main object which will connects to Xen servers and
|
||||
# manages all the models.
|
||||
xo = new $XO()
|
||||
|
||||
# Starts it.
|
||||
xo.start {
|
||||
redis: {
|
||||
uri: $nconf.get 'redis:uri'
|
||||
}
|
||||
}
|
||||
|
||||
# Static file serving (e.g. for XO-Web).
|
||||
connect = $connect()
|
||||
for urlPath, filePaths of $nconf.get 'http:mounts'
|
||||
filePaths = [filePaths] unless $_.isArray filePaths
|
||||
for filePath in filePaths
|
||||
connect.use urlPath, $serveStatic filePath
|
||||
webServer.on 'request', connect
|
||||
|
||||
# Creates the API.
|
||||
api = new $API xo
|
||||
|
||||
conId = 0
|
||||
unregisterConnection = ->
|
||||
delete xo.connections[@id]
|
||||
|
||||
# JSON-RPC over WebSocket.
|
||||
wsServer = new $WSServer {
|
||||
server: webServer
|
||||
path: '/api/'
|
||||
}
|
||||
wsServer.on 'connection', (socket) ->
|
||||
connection = new $Connection {
|
||||
close: socket.close.bind socket
|
||||
send: socket.send.bind socket
|
||||
}
|
||||
connection.id = conId++
|
||||
xo.connections[connection.id] = connection
|
||||
connection.on 'close', unregisterConnection
|
||||
|
||||
socket.on 'close', connection.close.bind connection
|
||||
|
||||
# Handles each request in a separate fiber.
|
||||
socket.on 'message', $fiberize (request) ->
|
||||
response = $handleJsonRpcCall api, connection, request
|
||||
|
||||
# The socket may have closed between the request and the
|
||||
# response.
|
||||
socket.send response if socket.readyState is socket.OPEN
|
||||
|
||||
socket.on 'error', $fiberize (error) ->
|
||||
console.error '[WARN] WebSocket connection', error
|
||||
socket.close()
|
||||
wsServer.on 'error', $fiberize (error) ->
|
||||
console.error '[WARN] WebSocket server', error
|
||||
wsServer.close()
|
||||
|
||||
# Creates a default user if there is none.
|
||||
unless $wait xo.users.exists()
|
||||
email = 'admin@admin.net'
|
||||
password = 'admin' # TODO: Should be generated.
|
||||
xo.users.create email, password, 'admin'
|
||||
console.log "[INFO] Default user: “#{email}” with password “#{password}”"
|
||||
|
||||
return $eventToPromise webServer, 'close'
|
||||
48
src/math.js
Normal file
48
src/math.js
Normal file
@@ -0,0 +1,48 @@
|
||||
import assign from 'lodash/assign'
|
||||
|
||||
const _combine = (vectors, n, cb) => {
|
||||
if (!n) {
|
||||
return
|
||||
}
|
||||
|
||||
const nLast = n - 1
|
||||
|
||||
const vector = vectors[nLast]
|
||||
const m = vector.length
|
||||
if (n === 1) {
|
||||
for (let i = 0; i < m; ++i) {
|
||||
cb([ vector[i] ])
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
for (let i = 0; i < m; ++i) {
|
||||
const value = vector[i]
|
||||
|
||||
_combine(vectors, nLast, (vector) => {
|
||||
vector.push(value)
|
||||
cb(vector)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Compute all combinations from vectors.
|
||||
//
|
||||
// Ex: combine([[2, 3], [5, 7]])
|
||||
// => [ [ 2, 5 ], [ 3, 5 ], [ 2, 7 ], [ 3, 7 ] ]
|
||||
export const combine = vectors => cb => _combine(vectors, vectors.length, cb)
|
||||
|
||||
// Merge the properties of an objects set in one object.
|
||||
//
|
||||
// Ex: mergeObjects([ { a: 1 }, { b: 2 } ]) => { a: 1, b: 2 }
|
||||
export const mergeObjects = objects => assign({}, ...objects)
|
||||
|
||||
// Compute a cross product between vectors.
|
||||
//
|
||||
// Ex: crossProduct([ [ { a: 2 }, { b: 3 } ], [ { c: 5 }, { d: 7 } ] ] )
|
||||
// => [ { a: 2, c: 5 }, { b: 3, c: 5 }, { a: 2, d: 7 }, { b: 3, d: 7 } ]
|
||||
export const crossProduct = (vectors, mergeFn = mergeObjects) => cb => (
|
||||
combine(vectors)(vector => {
|
||||
cb(mergeFn(vector))
|
||||
})
|
||||
)
|
||||
72
src/math.spec.js
Normal file
72
src/math.spec.js
Normal file
@@ -0,0 +1,72 @@
|
||||
/* eslint-env mocha */
|
||||
|
||||
import { expect } from 'chai'
|
||||
import leche from 'leche'
|
||||
|
||||
import { thunkToArray } from './utils'
|
||||
import {
|
||||
crossProduct,
|
||||
mergeObjects
|
||||
} from './math'
|
||||
|
||||
describe('mergeObjects', function () {
|
||||
leche.withData({
|
||||
'Two sets of one': [
|
||||
{a: 1, b: 2}, {a: 1}, {b: 2}
|
||||
],
|
||||
'Two sets of two': [
|
||||
{a: 1, b: 2, c: 3, d: 4}, {a: 1, b: 2}, {c: 3, d: 4}
|
||||
],
|
||||
'Three sets': [
|
||||
{a: 1, b: 2, c: 3, d: 4, e: 5, f: 6}, {a: 1}, {b: 2, c: 3}, {d: 4, e: 5, f: 6}
|
||||
],
|
||||
'One set': [
|
||||
{a: 1, b: 2}, {a: 1, b: 2}
|
||||
],
|
||||
'Empty set': [
|
||||
{a: 1}, {a: 1}, {}
|
||||
],
|
||||
'All empty': [
|
||||
{}, {}, {}
|
||||
],
|
||||
'No set': [
|
||||
{}
|
||||
]
|
||||
}, function (resultSet, ...sets) {
|
||||
it('Assembles all given param sets in on set', function () {
|
||||
expect(mergeObjects(sets)).to.eql(resultSet)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('crossProduct', function () {
|
||||
// Gives the sum of all args
|
||||
const addTest = args => args.reduce((prev, curr) => prev + curr, 0)
|
||||
// Gives the product of all args
|
||||
const multiplyTest = args => args.reduce((prev, curr) => prev * curr, 1)
|
||||
|
||||
leche.withData({
|
||||
'2 sets of 2 items to multiply': [
|
||||
[10, 14, 15, 21], [[2, 3], [5, 7]], multiplyTest
|
||||
],
|
||||
'3 sets of 2 items to multiply': [
|
||||
[110, 130, 154, 182, 165, 195, 231, 273], [[2, 3], [5, 7], [11, 13]], multiplyTest
|
||||
],
|
||||
'2 sets of 3 items to multiply': [
|
||||
[14, 22, 26, 21, 33, 39, 35, 55, 65], [[2, 3, 5], [7, 11, 13]], multiplyTest
|
||||
],
|
||||
'2 sets of 2 items to add': [
|
||||
[7, 9, 8, 10], [[2, 3], [5, 7]], addTest
|
||||
],
|
||||
'3 sets of 2 items to add': [
|
||||
[18, 20, 20, 22, 19, 21, 21, 23], [[2, 3], [5, 7], [11, 13]], addTest
|
||||
],
|
||||
'2 sets of 3 items to add': [
|
||||
[9, 13, 15, 10, 14, 16, 12, 16, 18], [[2, 3, 5], [7, 11, 13]], addTest
|
||||
]
|
||||
}, function (product, items, cb) {
|
||||
it('Crosses sets of values with a crossProduct callback', function () {
|
||||
expect(thunkToArray(crossProduct(items, cb))).to.have.members(product)
|
||||
})
|
||||
})
|
||||
})
|
||||
174
src/model.js
174
src/model.js
@@ -1,113 +1,73 @@
|
||||
'use strict';
|
||||
import {EventEmitter} from 'events'
|
||||
|
||||
var _ = require('underscore');
|
||||
import {
|
||||
forEach,
|
||||
isEmpty,
|
||||
isString
|
||||
} from './utils'
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// ===================================================================
|
||||
|
||||
function Model(properties)
|
||||
{
|
||||
// Parent constructor.
|
||||
Model.super_.call(this);
|
||||
export default class Model extends EventEmitter {
|
||||
constructor (properties) {
|
||||
super()
|
||||
|
||||
this.properties = _.extend({}, this['default']);
|
||||
this.properties = { ...this.default }
|
||||
|
||||
if (properties)
|
||||
{
|
||||
this.set(properties);
|
||||
}
|
||||
if (properties) {
|
||||
this.set(properties)
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize the model after construction.
|
||||
initialize () {}
|
||||
|
||||
// Validate the defined properties.
|
||||
//
|
||||
// Returns the error if any.
|
||||
validate (properties) {}
|
||||
|
||||
// Get a property.
|
||||
get (name, def) {
|
||||
const value = this.properties[name]
|
||||
return value !== undefined ? value : def
|
||||
}
|
||||
|
||||
// Check whether a property exists.
|
||||
has (name) {
|
||||
return (this.properties[name] !== undefined)
|
||||
}
|
||||
|
||||
// Set properties.
|
||||
set (properties, value) {
|
||||
// This method can also be used with two arguments to set a single
|
||||
// property.
|
||||
if (isString(properties)) {
|
||||
properties = { [properties]: value }
|
||||
}
|
||||
|
||||
const previous = {}
|
||||
|
||||
forEach(properties, (value, name) => {
|
||||
const prev = this.properties[name]
|
||||
|
||||
if (value !== prev) {
|
||||
previous[name] = prev
|
||||
|
||||
if (value === undefined) {
|
||||
delete this.properties[name]
|
||||
} else {
|
||||
this.properties[name] = value
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (!isEmpty(previous)) {
|
||||
this.emit('change', previous)
|
||||
|
||||
forEach(previous, (value, name) => {
|
||||
this.emit('change:' + name, value)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
require('util').inherits(Model, require('events').EventEmitter);
|
||||
|
||||
/**
|
||||
* Initializes the model after construction.
|
||||
*/
|
||||
Model.prototype.initialize = function () {};
|
||||
|
||||
/**
|
||||
* Validates the defined properties.
|
||||
*
|
||||
* @returns {undefined|mixed} Returns something else than undefined if
|
||||
* there was an error.
|
||||
*/
|
||||
Model.prototype.validate = function (/*properties*/) {};
|
||||
|
||||
/**
|
||||
* Gets property.
|
||||
*/
|
||||
Model.prototype.get = function (property, def) {
|
||||
var prop = this.properties[property];
|
||||
if (undefined !== prop)
|
||||
{
|
||||
return prop;
|
||||
}
|
||||
|
||||
return def;
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if a property exists.
|
||||
*/
|
||||
Model.prototype.has = function (property) {
|
||||
return (undefined !== this.properties[property]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets properties.
|
||||
*/
|
||||
Model.prototype.set = function (properties, value) {
|
||||
if (undefined !== value)
|
||||
{
|
||||
var property = properties;
|
||||
properties = {};
|
||||
properties[property] = value;
|
||||
}
|
||||
|
||||
var previous = {};
|
||||
|
||||
var model = this;
|
||||
_.each(properties, function (value, key) {
|
||||
if (undefined === value)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var prev = model.get(key);
|
||||
|
||||
// New value.
|
||||
if (value !== prev)
|
||||
{
|
||||
previous[key] = prev;
|
||||
model.properties[key] = value;
|
||||
}
|
||||
});
|
||||
|
||||
if (!_.isEmpty(previous))
|
||||
{
|
||||
this.emit('change', previous);
|
||||
|
||||
_.each(previous, function (previous, property) {
|
||||
this.emit('change:'+ property, previous);
|
||||
}, this);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Unsets properties.
|
||||
*/
|
||||
Model.prototype.unset = function (properties) {
|
||||
// TODO: Events.
|
||||
this.properties = _.omit(this.properties, properties);
|
||||
};
|
||||
|
||||
/**
|
||||
* Default properties.
|
||||
*
|
||||
* @type {Object}
|
||||
*/
|
||||
Model.prototype['default'] = {};
|
||||
|
||||
Model.extend = require('extendable');
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
|
||||
module.exports = Model;
|
||||
|
||||
79
src/models/acl.js
Normal file
79
src/models/acl.js
Normal file
@@ -0,0 +1,79 @@
|
||||
import Collection from '../collection/redis'
|
||||
import Model from '../model'
|
||||
import {
|
||||
forEach,
|
||||
mapToArray,
|
||||
multiKeyHash
|
||||
} from '../utils'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
// Up until now, there were no actions, therefore the default
|
||||
// action is used to update existing entries.
|
||||
const DEFAULT_ACTION = 'admin'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export default class Acl extends Model {}
|
||||
|
||||
Acl.create = (subject, object, action) => {
|
||||
return Acl.hash(subject, object, action).then(hash => new Acl({
|
||||
id: hash,
|
||||
subject,
|
||||
object,
|
||||
action
|
||||
}))
|
||||
}
|
||||
|
||||
Acl.hash = (subject, object, action) => multiKeyHash(subject, object, action)
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export class Acls extends Collection {
|
||||
get Model () {
|
||||
return Acl
|
||||
}
|
||||
|
||||
create (subject, object, action) {
|
||||
return Acl.create(subject, object, action).then(acl => this.add(acl))
|
||||
}
|
||||
|
||||
delete (subject, object, action) {
|
||||
return Acl.hash(subject, object, action).then(hash => this.remove(hash))
|
||||
}
|
||||
|
||||
aclExists (subject, object, action) {
|
||||
return Acl.hash(subject, object, action).then(hash => this.exists(hash))
|
||||
}
|
||||
|
||||
async get (properties) {
|
||||
const acls = await super.get(properties)
|
||||
|
||||
// Finds all records that are missing a action and need to be updated.
|
||||
const toUpdate = []
|
||||
forEach(acls, acl => {
|
||||
if (!acl.action) {
|
||||
acl.action = DEFAULT_ACTION
|
||||
toUpdate.push(acl)
|
||||
}
|
||||
})
|
||||
if (toUpdate.length) {
|
||||
// Removes all existing entries.
|
||||
await this.remove(mapToArray(toUpdate, 'id'))
|
||||
|
||||
// Compute the new ids (new hashes).
|
||||
const {hash} = Acl
|
||||
await Promise.all(mapToArray(
|
||||
toUpdate,
|
||||
(acl) => hash(acl.subject, acl.object, acl.action).then(id => {
|
||||
acl.id = id
|
||||
})
|
||||
))
|
||||
|
||||
// Inserts the new (updated) entries.
|
||||
await this.add(toUpdate)
|
||||
}
|
||||
|
||||
return acls
|
||||
}
|
||||
}
|
||||
47
src/models/group.js
Normal file
47
src/models/group.js
Normal file
@@ -0,0 +1,47 @@
|
||||
import Collection from '../collection/redis'
|
||||
import Model from '../model'
|
||||
|
||||
import { forEach } from '../utils'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export default class Group extends Model {}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export class Groups extends Collection {
|
||||
get Model () {
|
||||
return Group
|
||||
}
|
||||
|
||||
create (name) {
|
||||
return this.add(new Group({
|
||||
name,
|
||||
users: '[]'
|
||||
}))
|
||||
}
|
||||
|
||||
async save (group) {
|
||||
// Serializes.
|
||||
group.users = JSON.stringify(group.users)
|
||||
|
||||
return /* await */ this.update(group)
|
||||
}
|
||||
|
||||
async get (properties) {
|
||||
const groups = await super.get(properties)
|
||||
|
||||
// Deserializes.
|
||||
forEach(groups, group => {
|
||||
const {users} = group
|
||||
try {
|
||||
group.users = JSON.parse(users)
|
||||
} catch (error) {
|
||||
console.warn('cannot parse group.users:', users)
|
||||
group.users = []
|
||||
}
|
||||
})
|
||||
|
||||
return groups
|
||||
}
|
||||
}
|
||||
42
src/models/job.js
Normal file
42
src/models/job.js
Normal file
@@ -0,0 +1,42 @@
|
||||
import Collection from '../collection/redis'
|
||||
import Model from '../model'
|
||||
import { forEach } from '../utils'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export default class Job extends Model {}
|
||||
|
||||
export class Jobs extends Collection {
|
||||
get Model () {
|
||||
return Job
|
||||
}
|
||||
|
||||
async create (job) {
|
||||
// Serializes.
|
||||
job.paramsVector = JSON.stringify(job.paramsVector)
|
||||
return /* await */ this.add(new Job(job))
|
||||
}
|
||||
|
||||
async save (job) {
|
||||
// Serializes.
|
||||
job.paramsVector = JSON.stringify(job.paramsVector)
|
||||
return /* await */ this.update(job)
|
||||
}
|
||||
|
||||
async get (properties) {
|
||||
const jobs = await super.get(properties)
|
||||
|
||||
// Deserializes.
|
||||
forEach(jobs, job => {
|
||||
const {paramsVector} = job
|
||||
try {
|
||||
job.paramsVector = JSON.parse(paramsVector)
|
||||
} catch (error) {
|
||||
console.warn('cannot parse job.paramsVector:', paramsVector) // FIXME this is a warning as I copy/paste acl.js, but...
|
||||
job.paramsVector = {}
|
||||
}
|
||||
})
|
||||
|
||||
return jobs
|
||||
}
|
||||
}
|
||||
53
src/models/plugin-metadata.js
Normal file
53
src/models/plugin-metadata.js
Normal file
@@ -0,0 +1,53 @@
|
||||
import Collection from '../collection/redis'
|
||||
import Model from '../model'
|
||||
import { forEach } from '../utils'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export default class PluginMetadata extends Model {}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export class PluginsMetadata extends Collection {
|
||||
get Model () {
|
||||
return PluginMetadata
|
||||
}
|
||||
|
||||
async save ({ id, autoload, configuration }) {
|
||||
return /* await */ this.update({
|
||||
id,
|
||||
autoload: autoload ? 'true' : 'false',
|
||||
configuration: configuration && JSON.stringify(configuration)
|
||||
})
|
||||
}
|
||||
|
||||
async merge (id, data) {
|
||||
const pluginMetadata = await this.first(id)
|
||||
if (!pluginMetadata) {
|
||||
throw new Error('no such plugin metadata')
|
||||
}
|
||||
|
||||
return /* await */ this.save({
|
||||
...pluginMetadata.properties,
|
||||
...data
|
||||
})
|
||||
}
|
||||
|
||||
async get (properties) {
|
||||
const pluginsMetadata = await super.get(properties)
|
||||
|
||||
// Deserializes.
|
||||
forEach(pluginsMetadata, pluginMetadata => {
|
||||
const { autoload, configuration } = pluginMetadata
|
||||
pluginMetadata.autoload = autoload === 'true'
|
||||
try {
|
||||
pluginMetadata.configuration = configuration && JSON.parse(configuration)
|
||||
} catch (error) {
|
||||
console.warn('cannot parse pluginMetadata.configuration:', configuration)
|
||||
pluginMetadata.configuration = []
|
||||
}
|
||||
})
|
||||
|
||||
return pluginsMetadata
|
||||
}
|
||||
}
|
||||
36
src/models/remote.js
Normal file
36
src/models/remote.js
Normal file
@@ -0,0 +1,36 @@
|
||||
import Collection from '../collection/redis'
|
||||
import Model from '../model'
|
||||
import {
|
||||
forEach
|
||||
} from '../utils'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export default class Remote extends Model {}
|
||||
|
||||
export class Remotes extends Collection {
|
||||
get Model () {
|
||||
return Remote
|
||||
}
|
||||
|
||||
create (name, url) {
|
||||
return this.add(new Remote({
|
||||
name,
|
||||
url,
|
||||
enabled: false,
|
||||
error: ''
|
||||
}))
|
||||
}
|
||||
|
||||
async save (remote) {
|
||||
return /* await */ this.update(remote)
|
||||
}
|
||||
|
||||
async get (properties) {
|
||||
const remotes = await super.get(properties)
|
||||
forEach(remotes, remote => {
|
||||
remote.enabled = (remote.enabled === 'true')
|
||||
})
|
||||
return remotes
|
||||
}
|
||||
}
|
||||
36
src/models/schedule.js
Normal file
36
src/models/schedule.js
Normal file
@@ -0,0 +1,36 @@
|
||||
import Collection from '../collection/redis'
|
||||
import Model from '../model'
|
||||
import { forEach } from '../utils'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export default class Schedule extends Model {}
|
||||
|
||||
export class Schedules extends Collection {
|
||||
get Model () {
|
||||
return Schedule
|
||||
}
|
||||
|
||||
create (userId, job, cron, enabled, name = undefined, timezone = undefined) {
|
||||
return this.add(new Schedule({
|
||||
userId,
|
||||
job,
|
||||
cron,
|
||||
enabled,
|
||||
name,
|
||||
timezone
|
||||
}))
|
||||
}
|
||||
|
||||
async save (schedule) {
|
||||
return /* await */ this.update(schedule)
|
||||
}
|
||||
|
||||
async get (properties) {
|
||||
const schedules = await super.get(properties)
|
||||
forEach(schedules, schedule => {
|
||||
schedule.enabled = (schedule.enabled === 'true')
|
||||
})
|
||||
return schedules
|
||||
}
|
||||
}
|
||||
22
src/models/server.js
Normal file
22
src/models/server.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import Collection from '../collection/redis'
|
||||
import Model from '../model'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export default class Server extends Model {}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export class Servers extends Collection {
|
||||
get Model () {
|
||||
return Server
|
||||
}
|
||||
|
||||
async create ({host, username, password, readOnly}) {
|
||||
if (await this.exists({host})) {
|
||||
throw new Error('server already exists')
|
||||
}
|
||||
|
||||
return /* await */ this.add({host, username, password, readOnly})
|
||||
}
|
||||
}
|
||||
10
src/models/token.js
Normal file
10
src/models/token.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import Collection from '../collection/redis'
|
||||
import Model from '../model'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export default class Token extends Model {}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export class Tokens extends Collection {}
|
||||
76
src/models/user.js
Normal file
76
src/models/user.js
Normal file
@@ -0,0 +1,76 @@
|
||||
import isEmpty from 'lodash/isEmpty'
|
||||
|
||||
import Collection from '../collection/redis'
|
||||
import Model from '../model'
|
||||
import { forEach } from '../utils'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export default class User extends Model {}
|
||||
|
||||
User.prototype.default = {
|
||||
permission: 'none'
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
const parseProp = (obj, name) => {
|
||||
const value = obj[name]
|
||||
if (value == null) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
return JSON.parse(value)
|
||||
} catch (error) {
|
||||
console.warn('cannot parse user[%s] (%s):', name, value, error)
|
||||
}
|
||||
}
|
||||
|
||||
export class Users extends Collection {
|
||||
get Model () {
|
||||
return User
|
||||
}
|
||||
|
||||
async create (properties) {
|
||||
const { email } = properties
|
||||
|
||||
// Avoid duplicates.
|
||||
if (await this.exists({email})) {
|
||||
throw new Error(`the user ${email} already exists`)
|
||||
}
|
||||
|
||||
// Create the user object.
|
||||
const user = new User(properties)
|
||||
|
||||
// Adds the user to the collection.
|
||||
return /* await */ this.add(user)
|
||||
}
|
||||
|
||||
async save (user) {
|
||||
// Serializes.
|
||||
let tmp
|
||||
if (!isEmpty(tmp = user.groups)) {
|
||||
user.groups = JSON.stringify(tmp)
|
||||
}
|
||||
if (!isEmpty(tmp = user.preferences)) {
|
||||
user.preferences = JSON.stringify(tmp)
|
||||
}
|
||||
|
||||
return /* await */ this.update(user)
|
||||
}
|
||||
|
||||
async get (properties) {
|
||||
const users = await super.get(properties)
|
||||
|
||||
// Deserializes
|
||||
forEach(users, user => {
|
||||
let tmp
|
||||
user.groups = ((tmp = parseProp(user, 'groups')) && tmp.length)
|
||||
? tmp
|
||||
: undefined
|
||||
user.preferences = parseProp(user, 'preferences')
|
||||
})
|
||||
|
||||
return users
|
||||
}
|
||||
}
|
||||
76
src/proxy-console.js
Normal file
76
src/proxy-console.js
Normal file
@@ -0,0 +1,76 @@
|
||||
import createDebug from 'debug'
|
||||
import partialStream from 'partial-stream'
|
||||
import {connect} from 'tls'
|
||||
import {parse} from 'url'
|
||||
|
||||
const debug = createDebug('xo:proxy-console')
|
||||
|
||||
export default function proxyConsole (ws, vmConsole, sessionId) {
|
||||
const url = parse(vmConsole.location)
|
||||
|
||||
let closed = false
|
||||
|
||||
const socket = connect({
|
||||
host: url.host,
|
||||
port: url.port || 443,
|
||||
rejectUnauthorized: false
|
||||
}, () => {
|
||||
// Write headers.
|
||||
socket.write([
|
||||
`CONNECT ${url.path} HTTP/1.0`,
|
||||
`Host: ${url.hostname}`,
|
||||
`Cookie: session_id=${sessionId}`,
|
||||
'', ''
|
||||
].join('\r\n'))
|
||||
|
||||
const onSend = (error) => {
|
||||
if (error) {
|
||||
debug('error sending to the XO client: %s', error.stack || error.message || error)
|
||||
}
|
||||
}
|
||||
|
||||
socket.pipe(partialStream('\r\n\r\n', headers => {
|
||||
// TODO: check status code 200.
|
||||
debug('connected')
|
||||
})).on('data', data => {
|
||||
if (!closed) {
|
||||
// Encode to base 64.
|
||||
ws.send(data.toString('base64'), onSend)
|
||||
}
|
||||
}).on('end', () => {
|
||||
if (!closed) {
|
||||
closed = true
|
||||
debug('disconnected from the console')
|
||||
}
|
||||
|
||||
ws.close()
|
||||
})
|
||||
|
||||
ws
|
||||
.on('error', error => {
|
||||
closed = true
|
||||
debug('error from the XO client: %s', error.stack || error.message || error)
|
||||
|
||||
socket.close()
|
||||
})
|
||||
.on('message', data => {
|
||||
if (!closed) {
|
||||
// Decode from base 64.
|
||||
socket.write(new Buffer(data, 'base64'))
|
||||
}
|
||||
})
|
||||
.on('close', () => {
|
||||
if (!closed) {
|
||||
closed = true
|
||||
debug('disconnected from the XO client')
|
||||
}
|
||||
|
||||
socket.end()
|
||||
})
|
||||
}).on('error', error => {
|
||||
closed = true
|
||||
debug('error from the console: %s', error.stack || error.message || error)
|
||||
|
||||
ws.close()
|
||||
})
|
||||
}
|
||||
217
src/remote-handlers/abstract.js
Normal file
217
src/remote-handlers/abstract.js
Normal file
@@ -0,0 +1,217 @@
|
||||
import eventToPromise from 'event-to-promise'
|
||||
import through2 from 'through2'
|
||||
|
||||
import {
|
||||
parse
|
||||
} from 'xo-remote-parser'
|
||||
|
||||
import {
|
||||
addChecksumToReadStream,
|
||||
getPseudoRandomBytes,
|
||||
noop,
|
||||
pCatch,
|
||||
streamToBuffer,
|
||||
validChecksumOfReadStream
|
||||
} from '../utils'
|
||||
|
||||
export default class RemoteHandlerAbstract {
|
||||
constructor (remote) {
|
||||
this._remote = {...remote, ...parse(remote.url)}
|
||||
if (this._remote.type !== this.type) {
|
||||
throw new Error('Incorrect remote type')
|
||||
}
|
||||
}
|
||||
|
||||
get type () {
|
||||
throw new Error('Not implemented')
|
||||
}
|
||||
|
||||
/**
|
||||
* Asks the handler to sync the state of the effective remote with its' metadata
|
||||
*/
|
||||
async sync () {
|
||||
return this._sync()
|
||||
}
|
||||
|
||||
async _sync () {
|
||||
throw new Error('Not implemented')
|
||||
}
|
||||
|
||||
/**
|
||||
* Free the resources possibly dedicated to put the remote at work, when it is no more needed
|
||||
*/
|
||||
async forget () {
|
||||
return this._forget()
|
||||
}
|
||||
|
||||
async _forget () {
|
||||
throw new Error('Not implemented')
|
||||
}
|
||||
|
||||
async test () {
|
||||
const testFileName = `${Date.now()}.test`
|
||||
const data = getPseudoRandomBytes(1024 * 1024)
|
||||
let step = 'write'
|
||||
try {
|
||||
await this.outputFile(testFileName, data)
|
||||
step = 'read'
|
||||
const read = await this.readFile(testFileName)
|
||||
if (data.compare(read) !== 0) {
|
||||
throw new Error('output and input did not match')
|
||||
}
|
||||
return {
|
||||
success: true
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
step,
|
||||
file: testFileName,
|
||||
error: error.message || String(error)
|
||||
}
|
||||
} finally {
|
||||
this.unlink(testFileName).catch(noop)
|
||||
}
|
||||
}
|
||||
|
||||
async outputFile (file, data, options) {
|
||||
return this._outputFile(file, data, {
|
||||
flags: 'wx',
|
||||
...options
|
||||
})
|
||||
}
|
||||
|
||||
async _outputFile (file, data, options) {
|
||||
const stream = await this.createOutputStream(file, options)
|
||||
const promise = eventToPromise(stream, 'finish')
|
||||
stream.end(data)
|
||||
return promise
|
||||
}
|
||||
|
||||
async readFile (file, options) {
|
||||
return this._readFile(file, options)
|
||||
}
|
||||
|
||||
_readFile (file, options) {
|
||||
return this.createReadStream(file, options).then(streamToBuffer)
|
||||
}
|
||||
|
||||
async rename (oldPath, newPath) {
|
||||
return this._rename(oldPath, newPath)
|
||||
}
|
||||
|
||||
async _rename (oldPath, newPath) {
|
||||
throw new Error('Not implemented')
|
||||
}
|
||||
|
||||
async list (dir = '.') {
|
||||
return this._list(dir)
|
||||
}
|
||||
|
||||
async _list (dir) {
|
||||
throw new Error('Not implemented')
|
||||
}
|
||||
|
||||
async createReadStream (file, {
|
||||
checksum = false,
|
||||
ignoreMissingChecksum = false,
|
||||
...options
|
||||
} = {}) {
|
||||
const streamP = this._createReadStream(file, options).then(async stream => {
|
||||
await eventToPromise(stream, 'readable')
|
||||
|
||||
if (stream.length === undefined) {
|
||||
stream.length = await this.getSize(file)::pCatch(noop)
|
||||
}
|
||||
|
||||
return stream
|
||||
})
|
||||
|
||||
if (!checksum) {
|
||||
return streamP
|
||||
}
|
||||
|
||||
try {
|
||||
checksum = await this.readFile(`${file}.checksum`)
|
||||
} catch (error) {
|
||||
if (error.code === 'ENOENT' && ignoreMissingChecksum) {
|
||||
return streamP
|
||||
}
|
||||
|
||||
throw error
|
||||
}
|
||||
|
||||
let stream = await streamP
|
||||
|
||||
const { length } = stream
|
||||
stream = validChecksumOfReadStream(stream, checksum.toString())
|
||||
stream.length = length
|
||||
|
||||
return stream
|
||||
}
|
||||
|
||||
async _createReadStream (file, options) {
|
||||
throw new Error('Not implemented')
|
||||
}
|
||||
|
||||
async refreshChecksum (path) {
|
||||
const stream = addChecksumToReadStream(await this.createReadStream(path))
|
||||
stream.resume() // start reading the whole file
|
||||
const checksum = await stream.checksum
|
||||
await this.outputFile(`${path}.checksum`, checksum)
|
||||
}
|
||||
|
||||
async createOutputStream (file, {
|
||||
checksum = false,
|
||||
...options
|
||||
} = {}) {
|
||||
const streamP = this._createOutputStream(file, {
|
||||
flags: 'wx',
|
||||
...options
|
||||
})
|
||||
|
||||
if (!checksum) {
|
||||
return streamP
|
||||
}
|
||||
|
||||
const connectorStream = through2()
|
||||
const forwardError = error => {
|
||||
connectorStream.emit('error', error)
|
||||
}
|
||||
|
||||
const streamWithChecksum = addChecksumToReadStream(connectorStream)
|
||||
streamWithChecksum.pipe(await streamP)
|
||||
|
||||
streamWithChecksum.checksum
|
||||
.then(value => this.outputFile(`${file}.checksum`, value))
|
||||
.catch(forwardError)
|
||||
|
||||
return connectorStream
|
||||
}
|
||||
|
||||
async _createOutputStream (file, options) {
|
||||
throw new Error('Not implemented')
|
||||
}
|
||||
|
||||
async unlink (file, {
|
||||
checksum = false
|
||||
} = {}) {
|
||||
if (checksum) {
|
||||
this._unlink(`${file}.checksum`)::pCatch(noop)
|
||||
}
|
||||
|
||||
return this._unlink(file)
|
||||
}
|
||||
|
||||
async _unlink (file) {
|
||||
throw new Error('Not implemented')
|
||||
}
|
||||
|
||||
async getSize (file) {
|
||||
return this._getSize(file)
|
||||
}
|
||||
|
||||
async _getSize (file) {
|
||||
throw new Error('Not implemented')
|
||||
}
|
||||
}
|
||||
90
src/remote-handlers/local.js
Normal file
90
src/remote-handlers/local.js
Normal file
@@ -0,0 +1,90 @@
|
||||
import fs from 'fs-promise'
|
||||
import startsWith from 'lodash/startsWith'
|
||||
import {
|
||||
dirname,
|
||||
resolve
|
||||
} from 'path'
|
||||
|
||||
import RemoteHandlerAbstract from './abstract'
|
||||
import {
|
||||
noop
|
||||
} from '../utils'
|
||||
|
||||
export default class LocalHandler extends RemoteHandlerAbstract {
|
||||
get type () {
|
||||
return 'file'
|
||||
}
|
||||
|
||||
_getRealPath () {
|
||||
return this._remote.path
|
||||
}
|
||||
|
||||
_getFilePath (file) {
|
||||
const realPath = this._getRealPath()
|
||||
const parts = [realPath]
|
||||
if (file) {
|
||||
parts.push(file)
|
||||
}
|
||||
const path = resolve.apply(null, parts)
|
||||
if (!startsWith(path, realPath)) {
|
||||
throw new Error('Remote path is unavailable')
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
async _sync () {
|
||||
if (this._remote.enabled) {
|
||||
try {
|
||||
const path = this._getRealPath()
|
||||
await fs.ensureDir(path)
|
||||
await fs.access(path, fs.R_OK | fs.W_OK)
|
||||
} catch (exc) {
|
||||
this._remote.enabled = false
|
||||
this._remote.error = exc.message
|
||||
}
|
||||
}
|
||||
return this._remote
|
||||
}
|
||||
|
||||
async _forget () {
|
||||
return noop()
|
||||
}
|
||||
|
||||
async _outputFile (file, data, options) {
|
||||
const path = this._getFilePath(file)
|
||||
await fs.ensureDir(dirname(path))
|
||||
await fs.writeFile(path, data, options)
|
||||
}
|
||||
|
||||
async _readFile (file, options) {
|
||||
return fs.readFile(this._getFilePath(file), options)
|
||||
}
|
||||
|
||||
async _rename (oldPath, newPath) {
|
||||
return fs.rename(this._getFilePath(oldPath), this._getFilePath(newPath))
|
||||
}
|
||||
|
||||
async _list (dir = '.') {
|
||||
return fs.readdir(this._getFilePath(dir))
|
||||
}
|
||||
|
||||
async _createReadStream (file, options) {
|
||||
return fs.createReadStream(this._getFilePath(file), options)
|
||||
}
|
||||
|
||||
async _createOutputStream (file, options) {
|
||||
const path = this._getFilePath(file)
|
||||
await fs.ensureDir(dirname(path))
|
||||
return fs.createWriteStream(path, options)
|
||||
}
|
||||
|
||||
async _unlink (file) {
|
||||
return fs.unlink(this._getFilePath(file))
|
||||
}
|
||||
|
||||
async _getSize (file) {
|
||||
const stats = await fs.stat(this._getFilePath(file))
|
||||
return stats.size
|
||||
}
|
||||
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user