Compare commits

...

458 Commits

Author SHA1 Message Date
Olivier Lambert
74b0697da1 4.0.3 2015-06-07 16:11:41 +02:00
Julien Fontanet
0ca409bb22 4.0.3-alpha.2 2015-06-05 18:52:35 +02:00
Julien Fontanet
527cf25d1b Explicitly depends on redis. 2015-06-05 18:50:29 +02:00
Julien Fontanet
a49594e6a5 4.0.3-alpha.1 2015-06-05 18:38:11 +02:00
Julien Fontanet
5cd22b41d6 No longer use fibers. 2015-06-05 18:34:04 +02:00
Julien Fontanet
934949c514 Remove unused dependencies. 2015-06-05 17:42:54 +02:00
Julien Fontanet
cc61e8d334 Require package.json instead of package.
Not using the extension confuses dependency-check.
2015-06-05 17:21:34 +02:00
Julien Fontanet
81be499c49 4.0.3-alpha.0 2015-06-05 16:02:35 +02:00
Julien Fontanet
082aa55566 Disable Babel experiemental transformers. 2015-06-05 15:45:57 +02:00
Julien Fontanet
c783039557 Use julien-f-unzip instead of @julien-f/unzip. 2015-06-05 15:45:49 +02:00
Julien Fontanet
206dfeb879 Use json-rpc-peer & json-rpc-protocol instead of deprecated @julien-f/json-rpc. 2015-06-05 15:00:08 +02:00
Julien Fontanet
f839e76f4b Show app-conf debug messages. 2015-06-05 15:00:08 +02:00
Julien Fontanet
c67151f922 Disable Babel minification.deadCodeElimination which has issues with Node 0.12. 2015-06-05 12:53:34 +02:00
Julien Fontanet
aedec5de18 Update source-map-support to 0.3. 2015-06-05 12:04:44 +02:00
Julien Fontanet
3df973a1ea External Babel configuration. 2015-06-03 17:34:50 +02:00
Julien Fontanet
0253031d7f Use standard with babel-eslint. 2015-06-03 17:34:49 +02:00
Olivier Lambert
f4445d4681 4.0.2 2015-06-02 20:33:49 +02:00
Olivier Lambert
e399dfa7e6 fix permissions issues 2015-06-02 18:13:54 +02:00
Olivier Lambert
787f95ac3a 4.0.1 2015-06-01 10:45:29 +02:00
Fabrice Marsaud
d2f35d46d2 Fix for xo-web Issue #264 password not saving 2015-06-01 09:34:56 +02:00
Julien Fontanet
b21d078c5d 4.0.0 2015-05-29 16:14:39 +02:00
Julien Fontanet
032c3fb856 Merge branch 'next-release' 2015-05-29 16:14:22 +02:00
Julien Fontanet
bbd79379ce Remove unused import. 2015-05-29 16:11:43 +02:00
Julien Fontanet
d01d544a0a Remove a fixed FIXME. 2015-05-29 15:23:53 +02:00
Julien Fontanet
01ecd76976 vm.ejectCd() succeed even if the VBD cannot be destroyed. 2015-05-29 15:23:36 +02:00
Julien Fontanet
26e8ae4bf3 position is now optional for vm.attachDisk(). 2015-05-29 11:55:59 +02:00
Julien Fontanet
3befdbc93d Fix group.setUsers(). 2015-05-29 10:21:49 +02:00
Olivier Lambert
c91a890d42 add ACLs in objects 2015-05-29 09:35:20 +02:00
Julien Fontanet
3369ab601a Stupide error -_-". 2015-05-28 23:26:07 +02:00
Julien Fontanet
bfe5b71f19 Various updates. 2015-05-28 23:12:32 +02:00
Julien Fontanet
eb25cf65dd acl.getCurrent() handles groups. 2015-05-28 22:42:57 +02:00
Julien Fontanet
aa1ca3be64 Various updates. 2015-05-28 22:32:30 +02:00
Julien Fontanet
a4e03daeee Various updates. 2015-05-28 22:14:20 +02:00
Julien Fontanet
cbd0b9db1d Users must also linked to their groups. 2015-05-28 20:47:42 +02:00
Julien Fontanet
621e8e89a5 Implements groups. 2015-05-28 19:26:16 +02:00
Julien Fontanet
c9eca5ec7e ACLs have a role and existing ACLs are automatically upadted. 2015-05-28 18:08:36 +02:00
Julien Fontanet
05063b76eb Initial role API. 2015-05-28 16:57:11 +02:00
Julien Fontanet
7d1d34d1eb Remove unused code. 2015-05-28 16:56:45 +02:00
Julien Fontanet
40423a0547 Set debug to xo:main only in production. 2015-05-28 14:57:04 +02:00
Julien Fontanet
682804b1ad Fix consoles proxy. 2015-05-28 12:45:37 +02:00
Julien Fontanet
790e67866c Fix VM-template.template_info.disks. 2015-05-28 12:25:59 +02:00
Julien Fontanet
8399edb4dc Fix vm.docker.version. 2015-05-28 11:42:30 +02:00
Julien Fontanet
55a1c27d6d Fix VIF creation. 2015-05-28 11:42:13 +02:00
Julien Fontanet
35bf7dc484 Better message when a object param is not found. 2015-05-28 11:41:51 +02:00
Julien Fontanet
4d3dfa1dca Typo. 2015-05-27 19:27:37 +02:00
Olivier Lambert
ac4686125f fix RRD fetching on VM view 2015-05-27 18:49:07 +02:00
Olivier Lambert
fb2ca3bb19 fix CPU parsing error in RRD 2015-05-27 18:43:26 +02:00
Julien Fontanet
c0122aace7 Various fixes. 2015-05-27 18:25:24 +02:00
Fabrice Marsaud
c141e92cc4 Group managment for ACLs (Mock. No implementation) 2015-05-27 17:22:07 +02:00
Julien Fontanet
5236441be0 New collection! 2015-05-27 17:00:35 +02:00
Olivier Lambert
94620748ab filter IOPS 2015-05-27 10:56:14 +02:00
Julien Fontanet
da70c03845 Use correct HTTP status code
See https://tools.ietf.org/html/rfc7231#section-6.5.10
2015-05-27 10:33:57 +02:00
Olivier Lambert
337eb0f27b remove useless debug 2015-05-26 19:57:10 +02:00
Olivier Lambert
26a63c4baf optimize the stats code 2015-05-26 19:43:51 +02:00
Julien Fontanet
8b03890f2a New util extractProperty() and some tests. 2015-05-26 17:59:52 +02:00
Julien Fontanet
61731e2c2e Detect blocked event loop. 2015-05-26 11:57:49 +02:00
Julien Fontanet
5c1611c484 @autobind decorator. 2015-05-25 16:50:27 +02:00
Julien Fontanet
a502965d19 Fix syntax. 2015-05-25 15:29:42 +02:00
Julien Fontanet
b55764db56 More code in Xapi. 2015-05-25 15:20:59 +02:00
Julien Fontanet
510897f672 Display error stack if available in Xo#_handleHttpRequest(). 2015-05-25 14:56:40 +02:00
Julien Fontanet
2689fd17d0 Correctly forward req & res in Xo#_handleHttpRequest(). 2015-05-25 14:56:06 +02:00
Julien Fontanet
75e3949cec Expose patch UUID. 2015-05-25 14:49:19 +02:00
Julien Fontanet
a7bb4b7104 Check for patch existence in Xapi#_getOrUploadPoolPatch(). 2015-05-25 14:44:15 +02:00
Julien Fontanet
9bcb2ac094 Fix snapshot deletion after VM export. 2015-05-25 12:51:42 +02:00
Julien Fontanet
9ab110277a Do not include conflicting patches in Xapi#listMissingPoolPatchesOnHost(). 2015-05-25 12:03:29 +02:00
Olivier Lambert
d1506bcdae start to work on patches conflicts and dependencies 2015-05-25 11:24:49 +02:00
Julien Fontanet
00c38c96cd Remove now unneeded code. 2015-05-25 10:51:58 +02:00
Julien Fontanet
9798d4ff6a Update deps. 2015-05-24 18:55:30 +02:00
Julien Fontanet
5801b29ede Do not crash when the console URL cannot be found. 2015-05-24 16:51:51 +02:00
Julien Fontanet
22ed022787 Updates @julien-f/json-rpc to 0.4.4. 2015-05-24 16:22:38 +02:00
Julien Fontanet
f7e7ecf5ae Minor coding style fix. 2015-05-24 15:24:27 +02:00
Julien Fontanet
c116d3f453 Use babel-standard instead of standard. 2015-05-24 15:24:18 +02:00
Julien Fontanet
d9b3d263ae Remove no longer used xo.$running_VMs. 2015-05-24 15:23:01 +02:00
Julien Fontanet
9a265a0437 New API method: test.wait(). 2015-05-22 16:37:26 +02:00
Julien Fontanet
16450e2133 Remove useless statement. 2015-05-22 11:58:31 +02:00
Julien Fontanet
5678742810 Fix SR editing. 2015-05-22 11:58:19 +02:00
Julien Fontanet
76d551a238 Move consoles in /api/consoles/ 2015-05-20 10:28:22 +02:00
Julien Fontanet
5467c4b1b8 Fix proxy URLs generation (fix #58). 2015-05-19 18:40:49 +02:00
Julien Fontanet
e48d277440 SPDX valid license. 2015-05-19 17:45:02 +02:00
Julien Fontanet
9d1da81557 Remove unnecessary traces. 2015-05-19 13:03:12 +02:00
Julien Fontanet
91f557ac9e TODO: sort proxies by descending prefix length. 2015-05-19 12:59:02 +02:00
Julien Fontanet
452826bd61 Missing token is not a auth provider bug. 2015-05-19 12:58:46 +02:00
Julien Fontanet
84564bb7fb WS proxy forwards the subprotocol. 2015-05-19 12:56:06 +02:00
Julien Fontanet
bf7647c737 WS proxy correctly handle subpaths. 2015-05-19 12:55:46 +02:00
Julien Fontanet
1f98d7e5ec HTTP proxy correctly handle subpaths. 2015-05-19 12:55:35 +02:00
Julien Fontanet
e4486f4c17 Fix send error messages in wsProxy. 2015-05-19 12:31:14 +02:00
Julien Fontanet
65daa23a74 Do not set rejectUnauthorized by default in wsProxy. 2015-05-19 12:27:02 +02:00
Julien Fontanet
523a30afb4 Fix initial user creation (fix #57). 2015-05-19 09:54:57 +02:00
Julien Fontanet
1d0de4584e Fix session.getUser(). 2015-05-18 11:56:37 +02:00
Julien Fontanet
7aac124407 Forward the upgrade event to connect. 2015-05-18 11:15:29 +02:00
Olivier Lambert
03e8b664ac add host stats 2015-05-17 23:08:16 +02:00
Julien Fontanet
66883ae37c Remove discouraged path for the config file from the doc. 2015-05-15 11:47:18 +02:00
Julien Fontanet
2a075d929a Xapi#_setObjectProperties() gets the object type itself. 2015-05-14 15:53:54 +02:00
Olivier Lambert
41147483d8 remove old docker method in VM and add the bootOrder method 2015-05-13 15:25:36 +02:00
Julien Fontanet
ca517784ed Fix many errors and styles. 2015-05-13 14:27:14 +02:00
Julien Fontanet
4dd3be1568 ACLs functions moved to Xo. 2015-05-13 12:00:01 +02:00
Julien Fontanet
7412d97bf3 Do not attempt to set up proxies if there are none. 2015-05-13 10:33:00 +02:00
Fabrice Marsaud
ab7b2da83b Mixed missing error import 2015-05-13 10:05:05 +02:00
Julien Fontanet
19e26729a8 Correctly initialize Xo#_httpRequestWatchers. 2015-05-12 18:49:40 +02:00
Julien Fontanet
0101365ebc Configurable proxies. 2015-05-12 18:45:21 +02:00
Julien Fontanet
1f56e63f9c Comments. 2015-05-12 18:24:52 +02:00
Julien Fontanet
883a30c7ad Patch upload reworked. 2015-05-12 17:34:13 +02:00
Julien Fontanet
6e151a9f8b Properties settings moved into xapi.js 2015-05-12 17:08:38 +02:00
Julien Fontanet
321bb299b1 Fix grammar in error message. 2015-05-12 17:03:58 +02:00
Julien Fontanet
2ca18340c7 Remove 2 unused imports. 2015-05-12 14:07:16 +02:00
Julien Fontanet
74d4237913 Fix docker methods. 2015-05-12 12:52:29 +02:00
Olivier Lambert
a4f9b9208d fix registration 2015-05-09 11:43:21 +02:00
Olivier Lambert
8fd65b7365 replace unregister by deregister 2015-05-09 00:29:16 +02:00
Olivier Lambert
4f4d0bf6aa work in progress. Find where (un)register raise UNKNOWN_XENAPI_PLUGIN_FUNCTION. Because it shouldn't. 2015-05-09 00:26:14 +02:00
Olivier Lambert
873e2aed94 enable/disable docker and more robust spec with XML parsing for docker stuff 2015-05-08 19:03:25 +02:00
Olivier Lambert
89d485e188 remove typo in id 2015-05-08 16:12:15 +02:00
Julien Fontanet
2e9870014f Fix docker.*() functions export. 2015-05-08 16:02:46 +02:00
Julien Fontanet
528529c0d1 docker.*() methods. 2015-05-08 15:51:53 +02:00
Olivier Lambert
9bddec2dfd first Docker attempt 2015-05-08 15:37:30 +02:00
Olivier Lambert
f986487df9 Update README.md 2015-05-07 19:42:34 +02:00
Julien Fontanet
99461a70e6 Remove useless $coroutine. 2015-05-06 17:53:36 +02:00
Julien Fontanet
adbbb15a92 Fix possibly long standing bug in Api#call(). 2015-05-06 17:53:20 +02:00
Julien Fontanet
d85a4c9ad4 Fix patch upload! 2015-05-06 17:37:31 +02:00
Julien Fontanet
41baea780a Xapi#_watchTask() unpack the ref if necessary. 2015-05-06 17:23:38 +02:00
Julien Fontanet
a165884bcb Fix debug message. 2015-05-06 17:23:17 +02:00
Julien Fontanet
456adc5d0b Add missing Error class. 2015-05-06 17:22:46 +02:00
Julien Fontanet
cfc42906b9 Add debug traces for patch install. 2015-05-06 16:49:26 +02:00
Julien Fontanet
738d657c8e Fix copy pasta -_-". 2015-05-06 16:19:17 +02:00
Julien Fontanet
a51452ee7c Remove unused var. 2015-05-06 16:13:58 +02:00
Julien Fontanet
5d2a41082a Fix server management due to f*** merge. 2015-05-06 16:06:04 +02:00
Julien Fontanet
f9dd00b79b Do not XO & XAPI objects. 2015-05-06 15:36:27 +02:00
Olivier Lambert
898244d04d link pool_patches to a pool 2015-05-06 15:34:57 +02:00
Julien Fontanet
33334830cc Various fixes. 2015-05-06 15:33:09 +02:00
Julien Fontanet
8503350bfd Missing dep (xo-collection). 2015-05-06 15:01:59 +02:00
Julien Fontanet
a4bb2aaf12 Missing file. 2015-05-06 15:01:26 +02:00
Julien Fontanet
da443045bf Merge branch 'patch' into next-release 2015-05-06 14:50:40 +02:00
Julien Fontanet
b9927cd48d pool.installPatch() & host.installPatch(). 2015-05-06 14:33:49 +02:00
Julien Fontanet
7af3f7e881 Xapi#_getXenUpdates() should be static. 2015-05-06 10:54:28 +02:00
Julien Fontanet
ee81febc89 Fix @debounce to work correctly with multiple instances. 2015-05-06 10:54:10 +02:00
Julien Fontanet
8146bee846 Upload to pool master. 2015-05-05 19:00:22 +02:00
Julien Fontanet
53e94378ae Correctly handle patch upload error. 2015-05-05 19:00:22 +02:00
Julien Fontanet
8592ead0e3 Missing import. 2015-05-05 19:00:22 +02:00
Olivier Lambert
67699372f2 add url parameter 2015-05-05 18:44:29 +02:00
Julien Fontanet
95a8ced558 Modularize. 2015-05-05 18:42:02 +02:00
Julien Fontanet
ae437be6e7 Memoize Xapi#listMissingHostPatches() 2015-05-05 18:41:47 +02:00
Julien Fontanet
1441d9f4ee host.listMissingPatches() & host.installPatchFromUrl(). 2015-05-05 17:06:52 +02:00
Julien Fontanet
adeb5c2344 Put high level Xen Server features in dedicated class. 2015-05-05 11:40:25 +02:00
Julien Fontanet
f0b0277b9d Fix Xen error handling (#56). 2015-05-05 10:24:13 +02:00
Olivier Lambert
6fb5fb63e7 allow to patch one host only and not the whole pool 2015-05-02 18:14:50 +02:00
Olivier Lambert
5330cc5ae9 fix a typo 2015-05-02 14:04:14 +02:00
Julien Fontanet
bcc2244fdb Various updates. 2015-04-30 18:31:42 +02:00
Julien Fontanet
ad2de95f32 Various updates. 2015-04-30 17:45:53 +02:00
Olivier Lambert
453dee33ba initial Docker control 2015-04-29 17:27:53 +02:00
Julien Fontanet
13f36b3f79 Various code simplifications and fixes. 2015-04-28 15:47:26 +02:00
Julien Fontanet
719b63ee02 Typo. 2015-04-28 14:44:44 +02:00
Julien Fontanet
38a5698f90 Use async functions instead of Bluebird.coroutine(). 2015-04-28 14:36:16 +02:00
Julien Fontanet
a05b60f48e Remove no-lone-blocks directive which is no longer needed. 2015-04-28 13:06:57 +02:00
Julien Fontanet
8694ecd417 xo: CoffeeScript to ES6 and split in multiple files. 2015-04-28 13:06:26 +02:00
Julien Fontanet
100d007271 3.9.2 2015-04-26 11:29:09 +02:00
Julien Fontanet
a1a764d807 Fix json-rpc dep. 2015-04-26 11:28:53 +02:00
Julien Fontanet
6cb30adf5d helpers.spec: CoffeeScript to ES6. 2015-04-24 17:52:05 +02:00
Julien Fontanet
9eb939e38f helpers: CoffeeScript to ES6. 2015-04-24 17:16:43 +02:00
Julien Fontanet
3e26060979 fibers-urils: CoffeeScript to ES6 and tests fixed. 2015-04-24 14:01:12 +02:00
Julien Fontanet
cc60aa7b84 Remove Coffeelint as it is not really used. 2015-04-24 11:23:04 +02:00
Julien Fontanet
bdfdafaec0 Install json-rpc from the npm repository. 2015-04-24 10:58:49 +02:00
Julien Fontanet
7737dc6b6c Convert last ES5 files to ES6. 2015-04-22 14:28:30 +02:00
Julien Fontanet
1a89465201 Replace AMAP var by const. 2015-04-22 14:10:04 +02:00
Julien Fontanet
fe3ce45b8e Remove unused dep. 2015-04-21 14:40:12 +02:00
Julien Fontanet
7af0883f08 gulp-sourcemaps is a dev dep. 2015-04-21 14:39:20 +02:00
Julien Fontanet
f48c21b124 Remove unused code. 2015-04-21 14:38:21 +02:00
Julien Fontanet
abc2e74f2c 3.9.1 2015-04-21 11:10:11 +02:00
Julien Fontanet
6130c49b83 Merge branch 'next-release' 2015-04-21 11:10:05 +02:00
Julien Fontanet
433b58511c Fix VM_guest_metrics. 2015-04-20 19:47:58 +02:00
Olivier Lambert
b04111c79b add snapshot type for certain VM actions 2015-04-20 18:30:26 +02:00
Olivier Lambert
06ca0079b3 better error message if host unreach 2015-04-20 18:30:02 +02:00
Julien Fontanet
ff53c6b49d 3.9.0 2015-04-20 17:40:09 +02:00
Julien Fontanet
da58458fb7 Merge branch 'next-release' 2015-04-20 17:40:01 +02:00
Julien Fontanet
1a21989ad1 Use lodash.includes instead of lodash.contains. 2015-04-20 16:26:14 +02:00
Julien Fontanet
76d54b8914 Update to make-error v1. 2015-04-20 12:58:46 +02:00
Julien Fontanet
d22d64f68c Minor fixes. 2015-04-18 19:35:27 +02:00
Julien Fontanet
580ae005f4 Fix some compilation errors. 2015-04-18 19:35:18 +02:00
Julien Fontanet
75ab9d2e8c Use Babel 5. 2015-04-18 19:34:58 +02:00
Fabrice Marsaud
6c246768e9 Fixed broken graphs after reboot #228 2015-04-15 18:45:16 +02:00
Olivier Lambert
bc75bc199b remove admin permission for stats 2015-04-15 14:19:08 +02:00
Julien Fontanet
f234b6a540 Better error message when authentication fails in server.connect(). 2015-04-14 18:15:21 +02:00
Julien Fontanet
bff8bfea7b server.add() does not wait for the connection. 2015-04-14 17:01:08 +02:00
Julien Fontanet
48bf0d1942 Do not compact the dev build. 2015-04-14 15:24:52 +02:00
Julien Fontanet
04bbb84845 Typos. 2015-04-14 15:24:28 +02:00
Julien Fontanet
311f8cd00f pool.$id not pool.id!! 2015-04-14 11:45:06 +02:00
Julien Fontanet
ed0ab78048 Better error messages in XO#getXAPI(). 2015-04-14 11:40:46 +02:00
Julien Fontanet
0eec1c1f61 Use standard style. 2015-04-13 18:33:15 +02:00
Julien Fontanet
b4a3b832dc Update to latest xen-api. 2015-04-13 17:47:16 +02:00
Julien Fontanet
8e7830dd7d Minor fixes. 2015-04-13 16:50:00 +02:00
Julien Fontanet
d32a18d965 Increment VIF # when creating a VM (fix #229). 2015-04-13 16:27:53 +02:00
Julien Fontanet
7d00d47cb6 Port XAPI code to xen-api. 2015-04-13 16:21:44 +02:00
Julien Fontanet
b0853eb119 ES6 style. 2015-04-13 16:21:44 +02:00
Julien Fontanet
25d29fb389 server: CoffeeScript → ES6. 2015-04-13 16:21:44 +02:00
Olivier Lambert
ed241ede9d fix force reboot method 2015-04-13 13:12:13 +02:00
Julien Fontanet
04a27d5778 Authentication providers can throw null. 2015-04-10 11:20:35 +02:00
Fabrice Marsaud
70a2067a06 Fixed date in vm stat data 2015-04-08 12:54:26 +02:00
Fabrice Marsaud
4a13c01817 First stat view delivery for #228 2015-04-08 12:54:26 +02:00
Fabrice Marsaud
151c2b573c vm stats code enhancment 2015-04-08 12:53:34 +02:00
Olivier Lambert
4cdb3f4c6a allow variable CPUs number 2015-04-08 12:53:34 +02:00
Olivier Lambert
3cf0384bc5 use a promise 2015-04-08 12:53:34 +02:00
Olivier Lambert
ad2f6ebe93 add npm dependencies 2015-04-08 12:53:34 +02:00
Olivier Lambert
178a429f26 start to work on RRD 2015-04-08 12:52:56 +02:00
Olivier Lambert
71194d5b4e use get dedicated call to avoid confusions between UUIDs and Refs 2015-04-01 21:54:34 +02:00
Olivier Lambert
771c530b85 add createNetwork method for issue https://github.com/vatesfr/xo-web/issues/225 2015-04-01 21:29:26 +02:00
Olivier Lambert
78a6b622fb refactoring of VM auto power 2015-04-01 15:40:08 +02:00
Olivier Lambert
0177bbebe0 autopower VM feature, see https://github.com/vatesfr/xo-web/issues/224 2015-04-01 15:22:25 +02:00
Olivier Lambert
8deed4a9cd better handling of other_config value 2015-04-01 14:56:33 +02:00
Julien Fontanet
60b2576ce8 Fix #53. 2015-03-30 23:24:50 +02:00
Julien Fontanet
90cc58a8fe Fix authentication providers iteration. 2015-03-30 18:37:41 +02:00
Julien Fontanet
d79f750e30 Add missing files to distribution. 2015-03-30 18:09:36 +02:00
Julien Fontanet
9f9ab01508 Proper compilation phase (fix #50). 2015-03-30 17:58:13 +02:00
Olivier Lambert
4dc89c9082 add local LVM SR creation 2015-03-30 12:02:09 +02:00
Olivier Lambert
500349a8bd start to implement vgpus and pgpus 2015-03-29 14:25:08 +02:00
Olivier Lambert
e299f3e510 initial work on PCI attach devices 2015-03-29 01:48:48 +01:00
Julien Fontanet
6114f4644f 3.8.0 2015-03-27 15:13:15 +01:00
Julien Fontanet
eb664404e1 Update proxy-http-request. 2015-03-27 12:51:11 +01:00
Olivier Lambert
370f645cf0 add createInterface in VM object 2015-03-26 00:14:56 +01:00
Julien Fontanet
bd5b18a163 Minor changes. 2015-03-25 17:20:25 +01:00
Olivier Lambert
c51b0c6a41 add destroy task 2015-03-25 17:07:07 +01:00
Julien Fontanet
d56f6e75f9 Fix host.*(). 2015-03-25 15:22:24 +01:00
Olivier Lambert
c743348872 add setter for VBD position 2015-03-25 11:26:37 +01:00
Olivier Lambert
578049bfb6 fix wrong case VDI call 2015-03-23 15:56:24 +01:00
Julien Fontanet
564cd37628 Only send objects notifs to authenticated clients (2/2). 2015-03-20 13:37:35 +01:00
Julien Fontanet
60ecf91935 Only send objects notifs to authenticated clients. 2015-03-20 13:22:28 +01:00
Julien Fontanet
7b452e93b2 xo.getAllObjects() requires authentication. 2015-03-20 10:36:00 +01:00
Julien Fontanet
0bc1f7bf8c New disk.create(). 2015-03-19 17:17:03 +01:00
Olivier Lambert
62c2421d85 already implemented in vm with attach disk 2015-03-19 16:41:07 +01:00
Olivier Lambert
f2edf56d02 add method description 2015-03-19 16:27:52 +01:00
Olivier Lambert
376e5aeb45 add VBD create method 2015-03-19 16:23:04 +01:00
Julien Fontanet
3d1c7e0bc1 Fix sr.set(). 2015-03-19 15:18:10 +01:00
Julien Fontanet
a2603f882d Remove unused code in fiber-utils. 2015-03-19 14:31:47 +01:00
Julien Fontanet
0c1dcafc35 Cast model ids to string. 2015-03-19 13:24:05 +01:00
Julien Fontanet
d5108f8007 Remove dead code. 2015-03-19 13:23:42 +01:00
Julien Fontanet
ebeca9aa04 Fix token.create(). 2015-03-19 12:07:38 +01:00
Julien Fontanet
7307d9f7f1 Collection returns either an array of properties or an instance of Model. 2015-03-19 11:32:33 +01:00
Julien Fontanet
eac7cdae1c Fix handling of no password in PasswordAuthenticationProvider. 2015-03-19 11:24:21 +01:00
Julien Fontanet
7e548cb133 Coding style. 2015-03-19 11:23:51 +01:00
Julien Fontanet
0a61512fc7 Clean fiber-utils. 2015-03-19 11:05:30 +01:00
Julien Fontanet
479f2010a9 Restore session.signOut(). 2015-03-18 18:02:43 +01:00
Julien Fontanet
55796932c4 Better handling of plugin short names + Debug traces. 2015-03-18 17:38:59 +01:00
Julien Fontanet
9267adf79a Create an authenticated user if necessary. 2015-03-18 17:09:00 +01:00
Julien Fontanet
cc2f86cb06 A user can be created without a password. 2015-03-18 17:08:34 +01:00
Julien Fontanet
14c6895135 User permission default to none. 2015-03-18 17:08:19 +01:00
Julien Fontanet
83f6647352 Extend Babel ignore regexp. 2015-03-18 17:07:45 +01:00
Julien Fontanet
792ecee399 FIXME: VM restarting is not properly handled in console. 2015-03-18 14:17:45 +01:00
Julien Fontanet
93a1ef6bdb session.getUser() description. 2015-03-18 14:16:59 +01:00
Julien Fontanet
62607b16f8 session.signIn() 2015-03-18 14:16:44 +01:00
Julien Fontanet
ca60376447 Password & Token auth providers. 2015-03-18 14:16:28 +01:00
Julien Fontanet
3cc4b5db79 Initial plugin system (#37). 2015-03-18 14:14:08 +01:00
Julien Fontanet
f8179c83e7 Fix signInWithPassword(). 2015-03-17 19:05:10 +01:00
Julien Fontanet
b2233f61e4 Use proxy-http-request. 2015-03-17 19:05:10 +01:00
Julien Fontanet
a7e2f776e4 Update deps. 2015-03-17 19:05:09 +01:00
Olivier Lambert
50d672892c Enable Babel for XO-Server files 2015-03-17 16:07:26 +01:00
Julien Fontanet
e6154db6e5 API session: CoffeeScript → ES6. 2015-03-09 15:28:20 +01:00
Julien Fontanet
34e8f57f7d 3.7.0 2015-03-06 17:04:46 +01:00
Julien Fontanet
4a8c089fa9 Merge branch 'next-release' 2015-03-06 17:02:38 +01:00
Julien Fontanet
b3aa5ee247 Fix various vm.*() methods. 2015-03-06 16:52:50 +01:00
Julien Fontanet
64bf98a7d3 Fix VM creation. 2015-03-06 16:45:30 +01:00
Julien Fontanet
e8e38beeb8 Fix task.cancel(). 2015-03-06 16:31:49 +01:00
Julien Fontanet
de96a96ac6 Use babel instead of legacy 6to5 for the test runner. 2015-03-06 15:32:38 +01:00
Julien Fontanet
ee3ad17163 Minor fix. 2015-03-06 15:31:25 +01:00
Julien Fontanet
fafd8a5d51 Fix sr.reattachIso(). 2015-03-06 15:30:33 +01:00
Julien Fontanet
c5879f17f8 ACLs for task.cancel(). 2015-03-06 15:30:23 +01:00
Julien Fontanet
999cbd314c Fix vif.connect(). 2015-03-06 15:21:15 +01:00
Julien Fontanet
a9d34a223a Fix VM-snapshot authorization. 2015-03-06 15:18:50 +01:00
Julien Fontanet
3381030ed5 Add VIF authorization. 2015-03-06 15:09:35 +01:00
Julien Fontanet
2bacc6cfe8 vm.clone() is only authorized for admins. 2015-03-06 15:09:22 +01:00
Julien Fontanet
973c936514 Fix VBD authorization. 2015-03-06 14:59:39 +01:00
Julien Fontanet
c5121a7fc5 More ACLs. 2015-03-06 14:55:25 +01:00
Julien Fontanet
37bf0f6b53 ACLs for message.*(). 2015-03-06 14:27:42 +01:00
Julien Fontanet
203d51cdbf ACLs for host.*(). 2015-03-06 14:15:17 +01:00
Julien Fontanet
9669d8eb8b ACLs for vm.*(). 2015-03-06 14:10:03 +01:00
Julien Fontanet
1b7571be5b Improve authorizations checking. 2015-03-06 14:10:03 +01:00
Julien Fontanet
614ff2a30e Update to latest http-server-plus. 2015-03-06 14:10:03 +01:00
Julien Fontanet
5b11671997 Snapshots inherit ACLs of their VM. 2015-03-06 14:10:03 +01:00
Julien Fontanet
5158e08901 Properly handle optional params in resolve. 2015-03-06 14:10:03 +01:00
Julien Fontanet
2cb9c7211e Some types do not require explicit authorization. 2015-03-06 14:10:03 +01:00
Olivier Lambert
24e26c95ff add reattach iso and fix reattach 2015-03-05 16:30:50 +01:00
Julien Fontanet
b8286af8fa Gracefully shutdown on SIGINT & SIGTERM. 2015-03-05 16:15:58 +01:00
Olivier Lambert
735279c27c ACLization of SR object 2015-03-04 17:36:53 +01:00
Olivier Lambert
d75be22d1f add reattach for iscsi and nfs 2015-03-04 17:09:57 +01:00
Olivier Lambert
40f1b1c665 add probeNfsExists and fix array 2015-03-04 16:53:56 +01:00
Olivier Lambert
d076c875ed fix bad parameter name 2015-03-04 15:28:34 +01:00
Olivier Lambert
771c7fe863 fix serverPath parameter 2015-03-04 15:08:21 +01:00
Olivier Lambert
369454c12a return UUID of createIscsi and createNfs created 2015-03-04 14:57:23 +01:00
Olivier Lambert
1784eacf58 ACLization of PIF object 2015-03-04 14:54:12 +01:00
Olivier Lambert
e86f5b3b7c return UUID of createISO created 2015-03-04 14:53:35 +01:00
Olivier Lambert
80ff6cda04 ACLization of PBD object 2015-03-04 14:24:58 +01:00
Julien Fontanet
dff96cfd95 Remove a proxy request after one use. 2015-03-04 14:04:16 +01:00
Julien Fontanet
31d244ef78 Remove an unnecessary import. 2015-03-04 14:04:16 +01:00
Olivier Lambert
8325a84ab2 fix typo in PBD class 2015-03-04 13:48:46 +01:00
Olivier Lambert
793839c7d5 new acl model for import 2015-03-03 19:28:46 +01:00
Olivier Lambert
23cf87dbc0 add requiered parameters 2015-03-03 11:53:04 +01:00
Julien Fontanet
7171de336d Remove an unnecessary import. 2015-03-02 13:31:45 +01:00
Julien Fontanet
e206cfe6d6 Fix acl.get(). 2015-03-02 13:31:17 +01:00
Julien Fontanet
1a71cc9223 Implement method.resolve
API methods can now have some of their objects automatically fetched:

```
method.resolve = {
	vm: ['vm', 'VM'],
};
```

The key is the name of the property with which the object will be
attached in the parameters dictionary, the first entry of the array is
the name of the parameter to use to fetch the object and the second
the expected type/types of the object.

Note that permissions are automatically checked via the ACLs.
2015-03-02 13:27:37 +01:00
Julien Fontanet
ed6fcf5ae7 Move ensureArray() in utils. 2015-02-27 17:31:11 +01:00
Olivier Lambert
bb31693a4d fix array check 2015-02-27 15:00:02 +01:00
Olivier Lambert
d15c8b16f3 add NFS SR 2015-02-26 23:15:58 +01:00
Olivier Lambert
8b9c932b80 add optionnal port 2015-02-26 22:34:52 +01:00
Olivier Lambert
c10e8f5f9a handle correctly iSCSI exists 2015-02-26 15:55:35 +01:00
Olivier Lambert
cd27e43994 iscsi target probe 2015-02-26 14:17:17 +01:00
Olivier Lambert
20614cf64b start to work on iscsi SR 2015-02-26 13:16:44 +01:00
Julien Fontanet
c69c02bcb3 formatXml() & parseXml(). 2015-02-26 12:58:27 +01:00
Julien Fontanet
21b177cbb4 Cannot use arrow function here. (fix #44) 2015-02-26 10:23:23 +01:00
Olivier Lambert
31a7e48768 add methods for SR: createIso, destroy and forget 2015-02-25 18:10:10 +01:00
Julien Fontanet
fd3d60ed7d Fix vdi.migrate() & vdi.delete() parameters. 2015-02-25 17:10:08 +01:00
Julien Fontanet
e2e369a463 Cannot use arrow functions. 2015-02-25 16:08:55 +01:00
Julien Fontanet
0c3304f041 API sr.*() converted to ES6. 2015-02-25 16:07:10 +01:00
Julien Fontanet
0d4b9b4bce Update Api code. 2015-02-24 18:28:56 +01:00
Julien Fontanet
1192bf6a87 The identifier of an object is its UUID not its ref. 2015-02-23 18:37:01 +01:00
Julien Fontanet
2366a91e8d Shameful typo (fix consoles). 2015-02-23 17:44:46 +01:00
Julien Fontanet
2c4e46c630 Rename acl.* methods and always accept identifiers. 2015-02-19 16:28:16 +01:00
Julien Fontanet
a989e296b0 Missing file. 2015-02-19 12:54:53 +01:00
Julien Fontanet
26648dbcd2 Remove debug trace. 2015-02-19 12:46:44 +01:00
Julien Fontanet
147d759d35 Minor fixes. 2015-02-19 12:45:49 +01:00
Julien Fontanet
f6b07c5609 Remove no longer relevant comment. 2015-02-19 12:40:42 +01:00
Julien Fontanet
29fa7a053f Minor fixes. 2015-02-19 12:40:31 +01:00
Julien Fontanet
f380f245c6 Minor fix. 2015-02-18 16:06:40 +01:00
Julien Fontanet
1824a30cde 6to5 has been renamed Babel. 2015-02-18 14:52:56 +01:00
Julien Fontanet
8ab86fd6bb Remove unused deps. 2015-02-18 14:48:11 +01:00
Julien Fontanet
2c7bdc54c1 Minor fixes. 2015-02-18 13:35:28 +01:00
Julien Fontanet
39b57da42b Remove a debugging trace. 2015-02-18 11:58:59 +01:00
Julien Fontanet
a98a9fd97a Remove unused memory collection implementation. 2015-02-18 11:58:36 +01:00
Julien Fontanet
093a5c1019 Minor fixes. 2015-02-18 11:56:11 +01:00
Julien Fontanet
ba25acaab9 Redis implementation converted to ES6. 2015-02-18 11:53:51 +01:00
Julien Fontanet
9783802370 ACL model and collection. 2015-02-17 18:15:00 +01:00
Julien Fontanet
47bb02ac08 Minor fixes. 2015-02-17 18:14:32 +01:00
Julien Fontanet
1f952d81aa API methods never return undefined. 2015-02-17 18:13:26 +01:00
Julien Fontanet
0632019e44 Collection & Model rewritten in ES6. 2015-02-17 17:01:12 +01:00
Julien Fontanet
bc14d0f580 Revert problematic changes. 2015-02-12 18:06:59 +01:00
Julien Fontanet
4bc3998010 Update EditorConfig. 2015-02-12 18:03:50 +01:00
Julien Fontanet
cd9c2d1988 Remove unused $waitEvent(). 2015-02-12 16:54:50 +01:00
Julien Fontanet
5d597c22bf Remove $fiberize in favor of $coroutine. 2015-02-12 16:52:09 +01:00
Julien Fontanet
574144f9b1 Minor fix in tests. 2015-02-12 16:01:40 +01:00
Julien Fontanet
b26bee3524 Fix tests using ES6. 2015-02-12 15:58:44 +01:00
Julien Fontanet
c835cf7829 utils: CoffeeScript → ES6. 2015-02-12 15:58:29 +01:00
Julien Fontanet
a1d819dcb6 Remove unused code. 2015-02-12 14:47:36 +01:00
Julien Fontanet
2234cc9334 Always use let instead of var in ES6. 2015-02-12 14:18:40 +01:00
Julien Fontanet
cabd1506b7 Remove unused import. 2015-02-12 13:45:29 +01:00
Julien Fontanet
a7c2e321bf Remove incorrectly saved dep. 2015-02-12 13:45:16 +01:00
Julien Fontanet
31b75179bd Remove unused dep. 2015-02-12 13:32:19 +01:00
Julien Fontanet
06152f3131 Use graceful-fs. 2015-02-12 12:28:06 +01:00
Julien Fontanet
c82f8c997f Use fs-promise. 2015-02-12 12:27:48 +01:00
Julien Fontanet
f06840f4b8 Main startup file: CoffeeScript → ES6. 2015-02-12 11:54:36 +01:00
Julien Fontanet
11c3d6d056 Async methods must return promises. 2015-02-12 11:40:45 +01:00
Julien Fontanet
814a566845 Update to http-server-plus 0.5. 2015-02-12 11:32:09 +01:00
Julien Fontanet
f3bcaf2710 Typo. 2015-02-11 11:05:30 +01:00
Julien Fontanet
f9dba9266f Fix: NoSuchMethod → MethodNotFound. 2015-02-11 11:05:30 +01:00
Olivier Lambert
00f7df3982 add bios strings 2015-02-10 23:32:54 +01:00
Olivier Lambert
b873a77409 add task cancel 2015-02-05 13:40:49 +01:00
Julien Fontanet
cd5a26398a Fix host.tasks. 2015-02-05 11:18:20 +01:00
Julien Fontanet
5e8a614d82 Unregister task watchers when finished. 2015-02-04 16:34:17 +01:00
Julien Fontanet
bad601edb1 Main file is now much cleaner. 2015-02-04 16:30:37 +01:00
Julien Fontanet
58297219a8 Update deps. 2015-02-04 15:06:03 +01:00
Julien Fontanet
087b191e5b More traces during startup. 2015-02-04 15:05:56 +01:00
Julien Fontanet
de4468a15a Enable console for hosts. 2015-02-04 13:54:43 +01:00
Julien Fontanet
b73de087d2 Console proxy (fix vatesfr/xo#14). 2015-02-03 17:39:12 +01:00
Julien Fontanet
39fd092055 Update .jshintrc. 2015-02-03 15:38:07 +01:00
Julien Fontanet
dec88bd601 Correctly handle WebSocket send errors. 2015-02-03 14:34:15 +01:00
Julien Fontanet
bc9975baa1 Fix connection double closing. 2015-02-03 14:33:39 +01:00
Julien Fontanet
d2d401883e Do not display “starting...” log when --help. 2015-01-30 10:59:50 +01:00
Julien Fontanet
3b3ac1688a Update deps. 2015-01-27 18:28:44 +01:00
Julien Fontanet
6bb0ca22d0 Enable ES6. 2015-01-27 18:26:32 +01:00
Julien Fontanet
e157bc7b97 Fix JSON-RPC. 2015-01-24 15:27:33 +01:00
Julien Fontanet
578da0f1a7 Fix JSON-RPC. 2015-01-24 15:27:11 +01:00
Olivier Lambert
8f0dd0b0c6 manage suspend/resume 2015-01-12 14:04:52 +01:00
Julien Fontanet
9e05bc4fad Compatibility with Node 0.11.14. 2015-01-09 18:12:55 +01:00
Olivier Lambert
4cee341ce5 solve the issue https://github.com/vatesfr/xo-web/issues/126 2015-01-02 16:52:22 +01:00
Olivier Lambert
c40192aa46 Update README.md 2014-12-29 20:43:51 +01:00
Olivier Lambert
651748cd4e add VDI migration on another SR 2014-12-27 20:30:35 +01:00
Olivier Lambert
9e8f8357b1 Authorize storage motion to a host with different CPU config 2014-12-19 16:37:56 +01:00
Julien Fontanet
3cc4c07fa1 3.6.0 2014-11-28 17:23:41 +01:00
Julien Fontanet
8ed52af203 Merge branch 'next-release' 2014-11-28 17:23:17 +01:00
Julien Fontanet
8e9a941b5d Minor comments & coding style. 2014-11-28 15:50:12 +01:00
Olivier Lambert
4538a4d33a add pif object 2014-11-24 21:01:51 +01:00
Olivier Lambert
a8469456ce add VIF management 2014-11-24 20:42:57 +01:00
Julien Fontanet
6ca25f913a Do not log API/XAPI parameters (might be sensitive). 2014-11-24 18:53:03 +01:00
Julien Fontanet
da74c7df8a Improve API logging. 2014-11-24 18:52:25 +01:00
Julien Fontanet
278aa87753 Various API cleanup. 2014-11-24 18:39:14 +01:00
Olivier Lambert
0f77294718 add pbd plug and unplug 2014-11-24 18:28:11 +01:00
Olivier Lambert
38df160a5e add patches in the data model 2014-11-24 13:55:03 +01:00
Olivier Lambert
dc61a3307c resolve a snapshot rename bug 2014-11-20 15:21:38 +01:00
Olivier Lambert
2724f8d3c5 add position to VBD 2014-11-19 20:14:47 +01:00
Olivier Lambert
36e1c1eff0 add vbd connect method 2014-11-19 20:14:29 +01:00
Olivier Lambert
81f830fe23 remove bootable because xo-cli can't send boolean 2014-11-19 15:10:41 +01:00
Olivier Lambert
6a42482b92 try to add diskattach feature 2014-11-16 15:35:53 +01:00
Olivier Lambert
8be83c278b first work 2014-11-14 18:28:58 +01:00
Olivier Lambert
eb82980cbc fix small bug 2014-11-11 13:37:48 +01:00
Olivier Lambert
08a6d28868 modify variable ha name 2014-11-10 17:29:48 +01:00
Olivier Lambert
37afbc8e9d expose higher level call for HA on a VM: enable or disable it 2014-11-10 17:01:47 +01:00
Olivier Lambert
12546b3f17 add the possibility to get and set HA for a VM 2014-11-10 16:12:13 +01:00
Julien Fontanet
70821c5d26 Fix XAPI double logins. 2014-11-08 16:23:26 +01:00
Julien Fontanet
2f7725c5a9 Debug traces in XAPI. 2014-11-08 15:03:57 +01:00
Julien Fontanet
a96d26c3bd Handle ETIMEDOUT error. 2014-11-08 14:50:28 +01:00
Julien Fontanet
a9f37c9238 Use the make-error package. 2014-10-11 18:11:35 +02:00
Olivier Lambert
239ebcdcb8 pool.patch() also applies the patch. 2014-10-01 15:12:34 +02:00
Olivier Lambert
9e6e62c5e8 Use task watching in vm.export(). 2014-10-01 15:12:04 +02:00
Olivier Lambert
3a0e9f422e Use task ref instead of UUIDs and minor fixes. 2014-10-01 15:11:44 +02:00
Julien Fontanet
ef8beb9310 xo.watchTask(uuid) 2014-10-01 14:27:15 +02:00
Julien Fontanet
260094a666 Add separators in spec. 2014-10-01 14:04:28 +02:00
Julien Fontanet
659f9a8f18 Add generic id field. 2014-10-01 14:04:28 +02:00
Olivier Lambert
cf2e3c8018 add basic patch upload 2014-10-01 14:01:03 +02:00
Olivier Lambert
a05901e792 add a small todo 2014-09-26 22:33:16 +02:00
Olivier Lambert
0928ec5c4c vm.export() works for running VM 2014-09-26 13:26:03 +02:00
Julien Fontanet
ef843d02c0 vm.snapshot() returns the ref of the new snapshot. 2014-09-26 11:03:46 +02:00
Julien Fontanet
07cdc6bf2f Merge branch 'file-transfer' into next-release 2014-09-25 15:36:04 +02:00
Julien Fontanet
832c1ef83c Better handling of proxy requests errors. 2014-09-24 20:30:26 +02:00
Julien Fontanet
f5d0fc8672 Put API debug in the correct file. 2014-09-24 19:40:43 +02:00
Julien Fontanet
4c0c917fb5 Minor coding style update. 2014-09-24 17:26:18 +02:00
Julien Fontanet
9e5ac261e2 Can proxy a request using a different method. 2014-09-24 17:25:45 +02:00
Olivier Lambert
06abfc4337 add PUT method 2014-09-24 17:10:16 +02:00
Olivier Lambert
c9c54200aa minor fix 2014-09-24 14:55:30 +02:00
Julien Fontanet
c5f948099d Parameter names should be more semantic. 2014-09-24 14:14:31 +02:00
Julien Fontanet
a78cb59bc3 Fix operations on VM snapshots (fixes vatesfr/xo-web#147 and fixes vatesfr/xo-cli#5). 2014-09-24 10:43:32 +02:00
Julien Fontanet
fb5ff24747 Minor improvements in proxy request. 2014-09-24 09:36:45 +02:00
Julien Fontanet
acb2ede658 Proxy requests. 2014-09-23 17:30:19 +02:00
Julien Fontanet
05b1bffeef Small fix in token generation. 2014-09-22 13:55:08 +02:00
Julien Fontanet
b330b55054 Various updates. 2014-09-22 12:51:56 +02:00
Julien Fontanet
9a67e63e9b Proper constructor for XO. 2014-09-15 22:48:35 +02:00
Julien Fontanet
0d629a5385 Minor fix. 2014-09-15 22:47:51 +02:00
Julien Fontanet
4e6a214581 Comments. 2014-09-15 19:17:05 +02:00
Julien Fontanet
333c591771 Parallelization of first connection, logs & fixes. 2014-09-15 19:03:15 +02:00
Julien Fontanet
e6645101ad Minor debug update. 2014-09-15 17:30:37 +02:00
Julien Fontanet
48788986cb Missing file api-errors.js 2014-09-15 16:43:40 +02:00
Julien Fontanet
d400beffb6 Various fixes. 2014-09-15 16:42:43 +02:00
Julien Fontanet
7762135cb1 More debugging logs. 2014-09-15 16:42:30 +02:00
Julien Fontanet
d0d594df69 http-server-plus v0.3.0 2014-09-15 16:01:03 +02:00
Julien Fontanet
ed97f2d786 Code not used due to event-to-promise. 2014-09-15 15:21:57 +02:00
Julien Fontanet
f3a724237f Minor bug fix. 2014-09-15 01:40:27 +02:00
Julien Fontanet
aeb3a308a8 Coding style updates in fiber utils. 2014-09-15 01:26:33 +02:00
Julien Fontanet
62cc97bc44 Rename $promisify() to $coroutine() to better align with Bluebird. 2014-09-14 22:43:59 +02:00
Julien Fontanet
20928c7a7f Typo. 2014-09-14 22:40:51 +02:00
Julien Fontanet
6f4a865604 Proper error classes for API. 2014-09-14 22:40:30 +02:00
Julien Fontanet
e7919416d5 WiP. 2014-09-12 20:56:17 +02:00
Julien Fontanet
cf8a512af4 Add coffeelint config. 2014-09-12 20:55:40 +02:00
Julien Fontanet
3efef7bbbb Use lodash.forEach() instead of own code. 2014-09-12 15:58:27 +02:00
Julien Fontanet
65d8e01529 Fix error when deleting users without tokens (fix #34). 2014-09-12 15:40:10 +02:00
Julien Fontanet
e0dd7a9b4b Minor fixes. 2014-09-12 11:31:53 +02:00
Julien Fontanet
3a32d220dd Missing dep. 2014-09-12 11:29:43 +02:00
Julien Fontanet
5efaf7b23e Underscore.js is no more. 2014-09-12 11:29:36 +02:00
Julien Fontanet
91375a5447 More coding style & lodash. 2014-09-11 18:56:03 +02:00
Julien Fontanet
f6574d346c Coding style updates & use lodash. 2014-09-11 13:48:40 +02:00
Olivier Lambert
b9b29b11d4 add XenStorage motion with automatic parameters 2014-09-03 16:18:55 +02:00
Julien Fontanet
54d062156d Check object type before using them. 2014-09-02 17:53:34 +02:00
Olivier Lambert
4dc24e48d8 Interpool migration 2014-09-02 17:38:40 +02:00
Julien Fontanet
3a24464c90 Better detection of host.power_state. 2014-08-28 11:42:12 +02:00
Olivier Lambert
f2ad9395fc add start method for h host 2014-08-27 19:17:44 +02:00
Julien Fontanet
61539416e5 Fix invalid session in XAPI connection (should fix #20). 2014-08-27 19:10:45 +02:00
Julien Fontanet
ecf7c1f3f3 Update deps. 2014-08-27 12:01:03 +02:00
Julien Fontanet
d18886ed7c Hung XAPI calls timeout after 10 seconds. 2014-08-27 11:58:07 +02:00
Julien Fontanet
07cc529f6e Ignore config files of unknown formats. 2014-08-27 11:57:15 +02:00
Julien Fontanet
01b87bfbfd 3.5.0 2014-08-14 16:30:30 +02:00
Julien Fontanet
b126f0cb79 systemd configuration 2014-08-14 16:26:56 +02:00
Julien Fontanet
10c6901a04 No longer relevant 2014-08-14 16:10:08 +02:00
Julien Fontanet
c26efb111e 3.5.0-alpha2 2014-08-14 16:06:35 +02:00
Julien Fontanet
f07cbe0087 Config file can now be in /etc/xo-server & basic help message. 2014-08-14 16:05:23 +02:00
Julien Fontanet
7d72e196e0 Remove unused deps. 2014-08-12 16:13:28 +02:00
Julien Fontanet
9bc935da96 Update JSHint config. 2014-08-12 16:13:28 +02:00
76 changed files with 6746 additions and 5805 deletions

15
.babelrc Normal file
View 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"
]
}

View File

@@ -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
View File

@@ -1,2 +1,5 @@
/dist/
/node_modules/
npm-debug.log
.xo-server.*

205
.jshintrc
View File

@@ -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
}

View File

@@ -1,5 +1,7 @@
# Xen Orchestra Server
![](http://i.imgur.com/HVFMrTk.png)
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?

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env node
'use strict';
'use strict'
//====================================================================
// ===================================================================
require('exec-promise')(require('../'));
require('exec-promise')(require('../'))

1
config/.gitignore vendored
View File

@@ -1 +0,0 @@
/local.yaml

63
gulpfile.js Normal file
View 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'))

View File

@@ -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')

View File

@@ -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"
}
}

View File

@@ -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()

View File

@@ -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.

View File

@@ -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}

View File

@@ -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
View 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)
}
}

View File

@@ -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
View 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
View 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
View 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
View 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 }
}

View File

@@ -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;

View File

@@ -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
View 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
View 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
View 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']
}

View File

@@ -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
View 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
View File

@@ -0,0 +1,3 @@
export async function getAll () {
return await this.getRoles()
}

View File

@@ -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
View 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'
}
}

View File

@@ -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
View 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'

View File

@@ -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
View 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
View 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
View 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'
}
}

View File

@@ -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
View 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' }
}

View File

@@ -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
View 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 }
}

View File

@@ -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

View File

@@ -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
View 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
View File

@@ -0,0 +1,5 @@
export function getAllObjects () {
return this.getObjects()
}
getAllObjects.permission = ''

View File

@@ -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
)
}
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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
View 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
View 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)
})
})

View File

@@ -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
}

View File

@@ -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();
})();
});
});

View File

@@ -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
}

View File

@@ -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
View 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')
}

View File

@@ -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'

View File

@@ -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
View 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
View 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
View 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
View 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
View 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
}
}

View File

@@ -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

View File

@@ -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.

View File

@@ -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
View 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
View 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
View 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
View 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')
}
}

View File

@@ -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
View 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)
}
// =================================================================
}

View File

@@ -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
View 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
View 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