Compare commits
605 Commits
5.42
...
nr-nbd-pro
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0a7af4b1c7 | ||
|
|
28559cde02 | ||
|
|
6970d48cc3 | ||
|
|
52801c5afc | ||
|
|
7797bce814 | ||
|
|
18762dc624 | ||
|
|
5a828a6465 | ||
|
|
eaa9f36478 | ||
|
|
2b63134bcf | ||
|
|
8dcff63aea | ||
|
|
c2777607be | ||
|
|
9ba2b18fdb | ||
|
|
4ebc10db6a | ||
|
|
610b6c7bb0 | ||
|
|
357333c4e4 | ||
|
|
723334a685 | ||
|
|
b2c218ff83 | ||
|
|
adabd6966d | ||
|
|
b3eb1270dd | ||
|
|
7659a195d3 | ||
|
|
8d2e23f4a8 | ||
|
|
539d7dab5d | ||
|
|
06d43cdb24 | ||
|
|
af7bcf19ab | ||
|
|
7ebeb37881 | ||
|
|
4911bbe3a2 | ||
|
|
e0b6ab3f8a | ||
|
|
8736c2cf9a | ||
|
|
d825c33b55 | ||
|
|
171ecaaf62 | ||
|
|
5e6d5d4eb0 | ||
|
|
3733a3c335 | ||
|
|
7fca6defd6 | ||
|
|
2a270b399e | ||
|
|
64109aee05 | ||
|
|
e1d9395128 | ||
|
|
32eec95c26 | ||
|
|
f41cca45aa | ||
|
|
48eeab974c | ||
|
|
eed44156ae | ||
|
|
1177d9bdd8 | ||
|
|
d151a94285 | ||
|
|
a7fe6453ee | ||
|
|
313eb136f4 | ||
|
|
98591ff83d | ||
|
|
0b9d78560b | ||
|
|
32a930e598 | ||
|
|
edd8512196 | ||
|
|
7a6aec34ae | ||
|
|
009a0c5703 | ||
|
|
a99086b6bd | ||
|
|
a186672447 | ||
|
|
0b8a7c0d09 | ||
|
|
1990bf3d7a | ||
|
|
ea74a7e401 | ||
|
|
bf12c3ff74 | ||
|
|
9d261aae76 | ||
|
|
3d8c8fd745 | ||
|
|
6ad7db522a | ||
|
|
385984b1d8 | ||
|
|
4f3d4b06b5 | ||
|
|
2291986e2c | ||
|
|
fc81cf4d70 | ||
|
|
fdeab86a87 | ||
|
|
3616b7a67b | ||
|
|
83ea57d825 | ||
|
|
24a69bcade | ||
|
|
58dc3244be | ||
|
|
61e580b992 | ||
|
|
1116530a6b | ||
|
|
8cfaabedeb | ||
|
|
66ba05dcd0 | ||
|
|
d1db616d1e | ||
|
|
aed09b152a | ||
|
|
f755365e23 | ||
|
|
ccd34c1610 | ||
|
|
f9104e6cc9 | ||
|
|
4bb702fe89 | ||
|
|
511a04dad5 | ||
|
|
f3527a44d7 | ||
|
|
fdbe84cb1e | ||
|
|
45fe70f0fa | ||
|
|
2aed2fd534 | ||
|
|
a523fa9733 | ||
|
|
0f42f032e4 | ||
|
|
4575b98fd5 | ||
|
|
3a0cc0d6f6 | ||
|
|
626e2fcb12 | ||
|
|
592feb54b7 | ||
|
|
9c6b63e7e4 | ||
|
|
4364a74b7a | ||
|
|
00f13102f8 | ||
|
|
3f17389871 | ||
|
|
726ba287b1 | ||
|
|
42ee29cb3c | ||
|
|
8a98b6b012 | ||
|
|
14ab694804 | ||
|
|
14b8cda543 | ||
|
|
4264e34ffd | ||
|
|
bd9bf55e43 | ||
|
|
7c802bbd33 | ||
|
|
9e37f3f586 | ||
|
|
1d4f5d068a | ||
|
|
5be5eb80e8 | ||
|
|
12c774a34a | ||
|
|
14c3fa4378 | ||
|
|
2f17420721 | ||
|
|
8d7f8d156f | ||
|
|
38248d8c35 | ||
|
|
edaae02892 | ||
|
|
846eff4984 | ||
|
|
481adf3a1e | ||
|
|
d622f7a65c | ||
|
|
a479501aef | ||
|
|
2456374e5a | ||
|
|
c77016ea44 | ||
|
|
6fd45a37e2 | ||
|
|
9be56d3ab8 | ||
|
|
24b264b6c9 | ||
|
|
7f9130470b | ||
|
|
b82aa1daa5 | ||
|
|
53cb325974 | ||
|
|
1256c320e3 | ||
|
|
15bc30a2d5 | ||
|
|
fc3bc8468f | ||
|
|
b4e068f630 | ||
|
|
08eef80673 | ||
|
|
152f73ebf0 | ||
|
|
38de5048bc | ||
|
|
c4d96fbc49 | ||
|
|
ff25d402c1 | ||
|
|
f957024605 | ||
|
|
006e54e2fd | ||
|
|
5f7bc58788 | ||
|
|
bdd93603aa | ||
|
|
8392a17cb2 | ||
|
|
5f7f0b777e | ||
|
|
3f574606d9 | ||
|
|
45f0f93895 | ||
|
|
af2710135b | ||
|
|
95ed6094fe | ||
|
|
6af8ce9eeb | ||
|
|
3ff37f00fe | ||
|
|
ed5b066cbe | ||
|
|
cec5593c70 | ||
|
|
04924884ad | ||
|
|
3ccf64fcd3 | ||
|
|
8eb7f9b91c | ||
|
|
f25c50c629 | ||
|
|
e524a1b865 | ||
|
|
ac15e3355e | ||
|
|
0930a37819 | ||
|
|
d62f91a9e6 | ||
|
|
2789ead999 | ||
|
|
f25fd267dd | ||
|
|
47999f1f72 | ||
|
|
095bbcd15c | ||
|
|
9177bb8451 | ||
|
|
119bf9b0ff | ||
|
|
015c6037c4 | ||
|
|
452a7e7445 | ||
|
|
407586e2d5 | ||
|
|
ffa431a3cd | ||
|
|
281a5ff991 | ||
|
|
92db9bd284 | ||
|
|
ea8f319f45 | ||
|
|
a11e9fe04e | ||
|
|
27367bd1fc | ||
|
|
c6f48ae054 | ||
|
|
7d6efe3694 | ||
|
|
f4aad05edc | ||
|
|
d8f7637ca0 | ||
|
|
f9a7bd199e | ||
|
|
68b7ed284a | ||
|
|
e782895cf5 | ||
|
|
a5935b40d5 | ||
|
|
035d2cb440 | ||
|
|
2a74a49995 | ||
|
|
902953a1fa | ||
|
|
1ffef91b7a | ||
|
|
3d13d9b0dc | ||
|
|
adcc5d5692 | ||
|
|
c49d70170e | ||
|
|
349a78a5bd | ||
|
|
48734c6896 | ||
|
|
0f60a3b24d | ||
|
|
d3a88011a6 | ||
|
|
9b6e4c605b | ||
|
|
7c91524111 | ||
|
|
e1573069e4 | ||
|
|
f2459c964b | ||
|
|
43aa0b815d | ||
|
|
0740630e05 | ||
|
|
c9244b2b13 | ||
|
|
0d398f867f | ||
|
|
b74ec2d7d3 | ||
|
|
26a295c8ed | ||
|
|
2a71d3d20c | ||
|
|
b79605b692 | ||
|
|
ea0fc68a53 | ||
|
|
1ca5c32de3 | ||
|
|
f51bcfa05a | ||
|
|
e1bf68ab38 | ||
|
|
99e03b7ce5 | ||
|
|
cd70d3ea46 | ||
|
|
d387227cef | ||
|
|
2f4530e426 | ||
|
|
4db181d8bf | ||
|
|
9a7a1cc752 | ||
|
|
59ca6c6708 | ||
|
|
fe7901ca7f | ||
|
|
9351b4a5bb | ||
|
|
dfdd0a0496 | ||
|
|
cda39ec256 | ||
|
|
3720a46ff3 | ||
|
|
7ea50ea41e | ||
|
|
60a696916b | ||
|
|
b6a255d96f | ||
|
|
44a0cce7f2 | ||
|
|
f580e0d26f | ||
|
|
6beefe86e2 | ||
|
|
cbada35788 | ||
|
|
44ff2f872d | ||
|
|
2198853662 | ||
|
|
4636109081 | ||
|
|
1c042778b6 | ||
|
|
34b5962eac | ||
|
|
fc7af59eb7 | ||
|
|
7e557ca059 | ||
|
|
1d0cea8ad0 | ||
|
|
5c901d7c1e | ||
|
|
1dffab0bb8 | ||
|
|
ae89e14ea2 | ||
|
|
908255060c | ||
|
|
88278d0041 | ||
|
|
86bfd91c9d | ||
|
|
0ee412ccb9 | ||
|
|
b8bd6ea820 | ||
|
|
98a1ab3033 | ||
|
|
e360f53a40 | ||
|
|
237ec38003 | ||
|
|
30ea1bbf87 | ||
|
|
0d0aef6014 | ||
|
|
1b7441715c | ||
|
|
e3223b6124 | ||
|
|
41fb06187b | ||
|
|
adf0e8ae3b | ||
|
|
42dd1efb41 | ||
|
|
b6a6694abf | ||
|
|
04f2f50d6d | ||
|
|
6d1048e5c5 | ||
|
|
fe722c8b31 | ||
|
|
0326ce1d85 | ||
|
|
183ddb68d3 | ||
|
|
d7fe1afc08 | ||
|
|
ae9aeaf5fd | ||
|
|
ec9476216f | ||
|
|
619f2ef119 | ||
|
|
52020abde8 | ||
|
|
1bd504d67e | ||
|
|
edc4414de4 | ||
|
|
c1d588264c | ||
|
|
94b84b75ad | ||
|
|
b72a4c5aa9 | ||
|
|
857a9f3efc | ||
|
|
ce53128657 | ||
|
|
d9211053ce | ||
|
|
e8316178a0 | ||
|
|
bf763d2cf4 | ||
|
|
eba5b34982 | ||
|
|
afb8b3dd6b | ||
|
|
c5fa94894b | ||
|
|
4137758caa | ||
|
|
3578d16e9e | ||
|
|
3ef263a5cc | ||
|
|
510460c966 | ||
|
|
f74ecc53ae | ||
|
|
c4121073ad | ||
|
|
9ded2641a7 | ||
|
|
295ca68d02 | ||
|
|
27f53f262b | ||
|
|
3fc16cb414 | ||
|
|
90db25d732 | ||
|
|
bbb359470e | ||
|
|
319652c7c7 | ||
|
|
c9c271fee8 | ||
|
|
ca0755e92b | ||
|
|
acd38597f6 | ||
|
|
f4a5a80f3c | ||
|
|
c45d00fee8 | ||
|
|
ffae59fa1c | ||
|
|
b697178f68 | ||
|
|
83ade5eecb | ||
|
|
6973b92c4a | ||
|
|
6261f8a778 | ||
|
|
6048493ac6 | ||
|
|
1cbd715235 | ||
|
|
703fcbccd6 | ||
|
|
2f9cbec07e | ||
|
|
9f0b22d3e9 | ||
|
|
ab5907c09c | ||
|
|
fae0b168f6 | ||
|
|
f18e98a63e | ||
|
|
3524886d5d | ||
|
|
fb44eea06c | ||
|
|
3ea4c757e6 | ||
|
|
cfb8d79049 | ||
|
|
1ea86da7af | ||
|
|
e289f2dba2 | ||
|
|
7f64cd1801 | ||
|
|
d4526e1ed2 | ||
|
|
34f42216c8 | ||
|
|
a26a24a8ad | ||
|
|
4530fd4164 | ||
|
|
9156b8f48c | ||
|
|
6212109fc1 | ||
|
|
c22a080e23 | ||
|
|
834a7109f9 | ||
|
|
7cbf32202d | ||
|
|
d0b9380dca | ||
|
|
13fd9be566 | ||
|
|
53a9aa6ad2 | ||
|
|
c2ce4aca1b | ||
|
|
567f6d7cc0 | ||
|
|
489c0b27f9 | ||
|
|
343f988584 | ||
|
|
7f676c56c8 | ||
|
|
3c0ca7026f | ||
|
|
2ceba11aa7 | ||
|
|
6db5e0b27c | ||
|
|
dfecb801db | ||
|
|
c10bfe3db2 | ||
|
|
20fb2c99bc | ||
|
|
b4a0b5c58b | ||
|
|
faa46c2a21 | ||
|
|
9691199ae8 | ||
|
|
f736381933 | ||
|
|
f44e5b3b7a | ||
|
|
6e24bf5f8c | ||
|
|
8fb43e31c5 | ||
|
|
0860c80e51 | ||
|
|
66fc25756f | ||
|
|
f008e240cd | ||
|
|
8d7e95d6e9 | ||
|
|
3e3ce543a8 | ||
|
|
6c447a82f1 | ||
|
|
64a0918ff1 | ||
|
|
9274223701 | ||
|
|
1368c18844 | ||
|
|
67a60a7557 | ||
|
|
3d5fd47748 | ||
|
|
b9a18807ae | ||
|
|
088c0b6321 | ||
|
|
ecee11a24c | ||
|
|
ec8df7ce57 | ||
|
|
4159fd2ffb | ||
|
|
1a1d21bbb3 | ||
|
|
be1045fed9 | ||
|
|
e43773c712 | ||
|
|
30d69dadbb | ||
|
|
b138438036 | ||
|
|
d649211330 | ||
|
|
6cf211a9ad | ||
|
|
3388e5e8a4 | ||
|
|
5d497a1908 | ||
|
|
c820646fb6 | ||
|
|
5870f6f734 | ||
|
|
6732150121 | ||
|
|
1dead8b080 | ||
|
|
d547aa8ebd | ||
|
|
1da889e420 | ||
|
|
5d0a308d1d | ||
|
|
f9886d52da | ||
|
|
4f8e48b7d4 | ||
|
|
258e07c2ca | ||
|
|
cc32c50665 | ||
|
|
ec1d91f73e | ||
|
|
eb2f429964 | ||
|
|
1ad067309d | ||
|
|
48ce7df43a | ||
|
|
6555e2c440 | ||
|
|
a05191e112 | ||
|
|
b8eeee1d5d | ||
|
|
4aa87f3fa5 | ||
|
|
40c37d923b | ||
|
|
5a5837b8ed | ||
|
|
1e0b521070 | ||
|
|
35ed58cc5e | ||
|
|
c4a1579197 | ||
|
|
e471706422 | ||
|
|
d78b7350b5 | ||
|
|
47b29d5a49 | ||
|
|
e5946a51d1 | ||
|
|
a88798cc22 | ||
|
|
6fbd32523a | ||
|
|
94b1cc2bdd | ||
|
|
0ed5c8f0ae | ||
|
|
5f883f552b | ||
|
|
9db99ab4a5 | ||
|
|
287214c2b2 | ||
|
|
317a020841 | ||
|
|
b50e3aec5f | ||
|
|
21a9e0e2a7 | ||
|
|
7775df8ef1 | ||
|
|
53f9b5d131 | ||
|
|
bf4d4a4742 | ||
|
|
0dbbe7104d | ||
|
|
561ef00680 | ||
|
|
85428fa72e | ||
|
|
29d0593b86 | ||
|
|
ac524dd799 | ||
|
|
aba8b764b6 | ||
|
|
a9050e0f41 | ||
|
|
15ef84e238 | ||
|
|
09096fef5b | ||
|
|
19b08e1019 | ||
|
|
06d67642dd | ||
|
|
ceb6c450c0 | ||
|
|
a745e42cf5 | ||
|
|
462d6a4450 | ||
|
|
a2e39c5e2e | ||
|
|
ec899be3b5 | ||
|
|
c79ebfdd0a | ||
|
|
cd95e6c552 | ||
|
|
f676145302 | ||
|
|
0847267069 | ||
|
|
6fe1da1587 | ||
|
|
7ab555d869 | ||
|
|
4a35e9e60d | ||
|
|
5e8dfdfd9b | ||
|
|
278a1b8ab3 | ||
|
|
6cb03dded1 | ||
|
|
4752ec1b67 | ||
|
|
e641371544 | ||
|
|
8d9a7e9af1 | ||
|
|
2b8e9bf887 | ||
|
|
a16c55c679 | ||
|
|
9a72c40149 | ||
|
|
8424fc4c19 | ||
|
|
9f29a047a7 | ||
|
|
af4904ce8d | ||
|
|
6f2a323063 | ||
|
|
1da8ecfaac | ||
|
|
1ba386bbd2 | ||
|
|
d00d791cda | ||
|
|
51aabd7b21 | ||
|
|
7840c3bdc8 | ||
|
|
07d13002b0 | ||
|
|
23abe2ba06 | ||
|
|
4d73821d21 | ||
|
|
f2687cf807 | ||
|
|
8abce0d4cf | ||
|
|
62ad3848c4 | ||
|
|
521e9969f1 | ||
|
|
5ee1ceced3 | ||
|
|
efffbafa42 | ||
|
|
899cec8814 | ||
|
|
55beb993e5 | ||
|
|
fba46a1e00 | ||
|
|
68c4d6f295 | ||
|
|
2ad07c018e | ||
|
|
c2418559f1 | ||
|
|
de0277eb3b | ||
|
|
b58802b495 | ||
|
|
898e20e8b2 | ||
|
|
75bf10d83d | ||
|
|
e8e58cc4a2 | ||
|
|
342d02c8a8 | ||
|
|
ae885eaddc | ||
|
|
b75bb4ed9f | ||
|
|
a171863591 | ||
|
|
bcc2286018 | ||
|
|
beb8b9723f | ||
|
|
ab3129b9c3 | ||
|
|
c061505bf8 | ||
|
|
171bbedcde | ||
|
|
ae655727c0 | ||
|
|
288b3783c3 | ||
|
|
320612c826 | ||
|
|
9b0a293d2b | ||
|
|
d5b6f27e97 | ||
|
|
cf8bd759e3 | ||
|
|
c2142c5cd9 | ||
|
|
eeef536803 | ||
|
|
ec51cf7606 | ||
|
|
ec83b76e46 | ||
|
|
6bc73f8f43 | ||
|
|
65127e04aa | ||
|
|
2726045fb0 | ||
|
|
d4046c3295 | ||
|
|
97d4f5583c | ||
|
|
96dc36cd16 | ||
|
|
ab2344ebfc | ||
|
|
4dd7d86ea8 | ||
|
|
6adfa618a3 | ||
|
|
855e896a5a | ||
|
|
2bacb0f073 | ||
|
|
399131a5a9 | ||
|
|
4c77be9e83 | ||
|
|
8ed5463f19 | ||
|
|
63739df903 | ||
|
|
3d72232bb9 | ||
|
|
d5ff811de7 | ||
|
|
f74f47965c | ||
|
|
79adb7225e | ||
|
|
11e79914ef | ||
|
|
9b34fc369b | ||
|
|
ae812806a1 | ||
|
|
956c7728bf | ||
|
|
6d5a5a46e4 | ||
|
|
b244126d60 | ||
|
|
2a7f8c5229 | ||
|
|
59fddf7c59 | ||
|
|
d21fd2a1ed | ||
|
|
6828a5d9a0 | ||
|
|
32960332b9 | ||
|
|
9ba77c9498 | ||
|
|
65f9a9dcc1 | ||
|
|
13315eec42 | ||
|
|
e43ccd155b | ||
|
|
e92960a413 | ||
|
|
e82eec0cd6 | ||
|
|
23b687c528 | ||
|
|
5044a814a1 | ||
|
|
3250b95f5e | ||
|
|
2a0934ec28 | ||
|
|
f1752abc5d | ||
|
|
e931ec74dd | ||
|
|
528d823c55 | ||
|
|
6475b58541 | ||
|
|
59e8b26015 | ||
|
|
0894c21296 | ||
|
|
4223bdd4ad | ||
|
|
c55ed42b2b | ||
|
|
7abc833ebe | ||
|
|
6abe399e36 | ||
|
|
555b1a16da | ||
|
|
52b956e677 | ||
|
|
0296cbe9e9 | ||
|
|
8315d7790a | ||
|
|
dd60d289ff | ||
|
|
8c61fd0bf7 | ||
|
|
814e08edd4 | ||
|
|
44956dff85 | ||
|
|
18685d061a | ||
|
|
692d5be166 | ||
|
|
60bdaef716 | ||
|
|
cb51f44a45 | ||
|
|
88f43a8124 | ||
|
|
7de7cdba60 | ||
|
|
ba140c60e3 | ||
|
|
95a5c7a001 | ||
|
|
b399da72d8 | ||
|
|
f9a10d8932 | ||
|
|
549ce6fbf9 | ||
|
|
e63d27a035 | ||
|
|
ab3621fe3c | ||
|
|
8b99f2ecbc | ||
|
|
433b309907 | ||
|
|
ac40cec138 | ||
|
|
93702ece48 | ||
|
|
1678474830 | ||
|
|
4260099c23 | ||
|
|
6139cb50bc | ||
|
|
49e334d726 | ||
|
|
6beded153b | ||
|
|
750308a16a | ||
|
|
57fa00b765 | ||
|
|
b57d4fdbec | ||
|
|
93aca81265 | ||
|
|
5c704e142e | ||
|
|
1a4a77066e | ||
|
|
a690b9d825 | ||
|
|
b70969fd03 | ||
|
|
daf5fff83f | ||
|
|
aef1f9b857 | ||
|
|
c1c8ea7df0 | ||
|
|
b75034b40c | ||
|
|
cb41a79b36 | ||
|
|
cedc20ce6a | ||
|
|
43bc1e9116 | ||
|
|
6fa1379c1e | ||
|
|
cb16438b1c | ||
|
|
e6c8d6cc7d | ||
|
|
f0e87e71ab | ||
|
|
cdd28b8e31 | ||
|
|
9bff564ace | ||
|
|
62c616c6af | ||
|
|
8a35f5d6ca | ||
|
|
e1b051324d | ||
|
|
a947c3152a | ||
|
|
cca43040d3 | ||
|
|
9b42657ca7 | ||
|
|
bb19c55c3a | ||
|
|
1ec6611410 | ||
|
|
218bd0ffc1 | ||
|
|
d649a22b80 | ||
|
|
0bffbbfe65 | ||
|
|
04a562372b | ||
|
|
11b08ce53a | ||
|
|
70f8f9679d | ||
|
|
4773f9ebf6 | ||
|
|
c1b6d1706a | ||
|
|
9f94b8f915 | ||
|
|
3abd97d0fb |
@@ -1,6 +1,6 @@
|
||||
module.exports = {
|
||||
arrowParens: 'avoid',
|
||||
jsxSingleQuote: true,
|
||||
semi: false,
|
||||
singleQuote: true,
|
||||
trailingComma: 'es5',
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
#- stable # disable for now due to an issue of indirect dep upath with Node 9
|
||||
- 8
|
||||
- 12
|
||||
|
||||
# Use containers.
|
||||
# http://docs.travis-ci.com/user/workers/container-based-infrastructure/
|
||||
|
||||
46
@vates/coalesce-calls/README.md
Normal file
46
@vates/coalesce-calls/README.md
Normal file
@@ -0,0 +1,46 @@
|
||||
<!-- DO NOT EDIT MANUALLY, THIS FILE HAS BEEN GENERATED -->
|
||||
|
||||
# @vates/coalesce-calls
|
||||
|
||||
[](https://npmjs.org/package/@vates/coalesce-calls)  [](https://bundlephobia.com/result?p=@vates/coalesce-calls) [](https://npmjs.org/package/@vates/coalesce-calls)
|
||||
|
||||
> Wraps an async function so that concurrent calls will be coalesced
|
||||
|
||||
## Install
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/@vates/coalesce-calls):
|
||||
|
||||
```
|
||||
> npm install --save @vates/coalesce-calls
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```js
|
||||
import { coalesceCalls } from '@vates/coalesce-calls'
|
||||
|
||||
const connect = coalesceCalls(async function () {
|
||||
// async operation
|
||||
})
|
||||
|
||||
connect()
|
||||
|
||||
// the previous promise result will be returned if the operation is not
|
||||
// complete yet
|
||||
connect()
|
||||
```
|
||||
|
||||
## Contributions
|
||||
|
||||
Contributions are _very_ welcomed, either on the documentation or on
|
||||
the code.
|
||||
|
||||
You may:
|
||||
|
||||
- report any [issue](https://github.com/vatesfr/xen-orchestra/issues)
|
||||
you've encountered;
|
||||
- fork and create a pull request.
|
||||
|
||||
## License
|
||||
|
||||
[ISC](https://spdx.org/licenses/ISC) © [Vates SAS](https://vates.fr)
|
||||
13
@vates/coalesce-calls/USAGE.md
Normal file
13
@vates/coalesce-calls/USAGE.md
Normal file
@@ -0,0 +1,13 @@
|
||||
```js
|
||||
import { coalesceCalls } from '@vates/coalesce-calls'
|
||||
|
||||
const connect = coalesceCalls(async function () {
|
||||
// async operation
|
||||
})
|
||||
|
||||
connect()
|
||||
|
||||
// the previous promise result will be returned if the operation is not
|
||||
// complete yet
|
||||
connect()
|
||||
```
|
||||
14
@vates/coalesce-calls/index.js
Normal file
14
@vates/coalesce-calls/index.js
Normal file
@@ -0,0 +1,14 @@
|
||||
exports.coalesceCalls = function (fn) {
|
||||
let promise
|
||||
const clean = () => {
|
||||
promise = undefined
|
||||
}
|
||||
return function () {
|
||||
if (promise !== undefined) {
|
||||
return promise
|
||||
}
|
||||
promise = fn.apply(this, arguments)
|
||||
promise.then(clean, clean)
|
||||
return promise
|
||||
}
|
||||
}
|
||||
33
@vates/coalesce-calls/index.spec.js
Normal file
33
@vates/coalesce-calls/index.spec.js
Normal file
@@ -0,0 +1,33 @@
|
||||
/* eslint-env jest */
|
||||
|
||||
const { coalesceCalls } = require('./')
|
||||
|
||||
const pDefer = () => {
|
||||
const r = {}
|
||||
r.promise = new Promise((resolve, reject) => {
|
||||
r.reject = reject
|
||||
r.resolve = resolve
|
||||
})
|
||||
return r
|
||||
}
|
||||
|
||||
describe('coalesceCalls', () => {
|
||||
it('decorates an async function', async () => {
|
||||
const fn = coalesceCalls(promise => promise)
|
||||
|
||||
const defer1 = pDefer()
|
||||
const promise1 = fn(defer1.promise)
|
||||
const defer2 = pDefer()
|
||||
const promise2 = fn(defer2.promise)
|
||||
|
||||
defer1.resolve('foo')
|
||||
expect(await promise1).toBe('foo')
|
||||
expect(await promise2).toBe('foo')
|
||||
|
||||
const defer3 = pDefer()
|
||||
const promise3 = fn(defer3.promise)
|
||||
|
||||
defer3.resolve('bar')
|
||||
expect(await promise3).toBe('bar')
|
||||
})
|
||||
})
|
||||
38
@vates/coalesce-calls/package.json
Normal file
38
@vates/coalesce-calls/package.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"private": false,
|
||||
"name": "@vates/coalesce-calls",
|
||||
"description": "Wraps an async function so that concurrent calls will be coalesced",
|
||||
"keywords": [
|
||||
"async",
|
||||
"calls",
|
||||
"coalesce",
|
||||
"decorate",
|
||||
"decorator",
|
||||
"merge",
|
||||
"promise",
|
||||
"wrap",
|
||||
"wrapper"
|
||||
],
|
||||
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@vates/coalesce-calls",
|
||||
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
|
||||
"repository": {
|
||||
"directory": "@vates/coalesce-calls",
|
||||
"type": "git",
|
||||
"url": "https://github.com/vatesfr/xen-orchestra.git"
|
||||
},
|
||||
"files": [
|
||||
"index.js"
|
||||
],
|
||||
"author": {
|
||||
"name": "Vates SAS",
|
||||
"url": "https://vates.fr"
|
||||
},
|
||||
"license": "ISC",
|
||||
"version": "0.1.0",
|
||||
"engines": {
|
||||
"node": ">=8.10"
|
||||
},
|
||||
"scripts": {
|
||||
"postversion": "npm publish --access public"
|
||||
}
|
||||
}
|
||||
45
@vates/decorate-with/README.md
Normal file
45
@vates/decorate-with/README.md
Normal file
@@ -0,0 +1,45 @@
|
||||
<!-- DO NOT EDIT MANUALLY, THIS FILE HAS BEEN GENERATED -->
|
||||
|
||||
# @vates/decorate-with
|
||||
|
||||
[](https://npmjs.org/package/@vates/decorate-with)  [](https://bundlephobia.com/result?p=@vates/decorate-with) [](https://npmjs.org/package/@vates/decorate-with)
|
||||
|
||||
> Creates a decorator from a function wrapper
|
||||
|
||||
## Install
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/@vates/decorate-with):
|
||||
|
||||
```
|
||||
> npm install --save @vates/decorate-with
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
For instance, allows using Lodash's functions as decorators:
|
||||
|
||||
```js
|
||||
import { decorateWith } from '@vates/decorate-with'
|
||||
|
||||
class Foo {
|
||||
@decorateWith(lodash.debounce, 150)
|
||||
bar() {
|
||||
// body
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Contributions
|
||||
|
||||
Contributions are _very_ welcomed, either on the documentation or on
|
||||
the code.
|
||||
|
||||
You may:
|
||||
|
||||
- report any [issue](https://github.com/vatesfr/xen-orchestra/issues)
|
||||
you've encountered;
|
||||
- fork and create a pull request.
|
||||
|
||||
## License
|
||||
|
||||
[ISC](https://spdx.org/licenses/ISC) © [Vates SAS](https://vates.fr)
|
||||
12
@vates/decorate-with/USAGE.md
Normal file
12
@vates/decorate-with/USAGE.md
Normal file
@@ -0,0 +1,12 @@
|
||||
For instance, allows using Lodash's functions as decorators:
|
||||
|
||||
```js
|
||||
import { decorateWith } from '@vates/decorate-with'
|
||||
|
||||
class Foo {
|
||||
@decorateWith(lodash.debounce, 150)
|
||||
bar() {
|
||||
// body
|
||||
}
|
||||
}
|
||||
```
|
||||
4
@vates/decorate-with/index.js
Normal file
4
@vates/decorate-with/index.js
Normal file
@@ -0,0 +1,4 @@
|
||||
exports.decorateWith = (fn, ...args) => (target, name, descriptor) => ({
|
||||
...descriptor,
|
||||
value: fn(descriptor.value, ...args),
|
||||
})
|
||||
30
@vates/decorate-with/package.json
Normal file
30
@vates/decorate-with/package.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"private": false,
|
||||
"name": "@vates/decorate-with",
|
||||
"description": "Creates a decorator from a function wrapper",
|
||||
"keywords": [
|
||||
"apply",
|
||||
"decorator",
|
||||
"factory",
|
||||
"wrapper"
|
||||
],
|
||||
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@vates/decorate-with",
|
||||
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
|
||||
"repository": {
|
||||
"directory": "@vates/decorate-with",
|
||||
"type": "git",
|
||||
"url": "https://github.com/vatesfr/xen-orchestra.git"
|
||||
},
|
||||
"author": {
|
||||
"name": "Vates SAS",
|
||||
"url": "https://vates.fr"
|
||||
},
|
||||
"license": "ISC",
|
||||
"version": "0.0.1",
|
||||
"engines": {
|
||||
"node": ">=8.10"
|
||||
},
|
||||
"scripts": {
|
||||
"postversion": "npm publish --access public"
|
||||
}
|
||||
}
|
||||
47
@vates/parse-duration/README.md
Normal file
47
@vates/parse-duration/README.md
Normal file
@@ -0,0 +1,47 @@
|
||||
<!-- DO NOT EDIT MANUALLY, THIS FILE HAS BEEN GENERATED -->
|
||||
|
||||
# @vates/parse-duration
|
||||
|
||||
[](https://npmjs.org/package/@vates/parse-duration)  [](https://bundlephobia.com/result?p=@vates/parse-duration) [](https://npmjs.org/package/@vates/parse-duration)
|
||||
|
||||
> Small wrapper around ms to parse a duration
|
||||
|
||||
## Install
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/@vates/parse-duration):
|
||||
|
||||
```
|
||||
> npm install --save @vates/parse-duration
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
`ms` without magic: always parse a duration and throws if invalid.
|
||||
|
||||
```js
|
||||
import { parseDuration } from '@vates/parse-duration'
|
||||
|
||||
parseDuration('2 days')
|
||||
// 172800000
|
||||
|
||||
parseDuration(172800000)
|
||||
// 172800000
|
||||
|
||||
parseDuration(undefined)
|
||||
// throws TypeError('not a valid duration: undefined')
|
||||
```
|
||||
|
||||
## Contributions
|
||||
|
||||
Contributions are _very_ welcomed, either on the documentation or on
|
||||
the code.
|
||||
|
||||
You may:
|
||||
|
||||
- report any [issue](https://github.com/vatesfr/xen-orchestra/issues)
|
||||
you've encountered;
|
||||
- fork and create a pull request.
|
||||
|
||||
## License
|
||||
|
||||
[AGPL-3.0-or-later](https://spdx.org/licenses/AGPL-3.0-or-later) © [Vates SAS](https://vates.fr)
|
||||
14
@vates/parse-duration/USAGE.md
Normal file
14
@vates/parse-duration/USAGE.md
Normal file
@@ -0,0 +1,14 @@
|
||||
`ms` without magic: always parse a duration and throws if invalid.
|
||||
|
||||
```js
|
||||
import { parseDuration } from '@vates/parse-duration'
|
||||
|
||||
parseDuration('2 days')
|
||||
// 172800000
|
||||
|
||||
parseDuration(172800000)
|
||||
// 172800000
|
||||
|
||||
parseDuration(undefined)
|
||||
// throws TypeError('not a valid duration: undefined')
|
||||
```
|
||||
@@ -1,6 +1,6 @@
|
||||
import ms from 'ms'
|
||||
const ms = require('ms')
|
||||
|
||||
export default value => {
|
||||
exports.parseDuration = value => {
|
||||
if (typeof value === 'number') {
|
||||
return value
|
||||
}
|
||||
32
@vates/parse-duration/package.json
Normal file
32
@vates/parse-duration/package.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"private": false,
|
||||
"name": "@vates/parse-duration",
|
||||
"description": "Small wrapper around ms to parse a duration",
|
||||
"keywords": [
|
||||
"duration",
|
||||
"ms",
|
||||
"parse"
|
||||
],
|
||||
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@vates/parse-duration",
|
||||
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
|
||||
"repository": {
|
||||
"directory": "@vates/parse-duration",
|
||||
"type": "git",
|
||||
"url": "https://github.com/vatesfr/xen-orchestra.git"
|
||||
},
|
||||
"author": {
|
||||
"name": "Vates SAS",
|
||||
"url": "https://vates.fr"
|
||||
},
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"version": "0.1.0",
|
||||
"engines": {
|
||||
"node": ">=8.10"
|
||||
},
|
||||
"dependencies": {
|
||||
"ms": "^2.1.2"
|
||||
},
|
||||
"scripts": {
|
||||
"postversion": "npm publish --access public"
|
||||
}
|
||||
}
|
||||
46
@vates/read-chunk/README.md
Normal file
46
@vates/read-chunk/README.md
Normal file
@@ -0,0 +1,46 @@
|
||||
<!-- DO NOT EDIT MANUALLY, THIS FILE HAS BEEN GENERATED -->
|
||||
|
||||
# @vates/read-chunk
|
||||
|
||||
[](https://npmjs.org/package/@vates/read-chunk)  [](https://bundlephobia.com/result?p=@vates/read-chunk) [](https://npmjs.org/package/@vates/read-chunk)
|
||||
|
||||
> Read a chunk of a Node stream
|
||||
|
||||
## Install
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/@vates/read-chunk):
|
||||
|
||||
```
|
||||
> npm install --save @vates/read-chunk
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
- returns the next available chunk of data
|
||||
- like `stream.read()`, a number of bytes can be specified
|
||||
- returns `null` if the stream has ended
|
||||
|
||||
```js
|
||||
import { readChunk } from '@vates/read-chunk'
|
||||
;(async () => {
|
||||
let chunk
|
||||
while ((chunk = await readChunk(stream, 1024)) !== null) {
|
||||
// do something with chunk
|
||||
}
|
||||
})()
|
||||
```
|
||||
|
||||
## Contributions
|
||||
|
||||
Contributions are _very_ welcomed, either on the documentation or on
|
||||
the code.
|
||||
|
||||
You may:
|
||||
|
||||
- report any [issue](https://github.com/vatesfr/xen-orchestra/issues)
|
||||
you've encountered;
|
||||
- fork and create a pull request.
|
||||
|
||||
## License
|
||||
|
||||
[ISC](https://spdx.org/licenses/ISC) © [Vates SAS](https://vates.fr)
|
||||
13
@vates/read-chunk/USAGE.md
Normal file
13
@vates/read-chunk/USAGE.md
Normal file
@@ -0,0 +1,13 @@
|
||||
- returns the next available chunk of data
|
||||
- like `stream.read()`, a number of bytes can be specified
|
||||
- returns `null` if the stream has ended
|
||||
|
||||
```js
|
||||
import { readChunk } from '@vates/read-chunk'
|
||||
;(async () => {
|
||||
let chunk
|
||||
while ((chunk = await readChunk(stream, 1024)) !== null) {
|
||||
// do something with chunk
|
||||
}
|
||||
})()
|
||||
```
|
||||
27
@vates/read-chunk/index.js
Normal file
27
@vates/read-chunk/index.js
Normal file
@@ -0,0 +1,27 @@
|
||||
exports.readChunk = (stream, size) =>
|
||||
new Promise((resolve, reject) => {
|
||||
function onEnd() {
|
||||
resolve(null)
|
||||
removeListeners()
|
||||
}
|
||||
function onError(error) {
|
||||
reject(error)
|
||||
removeListeners()
|
||||
}
|
||||
function onReadable() {
|
||||
const data = stream.read(size)
|
||||
if (data !== null) {
|
||||
resolve(data)
|
||||
removeListeners()
|
||||
}
|
||||
}
|
||||
function removeListeners() {
|
||||
stream.removeListener('end', onEnd)
|
||||
stream.removeListener('error', onError)
|
||||
stream.removeListener('readable', onReadable)
|
||||
}
|
||||
stream.on('end', onEnd)
|
||||
stream.on('error', onError)
|
||||
stream.on('readable', onReadable)
|
||||
onReadable()
|
||||
})
|
||||
33
@vates/read-chunk/package.json
Normal file
33
@vates/read-chunk/package.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"private": false,
|
||||
"name": "@vates/read-chunk",
|
||||
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@vates/read-chunk",
|
||||
"description": "Read a chunk of a Node stream",
|
||||
"license": "ISC",
|
||||
"keywords": [
|
||||
"async",
|
||||
"chunk",
|
||||
"data",
|
||||
"node",
|
||||
"promise",
|
||||
"read",
|
||||
"stream"
|
||||
],
|
||||
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
|
||||
"repository": {
|
||||
"directory": "@vates/read-chunk",
|
||||
"type": "git",
|
||||
"url": "https://github.com/vatesfr/xen-orchestra.git"
|
||||
},
|
||||
"version": "0.1.1",
|
||||
"engines": {
|
||||
"node": ">=8.10"
|
||||
},
|
||||
"scripts": {
|
||||
"postversion": "npm publish --access public"
|
||||
},
|
||||
"author": {
|
||||
"name": "Vates SAS",
|
||||
"url": "https://vates.fr"
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,10 @@
|
||||
# @xen-orchestra/async-map [](https://travis-ci.org/vatesfr/xen-orchestra)
|
||||
<!-- DO NOT EDIT MANUALLY, THIS FILE HAS BEEN GENERATED -->
|
||||
|
||||
> ${pkg.description}
|
||||
# @xen-orchestra/async-map
|
||||
|
||||
[](https://npmjs.org/package/@xen-orchestra/async-map)  [](https://bundlephobia.com/result?p=@xen-orchestra/async-map) [](https://npmjs.org/package/@xen-orchestra/async-map)
|
||||
|
||||
> Similar to Promise.all + lodash.map but wait for all promises to be settled
|
||||
|
||||
## Install
|
||||
|
||||
@@ -12,25 +16,10 @@ Installation of the [npm package](https://npmjs.org/package/@xen-orchestra/async
|
||||
|
||||
## Usage
|
||||
|
||||
**TODO**
|
||||
```js
|
||||
import asyncMap from '@xen-orchestra/async-map'
|
||||
|
||||
## Development
|
||||
|
||||
```
|
||||
# Install dependencies
|
||||
> yarn
|
||||
|
||||
# Run the tests
|
||||
> yarn test
|
||||
|
||||
# Continuously compile
|
||||
> yarn dev
|
||||
|
||||
# Continuously run the tests
|
||||
> yarn dev-test
|
||||
|
||||
# Build for production (automatically called by npm install)
|
||||
> yarn build
|
||||
const array = await asyncMap(collection, iteratee)
|
||||
```
|
||||
|
||||
## Contributions
|
||||
@@ -46,4 +35,4 @@ You may:
|
||||
|
||||
## License
|
||||
|
||||
ISC © [Vates SAS](https://vates.fr)
|
||||
[ISC](https://spdx.org/licenses/ISC) © [Vates SAS](https://vates.fr)
|
||||
|
||||
5
@xen-orchestra/async-map/USAGE.md
Normal file
5
@xen-orchestra/async-map/USAGE.md
Normal file
@@ -0,0 +1,5 @@
|
||||
```js
|
||||
import asyncMap from '@xen-orchestra/async-map'
|
||||
|
||||
const array = await asyncMap(collection, iteratee)
|
||||
```
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "@xen-orchestra/async-map",
|
||||
"version": "0.0.0",
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
"description": "Similar to Promise.all + lodash.map but wait for all promises to be settled",
|
||||
"keywords": [],
|
||||
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/async-map",
|
||||
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
|
||||
@@ -13,8 +13,8 @@
|
||||
"url": "https://github.com/vatesfr/xen-orchestra.git"
|
||||
},
|
||||
"author": {
|
||||
"name": "Julien Fontanet",
|
||||
"email": "julien.fontanet@isonoe.net"
|
||||
"name": "Vates SAS",
|
||||
"url": "https://vates.fr"
|
||||
},
|
||||
"preferGlobal": false,
|
||||
"main": "dist/",
|
||||
@@ -37,7 +37,7 @@
|
||||
"@babel/preset-env": "^7.0.0",
|
||||
"@babel/preset-flow": "^7.0.0",
|
||||
"babel-plugin-lodash": "^3.3.2",
|
||||
"cross-env": "^6.0.3",
|
||||
"cross-env": "^7.0.2",
|
||||
"rimraf": "^3.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
@@ -46,7 +46,6 @@
|
||||
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
|
||||
"prebuild": "yarn run clean",
|
||||
"predev": "yarn run prebuild",
|
||||
"prepare": "yarn run build",
|
||||
"prepublishOnly": "yarn run build",
|
||||
"postversion": "npm publish"
|
||||
}
|
||||
|
||||
3
@xen-orchestra/audit-core/.babelrc.js
Normal file
3
@xen-orchestra/audit-core/.babelrc.js
Normal file
@@ -0,0 +1,3 @@
|
||||
module.exports = require('../../@xen-orchestra/babel-config')(
|
||||
require('./package.json')
|
||||
)
|
||||
24
@xen-orchestra/audit-core/.npmignore
Normal file
24
@xen-orchestra/audit-core/.npmignore
Normal file
@@ -0,0 +1,24 @@
|
||||
/benchmark/
|
||||
/benchmarks/
|
||||
*.bench.js
|
||||
*.bench.js.map
|
||||
|
||||
/examples/
|
||||
example.js
|
||||
example.js.map
|
||||
*.example.js
|
||||
*.example.js.map
|
||||
|
||||
/fixture/
|
||||
/fixtures/
|
||||
*.fixture.js
|
||||
*.fixture.js.map
|
||||
*.fixtures.js
|
||||
*.fixtures.js.map
|
||||
|
||||
/test/
|
||||
/tests/
|
||||
*.spec.js
|
||||
*.spec.js.map
|
||||
|
||||
__snapshots__/
|
||||
28
@xen-orchestra/audit-core/README.md
Normal file
28
@xen-orchestra/audit-core/README.md
Normal file
@@ -0,0 +1,28 @@
|
||||
<!-- DO NOT EDIT MANUALLY, THIS FILE HAS BEEN GENERATED -->
|
||||
|
||||
# @xen-orchestra/audit-core
|
||||
|
||||
[](https://npmjs.org/package/@xen-orchestra/audit-core)  [](https://bundlephobia.com/result?p=@xen-orchestra/audit-core) [](https://npmjs.org/package/@xen-orchestra/audit-core)
|
||||
|
||||
## Install
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/@xen-orchestra/audit-core):
|
||||
|
||||
```
|
||||
> npm install --save @xen-orchestra/audit-core
|
||||
```
|
||||
|
||||
## Contributions
|
||||
|
||||
Contributions are _very_ welcomed, either on the documentation or on
|
||||
the code.
|
||||
|
||||
You may:
|
||||
|
||||
- report any [issue](https://github.com/vatesfr/xen-orchestra/issues)
|
||||
you've encountered;
|
||||
- fork and create a pull request.
|
||||
|
||||
## License
|
||||
|
||||
[AGPL-3.0-or-later](https://spdx.org/licenses/AGPL-3.0-or-later) © [Vates SAS](https://vates.fr)
|
||||
0
@xen-orchestra/audit-core/USAGE.md
Normal file
0
@xen-orchestra/audit-core/USAGE.md
Normal file
45
@xen-orchestra/audit-core/package.json
Normal file
45
@xen-orchestra/audit-core/package.json
Normal file
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"name": "@xen-orchestra/audit-core",
|
||||
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/audit-core",
|
||||
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
|
||||
"repository": {
|
||||
"directory": "@xen-orchestra/audit-core",
|
||||
"type": "git",
|
||||
"url": "https://github.com/vatesfr/xen-orchestra.git"
|
||||
},
|
||||
"version": "0.2.0",
|
||||
"engines": {
|
||||
"node": ">=8.10"
|
||||
},
|
||||
"main": "dist/",
|
||||
"scripts": {
|
||||
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
|
||||
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
|
||||
"postversion": "npm publish --access public",
|
||||
"prebuild": "rimraf dist/",
|
||||
"predev": "yarn run prebuild",
|
||||
"prepublishOnly": "yarn run build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.7.4",
|
||||
"@babel/core": "^7.7.4",
|
||||
"@babel/plugin-proposal-decorators": "^7.8.0",
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.8.0",
|
||||
"@babel/preset-env": "^7.7.4",
|
||||
"cross": "^1.0.0",
|
||||
"rimraf": "^3.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@xen-orchestra/log": "^0.2.0",
|
||||
"core-js": "^3.6.4",
|
||||
"golike-defer": "^0.4.1",
|
||||
"lodash": "^4.17.15",
|
||||
"object-hash": "^2.0.1"
|
||||
},
|
||||
"private": false,
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"author": {
|
||||
"name": "Vates SAS",
|
||||
"url": "https://vates.fr"
|
||||
}
|
||||
}
|
||||
195
@xen-orchestra/audit-core/src/index.js
Normal file
195
@xen-orchestra/audit-core/src/index.js
Normal file
@@ -0,0 +1,195 @@
|
||||
// see https://github.com/babel/babel/issues/8450
|
||||
import 'core-js/features/symbol/async-iterator'
|
||||
|
||||
import assert from 'assert'
|
||||
import createLogger from '@xen-orchestra/log'
|
||||
import defer from 'golike-defer'
|
||||
import hash from 'object-hash'
|
||||
|
||||
const log = createLogger('xo:audit-core')
|
||||
|
||||
export class Storage {
|
||||
constructor() {
|
||||
this._lock = Promise.resolve()
|
||||
}
|
||||
|
||||
async acquireLock() {
|
||||
const lock = this._lock
|
||||
let releaseLock
|
||||
this._lock = new Promise(resolve => {
|
||||
releaseLock = resolve
|
||||
})
|
||||
await lock
|
||||
return releaseLock
|
||||
}
|
||||
}
|
||||
|
||||
// Format: $<algorithm>$<salt>$<encrypted>
|
||||
//
|
||||
// http://man7.org/linux/man-pages/man3/crypt.3.html#NOTES
|
||||
const ID_TO_ALGORITHM = {
|
||||
5: 'sha256',
|
||||
}
|
||||
|
||||
export class AlteredRecordError extends Error {
|
||||
constructor(id, nValid, record) {
|
||||
super('altered record')
|
||||
|
||||
this.id = id
|
||||
this.nValid = nValid
|
||||
this.record = record
|
||||
}
|
||||
}
|
||||
|
||||
export class MissingRecordError extends Error {
|
||||
constructor(id, nValid) {
|
||||
super('missing record')
|
||||
|
||||
this.id = id
|
||||
this.nValid = nValid
|
||||
}
|
||||
}
|
||||
|
||||
export const NULL_ID = 'nullId'
|
||||
|
||||
const HASH_ALGORITHM_ID = '5'
|
||||
const createHash = (data, algorithmId = HASH_ALGORITHM_ID) =>
|
||||
`$${algorithmId}$$${hash(data, {
|
||||
algorithm: ID_TO_ALGORITHM[algorithmId],
|
||||
excludeKeys: key => key === 'id',
|
||||
})}`
|
||||
|
||||
export class AuditCore {
|
||||
constructor(storage) {
|
||||
assert.notStrictEqual(storage, undefined)
|
||||
this._storage = storage
|
||||
}
|
||||
|
||||
@defer
|
||||
async add($defer, subject, event, data) {
|
||||
const time = Date.now()
|
||||
$defer(await this._storage.acquireLock())
|
||||
return this._addUnsafe({
|
||||
data,
|
||||
event,
|
||||
subject,
|
||||
time,
|
||||
})
|
||||
}
|
||||
|
||||
async _addUnsafe({ data, event, subject, time }) {
|
||||
const storage = this._storage
|
||||
|
||||
// delete "undefined" properties and normalize data with JSON.stringify
|
||||
const record = JSON.parse(
|
||||
JSON.stringify({
|
||||
data,
|
||||
event,
|
||||
previousId: (await storage.getLastId()) ?? NULL_ID,
|
||||
subject,
|
||||
time,
|
||||
})
|
||||
)
|
||||
record.id = createHash(record)
|
||||
await storage.put(record)
|
||||
await storage.setLastId(record.id)
|
||||
return record
|
||||
}
|
||||
|
||||
async checkIntegrity(oldest, newest) {
|
||||
const storage = this._storage
|
||||
|
||||
// handle separated chains case
|
||||
if (newest !== (await storage.getLastId())) {
|
||||
let isNewestAccessible = false
|
||||
for await (const { id } of this.getFrom()) {
|
||||
if (id === newest) {
|
||||
isNewestAccessible = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if (!isNewestAccessible) {
|
||||
throw new MissingRecordError(newest, 0)
|
||||
}
|
||||
}
|
||||
|
||||
let nValid = 0
|
||||
while (newest !== oldest) {
|
||||
const record = await storage.get(newest)
|
||||
if (record === undefined) {
|
||||
throw new MissingRecordError(newest, nValid)
|
||||
}
|
||||
if (
|
||||
newest !== createHash(record, newest.slice(1, newest.indexOf('$', 1)))
|
||||
) {
|
||||
throw new AlteredRecordError(newest, nValid, record)
|
||||
}
|
||||
newest = record.previousId
|
||||
nValid++
|
||||
}
|
||||
return nValid
|
||||
}
|
||||
|
||||
async *getFrom(newest) {
|
||||
const storage = this._storage
|
||||
|
||||
let id = newest ?? (await storage.getLastId())
|
||||
if (id === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
let record
|
||||
while ((record = await storage.get(id)) !== undefined) {
|
||||
yield record
|
||||
id = record.previousId
|
||||
}
|
||||
}
|
||||
|
||||
async deleteFrom(newest) {
|
||||
assert.notStrictEqual(newest, undefined)
|
||||
for await (const { id } of this.getFrom(newest)) {
|
||||
await this._storage.del(id)
|
||||
}
|
||||
}
|
||||
|
||||
@defer
|
||||
async deleteRangeAndRewrite($defer, newest, oldest) {
|
||||
assert.notStrictEqual(newest, undefined)
|
||||
assert.notStrictEqual(oldest, undefined)
|
||||
|
||||
const storage = this._storage
|
||||
$defer(await storage.acquireLock())
|
||||
|
||||
assert.notStrictEqual(await storage.get(newest), undefined)
|
||||
const oldestRecord = await storage.get(oldest)
|
||||
assert.notStrictEqual(oldestRecord, undefined)
|
||||
|
||||
const lastId = await storage.getLastId()
|
||||
const recentRecords = []
|
||||
for await (const record of this.getFrom(lastId)) {
|
||||
if (record.id === newest) {
|
||||
break
|
||||
}
|
||||
|
||||
recentRecords.push(record)
|
||||
}
|
||||
|
||||
for await (const record of this.getFrom(newest)) {
|
||||
await storage.del(record.id)
|
||||
if (record.id === oldest) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
await storage.setLastId(oldestRecord.previousId)
|
||||
|
||||
for (const record of recentRecords) {
|
||||
try {
|
||||
await this._addUnsafe(record)
|
||||
await storage.del(record.id)
|
||||
} catch (error) {
|
||||
log.error(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
126
@xen-orchestra/audit-core/src/index.spec.js
Normal file
126
@xen-orchestra/audit-core/src/index.spec.js
Normal file
@@ -0,0 +1,126 @@
|
||||
/* eslint-env jest */
|
||||
|
||||
import {
|
||||
AlteredRecordError,
|
||||
AuditCore,
|
||||
MissingRecordError,
|
||||
NULL_ID,
|
||||
Storage,
|
||||
} from '.'
|
||||
|
||||
const asyncIteratorToArray = async asyncIterator => {
|
||||
const array = []
|
||||
for await (const entry of asyncIterator) {
|
||||
array.push(entry)
|
||||
}
|
||||
return array
|
||||
}
|
||||
|
||||
class DB extends Storage {
|
||||
constructor() {
|
||||
super()
|
||||
|
||||
this._db = new Map()
|
||||
this._lastId = undefined
|
||||
}
|
||||
|
||||
async put(record) {
|
||||
this._db.set(record.id, record)
|
||||
}
|
||||
|
||||
async setLastId(id) {
|
||||
this._lastId = id
|
||||
}
|
||||
|
||||
async getLastId() {
|
||||
return this._lastId
|
||||
}
|
||||
|
||||
async del(id) {
|
||||
this._db.delete(id)
|
||||
}
|
||||
|
||||
async get(id) {
|
||||
return this._db.get(id)
|
||||
}
|
||||
|
||||
_clear() {
|
||||
return this._db.clear()
|
||||
}
|
||||
}
|
||||
|
||||
const DATA = [
|
||||
[
|
||||
{
|
||||
name: 'subject0',
|
||||
},
|
||||
'event0',
|
||||
{},
|
||||
],
|
||||
[
|
||||
{
|
||||
name: 'subject1',
|
||||
},
|
||||
'event1',
|
||||
{},
|
||||
],
|
||||
[
|
||||
{
|
||||
name: 'subject2',
|
||||
},
|
||||
'event2',
|
||||
{},
|
||||
],
|
||||
]
|
||||
|
||||
const db = new DB()
|
||||
const auditCore = new AuditCore(db)
|
||||
const storeAuditRecords = async () => {
|
||||
await Promise.all(DATA.map(data => auditCore.add(...data)))
|
||||
const records = await asyncIteratorToArray(auditCore.getFrom())
|
||||
expect(records.length).toBe(DATA.length)
|
||||
return records
|
||||
}
|
||||
|
||||
describe('auditCore', () => {
|
||||
afterEach(() => db._clear())
|
||||
|
||||
it('detects that a record is missing', async () => {
|
||||
const [newestRecord, deletedRecord] = await storeAuditRecords()
|
||||
|
||||
const nValidRecords = await auditCore.checkIntegrity(
|
||||
NULL_ID,
|
||||
newestRecord.id
|
||||
)
|
||||
expect(nValidRecords).toBe(DATA.length)
|
||||
|
||||
await db.del(deletedRecord.id)
|
||||
await expect(
|
||||
auditCore.checkIntegrity(NULL_ID, newestRecord.id)
|
||||
).rejects.toEqual(new MissingRecordError(deletedRecord.id, 1))
|
||||
})
|
||||
|
||||
it('detects that a record has been altered', async () => {
|
||||
const [newestRecord, alteredRecord] = await storeAuditRecords()
|
||||
|
||||
alteredRecord.event = ''
|
||||
await db.put(alteredRecord)
|
||||
|
||||
await expect(
|
||||
auditCore.checkIntegrity(NULL_ID, newestRecord.id)
|
||||
).rejects.toEqual(
|
||||
new AlteredRecordError(alteredRecord.id, 1, alteredRecord)
|
||||
)
|
||||
})
|
||||
|
||||
it('confirms interval integrity after deletion of records outside of the interval', async () => {
|
||||
const [thirdRecord, secondRecord, firstRecord] = await storeAuditRecords()
|
||||
|
||||
await auditCore.deleteFrom(secondRecord.id)
|
||||
|
||||
expect(await db.get(firstRecord.id)).toBe(undefined)
|
||||
expect(await db.get(secondRecord.id)).toBe(undefined)
|
||||
|
||||
await auditCore.checkIntegrity(secondRecord.id, thirdRecord.id)
|
||||
})
|
||||
})
|
||||
26
@xen-orchestra/audit-core/src/specification.ts
Normal file
26
@xen-orchestra/audit-core/src/specification.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
class Storage {
|
||||
acquire: () => Promise<() => undefined>
|
||||
del: (id: string) => Promise<void>
|
||||
get: (id: string) => Promise<Record | void>
|
||||
getLastId: () => Promise<string | void>
|
||||
put: (record: Record) => Promise<void>
|
||||
setLastId: (id: string) => Promise<void>
|
||||
}
|
||||
|
||||
interface Record {
|
||||
data: object
|
||||
event: string
|
||||
id: string
|
||||
previousId: string
|
||||
subject: object
|
||||
time: number
|
||||
}
|
||||
|
||||
export class AuditCore {
|
||||
constructor(storage: Storage) { }
|
||||
public add(subject: any, event: string, data: any): Promise<Record> { }
|
||||
public checkIntegrity(oldest: string, newest: string): Promise<number> { }
|
||||
public getFrom(newest?: string): AsyncIterator { }
|
||||
public deleteFrom(newest: string): Promise<void> { }
|
||||
public deleteRangeAndRewrite(newest: string, oldest: string): Promise<void> { }
|
||||
}
|
||||
18
@xen-orchestra/babel-config/README.md
Normal file
18
@xen-orchestra/babel-config/README.md
Normal file
@@ -0,0 +1,18 @@
|
||||
<!-- DO NOT EDIT MANUALLY, THIS FILE HAS BEEN GENERATED -->
|
||||
|
||||
# @xen-orchestra/babel-config
|
||||
|
||||
## Contributions
|
||||
|
||||
Contributions are _very_ welcomed, either on the documentation or on
|
||||
the code.
|
||||
|
||||
You may:
|
||||
|
||||
- report any [issue](https://github.com/vatesfr/xen-orchestra/issues)
|
||||
you've encountered;
|
||||
- fork and create a pull request.
|
||||
|
||||
## License
|
||||
|
||||
[AGPL-3.0-or-later](https://spdx.org/licenses/AGPL-3.0-or-later) © [Vates SAS](https://vates.fr)
|
||||
0
@xen-orchestra/babel-config/USAGE.md
Normal file
0
@xen-orchestra/babel-config/USAGE.md
Normal file
@@ -32,7 +32,6 @@ const configs = {
|
||||
}
|
||||
return { browsers: pkg.browserslist, node }
|
||||
})(),
|
||||
useBuiltIns: '@babel/polyfill' in (pkg.dependencies || {}) && 'usage',
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -52,7 +51,7 @@ const pluginsOrder = [
|
||||
'@babel/plugin-proposal-class-properties',
|
||||
]
|
||||
|
||||
module.exports = function(pkg, plugins, presets) {
|
||||
module.exports = function (pkg, plugins, presets) {
|
||||
plugins === undefined && (plugins = {})
|
||||
presets === undefined && (presets = {})
|
||||
|
||||
|
||||
@@ -11,5 +11,10 @@
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
},
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"author": {
|
||||
"name": "Vates SAS",
|
||||
"url": "https://vates.fr"
|
||||
}
|
||||
}
|
||||
|
||||
28
@xen-orchestra/backups-cli/README.md
Normal file
28
@xen-orchestra/backups-cli/README.md
Normal file
@@ -0,0 +1,28 @@
|
||||
<!-- DO NOT EDIT MANUALLY, THIS FILE HAS BEEN GENERATED -->
|
||||
|
||||
# @xen-orchestra/backups-cli
|
||||
|
||||
[](https://npmjs.org/package/@xen-orchestra/backups-cli)  [](https://bundlephobia.com/result?p=@xen-orchestra/backups-cli) [](https://npmjs.org/package/@xen-orchestra/backups-cli)
|
||||
|
||||
## Install
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/@xen-orchestra/backups-cli):
|
||||
|
||||
```
|
||||
> npm install --global @xen-orchestra/backups-cli
|
||||
```
|
||||
|
||||
## Contributions
|
||||
|
||||
Contributions are _very_ welcomed, either on the documentation or on
|
||||
the code.
|
||||
|
||||
You may:
|
||||
|
||||
- report any [issue](https://github.com/vatesfr/xen-orchestra/issues)
|
||||
you've encountered;
|
||||
- fork and create a pull request.
|
||||
|
||||
## License
|
||||
|
||||
[AGPL-3.0-or-later](https://spdx.org/licenses/AGPL-3.0-or-later) © [Vates SAS](https://vates.fr)
|
||||
0
@xen-orchestra/backups-cli/USAGE.md
Normal file
0
@xen-orchestra/backups-cli/USAGE.md
Normal file
@@ -3,7 +3,7 @@ const getopts = require('getopts')
|
||||
const { version } = require('./package.json')
|
||||
|
||||
module.exports = commands =>
|
||||
async function(args, prefix) {
|
||||
async function (args, prefix) {
|
||||
const opts = getopts(args, {
|
||||
alias: {
|
||||
help: 'h',
|
||||
|
||||
@@ -3,6 +3,17 @@ const { dirname } = require('path')
|
||||
const fs = require('promise-toolbox/promisifyAll')(require('fs'))
|
||||
module.exports = fs
|
||||
|
||||
fs.getSize = path =>
|
||||
fs.stat(path).then(
|
||||
_ => _.size,
|
||||
error => {
|
||||
if (error.code === 'ENOENT') {
|
||||
return 0
|
||||
}
|
||||
throw error
|
||||
}
|
||||
)
|
||||
|
||||
fs.mktree = async function mkdirp(path) {
|
||||
try {
|
||||
await fs.mkdir(path)
|
||||
@@ -34,14 +45,15 @@ fs.readdir2 = path =>
|
||||
return entries
|
||||
},
|
||||
error => {
|
||||
if (
|
||||
error != null &&
|
||||
(error.code === 'ENOENT' || error.code === 'ENOTDIR')
|
||||
) {
|
||||
const { code } = error
|
||||
if (code === 'ENOENT') {
|
||||
// do nothing
|
||||
} else if (code === 'ENOTDIR') {
|
||||
console.warn('WARN: readdir(%s)', path, error)
|
||||
return []
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
throw error
|
||||
return []
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -8,12 +8,13 @@ let force
|
||||
const assert = require('assert')
|
||||
const flatten = require('lodash/flatten')
|
||||
const getopts = require('getopts')
|
||||
const isValidXva = require('@xen-orchestra/backups/isValidXva')
|
||||
const limitConcurrency = require('limit-concurrency-decorator').default
|
||||
const lockfile = require('proper-lockfile')
|
||||
const pipe = require('promise-toolbox/pipe')
|
||||
const { default: Vhd } = require('vhd-lib')
|
||||
const { default: Vhd, mergeVhd } = require('vhd-lib')
|
||||
const { dirname, resolve } = require('path')
|
||||
const { DISK_TYPE_DIFFERENCING } = require('vhd-lib/dist/_constants')
|
||||
const { isValidXva } = require('@xen-orchestra/backups/isValidXva')
|
||||
|
||||
const asyncMap = require('../_asyncMap')
|
||||
const fs = require('../_fs')
|
||||
@@ -26,10 +27,10 @@ const handler = require('@xen-orchestra/fs').getHandler({ url: 'file://' })
|
||||
//
|
||||
// the whole chain will be merged into parent, parent will be renamed to child
|
||||
// and all the others will deleted
|
||||
async function mergeVhdChain(chain) {
|
||||
const mergeVhdChain = limitConcurrency(1)(async function mergeVhdChain(chain) {
|
||||
assert(chain.length >= 2)
|
||||
|
||||
const child = chain[0]
|
||||
let child = chain[0]
|
||||
const parent = chain[chain.length - 1]
|
||||
const children = chain.slice(0, -1).reverse()
|
||||
|
||||
@@ -46,15 +47,36 @@ async function mergeVhdChain(chain) {
|
||||
// `mergeVhd` does not work with a stream, either
|
||||
// - make it accept a stream
|
||||
// - or create synthetic VHD which is not a stream
|
||||
return console.warn('TODO: implement merge')
|
||||
// await mergeVhd(
|
||||
// handler,
|
||||
// parent,
|
||||
// handler,
|
||||
// children.length === 1
|
||||
// ? child
|
||||
// : await createSyntheticStream(handler, children)
|
||||
// )
|
||||
if (children.length !== 1) {
|
||||
console.warn('TODO: implement merging multiple children')
|
||||
children.length = 1
|
||||
child = children[0]
|
||||
}
|
||||
|
||||
let done, total
|
||||
const handle = setInterval(() => {
|
||||
if (done !== undefined) {
|
||||
console.log('merging %s: %s/%s', child, done, total)
|
||||
}
|
||||
}, 10e3)
|
||||
|
||||
await mergeVhd(
|
||||
handler,
|
||||
parent,
|
||||
handler,
|
||||
child,
|
||||
// children.length === 1
|
||||
// ? child
|
||||
// : await createSyntheticStream(handler, children),
|
||||
{
|
||||
onProgress({ done: d, total: t }) {
|
||||
done = d
|
||||
total = t
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
clearInterval(handle)
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
@@ -66,7 +88,7 @@ async function mergeVhdChain(chain) {
|
||||
return force && handler.unlink(child)
|
||||
}),
|
||||
])
|
||||
}
|
||||
})
|
||||
|
||||
const listVhds = pipe([
|
||||
vmDir => vmDir + '/vdis',
|
||||
@@ -152,11 +174,12 @@ async function handleVm(vmDir) {
|
||||
await Promise.all(deletions)
|
||||
}
|
||||
|
||||
const [jsons, xvas] = await fs
|
||||
const [jsons, xvas, xvaSums] = await fs
|
||||
.readdir2(vmDir)
|
||||
.then(entries => [
|
||||
entries.filter(_ => _.endsWith('.json')),
|
||||
new Set(entries.filter(_ => _.endsWith('.xva'))),
|
||||
entries.filter(_ => _.endsWith('.xva.cheksum')),
|
||||
])
|
||||
|
||||
await asyncMap(xvas, async path => {
|
||||
@@ -274,6 +297,15 @@ async function handleVm(vmDir) {
|
||||
console.warn('')
|
||||
return force && handler.unlink(path)
|
||||
}),
|
||||
asyncMap(xvaSums, path => {
|
||||
// no need to handle checksums for XVAs deleted by the script, they will be handled by `unlink()`
|
||||
if (!xvas.has(path.slice(0, -'.checksum'.length))) {
|
||||
console.warn('Unused XVA checksum', path)
|
||||
force && console.warn(' deleting…')
|
||||
console.warn('')
|
||||
return force && handler.unlink(path)
|
||||
}
|
||||
}),
|
||||
])
|
||||
}
|
||||
|
||||
|
||||
58
@xen-orchestra/backups-cli/commands/info.js
Normal file
58
@xen-orchestra/backups-cli/commands/info.js
Normal file
@@ -0,0 +1,58 @@
|
||||
const groupBy = require('lodash/groupBy')
|
||||
const { createHash } = require('crypto')
|
||||
const { dirname, resolve } = require('path')
|
||||
|
||||
const asyncMap = require('../_asyncMap')
|
||||
const { readdir2, readFile, getSize } = require('../_fs')
|
||||
|
||||
const sha512 = str => createHash('sha512').update(str).digest('hex')
|
||||
const sum = values => values.reduce((a, b) => a + b)
|
||||
|
||||
module.exports = async function info(vmDirs) {
|
||||
const jsonFiles = (
|
||||
await asyncMap(vmDirs, async vmDir =>
|
||||
(await readdir2(vmDir)).filter(_ => _.endsWith('.json'))
|
||||
)
|
||||
).flat()
|
||||
|
||||
const hashes = { __proto__: null }
|
||||
|
||||
const info = (
|
||||
await asyncMap(jsonFiles, async jsonFile => {
|
||||
try {
|
||||
const jsonDir = dirname(jsonFile)
|
||||
const json = await readFile(jsonFile)
|
||||
|
||||
const hash = sha512(json)
|
||||
if (hash in hashes) {
|
||||
console.log(jsonFile, 'duplicate of', hashes[hash])
|
||||
return
|
||||
}
|
||||
hashes[hash] = jsonFile
|
||||
|
||||
const metadata = JSON.parse(json)
|
||||
|
||||
return {
|
||||
jsonDir,
|
||||
jsonFile,
|
||||
metadata,
|
||||
size:
|
||||
json.length +
|
||||
(await (metadata.mode === 'delta'
|
||||
? asyncMap(Object.values(metadata.vhds), _ =>
|
||||
getSize(resolve(jsonDir, _))
|
||||
).then(sum)
|
||||
: getSize(resolve(jsonDir, metadata.xva)))),
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(jsonFile, error)
|
||||
}
|
||||
})
|
||||
).filter(_ => _ !== undefined)
|
||||
const byJobs = groupBy(info, 'metadata.jobId')
|
||||
Object.keys(byJobs)
|
||||
.sort()
|
||||
.forEach(jobId => {
|
||||
console.log(jobId, sum(byJobs[jobId].map(_ => _.size)))
|
||||
})
|
||||
}
|
||||
@@ -13,6 +13,12 @@ require('./_composeCommands')({
|
||||
},
|
||||
usage: 'xo-vm-backups <field path>',
|
||||
},
|
||||
info: {
|
||||
get main() {
|
||||
return require('./commands/info')
|
||||
},
|
||||
usage: 'xo-vm-backups/*',
|
||||
},
|
||||
})(process.argv.slice(2), 'xo-backups').catch(error => {
|
||||
console.error('main', error)
|
||||
process.exitCode = 1
|
||||
|
||||
@@ -3,16 +3,18 @@
|
||||
"bin": {
|
||||
"xo-backups": "index.js"
|
||||
},
|
||||
"preferGlobal": true,
|
||||
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
|
||||
"dependencies": {
|
||||
"@xen-orchestra/backups": "^0.0.0",
|
||||
"@xen-orchestra/fs": "^0.10.2",
|
||||
"@xen-orchestra/backups": "^0.1.1",
|
||||
"@xen-orchestra/fs": "^0.11.1",
|
||||
"filenamify": "^4.1.0",
|
||||
"getopts": "^2.2.5",
|
||||
"limit-concurrency-decorator": "^0.4.0",
|
||||
"lodash": "^4.17.15",
|
||||
"promise-toolbox": "^0.15.0",
|
||||
"proper-lockfile": "^4.1.1",
|
||||
"vhd-lib": "^0.7.2"
|
||||
"vhd-lib": "^0.8.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=7.10.1"
|
||||
@@ -31,5 +33,10 @@
|
||||
"scripts": {
|
||||
"postversion": "npm publish --access public"
|
||||
},
|
||||
"version": "0.0.0"
|
||||
"version": "0.2.1",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"author": {
|
||||
"name": "Vates SAS",
|
||||
"url": "https://vates.fr"
|
||||
}
|
||||
}
|
||||
|
||||
28
@xen-orchestra/backups/README.md
Normal file
28
@xen-orchestra/backups/README.md
Normal file
@@ -0,0 +1,28 @@
|
||||
<!-- DO NOT EDIT MANUALLY, THIS FILE HAS BEEN GENERATED -->
|
||||
|
||||
# @xen-orchestra/backups
|
||||
|
||||
[](https://npmjs.org/package/@xen-orchestra/backups)  [](https://bundlephobia.com/result?p=@xen-orchestra/backups) [](https://npmjs.org/package/@xen-orchestra/backups)
|
||||
|
||||
## Install
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/@xen-orchestra/backups):
|
||||
|
||||
```
|
||||
> npm install --save @xen-orchestra/backups
|
||||
```
|
||||
|
||||
## Contributions
|
||||
|
||||
Contributions are _very_ welcomed, either on the documentation or on
|
||||
the code.
|
||||
|
||||
You may:
|
||||
|
||||
- report any [issue](https://github.com/vatesfr/xen-orchestra/issues)
|
||||
you've encountered;
|
||||
- fork and create a pull request.
|
||||
|
||||
## License
|
||||
|
||||
[AGPL-3.0-or-later](https://spdx.org/licenses/AGPL-3.0-or-later) © [Vates SAS](https://vates.fr)
|
||||
0
@xen-orchestra/backups/USAGE.md
Normal file
0
@xen-orchestra/backups/USAGE.md
Normal file
30
@xen-orchestra/backups/extractIdsFromSimplePattern.js
Normal file
30
@xen-orchestra/backups/extractIdsFromSimplePattern.js
Normal file
@@ -0,0 +1,30 @@
|
||||
function extractIdsFromSimplePattern(pattern) {
|
||||
if (pattern === undefined) {
|
||||
return []
|
||||
}
|
||||
|
||||
if (pattern !== null && typeof pattern === 'object') {
|
||||
let keys = Object.keys(pattern)
|
||||
|
||||
if (keys.length === 1 && keys[0] === 'id') {
|
||||
pattern = pattern.id
|
||||
if (typeof pattern === 'string') {
|
||||
return [pattern]
|
||||
}
|
||||
if (pattern !== null && typeof pattern === 'object') {
|
||||
keys = Object.keys(pattern)
|
||||
if (
|
||||
keys.length === 1 &&
|
||||
keys[0] === '__or' &&
|
||||
Array.isArray((pattern = pattern.__or)) &&
|
||||
pattern.every(_ => typeof _ === 'string')
|
||||
) {
|
||||
return pattern
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('invalid pattern')
|
||||
}
|
||||
exports.extractIdsFromSimplePattern = extractIdsFromSimplePattern
|
||||
6
@xen-orchestra/backups/filenameDate.js
Normal file
6
@xen-orchestra/backups/filenameDate.js
Normal file
@@ -0,0 +1,6 @@
|
||||
const { utcFormat, utcParse } = require('d3-time-format')
|
||||
|
||||
// Format a date in ISO 8601 in a safe way to be used in filenames
|
||||
// (even on Windows).
|
||||
exports.formatFilenameDate = utcFormat('%Y%m%dT%H%M%SZ')
|
||||
exports.parseFilenameDate = utcParse('%Y%m%dT%H%M%SZ')
|
||||
7
@xen-orchestra/backups/getOldEntries.js
Normal file
7
@xen-orchestra/backups/getOldEntries.js
Normal file
@@ -0,0 +1,7 @@
|
||||
// returns all entries but the last retention-th
|
||||
exports.getOldEntries = (retention, entries) =>
|
||||
entries === undefined
|
||||
? []
|
||||
: retention > 0
|
||||
? entries.slice(0, -retention)
|
||||
: entries
|
||||
@@ -5,7 +5,7 @@ const isGzipFile = async fd => {
|
||||
// https://tools.ietf.org/html/rfc1952.html#page-5
|
||||
const magicNumber = Buffer.allocUnsafe(2)
|
||||
assert.strictEqual(
|
||||
await fs.read(fd, magicNumber, 0, magicNumber.length, 0),
|
||||
(await fs.read(fd, magicNumber, 0, magicNumber.length, 0)).bytesRead,
|
||||
magicNumber.length
|
||||
)
|
||||
return magicNumber[0] === 31 && magicNumber[1] === 139
|
||||
@@ -31,7 +31,7 @@ const isValidTar = async (size, fd) => {
|
||||
|
||||
const buf = Buffer.allocUnsafe(1024)
|
||||
assert.strictEqual(
|
||||
await fs.read(fd, buf, 0, buf.length, size - buf.length),
|
||||
(await fs.read(fd, buf, 0, buf.length, size - buf.length)).bytesRead,
|
||||
buf.length
|
||||
)
|
||||
return buf.every(_ => _ === 0)
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"type": "git",
|
||||
"url": "https://github.com/vatesfr/xen-orchestra.git"
|
||||
},
|
||||
"version": "0.0.0",
|
||||
"version": "0.1.1",
|
||||
"engines": {
|
||||
"node": ">=8.10"
|
||||
},
|
||||
@@ -16,6 +16,12 @@
|
||||
"postversion": "npm publish --access public"
|
||||
},
|
||||
"dependencies": {
|
||||
"fs-extra": "^8.1.0"
|
||||
"d3-time-format": "^3.0.0",
|
||||
"fs-extra": "^9.0.0"
|
||||
},
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"author": {
|
||||
"name": "Vates SAS",
|
||||
"url": "https://vates.fr"
|
||||
}
|
||||
}
|
||||
|
||||
11
@xen-orchestra/backups/watchStreamSize.js
Normal file
11
@xen-orchestra/backups/watchStreamSize.js
Normal file
@@ -0,0 +1,11 @@
|
||||
exports.watchStreamSize = stream => {
|
||||
const container = { size: 0 }
|
||||
const isPaused = stream.isPaused()
|
||||
stream.on('data', data => {
|
||||
container.size += data.length
|
||||
})
|
||||
if (isPaused) {
|
||||
stream.pause()
|
||||
}
|
||||
return container
|
||||
}
|
||||
28
@xen-orchestra/cr-seed-cli/README.md
Normal file
28
@xen-orchestra/cr-seed-cli/README.md
Normal file
@@ -0,0 +1,28 @@
|
||||
<!-- DO NOT EDIT MANUALLY, THIS FILE HAS BEEN GENERATED -->
|
||||
|
||||
# @xen-orchestra/cr-seed-cli
|
||||
|
||||
[](https://npmjs.org/package/@xen-orchestra/cr-seed-cli)  [](https://bundlephobia.com/result?p=@xen-orchestra/cr-seed-cli) [](https://npmjs.org/package/@xen-orchestra/cr-seed-cli)
|
||||
|
||||
## Install
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/@xen-orchestra/cr-seed-cli):
|
||||
|
||||
```
|
||||
> npm install --global @xen-orchestra/cr-seed-cli
|
||||
```
|
||||
|
||||
## Contributions
|
||||
|
||||
Contributions are _very_ welcomed, either on the documentation or on
|
||||
the code.
|
||||
|
||||
You may:
|
||||
|
||||
- report any [issue](https://github.com/vatesfr/xen-orchestra/issues)
|
||||
you've encountered;
|
||||
- fork and create a pull request.
|
||||
|
||||
## License
|
||||
|
||||
[AGPL-3.0-or-later](https://spdx.org/licenses/AGPL-3.0-or-later) © [Vates SAS](https://vates.fr)
|
||||
0
@xen-orchestra/cr-seed-cli/USAGE.md
Normal file
0
@xen-orchestra/cr-seed-cli/USAGE.md
Normal file
@@ -5,7 +5,7 @@ const { NULL_REF, Xapi } = require('xen-api')
|
||||
|
||||
const pkg = require('./package.json')
|
||||
|
||||
Xapi.prototype.getVmDisks = async function(vm) {
|
||||
Xapi.prototype.getVmDisks = async function (vm) {
|
||||
const disks = { __proto__: null }
|
||||
await Promise.all([
|
||||
...vm.VBDs.map(async vbdRef => {
|
||||
|
||||
@@ -15,11 +15,17 @@
|
||||
"bin": {
|
||||
"xo-cr-seed": "./index.js"
|
||||
},
|
||||
"preferGlobal": true,
|
||||
"dependencies": {
|
||||
"golike-defer": "^0.4.1",
|
||||
"xen-api": "^0.27.3"
|
||||
"xen-api": "^0.29.0"
|
||||
},
|
||||
"scripts": {
|
||||
"postversion": "npm publish"
|
||||
},
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"author": {
|
||||
"name": "Vates SAS",
|
||||
"url": "https://vates.fr"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
# @xen-orchestra/cron [](https://travis-ci.org/vatesfr/xen-orchestra)
|
||||
<!-- DO NOT EDIT MANUALLY, THIS FILE HAS BEEN GENERATED -->
|
||||
|
||||
# @xen-orchestra/cron
|
||||
|
||||
[](https://npmjs.org/package/@xen-orchestra/cron)  [](https://bundlephobia.com/result?p=@xen-orchestra/cron) [](https://npmjs.org/package/@xen-orchestra/cron)
|
||||
|
||||
> Focused, well maintained, cron parser/scheduler
|
||||
|
||||
@@ -10,6 +14,8 @@ Installation of the [npm package](https://npmjs.org/package/@xen-orchestra/cron)
|
||||
> npm install --save @xen-orchestra/cron
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Pattern syntax
|
||||
|
||||
```
|
||||
@@ -109,25 +115,6 @@ job.start()
|
||||
job.stop()
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
```
|
||||
# Install dependencies
|
||||
> yarn
|
||||
|
||||
# Run the tests
|
||||
> yarn test
|
||||
|
||||
# Continuously compile
|
||||
> yarn dev
|
||||
|
||||
# Continuously run the tests
|
||||
> yarn dev-test
|
||||
|
||||
# Build for production (automatically called by npm install)
|
||||
> yarn build
|
||||
```
|
||||
|
||||
## Contributions
|
||||
|
||||
Contributions are _very_ welcomed, either on the documentation or on
|
||||
@@ -141,4 +128,4 @@ You may:
|
||||
|
||||
## License
|
||||
|
||||
ISC © [Vates SAS](https://vates.fr)
|
||||
[ISC](https://spdx.org/licenses/ISC) © [Vates SAS](https://vates.fr)
|
||||
|
||||
98
@xen-orchestra/cron/USAGE.md
Normal file
98
@xen-orchestra/cron/USAGE.md
Normal file
@@ -0,0 +1,98 @@
|
||||
### Pattern syntax
|
||||
|
||||
```
|
||||
<minute> <hour> <day of month> <month> <day of week>
|
||||
```
|
||||
|
||||
Each entry can be:
|
||||
|
||||
- a single value
|
||||
- a range (`0-23` or `*/2`)
|
||||
- a list of values/ranges (`1,8-12`)
|
||||
|
||||
A wildcard (`*`) can be used as a shortcut for the whole range
|
||||
(`first-last`).
|
||||
|
||||
Step values can be used in conjunctions with ranges. For instance,
|
||||
`1-7/2` is the same as `1,3,5,7`.
|
||||
|
||||
| Field | Allowed values |
|
||||
| ---------------- | ------------------------------------------------------------------ |
|
||||
| minute | 0-59 |
|
||||
| hour | 0-23 |
|
||||
| day of the month | 1-31 or 3-letter names (`jan`, `feb`, …) |
|
||||
| month | 0-11 |
|
||||
| day of week | 0-7 (0 and 7 both mean Sunday) or 3-letter names (`mon`, `tue`, …) |
|
||||
|
||||
> Note: the month range is 0-11 to be compatible with
|
||||
> [cron](https://github.com/kelektiv/node-cron), it does not appear to
|
||||
> be very standard though.
|
||||
|
||||
### API
|
||||
|
||||
`createSchedule(pattern: string, zone: string = 'utc'): Schedule`
|
||||
|
||||
> Create a new schedule.
|
||||
|
||||
- `pattern`: the pattern to use, see [the syntax](#pattern-syntax)
|
||||
- `zone`: the timezone to use, use `'local'` for the local timezone
|
||||
|
||||
```js
|
||||
import { createSchedule } from '@xen-orchestra/cron'
|
||||
|
||||
const schedule = createSchedule('0 0 * * sun', 'America/New_York')
|
||||
```
|
||||
|
||||
`Schedule#createJob(fn: Function): Job`
|
||||
|
||||
> Create a new job from this schedule.
|
||||
|
||||
- `fn`: function to execute, if it returns a promise, it will be
|
||||
awaited before scheduling the next run.
|
||||
|
||||
```js
|
||||
const job = schedule.createJob(() => {
|
||||
console.log(new Date())
|
||||
})
|
||||
```
|
||||
|
||||
`Schedule#next(n: number): Array<Date>`
|
||||
|
||||
> Returns the next dates matching this schedule.
|
||||
|
||||
- `n`: number of dates to return
|
||||
|
||||
```js
|
||||
schedule.next(2)
|
||||
// [ 2018-02-11T05:00:00.000Z, 2018-02-18T05:00:00.000Z ]
|
||||
```
|
||||
|
||||
`Schedule#startJob(fn: Function): () => void`
|
||||
|
||||
> Start a new job from this schedule and return a function to stop it.
|
||||
|
||||
- `fn`: function to execute, if it returns a promise, it will be
|
||||
awaited before scheduling the next run.
|
||||
|
||||
```js
|
||||
const stopJob = schedule.startJob(() => {
|
||||
console.log(new Date())
|
||||
})
|
||||
stopJob()
|
||||
```
|
||||
|
||||
`Job#start(): void`
|
||||
|
||||
> Start this job.
|
||||
|
||||
```js
|
||||
job.start()
|
||||
```
|
||||
|
||||
`Job#stop(): void`
|
||||
|
||||
> Stop this job.
|
||||
|
||||
```js
|
||||
job.stop()
|
||||
```
|
||||
@@ -23,8 +23,8 @@
|
||||
"url": "https://github.com/vatesfr/xen-orchestra.git"
|
||||
},
|
||||
"author": {
|
||||
"name": "Julien Fontanet",
|
||||
"email": "julien.fontanet@isonoe.net"
|
||||
"name": "Vates SAS",
|
||||
"url": "https://vates.fr"
|
||||
},
|
||||
"preferGlobal": false,
|
||||
"main": "dist/",
|
||||
@@ -47,7 +47,7 @@
|
||||
"@babel/core": "^7.0.0",
|
||||
"@babel/preset-env": "^7.0.0",
|
||||
"@babel/preset-flow": "^7.0.0",
|
||||
"cross-env": "^6.0.3",
|
||||
"cross-env": "^7.0.2",
|
||||
"rimraf": "^3.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# @xen-orchestra/defined [](https://travis-ci.org/${pkg.shortGitHubPath})
|
||||
<!-- DO NOT EDIT MANUALLY, THIS FILE HAS BEEN GENERATED -->
|
||||
|
||||
> ${pkg.description}
|
||||
# @xen-orchestra/defined
|
||||
|
||||
[](https://npmjs.org/package/@xen-orchestra/defined)  [](https://bundlephobia.com/result?p=@xen-orchestra/defined) [](https://npmjs.org/package/@xen-orchestra/defined)
|
||||
|
||||
## Install
|
||||
|
||||
@@ -10,29 +12,6 @@ Installation of the [npm package](https://npmjs.org/package/@xen-orchestra/defin
|
||||
> npm install --save @xen-orchestra/defined
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
**TODO**
|
||||
|
||||
## Development
|
||||
|
||||
```
|
||||
# Install dependencies
|
||||
> yarn
|
||||
|
||||
# Run the tests
|
||||
> yarn test
|
||||
|
||||
# Continuously compile
|
||||
> yarn dev
|
||||
|
||||
# Continuously run the tests
|
||||
> yarn dev-test
|
||||
|
||||
# Build for production (automatically called by npm install)
|
||||
> yarn build
|
||||
```
|
||||
|
||||
## Contributions
|
||||
|
||||
Contributions are _very_ welcomed, either on the documentation or on
|
||||
@@ -46,4 +25,4 @@ You may:
|
||||
|
||||
## License
|
||||
|
||||
ISC © [Vates SAS](https://vates.fr)
|
||||
[ISC](https://spdx.org/licenses/ISC) © [Vates SAS](https://vates.fr)
|
||||
|
||||
0
@xen-orchestra/defined/USAGE.md
Normal file
0
@xen-orchestra/defined/USAGE.md
Normal file
@@ -13,8 +13,8 @@
|
||||
"url": "https://github.com/vatesfr/xen-orchestra.git"
|
||||
},
|
||||
"author": {
|
||||
"name": "Julien Fontanet",
|
||||
"email": "julien.fontanet@vates.fr"
|
||||
"name": "Vates SAS",
|
||||
"url": "https://vates.fr"
|
||||
},
|
||||
"preferGlobal": false,
|
||||
"main": "dist/",
|
||||
@@ -35,7 +35,7 @@
|
||||
"@babel/preset-env": "^7.0.0",
|
||||
"@babel/preset-flow": "^7.0.0",
|
||||
"babel-plugin-lodash": "^3.3.2",
|
||||
"cross-env": "^6.0.3",
|
||||
"cross-env": "^7.0.2",
|
||||
"rimraf": "^3.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# @xen-orchestra/emit-async [](https://travis-ci.org/${pkg.shortGitHubPath})
|
||||
<!-- DO NOT EDIT MANUALLY, THIS FILE HAS BEEN GENERATED -->
|
||||
|
||||
> ${pkg.description}
|
||||
# @xen-orchestra/emit-async
|
||||
|
||||
[](https://npmjs.org/package/@xen-orchestra/emit-async)  [](https://bundlephobia.com/result?p=@xen-orchestra/emit-async) [](https://npmjs.org/package/@xen-orchestra/emit-async)
|
||||
|
||||
## Install
|
||||
|
||||
@@ -19,7 +21,7 @@ import emitAsync from '@xen-orchestra/emit-async'
|
||||
const ee = new EE()
|
||||
ee.emitAsync = emitAsync
|
||||
|
||||
ee.on('start', async function() {
|
||||
ee.on('start', async function () {
|
||||
// whatever
|
||||
})
|
||||
|
||||
@@ -39,25 +41,6 @@ await ee.emitAsync(
|
||||
)
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
```
|
||||
# Install dependencies
|
||||
> yarn
|
||||
|
||||
# Run the tests
|
||||
> yarn test
|
||||
|
||||
# Continuously compile
|
||||
> yarn dev
|
||||
|
||||
# Continuously run the tests
|
||||
> yarn dev-test
|
||||
|
||||
# Build for production (automatically called by npm install)
|
||||
> yarn build
|
||||
```
|
||||
|
||||
## Contributions
|
||||
|
||||
Contributions are _very_ welcomed, either on the documentation or on
|
||||
@@ -71,4 +54,4 @@ You may:
|
||||
|
||||
## License
|
||||
|
||||
ISC © [Vates SAS](https://vates.fr)
|
||||
[ISC](https://spdx.org/licenses/ISC) © [Vates SAS](https://vates.fr)
|
||||
|
||||
26
@xen-orchestra/emit-async/USAGE.md
Normal file
26
@xen-orchestra/emit-async/USAGE.md
Normal file
@@ -0,0 +1,26 @@
|
||||
```js
|
||||
import EE from 'events'
|
||||
import emitAsync from '@xen-orchestra/emit-async'
|
||||
|
||||
const ee = new EE()
|
||||
ee.emitAsync = emitAsync
|
||||
|
||||
ee.on('start', async function () {
|
||||
// whatever
|
||||
})
|
||||
|
||||
// similar to EventEmmiter#emit() but returns a promise which resolves when all
|
||||
// listeners have resolved
|
||||
await ee.emitAsync('start')
|
||||
|
||||
// by default, it will rejects as soon as one listener reject, you can customise
|
||||
// error handling though:
|
||||
await ee.emitAsync(
|
||||
{
|
||||
onError(error) {
|
||||
console.warn(error)
|
||||
},
|
||||
},
|
||||
'start'
|
||||
)
|
||||
```
|
||||
@@ -13,8 +13,8 @@
|
||||
"url": "https://github.com/vatesfr/xen-orchestra.git"
|
||||
},
|
||||
"author": {
|
||||
"name": "Julien Fontanet",
|
||||
"email": "julien.fontanet@vates.fr"
|
||||
"name": "Vates SAS",
|
||||
"url": "https://vates.fr"
|
||||
},
|
||||
"preferGlobal": false,
|
||||
"main": "dist/",
|
||||
@@ -34,7 +34,7 @@
|
||||
"@babel/core": "^7.0.0",
|
||||
"@babel/preset-env": "^7.0.0",
|
||||
"babel-plugin-lodash": "^3.3.2",
|
||||
"cross-env": "^6.0.3",
|
||||
"cross-env": "^7.0.2",
|
||||
"rimraf": "^3.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
30
@xen-orchestra/fs/README.md
Normal file
30
@xen-orchestra/fs/README.md
Normal file
@@ -0,0 +1,30 @@
|
||||
<!-- DO NOT EDIT MANUALLY, THIS FILE HAS BEEN GENERATED -->
|
||||
|
||||
# @xen-orchestra/fs
|
||||
|
||||
[](https://npmjs.org/package/@xen-orchestra/fs)  [](https://bundlephobia.com/result?p=@xen-orchestra/fs) [](https://npmjs.org/package/@xen-orchestra/fs)
|
||||
|
||||
> The File System for Xen Orchestra backups.
|
||||
|
||||
## Install
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/@xen-orchestra/fs):
|
||||
|
||||
```
|
||||
> npm install --global @xen-orchestra/fs
|
||||
```
|
||||
|
||||
## Contributions
|
||||
|
||||
Contributions are _very_ welcomed, either on the documentation or on
|
||||
the code.
|
||||
|
||||
You may:
|
||||
|
||||
- report any [issue](https://github.com/vatesfr/xen-orchestra/issues)
|
||||
you've encountered;
|
||||
- fork and create a pull request.
|
||||
|
||||
## License
|
||||
|
||||
[AGPL-3.0-or-later](https://spdx.org/licenses/AGPL-3.0-or-later) © [Vates SAS](https://vates.fr)
|
||||
0
@xen-orchestra/fs/USAGE.md
Normal file
0
@xen-orchestra/fs/USAGE.md
Normal file
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"private": false,
|
||||
"name": "@xen-orchestra/fs",
|
||||
"version": "0.10.2",
|
||||
"license": "AGPL-3.0",
|
||||
"version": "0.11.1",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"description": "The File System for Xen Orchestra backups.",
|
||||
"keywords": [],
|
||||
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/fs",
|
||||
@@ -25,17 +25,18 @@
|
||||
"@marsaud/smb2": "^0.15.0",
|
||||
"@sindresorhus/df": "^3.1.1",
|
||||
"@xen-orchestra/async-map": "^0.0.0",
|
||||
"aws-sdk": "^2.686.0",
|
||||
"decorator-synchronized": "^0.5.0",
|
||||
"execa": "^3.2.0",
|
||||
"fs-extra": "^8.0.1",
|
||||
"get-stream": "^5.1.0",
|
||||
"execa": "^4.0.2",
|
||||
"fs-extra": "^9.0.0",
|
||||
"get-stream": "^6.0.0",
|
||||
"limit-concurrency-decorator": "^0.4.0",
|
||||
"lodash": "^4.17.4",
|
||||
"promise-toolbox": "^0.15.0",
|
||||
"readable-stream": "^3.0.6",
|
||||
"through2": "^3.0.0",
|
||||
"tmp": "^0.1.0",
|
||||
"xo-remote-parser": "^0.5.0"
|
||||
"through2": "^4.0.2",
|
||||
"tmp": "^0.2.1",
|
||||
"xo-remote-parser": "^0.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.0.0",
|
||||
@@ -47,7 +48,7 @@
|
||||
"@babel/preset-flow": "^7.0.0",
|
||||
"async-iterator-to-stream": "^1.1.0",
|
||||
"babel-plugin-lodash": "^3.3.2",
|
||||
"cross-env": "^6.0.3",
|
||||
"cross-env": "^7.0.2",
|
||||
"dotenv": "^8.0.0",
|
||||
"index-modules": "^0.3.0",
|
||||
"rimraf": "^3.0.0"
|
||||
@@ -58,7 +59,11 @@
|
||||
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
|
||||
"prebuild": "yarn run clean",
|
||||
"predev": "yarn run clean",
|
||||
"prepare": "yarn run build",
|
||||
"prepublishOnly": "yarn run build",
|
||||
"postversion": "npm publish"
|
||||
},
|
||||
"author": {
|
||||
"name": "Vates SAS",
|
||||
"url": "https://vates.fr"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,16 +25,13 @@ export default class MountHandler extends LocalHandler {
|
||||
this._keeper = undefined
|
||||
this._params = {
|
||||
...params,
|
||||
options: [params.options, remote.options]
|
||||
options: [params.options, remote.options ?? params.defaultOptions]
|
||||
.filter(_ => _ !== undefined)
|
||||
.join(','),
|
||||
}
|
||||
this._realPath = join(
|
||||
mountsDir,
|
||||
remote.id ||
|
||||
Math.random()
|
||||
.toString(36)
|
||||
.slice(2)
|
||||
remote.id || Math.random().toString(36).slice(2)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -75,9 +72,12 @@ export default class MountHandler extends LocalHandler {
|
||||
|
||||
try {
|
||||
const { type, device, options, env } = this._params
|
||||
|
||||
// Linux mount is more flexible in which order the mount arguments appear.
|
||||
// But FreeBSD requires this order of the arguments.
|
||||
await this._execa(
|
||||
'mount',
|
||||
['-t', type, device, realPath, '-o', options],
|
||||
['-o', options, '-t', type, device, realPath],
|
||||
{
|
||||
env: {
|
||||
LANG: 'C',
|
||||
|
||||
@@ -5,7 +5,7 @@ import getStream from 'get-stream'
|
||||
|
||||
import asyncMap from '@xen-orchestra/async-map'
|
||||
import limit from 'limit-concurrency-decorator'
|
||||
import path from 'path'
|
||||
import path, { basename } from 'path'
|
||||
import synchronized from 'decorator-synchronized'
|
||||
import { fromCallback, fromEvent, ignoreErrors, timeout } from 'promise-toolbox'
|
||||
import { parse } from 'xo-remote-parser'
|
||||
@@ -118,9 +118,10 @@ export default class RemoteHandlerAbstract {
|
||||
}
|
||||
|
||||
async closeFile(fd: FileDescriptor): Promise<void> {
|
||||
await timeout.call(this._closeFile(fd.fd), this._timeout)
|
||||
await this.__closeFile(fd)
|
||||
}
|
||||
|
||||
// TODO: remove method
|
||||
async createOutputStream(
|
||||
file: File,
|
||||
{ checksum = false, ...options }: Object = {}
|
||||
@@ -221,19 +222,15 @@ export default class RemoteHandlerAbstract {
|
||||
)
|
||||
}
|
||||
|
||||
createWriteStream(
|
||||
file: File,
|
||||
options: { end?: number, flags?: string, start?: number } = {}
|
||||
): Promise<LaxWritable> {
|
||||
return timeout.call(
|
||||
this._createWriteStream(
|
||||
typeof file === 'string' ? normalizePath(file) : file,
|
||||
{
|
||||
flags: 'wx',
|
||||
...options,
|
||||
}
|
||||
)
|
||||
)
|
||||
// write a stream to a file using a temporary file
|
||||
async outputStream(
|
||||
input: Readable | Promise<Readable>,
|
||||
path: string,
|
||||
{ checksum = true }: { checksum?: boolean } = {}
|
||||
): Promise<void> {
|
||||
path = normalizePath(path)
|
||||
input = await input
|
||||
return this._outputStream(await input, normalizePath(path), { checksum })
|
||||
}
|
||||
|
||||
// Free the resources possibly dedicated to put the remote at work, when it
|
||||
@@ -283,30 +280,15 @@ export default class RemoteHandlerAbstract {
|
||||
}
|
||||
|
||||
async mkdir(dir: string): Promise<void> {
|
||||
dir = normalizePath(dir)
|
||||
try {
|
||||
await this._mkdir(dir)
|
||||
} catch (error) {
|
||||
if (error == null || error.code !== 'EEXIST') {
|
||||
throw error
|
||||
}
|
||||
|
||||
// this operation will throw if it's not already a directory
|
||||
await this._list(dir)
|
||||
}
|
||||
await this.__mkdir(normalizePath(dir))
|
||||
}
|
||||
|
||||
async mktree(dir: string): Promise<void> {
|
||||
await this._mktree(normalizePath(dir))
|
||||
}
|
||||
|
||||
async openFile(path: string, flags: string): Promise<FileDescriptor> {
|
||||
path = normalizePath(path)
|
||||
|
||||
return {
|
||||
fd: await timeout.call(this._openFile(path, flags), this._timeout),
|
||||
path,
|
||||
}
|
||||
openFile(path: string, flags: string): Promise<FileDescriptor> {
|
||||
return this.__openFile(path, flags)
|
||||
}
|
||||
|
||||
async outputFile(
|
||||
@@ -336,18 +318,6 @@ export default class RemoteHandlerAbstract {
|
||||
return this._readFile(normalizePath(file), { flags })
|
||||
}
|
||||
|
||||
async refreshChecksum(path: string): Promise<void> {
|
||||
path = normalizePath(path)
|
||||
|
||||
const stream = (await this._createReadStream(path, { flags: 'r' })).pipe(
|
||||
createChecksumStream()
|
||||
)
|
||||
stream.resume() // start reading the whole file
|
||||
await this._outputFile(checksumFile(path), await stream.checksum, {
|
||||
flags: 'wx',
|
||||
})
|
||||
}
|
||||
|
||||
async rename(
|
||||
oldPath: string,
|
||||
newPath: string,
|
||||
@@ -455,6 +425,34 @@ export default class RemoteHandlerAbstract {
|
||||
await this._writeFile(normalizePath(file), data, { flags })
|
||||
}
|
||||
|
||||
// Methods that can be called by private methods to avoid parallel limit on public methods
|
||||
|
||||
async __closeFile(fd: FileDescriptor): Promise<void> {
|
||||
await timeout.call(this._closeFile(fd.fd), this._timeout)
|
||||
}
|
||||
|
||||
async __mkdir(dir: string): Promise<void> {
|
||||
try {
|
||||
await this._mkdir(dir)
|
||||
} catch (error) {
|
||||
if (error == null || error.code !== 'EEXIST') {
|
||||
throw error
|
||||
}
|
||||
|
||||
// this operation will throw if it's not already a directory
|
||||
await this._list(dir)
|
||||
}
|
||||
}
|
||||
|
||||
async __openFile(path: string, flags: string): Promise<FileDescriptor> {
|
||||
path = normalizePath(path)
|
||||
|
||||
return {
|
||||
fd: await timeout.call(this._openFile(path, flags), this._timeout),
|
||||
path,
|
||||
}
|
||||
}
|
||||
|
||||
// Methods that can be implemented by inheriting classes
|
||||
|
||||
async _closeFile(fd: mixed): Promise<void> {
|
||||
@@ -503,7 +501,7 @@ export default class RemoteHandlerAbstract {
|
||||
|
||||
async _mktree(dir: string): Promise<void> {
|
||||
try {
|
||||
return await this.mkdir(dir)
|
||||
return await this.__mkdir(dir)
|
||||
} catch (error) {
|
||||
if (error.code !== 'ENOENT') {
|
||||
throw error
|
||||
@@ -535,6 +533,22 @@ export default class RemoteHandlerAbstract {
|
||||
return this._outputFile(file, data, options)
|
||||
}
|
||||
|
||||
async _outputStream(input, path, { checksum }) {
|
||||
const tmpPath = `${dirname(path)}/.${basename(path)}`
|
||||
const output = await this.createOutputStream(tmpPath, { checksum })
|
||||
try {
|
||||
input.pipe(output)
|
||||
await fromEvent(output, 'finish')
|
||||
await output.checksumWritten
|
||||
// $FlowFixMe
|
||||
await input.task
|
||||
await this.rename(tmpPath, path, { checksum })
|
||||
} catch (error) {
|
||||
await this.unlink(tmpPath, { checksum })
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
_read(
|
||||
file: File,
|
||||
buffer: Buffer,
|
||||
@@ -586,13 +600,13 @@ export default class RemoteHandlerAbstract {
|
||||
async _write(file: File, buffer: Buffer, position: number): Promise<void> {
|
||||
const isPath = typeof file === 'string'
|
||||
if (isPath) {
|
||||
file = await this.openFile(file, 'r+')
|
||||
file = await this.__openFile(file, 'r+')
|
||||
}
|
||||
try {
|
||||
return await this._writeFd(file, buffer, position)
|
||||
} finally {
|
||||
if (isPath) {
|
||||
await this.closeFile(file)
|
||||
await this.__closeFile(file)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -635,7 +649,7 @@ function createPrefixWrapperMethods() {
|
||||
return
|
||||
}
|
||||
|
||||
descriptor.value = function() {
|
||||
descriptor.value = function () {
|
||||
let path
|
||||
if (arguments.length !== 0 && typeof (path = arguments[0]) === 'string') {
|
||||
arguments[0] = this._resolve(path)
|
||||
|
||||
@@ -42,18 +42,6 @@ describe('createOutputStream()', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('createReadStream()', () => {
|
||||
it(`throws in case of timeout`, async () => {
|
||||
const testHandler = new TestHandler({
|
||||
createReadStream: () => new Promise(() => {}),
|
||||
})
|
||||
|
||||
const promise = testHandler.createReadStream('file')
|
||||
jest.advanceTimersByTime(TIMEOUT)
|
||||
await expect(promise).rejects.toThrowError(TimeoutError)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getInfo()', () => {
|
||||
it('throws in case of timeout', async () => {
|
||||
const testHandler = new TestHandler({
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import 'dotenv/config'
|
||||
import asyncIteratorToStream from 'async-iterator-to-stream'
|
||||
import getStream from 'get-stream'
|
||||
import { forOwn, random } from 'lodash'
|
||||
import { fromCallback } from 'promise-toolbox'
|
||||
import { pipeline } from 'readable-stream'
|
||||
@@ -28,7 +27,7 @@ const unsecureRandomBytes = n => {
|
||||
|
||||
const TEST_DATA_LEN = 1024
|
||||
const TEST_DATA = unsecureRandomBytes(TEST_DATA_LEN)
|
||||
const createTestDataStream = asyncIteratorToStream(function*() {
|
||||
const createTestDataStream = asyncIteratorToStream(function* () {
|
||||
yield TEST_DATA
|
||||
})
|
||||
|
||||
@@ -91,31 +90,6 @@ handlers.forEach(url => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('#createReadStream()', () => {
|
||||
beforeEach(() => handler.outputFile('file', TEST_DATA))
|
||||
|
||||
testWithFileDescriptor('file', 'r', async ({ file, flags }) => {
|
||||
await expect(
|
||||
await getStream.buffer(
|
||||
await handler.createReadStream(file, { flags })
|
||||
)
|
||||
).toEqual(TEST_DATA)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#createWriteStream()', () => {
|
||||
testWithFileDescriptor('file', 'wx', async ({ file, flags }) => {
|
||||
const stream = await handler.createWriteStream(file, { flags })
|
||||
await fromCallback(pipeline, createTestDataStream(), stream)
|
||||
await expect(await handler.readFile('file')).toEqual(TEST_DATA)
|
||||
})
|
||||
|
||||
it('fails if parent dir is missing', async () => {
|
||||
const error = await rejectionOf(handler.createWriteStream('dir/file'))
|
||||
expect(error.code).toBe('ENOENT')
|
||||
})
|
||||
})
|
||||
|
||||
describe('#getInfo()', () => {
|
||||
let info
|
||||
beforeAll(async () => {
|
||||
@@ -219,6 +193,12 @@ handlers.forEach(url => {
|
||||
const error = await rejectionOf(handler.outputFile('file', ''))
|
||||
expect(error.code).toBe('EEXIST')
|
||||
})
|
||||
|
||||
it("shouldn't timeout in case of the respect of the parallel execution restriction", async () => {
|
||||
const handler = getHandler({ url }, { maxParallelOperations: 1 })
|
||||
await handler.sync()
|
||||
await handler.outputFile(`xo-fs-tests-${Date.now()}/test`, '')
|
||||
}, 40)
|
||||
})
|
||||
|
||||
describe('#read()', () => {
|
||||
|
||||
@@ -4,6 +4,7 @@ import execa from 'execa'
|
||||
import type RemoteHandler from './abstract'
|
||||
import RemoteHandlerLocal from './local'
|
||||
import RemoteHandlerNfs from './nfs'
|
||||
import RemoteHandlerS3 from './s3'
|
||||
import RemoteHandlerSmb from './smb'
|
||||
import RemoteHandlerSmbMount from './smb-mount'
|
||||
|
||||
@@ -13,6 +14,7 @@ export type Remote = { url: string }
|
||||
const HANDLERS = {
|
||||
file: RemoteHandlerLocal,
|
||||
nfs: RemoteHandlerNfs,
|
||||
s3: RemoteHandlerS3,
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
@@ -2,15 +2,13 @@ import { parse } from 'xo-remote-parser'
|
||||
|
||||
import MountHandler from './_mount'
|
||||
|
||||
const DEFAULT_NFS_OPTIONS = 'vers=3'
|
||||
|
||||
export default class NfsHandler extends MountHandler {
|
||||
constructor(remote, opts) {
|
||||
const { host, port, path } = parse(remote.url)
|
||||
super(remote, opts, {
|
||||
type: 'nfs',
|
||||
device: `${host}${port !== undefined ? ':' + port : ''}:${path}`,
|
||||
options: DEFAULT_NFS_OPTIONS,
|
||||
defaultOptions: 'vers=3',
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
284
@xen-orchestra/fs/src/s3.js
Normal file
284
@xen-orchestra/fs/src/s3.js
Normal file
@@ -0,0 +1,284 @@
|
||||
import AWS from 'aws-sdk'
|
||||
import { parse } from 'xo-remote-parser'
|
||||
|
||||
import RemoteHandlerAbstract from './abstract'
|
||||
import { createChecksumStream } from './checksum'
|
||||
|
||||
// endpoints https://docs.aws.amazon.com/general/latest/gr/s3.html
|
||||
|
||||
// limits: https://docs.aws.amazon.com/AmazonS3/latest/dev/qfacts.html
|
||||
const MIN_PART_SIZE = 1024 * 1024 * 5 // 5MB
|
||||
const MAX_PART_SIZE = 1024 * 1024 * 1024 * 5 // 5GB
|
||||
const MAX_PARTS_COUNT = 10000
|
||||
const MAX_OBJECT_SIZE = 1024 * 1024 * 1024 * 1024 * 5 // 5TB
|
||||
const IDEAL_FRAGMENT_SIZE = Math.ceil(MAX_OBJECT_SIZE / MAX_PARTS_COUNT) // the smallest fragment size that still allows a 5TB upload in 10000 fragments, about 524MB
|
||||
export default class S3Handler extends RemoteHandlerAbstract {
|
||||
constructor(remote, _opts) {
|
||||
super(remote)
|
||||
const { host, path, username, password } = parse(remote.url)
|
||||
|
||||
// https://www.zenko.io/blog/first-things-first-getting-started-scality-s3-server/
|
||||
this._s3 = new AWS.S3({
|
||||
accessKeyId: username,
|
||||
apiVersion: '2006-03-01',
|
||||
endpoint: host,
|
||||
s3ForcePathStyle: true,
|
||||
secretAccessKey: password,
|
||||
signatureVersion: 'v4',
|
||||
})
|
||||
const splitPath = path.split('/').filter(s => s.length)
|
||||
this._bucket = splitPath.shift()
|
||||
this._dir = splitPath.join('/')
|
||||
}
|
||||
|
||||
get type() {
|
||||
return 's3'
|
||||
}
|
||||
|
||||
_createParams(file) {
|
||||
return { Bucket: this._bucket, Key: this._dir + file }
|
||||
}
|
||||
|
||||
async _outputStream(input, path, { checksum }) {
|
||||
let inputStream = input
|
||||
if (checksum) {
|
||||
const checksumStream = createChecksumStream()
|
||||
const forwardError = error => {
|
||||
checksumStream.emit('error', error)
|
||||
}
|
||||
input.pipe(checksumStream)
|
||||
input.on('error', forwardError)
|
||||
inputStream = checksumStream
|
||||
}
|
||||
const upload = this._s3.upload(
|
||||
{
|
||||
...this._createParams(path),
|
||||
Body: inputStream,
|
||||
},
|
||||
{ partSize: IDEAL_FRAGMENT_SIZE }
|
||||
)
|
||||
await upload.promise()
|
||||
if (checksum) {
|
||||
const checksum = await inputStream.checksum
|
||||
const params = {
|
||||
...this._createParams(path + '.checksum'),
|
||||
Body: checksum,
|
||||
}
|
||||
await this._s3.upload(params).promise()
|
||||
}
|
||||
await input.task
|
||||
}
|
||||
|
||||
async _writeFile(file, data, options) {
|
||||
return this._s3
|
||||
.putObject({ ...this._createParams(file), Body: data })
|
||||
.promise()
|
||||
}
|
||||
|
||||
async _createReadStream(file, options) {
|
||||
return this._s3.getObject(this._createParams(file)).createReadStream()
|
||||
}
|
||||
|
||||
async _unlink(file) {
|
||||
return this._s3.deleteObject(this._createParams(file)).promise()
|
||||
}
|
||||
|
||||
async _list(dir) {
|
||||
function splitPath(path) {
|
||||
return path.split('/').filter(d => d.length)
|
||||
}
|
||||
|
||||
const prefix = [this._dir, dir].join('/')
|
||||
const splitPrefix = splitPath(prefix)
|
||||
const request = this._s3.listObjectsV2({
|
||||
Bucket: this._bucket,
|
||||
Prefix: splitPrefix.join('/'),
|
||||
})
|
||||
const result = await request.promise()
|
||||
const uniq = new Set()
|
||||
for (const entry of result.Contents) {
|
||||
const line = splitPath(entry.Key)
|
||||
if (line.length > splitPrefix.length) {
|
||||
uniq.add(line[splitPrefix.length])
|
||||
}
|
||||
}
|
||||
return [...uniq]
|
||||
}
|
||||
|
||||
async _rename(oldPath, newPath) {
|
||||
const params = {
|
||||
...this._createParams(newPath),
|
||||
CopySource: `/${this._bucket}/${this._dir}${oldPath}`,
|
||||
}
|
||||
await this._s3.copyObject(params).promise()
|
||||
await this._s3.deleteObject(this._createParams(oldPath)).promise()
|
||||
}
|
||||
|
||||
async _getSize(file) {
|
||||
if (typeof file !== 'string') {
|
||||
file = file.fd
|
||||
}
|
||||
const result = await this._s3.headObject(this._createParams(file)).promise()
|
||||
return +result.ContentLength
|
||||
}
|
||||
|
||||
async _read(file, buffer, position = 0) {
|
||||
if (typeof file !== 'string') {
|
||||
file = file.fd
|
||||
}
|
||||
const params = this._createParams(file)
|
||||
params.Range = `bytes=${position}-${position + buffer.length - 1}`
|
||||
const result = await this._s3.getObject(params).promise()
|
||||
result.Body.copy(buffer)
|
||||
return { bytesRead: result.Body.length, buffer }
|
||||
}
|
||||
|
||||
async _write(file, buffer, position) {
|
||||
if (typeof file !== 'string') {
|
||||
file = file.fd
|
||||
}
|
||||
const uploadParams = this._createParams(file)
|
||||
const fileSize = +(await this._s3.headObject(uploadParams).promise())
|
||||
.ContentLength
|
||||
if (fileSize < MIN_PART_SIZE) {
|
||||
const resultBuffer = Buffer.alloc(
|
||||
Math.max(fileSize, position + buffer.length)
|
||||
)
|
||||
const fileContent = (await this._s3.getObject(uploadParams).promise())
|
||||
.Body
|
||||
fileContent.copy(resultBuffer)
|
||||
buffer.copy(resultBuffer, position)
|
||||
await this._s3
|
||||
.putObject({ ...uploadParams, Body: resultBuffer })
|
||||
.promise()
|
||||
return { buffer, bytesWritten: buffer.length }
|
||||
} else {
|
||||
// using this trick: https://stackoverflow.com/a/38089437/72637
|
||||
// multipart fragments have a minimum size of 5Mo and a max of 5Go unless they are last
|
||||
// splitting the file in 3 parts: [prefix, edit, suffix]
|
||||
// if `prefix` is bigger than 5Mo, it will be sourced from uploadPartCopy()
|
||||
// otherwise otherwise it will be downloaded, concatenated to `edit`
|
||||
// `edit` will always be an upload part
|
||||
// `suffix` will ways be sourced from uploadPartCopy()
|
||||
const multipartParams = await this._s3
|
||||
.createMultipartUpload(uploadParams)
|
||||
.promise()
|
||||
try {
|
||||
const parts = []
|
||||
const prefixSize = position
|
||||
let suffixOffset = prefixSize + buffer.length
|
||||
let suffixSize = Math.max(0, fileSize - suffixOffset)
|
||||
let hasSuffix = suffixSize > 0
|
||||
let editBuffer = buffer
|
||||
let editBufferOffset = position
|
||||
let partNumber = 1
|
||||
if (prefixSize < MIN_PART_SIZE) {
|
||||
const downloadParams = {
|
||||
...uploadParams,
|
||||
Range: `bytes=0-${prefixSize - 1}`,
|
||||
}
|
||||
const prefixBuffer =
|
||||
prefixSize > 0
|
||||
? (await this._s3.getObject(downloadParams).promise()).Body
|
||||
: Buffer.alloc(0)
|
||||
editBuffer = Buffer.concat([prefixBuffer, buffer])
|
||||
editBufferOffset = 0
|
||||
} else {
|
||||
const fragmentsCount = Math.ceil(prefixSize / MAX_PART_SIZE)
|
||||
const prefixFragmentSize = Math.ceil(prefixSize / fragmentsCount)
|
||||
const lastFragmentSize =
|
||||
prefixFragmentSize * fragmentsCount - prefixSize
|
||||
let prefixPosition = 0
|
||||
for (let i = 0; i < fragmentsCount; i++) {
|
||||
const copyPrefixParams = {
|
||||
...multipartParams,
|
||||
PartNumber: partNumber++,
|
||||
CopySource: `/${this._bucket}/${this._dir + file}`,
|
||||
CopySourceRange: `bytes=${prefixPosition}-${
|
||||
prefixPosition + prefixFragmentSize - 1
|
||||
}`,
|
||||
}
|
||||
const prefixPart = (
|
||||
await this._s3.uploadPartCopy(copyPrefixParams).promise()
|
||||
).CopyPartResult
|
||||
parts.push({
|
||||
ETag: prefixPart.ETag,
|
||||
PartNumber: copyPrefixParams.PartNumber,
|
||||
})
|
||||
prefixPosition += prefixFragmentSize
|
||||
}
|
||||
if (lastFragmentSize) {
|
||||
}
|
||||
}
|
||||
if (hasSuffix && editBuffer.length < MIN_PART_SIZE) {
|
||||
// the edit fragment is too short and is not the last fragment
|
||||
// let's steal from the suffix fragment to reach the minimum size
|
||||
// the suffix might be too short and itself entirely absorbed in the edit fragment, making it the last one.
|
||||
const complementSize = Math.min(
|
||||
MIN_PART_SIZE - editBuffer.length,
|
||||
suffixSize
|
||||
)
|
||||
const complementOffset = editBufferOffset + editBuffer.length
|
||||
suffixOffset += complementSize
|
||||
suffixSize -= complementSize
|
||||
hasSuffix = suffixSize > 0
|
||||
const prefixRange = `bytes=${complementOffset}-${
|
||||
complementOffset + complementSize - 1
|
||||
}`
|
||||
const downloadParams = { ...uploadParams, Range: prefixRange }
|
||||
const complementBuffer = (
|
||||
await this._s3.getObject(downloadParams).promise()
|
||||
).Body
|
||||
editBuffer = Buffer.concat([editBuffer, complementBuffer])
|
||||
}
|
||||
const editParams = {
|
||||
...multipartParams,
|
||||
Body: editBuffer,
|
||||
PartNumber: partNumber++,
|
||||
}
|
||||
const editPart = await this._s3.uploadPart(editParams).promise()
|
||||
parts.push({ ETag: editPart.ETag, PartNumber: editParams.PartNumber })
|
||||
if (hasSuffix) {
|
||||
const suffixFragments = Math.ceil(suffixSize / MAX_PART_SIZE)
|
||||
const suffixFragmentsSize = Math.ceil(suffixSize / suffixFragments)
|
||||
let suffixFragmentOffset = suffixOffset
|
||||
for (let i = 0; i < suffixFragments; i++) {
|
||||
const fragmentEnd = suffixFragmentOffset + suffixFragmentsSize
|
||||
const suffixRange = `bytes=${suffixFragmentOffset}-${
|
||||
Math.min(fileSize, fragmentEnd) - 1
|
||||
}`
|
||||
const copySuffixParams = {
|
||||
...multipartParams,
|
||||
PartNumber: partNumber++,
|
||||
CopySource: `/${this._bucket}/${this._dir + file}`,
|
||||
CopySourceRange: suffixRange,
|
||||
}
|
||||
const suffixPart = (
|
||||
await this._s3.uploadPartCopy(copySuffixParams).promise()
|
||||
).CopyPartResult
|
||||
parts.push({
|
||||
ETag: suffixPart.ETag,
|
||||
PartNumber: copySuffixParams.PartNumber,
|
||||
})
|
||||
suffixFragmentOffset = fragmentEnd
|
||||
}
|
||||
}
|
||||
await this._s3
|
||||
.completeMultipartUpload({
|
||||
...multipartParams,
|
||||
MultipartUpload: { Parts: parts },
|
||||
})
|
||||
.promise()
|
||||
} catch (e) {
|
||||
await this._s3.abortMultipartUpload(multipartParams).promise()
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async _openFile(path, flags) {
|
||||
return path
|
||||
}
|
||||
|
||||
async _closeFile(fd) {}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
# @xen-orchestra/log [](https://travis-ci.org/vatesfr/xen-orchestra)
|
||||
<!-- DO NOT EDIT MANUALLY, THIS FILE HAS BEEN GENERATED -->
|
||||
|
||||
> ${pkg.description}
|
||||
# @xen-orchestra/log
|
||||
|
||||
[](https://npmjs.org/package/@xen-orchestra/log)  [](https://bundlephobia.com/result?p=@xen-orchestra/log) [](https://npmjs.org/package/@xen-orchestra/log)
|
||||
|
||||
## Install
|
||||
|
||||
@@ -135,25 +137,6 @@ configure(transportSyslog())
|
||||
configure(transportSyslog('tcp://syslog.company.lan'))
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
```
|
||||
# Install dependencies
|
||||
> yarn
|
||||
|
||||
# Run the tests
|
||||
> yarn test
|
||||
|
||||
# Continuously compile
|
||||
> yarn dev
|
||||
|
||||
# Continuously run the tests
|
||||
> yarn dev-test
|
||||
|
||||
# Build for production (automatically called by npm install)
|
||||
> yarn build
|
||||
```
|
||||
|
||||
## Contributions
|
||||
|
||||
Contributions are _very_ welcomed, either on the documentation or on
|
||||
@@ -161,10 +144,10 @@ the code.
|
||||
|
||||
You may:
|
||||
|
||||
- report any [issue](https://github.com/vatesfr/xo-web/issues/)
|
||||
- report any [issue](https://github.com/vatesfr/xen-orchestra/issues)
|
||||
you've encountered;
|
||||
- fork and create a pull request.
|
||||
|
||||
## License
|
||||
|
||||
ISC © [Vates SAS](https://vates.fr)
|
||||
[ISC](https://spdx.org/licenses/ISC) © [Vates SAS](https://vates.fr)
|
||||
|
||||
122
@xen-orchestra/log/USAGE.md
Normal file
122
@xen-orchestra/log/USAGE.md
Normal file
@@ -0,0 +1,122 @@
|
||||
Everywhere something should be logged:
|
||||
|
||||
```js
|
||||
import { createLogger } from '@xen-orchestra/log'
|
||||
|
||||
const log = createLogger('my-module')
|
||||
|
||||
log.debug('only useful for debugging')
|
||||
log.info('this information is relevant to the user')
|
||||
log.warn('something went wrong but did not prevent current action')
|
||||
log.error('something went wrong')
|
||||
log.fatal('service/app is going down')
|
||||
|
||||
// you can add contextual info
|
||||
log.debug('new API request', {
|
||||
method: 'foo',
|
||||
params: [ 'bar', 'baz' ]
|
||||
user: 'qux'
|
||||
})
|
||||
|
||||
// by convention, errors go into the `error` field
|
||||
log.error('could not join server', {
|
||||
error,
|
||||
server: 'example.org',
|
||||
})
|
||||
```
|
||||
|
||||
Then, at application level, configure the logs are handled:
|
||||
|
||||
```js
|
||||
import { createLogger } from '@xen-orchestra/log'
|
||||
import { configure, catchGlobalErrors } from '@xen-orchestra/log/configure'
|
||||
import transportConsole from '@xen-orchestra/log/transports/console'
|
||||
import transportEmail from '@xen-orchestra/log/transports/email'
|
||||
|
||||
const transport = transportEmail({
|
||||
service: 'gmail',
|
||||
auth: {
|
||||
user: 'jane.smith@gmail.com',
|
||||
pass: 'H&NbECcpXF|pyXe#%ZEb',
|
||||
},
|
||||
from: 'jane.smith@gmail.com',
|
||||
to: ['jane.smith@gmail.com', 'sam.doe@yahoo.com'],
|
||||
})
|
||||
|
||||
configure([
|
||||
{
|
||||
// if filter is a string, then it is pattern
|
||||
// (https://github.com/visionmedia/debug#wildcards) which is
|
||||
// matched against the namespace of the logs
|
||||
filter: process.env.DEBUG,
|
||||
|
||||
transport: transportConsole(),
|
||||
},
|
||||
{
|
||||
// only levels >= warn
|
||||
level: 'warn',
|
||||
|
||||
transport,
|
||||
},
|
||||
])
|
||||
|
||||
// send all global errors (uncaught exceptions, warnings, unhandled rejections)
|
||||
// to this logger
|
||||
catchGlobalErrors(createLogger('app'))
|
||||
```
|
||||
|
||||
### Transports
|
||||
|
||||
#### Console
|
||||
|
||||
```js
|
||||
import transportConsole from '@xen-orchestra/log/transports/console'
|
||||
|
||||
configure(transportConsole())
|
||||
```
|
||||
|
||||
#### Email
|
||||
|
||||
Optional dependency:
|
||||
|
||||
```
|
||||
> yarn add nodemailer pretty-format
|
||||
```
|
||||
|
||||
Configuration:
|
||||
|
||||
```js
|
||||
import transportEmail from '@xen-orchestra/log/transports/email'
|
||||
|
||||
configure(
|
||||
transportEmail({
|
||||
service: 'gmail',
|
||||
auth: {
|
||||
user: 'jane.smith@gmail.com',
|
||||
pass: 'H&NbECcpXF|pyXe#%ZEb',
|
||||
},
|
||||
from: 'jane.smith@gmail.com',
|
||||
to: ['jane.smith@gmail.com', 'sam.doe@yahoo.com'],
|
||||
})
|
||||
)
|
||||
```
|
||||
|
||||
#### Syslog
|
||||
|
||||
Optional dependency:
|
||||
|
||||
```
|
||||
> yarn add split-host syslog-client
|
||||
```
|
||||
|
||||
Configuration:
|
||||
|
||||
```js
|
||||
import transportSyslog from '@xen-orchestra/log/transports/syslog'
|
||||
|
||||
// By default, log to udp://localhost:514
|
||||
configure(transportSyslog())
|
||||
|
||||
// But TCP, a different host, or a different port can be used
|
||||
configure(transportSyslog('tcp://syslog.company.lan'))
|
||||
```
|
||||
@@ -13,8 +13,8 @@
|
||||
"url": "https://github.com/vatesfr/xen-orchestra.git"
|
||||
},
|
||||
"author": {
|
||||
"name": "Julien Fontanet",
|
||||
"email": "julien.fontanet@vates.fr"
|
||||
"name": "Vates SAS",
|
||||
"url": "https://vates.fr"
|
||||
},
|
||||
"preferGlobal": false,
|
||||
"main": "dist/",
|
||||
@@ -39,7 +39,7 @@
|
||||
"@babel/core": "^7.0.0",
|
||||
"@babel/preset-env": "^7.0.0",
|
||||
"babel-plugin-lodash": "^3.3.2",
|
||||
"cross-env": "^6.0.3",
|
||||
"cross-env": "^7.0.2",
|
||||
"index-modules": "^0.3.0",
|
||||
"rimraf": "^3.0.0"
|
||||
},
|
||||
@@ -49,7 +49,7 @@
|
||||
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
|
||||
"prebuild": "yarn run clean",
|
||||
"predev": "yarn run prebuild",
|
||||
"prepare": "yarn run build",
|
||||
"prepublishOnly": "yarn run build",
|
||||
"postversion": "npm publish"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ const createTransport = config => {
|
||||
if (Array.isArray(config)) {
|
||||
const transports = config.map(createTransport)
|
||||
const { length } = transports
|
||||
return function() {
|
||||
return function () {
|
||||
for (let i = 0; i < length; ++i) {
|
||||
transports[i].apply(this, arguments)
|
||||
}
|
||||
@@ -30,14 +30,14 @@ const createTransport = config => {
|
||||
}
|
||||
|
||||
const orig = transport
|
||||
transport = function(log) {
|
||||
transport = function (log) {
|
||||
if ((level !== undefined && log.level >= level) || filter(log)) {
|
||||
return orig.apply(this, arguments)
|
||||
}
|
||||
}
|
||||
} else if (level !== undefined) {
|
||||
const orig = transport
|
||||
transport = function(log) {
|
||||
transport = function (log) {
|
||||
if (log.level >= level) {
|
||||
return orig.apply(this, arguments)
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ const { prototype } = Logger
|
||||
for (const name in LEVELS) {
|
||||
const level = LEVELS[name]
|
||||
|
||||
prototype[name.toLowerCase()] = function(message, data) {
|
||||
prototype[name.toLowerCase()] = function (message, data) {
|
||||
if (typeof message !== 'string') {
|
||||
if (message instanceof Error) {
|
||||
data = { error: message }
|
||||
@@ -54,13 +54,13 @@ for (const name in LEVELS) {
|
||||
}
|
||||
}
|
||||
|
||||
prototype.wrap = function(message, fn) {
|
||||
prototype.wrap = function (message, fn) {
|
||||
const logger = this
|
||||
const warnAndRethrow = error => {
|
||||
logger.warn(message, { error })
|
||||
throw error
|
||||
}
|
||||
return function() {
|
||||
return function () {
|
||||
try {
|
||||
const result = fn.apply(this, arguments)
|
||||
const then = result != null && result.then
|
||||
|
||||
@@ -1,6 +1,85 @@
|
||||
import LEVELS, { NAMES } from '../levels'
|
||||
|
||||
const { ERROR, INFO, WARN } = LEVELS
|
||||
const { DEBUG, ERROR, FATAL, INFO, WARN } = LEVELS
|
||||
|
||||
let formatLevel, formatNamespace
|
||||
if (
|
||||
process.stdout !== undefined &&
|
||||
process.stdout.isTTY &&
|
||||
process.stderr !== undefined &&
|
||||
process.stderr.isTTY
|
||||
) {
|
||||
const ansi = (style, str) => `\x1b[${style}m${str}\x1b[0m`
|
||||
|
||||
const LEVEL_STYLES = {
|
||||
[DEBUG]: '2',
|
||||
[ERROR]: '1;31',
|
||||
[FATAL]: '1;31',
|
||||
[INFO]: '1',
|
||||
[WARN]: '1;33',
|
||||
}
|
||||
formatLevel = level => {
|
||||
const style = LEVEL_STYLES[level]
|
||||
const name = NAMES[level]
|
||||
return style === undefined ? name : ansi(style, name)
|
||||
}
|
||||
|
||||
const NAMESPACE_COLORS = [
|
||||
196,
|
||||
202,
|
||||
208,
|
||||
214,
|
||||
220,
|
||||
226,
|
||||
190,
|
||||
154,
|
||||
118,
|
||||
82,
|
||||
46,
|
||||
47,
|
||||
48,
|
||||
49,
|
||||
50,
|
||||
51,
|
||||
45,
|
||||
39,
|
||||
33,
|
||||
27,
|
||||
21,
|
||||
57,
|
||||
93,
|
||||
129,
|
||||
165,
|
||||
201,
|
||||
200,
|
||||
199,
|
||||
198,
|
||||
197,
|
||||
]
|
||||
formatNamespace = namespace => {
|
||||
// https://werxltd.com/wp/2010/05/13/javascript-implementation-of-javas-string-hashcode-method/
|
||||
let hash = 0
|
||||
for (let i = 0, n = namespace.length; i < n; ++i) {
|
||||
hash = ((hash << 5) - hash + namespace.charCodeAt(i)) | 0
|
||||
}
|
||||
// // select a hue (HSV)
|
||||
// const h = (Math.abs(hash) % 20) * 18
|
||||
// // convert to RGB
|
||||
// const f = (n, k = (n + h / 60) % 6) =>
|
||||
// Math.round(255 * (1 - Math.max(Math.min(k, 4 - k, 1), 0)))
|
||||
// const r = f(5)
|
||||
// const g = f(3)
|
||||
// const b = f(1)
|
||||
// return ansi(`38;2;${r};${g};${b}`, namespace)
|
||||
return ansi(
|
||||
`1;38;5;${NAMESPACE_COLORS[Math.abs(hash) % NAMESPACE_COLORS.length]}`,
|
||||
namespace
|
||||
)
|
||||
}
|
||||
} else {
|
||||
formatLevel = str => NAMES[str]
|
||||
formatNamespace = str => str
|
||||
}
|
||||
|
||||
const consoleTransport = ({ data, level, namespace, message, time }) => {
|
||||
const fn =
|
||||
@@ -14,7 +93,12 @@ const consoleTransport = ({ data, level, namespace, message, time }) => {
|
||||
: console.error
|
||||
/* eslint-enable no-console */
|
||||
|
||||
const args = [time.toISOString(), namespace, NAMES[level], message]
|
||||
const args = [
|
||||
time.toISOString(),
|
||||
formatNamespace(namespace),
|
||||
formatLevel(level),
|
||||
message,
|
||||
]
|
||||
if (data != null) {
|
||||
args.push(data)
|
||||
}
|
||||
|
||||
@@ -13,10 +13,7 @@ export const evalTemplate = (tpl, data) => {
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
const compileGlobPatternFragment = pattern =>
|
||||
pattern
|
||||
.split('*')
|
||||
.map(escapeRegExp)
|
||||
.join('.*')
|
||||
pattern.split('*').map(escapeRegExp).join('.*')
|
||||
|
||||
export const compileGlobPattern = pattern => {
|
||||
const no = []
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# @xen-orchestra/mixin [](https://travis-ci.org/${pkg.shortGitHubPath})
|
||||
<!-- DO NOT EDIT MANUALLY, THIS FILE HAS BEEN GENERATED -->
|
||||
|
||||
> ${pkg.description}
|
||||
# @xen-orchestra/mixin
|
||||
|
||||
[](https://npmjs.org/package/@xen-orchestra/mixin)  [](https://bundlephobia.com/result?p=@xen-orchestra/mixin) [](https://npmjs.org/package/@xen-orchestra/mixin)
|
||||
|
||||
## Install
|
||||
|
||||
@@ -10,29 +12,6 @@ Installation of the [npm package](https://npmjs.org/package/@xen-orchestra/mixin
|
||||
> npm install --save @xen-orchestra/mixin
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
**TODO**
|
||||
|
||||
## Development
|
||||
|
||||
```
|
||||
# Install dependencies
|
||||
> yarn
|
||||
|
||||
# Run the tests
|
||||
> yarn test
|
||||
|
||||
# Continuously compile
|
||||
> yarn dev
|
||||
|
||||
# Continuously run the tests
|
||||
> yarn dev-test
|
||||
|
||||
# Build for production (automatically called by npm install)
|
||||
> yarn build
|
||||
```
|
||||
|
||||
## Contributions
|
||||
|
||||
Contributions are _very_ welcomed, either on the documentation or on
|
||||
@@ -46,4 +25,4 @@ You may:
|
||||
|
||||
## License
|
||||
|
||||
ISC © [Vates SAS](https://vates.fr)
|
||||
[ISC](https://spdx.org/licenses/ISC) © [Vates SAS](https://vates.fr)
|
||||
|
||||
0
@xen-orchestra/mixin/USAGE.md
Normal file
0
@xen-orchestra/mixin/USAGE.md
Normal file
@@ -13,8 +13,8 @@
|
||||
"url": "https://github.com/vatesfr/xen-orchestra.git"
|
||||
},
|
||||
"author": {
|
||||
"name": "Julien Fontanet",
|
||||
"email": "julien.fontanet@vates.fr"
|
||||
"name": "Vates SAS",
|
||||
"url": "https://vates.fr"
|
||||
},
|
||||
"preferGlobal": false,
|
||||
"main": "dist/",
|
||||
@@ -37,7 +37,7 @@
|
||||
"@babel/preset-env": "^7.0.0",
|
||||
"babel-plugin-dev": "^1.0.0",
|
||||
"babel-plugin-lodash": "^3.3.2",
|
||||
"cross-env": "^6.0.3",
|
||||
"cross-env": "^7.0.2",
|
||||
"rimraf": "^3.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
3
@xen-orchestra/openflow/.babelrc.js
Normal file
3
@xen-orchestra/openflow/.babelrc.js
Normal file
@@ -0,0 +1,3 @@
|
||||
module.exports = require('../../@xen-orchestra/babel-config')(
|
||||
require('./package.json')
|
||||
)
|
||||
24
@xen-orchestra/openflow/.npmignore
Normal file
24
@xen-orchestra/openflow/.npmignore
Normal file
@@ -0,0 +1,24 @@
|
||||
/benchmark/
|
||||
/benchmarks/
|
||||
*.bench.js
|
||||
*.bench.js.map
|
||||
|
||||
/examples/
|
||||
example.js
|
||||
example.js.map
|
||||
*.example.js
|
||||
*.example.js.map
|
||||
|
||||
/fixture/
|
||||
/fixtures/
|
||||
*.fixture.js
|
||||
*.fixture.js.map
|
||||
*.fixtures.js
|
||||
*.fixtures.js.map
|
||||
|
||||
/test/
|
||||
/tests/
|
||||
*.spec.js
|
||||
*.spec.js.map
|
||||
|
||||
__snapshots__/
|
||||
141
@xen-orchestra/openflow/README.md
Normal file
141
@xen-orchestra/openflow/README.md
Normal file
@@ -0,0 +1,141 @@
|
||||
<!-- DO NOT EDIT MANUALLY, THIS FILE HAS BEEN GENERATED -->
|
||||
|
||||
# @xen-orchestra/openflow
|
||||
|
||||
[](https://npmjs.org/package/@xen-orchestra/openflow)  [](https://bundlephobia.com/result?p=@xen-orchestra/openflow) [](https://npmjs.org/package/@xen-orchestra/openflow)
|
||||
|
||||
> Pack and unpack OpenFlow messages
|
||||
|
||||
## Install
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/@xen-orchestra/openflow):
|
||||
|
||||
```
|
||||
> npm install --save @xen-orchestra/openflow
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Unpacking a received OpenFlow message from a socket:
|
||||
|
||||
```js
|
||||
import openflow from '@xen-orchestra/openflow'
|
||||
import parse from '@xen-orchestra/openflow/parse-socket'
|
||||
|
||||
const version = openflow.versions.openFlow11
|
||||
const ofProtocol = openflow.protocols[version]
|
||||
|
||||
function parseOpenFlowMessages(socket) {
|
||||
for await (const msg of parse(socket)) {
|
||||
if (msg.header !== undefined) {
|
||||
const ofType = msg.header.type
|
||||
switch (ofType) {
|
||||
case ofProtocol.type.hello:
|
||||
// Handle OFPT_HELLO
|
||||
break
|
||||
case ofProtocol.type.error:
|
||||
// Handle OFPT_ERROR
|
||||
break
|
||||
case ofProtocol.type.echoRequest:
|
||||
// Handle OFPT_ECHO_REQUEST
|
||||
break
|
||||
case ofProtocol.type.packetIn:
|
||||
// Handle OFPT_PACKET_IN
|
||||
break
|
||||
case ofProtocol.type.featuresReply:
|
||||
// Handle OFPT_FEATURES_REPLY
|
||||
break
|
||||
case ofProtocol.type.getConfigReply:
|
||||
// Handle OFPT_GET_CONFIG_REPLY
|
||||
break
|
||||
case ofProtocol.type.portStatus:
|
||||
// Handle OFPT_PORT_STATUS
|
||||
break
|
||||
case ofProtocol.type.flowRemoved:
|
||||
// Handle OFPT_FLOW_REMOVED
|
||||
break
|
||||
default:
|
||||
// Error: Invalid type
|
||||
break
|
||||
}
|
||||
} else {
|
||||
// Error: Message is unparseable
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Unpacking a OpenFlow message from a buffer:
|
||||
|
||||
```js
|
||||
import openflow from '@xen-orchestra/openflow'
|
||||
|
||||
const version = openflow.versions.openFlow11
|
||||
const ofProtocol = openflow.protocols[version]
|
||||
|
||||
function processOpenFlowMessage(buf) {
|
||||
const unpacked = openflow.unpack(buf)
|
||||
const ofType = unpacked.header.type
|
||||
switch (ofType) {
|
||||
case ofProtocol.type.hello:
|
||||
// Handle OFPT_HELLO
|
||||
break
|
||||
case ofProtocol.type.error:
|
||||
// Handle OFPT_ERROR
|
||||
break
|
||||
case ofProtocol.type.echoRequest:
|
||||
// Handle OFPT_ECHO_REQUEST
|
||||
break
|
||||
case ofProtocol.type.packetIn:
|
||||
// Handle OFPT_PACKET_IN
|
||||
break
|
||||
case ofProtocol.type.featuresReply:
|
||||
// Handle OFPT_FEATURES_REPLY
|
||||
break
|
||||
case ofProtocol.type.getConfigReply:
|
||||
// Handle OFPT_GET_CONFIG_REPLY
|
||||
break
|
||||
case ofProtocol.type.portStatus:
|
||||
// Handle OFPT_PORT_STATUS
|
||||
break
|
||||
case ofProtocol.type.flowRemoved:
|
||||
// Handle OFPT_FLOW_REMOVED
|
||||
break
|
||||
default:
|
||||
// Error: Invalid type
|
||||
break
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Packing an OpenFlow OFPT_HELLO message:
|
||||
|
||||
```js
|
||||
import openflow from '@xen-orchestra/openflow'
|
||||
|
||||
const version = openflow.versions.openFlow11
|
||||
const ofProtocol = openflow.protocols[version]
|
||||
|
||||
const buf = openflow.pack({
|
||||
header: {
|
||||
version,
|
||||
type: ofProtocol.type.hello,
|
||||
xid: 1,
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## Contributions
|
||||
|
||||
Contributions are _very_ welcomed, either on the documentation or on
|
||||
the code.
|
||||
|
||||
You may:
|
||||
|
||||
- report any [issue](https://github.com/vatesfr/xen-orchestra/issues)
|
||||
you've encountered;
|
||||
- fork and create a pull request.
|
||||
|
||||
## License
|
||||
|
||||
[ISC](https://spdx.org/licenses/ISC) © [Vates SAS](https://vates.fr)
|
||||
108
@xen-orchestra/openflow/USAGE.md
Normal file
108
@xen-orchestra/openflow/USAGE.md
Normal file
@@ -0,0 +1,108 @@
|
||||
Unpacking a received OpenFlow message from a socket:
|
||||
|
||||
```js
|
||||
import openflow from '@xen-orchestra/openflow'
|
||||
import parse from '@xen-orchestra/openflow/parse-socket'
|
||||
|
||||
const version = openflow.versions.openFlow11
|
||||
const ofProtocol = openflow.protocols[version]
|
||||
|
||||
function parseOpenFlowMessages(socket) {
|
||||
for await (const msg of parse(socket)) {
|
||||
if (msg.header !== undefined) {
|
||||
const ofType = msg.header.type
|
||||
switch (ofType) {
|
||||
case ofProtocol.type.hello:
|
||||
// Handle OFPT_HELLO
|
||||
break
|
||||
case ofProtocol.type.error:
|
||||
// Handle OFPT_ERROR
|
||||
break
|
||||
case ofProtocol.type.echoRequest:
|
||||
// Handle OFPT_ECHO_REQUEST
|
||||
break
|
||||
case ofProtocol.type.packetIn:
|
||||
// Handle OFPT_PACKET_IN
|
||||
break
|
||||
case ofProtocol.type.featuresReply:
|
||||
// Handle OFPT_FEATURES_REPLY
|
||||
break
|
||||
case ofProtocol.type.getConfigReply:
|
||||
// Handle OFPT_GET_CONFIG_REPLY
|
||||
break
|
||||
case ofProtocol.type.portStatus:
|
||||
// Handle OFPT_PORT_STATUS
|
||||
break
|
||||
case ofProtocol.type.flowRemoved:
|
||||
// Handle OFPT_FLOW_REMOVED
|
||||
break
|
||||
default:
|
||||
// Error: Invalid type
|
||||
break
|
||||
}
|
||||
} else {
|
||||
// Error: Message is unparseable
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Unpacking a OpenFlow message from a buffer:
|
||||
|
||||
```js
|
||||
import openflow from '@xen-orchestra/openflow'
|
||||
|
||||
const version = openflow.versions.openFlow11
|
||||
const ofProtocol = openflow.protocols[version]
|
||||
|
||||
function processOpenFlowMessage(buf) {
|
||||
const unpacked = openflow.unpack(buf)
|
||||
const ofType = unpacked.header.type
|
||||
switch (ofType) {
|
||||
case ofProtocol.type.hello:
|
||||
// Handle OFPT_HELLO
|
||||
break
|
||||
case ofProtocol.type.error:
|
||||
// Handle OFPT_ERROR
|
||||
break
|
||||
case ofProtocol.type.echoRequest:
|
||||
// Handle OFPT_ECHO_REQUEST
|
||||
break
|
||||
case ofProtocol.type.packetIn:
|
||||
// Handle OFPT_PACKET_IN
|
||||
break
|
||||
case ofProtocol.type.featuresReply:
|
||||
// Handle OFPT_FEATURES_REPLY
|
||||
break
|
||||
case ofProtocol.type.getConfigReply:
|
||||
// Handle OFPT_GET_CONFIG_REPLY
|
||||
break
|
||||
case ofProtocol.type.portStatus:
|
||||
// Handle OFPT_PORT_STATUS
|
||||
break
|
||||
case ofProtocol.type.flowRemoved:
|
||||
// Handle OFPT_FLOW_REMOVED
|
||||
break
|
||||
default:
|
||||
// Error: Invalid type
|
||||
break
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Packing an OpenFlow OFPT_HELLO message:
|
||||
|
||||
```js
|
||||
import openflow from '@xen-orchestra/openflow'
|
||||
|
||||
const version = openflow.versions.openFlow11
|
||||
const ofProtocol = openflow.protocols[version]
|
||||
|
||||
const buf = openflow.pack({
|
||||
header: {
|
||||
version,
|
||||
type: ofProtocol.type.hello,
|
||||
xid: 1,
|
||||
},
|
||||
})
|
||||
```
|
||||
40
@xen-orchestra/openflow/package.json
Normal file
40
@xen-orchestra/openflow/package.json
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"description": "Pack and unpack OpenFlow messages",
|
||||
"private": false,
|
||||
"name": "@xen-orchestra/openflow",
|
||||
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/openflow",
|
||||
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
|
||||
"repository": {
|
||||
"directory": "@xen-orchestra/openflow",
|
||||
"type": "git",
|
||||
"url": "https://github.com/vatesfr/xen-orchestra.git"
|
||||
},
|
||||
"version": "0.1.1",
|
||||
"engines": {
|
||||
"node": ">=8.10"
|
||||
},
|
||||
"main": "dist/",
|
||||
"scripts": {
|
||||
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
|
||||
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
|
||||
"postversion": "npm publish --access public",
|
||||
"prebuild": "rimraf dist/",
|
||||
"predev": "yarn run prebuild",
|
||||
"prepublishOnly": "yarn run build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.7.4",
|
||||
"@babel/core": "^7.7.4",
|
||||
"@babel/preset-env": "^7.7.4",
|
||||
"cross": "^1.0.0",
|
||||
"rimraf": "^3.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vates/read-chunk": "^0.1.0"
|
||||
},
|
||||
"author": {
|
||||
"name": "Vates SAS",
|
||||
"url": "https://vates.fr"
|
||||
},
|
||||
"license": "ISC"
|
||||
}
|
||||
1
@xen-orchestra/openflow/parse-socket.js
Normal file
1
@xen-orchestra/openflow/parse-socket.js
Normal file
@@ -0,0 +1 @@
|
||||
module.exports = require('./dist/parse-socket')
|
||||
9
@xen-orchestra/openflow/src/default-header-scheme.js
Normal file
9
@xen-orchestra/openflow/src/default-header-scheme.js
Normal file
@@ -0,0 +1,9 @@
|
||||
export default {
|
||||
size: 8,
|
||||
offsets: {
|
||||
version: 0,
|
||||
type: 1,
|
||||
length: 2,
|
||||
xid: 4,
|
||||
},
|
||||
}
|
||||
38
@xen-orchestra/openflow/src/index.js
Normal file
38
@xen-orchestra/openflow/src/index.js
Normal file
@@ -0,0 +1,38 @@
|
||||
import get from './util/get-from-map'
|
||||
import ofVersion from './version'
|
||||
// TODO: More openflow versions
|
||||
import of11 from './openflow-11/index'
|
||||
import scheme from './default-header-scheme'
|
||||
|
||||
// =============================================================================
|
||||
|
||||
const OPENFLOW = {
|
||||
[ofVersion.openFlow11]: of11,
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
|
||||
export default {
|
||||
versions: ofVersion,
|
||||
protocols: { [ofVersion.openFlow11]: of11.protocol },
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pack: object => {
|
||||
const version = object.header.version
|
||||
return get(
|
||||
OPENFLOW,
|
||||
version,
|
||||
`Unsupported OpenFlow version: ${version}`
|
||||
).pack(object)
|
||||
},
|
||||
|
||||
unpack: (buffer, offset = 0) => {
|
||||
const version = buffer.readUInt8(offset + scheme.offsets.version)
|
||||
return get(
|
||||
OPENFLOW,
|
||||
version,
|
||||
`Unsupported OpenFlow version: ${version}`
|
||||
).unpack(buffer, offset)
|
||||
},
|
||||
}
|
||||
58
@xen-orchestra/openflow/src/openflow-11/action/action.js
Normal file
58
@xen-orchestra/openflow/src/openflow-11/action/action.js
Normal file
@@ -0,0 +1,58 @@
|
||||
import get from '../../util/get-from-map'
|
||||
|
||||
import ofOutput from './output'
|
||||
import of from '../openflow-11'
|
||||
|
||||
// =============================================================================
|
||||
|
||||
const ACTION = {
|
||||
[of.actionType.output]: ofOutput,
|
||||
/* TODO:
|
||||
[of.actionType.group]: ,
|
||||
[of.actionType.setVlanId]: ,
|
||||
[of.actionType.setVlanPcp]: ,
|
||||
[of.actionType.setDlSrc]: ,
|
||||
[of.actionType.setDlDst]: ,
|
||||
[of.actionType.setNwSrc]: ,
|
||||
[of.actionType.setNwDst]: ,
|
||||
[of.actionType.setNwTos]: ,
|
||||
[of.actionType.setNwEcn]: ,
|
||||
[of.actionType.setTpSrc]: ,
|
||||
[of.actionType.setTpDst]: ,
|
||||
[of.actionType.copyTtlOut]: ,
|
||||
[of.actionType.copyTtlIn]: ,
|
||||
[of.actionType.setMplsLabel]: ,
|
||||
[of.actionType.setMplsTc]: ,
|
||||
[of.actionType.setMplsTtl]: ,
|
||||
[of.actionType.decMplsTtl]: ,
|
||||
[of.actionType.pushVlan]: ,
|
||||
[of.actionType.popVlan]: ,
|
||||
[of.actionType.pushMpls]: ,
|
||||
[of.actionType.popMpls]: ,
|
||||
[of.actionType.setQueue]: ,
|
||||
[of.actionType.setNwTtl]: ,
|
||||
[of.actionType.decNwTtl]: ,
|
||||
[of.actionType.experimenter]:
|
||||
*/
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
|
||||
export default {
|
||||
pack: (object, buffer = undefined, offset = 0) => {
|
||||
const { type } = object
|
||||
return get(ACTION, type, `Invalid action type: ${type}`).pack(
|
||||
object,
|
||||
buffer,
|
||||
offset
|
||||
)
|
||||
},
|
||||
|
||||
unpack: (buffer, offset = 0) => {
|
||||
const type = buffer.readUInt16BE(offset + of.offsets.actionHeader.type)
|
||||
return get(ACTION, type, `Invalid action type: ${type}`).unpack(
|
||||
buffer,
|
||||
offset
|
||||
)
|
||||
},
|
||||
}
|
||||
45
@xen-orchestra/openflow/src/openflow-11/action/output.js
Normal file
45
@xen-orchestra/openflow/src/openflow-11/action/output.js
Normal file
@@ -0,0 +1,45 @@
|
||||
import assert from 'assert'
|
||||
|
||||
import of from '../openflow-11'
|
||||
|
||||
// =============================================================================
|
||||
|
||||
const OFFSETS = of.offsets.actionOutput
|
||||
|
||||
const PAD_LENGTH = 6
|
||||
|
||||
// =============================================================================
|
||||
|
||||
export default {
|
||||
pack: (object, buffer = undefined, offset = 0) => {
|
||||
assert(object.type === of.actionType.output)
|
||||
object.len = of.sizes.actionOutput
|
||||
|
||||
buffer = buffer !== undefined ? buffer : Buffer.alloc(object.len)
|
||||
|
||||
buffer.writeUInt16BE(object.type, offset + OFFSETS.type)
|
||||
buffer.writeUInt16BE(object.len, offset + OFFSETS.len)
|
||||
|
||||
buffer.writeUInt32BE(object.port, offset + OFFSETS.port)
|
||||
buffer.writeUInt16BE(object.max_len, offset + OFFSETS.maxLen)
|
||||
|
||||
buffer.fill(0, offset + OFFSETS.pad, offset + OFFSETS.pad + PAD_LENGTH)
|
||||
|
||||
return buffer
|
||||
},
|
||||
|
||||
unpack: (buffer, offset = 0) => {
|
||||
const object = {}
|
||||
|
||||
object.type = buffer.readUInt16BE(offset + OFFSETS.type)
|
||||
assert(object.type === of.actionType.output)
|
||||
|
||||
object.len = buffer.readUInt16BE(offset + OFFSETS.len)
|
||||
assert(object.len === of.sizes.actionOutput)
|
||||
|
||||
object.port = buffer.readUInt32BE(offset + OFFSETS.port)
|
||||
object.max_len = buffer.readUInt16BE(offset + OFFSETS.maxLen)
|
||||
|
||||
return object
|
||||
},
|
||||
}
|
||||
49
@xen-orchestra/openflow/src/openflow-11/index.js
Normal file
49
@xen-orchestra/openflow/src/openflow-11/index.js
Normal file
@@ -0,0 +1,49 @@
|
||||
import get from '../util/get-from-map'
|
||||
|
||||
import echo from './message/echo'
|
||||
import error from './message/error'
|
||||
import hello from './message/hello'
|
||||
import featuresRequest from './message/features-request'
|
||||
import featuresReply from './message/features-reply'
|
||||
import getConfigRequest from './message/get-config-request'
|
||||
import switchConfig from './message/switch-config'
|
||||
import flowMod from './message/flow-mod'
|
||||
import of from './openflow-11'
|
||||
|
||||
// =============================================================================
|
||||
|
||||
const MESSAGE = {
|
||||
[of.type.hello]: hello,
|
||||
[of.type.error]: error,
|
||||
[of.type.featuresRequest]: featuresRequest,
|
||||
[of.type.featuresReply]: featuresReply,
|
||||
[of.type.echoRequest]: echo,
|
||||
[of.type.echoReply]: echo,
|
||||
[of.type.getConfigRequest]: getConfigRequest,
|
||||
[of.type.getConfigReply]: switchConfig,
|
||||
[of.type.setConfig]: switchConfig,
|
||||
[of.type.flowMod]: flowMod,
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
|
||||
export default {
|
||||
protocol: of,
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pack: object => {
|
||||
const type = object.header.type
|
||||
return get(MESSAGE, type, `Invalid OpenFlow message type: ${type}`).pack(
|
||||
object
|
||||
)
|
||||
},
|
||||
|
||||
unpack: (buffer, offset = 0) => {
|
||||
const type = buffer.readUInt8(offset + of.offsets.header.type)
|
||||
return get(MESSAGE, type, `Invalid OpenFlow message type: ${type}`).unpack(
|
||||
buffer,
|
||||
offset
|
||||
)
|
||||
},
|
||||
}
|
||||
102
@xen-orchestra/openflow/src/openflow-11/instruction/actions.js
Normal file
102
@xen-orchestra/openflow/src/openflow-11/instruction/actions.js
Normal file
@@ -0,0 +1,102 @@
|
||||
import assert from 'assert'
|
||||
|
||||
import get from '../../util/get-from-map'
|
||||
|
||||
import ofAction from '../action/action'
|
||||
import of from '../openflow-11'
|
||||
|
||||
// =============================================================================
|
||||
|
||||
const SIZES = {
|
||||
[of.actionType.output]: of.sizes.actionOutput,
|
||||
[of.actionType.group]: of.sizes.actionGroup,
|
||||
[of.actionType.setVlanId]: of.sizes.actionVlanId,
|
||||
[of.actionType.setVlanPcp]: of.sizes.actionVlanPcp,
|
||||
[of.actionType.setDlSrc]: of.sizes.actionDlAddr,
|
||||
[of.actionType.setDlDst]: of.sizes.actionDlAddr,
|
||||
[of.actionType.setNwSrc]: of.sizes.actionNwAddr,
|
||||
[of.actionType.setNwDst]: of.sizes.actionNwAddr,
|
||||
[of.actionType.setNwTos]: of.sizes.actionNwTos,
|
||||
[of.actionType.setNwEcn]: of.sizes.actionNwEcn,
|
||||
[of.actionType.setTpSrc]: of.sizes.actionTpPort,
|
||||
[of.actionType.setTpDst]: of.sizes.actionTpPort,
|
||||
[of.actionType.copyTtlOut]: of.sizes.actionHeader,
|
||||
[of.actionType.copyTtlIn]: of.sizes.actionHeader,
|
||||
[of.actionType.setMplsLabel]: of.sizes.actionMplsLabel,
|
||||
[of.actionType.setMplsTc]: of.sizes.actionMplsTc,
|
||||
[of.actionType.setMplsTtl]: of.sizes.actionMplsTtl,
|
||||
[of.actionType.decMplsTtl]: of.sizes.actionMplsTtl,
|
||||
[of.actionType.pushVlan]: of.sizes.actionPush,
|
||||
[of.actionType.popVlan]: of.sizes.actionHeader,
|
||||
[of.actionType.pushMpls]: of.sizes.actionPush,
|
||||
[of.actionType.popMpls]: of.sizes.actionPopMpls,
|
||||
[of.actionType.setQueue]: of.sizes.actionSetQueue,
|
||||
[of.actionType.setNwTtl]: of.sizes.actionNwTtl,
|
||||
[of.actionType.decNwTtl]: of.sizes.actionNwTtl,
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
const TYPES = [
|
||||
of.instructionType.clearActions,
|
||||
of.instructionType.writeActions,
|
||||
of.instructionType.applyActions,
|
||||
]
|
||||
const OFFSETS = of.offsets.instructionActions
|
||||
|
||||
const PAD_LENGTH = 4
|
||||
|
||||
// =============================================================================
|
||||
|
||||
export default {
|
||||
pack: (object, buffer = undefined, offset = 0) => {
|
||||
const { type } = object
|
||||
assert(TYPES.includes(type))
|
||||
object.len = of.sizes.instructionActions
|
||||
const { actions = [] } = object
|
||||
actions.forEach(action => {
|
||||
assert(Object.values(of.actionType).includes(action.type))
|
||||
// TODO: manage experimenter
|
||||
object.len += get(
|
||||
SIZES,
|
||||
action.type,
|
||||
`Invalid action type: ${action.type}`
|
||||
)
|
||||
})
|
||||
|
||||
buffer = buffer !== undefined ? buffer : Buffer.alloc(object.len)
|
||||
|
||||
buffer.writeUInt16BE(type, offset + OFFSETS.type)
|
||||
buffer.writeUInt16BE(object.len, offset + OFFSETS.len)
|
||||
buffer.fill(0, offset + OFFSETS.pad, offset + OFFSETS.pad + PAD_LENGTH)
|
||||
|
||||
let actionOffset = offset + OFFSETS.actions
|
||||
actions.forEach(action => {
|
||||
ofAction.pack(action, buffer, actionOffset)
|
||||
actionOffset += SIZES[action.type]
|
||||
})
|
||||
},
|
||||
|
||||
unpack: (buffer = undefined, offset = 0) => {
|
||||
const type = buffer.readUInt16BE(offset + OFFSETS.type)
|
||||
assert(TYPES.includes(type))
|
||||
|
||||
const object = { type }
|
||||
object.len = buffer.readUInt16BE(offset + OFFSETS.len)
|
||||
|
||||
if (type === of.instructionType.clearActions) {
|
||||
// No actions for this type
|
||||
return object
|
||||
}
|
||||
|
||||
object.actions = []
|
||||
let actionOffset = offset + OFFSETS.actions
|
||||
while (actionOffset < object.len) {
|
||||
const action = ofAction.unpack(buffer, actionOffset)
|
||||
actionOffset += action.len
|
||||
object.actions.push(action)
|
||||
}
|
||||
|
||||
return object
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import get from '../../util/get-from-map'
|
||||
|
||||
import actions from './actions'
|
||||
// import goToTable from './goToTable'
|
||||
import of from '../openflow-11'
|
||||
// import writeMetadata from './writeMetadata'
|
||||
|
||||
// =============================================================================
|
||||
|
||||
const INSTRUCTION = {
|
||||
/* TODO:
|
||||
[of.instructionType.goToTable]: goToTable,
|
||||
[of.instructionType.writeMetadata]: writeMetadata,
|
||||
*/
|
||||
[of.instructionType.writeActions]: actions,
|
||||
[of.instructionType.applyActions]: actions,
|
||||
[of.instructionType.clearActions]: actions,
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
const OFFSETS = of.offsets.instruction
|
||||
|
||||
// =============================================================================
|
||||
|
||||
export default {
|
||||
pack: (object, buffer = undefined, offset = 0) => {
|
||||
const { type } = object
|
||||
return get(INSTRUCTION, type, `Invalid instruction type: ${type}`).pack(
|
||||
object,
|
||||
buffer,
|
||||
offset
|
||||
)
|
||||
},
|
||||
|
||||
unpack: (buffer = undefined, offset = 0) => {
|
||||
const type = buffer.readUInt16BE(offset + OFFSETS.type)
|
||||
return get(INSTRUCTION, type, `Invalid instruction type: ${type}`).unpack(
|
||||
buffer,
|
||||
offset
|
||||
)
|
||||
},
|
||||
}
|
||||
46
@xen-orchestra/openflow/src/openflow-11/message/echo.js
Normal file
46
@xen-orchestra/openflow/src/openflow-11/message/echo.js
Normal file
@@ -0,0 +1,46 @@
|
||||
import assert from 'assert'
|
||||
import ofHeader from './header'
|
||||
import of from '../openflow-11'
|
||||
|
||||
// =============================================================================
|
||||
|
||||
const OFFSETS = of.offsets.echo
|
||||
const TYPES = [of.type.echoRequest, of.type.echoReply]
|
||||
|
||||
// =============================================================================
|
||||
|
||||
export default {
|
||||
pack: object => {
|
||||
const { header, data } = object
|
||||
assert(TYPES.includes(header.type))
|
||||
const dataSize = data !== undefined ? data.length : 0
|
||||
header.length = of.sizes.header + dataSize
|
||||
|
||||
const buffer = Buffer.alloc(header.length)
|
||||
ofHeader.pack(header, buffer, OFFSETS.header)
|
||||
if (dataSize > 0) {
|
||||
data.copy(buffer, OFFSETS.data, 0, dataSize)
|
||||
}
|
||||
|
||||
return buffer
|
||||
},
|
||||
|
||||
unpack: (buffer, offset = 0) => {
|
||||
const header = ofHeader.unpack(buffer, offset + OFFSETS.header)
|
||||
assert(TYPES.includes(header.type))
|
||||
|
||||
const object = { header }
|
||||
const dataSize = header.length - of.sizes.header
|
||||
if (dataSize > 0) {
|
||||
object.data = Buffer.alloc(dataSize)
|
||||
buffer.copy(
|
||||
object.data,
|
||||
0,
|
||||
offset + OFFSETS.data,
|
||||
offset + OFFSETS.data + dataSize
|
||||
)
|
||||
}
|
||||
|
||||
return object
|
||||
},
|
||||
}
|
||||
79
@xen-orchestra/openflow/src/openflow-11/message/error.js
Normal file
79
@xen-orchestra/openflow/src/openflow-11/message/error.js
Normal file
@@ -0,0 +1,79 @@
|
||||
import assert from 'assert'
|
||||
|
||||
import get from '../../util/get-from-map'
|
||||
|
||||
import ofHeader from './header'
|
||||
import of from '../openflow-11'
|
||||
|
||||
// =============================================================================
|
||||
|
||||
const ERROR_CODE = {
|
||||
[of.errorType.helloFailed]: of.helloFailedCode,
|
||||
[of.errorType.badRequest]: of.badRequestCode,
|
||||
[of.errorType.badAction]: of.badActionCode,
|
||||
[of.errorType.badInstruction]: of.badInstructionCode,
|
||||
[of.errorType.badMatch]: of.badMatchCode,
|
||||
[of.errorType.flowModFailed]: of.flowModFailedCode,
|
||||
[of.errorType.groupModFailed]: of.groupModFailedCode,
|
||||
[of.errorType.portModFailed]: of.portModFailedCode,
|
||||
[of.errorType.tableModFailed]: of.tableModFailedCode,
|
||||
[of.errorType.queueOpFailed]: of.queueOpFailedCode,
|
||||
[of.errorType.switchConfigFailed]: of.switchConfigFailedCode,
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
const OFFSETS = of.offsets.errorMsg
|
||||
|
||||
// =============================================================================
|
||||
|
||||
export default {
|
||||
pack: (object, buffer = undefined, offset = 0) => {
|
||||
const { header, type, code, data } = object
|
||||
assert(header.type === of.type.error)
|
||||
const errorCodes = get(ERROR_CODE, type, `Invalid error type: ${type}`)
|
||||
assert(Object.values(errorCodes).includes(code))
|
||||
|
||||
object.length = of.sizes.errorMsg
|
||||
if (data !== undefined) {
|
||||
object.length += data.length
|
||||
}
|
||||
|
||||
buffer = buffer !== undefined ? buffer : Buffer.alloc(object.length)
|
||||
|
||||
ofHeader.pack(header, buffer, offset + OFFSETS.header)
|
||||
buffer.writeUInt16BE(type, offset + OFFSETS.type)
|
||||
buffer.writeUInt16BE(code, offset + OFFSETS.code)
|
||||
|
||||
if (data !== undefined) {
|
||||
data.copy(buffer, offset + OFFSETS.data, 0, data.length)
|
||||
}
|
||||
|
||||
return buffer
|
||||
},
|
||||
|
||||
unpack: (buffer, offset = 0) => {
|
||||
const header = ofHeader.unpack(buffer, offset + OFFSETS.header)
|
||||
assert(header.type === of.type.error)
|
||||
|
||||
const type = buffer.readUInt16BE(offset + OFFSETS.type)
|
||||
const errorCodes = get(ERROR_CODE, type, `Invalid error type: ${type}`)
|
||||
|
||||
const code = buffer.readUInt16BE(offset + OFFSETS.code)
|
||||
assert(Object.values(errorCodes).includes(code))
|
||||
|
||||
const object = { header, type, code }
|
||||
const dataSize = header.length - of.sizes.errorMsg
|
||||
if (dataSize > 0) {
|
||||
object.data = Buffer.alloc(dataSize)
|
||||
buffer.copy(
|
||||
object.data,
|
||||
0,
|
||||
offset + OFFSETS.data,
|
||||
offset + OFFSETS.data + dataSize
|
||||
)
|
||||
}
|
||||
|
||||
return object
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import assert from 'assert'
|
||||
|
||||
import ofHeader from './header'
|
||||
import ofPort from '../struct/port'
|
||||
import of from '../openflow-11'
|
||||
|
||||
// =============================================================================
|
||||
|
||||
const OFFSETS = of.offsets.switchFeatures
|
||||
const PAD_LENGTH = 3
|
||||
|
||||
// =============================================================================
|
||||
|
||||
export default {
|
||||
pack: object => {
|
||||
const {
|
||||
header,
|
||||
datapath_id: did,
|
||||
n_buffers: nBufs,
|
||||
n_tables: nTables,
|
||||
capabilities,
|
||||
reserved,
|
||||
ports,
|
||||
} = object
|
||||
assert(header.type === of.type.featuresReply)
|
||||
|
||||
header.length = of.sizes.switchFeatures + ports.length * of.sizes.port
|
||||
|
||||
const buffer = Buffer.alloc(header.length)
|
||||
ofHeader.pack(header, buffer, OFFSETS.header)
|
||||
|
||||
buffer.writeBigUInt64BE(did, OFFSETS.datapathId)
|
||||
buffer.writeUInt32BE(nBufs, OFFSETS.nBuffers)
|
||||
buffer.writeUInt8(nTables, OFFSETS.nTables)
|
||||
buffer.fill(0, OFFSETS.pad, OFFSETS.pad + PAD_LENGTH)
|
||||
buffer.writeUInt32BE(capabilities, OFFSETS.capabilities)
|
||||
buffer.writeUInt32BE(reserved, OFFSETS.reserved)
|
||||
|
||||
let portsOffset = 0
|
||||
ports.forEach(port => {
|
||||
ofPort.pack(port, buffer, OFFSETS.ports + portsOffset++ * of.sizes.port)
|
||||
})
|
||||
|
||||
return buffer
|
||||
},
|
||||
|
||||
unpack: (buffer, offset = 0) => {
|
||||
const header = ofHeader.unpack(buffer, offset + OFFSETS.header)
|
||||
assert(header.type === of.type.featuresReply)
|
||||
|
||||
const object = { header }
|
||||
object.datapath_id = buffer.toString(
|
||||
'hex',
|
||||
offset + OFFSETS.datapathId,
|
||||
offset + OFFSETS.datapathId + 8
|
||||
)
|
||||
object.n_buffers = buffer.readUInt32BE(offset + OFFSETS.nBuffers)
|
||||
object.n_tables = buffer.readUInt8(offset + OFFSETS.nTables)
|
||||
|
||||
object.capabilities = buffer.readUInt32BE(offset + OFFSETS.capabilities)
|
||||
object.reserved = buffer.readUInt32BE(offset + OFFSETS.reserved)
|
||||
|
||||
object.ports = []
|
||||
const nPorts = (header.length - of.sizes.switchFeatures) / of.sizes.port
|
||||
for (let i = 0; i < nPorts; ++i) {
|
||||
object.ports.push(
|
||||
ofPort.unpack(buffer, offset + OFFSETS.ports + i * of.sizes.port)
|
||||
)
|
||||
}
|
||||
|
||||
return object
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import assert from 'assert'
|
||||
|
||||
import ofHeader from './header'
|
||||
import of from '../openflow-11'
|
||||
|
||||
// =============================================================================
|
||||
|
||||
export default {
|
||||
pack: object => {
|
||||
const { header } = object
|
||||
assert(header.type === of.type.featuresRequest)
|
||||
header.length = of.sizes.featuresRequest
|
||||
|
||||
return ofHeader.pack(header)
|
||||
},
|
||||
|
||||
unpack: (buffer, offset = 0) => {
|
||||
const header = ofHeader.unpack(buffer, offset)
|
||||
assert(header.type === of.type.featuresRequest)
|
||||
assert(header.length === of.sizes.featuresRequest)
|
||||
|
||||
return { header }
|
||||
},
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user