Compare commits
458 Commits
xo-server/
...
xo-server/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
74b0697da1 | ||
|
|
0ca409bb22 | ||
|
|
527cf25d1b | ||
|
|
a49594e6a5 | ||
|
|
5cd22b41d6 | ||
|
|
934949c514 | ||
|
|
cc61e8d334 | ||
|
|
81be499c49 | ||
|
|
082aa55566 | ||
|
|
c783039557 | ||
|
|
206dfeb879 | ||
|
|
f839e76f4b | ||
|
|
c67151f922 | ||
|
|
aedec5de18 | ||
|
|
3df973a1ea | ||
|
|
0253031d7f | ||
|
|
f4445d4681 | ||
|
|
e399dfa7e6 | ||
|
|
787f95ac3a | ||
|
|
d2f35d46d2 | ||
|
|
b21d078c5d | ||
|
|
032c3fb856 | ||
|
|
bbd79379ce | ||
|
|
d01d544a0a | ||
|
|
01ecd76976 | ||
|
|
26e8ae4bf3 | ||
|
|
3befdbc93d | ||
|
|
c91a890d42 | ||
|
|
3369ab601a | ||
|
|
bfe5b71f19 | ||
|
|
eb25cf65dd | ||
|
|
aa1ca3be64 | ||
|
|
a4e03daeee | ||
|
|
cbd0b9db1d | ||
|
|
621e8e89a5 | ||
|
|
c9eca5ec7e | ||
|
|
05063b76eb | ||
|
|
7d1d34d1eb | ||
|
|
40423a0547 | ||
|
|
682804b1ad | ||
|
|
790e67866c | ||
|
|
8399edb4dc | ||
|
|
55a1c27d6d | ||
|
|
35bf7dc484 | ||
|
|
4d3dfa1dca | ||
|
|
ac4686125f | ||
|
|
fb2ca3bb19 | ||
|
|
c0122aace7 | ||
|
|
c141e92cc4 | ||
|
|
5236441be0 | ||
|
|
94620748ab | ||
|
|
da70c03845 | ||
|
|
337eb0f27b | ||
|
|
26a63c4baf | ||
|
|
8b03890f2a | ||
|
|
61731e2c2e | ||
|
|
5c1611c484 | ||
|
|
a502965d19 | ||
|
|
b55764db56 | ||
|
|
510897f672 | ||
|
|
2689fd17d0 | ||
|
|
75e3949cec | ||
|
|
a7bb4b7104 | ||
|
|
9bcb2ac094 | ||
|
|
9ab110277a | ||
|
|
d1506bcdae | ||
|
|
00c38c96cd | ||
|
|
9798d4ff6a | ||
|
|
5801b29ede | ||
|
|
22ed022787 | ||
|
|
f7e7ecf5ae | ||
|
|
c116d3f453 | ||
|
|
d9b3d263ae | ||
|
|
9a265a0437 | ||
|
|
16450e2133 | ||
|
|
5678742810 | ||
|
|
76d551a238 | ||
|
|
5467c4b1b8 | ||
|
|
e48d277440 | ||
|
|
9d1da81557 | ||
|
|
91f557ac9e | ||
|
|
452826bd61 | ||
|
|
84564bb7fb | ||
|
|
bf7647c737 | ||
|
|
1f98d7e5ec | ||
|
|
e4486f4c17 | ||
|
|
65daa23a74 | ||
|
|
523a30afb4 | ||
|
|
1d0de4584e | ||
|
|
7aac124407 | ||
|
|
03e8b664ac | ||
|
|
66883ae37c | ||
|
|
2a075d929a | ||
|
|
41147483d8 | ||
|
|
ca517784ed | ||
|
|
4dd3be1568 | ||
|
|
7412d97bf3 | ||
|
|
ab7b2da83b | ||
|
|
19e26729a8 | ||
|
|
0101365ebc | ||
|
|
1f56e63f9c | ||
|
|
883a30c7ad | ||
|
|
6e151a9f8b | ||
|
|
321bb299b1 | ||
|
|
2ca18340c7 | ||
|
|
74d4237913 | ||
|
|
a4f9b9208d | ||
|
|
8fd65b7365 | ||
|
|
4f4d0bf6aa | ||
|
|
873e2aed94 | ||
|
|
89d485e188 | ||
|
|
2e9870014f | ||
|
|
528529c0d1 | ||
|
|
9bddec2dfd | ||
|
|
f986487df9 | ||
|
|
99461a70e6 | ||
|
|
adbbb15a92 | ||
|
|
d85a4c9ad4 | ||
|
|
41baea780a | ||
|
|
a165884bcb | ||
|
|
456adc5d0b | ||
|
|
cfc42906b9 | ||
|
|
738d657c8e | ||
|
|
a51452ee7c | ||
|
|
5d2a41082a | ||
|
|
f9dd00b79b | ||
|
|
898244d04d | ||
|
|
33334830cc | ||
|
|
8503350bfd | ||
|
|
a4bb2aaf12 | ||
|
|
da443045bf | ||
|
|
b9927cd48d | ||
|
|
7af3f7e881 | ||
|
|
ee81febc89 | ||
|
|
8146bee846 | ||
|
|
53e94378ae | ||
|
|
8592ead0e3 | ||
|
|
67699372f2 | ||
|
|
95a8ced558 | ||
|
|
ae437be6e7 | ||
|
|
1441d9f4ee | ||
|
|
adeb5c2344 | ||
|
|
f0b0277b9d | ||
|
|
6fb5fb63e7 | ||
|
|
5330cc5ae9 | ||
|
|
bcc2244fdb | ||
|
|
ad2de95f32 | ||
|
|
453dee33ba | ||
|
|
13f36b3f79 | ||
|
|
719b63ee02 | ||
|
|
38a5698f90 | ||
|
|
a05b60f48e | ||
|
|
8694ecd417 | ||
|
|
100d007271 | ||
|
|
a1a764d807 | ||
|
|
6cb30adf5d | ||
|
|
9eb939e38f | ||
|
|
3e26060979 | ||
|
|
cc60aa7b84 | ||
|
|
bdfdafaec0 | ||
|
|
7737dc6b6c | ||
|
|
1a89465201 | ||
|
|
fe3ce45b8e | ||
|
|
7af0883f08 | ||
|
|
f48c21b124 | ||
|
|
abc2e74f2c | ||
|
|
6130c49b83 | ||
|
|
433b58511c | ||
|
|
b04111c79b | ||
|
|
06ca0079b3 | ||
|
|
ff53c6b49d | ||
|
|
da58458fb7 | ||
|
|
1a21989ad1 | ||
|
|
76d54b8914 | ||
|
|
d22d64f68c | ||
|
|
580ae005f4 | ||
|
|
75ab9d2e8c | ||
|
|
6c246768e9 | ||
|
|
bc75bc199b | ||
|
|
f234b6a540 | ||
|
|
bff8bfea7b | ||
|
|
48bf0d1942 | ||
|
|
04bbb84845 | ||
|
|
311f8cd00f | ||
|
|
ed0ab78048 | ||
|
|
0eec1c1f61 | ||
|
|
b4a3b832dc | ||
|
|
8e7830dd7d | ||
|
|
d32a18d965 | ||
|
|
7d00d47cb6 | ||
|
|
b0853eb119 | ||
|
|
25d29fb389 | ||
|
|
ed241ede9d | ||
|
|
04a27d5778 | ||
|
|
70a2067a06 | ||
|
|
4a13c01817 | ||
|
|
151c2b573c | ||
|
|
4cdb3f4c6a | ||
|
|
3cf0384bc5 | ||
|
|
ad2f6ebe93 | ||
|
|
178a429f26 | ||
|
|
71194d5b4e | ||
|
|
771c530b85 | ||
|
|
78a6b622fb | ||
|
|
0177bbebe0 | ||
|
|
8deed4a9cd | ||
|
|
60b2576ce8 | ||
|
|
90cc58a8fe | ||
|
|
d79f750e30 | ||
|
|
9f9ab01508 | ||
|
|
4dc89c9082 | ||
|
|
500349a8bd | ||
|
|
e299f3e510 | ||
|
|
6114f4644f | ||
|
|
eb664404e1 | ||
|
|
370f645cf0 | ||
|
|
bd5b18a163 | ||
|
|
c51b0c6a41 | ||
|
|
d56f6e75f9 | ||
|
|
c743348872 | ||
|
|
578049bfb6 | ||
|
|
564cd37628 | ||
|
|
60ecf91935 | ||
|
|
7b452e93b2 | ||
|
|
0bc1f7bf8c | ||
|
|
62c2421d85 | ||
|
|
f2edf56d02 | ||
|
|
376e5aeb45 | ||
|
|
3d1c7e0bc1 | ||
|
|
a2603f882d | ||
|
|
0c1dcafc35 | ||
|
|
d5108f8007 | ||
|
|
ebeca9aa04 | ||
|
|
7307d9f7f1 | ||
|
|
eac7cdae1c | ||
|
|
7e548cb133 | ||
|
|
0a61512fc7 | ||
|
|
479f2010a9 | ||
|
|
55796932c4 | ||
|
|
9267adf79a | ||
|
|
cc2f86cb06 | ||
|
|
14c6895135 | ||
|
|
83f6647352 | ||
|
|
792ecee399 | ||
|
|
93a1ef6bdb | ||
|
|
62607b16f8 | ||
|
|
ca60376447 | ||
|
|
3cc4b5db79 | ||
|
|
f8179c83e7 | ||
|
|
b2233f61e4 | ||
|
|
a7e2f776e4 | ||
|
|
50d672892c | ||
|
|
e6154db6e5 | ||
|
|
34e8f57f7d | ||
|
|
4a8c089fa9 | ||
|
|
b3aa5ee247 | ||
|
|
64bf98a7d3 | ||
|
|
e8e38beeb8 | ||
|
|
de96a96ac6 | ||
|
|
ee3ad17163 | ||
|
|
fafd8a5d51 | ||
|
|
c5879f17f8 | ||
|
|
999cbd314c | ||
|
|
a9d34a223a | ||
|
|
3381030ed5 | ||
|
|
2bacc6cfe8 | ||
|
|
973c936514 | ||
|
|
c5121a7fc5 | ||
|
|
37bf0f6b53 | ||
|
|
203d51cdbf | ||
|
|
9669d8eb8b | ||
|
|
1b7571be5b | ||
|
|
614ff2a30e | ||
|
|
5b11671997 | ||
|
|
5158e08901 | ||
|
|
2cb9c7211e | ||
|
|
24e26c95ff | ||
|
|
b8286af8fa | ||
|
|
735279c27c | ||
|
|
d75be22d1f | ||
|
|
40f1b1c665 | ||
|
|
d076c875ed | ||
|
|
771c7fe863 | ||
|
|
369454c12a | ||
|
|
1784eacf58 | ||
|
|
e86f5b3b7c | ||
|
|
80ff6cda04 | ||
|
|
dff96cfd95 | ||
|
|
31d244ef78 | ||
|
|
8325a84ab2 | ||
|
|
793839c7d5 | ||
|
|
23cf87dbc0 | ||
|
|
7171de336d | ||
|
|
e206cfe6d6 | ||
|
|
1a71cc9223 | ||
|
|
ed6fcf5ae7 | ||
|
|
bb31693a4d | ||
|
|
d15c8b16f3 | ||
|
|
8b9c932b80 | ||
|
|
c10e8f5f9a | ||
|
|
cd27e43994 | ||
|
|
20614cf64b | ||
|
|
c69c02bcb3 | ||
|
|
21b177cbb4 | ||
|
|
31a7e48768 | ||
|
|
fd3d60ed7d | ||
|
|
e2e369a463 | ||
|
|
0c3304f041 | ||
|
|
0d4b9b4bce | ||
|
|
1192bf6a87 | ||
|
|
2366a91e8d | ||
|
|
2c4e46c630 | ||
|
|
a989e296b0 | ||
|
|
26648dbcd2 | ||
|
|
147d759d35 | ||
|
|
f6b07c5609 | ||
|
|
29fa7a053f | ||
|
|
f380f245c6 | ||
|
|
1824a30cde | ||
|
|
8ab86fd6bb | ||
|
|
2c7bdc54c1 | ||
|
|
39b57da42b | ||
|
|
a98a9fd97a | ||
|
|
093a5c1019 | ||
|
|
ba25acaab9 | ||
|
|
9783802370 | ||
|
|
47bb02ac08 | ||
|
|
1f952d81aa | ||
|
|
0632019e44 | ||
|
|
bc14d0f580 | ||
|
|
4bc3998010 | ||
|
|
cd9c2d1988 | ||
|
|
5d597c22bf | ||
|
|
574144f9b1 | ||
|
|
b26bee3524 | ||
|
|
c835cf7829 | ||
|
|
a1d819dcb6 | ||
|
|
2234cc9334 | ||
|
|
cabd1506b7 | ||
|
|
a7c2e321bf | ||
|
|
31b75179bd | ||
|
|
06152f3131 | ||
|
|
c82f8c997f | ||
|
|
f06840f4b8 | ||
|
|
11c3d6d056 | ||
|
|
814a566845 | ||
|
|
f3bcaf2710 | ||
|
|
f9dba9266f | ||
|
|
00f7df3982 | ||
|
|
b873a77409 | ||
|
|
cd5a26398a | ||
|
|
5e8a614d82 | ||
|
|
bad601edb1 | ||
|
|
58297219a8 | ||
|
|
087b191e5b | ||
|
|
de4468a15a | ||
|
|
b73de087d2 | ||
|
|
39fd092055 | ||
|
|
dec88bd601 | ||
|
|
bc9975baa1 | ||
|
|
d2d401883e | ||
|
|
3b3ac1688a | ||
|
|
6bb0ca22d0 | ||
|
|
e157bc7b97 | ||
|
|
578da0f1a7 | ||
|
|
8f0dd0b0c6 | ||
|
|
9e05bc4fad | ||
|
|
4cee341ce5 | ||
|
|
c40192aa46 | ||
|
|
651748cd4e | ||
|
|
9e8f8357b1 | ||
|
|
3cc4c07fa1 | ||
|
|
8ed52af203 | ||
|
|
8e9a941b5d | ||
|
|
4538a4d33a | ||
|
|
a8469456ce | ||
|
|
6ca25f913a | ||
|
|
da74c7df8a | ||
|
|
278aa87753 | ||
|
|
0f77294718 | ||
|
|
38df160a5e | ||
|
|
dc61a3307c | ||
|
|
2724f8d3c5 | ||
|
|
36e1c1eff0 | ||
|
|
81f830fe23 | ||
|
|
6a42482b92 | ||
|
|
8be83c278b | ||
|
|
eb82980cbc | ||
|
|
08a6d28868 | ||
|
|
37afbc8e9d | ||
|
|
12546b3f17 | ||
|
|
70821c5d26 | ||
|
|
2f7725c5a9 | ||
|
|
a96d26c3bd | ||
|
|
a9f37c9238 | ||
|
|
239ebcdcb8 | ||
|
|
9e6e62c5e8 | ||
|
|
3a0e9f422e | ||
|
|
ef8beb9310 | ||
|
|
260094a666 | ||
|
|
659f9a8f18 | ||
|
|
cf2e3c8018 | ||
|
|
a05901e792 | ||
|
|
0928ec5c4c | ||
|
|
ef843d02c0 | ||
|
|
07cdc6bf2f | ||
|
|
832c1ef83c | ||
|
|
f5d0fc8672 | ||
|
|
4c0c917fb5 | ||
|
|
9e5ac261e2 | ||
|
|
06abfc4337 | ||
|
|
c9c54200aa | ||
|
|
c5f948099d | ||
|
|
a78cb59bc3 | ||
|
|
fb5ff24747 | ||
|
|
acb2ede658 | ||
|
|
05b1bffeef | ||
|
|
b330b55054 | ||
|
|
9a67e63e9b | ||
|
|
0d629a5385 | ||
|
|
4e6a214581 | ||
|
|
333c591771 | ||
|
|
e6645101ad | ||
|
|
48788986cb | ||
|
|
d400beffb6 | ||
|
|
7762135cb1 | ||
|
|
d0d594df69 | ||
|
|
ed97f2d786 | ||
|
|
f3a724237f | ||
|
|
aeb3a308a8 | ||
|
|
62cc97bc44 | ||
|
|
20928c7a7f | ||
|
|
6f4a865604 | ||
|
|
e7919416d5 | ||
|
|
cf8a512af4 | ||
|
|
3efef7bbbb | ||
|
|
65d8e01529 | ||
|
|
e0dd7a9b4b | ||
|
|
3a32d220dd | ||
|
|
5efaf7b23e | ||
|
|
91375a5447 | ||
|
|
f6574d346c | ||
|
|
b9b29b11d4 | ||
|
|
54d062156d | ||
|
|
4dc24e48d8 | ||
|
|
3a24464c90 | ||
|
|
f2ad9395fc | ||
|
|
61539416e5 | ||
|
|
ecf7c1f3f3 | ||
|
|
d18886ed7c | ||
|
|
07cc529f6e | ||
|
|
01b87bfbfd | ||
|
|
b126f0cb79 | ||
|
|
10c6901a04 | ||
|
|
c26efb111e | ||
|
|
f07cbe0087 | ||
|
|
7d72e196e0 | ||
|
|
9bc935da96 |
15
.babelrc
Normal file
15
.babelrc
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"comments": false,
|
||||
"compact": true,
|
||||
"optional": [
|
||||
// This options are experimental for now and may (will) break the
|
||||
// code.
|
||||
// "minification.deadCodeElimination",
|
||||
// "minification.inlineExpressions",
|
||||
|
||||
"es7.asyncFunctions",
|
||||
"es7.decorators",
|
||||
"es7.functionBind",
|
||||
"runtime"
|
||||
]
|
||||
}
|
||||
@@ -1,30 +1,65 @@
|
||||
# EditorConfig is awesome: http://EditorConfig.org
|
||||
# http://EditorConfig.org
|
||||
#
|
||||
# Julien Fontanet's configuration
|
||||
# https://gist.github.com/julien-f/8096213
|
||||
|
||||
# top-most EditorConfig file
|
||||
# Top-most EditorConfig file.
|
||||
root = true
|
||||
|
||||
# Unix-style newlines with a newline ending every file
|
||||
#
|
||||
# Tab indentation (size of 4 spaces)
|
||||
# Common config.
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_size = 2
|
||||
indent_style = tab
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespaces = true
|
||||
|
||||
# YAML only allows spaces.
|
||||
[*.yaml]
|
||||
# CoffeeScript
|
||||
#
|
||||
# https://github.com/polarmobile/coffeescript-style-guide/blob/master/README.md
|
||||
[*.{,lit}coffee]
|
||||
indent_size = 2
|
||||
indent_style = space
|
||||
|
||||
# Special settings for NPM file.
|
||||
# Markdown
|
||||
[*.{md,mdwn,mdown,markdown}]
|
||||
indent_size = 4
|
||||
indent_style = space
|
||||
|
||||
# Package.json
|
||||
#
|
||||
# This indentation style is the one used by npm.
|
||||
[/package.json]
|
||||
indent_size = 2
|
||||
indent_style = space
|
||||
|
||||
# For CoffeeScript files, we follow this Polarmobile style guide (https://github.com/polarmobile/coffeescript-style-guide/blob/master/README.md).
|
||||
[*{,.spec}.{,lit}coffee]
|
||||
# Jade
|
||||
[*.jade]
|
||||
indent_size = 2
|
||||
indent_style = space
|
||||
|
||||
# JavaScript
|
||||
#
|
||||
# Two spaces seems to be the standard most common style, at least in
|
||||
# Node.js (http://nodeguide.com/style.html#tabs-vs-spaces).
|
||||
[*.js]
|
||||
indent_size = 2
|
||||
indent_style = space
|
||||
|
||||
# Less
|
||||
[*.less]
|
||||
indent_size = 2
|
||||
indent_style = space
|
||||
|
||||
# Sass
|
||||
#
|
||||
# Style used for http://libsass.com
|
||||
[*.s[ac]ss]
|
||||
indent_size = 2
|
||||
indent_style = space
|
||||
|
||||
# YAML
|
||||
#
|
||||
# Only spaces are allowed.
|
||||
[*.yaml]
|
||||
indent_size = 2
|
||||
indent_style = space
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,2 +1,5 @@
|
||||
/dist/
|
||||
/node_modules/
|
||||
npm-debug.log
|
||||
|
||||
.xo-server.*
|
||||
|
||||
205
.jshintrc
205
.jshintrc
@@ -1,126 +1,93 @@
|
||||
{
|
||||
// --------------------------------------------------------------------
|
||||
// JSHint Configuration, Node.js Edition
|
||||
// --------------------------------------------------------------------
|
||||
//
|
||||
// This is an options template for [JSHint][1], forked from
|
||||
// haschek's [JSHint template][2]:
|
||||
//
|
||||
// * the environment has been changed to `node`;
|
||||
// * recent options were added;
|
||||
// * coding style has been adapted to node (e.g. 2 spaces
|
||||
// indenting, global use strict).
|
||||
//
|
||||
// [1]: http://www.jshint.com/
|
||||
// [2]: https://gist.github.com/haschek/2595796
|
||||
//
|
||||
// @author Julien Fontanet <julien.fontanet@isonoe.net>
|
||||
// @license http://unlicense.org/
|
||||
// Julien Fontanet JSHint configuration
|
||||
// https://gist.github.com/julien-f/8095615
|
||||
//
|
||||
// Changes from defaults:
|
||||
// - all enforcing options (except `++` & `--`) enabled
|
||||
// - single quotes
|
||||
// - indentation set to 2 instead of 4
|
||||
// - almost all relaxing options disabled
|
||||
// - environments are set to Node.js
|
||||
//
|
||||
// See http://jshint.com/docs/ for more details
|
||||
|
||||
// == Enforcing Options ===============================================
|
||||
//
|
||||
// These options tell JSHint to be more strict towards your code. Use
|
||||
// them if you want to allow only a safe subset of JavaScript, very
|
||||
// useful when your codebase is shared with a big number of developers
|
||||
// with different skill levels.
|
||||
"maxerr" : 50, // {int} Maximum error before stopping
|
||||
|
||||
"bitwise" : true, // Prohibit bitwise operators (&, |, ^, etc.).
|
||||
"camelcase" : true, // Require variable names to use either camelCase or UPPER_CASE styles.
|
||||
"curly" : true, // Require {} for every new block or scope.
|
||||
"eqeqeq" : true, // Require triple equals i.e. `===`.
|
||||
"forin" : true, // Tolerate `for in` loops without `hasOwnPrototype`.
|
||||
"freeze" : true, // Prohibit modification of native objects' prototypes.
|
||||
"immed" : true, // Require immediate invocations to be wrapped in parens e.g. `( function(){}() );`
|
||||
"indent" : 2, // Specify indentation spacing
|
||||
"latedef" : true, // Prohibit variable use before definition.
|
||||
"newcap" : true, // Require capitalization of all constructor functions e.g. `new F()`.
|
||||
"noarg" : true, // Prohibit use of `arguments.caller` and `arguments.callee`.
|
||||
"noempty" : true, // Prohibit use of empty blocks.
|
||||
"nonew" : true, // Prohibit use of constructors for side-effects.
|
||||
"plusplus" : false, // Prohibit use of `++` & `--`.
|
||||
"quotmark" : "'", // Require single quotes.
|
||||
"undef" : true, // Require all non-global variables be declared before they are used.
|
||||
"unused" : true, // Prohibit unused variables.
|
||||
"strict" : true, // Require `use strict` pragma in every function.
|
||||
"trailing" : true, // Prohibit trailing whitespaces.
|
||||
"maxparams" : 4, // Prohibit more than 4 parameters per function definition.
|
||||
"maxdepth" : 3, // Prohibit nesting more than 3 control blocks.
|
||||
"maxstatements" : 20, // Prohibit more than 20 statements per function.
|
||||
"maxcomplexity" : 7, // Prohibit having to much branches in your code.
|
||||
"maxlen" : 80, // Prohibit line with more than 80 characters.
|
||||
// Enforcing
|
||||
"bitwise" : true, // true: Prohibit bitwise operators (&, |, ^, etc.)
|
||||
"camelcase" : true, // true: Identifiers must be in camelCase
|
||||
"curly" : true, // true: Require {} for every new block or scope
|
||||
"eqeqeq" : true, // true: Require triple equals (===) for comparison
|
||||
"forin" : true, // true: Require filtering for..in loops with obj.hasOwnProperty()
|
||||
"freeze" : true, // true: Prohibit overwriting prototypes of native objects (Array, Date, ...)
|
||||
"immed" : true, // true: Require immediate invocations to be wrapped in parens e.g. `(function () { } ());`
|
||||
"indent" : 2, // {int} Number of spaces to use for indentation
|
||||
"latedef" : true, // true: Require variables/functions to be defined before being used
|
||||
"newcap" : true, // true: Require capitalization of all constructor functions e.g. `new F()`
|
||||
"noarg" : true, // true: Prohibit use of `arguments.caller` and `arguments.callee`
|
||||
"noempty" : true, // true: Prohibit use of empty blocks
|
||||
"nonbsp" : true, // true: Prohibit use of non breakable spaces
|
||||
"nonew" : true, // true: Prohibit use of constructors for side-effects (without assignment)
|
||||
"plusplus" : false, // true: Prohibit use of `++` & `--`
|
||||
"quotmark" : "single", // Quotation mark consistency:
|
||||
// false : do nothing (default)
|
||||
// true : ensure whatever is used is consistent
|
||||
// "single" : require single quotes
|
||||
// "double" : require double quotes
|
||||
"undef" : true, // true: Require all non-global variables to be declared (prevents global leaks)
|
||||
"unused" : true, // true: Require all defined variables be used
|
||||
"strict" : false, // true: Requires all functions run in ES5 Strict Mode
|
||||
"maxcomplexity" : 7, // {int} Max cyclomatic complexity per function
|
||||
"maxdepth" : 3, // {int} Max depth of nested blocks (within functions)
|
||||
"maxlen" : 80, // {int} Max number of characters per line
|
||||
"maxparams" : 4, // {int} Max number of formal params allowed per function
|
||||
"maxstatements" : 20, // {int} Max number statements per function
|
||||
|
||||
// == Relaxing Options ================================================
|
||||
//
|
||||
// These options allow you to suppress certain types of warnings. Use
|
||||
// them only if you are absolutely positive that you know what you are
|
||||
// doing.
|
||||
// Relaxing
|
||||
"asi" : false, // true: Tolerate Automatic Semicolon Insertion (no semicolons)
|
||||
"boss" : false, // true: Tolerate assignments where comparisons would be expected
|
||||
"debug" : false, // true: Allow debugger statements e.g. browser breakpoints.
|
||||
"eqnull" : false, // true: Tolerate use of `== null`
|
||||
"esnext" : true, // true: Allow ES.next (ES6) syntax (ex: `const`)
|
||||
"evil" : false, // true: Tolerate use of `eval` and `new Function()`
|
||||
"expr" : false, // true: Tolerate `ExpressionStatement` as Programs
|
||||
"funcscope" : false, // true: Tolerate defining variables inside control statements
|
||||
"globalstrict" : false, // true: Allow global "use strict" (also enables 'strict')
|
||||
"iterator" : false, // true: Tolerate using the `__iterator__` property
|
||||
"lastsemic" : false, // true: Tolerate omitting a semicolon for the last statement of a 1-line block
|
||||
"laxbreak" : false, // true: Tolerate possibly unsafe line breakings
|
||||
"laxcomma" : false, // true: Tolerate comma-first style coding
|
||||
"loopfunc" : false, // true: Tolerate functions being defined in loops
|
||||
"moz" : false, // true: Allow Mozilla specific syntax (extends and overrides esnext features)
|
||||
// (ex: `for each`, multiple try/catch, function expression…)
|
||||
"multistr" : false, // true: Tolerate multi-line strings
|
||||
"notypeof" : false, // true: Tolerate typeof comparison with unknown values.
|
||||
"proto" : false, // true: Tolerate using the `__proto__` property
|
||||
"scripturl" : false, // true: Tolerate script-targeted URLs
|
||||
"shadow" : false, // true: Allows re-define variables later in code e.g. `var x=1; x=2;`
|
||||
"sub" : false, // true: Tolerate using `[]` notation when it can still be expressed in dot notation
|
||||
"supernew" : false, // true: Tolerate `new function () { ... };` and `new Object;`
|
||||
"validthis" : false, // true: Tolerate using this in a non-constructor function
|
||||
"noyield" : false, // true: Tolerate generators without yields
|
||||
|
||||
"asi" : false, // Tolerate Automatic Semicolon Insertion (no semicolons).
|
||||
"boss" : false, // Tolerate assignments inside if, for & while. Usually conditions & loops are for comparison, not assignments.
|
||||
"debug" : false, // Allow debugger statements e.g. browser breakpoints.
|
||||
"eqnull" : false, // Tolerate use of `== null`.
|
||||
"es5" : false, // Allow EcmaScript 5 syntax.
|
||||
"esnext" : true, // Allow ES.next specific features such as `const` and `let`.
|
||||
"evil" : false, // Tolerate use of `eval`.
|
||||
"expr" : true, // Tolerate `ExpressionStatement` as Programs. (Allowed for Mocha.)
|
||||
"funcscope" : false, // Tolerate declarations of variables inside of control structures while accessing them later from the outside.
|
||||
"gcl" : false, // Makes JSHint compatible with Google Closure Compiler.
|
||||
"globalstrict" : true, // Allow global "use strict" (also enables 'strict').
|
||||
"iterator" : false, // Allow usage of __iterator__ property.
|
||||
"lastsemic" : false, // Tolerat missing semicolons when the it is omitted for the last statement in a one-line block.
|
||||
"laxbreak" : false, // Tolerate unsafe line breaks e.g. `return [\n] x` without semicolons.
|
||||
"laxcomma" : false, // Suppress warnings about comma-first coding style.
|
||||
"loopfunc" : false, // Allow functions to be defined within loops.
|
||||
"maxerr" : 50, // Maximum errors before stopping.
|
||||
"moz" : false, // Tolerate Mozilla JavaScript extensions.
|
||||
"notypeof" : false, // Tolerate invalid typeof values.
|
||||
"multistr" : false, // Tolerate multi-line strings.
|
||||
"proto" : false, // Tolerate __proto__ property. This property is deprecated.
|
||||
"scripturl" : false, // Tolerate script-targeted URLs.
|
||||
"smarttabs" : false, // Tolerate mixed tabs and spaces when the latter are used for alignmnent only.
|
||||
"shadow" : false, // Allows re-define variables later in code e.g. `var x=1; x=2;`.
|
||||
"sub" : false, // Tolerate all forms of subscript notation besides dot notation e.g. `dict['key']` instead of `dict.key`.
|
||||
"supernew" : false, // Tolerate `new function () { ... };` and `new Object;`.
|
||||
"validthis" : false, // Tolerate strict violations when the code is running in strict mode and you use this in a non-constructor function.
|
||||
// Environments
|
||||
"browser" : false, // Web Browser (window, document, etc)
|
||||
"browserify" : false, // Browserify (node.js code in the browser)
|
||||
"couch" : false, // CouchDB
|
||||
"devel" : false, // Development/debugging (alert, confirm, etc)
|
||||
"dojo" : false, // Dojo Toolkit
|
||||
"jquery" : false, // jQuery
|
||||
"mocha" : false, // mocha
|
||||
"mootools" : false, // MooTools
|
||||
"node" : true, // Node.js
|
||||
"nonstandard" : false, // Widely adopted globals (escape, unescape, etc)
|
||||
"phantom" : false, // PhantomJS
|
||||
"prototypejs" : false, // Prototype and Scriptaculous
|
||||
"rhino" : false, // Rhino
|
||||
"worker" : false, // Web Workers
|
||||
"wsh" : false, // Windows Scripting Host
|
||||
"yui" : false, // Yahoo User Interface
|
||||
|
||||
// == Environments ====================================================
|
||||
//
|
||||
// These options pre-define global variables that are exposed by
|
||||
// popular JavaScript libraries and runtime environments—such as
|
||||
// browser or node.js.
|
||||
|
||||
"browser" : false, // Standard browser globals e.g. `window`, `document`.
|
||||
"couch" : false, // Enable globals exposed by CouchDB.
|
||||
"devel" : false, // Allow development statements e.g. `console.log();`.
|
||||
"dojo" : false, // Enable globals exposed by Dojo Toolkit.
|
||||
"jquery" : false, // Enable globals exposed by jQuery JavaScript library.
|
||||
"mootools" : false, // Enable globals exposed by MooTools JavaScript framework.
|
||||
"node" : true, // Enable globals available when code is running inside of the NodeJS runtime environment.
|
||||
"nonstandard" : false, // Define non-standard but widely adopted globals such as escape and unescape.
|
||||
"phantom" : false, // Enable globals exposed by PhantomJS.
|
||||
"prototypejs" : false, // Enable globals exposed by Prototype JavaScript framework.
|
||||
"rhino" : false, // Enable globals available when your code is running inside of the Rhino runtime environment.
|
||||
"worker" : false, // Enable globals exposed when running inside a Web Worker.
|
||||
"wsh" : false, // Enable globals available when your code is running as a script for the Windows Script Host.
|
||||
"yui" : false, // Enable globals exposed by YUI.
|
||||
|
||||
// == JSLint Legacy ===================================================
|
||||
//
|
||||
// These options are legacy from JSLint. Aside from bug fixes they will
|
||||
// not be improved in any way and might be removed at any point.
|
||||
|
||||
"nomen" : false, // Prohibit use of initial or trailing underbars in names.
|
||||
"onevar" : false, // Allow only one `var` statement per function.
|
||||
"passfail" : false, // Stop on first error.
|
||||
"white" : false, // Check against strict whitespace and indentation rules.
|
||||
|
||||
"globals": {
|
||||
// Mocha.
|
||||
"after" : false,
|
||||
"afterEach" : false,
|
||||
"before" : false,
|
||||
"beforeEach" : false,
|
||||
"describe" : false,
|
||||
"it" : false
|
||||
}
|
||||
// Custom Globals
|
||||
"globals" : {} // additional predefined global variables
|
||||
}
|
||||
|
||||
18
README.md
18
README.md
@@ -1,5 +1,7 @@
|
||||
# Xen Orchestra Server
|
||||
|
||||

|
||||
|
||||
XO-Server is part of [Xen Orchestra](https://github.com/vatesfr/xo), a web interface for XenServer or XAPI enabled hosts.
|
||||
|
||||
It contains all the logic of XO and handles:
|
||||
@@ -16,7 +18,21 @@ ___
|
||||
|
||||
## Installation
|
||||
|
||||
Manual install procedure is [available here](https://github.com/vatesfr/xo/blob/master/installation.md)
|
||||
Manual install procedure is [available here](https://github.com/vatesfr/xo/blob/master/doc/installation/README.md#installation).
|
||||
|
||||
## Compilation
|
||||
|
||||
Production build:
|
||||
|
||||
```
|
||||
$ npm run build
|
||||
```
|
||||
|
||||
Development build:
|
||||
|
||||
```
|
||||
$ npm run dev
|
||||
```
|
||||
|
||||
## How to report a bug?
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
'use strict';
|
||||
'use strict'
|
||||
|
||||
//====================================================================
|
||||
// ===================================================================
|
||||
|
||||
require('exec-promise')(require('../'));
|
||||
require('exec-promise')(require('../'))
|
||||
|
||||
1
config/.gitignore
vendored
1
config/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
/local.yaml
|
||||
63
gulpfile.js
Normal file
63
gulpfile.js
Normal file
@@ -0,0 +1,63 @@
|
||||
'use strict'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
var gulp = require('gulp')
|
||||
|
||||
var babel = require('gulp-babel')
|
||||
var coffee = require('gulp-coffee')
|
||||
var plumber = require('gulp-plumber')
|
||||
var sourceMaps = require('gulp-sourcemaps')
|
||||
var watch = require('gulp-watch')
|
||||
|
||||
// ===================================================================
|
||||
|
||||
var SRC_DIR = __dirname + '/src'
|
||||
var DIST_DIR = __dirname + '/dist'
|
||||
|
||||
var PRODUCTION = process.argv.indexOf('--production') !== -1
|
||||
|
||||
// ===================================================================
|
||||
|
||||
function src (patterns) {
|
||||
return PRODUCTION ?
|
||||
gulp.src(patterns, {
|
||||
base: SRC_DIR,
|
||||
cwd: SRC_DIR
|
||||
}) :
|
||||
watch(patterns, {
|
||||
base: SRC_DIR,
|
||||
cwd: SRC_DIR,
|
||||
ignoreInitial: false,
|
||||
verbose: true
|
||||
})
|
||||
.pipe(plumber())
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
gulp.task(function buildCoffee () {
|
||||
return src('**/*.coffee')
|
||||
.pipe(sourceMaps.init())
|
||||
.pipe(coffee({
|
||||
bare: true
|
||||
}))
|
||||
|
||||
// Necessary to correctly compile generators.
|
||||
.pipe(babel())
|
||||
|
||||
.pipe(sourceMaps.write('.'))
|
||||
.pipe(gulp.dest(DIST_DIR))
|
||||
})
|
||||
|
||||
gulp.task(function buildEs6 () {
|
||||
return src('**/*.js')
|
||||
.pipe(sourceMaps.init())
|
||||
.pipe(babel())
|
||||
.pipe(sourceMaps.write('.'))
|
||||
.pipe(gulp.dest(DIST_DIR))
|
||||
})
|
||||
|
||||
// ===================================================================
|
||||
|
||||
gulp.task('build', gulp.parallel('buildCoffee', 'buildEs6'))
|
||||
16
index.js
16
index.js
@@ -1,6 +1,14 @@
|
||||
'use strict';
|
||||
'use strict'
|
||||
|
||||
//====================================================================
|
||||
// ===================================================================
|
||||
|
||||
require('coffee-script/register');
|
||||
module.exports = require('./src/main');
|
||||
// Enable xo logs by default.
|
||||
if (process.env.DEBUG === undefined) {
|
||||
process.env.DEBUG = 'app-conf,xen-api,xo:*'
|
||||
}
|
||||
|
||||
// Enable source maps support for traces.
|
||||
require('source-map-support').install()
|
||||
|
||||
// Import the real main module.
|
||||
module.exports = require('./dist')
|
||||
|
||||
114
package.json
114
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "xo-server",
|
||||
"version": "3.5.0-alpha1",
|
||||
"license": "AGPL3",
|
||||
"version": "4.0.3",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "Server part of Xen-Orchestra",
|
||||
"keywords": [
|
||||
"xen",
|
||||
@@ -15,6 +15,11 @@
|
||||
},
|
||||
"author": "Julien Fontanet <julien.fontanet@vates.fr>",
|
||||
"preferGlobal": true,
|
||||
"files": [
|
||||
"bin/",
|
||||
"dist/",
|
||||
"index.js"
|
||||
],
|
||||
"directories": {
|
||||
"bin": "bin"
|
||||
},
|
||||
@@ -23,38 +28,87 @@
|
||||
"url": "git://github.com/vatesfr/xo-server.git"
|
||||
},
|
||||
"dependencies": {
|
||||
"backoff": "~2.4.0",
|
||||
"bluebird": "^2.2.2",
|
||||
"coffee-script": "~1.7.1",
|
||||
"connect": "^3.1.0",
|
||||
"event-to-promise": "^0.3.0",
|
||||
"exec-promise": "^0.3.0",
|
||||
"extendable": "~0.0.6",
|
||||
"fibers": "~1.0.1",
|
||||
"hashy": "~0.3.6",
|
||||
"hiredis": "~0.1.17",
|
||||
"http-server-plus": "^0.2.3",
|
||||
"js-yaml": "~3.1.0",
|
||||
"nconf": "~0.6.9",
|
||||
"redis": "~0.11.0",
|
||||
"require-tree": "~0.3.3",
|
||||
"schema-inspector": "^1.4.5",
|
||||
"serve-static": "^1.4.0",
|
||||
"then-redis": "~0.3.12",
|
||||
"underscore": "~1.6.0",
|
||||
"ws": "~0.4.31",
|
||||
"xml2js": "~0.4.4",
|
||||
"xmlrpc": "~1.2.0"
|
||||
"app-conf": "^0.3.4",
|
||||
"babel-runtime": "^5",
|
||||
"base64url": "1.0.4",
|
||||
"blocked": "^1.1.0",
|
||||
"bluebird": "^2.9.14",
|
||||
"connect": "^3.3.5",
|
||||
"debug": "^2.1.3",
|
||||
"event-to-promise": "^0.3.2",
|
||||
"exec-promise": "^0.5.1",
|
||||
"fs-promise": "^0.3.1",
|
||||
"got": "^3.2.0",
|
||||
"graceful-fs": "^3.0.6",
|
||||
"hashy": "~0.4.2",
|
||||
"http-server-plus": "^0.5.1",
|
||||
"human-format": "^0.3.0",
|
||||
"js-yaml": "^3.2.7",
|
||||
"json-rpc-peer": "^0.9.2",
|
||||
"json-rpc-protocol": "^0.9.0",
|
||||
"julien-f-unzip": "^0.2.1",
|
||||
"lodash.assign": "^3.0.0",
|
||||
"lodash.bind": "^3.0.0",
|
||||
"lodash.difference": "^3.2.0",
|
||||
"lodash.endswith": "^3.0.2",
|
||||
"lodash.filter": "^3.1.0",
|
||||
"lodash.find": "^3.0.0",
|
||||
"lodash.findindex": "^3.0.0",
|
||||
"lodash.foreach": "^3.0.1",
|
||||
"lodash.has": "^3.0.0",
|
||||
"lodash.includes": "^3.1.1",
|
||||
"lodash.isarray": "^3.0.0",
|
||||
"lodash.isempty": "^3.0.0",
|
||||
"lodash.isfunction": "^3.0.1",
|
||||
"lodash.isobject": "^3.0.0",
|
||||
"lodash.isstring": "^3.0.0",
|
||||
"lodash.keys": "^3.0.4",
|
||||
"lodash.map": "^3.0.0",
|
||||
"lodash.pick": "^3.0.0",
|
||||
"lodash.result": "^3.0.0",
|
||||
"lodash.startswith": "^3.0.1",
|
||||
"make-error": "^1",
|
||||
"multikey-hash": "^1.0.1",
|
||||
"proxy-http-request": "0.1.0",
|
||||
"redis": "^0.12.1",
|
||||
"request": "^2.53.0",
|
||||
"require-tree": "~1.0.1",
|
||||
"schema-inspector": "^1.5.1",
|
||||
"serve-static": "^1.9.2",
|
||||
"source-map-support": "^0.3.1",
|
||||
"then-redis": "~1.3.0",
|
||||
"ws": "~0.7.1",
|
||||
"xen-api": "^0.5.4",
|
||||
"xml2js": "~0.4.6",
|
||||
"xo-collection": "^0.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"chai": "~1.9.1",
|
||||
"glob": "~4.0.4",
|
||||
"mocha": "^1.21.0",
|
||||
"node-inspector": "^0.7.4",
|
||||
"sinon": "^1.10.3"
|
||||
"babel-eslint": "^3.1.9",
|
||||
"chai": "~2.1.2",
|
||||
"gulp": "git://github.com/gulpjs/gulp#4.0",
|
||||
"gulp-babel": "^5",
|
||||
"gulp-coffee": "^2.3.1",
|
||||
"gulp-plumber": "^1.0.0",
|
||||
"gulp-sourcemaps": "^1.5.1",
|
||||
"gulp-watch": "^4.2.2",
|
||||
"in-publish": "^1.1.1",
|
||||
"mocha": "^2.2.1",
|
||||
"node-inspector": "^0.10.1",
|
||||
"sinon": "^1.14.1",
|
||||
"standard": "^4.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "gulp build --production",
|
||||
"dev": "gulp build",
|
||||
"lint": "standard",
|
||||
"prepublish": "in-publish && npm run build || in-install",
|
||||
"start": "node bin/xo-server",
|
||||
"test": "coffee run-tests"
|
||||
"test": "mocha 'dist/**/*.spec.js'"
|
||||
},
|
||||
"standard": {
|
||||
"ignore": [
|
||||
"dist/**"
|
||||
],
|
||||
"parser": "babel-eslint"
|
||||
}
|
||||
}
|
||||
|
||||
29
run-tests
29
run-tests
@@ -1,29 +0,0 @@
|
||||
#!/usr/bin/env coffee
|
||||
|
||||
# Tests runner.
|
||||
$mocha = require 'mocha'
|
||||
|
||||
# Used to find the specification files.
|
||||
$glob = require 'glob'
|
||||
|
||||
#=====================================================================
|
||||
|
||||
do ->
|
||||
# Instantiates the tests runner.
|
||||
mocha = new $mocha {
|
||||
reporter: 'spec'
|
||||
}
|
||||
|
||||
# Processes arguments.
|
||||
do ->
|
||||
{argv} = process
|
||||
i = 2
|
||||
n = argv.length
|
||||
mocha.grep argv[i++] while i < n
|
||||
|
||||
$glob 'src/**/*.spec.{coffee,js}', (error, files) ->
|
||||
console.error(error) if error
|
||||
|
||||
mocha.addFile file for file in files
|
||||
|
||||
mocha.run()
|
||||
@@ -1,4 +1,12 @@
|
||||
# Note: Relative paths will be resolved from XO-Server's directory.
|
||||
# Example XO-Server configuration.
|
||||
|
||||
# This file is automatically looking for at the following places:
|
||||
# - `$HOME/.config/xo-server/config.yaml`
|
||||
# - `/etc/xo-server/config.yaml`
|
||||
#
|
||||
# The first entries have priority.
|
||||
|
||||
# Note: paths are relative to the configuration file.
|
||||
|
||||
#=====================================================================
|
||||
|
||||
@@ -44,7 +52,7 @@ http:
|
||||
# Sets it to '127.0.0.1' to listen only on the local host.
|
||||
#
|
||||
# Default: '0.0.0.0' (all addresses)
|
||||
#host: '127.0.0.1'
|
||||
#hostname: '127.0.0.1'
|
||||
|
||||
# Port on which the server is listening on.
|
||||
#
|
||||
@@ -62,7 +70,7 @@ http:
|
||||
# # The only difference is the presence of the certificate and the
|
||||
# # key.
|
||||
|
||||
# #host: '127.0.0.1'
|
||||
# #hostname: '127.0.0.1'
|
||||
# port: 443
|
||||
|
||||
# # File containing the certificate (PEM format).
|
||||
@@ -82,6 +90,10 @@ http:
|
||||
mounts:
|
||||
#'/': '/path/to/xo-web/dist/'
|
||||
|
||||
# List of proxied URLs (HTTP & WebSockets).
|
||||
proxies:
|
||||
# '/any/url': 'http://localhost:54722'
|
||||
|
||||
#=====================================================================
|
||||
|
||||
# Connection to the Redis server.
|
||||
@@ -1,439 +0,0 @@
|
||||
{EventEmitter: $EventEmitter} = require 'events'
|
||||
|
||||
#----------------------------------------------------------------------
|
||||
|
||||
$_ = require 'underscore'
|
||||
|
||||
#=====================================================================
|
||||
|
||||
{$each, $makeFunction, $mapInPlace} = require './utils'
|
||||
|
||||
#=====================================================================
|
||||
|
||||
class $MappedCollection extends $EventEmitter
|
||||
|
||||
# The dispatch function is called whenever a new item has to be
|
||||
# processed and returns the name of the rule to use.
|
||||
#
|
||||
# To change the way it is dispatched, just override this it.
|
||||
dispatch: ->
|
||||
(@genval and (@genval.rule ? @genval.type)) ? 'unknown'
|
||||
|
||||
# This function is called when an item has been dispatched to a
|
||||
# missing rule.
|
||||
#
|
||||
# The default behavior is to throw an error but you may instead
|
||||
# choose to create a rule:
|
||||
#
|
||||
# collection.missingRule = collection.rule
|
||||
missingRule: (name) ->
|
||||
throw new Error "undefined rule “#{name}”"
|
||||
|
||||
# This function is called when the new generator of an existing item has been
|
||||
# matched to a different rule.
|
||||
#
|
||||
# The default behavior is to throw an error as it usually indicates a bug but
|
||||
# you can ignore it.
|
||||
ruleConflict: (rule, item) ->
|
||||
throw new Error "the item “#{item.key}” was of rule “#{item.rule}” "+
|
||||
"but matches to “#{rule}”"
|
||||
|
||||
constructor: ->
|
||||
# Items are stored here indexed by key.
|
||||
#
|
||||
# The prototype of this object is set to `null` to avoid pollution
|
||||
# from enumerable properties of `Object.prototype` and the
|
||||
# performance hit of `hasOwnProperty o`.
|
||||
@_byKey = Object.create null
|
||||
|
||||
# Hooks are stored here indexed by moment.
|
||||
@_hooks = {
|
||||
beforeDispatch: []
|
||||
beforeUpdate: []
|
||||
beforeSave: []
|
||||
afterRule: []
|
||||
}
|
||||
|
||||
# Rules are stored here indexed by name.
|
||||
#
|
||||
# The prototype of this object is set to `null` to avoid pollution
|
||||
# from enumerable properties of `Object.prototype` and to be able
|
||||
# to use the `name of @_rules` syntax.
|
||||
@_rules = Object.create null
|
||||
|
||||
# Register a hook to run at a given point.
|
||||
#
|
||||
# A hook receives as parameter an event object with the following
|
||||
# properties:
|
||||
# - `preventDefault()`: prevents the next default action from
|
||||
# happening;
|
||||
# - `stopPropagation()`: prevents other hooks from being run.
|
||||
#
|
||||
# Note: if a hook throws an exception, `event.stopPropagation()`
|
||||
# then `event.preventDefault()` will be called and the exception
|
||||
# will be forwarded.
|
||||
#
|
||||
# # Item hook
|
||||
#
|
||||
# Valid items related moments are:
|
||||
# - beforeDispatch: even before the item has been dispatched;
|
||||
# - beforeUpdate: after the item has been dispatched but before
|
||||
# updating its value.
|
||||
# - beforeSave: after the item has been updated.
|
||||
#
|
||||
# An item hook is run in the context of the current item.
|
||||
#
|
||||
# # Rule hook
|
||||
#
|
||||
# Valid rules related moments are:
|
||||
# - afterRule: just after a new rule has been defined (even
|
||||
# singleton).
|
||||
#
|
||||
# An item hook is run in the context of the current rule.
|
||||
hook: (name, hook) ->
|
||||
# Allows a nicer syntax for CoffeeScript.
|
||||
if $_.isObject name
|
||||
# Extracts the name and the value from the first property of the
|
||||
# object.
|
||||
do ->
|
||||
object = name
|
||||
return for own name, hook of object
|
||||
|
||||
hooks = @_hooks[name]
|
||||
|
||||
@_assert(
|
||||
hooks?
|
||||
"invalid hook moment “#{name}”"
|
||||
)
|
||||
|
||||
hooks.push hook
|
||||
|
||||
# Register a new singleton rule.
|
||||
#
|
||||
# See the `rule()` method for more information.
|
||||
item: (name, definition) ->
|
||||
# Creates the corresponding rule.
|
||||
rule = @rule name, definition, true
|
||||
|
||||
# Creates the singleton.
|
||||
item = {
|
||||
rule: rule.name
|
||||
key: rule.key() # No context because there is not generator.
|
||||
val: undefined
|
||||
}
|
||||
@_updateItems [item], true
|
||||
|
||||
# Register a new rule.
|
||||
#
|
||||
# If the definition is a function, it will be run in the context of
|
||||
# an item-like object with the following properties:
|
||||
# - `key`: the definition for the key of this item;
|
||||
# - `val`: the definition for the value of this item.
|
||||
#
|
||||
# Warning: The definition function is run only once!
|
||||
rule: (name, definition, singleton = false) ->
|
||||
# Allows a nicer syntax for CoffeeScript.
|
||||
if $_.isObject name
|
||||
# Extracts the name and the definition from the first property
|
||||
# of the object.
|
||||
do ->
|
||||
object = name
|
||||
return for own name, definition of object
|
||||
|
||||
@_assert(
|
||||
name not of @_rules
|
||||
"the rule “#{name}” is already defined"
|
||||
)
|
||||
|
||||
# Extracts the rule definition.
|
||||
if $_.isFunction definition
|
||||
ctx = {
|
||||
name
|
||||
key: undefined
|
||||
data: undefined
|
||||
val: undefined
|
||||
singleton
|
||||
}
|
||||
definition.call ctx
|
||||
else
|
||||
ctx = {
|
||||
name
|
||||
key: definition?.key
|
||||
data: definition?.data
|
||||
val: definition?.val
|
||||
singleton
|
||||
}
|
||||
|
||||
# Runs the `afterRule` hook and returns if the registration has
|
||||
# been prevented.
|
||||
return unless @_runHook 'afterRule', ctx
|
||||
|
||||
{key, data, val} = ctx
|
||||
|
||||
# The default key.
|
||||
key ?= if singleton then -> name else -> @genkey
|
||||
|
||||
# The default value.
|
||||
val ?= -> @genval
|
||||
|
||||
# Makes sure `key` is a function for uniformity.
|
||||
key = $makeFunction key unless $_.isFunction key
|
||||
|
||||
# Register the new rule.
|
||||
@_rules[name] = {
|
||||
name
|
||||
key
|
||||
data
|
||||
val
|
||||
singleton
|
||||
}
|
||||
|
||||
#--------------------------------
|
||||
|
||||
get: (keys, ignoreMissingItems = false) ->
|
||||
if keys is undefined
|
||||
items = $_.map @_byKey, (item) -> item.val
|
||||
else
|
||||
items = @_fetchItems keys, ignoreMissingItems
|
||||
$mapInPlace items, (item) -> item.val
|
||||
|
||||
if $_.isString keys then items[0] else items
|
||||
|
||||
getRaw: (keys, ignoreMissingItems = false) ->
|
||||
if keys is undefined
|
||||
item for _, item of @_byKey
|
||||
else
|
||||
items = @_fetchItems keys, ignoreMissingItems
|
||||
|
||||
if $_.isString keys then items[0] else items
|
||||
|
||||
remove: (keys, ignoreMissingItems = false) ->
|
||||
@_removeItems (@_fetchItems keys, ignoreMissingItems)
|
||||
|
||||
set: (items, {add, update, remove} = {}) ->
|
||||
add = true unless add?
|
||||
update = true unless update?
|
||||
remove = false unless remove?
|
||||
|
||||
itemsToAdd = {}
|
||||
itemsToUpdate = {}
|
||||
|
||||
itemsToRemove = {}
|
||||
$_.extend itemsToRemove, @_byKey if remove
|
||||
|
||||
$each items, (genval, genkey) =>
|
||||
item = {
|
||||
rule: undefined
|
||||
key: undefined
|
||||
data: undefined
|
||||
val: undefined
|
||||
genkey
|
||||
genval
|
||||
}
|
||||
|
||||
return unless @_runHook 'beforeDispatch', item
|
||||
|
||||
# Searches for a rule to handle it.
|
||||
ruleName = @dispatch.call item
|
||||
rule = @_rules[ruleName]
|
||||
|
||||
unless rule?
|
||||
@missingRule ruleName
|
||||
|
||||
# If `missingRule()` has not created the rule, just keep this
|
||||
# item.
|
||||
rule = @_rules[ruleName]
|
||||
return unless rule?
|
||||
|
||||
# Checks if this is a singleton.
|
||||
@_assert(
|
||||
not rule.singleton
|
||||
"cannot add items to singleton rule “#{rule.name}”"
|
||||
)
|
||||
|
||||
# Computes its key.
|
||||
key = rule.key.call item
|
||||
|
||||
@_assert(
|
||||
$_.isString key
|
||||
"the key “#{key}” is not a string"
|
||||
)
|
||||
|
||||
# Updates known values.
|
||||
item.rule = rule.name
|
||||
item.key = key
|
||||
|
||||
if key of @_byKey
|
||||
# Marks this item as not to be removed.
|
||||
delete itemsToRemove[key]
|
||||
|
||||
if update
|
||||
# Fetches the existing entry.
|
||||
prev = @_byKey[key]
|
||||
|
||||
# Checks if there is a conflict in rules.
|
||||
unless item.rule is prev.rule
|
||||
@ruleConflict item.rule, prev
|
||||
item.prevRule = prev.rule
|
||||
else
|
||||
delete item.prevRule
|
||||
|
||||
# Gets its previous data/value.
|
||||
item.data = prev.data
|
||||
item.val = prev.val
|
||||
|
||||
# Registers the item to be updated.
|
||||
itemsToUpdate[key] = item
|
||||
|
||||
# Note: an item will be updated only once per `set()` and
|
||||
# only the last generator will be used.
|
||||
else
|
||||
if add
|
||||
|
||||
# Registers the item to be added.
|
||||
itemsToAdd[key] = item
|
||||
|
||||
# Adds items.
|
||||
@_updateItems itemsToAdd, true
|
||||
|
||||
# Updates items.
|
||||
@_updateItems itemsToUpdate
|
||||
|
||||
# Removes any items not seen (iff `remove` is true).
|
||||
@_removeItems itemsToRemove
|
||||
|
||||
# Forces items to update their value.
|
||||
touch: (keys) ->
|
||||
@_updateItems (@_fetchItems keys, true)
|
||||
|
||||
#--------------------------------
|
||||
|
||||
_assert: (cond, message) ->
|
||||
throw new Error message unless cond
|
||||
|
||||
# Emits item related event.
|
||||
_emitEvent: (event, items) ->
|
||||
getRule = if event is 'exit'
|
||||
(item) -> item.prevRule or item.rule
|
||||
else
|
||||
(item) -> item.rule
|
||||
|
||||
byRule = Object.create null
|
||||
|
||||
# One per item.
|
||||
$each items, (item) =>
|
||||
@emit "key=#{item.key}", event, item
|
||||
|
||||
(byRule[getRule item] ?= []).push item
|
||||
|
||||
# One per rule.
|
||||
@emit "rule=#{rule}", event, byRule[rule] for rule of byRule
|
||||
|
||||
# One for everything.
|
||||
@emit 'any', event, items
|
||||
|
||||
_fetchItems: (keys, ignoreMissingItems = false) ->
|
||||
unless $_.isArray keys
|
||||
keys = if $_.isObject keys then $_.keys keys else [keys]
|
||||
|
||||
items = []
|
||||
for key in keys
|
||||
item = @_byKey[key]
|
||||
if item?
|
||||
items.push item
|
||||
else
|
||||
@_assert(
|
||||
ignoreMissingItems
|
||||
"no item with key “#{key}”"
|
||||
)
|
||||
items
|
||||
|
||||
_removeItems: (items) ->
|
||||
return if $_.isEmpty items
|
||||
|
||||
$each items, (item) => delete @_byKey[item.key]
|
||||
|
||||
@_emitEvent 'exit', items
|
||||
|
||||
|
||||
# Runs hooks for the moment `name` with the given context and
|
||||
# returns false if the default action has been prevented.
|
||||
_runHook: (name, ctx) ->
|
||||
hooks = @_hooks[name]
|
||||
|
||||
# If no hooks, nothing to do.
|
||||
return true unless hooks? and (n = hooks.length) isnt 0
|
||||
|
||||
# Flags controlling the run.
|
||||
notStopped = true
|
||||
actionNotPrevented = true
|
||||
|
||||
# Creates the event object.
|
||||
event = {
|
||||
stopPropagation: -> notStopped = false
|
||||
|
||||
# TODO: Should `preventDefault()` imply `stopPropagation()`?
|
||||
preventDefault: -> actionNotPrevented = false
|
||||
}
|
||||
|
||||
i = 0
|
||||
while notStopped and i < n
|
||||
hooks[i++].call ctx, event
|
||||
|
||||
# TODO: Is exception handling necessary to have the wanted
|
||||
# behavior?
|
||||
|
||||
return actionNotPrevented
|
||||
|
||||
_updateItems: (items, areNew) ->
|
||||
return if $_.isEmpty items
|
||||
|
||||
# An update is similar to an exit followed by an enter.
|
||||
@_removeItems items unless areNew
|
||||
|
||||
$each items, (item) =>
|
||||
return unless @_runHook 'beforeUpdate', item
|
||||
|
||||
{rule: ruleName} = item
|
||||
|
||||
# Computes its value.
|
||||
do =>
|
||||
# Item is not passed directly to function to avoid direct
|
||||
# modification.
|
||||
#
|
||||
# This is not a true security but better than nothing.
|
||||
proxy = Object.create item
|
||||
|
||||
updateValue = (parent, prop, def) ->
|
||||
if not $_.isObject def
|
||||
parent[prop] = def
|
||||
else if $_.isFunction def
|
||||
parent[prop] = def.call proxy, parent[prop]
|
||||
else if $_.isArray def
|
||||
i = 0
|
||||
n = def.length
|
||||
|
||||
current = parent[prop] ?= new Array n
|
||||
while i < n
|
||||
updateValue current, i, def[i]
|
||||
++i
|
||||
else
|
||||
# It's a plain object.
|
||||
current = parent[prop] ?= {}
|
||||
for i of def
|
||||
updateValue current, i, def[i]
|
||||
|
||||
updateValue item, 'data', @_rules[ruleName].data
|
||||
updateValue item, 'val', @_rules[ruleName].val
|
||||
|
||||
unless @_runHook 'beforeSave', item
|
||||
# FIXME: should not be removed, only not saved.
|
||||
delete @_byKey[item.key]
|
||||
|
||||
# Really inserts the items and trigger events.
|
||||
$each items, (item) => @_byKey[item.key] = item
|
||||
@_emitEvent 'enter', items
|
||||
|
||||
#=====================================================================
|
||||
|
||||
module.exports = {$MappedCollection}
|
||||
@@ -1,121 +0,0 @@
|
||||
{expect: $expect} = require 'chai'
|
||||
|
||||
$sinon = require 'sinon'
|
||||
|
||||
#---------------------------------------------------------------------
|
||||
|
||||
{$MappedCollection} = require './MappedCollection.coffee'
|
||||
|
||||
#=====================================================================
|
||||
|
||||
describe '$MappedCollection', ->
|
||||
|
||||
# Shared variables.
|
||||
collection = null
|
||||
|
||||
beforeEach ->
|
||||
collection = new $MappedCollection()
|
||||
|
||||
#-------------------------------------------------------------------
|
||||
|
||||
describe '#dispatch()', ->
|
||||
|
||||
# Test data.
|
||||
beforeEach ->
|
||||
collection.rule test: {}
|
||||
|
||||
#------------------------------
|
||||
|
||||
it 'should have genkey and genval', ->
|
||||
collection.dispatch = ->
|
||||
$expect(@genkey).to.equal 'a key'
|
||||
$expect(@genval).to.equal 'a value'
|
||||
|
||||
'test'
|
||||
|
||||
collection.set {
|
||||
'a key': 'a value'
|
||||
}
|
||||
|
||||
#------------------------------
|
||||
|
||||
it 'should be used to dispatch an item', ->
|
||||
collection.dispatch = -> 'test'
|
||||
|
||||
collection.set [
|
||||
'any value'
|
||||
]
|
||||
|
||||
$expect(collection.getRaw('0').rule).to.equal 'test'
|
||||
|
||||
#-------------------------------------------------------------------
|
||||
|
||||
describe 'item hooks', ->
|
||||
|
||||
# Test data.
|
||||
beforeEach ->
|
||||
collection.rule test: {}
|
||||
|
||||
#------------------------------
|
||||
|
||||
it 'should be called in the correct order', ->
|
||||
|
||||
beforeDispatch = $sinon.spy()
|
||||
collection.hook {beforeDispatch}
|
||||
|
||||
dispatcher = $sinon.spy ->
|
||||
$expect(beforeDispatch.called).to.true
|
||||
|
||||
# It still is a dispatcher.
|
||||
'test'
|
||||
collection.dispatch = dispatcher
|
||||
|
||||
beforeUpdate = $sinon.spy ->
|
||||
$expect(dispatcher.called).to.true
|
||||
collection.hook {beforeUpdate}
|
||||
|
||||
beforeSave = $sinon.spy ->
|
||||
$expect(beforeUpdate.called).to.true
|
||||
collection.hook {beforeSave}
|
||||
|
||||
collection.set [
|
||||
'any value'
|
||||
]
|
||||
|
||||
$expect(beforeSave.called).to.be.true
|
||||
|
||||
#-------------------------------------------------------------------
|
||||
|
||||
describe 'adding new items', ->
|
||||
|
||||
beforeEach ->
|
||||
collection.rule test: {}
|
||||
collection.dispatch = -> 'test'
|
||||
|
||||
#------------------------------
|
||||
|
||||
it 'should trigger three `enter` events', ->
|
||||
keySpy = $sinon.spy()
|
||||
ruleSpy = $sinon.spy()
|
||||
anySpy = $sinon.spy()
|
||||
|
||||
collection.on 'key=a key', keySpy
|
||||
collection.on 'rule=test', ruleSpy
|
||||
collection.on 'any', anySpy
|
||||
|
||||
collection.set {
|
||||
'a key': 'a value'
|
||||
}
|
||||
|
||||
item = collection.getRaw 'a key'
|
||||
|
||||
# TODO: items can be an array or a object (it is not defined).
|
||||
$expect(keySpy.args).to.deep.equal [
|
||||
['enter', item]
|
||||
]
|
||||
$expect(ruleSpy.args).to.deep.equal [
|
||||
['enter', [item]]
|
||||
]
|
||||
$expect(anySpy.args).to.deep.equal [
|
||||
['enter', {'a key': item}]
|
||||
]
|
||||
51
src/api-errors.js
Normal file
51
src/api-errors.js
Normal file
@@ -0,0 +1,51 @@
|
||||
import {JsonRpcError} from 'json-rpc-protocol'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
// Export standard JSON-RPC errors.
|
||||
export {
|
||||
InvalidJson,
|
||||
InvalidParameters,
|
||||
InvalidRequest,
|
||||
MethodNotFound
|
||||
} from 'json-rpc-protocol'
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export class NotImplemented extends JsonRpcError {
|
||||
constructor () {
|
||||
super('not implemented', 0)
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export class NoSuchObject extends JsonRpcError {
|
||||
constructor (id, type) {
|
||||
super('no such object', 1, {id, type})
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export class Unauthorized extends JsonRpcError {
|
||||
constructor () {
|
||||
super('not authenticated or not enough permissions', 2)
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export class InvalidCredential extends JsonRpcError {
|
||||
constructor () {
|
||||
super('invalid credential', 3)
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export class AlreadyAuthenticated extends JsonRpcError {
|
||||
constructor () {
|
||||
super('already authenticated', 4)
|
||||
}
|
||||
}
|
||||
555
src/api.js
555
src/api.js
@@ -1,337 +1,318 @@
|
||||
'use strict';
|
||||
import createDebug from 'debug'
|
||||
const debug = createDebug('xo:api')
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
import assign from 'lodash.assign'
|
||||
import Bluebird from 'bluebird'
|
||||
import forEach from 'lodash.foreach'
|
||||
import getKeys from 'lodash.keys'
|
||||
import isFunction from 'lodash.isfunction'
|
||||
import map from 'lodash.map'
|
||||
import requireTree from 'require-tree'
|
||||
import schemaInspector from 'schema-inspector'
|
||||
|
||||
var $_ = require('underscore');
|
||||
import {
|
||||
InvalidParameters,
|
||||
MethodNotFound,
|
||||
NoSuchObject,
|
||||
Unauthorized
|
||||
} from './api-errors'
|
||||
|
||||
var $requireTree = require('require-tree');
|
||||
// ===================================================================
|
||||
|
||||
var $schemaInspector = require('schema-inspector');
|
||||
// FIXME: this function is specific to XO and should not be defined in
|
||||
// this file.
|
||||
function checkPermission (method) {
|
||||
/* jshint validthis: true */
|
||||
|
||||
//--------------------------------------------------------------------
|
||||
const {permission} = method
|
||||
|
||||
var $wait = require('./fibers-utils').$wait;
|
||||
// No requirement.
|
||||
if (permission === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
const {user} = this
|
||||
if (!user) {
|
||||
throw new Unauthorized()
|
||||
}
|
||||
|
||||
function $deprecated(fn)
|
||||
{
|
||||
return function (session, req) {
|
||||
console.warn(req.method +' is deprecated!');
|
||||
// The only requirement is login.
|
||||
if (!permission) {
|
||||
return
|
||||
}
|
||||
|
||||
return fn.apply(this, arguments);
|
||||
};
|
||||
if (!user.hasPermission(permission)) {
|
||||
throw new Unauthorized()
|
||||
}
|
||||
}
|
||||
|
||||
var wrap = function (val) {
|
||||
return function () {
|
||||
return val;
|
||||
};
|
||||
};
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
function checkParams (method, params) {
|
||||
const schema = method.params
|
||||
if (!schema) {
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: Helper functions that could be written:
|
||||
// - checkParams(req.params, param1, ..., paramN)
|
||||
const result = schemaInspector.validate({
|
||||
type: 'object',
|
||||
properties: schema
|
||||
}, params)
|
||||
|
||||
var helpers = {};
|
||||
|
||||
helpers.checkPermission = function (permission)
|
||||
{
|
||||
// TODO: Handle token permission.
|
||||
|
||||
var userId = this.session.get('user_id', undefined);
|
||||
|
||||
if (undefined === userId)
|
||||
{
|
||||
throw Api.err.UNAUTHORIZED;
|
||||
}
|
||||
|
||||
if (!permission)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var user = $wait(this.users.first(userId));
|
||||
// The user MUST exist at this time.
|
||||
|
||||
if (!user.hasPermission(permission))
|
||||
{
|
||||
throw Api.err.UNAUTHORIZED;
|
||||
}
|
||||
};
|
||||
|
||||
// Checks and returns parameters.
|
||||
helpers.getParams = function (schema) {
|
||||
var params = this.request.params;
|
||||
|
||||
schema = {
|
||||
type: 'object',
|
||||
properties: schema,
|
||||
};
|
||||
|
||||
var result = $schemaInspector.validate(schema, params);
|
||||
|
||||
if (!result.valid)
|
||||
{
|
||||
this.throw('INVALID_PARAMS', result.error);
|
||||
}
|
||||
|
||||
return params;
|
||||
};
|
||||
|
||||
helpers.getUserPublicProperties = function (user) {
|
||||
// Handles both properties and wrapped models.
|
||||
var properties = user.properties || user;
|
||||
|
||||
return $_.pick(properties, 'id', 'email', 'permission');
|
||||
};
|
||||
|
||||
helpers.getServerPublicProperties = function (server) {
|
||||
// Handles both properties and wrapped models.
|
||||
var properties = server.properties || server;
|
||||
|
||||
return $_.pick(properties, 'id', 'host', 'username');
|
||||
};
|
||||
|
||||
helpers.throw = function (errorId, data) {
|
||||
var error = Api.err[errorId];
|
||||
|
||||
if (!error)
|
||||
{
|
||||
console.error('Invalid error:', errorId);
|
||||
throw Api.err.SERVER_ERROR;
|
||||
}
|
||||
|
||||
if (data)
|
||||
{
|
||||
error = $_.extend({}, error, {data: data});
|
||||
}
|
||||
|
||||
throw error;
|
||||
};
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
|
||||
function Api(xo)
|
||||
{
|
||||
if ( !(this instanceof Api) )
|
||||
{
|
||||
return new Api(xo);
|
||||
}
|
||||
|
||||
this.xo = xo;
|
||||
if (!result.valid) {
|
||||
throw new InvalidParameters(result.error)
|
||||
}
|
||||
}
|
||||
|
||||
Api.prototype.exec = function (session, request) {
|
||||
var ctx = Object.create(this.xo);
|
||||
$_.extend(ctx, helpers, {
|
||||
session: session,
|
||||
request: request,
|
||||
});
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
var method = this.getMethod(request.method);
|
||||
// Forward declaration.
|
||||
let checkAuthorization
|
||||
|
||||
if (!method)
|
||||
{
|
||||
console.warn('Invalid method: '+ request.method);
|
||||
throw Api.err.INVALID_METHOD;
|
||||
}
|
||||
|
||||
if ('permission' in method)
|
||||
{
|
||||
helpers.checkPermission.call(ctx, method.permission)
|
||||
}
|
||||
|
||||
if (method.params)
|
||||
{
|
||||
helpers.getParams.call(ctx, method.params);
|
||||
}
|
||||
|
||||
return method.call(ctx, request.params);
|
||||
};
|
||||
|
||||
Api.prototype.getMethod = function (name) {
|
||||
var parts = name.split('.');
|
||||
|
||||
var current = Api.fn;
|
||||
for (
|
||||
var i = 0, n = parts.length;
|
||||
(i < n) && (current = current[parts[i]]);
|
||||
++i
|
||||
)
|
||||
{
|
||||
/* jshint noempty:false */
|
||||
}
|
||||
|
||||
// Method found.
|
||||
if ($_.isFunction(current))
|
||||
{
|
||||
return current;
|
||||
}
|
||||
|
||||
// It's a (deprecated) alias.
|
||||
if ($_.isString(current))
|
||||
{
|
||||
return $deprecated(this.getMethod(current));
|
||||
}
|
||||
|
||||
// No entry found, looking for a catch-all method.
|
||||
current = Api.fn;
|
||||
var catchAll;
|
||||
for (i = 0; (i < n) && (current = current[parts[i]]); ++i)
|
||||
{
|
||||
catchAll = current.__catchAll || catchAll;
|
||||
}
|
||||
|
||||
return catchAll;
|
||||
};
|
||||
|
||||
module.exports = Api;
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
|
||||
function err(code, message)
|
||||
{
|
||||
return {
|
||||
'code': code,
|
||||
'message': message
|
||||
};
|
||||
function authorized () {}
|
||||
// function forbiddden () {
|
||||
// // We don't care about an error object.
|
||||
// /* eslint no-throw-literal: 0 */
|
||||
// throw null
|
||||
// }
|
||||
function checkMemberAuthorization (member) {
|
||||
return function (userId, object, permission) {
|
||||
const memberObject = this.getObject(object[member])
|
||||
return checkAuthorization.call(this, userId, memberObject, permission)
|
||||
}
|
||||
}
|
||||
|
||||
Api.err = {
|
||||
const checkAuthorizationByTypes = {
|
||||
// Objects of these types do not requires any authorization.
|
||||
'network': authorized,
|
||||
'VM-template': authorized,
|
||||
|
||||
//////////////////////////////////////////////////////////////////
|
||||
// JSON-RPC errors.
|
||||
//////////////////////////////////////////////////////////////////
|
||||
message: checkMemberAuthorization('$object'),
|
||||
|
||||
'INVALID_JSON': err(-32700, 'invalid JSON'),
|
||||
task: checkMemberAuthorization('$host'),
|
||||
|
||||
'INVALID_REQUEST': err(-32600, 'invalid JSON-RPC request'),
|
||||
VBD: checkMemberAuthorization('VDI'),
|
||||
|
||||
'INVALID_METHOD': err(-32601, 'method not found'),
|
||||
// Access to a VDI is granted if the user has access to the
|
||||
// containing SR or to a linked VM.
|
||||
VDI (userId, vdi, permission) {
|
||||
// Check authorization for each of the connected VMs.
|
||||
const promises = map(this.getObjects(vdi.$VBDs, 'VBD'), vbd => {
|
||||
const vm = this.getObject(vbd.VM, 'VM')
|
||||
return checkAuthorization.call(this, userId, vm, permission)
|
||||
})
|
||||
|
||||
'INVALID_PARAMS': err(-32602, 'invalid parameter(s)'),
|
||||
// Check authorization for the containing SR.
|
||||
const sr = this.getObject(vdi.$SR, 'SR')
|
||||
promises.push(checkAuthorization.call(this, userId, sr, permission))
|
||||
|
||||
'SERVER_ERROR': err(-32603, 'unknown error from the server'),
|
||||
// We need at least one success
|
||||
return Bluebird.any(promises)
|
||||
},
|
||||
|
||||
//////////////////////////////////////////////////////////////////
|
||||
// XO errors.
|
||||
//////////////////////////////////////////////////////////////////
|
||||
VIF (userId, vif, permission) {
|
||||
const network = this.getObject(vif.$network)
|
||||
const vm = this.getObject(vif.$VM)
|
||||
|
||||
'NOT_IMPLEMENTED': err(0, 'not implemented'),
|
||||
return Bluebird.any([
|
||||
checkAuthorization.call(this, userId, network, permission),
|
||||
checkAuthorization.call(this, userId, vm, permission)
|
||||
])
|
||||
},
|
||||
|
||||
'NO_SUCH_OBJECT': err(1, 'no such object'),
|
||||
'VM-snapshot': checkMemberAuthorization('$snapshot_of')
|
||||
}
|
||||
|
||||
// Not authenticated or not enough permissions.
|
||||
'UNAUTHORIZED': err(2, 'not authenticated or not enough permissions'),
|
||||
function throwIfFail (success) {
|
||||
if (!success) {
|
||||
// We don't care about an error object.
|
||||
/* eslint no-throw-literal: 0 */
|
||||
throw null
|
||||
}
|
||||
}
|
||||
|
||||
// Invalid email & passwords or token.
|
||||
'INVALID_CREDENTIAL': err(3, 'invalid credential'),
|
||||
function defaultCheckAuthorization (userId, object, permission) {
|
||||
return this.hasPermission(userId, object.id, permission).then(throwIfFail)
|
||||
}
|
||||
|
||||
'ALREADY_AUTHENTICATED': err(4, 'already authenticated'),
|
||||
};
|
||||
checkAuthorization = Bluebird.method(function (userId, object, permission) {
|
||||
const fn = checkAuthorizationByTypes[object.type] || defaultCheckAuthorization
|
||||
return fn.call(this, userId, object, permission)
|
||||
})
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
function resolveParams (method, params) {
|
||||
const resolve = method.resolve
|
||||
if (!resolve) {
|
||||
return params
|
||||
}
|
||||
|
||||
var $register = function (path, fn, params) {
|
||||
var component, current;
|
||||
const {user} = this
|
||||
if (!user) {
|
||||
throw new Unauthorized()
|
||||
}
|
||||
|
||||
if (params)
|
||||
{
|
||||
fn.params = params;
|
||||
}
|
||||
const userId = user.get('id')
|
||||
const isAdmin = this.user.hasPermission('admin')
|
||||
|
||||
if (!$_.isArray(path))
|
||||
{
|
||||
path = path.split('.');
|
||||
}
|
||||
const promises = []
|
||||
forEach(resolve, ([param, types, permission = 'administrate'], key) => {
|
||||
const id = params[param]
|
||||
if (id === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
current = Api.fn;
|
||||
for (var i = 0, n = path.length - 1; i < n; ++i)
|
||||
{
|
||||
component = path[i];
|
||||
current = (current[component] || (current[component] = {}));
|
||||
}
|
||||
const object = this.getObject(params[param], types)
|
||||
|
||||
if ($_.isFunction(fn))
|
||||
{
|
||||
current[path[n]] = fn;
|
||||
}
|
||||
else if ($_.isObject(fn) && !$_.isArray(fn))
|
||||
{
|
||||
// If it is not an function but an object, copies its
|
||||
// properties.
|
||||
// This parameter has been handled, remove it.
|
||||
delete params[param]
|
||||
|
||||
component = path[n];
|
||||
current = (current[component] || (current[component] = {}));
|
||||
// Register this new value.
|
||||
params[key] = object
|
||||
|
||||
for (var prop in fn)
|
||||
{
|
||||
current[prop] = fn[prop];
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
current[path[n]] = wrap(fn);
|
||||
}
|
||||
};
|
||||
if (!isAdmin) {
|
||||
promises.push(checkAuthorization.call(this, userId, object, permission))
|
||||
}
|
||||
})
|
||||
|
||||
Api.fn = $requireTree('./api');
|
||||
return Bluebird.all(promises).catch(() => {
|
||||
throw new Unauthorized()
|
||||
}).return(params)
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------
|
||||
// ===================================================================
|
||||
|
||||
$register('system.getVersion', wrap('0.1'));
|
||||
function getMethodsInfo () {
|
||||
const methods = {}
|
||||
|
||||
$register('xo.getAllObjects', function () {
|
||||
return this.getObjects();
|
||||
});
|
||||
forEach(this.api._methods, function (method, name) {
|
||||
this[name] = assign({}, {
|
||||
description: method.description,
|
||||
params: method.params || {},
|
||||
permission: method.permission
|
||||
})
|
||||
}, methods)
|
||||
|
||||
// Returns the list of available methods similar to XML-RPC
|
||||
// introspection (http://xmlrpc-c.sourceforge.net/introspection.html).
|
||||
(function () {
|
||||
var methods = {};
|
||||
return methods
|
||||
}
|
||||
getMethodsInfo.description = 'returns the signatures of all available API methods'
|
||||
|
||||
(function browse(container, path) {
|
||||
var n = path.length;
|
||||
$_.each(container, function (content, key) {
|
||||
path[n] = key;
|
||||
if ($_.isFunction(content))
|
||||
{
|
||||
methods[path.join('.')] = {
|
||||
description: content.description,
|
||||
params: content.params || {},
|
||||
permission: content.permission,
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
browse(content, path);
|
||||
}
|
||||
});
|
||||
path.pop();
|
||||
})(Api.fn, []);
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
$register('system.listMethods', wrap($_.keys(methods)));
|
||||
$register('system.methodSignature', function (params) {
|
||||
var method = methods[params.name];
|
||||
const getVersion = () => '0.1'
|
||||
getVersion.description = 'API version (unstable)'
|
||||
|
||||
if (!method)
|
||||
{
|
||||
this.throw('NO_SUCH_OBJECT');
|
||||
}
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
// XML-RPC can have multiple signatures per method.
|
||||
return [
|
||||
// XML-RPC requires the method name.
|
||||
$_.extend({name: name}, method)
|
||||
];
|
||||
}, {
|
||||
name: {
|
||||
description: 'method to describe',
|
||||
type: 'string',
|
||||
},
|
||||
});
|
||||
function listMethods () {
|
||||
return getKeys(this.api._methods)
|
||||
}
|
||||
listMethods.description = 'returns the name of all available API methods'
|
||||
|
||||
$register('system.getMethodsInfo', wrap(methods));
|
||||
})();
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
function methodSignature ({method: name}) {
|
||||
const method = this.api.getMethod(name)
|
||||
|
||||
if (!method) {
|
||||
throw new NoSuchObject()
|
||||
}
|
||||
|
||||
// Return an array for compatibility with XML-RPC.
|
||||
return [
|
||||
// XML-RPC require the name of the method.
|
||||
assign({ name }, {
|
||||
description: method.description,
|
||||
params: method.params || {},
|
||||
permission: method.permission
|
||||
})
|
||||
]
|
||||
}
|
||||
methodSignature.description = 'returns the signature of an API method'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export default class Api {
|
||||
constructor ({context} = {}) {
|
||||
this._methods = Object.create(null)
|
||||
this.context = context
|
||||
|
||||
this.addMethods({
|
||||
system: {
|
||||
getMethodsInfo,
|
||||
getVersion,
|
||||
listMethods,
|
||||
methodSignature
|
||||
}
|
||||
})
|
||||
|
||||
// FIXME: this too is specific to XO and should be moved out of this file.
|
||||
this.addMethods(requireTree('./api'))
|
||||
}
|
||||
|
||||
addMethod (name, method) {
|
||||
this._methods[name] = method
|
||||
}
|
||||
|
||||
addMethods (methods) {
|
||||
let base = ''
|
||||
forEach(methods, function addMethod (method, name) {
|
||||
name = base + name
|
||||
|
||||
if (isFunction(method)) {
|
||||
this.addMethod(name, method)
|
||||
return
|
||||
}
|
||||
|
||||
const oldBase = base
|
||||
base = name + '.'
|
||||
forEach(method, addMethod, this)
|
||||
base = oldBase
|
||||
}, this)
|
||||
}
|
||||
|
||||
async call (session, name, params) {
|
||||
debug('%s(...)', name)
|
||||
|
||||
const method = this.getMethod(name)
|
||||
if (!method) {
|
||||
throw new MethodNotFound(name)
|
||||
}
|
||||
|
||||
const context = Object.create(this.context)
|
||||
context.api = this // Used by system.*().
|
||||
context.session = session
|
||||
|
||||
// FIXME: too coupled with XO.
|
||||
// Fetch and inject the current user.
|
||||
const userId = session.get('user_id', undefined)
|
||||
if (userId) {
|
||||
context.user = await context._getUser(userId)
|
||||
}
|
||||
|
||||
await checkPermission.call(context, method)
|
||||
checkParams(method, params)
|
||||
|
||||
await resolveParams.call(context, method, params)
|
||||
try {
|
||||
let result = await method.call(context, params)
|
||||
|
||||
// If nothing was returned, consider this operation a success
|
||||
// and return true.
|
||||
if (result === undefined) {
|
||||
result = true
|
||||
}
|
||||
|
||||
debug('%s(...) → %s', name, typeof result)
|
||||
|
||||
return result
|
||||
} catch (error) {
|
||||
debug('Error: %s(...) → %s', name, error)
|
||||
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
getMethod (name) {
|
||||
return this._methods[name]
|
||||
}
|
||||
}
|
||||
|
||||
49
src/api/acl.js
Normal file
49
src/api/acl.js
Normal file
@@ -0,0 +1,49 @@
|
||||
export async function get () {
|
||||
return await this.getAllAcls()
|
||||
}
|
||||
|
||||
get.permission = 'admin'
|
||||
|
||||
get.description = 'get existing ACLs'
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export async function getCurrent () {
|
||||
return await this.getAclsForUser(this.session.get('user_id'))
|
||||
}
|
||||
|
||||
getCurrent.permission = ''
|
||||
|
||||
getCurrent.description = 'get existing ACLs concerning current user'
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export async function add ({subject, object, action}) {
|
||||
await this.addAcl(subject, object, action)
|
||||
}
|
||||
|
||||
add.permission = 'admin'
|
||||
|
||||
add.params = {
|
||||
subject: { type: 'string' },
|
||||
object: { type: 'string' },
|
||||
action: { type: 'string' }
|
||||
}
|
||||
|
||||
add.description = 'add a new ACL entry'
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export async function remove ({subject, object, action}) {
|
||||
await this.removeAcl(subject, object, action)
|
||||
}
|
||||
|
||||
remove.permission = 'admin'
|
||||
|
||||
remove.params = {
|
||||
subject: { type: 'string' },
|
||||
object: { type: 'string' },
|
||||
action: { type: 'string' }
|
||||
}
|
||||
|
||||
remove.description = 'remove an existing ACL entry'
|
||||
31
src/api/disk.js
Normal file
31
src/api/disk.js
Normal file
@@ -0,0 +1,31 @@
|
||||
import {parseSize} from '../utils'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export async function create ({name, size, sr}) {
|
||||
const xapi = this.getXAPI(sr)
|
||||
|
||||
const ref = await xapi.call('VDI.create', {
|
||||
name_label: name,
|
||||
other_config: {},
|
||||
read_only: false,
|
||||
sharable: false,
|
||||
SR: sr.ref,
|
||||
type: 'user',
|
||||
virtual_size: String(parseSize(size))
|
||||
})
|
||||
|
||||
return (await xapi.call('VDI.get_record', ref)).uuid
|
||||
}
|
||||
|
||||
create.description = 'create a new disk on a SR'
|
||||
|
||||
create.params = {
|
||||
name: { type: 'string' },
|
||||
size: { type: 'string' },
|
||||
sr: { type: 'string' }
|
||||
}
|
||||
|
||||
create.resolve = {
|
||||
sr: ['sr', 'SR', 'administrate']
|
||||
}
|
||||
66
src/api/docker.js
Normal file
66
src/api/docker.js
Normal file
@@ -0,0 +1,66 @@
|
||||
export async function register ({vm}) {
|
||||
await this.getXAPI(vm).registerDockerContainer(vm.id)
|
||||
}
|
||||
register.permission = 'admin'
|
||||
|
||||
register.description = 'Register the VM for Docker management'
|
||||
|
||||
register.params = {
|
||||
vm: { type: 'string' }
|
||||
}
|
||||
|
||||
register.resolve = {
|
||||
vm: ['vm', 'VM', 'administrate']
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
export async function deregister ({vm}) {
|
||||
await this.getXAPI(vm).unregisterDockerContainer(vm.id)
|
||||
}
|
||||
deregister.permission = 'admin'
|
||||
|
||||
deregister.description = 'Deregister the VM for Docker management'
|
||||
|
||||
deregister.params = {
|
||||
vm: { type: 'string' }
|
||||
}
|
||||
|
||||
deregister.resolve = {
|
||||
vm: ['vm', 'VM', 'administrate']
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
export async function start ({vm, container}) {
|
||||
await this.getXAPI(vm).startDockerContainer(vm.id, container)
|
||||
}
|
||||
|
||||
export async function stop ({vm, container}) {
|
||||
await this.getXAPI(vm).stopDockerContainer(vm.id, container)
|
||||
}
|
||||
|
||||
export async function restart ({vm, container}) {
|
||||
await this.getXAPI(vm).restartDockerContainer(vm.id, container)
|
||||
}
|
||||
|
||||
export async function pause ({vm, container}) {
|
||||
await this.getXAPI(vm).pauseDockerContainer(vm.id, container)
|
||||
}
|
||||
|
||||
export async function unpause ({vm, container}) {
|
||||
await this.getXAPI(vm).unpauseDockerContainer(vm.id, container)
|
||||
}
|
||||
|
||||
for (let fn of [start, stop, restart, pause, unpause]) {
|
||||
fn.permission = 'admin'
|
||||
|
||||
fn.params = {
|
||||
vm: { type: 'string' },
|
||||
container: { type: 'string' }
|
||||
}
|
||||
|
||||
fn.resolve = {
|
||||
vm: ['vm', 'VM', 'operate']
|
||||
}
|
||||
}
|
||||
94
src/api/group.js
Normal file
94
src/api/group.js
Normal file
@@ -0,0 +1,94 @@
|
||||
export async function create ({name}) {
|
||||
return (await this.createGroup({name})).id
|
||||
}
|
||||
|
||||
create.description = 'creates a new group'
|
||||
create.permission = 'admin'
|
||||
create.params = {
|
||||
name: {type: 'string'}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
// Deletes an existing group.
|
||||
async function delete_ ({id}) {
|
||||
await this.deleteGroup(id)
|
||||
}
|
||||
|
||||
// delete is not a valid identifier.
|
||||
export {delete_ as delete}
|
||||
|
||||
delete_.description = 'deletes an existing group'
|
||||
delete_.permission = 'admin'
|
||||
delete_.params = {
|
||||
id: {type: 'string'}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export async function getAll () {
|
||||
return await this._groups.get()
|
||||
}
|
||||
|
||||
delete_.description = 'returns all the existing group'
|
||||
delete_.permission = 'admin'
|
||||
delete_.params = {
|
||||
id: {type: 'string'}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
// sets group.users with an array of user ids
|
||||
export async function setUsers ({id, userIds}) {
|
||||
await this.setGroupUsers(id, userIds)
|
||||
}
|
||||
|
||||
setUsers.description = 'sets the users belonging to a group'
|
||||
setUsers.permission = 'admin'
|
||||
setUsers.params = {
|
||||
id: {type: 'string'},
|
||||
userIds: {}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
// adds the user id to group.users
|
||||
export async function addUser ({id, userId}) {
|
||||
await this.addUserToGroup(userId, id)
|
||||
}
|
||||
|
||||
addUser.description = 'adds a user to a group'
|
||||
addUser.permission = 'admin'
|
||||
addUser.params = {
|
||||
id: {type: 'string'},
|
||||
userId: {type: 'string'}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
// remove the user id from group.users
|
||||
export async function removeUser ({id, userId}) {
|
||||
await this.removeUserFromGroup(userId, id)
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
removeUser.description = 'removes a user from a group'
|
||||
removeUser.permission = 'admin'
|
||||
removeUser.params = {
|
||||
id: {type: 'string'},
|
||||
userId: {type: 'string'}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export async function set ({id, name}) {
|
||||
await this.updateGroup(id, {name})
|
||||
}
|
||||
|
||||
set.description = 'changes the properties of an existing group'
|
||||
set.permission = 'admin'
|
||||
set.params = {
|
||||
id: { type: 'string' },
|
||||
name: { type: 'string', optional: true }
|
||||
}
|
||||
@@ -1,13 +1,17 @@
|
||||
{$wait} = require '../fibers-utils'
|
||||
$debug = (require 'debug') 'xo:api:vm'
|
||||
$find = require 'lodash.find'
|
||||
$findIndex = require 'lodash.findindex'
|
||||
$forEach = require 'lodash.foreach'
|
||||
$request = require('bluebird').promisify(require('request'))
|
||||
endsWith = require 'lodash.endswith'
|
||||
startsWith = require 'lodash.startswith'
|
||||
{coroutine: $coroutine} = require 'bluebird'
|
||||
{parseXml} = require '../utils'
|
||||
|
||||
#=====================================================================
|
||||
|
||||
exports.set = (params) ->
|
||||
try
|
||||
host = @getObject params.id
|
||||
catch
|
||||
@throw 'NO_SUCH_OBJECT'
|
||||
|
||||
set = $coroutine (params) ->
|
||||
{host} = params
|
||||
xapi = @getXAPI host
|
||||
|
||||
for param, field of {
|
||||
@@ -17,11 +21,11 @@ exports.set = (params) ->
|
||||
}
|
||||
continue unless param of params
|
||||
|
||||
$wait xapi.call "host.set_#{field}", host.ref, params[param]
|
||||
yield xapi.call "host.set_#{field}", host.ref, params[param]
|
||||
|
||||
return true
|
||||
exports.set.permission = 'admin'
|
||||
exports.set.params =
|
||||
|
||||
set.params =
|
||||
id: type: 'string'
|
||||
name_label:
|
||||
type: 'string'
|
||||
@@ -33,102 +37,307 @@ exports.set.params =
|
||||
type: 'boolean'
|
||||
optional: true
|
||||
|
||||
exports.restart = ({id}) ->
|
||||
@checkPermission 'admin'
|
||||
set.resolve = {
|
||||
host: ['id', 'host', 'administrate'],
|
||||
}
|
||||
|
||||
try
|
||||
host = @getObject id
|
||||
catch
|
||||
@throw 'NO_SUCH_OBJECT'
|
||||
exports.set = set
|
||||
|
||||
#---------------------------------------------------------------------
|
||||
|
||||
restart = $coroutine ({host}) ->
|
||||
xapi = @getXAPI host
|
||||
|
||||
$wait xapi.call 'host.disable', host.ref
|
||||
$wait xapi.call 'host.reboot', host.ref
|
||||
yield xapi.call 'host.disable', host.ref
|
||||
yield xapi.call 'host.reboot', host.ref
|
||||
|
||||
return true
|
||||
exports.restart.permission = 'admin'
|
||||
exports.restart.params = {
|
||||
|
||||
restart.params = {
|
||||
id: { type: 'string' }
|
||||
}
|
||||
|
||||
exports.restart_agent = ({id}) ->
|
||||
try
|
||||
host = @getObject id
|
||||
catch
|
||||
@throw 'NO_SUCH_OBJECT'
|
||||
restart.resolve = {
|
||||
host: ['id', 'host', 'operate'],
|
||||
}
|
||||
|
||||
exports.restart = restart
|
||||
|
||||
#---------------------------------------------------------------------
|
||||
|
||||
restartAgent = $coroutine ({host}) ->
|
||||
xapi = @getXAPI host
|
||||
|
||||
$wait xapi.call 'host.restart_agent', host.ref
|
||||
yield xapi.call 'host.restart_agent', host.ref
|
||||
|
||||
return true
|
||||
exports.restart_agent.permission = 'admin'
|
||||
exports.restart_agent.params = {
|
||||
|
||||
restartAgent.params = {
|
||||
id: { type: 'string' }
|
||||
}
|
||||
|
||||
exports.stop = ({id}) ->
|
||||
try
|
||||
host = @getObject id
|
||||
catch
|
||||
@throw 'NO_SUCH_OBJECT'
|
||||
restartAgent.resolve = {
|
||||
host: ['id', 'host', 'operate'],
|
||||
}
|
||||
|
||||
# TODO camel case
|
||||
exports.restart_agent = restartAgent
|
||||
|
||||
#---------------------------------------------------------------------
|
||||
|
||||
start = $coroutine ({host}) ->
|
||||
xapi = @getXAPI host
|
||||
|
||||
$wait xapi.call 'host.disable', host.ref
|
||||
$wait xapi.call 'host.shutdown', host.ref
|
||||
yield xapi.call 'host.power_on', host.ref
|
||||
|
||||
return true
|
||||
exports.stop.permission = 'admin'
|
||||
exports.stop.params = {
|
||||
|
||||
start.params = {
|
||||
id: { type: 'string' }
|
||||
}
|
||||
|
||||
exports.detach = ({id}) ->
|
||||
try
|
||||
host = @getObject id
|
||||
catch
|
||||
@throw 'NO_SUCH_OBJECT'
|
||||
start.resolve = {
|
||||
host: ['id', 'host', 'operate'],
|
||||
}
|
||||
|
||||
exports.start = start
|
||||
|
||||
#---------------------------------------------------------------------
|
||||
|
||||
stop = $coroutine ({host}) ->
|
||||
xapi = @getXAPI host
|
||||
|
||||
$wait xapi.call 'pool.eject', host.ref
|
||||
yield xapi.call 'host.disable', host.ref
|
||||
yield xapi.call 'host.shutdown', host.ref
|
||||
|
||||
return true
|
||||
exports.detach.permission = 'admin'
|
||||
exports.detach.params = {
|
||||
|
||||
stop.params = {
|
||||
id: { type: 'string' }
|
||||
}
|
||||
|
||||
exports.enable = ({id}) ->
|
||||
try
|
||||
host = @getObject id
|
||||
catch
|
||||
@throw 'NO_SUCH_OBJECT'
|
||||
stop.resolve = {
|
||||
host: ['id', 'host', 'operate'],
|
||||
}
|
||||
|
||||
exports.stop = stop
|
||||
|
||||
#---------------------------------------------------------------------
|
||||
|
||||
detach = $coroutine ({host}) ->
|
||||
xapi = @getXAPI host
|
||||
|
||||
$wait xapi.call 'host.enable', host.ref
|
||||
yield xapi.call 'pool.eject', host.ref
|
||||
|
||||
return true
|
||||
exports.stop.permission = 'admin'
|
||||
exports.stop.params = {
|
||||
|
||||
detach.params = {
|
||||
id: { type: 'string' }
|
||||
}
|
||||
|
||||
exports.disable = ({id}) ->
|
||||
try
|
||||
host = @getObject id
|
||||
catch
|
||||
@throw 'NO_SUCH_OBJECT'
|
||||
detach.resolve = {
|
||||
host: ['id', 'host', 'administrate'],
|
||||
}
|
||||
|
||||
exports.detach = detach
|
||||
|
||||
#---------------------------------------------------------------------
|
||||
|
||||
enable = $coroutine ({host}) ->
|
||||
xapi = @getXAPI host
|
||||
|
||||
yield xapi.call 'host.enable', host.ref
|
||||
|
||||
return true
|
||||
|
||||
enable.params = {
|
||||
id: { type: 'string' }
|
||||
}
|
||||
|
||||
enable.resolve = {
|
||||
host: ['id', 'host', 'administrate'],
|
||||
}
|
||||
|
||||
exports.enable = enable
|
||||
|
||||
#---------------------------------------------------------------------
|
||||
|
||||
disable = $coroutine ({host}) ->
|
||||
xapi = @getXAPI host
|
||||
|
||||
yield xapi.call 'host.disable', host.ref
|
||||
|
||||
return true
|
||||
|
||||
disable.params = {
|
||||
id: { type: 'string' }
|
||||
}
|
||||
|
||||
disable.resolve = {
|
||||
host: ['id', 'host', 'administrate'],
|
||||
}
|
||||
|
||||
exports.disable = disable
|
||||
|
||||
#---------------------------------------------------------------------
|
||||
|
||||
createNetwork = $coroutine ({host, name, description, pif, mtu, vlan}) ->
|
||||
xapi = @getXAPI host
|
||||
|
||||
description = description ? 'Created with Xen Orchestra'
|
||||
|
||||
network_ref = yield xapi.call 'network.create', {
|
||||
name_label: name,
|
||||
name_description: description,
|
||||
MTU: mtu ? '1500'
|
||||
other_config: {}
|
||||
}
|
||||
|
||||
if pif?
|
||||
vlan = vlan ? '0'
|
||||
pif = @getObject pif, 'PIF'
|
||||
yield xapi.call 'pool.create_VLAN_from_PIF', pif.ref, network_ref, vlan
|
||||
|
||||
return true
|
||||
|
||||
createNetwork.params = {
|
||||
host: { type: 'string' }
|
||||
name: { type: 'string' }
|
||||
description: { type: 'string', optional: true }
|
||||
pif: { type: 'string', optional: true }
|
||||
mtu: { type: 'string', optional: true }
|
||||
vlan: { type: 'string', optional: true }
|
||||
}
|
||||
|
||||
createNetwork.resolve = {
|
||||
host: ['host', 'host', 'administrate'],
|
||||
}
|
||||
createNetwork.permission = 'admin'
|
||||
exports.createNetwork = createNetwork
|
||||
|
||||
#---------------------------------------------------------------------
|
||||
# Returns an array of missing new patches in the host
|
||||
# Returns an empty array if up-to-date
|
||||
# Throws an error if the host is not running the latest XS version
|
||||
|
||||
listMissingPatches = ({host}) ->
|
||||
return @getXAPI(host).listMissingPoolPatchesOnHost(host.id)
|
||||
|
||||
listMissingPatches.params = {
|
||||
host: { type: 'string' }
|
||||
}
|
||||
|
||||
listMissingPatches.resolve = {
|
||||
host: ['host', 'host', 'view'],
|
||||
}
|
||||
|
||||
exports.listMissingPatches = listMissingPatches
|
||||
|
||||
#---------------------------------------------------------------------
|
||||
|
||||
installPatch = ({host, patch: patchUuid}) ->
|
||||
return @getXAPI(host).installPoolPatchOnHost(patchUuid, host.id)
|
||||
|
||||
installPatch.params = {
|
||||
host: { type: 'string' }
|
||||
patch: { type: 'string' }
|
||||
}
|
||||
|
||||
installPatch.resolve = {
|
||||
host: ['host', 'host', 'administrate']
|
||||
}
|
||||
|
||||
exports.installPatch = installPatch
|
||||
|
||||
#---------------------------------------------------------------------
|
||||
|
||||
|
||||
stats = $coroutine ({host}) ->
|
||||
|
||||
xapi = @getXAPI host
|
||||
|
||||
$wait xapi.call 'host.disable', host.ref
|
||||
[response, body] = yield $request {
|
||||
method: 'get'
|
||||
rejectUnauthorized: false
|
||||
url: 'https://'+host.address+'/host_rrd?session_id='+xapi.sessionId
|
||||
}
|
||||
|
||||
return true
|
||||
exports.stop.permission = 'admin'
|
||||
exports.stop.params = {
|
||||
id: { type: 'string' }
|
||||
}
|
||||
if response.statusCode isnt 200
|
||||
throw new Error('Cannot fetch the RRDs')
|
||||
|
||||
json = parseXml(body)
|
||||
|
||||
# Find index of needed objects for getting their values after
|
||||
cpusIndexes = []
|
||||
pifsIndexes = []
|
||||
memoryFreeIndex = []
|
||||
memoryIndex = []
|
||||
loadIndex = []
|
||||
index = 0
|
||||
|
||||
$forEach(json.rrd.ds, (value, i) ->
|
||||
if /^cpu[0-9]+$/.test(value.name)
|
||||
cpusIndexes.push(i)
|
||||
else if startsWith(value.name, 'pif_eth') && endsWith(value.name, '_tx')
|
||||
pifsIndexes.push(i)
|
||||
else if startsWith(value.name, 'pif_eth') && endsWith(value.name, '_rx')
|
||||
pifsIndexes.push(i)
|
||||
else if startsWith(value.name, 'loadavg')
|
||||
loadIndex.push(i)
|
||||
else if startsWith(value.name, 'memory_free_kib')
|
||||
memoryFreeIndex.push(i)
|
||||
else if startsWith(value.name, 'memory_total_kib')
|
||||
memoryIndex.push(i)
|
||||
|
||||
return
|
||||
)
|
||||
|
||||
memoryFree = []
|
||||
memoryUsed = []
|
||||
memory = []
|
||||
load = []
|
||||
cpus = []
|
||||
pifs = []
|
||||
date = [] #TODO
|
||||
baseDate = json.rrd.lastupdate
|
||||
dateStep = json.rrd.step
|
||||
numStep = json.rrd.rra[0].database.row.length - 1
|
||||
|
||||
$forEach json.rrd.rra[0].database.row, (n, key) ->
|
||||
memoryFree.push(Math.round(parseInt(n.v[memoryFreeIndex])))
|
||||
memoryUsed.push(Math.round(parseInt(n.v[memoryIndex])-(n.v[memoryFreeIndex])))
|
||||
memory.push(parseInt(n.v[memoryIndex]))
|
||||
load.push(n.v[loadIndex])
|
||||
date.push(baseDate - (dateStep * (numStep - key)))
|
||||
# build the multi dimensional arrays
|
||||
$forEach cpusIndexes, (value, key) ->
|
||||
cpus[key] ?= []
|
||||
cpus[key].push(n.v[value]*100)
|
||||
return
|
||||
$forEach pifsIndexes, (value, key) ->
|
||||
pifs[key] ?= []
|
||||
pifs[key].push(if n.v[value] == 'NaN' then null else n.v[value]) # * (if key % 2 then -1 else 1))
|
||||
return
|
||||
return
|
||||
|
||||
|
||||
# the final object
|
||||
return {
|
||||
memoryFree: memoryFree
|
||||
memoryUsed: memoryUsed
|
||||
memory: memory
|
||||
date: date
|
||||
cpus: cpus
|
||||
pifs: pifs
|
||||
load: load
|
||||
}
|
||||
|
||||
stats.params = {
|
||||
host: { type: 'string' }
|
||||
}
|
||||
|
||||
stats.resolve = {
|
||||
host: ['host', 'host', 'view']
|
||||
}
|
||||
|
||||
exports.stats = stats;
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
{$wait} = require '../fibers-utils'
|
||||
|
||||
#=====================================================================
|
||||
|
||||
exports.delete = ({id}) ->
|
||||
try
|
||||
message = @getObject id
|
||||
catch
|
||||
@throw 'NO_SUCH_OBJECT'
|
||||
|
||||
xapi = @getXAPI message
|
||||
|
||||
$wait xapi.call 'message.destroy', message.ref
|
||||
|
||||
return true
|
||||
exports.delete.permission = 'admin'
|
||||
exports.delete.params =
|
||||
id:
|
||||
type: 'string'
|
||||
12
src/api/message.js
Normal file
12
src/api/message.js
Normal file
@@ -0,0 +1,12 @@
|
||||
async function delete_ ({message}) {
|
||||
await this.getXAPI(message).call('message.destroy', message.ref)
|
||||
}
|
||||
export {delete_ as delete}
|
||||
|
||||
delete_.params = {
|
||||
id: { type: 'string' }
|
||||
}
|
||||
|
||||
delete_.resolve = {
|
||||
message: ['id', 'message', 'administrate']
|
||||
}
|
||||
50
src/api/pbd.js
Normal file
50
src/api/pbd.js
Normal file
@@ -0,0 +1,50 @@
|
||||
// FIXME: too low level, should be removed.
|
||||
|
||||
// ===================================================================
|
||||
// Delete
|
||||
|
||||
async function delete_ ({PBD}) {
|
||||
// TODO: check if PBD is attached before
|
||||
await this.getXAPI(PBD).call('PBD.destroy', PBD.ref)
|
||||
}
|
||||
export {delete_ as delete}
|
||||
|
||||
delete_.params = {
|
||||
id: { type: 'string' }
|
||||
}
|
||||
|
||||
delete_.resolve = {
|
||||
PBD: ['id', 'PBD', 'administrate']
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// Disconnect
|
||||
|
||||
export async function disconnect ({PBD}) {
|
||||
// TODO: check if PBD is attached before
|
||||
await this.getXAPI(PBD).call('PBD.unplug', PBD.ref)
|
||||
}
|
||||
|
||||
disconnect.params = {
|
||||
id: { type: 'string' }
|
||||
}
|
||||
|
||||
disconnect.resolve = {
|
||||
PBD: ['id', 'PBD', 'administrate']
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// Connect
|
||||
|
||||
export async function connect ({PBD}) {
|
||||
// TODO: check if PBD is attached before
|
||||
await this.getXAPI(PBD).call('PBD.plug', PBD.ref)
|
||||
}
|
||||
|
||||
connect.params = {
|
||||
id: { type: 'string' }
|
||||
}
|
||||
|
||||
connect.resolve = {
|
||||
PBD: ['id', 'PBD', 'administrate']
|
||||
}
|
||||
47
src/api/pif.js
Normal file
47
src/api/pif.js
Normal file
@@ -0,0 +1,47 @@
|
||||
// ===================================================================
|
||||
// Delete
|
||||
|
||||
async function delete_ ({PIF}) {
|
||||
// TODO: check if PIF is attached before
|
||||
await this.getXAPI(PIF).call('PIF.destroy', PIF.ref)
|
||||
}
|
||||
export {delete_ as delete}
|
||||
|
||||
delete_.params = {
|
||||
id: { type: 'string' }
|
||||
}
|
||||
|
||||
delete_.resolve = {
|
||||
PIF: ['id', 'PIF', 'administrate']
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// Disconnect
|
||||
|
||||
export async function disconnect ({PIF}) {
|
||||
// TODO: check if PIF is attached before
|
||||
await this.getXAPI(PIF).call('PIF.unplug', PIF.ref)
|
||||
}
|
||||
|
||||
disconnect.params = {
|
||||
id: { type: 'string' }
|
||||
}
|
||||
|
||||
disconnect.resolve = {
|
||||
PIF: ['id', 'PIF', 'administrate']
|
||||
}
|
||||
// ===================================================================
|
||||
// Connect
|
||||
|
||||
export async function connect ({PIF}) {
|
||||
// TODO: check if PIF is attached before
|
||||
await this.getXAPI(PIF).call('PIF.plug', PIF.ref)
|
||||
}
|
||||
|
||||
connect.params = {
|
||||
id: { type: 'string' }
|
||||
}
|
||||
|
||||
connect.resolve = {
|
||||
PIF: ['id', 'PIF', 'administrate']
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
{$wait} = require '../fibers-utils'
|
||||
|
||||
#=====================================================================
|
||||
|
||||
exports.set = ->
|
||||
try
|
||||
pool = @getObject params.id
|
||||
catch
|
||||
@throw 'NO_SUCH_OBJECT'
|
||||
|
||||
xapi = @getXAPI pool
|
||||
|
||||
for param, field of {
|
||||
'name_label'
|
||||
'name_description'
|
||||
}
|
||||
continue unless param of params
|
||||
|
||||
$wait xapi.call "pool.set_#{field}", pool.ref, params[param]
|
||||
|
||||
return true
|
||||
exports.set.permission = 'admin'
|
||||
exports.set.params =
|
||||
id:
|
||||
type: 'string'
|
||||
name_label:
|
||||
type: 'string'
|
||||
optional: true
|
||||
name_description:
|
||||
type: 'string'
|
||||
optional: true
|
||||
77
src/api/pool.js
Normal file
77
src/api/pool.js
Normal file
@@ -0,0 +1,77 @@
|
||||
// ===================================================================
|
||||
|
||||
export async function set (params) {
|
||||
const {pool} = params
|
||||
delete params.pool
|
||||
|
||||
await this.getXAPI(pool).setPoolProperties(params)
|
||||
}
|
||||
|
||||
set.params = {
|
||||
id: {
|
||||
type: 'string'
|
||||
},
|
||||
name_label: {
|
||||
type: 'string',
|
||||
optional: true
|
||||
},
|
||||
name_description: {
|
||||
type: 'string',
|
||||
optional: true
|
||||
}
|
||||
}
|
||||
|
||||
set.resolve = {
|
||||
pool: ['id', 'pool', 'administrate']
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export async function installPatch ({pool, patch: patchUuid}) {
|
||||
await this.getXAPI(pool).installPoolPatchOnAllHosts(patchUuid)
|
||||
}
|
||||
|
||||
installPatch.params = {
|
||||
pool: {
|
||||
type: 'string'
|
||||
},
|
||||
patch: {
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
|
||||
installPatch.resolve = {
|
||||
pool: ['pool', 'pool', 'administrate']
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
async function handlePatchUpload (req, res, {pool}) {
|
||||
const {headers: {['content-length']: contentLength}} = req
|
||||
if (!contentLength) {
|
||||
res.writeHead(411)
|
||||
res.end('Content length is mandatory')
|
||||
return
|
||||
}
|
||||
|
||||
await this.getXAPI(pool).uploadPoolPatch(req, contentLength)
|
||||
}
|
||||
|
||||
export async function uploadPatch ({pool}) {
|
||||
return {
|
||||
$sendTo: await this.registerHttpRequest(handlePatchUpload, {pool})
|
||||
}
|
||||
}
|
||||
|
||||
uploadPatch.params = {
|
||||
pool: { type: 'string' }
|
||||
}
|
||||
|
||||
uploadPatch.resolve = {
|
||||
pool: ['pool', 'pool', 'administrate']
|
||||
}
|
||||
|
||||
// Compatibility
|
||||
//
|
||||
// TODO: remove when no longer used in xo-web
|
||||
export {uploadPatch as patch}
|
||||
3
src/api/role.js
Normal file
3
src/api/role.js
Normal file
@@ -0,0 +1,3 @@
|
||||
export async function getAll () {
|
||||
return await this.getRoles()
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
{$wait} = require '../fibers-utils'
|
||||
|
||||
#=====================================================================
|
||||
|
||||
# FIXME: We are storing passwords which is bad!
|
||||
# Could we use tokens instead?
|
||||
|
||||
# Adds a new server.
|
||||
exports.add = ({host, username, password}) ->
|
||||
server = $wait @servers.add {
|
||||
host
|
||||
username
|
||||
password
|
||||
}
|
||||
|
||||
return server.id
|
||||
exports.add.description = 'Add a new Xen server to XO'
|
||||
exports.add.permission = 'admin'
|
||||
exports.add.params =
|
||||
host:
|
||||
type: 'string'
|
||||
username:
|
||||
type: 'string'
|
||||
password:
|
||||
type: 'string'
|
||||
|
||||
# Removes an existing server.
|
||||
exports.remove = ({id}) ->
|
||||
# Throws an error if the server did not exist.
|
||||
@throw 'NO_SUCH_OBJECT' unless $wait @servers.remove id
|
||||
|
||||
return true
|
||||
exports.remove.permission = 'admin'
|
||||
exports.remove.params =
|
||||
id:
|
||||
type: 'string'
|
||||
|
||||
# Returns all servers.
|
||||
exports.getAll = ->
|
||||
# Retrieves the servers.
|
||||
servers = $wait @servers.get()
|
||||
|
||||
# Filters out private properties.
|
||||
for server, i in servers
|
||||
servers[i] = @getServerPublicProperties server
|
||||
|
||||
return servers
|
||||
exports.getAll.permission = 'admin'
|
||||
|
||||
# Changes the properties of an existing server.
|
||||
exports.set = ({id, host, username, password}) ->
|
||||
# Retrieves the server.
|
||||
server = $wait @servers.first id
|
||||
|
||||
# Throws an error if it did not exist.
|
||||
@throw 'NO_SUCH_OBJECT' unless server
|
||||
|
||||
# Updates the provided properties.
|
||||
server.set {host} if host?
|
||||
server.set {username} if username?
|
||||
server.set {password} if password?
|
||||
|
||||
# Updates the server.
|
||||
$wait @servers.update server
|
||||
|
||||
return true
|
||||
exports.set.permission = 'admin'
|
||||
exports.set.params =
|
||||
id:
|
||||
type: 'string'
|
||||
host:
|
||||
type: 'string'
|
||||
optional: true
|
||||
username:
|
||||
type: 'string'
|
||||
optional: true
|
||||
password:
|
||||
type: 'string'
|
||||
optional: true
|
||||
|
||||
|
||||
# Connects to an existing server.
|
||||
exports.connect = ->
|
||||
@throw 'NOT_IMPLEMENTED'
|
||||
|
||||
# Disconnects from an existing server.
|
||||
exports.disconnect = ->
|
||||
@throw 'NOT_IMPLEMENTED'
|
||||
133
src/api/server.js
Normal file
133
src/api/server.js
Normal file
@@ -0,0 +1,133 @@
|
||||
import {coroutine} from 'bluebird'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export async function add ({
|
||||
host,
|
||||
username,
|
||||
password,
|
||||
autoConnect = true
|
||||
}) {
|
||||
const server = await this.registerXenServer({host, username, password})
|
||||
|
||||
if (autoConnect) {
|
||||
// Connect asynchronously, ignore any error.
|
||||
this.connectXenServer(server.id).catch(() => {})
|
||||
}
|
||||
|
||||
return server.id
|
||||
}
|
||||
|
||||
add.description = 'register a new Xen server'
|
||||
|
||||
add.permission = 'admin'
|
||||
|
||||
add.params = {
|
||||
host: {
|
||||
type: 'string'
|
||||
},
|
||||
username: {
|
||||
type: 'string'
|
||||
},
|
||||
password: {
|
||||
type: 'string'
|
||||
},
|
||||
autoConnect: {
|
||||
optional: true,
|
||||
type: 'boolean'
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export async function remove ({id}) {
|
||||
this.unregisterXenServer(id)
|
||||
}
|
||||
|
||||
remove.description = 'unregister a Xen server'
|
||||
|
||||
remove.permission = 'admin'
|
||||
|
||||
remove.params = {
|
||||
id: {
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
// TODO: remove this function when users are integrated to the main
|
||||
// collection.
|
||||
export const getAll = coroutine(function * () {
|
||||
const servers = yield this._servers.get()
|
||||
|
||||
for (let i = 0, n = servers.length; i < n; ++i) {
|
||||
servers[i] = this.getServerPublicProperties(servers[i])
|
||||
}
|
||||
|
||||
return servers
|
||||
})
|
||||
|
||||
getAll.description = 'returns all the registered Xen server'
|
||||
|
||||
getAll.permission = 'admin'
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export async function set ({id, host, username, password}) {
|
||||
await this.updateXenServer(id, {host, username, password})
|
||||
}
|
||||
|
||||
set.description = 'changes the propeorties of a Xen server'
|
||||
|
||||
set.permission = 'admin'
|
||||
|
||||
set.params = {
|
||||
id: {
|
||||
type: 'string'
|
||||
},
|
||||
host: {
|
||||
type: 'string',
|
||||
optional: true
|
||||
},
|
||||
username: {
|
||||
type: 'string',
|
||||
optional: true
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
optional: true
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export async function connect ({id}) {
|
||||
await this.connectXenServer(id)
|
||||
}
|
||||
|
||||
connect.description = 'connect a Xen server'
|
||||
|
||||
connect.permission = 'admin'
|
||||
|
||||
connect.params = {
|
||||
id: {
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export async function disconnect ({id}) {
|
||||
await this.disconnectXenServer(id)
|
||||
}
|
||||
|
||||
disconnect.description = 'disconnect a Xen server'
|
||||
|
||||
disconnect.permission = 'admin'
|
||||
|
||||
disconnect.params = {
|
||||
id: {
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
{$wait} = require '../fibers-utils'
|
||||
|
||||
#=====================================================================
|
||||
|
||||
# Signs a user in with its email/password.
|
||||
exports.signInWithPassword = ({email, password}) ->
|
||||
@throw 'ALREADY_AUTHENTICATED' if @session.has 'user_id'
|
||||
|
||||
# Gets the user.
|
||||
user = $wait @users.first {email}
|
||||
|
||||
# Invalid credentials if the user does not exists or if the password
|
||||
# does not check.
|
||||
@throw 'INVALID_CREDENTIAL' unless user and user.checkPassword password
|
||||
|
||||
# Stores the user identifier in the session.
|
||||
@session.set 'user_id', user.get 'id'
|
||||
|
||||
# Returns the user.
|
||||
return @getUserPublicProperties user
|
||||
exports.signInWithPassword.params = {
|
||||
email: { type: 'string' }
|
||||
password: { type: 'string' }
|
||||
}
|
||||
|
||||
# Signs a user in with a token.
|
||||
exports.signInWithToken = ({token}) ->
|
||||
@throw 'ALREADY_AUTHENTICATED' if @session.has 'user_id'
|
||||
|
||||
# Gets the token.
|
||||
token = $wait @tokens.first token
|
||||
@throw 'INVALID_CREDENTIAL' unless token?
|
||||
|
||||
# Stores the user and the token identifiers in the session.
|
||||
user_id = token.get('user_id')
|
||||
@session.set 'token_id', token.get 'id'
|
||||
@session.set 'user_id', user_id
|
||||
|
||||
# Returns the user.
|
||||
user = $wait @users.first user_id
|
||||
return @getUserPublicProperties user
|
||||
exports.signInWithToken.params = {
|
||||
token: { type: 'string' }
|
||||
}
|
||||
|
||||
exports.signOut = ->
|
||||
@session.unset 'token_id'
|
||||
@session.unset 'user_id'
|
||||
|
||||
return true
|
||||
|
||||
# Gets the the currently signed in user.
|
||||
exports.getUser = ->
|
||||
id = @session.get 'user_id', null
|
||||
|
||||
# If the user is not signed in, returns null.
|
||||
return null unless id?
|
||||
|
||||
# Returns the user.
|
||||
user = $wait @users.first id
|
||||
return @getUserPublicProperties user
|
||||
61
src/api/session.js
Normal file
61
src/api/session.js
Normal file
@@ -0,0 +1,61 @@
|
||||
import {deprecate} from 'util'
|
||||
|
||||
import {InvalidCredential, AlreadyAuthenticated} from '../api-errors'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export async function signIn (credentials) {
|
||||
if (this.session.has('user_id')) {
|
||||
throw new AlreadyAuthenticated()
|
||||
}
|
||||
|
||||
const user = await this.authenticateUser(credentials)
|
||||
if (!user) {
|
||||
throw new InvalidCredential()
|
||||
}
|
||||
this.session.set('user_id', user.get('id'))
|
||||
|
||||
return this.getUserPublicProperties(user)
|
||||
}
|
||||
|
||||
signIn.description = 'sign in'
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export const signInWithPassword = deprecate(signIn, 'use session.signIn() instead')
|
||||
|
||||
signInWithPassword.params = {
|
||||
email: { type: 'string' },
|
||||
password: { type: 'string' }
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export const signInWithToken = deprecate(signIn, 'use session.signIn() instead')
|
||||
|
||||
signInWithToken.params = {
|
||||
token: { type: 'string' }
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export function signOut () {
|
||||
this.session.unset('user_id')
|
||||
}
|
||||
|
||||
signOut.description = 'sign out the user from the current session'
|
||||
|
||||
// This method requires the user to be signed in.
|
||||
signOut.permission = ''
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export async function getUser () {
|
||||
const userId = this.session.get('user_id')
|
||||
|
||||
return userId === undefined ?
|
||||
null :
|
||||
this.getUserPublicProperties(await this.getUser(userId))
|
||||
}
|
||||
|
||||
getUser.description = 'return the currently connected user'
|
||||
@@ -1,46 +0,0 @@
|
||||
{$wait} = require '../fibers-utils'
|
||||
|
||||
#=====================================================================
|
||||
|
||||
exports.set = (params) ->
|
||||
try
|
||||
SR = @getObject params.id
|
||||
catch
|
||||
@throw 'NO_SUCH_OBJECT'
|
||||
|
||||
xapi = @getXAPI SR
|
||||
|
||||
for param, field of {
|
||||
'name_label'
|
||||
'name_description'
|
||||
}
|
||||
continue unless param of params
|
||||
|
||||
$wait xapi.call "SR.set_#{field}", SR.ref, params[param]
|
||||
|
||||
return true
|
||||
exports.set.permission = 'admin'
|
||||
exports.set.params = {
|
||||
id: { type: 'string' }
|
||||
|
||||
name_label: { type: 'string', optional: true }
|
||||
|
||||
name_description: { type: 'string', optional: true }
|
||||
}
|
||||
|
||||
|
||||
exports.scan = ({id}) ->
|
||||
try
|
||||
SR = @getObject id
|
||||
catch
|
||||
@throw 'NO_SUCH_OBJECT'
|
||||
|
||||
xapi = @getXAPI SR
|
||||
|
||||
$wait xapi.call 'SR.scan', SR.ref
|
||||
|
||||
return true
|
||||
exports.scan.permission = 'admin'
|
||||
exports.scan.params = {
|
||||
id: { type: 'string' }
|
||||
}
|
||||
668
src/api/sr.js
Normal file
668
src/api/sr.js
Normal file
@@ -0,0 +1,668 @@
|
||||
import forEach from 'lodash.foreach'
|
||||
import {ensureArray, parseXml} from '../utils'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export async function set (params) {
|
||||
const {sr} = params
|
||||
delete params.sr
|
||||
|
||||
await this.getXAPI(sr).setSrProperties(sr.id, params)
|
||||
}
|
||||
|
||||
set.params = {
|
||||
id: { type: 'string' },
|
||||
|
||||
name_label: { type: 'string', optional: true },
|
||||
|
||||
name_description: { type: 'string', optional: true }
|
||||
}
|
||||
|
||||
set.resolve = {
|
||||
sr: ['id', 'SR', 'operate']
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export async function scan ({SR}) {
|
||||
await this.getXAPI(SR).call('SR.scan', SR.ref)
|
||||
}
|
||||
|
||||
scan.params = {
|
||||
id: { type: 'string' }
|
||||
}
|
||||
|
||||
scan.resolve = {
|
||||
SR: ['id', 'SR', 'operate']
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
// TODO: find a way to call this "delete" and not destroy
|
||||
export async function destroy ({SR}) {
|
||||
await this.getXAPI(SR).call('SR.destroy', SR.ref)
|
||||
}
|
||||
|
||||
destroy.params = {
|
||||
id: { type: 'string' }
|
||||
}
|
||||
|
||||
destroy.resolve = {
|
||||
SR: ['id', 'SR', 'administrate']
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export async function forget ({SR}) {
|
||||
await this.getXAPI(SR).call('SR.forget', SR.ref)
|
||||
}
|
||||
|
||||
forget.params = {
|
||||
id: { type: 'string' }
|
||||
}
|
||||
|
||||
forget.resolve = {
|
||||
SR: ['id', 'SR', 'administrate']
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export async function createIso ({
|
||||
host,
|
||||
nameLabel,
|
||||
nameDescription,
|
||||
path
|
||||
}) {
|
||||
const xapi = this.getXAPI(host)
|
||||
|
||||
// FIXME: won't work for IPv6
|
||||
// Detect if NFS or local path for ISO files
|
||||
const deviceConfig = {location: path}
|
||||
if (path.indexOf(':') === -1) { // not NFS share
|
||||
// TODO: legacy will be removed in XAPI soon by FileSR
|
||||
deviceConfig.legacy_mode = 'true'
|
||||
}
|
||||
const srRef = await xapi.call(
|
||||
'SR.create',
|
||||
host.ref,
|
||||
deviceConfig,
|
||||
'0', // SR size 0 because ISO
|
||||
nameLabel,
|
||||
nameDescription,
|
||||
'iso', // SR type ISO
|
||||
'iso', // SR content type ISO
|
||||
true,
|
||||
{}
|
||||
)
|
||||
|
||||
const sr = await xapi.call('SR.get_record', srRef)
|
||||
return sr.uuid
|
||||
}
|
||||
|
||||
createIso.params = {
|
||||
host: { type: 'string' },
|
||||
nameLabel: { type: 'string' },
|
||||
nameDescription: { type: 'string' },
|
||||
path: { type: 'string' }
|
||||
}
|
||||
|
||||
createIso.resolve = {
|
||||
host: ['host', 'host', 'administrate']
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// NFS SR
|
||||
|
||||
// This functions creates a NFS SR
|
||||
|
||||
export async function createNfs ({
|
||||
host,
|
||||
nameLabel,
|
||||
nameDescription,
|
||||
server,
|
||||
serverPath,
|
||||
nfsVersion
|
||||
}) {
|
||||
const xapi = this.getXAPI(host)
|
||||
|
||||
const deviceConfig = {
|
||||
server,
|
||||
serverpath: serverPath
|
||||
}
|
||||
|
||||
// if NFS version given
|
||||
if (nfsVersion) {
|
||||
deviceConfig.nfsversion = nfsVersion
|
||||
}
|
||||
|
||||
const srRef = await xapi.call(
|
||||
'SR.create',
|
||||
host.ref,
|
||||
deviceConfig,
|
||||
'0',
|
||||
nameLabel,
|
||||
nameDescription,
|
||||
'nfs', // SR LVM over iSCSI
|
||||
'user', // recommended by Citrix
|
||||
true,
|
||||
{}
|
||||
)
|
||||
|
||||
const sr = await xapi.call('SR.get_record', srRef)
|
||||
return sr.uuid
|
||||
}
|
||||
|
||||
createNfs.params = {
|
||||
host: { type: 'string' },
|
||||
nameLabel: { type: 'string' },
|
||||
nameDescription: { type: 'string' },
|
||||
server: { type: 'string' },
|
||||
serverPath: { type: 'string' },
|
||||
nfsVersion: { type: 'string', optional: true }
|
||||
}
|
||||
|
||||
createNfs.resolve = {
|
||||
host: ['host', 'host', 'administrate']
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// Local LVM SR
|
||||
|
||||
// This functions creates a local LVM SR
|
||||
|
||||
export async function createLvm ({
|
||||
host,
|
||||
nameLabel,
|
||||
nameDescription,
|
||||
device
|
||||
}) {
|
||||
const xapi = this.getXAPI(host)
|
||||
|
||||
const deviceConfig = {
|
||||
device
|
||||
}
|
||||
|
||||
const srRef = await xapi.call(
|
||||
'SR.create',
|
||||
host.ref,
|
||||
deviceConfig,
|
||||
'0',
|
||||
nameLabel,
|
||||
nameDescription,
|
||||
'lvm', // SR LVM
|
||||
'user', // recommended by Citrix
|
||||
false,
|
||||
{}
|
||||
)
|
||||
|
||||
const sr = await xapi.call('SR.get_record', srRef)
|
||||
return sr.uuid
|
||||
}
|
||||
|
||||
createLvm.params = {
|
||||
host: { type: 'string' },
|
||||
nameLabel: { type: 'string' },
|
||||
nameDescription: { type: 'string' },
|
||||
device: { type: 'string' }
|
||||
}
|
||||
|
||||
createLvm.resolve = {
|
||||
host: ['host', 'host', 'administrate']
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// This function helps to detect all NFS shares (exports) on a NFS server
|
||||
// Return a table of exports with their paths and ACLs
|
||||
|
||||
export async function probeNfs ({
|
||||
host,
|
||||
server
|
||||
}) {
|
||||
const xapi = this.getXAPI(host)
|
||||
|
||||
const deviceConfig = {
|
||||
server
|
||||
}
|
||||
|
||||
let xml
|
||||
|
||||
try {
|
||||
await xapi.call(
|
||||
'SR.probe',
|
||||
host.ref,
|
||||
deviceConfig,
|
||||
'nfs',
|
||||
{}
|
||||
)
|
||||
|
||||
throw new Error('the call above should have thrown an error')
|
||||
} catch (error) {
|
||||
if (error.code !== 'SR_BACKEND_FAILURE_101') {
|
||||
throw error
|
||||
}
|
||||
|
||||
xml = parseXml(error.params[2])
|
||||
}
|
||||
|
||||
const nfsExports = []
|
||||
forEach(ensureArray(xml['nfs-exports'].Export), nfsExport => {
|
||||
nfsExports.push({
|
||||
path: nfsExport.Path.trim(),
|
||||
acl: nfsExport.Accesslist.trim()
|
||||
})
|
||||
})
|
||||
|
||||
return nfsExports
|
||||
}
|
||||
|
||||
probeNfs.params = {
|
||||
host: { type: 'string' },
|
||||
server: { type: 'string' }
|
||||
}
|
||||
|
||||
probeNfs.resolve = {
|
||||
host: ['host', 'host', 'administrate']
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// ISCSI SR
|
||||
|
||||
// This functions creates a iSCSI SR
|
||||
|
||||
export async function createIscsi ({
|
||||
host,
|
||||
nameLabel,
|
||||
nameDescription,
|
||||
size,
|
||||
target,
|
||||
port,
|
||||
targetIqn,
|
||||
scsiId,
|
||||
chapUser,
|
||||
chapPassword
|
||||
}) {
|
||||
const xapi = this.getXAPI(host)
|
||||
|
||||
const deviceConfig = {
|
||||
target,
|
||||
targetIQN: targetIqn,
|
||||
SCSIid: scsiId
|
||||
}
|
||||
|
||||
// if we give user and password
|
||||
if (chapUser && chapPassword) {
|
||||
deviceConfig.chapUser = chapUser
|
||||
deviceConfig.chapPassword = chapPassword
|
||||
}
|
||||
|
||||
// if we give another port than default iSCSI
|
||||
if (port) {
|
||||
deviceConfig.port = port
|
||||
}
|
||||
|
||||
const srRef = await xapi.call(
|
||||
'SR.create',
|
||||
host.ref,
|
||||
deviceConfig,
|
||||
'0',
|
||||
nameLabel,
|
||||
nameDescription,
|
||||
'lvmoiscsi', // SR LVM over iSCSI
|
||||
'user', // recommended by Citrix
|
||||
true,
|
||||
{}
|
||||
)
|
||||
|
||||
const sr = await xapi.call('SR.get_record', srRef)
|
||||
return sr.uuid
|
||||
}
|
||||
|
||||
createIscsi.params = {
|
||||
host: { type: 'string' },
|
||||
nameLabel: { type: 'string' },
|
||||
nameDescription: { type: 'string' },
|
||||
target: { type: 'string' },
|
||||
port: { type: 'integer', optional: true},
|
||||
targetIqn: { type: 'string' },
|
||||
scsiId: { type: 'string' },
|
||||
chapUser: { type: 'string', optional: true },
|
||||
chapPassword: { type: 'string', optional: true }
|
||||
}
|
||||
|
||||
createIscsi.resolve = {
|
||||
host: ['host', 'host', 'administrate']
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// This function helps to detect all iSCSI IQN on a Target (iSCSI "server")
|
||||
// Return a table of IQN or empty table if no iSCSI connection to the target
|
||||
|
||||
export async function probeIscsiIqns ({
|
||||
host,
|
||||
target: targetIp,
|
||||
port,
|
||||
chapUser,
|
||||
chapPassword
|
||||
}) {
|
||||
const xapi = this.getXAPI(host)
|
||||
|
||||
const deviceConfig = {
|
||||
target: targetIp
|
||||
}
|
||||
|
||||
// if we give user and password
|
||||
if (chapUser && chapPassword) {
|
||||
deviceConfig.chapUser = chapUser
|
||||
deviceConfig.chapPassword = chapPassword
|
||||
}
|
||||
|
||||
// if we give another port than default iSCSI
|
||||
if (port) {
|
||||
deviceConfig.port = port
|
||||
}
|
||||
|
||||
let xml
|
||||
|
||||
try {
|
||||
await xapi.call(
|
||||
'SR.probe',
|
||||
host.ref,
|
||||
deviceConfig,
|
||||
'lvmoiscsi',
|
||||
{}
|
||||
)
|
||||
|
||||
throw new Error('the call above should have thrown an error')
|
||||
} catch (error) {
|
||||
if (error.code === 'SR_BACKEND_FAILURE_141') {
|
||||
return []
|
||||
}
|
||||
if (error.code !== 'SR_BACKEND_FAILURE_96') {
|
||||
throw error
|
||||
}
|
||||
|
||||
xml = parseXml(error.params[2])
|
||||
}
|
||||
|
||||
const targets = []
|
||||
forEach(ensureArray(xml['iscsi-target-iqns'].TGT), target => {
|
||||
// if the target is on another IP adress, do not display it
|
||||
if (target.IPAddress.trim() === targetIp) {
|
||||
targets.push({
|
||||
iqn: target.TargetIQN.trim(),
|
||||
ip: target.IPAddress.trim()
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return targets
|
||||
}
|
||||
|
||||
probeIscsiIqns.params = {
|
||||
host: { type: 'string' },
|
||||
target: { type: 'string' },
|
||||
port: { type: 'integer', optional: true },
|
||||
chapUser: { type: 'string', optional: true },
|
||||
chapPassword: { type: 'string', optional: true }
|
||||
}
|
||||
probeIscsiIqns.resolve = {
|
||||
host: ['host', 'host', 'administrate']
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// This function helps to detect all iSCSI ID and LUNs on a Target
|
||||
// It will return a LUN table
|
||||
|
||||
export async function probeIscsiLuns ({
|
||||
host,
|
||||
target: targetIp,
|
||||
port,
|
||||
targetIqn,
|
||||
chapUser,
|
||||
chapPassword
|
||||
}) {
|
||||
const xapi = this.getXAPI(host)
|
||||
|
||||
const deviceConfig = {
|
||||
target: targetIp,
|
||||
targetIQN: targetIqn
|
||||
}
|
||||
|
||||
// if we give user and password
|
||||
if (chapUser && chapPassword) {
|
||||
deviceConfig.chapUser = chapUser
|
||||
deviceConfig.chapPassword = chapPassword
|
||||
}
|
||||
|
||||
// if we give another port than default iSCSI
|
||||
if (port) {
|
||||
deviceConfig.port = port
|
||||
}
|
||||
|
||||
let xml
|
||||
|
||||
try {
|
||||
await xapi.call(
|
||||
'SR.probe',
|
||||
host.ref,
|
||||
deviceConfig,
|
||||
'lvmoiscsi',
|
||||
{}
|
||||
)
|
||||
|
||||
throw new Error('the call above should have thrown an error')
|
||||
} catch (error) {
|
||||
if (error.code !== 'SR_BACKEND_FAILURE_107') {
|
||||
throw error
|
||||
}
|
||||
|
||||
xml = parseXml(error.params[2])
|
||||
}
|
||||
|
||||
const luns = []
|
||||
forEach(ensureArray(xml['iscsi-target'].LUN), lun => {
|
||||
luns.push({
|
||||
id: lun.LUNid.trim(),
|
||||
vendor: lun.vendor.trim(),
|
||||
serial: lun.serial.trim(),
|
||||
size: lun.size.trim(),
|
||||
scsiId: lun.SCSIid.trim()
|
||||
})
|
||||
})
|
||||
|
||||
return luns
|
||||
}
|
||||
|
||||
probeIscsiLuns.params = {
|
||||
host: { type: 'string' },
|
||||
target: { type: 'string' },
|
||||
port: { type: 'integer', optional: true},
|
||||
targetIqn: { type: 'string' },
|
||||
chapUser: { type: 'string', optional: true },
|
||||
chapPassword: { type: 'string', optional: true }
|
||||
}
|
||||
|
||||
probeIscsiLuns.resolve = {
|
||||
host: ['host', 'host', 'administrate']
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// This function helps to detect if this target already exists in XAPI
|
||||
// It returns a table of SR UUID, empty if no existing connections
|
||||
|
||||
export async function probeIscsiExists ({
|
||||
host,
|
||||
target: targetIp,
|
||||
port,
|
||||
targetIqn,
|
||||
scsiId,
|
||||
chapUser,
|
||||
chapPassword
|
||||
}) {
|
||||
const xapi = this.getXAPI(host)
|
||||
|
||||
const deviceConfig = {
|
||||
target: targetIp,
|
||||
targetIQN: targetIqn,
|
||||
SCSIid: scsiId
|
||||
}
|
||||
|
||||
// if we give user and password
|
||||
if (chapUser && chapPassword) {
|
||||
deviceConfig.chapUser = chapUser
|
||||
deviceConfig.chapPassword = chapPassword
|
||||
}
|
||||
|
||||
// if we give another port than default iSCSI
|
||||
if (port) {
|
||||
deviceConfig.port = port
|
||||
}
|
||||
|
||||
const xml = parseXml(await xapi.call('SR.probe', host.ref, deviceConfig, 'lvmoiscsi', {}))
|
||||
|
||||
const srs = []
|
||||
forEach(ensureArray(xml['SRlist'].SR), sr => {
|
||||
// get the UUID of SR connected to this LUN
|
||||
srs.push({uuid: sr.UUID.trim()})
|
||||
})
|
||||
|
||||
return srs
|
||||
}
|
||||
|
||||
probeIscsiExists.params = {
|
||||
host: { type: 'string' },
|
||||
target: { type: 'string' },
|
||||
port: { type: 'integer', optional: true },
|
||||
targetIqn: { type: 'string' },
|
||||
scsiId: { type: 'string' },
|
||||
chapUser: { type: 'string', optional: true },
|
||||
chapPassword: { type: 'string', optional: true }
|
||||
}
|
||||
|
||||
probeIscsiExists.resolve = {
|
||||
host: ['host', 'host', 'administrate']
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// This function helps to detect if this NFS SR already exists in XAPI
|
||||
// It returns a table of SR UUID, empty if no existing connections
|
||||
|
||||
export async function probeNfsExists ({
|
||||
host,
|
||||
server,
|
||||
serverPath,
|
||||
}) {
|
||||
const xapi = this.getXAPI(host)
|
||||
|
||||
const deviceConfig = {
|
||||
server,
|
||||
serverpath: serverPath
|
||||
}
|
||||
|
||||
const xml = parseXml(await xapi.call('SR.probe', host.ref, deviceConfig, 'nfs', {}))
|
||||
|
||||
const srs = []
|
||||
|
||||
forEach(ensureArray(xml['SRlist'].SR), sr => {
|
||||
// get the UUID of SR connected to this LUN
|
||||
srs.push({uuid: sr.UUID.trim()})
|
||||
})
|
||||
|
||||
return srs
|
||||
}
|
||||
|
||||
probeNfsExists.params = {
|
||||
host: { type: 'string' },
|
||||
server: { type: 'string' },
|
||||
serverPath: { type: 'string' }
|
||||
}
|
||||
|
||||
probeNfsExists.resolve = {
|
||||
host: ['host', 'host', 'administrate']
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// This function helps to reattach a forgotten NFS/iSCSI SR
|
||||
|
||||
export async function reattach ({
|
||||
host,
|
||||
uuid,
|
||||
nameLabel,
|
||||
nameDescription,
|
||||
type,
|
||||
}) {
|
||||
const xapi = this.getXAPI(host)
|
||||
|
||||
if (type === 'iscsi') {
|
||||
type = 'lvmoiscsi' // the internal XAPI name
|
||||
}
|
||||
|
||||
const srRef = await xapi.call(
|
||||
'SR.introduce',
|
||||
uuid,
|
||||
nameLabel,
|
||||
nameDescription,
|
||||
type,
|
||||
'user',
|
||||
true,
|
||||
{}
|
||||
)
|
||||
|
||||
const sr = await xapi.call('SR.get_record', srRef)
|
||||
return sr.uuid
|
||||
}
|
||||
|
||||
reattach.params = {
|
||||
host: { type: 'string' },
|
||||
uuid: { type: 'string' },
|
||||
nameLabel: { type: 'string' },
|
||||
nameDescription: { type: 'string' },
|
||||
type: { type: 'string' }
|
||||
}
|
||||
|
||||
reattach.resolve = {
|
||||
host: ['host', 'host', 'administrate']
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// This function helps to reattach a forgotten ISO SR
|
||||
|
||||
export async function reattachIso ({
|
||||
host,
|
||||
uuid,
|
||||
nameLabel,
|
||||
nameDescription,
|
||||
type,
|
||||
}) {
|
||||
const xapi = this.getXAPI(host)
|
||||
|
||||
if (type === 'iscsi') {
|
||||
type = 'lvmoiscsi' // the internal XAPI name
|
||||
}
|
||||
|
||||
const srRef = await xapi.call(
|
||||
'SR.introduce',
|
||||
uuid,
|
||||
nameLabel,
|
||||
nameDescription,
|
||||
type,
|
||||
'iso',
|
||||
true,
|
||||
{}
|
||||
)
|
||||
|
||||
const sr = await xapi.call('SR.get_record', srRef)
|
||||
return sr.uuid
|
||||
}
|
||||
|
||||
reattachIso.params = {
|
||||
host: { type: 'string' },
|
||||
uuid: { type: 'string' },
|
||||
nameLabel: { type: 'string' },
|
||||
nameDescription: { type: 'string' },
|
||||
type: { type: 'string' }
|
||||
}
|
||||
|
||||
reattachIso.resolve = {
|
||||
host: ['host', 'host', 'administrate']
|
||||
}
|
||||
25
src/api/task.js
Normal file
25
src/api/task.js
Normal file
@@ -0,0 +1,25 @@
|
||||
export async function cancel ({task}) {
|
||||
await this.getXAPI(task).call('task.cancel', task.ref)
|
||||
}
|
||||
|
||||
cancel.params = {
|
||||
id: { type: 'string' }
|
||||
}
|
||||
|
||||
cancel.resolve = {
|
||||
task: ['id', 'task', 'administrate']
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export async function destroy ({task}) {
|
||||
await this.getXAPI(task).call('task.destroy', task.ref)
|
||||
}
|
||||
|
||||
destroy.params = {
|
||||
id: { type: 'string' }
|
||||
}
|
||||
|
||||
destroy.resolve = {
|
||||
task: ['id', 'task', 'administrate']
|
||||
}
|
||||
33
src/api/test.js
Normal file
33
src/api/test.js
Normal file
@@ -0,0 +1,33 @@
|
||||
import {delay} from 'bluebird'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export function hasPermission ({userId, objectId, permission}) {
|
||||
return this.hasPermission(userId, objectId, permission)
|
||||
}
|
||||
|
||||
hasPermission.permission = 'admin'
|
||||
|
||||
hasPermission.params = {
|
||||
userId: {
|
||||
type: 'string'
|
||||
},
|
||||
objectId: {
|
||||
type: 'string'
|
||||
},
|
||||
permission: {
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export function wait ({duration, returnValue}) {
|
||||
return delay(returnValue, +duration)
|
||||
}
|
||||
|
||||
wait.params = {
|
||||
duration: {
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
{$wait} = require '../fibers-utils'
|
||||
|
||||
#=====================================================================
|
||||
|
||||
# Creates a new token.
|
||||
#
|
||||
# TODO: Token permission.
|
||||
exports.create = ->
|
||||
userId = @session.get 'user_id'
|
||||
|
||||
# The user MUST be signed in and not with a token
|
||||
@throw 'UNAUTHORIZED' if not userId? or @session.has 'token_id'
|
||||
|
||||
# Creates the token.
|
||||
token = $wait @tokens.generate userId
|
||||
|
||||
return token.id
|
||||
|
||||
# Deletes a token.
|
||||
exports.delete = ({token: tokenId}) ->
|
||||
# Gets the token.
|
||||
token = $wait @tokens.first tokenId
|
||||
@throw 'NO_SUCH_OBJECT' unless token?
|
||||
|
||||
# Deletes the token.
|
||||
$wait @tokens.remove tokenId
|
||||
|
||||
return true
|
||||
exports.delete.params = {
|
||||
token: { type: 'string' }
|
||||
}
|
||||
34
src/api/token.js
Normal file
34
src/api/token.js
Normal file
@@ -0,0 +1,34 @@
|
||||
import {Unauthorized} from '../api-errors'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
// TODO: Token permission.
|
||||
export async function create () {
|
||||
// The user MUST not be signed with a token
|
||||
if (this.session.has('token_id')) {
|
||||
throw new Unauthorized()
|
||||
}
|
||||
|
||||
const userId = this.session.get('user_id')
|
||||
return (await this.createAuthenticationToken({userId})).id
|
||||
}
|
||||
|
||||
create.description = 'create a new authentication token'
|
||||
|
||||
create.permission = '' // sign in
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
async function delete_ ({token: id}) {
|
||||
await this.deleteAuthenticationToken(id)
|
||||
}
|
||||
|
||||
export {delete_ as delete}
|
||||
|
||||
delete_.description = 'delete an existing authentication token'
|
||||
|
||||
delete_.permission = 'admin'
|
||||
|
||||
delete_.params = {
|
||||
token: { type: 'string' }
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
{$wait} = require '../fibers-utils'
|
||||
|
||||
#=====================================================================
|
||||
|
||||
# Creates a new user.
|
||||
exports.create = ({email, password, permission}) ->
|
||||
# Creates the user.
|
||||
user = $wait @users.create email, password, permission
|
||||
|
||||
return user.id
|
||||
exports.create.permission = 'admin'
|
||||
exports.create.params = {
|
||||
email: { type: 'string' }
|
||||
password: { type: 'string' }
|
||||
permission: { type: 'string', optional: true}
|
||||
}
|
||||
|
||||
# Deletes an existing user.
|
||||
#
|
||||
# FIXME: a user should not be able to delete itself.
|
||||
exports.delete = ({id}) ->
|
||||
# The user cannot delete himself.
|
||||
@throw 'INVALID_PARAMS' if id is @session.get 'user_id'
|
||||
|
||||
# Throws an error if the user did not exist.
|
||||
@throw 'NO_SUCH_OBJECT' unless $wait @users.remove id
|
||||
|
||||
return true
|
||||
exports.delete.permission = 'admin'
|
||||
exports.delete.params = {
|
||||
id: { type: 'string' }
|
||||
}
|
||||
|
||||
# Changes the password of the current user.
|
||||
exports.changePassword = ({old, new: newP}) ->
|
||||
# Gets the current user (which MUST exist).
|
||||
user = $wait @users.first @session.get 'user_id'
|
||||
|
||||
# Checks its old password.
|
||||
@throw 'INVALID_CREDENTIAL' unless user.checkPassword old
|
||||
|
||||
# Sets the new password.
|
||||
user.setPassword newP
|
||||
|
||||
# Updates the user.
|
||||
$wait @users.update user
|
||||
|
||||
return true
|
||||
exports.changePassword.permission = '' # Signed in.
|
||||
exports.changePassword.params = {
|
||||
old: { type: 'string' }
|
||||
new: { type: 'string' }
|
||||
}
|
||||
|
||||
# Returns the user with a given identifier.
|
||||
exports.get = ({id}) ->
|
||||
# Only an administrator can see another user.
|
||||
@checkPermission 'admin' unless @session.get 'user_id' is id
|
||||
|
||||
# Retrieves the user.
|
||||
user = $wait @users.first id
|
||||
|
||||
# Throws an error if it did not exist.
|
||||
@throw 'NO_SUCH_OBJECT' unless user
|
||||
|
||||
return @getUserPublicProperties user
|
||||
exports.get.params = {
|
||||
id: { type: 'string' }
|
||||
}
|
||||
|
||||
# Returns all users.
|
||||
exports.getAll = ->
|
||||
# Retrieves the users.
|
||||
users = $wait @users.get()
|
||||
|
||||
# Filters out private properties.
|
||||
for user, i in users
|
||||
users[i] = @getUserPublicProperties user
|
||||
|
||||
return users
|
||||
exports.getAll.permission = 'admin'
|
||||
|
||||
# Changes the properties of an existing user.
|
||||
exports.set = ({id, email, password, permission}) ->
|
||||
# Retrieves the user.
|
||||
user = $wait @users.first id
|
||||
|
||||
# Throws an error if it did not exist.
|
||||
@throw 'NO_SUCH_OBJECT' unless user
|
||||
|
||||
# Updates the provided properties.
|
||||
user.set {email} if email?
|
||||
user.set {permission} if permission?
|
||||
user.setPassword password if password?
|
||||
|
||||
# Updates the user.
|
||||
$wait @users.update user
|
||||
|
||||
return true
|
||||
exports.set.permission = 'admin'
|
||||
exports.set.params = {
|
||||
id: { type: 'string' }
|
||||
email: { type: 'string', optional: true }
|
||||
password: { type: 'string', optional: true }
|
||||
permission: { type: 'string', optional: true }
|
||||
}
|
||||
74
src/api/user.js
Normal file
74
src/api/user.js
Normal file
@@ -0,0 +1,74 @@
|
||||
import map from 'lodash.map'
|
||||
|
||||
import {InvalidParameters} from '../api-errors'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export async function create ({email, password, permission}) {
|
||||
return (await this.createUser({email, password, permission})).id
|
||||
}
|
||||
|
||||
create.description = 'creates a new user'
|
||||
|
||||
create.permission = 'admin'
|
||||
|
||||
create.params = {
|
||||
email: { type: 'string' },
|
||||
password: { type: 'string' },
|
||||
permission: { type: 'string', optional: true}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
// Deletes an existing user.
|
||||
async function delete_ ({id}) {
|
||||
if (id === this.session.get('user_id')) {
|
||||
throw new InvalidParameters('an user cannot delete itself')
|
||||
}
|
||||
|
||||
await this.deleteUser(id)
|
||||
}
|
||||
|
||||
// delete is not a valid identifier.
|
||||
export {delete_ as delete}
|
||||
|
||||
delete_.description = 'deletes an existing user'
|
||||
|
||||
delete_.permission = 'admin'
|
||||
|
||||
delete_.params = {
|
||||
id: { type: 'string' }
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
// TODO: remove this function when users are integrated to the main
|
||||
// collection.
|
||||
export async function getAll () {
|
||||
// Retrieves the users.
|
||||
const users = await this._users.get()
|
||||
|
||||
// Filters out private properties.
|
||||
return map(users, this.getUserPublicProperties)
|
||||
}
|
||||
|
||||
getAll.description = 'returns all the existing users'
|
||||
|
||||
getAll.permission = 'admin'
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export async function set ({id, email, password, permission}) {
|
||||
await this.updateUser(id, {email, password, permission})
|
||||
}
|
||||
|
||||
set.description = 'changes the properties of an existing user'
|
||||
|
||||
set.permission = 'admin'
|
||||
|
||||
set.params = {
|
||||
id: { type: 'string' },
|
||||
email: { type: 'string', optional: true },
|
||||
password: { type: 'string', optional: true },
|
||||
permission: { type: 'string', optional: true }
|
||||
}
|
||||
@@ -1,37 +1,89 @@
|
||||
{$wait} = require '../fibers-utils'
|
||||
# FIXME: too low level, should be removed.
|
||||
|
||||
{coroutine: $coroutine} = require 'bluebird'
|
||||
|
||||
#=====================================================================
|
||||
|
||||
exports.delete = ({id}) ->
|
||||
try
|
||||
VBD = @getObject params.id
|
||||
catch
|
||||
@throw 'NO_SUCH_OBJECT'
|
||||
|
||||
xapi = @getXAPI VBD
|
||||
delete_ = $coroutine ({vbd}) ->
|
||||
xapi = @getXAPI vbd
|
||||
|
||||
# TODO: check if VBD is attached before
|
||||
$wait xapi.call 'VBD.destroy', VBD.ref
|
||||
yield xapi.call 'VBD.destroy', vbd.ref
|
||||
|
||||
return true
|
||||
exports.delete.permission = 'admin'
|
||||
exports.delete.params = {
|
||||
|
||||
delete_.params = {
|
||||
id: { type: 'string' }
|
||||
}
|
||||
|
||||
exports.disconnect = ({id}) ->
|
||||
try
|
||||
VBD = @getObject params.id
|
||||
catch
|
||||
@throw 'NO_SUCH_OBJECT'
|
||||
delete_.resolve = {
|
||||
vbd: ['id', 'VBD', 'administrate'],
|
||||
}
|
||||
|
||||
xapi = @getXAPI VBD
|
||||
exports.delete = delete_
|
||||
|
||||
#---------------------------------------------------------------------
|
||||
|
||||
disconnect = $coroutine ({vbd}) ->
|
||||
xapi = @getXAPI vbd
|
||||
|
||||
# TODO: check if VBD is attached before
|
||||
$wait xapi.call 'VBD.unplug_force', VBD.ref
|
||||
yield xapi.call 'VBD.unplug_force', vbd.ref
|
||||
|
||||
return true
|
||||
exports.disconnect.permission = 'admin'
|
||||
exports.disconnect.params = {
|
||||
|
||||
disconnect.params = {
|
||||
id: { type: 'string' }
|
||||
}
|
||||
|
||||
disconnect.resolve = {
|
||||
vbd: ['id', 'VBD', 'administrate'],
|
||||
}
|
||||
|
||||
exports.disconnect = disconnect
|
||||
|
||||
#---------------------------------------------------------------------
|
||||
|
||||
connect = $coroutine ({vbd}) ->
|
||||
xapi = @getXAPI vbd
|
||||
|
||||
# TODO: check if VBD is attached before
|
||||
yield xapi.call 'VBD.plug', vbd.ref
|
||||
|
||||
return true
|
||||
|
||||
connect.params = {
|
||||
id: { type: 'string' }
|
||||
}
|
||||
|
||||
connect.resolve = {
|
||||
vbd: ['id', 'VBD', 'administrate'],
|
||||
}
|
||||
|
||||
exports.connect = connect
|
||||
|
||||
#---------------------------------------------------------------------
|
||||
|
||||
set = $coroutine (params) ->
|
||||
{vbd} = params
|
||||
xapi = @getXAPI vbd
|
||||
|
||||
{ref} = vbd
|
||||
|
||||
# VBD position
|
||||
if 'position' of params
|
||||
yield xapi.call 'VBD.set_userdevice', ref, params.position
|
||||
|
||||
set.params = {
|
||||
# Identifier of the VBD to update.
|
||||
id: { type: 'string' }
|
||||
|
||||
position: { type: 'string', optional: true }
|
||||
|
||||
}
|
||||
|
||||
set.resolve = {
|
||||
vbd: ['id', 'VBD', 'administrate'],
|
||||
}
|
||||
|
||||
exports.set = set
|
||||
|
||||
@@ -1,49 +1,51 @@
|
||||
{isArray: $isArray} = require 'underscore'
|
||||
# FIXME: rename to disk.*
|
||||
|
||||
$isArray = require 'lodash.isarray'
|
||||
|
||||
#---------------------------------------------------------------------
|
||||
|
||||
{$wait} = require '../fibers-utils'
|
||||
{coroutine: $coroutine} = require 'bluebird'
|
||||
|
||||
#=====================================================================
|
||||
|
||||
exports.delete = ({id}) ->
|
||||
try
|
||||
VDI = @getObject id
|
||||
catch
|
||||
@throw 'NO_SUCH_OBJECT'
|
||||
|
||||
xapi = @getXAPI VDI
|
||||
delete_ = $coroutine ({vdi}) ->
|
||||
xapi = @getXAPI vdi
|
||||
|
||||
# TODO: check if VDI is attached before
|
||||
$wait xapi.call 'VDI.destroy', VDI.ref
|
||||
yield xapi.call 'VDI.destroy', vdi.ref
|
||||
|
||||
return true
|
||||
exports.delete.permission = 'admin'
|
||||
exports.delete.params =
|
||||
id:
|
||||
type: 'string'
|
||||
|
||||
exports.set = (params) ->
|
||||
try
|
||||
VDI = @getObject params.id
|
||||
catch
|
||||
@throw 'NO_SUCH_OBJECT'
|
||||
delete_.params = {
|
||||
id: { type: 'string' },
|
||||
}
|
||||
|
||||
xapi = @getXAPI VDI
|
||||
delete_.resolve = {
|
||||
vdi: ['id', 'VDI', 'administrate'],
|
||||
}
|
||||
|
||||
{ref} = VDI
|
||||
exports.delete = delete_
|
||||
|
||||
#---------------------------------------------------------------------
|
||||
|
||||
# FIXME: human readable strings should be handled.
|
||||
set = $coroutine (params) ->
|
||||
{vdi} = params
|
||||
xapi = @getXAPI vdi
|
||||
|
||||
{ref} = vdi
|
||||
|
||||
# Size.
|
||||
if 'size' of params
|
||||
{size} = params
|
||||
|
||||
if size < VDI.size
|
||||
if size < vdi.size
|
||||
@throw(
|
||||
'INVALID_SIZE'
|
||||
"cannot set new size below the current size (#{VDI.size})"
|
||||
"cannot set new size below the current size (#{vdi.size})"
|
||||
)
|
||||
|
||||
$wait xapi.call 'VDI.resize_online', ref, "#{size}"
|
||||
yield xapi.call 'VDI.resize_online', ref, "#{size}"
|
||||
|
||||
# Other fields.
|
||||
for param, fields of {
|
||||
@@ -53,11 +55,11 @@ exports.set = (params) ->
|
||||
continue unless param of params
|
||||
|
||||
for field in (if $isArray fields then fields else [fields])
|
||||
$wait xapi.call "VDI.set_#{field}", ref, "#{params[param]}"
|
||||
yield xapi.call "VDI.set_#{field}", ref, "#{params[param]}"
|
||||
|
||||
return true
|
||||
exports.set.permission = 'admin'
|
||||
exports.set.params = {
|
||||
|
||||
set.params = {
|
||||
# Identifier of the VDI to update.
|
||||
id: { type: 'string' }
|
||||
|
||||
@@ -68,3 +70,31 @@ exports.set.params = {
|
||||
# size of VDI
|
||||
size: { type: 'integer', optional: true }
|
||||
}
|
||||
|
||||
set.resolve = {
|
||||
vdi: ['id', 'VDI', 'administrate'],
|
||||
}
|
||||
|
||||
exports.set = set
|
||||
|
||||
#---------------------------------------------------------------------
|
||||
|
||||
migrate = $coroutine ({vdi, sr}) ->
|
||||
xapi = @getXAPI vdi
|
||||
|
||||
# TODO: check if VDI is attached before
|
||||
yield xapi.call 'VDI.pool_migrate', vdi.ref, sr.ref, {}
|
||||
|
||||
return true
|
||||
|
||||
migrate.params = {
|
||||
id: { type: 'string' }
|
||||
sr_id: { type: 'string' }
|
||||
}
|
||||
|
||||
migrate.resolve = {
|
||||
vdi: ['id', 'VDI', 'administrate'],
|
||||
sr: ['sr_id', 'SR', 'administrate'],
|
||||
}
|
||||
|
||||
exports.migrate = migrate
|
||||
|
||||
43
src/api/vif.js
Normal file
43
src/api/vif.js
Normal file
@@ -0,0 +1,43 @@
|
||||
async function delete_ ({vif}) {
|
||||
// TODO: check if VIF is attached before
|
||||
await this.getXAPI(vif).call('VIF.destroy', vif.ref)
|
||||
}
|
||||
export {delete_ as delete}
|
||||
|
||||
delete_.params = {
|
||||
id: { type: 'string' }
|
||||
}
|
||||
|
||||
delete_.resolve = {
|
||||
vif: ['id', 'VIF', 'administrate']
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export async function disconnect ({vif}) {
|
||||
// TODO: check if VIF is attached before
|
||||
await this.getXAPI(vif).call('VIF.unplug_force', vif.ref)
|
||||
}
|
||||
|
||||
disconnect.params = {
|
||||
id: { type: 'string' }
|
||||
}
|
||||
|
||||
disconnect.resolve = {
|
||||
vif: ['id', 'VIF', 'operate']
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export async function connect ({vif}) {
|
||||
// TODO: check if VIF is attached before
|
||||
await this.getXAPI(vif).call('VIF.plug', vif.ref)
|
||||
}
|
||||
|
||||
connect.params = {
|
||||
id: { type: 'string' }
|
||||
}
|
||||
|
||||
connect.resolve = {
|
||||
vif: ['id', 'VIF', 'operate']
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
5
src/api/xo.js
Normal file
5
src/api/xo.js
Normal file
@@ -0,0 +1,5 @@
|
||||
export function getAllObjects () {
|
||||
return this.getObjects()
|
||||
}
|
||||
|
||||
getAllObjects.permission = ''
|
||||
@@ -1,255 +1,173 @@
|
||||
'use strict';
|
||||
import Bluebird from 'bluebird'
|
||||
import isArray from 'lodash.isarray'
|
||||
import isObject from 'lodash.isobject'
|
||||
import Model from './model'
|
||||
import {BaseError} from 'make-error'
|
||||
import {EventEmitter} from 'events'
|
||||
import {mapInPlace} from './utils'
|
||||
|
||||
//====================================================================
|
||||
// ===================================================================
|
||||
|
||||
var _ = require('underscore');
|
||||
var Promise = require('bluebird');
|
||||
|
||||
//====================================================================
|
||||
|
||||
function Collection()
|
||||
{
|
||||
// Parent constructor.
|
||||
Collection.super_.call(this);
|
||||
export class ModelAlreadyExists extends BaseError {
|
||||
constructor (id) {
|
||||
super('this model already exists: ' + id)
|
||||
}
|
||||
}
|
||||
require('util').inherits(Collection, require('events').EventEmitter);
|
||||
|
||||
Collection.prototype.model = require('./model');
|
||||
// ===================================================================
|
||||
|
||||
/**
|
||||
* Adds new models to this collection.
|
||||
*/
|
||||
Collection.prototype.add = function (models, options) {
|
||||
var array = true;
|
||||
if (!_.isArray(models))
|
||||
{
|
||||
models = [models];
|
||||
array = false;
|
||||
}
|
||||
export default class Collection extends EventEmitter {
|
||||
// Default value for Model.
|
||||
get Model () {
|
||||
return Model
|
||||
}
|
||||
|
||||
for (var i = 0, n = models.length; i < n; ++i)
|
||||
{
|
||||
var model = models[i];
|
||||
// Make this property writable.
|
||||
set Model (Model) {
|
||||
Object.defineProperty(this, 'Model', {
|
||||
configurable: true,
|
||||
enumerale: true,
|
||||
value: Model,
|
||||
writable: true
|
||||
})
|
||||
}
|
||||
|
||||
if ( !(model instanceof this.model) )
|
||||
{
|
||||
model = new this.model(model);
|
||||
}
|
||||
constructor () {
|
||||
super()
|
||||
}
|
||||
|
||||
var error = model.validate();
|
||||
if (undefined !== error)
|
||||
{
|
||||
// TODO: Better system inspired by Backbone.js.
|
||||
throw error;
|
||||
}
|
||||
add (models, opts) {
|
||||
const array = isArray(models)
|
||||
if (!array) {
|
||||
models = [models]
|
||||
|
||||
models[i] = model.properties;
|
||||
}
|
||||
}
|
||||
|
||||
var self = this;
|
||||
return Promise.cast(this._add(models, options)).then(function (models) {
|
||||
self.emit('add', models);
|
||||
const {Model} = this
|
||||
mapInPlace(models, model => {
|
||||
if (!(model instanceof Model)) {
|
||||
model = new Model(model)
|
||||
}
|
||||
|
||||
if (!array)
|
||||
{
|
||||
return models[0];
|
||||
}
|
||||
return models;
|
||||
});
|
||||
};
|
||||
const error = model.validate()
|
||||
if (error) {
|
||||
// TODO: Better system inspired by Backbone.js
|
||||
throw error
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
Collection.prototype.first = function (properties) {
|
||||
if (!_.isObject(properties))
|
||||
{
|
||||
properties = (undefined !== properties)
|
||||
? { 'id': properties }
|
||||
: {}
|
||||
;
|
||||
}
|
||||
return model.properties
|
||||
})
|
||||
|
||||
var self = this;
|
||||
return Promise.cast(this._first(properties)).then(function (model) {
|
||||
if (!model)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return Bluebird.try(this._add, [models, opts], this).then(models => {
|
||||
this.emit('add', models)
|
||||
|
||||
return new self.model(model);
|
||||
});
|
||||
};
|
||||
return array ? models : new this.Model(models[0])
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all models which have a given set of properties.
|
||||
*
|
||||
* /!\: Does not return instances of this.model.
|
||||
*/
|
||||
Collection.prototype.get = function (properties) {
|
||||
// For coherence with other methods.
|
||||
if (!_.isObject(properties))
|
||||
{
|
||||
properties = (undefined !== properties)
|
||||
? { 'id': properties }
|
||||
: {}
|
||||
;
|
||||
}
|
||||
first (properties) {
|
||||
if (!isObject(properties)) {
|
||||
properties = (properties !== undefined) ?
|
||||
{ id: properties } :
|
||||
{}
|
||||
}
|
||||
|
||||
/* jshint newcap: false */
|
||||
return Promise.cast(this._get(properties));
|
||||
};
|
||||
return Bluebird.try(this._first, [properties], this).then(
|
||||
model => model && new this.Model(model)
|
||||
)
|
||||
}
|
||||
|
||||
get (properties) {
|
||||
if (!isObject(properties)) {
|
||||
properties = (properties !== undefined) ?
|
||||
{ id: properties } :
|
||||
{}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes models from this collection.
|
||||
*/
|
||||
Collection.prototype.remove = function (ids) {
|
||||
if (!_.isArray(ids))
|
||||
{
|
||||
ids = [ids];
|
||||
}
|
||||
return Bluebird.try(this._get, [properties], this)
|
||||
}
|
||||
|
||||
var self = this;
|
||||
return Promise.cast(this._remove(ids)).then(function () {
|
||||
self.emit('remove', ids);
|
||||
return true;
|
||||
});
|
||||
};
|
||||
remove (ids) {
|
||||
if (!isArray(ids)) {
|
||||
ids = [ids]
|
||||
}
|
||||
|
||||
/**
|
||||
* Smartly updates the collection.
|
||||
*
|
||||
* - Adds new models.
|
||||
* - Updates existing models.
|
||||
* - Removes missing models.
|
||||
*/
|
||||
// Collection.prototype.set = function (/*models*/) {
|
||||
// // TODO:
|
||||
// };
|
||||
return Bluebird.try(this._remove, [ids], this).then(() => {
|
||||
this.emit('remove', ids)
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates existing models.
|
||||
*/
|
||||
Collection.prototype.update = function (models) {
|
||||
var array = true;
|
||||
if (!_.isArray(models))
|
||||
{
|
||||
models = [models];
|
||||
array = false;
|
||||
}
|
||||
update (models) {
|
||||
const array = isArray(models)
|
||||
if (!isArray(models)) {
|
||||
models = [models]
|
||||
}
|
||||
|
||||
for (var i = 0, n = models.length; i < n; i++)
|
||||
{
|
||||
var model = models[i];
|
||||
const {Model} = this
|
||||
mapInPlace(models, model => {
|
||||
if (!(model instanceof Model)) {
|
||||
// TODO: Problems, we may be mixing in some default
|
||||
// properties which will overwrite existing ones.
|
||||
model = new Model(model)
|
||||
}
|
||||
|
||||
if ( !(model instanceof this.model) )
|
||||
{
|
||||
// TODO: Problems, we may be mixing in some default
|
||||
// properties which will overwrite existing ones.
|
||||
model = new this.model(model);
|
||||
}
|
||||
const id = model.get('id')
|
||||
|
||||
var id = model.get('id');
|
||||
// Missing models should be added not updated.
|
||||
if (id === undefined) {
|
||||
// FIXME: should not throw an exception but return a rejected promise.
|
||||
throw new Error('a model without an id cannot be updated')
|
||||
}
|
||||
|
||||
// Missing models should be added not updated.
|
||||
if (!id)
|
||||
{
|
||||
return Promise.reject('a model without an id cannot be updated');
|
||||
}
|
||||
const error = model.validate()
|
||||
if (error !== undefined) {
|
||||
// TODO: Better system inspired by Backbone.js.
|
||||
throw error
|
||||
}
|
||||
|
||||
var error = model.validate();
|
||||
if (undefined !== error)
|
||||
{
|
||||
// TODO: Better system inspired by Backbone.js.
|
||||
throw error;
|
||||
}
|
||||
return model.properties
|
||||
})
|
||||
|
||||
models[i] = model.properties;
|
||||
}
|
||||
return Bluebird.try(this._update, [models], this).then(models => {
|
||||
this.emit('update', models)
|
||||
|
||||
var self = this;
|
||||
return Promise.cast(this._update(models)).then(function (models) {
|
||||
self.emit('update', models);
|
||||
return array ? models : new this.Model(models[0])
|
||||
})
|
||||
}
|
||||
|
||||
if (!array)
|
||||
{
|
||||
return models[0];
|
||||
}
|
||||
return models;
|
||||
});
|
||||
};
|
||||
// Methods to override in implementations.
|
||||
|
||||
//Collection.extend = require('extendable');
|
||||
_add () {
|
||||
throw new Error('not implemented')
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// Methods to override in implementations.
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
_get () {
|
||||
throw new Error('not implemented')
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
Collection.prototype._add = function (models, options) {
|
||||
throw 'not implemented';
|
||||
};
|
||||
_remove () {
|
||||
throw new Error('not implemented')
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
Collection.prototype._get = function (properties) {
|
||||
throw 'not implemented';
|
||||
};
|
||||
_update () {
|
||||
throw new Error('not implemented')
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
Collection.prototype._remove = function (ids) {
|
||||
throw 'not implemented';
|
||||
};
|
||||
// Methods which may be overridden in implementations.
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
Collection.prototype._update = function (models) {
|
||||
throw 'not implemented';
|
||||
};
|
||||
count (properties) {
|
||||
return this.get(properties).get('count')
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// Methods which may be overriden in implementations.
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
exists (properties) {
|
||||
/* jshint eqnull: true */
|
||||
return this.first(properties).then(model => model != null)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
Collection.prototype.count = function (properties) {
|
||||
return this.get(properties).then(function (models) {
|
||||
return models.length;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
Collection.prototype.exists = function (properties) {
|
||||
return this.first(properties).then(function (model) {
|
||||
return (null !== model);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
Collection.prototype._first = function (properties) {
|
||||
return Promise.cast(this.get(properties)).then(function (models) {
|
||||
if (0 === models.length)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return models[0];
|
||||
});
|
||||
};
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
|
||||
module.exports = Collection;
|
||||
_first (properties) {
|
||||
return Bluebird.try(this.get, [properties], this).then(
|
||||
models => models.length ? models[0] : null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
//====================================================================
|
||||
|
||||
var _ = require('underscore');
|
||||
var Promise = require('bluebird');
|
||||
|
||||
//====================================================================
|
||||
|
||||
function Memory(models)
|
||||
{
|
||||
Memory.super_.call(this);
|
||||
|
||||
this.models = {};
|
||||
this.next_id = 0;
|
||||
|
||||
if (models)
|
||||
{
|
||||
this.add(models);
|
||||
}
|
||||
}
|
||||
require('util').inherits(Memory, require('../collection'));
|
||||
|
||||
Memory.prototype._add = function (models, options) {
|
||||
// TODO: Temporary mesure, implement “set()” instead.
|
||||
var replace = !!(options && options.replace);
|
||||
|
||||
for (var i = 0, n = models.length; i < n; ++i)
|
||||
{
|
||||
var model = models[i];
|
||||
|
||||
var id = model.id;
|
||||
|
||||
if (undefined === id)
|
||||
{
|
||||
model.id = id = ''+ this.next_id++;
|
||||
}
|
||||
else if (!replace && this.models[id])
|
||||
{
|
||||
// Existing models are ignored.
|
||||
return Promise.reject('cannot add existing models!');
|
||||
}
|
||||
|
||||
this.models[id] = model;
|
||||
}
|
||||
|
||||
return models;
|
||||
};
|
||||
|
||||
Memory.prototype._first = function (properties) {
|
||||
if (_.isEmpty(properties))
|
||||
{
|
||||
// Return the first model if any.
|
||||
for (var id in this.models)
|
||||
{
|
||||
return this.models[id];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return _.findWhere(this.models, properties);
|
||||
};
|
||||
|
||||
Memory.prototype._get = function (properties) {
|
||||
if (_.isEmpty(properties))
|
||||
{
|
||||
return _.values(this.models);
|
||||
}
|
||||
|
||||
return _.where(this.models, properties);
|
||||
};
|
||||
|
||||
Memory.prototype._remove = function (ids) {
|
||||
for (var i = 0, n = ids.length; i < n; ++i)
|
||||
{
|
||||
delete this.models[ids[i]];
|
||||
}
|
||||
};
|
||||
|
||||
Memory.prototype._update = function (models) {
|
||||
for (var i = 0, n = models.length; i < n; i++)
|
||||
{
|
||||
var model = models[i];
|
||||
|
||||
var id = model.id;
|
||||
|
||||
// Missing models should be added not updated.
|
||||
if (!this.models[id])
|
||||
{
|
||||
return Promise.reject('missing model');
|
||||
}
|
||||
|
||||
_.extend(this.models[id], model);
|
||||
}
|
||||
return models;
|
||||
};
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
|
||||
Memory.extend = require('extendable');
|
||||
module.exports = Memory;
|
||||
|
||||
@@ -1,22 +1,21 @@
|
||||
'use strict';
|
||||
import Bluebird, {coroutine} from 'bluebird'
|
||||
import Collection, {ModelAlreadyExists} from '../collection'
|
||||
import difference from 'lodash.difference'
|
||||
import filter from 'lodash.filter'
|
||||
import forEach from 'lodash.foreach'
|
||||
import getKey from 'lodash.keys'
|
||||
import isEmpty from 'lodash.isempty'
|
||||
import map from 'lodash.map'
|
||||
import thenRedis from 'then-redis'
|
||||
|
||||
//====================================================================
|
||||
|
||||
var _ = require('underscore');
|
||||
var Promise = require('bluebird');
|
||||
|
||||
var thenRedis = require('then-redis');
|
||||
|
||||
//====================================================================
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// ///////////////////////////////////////////////////////////////////
|
||||
// Data model:
|
||||
// - prefix +'_id': value of the last generated identifier;
|
||||
// - prefix +'_ids': set containing identifier of all models;
|
||||
// - prefix +'_'+ index +':' + value: set of identifiers which have
|
||||
// value for the given index.
|
||||
// - prefix +':'+ id: hash containing the properties of a model;
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// ///////////////////////////////////////////////////////////////////
|
||||
|
||||
// TODO: then-redis sends commands in order, we should use this
|
||||
// semantic to simplify the code.
|
||||
@@ -26,208 +25,138 @@ var thenRedis = require('then-redis');
|
||||
|
||||
// TODO: Remote events.
|
||||
|
||||
function Redis(options, models)
|
||||
{
|
||||
if (!options)
|
||||
{
|
||||
options = {};
|
||||
}
|
||||
export default class Redis extends Collection {
|
||||
constructor ({
|
||||
connection,
|
||||
indexes = [],
|
||||
prefix,
|
||||
uri = 'tcp://localhost:6379',
|
||||
}) {
|
||||
super()
|
||||
|
||||
_.defaults(options, {
|
||||
'uri': 'tcp://localhost:6379',
|
||||
'indexes': [],
|
||||
});
|
||||
this.indexes = indexes
|
||||
this.prefix = prefix
|
||||
this.redis = connection || thenRedis.createClient(uri)
|
||||
}
|
||||
|
||||
if (!options.prefix)
|
||||
{
|
||||
throw 'missing option: prefix';
|
||||
}
|
||||
_extract (ids) {
|
||||
const prefix = this.prefix + ':'
|
||||
const {redis} = this
|
||||
|
||||
Redis.super_.call(this, models);
|
||||
const models = []
|
||||
return Bluebird.map(ids, id => {
|
||||
return redis.hgetall(prefix + id).then(model => {
|
||||
// If empty, consider it a no match.
|
||||
if (isEmpty(model)) {
|
||||
return
|
||||
}
|
||||
|
||||
this.redis = options.connection || thenRedis.createClient(options.uri);
|
||||
this.prefix = options.prefix;
|
||||
this.indexes = options.indexes;
|
||||
// Mix the identifier in.
|
||||
model.id = id
|
||||
|
||||
models.push(model)
|
||||
})
|
||||
}).return(models)
|
||||
}
|
||||
|
||||
_add (models, {replace = false} = {}) {
|
||||
// TODO: remove “replace” which is a temporary measure, implement
|
||||
// “set()” instead.
|
||||
|
||||
const {indexes, prefix, redis, idPrefix = ''} = this
|
||||
|
||||
return Bluebird.map(models, coroutine(function * (model) {
|
||||
// Generate a new identifier if necessary.
|
||||
if (model.id === undefined) {
|
||||
model.id = idPrefix + String(yield redis.incr(prefix + '_id'))
|
||||
}
|
||||
|
||||
const success = yield redis.sadd(prefix + '_ids', model.id)
|
||||
|
||||
// The entry already exists an we are not in replace mode.
|
||||
if (!success && !replace) {
|
||||
throw new ModelAlreadyExists(model.id)
|
||||
}
|
||||
|
||||
// TODO: Remove existing fields.
|
||||
|
||||
const params = []
|
||||
forEach(model, (value, name) => {
|
||||
// No need to store the identifier (already in the key).
|
||||
if (name === 'id') {
|
||||
return
|
||||
}
|
||||
|
||||
params.push(name, value)
|
||||
})
|
||||
|
||||
const promises = [
|
||||
redis.hmset(prefix + ':' + model.id, ...params)
|
||||
]
|
||||
|
||||
// Update indexes.
|
||||
forEach(indexes, (index) => {
|
||||
const value = model[index]
|
||||
if (value === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
const key = prefix + '_' + index + ':' + value
|
||||
promises.push(redis.sadd(key, model.id))
|
||||
})
|
||||
|
||||
yield Bluebird.all(promises)
|
||||
|
||||
return model
|
||||
}))
|
||||
}
|
||||
|
||||
_get (properties) {
|
||||
const {prefix, redis} = this
|
||||
|
||||
if (isEmpty(properties)) {
|
||||
return redis.smembers(prefix + '_ids').then(ids => this._extract(ids))
|
||||
}
|
||||
|
||||
// Special treatment for the identifier.
|
||||
const id = properties.id
|
||||
if (id !== undefined) {
|
||||
delete properties.id
|
||||
return this._extract([id]).then(models => {
|
||||
return (models.length && !isEmpty(properties)) ?
|
||||
filter(models) :
|
||||
models
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
const {indexes} = this
|
||||
|
||||
// Check for non indexed fields.
|
||||
const unfit = difference(getKey(properties), indexes)
|
||||
if (unfit.length) {
|
||||
throw new Error('fields not indexed: ' + unfit.join())
|
||||
}
|
||||
|
||||
const keys = map(properties, (value, index) => prefix + '_' + index + ':' + value)
|
||||
return redis.sinter(...keys).then(ids => this._extract(ids))
|
||||
}
|
||||
|
||||
_remove (ids) {
|
||||
const {prefix, redis} = this
|
||||
|
||||
// TODO: handle indexes.
|
||||
|
||||
return Bluebird.all([
|
||||
// Remove the identifiers from the main index.
|
||||
redis.srem(prefix + '_ids', ...ids),
|
||||
|
||||
// Remove the models.
|
||||
redis.del(map(ids, id => prefix + ':' + id))
|
||||
])
|
||||
}
|
||||
|
||||
_update (models) {
|
||||
return this._add(models, { replace: true })
|
||||
}
|
||||
}
|
||||
require('util').inherits(Redis, require('../collection'));
|
||||
|
||||
// Private method.
|
||||
Redis.prototype._extract = function (ids) {
|
||||
var redis = this.redis;
|
||||
var prefix = this.prefix +':';
|
||||
|
||||
var promises = [];
|
||||
|
||||
_.each(ids, function (id) {
|
||||
promises.push(redis.hgetall(prefix + id).then(function (model) {
|
||||
// If empty, considers it a no match and returns null.
|
||||
if (_.isEmpty(model))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Mix the identifier in.
|
||||
model.id = id;
|
||||
return model;
|
||||
}));
|
||||
});
|
||||
|
||||
return Promise.all(promises).then(function (models) {
|
||||
return _.filter(models, function (model) {
|
||||
return (null !== model);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
Redis.prototype._add = function (models, options) {
|
||||
// TODO: Temporary mesure, implement “set()” instead.
|
||||
var replace = !!(options && options.replace);
|
||||
|
||||
var redis = this.redis;
|
||||
var prefix = this.prefix;
|
||||
var indexes = this.indexes;
|
||||
|
||||
var promises = [];
|
||||
|
||||
_.each(models, function (model) {
|
||||
var promise;
|
||||
|
||||
// Generates a new identifier if necessary.
|
||||
if (undefined === model.id)
|
||||
{
|
||||
promise = redis.incr(prefix +'_id').then(function (id) {
|
||||
model.id = id;
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
// Ensures the promise chain is correctly initialized.
|
||||
promise = Promise.cast();
|
||||
}
|
||||
|
||||
promise = promise.then(function () {
|
||||
// Adds the identifier to the models' ids set.
|
||||
return redis.sadd(prefix +'_ids', model.id);
|
||||
}).then(function (success) {
|
||||
// The entry already existed an we are not in replace mode.
|
||||
if (!success && !replace)
|
||||
{
|
||||
throw 'cannot add existing model: '+ model.id;
|
||||
}
|
||||
|
||||
// TODO: Remove existing fields.
|
||||
|
||||
var params = [prefix +':'+ model.id];
|
||||
_.each(model, function (value, prop) {
|
||||
// No need to store the id (already in the key.)
|
||||
if ('id' === prop)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
params.push(prop, value);
|
||||
});
|
||||
|
||||
var promises = [
|
||||
redis.send('hmset', params),
|
||||
];
|
||||
|
||||
// Adds indexes.
|
||||
_.each(indexes, function (index) {
|
||||
var value = model[index];
|
||||
if (undefined === value)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var key = prefix +'_'+ index +':'+ value;
|
||||
promises.push(redis.sadd(key, model.id));
|
||||
});
|
||||
|
||||
return Promise.all(promises);
|
||||
|
||||
}).then(function () { return model; });
|
||||
|
||||
promises.push(promise);
|
||||
});
|
||||
|
||||
return Promise.all(promises);
|
||||
};
|
||||
|
||||
Redis.prototype._get = function (properties) {
|
||||
var prefix = this.prefix;
|
||||
var redis = this.redis;
|
||||
var self = this;
|
||||
|
||||
if (_.isEmpty(properties))
|
||||
{
|
||||
return redis.smembers(prefix +'_ids').then(function (ids) {
|
||||
return self._extract(ids);
|
||||
});
|
||||
}
|
||||
|
||||
// Special treatment for 'id'.
|
||||
var id = properties.id;
|
||||
delete properties.id;
|
||||
|
||||
// Special case where we only match against id.
|
||||
if (_.isEmpty(properties))
|
||||
{
|
||||
return this._extract([id]);
|
||||
}
|
||||
|
||||
var indexes = this.indexes;
|
||||
var unfit = _.difference(_.keys(properties), indexes);
|
||||
if (0 !== unfit.length)
|
||||
{
|
||||
throw 'not indexed fields: '+ unfit.join();
|
||||
}
|
||||
|
||||
var keys = _.map(properties, function (value, index) {
|
||||
return (prefix +'_'+ index +':'+ value);
|
||||
});
|
||||
return redis.send('sinter', keys).then(function (ids) {
|
||||
if (undefined !== id)
|
||||
{
|
||||
if (!_.contains(ids, id))
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
ids = [id];
|
||||
}
|
||||
|
||||
return self._extract(ids);
|
||||
});
|
||||
};
|
||||
|
||||
Redis.prototype._remove = function (ids) {
|
||||
var redis = this.redis;
|
||||
var prefix = this.prefix;
|
||||
|
||||
var promises = [];
|
||||
|
||||
var keys = [];
|
||||
for (var i = 0, n = ids.length; i < n; ++i)
|
||||
{
|
||||
keys.push(prefix +':'+ ids[i]);
|
||||
}
|
||||
|
||||
// TODO: Handle indexes.
|
||||
promises.push(
|
||||
redis.send('srem', [prefix +'_ids'].concat(ids)),
|
||||
redis.send('del', keys)
|
||||
);
|
||||
|
||||
return Promise.all(promises);
|
||||
};
|
||||
|
||||
Redis.prototype._update = function (models) {
|
||||
// TODO:
|
||||
return this._add(models, { 'replace': true });
|
||||
};
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
|
||||
Redis.extend = require('extendable');
|
||||
module.exports = Redis;
|
||||
|
||||
@@ -1,81 +1,52 @@
|
||||
'use strict';
|
||||
import {EventEmitter} from 'events'
|
||||
|
||||
//====================================================================
|
||||
// ===================================================================
|
||||
|
||||
var EventEmitter = require('events').EventEmitter;
|
||||
var inherits = require('util').inherits;
|
||||
// const noop = () => {}
|
||||
|
||||
//--------------------------------------------------------------------
|
||||
// ===================================================================
|
||||
|
||||
var extend = require('underscore').extend;
|
||||
export default class Connection extends EventEmitter {
|
||||
constructor () {
|
||||
super()
|
||||
|
||||
//====================================================================
|
||||
this._data = Object.create(null)
|
||||
}
|
||||
|
||||
var has = Object.prototype.hasOwnProperty;
|
||||
has = has.call.bind(has);
|
||||
// Close the connection.
|
||||
close () {
|
||||
// Prevent errors when the connection is closed more than once.
|
||||
// this.close = noop
|
||||
|
||||
//====================================================================
|
||||
this.emit('close')
|
||||
}
|
||||
|
||||
var Connection = function Connection(adapter) {
|
||||
this.data = Object.create(null);
|
||||
// Gets the value for this key.
|
||||
get (key, defaultValue) {
|
||||
const {_data: data} = this
|
||||
|
||||
this._adapter = adapter;
|
||||
};
|
||||
inherits(Connection, EventEmitter);
|
||||
if (key in data) {
|
||||
return data[key]
|
||||
}
|
||||
|
||||
extend(Connection.prototype, {
|
||||
// Close the connection.
|
||||
close: function () {
|
||||
this._adapter.close();
|
||||
this.emit('close');
|
||||
if (arguments.length >= 2) {
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
// Releases values AMAP to ease the garbage collecting.
|
||||
for (var key in this)
|
||||
{
|
||||
if (has(this, key))
|
||||
{
|
||||
delete this[key];
|
||||
}
|
||||
}
|
||||
},
|
||||
throw new Error('no value for `' + key + '`')
|
||||
}
|
||||
|
||||
// Gets the value for this key.
|
||||
get: function (key, defaultValue) {
|
||||
var data = this.data;
|
||||
// Checks whether there is a value for this key.
|
||||
has (key) {
|
||||
return key in this._data
|
||||
}
|
||||
|
||||
if (key in data)
|
||||
{
|
||||
return data[key];
|
||||
}
|
||||
// Sets the value for this key.
|
||||
set (key, value) {
|
||||
this._data[key] = value
|
||||
}
|
||||
|
||||
if (arguments.length >= 2)
|
||||
{
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
throw new Error('no value for `'+ key +'`');
|
||||
},
|
||||
|
||||
// Checks whether there is a value for this key.
|
||||
has: function (key) {
|
||||
return key in this.data;
|
||||
},
|
||||
|
||||
// Sets the value for this key.
|
||||
set: function (key, value) {
|
||||
this.data[key] = value;
|
||||
},
|
||||
|
||||
// Sends a message.
|
||||
send: function (name, data) {
|
||||
this._adapter.send(name, data);
|
||||
},
|
||||
|
||||
unset: function (key) {
|
||||
delete this.data[key];
|
||||
},
|
||||
});
|
||||
|
||||
//====================================================================
|
||||
|
||||
module.exports = Connection;
|
||||
unset (key) {
|
||||
delete this._data[key]
|
||||
}
|
||||
}
|
||||
|
||||
87
src/decorators.js
Normal file
87
src/decorators.js
Normal file
@@ -0,0 +1,87 @@
|
||||
import bind from 'lodash.bind'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const {defineProperty} = Object
|
||||
|
||||
// ===================================================================
|
||||
|
||||
// See: https://github.com/jayphelps/core-decorators.js#autobind
|
||||
export function autobind (target, key, {
|
||||
configurable,
|
||||
enumerable,
|
||||
value: fn,
|
||||
writable
|
||||
}) {
|
||||
return {
|
||||
configurable,
|
||||
enumerable,
|
||||
|
||||
get () {
|
||||
const bounded = bind(fn, this)
|
||||
|
||||
defineProperty(this, key, {
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
value: bounded,
|
||||
writable: true
|
||||
})
|
||||
|
||||
return bounded
|
||||
},
|
||||
set (newValue) {
|
||||
if (this === target) {
|
||||
// New value directly set on the prototype.
|
||||
delete this[key]
|
||||
this[key] = newValue
|
||||
} else {
|
||||
// New value set on a child object.
|
||||
|
||||
// Cannot use assignment because it will call the setter on
|
||||
// the prototype.
|
||||
defineProperty(this, key, {
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
value: newValue,
|
||||
writable: true
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
// Debounce decorator for methods.
|
||||
//
|
||||
// See: https://github.com/wycats/javascript-decorators
|
||||
export const debounce = (duration) => (target, name, descriptor) => {
|
||||
const {value: fn} = descriptor
|
||||
|
||||
// This symbol is used to store the related data directly on the
|
||||
// current object.
|
||||
const s = Symbol()
|
||||
|
||||
function debounced () {
|
||||
let data = this[s] || (this[s] = {
|
||||
lastCall: 0,
|
||||
wrapper: null
|
||||
})
|
||||
|
||||
const now = Date.now()
|
||||
if (now > data.lastCall + duration) {
|
||||
data.lastCall = now
|
||||
try {
|
||||
const result = fn.apply(this, arguments)
|
||||
data.wrapper = () => result
|
||||
} catch (error) {
|
||||
data.wrapper = () => { throw error }
|
||||
}
|
||||
}
|
||||
return data.wrapper()
|
||||
}
|
||||
debounced.reset = (obj) => { delete obj[s] }
|
||||
|
||||
descriptor.value = debounced
|
||||
return descriptor
|
||||
}
|
||||
78
src/decorators.spec.js
Normal file
78
src/decorators.spec.js
Normal file
@@ -0,0 +1,78 @@
|
||||
/* eslint-env mocha */
|
||||
|
||||
import {expect} from 'chai'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
import {autobind, debounce} from './decorators'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
describe('autobind', function () {
|
||||
class Foo {
|
||||
@autobind
|
||||
getFoo () {
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
it('returns a bound instance for a method', function () {
|
||||
const foo = new Foo()
|
||||
const {getFoo} = foo
|
||||
|
||||
expect(getFoo()).to.equal(foo)
|
||||
})
|
||||
|
||||
it('returns the same bound instance each time', function () {
|
||||
const foo = new Foo()
|
||||
|
||||
expect(foo.getFoo).to.equal(foo.getFoo)
|
||||
})
|
||||
|
||||
it('works with multiple instances of the same class', function () {
|
||||
const foo1 = new Foo()
|
||||
const foo2 = new Foo()
|
||||
|
||||
const {getFoo: getFoo1} = foo1
|
||||
const {getFoo: getFoo2} = foo2
|
||||
|
||||
expect(getFoo1()).to.equal(foo1)
|
||||
expect(getFoo2()).to.equal(foo2)
|
||||
})
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
describe('debounce', function () {
|
||||
let i
|
||||
|
||||
class Foo {
|
||||
@debounce(1e1)
|
||||
foo () {
|
||||
++i
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
i = 0
|
||||
})
|
||||
|
||||
it('works', function (done) {
|
||||
const foo = new Foo()
|
||||
|
||||
expect(i).to.equal(0)
|
||||
|
||||
foo.foo()
|
||||
expect(i).to.equal(1)
|
||||
|
||||
foo.foo()
|
||||
expect(i).to.equal(1)
|
||||
|
||||
setTimeout(function () {
|
||||
foo.foo()
|
||||
expect(i).to.equal(2)
|
||||
|
||||
done()
|
||||
}, 2e1)
|
||||
})
|
||||
})
|
||||
@@ -1,141 +0,0 @@
|
||||
# Low level tools.
|
||||
$_ = require 'underscore'
|
||||
|
||||
# Async code is easier with fibers (light threads)!
|
||||
$fiber = require 'fibers'
|
||||
|
||||
$Promise = require 'bluebird'
|
||||
|
||||
#=====================================================================
|
||||
|
||||
$isPromise = (obj) -> obj? and $_.isFunction obj.then
|
||||
|
||||
# The value is guarantee to resolve asynchronously.
|
||||
$runAsync = (value, resolve, reject) ->
|
||||
if $isPromise value
|
||||
return value.then resolve, reject
|
||||
|
||||
if $_.isFunction value # Continuable
|
||||
async = false
|
||||
handler = (error, result) ->
|
||||
unless async
|
||||
return process.nextTick handler.bind null, error, result
|
||||
if error?
|
||||
return reject error
|
||||
resolve result
|
||||
value handler
|
||||
async = true
|
||||
return
|
||||
|
||||
unless $_.isObject value
|
||||
return process.nextTick -> resolve value
|
||||
|
||||
left = 0
|
||||
results = if $_.isArray value
|
||||
new Array value.length
|
||||
else
|
||||
Object.create null
|
||||
|
||||
$_.each value, (value, index) ->
|
||||
++left
|
||||
$runAsync(
|
||||
value
|
||||
(result) ->
|
||||
# Returns if already rejected.
|
||||
return unless results
|
||||
|
||||
results[index] = result
|
||||
resolve results unless --left
|
||||
(error) ->
|
||||
# Returns if already rejected.
|
||||
return unless results
|
||||
|
||||
# Frees the reference ASAP.
|
||||
results = null
|
||||
|
||||
reject error
|
||||
)
|
||||
|
||||
if left is 0
|
||||
process.nextTick -> resolve value
|
||||
|
||||
#=====================================================================
|
||||
|
||||
# Makes a function running in its own fiber.
|
||||
$fiberize = (fn) ->
|
||||
(args...) ->
|
||||
$fiber(=>
|
||||
try
|
||||
fn.apply this, args
|
||||
catch error
|
||||
process.nextTick ->
|
||||
throw error
|
||||
).run()
|
||||
|
||||
# Makes a function run in its own fiber and returns a promise.
|
||||
$promisify = (fn) ->
|
||||
(args...) ->
|
||||
new $Promise (resolve, reject) ->
|
||||
$fiber(=>
|
||||
try
|
||||
resolve fn.apply this, args
|
||||
catch error
|
||||
reject error
|
||||
).run()
|
||||
|
||||
# Waits for an event.
|
||||
#
|
||||
# Note: if the *error* event is emitted, this function will throw.
|
||||
$waitEvent = (emitter, event) ->
|
||||
fiber = $fiber.current
|
||||
throw new Error 'not running in a fiber' unless fiber?
|
||||
|
||||
errorHandler = null
|
||||
handler = (args...) ->
|
||||
emitter.removeListener 'error', errorHandler
|
||||
fiber.run args
|
||||
errorHandler = (error) ->
|
||||
emitter.removeListener event, handler
|
||||
fiber.throwInto error
|
||||
|
||||
emitter.once event, handler
|
||||
emitter.once 'error', errorHandler
|
||||
|
||||
$fiber.yield()
|
||||
|
||||
# Waits for a promise or a continuable to end.
|
||||
#
|
||||
# If value is composed (array or map), every asynchronous value is
|
||||
# resolved before returning (parallelization).
|
||||
$wait = (value) ->
|
||||
fiber = $fiber.current
|
||||
throw new Error 'not running in a fiber' unless fiber?
|
||||
|
||||
if $wait._stash
|
||||
value = $wait._stash
|
||||
delete $wait._stash
|
||||
|
||||
$runAsync(
|
||||
value
|
||||
fiber.run.bind fiber
|
||||
fiber.throwInto.bind fiber
|
||||
)
|
||||
|
||||
$fiber.yield()
|
||||
|
||||
$wait.register = ->
|
||||
throw new Error 'something has already been registered' if $wait._stash
|
||||
|
||||
deferred = $Promise.defer()
|
||||
$wait._stash = deferred.promise
|
||||
|
||||
deferred.callback
|
||||
|
||||
#=====================================================================
|
||||
|
||||
module.exports = {
|
||||
$fiberize
|
||||
$promisify
|
||||
$waitEvent
|
||||
$wait
|
||||
}
|
||||
@@ -1,247 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
//====================================================================
|
||||
|
||||
var expect = require('chai').expect;
|
||||
|
||||
//--------------------------------------------------------------------
|
||||
|
||||
var Promise = require('bluebird');
|
||||
|
||||
//--------------------------------------------------------------------
|
||||
|
||||
var utils = require('./fibers-utils');
|
||||
var $fiberize = utils.$fiberize;
|
||||
|
||||
//====================================================================
|
||||
|
||||
describe('$fiberize', function () {
|
||||
it('creates a function which runs in a new fiber', function () {
|
||||
var previous = require('fibers').current;
|
||||
|
||||
var fn = $fiberize(function () {
|
||||
var current = require('fibers').current;
|
||||
|
||||
expect(current).to.exists;
|
||||
expect(current).to.not.equal(previous);
|
||||
});
|
||||
|
||||
fn();
|
||||
});
|
||||
|
||||
it('forwards all arguments (even this)', function () {
|
||||
var self = {};
|
||||
var arg1 = {};
|
||||
var arg2 = {};
|
||||
|
||||
$fiberize(function (arg1, arg2) {
|
||||
expect(this).to.equal(self);
|
||||
expect(arg1).to.equal(arg1);
|
||||
expect(arg2).to.equal(arg2);
|
||||
}).call(self, arg1, arg2);
|
||||
});
|
||||
});
|
||||
|
||||
//--------------------------------------------------------------------
|
||||
|
||||
describe('$wait', function () {
|
||||
var $wait = utils.$wait;
|
||||
|
||||
it('waits for a promise', function (done) {
|
||||
$fiberize(function () {
|
||||
var value = {};
|
||||
var promise = Promise.cast(value);
|
||||
|
||||
expect($wait(promise)).to.equal(value);
|
||||
|
||||
done();
|
||||
})();
|
||||
});
|
||||
|
||||
it('handles promise rejection', function (done) {
|
||||
$fiberize(function () {
|
||||
var promise = Promise.reject('an exception');
|
||||
|
||||
expect(function () {
|
||||
$wait(promise);
|
||||
}).to.throw('an exception');
|
||||
|
||||
done();
|
||||
})();
|
||||
});
|
||||
|
||||
it('waits for a continuable', function (done) {
|
||||
$fiberize(function () {
|
||||
var value = {};
|
||||
var continuable = function (callback) {
|
||||
callback(null, value);
|
||||
};
|
||||
|
||||
expect($wait(continuable)).to.equal(value);
|
||||
|
||||
done();
|
||||
})();
|
||||
});
|
||||
|
||||
it('handles continuable error', function (done) {
|
||||
$fiberize(function () {
|
||||
var continuable = function (callback) {
|
||||
callback('an exception');
|
||||
};
|
||||
|
||||
expect(function () {
|
||||
$wait(continuable);
|
||||
}).to.throw('an exception');
|
||||
|
||||
done();
|
||||
})();
|
||||
});
|
||||
|
||||
it('forwards scalar values', function (done) {
|
||||
$fiberize(function () {
|
||||
var value = 'a scalar value';
|
||||
expect($wait(value)).to.equal(value);
|
||||
|
||||
value = [
|
||||
'foo',
|
||||
'bar',
|
||||
'baz',
|
||||
];
|
||||
expect($wait(value)).to.deep.equal(value);
|
||||
|
||||
value = [];
|
||||
expect($wait(value)).to.deep.equal(value);
|
||||
|
||||
value = {
|
||||
foo: 'foo',
|
||||
bar: 'bar',
|
||||
baz: 'baz',
|
||||
};
|
||||
expect($wait(value)).to.deep.equal(value);
|
||||
|
||||
value = {};
|
||||
expect($wait(value)).to.deep.equal(value);
|
||||
|
||||
done();
|
||||
})();
|
||||
});
|
||||
|
||||
it('handles arrays of promises/continuables', function (done) {
|
||||
$fiberize(function () {
|
||||
var value1 = {};
|
||||
var value2 = {};
|
||||
|
||||
var promise = Promise.cast(value1);
|
||||
var continuable = function (callback) {
|
||||
callback(null, value2);
|
||||
};
|
||||
|
||||
var results = $wait([promise, continuable]);
|
||||
expect(results[0]).to.equal(value1);
|
||||
expect(results[1]).to.equal(value2);
|
||||
|
||||
done();
|
||||
})();
|
||||
});
|
||||
|
||||
it('handles maps of promises/continuable', function (done) {
|
||||
$fiberize(function () {
|
||||
var value1 = {};
|
||||
var value2 = {};
|
||||
|
||||
var promise = Promise.cast(value1);
|
||||
var continuable = function (callback) {
|
||||
callback(null, value2);
|
||||
};
|
||||
|
||||
var results = $wait({
|
||||
foo: promise,
|
||||
bar: continuable
|
||||
});
|
||||
expect(results.foo).to.equal(value1);
|
||||
expect(results.bar).to.equal(value2);
|
||||
|
||||
done();
|
||||
})();
|
||||
});
|
||||
|
||||
it('handles nested arrays/maps', function (done) {
|
||||
var promise = Promise.cast('a promise');
|
||||
var continuable = function (callback) {
|
||||
callback(null, 'a continuable');
|
||||
};
|
||||
|
||||
$fiberize(function () {
|
||||
expect($wait({
|
||||
foo: promise,
|
||||
bar: [
|
||||
continuable,
|
||||
'a scalar'
|
||||
]
|
||||
})).to.deep.equal({
|
||||
foo: 'a promise',
|
||||
bar: [
|
||||
'a continuable',
|
||||
'a scalar'
|
||||
]
|
||||
});
|
||||
|
||||
done();
|
||||
})();
|
||||
});
|
||||
|
||||
describe('#register()', function () {
|
||||
it('registers a callback-based function to be waited', function (done) {
|
||||
$fiberize(function () {
|
||||
var fn = function (value, callback) {
|
||||
callback(null, value);
|
||||
};
|
||||
|
||||
var value = {};
|
||||
expect($wait(fn(value, $wait.register()))).to.equal(value);
|
||||
|
||||
value = {};
|
||||
expect($wait(fn(value, $wait.register()))).to.equal(value);
|
||||
|
||||
done();
|
||||
})();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
//--------------------------------------------------------------------
|
||||
|
||||
describe('$waitEvent', function () {
|
||||
var $waitEvent = utils.$waitEvent;
|
||||
|
||||
it('waits for an event', function (done) {
|
||||
$fiberize(function () {
|
||||
var emitter = new (require('events').EventEmitter)();
|
||||
|
||||
var value = {};
|
||||
process.nextTick(function () {
|
||||
emitter.emit('foo', value);
|
||||
});
|
||||
|
||||
expect($waitEvent(emitter, 'foo')[0]).to.equal(value);
|
||||
|
||||
done();
|
||||
})();
|
||||
});
|
||||
|
||||
it('handles the error event', function (done) {
|
||||
$fiberize(function () {
|
||||
var emitter = new (require('events').EventEmitter)();
|
||||
|
||||
process.nextTick(function () {
|
||||
emitter.emit('error', 'an error');
|
||||
});
|
||||
|
||||
expect(function () {
|
||||
$waitEvent(emitter, 'foo');
|
||||
}).to.throw('an error');
|
||||
|
||||
done();
|
||||
})();
|
||||
});
|
||||
});
|
||||
@@ -1,330 +0,0 @@
|
||||
$_ = require 'underscore'
|
||||
|
||||
# FIXME: This file name should reflect what's inside!
|
||||
|
||||
#=====================================================================
|
||||
|
||||
$asArray = (val) -> if $_.isArray val then val else [val]
|
||||
$asFunction = (val) -> if $_.isFunction val then val else -> val
|
||||
|
||||
$each = $_.each
|
||||
|
||||
$first = (collection, def) ->
|
||||
if (n = collection.length)?
|
||||
return collection[0] unless n is 0
|
||||
else
|
||||
return value for own _, value of collection
|
||||
|
||||
# Nothing was found, returns the `def` value.
|
||||
def
|
||||
|
||||
$removeValue = (array, value) ->
|
||||
index = array.indexOf value
|
||||
return false if index is -1
|
||||
array.splice index, 1
|
||||
true
|
||||
|
||||
#---------------------------------------------------------------------
|
||||
|
||||
# TODO: currently the watch can be updated multiple times per
|
||||
# “$MappedCollection.set()” which is inefficient: it should be
|
||||
# possible to address that.
|
||||
|
||||
$watch = (collection, {
|
||||
# Key(s) of the “remote” objects watched.
|
||||
#
|
||||
# If it is a function, it is evaluated in the scope of the “current”
|
||||
# object. (TODO)
|
||||
#
|
||||
# Default: undefined
|
||||
keys
|
||||
|
||||
# Alias for `keys`.
|
||||
key
|
||||
|
||||
# Rule(s) of the “remote” objects watched.
|
||||
#
|
||||
# If it is a function, it is evaluated in the scope of the “current”
|
||||
# object. (TODO)
|
||||
#
|
||||
# Note: `key`/`keys` and `rule`/`rules` cannot be used both.
|
||||
#
|
||||
# Default: undefined
|
||||
rules
|
||||
|
||||
# Alias for `rules`.
|
||||
rule
|
||||
|
||||
# Value to add to the set.
|
||||
#
|
||||
# If it is a function, it is evaluated in the scope of the “remote”
|
||||
# object.
|
||||
#
|
||||
# Default: -> @val
|
||||
val
|
||||
|
||||
# Predicates the “remote” object must fulfill to be used.
|
||||
#
|
||||
# Default: -> true
|
||||
if: cond
|
||||
|
||||
# Function evaluated in the scope of the “remote” object which
|
||||
# returns the key of the object to update (usually the current one).
|
||||
#
|
||||
# TODO: Does it make sense to return an array?
|
||||
#
|
||||
# Default: undefined
|
||||
bind
|
||||
|
||||
# Initial value.
|
||||
init
|
||||
|
||||
# Function called when a loop is detected.
|
||||
#
|
||||
# Usually it is used to either throw an exception or do nothing to
|
||||
# stop the loop.
|
||||
#
|
||||
# Note: The function may also returns `true` to force the processing
|
||||
# to continue.
|
||||
#
|
||||
# Default: (number_of_loops) -> throw new Error 'loop detected'
|
||||
loopDetected
|
||||
}, fn) ->
|
||||
val = if val is undefined
|
||||
# The default value is simply the value of the item.
|
||||
-> @val
|
||||
else
|
||||
$asFunction val
|
||||
|
||||
loopDetected ?= -> throw new Error 'loop detected'
|
||||
|
||||
# Method allowing the cleanup when the helper is no longer used.
|
||||
#cleanUp = -> # TODO: noop for now.
|
||||
|
||||
# Keys of items using the current helper.
|
||||
consumers = Object.create null
|
||||
|
||||
# Current values.
|
||||
values = Object.create null
|
||||
values.common = init
|
||||
|
||||
# The number of nested processing for this watcher is counted to
|
||||
# avoid an infinite loop.
|
||||
loops = 0
|
||||
|
||||
updating = false
|
||||
|
||||
process = (event, items) ->
|
||||
return if updating
|
||||
|
||||
# Values are grouped by namespace.
|
||||
valuesByNamespace = Object.create null
|
||||
|
||||
$each items, (item, key) -> # `key` is a local variable.
|
||||
return unless not cond? or cond.call item
|
||||
|
||||
if bind?
|
||||
key = bind.call item
|
||||
|
||||
# If bind did not return a key, ignores this value.
|
||||
return unless key?
|
||||
|
||||
namespace = "$#{key}"
|
||||
else
|
||||
namespace = 'common'
|
||||
|
||||
# Computes the current value.
|
||||
value = val.call item
|
||||
|
||||
(valuesByNamespace[namespace] ?= []).push value
|
||||
|
||||
# Stops here if no values were computed.
|
||||
return if do ->
|
||||
return false for _ of valuesByNamespace
|
||||
true
|
||||
|
||||
if loops
|
||||
return unless (loopDetected loops) is true
|
||||
previousLoops = loops++
|
||||
|
||||
# For each namespace.
|
||||
for namespace, values_ of valuesByNamespace
|
||||
|
||||
# Updates the value.
|
||||
value = values[namespace]
|
||||
ctx = {
|
||||
# TODO: test the $_.clone
|
||||
value: if value is undefined then $_.clone init else value
|
||||
}
|
||||
changed = if event is 'enter'
|
||||
fn.call ctx, values_, {}
|
||||
else
|
||||
fn.call ctx, {}, values_
|
||||
|
||||
# Notifies watchers unless it is known the value has not
|
||||
# changed.
|
||||
unless changed is false
|
||||
values[namespace] = ctx.value
|
||||
updating = true
|
||||
if namespace is 'common'
|
||||
collection.touch consumers
|
||||
else
|
||||
collection.touch (namespace.substr 1)
|
||||
updating = false
|
||||
|
||||
loops = previousLoops
|
||||
|
||||
processOne = (event, item) ->
|
||||
process event, [item]
|
||||
|
||||
# Sets up the watch based on the provided criteria.
|
||||
#
|
||||
# TODO: provides a way to clean this when no longer used.
|
||||
keys = $asArray (keys ? key ? [])
|
||||
rules = $asArray (rules ? rule ? [])
|
||||
if not $_.isEmpty keys
|
||||
# Matching is done on the keys.
|
||||
|
||||
throw new Error 'cannot use keys and rules' unless $_.isEmpty rules
|
||||
|
||||
$each keys, (key) -> collection.on "key=#{key}", processOne
|
||||
|
||||
# Handles existing items.
|
||||
process 'enter', (collection.getRaw keys, true)
|
||||
else if not $_.isEmpty rules
|
||||
# Matching is done the rules.
|
||||
|
||||
$each rules, (rule) -> collection.on "rule=#{rule}", process
|
||||
|
||||
# TODO: Inefficient, is there another way?
|
||||
rules = do -> # Minor optimization.
|
||||
tmp = Object.create null
|
||||
tmp[rule] = true for rule in rules
|
||||
tmp
|
||||
$each collection.getRaw(), (item) ->
|
||||
processOne 'enter', item if item.rule of rules
|
||||
else
|
||||
# No matching done.
|
||||
|
||||
collection.on 'any', process
|
||||
|
||||
# Handles existing items.
|
||||
process 'enter', collection.getRaw()
|
||||
|
||||
# Creates the generator: the function which items will used to
|
||||
# register to this watcher and to get the current value.
|
||||
generator = do (key) -> # Declare a local variable.
|
||||
->
|
||||
{key} = this
|
||||
|
||||
# Register this item has a consumer.
|
||||
consumers[key] = true
|
||||
|
||||
# Returns the value for this item if any or the common value.
|
||||
namespace = "$#{key}"
|
||||
if namespace of values
|
||||
values[namespace]
|
||||
else
|
||||
values.common
|
||||
|
||||
# Creates a helper to unregister an item from this watcher.
|
||||
generator.unregister = do (key) -> # Declare a local variable.
|
||||
->
|
||||
{key} = this
|
||||
delete consumers[key]
|
||||
delete values["$#{key}"]
|
||||
|
||||
# Creates a helper to get the value without using an item.
|
||||
generator.raw = (key) ->
|
||||
values[if key? then "$#{key}" else 'common']
|
||||
|
||||
# Returns the generator.
|
||||
generator
|
||||
|
||||
#=====================================================================
|
||||
|
||||
$map = (options) ->
|
||||
options.init = Object.create null
|
||||
|
||||
$watch this, options, (entered, exited) ->
|
||||
changed = false
|
||||
|
||||
$each entered, ([key, value]) =>
|
||||
unless @value[key] is value
|
||||
@value[key] = value
|
||||
changed = true
|
||||
$each exited, ([key, value]) =>
|
||||
if key of @value
|
||||
delete @value[key]
|
||||
changed = true
|
||||
|
||||
changed
|
||||
|
||||
#---------------------------------------------------------------------
|
||||
|
||||
# Creates a set of value from various items.
|
||||
$set = (options) ->
|
||||
# Contrary to other helpers, the default value is the key.
|
||||
options.val ?= -> @key
|
||||
|
||||
options.init = []
|
||||
|
||||
$watch this, options, (entered, exited) ->
|
||||
changed = false
|
||||
|
||||
$each entered, (value) =>
|
||||
if (@value.indexOf value) is -1
|
||||
@value.push value
|
||||
changed = true
|
||||
|
||||
$each exited, (value) =>
|
||||
changed = true if $removeValue @value, value
|
||||
|
||||
changed
|
||||
|
||||
#---------------------------------------------------------------------
|
||||
|
||||
$sum = (options) ->
|
||||
options.init ?= 0
|
||||
|
||||
$watch this, options, (entered, exited) ->
|
||||
prev = @value
|
||||
|
||||
$each entered, (value) => @value += value
|
||||
$each exited, (value) => @value -= value
|
||||
|
||||
@value isnt prev
|
||||
|
||||
#---------------------------------------------------------------------
|
||||
|
||||
# Uses a value from another item.
|
||||
#
|
||||
# Important note: Behavior is not specified when binding to multiple
|
||||
# items.
|
||||
$val = (options) ->
|
||||
# The default value.
|
||||
def = options.default
|
||||
delete options.default
|
||||
|
||||
options.init ?= def
|
||||
|
||||
# Should the last value be kept instead of returning to the default
|
||||
# value when no items are available!
|
||||
keepLast = !!options.keepLast
|
||||
delete options.keepLast
|
||||
|
||||
$watch this, options, (entered, exited) ->
|
||||
prev = @value
|
||||
|
||||
@value = $first entered, (if keepLast then @value else def)
|
||||
|
||||
@value isnt prev
|
||||
|
||||
#=====================================================================
|
||||
|
||||
module.exports = {
|
||||
$map
|
||||
$set
|
||||
$sum
|
||||
$val
|
||||
}
|
||||
@@ -1,244 +0,0 @@
|
||||
{expect: $expect} = require 'chai'
|
||||
|
||||
$sinon = require 'sinon'
|
||||
|
||||
#---------------------------------------------------------------------
|
||||
|
||||
{$MappedCollection} = require './MappedCollection.coffee'
|
||||
|
||||
$nonBindedHelpers = require './helpers'
|
||||
|
||||
#=====================================================================
|
||||
|
||||
describe 'Helper', ->
|
||||
|
||||
# Shared variables.
|
||||
collection = $set = $sum = $val = null
|
||||
beforeEach ->
|
||||
# Creates the collection.
|
||||
collection = new $MappedCollection()
|
||||
|
||||
# Dispatcher used for tests.
|
||||
collection.dispatch = -> (@genkey.split '.')[0]
|
||||
|
||||
# Missing rules should be automatically created.
|
||||
collection.missingRule = collection.rule
|
||||
|
||||
# # Monkey patch the collection to see all emitted events.
|
||||
# emit = collection.emit
|
||||
# collection.emit = (args...) ->
|
||||
# console.log args...
|
||||
# emit.call collection, args...
|
||||
|
||||
# Binds helpers to this collection.
|
||||
{$set, $sum, $val} = do ->
|
||||
helpers = {}
|
||||
helpers[name] = fn.bind collection for name, fn of $nonBindedHelpers
|
||||
helpers
|
||||
|
||||
#-------------------------------------------------------------------
|
||||
|
||||
# All helpers share the same logical code, we need only to test one
|
||||
# extensively and test the others basically.
|
||||
#
|
||||
# $sum was chosen because it is the simplest helper to test.
|
||||
describe '$sum', ->
|
||||
|
||||
it 'with single key', ->
|
||||
collection.set foo: 1
|
||||
|
||||
collection.item sum: ->
|
||||
@val = $sum {
|
||||
key: 'foo'
|
||||
}
|
||||
|
||||
$expect(collection.get 'sum').to.equal 1
|
||||
|
||||
collection.set foo:2
|
||||
|
||||
$expect(collection.get 'sum').to.equal 2
|
||||
|
||||
collection.remove 'foo'
|
||||
|
||||
$expect(collection.get 'sum').to.equal 0
|
||||
|
||||
it 'with multiple keys', ->
|
||||
collection.set {
|
||||
foo: 1
|
||||
bar: 2
|
||||
}
|
||||
|
||||
collection.item sum: ->
|
||||
@val = $sum {
|
||||
keys: ['foo', 'bar']
|
||||
}
|
||||
|
||||
$expect(collection.get 'sum').to.equal 3
|
||||
|
||||
collection.set bar:3
|
||||
|
||||
$expect(collection.get 'sum').to.equal 4
|
||||
|
||||
collection.remove 'foo'
|
||||
|
||||
$expect(collection.get 'sum').to.equal 3
|
||||
|
||||
# FIXME: This test fails but this feature is not used.
|
||||
it.skip 'with dynamic keys', ->
|
||||
collection.set {
|
||||
foo: 1
|
||||
bar: 2
|
||||
}
|
||||
|
||||
collection.rule sum: ->
|
||||
@val = $sum {
|
||||
key: -> (@key.split '.')[1]
|
||||
}
|
||||
collection.set {
|
||||
'sum.foo': null
|
||||
'sum.bar': null
|
||||
}
|
||||
|
||||
$expect(collection.get 'sum.foo').to.equal 1
|
||||
$expect(collection.get 'sum.bar').to.equal 2
|
||||
|
||||
collection.remove 'bar'
|
||||
|
||||
$expect(collection.get 'sum.foo').to.equal 1
|
||||
$expect(collection.get 'sum.bar').to.equal 0
|
||||
|
||||
it 'with single rule', ->
|
||||
collection.set {
|
||||
'foo.1': 1
|
||||
'foo.2': 2
|
||||
}
|
||||
|
||||
collection.item sum: ->
|
||||
@val = $sum {
|
||||
rule: 'foo'
|
||||
}
|
||||
|
||||
$expect(collection.get 'sum').to.equal 3
|
||||
|
||||
collection.set 'foo.2':3
|
||||
|
||||
$expect(collection.get 'sum').to.equal 4
|
||||
|
||||
collection.remove 'foo.1'
|
||||
|
||||
$expect(collection.get 'sum').to.equal 3
|
||||
|
||||
it 'with multiple rules', ->
|
||||
collection.set {
|
||||
'foo': 1
|
||||
'bar.1': 2
|
||||
'bar.2': 3
|
||||
}
|
||||
|
||||
collection.item sum: ->
|
||||
@val = $sum {
|
||||
rules: ['foo', 'bar']
|
||||
}
|
||||
|
||||
$expect(collection.get 'sum').to.equal 6
|
||||
|
||||
collection.set 'bar.1':3
|
||||
|
||||
$expect(collection.get 'sum').to.equal 7
|
||||
|
||||
collection.remove 'bar.2'
|
||||
|
||||
$expect(collection.get 'sum').to.equal 4
|
||||
|
||||
it 'with bind', ->
|
||||
collection.set {
|
||||
'foo': {
|
||||
sum: 2 # This item will participate to `sum.2`.
|
||||
val: 1
|
||||
}
|
||||
'bar': {
|
||||
sum: 1 # This item will participate to `sum.1`.
|
||||
val: 2
|
||||
}
|
||||
}
|
||||
|
||||
collection.rule sum: ->
|
||||
@val = $sum {
|
||||
bind: ->
|
||||
id = @val.sum
|
||||
return unless id?
|
||||
"sum.#{id}"
|
||||
val: -> @val.val
|
||||
}
|
||||
collection.set {
|
||||
'sum.1': null
|
||||
'sum.2': null
|
||||
}
|
||||
|
||||
$expect(collection.get 'sum.1').equal 2
|
||||
$expect(collection.get 'sum.2').equal 1
|
||||
|
||||
collection.set {
|
||||
'foo': {
|
||||
sum: 1
|
||||
val: 3
|
||||
}
|
||||
}
|
||||
|
||||
$expect(collection.get 'sum.1').equal 5
|
||||
$expect(collection.get 'sum.2').equal 0
|
||||
|
||||
collection.remove 'bar'
|
||||
|
||||
$expect(collection.get 'sum.1').equal 3
|
||||
$expect(collection.get 'sum.2').equal 0
|
||||
|
||||
|
||||
it 'with predicate', ->
|
||||
collection.set {
|
||||
foo: 1
|
||||
bar: 2
|
||||
baz: 3
|
||||
}
|
||||
|
||||
collection.item sum: ->
|
||||
@val = $sum {
|
||||
if: -> /^b/.test @rule
|
||||
}
|
||||
|
||||
$expect(collection.get 'sum').equal 5
|
||||
|
||||
collection.set foo:4
|
||||
|
||||
$expect(collection.get 'sum').equal 5
|
||||
|
||||
collection.set bar:5
|
||||
|
||||
$expect(collection.get 'sum').equal 8
|
||||
|
||||
collection.remove 'baz'
|
||||
|
||||
$expect(collection.get 'sum').equal 5
|
||||
|
||||
it 'with initial value', ->
|
||||
collection.set foo: 1
|
||||
|
||||
collection.item sum: ->
|
||||
@val = $sum {
|
||||
key: 'foo'
|
||||
init: 2
|
||||
}
|
||||
|
||||
$expect(collection.get 'sum').to.equal 3
|
||||
|
||||
collection.set foo:2
|
||||
|
||||
$expect(collection.get 'sum').to.equal 4
|
||||
|
||||
collection.remove 'foo'
|
||||
|
||||
$expect(collection.get 'sum').to.equal 2
|
||||
|
||||
# TODO:
|
||||
# - dynamic keys
|
||||
# - dynamic rules
|
||||
458
src/index.js
Normal file
458
src/index.js
Normal file
@@ -0,0 +1,458 @@
|
||||
import createLogger from 'debug'
|
||||
const debug = createLogger('xo:main')
|
||||
|
||||
import Bluebird from 'bluebird'
|
||||
Bluebird.longStackTraces()
|
||||
|
||||
import appConf from 'app-conf'
|
||||
import assign from 'lodash.assign'
|
||||
import bind from 'lodash.bind'
|
||||
import blocked from 'blocked'
|
||||
import createConnectApp from 'connect'
|
||||
import eventToPromise from 'event-to-promise'
|
||||
import forEach from 'lodash.foreach'
|
||||
import has from 'lodash.has'
|
||||
import isArray from 'lodash.isarray'
|
||||
import isFunction from 'lodash.isfunction'
|
||||
import map from 'lodash.map'
|
||||
import pick from 'lodash.pick'
|
||||
import proxyRequest from 'proxy-http-request'
|
||||
import serveStatic from 'serve-static'
|
||||
import WebSocket from 'ws'
|
||||
import {
|
||||
AlreadyAuthenticated,
|
||||
InvalidCredential,
|
||||
InvalidParameters,
|
||||
NoSuchObject,
|
||||
NotImplemented
|
||||
} from './api-errors'
|
||||
import JsonRpcPeer from 'json-rpc-peer'
|
||||
import {readFile} from 'fs-promise'
|
||||
|
||||
import Api from './api'
|
||||
import WebServer from 'http-server-plus'
|
||||
import wsProxy from './ws-proxy'
|
||||
import Xo from './xo'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const info = (...args) => {
|
||||
console.info('[Info]', ...args)
|
||||
}
|
||||
|
||||
const warn = (...args) => {
|
||||
console.warn('[Warn]', ...args)
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const DEFAULTS = {
|
||||
http: {
|
||||
listen: [
|
||||
{ port: 80 }
|
||||
],
|
||||
mounts: {}
|
||||
}
|
||||
}
|
||||
|
||||
const DEPRECATED_ENTRIES = [
|
||||
'users',
|
||||
'servers'
|
||||
]
|
||||
|
||||
async function loadConfiguration () {
|
||||
const config = await appConf.load('xo-server', {
|
||||
defaults: DEFAULTS,
|
||||
ignoreUnknownFormats: true
|
||||
})
|
||||
|
||||
debug('Configuration loaded.')
|
||||
|
||||
// Print a message if deprecated entries are specified.
|
||||
forEach(DEPRECATED_ENTRIES, entry => {
|
||||
if (has(config, entry)) {
|
||||
warn(`${entry} configuration is deprecated.`)
|
||||
}
|
||||
})
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const debugPlugin = createLogger('xo:plugin')
|
||||
|
||||
const loadPlugin = Bluebird.method(function (pluginConf, pluginName) {
|
||||
debugPlugin('loading %s', pluginName)
|
||||
|
||||
const pluginPath = (function (name) {
|
||||
try {
|
||||
return require.resolve('xo-server-' + name)
|
||||
} catch (e) {
|
||||
return require.resolve(name)
|
||||
}
|
||||
})(pluginName)
|
||||
|
||||
let plugin = require(pluginPath)
|
||||
|
||||
if (isFunction(plugin)) {
|
||||
plugin = plugin(pluginConf)
|
||||
}
|
||||
|
||||
return plugin.load(this)
|
||||
})
|
||||
|
||||
const loadPlugins = function (plugins, xo) {
|
||||
return Bluebird.all(map(plugins, loadPlugin, xo)).then(() => {
|
||||
debugPlugin('all plugins loaded')
|
||||
})
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
async function makeWebServerListen (opts) {
|
||||
// Read certificate and key if necessary.
|
||||
const {certificate, key} = opts
|
||||
if (certificate && key) {
|
||||
[opts.certificate, opts.key] = await Bluebird.all([
|
||||
readFile(certificate),
|
||||
readFile(key)
|
||||
])
|
||||
}
|
||||
|
||||
try {
|
||||
const niceAddress = await this.listen(opts)
|
||||
debug(`Web server listening on ${niceAddress}`)
|
||||
} catch (error) {
|
||||
warn(`Web server could not listen on ${error.niceAddress}`)
|
||||
|
||||
const {code} = error
|
||||
if (code === 'EACCES') {
|
||||
warn(' Access denied.')
|
||||
warn(' Ports < 1024 are often reserved to privileges users.')
|
||||
} else if (code === 'EADDRINUSE') {
|
||||
warn(' Address already in use.')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const createWebServer = opts => {
|
||||
const webServer = new WebServer()
|
||||
|
||||
return Bluebird
|
||||
.bind(webServer).return(opts).map(makeWebServerListen)
|
||||
.return(webServer)
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const setUpProxies = (connect, opts) => {
|
||||
if (!opts) {
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: sort proxies by descending prefix length.
|
||||
|
||||
// HTTP request proxy.
|
||||
forEach(opts, (target, url) => {
|
||||
connect.use(url, (req, res) => {
|
||||
proxyRequest(target + req.url, req, res)
|
||||
})
|
||||
})
|
||||
|
||||
// WebSocket proxy.
|
||||
const webSocketServer = new WebSocket.Server({
|
||||
noServer: true
|
||||
})
|
||||
connect.on('upgrade', (req, socket, head) => {
|
||||
const {url} = req
|
||||
|
||||
for (let prefix in opts) {
|
||||
if (url.lastIndexOf(prefix, 0) !== -1) {
|
||||
const target = opts[prefix] + url.slice(prefix.length)
|
||||
webSocketServer.handleUpgrade(req, socket, head, socket => {
|
||||
wsProxy(socket, target)
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const setUpStaticFiles = (connect, opts) => {
|
||||
forEach(opts, (paths, url) => {
|
||||
if (!isArray(paths)) {
|
||||
paths = [paths]
|
||||
}
|
||||
|
||||
forEach(paths, path => {
|
||||
debug('Setting up %s → %s', url, path)
|
||||
|
||||
connect.use(url, serveStatic(path))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const errorClasses = {
|
||||
ALREADY_AUTHENTICATED: AlreadyAuthenticated,
|
||||
INVALID_CREDENTIAL: InvalidCredential,
|
||||
INVALID_PARAMS: InvalidParameters,
|
||||
NO_SUCH_OBJECT: NoSuchObject,
|
||||
NOT_IMPLEMENTED: NotImplemented
|
||||
}
|
||||
|
||||
const apiHelpers = {
|
||||
getUserPublicProperties (user) {
|
||||
// Handles both properties and wrapped models.
|
||||
const properties = user.properties || user
|
||||
|
||||
return pick(properties, 'id', 'email', 'groups', 'permission')
|
||||
},
|
||||
|
||||
getServerPublicProperties (server) {
|
||||
// Handles both properties and wrapped models.
|
||||
const properties = server.properties || server
|
||||
|
||||
server = pick(properties, 'id', 'host', 'username')
|
||||
|
||||
// Injects connection status.
|
||||
const xapi = this._xapis[server.id]
|
||||
server.status = xapi ? xapi.status : 'disconnected'
|
||||
|
||||
return server
|
||||
},
|
||||
|
||||
throw (errorId, data) {
|
||||
throw new (errorClasses[errorId])(data)
|
||||
}
|
||||
}
|
||||
|
||||
const setUpApi = (webServer, xo) => {
|
||||
const context = Object.create(xo)
|
||||
assign(xo, apiHelpers)
|
||||
|
||||
const api = new Api({
|
||||
context
|
||||
})
|
||||
|
||||
const webSocketServer = new WebSocket.Server({
|
||||
server: webServer,
|
||||
path: '/api/'
|
||||
})
|
||||
|
||||
webSocketServer.on('connection', socket => {
|
||||
debug('+ WebSocket connection')
|
||||
|
||||
// Create the abstract XO object for this connection.
|
||||
const connection = xo.createUserConnection()
|
||||
connection.once('close', () => {
|
||||
socket.close()
|
||||
})
|
||||
|
||||
// Create the JSON-RPC server for this connection.
|
||||
const jsonRpc = new JsonRpcPeer(message => {
|
||||
if (message.type === 'request') {
|
||||
return api.call(connection, message.method, message.params)
|
||||
}
|
||||
})
|
||||
connection.notify = bind(jsonRpc.notify, jsonRpc)
|
||||
|
||||
// Close the XO connection with this WebSocket.
|
||||
socket.once('close', () => {
|
||||
debug('- WebSocket connection')
|
||||
|
||||
connection.close()
|
||||
})
|
||||
|
||||
// Connect the WebSocket to the JSON-RPC server.
|
||||
socket.on('message', message => {
|
||||
jsonRpc.write(message)
|
||||
})
|
||||
|
||||
const onSend = error => {
|
||||
if (error) {
|
||||
warn('WebSocket send:', error.stack)
|
||||
}
|
||||
}
|
||||
jsonRpc.on('data', data => {
|
||||
// The socket may have been closed during the API method
|
||||
// execution.
|
||||
if (socket.readyState === WebSocket.OPEN) {
|
||||
socket.send(data, onSend)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const CONSOLE_PROXY_PATH_RE = /^\/api\/consoles\/(.*)$/
|
||||
|
||||
const setUpConsoleProxy = (webServer, xo) => {
|
||||
const webSocketServer = new WebSocket.Server({
|
||||
noServer: true
|
||||
})
|
||||
|
||||
webServer.on('upgrade', (req, socket, head) => {
|
||||
const matches = CONSOLE_PROXY_PATH_RE.exec(req.url)
|
||||
if (!matches) {
|
||||
return
|
||||
}
|
||||
|
||||
const [, id] = matches
|
||||
try {
|
||||
const url = xo.getXAPI(id, ['VM', 'VM-controller']).getVmConsoleUrl(id)
|
||||
|
||||
// FIXME: lost connection due to VM restart is not detected.
|
||||
webSocketServer.handleUpgrade(req, socket, head, connection => {
|
||||
wsProxy(connection, url, {
|
||||
rejectUnauthorized: false
|
||||
})
|
||||
})
|
||||
} catch (_) {}
|
||||
})
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const registerPasswordAuthenticationProvider = (xo) => {
|
||||
async function passwordAuthenticationProvider ({
|
||||
email,
|
||||
password,
|
||||
}) {
|
||||
/* eslint no-throw-literal: 0 */
|
||||
|
||||
if (email === undefined || password === undefined) {
|
||||
throw null
|
||||
}
|
||||
|
||||
// TODO: this is deprecated and should be removed.
|
||||
const user = await xo._users.first({email})
|
||||
if (!user || !(await user.checkPassword(password))) {
|
||||
throw null
|
||||
}
|
||||
return user
|
||||
}
|
||||
|
||||
xo.registerAuthenticationProvider(passwordAuthenticationProvider)
|
||||
}
|
||||
|
||||
const registerTokenAuthenticationProvider = (xo) => {
|
||||
async function tokenAuthenticationProvider ({
|
||||
token: tokenId,
|
||||
}) {
|
||||
/* eslint no-throw-literal: 0 */
|
||||
|
||||
if (!tokenId) {
|
||||
throw null
|
||||
}
|
||||
|
||||
try {
|
||||
return (await xo.getAuthenticationToken(tokenId)).user_id
|
||||
} catch (e) {
|
||||
// It is not an error if the token does not exists.
|
||||
throw null
|
||||
}
|
||||
}
|
||||
|
||||
xo.registerAuthenticationProvider(tokenAuthenticationProvider)
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const help = (function ({name, version}) {
|
||||
return () => `${name} v${version}`
|
||||
})(require('../package.json'))
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export default async function main (args) {
|
||||
if (args.indexOf('--help') !== -1 || args.indexOf('-h') !== -1) {
|
||||
return help()
|
||||
}
|
||||
|
||||
{
|
||||
const debug = createLogger('xo:perf')
|
||||
blocked(ms => {
|
||||
debug('blocked for %sms', ms | 0)
|
||||
})
|
||||
}
|
||||
|
||||
const config = await loadConfiguration()
|
||||
|
||||
const webServer = await createWebServer(config.http.listen)
|
||||
|
||||
// Now the web server is listening, drop privileges.
|
||||
try {
|
||||
const {user, group} = config
|
||||
if (group) {
|
||||
process.setgid(group)
|
||||
debug('Group changed to', group)
|
||||
}
|
||||
if (user) {
|
||||
process.setuid(user)
|
||||
debug('User changed to', user)
|
||||
}
|
||||
} catch (error) {
|
||||
warn('Failed to change user/group:', error)
|
||||
}
|
||||
|
||||
// Create the main object which will connects to Xen servers and
|
||||
// manages all the models.
|
||||
const xo = new Xo()
|
||||
await xo.start({
|
||||
redis: {
|
||||
uri: config.redis && config.redis.uri
|
||||
}
|
||||
})
|
||||
|
||||
// Loads default authentication providers.
|
||||
registerPasswordAuthenticationProvider(xo)
|
||||
registerTokenAuthenticationProvider(xo)
|
||||
|
||||
if (config.plugins) {
|
||||
await loadPlugins(config.plugins, xo)
|
||||
}
|
||||
|
||||
// Connect is used to manage non WebSocket connections.
|
||||
const connect = createConnectApp()
|
||||
webServer.on('request', connect)
|
||||
webServer.on('upgrade', (req, socket, head) => {
|
||||
connect.emit('upgrade', req, socket, head)
|
||||
})
|
||||
|
||||
// Must be set up before the API.
|
||||
setUpConsoleProxy(webServer, xo)
|
||||
|
||||
// Must be set up before the API.
|
||||
connect.use(bind(xo._handleHttpRequest, xo))
|
||||
|
||||
// TODO: remove when no longer necessary.
|
||||
connect.use(bind(xo._handleProxyRequest, xo))
|
||||
|
||||
// Must be set up before the static files.
|
||||
setUpApi(webServer, xo)
|
||||
|
||||
setUpProxies(connect, config.http.proxies)
|
||||
|
||||
setUpStaticFiles(connect, config.http.mounts)
|
||||
|
||||
if (!(await xo._users.exists())) {
|
||||
const email = 'admin@admin.net'
|
||||
const password = 'admin'
|
||||
|
||||
await xo.createUser({email, password, permission: 'admin'})
|
||||
info('Default user created:', email, ' with password', password)
|
||||
}
|
||||
|
||||
// Handle gracefully shutdown.
|
||||
const closeWebServer = () => { webServer.close() }
|
||||
process.on('SIGINT', closeWebServer)
|
||||
process.on('SIGTERM', closeWebServer)
|
||||
|
||||
return eventToPromise(webServer, 'close')
|
||||
}
|
||||
230
src/main.coffee
230
src/main.coffee
@@ -1,230 +0,0 @@
|
||||
# File system handling.
|
||||
$fs = require 'fs'
|
||||
|
||||
#---------------------------------------------------------------------
|
||||
|
||||
# Low level tools.
|
||||
$_ = require 'underscore'
|
||||
|
||||
# HTTP(s) middleware framework.
|
||||
$connect = require 'connect'
|
||||
$serveStatic = require 'serve-static'
|
||||
|
||||
$eventToPromise = require 'event-to-promise'
|
||||
|
||||
# Configuration handling.
|
||||
$nconf = require 'nconf'
|
||||
|
||||
$Promise = require 'bluebird'
|
||||
$Promise.longStackTraces()
|
||||
|
||||
# WebSocket server.
|
||||
{Server: $WSServer} = require 'ws'
|
||||
|
||||
# YAML formatting and parsing.
|
||||
$YAML = require 'js-yaml'
|
||||
|
||||
#---------------------------------------------------------------------
|
||||
|
||||
$API = require './api'
|
||||
$Connection = require './connection'
|
||||
$XO = require './xo'
|
||||
|
||||
# Helpers for dealing with fibers.
|
||||
{$fiberize, $promisify, $waitEvent, $wait} = require './fibers-utils'
|
||||
|
||||
# HTTP/HTTPS server which can listen on multiple ports.
|
||||
$WebServer = require 'http-server-plus'
|
||||
|
||||
#=====================================================================
|
||||
|
||||
$readFile = $Promise.promisify $fs.readFile
|
||||
|
||||
$handleJsonRpcCall = (api, session, encodedRequest) ->
|
||||
request = {
|
||||
id: null
|
||||
}
|
||||
|
||||
formatError = (error) -> JSON.stringify {
|
||||
jsonrpc: '2.0'
|
||||
error: error
|
||||
id: request.id
|
||||
}
|
||||
|
||||
# Parses the JSON.
|
||||
try
|
||||
request = JSON.parse encodedRequest.toString()
|
||||
catch error
|
||||
return formatError (
|
||||
if error instanceof SyntaxError
|
||||
$API.err.INVALID_JSON
|
||||
else
|
||||
$API.err.SERVER_ERROR
|
||||
)
|
||||
|
||||
# Checks it is a compliant JSON-RPC 2.0 request.
|
||||
if (
|
||||
not request.method? or
|
||||
not request.params? or
|
||||
not request.id? or
|
||||
request.jsonrpc isnt '2.0'
|
||||
)
|
||||
return formatError $API.err.INVALID_REQUEST
|
||||
|
||||
# Executes the requested method on the API.
|
||||
try
|
||||
JSON.stringify {
|
||||
jsonrpc: '2.0'
|
||||
result: $wait api.exec session, request
|
||||
id: request.id
|
||||
}
|
||||
catch error
|
||||
# If it is not a valid API error, hides it with a generic server error.
|
||||
unless (error not instanceof Error) and error.code? and error.message?
|
||||
console.error error.stack ? error
|
||||
error = $API.err.SERVER_ERROR
|
||||
|
||||
formatError error
|
||||
|
||||
#=====================================================================
|
||||
|
||||
# Main.
|
||||
module.exports = $promisify (args) ->
|
||||
|
||||
# Relative paths in the configuration are relative to this
|
||||
# directory's parent.
|
||||
process.chdir "#{__dirname}/.."
|
||||
|
||||
# Loads the environment.
|
||||
$nconf.env()
|
||||
|
||||
# Parses process' arguments.
|
||||
$nconf.argv()
|
||||
|
||||
# Loads the configuration files.
|
||||
format =
|
||||
stringify: $YAML.safeDump
|
||||
parse: $YAML.safeLoad
|
||||
$nconf.use 'file', {
|
||||
file: "#{__dirname}/../config/local.yaml"
|
||||
format
|
||||
}
|
||||
|
||||
# Defines defaults configuration.
|
||||
$nconf.defaults {
|
||||
http: {
|
||||
listen: [
|
||||
port: 80
|
||||
]
|
||||
mounts: []
|
||||
}
|
||||
redis: {
|
||||
# Default values are handled by `redis`.
|
||||
}
|
||||
}
|
||||
|
||||
# Prints a message if deprecated entries are specified.
|
||||
for entry in ['users', 'servers']
|
||||
if $nconf.get entry
|
||||
console.warn "[Warn] `#{entry}` configuration is deprecated."
|
||||
|
||||
# Creates the web server according to the configuration.
|
||||
webServer = new $WebServer()
|
||||
$wait $Promise.map ($nconf.get 'http:listen'), (options) ->
|
||||
# Reads certificate and key if necessary.
|
||||
if options.certificate? and options.key?
|
||||
options.certificate = $wait $readFile options.certificate
|
||||
options.key = $wait $readFile options.key
|
||||
|
||||
# Starts listening
|
||||
webServer.listen options
|
||||
.then ->
|
||||
console.log "WebServer listening on #{@niceAddress()}"
|
||||
.catch (error) ->
|
||||
console.warn "[WARN] WebServer could not listen on #{@niceAddress()}"
|
||||
switch error.code
|
||||
when 'EACCES'
|
||||
console.warn ' Access denied.'
|
||||
console.warn ' Ports < 1024 are often reserved to privileges users.'
|
||||
when 'EADDRINUSE'
|
||||
console.warn ' Address already in use.'
|
||||
|
||||
# Now the web server is listening, drop privileges.
|
||||
try
|
||||
if (group = $nconf.get 'group')?
|
||||
process.setgid group
|
||||
if (user = $nconf.get 'user')?
|
||||
process.setuid user
|
||||
catch error
|
||||
console.warn "[WARN] Failed to change the user or group: #{error.message}"
|
||||
|
||||
# Handles error as gracefully as possible.
|
||||
webServer.on 'error', (error) ->
|
||||
console.error '[ERR] Web server', error
|
||||
webServer.close()
|
||||
|
||||
# Creates the main object which will connects to Xen servers and
|
||||
# manages all the models.
|
||||
xo = new $XO()
|
||||
|
||||
# Starts it.
|
||||
xo.start {
|
||||
redis: {
|
||||
uri: $nconf.get 'redis:uri'
|
||||
}
|
||||
}
|
||||
|
||||
# Static file serving (e.g. for XO-Web).
|
||||
connect = $connect()
|
||||
for urlPath, filePaths of $nconf.get 'http:mounts'
|
||||
filePaths = [filePaths] unless $_.isArray filePaths
|
||||
for filePath in filePaths
|
||||
connect.use urlPath, $serveStatic filePath
|
||||
webServer.on 'request', connect
|
||||
|
||||
# Creates the API.
|
||||
api = new $API xo
|
||||
|
||||
conId = 0
|
||||
unregisterConnection = ->
|
||||
delete xo.connections[@id]
|
||||
|
||||
# JSON-RPC over WebSocket.
|
||||
wsServer = new $WSServer {
|
||||
server: webServer
|
||||
path: '/api/'
|
||||
}
|
||||
wsServer.on 'connection', (socket) ->
|
||||
connection = new $Connection {
|
||||
close: socket.close.bind socket
|
||||
send: socket.send.bind socket
|
||||
}
|
||||
connection.id = conId++
|
||||
xo.connections[connection.id] = connection
|
||||
connection.on 'close', unregisterConnection
|
||||
|
||||
socket.on 'close', connection.close.bind connection
|
||||
|
||||
# Handles each request in a separate fiber.
|
||||
socket.on 'message', $fiberize (request) ->
|
||||
response = $handleJsonRpcCall api, connection, request
|
||||
|
||||
# The socket may have closed between the request and the
|
||||
# response.
|
||||
socket.send response if socket.readyState is socket.OPEN
|
||||
|
||||
socket.on 'error', $fiberize (error) ->
|
||||
console.error '[WARN] WebSocket connection', error
|
||||
socket.close()
|
||||
wsServer.on 'error', $fiberize (error) ->
|
||||
console.error '[WARN] WebSocket server', error
|
||||
wsServer.close()
|
||||
|
||||
# Creates a default user if there is none.
|
||||
unless $wait xo.users.exists()
|
||||
email = 'admin@admin.net'
|
||||
password = 'admin' # TODO: Should be generated.
|
||||
xo.users.create email, password, 'admin'
|
||||
console.log "[INFO] Default user: “#{email}” with password “#{password}”"
|
||||
|
||||
return $eventToPromise webServer, 'close'
|
||||
171
src/model.js
171
src/model.js
@@ -1,113 +1,70 @@
|
||||
'use strict';
|
||||
import assign from 'lodash.assign'
|
||||
import forEach from 'lodash.foreach'
|
||||
import isEmpty from 'lodash.isempty'
|
||||
import {EventEmitter} from 'events'
|
||||
|
||||
var _ = require('underscore');
|
||||
// ===================================================================
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
export default class Model extends EventEmitter {
|
||||
constructor (properties) {
|
||||
super()
|
||||
|
||||
function Model(properties)
|
||||
{
|
||||
// Parent constructor.
|
||||
Model.super_.call(this);
|
||||
this.properties = assign({}, this.default)
|
||||
|
||||
this.properties = _.extend({}, this['default']);
|
||||
if (properties) {
|
||||
this.set(properties)
|
||||
}
|
||||
}
|
||||
|
||||
if (properties)
|
||||
{
|
||||
this.set(properties);
|
||||
}
|
||||
// Initialize the model after construction.
|
||||
initialize () {}
|
||||
|
||||
// Validate the defined properties.
|
||||
//
|
||||
// Returns the error if any.
|
||||
validate (properties) {}
|
||||
|
||||
// Get a property.
|
||||
get (name, def) {
|
||||
const value = this.properties[name]
|
||||
return value !== undefined ? value : def
|
||||
}
|
||||
|
||||
// Check whether a property exists.
|
||||
has (name) {
|
||||
return (this.properties[name] !== undefined)
|
||||
}
|
||||
|
||||
// Set properties.
|
||||
set (properties, value) {
|
||||
// This method can also be used with two arguments to set a single
|
||||
// property.
|
||||
if (value !== undefined) {
|
||||
properties = { [properties]: value }
|
||||
}
|
||||
|
||||
const previous = {}
|
||||
|
||||
forEach(properties, (value, name) => {
|
||||
const prev = this.properties[name]
|
||||
|
||||
if (value !== prev) {
|
||||
previous[name] = prev
|
||||
|
||||
if (value === undefined) {
|
||||
delete this.properties[name]
|
||||
} else {
|
||||
this.properties[name] = value
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (!isEmpty(previous)) {
|
||||
this.emit('change', previous)
|
||||
|
||||
forEach(previous, (value, name) => {
|
||||
this.emit('change:' + name, value)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
require('util').inherits(Model, require('events').EventEmitter);
|
||||
|
||||
/**
|
||||
* Initializes the model after construction.
|
||||
*/
|
||||
Model.prototype.initialize = function () {};
|
||||
|
||||
/**
|
||||
* Validates the defined properties.
|
||||
*
|
||||
* @returns {undefined|mixed} Returns something else than undefined if
|
||||
* there was an error.
|
||||
*/
|
||||
Model.prototype.validate = function (/*properties*/) {};
|
||||
|
||||
/**
|
||||
* Gets property.
|
||||
*/
|
||||
Model.prototype.get = function (property, def) {
|
||||
var prop = this.properties[property];
|
||||
if (undefined !== prop)
|
||||
{
|
||||
return prop;
|
||||
}
|
||||
|
||||
return def;
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if a property exists.
|
||||
*/
|
||||
Model.prototype.has = function (property) {
|
||||
return (undefined !== this.properties[property]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets properties.
|
||||
*/
|
||||
Model.prototype.set = function (properties, value) {
|
||||
if (undefined !== value)
|
||||
{
|
||||
var property = properties;
|
||||
properties = {};
|
||||
properties[property] = value;
|
||||
}
|
||||
|
||||
var previous = {};
|
||||
|
||||
var model = this;
|
||||
_.each(properties, function (value, key) {
|
||||
if (undefined === value)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var prev = model.get(key);
|
||||
|
||||
// New value.
|
||||
if (value !== prev)
|
||||
{
|
||||
previous[key] = prev;
|
||||
model.properties[key] = value;
|
||||
}
|
||||
});
|
||||
|
||||
if (!_.isEmpty(previous))
|
||||
{
|
||||
this.emit('change', previous);
|
||||
|
||||
_.each(previous, function (previous, property) {
|
||||
this.emit('change:'+ property, previous);
|
||||
}, this);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Unsets properties.
|
||||
*/
|
||||
Model.prototype.unset = function (properties) {
|
||||
// TODO: Events.
|
||||
this.properties = _.omit(this.properties, properties);
|
||||
};
|
||||
|
||||
/**
|
||||
* Default properties.
|
||||
*
|
||||
* @type {Object}
|
||||
*/
|
||||
Model.prototype['default'] = {};
|
||||
|
||||
Model.extend = require('extendable');
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
|
||||
module.exports = Model;
|
||||
|
||||
78
src/models/acl.js
Normal file
78
src/models/acl.js
Normal file
@@ -0,0 +1,78 @@
|
||||
import forEach from 'lodash.foreach'
|
||||
import map from 'lodash.map'
|
||||
|
||||
import Collection from '../collection/redis'
|
||||
import Model from '../model'
|
||||
import {multiKeyHash} from '../utils'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
// Up until now, there were no actions, therefore the default
|
||||
// action is used to update existing entries.
|
||||
const DEFAULT_ACTION = 'admin'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export default class Acl extends Model {}
|
||||
|
||||
Acl.create = (subject, object, action) => {
|
||||
return Acl.hash(subject, object, action).then(hash => new Acl({
|
||||
id: hash,
|
||||
subject,
|
||||
object,
|
||||
action
|
||||
}))
|
||||
}
|
||||
|
||||
Acl.hash = (subject, object, action) => multiKeyHash(subject, object, action)
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export class Acls extends Collection {
|
||||
get Model () {
|
||||
return Acl
|
||||
}
|
||||
|
||||
create (subject, object, action) {
|
||||
return Acl.create(subject, object, action).then(acl => this.add(acl))
|
||||
}
|
||||
|
||||
delete (subject, object, action) {
|
||||
return Acl.hash(subject, object, action).then(hash => this.remove(hash))
|
||||
}
|
||||
|
||||
aclExists (subject, object, action) {
|
||||
return Acl.hash(subject, object, action).then(hash => this.exists(hash))
|
||||
}
|
||||
|
||||
async get (properties) {
|
||||
const acls = await super.get(properties)
|
||||
|
||||
// Finds all records that are missing a action and need to be updated.
|
||||
const toUpdate = []
|
||||
forEach(acls, acl => {
|
||||
if (!acl.action) {
|
||||
acl.action = DEFAULT_ACTION
|
||||
toUpdate.push(acl)
|
||||
}
|
||||
})
|
||||
if (toUpdate.length) {
|
||||
// Removes all existing entries.
|
||||
await this.remove(map(toUpdate, 'id'))
|
||||
|
||||
// Compute the new ids (new hashes).
|
||||
const {hash} = Acl
|
||||
await Promise.all(map(
|
||||
toUpdate,
|
||||
(acl) => hash(acl.subject, acl.object, acl.action).then(id => {
|
||||
acl.id = id
|
||||
})
|
||||
))
|
||||
|
||||
// Inserts the new (updated) entries.
|
||||
await this.add(toUpdate)
|
||||
}
|
||||
|
||||
return acls
|
||||
}
|
||||
}
|
||||
51
src/models/group.js
Normal file
51
src/models/group.js
Normal file
@@ -0,0 +1,51 @@
|
||||
import forEach from 'lodash.foreach'
|
||||
|
||||
import Collection from '../collection/redis'
|
||||
import Model from '../model'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export default class Group extends Model {}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export class Groups extends Collection {
|
||||
get Model () {
|
||||
return Group
|
||||
}
|
||||
|
||||
get idPrefix () {
|
||||
return 'group:'
|
||||
}
|
||||
|
||||
create (name) {
|
||||
return this.add(new Group({
|
||||
name,
|
||||
users: '[]'
|
||||
}))
|
||||
}
|
||||
|
||||
async save (group) {
|
||||
// Serializes.
|
||||
group.users = JSON.stringify(group.users)
|
||||
|
||||
return await this.update(group)
|
||||
}
|
||||
|
||||
async get (properties) {
|
||||
const groups = await super.get(properties)
|
||||
|
||||
// Deserializes.
|
||||
forEach(groups, group => {
|
||||
const {users} = group
|
||||
try {
|
||||
group.users = JSON.parse(users)
|
||||
} catch (error) {
|
||||
console.warn('cannot parse group.users:', users)
|
||||
group.users = []
|
||||
}
|
||||
})
|
||||
|
||||
return groups
|
||||
}
|
||||
}
|
||||
14
src/models/server.js
Normal file
14
src/models/server.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import Collection from '../collection/redis'
|
||||
import Model from '../model'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export default class Server extends Model {}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export class Servers extends Collection {
|
||||
get Model () {
|
||||
return Server
|
||||
}
|
||||
}
|
||||
26
src/models/token.js
Normal file
26
src/models/token.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import Collection from '../collection/redis'
|
||||
import Model from '../model'
|
||||
import {generateToken} from '../utils'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export default class Token extends Model {}
|
||||
|
||||
Token.generate = (userId) => {
|
||||
return generateToken().then(token => new Token({
|
||||
id: token,
|
||||
user_id: userId
|
||||
}))
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export class Tokens extends Collection {
|
||||
get Model () {
|
||||
return Token
|
||||
}
|
||||
|
||||
generate (userId) {
|
||||
return Token.generate(userId).then(token => this.add(token))
|
||||
}
|
||||
}
|
||||
93
src/models/user.js
Normal file
93
src/models/user.js
Normal file
@@ -0,0 +1,93 @@
|
||||
import forEach from 'lodash.foreach'
|
||||
import {hash, needsRehash, verify} from 'hashy'
|
||||
|
||||
import Collection from '../collection/redis'
|
||||
import Model from '../model'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const PERMISSIONS = {
|
||||
none: 0,
|
||||
read: 1,
|
||||
write: 2,
|
||||
admin: 3
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export default class User extends Model {
|
||||
async checkPassword (password) {
|
||||
const hash = this.get('pw_hash')
|
||||
|
||||
if (!(hash && await verify(password, hash))) {
|
||||
return false
|
||||
}
|
||||
|
||||
// There might be no hash if the user authenticate with another
|
||||
// method (e.g. LDAP).
|
||||
if (needsRehash(hash)) {
|
||||
await this.setPassword(password)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
hasPermission (permission) {
|
||||
return PERMISSIONS[this.get('permission')] >= PERMISSIONS[permission]
|
||||
}
|
||||
|
||||
setPassword (password) {
|
||||
return hash(password).then(hash => {
|
||||
return this.set('pw_hash', hash)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
User.prototype.default = {
|
||||
permission: 'none'
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export class Users extends Collection {
|
||||
get Model () {
|
||||
return User
|
||||
}
|
||||
|
||||
async create (email, password, permission = 'none') {
|
||||
const user = new User({
|
||||
email,
|
||||
permission
|
||||
})
|
||||
|
||||
if (password != null) {
|
||||
await user.setPassword(password)
|
||||
}
|
||||
|
||||
return this.add(user)
|
||||
}
|
||||
|
||||
async save (user) {
|
||||
// Serializes.
|
||||
user.groups = JSON.stringify(user.groups)
|
||||
|
||||
return await this.update(user)
|
||||
}
|
||||
|
||||
async get (properties) {
|
||||
const users = await super.get(properties)
|
||||
|
||||
// Deserializes
|
||||
forEach(users, user => {
|
||||
const {groups} = user
|
||||
try {
|
||||
user.groups = groups ? JSON.parse(groups) : []
|
||||
} catch (_) {
|
||||
console.warn('cannot parse user.groups:', groups)
|
||||
user.groups = []
|
||||
}
|
||||
})
|
||||
|
||||
return users
|
||||
}
|
||||
}
|
||||
770
src/spec.coffee
770
src/spec.coffee
@@ -1,770 +0,0 @@
|
||||
$_ = require 'underscore'
|
||||
|
||||
#---------------------------------------------------------------------
|
||||
|
||||
$xml2js = require 'xml2js'
|
||||
|
||||
#---------------------------------------------------------------------
|
||||
|
||||
$helpers = require './helpers'
|
||||
|
||||
#=====================================================================
|
||||
|
||||
$isVMRunning = ->
|
||||
switch @val.power_state
|
||||
when 'Paused', 'Running'
|
||||
true
|
||||
else
|
||||
false
|
||||
|
||||
$isHostRunning = ->
|
||||
@val.power_state is 'Running'
|
||||
|
||||
$isTaskLive = ->
|
||||
@val.status is 'pending' or @val.status is 'cancelling'
|
||||
|
||||
# $xml2js.parseString() uses callback for synchronous code.
|
||||
$parseXML = (XML) ->
|
||||
options = {
|
||||
mergeAttrs: true
|
||||
explicitArray: false
|
||||
}
|
||||
result = null
|
||||
$xml2js.parseString XML, options, (error, result_) ->
|
||||
throw error if error?
|
||||
result = result_
|
||||
result
|
||||
|
||||
$retrieveTags = -> [] # TODO
|
||||
|
||||
$toTimestamp = (date) ->
|
||||
# Weird behavior from the XAPI.
|
||||
return null if date is '1969-12-31T23:00:00.000Z'
|
||||
|
||||
if date?
|
||||
Math.round (Date.parse date) / 1000
|
||||
else
|
||||
null
|
||||
|
||||
#=====================================================================
|
||||
|
||||
module.exports = ->
|
||||
|
||||
# Binds the helpers to the collection.
|
||||
{
|
||||
$set
|
||||
$sum
|
||||
$val
|
||||
} = do =>
|
||||
helpers = {}
|
||||
helpers[name] = fn.bind this for name, fn of $helpers
|
||||
helpers
|
||||
|
||||
collection = this
|
||||
# do (emit = collection.emit) ->
|
||||
# collection.emit = (event, items) ->
|
||||
# console.log event
|
||||
# emit.call collection, event, items
|
||||
|
||||
$link = (keyFn, valFn = (-> @val), once = false) ->
|
||||
valuePerItem = Object.create null
|
||||
updating = false
|
||||
|
||||
->
|
||||
{key} = this
|
||||
|
||||
# Returns the value if already defined.
|
||||
return valuePerItem[key] if key of valuePerItem
|
||||
|
||||
# Gets the key of the remote object.
|
||||
remoteKey = keyFn.call this
|
||||
|
||||
# Special case for `OpaqueRef:NULL`.
|
||||
if remoteKey is 'OpaqueRef:NULL'
|
||||
return valuePerItem[key] = null
|
||||
|
||||
# Tries to find the remote object in the collection.
|
||||
try
|
||||
return valuePerItem[key] = valFn.call (collection.getRaw remoteKey)
|
||||
|
||||
# If not found, listens for its apparition.
|
||||
eventName = "key=#{remoteKey}"
|
||||
listener = (event, item) ->
|
||||
# If the events are due to an update of this link or if the item is
|
||||
# exiting, just returns.
|
||||
return if updating or event isnt 'enter'
|
||||
|
||||
# Register its value.
|
||||
valuePerItem[key] = valFn.call item
|
||||
|
||||
if once
|
||||
# Removes the now unnecessary listener.
|
||||
collection.removeListener eventName, listener
|
||||
|
||||
# Force the object to update.
|
||||
try
|
||||
updating = true
|
||||
collection.touch key
|
||||
finally
|
||||
updating = false
|
||||
collection.on eventName, listener
|
||||
|
||||
# Returns `null` for now.
|
||||
valuePerItem[key] = null
|
||||
|
||||
$map = (valFn) ->
|
||||
map = Object.create null
|
||||
subscribers = Object.create null
|
||||
updating = false
|
||||
|
||||
# First, initializes the map with existing items.
|
||||
$_.each collection.getRaw(), (item) ->
|
||||
val = valFn.call item
|
||||
map[val[0]] = val[1] if val
|
||||
|
||||
# Listens to any new item.
|
||||
collection.on 'any', (event, items) ->
|
||||
# If the events are due to an update of this map or if items are exiting,
|
||||
# just returns.
|
||||
return if updating or event isnt 'enter'
|
||||
|
||||
# No need to trigger an update if nothing has changed.
|
||||
changed = false
|
||||
|
||||
$_.each items, (item) ->
|
||||
val = valFn.call item
|
||||
if val and map[val[0]] isnt val[1]
|
||||
changed = true
|
||||
map[val[0]] = val[1]
|
||||
|
||||
if changed
|
||||
try
|
||||
updating = true
|
||||
collection.touch subscribers
|
||||
finally
|
||||
updating = false
|
||||
|
||||
generator = ->
|
||||
subscribers[@key] = true
|
||||
map
|
||||
generator.unsubscribe = ->
|
||||
delete subscribers[@key]
|
||||
|
||||
generator
|
||||
|
||||
# Shared watchers.
|
||||
UUIDsToKeys = $map ->
|
||||
{UUID} = @val
|
||||
return false unless UUID
|
||||
[UUID, "#{@key}"]
|
||||
messages = $set {
|
||||
rule: 'message'
|
||||
bind: -> @val.$object or @val.poolRef
|
||||
}
|
||||
|
||||
# Classes in XAPI are not always delivered with the same case,
|
||||
# therefore a map is needed to make sure they always map to the same
|
||||
# rule.
|
||||
rulesMap = {}
|
||||
|
||||
# Defines which rule should be used for this item.
|
||||
#
|
||||
# Note: If the rule does not exists, a temporary item is created. FIXME
|
||||
@dispatch = ->
|
||||
{$type: type} = @genval
|
||||
|
||||
# Normalizes the type.
|
||||
type = rulesMap[type.toLowerCase()] ? type
|
||||
|
||||
# Subtypes handling for VMs.
|
||||
if type is 'VM'
|
||||
return 'VM-controller' if @genval.is_control_domain
|
||||
return 'VM-snapshot' if @genval.is_a_snapshot
|
||||
return 'VM-template' if @genval.is_a_template
|
||||
|
||||
type
|
||||
|
||||
# Missing rules should be created.
|
||||
@missingRule = @rule
|
||||
|
||||
# Rule conflicts are possible (e.g. VM-template to VM).
|
||||
@ruleConflict = ( -> )
|
||||
|
||||
# Used to apply common definition to rules.
|
||||
@hook afterRule: ->
|
||||
# Registers this rule in the map.
|
||||
rulesMap[@name.toLowerCase()] = @name
|
||||
|
||||
# TODO: explain.
|
||||
return unless @val?
|
||||
|
||||
unless $_.isObject @val
|
||||
throw new Error 'the value should be an object'
|
||||
|
||||
# Injects various common definitions.
|
||||
@val.type = @name
|
||||
if @singleton
|
||||
@val.ref = -> @key
|
||||
else
|
||||
# This definition are for non singleton items only.
|
||||
@key = -> @genval.$ref
|
||||
@val.UUID = -> @genval.uuid
|
||||
@val.ref = -> @genval.$ref
|
||||
@val.poolRef = -> @genval.$poolRef
|
||||
|
||||
# Main objects all can have associated messages and tags.
|
||||
if @name in ['host', 'pool', 'SR', 'VM', 'VM-controller']
|
||||
@val.messages = messages
|
||||
|
||||
@val.tags = $retrieveTags
|
||||
|
||||
# Helper to create multiple rules with the same definition.
|
||||
rules = (rules, definition) =>
|
||||
@rule rule, definition for rule in rules
|
||||
|
||||
# An item is equivalent to a rule but one and only one instance of
|
||||
# this rule is created without any generator.
|
||||
@item xo: ->
|
||||
@val = {
|
||||
|
||||
# TODO: Maybe there should be high-level hosts: those who do not
|
||||
# belong to a pool.
|
||||
|
||||
pools: $set {
|
||||
rule: 'pool'
|
||||
}
|
||||
|
||||
$CPUs: $sum {
|
||||
rule: 'host'
|
||||
val: -> +(@val.CPUs.cpu_count)
|
||||
}
|
||||
|
||||
$running_VMs: $set {
|
||||
rule: 'VM'
|
||||
if: $isVMRunning
|
||||
}
|
||||
|
||||
$vCPUs: $sum {
|
||||
rule: 'VM'
|
||||
val: -> @val.CPUs.number
|
||||
if: $isVMRunning
|
||||
}
|
||||
|
||||
# Do not work due to problem in host rule.
|
||||
$memory: {
|
||||
usage: $sum {
|
||||
rule: 'host'
|
||||
if: $isHostRunning
|
||||
val: -> @val.memory.usage
|
||||
}
|
||||
size: $sum {
|
||||
rule: 'host'
|
||||
if: $isHostRunning
|
||||
val: -> @val.memory.size
|
||||
}
|
||||
}
|
||||
|
||||
# Maps the UUIDs to keys (i.e. opaque references).
|
||||
$UUIDsToKeys: UUIDsToKeys
|
||||
}
|
||||
|
||||
@rule pool: ->
|
||||
@val = {
|
||||
name_label: -> @genval.name_label
|
||||
|
||||
name_description: -> @genval.name_description
|
||||
|
||||
SRs: $set {
|
||||
rule: 'SR'
|
||||
bind: -> @val.$container
|
||||
}
|
||||
|
||||
default_SR: ->
|
||||
SR = @genval.default_SR
|
||||
if SR is 'OpaqueRef:NULL'
|
||||
null
|
||||
else
|
||||
SR
|
||||
|
||||
HA_enabled: -> @genval.ha_enabled
|
||||
|
||||
hosts: $set {
|
||||
rule: 'host'
|
||||
bind: -> @genval.$poolRef
|
||||
}
|
||||
|
||||
master: -> @genval.master
|
||||
|
||||
networks: $set {
|
||||
rule: 'network'
|
||||
bind: -> @genval.$poolRef
|
||||
}
|
||||
|
||||
templates: $set {
|
||||
rule: 'VM-template'
|
||||
bind: -> @val.$container
|
||||
}
|
||||
|
||||
VMs: $set {
|
||||
rule: 'VM'
|
||||
bind: -> @val.$container
|
||||
}
|
||||
|
||||
$running_hosts: $set {
|
||||
rule: 'host'
|
||||
bind: -> @genval.$poolRef
|
||||
if: $isHostRunning
|
||||
}
|
||||
|
||||
$running_VMs: $set {
|
||||
rule: 'VM'
|
||||
bind: -> @genval.$poolRef
|
||||
if: $isHostRunning
|
||||
}
|
||||
|
||||
$VMs: $set {
|
||||
rule: 'VM'
|
||||
bind: -> @genval.$poolRef
|
||||
}
|
||||
|
||||
# FIXME: Should be remove ASAP!
|
||||
$sessionId : -> @genval.$sessionId ? @val.$sessionId
|
||||
}
|
||||
|
||||
@rule host: ->
|
||||
# Private properties used to helps construction.
|
||||
@data = {
|
||||
metrics: $link -> @genval.metrics
|
||||
}
|
||||
|
||||
@val = {
|
||||
name_label: -> @genval.name_label
|
||||
|
||||
name_description: -> @genval.name_description
|
||||
|
||||
address: -> @genval.address
|
||||
|
||||
controller: $val {
|
||||
rule: 'VM-controller'
|
||||
bind: -> @val.$container
|
||||
val: -> @key
|
||||
}
|
||||
|
||||
CPUs: -> @genval.cpu_info
|
||||
|
||||
enabled: -> @genval.enabled
|
||||
|
||||
current_operations: -> @genval.current_operations
|
||||
|
||||
hostname: -> @genval.hostname
|
||||
|
||||
iSCSI_name: -> @genval.other_config?.iscsi_iqn ? null
|
||||
|
||||
memory: ->
|
||||
{metrics} = @data
|
||||
if metrics
|
||||
{
|
||||
usage: +metrics.memory_total - metrics.memory_free
|
||||
size: +metrics.memory_total
|
||||
}
|
||||
else
|
||||
{
|
||||
usage: 0
|
||||
size: 0
|
||||
}
|
||||
|
||||
power_state: ->
|
||||
if (
|
||||
@genval.enabled or
|
||||
not $_.contains @genval.current_operations, 'shutdown'
|
||||
)
|
||||
'Running'
|
||||
else
|
||||
'Halted'
|
||||
|
||||
# Local SRs are handled directly in `SR.$container`.
|
||||
SRs: $set {
|
||||
rule: 'SR'
|
||||
bind: -> @val.$container
|
||||
}
|
||||
|
||||
# What are local templates?
|
||||
templates: $set {
|
||||
rule: 'VM-template'
|
||||
bind: -> @val.$container
|
||||
}
|
||||
|
||||
# Local VMs are handled directly in `VM.$container`.
|
||||
VMs: $set {
|
||||
rule: 'VM'
|
||||
bind: -> @val.$container
|
||||
}
|
||||
|
||||
$PBDs: -> @genval.PBDs
|
||||
|
||||
PIFs: -> @genval.PIFs
|
||||
$PIFs: -> @val.PIFs
|
||||
|
||||
tasks: $set {
|
||||
rule: 'task'
|
||||
bind: -> @val.$container
|
||||
if: $isTaskLive
|
||||
}
|
||||
|
||||
$running_VMs: $set {
|
||||
rule: 'VM'
|
||||
bind: -> @val.$container
|
||||
if: $isVMRunning
|
||||
}
|
||||
|
||||
$vCPUs: $sum {
|
||||
rule: 'VM'
|
||||
bind: -> @val.$container
|
||||
if: $isVMRunning
|
||||
val: -> @val.CPUs.number
|
||||
}
|
||||
}
|
||||
|
||||
# This definition is shared.
|
||||
VMdef = ->
|
||||
@data = {
|
||||
metrics: $link -> @genval.metrics
|
||||
guest_metrics: $link -> @genval.guest_metrics
|
||||
}
|
||||
|
||||
@val = {
|
||||
name_label: -> @genval.name_label
|
||||
|
||||
name_description: -> @genval.name_description
|
||||
|
||||
addresses: ->
|
||||
{guest_metrics} = @data
|
||||
if guest_metrics
|
||||
guest_metrics.networks
|
||||
else
|
||||
null
|
||||
|
||||
consoles: $set {
|
||||
rule: 'console'
|
||||
bind: -> @genval.VM
|
||||
val: -> @val
|
||||
}
|
||||
|
||||
current_operations: -> @genval.current_operations
|
||||
|
||||
os_version: ->
|
||||
{guest_metrics} = @data
|
||||
if guest_metrics
|
||||
guest_metrics.os_version
|
||||
else
|
||||
null
|
||||
|
||||
power_state: -> @genval.power_state
|
||||
|
||||
memory: ->
|
||||
{metrics, guest_metrics} = @data
|
||||
|
||||
memory = {
|
||||
dynamic: [
|
||||
+@genval.memory_dynamic_min
|
||||
+@genval.memory_dynamic_max
|
||||
]
|
||||
static: [
|
||||
+@genval.memory_static_min
|
||||
+@genval.memory_static_max
|
||||
]
|
||||
}
|
||||
|
||||
memory.size = if not $isVMRunning.call this
|
||||
+@genval.memory_dynamic_max
|
||||
else if (gmmemory = guest_metrics?.memory)?.used
|
||||
memory.usage = +gmmemory.used
|
||||
+gmmemory.total
|
||||
else if metrics
|
||||
+metrics.memory_actual
|
||||
else
|
||||
+@genval.memory_dynamic_max
|
||||
|
||||
memory
|
||||
|
||||
PV_drivers: ->
|
||||
{guest_metrics} = @data
|
||||
if guest_metrics
|
||||
guest_metrics.PV_drivers_up_to_date
|
||||
else
|
||||
false
|
||||
|
||||
CPUs: ->
|
||||
{metrics} = @data
|
||||
|
||||
CPUs = {
|
||||
max: +@genval.VCPUs_max
|
||||
number: if ($isVMRunning.call this) and metrics
|
||||
+metrics.VCPUs_number
|
||||
else
|
||||
+@genval.VCPUs_at_startup
|
||||
}
|
||||
|
||||
$CPU_usage: null #TODO
|
||||
|
||||
# FIXME: $container should contains the pool UUID when the VM is
|
||||
# not on a host.
|
||||
$container: ->
|
||||
if $isVMRunning.call this
|
||||
@genval.resident_on
|
||||
else
|
||||
# TODO: Handle local VMs. (`get_possible_hosts()`).
|
||||
@genval.$poolRef
|
||||
|
||||
snapshots: -> @genval.snapshots
|
||||
|
||||
snapshot_time: -> $toTimestamp @genval.snapshot_time
|
||||
|
||||
$VBDs: -> @genval.VBDs
|
||||
|
||||
VIFs: -> @genval.VIFs
|
||||
}
|
||||
@rule VM: VMdef
|
||||
@rule 'VM-controller': VMdef
|
||||
@rule 'VM-snapshot': VMdef
|
||||
|
||||
# VM-template starts with the same definition but extends it.
|
||||
@rule 'VM-template': ->
|
||||
VMdef.call this
|
||||
|
||||
@val.CPUs.number = -> +@genval.VCPUs_at_startup
|
||||
|
||||
@val.template_info = {
|
||||
arch: -> @genval.other_config?['install-arch']
|
||||
disks: ->
|
||||
#console.log @genval.other_config
|
||||
disks = @genval.other_config?.disks
|
||||
return [] unless disks?
|
||||
|
||||
disks = ($parseXML disks)?.provision?.disk
|
||||
return [] unless disks?
|
||||
|
||||
disks = [disks] unless $_.isArray disks
|
||||
# Normalize entries.
|
||||
for disk in disks
|
||||
disk.bootable = disk.bootable is 'true'
|
||||
disk.size = +disk.size
|
||||
disk.SR = disk.sr
|
||||
delete disk.sr
|
||||
disks
|
||||
install_methods: ->
|
||||
methods = @genval.other_config?['install-methods']
|
||||
return [] unless methods?
|
||||
methods.split ','
|
||||
}
|
||||
|
||||
@rule SR: ->
|
||||
@data = {
|
||||
# Note: not dynamic.
|
||||
host: $link(
|
||||
-> @genval.PBDs[0] ? 'OpaqueRef:NULL'
|
||||
-> @val.host
|
||||
)
|
||||
}
|
||||
|
||||
@val = {
|
||||
name_label: -> @genval.name_label
|
||||
|
||||
name_description: -> @genval.name_description
|
||||
|
||||
SR_type: -> @genval.type
|
||||
|
||||
content_type: -> @genval.content_type
|
||||
|
||||
physical_usage: -> +@genval.physical_utilisation
|
||||
|
||||
usage: -> +@genval.virtual_allocation
|
||||
|
||||
size: -> +@genval.physical_size
|
||||
|
||||
$container: ->
|
||||
if @genval.shared
|
||||
@genval.$poolRef
|
||||
else
|
||||
@data.host
|
||||
|
||||
$PBDs: -> @genval.PBDs
|
||||
|
||||
VDIs: -> @genval.VDIs
|
||||
}
|
||||
|
||||
@rule PBD: ->
|
||||
@val = {
|
||||
attached: -> @genval.currently_attached
|
||||
|
||||
host: -> @genval.host
|
||||
|
||||
SR: -> @genval.SR
|
||||
}
|
||||
|
||||
@rule PIF: ->
|
||||
@val = {
|
||||
attached: -> @genval.currently_attached
|
||||
|
||||
device: -> @genval.device
|
||||
|
||||
IP: -> @genval.IP
|
||||
|
||||
$host: -> @genval.host
|
||||
|
||||
MAC: -> @genval.MAC
|
||||
|
||||
# TODO: Find a more meaningful name.
|
||||
management: -> @genval.management
|
||||
|
||||
mode: -> @genval.ip_configuration_mode
|
||||
|
||||
MTU: -> +@genval.MTU
|
||||
|
||||
netmask: -> @genval.netmask
|
||||
|
||||
$network: -> @genval.network
|
||||
|
||||
vlan: -> @genval.VLAN
|
||||
|
||||
# TODO: What is it?
|
||||
#
|
||||
# Could it mean “is this a physical interface?”.
|
||||
# How could a PIF not be physical?
|
||||
#physical: -> @genval.physical
|
||||
}
|
||||
|
||||
@rule VDI: ->
|
||||
@val = {
|
||||
name_label: -> @genval.name_label
|
||||
|
||||
name_description: -> @genval.name_description
|
||||
|
||||
# TODO: determine whether or not tags are required for a VDI.
|
||||
#tags: $retrieveTags
|
||||
|
||||
usage: -> +@genval.physical_utilisation
|
||||
|
||||
size: -> +@genval.virtual_size
|
||||
|
||||
$snapshot_of: ->
|
||||
original = @genval.snapshot_of
|
||||
if original is 'OpaqueRef:NULL'
|
||||
null
|
||||
else
|
||||
original
|
||||
|
||||
snapshots: -> @genval.snapshots
|
||||
|
||||
# TODO: Does the name fit?
|
||||
#snapshot_time: -> @genval.snapshot_time
|
||||
|
||||
$SR: -> @genval.SR
|
||||
|
||||
$VBDs: -> @genval.VBDs
|
||||
|
||||
$VBD: -> # Deprecated
|
||||
{VBDs} = @genval
|
||||
|
||||
if VBDs.length is 0 then null else VBDs[0]
|
||||
}
|
||||
|
||||
@rule VBD: ->
|
||||
@val = {
|
||||
attached: -> @genval.currently_attached
|
||||
|
||||
bootable: -> @genval.bootable
|
||||
|
||||
read_only: -> @genval.mode is 'RO'
|
||||
|
||||
is_cd_drive: -> @genval.type is 'CD'
|
||||
|
||||
# null if empty.
|
||||
#
|
||||
# TODO: Is it really equivalent?
|
||||
VDI: ->
|
||||
VDI = @genval.VDI
|
||||
if VDI is 'OpaqueRef:NULL'
|
||||
null
|
||||
else
|
||||
VDI
|
||||
|
||||
VM: -> @genval.VM
|
||||
}
|
||||
|
||||
@rule VIF: ->
|
||||
@val = {
|
||||
attached: -> @genval.currently_attached
|
||||
|
||||
# TODO: Should it be cast to a number?
|
||||
device: -> @genval.device
|
||||
|
||||
MAC: -> @genval.MAC
|
||||
|
||||
MTU: -> +@genval.MTU
|
||||
|
||||
$network: -> @genval.network
|
||||
|
||||
$VM: -> @genval.VM
|
||||
}
|
||||
|
||||
@rule network: ->
|
||||
@val = {
|
||||
name_label: -> @genval.name_label
|
||||
|
||||
name_description: -> @genval.name_description
|
||||
|
||||
# TODO: determine whether or not tags are required for a VDI.
|
||||
#tags: $retrieveTags
|
||||
|
||||
bridge: -> @genval.bridge
|
||||
|
||||
MTU: -> +@genval.MTU
|
||||
|
||||
PIFs: -> @genval.PIFs
|
||||
|
||||
VIFs: -> @genval.VIFs
|
||||
}
|
||||
|
||||
@rule message: ->
|
||||
@val = {
|
||||
time: -> $toTimestamp @genval.timestamp
|
||||
|
||||
$object: ->
|
||||
# If the key of the concerned object has already be resolved
|
||||
# returns the known value.
|
||||
return @val.$object if @val.$object?
|
||||
|
||||
# Tries to resolve the key of the concerned object.
|
||||
object = (UUIDsToKeys.call this)[@genval.obj_uuid]
|
||||
|
||||
# If resolved, unregister from the watcher.
|
||||
UUIDsToKeys.unsubscribe.call this if object?
|
||||
|
||||
object
|
||||
|
||||
# TODO: Are these names meaningful?
|
||||
name: -> @genval.name
|
||||
body: -> @genval.body
|
||||
}
|
||||
|
||||
@rule task: ->
|
||||
@val = {
|
||||
name_label: -> @genval.name_label
|
||||
|
||||
name_description: -> @genval.name_description
|
||||
|
||||
progress: -> +@genval.progress
|
||||
|
||||
result: -> @genval.result
|
||||
|
||||
$host: -> @genval.resident_on
|
||||
|
||||
created: -> @genval.created
|
||||
|
||||
finished: -> @genval.finished
|
||||
|
||||
current_operations: -> @genval.current_operations
|
||||
|
||||
status: -> @genval.status
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -1,763 +0,0 @@
|
||||
{expect: $expect} = require 'chai'
|
||||
|
||||
$sinon = require 'sinon'
|
||||
|
||||
#---------------------------------------------------------------------
|
||||
|
||||
{$MappedCollection} = require './MappedCollection.coffee'
|
||||
|
||||
# Helpers for dealing with fibers.
|
||||
{$promisify} = require './fibers-utils'
|
||||
|
||||
#=====================================================================
|
||||
|
||||
describe 'spec', ->
|
||||
|
||||
collection = null
|
||||
before $promisify ->
|
||||
# Creates the collection.
|
||||
collection = new $MappedCollection()
|
||||
|
||||
# Loads the spec.
|
||||
(require './spec').call collection
|
||||
|
||||
# Loads the mockup data.
|
||||
collection.set (require './spec.spec-data')
|
||||
|
||||
#console.log collection.get()
|
||||
|
||||
it 'xo', ->
|
||||
xo = collection.get 'xo'
|
||||
|
||||
#console.log xo
|
||||
|
||||
$expect(xo).to.be.an 'object'
|
||||
|
||||
$expect(xo.type).to.equal 'xo'
|
||||
|
||||
$expect(xo.pools).to.have.members [
|
||||
'OpaqueRef:6462d0b3-8f20-ef76-fddf-002f7af3452e'
|
||||
]
|
||||
|
||||
$expect(xo.$CPUs).to.equal 8
|
||||
|
||||
$expect(xo.$running_VMs).to.have.members [
|
||||
'OpaqueRef:fdaba312-c3a5-0190-b1a1-bf389567e620'
|
||||
'OpaqueRef:46fa4c52-5e93-6cf7-32e3-c51fb4ed106d'
|
||||
'OpaqueRef:c0fa9288-2a6b-cd8e-b9a8-cc5afc63b386'
|
||||
'OpaqueRef:be2390b2-cd08-53f5-3fae-b76f6f3725bf'
|
||||
'OpaqueRef:8f9966ea-38ef-ac4c-b634-81e31ef1e7c1'
|
||||
'OpaqueRef:646297e5-4fd6-c70d-6365-ef19b9807f64'
|
||||
'OpaqueRef:1ef43ee8-bc18-6c4f-4919-0e42a3ac6e4b'
|
||||
]
|
||||
|
||||
$expect(xo.$vCPUs).to.equal 10
|
||||
|
||||
$expect(xo.$memory).to.be.an 'object'
|
||||
$expect(xo.$memory.usage).to.equal 15185723392
|
||||
$expect(xo.$memory.size).to.equal 33532379136
|
||||
|
||||
UUIDsToKeys = {}
|
||||
UUIDsToKeys[obj.UUID] = "#{obj.ref}" for obj in collection.get() when obj.UUID?
|
||||
$expect(xo.$UUIDsToKeys).to.deep.equal UUIDsToKeys
|
||||
|
||||
it 'pool', ->
|
||||
pool = collection.get 'OpaqueRef:6462d0b3-8f20-ef76-fddf-002f7af3452e'
|
||||
|
||||
#console.log pool
|
||||
|
||||
$expect(pool).to.be.an 'object'
|
||||
|
||||
$expect(pool.type).to.equal 'pool'
|
||||
|
||||
$expect(pool.name_label).to.equal 'Lab Pool'
|
||||
|
||||
$expect(pool.name_description).to.equal 'Vates dev pool at our HQ'
|
||||
|
||||
$expect(pool.tags).to.have.members []
|
||||
|
||||
$expect(pool.SRs).to.have.members [
|
||||
'OpaqueRef:d6fe49bf-dd48-c929-5aab-b2786a2e7aee'
|
||||
'OpaqueRef:6637b7d7-9e5c-f331-c7e4-a7f68f77a047'
|
||||
'OpaqueRef:557155b2-f092-3417-f509-7ee35b1d42da'
|
||||
]
|
||||
|
||||
$expect(pool.default_SR).to.equal 'OpaqueRef:d6fe49bf-dd48-c929-5aab-b2786a2e7aee'
|
||||
|
||||
$expect(pool.HA_enabled).to.be.false
|
||||
|
||||
$expect(pool.hosts).to.have.members [
|
||||
'OpaqueRef:cd0f68c5-5245-5ae8-f0e1-324e2201c692'
|
||||
'OpaqueRef:bbc98f5e-1a17-2030-28af-0df2393f3145'
|
||||
]
|
||||
|
||||
$expect(pool.master).to.equal 'OpaqueRef:bbc98f5e-1a17-2030-28af-0df2393f3145'
|
||||
|
||||
$expect(pool.networks).to.have.members [
|
||||
'OpaqueRef:dbc93777-f2c0-e888-967d-dd9beeffb3c0'
|
||||
'OpaqueRef:4e265829-7517-3520-6a97-56b6ac0730c9'
|
||||
'OpaqueRef:16013d48-b9eb-84c0-0e62-d809211b0632'
|
||||
]
|
||||
|
||||
$expect(pool.templates).to.have.members [
|
||||
'OpaqueRef:f81c6db6-4227-55a5-0c2f-b670ca5d8d3f'
|
||||
'OpaqueRef:f449b8ec-ac86-1b6d-2347-37ec36c41bc5'
|
||||
'OpaqueRef:f02a3c19-447b-c618-fb51-a9cde79be17c'
|
||||
'OpaqueRef:ee2e2c00-8011-4847-ba7e-c288d5fb01f5'
|
||||
'OpaqueRef:ebc96e49-11d4-471d-c21f-625a95c34ff9'
|
||||
'OpaqueRef:e9fb38c8-acc3-dbb8-cc6f-f1f89b03c1ae'
|
||||
'OpaqueRef:e803bc1b-d3be-b95f-f3cc-a26a174ec93c'
|
||||
'OpaqueRef:e373c644-3576-985e-9c8f-67062c81d0d2'
|
||||
'OpaqueRef:e3035b8b-cd27-3e7c-ecbf-54a18a2da59e'
|
||||
'OpaqueRef:d99a46bf-1b68-072c-00db-444d099466cd'
|
||||
'OpaqueRef:d45b3989-7350-5166-eeaa-7b789a32addd'
|
||||
'OpaqueRef:d18c965e-0cef-48b0-2f8d-d48ef6663c32'
|
||||
'OpaqueRef:d15de0db-1dc5-2a00-331a-c0f7d3c2e123'
|
||||
'OpaqueRef:cfe620f9-5c68-0f35-ce9f-8f5227fda1c8'
|
||||
'OpaqueRef:cb865487-9139-3fbc-4aac-68abdb663925'
|
||||
'OpaqueRef:c8bf31d6-9888-4256-1547-c722016a0079'
|
||||
'OpaqueRef:c651901b-0944-be6b-aabf-a87d9a037edd'
|
||||
'OpaqueRef:c5a9e2de-1916-7f4c-aa2a-ce95d138032b'
|
||||
'OpaqueRef:c22bce1f-16a0-7745-179d-dcbd5c5deab3'
|
||||
'OpaqueRef:be6abc7d-dd7a-5ee6-9c95-8e562a69d992'
|
||||
'OpaqueRef:b9587bb6-6efe-0c71-e01c-2c750c9ab774'
|
||||
'OpaqueRef:b6f58482-8b60-b3b4-2a01-0d6113411bf2'
|
||||
'OpaqueRef:ad21fbbb-6cf9-e6ca-c415-1f428f20da1f'
|
||||
'OpaqueRef:aa2d04ec-0512-c128-8820-c8ecde93baa4'
|
||||
'OpaqueRef:a247a02f-8909-5044-64a0-82460b25e740'
|
||||
'OpaqueRef:9d28dba9-aee6-cafd-06af-54ebdfb1c271'
|
||||
'OpaqueRef:9796cc01-6640-211f-09f9-fee94f9cd720'
|
||||
'OpaqueRef:922b3a98-f238-4cea-8b75-c38e90ac11ee'
|
||||
'OpaqueRef:8e720505-e75b-eda3-3b14-fd1471890cc1'
|
||||
'OpaqueRef:8e3211dc-fdaf-22c7-41b2-c3a892529679'
|
||||
'OpaqueRef:89919714-1184-ce4b-3cb5-67059640b3a7'
|
||||
'OpaqueRef:892768c0-4d15-769f-e760-b781a0291ddb'
|
||||
'OpaqueRef:838ff163-ae6e-d98e-9cef-4d783f81dcb0'
|
||||
'OpaqueRef:8079d64b-fe87-0ecf-e558-7b607b0e1524'
|
||||
'OpaqueRef:773d92c9-898b-bc25-a50d-d868bbf933a4'
|
||||
'OpaqueRef:770d2193-ab69-4fc3-c462-7f75a79d497c'
|
||||
'OpaqueRef:75441e00-55df-85f5-1780-731110df91de'
|
||||
'OpaqueRef:6ee1cc24-ebbb-b02a-88b0-a921c7a5f217'
|
||||
'OpaqueRef:6b5be573-b116-6238-9cff-bde0658d6f18'
|
||||
'OpaqueRef:6a09a6de-e778-a474-4ebd-f617db5b5d5e'
|
||||
'OpaqueRef:616942c0-1e1b-e733-3c4c-7236fd3de158'
|
||||
'OpaqueRef:5e93cf73-a212-a83f-d3f9-a539be98d320'
|
||||
'OpaqueRef:56af2e14-d4bb-20e9-421b-00d75dfb89f2'
|
||||
'OpaqueRef:5059cc2d-b414-97eb-6aac-ce816b72b2bd'
|
||||
'OpaqueRef:4a43ad28-b809-2c8f-aa24-70d8bd4954f2'
|
||||
'OpaqueRef:466d7dc3-f2df-8c8d-685d-eef256fe2b43'
|
||||
'OpaqueRef:4347e9d6-7faf-90e4-4f5f-d513cf44b3cc'
|
||||
'OpaqueRef:3c4558e8-ed88-ce88-81a9-111ac2cc56d6'
|
||||
'OpaqueRef:3b97e45b-aa4e-d175-95e5-e95ceefa0b6b'
|
||||
'OpaqueRef:2e3b5ada-5083-87b1-d6fb-aaa0e5bd862d'
|
||||
'OpaqueRef:2b6e3248-52b0-85d1-7415-4f91a0a90a3a'
|
||||
'OpaqueRef:2a838052-3aa3-d09d-1eae-8293a565fef5'
|
||||
'OpaqueRef:2a092eee-7c6a-058b-0368-b37362328678'
|
||||
'OpaqueRef:2968283f-8656-6e31-816c-e96325e66ebf'
|
||||
'OpaqueRef:27ad4e06-a7b2-20a2-4fd9-7f1b54fdc5a2'
|
||||
'OpaqueRef:217d930f-8e65-14e6-eb20-63d55158093f'
|
||||
'OpaqueRef:20377446-2388-5c8f-d3f2-6e9c883c61d9'
|
||||
'OpaqueRef:201cf416-bfd0-00d3-a4d2-b19226c43c82'
|
||||
'OpaqueRef:1ed4ee31-56e0-98da-65d4-00c776716b9c'
|
||||
'OpaqueRef:1c0b590d-563b-5061-a253-f98535ab8389'
|
||||
'OpaqueRef:1be0fe3b-1944-06db-3734-b6bb888cfe78'
|
||||
'OpaqueRef:12d0dfc0-ce63-a072-3cd0-ccba7bd3c200'
|
||||
'OpaqueRef:039273c3-b4b2-5c68-63e4-c5610a738fe3'
|
||||
'OpaqueRef:030314a2-0909-9e7a-418a-9f38746aaf0c',
|
||||
]
|
||||
|
||||
$expect(pool.VMs).to.have.members [
|
||||
'OpaqueRef:d4fa8fba-ec86-5928-a1bb-dd78b6fb5944'
|
||||
'OpaqueRef:8491f148-3e78-9c74-ab98-84445c5f2861'
|
||||
'OpaqueRef:13b9ec24-04ea-ae04-78e6-6ec4b81a8deb'
|
||||
]
|
||||
|
||||
$expect(pool.$running_hosts).to.have.members [
|
||||
'OpaqueRef:cd0f68c5-5245-5ae8-f0e1-324e2201c692'
|
||||
'OpaqueRef:bbc98f5e-1a17-2030-28af-0df2393f3145'
|
||||
]
|
||||
|
||||
$expect(pool.$running_VMs).to.have.members [
|
||||
'OpaqueRef:fdaba312-c3a5-0190-b1a1-bf389567e620'
|
||||
'OpaqueRef:46fa4c52-5e93-6cf7-32e3-c51fb4ed106d'
|
||||
'OpaqueRef:c0fa9288-2a6b-cd8e-b9a8-cc5afc63b386'
|
||||
'OpaqueRef:be2390b2-cd08-53f5-3fae-b76f6f3725bf'
|
||||
'OpaqueRef:8f9966ea-38ef-ac4c-b634-81e31ef1e7c1'
|
||||
'OpaqueRef:646297e5-4fd6-c70d-6365-ef19b9807f64'
|
||||
'OpaqueRef:1ef43ee8-bc18-6c4f-4919-0e42a3ac6e4b'
|
||||
]
|
||||
|
||||
$expect(pool.$VMs).to.have.members [
|
||||
'OpaqueRef:fdaba312-c3a5-0190-b1a1-bf389567e620'
|
||||
'OpaqueRef:46fa4c52-5e93-6cf7-32e3-c51fb4ed106d'
|
||||
'OpaqueRef:d4fa8fba-ec86-5928-a1bb-dd78b6fb5944'
|
||||
'OpaqueRef:8491f148-3e78-9c74-ab98-84445c5f2861'
|
||||
'OpaqueRef:13b9ec24-04ea-ae04-78e6-6ec4b81a8deb'
|
||||
'OpaqueRef:c0fa9288-2a6b-cd8e-b9a8-cc5afc63b386'
|
||||
'OpaqueRef:be2390b2-cd08-53f5-3fae-b76f6f3725bf'
|
||||
'OpaqueRef:8f9966ea-38ef-ac4c-b634-81e31ef1e7c1'
|
||||
'OpaqueRef:646297e5-4fd6-c70d-6365-ef19b9807f64'
|
||||
'OpaqueRef:1ef43ee8-bc18-6c4f-4919-0e42a3ac6e4b'
|
||||
]
|
||||
|
||||
$expect(pool.messages).to.have.members [
|
||||
'OpaqueRef:0241d2be-fcda-64b7-b95d-550399f22000'
|
||||
'OpaqueRef:08093780-5d87-46f4-400d-fc8406bdd6c2'
|
||||
'OpaqueRef:0c565205-db69-eb0f-b80b-a8e356ae43ae'
|
||||
'OpaqueRef:0f955091-d6e6-ed3e-2bbe-94d914e6efbe'
|
||||
'OpaqueRef:15f61c91-5ac8-6234-78bb-2edbdcf9164f'
|
||||
'OpaqueRef:1b04b4db-3622-4d54-e8fa-a2f6661c6e43'
|
||||
'OpaqueRef:20aadafb-47c8-0796-e3c2-4e497bcb0205'
|
||||
'OpaqueRef:2243e321-e4bd-50dd-1451-f329df240517'
|
||||
'OpaqueRef:226e9274-77d6-9805-a0f3-396d1e54fe72'
|
||||
'OpaqueRef:230d01c6-3e25-b877-9e35-13a707335e23'
|
||||
'OpaqueRef:279e9aed-7d9e-13bc-e4d2-d477abbf9f6a'
|
||||
'OpaqueRef:2c460c86-2e1c-cd0d-cbaf-95bf771af2bc'
|
||||
'OpaqueRef:300a2868-2b8a-4f0c-788d-4e2ba4a160da'
|
||||
'OpaqueRef:323297f9-4a0b-c517-1ff7-eacad80fc796'
|
||||
'OpaqueRef:33d58ecd-d2a4-f63a-46bb-307a7c7762a6'
|
||||
'OpaqueRef:3962ad4b-18e9-53ce-ff72-b2ef3d6692ec'
|
||||
'OpaqueRef:3a8a42d6-f5b3-1479-3ad6-2c7caed94459'
|
||||
'OpaqueRef:3f77ad7a-de22-0b05-4005-7cfdc5d8bc86'
|
||||
'OpaqueRef:4147a60c-2b41-4dc7-491d-3470466abbc7'
|
||||
'OpaqueRef:443c4e46-d98a-87d6-92f5-c35bb5b65a5c'
|
||||
'OpaqueRef:4a3aebd9-e670-c796-4938-e29e178f1959'
|
||||
'OpaqueRef:50f02c5f-b2d0-a42a-a156-7905b78a918a'
|
||||
'OpaqueRef:5f34bfc5-f92f-9830-b3e9-06367ef56a77'
|
||||
'OpaqueRef:69d3511e-ec73-69c9-819e-14b85236059d'
|
||||
'OpaqueRef:6b04d423-8991-c838-d981-aca1b9c7be7d'
|
||||
'OpaqueRef:6e161f6f-df2b-195f-be46-530884a2c24a'
|
||||
'OpaqueRef:6f9b4c87-c7ba-1a87-073d-569051f307a8'
|
||||
'OpaqueRef:72360954-3629-1e09-b1bf-b819732bddfd'
|
||||
'OpaqueRef:79f9e82b-1a0e-75b7-efc5-8689a4cd4aed'
|
||||
'OpaqueRef:844844c6-5e82-4d9c-7ed9-01c46d46e67c'
|
||||
'OpaqueRef:84a7efe6-2a37-d4be-5f9a-aa66adfe3104'
|
||||
'OpaqueRef:9a645810-7308-c296-d9df-cc5d91f8f2a4'
|
||||
'OpaqueRef:a073f53c-557a-fd67-878d-b3a881ebd935'
|
||||
'OpaqueRef:a08f1c9a-34de-5441-b847-18533244910d'
|
||||
'OpaqueRef:a4fd777c-f417-23e9-8338-30d8097a8430'
|
||||
'OpaqueRef:a5296901-25c3-b600-7be7-16a20ba86600'
|
||||
'OpaqueRef:a99badbe-75fa-8bc8-22b3-78c616873b62'
|
||||
'OpaqueRef:ab16dfa7-3c86-56c3-038c-c6bcfe0b64c1'
|
||||
'OpaqueRef:af840b26-91b6-56aa-e2a0-266ce7dd411b'
|
||||
'OpaqueRef:b857ac11-36a0-38e4-4d9c-13586e381f7a'
|
||||
'OpaqueRef:c0b26952-1a46-9dfb-a826-78cbfeaa1b00'
|
||||
'OpaqueRef:cdeda917-3496-c407-95fd-2ef63bf5e79e'
|
||||
'OpaqueRef:d5ab7d13-0ebb-5805-b767-608cb7737690'
|
||||
'OpaqueRef:dae9fbe3-a709-3433-e8e3-491b3a79df84'
|
||||
'OpaqueRef:dd735a0f-d2fd-9475-7dd3-b387251f4426'
|
||||
'OpaqueRef:df07d60e-8a03-6979-3e61-4460bc8197b3'
|
||||
'OpaqueRef:e6a0aa45-f8e0-ae7d-7b3a-d76b95a03c95'
|
||||
'OpaqueRef:eaad760a-0e23-4e2b-3f96-2f65170a1dd7'
|
||||
'OpaqueRef:ebead5cf-4a48-ad28-4241-ad5869fa9752'
|
||||
'OpaqueRef:ecc7b91d-6f50-94c6-6f51-2d609dc3ebe7'
|
||||
'OpaqueRef:f3492f88-e0b0-405a-5723-f83429e016c5'
|
||||
]
|
||||
|
||||
it 'host', ->
|
||||
host = collection.get 'OpaqueRef:bbc98f5e-1a17-2030-28af-0df2393f3145'
|
||||
|
||||
#console.log host
|
||||
|
||||
$expect(host).to.be.an 'object'
|
||||
|
||||
$expect(host.type).to.equal 'host'
|
||||
|
||||
$expect(host.name_label).to.equal 'lab1'
|
||||
|
||||
$expect(host.name_description).to.equal 'Default install of XenServer'
|
||||
|
||||
$expect(host.tags).to.have.members []
|
||||
|
||||
$expect(host.address).to.equal '192.168.1.1'
|
||||
|
||||
$expect(host.controller).to.equal 'OpaqueRef:719e4877-c7ad-68be-6b04-5750c8dcfeed'
|
||||
|
||||
# Burk.
|
||||
$expect(host.CPUs).to.deep.equal {
|
||||
cpu_count: '4'
|
||||
socket_count: '1'
|
||||
vendor: 'GenuineIntel'
|
||||
speed: '3192.858'
|
||||
modelname: 'Intel(R) Core(TM) i5-3470 CPU @ 3.20GHz'
|
||||
family: '6'
|
||||
model: '58'
|
||||
stepping: '9'
|
||||
flags: 'fpu de tsc msr pae mce cx8 apic sep mtrr mca cmov pat clflush acpi mmx fxsr sse sse2 ss ht nx constant_tsc nonstop_tsc aperfmperf pni pclmulqdq vmx est ssse3 sse4_1 sse4_2 x2apic popcnt aes hypervisor ida arat tpr_shadow vnmi flexpriority ept vpid'
|
||||
features: '77bae3ff-bfebfbff-00000001-28100800'
|
||||
features_after_reboot: '77bae3ff-bfebfbff-00000001-28100800'
|
||||
physical_features: '77bae3ff-bfebfbff-00000001-28100800'
|
||||
maskable: 'full'
|
||||
}
|
||||
|
||||
$expect(host.enabled).to.be.true
|
||||
|
||||
$expect(host.hostname).to.equal 'lab1'
|
||||
|
||||
$expect(host.iSCSI_name).to.equal 'iqn.2013-07.com.example:83ba9261'
|
||||
|
||||
$expect(host.memory).to.be.an 'object'
|
||||
$expect(host.memory.usage).to.equal 2564788224
|
||||
$expect(host.memory.size).to.equal 8502759424
|
||||
|
||||
$expect(host.power_state).to.equal 'Running'
|
||||
|
||||
$expect(host.SRs).to.have.members [
|
||||
'OpaqueRef:31be9b5e-882a-a8ae-0edf-bf8942b49b5a'
|
||||
'OpaqueRef:7c88a8c6-fc48-8836-28fa-212f67c42d2f'
|
||||
'OpaqueRef:ec76bd6a-f2c0-636d-ca72-de8fb42d6eea'
|
||||
]
|
||||
|
||||
$expect(host.templates).to.have.members [
|
||||
# TODO
|
||||
]
|
||||
|
||||
$expect(host.VMs).to.have.members [
|
||||
'OpaqueRef:fdaba312-c3a5-0190-b1a1-bf389567e620'
|
||||
'OpaqueRef:46fa4c52-5e93-6cf7-32e3-c51fb4ed106d'
|
||||
]
|
||||
|
||||
$expect(host.$PBDs).to.have.members [
|
||||
'OpaqueRef:ff32de74-138c-9d80-ab58-c631d2aa0e71'
|
||||
'OpaqueRef:f0f98779-5cf8-cabc-edc3-631a2d63d89c'
|
||||
'OpaqueRef:b70f8e06-07a8-a5e7-2856-f221c822e9b2'
|
||||
'OpaqueRef:b641552a-8c92-71b3-c0a2-e4dd3d04c215'
|
||||
'OpaqueRef:93320534-824f-850a-64a2-bcbfdc2e0927'
|
||||
'OpaqueRef:0c1d3862-5a38-e4cc-4a46-d8358a622461'
|
||||
]
|
||||
|
||||
$expect(host.$PIFs).to.have.members [
|
||||
'OpaqueRef:aef57ed4-e4d9-7f72-0376-b781a19bb9d2'
|
||||
'OpaqueRef:06f53e3d-d8de-d4ed-6359-9e20b4fb0d21'
|
||||
]
|
||||
|
||||
$expect(host.messages).to.have.members [
|
||||
'OpaqueRef:cb515b9a-ef8c-13d4-88ea-e0d3ee88d22a'
|
||||
'OpaqueRef:6ba7c244-3b44-2ed2-ec81-4fa13ea82465'
|
||||
'OpaqueRef:0e3fc97f-45ce-26c3-9435-899be96b35c4'
|
||||
'OpaqueRef:6ca16f45-6266-6cff-55cd-19a8ef0acf1a'
|
||||
'OpaqueRef:11452a2a-1ccd-e4df-25d8-ba99bba710db'
|
||||
'OpaqueRef:9ddc8eb2-969f-ba56-757a-efd482da5ce9'
|
||||
'OpaqueRef:68c8d0c6-e5a2-8ade-569a-dfc732e7994d'
|
||||
'OpaqueRef:ddb628ca-24f1-04d2-0b2c-9996aaab59f2'
|
||||
'OpaqueRef:0e7044a7-542b-4dd9-65bc-cded0e41853a'
|
||||
'OpaqueRef:ee26daf0-2ff7-734e-438d-9a521aaaa0c5'
|
||||
'OpaqueRef:40f8459f-1b6b-1625-1284-0f2878c3203d'
|
||||
'OpaqueRef:739ca434-6dca-b633-0097-b3f3183150a7'
|
||||
'OpaqueRef:cf655e45-c8c7-bdb9-e56c-5b67d6952f15'
|
||||
'OpaqueRef:3e33b140-f7e8-7dcc-3475-97dcc2fbfb5b'
|
||||
'OpaqueRef:8f3e2923-e690-e859-4f9e-a3e711a1e230'
|
||||
'OpaqueRef:ed7b1960-1ab7-4f47-8ef1-7a7769e09207'
|
||||
'OpaqueRef:6a0c4183-2f95-661f-9b19-0df0015867ca'
|
||||
'OpaqueRef:8d04b3fa-e81d-c6ae-d072-bd3a1ea22189'
|
||||
'OpaqueRef:dada1bd4-d7ed-429f-0a1a-585a3bfbf7e6'
|
||||
'OpaqueRef:a5648ca1-b37a-0765-9192-ebfb9ff376e8'
|
||||
'OpaqueRef:78c09b42-ad6f-0e66-0349-80b45264120d'
|
||||
'OpaqueRef:9c657a2b-560c-2050-014a-20e8cf5bd235'
|
||||
'OpaqueRef:1d50d25b-41f6-ffd3-5410-0de4fbed8543'
|
||||
'OpaqueRef:cb515b9a-ef8c-13d4-88ea-e0d3ee88d22a'
|
||||
'OpaqueRef:6ba7c244-3b44-2ed2-ec81-4fa13ea82465'
|
||||
'OpaqueRef:0e3fc97f-45ce-26c3-9435-899be96b35c4'
|
||||
'OpaqueRef:6ca16f45-6266-6cff-55cd-19a8ef0acf1a'
|
||||
'OpaqueRef:11452a2a-1ccd-e4df-25d8-ba99bba710db'
|
||||
'OpaqueRef:9ddc8eb2-969f-ba56-757a-efd482da5ce9'
|
||||
'OpaqueRef:68c8d0c6-e5a2-8ade-569a-dfc732e7994d'
|
||||
'OpaqueRef:ddb628ca-24f1-04d2-0b2c-9996aaab59f2'
|
||||
'OpaqueRef:0e7044a7-542b-4dd9-65bc-cded0e41853a'
|
||||
'OpaqueRef:ee26daf0-2ff7-734e-438d-9a521aaaa0c5'
|
||||
'OpaqueRef:40f8459f-1b6b-1625-1284-0f2878c3203d'
|
||||
'OpaqueRef:739ca434-6dca-b633-0097-b3f3183150a7'
|
||||
'OpaqueRef:cf655e45-c8c7-bdb9-e56c-5b67d6952f15'
|
||||
'OpaqueRef:3e33b140-f7e8-7dcc-3475-97dcc2fbfb5b'
|
||||
'OpaqueRef:8f3e2923-e690-e859-4f9e-a3e711a1e230'
|
||||
'OpaqueRef:ed7b1960-1ab7-4f47-8ef1-7a7769e09207'
|
||||
'OpaqueRef:6a0c4183-2f95-661f-9b19-0df0015867ca'
|
||||
'OpaqueRef:8d04b3fa-e81d-c6ae-d072-bd3a1ea22189'
|
||||
'OpaqueRef:dada1bd4-d7ed-429f-0a1a-585a3bfbf7e6'
|
||||
'OpaqueRef:a5648ca1-b37a-0765-9192-ebfb9ff376e8'
|
||||
'OpaqueRef:78c09b42-ad6f-0e66-0349-80b45264120d'
|
||||
'OpaqueRef:9c657a2b-560c-2050-014a-20e8cf5bd235'
|
||||
'OpaqueRef:1d50d25b-41f6-ffd3-5410-0de4fbed8543'
|
||||
]
|
||||
|
||||
$expect(host.tasks).to.have.members [
|
||||
# TODO
|
||||
]
|
||||
|
||||
$expect(host.$running_VMs).to.have.members [
|
||||
'OpaqueRef:fdaba312-c3a5-0190-b1a1-bf389567e620'
|
||||
'OpaqueRef:46fa4c52-5e93-6cf7-32e3-c51fb4ed106d'
|
||||
]
|
||||
|
||||
$expect(host.$vCPUs).to.equal 2
|
||||
|
||||
it 'VM', ->
|
||||
vm = collection.get 'OpaqueRef:fdaba312-c3a5-0190-b1a1-bf389567e620'
|
||||
|
||||
#console.log vm
|
||||
|
||||
$expect(vm).to.be.an 'object'
|
||||
|
||||
$expect(vm.type).to.equal 'VM'
|
||||
|
||||
$expect(vm.name_label).to.equal 'ceph3'
|
||||
|
||||
$expect(vm.name_description).to.equal ''
|
||||
|
||||
$expect(vm.tags).to.have.members []
|
||||
|
||||
$expect(vm.addresses).to.deep.equal {
|
||||
'0/ip': '192.168.1.116'
|
||||
'0/ipv6/0': 'fe80::cc20:2bff:fe38:7ffd'
|
||||
}
|
||||
|
||||
$expect(vm.consoles).to.deep.equal [
|
||||
{
|
||||
uuid: 'b7f85b67-4b8a-0586-b279-6146da76642f'
|
||||
protocol: 'rfb'
|
||||
location: 'https://192.168.1.1/console?uuid=b7f85b67-4b8a-0586-b279-6146da76642f'
|
||||
VM: 'OpaqueRef:fdaba312-c3a5-0190-b1a1-bf389567e620'
|
||||
other_config: {}
|
||||
'$pool': '313624ab-0958-bb1e-45b5-7556a463a10b'
|
||||
'$poolRef': 'OpaqueRef:6462d0b3-8f20-ef76-fddf-002f7af3452e'
|
||||
'$ref': 'OpaqueRef:69b8dbde-161c-b3fa-bd1a-3567e7efdbda'
|
||||
'$type': 'console'
|
||||
}
|
||||
]
|
||||
|
||||
$expect(vm.current_operations).to.deep.equal {
|
||||
# No data for this test.
|
||||
}
|
||||
|
||||
$expect(vm.memory).to.deep.equal {
|
||||
dynamic: [
|
||||
536870912
|
||||
536870912
|
||||
]
|
||||
static: [
|
||||
134217728
|
||||
536870912
|
||||
]
|
||||
size: 536838144
|
||||
}
|
||||
|
||||
$expect(vm.messages).to.have.members [
|
||||
'OpaqueRef:a242799a-03bf-b55e-ecde-ddfe902fa69e'
|
||||
'OpaqueRef:5cec485b-e276-c45b-09cb-dd02bb1d00f3'
|
||||
'OpaqueRef:ff3b6df1-b761-0d75-e80e-4ef137eec9e6'
|
||||
'OpaqueRef:a8d94d7e-7a6e-0cc1-b7a0-8f18940410fd'
|
||||
'OpaqueRef:35585a79-caf7-6522-18ee-8d3e8459441d'
|
||||
'OpaqueRef:68d1102f-eadc-e1f3-7949-3f62248c165c'
|
||||
'OpaqueRef:974bef10-184a-c063-aa32-c318fd39e400'
|
||||
'OpaqueRef:e092c4e1-a211-204a-f773-49cc3a4611be'
|
||||
'OpaqueRef:013a4a12-1981-fbc8-92ac-1fa45d2e9c9c'
|
||||
'OpaqueRef:a77fc714-b5b1-0c37-d006-0935506bb8cd'
|
||||
'OpaqueRef:554ec983-e67a-fc8b-7d2a-00c55be5f266'
|
||||
'OpaqueRef:38404a18-4c1b-0bf5-1d45-c47243bbc69d'
|
||||
'OpaqueRef:0f98e883-a4d5-0fd8-3aa3-92be69adc4e3'
|
||||
'OpaqueRef:b3e9ac53-f6b8-4c49-f096-57f680136477'
|
||||
'OpaqueRef:1aa65d64-a00b-4c0b-be07-95f6eec7fd87'
|
||||
'OpaqueRef:be431f8c-f39b-4a64-5fc2-de9744ced26a'
|
||||
'OpaqueRef:0e571611-6194-6ce6-bae0-94bbe57576c6'
|
||||
'OpaqueRef:114fdd8a-844c-6bb5-0855-e3427bc8f073'
|
||||
'OpaqueRef:a486606c-1c75-e1c3-56de-c6e1bc3df980'
|
||||
'OpaqueRef:b6975094-843e-a19a-6101-ee7953e40580'
|
||||
'OpaqueRef:f15d7d4c-32d1-45e1-5f6f-ddc68733bab6'
|
||||
'OpaqueRef:1b04b1a2-e8b2-df82-6618-0d0a741d8bbb'
|
||||
'OpaqueRef:dcd41e75-47fc-5ae5-1d59-5176a7b76eaa'
|
||||
'OpaqueRef:71ed5eba-33c9-6deb-6dc2-ab670a6c968b'
|
||||
'OpaqueRef:59ee665c-9270-64a4-3829-aef3e045a705'
|
||||
'OpaqueRef:88979f4b-16ef-3b99-a616-aa1e2787bebe'
|
||||
'OpaqueRef:80a3e419-5a81-a7df-103d-5cf60bbde793'
|
||||
'OpaqueRef:38737284-e4e1-5172-2bf3-f9d70dcaadfa'
|
||||
'OpaqueRef:456d4d7f-77f8-ef40-aadd-f56601bc7c2b'
|
||||
'OpaqueRef:4a949518-cc01-a003-f386-b3319db6d7a6'
|
||||
'OpaqueRef:c8834c52-f15b-437d-1e09-958fedbf3c5b'
|
||||
'OpaqueRef:07d40d2c-4f6e-4f5f-0c3e-c2ea028d4fc4'
|
||||
'OpaqueRef:6df45555-1b11-2873-8947-2b6e7c9445be'
|
||||
'OpaqueRef:d3c60e69-2cf8-191f-9679-d6ae0ecdf5f9'
|
||||
'OpaqueRef:ed499671-2c01-3dc9-f6cd-553fef4b6716'
|
||||
]
|
||||
|
||||
$expect(vm.power_state).to.equal 'Running'
|
||||
|
||||
$expect(vm.CPUs).to.deep.equal {
|
||||
max: 1
|
||||
number: 1
|
||||
}
|
||||
|
||||
$expect(vm.$CPU_usage).to.be.null
|
||||
|
||||
$expect(vm.$container).to.equal 'OpaqueRef:bbc98f5e-1a17-2030-28af-0df2393f3145'
|
||||
|
||||
$expect(vm.snapshots).to.have.members []
|
||||
|
||||
$expect(vm.snapshot_time).to.equal null
|
||||
|
||||
$expect(vm.$VBDs).to.have.members [
|
||||
'OpaqueRef:dbb53525-e1a3-741b-4924-9944b845bc0c'
|
||||
'OpaqueRef:1bd20244-01a0-fec3-eb00-79a453a56446'
|
||||
]
|
||||
|
||||
$expect(vm.VIFs).to.have.members [
|
||||
'OpaqueRef:20349ad5-0a0d-4b80-dcc0-0037fa647182'
|
||||
]
|
||||
|
||||
it 'VM-template', ->
|
||||
vm = collection.get 'OpaqueRef:f02a3c19-447b-c618-fb51-a9cde79be17c'
|
||||
|
||||
#console.log vm
|
||||
|
||||
# Only specific VM-templates fields will be tested.
|
||||
|
||||
$expect(vm.type).to.equal 'VM-template'
|
||||
|
||||
$expect(vm.template_info).to.be.an 'object'
|
||||
|
||||
$expect(vm.template_info.arch).to.equal 'amd64'
|
||||
|
||||
$expect(vm.template_info.disks).to.deep.equal [
|
||||
{
|
||||
bootable: true
|
||||
device: '0'
|
||||
size: 8589934592
|
||||
SR: ''
|
||||
type: 'system'
|
||||
}
|
||||
]
|
||||
|
||||
$expect(vm.template_info.install_methods).to.have.members [
|
||||
'cdrom'
|
||||
'http'
|
||||
'ftp'
|
||||
]
|
||||
|
||||
it 'SR', ->
|
||||
sr = collection.get 'OpaqueRef:d6fe49bf-dd48-c929-5aab-b2786a2e7aee'
|
||||
|
||||
#console.log sr
|
||||
|
||||
$expect(sr).to.be.an 'object'
|
||||
|
||||
$expect(sr.type).to.equal 'SR'
|
||||
|
||||
$expect(sr.name_label).to.equal 'Zfs'
|
||||
|
||||
$expect(sr.name_description).to.equal 'iSCSI SR [192.168.0.100 (iqn.1986-03.com.sun:02:ba2ab54c-2d14-eb74-d6f9-ef7c4f28ff1e; LUN 0: A83BCKLAF: 2048 GB (NEXENTA))]'
|
||||
|
||||
$expect(sr.SR_type).to.equal 'lvmoiscsi'
|
||||
|
||||
$expect(sr.content_type).to.equal ''
|
||||
|
||||
$expect(sr.physical_usage).to.equal 205831274496
|
||||
|
||||
$expect(sr.usage).to.equal 202358390784
|
||||
|
||||
$expect(sr.size).to.equal 2199010672640
|
||||
|
||||
$expect(sr.$container).to.equal 'OpaqueRef:6462d0b3-8f20-ef76-fddf-002f7af3452e'
|
||||
|
||||
$expect(sr.$PBDs).to.have.members [
|
||||
'OpaqueRef:ff32de74-138c-9d80-ab58-c631d2aa0e71'
|
||||
'OpaqueRef:200674ae-d9ab-2caa-a283-4fa3d14592fd'
|
||||
]
|
||||
|
||||
$expect(sr.VDIs).to.have.members [
|
||||
'OpaqueRef:b4a1573f-c235-8acd-4625-dfbcb2beb523'
|
||||
'OpaqueRef:098a2155-605b-241e-f775-a05c2133874e'
|
||||
'OpaqueRef:f7d900f9-a4fe-9a3e-ead8-28db301d26e8'
|
||||
'OpaqueRef:f26d2af5-b529-4d16-21d1-a56965e7bfb1'
|
||||
'OpaqueRef:ec5ce10e-023e-9a9f-eef7-a64e4c6d7b28'
|
||||
'OpaqueRef:e0eb5eb1-a485-fcfc-071e-fafa17f9ac48'
|
||||
'OpaqueRef:c4aa5d87-4115-c359-9cdf-c16fbf56cf2c'
|
||||
'OpaqueRef:b06a9d3f-5132-e58f-25c4-ef94d5b38986'
|
||||
'OpaqueRef:a4dd8a73-5393-81ce-abce-fc1502490a6d'
|
||||
'OpaqueRef:83331526-8bd8-9644-0a7d-9f645f5fcd70'
|
||||
'OpaqueRef:693bef17-aa19-63f8-3775-7d3b2dbce9d6'
|
||||
'OpaqueRef:67618138-57df-e90a-74c6-402ad62d657b'
|
||||
'OpaqueRef:5f1d5117-1033-b12a-92a8-99f206c9dbba'
|
||||
'OpaqueRef:287084c1-241a-58df-929a-cbe2e7454a56'
|
||||
'OpaqueRef:1f7f9828-f4e7-41dd-20e6-3bf57c559a78'
|
||||
]
|
||||
|
||||
$expect(sr.messages).to.have.members [
|
||||
# No data for this test.
|
||||
]
|
||||
|
||||
it 'PBD', ->
|
||||
pbd = collection.get 'OpaqueRef:ff32de74-138c-9d80-ab58-c631d2aa0e71'
|
||||
|
||||
#console.log pbd
|
||||
|
||||
$expect(pbd).to.an 'object'
|
||||
|
||||
$expect(pbd.type).to.equal 'PBD'
|
||||
|
||||
$expect(pbd.attached).to.be.true
|
||||
|
||||
$expect(pbd.host).to.equal 'OpaqueRef:bbc98f5e-1a17-2030-28af-0df2393f3145'
|
||||
|
||||
$expect(pbd.SR).to.equal 'OpaqueRef:d6fe49bf-dd48-c929-5aab-b2786a2e7aee'
|
||||
|
||||
it 'PIF', ->
|
||||
pif = collection.get 'OpaqueRef:aef57ed4-e4d9-7f72-0376-b781a19bb9d2'
|
||||
|
||||
#console.log pif
|
||||
|
||||
$expect(pif).to.an 'object'
|
||||
|
||||
$expect(pif.type).to.equal 'PIF'
|
||||
|
||||
$expect(pif.attached).to.be.true
|
||||
|
||||
$expect(pif.device).to.equal 'eth0'
|
||||
|
||||
$expect(pif.IP).to.equal '192.168.1.1'
|
||||
|
||||
$expect(pif.$host).to.equal 'OpaqueRef:bbc98f5e-1a17-2030-28af-0df2393f3145'
|
||||
|
||||
$expect(pif.MAC).to.equal '90:2b:34:d3:ce:75'
|
||||
|
||||
$expect(pif.management).to.be.true
|
||||
|
||||
$expect(pif.mode).to.equal 'Static'
|
||||
|
||||
$expect(pif.MTU).to.equal 1500
|
||||
|
||||
$expect(pif.netmask).to.equal '255.255.255.0'
|
||||
|
||||
$expect(pif.$network).to.equal 'OpaqueRef:dbc93777-f2c0-e888-967d-dd9beeffb3c0'
|
||||
|
||||
it 'VDI', ->
|
||||
vdi = collection.get 'OpaqueRef:1f7f9828-f4e7-41dd-20e6-3bf57c559a78'
|
||||
|
||||
#console.log vdi
|
||||
|
||||
$expect(vdi).to.an 'object'
|
||||
|
||||
$expect(vdi.type).to.equal 'VDI'
|
||||
|
||||
$expect(vdi.name_label).to.equal 'ceph'
|
||||
|
||||
$expect(vdi.name_description).to.equal ''
|
||||
|
||||
$expect(vdi.usage).to.equal 21525168128
|
||||
|
||||
$expect(vdi.size).to.equal 21474836480
|
||||
|
||||
$expect(vdi.$snapshot_of).to.equal null
|
||||
|
||||
$expect(vdi.snapshots).to.have.members [
|
||||
'OpaqueRef:b4a1573f-c235-8acd-4625-dfbcb2beb523'
|
||||
]
|
||||
|
||||
$expect(vdi.$SR).to.equal 'OpaqueRef:d6fe49bf-dd48-c929-5aab-b2786a2e7aee'
|
||||
|
||||
$expect(vdi.$VBDs).to.have.members [
|
||||
'OpaqueRef:9f15200b-3cac-7a61-b3e8-dd2fc0a5572d'
|
||||
]
|
||||
|
||||
it 'VBD', ->
|
||||
vbd = collection.get 'OpaqueRef:9f15200b-3cac-7a61-b3e8-dd2fc0a5572d'
|
||||
|
||||
#console.log vbd
|
||||
|
||||
$expect(vbd).to.an 'object'
|
||||
|
||||
$expect(vbd.type).to.equal 'VBD'
|
||||
|
||||
$expect(vbd.attached).to.be.true
|
||||
|
||||
$expect(vbd.bootable).to.be.false
|
||||
|
||||
$expect(vbd.is_cd_drive).to.be.false
|
||||
|
||||
$expect(vbd.read_only).to.be.false
|
||||
|
||||
$expect(vbd.VDI).to.equal 'OpaqueRef:1f7f9828-f4e7-41dd-20e6-3bf57c559a78'
|
||||
|
||||
$expect(vbd.VM).to.equal 'OpaqueRef:be2390b2-cd08-53f5-3fae-b76f6f3725bf'
|
||||
|
||||
it 'VIF', ->
|
||||
vif = collection.get 'OpaqueRef:20349ad5-0a0d-4b80-dcc0-0037fa647182'
|
||||
|
||||
#console.log vif
|
||||
|
||||
$expect(vif).to.an 'object'
|
||||
|
||||
$expect(vif.type).to.equal 'VIF'
|
||||
|
||||
$expect(vif.attached).to.be.true
|
||||
|
||||
$expect(vif.device).to.equal '0'
|
||||
|
||||
$expect(vif.MAC).to.equal 'ce:20:2b:38:7f:fd'
|
||||
|
||||
$expect(vif.MTU).to.equal 1500
|
||||
|
||||
$expect(vif.$network).to.equal 'OpaqueRef:dbc93777-f2c0-e888-967d-dd9beeffb3c0'
|
||||
|
||||
$expect(vif.$VM).to.equal 'OpaqueRef:fdaba312-c3a5-0190-b1a1-bf389567e620'
|
||||
|
||||
it 'network', ->
|
||||
network = collection.get 'OpaqueRef:dbc93777-f2c0-e888-967d-dd9beeffb3c0'
|
||||
|
||||
#console.log network
|
||||
|
||||
$expect(network).to.be.an 'object'
|
||||
|
||||
$expect(network.type).to.equal 'network'
|
||||
|
||||
$expect(network.name_label).to.equal 'Pool-wide network associated with eth0'
|
||||
|
||||
$expect(network.name_description).to.equal ''
|
||||
|
||||
$expect(network.bridge).to.equal 'xenbr0'
|
||||
|
||||
$expect(network.MTU).to.equal 1500
|
||||
|
||||
$expect(network.PIFs).to.have.members [
|
||||
'OpaqueRef:aef57ed4-e4d9-7f72-0376-b781a19bb9d2'
|
||||
'OpaqueRef:971d6bc5-60f4-a331-bdee-444ee7cbf678'
|
||||
]
|
||||
|
||||
$expect(network.VIFs).to.have.members [
|
||||
'OpaqueRef:fc86d17e-d9d1-5534-69d6-d15edbe36d22'
|
||||
'OpaqueRef:ed2d89ca-1f4e-09ff-f80e-991d6b01de45'
|
||||
'OpaqueRef:c6651d03-cefe-accf-920b-636e32fee23c'
|
||||
'OpaqueRef:c5977d9b-cb50-a615-8488-1dd105d69802'
|
||||
'OpaqueRef:c391575b-168f-e52b-59f7-9f852a2c6854'
|
||||
'OpaqueRef:bf4da755-480b-e3fd-2bfe-f53e7204c8ae'
|
||||
'OpaqueRef:ba41d1a6-724e-aae8-3447-20f74014eb75'
|
||||
'OpaqueRef:b8df4453-542e-6c14-0eb1-174d48373bca'
|
||||
'OpaqueRef:b5980de3-1a74-9f57-1e98-2a74184211dc'
|
||||
'OpaqueRef:aaae3669-faee-4338-3156-0ce8c06c75cf'
|
||||
'OpaqueRef:aa874254-b67c-e9e3-6a08-1c770c2dd8ac'
|
||||
'OpaqueRef:7b8ecb18-5bc5-7650-3ac4-6bc22322e8ba'
|
||||
'OpaqueRef:59b884b0-521f-7b3e-6a91-319ded893e68'
|
||||
'OpaqueRef:20349ad5-0a0d-4b80-dcc0-0037fa647182'
|
||||
]
|
||||
|
||||
it 'message', ->
|
||||
message = collection.get 'OpaqueRef:cb515b9a-ef8c-13d4-88ea-e0d3ee88d22a'
|
||||
|
||||
#console.log message
|
||||
|
||||
$expect(message.type).to.equal 'message'
|
||||
|
||||
$expect(message.time).to.equal 1389449056
|
||||
|
||||
$expect(message.$object).to.equal 'OpaqueRef:bbc98f5e-1a17-2030-28af-0df2393f3145'
|
||||
|
||||
$expect(message.name).to.equal 'PBD_PLUG_FAILED_ON_SERVER_START'
|
||||
|
||||
$expect(message.body).to.equal ''
|
||||
|
||||
it 'task', ->
|
||||
all = collection.get()
|
||||
|
||||
for object in all
|
||||
if object.type is 'task'
|
||||
console.log object
|
||||
|
||||
# FIXME: we need to update the tests data to complete this test.
|
||||
@@ -1,73 +0,0 @@
|
||||
$done = {}
|
||||
|
||||
# Similar to `$_.each()` but can be interrupted by returning the
|
||||
# special value `done` provided as the forth argument.
|
||||
exports.$each = (col, iterator, ctx) ->
|
||||
# The default context is inherited.
|
||||
ctx ?= this
|
||||
|
||||
if (n = col.length)?
|
||||
# Array-like object.
|
||||
i = 0
|
||||
while i < n and (iterator.call ctx, col[i], "#{i}", col, $done) isnt $done
|
||||
++i
|
||||
else
|
||||
for key of col
|
||||
break if (iterator.call ctx, col[key], key, $done) is $done
|
||||
|
||||
# For performance.
|
||||
undefined
|
||||
|
||||
exports.$makeFunction = (val) -> -> val
|
||||
|
||||
# Similar to `$_.map()` for array and `$_.mapValues()` for objects.
|
||||
#
|
||||
# Note: can be interrupted by returning the special value `done`
|
||||
# provided as the forth argument.
|
||||
exports.$map = (col, iterator, ctx) ->
|
||||
# The default context is inherited.
|
||||
ctx ?= this
|
||||
|
||||
if (n = col.length)?
|
||||
result = []
|
||||
# Array-like object.
|
||||
i = 0
|
||||
while i < n
|
||||
value = iterator.call ctx, col[i], "#{i}", col, $done
|
||||
break if value is $done
|
||||
result.push value
|
||||
++i
|
||||
else
|
||||
result = {}
|
||||
for key of col
|
||||
value = iterator.call ctx, col[key], key, $done
|
||||
break if value is $done
|
||||
result.push value
|
||||
|
||||
# The new collection is returned.
|
||||
result
|
||||
|
||||
# Similar to `$map()` but change the current collection.
|
||||
#
|
||||
# Note: can be interrupted by returning the special value `done`
|
||||
# provided as the forth argument.
|
||||
exports.$mapInPlace = (col, iterator, ctx) ->
|
||||
# The default context is inherited.
|
||||
ctx ?= this
|
||||
|
||||
if (n = col.length)?
|
||||
# Array-like object.
|
||||
i = 0
|
||||
while i < n
|
||||
value = iterator.call ctx, col[i], "#{i}", col, $done
|
||||
break if value is $done
|
||||
col[i] = value
|
||||
++i
|
||||
else
|
||||
for key of col
|
||||
value = iterator.call ctx, col[key], key, $done
|
||||
break if value is $done
|
||||
col[key] = value
|
||||
|
||||
# The collection is returned.
|
||||
col
|
||||
163
src/utils.js
Normal file
163
src/utils.js
Normal file
@@ -0,0 +1,163 @@
|
||||
import base64url from 'base64url'
|
||||
import forEach from 'lodash.foreach'
|
||||
import has from 'lodash.has'
|
||||
import humanFormat from 'human-format'
|
||||
import isArray from 'lodash.isarray'
|
||||
import multiKeyHashInt from 'multikey-hash'
|
||||
import xml2js from 'xml2js'
|
||||
import {promisify, method} from 'bluebird'
|
||||
import {randomBytes} from 'crypto'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
// Ensure the value is an array, wrap it if necessary.
|
||||
export const ensureArray = (value) => {
|
||||
if (value === undefined) {
|
||||
return []
|
||||
}
|
||||
|
||||
return isArray(value) ? value : [value]
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
// Returns the value of a property and removes it from the object.
|
||||
export function extractProperty (obj, prop) {
|
||||
const value = obj[prop]
|
||||
delete obj[prop]
|
||||
return value
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
// Generate a secure random Base64 string.
|
||||
export const generateToken = (function (randomBytes) {
|
||||
return (n = 32) => randomBytes(n).then(base64url)
|
||||
})(promisify(randomBytes))
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export const formatXml = (function () {
|
||||
const builder = new xml2js.Builder({
|
||||
xmldec: {
|
||||
// Do not include an XML header.
|
||||
//
|
||||
// This is not how this setting should be set but due to the
|
||||
// implementation of both xml2js and xmlbuilder-js it works.
|
||||
//
|
||||
// TODO: Find a better alternative.
|
||||
headless: true
|
||||
}
|
||||
})
|
||||
|
||||
return (...args) => builder.buildObject(...args)
|
||||
})()
|
||||
|
||||
export const parseXml = (function () {
|
||||
const opts = {
|
||||
mergeAttrs: true,
|
||||
explicitArray: false
|
||||
}
|
||||
|
||||
return (xml) => {
|
||||
let result
|
||||
|
||||
// xml2js.parseString() use a callback for synchronous code.
|
||||
xml2js.parseString(xml, opts, (error, result_) => {
|
||||
if (error) {
|
||||
throw error
|
||||
}
|
||||
|
||||
result = result_
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
})()
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
// This function does nothing and returns undefined.
|
||||
//
|
||||
// It is often used to swallow promise's errors.
|
||||
export function noop () {}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
// Ponyfill for Promise.finally(cb)
|
||||
export const pFinally = (promise, cb) => {
|
||||
return promise.then(
|
||||
(value) => constructor.resolve(cb()).then(() => value),
|
||||
(reason) => constructor.resolve(cb()).then(() => {
|
||||
throw reason
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export function parseSize (size) {
|
||||
let bytes = humanFormat.parse.raw(size, { scale: 'binary' })
|
||||
if (bytes.unit && bytes.unit !== 'B') {
|
||||
bytes = humanFormat.parse.raw(size)
|
||||
|
||||
if (bytes.unit && bytes.unit !== 'B') {
|
||||
throw new Error('invalid size: ' + size)
|
||||
}
|
||||
}
|
||||
return Math.floor(bytes.value * bytes.factor)
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
// Special value which can be returned to stop an iteration in map()
|
||||
// and mapInPlace().
|
||||
export const done = {}
|
||||
|
||||
// Similar to `lodash.map()` for array and `lodash.mapValues()` for
|
||||
// objects.
|
||||
//
|
||||
// Note: can be interrupted by returning the special value `done`
|
||||
// provided as the forth argument.
|
||||
export function map (col, iterator, thisArg = this) {
|
||||
const result = has(col, 'length') ? [] : {}
|
||||
forEach(col, (item, i) => {
|
||||
const value = iterator.call(thisArg, item, i, done)
|
||||
if (value === done) {
|
||||
return false
|
||||
}
|
||||
|
||||
result[i] = value
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
// Create a hash from multiple values.
|
||||
export const multiKeyHash = method((...args) => {
|
||||
const hash = multiKeyHashInt(...args)
|
||||
|
||||
const buf = new Buffer(4)
|
||||
buf.writeUInt32LE(hash, 0)
|
||||
|
||||
return base64url(buf)
|
||||
})
|
||||
|
||||
// Similar to `map()` but change the current collection.
|
||||
//
|
||||
// Note: can be interrupted by returning the special value `done`
|
||||
// provided as the forth argument.
|
||||
export function mapInPlace (col, iterator, thisArg = this) {
|
||||
forEach(col, (item, i) => {
|
||||
const value = iterator.call(thisArg, item, i, done)
|
||||
if (value === done) {
|
||||
return false
|
||||
}
|
||||
|
||||
col[i] = value
|
||||
})
|
||||
|
||||
return col
|
||||
}
|
||||
|
||||
// Wrap a value in a function.
|
||||
export const wrap = (value) => () => value
|
||||
49
src/utils.spec.js
Normal file
49
src/utils.spec.js
Normal file
@@ -0,0 +1,49 @@
|
||||
/* eslint-env mocha */
|
||||
|
||||
import {expect} from 'chai'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
import {
|
||||
ensureArray,
|
||||
extractProperty
|
||||
} from './utils'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
describe('ensureArray', function () {
|
||||
it('returns an empty array for undefined', function () {
|
||||
expect(ensureArray(undefined)).to.eql([])
|
||||
})
|
||||
|
||||
it('returns the object itself if is already an array', function () {
|
||||
const array = ['foo', 'bar', 'baz']
|
||||
|
||||
expect(ensureArray(array)).to.equal(array)
|
||||
})
|
||||
|
||||
it('wrap the value in an object', function () {
|
||||
const value = {}
|
||||
|
||||
expect(ensureArray(value)).to.includes(value)
|
||||
})
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
describe('extractProperty', function () {
|
||||
it('returns the value of the property', function () {
|
||||
const value = {}
|
||||
const obj = { prop: value }
|
||||
|
||||
expect(extractProperty(obj, 'prop')).to.equal(value)
|
||||
})
|
||||
|
||||
it('removes the property from the object', function () {
|
||||
const value = {}
|
||||
const obj = { prop: value }
|
||||
|
||||
expect(extractProperty(obj, 'prop')).to.equal(value)
|
||||
expect(obj).to.not.have.property('prop')
|
||||
})
|
||||
})
|
||||
52
src/ws-proxy.js
Normal file
52
src/ws-proxy.js
Normal file
@@ -0,0 +1,52 @@
|
||||
import assign from 'lodash.assign'
|
||||
import createDebug from 'debug'
|
||||
import WebSocket from 'ws'
|
||||
|
||||
const debug = createDebug('xo:wsProxy')
|
||||
|
||||
const defaults = {
|
||||
// Automatically close the client connection when the remote close.
|
||||
autoClose: true
|
||||
}
|
||||
|
||||
// Proxy a WebSocket `client` to a remote server which has `url` as
|
||||
// address.
|
||||
export default function wsProxy (client, url, opts) {
|
||||
opts = assign({}, defaults, {
|
||||
protocol: client.protocol
|
||||
}, opts)
|
||||
const autoClose = !!opts.autoClose
|
||||
delete opts.autoClose
|
||||
|
||||
function onClientSend (error) {
|
||||
if (error) {
|
||||
debug('client send error', error)
|
||||
}
|
||||
}
|
||||
function onRemoteSend (error) {
|
||||
if (error) {
|
||||
debug('remote send error', error)
|
||||
}
|
||||
}
|
||||
|
||||
const remote = new WebSocket(url, opts).once('open', function () {
|
||||
debug('connected to %s', url)
|
||||
}).once('close', function () {
|
||||
debug('remote closed')
|
||||
|
||||
if (autoClose) {
|
||||
client.close()
|
||||
}
|
||||
}).once('error', function (error) {
|
||||
debug('remote error: %s', error)
|
||||
}).on('message', function (message) {
|
||||
client.send(message, onClientSend)
|
||||
})
|
||||
|
||||
client.once('close', function () {
|
||||
debug('client closed')
|
||||
remote.close()
|
||||
}).on('message', function (message) {
|
||||
remote.send(message, onRemoteSend)
|
||||
})
|
||||
}
|
||||
491
src/xapi-objects-to-xo.js
Normal file
491
src/xapi-objects-to-xo.js
Normal file
@@ -0,0 +1,491 @@
|
||||
import forEach from 'lodash.foreach'
|
||||
import isArray from 'lodash.isarray'
|
||||
import map from 'lodash.map'
|
||||
|
||||
import {
|
||||
ensureArray,
|
||||
extractProperty,
|
||||
parseXml
|
||||
} from './utils'
|
||||
import {
|
||||
isHostRunning,
|
||||
isVmRunning
|
||||
} from './xapi'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
function link (obj, prop) {
|
||||
const dynamicValue = obj[`$${prop}`]
|
||||
if (dynamicValue == null) {
|
||||
return dynamicValue // Properly handles null and undefined.
|
||||
}
|
||||
|
||||
if (isArray(dynamicValue)) {
|
||||
return map(dynamicValue, '$id')
|
||||
}
|
||||
|
||||
return dynamicValue.$id
|
||||
}
|
||||
|
||||
function toTimestamp (date) {
|
||||
// Weird behavior from the XAPI.
|
||||
if (!date || date === '1969-12-31T23:00:00.000Z') {
|
||||
return null
|
||||
}
|
||||
|
||||
return Math.round(Date.parse(date) / 1000)
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export function pool (obj) {
|
||||
return {
|
||||
default_SR: link(obj, 'default_SR'),
|
||||
HA_enabled: obj.ha_enabled,
|
||||
master: link(obj, 'master'),
|
||||
name_description: obj.name_description,
|
||||
name_label: obj.name_label
|
||||
|
||||
// TODO
|
||||
// - ? networks = networksByPool.items[pool.id] (network.$pool.id)
|
||||
// - hosts = hostsByPool.items[pool.id] (host.$pool.$id)
|
||||
// - patches = poolPatchesByPool.items[pool.id] (poolPatch.$pool.id)
|
||||
// - SRs = srsByContainer.items[pool.id] (sr.$container.id)
|
||||
// - templates = vmTemplatesByContainer.items[pool.id] (vmTemplate.$container.$id)
|
||||
// - VMs = vmsByContainer.items[pool.id] (vm.$container.id)
|
||||
// - $running_hosts = runningHostsByPool.items[pool.id] (runningHost.$pool.id)
|
||||
// - $running_VMs = runningVmsByPool.items[pool.id] (runningHost.$pool.id)
|
||||
// - $VMs = vmsByPool.items[pool.id] (vm.$pool.id)
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export function host (obj) {
|
||||
const {
|
||||
$metrics: metrics,
|
||||
other_config: otherConfig
|
||||
} = obj
|
||||
|
||||
const isRunning = isHostRunning(obj)
|
||||
|
||||
return {
|
||||
address: obj.address,
|
||||
bios_strings: obj.bios_strings,
|
||||
build: obj.software_version.build_number,
|
||||
CPUs: obj.cpu_info,
|
||||
enabled: obj.enabled,
|
||||
current_operations: obj.current_operations,
|
||||
hostname: obj.hostname,
|
||||
iSCSI_name: otherConfig.iscsi_iqn || null,
|
||||
name_description: obj.name_description,
|
||||
name_label: obj.name_label,
|
||||
memory: (function () {
|
||||
if (metrics) {
|
||||
const free = +metrics.memory_free
|
||||
const total = +metrics.memory_total
|
||||
|
||||
return {
|
||||
usage: total - free,
|
||||
size: total
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
usage: 0,
|
||||
total: 0
|
||||
}
|
||||
})(),
|
||||
patches: link(obj, 'patches'),
|
||||
power_state: isRunning ? 'Running' : 'Halted',
|
||||
version: obj.software_version.product_version,
|
||||
|
||||
// TODO: dedupe.
|
||||
PIFs: link(obj, 'PIFs'),
|
||||
$PIFs: link(obj, 'PIFs'),
|
||||
PCIs: link(obj, 'PCIs'),
|
||||
$PCIs: link(obj, 'PCIs'),
|
||||
PGPUs: link(obj, 'PGPUs'),
|
||||
$PGPUs: link(obj, 'PGPUs'),
|
||||
|
||||
$PBDs: link(obj, 'PBDs')
|
||||
|
||||
// TODO:
|
||||
// - controller = vmControllersByContainer.items[host.id]
|
||||
// - SRs = srsByContainer.items[host.id]
|
||||
// - tasks = tasksByHost.items[host.id]
|
||||
// - templates = vmTemplatesByContainer.items[host.id]
|
||||
// - VMs = vmsByContainer.items[host.id]
|
||||
// - $vCPUs = sum(host.VMs, vm => host.CPUs.number)
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export function vm (obj) {
|
||||
const {
|
||||
$guest_metrics: guestMetrics,
|
||||
$metrics: metrics,
|
||||
other_config: otherConfig
|
||||
} = obj
|
||||
|
||||
const isRunning = isVmRunning(obj)
|
||||
|
||||
const vm = {
|
||||
// type is redefined after for controllers/, templates &
|
||||
// snapshots.
|
||||
type: 'VM',
|
||||
|
||||
addresses: guestMetrics && guestMetrics.networks || null,
|
||||
auto_poweron: Boolean(otherConfig.auto_poweron),
|
||||
boot: obj.HVM_boot_params,
|
||||
CPUs: {
|
||||
max: +obj.VCPUs_max,
|
||||
number: (
|
||||
isRunning && metrics ?
|
||||
+metrics.VCPUs_number :
|
||||
+obj.VCPUs_at_startup
|
||||
)
|
||||
},
|
||||
current_operations: obj.current_operations,
|
||||
docker: (function () {
|
||||
const monitor = otherConfig['xscontainer-monitor']
|
||||
if (!monitor) {
|
||||
return
|
||||
}
|
||||
|
||||
if (monitor === 'False') {
|
||||
return {
|
||||
enabled: false
|
||||
}
|
||||
}
|
||||
|
||||
const {
|
||||
docker_ps: process,
|
||||
docker_info: info,
|
||||
docker_version: version
|
||||
} = otherConfig
|
||||
|
||||
return {
|
||||
enabled: true,
|
||||
info: info && parseXml(info).docker_info,
|
||||
process: process && parseXml(process).docker_ps,
|
||||
version: version && parseXml(version).docker_version
|
||||
}
|
||||
})(),
|
||||
|
||||
// TODO: there is two possible value: "best-effort" and "restart"
|
||||
high_availability: Boolean(obj.ha_restart_priority),
|
||||
|
||||
memory: (function () {
|
||||
const dynamicMin = +obj.memory_dynamic_min
|
||||
const dynamicMax = +obj.memory_dynamic_max
|
||||
const staticMin = +obj.memory_static_min
|
||||
const staticMax = +obj.memory_static_max
|
||||
|
||||
const memory = {
|
||||
dynamic: [ dynamicMin, dynamicMax ],
|
||||
static: [ staticMin, staticMax ]
|
||||
}
|
||||
|
||||
const gmMemory = guestMetrics && guestMetrics.memory
|
||||
|
||||
if (!isRunning) {
|
||||
memory.size = dynamicMax
|
||||
} else if (gmMemory && gmMemory.used) {
|
||||
memory.usage = +gmMemory.used
|
||||
memory.size = +gmMemory.total
|
||||
} else if (metrics) {
|
||||
memory.size = +metrics.memory_actual
|
||||
} else {
|
||||
memory.size = dynamicMax
|
||||
}
|
||||
|
||||
return memory
|
||||
})(),
|
||||
name_description: obj.name_description,
|
||||
name_label: obj.name_label,
|
||||
other: otherConfig,
|
||||
os_version: guestMetrics && guestMetrics.os_version || null,
|
||||
power_state: obj.power_state,
|
||||
PV_drivers: Boolean(guestMetrics && guestMetrics.PV_drivers_up_to_date),
|
||||
snapshot_time: toTimestamp(obj.snapshot_time),
|
||||
snapshots: link(obj, 'snapshots'),
|
||||
VIFs: link(obj, 'VIFs'),
|
||||
|
||||
$container: (
|
||||
isRunning ?
|
||||
link(obj, 'resident_on') :
|
||||
link(obj, 'pool') // TODO: handle local VMs (`VM.get_possible_hosts()`).
|
||||
),
|
||||
$VBDs: link(obj, 'VBDs'),
|
||||
|
||||
// TODO: dedupe
|
||||
VGPUs: link(obj, 'VGPUs'),
|
||||
$VGPUs: link(obj, 'VGPUs')
|
||||
}
|
||||
|
||||
if (obj.is_control_domain) {
|
||||
vm.type += '-controller'
|
||||
} else if (obj.is_a_snapshot) {
|
||||
vm.type += '-snapshot'
|
||||
|
||||
vm.$snapshot_of = link(obj, 'snapshot_of')
|
||||
} else if (obj.is_a_template) {
|
||||
vm.type += '-template'
|
||||
|
||||
vm.CPUs.number = +obj.VCPUs_at_startup
|
||||
vm.template_info = {
|
||||
arch: otherConfig['install-arch'],
|
||||
disks: (function () {
|
||||
const {disks: xml} = otherConfig
|
||||
if (!xml) {
|
||||
return []
|
||||
}
|
||||
|
||||
const disks = ensureArray(parseXml(xml).provision.disk)
|
||||
forEach(disks, function normalize (disk) {
|
||||
disk.bootable = disk.bootable === 'true'
|
||||
disk.size = +disk.size
|
||||
disk.SR = extractProperty(disk, 'sr')
|
||||
})
|
||||
|
||||
return disks
|
||||
})(),
|
||||
install_methods: (function () {
|
||||
const {['install-methods']: methods} = otherConfig
|
||||
|
||||
return methods ? methods.split(',') : []
|
||||
})()
|
||||
}
|
||||
}
|
||||
|
||||
return vm
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export function sr (obj) {
|
||||
return {
|
||||
type: 'SR',
|
||||
|
||||
content_type: obj.content_type,
|
||||
name_description: obj.name_description,
|
||||
name_label: obj.name_label,
|
||||
physical_usage: +obj.physical_utilisation,
|
||||
size: +obj.physical_size,
|
||||
SR_type: obj.type,
|
||||
usage: +obj.virtual_allocation,
|
||||
VDIs: link(obj, 'VDIs'),
|
||||
|
||||
$container: (
|
||||
obj.shared ?
|
||||
link(obj, 'pool') :
|
||||
obj.$PBDs[0] && link(obj.$PBDs[0], 'host')
|
||||
),
|
||||
$PBDs: link(obj, 'PBDs')
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export function pbd (obj) {
|
||||
return {
|
||||
type: 'PBD',
|
||||
|
||||
attached: obj.currently_attached,
|
||||
host: link(obj, 'host'),
|
||||
SR: link(obj, 'SR')
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export function pif (obj) {
|
||||
return {
|
||||
type: 'PIF',
|
||||
|
||||
attached: obj.currently_attached,
|
||||
device: obj.device,
|
||||
IP: obj.IP,
|
||||
MAC: obj.MAC,
|
||||
management: obj.management, // TODO: find a better name.
|
||||
mode: obj.ip_configuration_mode,
|
||||
MTU: +obj.MTU,
|
||||
netmask: obj.netmask,
|
||||
vlan: +obj.VLAN,
|
||||
|
||||
// TODO: What is it?
|
||||
//
|
||||
// Could it mean “is this a physical interface?”.
|
||||
// How could a PIF not be physical?
|
||||
// physical: obj.physical,
|
||||
|
||||
$host: link(obj, 'host'),
|
||||
$network: link(obj, 'network')
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
// TODO: should we have a VDI-snapshot type like we have with VMs?
|
||||
export function vdi (obj) {
|
||||
return {
|
||||
type: 'VDI',
|
||||
|
||||
name_description: obj.name_description,
|
||||
name_label: obj.name_label,
|
||||
size: +obj.virtual_size,
|
||||
snapshots: link(obj, 'snapshots'),
|
||||
snapshot_time: toTimestamp(obj.snapshot_time),
|
||||
usage: +obj.physical_utilisation,
|
||||
|
||||
$snapshot_of: link(obj, 'snapshot_of'),
|
||||
$SR: link(obj, 'SR'),
|
||||
$VBDs: link(obj, 'VBDs')
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export function vbd (obj) {
|
||||
return {
|
||||
type: 'VBD',
|
||||
|
||||
attached: obj.currently_attached,
|
||||
bootable: obj.bootable,
|
||||
is_cd_drive: obj.type === 'CD',
|
||||
position: obj.userdevice,
|
||||
read_only: obj.mode === 'RO',
|
||||
VDI: link(obj, 'VDI'),
|
||||
VM: link(obj, 'VM')
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export function vif (obj) {
|
||||
return {
|
||||
type: 'VIF',
|
||||
|
||||
attached: obj.currently_attached,
|
||||
device: obj.device, // TODO: should it be cast to a number?
|
||||
MAC: obj.MAC,
|
||||
MTU: +obj.MTU,
|
||||
|
||||
$network: link(obj, 'network'),
|
||||
$VM: link(obj, 'VM')
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export function network (obj) {
|
||||
return {
|
||||
bridge: obj.bridge,
|
||||
MTU: +obj.MTU,
|
||||
name_description: obj.name_description,
|
||||
name_label: obj.name_label,
|
||||
PIFs: link(obj, 'PIFs'),
|
||||
VIFs: link(obj, 'VIFs')
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export function message (obj) {
|
||||
return {
|
||||
body: obj.body,
|
||||
name: obj.name,
|
||||
time: toTimestamp(obj.timestamp),
|
||||
|
||||
$object: obj.obj_uuid // Special link as it is already an UUID.
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export function task (obj) {
|
||||
return {
|
||||
created: toTimestamp(obj.created),
|
||||
current_operations: obj.current_operations,
|
||||
finished: toTimestamp(obj.finished),
|
||||
name_description: obj.name_description,
|
||||
name_label: obj.name_label,
|
||||
progress: +obj.progress,
|
||||
result: obj.result,
|
||||
status: obj.status,
|
||||
|
||||
$host: link(obj, 'resident_on')
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export function host_patch (obj) {
|
||||
return {
|
||||
applied: obj.applied,
|
||||
time: toTimestamp(obj.timestamp_applied),
|
||||
pool_patch: link(obj, 'pool_patch'),
|
||||
|
||||
$host: link(obj, 'host')
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export function pool_patch (obj) {
|
||||
return {
|
||||
applied: obj.pool_applied,
|
||||
name_description: obj.name_description,
|
||||
name_label: obj.name_label,
|
||||
size: +obj.size,
|
||||
version: obj.version,
|
||||
|
||||
// TODO: host.[$]pool_patches ←→ pool.[$]host_patches
|
||||
$host_patches: link(obj, 'host_patches')
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export function pci (obj) {
|
||||
return {
|
||||
type: 'PCI',
|
||||
|
||||
class_name: obj.class_name,
|
||||
device_name: obj.device_name,
|
||||
pci_id: obj.pci_id,
|
||||
|
||||
$host: link(obj, 'host')
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export function pgpu (obj) {
|
||||
return {
|
||||
type: 'PGPU',
|
||||
|
||||
pci: link(obj, 'PCI'),
|
||||
|
||||
// TODO: dedupe.
|
||||
host: link(obj, 'host'),
|
||||
$host: link(obj, 'host'),
|
||||
vgpus: link(obj, 'resident_VGPUs'),
|
||||
$vgpus: link(obj, 'resident_VGPUs')
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export function vgpu (obj) {
|
||||
return {
|
||||
type: 'VGPU',
|
||||
|
||||
currentlyAttached: obj.currently_attached,
|
||||
device: obj.device,
|
||||
resident_on: link(obj, 'resident_on'),
|
||||
vm: link(obj, 'VM')
|
||||
}
|
||||
}
|
||||
117
src/xapi.coffee
117
src/xapi.coffee
@@ -1,117 +0,0 @@
|
||||
# URL parsing.
|
||||
{parse: $parseUrl} = require 'url'
|
||||
|
||||
#---------------------------------------------------------------------
|
||||
|
||||
$xmlrpc = require 'xmlrpc'
|
||||
|
||||
#---------------------------------------------------------------------
|
||||
|
||||
# Helpers for dealing with fibers.
|
||||
{$wait} = require './fibers-utils'
|
||||
|
||||
#=====================================================================
|
||||
|
||||
$sleep = (delay) ->
|
||||
(cb) -> setTimeout cb, delay
|
||||
|
||||
#=====================================================================
|
||||
|
||||
# Note: All methods are synchronous (using fibers).
|
||||
class $XAPI
|
||||
# Number of tries when the connection fails (TCP or XAPI).
|
||||
tries: 10
|
||||
|
||||
constructor: ({@host, @username, @password}) ->
|
||||
@connect()
|
||||
|
||||
connect: (force = false) ->
|
||||
{hostname, port} = $parseUrl "http://#{@host}"
|
||||
|
||||
# Returns nothing if already connected to this host and not force.
|
||||
if !force and (hostname is @xmlrpc?.options.host)
|
||||
return
|
||||
|
||||
# Makes sure there is not session id left.
|
||||
delete @sessionId
|
||||
|
||||
@xmlrpc = $xmlrpc.createSecureClient {
|
||||
host: hostname
|
||||
port: port ? 443
|
||||
rejectUnauthorized: false
|
||||
}
|
||||
|
||||
# Logs in.
|
||||
@logIn()
|
||||
|
||||
call: (method, args...) ->
|
||||
@connect() unless @xmlrpc
|
||||
|
||||
args.unshift @sessionId if @sessionId
|
||||
|
||||
tries = @tries
|
||||
do helper = =>
|
||||
try
|
||||
result = $wait (callback) =>
|
||||
@xmlrpc.methodCall method, args, callback
|
||||
|
||||
# Returns the plain result if it does not have a valid XAPI format.
|
||||
return result unless result.Status?
|
||||
|
||||
# Returns the result's value if all went well.
|
||||
return result.Value if result.Status is 'Success'
|
||||
|
||||
# Something went wrong.
|
||||
error = result.ErrorDescription or value
|
||||
catch error # Captures the error if it was thrown.
|
||||
|
||||
# If it failed too much times, just stops.
|
||||
throw error unless --tries
|
||||
|
||||
# Gets the error code for transport errors and XAPI errors.
|
||||
code = error.code or error[0]
|
||||
|
||||
switch code
|
||||
|
||||
# XAPI sometimes close the connection when the server is no
|
||||
# longer pool master (`event.next`), so we have to retry at
|
||||
# least once to know who is the new pool master.
|
||||
when 'ECONNRESET', \
|
||||
'ECONNREFUSED', \
|
||||
'EHOSTUNREACH', \
|
||||
'HOST_STILL_BOOTING', \
|
||||
'HOST_HAS_NO_MANAGEMENT_IP'
|
||||
# Node.js seems to reuse the broken socket, so we add a small
|
||||
# delay.
|
||||
#
|
||||
# TODO Magic number!!!
|
||||
#
|
||||
# I would like to be able to use a shorter delay but for
|
||||
# some reason, when we connect to XAPI at a given moment,
|
||||
# the connection hangs.
|
||||
$sleep 500
|
||||
helper()
|
||||
|
||||
# XAPI is sometimes reinitialized and sessions are lost.
|
||||
# We try log in again if necessary.
|
||||
when 'SESSION_INVALID'
|
||||
@logIn()
|
||||
helper()
|
||||
|
||||
# If the current host is a slave, changes the current host,
|
||||
# reconnect and retry.
|
||||
when 'HOST_IS_SLAVE'
|
||||
@host = error[1]
|
||||
@connect()
|
||||
helper()
|
||||
|
||||
# This error has not been handled, just forwards it.
|
||||
else
|
||||
throw error
|
||||
|
||||
logIn: ->
|
||||
@sessionId = @call 'session.login_with_password', @username, @password
|
||||
|
||||
#=====================================================================
|
||||
|
||||
module.exports = $XAPI
|
||||
600
src/xapi.js
Normal file
600
src/xapi.js
Normal file
@@ -0,0 +1,600 @@
|
||||
import createDebug from 'debug'
|
||||
import eventToPromise from 'event-to-promise'
|
||||
import find from 'lodash.find'
|
||||
import forEach from 'lodash.foreach'
|
||||
import got from 'got'
|
||||
import map from 'lodash.map'
|
||||
import unzip from 'julien-f-unzip'
|
||||
import {PassThrough} from 'stream'
|
||||
import {promisify} from 'bluebird'
|
||||
import {Xapi as XapiBase} from 'xen-api'
|
||||
|
||||
import {debounce} from './decorators'
|
||||
import {ensureArray, noop, parseXml, pFinally} from './utils'
|
||||
import {JsonRpcError} from './api-errors'
|
||||
|
||||
const debug = createDebug('xo:xapi')
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const gotPromise = promisify(got)
|
||||
|
||||
const wrapError = error => {
|
||||
const e = new Error(error[0])
|
||||
e.code = error[0]
|
||||
e.params = error.slice(1)
|
||||
return e
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const typeToNamespace = Object.create(null)
|
||||
forEach([
|
||||
'Bond',
|
||||
'DR_task',
|
||||
'GPU_group',
|
||||
'PBD',
|
||||
'PCI',
|
||||
'PGPU',
|
||||
'PIF',
|
||||
'PIF_metrics',
|
||||
'SM',
|
||||
'SR',
|
||||
'VBD',
|
||||
'VBD_metrics',
|
||||
'VDI',
|
||||
'VGPU',
|
||||
'VGPU_type',
|
||||
'VLAN',
|
||||
'VM',
|
||||
'VM_appliance',
|
||||
'VM_guest_metrics',
|
||||
'VM_metrics',
|
||||
'VMPP',
|
||||
'VTPM'
|
||||
], namespace => {
|
||||
typeToNamespace[namespace.toLowerCase()] = namespace
|
||||
})
|
||||
|
||||
// Object types given by `xen-api` are always lowercase but the
|
||||
// namespaces in the Xen API can have a different casing.
|
||||
const getNamespaceForType = (type) => typeToNamespace[type] || type
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export const isHostRunning = (host) => {
|
||||
const {$metrics: metrics} = host
|
||||
|
||||
return metrics && metrics.live
|
||||
}
|
||||
|
||||
const VM_RUNNING_POWER_STATES = {
|
||||
Running: true,
|
||||
Paused: true
|
||||
}
|
||||
export const isVmRunning = (vm) => VM_RUNNING_POWER_STATES[vm.power_state]
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export default class Xapi extends XapiBase {
|
||||
constructor (...args) {
|
||||
super(...args)
|
||||
|
||||
const objectsWatchers = this._objectWatchers = Object.create(null)
|
||||
const taskWatchers = this._taskWatchers = Object.create(null)
|
||||
|
||||
const onAddOrUpdate = objects => {
|
||||
forEach(objects, object => {
|
||||
const {
|
||||
$id: id,
|
||||
$ref: ref
|
||||
} = object
|
||||
|
||||
// Watched object.
|
||||
if (id in objectsWatchers) {
|
||||
objectsWatchers[id].resolve(object)
|
||||
delete objectsWatchers[id]
|
||||
}
|
||||
if (ref in objectsWatchers) {
|
||||
objectsWatchers[ref].resolve(object)
|
||||
delete objectsWatchers[ref]
|
||||
}
|
||||
|
||||
// Watched task.
|
||||
if (ref in taskWatchers) {
|
||||
const {status} = object
|
||||
|
||||
if (status === 'success') {
|
||||
taskWatchers[ref].resolve(object.result)
|
||||
} else if (status === 'failure') {
|
||||
taskWatchers[ref].reject(wrapError(object.error_info))
|
||||
} else {
|
||||
return
|
||||
}
|
||||
|
||||
delete taskWatchers[ref]
|
||||
}
|
||||
})
|
||||
}
|
||||
this.objects.on('add', onAddOrUpdate)
|
||||
this.objects.on('update', onAddOrUpdate)
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
|
||||
// Wait for an object to appear or to be updated.
|
||||
//
|
||||
// TODO: implements a timeout.
|
||||
_waitObject (idOrUuidOrRef) {
|
||||
let watcher = this._objectWatchers[idOrUuidOrRef]
|
||||
if (!watcher) {
|
||||
let resolve, reject
|
||||
const promise = new Promise((resolve_, reject_) => {
|
||||
resolve = resolve_
|
||||
reject = reject_
|
||||
})
|
||||
|
||||
// Register the watcher.
|
||||
watcher = this._objectWatchers[idOrUuidOrRef] = {
|
||||
promise,
|
||||
resolve,
|
||||
reject
|
||||
}
|
||||
}
|
||||
|
||||
return watcher.promise
|
||||
}
|
||||
|
||||
// Returns the objects if already presents or waits for it.
|
||||
async _getOrWaitObject (idOrUuidOrRef) {
|
||||
return (
|
||||
this.getObject(idOrUuidOrRef, undefined) ||
|
||||
this._waitObject(idOrUuidOrRef)
|
||||
)
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
|
||||
// Create a task.
|
||||
//
|
||||
// Returns the task object from the Xapi.
|
||||
async _createTask (name = 'untitled task', description = '') {
|
||||
const ref = await this.call('task.create', `[XO] ${name}`, description)
|
||||
debug('task created: %s', name)
|
||||
|
||||
pFinally(this._watchTask(ref), () => {
|
||||
this.call('task.destroy', ref).then(() => {
|
||||
debug('task destroyed: %s', name)
|
||||
})
|
||||
})
|
||||
|
||||
return this._getOrWaitObject(ref)
|
||||
}
|
||||
|
||||
// Waits for a task to be resolved.
|
||||
_watchTask (ref) {
|
||||
// If a task object is passed, unpacked the ref.
|
||||
if (typeof ref === 'object' && ref.$ref) ref = ref.$ref
|
||||
|
||||
let watcher = this._taskWatchers[ref]
|
||||
if (!watcher) {
|
||||
let resolve, reject
|
||||
const promise = new Promise((resolve_, reject_) => {
|
||||
resolve = resolve_
|
||||
reject = reject_
|
||||
})
|
||||
|
||||
// Register the watcher.
|
||||
watcher = this._taskWatchers[ref] = {
|
||||
promise,
|
||||
resolve,
|
||||
reject
|
||||
}
|
||||
}
|
||||
|
||||
return watcher.promise
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
|
||||
async _setObjectProperties (id, props) {
|
||||
const {
|
||||
$ref: ref,
|
||||
$type: type
|
||||
} = this.getObject(id)
|
||||
|
||||
const namespace = getNamespaceForType(type)
|
||||
|
||||
// TODO: the thrown error should contain the name of the
|
||||
// properties that failed to be set.
|
||||
await Promise.all(map(props, (value, name) => {
|
||||
if (value != null) {
|
||||
return this.call(`${namespace}.set_${name}`, ref, value)
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
async setPoolProperties ({
|
||||
name_label,
|
||||
name_description
|
||||
}) {
|
||||
await this._setObjectProperties(this.pool.$id, {
|
||||
name_label,
|
||||
name_description
|
||||
})
|
||||
}
|
||||
|
||||
async setSrProperties (id, {
|
||||
name_label,
|
||||
name_description
|
||||
}) {
|
||||
await this._setObjectProperties(id, {
|
||||
name_label,
|
||||
name_description
|
||||
})
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
|
||||
// FIXME: should be static
|
||||
@debounce(24 * 60 * 60 * 1000)
|
||||
async _getXenUpdates () {
|
||||
const [body, {statusCode}] = await gotPromise(
|
||||
'http://updates.xensource.com/XenServer/updates.xml'
|
||||
)
|
||||
|
||||
if (statusCode !== 200) {
|
||||
throw new JsonRpcError('cannot fetch patches list from Citrix')
|
||||
}
|
||||
|
||||
const {patchdata: data} = parseXml(body)
|
||||
|
||||
const patches = Object.create(null)
|
||||
forEach(data.patches.patch, patch => {
|
||||
patches[patch.uuid] = {
|
||||
date: patch.timestamp,
|
||||
description: patch['name-description'],
|
||||
documentationUrl: patch.url,
|
||||
guidance: patch['after-apply-guidance'],
|
||||
name: patch['name-label'],
|
||||
url: patch['patch-url'],
|
||||
uuid: patch.uuid,
|
||||
conflicts: map(ensureArray(patch.conflictingpatches), patch => {
|
||||
return patch.conflictingpatch.uuid
|
||||
}),
|
||||
requirements: map(ensureArray(patch.requiredpatches), patch => {
|
||||
return patch.requiredpatch.uuid
|
||||
})
|
||||
|
||||
// TODO: what does it mean, should we handle it?
|
||||
// version: patch.version,
|
||||
}
|
||||
})
|
||||
|
||||
const resolveVersionPatches = function (uuids) {
|
||||
const versionPatches = Object.create(null)
|
||||
|
||||
forEach(uuids, ({uuid}) => {
|
||||
versionPatches[uuid] = patches[uuid]
|
||||
})
|
||||
|
||||
return versionPatches
|
||||
}
|
||||
|
||||
const versions = Object.create(null)
|
||||
let latestVersion
|
||||
forEach(data.serverversions.version, version => {
|
||||
versions[version.value] = {
|
||||
date: version.timestamp,
|
||||
name: version.name,
|
||||
id: version.value,
|
||||
documentationUrl: version.url,
|
||||
patches: resolveVersionPatches(version.patch)
|
||||
}
|
||||
|
||||
if (version.latest) {
|
||||
latestVersion = versions[version.value]
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
patches,
|
||||
latestVersion,
|
||||
versions
|
||||
}
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
|
||||
async listMissingPoolPatchesOnHost (hostId) {
|
||||
const host = this.getObject(hostId)
|
||||
const {product_version: version} = host.software_version
|
||||
|
||||
const all = (await this._getXenUpdates()).versions[version].patches
|
||||
|
||||
const installed = Object.create(null)
|
||||
forEach(host.$patches, hostPatch => {
|
||||
installed[hostPatch.$pool_patch.uuid] = true
|
||||
})
|
||||
|
||||
const installable = []
|
||||
forEach(all, (patch, uuid) => {
|
||||
if (installed[uuid]) {
|
||||
return
|
||||
}
|
||||
|
||||
for (let uuid of patch.conflicts) {
|
||||
if (uuid in installed) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
installable.push(patch)
|
||||
})
|
||||
|
||||
return installable
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
async uploadPoolPatch (stream, length) {
|
||||
const task = await this._createTask('Patch upload')
|
||||
|
||||
const [, patchRef] = await Promise.all([
|
||||
gotPromise('http://' + this.pool.$master.address + '/pool_patch_upload', {
|
||||
method: 'put',
|
||||
body: stream,
|
||||
query: {
|
||||
session_id: this.sessionId,
|
||||
task_id: task.$ref
|
||||
},
|
||||
headers: {
|
||||
'content-length': length
|
||||
}
|
||||
}),
|
||||
this._watchTask(task)
|
||||
])
|
||||
|
||||
return this._getOrWaitObject(patchRef)
|
||||
}
|
||||
|
||||
async _getOrUploadPoolPatch (uuid) {
|
||||
try {
|
||||
return this.getObjectByUuid(uuid)
|
||||
} catch (error) {}
|
||||
|
||||
debug('downloading patch %s', uuid)
|
||||
|
||||
const patchInfo = (await this._getXenUpdates()).patches[uuid]
|
||||
if (!patchInfo) {
|
||||
throw new Error('no such patch ' + uuid)
|
||||
}
|
||||
|
||||
const PATCH_RE = /\.xsupdate$/
|
||||
const proxy = new PassThrough()
|
||||
got(patchInfo.url).on('error', error => {
|
||||
// TODO: better error handling
|
||||
console.error(error)
|
||||
}).pipe(unzip.Parse()).on('entry', entry => {
|
||||
if (PATCH_RE.test(entry.path)) {
|
||||
proxy.emit('length', entry.size)
|
||||
entry.pipe(proxy)
|
||||
} else {
|
||||
entry.autodrain()
|
||||
}
|
||||
}).on('error', error => {
|
||||
// TODO: better error handling
|
||||
console.error(error)
|
||||
})
|
||||
|
||||
const length = await eventToPromise(proxy, 'length')
|
||||
return this.uploadPoolPatch(proxy, length)
|
||||
}
|
||||
|
||||
async installPoolPatchOnHost (patchUuid, hostId) {
|
||||
const patch = await this._getOrUploadPoolPatch(patchUuid)
|
||||
const host = this.getObject(hostId)
|
||||
|
||||
debug('installing patch %s', patchUuid)
|
||||
|
||||
await this.call('pool_patch.apply', patch.$ref, host.$ref)
|
||||
}
|
||||
|
||||
async installPoolPatchOnAllHosts (patchUuid) {
|
||||
const patch = await this._getOrUploadPoolPatch(patchUuid)
|
||||
|
||||
await this.call('pool_patch.pool_apply', patch.$ref)
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
|
||||
async _deleteVdi (vdiId) {
|
||||
const vdi = this.getObject(vdiId)
|
||||
|
||||
await this.call('VDI.destroy', vdi.$ref)
|
||||
}
|
||||
|
||||
async _snapshotVm (vm, nameLabel = vm.name_label) {
|
||||
const ref = await this.call('VM.snapshot', vm.$ref, nameLabel)
|
||||
|
||||
// Convert the template to a VM.
|
||||
await this.call('VM.set_is_a_template', ref, false)
|
||||
|
||||
return ref
|
||||
}
|
||||
|
||||
async deleteVm (vmId, deleteDisks = false) {
|
||||
const vm = this.getObject(vmId)
|
||||
|
||||
if (isVmRunning(vm)) {
|
||||
throw new Error('running VMs cannot be deleted')
|
||||
}
|
||||
|
||||
if (deleteDisks) {
|
||||
await Promise.all(map(vm.$VBDs, vbd => {
|
||||
try {
|
||||
return this._deleteVdi(vbd.$VDI).catch(noop)
|
||||
} catch (_) {}
|
||||
}))
|
||||
}
|
||||
|
||||
await this.call('VM.destroy', vm.$ref)
|
||||
}
|
||||
|
||||
getVmConsoleUrl (vmId) {
|
||||
const vm = this.getObject(vmId)
|
||||
|
||||
const console = find(vm.$consoles, { protocol: 'rfb' })
|
||||
if (!console) {
|
||||
throw new Error('no RFB console found')
|
||||
}
|
||||
|
||||
return `${console.location}&session_id=${this.sessionId}`
|
||||
}
|
||||
|
||||
// Returns a stream to the exported VM.
|
||||
async exportVm (vmId, {compress = true} = {}) {
|
||||
const vm = this.getObject(vmId)
|
||||
|
||||
let host
|
||||
let snapshotRef
|
||||
if (isVmRunning(vm)) {
|
||||
host = vm.$resident_on
|
||||
snapshotRef = await this._snapshotVm(vm)
|
||||
} else {
|
||||
host = this.pool.$master
|
||||
}
|
||||
|
||||
const task = await this._createTask('VM Snapshot', vm.name_label)
|
||||
pFinally(this._watchTask(task), () => {
|
||||
if (snapshotRef) {
|
||||
this.deleteVm(snapshotRef, true)
|
||||
}
|
||||
})
|
||||
|
||||
const stream = got({
|
||||
hostname: host.address,
|
||||
path: '/export/'
|
||||
}, {
|
||||
query: {
|
||||
ref: snapshotRef || vm.$ref,
|
||||
session_id: this.sessionId,
|
||||
task_id: task.$ref,
|
||||
use_compression: compress ? 'true' : 'false'
|
||||
}
|
||||
})
|
||||
stream.response = eventToPromise(stream, 'response')
|
||||
|
||||
return stream
|
||||
}
|
||||
|
||||
async snapshotVm (vmId) {
|
||||
return await this._getOrWaitObject(
|
||||
await this._snapshotVm(
|
||||
this.getObject(vmId)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
async attachVdiToVm (vdiId, vmId, {
|
||||
bootable = false,
|
||||
mode = 'RW',
|
||||
position
|
||||
} = {}) {
|
||||
const vdi = this.getObject(vdiId)
|
||||
const vm = this.getObject(vmId)
|
||||
|
||||
if (position == null) {
|
||||
forEach(vm.$VBDs, vbd => {
|
||||
const curPos = +vbd.userdevice
|
||||
if (!(position > curPos)) {
|
||||
position = curPos
|
||||
}
|
||||
})
|
||||
|
||||
position = position == null ? 0 : position + 1
|
||||
}
|
||||
|
||||
const vbdRef = await this.call('VBD.create', {
|
||||
bootable,
|
||||
empty: false,
|
||||
mode,
|
||||
other_config: {},
|
||||
qos_algorithm_params: {},
|
||||
qos_algorithm_type: '',
|
||||
type: 'Disk',
|
||||
userdevice: String(position),
|
||||
VDI: vdi.$ref,
|
||||
VM: vm.$ref
|
||||
})
|
||||
|
||||
await this.call('VBD.plug', vbdRef)
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
|
||||
async createVirtualInterface (vmId, networkId, {
|
||||
mac = '',
|
||||
mtu = 1500,
|
||||
position = 0
|
||||
} = {}) {
|
||||
const vm = this.getObject(vmId)
|
||||
const network = this.getObject(networkId)
|
||||
|
||||
const ref = await this.call('VIF.create', {
|
||||
device: String(position),
|
||||
MAC: String(mac),
|
||||
MTU: String(mtu),
|
||||
network: network.$ref,
|
||||
other_config: {},
|
||||
qos_algorithm_params: {},
|
||||
qos_algorithm_type: '',
|
||||
VM: vm.$ref
|
||||
})
|
||||
|
||||
return await this._getOrWaitObject(ref)
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
|
||||
async _doDockerAction (vmId, action, containerId) {
|
||||
const vm = this.getObject(vmId)
|
||||
const host = vm.$resident_on
|
||||
|
||||
return await this.call('host.call_plugin', host.$ref, 'xscontainer', action, {
|
||||
vmuuid: vm.uuid,
|
||||
container: containerId
|
||||
})
|
||||
}
|
||||
|
||||
async registerDockerContainer (vmId) {
|
||||
await this._doDockerAction(vmId, 'register')
|
||||
}
|
||||
|
||||
async deregisterDockerContainer (vmId) {
|
||||
await this._doDockerAction(vmId, 'deregister')
|
||||
}
|
||||
|
||||
async startDockerContainer (vmId, containerId) {
|
||||
await this._doDockerAction(vmId, 'start', containerId)
|
||||
}
|
||||
|
||||
async stopDockerContainer (vmId, containerId) {
|
||||
await this._doDockerAction(vmId, 'stop', containerId)
|
||||
}
|
||||
|
||||
async restartDockerContainer (vmId, containerId) {
|
||||
await this._doDockerAction(vmId, 'restart', containerId)
|
||||
}
|
||||
|
||||
async pauseDockerContainer (vmId, containerId) {
|
||||
await this._doDockerAction(vmId, 'pause', containerId)
|
||||
}
|
||||
|
||||
async unpauseDockerContainer (vmId, containerId) {
|
||||
await this._doDockerAction(vmId, 'unpause', containerId)
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
|
||||
}
|
||||
396
src/xo.coffee
396
src/xo.coffee
@@ -1,396 +0,0 @@
|
||||
# Cryptographic tools.
|
||||
$crypto = require 'crypto'
|
||||
|
||||
# Events handling.
|
||||
{EventEmitter: $EventEmitter} = require 'events'
|
||||
|
||||
#---------------------------------------------------------------------
|
||||
|
||||
# Low level tools.
|
||||
$_ = require 'underscore'
|
||||
|
||||
# Password hashing.
|
||||
$hashy = require 'hashy'
|
||||
|
||||
# Redis.
|
||||
$createRedisClient = (require 'then-redis').createClient
|
||||
|
||||
$Promise = require 'bluebird'
|
||||
|
||||
#---------------------------------------------------------------------
|
||||
|
||||
# A mapped collection is generated from another collection through a
|
||||
# specification.
|
||||
{$MappedCollection} = require './MappedCollection'
|
||||
|
||||
# Collection where models are stored in a Redis DB.
|
||||
$RedisCollection = require './collection/redis'
|
||||
|
||||
# Base class for a model.
|
||||
$Model = require './model'
|
||||
|
||||
# Connection to XAPI.
|
||||
$XAPI = require './xapi'
|
||||
|
||||
# Helpers for dealing with fibers.
|
||||
{$fiberize, $wait} = require './fibers-utils'
|
||||
|
||||
#=====================================================================
|
||||
|
||||
# Promise versions of asynchronous functions.
|
||||
$randomBytes = $Promise.promisify $crypto.randomBytes
|
||||
|
||||
$hash = $hashy.hash
|
||||
$needsRehash = $hashy.needsRehash
|
||||
$verifyHash = $hashy.verify
|
||||
|
||||
#=====================================================================
|
||||
# Models and collections.
|
||||
|
||||
class $Server extends $Model
|
||||
validate: -> # TODO
|
||||
|
||||
class $Servers extends $RedisCollection
|
||||
model: $Server
|
||||
|
||||
#---------------------------------------------------------------------
|
||||
|
||||
class $Token extends $Model
|
||||
@generate: (userId) ->
|
||||
new $Token {
|
||||
id: ($wait $randomBytes 32).toString 'base64'
|
||||
user_id: userId
|
||||
}
|
||||
|
||||
validate: -> # TODO
|
||||
|
||||
class $Tokens extends $RedisCollection
|
||||
model: $Token
|
||||
|
||||
generate: (userId) ->
|
||||
@add $Token.generate userId
|
||||
|
||||
#---------------------------------------------------------------------
|
||||
|
||||
class $User extends $Model
|
||||
default: {
|
||||
permission: 'none'
|
||||
}
|
||||
|
||||
validate: -> # TODO
|
||||
|
||||
setPassword: (password) ->
|
||||
@set 'pw_hash', $wait $hash password
|
||||
|
||||
# Checks the password and updates the hash if necessary.
|
||||
checkPassword: (password) ->
|
||||
hash = @get 'pw_hash'
|
||||
|
||||
unless $wait $verifyHash password, hash
|
||||
return false
|
||||
|
||||
if $needsRehash hash
|
||||
@setPassword password
|
||||
|
||||
true
|
||||
|
||||
hasPermission: (permission) ->
|
||||
perms = {
|
||||
none: 0
|
||||
read: 1
|
||||
write: 2
|
||||
admin: 3
|
||||
}
|
||||
|
||||
perms[@get 'permission'] >= perms[permission]
|
||||
|
||||
class $Users extends $RedisCollection
|
||||
model: $User
|
||||
|
||||
create: (email, password, permission) ->
|
||||
user = new $User {
|
||||
email: email
|
||||
}
|
||||
user.setPassword password
|
||||
user.set 'permission', permission unless permission is undefined
|
||||
|
||||
@add user
|
||||
|
||||
#=====================================================================
|
||||
|
||||
class $XO extends $EventEmitter
|
||||
|
||||
start: (config) ->
|
||||
# Connects to Redis.
|
||||
redis = $createRedisClient config.redis.uri
|
||||
|
||||
# Creates persistent collections.
|
||||
@servers = new $Servers {
|
||||
connection: redis
|
||||
prefix: 'xo:server'
|
||||
indexes: ['host']
|
||||
}
|
||||
@tokens = new $Tokens {
|
||||
connection: redis
|
||||
prefix: 'xo:token'
|
||||
indexes: ['user_id']
|
||||
}
|
||||
@users = new $Users {
|
||||
connection: redis
|
||||
prefix: 'xo:user'
|
||||
indexes: ['email']
|
||||
}
|
||||
|
||||
# Proxies tokens/users related events to XO and removes tokens
|
||||
# when their related user is removed.
|
||||
@tokens.on 'remove', (ids) =>
|
||||
@emit "token.revoked:#{id}" for id in ids
|
||||
@users.on 'remove', (ids) =>
|
||||
@emit "user.revoked:#{id}" for id in ids
|
||||
tokens = @tokens.get {user_id: id}
|
||||
@tokens.remove (token.id for token in tokens)
|
||||
|
||||
# Collections of XAPI objects mapped to XO API.
|
||||
@_xobjs = new $MappedCollection()
|
||||
(require './spec').call @_xobjs
|
||||
|
||||
# When objects enter or exists, sends a notification to all
|
||||
# connected clients.
|
||||
do =>
|
||||
entered = {}
|
||||
exited = {}
|
||||
|
||||
dispatcherRegistered = false
|
||||
dispatcher = =>
|
||||
entered = $_.pluck entered, 'val'
|
||||
enterEvent = if entered.length
|
||||
JSON.stringify {
|
||||
jsonrpc: '2.0'
|
||||
method: 'all'
|
||||
params: {
|
||||
type: 'enter'
|
||||
items: entered
|
||||
}
|
||||
}
|
||||
exited = $_.pluck exited, 'val'
|
||||
exitEvent = if exited.length
|
||||
JSON.stringify {
|
||||
jsonrpc: '2.0'
|
||||
method: 'all'
|
||||
params: {
|
||||
type: 'exit'
|
||||
items: exited
|
||||
}
|
||||
}
|
||||
|
||||
if entered.length
|
||||
connection.send enterEvent for id, connection of @connections
|
||||
if exited.length
|
||||
connection.send exitEvent for id, connection of @connections
|
||||
dispatcherRegistered = false
|
||||
entered = {}
|
||||
exited = {}
|
||||
|
||||
@_xobjs.on 'any', (event, items) ->
|
||||
unless dispatcherRegistered
|
||||
dispatcherRegistered = true
|
||||
process.nextTick dispatcher
|
||||
|
||||
if event is 'exit'
|
||||
$_.each items, (item) ->
|
||||
{key} = item
|
||||
delete entered[key]
|
||||
exited[key] = item
|
||||
else
|
||||
$_.each items, (item) ->
|
||||
{key} = item
|
||||
delete exited[key]
|
||||
entered[key] = item
|
||||
|
||||
# Exports the map from UUIDs to keys.
|
||||
{$UUIDsToKeys: @_UUIDsToKeys} = (@_xobjs.get 'xo')
|
||||
|
||||
# XAPI connections.
|
||||
@_xapis = Object.create null
|
||||
|
||||
# This function asynchronously connects to a server, retrieves
|
||||
# all its objects and monitors events.
|
||||
connect = (server) =>
|
||||
# Identifier of the connection.
|
||||
id = server.id
|
||||
|
||||
# Reference of the pool of this connection.
|
||||
poolRef = undefined
|
||||
|
||||
xapi = @_xapis[id] = new $XAPI {
|
||||
host: server.host
|
||||
username: server.username
|
||||
password: server.password
|
||||
}
|
||||
|
||||
# First construct the list of retrievable types. except pool
|
||||
# which will handled specifically.
|
||||
retrievableTypes = do ->
|
||||
methods = $wait xapi.call 'system.listMethods'
|
||||
|
||||
types = []
|
||||
for method in methods
|
||||
[type, method] = method.split '.'
|
||||
if method is 'get_all_records' and type isnt 'pool'
|
||||
types.push type
|
||||
types
|
||||
|
||||
# This helper normalizes a record by inserting its type.
|
||||
normalizeObject = (object, ref, type) ->
|
||||
object.$poolRef = poolRef
|
||||
object.$ref = ref
|
||||
object.$type = type
|
||||
|
||||
objects = {}
|
||||
|
||||
# Then retrieve the pool.
|
||||
pools = $wait xapi.call 'pool.get_all_records'
|
||||
|
||||
# Gets the first pool and ensures it is the only one.
|
||||
ref = pool = null
|
||||
for ref of pools
|
||||
throw new Error 'more than one pool!' if pool?
|
||||
pool = pools[ref]
|
||||
throw new Error 'no pool found' unless pool?
|
||||
|
||||
# Remembers its reference.
|
||||
poolRef = ref
|
||||
|
||||
# Makes the connection accessible through the pool reference.
|
||||
# TODO: Properly handle disconnections.
|
||||
@_xapis[poolRef] = xapi
|
||||
|
||||
# Normalizes the records.
|
||||
normalizeObject pool, ref, 'pool'
|
||||
|
||||
# FIXME: Remove this security flaw (currently necessary for consoles).
|
||||
pool.$sessionId = xapi.sessionId
|
||||
|
||||
objects[ref] = pool
|
||||
|
||||
# Then retrieve all other objects.
|
||||
for type in retrievableTypes
|
||||
try
|
||||
for ref, object of $wait xapi.call "#{type}.get_all_records"
|
||||
normalizeObject object, ref, type
|
||||
|
||||
objects[ref] = object
|
||||
catch error
|
||||
# It is possible that the method `TYPE.get_all_records` has
|
||||
# been deprecated, if that's the case, just ignores it.
|
||||
throw error unless error[0] is 'MESSAGE_REMOVED'
|
||||
|
||||
# Stores all objects.
|
||||
@_xobjs.set objects, {
|
||||
add: true
|
||||
update: false
|
||||
remove: false
|
||||
}
|
||||
|
||||
# Finally, monitors events.
|
||||
loop
|
||||
$wait xapi.call 'event.register', ['*']
|
||||
|
||||
try
|
||||
# Once the session is registered, just handle events.
|
||||
loop
|
||||
event = $wait xapi.call 'event.next'
|
||||
|
||||
updatedObjects = {}
|
||||
removedObjects = {}
|
||||
|
||||
for {operation, class: type, ref, snapshot: object} in event
|
||||
# Normalizes the object.
|
||||
normalizeObject object, ref, type
|
||||
|
||||
# FIXME: Remove this security flaw (currently necessary
|
||||
# for consoles).
|
||||
object.$sessionId = xapi.sessionId if type is 'pool'
|
||||
|
||||
# Adds the object to the corresponding list (and ensures
|
||||
# it is not in the other).
|
||||
if operation is 'del'
|
||||
delete updatedObjects[ref]
|
||||
removedObjects[ref] = object
|
||||
else
|
||||
delete removedObjects[ref]
|
||||
updatedObjects[ref] = object
|
||||
|
||||
# Records the changes.
|
||||
@_xobjs.remove removedObjects, true
|
||||
@_xobjs.set updatedObjects, {
|
||||
add: true
|
||||
update: true
|
||||
remove: false
|
||||
}
|
||||
catch error
|
||||
if error[0] is 'EVENTS_LOST'
|
||||
# XAPI error, the program must unregister from events and then
|
||||
# register again.
|
||||
try
|
||||
$wait xapi.call 'event.unregister', ['*']
|
||||
else
|
||||
throw error unless error[0] is 'SESSION_NOT_REGISTERED'
|
||||
|
||||
# Prevents errors from stopping the server.
|
||||
connectSafe = $fiberize (server) ->
|
||||
try
|
||||
connect server
|
||||
catch error
|
||||
console.error(
|
||||
"[WARN] #{server.host}:"
|
||||
error[0] ? error.stack ? error.code ? error
|
||||
)
|
||||
|
||||
# Connects to existing servers.
|
||||
connectSafe server for server in $wait @servers.get()
|
||||
|
||||
# Automatically connects to new servers.
|
||||
@servers.on 'add', (servers) ->
|
||||
connectSafe server for server in servers
|
||||
|
||||
# TODO: Automatically disconnects from removed servers.
|
||||
|
||||
# Connections to users.
|
||||
@connections = {}
|
||||
|
||||
# Returns an object from its key or UUID.
|
||||
getObject: (key) ->
|
||||
# Gracefully handles UUIDs.
|
||||
if key of @_UUIDsToKeys
|
||||
key = @_UUIDsToKeys[key]
|
||||
|
||||
@_xobjs.get key
|
||||
|
||||
# Returns objects.
|
||||
getObjects: (keys) ->
|
||||
# Returns all objects if no keys are passed.
|
||||
return @_xobjs.get() unless keys
|
||||
|
||||
# Resolves all UUIDs.
|
||||
{_UUIDsToKeys: UUIDsToKeys} = this
|
||||
for key, index in keys
|
||||
keys[index] = UUIDsToKeys[key] if key of UUIDsToKeys
|
||||
|
||||
# Fetches all objects ignore those missing.
|
||||
@_xobjs.get keys, true
|
||||
|
||||
# Returns the XAPI connection associated to an object.
|
||||
getXAPI: (object) ->
|
||||
if $_.isString object
|
||||
object = @getObject object
|
||||
|
||||
{poolRef} = object
|
||||
unless poolRef
|
||||
throw new Error "no XAPI found for #{object.UUID}"
|
||||
|
||||
@_xapis[poolRef]
|
||||
|
||||
#=====================================================================
|
||||
|
||||
module.exports = $XO
|
||||
898
src/xo.js
Normal file
898
src/xo.js
Normal file
@@ -0,0 +1,898 @@
|
||||
import Bluebird from 'bluebird'
|
||||
import filter from 'lodash.filter'
|
||||
import forEach from 'lodash.foreach'
|
||||
import includes from 'lodash.includes'
|
||||
import isEmpty from 'lodash.isempty'
|
||||
import isString from 'lodash.isstring'
|
||||
import map from 'lodash.map'
|
||||
import proxyRequest from 'proxy-http-request'
|
||||
import XoCollection from 'xo-collection'
|
||||
import XoUniqueIndex from 'xo-collection/unique-index'
|
||||
// import XoView from 'xo-collection/view'
|
||||
import {createClient as createRedisClient} from 'then-redis'
|
||||
import {EventEmitter} from 'events'
|
||||
import {parse as parseUrl} from 'url'
|
||||
|
||||
import * as xapiObjectsToXo from './xapi-objects-to-xo'
|
||||
import Connection from './connection'
|
||||
import User, {Users} from './models/user'
|
||||
import Xapi from './xapi'
|
||||
import {Acls} from './models/acl'
|
||||
import {autobind} from './decorators'
|
||||
import {generateToken} from './utils'
|
||||
import {Groups} from './models/group'
|
||||
import {JsonRpcError, NoSuchObject} from './api-errors'
|
||||
import {ModelAlreadyExists} from './collection'
|
||||
import {Servers} from './models/server'
|
||||
import {Tokens} from './models/token'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
class NoSuchAuthenticationToken extends NoSuchObject {
|
||||
constructor (id) {
|
||||
super(id, 'authentication token')
|
||||
}
|
||||
}
|
||||
|
||||
class NoSuchGroup extends NoSuchObject {
|
||||
constructor (id) {
|
||||
super(id, 'group')
|
||||
}
|
||||
}
|
||||
|
||||
class NoSuchUser extends NoSuchObject {
|
||||
constructor (id) {
|
||||
super(id, 'user')
|
||||
}
|
||||
}
|
||||
|
||||
class NoSuchXenServer extends NoSuchObject {
|
||||
constructor (id) {
|
||||
super(id, 'xen server')
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export default class Xo extends EventEmitter {
|
||||
constructor () {
|
||||
super()
|
||||
|
||||
this._objects = new XoCollection()
|
||||
this._objects.createIndex('byRef', new XoUniqueIndex('ref'))
|
||||
|
||||
// These will be initialized in start()
|
||||
//
|
||||
// TODO: remove and put everything in the `_objects` collection.
|
||||
this._acls = null
|
||||
this._groups = null
|
||||
this._servers = null
|
||||
this._tokens = null
|
||||
this._users = null
|
||||
this._UUIDsToKeys = null
|
||||
|
||||
// Connections to Xen servers.
|
||||
this._xapis = Object.create(null)
|
||||
|
||||
// Connections to users.
|
||||
this._nextConId = 0
|
||||
this._connections = Object.create(null)
|
||||
|
||||
this._httpRequestWatchers = Object.create(null)
|
||||
|
||||
// TODO: remove when no longer necessary.
|
||||
this._proxyRequests = Object.create(null)
|
||||
|
||||
this._authenticationProviders = new Set()
|
||||
|
||||
this._watchObjects()
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
async start (config) {
|
||||
// Connects to Redis.
|
||||
const redis = createRedisClient(config.redis && config.redis.uri)
|
||||
|
||||
// Creates persistent collections.
|
||||
this._acls = new Acls({
|
||||
connection: redis,
|
||||
prefix: 'xo:acl',
|
||||
indexes: ['subject', 'object']
|
||||
})
|
||||
this._groups = new Groups({
|
||||
connection: redis,
|
||||
prefix: 'xo:group'
|
||||
})
|
||||
this._servers = new Servers({
|
||||
connection: redis,
|
||||
prefix: 'xo:server',
|
||||
indexes: ['host']
|
||||
})
|
||||
this._tokens = new Tokens({
|
||||
connection: redis,
|
||||
prefix: 'xo:token',
|
||||
indexes: ['user_id']
|
||||
})
|
||||
this._users = new Users({
|
||||
connection: redis,
|
||||
prefix: 'xo:user',
|
||||
indexes: ['email']
|
||||
})
|
||||
|
||||
// Proxies tokens/users related events to XO and removes tokens
|
||||
// when their related user is removed.
|
||||
this._tokens.on('remove', ids => {
|
||||
for (let id of ids) {
|
||||
this.emit(`token.revoked:${id}`)
|
||||
}
|
||||
})
|
||||
this._users.on('remove', async function (ids) {
|
||||
for (let id of ids) {
|
||||
this.emit(`user.revoked:${id}`)
|
||||
const tokens = await this._tokens.get({ user_id: id })
|
||||
for (let token of tokens) {
|
||||
this._tokens.remove(token.id)
|
||||
}
|
||||
}
|
||||
}.bind(this))
|
||||
|
||||
// Connects to existing servers.
|
||||
for (let server of await this._servers.get()) {
|
||||
this.connectXenServer(server.id).catch(error => {
|
||||
console.error(
|
||||
`[WARN] ${server.host}:`,
|
||||
error[0] || error.stack || error.code || error
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
async addAcl (subjectId, objectId, action) {
|
||||
try {
|
||||
await this._acls.create(subjectId, objectId, action)
|
||||
} catch (error) {
|
||||
if (!(error instanceof ModelAlreadyExists)) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async removeAcl (subjectId, objectId, action) {
|
||||
await this._acls.delete(subjectId, objectId, action)
|
||||
}
|
||||
|
||||
async getAclsForUser (userId) {
|
||||
const subjects = (await this.getUser(userId)).groups.concat(userId)
|
||||
|
||||
const acls = []
|
||||
const pushAcls = (function (push) {
|
||||
return function (entries) {
|
||||
push.apply(acls, entries)
|
||||
}
|
||||
})(acls.push)
|
||||
|
||||
const {_acls: collection} = this
|
||||
await Promise.all(map(
|
||||
subjects,
|
||||
subject => collection.get({subject}).then(pushAcls)
|
||||
))
|
||||
|
||||
return acls
|
||||
}
|
||||
|
||||
// TODO: remove when new collection.
|
||||
async getAllAcls () {
|
||||
return this._acls.get()
|
||||
}
|
||||
|
||||
async hasPermission (userId, objectId, permission) {
|
||||
const user = await this.getUser(userId)
|
||||
|
||||
// Special case for super XO administrators.
|
||||
//
|
||||
// TODO: restore when necessary, for now it is already implemented
|
||||
// in resolveParams().
|
||||
// if (user.permission === 'admin') {
|
||||
// return true
|
||||
// }
|
||||
|
||||
const subjects = user.groups.concat(userId)
|
||||
let actions = (await this.getRolesForPermission(permission)).concat(permission)
|
||||
|
||||
const promises = []
|
||||
{
|
||||
const {_acls: acls} = this
|
||||
const throwIfFail = function (success) {
|
||||
if (!success) {
|
||||
// We don't care about an error object.
|
||||
/* eslint no-throw-literal: 0 */
|
||||
throw null
|
||||
}
|
||||
}
|
||||
forEach(subjects, subject => {
|
||||
forEach(actions, action => {
|
||||
promises.push(
|
||||
acls.aclExists(subject, objectId, action).then(throwIfFail)
|
||||
)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
await Bluebird.any(promises)
|
||||
return true
|
||||
} catch (_) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
async createUser ({email, password, permission}) {
|
||||
// TODO: use plain objects
|
||||
const user = await this._users.create(email, password, permission)
|
||||
|
||||
return user.properties
|
||||
}
|
||||
|
||||
async deleteUser (id) {
|
||||
if (!await this._users.remove(id)) {
|
||||
throw new NoSuchUser(id)
|
||||
}
|
||||
}
|
||||
|
||||
async updateUser (id, {email, password, permission}) {
|
||||
const user = await this._getUser(id)
|
||||
|
||||
if (email) user.set('email', email)
|
||||
if (permission) user.set('permission', permission)
|
||||
if (password) {
|
||||
await user.setPassword(password)
|
||||
}
|
||||
|
||||
await this._users.save(user.properties)
|
||||
}
|
||||
|
||||
// Merge this method in getUser() when plain objects.
|
||||
async _getUser (id) {
|
||||
const user = await this._users.first(id)
|
||||
if (!user) {
|
||||
throw new NoSuchUser(id)
|
||||
}
|
||||
|
||||
return user
|
||||
}
|
||||
|
||||
// TODO: this method will no longer be async when users are
|
||||
// integrated to the main collection.
|
||||
async getUser (id) {
|
||||
return (await this._getUser(id)).properties
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
async createGroup ({name}) {
|
||||
// TODO: use plain objects.
|
||||
const group = (await this._groups.create(name)).properties
|
||||
|
||||
group.users = JSON.parse(group.users)
|
||||
return group
|
||||
}
|
||||
|
||||
async deleteGroup (id) {
|
||||
if (!await this._groups.remove(id)) {
|
||||
throw new NoSuchGroup(id)
|
||||
}
|
||||
}
|
||||
|
||||
async updateGroup (id, {name}) {
|
||||
const group = await this.getGroup(id)
|
||||
|
||||
if (name) group.name = name
|
||||
|
||||
await this._groups.save(group)
|
||||
}
|
||||
|
||||
async getGroup (id) {
|
||||
const group = (await this._groups.first(id))
|
||||
if (!group) {
|
||||
throw new NoSuchGroup(id)
|
||||
}
|
||||
|
||||
return group.properties
|
||||
}
|
||||
|
||||
async addUserToGroup (userId, groupId) {
|
||||
const [user, group] = await Promise.all([
|
||||
this.getUser(userId),
|
||||
this.getGroup(groupId)
|
||||
])
|
||||
|
||||
const {groups} = user
|
||||
if (!includes(groups, groupId)) {
|
||||
user.groups.push(groupId)
|
||||
}
|
||||
|
||||
const {users} = group
|
||||
if (!includes(users, userId)) {
|
||||
group.users.push(userId)
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
this._users.save(user),
|
||||
this._groups.save(group)
|
||||
])
|
||||
}
|
||||
|
||||
async removeUserFromGroup (userId, groupId) {
|
||||
const [user, group] = await Promise.all([
|
||||
this.getUser(userId),
|
||||
this.getGroup(groupId)
|
||||
])
|
||||
|
||||
// TODO: maybe not iterating through the whole arrays?
|
||||
user.groups = filter(user.groups, id => id !== groupId)
|
||||
group.users = filter(group.users, id => id !== userId)
|
||||
|
||||
await Promise.all([
|
||||
this._users.save(user),
|
||||
this._groups.save(group)
|
||||
])
|
||||
}
|
||||
|
||||
async setGroupUsers (groupId, userIds) {
|
||||
const group = await this.getGroup(groupId)
|
||||
|
||||
const newUsersIds = Object.create(null)
|
||||
const oldUsersIds = Object.create(null)
|
||||
forEach(userIds, id => {
|
||||
newUsersIds[id] = null
|
||||
})
|
||||
forEach(group.users, id => {
|
||||
if (id in newUsersIds) {
|
||||
delete newUsersIds[id]
|
||||
} else {
|
||||
oldUsersIds[id] = null
|
||||
}
|
||||
})
|
||||
|
||||
const [newUsers, oldUsers] = await Promise.all([
|
||||
Promise.all(map(newUsersIds, (_, id) => this.getUser(id))),
|
||||
Promise.all(map(oldUsersIds, (_, id) => this.getUser(id)))
|
||||
])
|
||||
|
||||
forEach(newUsers, user => {
|
||||
const {groups} = user
|
||||
if (!includes(groups, groupId)) {
|
||||
user.groups.push(groupId)
|
||||
}
|
||||
})
|
||||
forEach(oldUsers, user => {
|
||||
user.groups = filter(user.groups, id => id !== groupId)
|
||||
})
|
||||
|
||||
group.users = userIds
|
||||
|
||||
await Promise.all([
|
||||
Promise.all(map(newUsers, this._users.save, this._users)),
|
||||
Promise.all(map(oldUsers, this._users.save, this._users)),
|
||||
this._groups.save(group)
|
||||
])
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
// TODO: delete when merged with the new collection.
|
||||
async getRoles () {
|
||||
return [
|
||||
{
|
||||
id: 'viewer',
|
||||
name: 'Viewer',
|
||||
permissions: [
|
||||
'view'
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'operator',
|
||||
name: 'Operator',
|
||||
permissions: [
|
||||
'view',
|
||||
'operate'
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'admin',
|
||||
name: 'Admin',
|
||||
permissions: [
|
||||
'view',
|
||||
'operate',
|
||||
'administrate'
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// Returns an array of roles which have a given permission.
|
||||
async getRolesForPermission (permission) {
|
||||
const roles = []
|
||||
|
||||
forEach(await this.getRoles(), role => {
|
||||
if (includes(role.permissions, permission)) {
|
||||
roles.push(role.id)
|
||||
}
|
||||
})
|
||||
|
||||
return roles
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
async createAuthenticationToken ({userId}) {
|
||||
// TODO: use plain objects
|
||||
const token = await this._tokens.generate(userId)
|
||||
|
||||
return token.properties
|
||||
}
|
||||
|
||||
async deleteAuthenticationToken (id) {
|
||||
if (!await this._token.remove(id)) {
|
||||
throw new NoSuchAuthenticationToken(id)
|
||||
}
|
||||
}
|
||||
|
||||
async getAuthenticationToken (id) {
|
||||
const token = await this._tokens.first(id)
|
||||
if (!token) {
|
||||
throw new NoSuchAuthenticationToken(id)
|
||||
}
|
||||
|
||||
return token.properties
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
async registerXenServer ({host, username, password}) {
|
||||
// FIXME: We are storing passwords which is bad!
|
||||
// Could we use tokens instead?
|
||||
// TODO: use plain objects
|
||||
const server = await this._servers.add({host, username, password})
|
||||
|
||||
return server.properties
|
||||
}
|
||||
|
||||
async unregisterXenServer (id) {
|
||||
this.disconnectXenServer(id).catch(() => {})
|
||||
|
||||
if (!await this._servers.remove(id)) {
|
||||
throw new NoSuchXenServer(id)
|
||||
}
|
||||
}
|
||||
|
||||
async updateXenServer (id, {host, username, password}) {
|
||||
const server = await this._getXenServer(id)
|
||||
|
||||
if (host) server.set('host', host)
|
||||
if (username) server.set('username', username)
|
||||
if (password) server.set('password', password)
|
||||
|
||||
await this._servers.update(server)
|
||||
}
|
||||
|
||||
// TODO: this method will no longer be async when servers are
|
||||
// integrated to the main collection.
|
||||
async _getXenServer (id) {
|
||||
const server = await this._servers.first(id)
|
||||
if (!server) {
|
||||
throw new NoSuchXenServer(id)
|
||||
}
|
||||
|
||||
return server
|
||||
}
|
||||
|
||||
@autobind
|
||||
_onXenAdd (xapiObjects) {
|
||||
const {_objects: objects} = this
|
||||
forEach(xapiObjects, (xapiObject, id) => {
|
||||
const transform = xapiObjectsToXo[xapiObject.$type]
|
||||
if (!transform) {
|
||||
return
|
||||
}
|
||||
|
||||
const xoObject = transform(xapiObject)
|
||||
xoObject.id = id
|
||||
xoObject.ref = xapiObject.$ref
|
||||
if (!xoObject.type) {
|
||||
xoObject.type = xapiObject.$type
|
||||
}
|
||||
|
||||
const {$pool: pool} = xapiObject
|
||||
Object.defineProperties(xoObject, {
|
||||
poolRef: { value: pool.$ref },
|
||||
$poolId: {
|
||||
enumerable: true,
|
||||
value: pool.$id
|
||||
},
|
||||
ref: { value: xapiObject.$ref }
|
||||
})
|
||||
|
||||
objects.set(id, xoObject)
|
||||
})
|
||||
}
|
||||
|
||||
@autobind
|
||||
_onXenRemove (xapiObjects) {
|
||||
const {_objects: objects} = this
|
||||
forEach(xapiObjects, (_, id) => {
|
||||
if (objects.has(id)) {
|
||||
objects.remove(id)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TODO the previous state should be marked as connected.
|
||||
async connectXenServer (id) {
|
||||
const server = (await this._getXenServer(id)).properties
|
||||
|
||||
const xapi = this._xapis[server.id] = new Xapi({
|
||||
url: server.host,
|
||||
auth: {
|
||||
user: server.username,
|
||||
password: server.password
|
||||
}
|
||||
})
|
||||
|
||||
const {objects} = xapi
|
||||
objects.on('add', this._onXenAdd)
|
||||
objects.on('update', this._onXenAdd)
|
||||
objects.on('remove', this._onXenRemove)
|
||||
|
||||
// Each time objects are refreshed, registers the connection with
|
||||
// the pool identifier.
|
||||
objects.on('finish', () => {
|
||||
this._xapis[xapi.pool.$id] = xapi
|
||||
})
|
||||
|
||||
try {
|
||||
await xapi.connect()
|
||||
} catch (error) {
|
||||
if (error.code === 'SESSION_AUTHENTICATION_FAILED') {
|
||||
throw new JsonRpcError('authentication failed')
|
||||
}
|
||||
if (error.code === 'EHOSTUNREACH') {
|
||||
throw new JsonRpcError('host unreachable')
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// TODO the previous state should be marked as disconnected.
|
||||
async disconnectXenServer (id) {
|
||||
const xapi = this._xapis[id]
|
||||
if (!xapi) {
|
||||
throw new NoSuchXenServer(id)
|
||||
}
|
||||
|
||||
delete this._xapis[id]
|
||||
if (xapi.pool) {
|
||||
delete this._xapis[xapi.pool.id]
|
||||
}
|
||||
|
||||
return xapi.disconnect()
|
||||
}
|
||||
|
||||
// Returns the XAPI connection associated to an object.
|
||||
getXAPI (object, type) {
|
||||
if (isString(object)) {
|
||||
object = this.getObject(object, type)
|
||||
}
|
||||
|
||||
const {$poolId: poolId} = object
|
||||
if (!poolId) {
|
||||
throw new Error(`object ${object.id} does not belong to a pool`)
|
||||
}
|
||||
|
||||
const xapi = this._xapis[poolId]
|
||||
if (!xapi) {
|
||||
throw new Error(`no connection found for object ${object.id}`)
|
||||
}
|
||||
|
||||
return xapi
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
// Returns an object from its key or UUID.
|
||||
//
|
||||
// TODO: should throw a NoSuchObject error on failure.
|
||||
getObject (key, type) {
|
||||
const {
|
||||
all,
|
||||
indexes: {
|
||||
byRef
|
||||
}
|
||||
} = this._objects
|
||||
|
||||
const obj = all[key] || byRef[key]
|
||||
if (!obj) {
|
||||
throw new NoSuchObject(key, type)
|
||||
}
|
||||
|
||||
if (type != null && (
|
||||
isString(type) && type !== obj.type ||
|
||||
!includes(type, obj.type) // Array
|
||||
)) {
|
||||
throw new NoSuchObject(key, type)
|
||||
}
|
||||
|
||||
return obj
|
||||
}
|
||||
|
||||
getObjects (keys) {
|
||||
const {
|
||||
all,
|
||||
indexes: {
|
||||
byRef
|
||||
}
|
||||
} = this._objects
|
||||
|
||||
// Returns all objects if no keys have been passed.
|
||||
if (!keys) {
|
||||
return all
|
||||
}
|
||||
|
||||
// Fetches all objects and ignores those missing.
|
||||
const result = []
|
||||
forEach(keys, key => {
|
||||
const object = all[key] || byRef[key]
|
||||
if (object) {
|
||||
result.push(object)
|
||||
}
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
createUserConnection () {
|
||||
const {_connections: connections} = this
|
||||
|
||||
const connection = new Connection()
|
||||
const id = connection.id = this._nextConId++
|
||||
|
||||
connections[id] = connection
|
||||
connection.on('close', () => {
|
||||
delete connections[id]
|
||||
})
|
||||
|
||||
return connection
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
_handleHttpRequest (req, res, next) {
|
||||
const {url} = req
|
||||
|
||||
const {_httpRequestWatchers: watchers} = this
|
||||
const watcher = watchers[url]
|
||||
if (!watcher) {
|
||||
next()
|
||||
return
|
||||
}
|
||||
delete watchers[url]
|
||||
|
||||
const {fn, data} = watcher
|
||||
Bluebird.try(fn, [req, res, data]).then(
|
||||
result => {
|
||||
if (result != null) {
|
||||
res.end(JSON.stringify(result))
|
||||
}
|
||||
},
|
||||
error => {
|
||||
console.error('HTTP request error', error.stack || error)
|
||||
|
||||
if (!res.headersSent) {
|
||||
res.writeHead(500)
|
||||
}
|
||||
res.end('unknown error')
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async registerHttpRequest (fn, data) {
|
||||
const {_httpRequestWatchers: watchers} = this
|
||||
|
||||
const url = await (function generateUniqueUrl () {
|
||||
return generateToken().then(token => {
|
||||
const url = `/api/${token}`
|
||||
|
||||
return url in watchers ?
|
||||
generateUniqueUrl() :
|
||||
url
|
||||
})
|
||||
})()
|
||||
|
||||
watchers[url] = {
|
||||
fn,
|
||||
data
|
||||
}
|
||||
|
||||
return url
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
// TODO: remove when no longer necessary.
|
||||
_handleProxyRequest (req, res, next) {
|
||||
const {url} = req
|
||||
const request = this._proxyRequests[url]
|
||||
if (!request || req.method !== request.proxyMethod) {
|
||||
next()
|
||||
return
|
||||
}
|
||||
|
||||
// A proxy request can only be used once.
|
||||
delete this._proxyRequests[url]
|
||||
|
||||
proxyRequest(request, req, res)
|
||||
|
||||
if (request.onSuccess) {
|
||||
res.on('finish', request.onSuccess)
|
||||
}
|
||||
|
||||
const onFailure = request.onFailure || (() => {})
|
||||
|
||||
req.on('close', onFailure)
|
||||
|
||||
const closeConnection = () => {
|
||||
if (!res.headerSent) {
|
||||
res.writeHead(500)
|
||||
}
|
||||
res.end()
|
||||
|
||||
onFailure()
|
||||
}
|
||||
req.on('error', error => {
|
||||
console.warn('request error', error.stack || error)
|
||||
closeConnection()
|
||||
})
|
||||
res.on('error', error => {
|
||||
console.warn('response error', error.stack || error)
|
||||
closeConnection()
|
||||
})
|
||||
}
|
||||
async registerProxyRequest (opts) {
|
||||
if (isString(opts)) {
|
||||
opts = parseUrl(opts)
|
||||
} else {
|
||||
opts.method = opts.method != null ?
|
||||
opts.method.toUpperCase() :
|
||||
'GET'
|
||||
|
||||
opts.proxyMethod = opts.proxyMethod != null ?
|
||||
opts.proxyMethod.toUpperCase() :
|
||||
opts.method
|
||||
}
|
||||
|
||||
opts.createdAt = Date.now()
|
||||
|
||||
const url = `/${await generateToken()}`
|
||||
this._proxyRequests[url] = opts
|
||||
|
||||
return url
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
registerAuthenticationProvider (provider) {
|
||||
return this._authenticationProviders.add(provider)
|
||||
}
|
||||
|
||||
unregisterAuthenticationProvider (provider) {
|
||||
return this._authenticationProviders.remove(provider)
|
||||
}
|
||||
|
||||
async authenticateUser (credentials) {
|
||||
// TODO: remove when email has been replaced by username.
|
||||
if (credentials.email) {
|
||||
credentials.username = credentials.email
|
||||
} else if (credentials.username) {
|
||||
credentials.email = credentials.username
|
||||
}
|
||||
|
||||
for (let provider of this._authenticationProviders) {
|
||||
try {
|
||||
const result = await provider(credentials)
|
||||
|
||||
if (result instanceof User) {
|
||||
return result
|
||||
}
|
||||
|
||||
// TODO: replace by email by username.
|
||||
if (result.username) {
|
||||
result.email = result.username
|
||||
delete result.username
|
||||
}
|
||||
|
||||
const user = await this._users.first(result)
|
||||
if (user) return user
|
||||
|
||||
return this._users.create(result.email)
|
||||
} catch (error) {
|
||||
// Authentication providers may just throw `null` to indicate
|
||||
// they could not authenticate the user without any special
|
||||
// errors.
|
||||
if (error) console.error(error.stack || error)
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
// Watches objects changes.
|
||||
//
|
||||
// Some should be forwarded to connected clients.
|
||||
// Some should be persistently saved.
|
||||
_watchObjects () {
|
||||
const {
|
||||
_connections: connections,
|
||||
_objects: objects
|
||||
} = this
|
||||
|
||||
let entered, exited
|
||||
function reset () {
|
||||
entered = Object.create(null)
|
||||
exited = Object.create(null)
|
||||
}
|
||||
reset()
|
||||
|
||||
function onAdd (items) {
|
||||
forEach(items, (item, id) => {
|
||||
entered[id] = item
|
||||
})
|
||||
}
|
||||
objects.on('add', onAdd)
|
||||
objects.on('update', onAdd)
|
||||
|
||||
objects.on('remove', (items) => {
|
||||
forEach(items, (_, id) => {
|
||||
// We don't care about the value here, so we choose `0`
|
||||
// because it is small in JSON.
|
||||
exited[id] = 0
|
||||
})
|
||||
})
|
||||
|
||||
objects.on('finish', () => {
|
||||
const enteredMessage = !isEmpty(entered) && {
|
||||
type: 'enter',
|
||||
items: entered
|
||||
}
|
||||
const exitedMessage = !isEmpty(exited) && {
|
||||
type: 'exit',
|
||||
items: exited
|
||||
}
|
||||
|
||||
if (!enteredMessage && !exitedMessage) {
|
||||
return
|
||||
}
|
||||
|
||||
forEach(connections, connection => {
|
||||
// Notifies only authenticated clients.
|
||||
if (connection.has('user_id')) {
|
||||
if (enteredMessage) {
|
||||
connection.notify('all', enteredMessage)
|
||||
}
|
||||
if (exitedMessage) {
|
||||
connection.notify('all', exitedMessage)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
reset()
|
||||
})
|
||||
}
|
||||
}
|
||||
14
xo-server.service
Normal file
14
xo-server.service
Normal file
@@ -0,0 +1,14 @@
|
||||
# systemd service for XO-Server.
|
||||
|
||||
[Unit]
|
||||
Description= XO Server
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
Environment="DEBUG=xo:main"
|
||||
ExecStart=/usr/local/bin/xo-server
|
||||
Restart=always
|
||||
SyslogIdentifier=xo-server
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
Reference in New Issue
Block a user