Compare commits

..

212 Commits

Author SHA1 Message Date
Pierre Donias
19d191a472 feat(xo-web): 5.31.0 2018-11-29 14:15:55 +01:00
Pierre Donias
d906fec236 feat(xo-server): 5.31.0 2018-11-29 14:15:18 +01:00
Pierre Donias
552482275d feat(xo-server-usage-report): 0.7.1 2018-11-29 14:13:54 +01:00
Pierre Donias
f06d40cf95 feat(xo-server-perf-alert): 0.2.0 2018-11-29 14:13:03 +01:00
Pierre Donias
cf3f1a1705 feat(xen-api): 0.22.0 2018-11-29 14:10:42 +01:00
Pierre Donias
08583c06ef feat({xo-server,xo-web}/VM): pause/unpause (#3731)
Fixes #3727
2018-11-29 11:51:42 +01:00
Julien Fontanet
5271a5c984 chore(xo-web/addSubscriptions): dont use deprecated componentWillMount (#3734) 2018-11-29 11:17:46 +01:00
Julien Fontanet
e69610643b feat(xo-server/xosan): use XOSAN as VM tag (#3735)
It previously was `XOSAN-${pool.name_label}` which is unnecessary, not easy to use for filtering and problematic for #2128.
2018-11-28 22:27:45 +01:00
Julien Fontanet
ef61e4fe6d chore(xo-server/api/xosan): reformat code 2018-11-28 22:10:12 +01:00
Julien Fontanet
4f776e1370 chore(fs): normalize paths in Abstract (#3732) 2018-11-28 21:44:49 +01:00
Julien Fontanet
aa72708996 chore: reformat some files 2018-11-28 19:10:51 +01:00
Enishowk
8751180634 feat(fs): rmdir method (#3730) 2018-11-28 17:42:32 +01:00
Julien Fontanet
2e327be49d chore(xo-web): reformat some files 2018-11-28 17:40:56 +01:00
badrAZ
f06a937c9c fix(xo-server/mergeXenPools): pool id → server id (#3728)
See #2238
2018-11-28 15:56:30 +01:00
Julien Fontanet
e65b3200cd chore(fs): fix some Flow types 2018-11-28 13:39:38 +01:00
Julien Fontanet
30d3701ab1 chore(fs): reformat code 2018-11-28 13:34:12 +01:00
badrAZ
05fa76dad3 feat(xo-server/xen-servers): prevent server connection when pool already connected (#3724)
See #2238
2018-11-28 10:42:55 +01:00
Julien Fontanet
4020081492 feat(xo-server/api): use per message deflate (#3720)
Related to #3699
2018-11-27 17:51:26 +01:00
Pierre Donias
2fbd4a62b2 chore(prettier): use jsxSingleQuote 2018-11-27 16:03:45 +01:00
Julien Fontanet
b773f5e821 fix(xo-server/xen-servers): pool.id → pool.$id (#3723)
Correctly unregister a Xapi connection from its pool.
2018-11-27 10:24:52 +01:00
Julien Fontanet
76c5ced1dd Update xen-servers.js 2018-11-27 10:23:46 +01:00
badrAZ
197768875b fix error 2018-11-27 10:21:39 +01:00
badrAZ
f0483862a5 fix(xo-server/xen-servers): a xapi object doesn't have an id 2018-11-27 10:12:47 +01:00
Julien Fontanet
ac46d3a5a2 chore: update dependencies
Fixes #3722
2018-11-27 09:26:39 +01:00
Julien Fontanet
2da576a1f8 fix(xo-web): avoid using TextDecoder (#3718)
This is an experimental API and not really necessary here.
2018-11-23 16:54:50 +01:00
Julien Fontanet
2e1ac27cf5 chore: dont format with ESLint 2018-11-23 16:38:24 +01:00
Julien Fontanet
258404affc fix(xo-web/settings/remotes): dont wait for test on create/edit 2018-11-23 16:03:00 +01:00
badrAZ
5121d9d1d7 feat(xo-web/logs): ability to filter logs by the VM name (#3711)
Fixes #2947

To be able to filter logs by the VM name, a property in the top level is needed. `vmNames` is an array which contains all found VMs name. It can be `undefined` if none VM is found.
2018-11-23 14:10:26 +01:00
Pierre Donias
f2a38c5ddd fix(xo-web/xoa/licenses): tip: XOA license mgmt not supported (#3714)
Fixes #3713
2018-11-21 13:12:29 +01:00
badrAZ
97a77b1a33 fix(xo-web/backup-ng-logs): differentiate not fetched yet from empty (#3712)
**NoObjects:**

The `NoObjects` is a wrapper which shows a spinner if a collection is `undefined` and a default message if it's empty.

**The Issue:**

In the line `323`, `groupBy` always returns an object even if `logs` is `undefined`.
2018-11-21 10:31:14 +01:00
Rajaa.BARHTAOUI
88ca41231f feat(xo-web/home/VMs): display pool's name (#3709)
Fixes #2226
2018-11-20 15:59:37 +01:00
badrAZ
9a8f84ccb5 chore(xo-server-perf-alert): add documentation (#3710)
This PR completes the merged PR #3675
2018-11-20 14:18:08 +01:00
Julien Fontanet
dd50fc37fe feat(xen-api): callTimeout option (#3707) 2018-11-20 10:12:58 +01:00
badrAZ
cafcadb286 feat(xo-server-perf-alert): notify if value is below threshold (#3675)
Fixes #3612
2018-11-20 10:07:42 +01:00
Pierre Donias
db3d6bba79 fix(xo-web/acls): action should be reset to undefined (#3704) 2018-11-19 16:14:25 +01:00
Julien Fontanet
11a0fc2a22 chore: update dependencies 2018-11-19 16:09:55 +01:00
badrAZ
1e0a8a5034 fix(CHANGELOG): missing entry (#3703) 2018-11-19 14:28:48 +01:00
Pierre Donias
34ef3e5998 fix(xo-web/xoa): tab icons (#3702) 2018-11-19 13:59:06 +01:00
Enishowk
e73fcc450d feat(xo-server/vm.set): use set_domain_type when available (#3700) 2018-11-19 09:39:16 +01:00
Julien Fontanet
2946eaa156 feat(xo-server): 5.30.1 2018-11-16 18:11:39 +01:00
Julien Fontanet
6dcae9a7d7 fix(xo-server/deleteVm): pass array to Promise.all 2018-11-16 18:10:47 +01:00
Pierre Donias
abeb36f06c chore(CHANGELOG): 5.28.2 2018-11-16 17:53:21 +01:00
Pierre Donias
41139578ba feat(xo-web): 0.30.0 2018-11-16 17:50:25 +01:00
Pierre Donias
cda7621b5d feat(xo-server): 5.30.0 2018-11-16 17:47:58 +01:00
Pierre Donias
b75dd2d424 feat(xo-acl-resolver): 0.4.0 2018-11-16 17:42:41 +01:00
Pierre Donias
273f208722 feat(xo-common): 0.2.0 2018-11-16 17:41:08 +01:00
Pierre Donias
c01e8e892e feat(xen-api): 0.21.0 2018-11-16 17:39:16 +01:00
Julien Fontanet
9dfd81c28f feat(xo-web/subscriptions): keep cached data for 10m (#3701)
Related to #3699

This should improve user experience when changing pages.
2018-11-16 17:37:33 +01:00
Pierre Donias
5dd26ebe33 fix(CHANGELOG): missing entries and links 2018-11-16 16:58:48 +01:00
Julien Fontanet
4c0fe3c14f fix(xo-server/importVdiContent): only throws when compiled with yarn dev
Related to #3678
2018-11-16 14:19:34 +01:00
badrAZ
2353581da8 chore(xo-web/backup-ng): improve setting power state implementation (#3684) 2018-11-16 14:05:58 +01:00
Julien Fontanet
2934b23d2f fix(xo-server/vm.delete): fix template handling (#3695)
Fixes #3498

It seems a recent version of XenServer forbids from destroying VM templates directly, we need to set `is_a_template` to `false` before calling `VM.destroy`.
2018-11-16 11:04:56 +01:00
Julien Fontanet
82e4197237 fix(xo-web/deleteVm): dont ask to force if error (#3696) 2018-11-16 10:39:09 +01:00
Julien Fontanet
a23189f132 fix(xo-server): update VM object when {,guest_}metrics have changed (#3694)
Fixes #3533
2018-11-16 10:32:13 +01:00
Julien Fontanet
47fa1ec81e fix(xo-web/settings/remotes): missing form id (#3697) 2018-11-16 09:35:14 +01:00
Julien Fontanet
4b468663f3 feat(xo-server): ability to set XAPI options in config (#3692) 2018-11-15 16:00:52 +01:00
Nicolas Raynaud
6628dc777d fix(xo-server/xosan.createSR): fix network detection (#3689)
Fixes #3688
2018-11-15 11:10:50 +01:00
Julien Fontanet
3ef3ae0166 fix(xo-server/remote.get{,All}): obfuscate sensitive values (#3687)
Fixes #3682
2018-11-14 14:39:20 +01:00
Julien Fontanet
bc6dbe2771 chore(xen-api/README): add 7.{4..6} compatibility 2018-11-14 14:18:14 +01:00
badrAZ
5651160d1c fix(xo-web/settings/remotes): fix incorrect input type (#3683)
Fixes #3681
2018-11-14 11:21:38 +01:00
Julien Fontanet
6da2669c6f fix(fs/smb#list): throws ENOTDIR instead of SMB error (#3685)
Fixes support#1014

This is more in line of other handlers and fix an issue in Backup NG where listing VM backups would fail when there are files in `xo-vm-backups/`.
2018-11-14 10:11:52 +01:00
Julien Fontanet
8094b5097f feat(xo-server/reaclette-utils/generateId): used as a computed for generated ID (#3680) 2018-11-13 22:58:37 +01:00
Pierre Donias
bdb0547b86 fix(xo-server/vm.create): merge double limit allocation (#3667)
This prevents unwanted error if the limits are exceeded in a transitory fashion.
2018-11-13 17:18:56 +01:00
Pierre Donias
ea08fbbfba fix(xo-server-cloud): missing protocol (#3686) 2018-11-13 17:01:03 +01:00
Julien Fontanet
b4cbd8b2b5 chore(fs): ignore enabled option
It makes no sense for this library to handled disabled remote.
2018-11-13 15:59:16 +01:00
Julien Fontanet
f8fbb6b7d3 feat(xen-api): add call params to errors (#3679)
It also moves `.method` to `.call.method`, but it appears it was not currently used in our code.
2018-11-13 12:03:05 +01:00
Julien Fontanet
c8da9fec0a chore: minor Flow fixes 2018-11-13 11:24:23 +01:00
Julien Fontanet
79fb3ec8bd fix(xo-server/plugin.get): hide sensitive config values (#3671) 2018-11-13 11:09:58 +01:00
Julien Fontanet
2243966ce1 feat(xo-server/checkPermissions): similar to hasPermissions but throws 2018-11-13 10:46:38 +01:00
Julien Fontanet
ca7d520997 feat(xo-acl-resolver/assert): throw error if unauthorized 2018-11-13 10:46:38 +01:00
Julien Fontanet
df44487363 chore(xo-web/backup-ng/log): disable bug reporting
Temporary measure because it is currently broken due to the size of the log in the HTTP request.
2018-11-13 10:13:20 +01:00
Julien Fontanet
b39eb0f60d chore(xo-server/api): add expected permission to unauthorized error 2018-11-12 17:34:15 +01:00
badrAZ
a3dcdc4fd5 chore(xo-server-perf-alert): unused property (#3672) 2018-11-12 15:38:58 +01:00
Julien Fontanet
2daac73c17 chore(xo-common/api-errors/unauthorized): add metadata
```ts
let data: {
  permission?: string,
  object: {
    id?: string,
    type?: string,
  },
}
```
2018-11-12 15:30:28 +01:00
Julien Fontanet
23eb3c3094 chore(xo-server/resourceSet.get{,All}): explicit permission check 2018-11-12 15:25:51 +01:00
Julien Fontanet
776d0f9e4a chore(xo-server/ipPool.getAll): explicit permission check 2018-11-12 15:25:51 +01:00
Enishowk
54bdcc6dd2 feat(xo-web/VM): switch virtualization mode (#3669)
Fixes #2372
2018-11-12 13:57:23 +01:00
Julien Fontanet
38084c8199 chore(xo-common/api-errors/unauthorized): remove "unauthorized" in message
It confuses users and the message is still relevant without in case of unauthorized users.
2018-11-12 13:50:33 +01:00
badrAZ
4525ee7491 fix(xo-server-usage-report): gracefully handle fetching stats error (#3656)
Fixes #3600
2018-11-09 16:10:54 +01:00
badrAZ
66a476bd21 feat(xo-web/backup-ng): warning & omitting VMs/pools on XS < 6.5 (#3668)
Fixes #3540
2018-11-09 15:02:29 +01:00
Pierre Donias
be6cc12632 fix(xo-server/vm.create): revert 37a906a2 (#3666)
Fixes #3658, fixes support#1064
2018-11-08 17:31:07 +01:00
Enishowk
673475dcb2 fix(xo-web/vm): restore display of pvhvm virtualization mode (#3662)
Fixes #3576
2018-11-08 17:05:46 +01:00
badrAZ
7dc1a80a83 fix(xo-server/xapi-stats/getVmStats): avoid sync exceptions (#3661) 2018-11-08 16:31:40 +01:00
Julien Fontanet
d49294849f chore(xo-server/vm.import): remove host parameter (#3663) 2018-11-08 16:30:50 +01:00
Rajaa.BARHTAOUI
6b394302c1 feat(xo-web/sortedTable): mutualize actions (#3594) 2018-11-08 14:40:54 +01:00
Julien Fontanet
00e1601f85 chore(xo-server): use VM.domain_type when available (#3664)
And fallback to our previous detection when unavailable.

Starting from this commit, `virtualizationMode` will no longer contain `'pvhvm'`, this must be computed UI side by using both `virtualizationMode` and `xenTools` properties.
2018-11-08 14:37:47 +01:00
Rajaa.BARHTAOUI
b75e746586 fix(xo-web/StrongConfirm): input loses focus (#3649) 2018-11-08 11:38:17 +01:00
Enishowk
32a9fa9bb0 feat(xo-web/migration): auto-select host/SR when there's only one (#3654)
Fixes #3502
2018-11-08 11:30:38 +01:00
badrAZ
79d68dece4 fix(xo-server/xapi-stats): re-fetch host stats if VM missing (#3660) 2018-11-08 11:28:17 +01:00
Julien Fontanet
1701e1d4ba feat(xo-server/_importVdiContent): require length in dev mode 2018-11-08 10:45:11 +01:00
Julien Fontanet
497b3eb296 feat(xo-server/disk.import): extract length from header 2018-11-08 10:44:35 +01:00
Julien Fontanet
ecfafa0fea chore: npm → yarn 2018-11-07 22:24:46 +01:00
Julien Fontanet
def66d8218 chore(xapi-explore-sr): remove Jest config/scripts 2018-11-07 22:23:17 +01:00
Julien Fontanet
eeb08abec2 chore(xapi-explore-sr): use Babel 7 2018-11-07 22:18:58 +01:00
Julien Fontanet
90923c657d chore: re-format code 2018-11-07 18:37:23 +01:00
Julien Fontanet
4ff6eeb424 chore: update dependencies 2018-11-07 18:15:57 +01:00
Julien Fontanet
2d98fb40f1 feat(xo-server/_assertConsistentHostServerTime): display delta 2018-11-07 17:06:17 +01:00
Julien Fontanet
256a58ded2 feat(xo-server/_assertConsistentHostServerTime): threshold 2s → 30s 2018-11-07 17:00:29 +01:00
badrAZ
bf3b31a9ef fix(xo-web/logs): properly display restore failures (#3648) 2018-11-07 15:20:25 +01:00
badrAZ
7fc8d59605 feat(xo-server/backup-ng): warnings for missing VMs (#3647) 2018-11-07 13:12:26 +01:00
Julien Fontanet
1a39b2113a fix(docs): XO requires Node 8 2018-11-07 10:53:02 +01:00
badrAZ
cb9f3fbb2c chore(xo-server/backup-ng): restore tasks documentation (#3652) 2018-11-07 10:44:33 +01:00
badrAZ
487f413cdd feat(xo-web/backup): move "restore/file-restore" to Backup NG view (#3610)
Fixes #3499
2018-11-07 10:22:31 +01:00
Pierre Donias
f847969206 fix(xo-server/patching): correctly ignore upgrade patches (#3651)
Possibly related to support#1009
2018-11-07 10:10:36 +01:00
Julien Fontanet
5d9aad44c2 Merge branch 'xapi-explore-sr/master' 2018-11-06 18:26:44 +01:00
Julien Fontanet
ba2027e6d7 feat(xapi-explore-sr): move all files to packages/xapi-explore-sr 2018-11-06 18:19:39 +01:00
Julien Fontanet
087da9376f Merge branch 'xo-import-servers-csv/master' 2018-11-06 18:11:58 +01:00
Julien Fontanet
218e3b46e0 feat(xo-import-servers-csv): move all files to packages/xo-import-servers-csv 2018-11-06 17:55:20 +01:00
Rajaa.BARHTAOUI
f9921e354e feat(xo-web/StrongConfirm): press Enter to validate (#2890)
Fixes #2735
2018-11-06 15:29:01 +01:00
badrAZ
341148a7d3 fix(xo-web/logs): fix restarting VMs with concurrency issue (#3634)
Fixes #3603
2018-11-06 13:45:30 +01:00
Julien Fontanet
7216165f1e chore: update dependencies 2018-11-06 13:27:33 +01:00
Julien Fontanet
a9557af04b fix(CHANGELOG): fix versions 2018-11-05 16:48:09 +01:00
Julien Fontanet
abb80270ad feat(xo-web): 5.29.3 2018-11-05 16:47:23 +01:00
Julien Fontanet
72e93384a5 feat(xo-server): 5.29.4 2018-11-05 16:46:54 +01:00
Julien Fontanet
663b1b76ec fix(CHANGELOG): move entry to correct release 2018-11-05 16:46:18 +01:00
Julien Fontanet
24b8c671fa fix(xo-server/mergeVhd): use remote options 2018-11-05 16:41:37 +01:00
Julien Fontanet
986fec1cd3 feat(xo-server): pass config to workers 2018-11-05 16:41:37 +01:00
badrAZ
f6c2cbc5cf chore(xo-web/backup): devs can create legacy backups (#3645)
Fixes #3624
2018-11-05 14:40:42 +01:00
Pierre Donias
289ed89a78 fix(xo-server/vm.create): dont extract cpus & memoryMax from params (#3646)
Fixes #3644
2018-11-05 14:34:58 +01:00
Enishowk
73de421d47 feat(xo-web/vm/advanced): add nested virt toggle (#3625)
Fixes #3619
2018-11-05 14:17:19 +01:00
badrAZ
dc1eb82295 chore(xo-server/backup-ng): tasks' documentation (#3640) 2018-11-05 14:16:36 +01:00
Enishowk
6629c12166 fix(xo-web/form/Toggle): dont emit onChange if disabled (#3643)
fix(xo-web/form/Toggle): dont emit onChange if disabled
2018-11-05 14:09:13 +01:00
badrAZ
ec5bc1db95 fix(xo-web/backup-ng-logs): incorrect started jobs filter (#3641)
Fixes #3636
2018-11-05 12:09:52 +01:00
Julien Fontanet
ac2c40c842 fix(xo-server/vm.*): ensure force params are optional 2018-11-05 10:42:28 +01:00
Julien Fontanet
61bf669252 feat(fs/nfs): ensure mount error not hidden 2018-11-05 10:11:57 +01:00
Julien Fontanet
4105c53155 chore(CHANGELOG): 5.28.1 2018-11-05 09:59:36 +01:00
Julien Fontanet
aeab2b2a08 feat(xo-web): 5.29.2 2018-11-05 09:57:50 +01:00
Julien Fontanet
95e33ee612 feat(xo-server): 5.29.3 2018-11-05 09:57:01 +01:00
Julien Fontanet
093bda7039 feat(fs): 0.4.1 2018-11-05 09:53:43 +01:00
Julien Fontanet
4e35b19ac5 fix(xo-web/xoa/update): fix re-registration 2018-11-05 09:46:22 +01:00
Julien Fontanet
244d8a51e8 chore(xo-web/xoa/update): dont hide error 2018-11-05 09:42:59 +01:00
Julien Fontanet
9d6cc77cc8 chore(changelog): add timeout entry 2018-11-03 17:45:50 +01:00
Julien Fontanet
d5e0150880 chore: update promise-toolbox to v0.11
`timeout()` now provides better stack traces and support 0 delay to
disable the timeout.
2018-11-03 17:45:50 +01:00
Julien Fontanet
5cf29a98b3 feat(fs): configurable timeout 2018-11-03 17:45:50 +01:00
Julien Fontanet
165c2262c0 fix(nfs): default timeout 10s → 10m 2018-11-03 17:45:50 +01:00
badrAZ
74f5d2e0cd fix(xo-server/backup-ng): "vms" should be a dictionary (#3635) 2018-11-02 16:29:44 +01:00
badrAZ
2d93456f52 feat(xo-server/backup-ng): log scheduled VMs if concurrency above 0 (#3633)
See #3603
2018-11-02 14:56:46 +01:00
Julien Fontanet
fd401ca335 fix(fs/nfs): opts param is optional 2018-11-02 10:09:19 +01:00
Julien Fontanet
97ba93a9ad chore(fs): configurable mounts dir (#3413) 2018-11-02 09:37:35 +01:00
Pierre Donias
0788c25710 feat(xo-web): 5.29.1 2018-10-31 17:20:15 +01:00
Julien Fontanet
82bba951db feat(xo-web/xoa-updater/logs): from old to new
Fixes #2708
2018-10-31 17:18:53 +01:00
Julien Fontanet
6efd611b80 feat(xo-web/xoa/update): use pre for updater logs 2018-10-31 17:18:53 +01:00
Julien Fontanet
b7d43b42b9 fix(xo-web/xoa/update): toggleState not compatible with ActionButton 2018-10-31 17:18:52 +01:00
Julien Fontanet
801b71d9ae fix(xo-web/xoa/update): typo 2018-10-31 17:18:52 +01:00
Pierre Donias
0ff7c2188a feat(xo-server): 5.29.2 2018-10-31 16:56:23 +01:00
Julien Fontanet
bc1667440f feat(log): 0.1.4 2018-10-31 16:55:11 +01:00
Julien Fontanet
227b464a8e fix(log): paths in transports/* 2018-10-31 16:54:35 +01:00
Pierre Donias
f6c43650b4 feat(xo-server): 5.29.1 2018-10-31 16:43:29 +01:00
Pierre Donias
597689fde0 chore(xo-server): use log 0.1.3 2018-10-31 16:42:03 +01:00
Julien Fontanet
da6b71fde8 feat(log): 0.1.3 2018-10-31 16:39:20 +01:00
Julien Fontanet
5f2590c858 fix(log): remove transports symlink
Issue with publication.
2018-10-31 16:38:23 +01:00
Julien Fontanet
37b0867151 feat(log): 0.1.1 2018-10-31 16:31:22 +01:00
Julien Fontanet
85031cfb9d fix(log): publish configure.js and transports 2018-10-31 16:30:55 +01:00
Pierre Donias
a13f86fb7c chore(CHANGELOG): 5.28.0 2018-10-31 16:03:35 +01:00
Pierre Donias
7cbc5e642f feat(xo-web): 5.29.0 2018-10-31 15:59:53 +01:00
Pierre Donias
48d4abc259 feat(xo-server): 5.29.0 2018-10-31 15:58:52 +01:00
Pierre Donias
c805f3b1a7 feat(xo-server-usage-report): 0.7.0 2018-10-31 15:55:26 +01:00
Pierre Donias
40212582a9 feat(xen-api): 0.20.0 2018-10-31 15:53:40 +01:00
Julien Fontanet
873db3bf26 0.2.1 2018-04-13 11:32:47 +02:00
Julien Fontanet
c795887a35 fix: display unmanaged snapshots as unmanaged 2018-04-13 11:32:33 +02:00
Julien Fontanet
23824bafe8 1.1.0 2018-04-09 16:07:31 +02:00
Julien Fontanet
5cca58f2b3 feat(README): add documentation 2018-04-09 16:03:25 +02:00
Julien Fontanet
d05c9b6133 chore: use console.error to display errors 2018-04-09 16:03:04 +02:00
Julien Fontanet
39a84a1ac0 feat: support allowUnauthorized, autoConnect and label 2018-04-09 16:02:46 +02:00
Julien Fontanet
b1c851c9d6 chore(package): prepublishOnly script 2018-04-09 16:02:17 +02:00
Julien Fontanet
6280a9365c chore(package): update dependencies 2018-04-09 16:02:04 +02:00
Julien Fontanet
2741dacd64 0.2.0 2018-04-09 14:01:47 +02:00
Julien Fontanet
4c2c2390bd chore(package): prepublish → prepublishOnly 2018-04-09 14:01:18 +02:00
Julien Fontanet
635b8ce5f0 chore(package): update dependencies 2018-04-09 14:00:17 +02:00
Julien Fontanet
efc13cc456 fix: display VDI with missing parent
Consider them parentless even though they are simply unknown.
2018-04-09 11:34:56 +02:00
Julien Fontanet
078f319fe1 0.1.1 2017-06-07 11:25:44 +02:00
Julien Fontanet
0f0e785871 fix: ensure vdi.physical_utilisation is a number 2017-06-07 11:25:39 +02:00
Julien Fontanet
4e4c85121c 0.1.0 2017-05-11 15:27:15 +02:00
Julien Fontanet
019d6f4cb6 feat: display VDI size 2017-05-11 15:27:06 +02:00
Julien Fontanet
725b0342d1 fix: Xen → XenServer 2017-05-11 15:22:49 +02:00
Julien Fontanet
c93ccb8111 feat: handle -h and --help flags 2017-05-11 15:22:12 +02:00
Julien Fontanet
670befdaf6 chore(package): update all dependencies 2017-05-11 15:19:08 +02:00
Julien Fontanet
55eefd865f 0.0.4 2017-03-30 16:51:27 +02:00
Julien Fontanet
43e5d610e3 fix(package): jest config testPathDirs → roots 2017-03-30 16:51:17 +02:00
Julien Fontanet
b1245bc5be fix(usage): Xen → XenServer 2017-03-30 16:50:44 +02:00
Julien Fontanet
c2feab245e fix: update yarn.lock 2017-03-30 16:48:18 +02:00
Julien Fontanet
cb3753213e fix(README): Xen → XenServer 2017-03-08 14:19:21 +01:00
greenkeeper[bot]
ec8c7a24af chore(package): update jest to version 19.0.1 (#2)
https://greenkeeper.io/
2017-02-22 12:10:24 +01:00
greenkeeper[bot]
2456be2da3 chore(package): update tslint-config-standard to version 3.0.0 (#6)
https://greenkeeper.io/
2017-01-19 10:10:08 +01:00
Julien Fontanet
8c5d4240f9 chore(package): update all dependencies 2017-01-17 10:34:25 +01:00
Julien Fontanet
b1e12d1542 chore: add yarn.lock 2017-01-17 10:28:34 +01:00
Julien Fontanet
a58d7d2ff4 chore(package): use husky instead of ghooks 2017-01-17 10:28:03 +01:00
Julien Fontanet
5308b8b9ed fix(README): should be installed globally 2017-01-17 10:26:32 +01:00
greenkeeper[bot]
c15dffce8f chore(package): update @types/node to version 7.0.0 (#5)
https://greenkeeper.io/
2017-01-11 09:22:18 +01:00
greenkeeper[bot]
874680462e chore(package): update dependencies (#4)
https://greenkeeper.io/
2016-11-28 15:13:31 +01:00
greenkeeper[bot]
bb42540775 chore(package): update tslint-config-standard to version 2.0.0 (#3)
https://greenkeeper.io/
2016-11-21 23:27:47 +01:00
Julien Fontanet
b18511c905 chore(package): update all dependencies 2016-11-08 15:44:21 +01:00
greenkeeper[bot]
5c660f4f64 chore(package): update dependencies (#1)
https://greenkeeper.io/
2016-11-02 09:33:33 +01:00
Julien Fontanet
f2bae73f77 0.0.3 2016-10-31 17:32:32 +01:00
Julien Fontanet
e54d34f269 feat(cli): prefix labels if colors not supported 2016-10-31 17:32:00 +01:00
Julien Fontanet
6470cbd2ee 0.0.2 2016-10-31 17:08:02 +01:00
Julien Fontanet
c06ebcb4a4 fix(askPassword): prompt on stderr 2016-10-31 17:07:34 +01:00
Julien Fontanet
3eaa72c98c 0.0.1 2016-10-31 16:37:40 +01:00
Julien Fontanet
694fff060d fix(package): fix bin 2016-10-31 16:37:36 +01:00
Julien Fontanet
2705062ac3 chore(README): replace placeholders 2016-10-31 16:31:59 +01:00
Julien Fontanet
3df055a296 chore(package): publish 2016-10-31 16:29:36 +01:00
Julien Fontanet
802bc15e0c initial commit 2016-10-31 16:27:15 +01:00
Julien Fontanet
ad2de40a9d chore(package): update @types/through2 to v2.0.29 2016-09-23 09:41:05 +02:00
Julien Fontanet
19298570f8 chore(package): remove unused dep 2016-09-19 14:55:50 +02:00
Julien Fontanet
1da4d1f1e9 chore: repo moved to vatesfr 2016-09-19 14:53:33 +02:00
Julien Fontanet
fe4e9c18fa feat(cli): print usage on missing argument 2016-09-19 14:48:52 +02:00
Julien Fontanet
2c9f84f17f feat(package): add description and keywords 2016-09-19 14:48:52 +02:00
Julien Fontanet
0b2e76600b feat(README): add usage 2016-09-19 14:48:52 +02:00
Julien Fontanet
873554fc01 It works! 2016-09-19 14:43:39 +02:00
Julien Fontanet
82e2d013ae chore(package): reorder entry in package.json 2016-09-19 10:23:33 +02:00
Julien Fontanet
1eb5e80f1f fix(types): fix type definitions 2016-09-19 10:23:18 +02:00
Julien Fontanet
9c0ab5b3cb Initial commit 2016-09-16 18:09:18 +02:00
164 changed files with 3950 additions and 2666 deletions

View File

@@ -1,9 +1,10 @@
module.exports = {
extends: ['standard', 'standard-jsx'],
extends: ['standard', 'standard-jsx', 'prettier'],
globals: {
__DEV__: true,
$Dict: true,
$Diff: true,
$ElementType: true,
$Exact: true,
$Keys: true,
$PropertyType: true,
@@ -16,12 +17,9 @@ module.exports = {
},
},
rules: {
'comma-dangle': ['error', 'always-multiline'],
indent: 'off',
'no-var': 'error',
'node/no-extraneous-import': 'error',
'node/no-extraneous-require': 'error',
'prefer-const': 'error',
'react/jsx-indent': 'off',
},
}

View File

@@ -1,4 +1,5 @@
module.exports = {
jsxSingleQuote: true,
semi: false,
singleQuote: true,
trailingComma: 'es5',

View File

@@ -42,8 +42,8 @@ const getConfig = (key, ...args) => {
return config === undefined
? {}
: typeof config === 'function'
? config(...args)
: config
? config(...args)
: config
}
module.exports = function (pkg, plugins, presets) {

View File

@@ -15,6 +15,6 @@
},
"dependencies": {
"golike-defer": "^0.4.1",
"xen-api": "^0.19.0"
"xen-api": "^0.22.0"
}
}

View File

@@ -50,8 +50,8 @@ class Schedule {
zone.toLowerCase() === 'utc'
? moment.utc
: zone === 'local'
? moment
: () => moment.tz(zone)
? moment
: () => moment.tz(zone)
}
createJob (fn) {

View File

@@ -1,6 +1,6 @@
{
"name": "@xen-orchestra/fs",
"version": "0.4.0",
"version": "0.4.1",
"license": "AGPL-3.0",
"description": "The File System for Xen Orchestra backups.",
"keywords": [],
@@ -21,12 +21,13 @@
},
"dependencies": {
"@marsaud/smb2": "^0.9.0",
"@xen-orchestra/async-map": "^0.0.0",
"execa": "^1.0.0",
"fs-extra": "^7.0.0",
"get-stream": "^4.0.0",
"lodash": "^4.17.4",
"promise-toolbox": "^0.10.1",
"through2": "^2.0.3",
"promise-toolbox": "^0.11.0",
"through2": "^3.0.0",
"tmp": "^0.0.33",
"xo-remote-parser": "^0.5.0"
},

View File

@@ -1,10 +1,13 @@
// @flow
// $FlowFixMe
import asyncMap from '@xen-orchestra/async-map'
import getStream from 'get-stream'
import { randomBytes } from 'crypto'
import { fromCallback, fromEvent, ignoreErrors, timeout } from 'promise-toolbox'
import { type Readable, type Writable } from 'stream'
import { parse } from 'xo-remote-parser'
import { randomBytes } from 'crypto'
import { resolve } from 'path'
import { type Readable, type Writable } from 'stream'
import { createChecksumStream, validChecksumOfReadStream } from './checksum'
@@ -17,11 +20,18 @@ type File = FileDescriptor | string
const checksumFile = file => file + '.checksum'
export const DEFAULT_TIMEOUT = 10000
// normalize the path:
// - does not contains `.` or `..` (cannot escape root dir)
// - always starts with `/`
const normalizePath = path => resolve('/', path)
const DEFAULT_TIMEOUT = 6e5 // 10 min
export default class RemoteHandlerAbstract {
_remote: Object
constructor (remote: any) {
_timeout: number
constructor(remote: any, options: Object = {}) {
if (remote.url === 'test://') {
this._remote = remote
} else {
@@ -30,36 +40,37 @@ export default class RemoteHandlerAbstract {
throw new Error('Incorrect remote type')
}
}
;({ timeout: this._timeout = DEFAULT_TIMEOUT } = options)
}
get type (): string {
get type(): string {
throw new Error('Not implemented')
}
/**
* Asks the handler to sync the state of the effective remote with its' metadata
*/
async sync (): Promise<mixed> {
async sync(): Promise<mixed> {
return this._sync()
}
async _sync (): Promise<mixed> {
async _sync(): Promise<mixed> {
throw new Error('Not implemented')
}
/**
* Free the resources possibly dedicated to put the remote at work, when it is no more needed
*/
async forget (): Promise<void> {
async forget(): Promise<void> {
await this._forget()
}
async _forget (): Promise<void> {
async _forget(): Promise<void> {
throw new Error('Not implemented')
}
async test (): Promise<Object> {
const testFileName = `${Date.now()}.test`
async test(): Promise<Object> {
const testFileName = `/${Date.now()}.test`
const data = await fromCallback(cb => randomBytes(1024 * 1024, cb))
let step = 'write'
try {
@@ -84,29 +95,33 @@ export default class RemoteHandlerAbstract {
}
}
async outputFile (file: string, data: Data, options?: Object): Promise<void> {
return this._outputFile(file, data, {
async outputFile(file: string, data: Data, options?: Object): Promise<void> {
return this._outputFile(normalizePath(file), data, {
flags: 'wx',
...options,
})
}
async _outputFile (file: string, data: Data, options?: Object): Promise<void> {
const stream = await this.createOutputStream(file, options)
async _outputFile(file: string, data: Data, options?: Object): Promise<void> {
const stream = await this.createOutputStream(normalizePath(file), options)
const promise = fromEvent(stream, 'finish')
stream.end(data)
await promise
}
async read (
async read(
file: File,
buffer: Buffer,
position?: number
): Promise<{| bytesRead: number, buffer: Buffer |}> {
return this._read(file, buffer, position)
return this._read(
typeof file === 'string' ? normalizePath(file) : file,
buffer,
position
)
}
_read (
_read(
file: File,
buffer: Buffer,
position?: number
@@ -114,20 +129,23 @@ export default class RemoteHandlerAbstract {
throw new Error('Not implemented')
}
async readFile (file: string, options?: Object): Promise<Buffer> {
return this._readFile(file, options)
async readFile(file: string, options?: Object): Promise<Buffer> {
return this._readFile(normalizePath(file), options)
}
_readFile (file: string, options?: Object): Promise<Buffer> {
_readFile(file: string, options?: Object): Promise<Buffer> {
return this.createReadStream(file, options).then(getStream.buffer)
}
async rename (
async rename(
oldPath: string,
newPath: string,
{ checksum = false }: Object = {}
) {
let p = timeout.call(this._rename(oldPath, newPath), DEFAULT_TIMEOUT)
oldPath = normalizePath(oldPath)
newPath = normalizePath(newPath)
let p = timeout.call(this._rename(oldPath, newPath), this._timeout)
if (checksum) {
p = Promise.all([
p,
@@ -137,18 +155,53 @@ export default class RemoteHandlerAbstract {
return p
}
async _rename (oldPath: string, newPath: string) {
async _rename(oldPath: string, newPath: string) {
throw new Error('Not implemented')
}
async list (
async rmdir(
dir: string,
{ recursive = false }: { recursive?: boolean } = {}
) {
dir = normalizePath(dir)
await (recursive ? this._rmtree(dir) : this._rmdir(dir))
}
async _rmdir(dir: string) {
throw new Error('Not implemented')
}
async _rmtree(dir: string) {
try {
return await this._rmdir(dir)
} catch (error) {
if (error.code !== 'ENOTEMPTY') {
throw error
}
}
const files = await this._list(dir)
await asyncMap(files, file =>
this._unlink(`${dir}/${file}`).catch(error => {
if (error.code === 'EISDIR') {
return this._rmtree(`${dir}/${file}`)
}
throw error
})
)
return this._rmtree(dir)
}
async list(
dir: string = '.',
{
filter,
prependDir = false,
}: { filter?: (name: string) => boolean, prependDir?: boolean } = {}
): Promise<string[]> {
let entries = await timeout.call(this._list(dir), DEFAULT_TIMEOUT)
dir = normalizePath(dir)
let entries = await timeout.call(this._list(dir), this._timeout)
if (filter !== undefined) {
entries = entries.filter(filter)
}
@@ -162,17 +215,20 @@ export default class RemoteHandlerAbstract {
return entries
}
async _list (dir: string): Promise<string[]> {
async _list(dir: string): Promise<string[]> {
throw new Error('Not implemented')
}
createReadStream (
file: string,
createReadStream(
file: File,
{ checksum = false, ignoreMissingChecksum = false, ...options }: Object = {}
): Promise<LaxReadable> {
if (typeof file === 'string') {
file = normalizePath(file)
}
const path = typeof file === 'string' ? file : file.path
const streamP = timeout
.call(this._createReadStream(file, options), DEFAULT_TIMEOUT)
.call(this._createReadStream(file, options), this._timeout)
.then(stream => {
// detect early errors
let promise = fromEvent(stream, 'readable')
@@ -224,33 +280,34 @@ export default class RemoteHandlerAbstract {
)
}
async _createReadStream (
file: string,
options?: Object
): Promise<LaxReadable> {
async _createReadStream(file: File, options?: Object): Promise<LaxReadable> {
throw new Error('Not implemented')
}
async openFile (path: string, flags?: string): Promise<FileDescriptor> {
async openFile(path: string, flags?: string): Promise<FileDescriptor> {
path = normalizePath(path)
return {
fd: await timeout.call(this._openFile(path, flags), DEFAULT_TIMEOUT),
fd: await timeout.call(this._openFile(path, flags), this._timeout),
path,
}
}
async _openFile (path: string, flags?: string): Promise<mixed> {
async _openFile(path: string, flags?: string): Promise<mixed> {
throw new Error('Not implemented')
}
async closeFile (fd: FileDescriptor): Promise<void> {
await timeout.call(this._closeFile(fd.fd), DEFAULT_TIMEOUT)
async closeFile(fd: FileDescriptor): Promise<void> {
await timeout.call(this._closeFile(fd.fd), this._timeout)
}
async _closeFile (fd: mixed): Promise<void> {
async _closeFile(fd: mixed): Promise<void> {
throw new Error('Not implemented')
}
async refreshChecksum (path: string): Promise<void> {
async refreshChecksum(path: string): Promise<void> {
path = normalizePath(path)
const stream = (await this.createReadStream(path)).pipe(
createChecksumStream()
)
@@ -258,17 +315,20 @@ export default class RemoteHandlerAbstract {
await this.outputFile(checksumFile(path), await stream.checksum)
}
async createOutputStream (
async createOutputStream(
file: File,
{ checksum = false, ...options }: Object = {}
): Promise<LaxWritable> {
if (typeof file === 'string') {
file = normalizePath(file)
}
const path = typeof file === 'string' ? file : file.path
const streamP = timeout.call(
this._createOutputStream(file, {
flags: 'wx',
...options,
}),
DEFAULT_TIMEOUT
this._timeout
)
if (!checksum) {
@@ -292,30 +352,35 @@ export default class RemoteHandlerAbstract {
return checksumStream
}
async _createOutputStream (
file: mixed,
async _createOutputStream(
file: File,
options?: Object
): Promise<LaxWritable> {
throw new Error('Not implemented')
}
async unlink (file: string, { checksum = true }: Object = {}): Promise<void> {
async unlink(file: string, { checksum = true }: Object = {}): Promise<void> {
file = normalizePath(file)
if (checksum) {
ignoreErrors.call(this._unlink(checksumFile(file)))
}
await timeout.call(this._unlink(file), DEFAULT_TIMEOUT)
await timeout.call(this._unlink(file), this._timeout)
}
async _unlink (file: mixed): Promise<void> {
async _unlink(file: string): Promise<void> {
throw new Error('Not implemented')
}
async getSize (file: mixed): Promise<number> {
return timeout.call(this._getSize(file), DEFAULT_TIMEOUT)
async getSize(file: File): Promise<number> {
return timeout.call(
this._getSize(typeof file === 'string' ? normalizePath(file) : file),
this._timeout
)
}
async _getSize (file: mixed): Promise<number> {
async _getSize(file: File): Promise<number> {
throw new Error('Not implemented')
}
}

View File

@@ -2,11 +2,13 @@
import { TimeoutError } from 'promise-toolbox'
import AbstractHandler, { DEFAULT_TIMEOUT } from './abstract'
import AbstractHandler from './abstract'
const TIMEOUT = 10e3
class TestHandler extends AbstractHandler {
constructor (impl) {
super({ url: 'test://' })
constructor(impl) {
super({ url: 'test://' }, { timeout: TIMEOUT })
Object.keys(impl).forEach(method => {
this[`_${method}`] = impl[method]
@@ -15,97 +17,97 @@ class TestHandler extends AbstractHandler {
}
describe('rename()', () => {
it(`return TimeoutError after ${DEFAULT_TIMEOUT} ms`, async () => {
it(`throws in case of timeout`, async () => {
const testHandler = new TestHandler({
rename: () => new Promise(() => {}),
})
const promise = testHandler.rename('oldPath', 'newPath')
jest.advanceTimersByTime(DEFAULT_TIMEOUT)
jest.advanceTimersByTime(TIMEOUT)
await expect(promise).rejects.toThrowError(TimeoutError)
})
})
describe('list()', () => {
it(`return TimeoutError after ${DEFAULT_TIMEOUT} ms`, async () => {
it(`throws in case of timeout`, async () => {
const testHandler = new TestHandler({
list: () => new Promise(() => {}),
})
const promise = testHandler.list()
jest.advanceTimersByTime(DEFAULT_TIMEOUT)
jest.advanceTimersByTime(TIMEOUT)
await expect(promise).rejects.toThrowError(TimeoutError)
})
})
describe('createReadStream()', () => {
it(`return TimeoutError after ${DEFAULT_TIMEOUT} ms`, async () => {
it(`throws in case of timeout`, async () => {
const testHandler = new TestHandler({
createReadStream: () => new Promise(() => {}),
})
const promise = testHandler.createReadStream('file')
jest.advanceTimersByTime(DEFAULT_TIMEOUT)
jest.advanceTimersByTime(TIMEOUT)
await expect(promise).rejects.toThrowError(TimeoutError)
})
})
describe('openFile()', () => {
it(`return TimeoutError after ${DEFAULT_TIMEOUT} ms`, async () => {
it(`throws in case of timeout`, async () => {
const testHandler = new TestHandler({
openFile: () => new Promise(() => {}),
})
const promise = testHandler.openFile('path')
jest.advanceTimersByTime(DEFAULT_TIMEOUT)
jest.advanceTimersByTime(TIMEOUT)
await expect(promise).rejects.toThrowError(TimeoutError)
})
})
describe('closeFile()', () => {
it(`return TimeoutError after ${DEFAULT_TIMEOUT} ms`, async () => {
it(`throws in case of timeout`, async () => {
const testHandler = new TestHandler({
closeFile: () => new Promise(() => {}),
})
const promise = testHandler.closeFile({ fd: undefined, path: '' })
jest.advanceTimersByTime(DEFAULT_TIMEOUT)
jest.advanceTimersByTime(TIMEOUT)
await expect(promise).rejects.toThrowError(TimeoutError)
})
})
describe('createOutputStream()', () => {
it(`return TimeoutError after ${DEFAULT_TIMEOUT} ms`, async () => {
it(`throws in case of timeout`, async () => {
const testHandler = new TestHandler({
createOutputStream: () => new Promise(() => {}),
})
const promise = testHandler.createOutputStream('File')
jest.advanceTimersByTime(DEFAULT_TIMEOUT)
jest.advanceTimersByTime(TIMEOUT)
await expect(promise).rejects.toThrowError(TimeoutError)
})
})
describe('unlink()', () => {
it(`return TimeoutError after ${DEFAULT_TIMEOUT} ms`, async () => {
it(`throws in case of timeout`, async () => {
const testHandler = new TestHandler({
unlink: () => new Promise(() => {}),
})
const promise = testHandler.unlink('')
jest.advanceTimersByTime(DEFAULT_TIMEOUT)
jest.advanceTimersByTime(TIMEOUT)
await expect(promise).rejects.toThrowError(TimeoutError)
})
})
describe('getSize()', () => {
it(`return TimeoutError after ${DEFAULT_TIMEOUT} ms`, async () => {
it(`throws in case of timeout`, async () => {
const testHandler = new TestHandler({
getSize: () => new Promise(() => {}),
})
const promise = testHandler.getSize('')
jest.advanceTimersByTime(DEFAULT_TIMEOUT)
jest.advanceTimersByTime(TIMEOUT)
await expect(promise).rejects.toThrowError(TimeoutError)
})
})

View File

@@ -1,6 +1,5 @@
// @flow
// $FlowFixMe
import through2 from 'through2'
import { createHash } from 'crypto'
import { defer, fromEvent } from 'promise-toolbox'

View File

@@ -14,7 +14,7 @@ const HANDLERS = {
nfs: RemoteHandlerNfs,
}
export const getHandler = (remote: Remote): RemoteHandler => {
export const getHandler = (remote: Remote, ...rest: any): RemoteHandler => {
// FIXME: should be done in xo-remote-parser.
const type = remote.url.split('://')[0]
@@ -22,5 +22,5 @@ export const getHandler = (remote: Remote): RemoteHandler => {
if (!Handler) {
throw new Error('Unhandled remote type')
}
return new Handler(remote)
return new Handler(remote, ...rest)
}

View File

@@ -1,51 +1,41 @@
import fs from 'fs-extra'
import { dirname, resolve } from 'path'
import { noop, startsWith } from 'lodash'
import { dirname } from 'path'
import { noop } from 'lodash'
import RemoteHandlerAbstract from './abstract'
export default class LocalHandler extends RemoteHandlerAbstract {
get type () {
get type() {
return 'file'
}
_getRealPath () {
_getRealPath() {
return this._remote.path
}
_getFilePath (file) {
const realPath = this._getRealPath()
const parts = [realPath]
if (file) {
parts.push(file)
}
const path = resolve.apply(null, parts)
if (!startsWith(path, realPath)) {
throw new Error('Remote path is unavailable')
}
return path
_getFilePath(file) {
return this._getRealPath() + file
}
async _sync () {
if (this._remote.enabled) {
const path = this._getRealPath()
await fs.ensureDir(path)
await fs.access(path, fs.R_OK | fs.W_OK)
}
async _sync() {
const path = this._getRealPath()
await fs.ensureDir(path)
await fs.access(path, fs.R_OK | fs.W_OK)
return this._remote
}
async _forget () {
async _forget() {
return noop()
}
async _outputFile (file, data, options) {
async _outputFile(file, data, options) {
const path = this._getFilePath(file)
await fs.ensureDir(dirname(path))
await fs.writeFile(path, data, options)
}
async _read (file, buffer, position) {
async _read(file, buffer, position) {
const needsClose = typeof file === 'string'
file = needsClose ? await fs.open(this._getFilePath(file), 'r') : file.fd
try {
@@ -63,19 +53,19 @@ export default class LocalHandler extends RemoteHandlerAbstract {
}
}
async _readFile (file, options) {
async _readFile(file, options) {
return fs.readFile(this._getFilePath(file), options)
}
async _rename (oldPath, newPath) {
async _rename(oldPath, newPath) {
return fs.rename(this._getFilePath(oldPath), this._getFilePath(newPath))
}
async _list (dir = '.') {
async _list(dir = '.') {
return fs.readdir(this._getFilePath(dir))
}
async _createReadStream (file, options) {
async _createReadStream(file, options) {
return typeof file === 'string'
? fs.createReadStream(this._getFilePath(file), options)
: fs.createReadStream('', {
@@ -85,7 +75,7 @@ export default class LocalHandler extends RemoteHandlerAbstract {
})
}
async _createOutputStream (file, options) {
async _createOutputStream(file, options) {
if (typeof file === 'string') {
const path = this._getFilePath(file)
await fs.ensureDir(dirname(path))
@@ -98,7 +88,7 @@ export default class LocalHandler extends RemoteHandlerAbstract {
})
}
async _unlink (file) {
async _unlink(file) {
return fs.unlink(this._getFilePath(file)).catch(error => {
// do not throw if the file did not exist
if (error == null || error.code !== 'ENOENT') {
@@ -107,18 +97,22 @@ export default class LocalHandler extends RemoteHandlerAbstract {
})
}
async _getSize (file) {
async _getSize(file) {
const stats = await fs.stat(
this._getFilePath(typeof file === 'string' ? file : file.path)
)
return stats.size
}
async _openFile (path, flags) {
async _openFile(path, flags) {
return fs.open(this._getFilePath(path), flags)
}
async _closeFile (fd) {
async _closeFile(fd) {
return fs.close(fd)
}
async _rmdir(dir) {
return fs.rmdir(dir)
}
}

View File

@@ -1,20 +1,31 @@
import execa from 'execa'
import fs from 'fs-extra'
import { join } from 'path'
import { tmpdir } from 'os'
import LocalHandler from './local'
const DEFAULT_NFS_OPTIONS = 'vers=3'
export default class NfsHandler extends LocalHandler {
get type () {
constructor(
remote,
{ mountsDir = join(tmpdir(), 'xo-fs-mounts'), ...opts } = {}
) {
super(remote, opts)
this._realPath = join(mountsDir, remote.id)
}
get type() {
return 'nfs'
}
_getRealPath () {
return `/run/xo-server/mounts/${this._remote.id}`
_getRealPath() {
return this._realPath
}
async _mount () {
async _mount() {
await fs.ensureDir(this._getRealPath())
const { host, path, port, options } = this._remote
return execa(
@@ -33,23 +44,23 @@ export default class NfsHandler extends LocalHandler {
},
}
).catch(error => {
if (!error.stderr.includes('already mounted')) {
if (
error == null ||
typeof error.stderr !== 'string' ||
!error.stderr.includes('already mounted')
) {
throw error
}
})
}
async _sync () {
if (this._remote.enabled) {
await this._mount()
} else {
await this._umount()
}
async _sync() {
await this._mount()
return this._remote
}
async _forget () {
async _forget() {
try {
await this._umount(this._remote)
} catch (_) {
@@ -57,13 +68,17 @@ export default class NfsHandler extends LocalHandler {
}
}
async _umount () {
async _umount() {
await execa('umount', ['--force', this._getRealPath()], {
env: {
LANG: 'C',
},
}).catch(error => {
if (!error.stderr.includes('not mounted')) {
if (
error == null ||
typeof error.stderr !== 'string' ||
!error.stderr.includes('not mounted')
) {
throw error
}
})

View File

@@ -6,33 +6,38 @@ import RemoteHandlerAbstract from './abstract'
const noop = () => {}
// Normalize the error code for file not found.
const normalizeError = error => {
class ErrorWrapper extends Error {
constructor(error, newCode) {
super(error.message)
this.cause = error
this.code = newCode
}
}
const normalizeError = (error, shouldBeDirectory) => {
const { code } = error
return code === 'STATUS_OBJECT_NAME_NOT_FOUND' ||
code === 'STATUS_OBJECT_PATH_NOT_FOUND'
? Object.create(error, {
code: {
configurable: true,
readable: true,
value: 'ENOENT',
writable: true,
},
})
? new ErrorWrapper(error, 'ENOENT')
: code === 'STATUS_NOT_SUPPORTED' || code === 'STATUS_INVALID_PARAMETER'
? new ErrorWrapper(error, shouldBeDirectory ? 'ENOTDIR' : 'EISDIR')
: error
}
export default class SmbHandler extends RemoteHandlerAbstract {
constructor (remote) {
super(remote)
constructor(remote, opts) {
super(remote, opts)
this._forget = noop
const prefix = this._remote.path
this._prefix = prefix !== '' ? prefix + '\\' : prefix
}
get type () {
get type() {
return 'smb'
}
_getClient () {
_getClient() {
const remote = this._remote
return new Smb2({
@@ -44,40 +49,24 @@ export default class SmbHandler extends RemoteHandlerAbstract {
})
}
_getFilePath (file) {
if (file === '.') {
file = undefined
}
let path = this._remote.path !== '' ? this._remote.path : ''
// Ensure remote path is a directory.
if (path !== '' && path[path.length - 1] !== '\\') {
path += '\\'
}
if (file) {
path += file.replace(/\//g, '\\')
}
return path
_getFilePath(file) {
return this._prefix + file.slice(1).replace(/\//g, '\\')
}
_dirname (file) {
_dirname(file) {
const parts = file.split('\\')
parts.pop()
return parts.join('\\')
}
async _sync () {
if (this._remote.enabled) {
// Check access (smb2 does not expose connect in public so far...)
await this.list()
}
async _sync() {
// Check access (smb2 does not expose connect in public so far...)
await this.list()
return this._remote
}
async _outputFile (file, data, options = {}) {
async _outputFile(file, data, options = {}) {
const client = this._getClient()
const path = this._getFilePath(file)
const dir = this._dirname(path)
@@ -91,7 +80,7 @@ export default class SmbHandler extends RemoteHandlerAbstract {
})
}
async _read (file, buffer, position) {
async _read(file, buffer, position) {
const needsClose = typeof file === 'string'
let client
@@ -112,7 +101,7 @@ export default class SmbHandler extends RemoteHandlerAbstract {
}
}
async _readFile (file, options = {}) {
async _readFile(file, options = {}) {
const client = this._getClient()
let content
@@ -129,7 +118,7 @@ export default class SmbHandler extends RemoteHandlerAbstract {
return content
}
async _rename (oldPath, newPath) {
async _rename(oldPath, newPath) {
const client = this._getClient()
try {
@@ -145,7 +134,7 @@ export default class SmbHandler extends RemoteHandlerAbstract {
}
}
async _list (dir = '.') {
async _list(dir = '.') {
const client = this._getClient()
let list
@@ -154,13 +143,13 @@ export default class SmbHandler extends RemoteHandlerAbstract {
client.disconnect()
})
} catch (error) {
throw normalizeError(error)
throw normalizeError(error, true)
}
return list
}
async _createReadStream (file, options = {}) {
async _createReadStream(file, options = {}) {
if (typeof file !== 'string') {
file = file.path
}
@@ -178,7 +167,7 @@ export default class SmbHandler extends RemoteHandlerAbstract {
return stream
}
async _createOutputStream (file, options = {}) {
async _createOutputStream(file, options = {}) {
if (typeof file !== 'string') {
file = file.path
}
@@ -199,7 +188,7 @@ export default class SmbHandler extends RemoteHandlerAbstract {
return stream
}
async _unlink (file) {
async _unlink(file) {
const client = this._getClient()
try {
@@ -211,7 +200,7 @@ export default class SmbHandler extends RemoteHandlerAbstract {
}
}
async _getSize (file) {
async _getSize(file) {
const client = await this._getClient()
let size
@@ -229,7 +218,7 @@ export default class SmbHandler extends RemoteHandlerAbstract {
}
// TODO: add flags
async _openFile (path) {
async _openFile(path) {
const client = this._getClient()
return {
client,
@@ -237,7 +226,7 @@ export default class SmbHandler extends RemoteHandlerAbstract {
}
}
async _closeFile ({ client, file }) {
async _closeFile({ client, file }) {
await client.close(file)
client.disconnect()
}

View File

@@ -1,6 +1,6 @@
{
"name": "@xen-orchestra/log",
"version": "0.1.0",
"version": "0.1.4",
"license": "ISC",
"description": "",
"keywords": [],
@@ -18,7 +18,9 @@
"main": "dist/",
"bin": {},
"files": [
"dist/"
"configure.js",
"dist/",
"transports/"
],
"browserslist": [
">2%"
@@ -28,7 +30,7 @@
},
"dependencies": {
"lodash": "^4.17.4",
"promise-toolbox": "^0.10.1"
"promise-toolbox": "^0.11.0"
},
"devDependencies": {
"@babel/cli": "^7.0.0",

View File

@@ -13,10 +13,10 @@ const consoleTransport = ({ data, level, namespace, message, time }) => {
level < INFO
? debugConsole
: level < WARN
? infoConsole
: level < ERROR
? warnConsole
: errorConsole
? infoConsole
: level < ERROR
? warnConsole
: errorConsole
fn('%s - %s - [%s] %s', time.toISOString(), namespace, NAMES[level], message)
data != null && fn(data)

View File

@@ -53,14 +53,12 @@ export default ({
fromCallback(cb =>
transporter.sendMail(
{
subject: evalTemplate(
subject,
key =>
key === 'level'
? NAMES[log.level]
: key === 'time'
? log.time.toISOString()
: log[key]
subject: evalTemplate(subject, key =>
key === 'level'
? NAMES[log.level]
: key === 'time'
? log.time.toISOString()
: log[key]
),
text: prettyFormat(log.data),
},

View File

@@ -1 +0,0 @@
dist/transports

View File

@@ -0,0 +1 @@
module.exports = require('../dist/transports/console.js')

View File

@@ -0,0 +1 @@
module.exports = require('../dist/transports/email.js')

View File

@@ -0,0 +1 @@
module.exports = require('../dist/transports/memory.js')

View File

@@ -0,0 +1 @@
module.exports = require('../dist/transports/syslog.js')

View File

@@ -4,6 +4,80 @@
### Enhancements
- [Perf alert] Ability to trigger an alarm if a host/VM/SR usage value is below the threshold [#3612](https://github.com/vatesfr/xen-orchestra/issues/3612) (PR [#3675](https://github.com/vatesfr/xen-orchestra/pull/3675))
- [Home/VMs] Display pool's name [#2226](https://github.com/vatesfr/xen-orchestra/issues/2226) (PR [#3709](https://github.com/vatesfr/xen-orchestra/pull/3709))
- [Servers] Prevent new connection if pool is already connected [#2238](https://github.com/vatesfr/xen-orchestra/issues/2238) (PR [#3724](https://github.com/vatesfr/xen-orchestra/pull/3724))
- [VM] Pause (like Suspend but doesn't copy RAM on disk) [#3727](https://github.com/vatesfr/xen-orchestra/issues/3727) (PR [#3731](https://github.com/vatesfr/xen-orchestra/pull/3731))
### Bug fixes
- [Servers] Fix deleting server on joining a pool [#2238](https://github.com/vatesfr/xen-orchestra/issues/2238) (PR [#3728](https://github.com/vatesfr/xen-orchestra/pull/3728))
### Released packages
- xen-api v0.22.0
- xo-server-perf-alert v0.2.0
- xo-server-usage-report v0.7.1
- xo-server v5.31.0
- xo-web v5.31.0
## **5.28.2** (2018-11-16)
### Enhancements
- [VM] Ability to set nested virtualization in settings [#3619](https://github.com/vatesfr/xen-orchestra/issues/3619) (PR [#3625](https://github.com/vatesfr/xen-orchestra/pull/3625))
- [Legacy Backup] Restore and File restore functionalities moved to the Backup NG view [#3499](https://github.com/vatesfr/xen-orchestra/issues/3499) (PR [#3610](https://github.com/vatesfr/xen-orchestra/pull/3610))
- [Backup NG logs] Display warning in case of missing VMs instead of a ghosts VMs tasks (PR [#3647](https://github.com/vatesfr/xen-orchestra/pull/3647))
- [VM] On migration, automatically selects the host and SR when only one is available [#3502](https://github.com/vatesfr/xen-orchestra/issues/3502) (PR [#3654](https://github.com/vatesfr/xen-orchestra/pull/3654))
- [VM] Display VGA and video RAM for PVHVM guests [#3576](https://github.com/vatesfr/xen-orchestra/issues/3576) (PR [#3664](https://github.com/vatesfr/xen-orchestra/pull/3664))
- [Backup NG form] Display a warning to let the user know that the Delta Backup and the Continuous Replication are not supported on XenServer < 6.5 [#3540](https://github.com/vatesfr/xen-orchestra/issues/3540) (PR [#3668](https://github.com/vatesfr/xen-orchestra/pull/3668))
- [Backup NG form] Omit VMs(Simple Backup)/pools(Smart Backup/Resident on) with XenServer < 6.5 from the selection when the Delta Backup mode or the Continuous Replication mode are selected [#3540](https://github.com/vatesfr/xen-orchestra/issues/3540) (PR [#3668](https://github.com/vatesfr/xen-orchestra/pull/3668))
- [VM] Allow to switch the Virtualization mode [#2372](https://github.com/vatesfr/xen-orchestra/issues/2372) (PR [#3669](https://github.com/vatesfr/xen-orchestra/pull/3669))
### Bug fixes
- [Backup ng logs] Fix restarting VMs with concurrency issue [#3603](https://github.com/vatesfr/xen-orchestra/issues/3603) (PR [#3634](https://github.com/vatesfr/xen-orchestra/pull/3634))
- Validate modal containing a confirm text input by pressing the Enter key [#2735](https://github.com/vatesfr/xen-orchestra/issues/2735) (PR [#2890](https://github.com/vatesfr/xen-orchestra/pull/2890))
- [Patches] Bulk install correctly ignores upgrade patches on licensed hosts (PR [#3651](https://github.com/vatesfr/xen-orchestra/pull/3651))
- [Backup NG logs] Handle failed restores (PR [#3648](https://github.com/vatesfr/xen-orchestra/pull/3648))
- [Self/New VM] Incorrect limit computation [#3658](https://github.com/vatesfr/xen-orchestra/issues/3658) (PR [#3666](https://github.com/vatesfr/xen-orchestra/pull/3666))
- [Plugins] Don't expose credentials in config to users (PR [#3671](https://github.com/vatesfr/xen-orchestra/pull/3671))
- [Self/New VM] `not enough … available in the set …` error in some cases (PR [#3667](https://github.com/vatesfr/xen-orchestra/pull/3667))
- [XOSAN] Creation stuck at "Configuring VMs" [#3688](https://github.com/vatesfr/xen-orchestra/issues/3688) (PR [#3689](https://github.com/vatesfr/xen-orchestra/pull/3689))
- [Backup NG] Errors listing backups on SMB remotes with extraneous files (PR [#3685](https://github.com/vatesfr/xen-orchestra/pull/3685))
- [Remotes] Don't expose credentials to users [#3682](https://github.com/vatesfr/xen-orchestra/issues/3682) (PR [#3687](https://github.com/vatesfr/xen-orchestra/pull/3687))
- [VM] Correctly display guest metrics updates (tools, network, etc.) [#3533](https://github.com/vatesfr/xen-orchestra/issues/3533) (PR [#3694](https://github.com/vatesfr/xen-orchestra/pull/3694))
- [VM Templates] Fix deletion [#3498](https://github.com/vatesfr/xen-orchestra/issues/3498) (PR [#3695](https://github.com/vatesfr/xen-orchestra/pull/3695))
### Released packages
- xen-api v0.21.0
- xo-common v0.2.0
- xo-acl-resolver v0.4.0
- xo-server v5.30.1
- xo-web v5.30.0
## **5.28.1** (2018-11-05)
### Enhancements
### Bug fixes
- [Backup NG] Increase timeout in stale remotes detection to limit false positives (PR [#3632](https://github.com/vatesfr/xen-orchestra/pull/3632))
- Fix re-registration issue ([4e35b19ac](https://github.com/vatesfr/xen-orchestra/commit/4e35b19ac56c60f61c0e771cde70a50402797b8a))
- [Backup NG logs] Fix started jobs filter [#3636](https://github.com/vatesfr/xen-orchestra/issues/3636) (PR [#3641](https://github.com/vatesfr/xen-orchestra/pull/3641))
- [New VM] CPU and memory user inputs were ignored since previous release [#3644](https://github.com/vatesfr/xen-orchestra/issues/3644) (PR [#3646](https://github.com/vatesfr/xen-orchestra/pull/3646))
### Released packages
- @xen-orchestra/fs v0.4.1
- xo-server v5.29.4
- xo-web v5.29.3
## **5.28.0** (2018-10-31)
### Enhancements
- [Usage Report] Add IOPS read/write/total per VM [#3309](https://github.com/vatesfr/xen-orchestra/issues/3309) (PR [#3455](https://github.com/vatesfr/xen-orchestra/pull/3455))
- [Self service] Sort resource sets by name (PR [#3507](https://github.com/vatesfr/xen-orchestra/pull/3507))
- [Usage Report] Add top 3 SRs which use the most IOPS read/write/total [#3306](https://github.com/vatesfr/xen-orchestra/issues/3306) (PR [#3508](https://github.com/vatesfr/xen-orchestra/pull/3508))
@@ -56,7 +130,7 @@
- xo-server v5.29.0
- xo-web v5.29.0
## **5.28.0** (2018-10-05)
## **5.27.2** (2018-10-05)
### Enhancements

View File

@@ -14,13 +14,13 @@ As you may have seen,in other parts of the documentation, XO is composed of two
### NodeJS
XO needs Node.js. **Please always use the LTS version of Node**.
XO needs Node.js. **Please use Node 8**.
We'll consider at this point that you've got a working node on your box. E.g:
```
$ node -v
v8.9.1
v8.12.0
```
If not, see [this page](https://nodejs.org/en/download/package-manager/) for instructions on how to install Node.

View File

@@ -103,6 +103,6 @@ encoding by prefixing with `json:`:
##### VM import
```
> xo-cli vm.import host=60a6939e-8b0a-4352-9954-5bde44bcdf7d @=vm.xva
> xo-cli vm.import sr=60a6939e-8b0a-4352-9954-5bde44bcdf7d @=vm.xva
```
> Note: `xo-cli` only supports the import of XVA files. It will not import OVA files. To import OVA images, you must use the XOA web UI.

View File

@@ -7,21 +7,22 @@
"babel-jest": "^23.0.1",
"benchmark": "^2.1.4",
"eslint": "^5.1.0",
"eslint-config-prettier": "^3.3.0",
"eslint-config-standard": "12.0.0",
"eslint-config-standard-jsx": "^6.0.2",
"eslint-plugin-import": "^2.8.0",
"eslint-plugin-node": "^7.0.1",
"eslint-plugin-node": "^8.0.0",
"eslint-plugin-promise": "^4.0.0",
"eslint-plugin-react": "^7.6.1",
"eslint-plugin-standard": "^4.0.0",
"exec-promise": "^0.7.0",
"flow-bin": "^0.82.0",
"flow-bin": "^0.86.0",
"globby": "^8.0.0",
"husky": "^1.0.0-rc.15",
"jest": "^23.0.1",
"lodash": "^4.17.4",
"prettier": "^1.10.2",
"promise-toolbox": "^0.10.1",
"promise-toolbox": "^0.11.0",
"sorted-object": "^2.0.1"
},
"engines": {

View File

@@ -410,11 +410,10 @@ class P {
static text (text) {
const { length } = text
return new P(
(input, pos) =>
input.startsWith(text, pos)
? new Success(pos + length, text)
: new Failure(pos, `'${text}'`)
return new P((input, pos) =>
input.startsWith(text, pos)
? new Success(pos + length, text)
: new Failure(pos, `'${text}'`)
)
}
@@ -478,17 +477,16 @@ class P {
}
}
P.eof = new P(
(input, pos, end) =>
pos < end ? new Failure(pos, 'end of input') : new Success(pos)
P.eof = new P((input, pos, end) =>
pos < end ? new Failure(pos, 'end of input') : new Success(pos)
)
// -------------------------------------------------------------------
const parser = P.grammar({
default: r =>
P.seq(r.ws, r.term.repeat(), P.eof).map(
([, terms]) => (terms.length === 0 ? new Null() : new And(terms))
P.seq(r.ws, r.term.repeat(), P.eof).map(([, terms]) =>
terms.length === 0 ? new Null() : new And(terms)
),
globPattern: new P((input, pos, end) => {
let value = ''

View File

@@ -26,7 +26,7 @@
"node": ">=6"
},
"dependencies": {
"@xen-orchestra/fs": "^0.4.0",
"@xen-orchestra/fs": "^0.4.1",
"cli-progress": "^2.0.0",
"exec-promise": "^0.7.0",
"struct-fu": "^1.2.0",
@@ -40,7 +40,7 @@
"cross-env": "^5.1.3",
"execa": "^1.0.0",
"index-modules": "^0.3.0",
"promise-toolbox": "^0.10.1",
"promise-toolbox": "^0.11.0",
"rimraf": "^2.6.1",
"tmp": "^0.0.33"
},

View File

@@ -25,7 +25,7 @@
"from2": "^2.3.0",
"fs-extra": "^7.0.0",
"limit-concurrency-decorator": "^0.4.0",
"promise-toolbox": "^0.10.1",
"promise-toolbox": "^0.11.0",
"struct-fu": "^1.2.0",
"uuid": "^3.0.1"
},
@@ -34,7 +34,7 @@
"@babel/core": "^7.0.0",
"@babel/preset-env": "^7.0.0",
"@babel/preset-flow": "^7.0.0",
"@xen-orchestra/fs": "^0.4.0",
"@xen-orchestra/fs": "^0.4.1",
"babel-plugin-lodash": "^3.3.2",
"cross-env": "^5.1.3",
"execa": "^1.0.0",

View File

@@ -228,16 +228,15 @@ export default class Vhd {
return this._read(
sectorsToBytes(blockAddr),
onlyBitmap ? this.bitmapSize : this.fullBlockSize
).then(
buf =>
onlyBitmap
? { id: blockId, bitmap: buf }
: {
id: blockId,
bitmap: buf.slice(0, this.bitmapSize),
data: buf.slice(this.bitmapSize),
buffer: buf,
}
).then(buf =>
onlyBitmap
? { id: blockId, bitmap: buf }
: {
id: blockId,
bitmap: buf.slice(0, this.bitmapSize),
data: buf.slice(this.bitmapSize),
buffer: buf,
}
)
}

View File

@@ -0,0 +1,3 @@
module.exports = require('../../@xen-orchestra/babel-config')(
require('./package.json')
)

View File

@@ -0,0 +1,24 @@
/benchmark/
/benchmarks/
*.bench.js
*.bench.js.map
/examples/
example.js
example.js.map
*.example.js
*.example.js.map
/fixture/
/fixtures/
*.fixture.js
*.fixture.js.map
*.fixtures.js
*.fixtures.js.map
/test/
/tests/
*.spec.js
*.spec.js.map
__snapshots__/

View File

@@ -0,0 +1,52 @@
# xapi-explore-sr [![Build Status](https://travis-ci.org/vatesfr/xen-orchestra.png?branch=master)](https://travis-ci.org/vatesfr/xen-orchestra)
> Display the list of VDIs (unmanaged and snapshots included) of a SR
## Install
Installation of the [npm package](https://npmjs.org/package/xapi-explore-sr):
```
> npm install --global xapi-explore-sr
```
## Usage
```
> xapi-explore-sr
Usage: xapi-explore-sr [--full] <SR UUID> <XenServer URL> <XenServer user> [<XenServer password>]
```
## Development
```
# Install dependencies
> npm install
# Run the tests
> npm test
# Continuously compile
> npm run dev
# Continuously run the tests
> npm run dev-test
# Build for production (automatically called by npm install)
> npm run build
```
## Contributions
Contributions are *very* welcomed, either on the documentation or on
the code.
You may:
- report any [issue](https://github.com/vatesfr/xen-orchestra/issues)
you've encountered;
- fork and create a pull request.
## License
ISC © [Vates SAS](https://vates.fr)

View File

@@ -0,0 +1,60 @@
{
"name": "xapi-explore-sr",
"version": "0.2.1",
"license": "ISC",
"description": "Display the list of VDIs (unmanaged and snapshots included) of a SR",
"keywords": [
"api",
"sr",
"vdi",
"vdis",
"xen",
"xen-api",
"xenapi"
],
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/packages/xapi-explore-sr",
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
"repository": {
"type": "git",
"url": "https://github.com/vatesfr/xen-orchestra.git"
},
"author": {
"name": "Julien Fontanet",
"email": "julien.fontanet@isonoe.net"
},
"preferGlobal": true,
"main": "dist/",
"bin": {
"xapi-explore-sr": "dist/index.js"
},
"files": [
"dist/"
],
"engines": {
"node": ">=8"
},
"dependencies": {
"archy": "^1.0.0",
"chalk": "^2.3.2",
"exec-promise": "^0.7.0",
"human-format": "^0.10.0",
"lodash": "^4.17.4",
"pw": "^0.0.4",
"xen-api": "^0.22.0"
},
"devDependencies": {
"@babel/cli": "^7.1.5",
"@babel/core": "^7.1.5",
"@babel/preset-env": "^7.1.5",
"babel-plugin-lodash": "^3.2.11",
"cross-env": "^5.1.4",
"rimraf": "^2.6.1"
},
"scripts": {
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
"prebuild": "rimraf dist/",
"predev": "yarn run prebuild",
"prepublishOnly": "yarn run build"
}
}

View File

@@ -0,0 +1,161 @@
#!/usr/bin/env node
import archy from 'archy'
import chalk from 'chalk'
import execPromise from 'exec-promise'
import humanFormat from 'human-format'
import pw from 'pw'
import { createClient } from 'xen-api'
import { forEach, map, orderBy } from 'lodash'
// ===================================================================
const askPassword = prompt =>
new Promise(resolve => {
prompt && process.stderr.write(`${prompt}: `)
pw(resolve)
})
const formatSize = bytes =>
humanFormat(bytes, {
prefix: 'Gi',
scale: 'binary',
})
const required = name => {
const e = `missing required argument <${name}>`
throw e
}
// -------------------------------------------------------------------
const STYLES = [
[
vdi => !vdi.managed,
chalk.enabled ? chalk.red : label => `[unmanaged] ${label}`,
],
[
vdi => vdi.is_a_snapshot,
chalk.enabled ? chalk.yellow : label => `[snapshot] ${label}`,
],
]
const getStyle = vdi => {
for (let i = 0, n = STYLES.length; i < n; ++i) {
const entry = STYLES[i]
if (entry[0](vdi)) {
return entry[1]
}
}
}
const mapFilter = (collection, iteratee, results = []) => {
forEach(collection, function () {
const result = iteratee.apply(this, arguments)
if (result !== undefined) {
results.push(result)
}
})
return results
}
// -------------------------------------------------------------------
execPromise(async args => {
if (args.length === 0 || args[0] === '-h' || args[0] === '--help') {
return `Usage: xapi-explore-sr [--full] <SR UUID> <XenServer URL> <XenServer user> [<XenServer password>]`
}
const full = args[0] === '--full'
if (full) {
args.shift()
}
const [
srUuid = required('SR UUID'),
url = required('XenServer URL'),
user = required('XenServer user'),
password = await askPassword('XenServer password'),
] = args
const xapi = createClient({
allowUnauthorized: true,
auth: { user, password },
readOnly: true,
url,
watchEvents: false,
})
await xapi.connect()
const srRef = await xapi.call('SR.get_by_uuid', srUuid)
const sr = await xapi.call('SR.get_record', srRef)
const vdisByRef = {}
await Promise.all(
map(sr.VDIs, async ref => {
const vdi = await xapi.call('VDI.get_record', ref)
vdisByRef[ref] = vdi
})
)
const hasParents = {}
const vhdChildrenByUuid = {}
forEach(vdisByRef, vdi => {
const vhdParent = vdi.sm_config['vhd-parent']
if (vhdParent) {
;(
vhdChildrenByUuid[vhdParent] || (vhdChildrenByUuid[vhdParent] = [])
).push(vdi)
} else if (!(vdi.snapshot_of in vdisByRef)) {
return
}
hasParents[vdi.uuid] = true
})
const makeVdiNode = vdi => {
const { uuid } = vdi
let label = `${vdi.name_label} - ${uuid} - ${formatSize(
+vdi.physical_utilisation
)}`
const nodes = []
const vhdChildren = vhdChildrenByUuid[uuid]
if (vhdChildren) {
mapFilter(
orderBy(vhdChildren, 'is_a_snapshot', 'desc'),
makeVdiNode,
nodes
)
}
mapFilter(
vdi.snapshots,
ref => {
const vdi = vdisByRef[ref]
if (full || !vdi.sm_config['vhd-parent']) {
return makeVdiNode(vdi)
}
},
nodes
)
const style = getStyle(vdi)
if (style) {
label = style(label)
}
return { label, nodes }
}
const nodes = mapFilter(orderBy(vdisByRef, ['name_label', 'uuid']), vdi => {
if (!hasParents[vdi.uuid]) {
return makeVdiNode(vdi)
}
})
return archy({
label: `${sr.name_label} (${sr.VDIs.length} VDIs)`,
nodes,
})
})

View File

@@ -4,6 +4,9 @@
Tested with:
- XenServer 7.6
- XenServer 7.5
- XenServer 7.4
- XenServer 7.3
- XenServer 7.2
- XenServer 7.1
@@ -44,6 +47,7 @@ Options:
- `allowUnauthorized`: whether to accept self-signed certificates
- `auth`: credentials used to sign in (can also be specified in the URL)
- `readOnly = false`: if true, no methods with side-effects can be called
- `callTimeout`: number of milliseconds after which a call is considered failed (can also be a map of timeouts by methods)
```js
// Force connection.

View File

@@ -1,6 +1,6 @@
{
"name": "xen-api",
"version": "0.19.0",
"version": "0.22.0",
"license": "ISC",
"description": "Connector to the Xen API",
"keywords": [
@@ -39,13 +39,13 @@
"http-request-plus": "^0.6.0",
"iterable-backoff": "^0.0.0",
"jest-diff": "^23.5.0",
"json-rpc-protocol": "^0.12.0",
"json-rpc-protocol": "^0.13.1",
"kindof": "^2.0.0",
"lodash": "^4.17.4",
"make-error": "^1.3.0",
"minimist": "^1.2.0",
"ms": "^2.1.1",
"promise-toolbox": "^0.10.1",
"promise-toolbox": "^0.11.0",
"pw": "0.0.4",
"xmlrpc": "^1.3.2",
"xo-collection": "^0.4.1"

View File

@@ -0,0 +1,17 @@
import mapValues from 'lodash/mapValues'
export default function replaceSensitiveValues (value, replacement) {
function helper (value, name) {
if (name === 'password' && typeof value === 'string') {
return replacement
}
if (typeof value !== 'object' || value === null) {
return value
}
return Array.isArray(value) ? value.map(helper) : mapValues(value, helper)
}
return helper(value)
}

View File

@@ -22,6 +22,7 @@ import {
cancelable,
defer,
fromEvents,
ignoreErrors,
pCatch,
pDelay,
pFinally,
@@ -30,6 +31,7 @@ import {
} from 'promise-toolbox'
import autoTransport from './transports/auto'
import replaceSensitiveValues from './_replaceSensitiveValues'
const debug = createDebug('xen-api')
@@ -86,14 +88,14 @@ const isSessionInvalid = ({ code }) => code === 'SESSION_INVALID'
// -------------------------------------------------------------------
class XapiError extends BaseError {
constructor (code, params) {
constructor(code, params) {
super(`${code}(${params.join(', ')})`)
this.code = code
this.params = params
// slots than can be assigned later
this.method = undefined
this.call = undefined
this.url = undefined
this.task = undefined
}
@@ -208,6 +210,24 @@ const getTaskResult = task => {
}
}
function defined() {
for (let i = 0, n = arguments.length; i < n; ++i) {
const arg = arguments[i]
if (arg !== undefined) {
return arg
}
}
}
const makeCallSetting = (setting, defaultValue) =>
setting === undefined
? () => defaultValue
: typeof setting === 'function'
? setting
: typeof setting !== 'object'
? () => setting
: method => defined(setting[method], setting['*'], defaultValue)
// -------------------------------------------------------------------
const RESERVED_FIELDS = {
@@ -226,15 +246,19 @@ const DISCONNECTED = 'disconnected'
// -------------------------------------------------------------------
export class Xapi extends EventEmitter {
constructor (opts) {
constructor(opts) {
super()
this._allowUnauthorized = opts.allowUnauthorized
this._auth = opts.auth
this._callTimeout = makeCallSetting(opts.callTimeout, 0)
this._debounce = opts.debounce == null ? 200 : opts.debounce
this._pool = null
this._readOnly = Boolean(opts.readOnly)
this._RecordsByType = createObject(null)
this._sessionId = null
;(this._objects = new Collection()).getKey = getKey
;(this._objectsByRef = createObject(null))[NULL_REF] = undefined
const url = (this._url = parseUrl(opts.url))
if (this._auth === undefined) {
@@ -249,39 +273,39 @@ export class Xapi extends EventEmitter {
}
}
// Memoize this function _addObject().
this._getPool = () => this._pool
if (opts.watchEvents !== false) {
this._debounce = opts.debounce == null ? 200 : opts.debounce
this._eventWatchers = createObject(null)
this._fromToken = ''
// Memoize this function _addObject().
this._getPool = () => this._pool
this._nTasks = 0
const objects = (this._objects = new Collection())
objects.getKey = getKey
this._objectsByRef = createObject(null)
this._objectsByRef[NULL_REF] = undefined
this._taskWatchers = Object.create(null)
this.on('connected', this._watchEvents)
this.on('disconnected', () => {
this._fromToken = ''
objects.clear()
})
this.watchEvents()
}
}
get _url () {
watchEvents() {
this._eventWatchers = createObject(null)
this._fromToken = ''
this._nTasks = 0
this._taskWatchers = Object.create(null)
if (this.status === CONNECTED) {
ignoreErrors.call(this._watchEvents())
}
this.on('connected', this._watchEvents)
this.on('disconnected', () => {
this._fromToken = ''
this._objects.clear()
})
}
get _url() {
return this.__url
}
set _url (url) {
set _url(url) {
this.__url = url
this._call = autoTransport({
allowUnauthorized: this._allowUnauthorized,
@@ -289,15 +313,15 @@ export class Xapi extends EventEmitter {
})
}
get readOnly () {
get readOnly() {
return this._readOnly
}
set readOnly (ro) {
set readOnly(ro) {
this._readOnly = Boolean(ro)
}
get sessionId () {
get sessionId() {
const id = this._sessionId
if (!id || id === CONNECTING) {
@@ -307,20 +331,20 @@ export class Xapi extends EventEmitter {
return id
}
get status () {
get status() {
const id = this._sessionId
return id ? (id === CONNECTING ? CONNECTING : CONNECTED) : DISCONNECTED
}
get _humanId () {
get _humanId() {
return `${this._auth.user}@${this._url.hostname}`
}
// ensure we have received all events up to this call
//
// optionally returns the up to date object for the given ref
barrier (ref) {
barrier(ref) {
const eventWatchers = this._eventWatchers
if (eventWatchers === undefined) {
return Promise.reject(
@@ -361,7 +385,7 @@ export class Xapi extends EventEmitter {
)
}
connect () {
connect() {
const { status } = this
if (status === CONNECTED) {
@@ -398,7 +422,7 @@ export class Xapi extends EventEmitter {
)
}
disconnect () {
disconnect() {
return Promise.resolve().then(() => {
const { status } = this
@@ -417,14 +441,14 @@ export class Xapi extends EventEmitter {
}
// High level calls.
call (method, ...args) {
call(method, ...args) {
return this._readOnly && !isReadOnlyCall(method, args)
? Promise.reject(new Error(`cannot call ${method}() in read only mode`))
: this._sessionCall(method, prepareParam(args))
}
@cancelable
callAsync ($cancelToken, method, ...args) {
callAsync($cancelToken, method, ...args) {
return this._readOnly && !isReadOnlyCall(method, args)
? Promise.reject(new Error(`cannot call ${method}() in read only mode`))
: this._sessionCall(`Async.${method}`, args).then(taskRef => {
@@ -443,7 +467,7 @@ export class Xapi extends EventEmitter {
//
// allowed even in read-only mode because it does not have impact on the
// XenServer and it's necessary for getResource()
createTask (nameLabel, nameDescription = '') {
createTask(nameLabel, nameDescription = '') {
const promise = this._sessionCall('task.create', [
nameLabel,
nameDescription,
@@ -461,7 +485,7 @@ export class Xapi extends EventEmitter {
// Nice getter which returns the object for a given $id (internal to
// this lib), UUID (unique identifier that some objects have) or
// opaque reference (internal to XAPI).
getObject (idOrUuidOrRef, defaultValue) {
getObject(idOrUuidOrRef, defaultValue) {
if (typeof idOrUuidOrRef === 'object') {
idOrUuidOrRef = idOrUuidOrRef.$id
}
@@ -478,7 +502,7 @@ export class Xapi extends EventEmitter {
// Returns the object for a given opaque reference (internal to
// XAPI).
getObjectByRef (ref, defaultValue) {
getObjectByRef(ref, defaultValue) {
const object = this._objectsByRef[ref]
if (object !== undefined) return object
@@ -490,7 +514,7 @@ export class Xapi extends EventEmitter {
// Returns the object for a given UUID (unique identifier that some
// objects have).
getObjectByUuid (uuid, defaultValue) {
getObjectByUuid(uuid, defaultValue) {
// Objects ids are already UUIDs if they have one.
const object = this._objects.all[uuid]
@@ -501,13 +525,20 @@ export class Xapi extends EventEmitter {
throw new Error('no object with UUID: ' + uuid)
}
async getRecord (type, ref) {
async getRecord(type, ref) {
return this._wrapRecord(
await this._sessionCall(`${type}.get_record`, [ref])
)
}
async getRecordByUuid (type, uuid) {
async getAllRecords(type) {
return map(
await this._sessionCall(`${type}.get_all_records`),
(record, ref) => this._wrapRecord(type, ref, record)
)
}
async getRecordByUuid(type, uuid) {
return this.getRecord(
type,
await this._sessionCall(`${type}.get_by_uuid`, [uuid])
@@ -515,7 +546,7 @@ export class Xapi extends EventEmitter {
}
@cancelable
getResource ($cancelToken, pathname, { host, query, task }) {
getResource($cancelToken, pathname, { host, query, task }) {
return this._autoTask(task, `Xapi#getResource ${pathname}`).then(
taskRef => {
query = { ...query, session_id: this.sessionId }
@@ -555,7 +586,7 @@ export class Xapi extends EventEmitter {
}
@cancelable
putResource ($cancelToken, body, pathname, { host, query, task } = {}) {
putResource($cancelToken, body, pathname, { host, query, task } = {}) {
if (this._readOnly) {
return Promise.reject(
new Error(new Error('cannot put resource in read only mode'))
@@ -661,11 +692,11 @@ export class Xapi extends EventEmitter {
)
}
setField ({ $type, $ref }, field, value) {
setField({ $type, $ref }, field, value) {
return this.call(`${$type}.set_${field}`, $ref, value).then(noop)
}
setFieldEntries (record, field, entries) {
setFieldEntries(record, field, entries) {
return Promise.all(
getKeys(entries).map(entry => {
const value = entries[entry]
@@ -678,7 +709,7 @@ export class Xapi extends EventEmitter {
).then(noop)
}
async setFieldEntry ({ $type, $ref }, field, entry, value) {
async setFieldEntry({ $type, $ref }, field, entry, value) {
while (true) {
try {
await this.call(`${$type}.add_to_${field}`, $ref, entry, value)
@@ -692,11 +723,11 @@ export class Xapi extends EventEmitter {
}
}
unsetFieldEntry ({ $type, $ref }, field, entry) {
unsetFieldEntry({ $type, $ref }, field, entry) {
return this.call(`${$type}.remove_from_${field}`, $ref, entry)
}
watchTask (ref) {
watchTask(ref) {
const watchers = this._taskWatchers
if (watchers === undefined) {
throw new Error('Xapi#watchTask() requires events watching')
@@ -721,16 +752,16 @@ export class Xapi extends EventEmitter {
return watcher.promise
}
get pool () {
get pool() {
return this._pool
}
get objects () {
get objects() {
return this._objects
}
// return a promise which resolves to a task ref or undefined
_autoTask (task = this._taskWatchers !== undefined, name) {
_autoTask(task = this._taskWatchers !== undefined, name) {
if (task === false) {
return Promise.resolve()
}
@@ -744,7 +775,7 @@ export class Xapi extends EventEmitter {
}
// Medium level call: handle session errors.
_sessionCall (method, args) {
_sessionCall(method, args) {
try {
if (startsWith(method, 'session.')) {
throw new Error('session.*() methods are disabled from this interface')
@@ -755,24 +786,27 @@ export class Xapi extends EventEmitter {
newArgs.push.apply(newArgs, args)
}
return pCatch.call(
this._transportCall(method, newArgs),
isSessionInvalid,
() => {
// XAPI is sometimes reinitialized and sessions are lost.
// Try to login again.
debug('%s: the session has been reinitialized', this._humanId)
return pTimeout.call(
pCatch.call(
this._transportCall(method, newArgs),
isSessionInvalid,
() => {
// XAPI is sometimes reinitialized and sessions are lost.
// Try to login again.
debug('%s: the session has been reinitialized', this._humanId)
this._sessionId = null
return this.connect().then(() => this._sessionCall(method, args))
}
this._sessionId = null
return this.connect().then(() => this._sessionCall(method, args))
}
),
this._callTimeout(method, args)
)
} catch (error) {
return Promise.reject(error)
}
}
_addObject (type, ref, object) {
_addObject(type, ref, object) {
object = this._wrapRecord(type, ref, object)
// Finally freezes the object.
@@ -819,7 +853,7 @@ export class Xapi extends EventEmitter {
}
}
_removeObject (type, ref) {
_removeObject(type, ref) {
const byRefs = this._objectsByRef
const object = byRefs[ref]
if (object !== undefined) {
@@ -842,7 +876,7 @@ export class Xapi extends EventEmitter {
}
}
_processEvents (events) {
_processEvents(events) {
forEach(events, event => {
const { class: type, ref } = event
if (event.operation === 'del') {
@@ -853,7 +887,7 @@ export class Xapi extends EventEmitter {
})
}
_watchEvents () {
_watchEvents() {
const loop = () =>
this.status === CONNECTED &&
pTimeout
@@ -926,7 +960,7 @@ export class Xapi extends EventEmitter {
// methods.
//
// It also has to manually get all objects first.
_watchEventsLegacy () {
_watchEventsLegacy() {
const getAllObjects = () => {
return this._sessionCall('system.listMethods').then(methods => {
// Uses introspection to determine the methods to use to get
@@ -978,7 +1012,7 @@ export class Xapi extends EventEmitter {
return getAllObjects().then(watchEvents)
}
_wrapRecord (type, ref, data) {
_wrapRecord(type, ref, data) {
const RecordsByType = this._RecordsByType
let Record = RecordsByType[type]
if (Record === undefined) {
@@ -989,7 +1023,7 @@ export class Xapi extends EventEmitter {
const objectsByRef = this._objectsByRef
const getObjectByRef = ref => objectsByRef[ref]
Record = function (ref, data) {
Record = function(ref, data) {
defineProperties(this, {
$id: { value: data.uuid || ref },
$ref: { value: ref },
@@ -1003,7 +1037,7 @@ export class Xapi extends EventEmitter {
const getters = { $pool: this._getPool }
const props = { $type: type }
fields.forEach(field => {
props[`set_${field}`] = function (value) {
props[`set_${field}`] = function(value) {
return xapi.setField(this, field, value)
}
@@ -1012,19 +1046,19 @@ export class Xapi extends EventEmitter {
const value = data[field]
if (isArray(value)) {
if (value.length === 0 || isOpaqueRef(value[0])) {
getters[$field] = function () {
getters[$field] = function() {
const value = this[field]
return value.length === 0 ? value : value.map(getObjectByRef)
}
}
props[`add_to_${field}`] = function (...values) {
props[`add_to_${field}`] = function(...values) {
return xapi
.call(`${type}.add_${field}`, this.$ref, values)
.then(noop)
}
} else if (value !== null && typeof value === 'object') {
getters[$field] = function () {
getters[$field] = function() {
const value = this[field]
const result = {}
getKeys(value).forEach(key => {
@@ -1032,11 +1066,11 @@ export class Xapi extends EventEmitter {
})
return result
}
props[`update_${field}`] = function (entries) {
props[`update_${field}`] = function(entries) {
return xapi.setFieldEntries(this, field, entries)
}
} else if (isOpaqueRef(value)) {
getters[$field] = function () {
getters[$field] = function() {
return objectsByRef[this[field]]
}
}
@@ -1065,18 +1099,21 @@ export class Xapi extends EventEmitter {
Xapi.prototype._transportCall = reduce(
[
function (method, args) {
function(method, args) {
return this._call(method, args).catch(error => {
if (!(error instanceof Error)) {
error = wrapError(error)
}
error.method = method
error.call = {
method,
params: replaceSensitiveValues(args, '* obfuscated *'),
}
throw error
})
},
call =>
function () {
function() {
let iterator // lazily created
const loop = () =>
pCatch.call(
@@ -1117,7 +1154,7 @@ Xapi.prototype._transportCall = reduce(
return loop()
},
call =>
function loop () {
function loop() {
return pCatch.call(
call.apply(this, arguments),
isHostSlave,
@@ -1140,7 +1177,7 @@ Xapi.prototype._transportCall = reduce(
)
},
call =>
function (method) {
function(method) {
const startTime = Date.now()
return call.apply(this, arguments).then(
result => {

View File

@@ -1,5 +1,9 @@
'use strict'
const { unauthorized } = require('xo-common/api-errors')
// ===================================================================
// These global variables are not a problem because the algorithm is
// synchronous.
let permissionsByObject
@@ -105,23 +109,26 @@ function checkAuthorization (objectId, permission) {
// -------------------------------------------------------------------
module.exports = (
function assertPermissions (
permissionsByObject_,
getObject_,
permissions,
permission
) => {
) {
// Assign global variables.
permissionsByObject = permissionsByObject_
getObject = getObject_
try {
if (permission) {
return checkAuthorization(permissions, permission)
if (permission !== undefined) {
const objectId = permissions
if (!checkAuthorization(objectId, permission)) {
throw unauthorized(permission, objectId)
}
} else {
for (const [objectId, permission] of permissions) {
if (!checkAuthorization(objectId, permission)) {
return false
throw unauthorized(permission, objectId)
}
}
}
@@ -132,3 +139,16 @@ module.exports = (
permissionsByObject = getObject = null
}
}
exports.assert = assertPermissions
exports.check = function checkPermissions () {
try {
assertPermissions.apply(undefined, arguments)
return true
} catch (error) {
if (unauthorized.is(error)) {
return false
}
throw error
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "xo-acl-resolver",
"version": "0.3.0",
"version": "0.4.0",
"license": "ISC",
"description": "Xen-Orchestra internal: do ACLs resolution",
"keywords": [],
@@ -21,5 +21,8 @@
],
"engines": {
"node": ">=6"
},
"dependencies": {
"xo-common": "^0.2.0"
}
}

View File

@@ -120,7 +120,7 @@ encoding by prefixing with `json:`:
##### VM import
```
> xo-cli vm.import host=60a6939e-8b0a-4352-9954-5bde44bcdf7d @=vm.xva
> xo-cli vm.import sr=60a6939e-8b0a-4352-9954-5bde44bcdf7d @=vm.xva
```
## Development

View File

@@ -42,7 +42,7 @@
"nice-pipe": "0.0.0",
"pretty-ms": "^4.0.0",
"progress-stream": "^2.0.0",
"promise-toolbox": "^0.10.1",
"promise-toolbox": "^0.11.0",
"pump": "^3.0.0",
"pw": "^0.0.4",
"strip-indent": "^2.0.0",

View File

@@ -1,6 +1,6 @@
{
"name": "xo-common",
"version": "0.1.2",
"version": "0.2.0",
"license": "AGPL-3.0",
"description": "Code shared between [XO](https://xen-orchestra.com) server and clients",
"keywords": [],

View File

@@ -37,8 +37,15 @@ export const noSuchObject = create(1, (id, type) => ({
message: `no such ${type || 'object'} ${id}`,
}))
export const unauthorized = create(2, () => ({
message: 'not authenticated or not enough permissions',
export const unauthorized = create(2, (permission, objectId, objectType) => ({
data: {
permission,
object: {
id: objectId,
type: objectType,
},
},
message: 'not enough permissions',
}))
export const invalidCredentials = create(3, () => ({

View File

@@ -0,0 +1,10 @@
/examples/
example.js
example.js.map
*.example.js
*.example.js.map
/test/
/tests/
*.spec.js
*.spec.js.map

View File

@@ -0,0 +1,64 @@
# xo-import-servers-csv [![Build Status](https://travis-ci.org/vatesfr/xen-orchestra.png?branch=master)](https://travis-ci.org/vatesfr/xen-orchestra)
> CLI to import servers in XO from a CSV file
## Install
Installation of the [npm package](https://npmjs.org/package/xo-import-servers-csv):
```
> npm install --global xo-import-servers-csv
```
## Usage
`servers.csv`:
```csv
host,username,password
xs1.company.net,user1,password1
xs2.company.net:8080,user2,password2
http://xs3.company.net,user3,password3
```
> The CSV file can also contains these optional fields: `label`, `autoConnect`, `allowUnauthorized`.
Shell command:
```
> xo-import-servers-csv 'https://xo.company.tld' admin@admin.net admin < servers.csv
```
## Development
```
# Install dependencies
> npm install
# Run the tests
> npm test
# Continuously compile
> npm run dev
# Continuously run the tests
> npm run dev-test
# Build for production (automatically called by npm install)
> npm run build
```
## Contributions
Contributions are *very* welcomed, either on the documentation or on
the code.
You may:
- report any [issue](https://github.com/vatesfr/xen-orchestra/issues)
you've encountered;
- fork and create a pull request.
## License
ISC © [Vates SAS](http://vates.fr)

View File

@@ -0,0 +1,59 @@
{
"name": "xo-import-servers-csv",
"version": "1.1.0",
"license": "ISC",
"description": "CLI to import servers in XO from a CSV file",
"keywords": [
"csv",
"host",
"import",
"orchestra",
"pool",
"server",
"xen",
"xen-orchestra"
],
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/packages/xo-import-servers-csv",
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
"repository": {
"type": "git",
"url": "https://github.com/vatesfr/xen-orchestra.git"
},
"author": {
"name": "Julien Fontanet",
"email": "julien.fontanet@vates.fr"
},
"main": "dist/",
"bin": {
"xo-import-servers-csv": "dist/index.js"
},
"files": [
"dist/"
],
"engines": {
"node": ">=4"
},
"dependencies": {
"csv-parser": "^2.1.0",
"end-of-stream": "^1.1.0",
"exec-promise": "^0.7.0",
"highland": "^2.10.1",
"through2": "^3.0.0",
"xo-lib": "^0.9.0"
},
"devDependencies": {
"@types/node": "^10.12.2",
"@types/through2": "^2.0.31",
"tslint": "^5.9.1",
"tslint-config-standard": "^8.0.1",
"typescript": "^3.1.6"
},
"scripts": {
"build": "tsc",
"dev": "tsc -w",
"lint": "tslint 'src/*.ts'",
"posttest": "yarn run lint",
"prepublishOnly": "yarn run build",
"start": "node dist/index.js"
}
}

View File

@@ -0,0 +1,23 @@
declare module 'csv-parser' {
function csvParser(opts?: Object): any
export = csvParser
}
declare module 'exec-promise' {
function execPromise(cb: (args: string[]) => any): void
export = execPromise
}
declare module 'xo-lib' {
export default class Xo {
user?: { email: string }
constructor(opts?: { credentials?: {}; url: string })
call(method: string, ...params: any[]): Promise<any>
open(): Promise<void>
signIn(credentials: {}): Promise<void>
}
}

View File

@@ -0,0 +1,87 @@
#!/usr/bin/env node
/// <reference path="./index.d.ts" />
import csvParser = require('csv-parser')
import execPromise = require('exec-promise')
import through2 = require('through2')
import Xo from 'xo-lib'
const parseBoolean = (
value: string,
defaultValue?: boolean
): boolean | undefined => {
if (value === undefined || value === '') {
return defaultValue
}
const lcValue = value.toLocaleLowerCase()
if (value === '0' || lcValue === 'false') {
return false
}
if (value === '1' || lcValue === 'true') {
return true
}
throw new Error(`invalid boolean value: ${value}`)
}
const requiredParam = (name: string) => {
throw `missing param: ${name}
Usage: xo-import-servers-csv $url $username $password < $csvFile`
}
execPromise(
async ([
url = requiredParam('url'),
username = requiredParam('username'),
password = requiredParam('password'),
]): Promise<void> => {
const xo = new Xo({ url })
await xo.open()
await xo.signIn({ username, password })
console.log('connected as', xo.user!.email)
const errors: any[] = []
const stream = process.stdin.pipe(csvParser()).pipe(
through2.obj(
(
{ allowUnauthorized, autoConnect, host, label, password, username },
_,
next
) => {
console.log('server', host)
xo.call('server.add', {
allowUnauthorized: parseBoolean(allowUnauthorized),
autoConnect: parseBoolean(autoConnect, false),
host,
label,
password,
username,
}).then(
() => next(),
(error: any) => {
errors.push({ host, error })
return next()
}
)
}
)
)
await new Promise((resolve, reject) => {
stream.on('error', reject)
stream.on('finish', resolve)
})
if (errors.length) {
console.error(errors)
}
}
)

View File

@@ -0,0 +1,14 @@
{
"compilerOptions": {
"module": "commonjs",
"moduleResolution": "node",
"newLine": "lf",
"noImplicitAny": true,
"outDir": "dist/",
"removeComments": true,
"sourceMap": true,
"strictNullChecks": true,
"target": "es2015"
},
"includes": "src/**/*"
}

View File

@@ -0,0 +1,3 @@
{
"extends": "tslint-config-standard"
}

View File

@@ -38,7 +38,7 @@
"inquirer": "^6.0.0",
"ldapjs": "^1.0.1",
"lodash": "^4.17.4",
"promise-toolbox": "^0.10.1"
"promise-toolbox": "^0.11.0"
},
"devDependencies": {
"@babel/cli": "^7.0.0",

View File

@@ -667,8 +667,8 @@ class BackupReportsXoPlugin {
const globalStatus = globalSuccess
? `Success`
: nFailures !== 0
? `Failure`
: `Skipped`
? `Failure`
: `Skipped`
let markdown = [
`## Global status: ${globalStatus}`,
@@ -727,8 +727,8 @@ class BackupReportsXoPlugin {
globalSuccess
? ICON_SUCCESS
: nFailures !== 0
? ICON_FAILURE
: ICON_SKIPPED
? ICON_FAILURE
: ICON_SKIPPED
}`,
nagiosStatus: globalSuccess ? 0 : 2,
nagiosMarkdown: globalSuccess

View File

@@ -1,9 +1,8 @@
import Client, { createBackoff } from 'jsonrpc-websocket-client'
import hrp from 'http-request-plus'
const UPDATER_URL = 'localhost'
const WS_PORT = 9001
const HTTP_PORT = 9002
const WS_URL = 'ws://localhost:9001'
const HTTP_URL = 'http://localhost:9002'
// ===================================================================
@@ -47,7 +46,7 @@ class XoServerCloud {
this
)
const updater = (this._updater = new Client(`${UPDATER_URL}:${WS_PORT}`))
const updater = (this._updater = new Client(WS_URL))
const connect = () =>
updater.open(createBackoff()).catch(error => {
console.error('xo-server-cloud: fail to connect to updater', error)
@@ -143,7 +142,7 @@ class XoServerCloud {
throw new Error('cannot get download token')
}
const response = await hrp(`${UPDATER_URL}:${HTTP_PORT}/`, {
const response = await hrp(HTTP_URL, {
headers: {
Authorization: `Bearer ${downloadToken}`,
},

View File

@@ -1,6 +1,6 @@
{
"name": "xo-server-perf-alert",
"version": "0.1.0",
"version": "0.2.0",
"license": "AGPL-3.0",
"description": "",
"keywords": [],

View File

@@ -3,14 +3,18 @@ import { createSchedule } from '@xen-orchestra/cron'
import { assign, forOwn, map, mean } from 'lodash'
import { utcParse } from 'd3-time-format'
const COMPARATOR_FN = {
'>': (a, b) => a > b,
'<': (a, b) => a < b,
}
const VM_FUNCTIONS = {
cpuUsage: {
name: 'VM CPU usage',
description:
'Raises an alarm when the average usage of any CPU is higher than the threshold',
'Raises an alarm when the average usage of any CPU is higher/lower than the threshold',
unit: '%',
comparator: '>',
createParser: (legend, threshold) => {
createParser: (comparator, legend, threshold) => {
const regex = /cpu[0-9]+/
const filteredLegends = legend.filter(l => l.name.match(regex))
const accumulator = Object.assign(
@@ -27,17 +31,17 @@ const VM_FUNCTIONS = {
})
},
getDisplayableValue,
shouldAlarm: () => getDisplayableValue() > threshold,
shouldAlarm: () =>
COMPARATOR_FN[comparator](getDisplayableValue(), threshold),
}
},
},
memoryUsage: {
name: 'VM memory usage',
description:
'Raises an alarm when the used memory % is higher than the threshold',
'Raises an alarm when the used memory % is higher/lower than the threshold',
unit: '% used',
comparator: '>',
createParser: (legend, threshold) => {
createParser: (comparator, legend, threshold) => {
const memoryBytesLegend = legend.find(l => l.name === 'memory')
const memoryKBytesFreeLegend = legend.find(
l => l.name === 'memory_internal_free'
@@ -52,9 +56,8 @@ const VM_FUNCTIONS = {
)
},
getDisplayableValue,
shouldAlarm: () => {
return getDisplayableValue() > threshold
},
shouldAlarm: () =>
COMPARATOR_FN[comparator](getDisplayableValue(), threshold),
}
},
},
@@ -64,10 +67,9 @@ const HOST_FUNCTIONS = {
cpuUsage: {
name: 'host CPU usage',
description:
'Raises an alarm when the average usage of any CPU is higher than the threshold',
'Raises an alarm when the average usage of any CPU is higher/lower than the threshold',
unit: '%',
comparator: '>',
createParser: (legend, threshold) => {
createParser: (comparator, legend, threshold) => {
const regex = /^cpu[0-9]+$/
const filteredLegends = legend.filter(l => l.name.match(regex))
const accumulator = Object.assign(
@@ -84,17 +86,17 @@ const HOST_FUNCTIONS = {
})
},
getDisplayableValue,
shouldAlarm: () => getDisplayableValue() > threshold,
shouldAlarm: () =>
COMPARATOR_FN[comparator](getDisplayableValue(), threshold),
}
},
},
memoryUsage: {
name: 'host memory usage',
description:
'Raises an alarm when the used memory % is higher than the threshold',
'Raises an alarm when the used memory % is higher/lower than the threshold',
unit: '% used',
comparator: '>',
createParser: (legend, threshold) => {
createParser: (comparator, legend, threshold) => {
const memoryKBytesLegend = legend.find(l => l.name === 'memory_total_kib')
const memoryKBytesFreeLegend = legend.find(
l => l.name === 'memory_free_kib'
@@ -109,7 +111,8 @@ const HOST_FUNCTIONS = {
)
},
getDisplayableValue,
shouldAlarm: () => getDisplayableValue() > threshold,
shouldAlarm: () =>
COMPARATOR_FN[comparator](getDisplayableValue(), threshold),
}
},
},
@@ -119,15 +122,15 @@ const SR_FUNCTIONS = {
storageUsage: {
name: 'SR storage usage',
description:
'Raises an alarm when the used disk space % is higher than the threshold',
'Raises an alarm when the used disk space % is higher/lower than the threshold',
unit: '% used',
comparator: '>',
createGetter: threshold => sr => {
createGetter: (comparator, threshold) => sr => {
const getDisplayableValue = () =>
(sr.physical_utilisation * 100) / sr.physical_size
return {
getDisplayableValue,
shouldAlarm: () => getDisplayableValue() > threshold,
shouldAlarm: () =>
COMPARATOR_FN[comparator](getDisplayableValue(), threshold),
}
},
},
@@ -139,6 +142,13 @@ const TYPE_FUNCTION_MAP = {
sr: SR_FUNCTIONS,
}
const COMPARATOR_ENTRY = {
title: 'Comparator',
type: 'string',
default: Object.keys(COMPARATOR_FN)[0],
enum: Object.keys(COMPARATOR_FN),
}
// list of currently ringing alarms, to avoid double notification
const currentAlarms = {}
@@ -182,10 +192,11 @@ export const configurationSchema = {
default: Object.keys(HOST_FUNCTIONS)[0],
enum: Object.keys(HOST_FUNCTIONS),
},
comparator: COMPARATOR_ENTRY,
alarmTriggerLevel: {
title: 'Threshold',
description:
'The direction of the crossing is given by the Alarm type',
'The direction of the crossing is given by the comparator type',
type: 'number',
default: 40,
},
@@ -222,7 +233,7 @@ export const configurationSchema = {
description: Object.keys(VM_FUNCTIONS)
.map(
k =>
` * ${k} (${VM_FUNCTIONS[k].unit}):${
` * ${k} (${VM_FUNCTIONS[k].unit}): ${
VM_FUNCTIONS[k].description
}`
)
@@ -231,10 +242,11 @@ export const configurationSchema = {
default: Object.keys(VM_FUNCTIONS)[0],
enum: Object.keys(VM_FUNCTIONS),
},
comparator: COMPARATOR_ENTRY,
alarmTriggerLevel: {
title: 'Threshold',
description:
'The direction of the crossing is given by the Alarm type',
'The direction of the crossing is given by the comparator type',
type: 'number',
default: 40,
},
@@ -281,10 +293,11 @@ export const configurationSchema = {
default: Object.keys(SR_FUNCTIONS)[0],
enum: Object.keys(SR_FUNCTIONS),
},
comparator: COMPARATOR_ENTRY,
alarmTriggerLevel: {
title: 'Threshold',
description:
'The direction of the crossing is given by the Alarm type',
'The direction of the crossing is given by the comparator type',
type: 'number',
default: 80,
},
@@ -440,6 +453,7 @@ ${monitorBodies.join('\n')}`
relatedNode[l.name] = l
})
const parser = typeFunction.createParser(
definition.comparator,
parsedLegend.filter(l => l.uuid === uuid),
definition.alarmTriggerLevel
)
@@ -454,7 +468,7 @@ ${monitorBodies.join('\n')}`
...definition,
alarmId,
vmFunction: typeFunction,
title: `${typeFunction.name} ${typeFunction.comparator} ${
title: `${typeFunction.name} ${definition.comparator} ${
definition.alarmTriggerLevel
}${typeFunction.unit}`,
snapshot: async () => {
@@ -463,7 +477,6 @@ ${monitorBodies.join('\n')}`
try {
const result = {
uuid,
name: definition.name,
object: this._xo.getXapi(uuid).getObject(uuid),
}
@@ -489,6 +502,7 @@ ${monitorBodies.join('\n')}`
} else {
// Stats via XAPI
const getter = typeFunction.createGetter(
definition.comparator,
definition.alarmTriggerLevel
)
const data = getter(result.object)
@@ -535,6 +549,40 @@ ${monitorBodies.join('\n')}`
)
}
// Sample of a monitor
// {
// uuids: ['8485ea1f-b475-f6f2-58a7-895ab626ce5d'],
// variableName: 'cpuUsage',
// comparator: '>',
// alarmTriggerLevel: 50,
// alarmTriggerPeriod: 60,
// objectType: 'host',
// alarmId: 'host|cpuUsage|50',
// title: 'host CPU usage > 50',
// vmFunction: {
// name: 'host CPU usage',
// description: 'Raises an alarm when the average usage of any CPU is higher/lower than the threshold',
// unit: '%',
// createParser: [Function: createParser],
// },
// snapshot: [Function: snapshot],
// }
//
// Sample of an entry of a snapshot
// {
// uuid: '8485ea1f-b475-f6f2-58a7-895ab626ce5d',
// object: host,
// objectLink: '[lab1](localhost:3000#/hosts/485ea1f-b475-f6f2-58a7-895ab626ce5d/stats)'
// rrd: stats,
// data: {
// parseRow: [Function: parseRow],
// getDisplayableValue: [Function: getDisplayableValue],
// shouldAlarm: [Function: shouldAlarm],
// },
// value: 70,
// shouldAlarm: true,
// listItem: ' * [lab1](localhost:3000#/hosts/485ea1f-b475-f6f2-58a7-895ab626ce5d/stats): 70%\n'
// }
async _checkMonitors () {
const monitors = this._getMonitors()
for (const monitor of monitors) {

View File

@@ -33,7 +33,7 @@
"dependencies": {
"nodemailer": "^4.4.1",
"nodemailer-markdown": "^1.0.1",
"promise-toolbox": "^0.10.1"
"promise-toolbox": "^0.11.0"
},
"devDependencies": {
"@babel/cli": "^7.0.0",

View File

@@ -32,7 +32,7 @@
"node": ">=6"
},
"dependencies": {
"promise-toolbox": "^0.10.1",
"promise-toolbox": "^0.11.0",
"slack-node": "^0.1.8"
},
"devDependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "xo-server-usage-report",
"version": "0.6.0",
"version": "0.7.1",
"license": "AGPL-3.0",
"description": "",
"keywords": [
@@ -36,11 +36,12 @@
"dependencies": {
"@xen-orchestra/async-map": "^0.0.0",
"@xen-orchestra/cron": "^1.0.3",
"@xen-orchestra/log": "^0.1.4",
"handlebars": "^4.0.6",
"html-minifier": "^3.5.8",
"human-format": "^0.10.0",
"lodash": "^4.17.4",
"promise-toolbox": "^0.10.1"
"promise-toolbox": "^0.11.0"
},
"devDependencies": {
"@babel/cli": "^7.0.0",

View File

@@ -1,4 +1,5 @@
import asyncMap from '@xen-orchestra/async-map'
import createLogger from '@xen-orchestra/log'
import Handlebars from 'handlebars'
import humanFormat from 'human-format'
import { createSchedule } from '@xen-orchestra/cron'
@@ -23,6 +24,8 @@ import { readFile, writeFile } from 'fs'
// ===================================================================
const log = createLogger('xo:xo-server-usage-report')
const GRANULARITY = 'days'
const pReadFile = promisify(readFile)
@@ -251,12 +254,10 @@ function getTop (objects, options) {
function computePercentage (curr, prev, options) {
return zipObject(
options,
map(
options,
opt =>
prev[opt] === 0 || prev[opt] === null
? 'NONE'
: `${((curr[opt] - prev[opt]) * 100) / prev[opt]}`
map(options, opt =>
prev[opt] === 0 || prev[opt] === null
? 'NONE'
: `${((curr[opt] - prev[opt]) * 100) / prev[opt]}`
)
)
}
@@ -287,7 +288,18 @@ async function getVmsStats ({ runningVms, xo }) {
return orderBy(
await Promise.all(
map(runningVms, async vm => {
const { stats } = await xo.getXapiVmStats(vm, GRANULARITY)
const { stats } = await xo
.getXapiVmStats(vm, GRANULARITY)
.catch(error => {
log.warn('Error on fetching VM stats', {
error,
vmId: vm.id,
})
return {
stats: {},
}
})
const iopsRead = METRICS_MEAN.iops(get(stats.iops, 'r'))
const iopsWrite = METRICS_MEAN.iops(get(stats.iops, 'w'))
return {
@@ -314,7 +326,18 @@ async function getHostsStats ({ runningHosts, xo }) {
return orderBy(
await Promise.all(
map(runningHosts, async host => {
const { stats } = await xo.getXapiHostStats(host, GRANULARITY)
const { stats } = await xo
.getXapiHostStats(host, GRANULARITY)
.catch(error => {
log.warn('Error on fetching host stats', {
error,
hostId: host.id,
})
return {
stats: {},
}
})
return {
uuid: host.uuid,
name: host.name_label,
@@ -351,7 +374,18 @@ async function getSrsStats ({ xo, xoObjects }) {
name += ` (${container.name_label})`
}
const { stats } = await xo.getXapiSrStats(sr.id, GRANULARITY)
const { stats } = await xo
.getXapiSrStats(sr.id, GRANULARITY)
.catch(error => {
log.warn('Error on fetching SR stats', {
error,
srId: sr.id,
})
return {
stats: {},
}
})
const iopsRead = computeMean(get(stats.iops, 'r'))
const iopsWrite = computeMean(get(stats.iops, 'w'))

View File

@@ -2,6 +2,13 @@
//
// See sample.config.yaml to override.
{
"apiWebSocketOptions": {
// https://github.com/websockets/ws#websocket-compression
// "perMessageDeflate": {
// "threshold": 524288 // 512kiB
// }
},
"http": {
"listen": [
{
@@ -27,6 +34,7 @@
"mounts": {}
},
"datadir": "/var/lib/xo-server/data",
// Should users be created on first sign in?
@@ -34,6 +42,12 @@
// Necessary for external authentication providers.
"createUserOnFirstSignin": true,
"remoteOptions": {
"mountsDir": "/run/xo-server/mounts",
"timeout": 600e3
},
// Whether API logs should contains the full request/response on
// errors.
//

View File

@@ -1,6 +1,6 @@
{
"name": "xo-server",
"version": "5.28.0",
"version": "5.31.0",
"license": "AGPL-3.0",
"description": "Server part of Xen-Orchestra",
"keywords": [
@@ -34,11 +34,11 @@
"@xen-orchestra/async-map": "^0.0.0",
"@xen-orchestra/cron": "^1.0.3",
"@xen-orchestra/emit-async": "^0.0.0",
"@xen-orchestra/fs": "^0.4.0",
"@xen-orchestra/log": "^0.1.0",
"@xen-orchestra/fs": "^0.4.1",
"@xen-orchestra/log": "^0.1.4",
"@xen-orchestra/mixin": "^0.0.0",
"ajv": "^6.1.1",
"app-conf": "^0.5.0",
"app-conf": "^0.6.0",
"archiver": "^3.0.0",
"async-iterator-to-stream": "^1.0.1",
"base64url": "^3.0.0",
@@ -95,7 +95,7 @@
"passport": "^0.4.0",
"passport-local": "^1.0.0",
"pretty-format": "^23.0.0",
"promise-toolbox": "^0.10.1",
"promise-toolbox": "^0.11.0",
"proxy-agent": "^3.0.0",
"pug": "^2.0.0-rc.4",
"pump": "^3.0.0",
@@ -109,17 +109,17 @@
"stoppable": "^1.0.5",
"struct-fu": "^1.2.0",
"tar-stream": "^1.5.5",
"through2": "^2.0.3",
"through2": "^3.0.0",
"tmp": "^0.0.33",
"uuid": "^3.0.1",
"value-matcher": "^0.2.0",
"vhd-lib": "^0.4.0",
"ws": "^6.0.0",
"xen-api": "^0.19.0",
"xen-api": "^0.22.0",
"xml2js": "^0.4.19",
"xo-acl-resolver": "^0.3.0",
"xo-acl-resolver": "^0.4.0",
"xo-collection": "^0.4.1",
"xo-common": "^0.1.2",
"xo-common": "^0.2.0",
"xo-remote-parser": "^0.5.0",
"xo-vmdk-to-vhd": "^0.1.5",
"yazl": "^2.4.3"
@@ -138,6 +138,7 @@
"@babel/preset-env": "^7.0.0",
"@babel/preset-flow": "^7.0.0",
"babel-plugin-lodash": "^3.3.2",
"babel-plugin-transform-dev": "^2.0.1",
"cross-env": "^5.1.3",
"index-modules": "^0.3.0",
"rimraf": "^2.6.2"

View File

@@ -1,7 +1,7 @@
import createLogger from '@xen-orchestra/log'
import pump from 'pump'
import { format } from 'json-rpc-peer'
import { noSuchObject, unauthorized } from 'xo-common/api-errors'
import { noSuchObject } from 'xo-common/api-errors'
import { parseSize } from '../utils'
@@ -31,9 +31,7 @@ export async function create ({ name, size, sr, vm, bootable, position, mode })
// the resource set does not exist, falls back to normal check
}
if (!(await this.hasPermissions(this.user.id, [[sr.id, 'administrate']]))) {
throw unauthorized()
}
await this.checkPermissions(this.user.id, [[sr.id, 'administrate']])
} while (false)
const xapi = this.getXapi(sr)
@@ -125,6 +123,7 @@ async function handleImportContent (req, res, { xapi, id }) {
req.setTimeout(43200000) // 12 hours
try {
req.length = +req.headers['content-length']
await xapi.importVdiContent(id, req)
res.end(format.response(0, true))
} catch (e) {

View File

@@ -1,5 +1,3 @@
import { unauthorized } from 'xo-common/api-errors'
export function create (props) {
return this.createIpPool(props)
}
@@ -22,15 +20,12 @@ delete_.description = 'Delete an ipPool'
export function getAll (params) {
const { user } = this
if (!user) {
throw unauthorized()
}
return this.getAllIpPools(
user.permission === 'admin' ? params && params.userId : user.id
)
}
getAll.permission = ''
getAll.description = 'List all ipPools'
// -------------------------------------------------------------------

View File

@@ -1,7 +1,3 @@
import { unauthorized } from 'xo-common/api-errors'
// ===================================================================
export function create ({ name, subjects, objects, limits }) {
return this.createResourceSet(name, subjects, objects, limits)
}
@@ -99,14 +95,10 @@ set.params = {
// -------------------------------------------------------------------
export function get ({ id }) {
const { user } = this
if (!user) {
throw unauthorized()
}
return this.getResourceSet(id)
}
get.permission = ''
get.params = {
id: {
type: 'string',
@@ -116,14 +108,10 @@ get.params = {
// -------------------------------------------------------------------
export async function getAll () {
const { user } = this
if (!user) {
throw unauthorized()
}
return this.getAllResourceSets(user.id)
return this.getAllResourceSets(this.user.id)
}
getAll.permission = ''
getAll.description = 'Get the list of all existing resource set'
// -------------------------------------------------------------------

View File

@@ -4,7 +4,7 @@ export async function add ({ autoConnect = true, ...props }) {
const server = await this.registerXenServer(props)
if (autoConnect) {
;this.connectXenServer(server.id)::ignoreErrors()
this.connectXenServer(server.id)::ignoreErrors()
}
return server.id
@@ -105,7 +105,7 @@ set.params = {
// -------------------------------------------------------------------
export async function connect ({ id }) {
;this.updateXenServer(id, { enabled: true })::ignoreErrors()
this.updateXenServer(id, { enabled: true })::ignoreErrors()
await this.connectXenServer(id)
}
@@ -122,7 +122,7 @@ connect.params = {
// -------------------------------------------------------------------
export async function disconnect ({ id }) {
;this.updateXenServer(id, { enabled: false })::ignoreErrors()
this.updateXenServer(id, { enabled: false })::ignoreErrors()
await this.disconnectXenServer(id)
}

View File

@@ -1,6 +1,6 @@
// FIXME: rename to disk.*
import { invalidParameters, unauthorized } from 'xo-common/api-errors'
import { invalidParameters } from 'xo-common/api-errors'
import { isArray, reduce } from 'lodash'
import { parseSize } from '../utils'
@@ -67,13 +67,8 @@ export async function set (params) {
{ disk: size - vdi.size },
resourceSetId
)
} else if (
!(
this.user.permission === 'admin' ||
(await this.hasPermissions(this.user.id, [[vdi.$SR, 'operate']]))
)
) {
throw unauthorized()
} else {
await this.checkPermissions(this.user.id, [[vdi.$SR, 'operate']])
}
await xapi.resizeVdi(ref, size)

View File

@@ -6,7 +6,7 @@ import { diffItems } from '../utils'
// TODO: move into vm and rename to removeInterface
async function delete_ ({ vif }) {
;this.allocIpAddresses(
this.allocIpAddresses(
vif.id,
null,
vif.allowedIpv4Addresses.concat(vif.allowedIpv6Addresses)

View File

@@ -1,7 +1,7 @@
import concat from 'lodash/concat'
import defer from 'golike-defer'
import { format } from 'json-rpc-peer'
import { ignoreErrors } from 'promise-toolbox'
import { assignWith, concat } from 'lodash'
import {
forbiddenOperation,
invalidParameters,
@@ -13,11 +13,11 @@ import { forEach, map, mapFilter, parseSize } from '../utils'
// ===================================================================
export function getHaValues () {
export function getHaValues() {
return ['best-effort', 'restart', '']
}
function checkPermissionOnSrs (vm, permission = 'operate') {
function checkPermissionOnSrs(vm, permission = 'operate') {
const permissions = []
forEach(vm.$VBDs, vbdId => {
const vbd = this.getObject(vbdId, 'VBD')
@@ -32,13 +32,7 @@ function checkPermissionOnSrs (vm, permission = 'operate') {
])
})
return this.hasPermissions(this.session.get('user_id'), permissions).then(
success => {
if (!success) {
throw unauthorized()
}
}
)
return this.checkPermissions(this.session.get('user_id'), permissions)
}
// ===================================================================
@@ -50,17 +44,14 @@ const extract = (obj, prop) => {
}
// TODO: Implement ACLs
export async function create (params) {
export async function create(params) {
const { user } = this
const resourceSet = extract(params, 'resourceSet')
const template = extract(params, 'template')
if (
resourceSet === undefined &&
!(await this.hasPermissions(this.user.id, [
if (resourceSet === undefined) {
await this.checkPermissions(this.user.id, [
[template.$pool, 'administrate'],
]))
) {
throw unauthorized()
])
}
params.template = template._xapiId
@@ -68,12 +59,10 @@ export async function create (params) {
const xapi = this.getXapi(template)
const objectIds = [template.id]
const cpus = extract(params, 'CPUs')
const memoryMax = extract(params, 'memoryMax')
const limits = {
cpus: cpus !== undefined ? cpus : template.CPUs.number,
cpus: template.CPUs.number,
disk: 0,
memory: memoryMax !== undefined ? memoryMax : template.memory.dynamic[1],
memory: template.memory.dynamic[1],
vms: 1,
}
const vdiSizesByDevice = {}
@@ -153,8 +142,10 @@ export async function create (params) {
if (resourceSet) {
await this.checkResourceSetConstraints(resourceSet, user.id, objectIds)
checkLimits = async limits2 => {
await this.allocateLimitsInResourceSet(limits, resourceSet)
await this.allocateLimitsInResourceSet(limits2, resourceSet)
await this.allocateLimitsInResourceSet(
assignWith({}, limits, limits2, (l1 = 0, l2) => l1 + l2),
resourceSet
)
}
}
@@ -334,7 +325,7 @@ create.resolve = {
// -------------------------------------------------------------------
async function delete_ ({
async function delete_({
delete_disks, // eslint-disable-line camelcase
force,
forceDeleteDefaultTemplate,
@@ -375,7 +366,7 @@ async function delete_ ({
vm.type === 'VM' && // only regular VMs
xapi.xo.getData(vm._xapiId, 'resourceSet') != null
) {
;this.setVmResourceSet(vm._xapiId, null)::ignoreErrors()
this.setVmResourceSet(vm._xapiId, null)::ignoreErrors()
}
return xapi.deleteVm(
@@ -412,7 +403,7 @@ export { delete_ as delete }
// -------------------------------------------------------------------
export async function ejectCd ({ vm }) {
export async function ejectCd({ vm }) {
await this.getXapi(vm).ejectCdFromVm(vm._xapiId)
}
@@ -426,14 +417,14 @@ ejectCd.resolve = {
// -------------------------------------------------------------------
export async function insertCd ({ vm, vdi, force }) {
export async function insertCd({ vm, vdi, force = true }) {
await this.getXapi(vm).insertCdIntoVm(vdi._xapiId, vm._xapiId, { force })
}
insertCd.params = {
id: { type: 'string' },
cd_id: { type: 'string' },
force: { type: 'boolean' },
force: { type: 'boolean', optional: true },
}
insertCd.resolve = {
@@ -445,7 +436,7 @@ insertCd.resolve = {
// -------------------------------------------------------------------
export async function migrate ({
export async function migrate({
vm,
host,
sr,
@@ -477,9 +468,7 @@ export async function migrate ({
})
}
if (!(await this.hasPermissions(this.session.get('user_id'), permissions))) {
throw unauthorized()
}
await this.checkPermissions(this.user.id, permissions)
await this.getXapi(vm).migrateVm(
vm._xapiId,
@@ -523,7 +512,7 @@ migrate.resolve = {
// -------------------------------------------------------------------
export async function set (params) {
export async function set(params) {
const VM = extract(params, 'VM')
const xapi = this.getXapi(VM)
const vmId = VM._xapiId
@@ -614,6 +603,8 @@ set.params = {
// Emulate HVM C000 PCI device for Windows Update to fetch or update PV drivers
hasVendorDevice: { type: 'boolean', optional: true },
expNestedHvm: { type: 'boolean', optional: true },
// Move the vm In to/Out of Self Service
resourceSet: { type: ['string', 'null'], optional: true },
@@ -629,7 +620,7 @@ set.resolve = {
// -------------------------------------------------------------------
export async function restart ({ vm, force }) {
export async function restart({ vm, force = false }) {
const xapi = this.getXapi(vm)
if (force) {
@@ -641,7 +632,7 @@ export async function restart ({ vm, force }) {
restart.params = {
id: { type: 'string' },
force: { type: 'boolean' },
force: { type: 'boolean', optional: true },
}
restart.resolve = {
@@ -650,7 +641,7 @@ restart.resolve = {
// -------------------------------------------------------------------
export const clone = defer(async function (
export const clone = defer(async function(
$defer,
{ vm, name, full_copy: fullCopy }
) {
@@ -692,7 +683,7 @@ clone.resolve = {
// -------------------------------------------------------------------
// TODO: implement resource sets
export async function copy ({ compress, name: nameLabel, sr, vm }) {
export async function copy({ compress, name: nameLabel, sr, vm }) {
if (vm.$pool === sr.$pool) {
if (vm.power_state === 'Running') {
await checkPermissionOnSrs.call(this, vm)
@@ -733,15 +724,9 @@ copy.resolve = {
// -------------------------------------------------------------------
export async function convertToTemplate ({ vm }) {
export async function convertToTemplate({ vm }) {
// Convert to a template requires pool admin permission.
if (
!(await this.hasPermissions(this.session.get('user_id'), [
[vm.$pool, 'administrate'],
]))
) {
throw unauthorized()
}
await this.checkPermissions(this.user.id, [[vm.$pool, 'administrate']])
await this.getXapi(vm).call('VM.set_is_a_template', vm._xapiRef, true)
}
@@ -760,7 +745,7 @@ export { convertToTemplate as convert }
// -------------------------------------------------------------------
// TODO: implement resource sets
export const snapshot = defer(async function (
export const snapshot = defer(async function(
$defer,
{ vm, name = `${vm.name_label}_${new Date().toISOString()}` }
) {
@@ -788,7 +773,7 @@ snapshot.resolve = {
// -------------------------------------------------------------------
export function rollingDeltaBackup ({
export function rollingDeltaBackup({
vm,
remote,
tag,
@@ -820,7 +805,7 @@ rollingDeltaBackup.permission = 'admin'
// -------------------------------------------------------------------
export function importDeltaBackup ({ sr, remote, filePath, mapVdisSrs }) {
export function importDeltaBackup({ sr, remote, filePath, mapVdisSrs }) {
const mapVdisSrsXapi = {}
forEach(mapVdisSrs, (srId, vdiId) => {
@@ -851,7 +836,7 @@ importDeltaBackup.permission = 'admin'
// -------------------------------------------------------------------
export function deltaCopy ({ force, vm, retention, sr }) {
export function deltaCopy({ force, vm, retention, sr }) {
return this.deltaCopyVm(vm, sr, force, retention)
}
@@ -869,7 +854,7 @@ deltaCopy.resolve = {
// -------------------------------------------------------------------
export async function rollingSnapshot ({ vm, tag, depth, retention = depth }) {
export async function rollingSnapshot({ vm, tag, depth, retention = depth }) {
await checkPermissionOnSrs.call(this, vm)
return this.rollingSnapshotVm(vm, tag, retention)
}
@@ -891,7 +876,7 @@ rollingSnapshot.description =
// -------------------------------------------------------------------
export function backup ({ vm, remoteId, file, compress }) {
export function backup({ vm, remoteId, file, compress }) {
return this.backupVm({ vm, remoteId, file, compress })
}
@@ -912,7 +897,7 @@ backup.description = 'Exports a VM to the file system'
// -------------------------------------------------------------------
export function importBackup ({ remote, file, sr }) {
export function importBackup({ remote, file, sr }) {
return this.importVmBackup(remote, file, sr)
}
@@ -933,7 +918,7 @@ importBackup.permission = 'admin'
// -------------------------------------------------------------------
export function rollingBackup ({
export function rollingBackup({
vm,
remoteId,
tag,
@@ -971,7 +956,7 @@ rollingBackup.description =
// -------------------------------------------------------------------
export function rollingDrCopy ({
export function rollingDrCopy({
vm,
pool,
sr,
@@ -1025,7 +1010,7 @@ rollingDrCopy.description =
// -------------------------------------------------------------------
export function start ({ vm, force, host }) {
export function start({ vm, force, host }) {
return this.getXapi(vm).startVm(vm._xapiId, host?._xapiId, force)
}
@@ -1046,7 +1031,7 @@ start.resolve = {
// - if !force → clean shutdown
// - if force is true → hard shutdown
// - if force is integer → clean shutdown and after force seconds, hard shutdown.
export async function stop ({ vm, force }) {
export async function stop({ vm, force }) {
const xapi = this.getXapi(vm)
// Hard shutdown
@@ -1081,7 +1066,7 @@ stop.resolve = {
// -------------------------------------------------------------------
export async function suspend ({ vm }) {
export async function suspend({ vm }) {
await this.getXapi(vm).call('VM.suspend', vm._xapiRef)
}
@@ -1095,7 +1080,21 @@ suspend.resolve = {
// -------------------------------------------------------------------
export function resume ({ vm }) {
export async function pause({ vm }) {
await this.getXapi(vm).call('VM.pause', vm._xapiRef)
}
pause.params = {
id: { type: 'string' },
}
pause.resolve = {
vm: ['id', 'VM', 'operate'],
}
// -------------------------------------------------------------------
export function resume({ vm }) {
return this.getXapi(vm).resumeVm(vm._xapiId)
}
@@ -1109,7 +1108,7 @@ resume.resolve = {
// -------------------------------------------------------------------
export function revert ({ snapshot, snapshotBefore }) {
export function revert({ snapshot, snapshotBefore }) {
return this.getXapi(snapshot).revertVm(snapshot._xapiId, snapshotBefore)
}
@@ -1124,7 +1123,7 @@ revert.resolve = {
// -------------------------------------------------------------------
async function handleExport (req, res, { xapi, id, compress }) {
async function handleExport(req, res, { xapi, id, compress }) {
const stream = await xapi.exportVm(id, {
compress: compress != null ? compress : true,
})
@@ -1141,7 +1140,7 @@ async function handleExport (req, res, { xapi, id, compress }) {
}
// TODO: integrate in xapi.js
async function export_ ({ vm, compress }) {
async function export_({ vm, compress }) {
if (vm.power_state === 'Running') {
await checkPermissionOnSrs.call(this, vm)
}
@@ -1172,7 +1171,7 @@ export { export_ as export }
// -------------------------------------------------------------------
async function handleVmImport (req, res, { data, srId, type, xapi }) {
async function handleVmImport(req, res, { data, srId, type, xapi }) {
// Timeout seems to be broken in Node 4.
// See https://github.com/nodejs/node/issues/3319
req.setTimeout(43200000) // 12 hours
@@ -1187,34 +1186,17 @@ async function handleVmImport (req, res, { data, srId, type, xapi }) {
}
// TODO: "sr_id" can be passed in URL to target a specific SR
async function import_ ({ data, host, sr, type }) {
let xapi
async function import_({ data, sr, type }) {
if (data && type === 'xva') {
throw invalidParameters('unsupported field data for the file type xva')
}
if (!sr) {
if (!host) {
throw invalidParameters('you must provide either host or SR')
}
xapi = this.getXapi(host)
sr = xapi.pool.$default_SR
if (!sr) {
throw invalidParameters('there is not default SR in this pool')
}
// FIXME: must have administrate permission on default SR.
} else {
xapi = this.getXapi(sr)
}
return {
$sendTo: await this.registerHttpRequest(handleVmImport, {
data,
srId: sr._xapiId,
type,
xapi,
xapi: this.getXapi(sr),
}),
}
}
@@ -1249,13 +1231,11 @@ import_.params = {
},
},
},
host: { type: 'string', optional: true },
type: { type: 'string', optional: true },
sr: { type: 'string', optional: true },
sr: { type: 'string' },
}
import_.resolve = {
host: ['host', 'host', 'administrate'],
sr: ['sr', 'SR', 'administrate'],
}
@@ -1265,7 +1245,7 @@ export { import_ as import }
// FIXME: if position is used, all other disks after this position
// should be shifted.
export async function attachDisk ({ vm, vdi, position, mode, bootable }) {
export async function attachDisk({ vm, vdi, position, mode, bootable }) {
await this.getXapi(vm).createVbd({
bootable,
mode,
@@ -1294,7 +1274,7 @@ attachDisk.resolve = {
// -------------------------------------------------------------------
// TODO: implement resource sets
export async function createInterface ({
export async function createInterface({
vm,
network,
position,
@@ -1307,10 +1287,8 @@ export async function createInterface ({
await this.checkResourceSetConstraints(resourceSet, this.user.id, [
network.id,
])
} else if (
!(await this.hasPermissions(this.user.id, [[network.id, 'view']]))
) {
throw unauthorized()
} else {
await this.checkPermissions(this.user.id, [[network.id, 'view']])
}
let ipAddresses
@@ -1365,7 +1343,7 @@ createInterface.resolve = {
// -------------------------------------------------------------------
export async function attachPci ({ vm, pciId }) {
export async function attachPci({ vm, pciId }) {
const xapi = this.getXapi(vm)
await xapi.call('VM.add_to_other_config', vm._xapiRef, 'pci', pciId)
@@ -1382,7 +1360,7 @@ attachPci.resolve = {
// -------------------------------------------------------------------
export async function detachPci ({ vm }) {
export async function detachPci({ vm }) {
const xapi = this.getXapi(vm)
await xapi.call('VM.remove_from_other_config', vm._xapiRef, 'pci')
@@ -1397,7 +1375,7 @@ detachPci.resolve = {
}
// -------------------------------------------------------------------
export function stats ({ vm, granularity }) {
export function stats({ vm, granularity }) {
return this.getXapiVmStats(vm._xapiId, granularity)
}
@@ -1417,7 +1395,7 @@ stats.resolve = {
// -------------------------------------------------------------------
export async function setBootOrder ({ vm, order }) {
export async function setBootOrder({ vm, order }) {
const xapi = this.getXapi(vm)
order = { order }
@@ -1440,7 +1418,7 @@ setBootOrder.resolve = {
// -------------------------------------------------------------------
export function recoveryStart ({ vm }) {
export function recoveryStart({ vm }) {
return this.getXapi(vm).startVmOnCd(vm._xapiId)
}
@@ -1454,7 +1432,7 @@ recoveryStart.resolve = {
// -------------------------------------------------------------------
export function getCloudInitConfig ({ template }) {
export function getCloudInitConfig({ template }) {
return this.getXapi(template).getCloudInitConfig(template._xapiId)
}
@@ -1468,7 +1446,7 @@ getCloudInitConfig.resolve = {
// -------------------------------------------------------------------
export async function createCloudInitConfigDrive ({ vm, sr, config, coreos }) {
export async function createCloudInitConfigDrive({ vm, sr, config, coreos }) {
const xapi = this.getXapi(vm)
if (coreos) {
// CoreOS is a special CloudConfig drive created by XS plugin
@@ -1495,7 +1473,7 @@ createCloudInitConfigDrive.resolve = {
// -------------------------------------------------------------------
export async function createVgpu ({ vm, gpuGroup, vgpuType }) {
export async function createVgpu({ vm, gpuGroup, vgpuType }) {
// TODO: properly handle device. Can a VM have 2 vGPUS?
await this.getXapi(vm).createVgpu(
vm._xapiId,
@@ -1518,7 +1496,7 @@ createVgpu.resolve = {
// -------------------------------------------------------------------
export async function deleteVgpu ({ vgpu }) {
export async function deleteVgpu({ vgpu }) {
await this.getXapi(vgpu).deleteVgpu(vgpu._xapiId)
}

View File

@@ -5,6 +5,7 @@ import execa from 'execa'
import fs from 'fs-extra'
import map from 'lodash/map'
import { tap, delay } from 'promise-toolbox'
import { NULL_REF } from 'xen-api'
import { invalidParameters } from 'xo-common/api-errors'
import { v4 as generateUuid } from 'uuid'
import { includes, remove, filter, find, range } from 'lodash'
@@ -25,7 +26,7 @@ const XOSAN_LICENSE_QUOTA = 50 * GIGABYTE
const CURRENT_POOL_OPERATIONS = {}
function getXosanConfig (xosansr, xapi = this.getXapi(xosansr)) {
function getXosanConfig(xosansr, xapi = this.getXapi(xosansr)) {
const data = xapi.xo.getData(xosansr, 'xosan_config')
if (data && data.networkPrefix === undefined) {
// some xosan might have been created before this field was added
@@ -36,7 +37,7 @@ function getXosanConfig (xosansr, xapi = this.getXapi(xosansr)) {
return data
}
function _getIPToVMDict (xapi, sr) {
function _getIPToVMDict(xapi, sr) {
const dict = {}
const data = getXosanConfig(sr, xapi)
if (data && data.nodes) {
@@ -54,7 +55,7 @@ function _getIPToVMDict (xapi, sr) {
return dict
}
function _getGlusterEndpoint (sr) {
function _getGlusterEndpoint(sr) {
const xapi = this.getXapi(sr)
const data = getXosanConfig(sr, xapi)
if (!data || !data.nodes) {
@@ -68,7 +69,7 @@ function _getGlusterEndpoint (sr) {
}
}
async function rateLimitedRetry (action, shouldRetry, retryCount = 20) {
async function rateLimitedRetry(action, shouldRetry, retryCount = 20) {
let retryDelay = 500 * (1 + Math.random() / 20)
let result
while (retryCount > 0 && (result = await action()) && shouldRetry(result)) {
@@ -80,8 +81,8 @@ async function rateLimitedRetry (action, shouldRetry, retryCount = 20) {
return result
}
function createVolumeInfoTypes () {
function parseHeal (parsed) {
function createVolumeInfoTypes() {
function parseHeal(parsed) {
const bricks = []
parsed['healInfo']['bricks']['brick'].forEach(brick => {
bricks.push(brick)
@@ -92,7 +93,7 @@ function createVolumeInfoTypes () {
return { commandStatus: true, result: { bricks } }
}
function parseStatus (parsed) {
function parseStatus(parsed) {
const brickDictByUuid = {}
const volume = parsed['volStatus']['volumes']['volume']
volume['node'].forEach(node => {
@@ -105,7 +106,7 @@ function createVolumeInfoTypes () {
}
}
async function parseInfo (parsed) {
async function parseInfo(parsed) {
const volume = parsed['volInfo']['volumes']['volume']
volume['bricks'] = volume['bricks']['brick']
volume['options'] = volume['options']['option']
@@ -113,7 +114,7 @@ function createVolumeInfoTypes () {
}
const sshInfoType = (command, handler) => {
return async function (sr) {
return async function(sr) {
const glusterEndpoint = this::_getGlusterEndpoint(sr)
const cmdShouldRetry = result =>
!result['commandStatus'] &&
@@ -128,8 +129,8 @@ function createVolumeInfoTypes () {
}
}
async function profileType (sr) {
async function parseProfile (parsed) {
async function profileType(sr) {
async function parseProfile(parsed) {
const volume = parsed['volProfile']
volume['bricks'] = ensureArray(volume['brick'])
delete volume['brick']
@@ -139,8 +140,8 @@ function createVolumeInfoTypes () {
return this::sshInfoType('profile xosan info', parseProfile)(sr)
}
async function profileTopType (sr) {
async function parseTop (parsed) {
async function profileTopType(sr) {
async function parseTop(parsed) {
const volume = parsed['volTop']
volume['bricks'] = ensureArray(volume['brick'])
delete volume['brick']
@@ -154,7 +155,7 @@ function createVolumeInfoTypes () {
}))
}
function checkHosts (sr) {
function checkHosts(sr) {
const xapi = this.getXapi(sr)
const data = getXosanConfig(sr, xapi)
const network = xapi.getObject(data.network)
@@ -179,7 +180,7 @@ function createVolumeInfoTypes () {
const VOLUME_INFO_TYPES = createVolumeInfoTypes()
export async function getVolumeInfo ({ sr, infoType }) {
export async function getVolumeInfo({ sr, infoType }) {
await this.checkXosanLicense({ srId: sr.uuid })
const glusterEndpoint = this::_getGlusterEndpoint(sr)
@@ -210,7 +211,7 @@ getVolumeInfo.resolve = {
sr: ['sr', 'SR', 'administrate'],
}
export async function profileStatus ({ sr, changeStatus = null }) {
export async function profileStatus({ sr, changeStatus = null }) {
await this.checkXosanLicense({ srId: sr.uuid })
const glusterEndpoint = this::_getGlusterEndpoint(sr)
@@ -239,7 +240,7 @@ profileStatus.resolve = {
sr: ['sr', 'SR', 'administrate'],
}
function reconfigurePifIP (xapi, pif, newIP) {
function reconfigurePifIP(xapi, pif, newIP) {
xapi.call(
'PIF.reconfigure_ip',
pif.$ref,
@@ -252,7 +253,7 @@ function reconfigurePifIP (xapi, pif, newIP) {
}
// this function should probably become fixSomething(thingToFix, parmas)
export async function fixHostNotInNetwork ({ xosanSr, host }) {
export async function fixHostNotInNetwork({ xosanSr, host }) {
await this.checkXosanLicense({ srId: xosanSr.uuid })
const xapi = this.getXapi(xosanSr)
@@ -296,22 +297,22 @@ fixHostNotInNetwork.resolve = {
sr: ['sr', 'SR', 'administrate'],
}
function floor2048 (value) {
function floor2048(value) {
return 2048 * Math.floor(value / 2048)
}
async function copyVm (xapi, originalVm, sr) {
async function copyVm(xapi, originalVm, sr) {
return { sr, vm: await xapi.copyVm(originalVm, sr) }
}
async function callPlugin (xapi, host, command, params) {
async function callPlugin(xapi, host, command, params) {
log.debug(`calling plugin ${host.address} ${command}`)
return JSON.parse(
await xapi.call('host.call_plugin', host.$ref, 'xosan.py', command, params)
)
}
async function remoteSsh (glusterEndpoint, cmd, ignoreError = false) {
async function remoteSsh(glusterEndpoint, cmd, ignoreError = false) {
let result
const formatSshError = result => {
const messageArray = []
@@ -367,7 +368,7 @@ async function remoteSsh (glusterEndpoint, cmd, ignoreError = false) {
)
}
function findErrorMessage (commandResut) {
function findErrorMessage(commandResut) {
if (commandResut['exit'] === 0 && commandResut.parsed) {
const cliOut = commandResut.parsed['cliOutput']
if (cliOut['opErrstr'] && cliOut['opErrstr'].length) {
@@ -383,7 +384,7 @@ function findErrorMessage (commandResut) {
: commandResut['stdout']
}
async function glusterCmd (glusterEndpoint, cmd, ignoreError = false) {
async function glusterCmd(glusterEndpoint, cmd, ignoreError = false) {
const result = await remoteSsh(
glusterEndpoint,
`gluster --mode=script --xml ${cmd}`,
@@ -413,7 +414,7 @@ async function glusterCmd (glusterEndpoint, cmd, ignoreError = false) {
return result
}
const createNetworkAndInsertHosts = defer(async function (
const createNetworkAndInsertHosts = defer(async function(
$defer,
xapi,
pif,
@@ -453,7 +454,7 @@ const createNetworkAndInsertHosts = defer(async function (
return xosanNetwork
})
async function getOrCreateSshKey (xapi) {
async function getOrCreateSshKey(xapi) {
let sshKey = xapi.xo.getData(xapi.pool, 'xosan_ssh_key')
if (!sshKey) {
@@ -486,7 +487,7 @@ async function getOrCreateSshKey (xapi) {
return sshKey
}
const _probePoolAndWaitForPresence = defer(async function (
const _probePoolAndWaitForPresence = defer(async function(
$defer,
glusterEndpoint,
addresses
@@ -498,7 +499,7 @@ const _probePoolAndWaitForPresence = defer(async function (
)
})
function shouldRetry (peers) {
function shouldRetry(peers) {
for (const peer of peers) {
if (peer.state === '4') {
return true
@@ -516,7 +517,7 @@ const _probePoolAndWaitForPresence = defer(async function (
return rateLimitedRetry(getPoolStatus, shouldRetry)
})
async function configureGluster (
async function configureGluster(
redundancy,
ipAndHosts,
glusterEndpoint,
@@ -603,7 +604,7 @@ async function configureGluster (
await _setQuota(glusterEndpoint)
}
async function _setQuota (glusterEndpoint) {
async function _setQuota(glusterEndpoint) {
await glusterCmd(glusterEndpoint, 'volume quota xosan enable', true)
await glusterCmd(
glusterEndpoint,
@@ -617,11 +618,11 @@ async function _setQuota (glusterEndpoint) {
)
}
async function _removeQuota (glusterEndpoint) {
async function _removeQuota(glusterEndpoint) {
await glusterCmd(glusterEndpoint, 'volume quota xosan disable', true)
}
export const createSR = defer(async function (
export const createSR = defer(async function(
$defer,
{
template,
@@ -854,7 +855,7 @@ createSR.resolve = {
pif: ['pif', 'PIF', 'administrate'],
}
async function umountDisk (localEndpoint, diskMountPoint) {
async function umountDisk(localEndpoint, diskMountPoint) {
await remoteSsh(
localEndpoint,
`killall -v -w /usr/sbin/xfs_growfs; fuser -v ${diskMountPoint}; umount ${diskMountPoint} && sed -i '\\_${diskMountPoint}\\S_d' /etc/fstab && rm -rf ${diskMountPoint}`
@@ -862,7 +863,7 @@ async function umountDisk (localEndpoint, diskMountPoint) {
}
// this is mostly what the LVM SR driver does, but we are avoiding the 2To limit it imposes.
async function createVDIOnLVMWithoutSizeLimit (xapi, lvmSr, diskSize) {
async function createVDIOnLVMWithoutSizeLimit(xapi, lvmSr, diskSize) {
const VG_PREFIX = 'VG_XenStorage-'
const LV_PREFIX = 'LV-'
const { type, uuid: srUuid, $PBDs } = xapi.getObject(lvmSr)
@@ -893,7 +894,7 @@ async function createVDIOnLVMWithoutSizeLimit (xapi, lvmSr, diskSize) {
}
}
async function createNewDisk (xapi, sr, vm, diskSize) {
async function createNewDisk(xapi, sr, vm, diskSize) {
const newDisk = await createVDIOnLVMWithoutSizeLimit(xapi, sr, diskSize)
await xapi.createVbd({ vdi: newDisk, vm })
let vbd = await xapi._waitObjectState(newDisk.$id, disk =>
@@ -903,7 +904,7 @@ async function createNewDisk (xapi, sr, vm, diskSize) {
return '/dev/' + vbd.device
}
async function mountNewDisk (localEndpoint, hostname, newDeviceFiledeviceFile) {
async function mountNewDisk(localEndpoint, hostname, newDeviceFiledeviceFile) {
const brickRootCmd =
'bash -c \'mkdir -p /bricks; for TESTVAR in {1..9}; do TESTDIR="/bricks/xosan$TESTVAR" ;if mkdir $TESTDIR; then echo $TESTDIR; exit 0; fi ; done ; exit 1\''
const newBrickRoot = (await remoteSsh(
@@ -916,7 +917,7 @@ async function mountNewDisk (localEndpoint, hostname, newDeviceFiledeviceFile) {
return brickName
}
async function replaceBrickOnSameVM (
async function replaceBrickOnSameVM(
xosansr,
previousBrick,
newLvmSr,
@@ -993,7 +994,7 @@ async function replaceBrickOnSameVM (
}
}
export async function replaceBrick ({
export async function replaceBrick({
xosansr,
previousBrick,
newLvmSr,
@@ -1085,7 +1086,7 @@ replaceBrick.resolve = {
xosansr: ['sr', 'SR', 'administrate'],
}
async function _prepareGlusterVm (
async function _prepareGlusterVm(
xapi,
lvmSr,
newVM,
@@ -1122,7 +1123,7 @@ async function _prepareGlusterVm (
}
}
}
await xapi.addTag(newVM.$id, `XOSAN-${xapi.pool.name_label}`)
await xapi.addTag(newVM.$id, 'XOSAN')
await xapi.editVm(newVM, {
name_label: `XOSAN - ${lvmSr.name_label} - ${
host.name_label
@@ -1138,9 +1139,12 @@ async function _prepareGlusterVm (
await xapi.startVm(newVM)
log.debug(`waiting for boot of ${ip}`)
// wait until we find the assigned IP in the networks, we are just checking the boot is complete
const vmIsUp = vm =>
Boolean(vm.$guest_metrics && includes(vm.$guest_metrics.networks, ip))
const vm = await xapi._waitObjectState(newVM.$id, vmIsUp)
// fix #3688
const vm = await xapi._waitObjectState(
newVM.$id,
_ => _.guest_metrics !== NULL_REF
)
await xapi._waitObjectState(vm.guest_metrics, _ => includes(_.networks, ip))
log.debug(`booted ${ip}`)
const localEndpoint = { xapi: xapi, hosts: [host], addresses: [ip] }
const srFreeSpace = sr.physical_size - sr.physical_utilisation
@@ -1162,7 +1166,7 @@ async function _prepareGlusterVm (
return { address: ip, host, vm, underlyingSr: lvmSr, brickName }
}
async function _importGlusterVM (xapi, template, lvmsrId) {
async function _importGlusterVM(xapi, template, lvmsrId) {
const templateStream = await this.requestResource(
'xosan',
template.id,
@@ -1180,11 +1184,11 @@ async function _importGlusterVM (xapi, template, lvmsrId) {
return xapi.barrier(newVM.$ref)
}
function _findAFreeIPAddress (nodes, networkPrefix) {
function _findAFreeIPAddress(nodes, networkPrefix) {
return _findIPAddressOutsideList(map(nodes, n => n.vm.ip), networkPrefix)
}
function _findIPAddressOutsideList (
function _findIPAddressOutsideList(
reservedList,
networkPrefix,
vmIpLastNumber = 101
@@ -1203,7 +1207,7 @@ const _median = arr => {
return arr[Math.floor(arr.length / 2)]
}
const insertNewGlusterVm = defer(async function (
const insertNewGlusterVm = defer(async function(
$defer,
xapi,
xosansr,
@@ -1253,7 +1257,7 @@ const insertNewGlusterVm = defer(async function (
return { data, newVM, addressAndHost, glusterEndpoint }
})
export const addBricks = defer(async function (
export const addBricks = defer(async function(
$defer,
{ xosansr, lvmsrs, brickSize }
) {
@@ -1349,7 +1353,7 @@ addBricks.resolve = {
lvmsrs: ['sr', 'SR', 'administrate'],
}
export const removeBricks = defer(async function ($defer, { xosansr, bricks }) {
export const removeBricks = defer(async function($defer, { xosansr, bricks }) {
await this.checkXosanLicense({ srId: xosansr.uuid })
const xapi = this.getXapi(xosansr)
@@ -1395,7 +1399,7 @@ removeBricks.params = {
}
removeBricks.resolve = { xosansr: ['sr', 'SR', 'administrate'] }
export function checkSrCurrentState ({ poolId }) {
export function checkSrCurrentState({ poolId }) {
return CURRENT_POOL_OPERATIONS[poolId]
}
@@ -1455,7 +1459,7 @@ POSSIBLE_CONFIGURATIONS[16] = [
{ layout: 'replica', redundancy: 2, capacity: 8 },
]
function computeBrickSize (srs, brickSize = Infinity) {
function computeBrickSize(srs, brickSize = Infinity) {
const xapi = this.getXapi(srs[0])
const srsObjects = map(srs, srId => xapi.getObject(srId))
const srSizes = map(
@@ -1468,7 +1472,7 @@ function computeBrickSize (srs, brickSize = Infinity) {
)
}
export async function computeXosanPossibleOptions ({
export async function computeXosanPossibleOptions({
lvmSrs,
brickSize = Infinity,
}) {
@@ -1501,7 +1505,7 @@ computeXosanPossibleOptions.params = {
// ---------------------------------------------------------------------
export async function unlock ({ licenseId, sr }) {
export async function unlock({ licenseId, sr }) {
await this.unlockXosanLicense({ licenseId, srId: sr.id })
const glusterEndpoint = this::_getGlusterEndpoint(sr.id)
@@ -1528,7 +1532,7 @@ unlock.resolve = {
// ---------------------------------------------------------------------
export async function downloadAndInstallXosanPack ({ id, version, pool }) {
export async function downloadAndInstallXosanPack({ id, version, pool }) {
if (!this.requestResource) {
throw new Error('requestResource is not a function')
}

View File

@@ -78,19 +78,18 @@ export default class Redis extends Collection {
.then(keys => keys.length !== 0 && redis.del(keys))
).then(() =>
asyncMap(redis.smembers(idsIndex), id =>
redis.hgetall(`${prefix}:${id}`).then(
values =>
values == null
? redis.srem(idsIndex, id) // entry no longer exists
: asyncMap(indexes, index => {
const value = values[index]
if (value !== undefined) {
return redis.sadd(
`${prefix}_${index}:${String(value).toLowerCase()}`,
id
)
}
})
redis.hgetall(`${prefix}:${id}`).then(values =>
values == null
? redis.srem(idsIndex, id) // entry no longer exists
: asyncMap(indexes, index => {
const value = values[index]
if (value !== undefined) {
return redis.sadd(
`${prefix}_${index}:${String(value).toLowerCase()}`,
id
)
}
})
)
)
)

View File

@@ -59,7 +59,7 @@ const log = createLogger('xo:main')
const DEPRECATED_ENTRIES = ['users', 'servers']
async function loadConfiguration () {
async function loadConfiguration() {
const config = await appConf.load('xo-server', {
appDir: joinPath(__dirname, '..'),
ignoreUnknownFormats: true,
@@ -79,7 +79,7 @@ async function loadConfiguration () {
// ===================================================================
function createExpressApp () {
function createExpressApp() {
const app = createExpress()
app.use(helmet())
@@ -111,7 +111,7 @@ function createExpressApp () {
return app
}
async function setUpPassport (express, xo) {
async function setUpPassport(express, xo) {
const strategies = { __proto__: null }
xo.registerPassportStrategy = strategy => {
passport.use(strategy)
@@ -214,7 +214,7 @@ async function setUpPassport (express, xo) {
// ===================================================================
async function registerPlugin (pluginPath, pluginName) {
async function registerPlugin(pluginPath, pluginName) {
const plugin = require(pluginPath)
const { description, version = 'unknown' } = (() => {
try {
@@ -257,7 +257,7 @@ async function registerPlugin (pluginPath, pluginName) {
const logPlugin = createLogger('xo:plugin')
function registerPluginWrapper (pluginPath, pluginName) {
function registerPluginWrapper(pluginPath, pluginName) {
logPlugin.info(`register ${pluginName}`)
return registerPlugin.call(this, pluginPath, pluginName).then(
@@ -274,7 +274,7 @@ function registerPluginWrapper (pluginPath, pluginName) {
const PLUGIN_PREFIX = 'xo-server-'
const PLUGIN_PREFIX_LENGTH = PLUGIN_PREFIX.length
async function registerPluginsInPath (path) {
async function registerPluginsInPath(path) {
const files = await readdir(path).catch(error => {
if (error.code === 'ENOENT') {
return []
@@ -295,7 +295,7 @@ async function registerPluginsInPath (path) {
)
}
async function registerPlugins (xo) {
async function registerPlugins(xo) {
await Promise.all(
mapToArray(
[`${__dirname}/../node_modules/`, '/usr/local/lib/node_modules/'],
@@ -306,7 +306,7 @@ async function registerPlugins (xo) {
// ===================================================================
async function makeWebServerListen (
async function makeWebServerListen(
webServer,
{
certificate,
@@ -348,7 +348,7 @@ async function makeWebServerListen (
}
}
async function createWebServer ({ listen, listenOptions }) {
async function createWebServer({ listen, listenOptions }) {
const webServer = stoppable(new WebServer())
await Promise.all(
@@ -433,8 +433,10 @@ const setUpStaticFiles = (express, opts) => {
// ===================================================================
const setUpApi = (webServer, xo, verboseLogsOnErrors) => {
const setUpApi = (webServer, xo, config) => {
const webSocketServer = new WebSocket.Server({
...config.apiWebSocketOptions,
noServer: true,
})
xo.on('stop', () => pFromCallback(cb => webSocketServer.close(cb)))
@@ -547,7 +549,7 @@ ${name} v${version}`)(require('../package.json'))
// ===================================================================
export default async function main (args) {
export default async function main(args) {
// makes sure the global Promise has not been changed by a lib
assert(global.Promise === require('bluebird'))
@@ -640,7 +642,7 @@ export default async function main (args) {
})
// Must be set up before the static files.
setUpApi(webServer, xo, config.verboseApiLogsOnErrors)
setUpApi(webServer, xo, config)
setUpProxies(express, config.http.proxies, xo)

View File

@@ -0,0 +1,15 @@
export default {
$schema: 'http://json-schema.org/draft-04/schema#',
type: 'object',
properties: {
event: {
enum: ['task.info'],
},
taskId: {
type: 'string',
description: 'identifier of the parent task or job',
},
data: {},
},
required: ['event', 'taskId'],
}

View File

@@ -0,0 +1,43 @@
import mapValues from 'lodash/mapValues'
// this random value is used to obfuscate real data
const OBFUSCATED_VALUE = 'q3oi6d9X8uenGvdLnHk2'
export const merge = (newValue, oldValue) => {
if (newValue === OBFUSCATED_VALUE) {
return oldValue
}
let isArray
if (
newValue === null ||
oldValue === null ||
typeof newValue !== 'object' ||
typeof oldValue !== 'object' ||
(isArray = Array.isArray(newValue)) !== Array.isArray(oldValue)
) {
return newValue
}
const iteratee = (v, k) => merge(v, oldValue[k])
return isArray ? newValue.map(iteratee) : mapValues(newValue, iteratee)
}
export const obfuscate = value => replace(value, OBFUSCATED_VALUE)
export function replace (value, replacement) {
function helper (value, name) {
if (name === 'password' && typeof value === 'string') {
return replacement
}
if (typeof value !== 'object' || value === null) {
return value
}
return Array.isArray(value) ? value.map(helper) : mapValues(value, helper)
}
return helper(value)
}

View File

@@ -1,6 +1,5 @@
// @flow
// $FlowFixMe
import through2 from 'through2'
import { type Readable } from 'stream'

View File

@@ -10,7 +10,12 @@ import {
mapToArray,
parseXml,
} from './utils'
import { isHostRunning, isVmHvm, isVmRunning, parseDateTime } from './xapi'
import {
getVmDomainType,
isHostRunning,
isVmRunning,
parseDateTime,
} from './xapi'
import { useUpdateSystem } from './xapi/utils'
// ===================================================================
@@ -218,14 +223,18 @@ const TRANSFORMS = {
// -----------------------------------------------------------------
vm (obj) {
vm (obj, dependents) {
dependents[obj.guest_metrics] = obj.$id
dependents[obj.metrics] = obj.$id
const {
$guest_metrics: guestMetrics,
$metrics: metrics,
other_config: otherConfig,
} = obj
const isHvm = isVmHvm(obj)
const domainType = getVmDomainType(obj)
const isHvm = domainType === 'hvm'
const isRunning = isVmRunning(obj)
const xenTools = (() => {
if (!isRunning || !metrics) {
@@ -302,7 +311,7 @@ const TRANSFORMS = {
version: version && parseXml(version).docker_version,
}
})(),
expNestedHvm: obj.platform['exp-nested-hvm'] === 'true',
high_availability: obj.ha_restart_priority,
memory: (function () {
@@ -343,11 +352,7 @@ const TRANSFORMS = {
startTime: metrics && toTimestamp(metrics.start_time),
tags: obj.tags,
VIFs: link(obj, 'VIFs'),
virtualizationMode: isHvm
? guestMetrics !== undefined && guestMetrics.PV_drivers_detected
? 'pvhvm'
: 'hvm'
: 'pv',
virtualizationMode: domainType,
// <=> Are the Xen Server tools installed?
//
@@ -739,13 +744,13 @@ const TRANSFORMS = {
// ===================================================================
export default xapiObj => {
export default function xapiObjectToXo (xapiObj, dependents) {
const transform = TRANSFORMS[xapiObj.$type.toLowerCase()]
if (!transform) {
return
}
const xoObj = transform(xapiObj)
const xoObj = transform(xapiObj, dependents)
if (!xoObj) {
return
}

View File

@@ -99,8 +99,8 @@ const testMetric = (test, type) =>
typeof test === 'string'
? test === type
: typeof test === 'function'
? test(type)
: test.exec(type)
? test(type)
: test.exec(type)
const findMetric = (metrics, metricType) => {
let testResult
@@ -321,8 +321,12 @@ export default class XapiStats {
const hostUuid = host.uuid
if (
!(
vmUuid !== undefined &&
get(this._statsByObject, [vmUuid, step]) === undefined
) &&
get(this._statsByObject, [hostUuid, step, 'localTimestamp']) + step >
getCurrentTimestamp()
getCurrentTimestamp()
) {
return this._getStats(hostUuid, step, vmUuid)
}
@@ -414,7 +418,7 @@ export default class XapiStats {
})
}
getVmStats (xapi, vmId, granularity) {
async getVmStats (xapi, vmId, granularity) {
const vm = xapi.getObject(vmId)
const host = vm.$resident_on
if (!host) {

View File

@@ -5,6 +5,7 @@ import createLogger from '@xen-orchestra/log'
import deferrable from 'golike-defer'
import fatfs from 'fatfs'
import mixin from '@xen-orchestra/mixin'
import ms from 'ms'
import synchronized from 'decorator-synchronized'
import tarStream from 'tar-stream'
import vmdkToVhd from 'xo-vmdk-to-vhd'
@@ -92,7 +93,7 @@ export const IPV6_CONFIG_MODES = ['None', 'DHCP', 'Static', 'Autoconf']
@mixin(mapToArray(mixins))
export default class Xapi extends XapiBase {
constructor (...args) {
constructor(...args) {
super(...args)
// Patch getObject to resolve _xapiId property.
@@ -131,7 +132,7 @@ export default class Xapi extends XapiBase {
this.objects.on('update', onAddOrUpdate)
}
call (...args) {
call(...args) {
const fn = super.call
const loop = () =>
@@ -145,13 +146,13 @@ export default class Xapi extends XapiBase {
return loop()
}
createTask (name = 'untitled task', description) {
createTask(name = 'untitled task', description) {
return super.createTask(`[XO] ${name}`, description)
}
// =================================================================
_registerGenericWatcher (fn) {
_registerGenericWatcher(fn) {
const watchers = this._genericWatchers
const id = String(Math.random())
@@ -168,7 +169,7 @@ export default class Xapi extends XapiBase {
// function.
//
// TODO: implements a timeout.
_waitObject (predicate) {
_waitObject(predicate) {
if (isFunction(predicate)) {
const { promise, resolve } = defer()
@@ -200,22 +201,22 @@ export default class Xapi extends XapiBase {
// Wait for an object to be in a given state.
//
// Faster than _waitObject() with a function.
_waitObjectState (idOrUuidOrRef, predicate) {
_waitObjectState(idOrUuidOrRef, predicate) {
const object = this.getObject(idOrUuidOrRef, null)
if (object && predicate(object)) {
return object
}
const loop = () =>
this._waitObject(idOrUuidOrRef).then(
object => (predicate(object) ? object : loop())
this._waitObject(idOrUuidOrRef).then(object =>
predicate(object) ? object : loop()
)
return loop()
}
// Returns the objects if already presents or waits for it.
async _getOrWaitObject (idOrUuidOrRef) {
async _getOrWaitObject(idOrUuidOrRef) {
return (
this.getObject(idOrUuidOrRef, null) || this._waitObject(idOrUuidOrRef)
)
@@ -223,7 +224,7 @@ export default class Xapi extends XapiBase {
// =================================================================
_setObjectProperty (object, name, value) {
_setObjectProperty(object, name, value) {
return this.call(
`${getNamespaceForType(object.$type)}.set_${camelToSnakeCase(name)}`,
object.$ref,
@@ -231,7 +232,7 @@ export default class Xapi extends XapiBase {
)
}
_setObjectProperties (object, props) {
_setObjectProperties(object, props) {
const { $ref: ref, $type: type } = object
const namespace = getNamespaceForType(type)
@@ -251,7 +252,7 @@ export default class Xapi extends XapiBase {
)::ignoreErrors()
}
async _updateObjectMapProperty (object, prop, values) {
async _updateObjectMapProperty(object, prop, values) {
const { $ref: ref, $type: type } = object
prop = camelToSnakeCase(prop)
@@ -276,14 +277,14 @@ export default class Xapi extends XapiBase {
)
}
async setHostProperties (id, { nameLabel, nameDescription }) {
async setHostProperties(id, { nameLabel, nameDescription }) {
await this._setObjectProperties(this.getObject(id), {
nameLabel,
nameDescription,
})
}
async setPoolProperties ({ autoPoweron, nameLabel, nameDescription }) {
async setPoolProperties({ autoPoweron, nameLabel, nameDescription }) {
const { pool } = this
await Promise.all([
@@ -298,14 +299,14 @@ export default class Xapi extends XapiBase {
])
}
async setSrProperties (id, { nameLabel, nameDescription }) {
async setSrProperties(id, { nameLabel, nameDescription }) {
await this._setObjectProperties(this.getObject(id), {
nameLabel,
nameDescription,
})
}
async setNetworkProperties (
async setNetworkProperties(
id,
{ nameLabel, nameDescription, defaultIsLocked }
) {
@@ -322,14 +323,14 @@ export default class Xapi extends XapiBase {
// =================================================================
async addTag (id, tag) {
async addTag(id, tag) {
const { $ref: ref, $type: type } = this.getObject(id)
const namespace = getNamespaceForType(type)
await this.call(`${namespace}.add_tags`, ref, tag)
}
async removeTag (id, tag) {
async removeTag(id, tag) {
const { $ref: ref, $type: type } = this.getObject(id)
const namespace = getNamespaceForType(type)
@@ -338,7 +339,7 @@ export default class Xapi extends XapiBase {
// =================================================================
async setDefaultSr (srId) {
async setDefaultSr(srId) {
this._setObjectProperties(this.pool, {
default_SR: this.getObject(srId).$ref,
})
@@ -346,13 +347,13 @@ export default class Xapi extends XapiBase {
// =================================================================
async setPoolMaster (hostId) {
async setPoolMaster(hostId) {
await this.call('pool.designate_new_master', this.getObject(hostId).$ref)
}
// =================================================================
async joinPool (masterAddress, masterUsername, masterPassword, force = false) {
async joinPool(masterAddress, masterUsername, masterPassword, force = false) {
await this.call(
force ? 'pool.join_force' : 'pool.join',
masterAddress,
@@ -363,7 +364,7 @@ export default class Xapi extends XapiBase {
// =================================================================
async emergencyShutdownHost (hostId) {
async emergencyShutdownHost(hostId) {
const host = this.getObject(hostId)
const vms = host.$resident_VMs
log.debug(`Emergency shutdown: ${host.name_label}`)
@@ -384,7 +385,7 @@ export default class Xapi extends XapiBase {
//
// If `force` is false and the evacuation failed, the host is re-
// enabled and the error is thrown.
async _clearHost ({ $ref: ref }, force) {
async _clearHost({ $ref: ref }, force) {
await this.call('host.disable', ref)
try {
@@ -398,38 +399,38 @@ export default class Xapi extends XapiBase {
}
}
async disableHost (hostId) {
async disableHost(hostId) {
await this.call('host.disable', this.getObject(hostId).$ref)
}
async forgetHost (hostId) {
async forgetHost(hostId) {
await this.call('host.destroy', this.getObject(hostId).$ref)
}
async ejectHostFromPool (hostId) {
async ejectHostFromPool(hostId) {
await this.call('pool.eject', this.getObject(hostId).$ref)
}
async enableHost (hostId) {
async enableHost(hostId) {
await this.call('host.enable', this.getObject(hostId).$ref)
}
async powerOnHost (hostId) {
async powerOnHost(hostId) {
await this.call('host.power_on', this.getObject(hostId).$ref)
}
async rebootHost (hostId, force = false) {
async rebootHost(hostId, force = false) {
const host = this.getObject(hostId)
await this._clearHost(host, force)
await this.call('host.reboot', host.$ref)
}
async restartHostAgent (hostId) {
async restartHostAgent(hostId) {
await this.call('host.restart_agent', this.getObject(hostId).$ref)
}
async setRemoteSyslogHost (hostId, syslogDestination) {
async setRemoteSyslogHost(hostId, syslogDestination) {
const host = this.getObject(hostId)
await this.call('host.set_logging', host.$ref, {
syslog_destination: syslogDestination,
@@ -437,7 +438,7 @@ export default class Xapi extends XapiBase {
await this.call('host.syslog_reconfigure', host.$ref)
}
async shutdownHost (hostId, force = false) {
async shutdownHost(hostId, force = false) {
const host = this.getObject(hostId)
await this._clearHost(host, force)
@@ -448,7 +449,7 @@ export default class Xapi extends XapiBase {
// Clone a VM: make a fast copy by fast copying each of its VDIs
// (using snapshots where possible) on the same SRs.
_cloneVm (vm, nameLabel = vm.name_label) {
_cloneVm(vm, nameLabel = vm.name_label) {
log.debug(
`Cloning VM ${vm.name_label}${
nameLabel !== vm.name_label ? ` as ${nameLabel}` : ''
@@ -462,7 +463,7 @@ export default class Xapi extends XapiBase {
//
// If a SR is specified, it will contains the copies of the VDIs,
// otherwise they will use the SRs they are on.
async _copyVm (vm, nameLabel = vm.name_label, sr = undefined) {
async _copyVm(vm, nameLabel = vm.name_label, sr = undefined) {
let snapshot
if (isVmRunning(vm)) {
snapshot = await this._snapshotVm(vm)
@@ -488,7 +489,7 @@ export default class Xapi extends XapiBase {
}
}
async cloneVm (vmId, { nameLabel = undefined, fast = true } = {}) {
async cloneVm(vmId, { nameLabel = undefined, fast = true } = {}) {
const vm = this.getObject(vmId)
const cloneRef = await (fast
@@ -498,13 +499,13 @@ export default class Xapi extends XapiBase {
return /* await */ this._getOrWaitObject(cloneRef)
}
async copyVm (vmId, srId, { nameLabel = undefined } = {}) {
async copyVm(vmId, srId, { nameLabel = undefined } = {}) {
return /* await */ this._getOrWaitObject(
await this._copyVm(this.getObject(vmId), nameLabel, this.getObject(srId))
)
}
async remoteCopyVm (
async remoteCopyVm(
vmId,
targetXapi,
targetSrId,
@@ -544,7 +545,7 @@ export default class Xapi extends XapiBase {
}
// Low level create VM.
_createVmRecord ({
_createVmRecord({
actions_after_crash,
actions_after_reboot,
actions_after_shutdown,
@@ -644,7 +645,7 @@ export default class Xapi extends XapiBase {
)
}
async _deleteVm (
async _deleteVm(
vm,
deleteDisks = true,
force = false,
@@ -657,23 +658,32 @@ export default class Xapi extends XapiBase {
// ensure the vm record is up-to-date
vm = await this.barrier($ref)
if (!force && 'destroy' in vm.blocked_operations) {
throw forbiddenOperation('destroy', vm.blocked_operations.destroy.reason)
}
if (
!forceDeleteDefaultTemplate &&
vm.other_config.default_template === 'true'
) {
throw forbiddenOperation('destroy', 'VM is default template')
}
// It is necessary for suspended VMs to be shut down
// to be able to delete their VDIs.
if (vm.power_state !== 'Halted') {
await this.call('VM.hard_shutdown', $ref)
}
if (force) {
await this._updateObjectMapProperty(vm, 'blocked_operations', {
await Promise.all([
this.call('VM.set_is_a_template', vm.$ref, false),
this._updateObjectMapProperty(vm, 'blocked_operations', {
destroy: null,
})
}
if (forceDeleteDefaultTemplate) {
await this._updateObjectMapProperty(vm, 'other_config', {
}),
this._updateObjectMapProperty(vm, 'other_config', {
default_template: null,
})
}
}),
])
// must be done before destroying the VM
const disks = getVmDisks(vm)
@@ -720,7 +730,7 @@ export default class Xapi extends XapiBase {
])
}
async deleteVm (vmId, deleteDisks, force, forceDeleteDefaultTemplate) {
async deleteVm(vmId, deleteDisks, force, forceDeleteDefaultTemplate) {
return /* await */ this._deleteVm(
this.getObject(vmId),
deleteDisks,
@@ -729,7 +739,7 @@ export default class Xapi extends XapiBase {
)
}
getVmConsole (vmId) {
getVmConsole(vmId) {
const vm = this.getObject(vmId)
const console = find(vm.$consoles, { protocol: 'rfb' })
@@ -743,7 +753,7 @@ export default class Xapi extends XapiBase {
// Returns a stream to the exported VM.
@concurrency(2, stream => stream.then(stream => fromEvent(stream, 'end')))
@cancelable
async exportVm ($cancelToken, vmId, { compress = true } = {}) {
async exportVm($cancelToken, vmId, { compress = true } = {}) {
const vm = this.getObject(vmId)
const useSnapshot = isVmRunning(vm)
const exportedVm = useSnapshot
@@ -772,7 +782,7 @@ export default class Xapi extends XapiBase {
return promise
}
_assertHealthyVdiChain (vdi, cache) {
_assertHealthyVdiChain(vdi, cache) {
if (vdi == null) {
return
}
@@ -804,7 +814,7 @@ export default class Xapi extends XapiBase {
)
}
_assertHealthyVdiChains (vm) {
_assertHealthyVdiChains(vm) {
const cache = { __proto__: null }
forEach(vm.$VBDs, ({ $VDI }) => {
this._assertHealthyVdiChain($VDI, cache)
@@ -815,7 +825,7 @@ export default class Xapi extends XapiBase {
// object.
@cancelable
@deferrable
async exportDeltaVm (
async exportDeltaVm(
$defer,
$cancelToken,
vmId: string,
@@ -876,7 +886,7 @@ export default class Xapi extends XapiBase {
//
// The snapshot must not exist otherwise it could break the
// next export.
;this._deleteVdi(vdi)::ignoreErrors()
this._deleteVdi(vdi)::ignoreErrors()
return
}
@@ -946,7 +956,7 @@ export default class Xapi extends XapiBase {
}
@deferrable
async importDeltaVm (
async importDeltaVm(
$defer,
delta: DeltaVmExport,
{
@@ -1127,7 +1137,7 @@ export default class Xapi extends XapiBase {
])
if (deleteBase && baseVm) {
;this._deleteVm(baseVm)::ignoreErrors()
this._deleteVm(baseVm)::ignoreErrors()
}
await Promise.all([
@@ -1145,7 +1155,7 @@ export default class Xapi extends XapiBase {
return { transferSize, vm }
}
async _migrateVmWithStorageMotion (
async _migrateVmWithStorageMotion(
vm,
hostXapi,
host,
@@ -1167,8 +1177,8 @@ export default class Xapi extends XapiBase {
mapVdisSrs && mapVdisSrs[vdi.$id]
? hostXapi.getObject(mapVdisSrs[vdi.$id]).$ref
: sr !== undefined
? hostXapi.getObject(sr).$ref
: defaultSr.$ref // Will error if there are no default SR.
? hostXapi.getObject(sr).$ref
: defaultSr.$ref // Will error if there are no default SR.
}
}
@@ -1211,7 +1221,7 @@ export default class Xapi extends XapiBase {
}
@synchronized
_callInstallationPlugin (hostRef, vdi) {
_callInstallationPlugin(hostRef, vdi) {
return this.call(
'host.call_plugin',
hostRef,
@@ -1227,7 +1237,7 @@ export default class Xapi extends XapiBase {
}
@deferrable
async installSupplementalPack ($defer, stream, { hostId }) {
async installSupplementalPack($defer, stream, { hostId }) {
if (!stream.length) {
throw new Error('stream must have a length')
}
@@ -1244,7 +1254,7 @@ export default class Xapi extends XapiBase {
}
@deferrable
async installSupplementalPackOnAllHosts ($defer, stream) {
async installSupplementalPackOnAllHosts($defer, stream) {
if (!stream.length) {
throw new Error('stream must have a length')
}
@@ -1306,7 +1316,7 @@ export default class Xapi extends XapiBase {
}
@cancelable
async _importVm ($cancelToken, stream, sr, onVmCreation = undefined) {
async _importVm($cancelToken, stream, sr, onVmCreation = undefined) {
const taskRef = await this.createTask('VM import')
const query = {}
@@ -1315,7 +1325,7 @@ export default class Xapi extends XapiBase {
}
if (onVmCreation != null) {
;this._waitObject(
this._waitObject(
obj =>
obj != null &&
obj.current_operations != null &&
@@ -1340,7 +1350,7 @@ export default class Xapi extends XapiBase {
}
@deferrable
async _importOvaVm (
async _importOvaVm(
$defer,
stream,
{ descriptionLabel, disks, memory, nameLabel, networks, nCpus, tables },
@@ -1436,7 +1446,7 @@ export default class Xapi extends XapiBase {
}
// TODO: an XVA can contain multiple VMs
async importVm (stream, { data, srId, type = 'xva' } = {}) {
async importVm(stream, { data, srId, type = 'xva' } = {}) {
const sr = srId && this.getObject(srId)
if (type === 'xva') {
@@ -1450,7 +1460,7 @@ export default class Xapi extends XapiBase {
throw new Error(`unsupported type: '${type}'`)
}
async migrateVm (
async migrateVm(
vmId,
hostXapi,
hostId,
@@ -1493,7 +1503,7 @@ export default class Xapi extends XapiBase {
@concurrency(2)
@cancelable
async _snapshotVm ($cancelToken, vm, nameLabel = vm.name_label) {
async _snapshotVm($cancelToken, vm, nameLabel = vm.name_label) {
log.debug(
`Snapshotting VM ${vm.name_label}${
nameLabel !== vm.name_label ? ` as ${nameLabel}` : ''
@@ -1545,17 +1555,17 @@ export default class Xapi extends XapiBase {
return snapshot
}
async snapshotVm (vmId, nameLabel = undefined) {
async snapshotVm(vmId, nameLabel = undefined) {
return /* await */ this._snapshotVm(this.getObject(vmId), nameLabel)
}
async setVcpuWeight (vmId, weight) {
async setVcpuWeight(vmId, weight) {
weight = weight || null // Take all falsy values as a removal (0 included)
const vm = this.getObject(vmId)
await this._updateObjectMapProperty(vm, 'VCPUs_params', { weight })
}
async _startVm (vm, host, force) {
async _startVm(vm, host, force) {
log.debug(`Starting VM ${vm.name_label}`)
if (force) {
@@ -1574,7 +1584,7 @@ export default class Xapi extends XapiBase {
: this.call('VM.start_on', vm.$ref, host.$ref, false, false)
}
async startVm (vmId, hostId, force) {
async startVm(vmId, hostId, force) {
try {
await this._startVm(
this.getObject(vmId),
@@ -1586,13 +1596,15 @@ export default class Xapi extends XapiBase {
throw forbiddenOperation('Start', e.params[1])
}
if (e.code === 'VM_BAD_POWER_STATE') {
return this.resumeVm(vmId)
return e.params[2] === 'paused'
? this.unpauseVm(vmId)
: this.resumeVm(vmId)
}
throw e
}
}
async startVmOnCd (vmId) {
async startVmOnCd(vmId) {
const vm = this.getObject(vmId)
if (isVmHvm(vm)) {
@@ -1653,19 +1665,19 @@ export default class Xapi extends XapiBase {
await this._startVm(vm)
} finally {
;this._setObjectProperties(vm, {
this._setObjectProperties(vm, {
PV_bootloader: bootloader,
})::ignoreErrors()
forEach(bootables, ([vbd, bootable]) => {
;this._setObjectProperties(vbd, { bootable })::ignoreErrors()
this._setObjectProperties(vbd, { bootable })::ignoreErrors()
})
}
}
}
// vm_operations: http://xapi-project.github.io/xen-api/classes/vm.html
async addForbiddenOperationToVm (vmId, operation, reason) {
async addForbiddenOperationToVm(vmId, operation, reason) {
await this.call(
'VM.add_to_blocked_operations',
this.getObject(vmId).$ref,
@@ -1674,7 +1686,7 @@ export default class Xapi extends XapiBase {
)
}
async removeForbiddenOperationFromVm (vmId, operation) {
async removeForbiddenOperationFromVm(vmId, operation) {
await this.call(
'VM.remove_from_blocked_operations',
this.getObject(vmId).$ref,
@@ -1684,7 +1696,7 @@ export default class Xapi extends XapiBase {
// =================================================================
async createVbd ({
async createVbd({
bootable = false,
other_config = {},
qos_algorithm_params = {},
@@ -1746,13 +1758,13 @@ export default class Xapi extends XapiBase {
}
}
_cloneVdi (vdi) {
_cloneVdi(vdi) {
log.debug(`Cloning VDI ${vdi.name_label}`)
return this.call('VDI.clone', vdi.$ref)
}
async createVdi ({
async createVdi({
name_description,
name_label,
other_config = {},
@@ -1788,7 +1800,7 @@ export default class Xapi extends XapiBase {
)
}
async moveVdi (vdiId, srId) {
async moveVdi(vdiId, srId) {
const vdi = this.getObject(vdiId)
const sr = this.getObject(srId)
@@ -1828,13 +1840,13 @@ export default class Xapi extends XapiBase {
}
// TODO: check whether the VDI is attached.
async _deleteVdi (vdi) {
async _deleteVdi(vdi) {
log.debug(`Deleting VDI ${vdi.name_label}`)
await this.call('VDI.destroy', vdi.$ref)
}
_resizeVdi (vdi, size) {
_resizeVdi(vdi, size) {
log.debug(
`Resizing VDI ${vdi.name_label} from ${vdi.virtual_size} to ${size}`
)
@@ -1842,7 +1854,7 @@ export default class Xapi extends XapiBase {
return this.call('VDI.resize', vdi.$ref, size)
}
_getVmCdDrive (vm) {
_getVmCdDrive(vm) {
for (const vbd of vm.$VBDs) {
if (vbd.type === 'CD') {
return vbd
@@ -1850,14 +1862,14 @@ export default class Xapi extends XapiBase {
}
}
async _ejectCdFromVm (vm) {
async _ejectCdFromVm(vm) {
const cdDrive = this._getVmCdDrive(vm)
if (cdDrive) {
await this.call('VBD.eject', cdDrive.$ref)
}
}
async _insertCdIntoVm (cd, vm, { bootable = false, force = false } = {}) {
async _insertCdIntoVm(cd, vm, { bootable = false, force = false } = {}) {
const cdDrive = await this._getVmCdDrive(vm)
if (cdDrive) {
try {
@@ -1886,11 +1898,11 @@ export default class Xapi extends XapiBase {
}
}
async connectVbd (vbdId) {
async connectVbd(vbdId) {
await this.call('VBD.plug', vbdId)
}
async _disconnectVbd (vbd) {
async _disconnectVbd(vbd) {
// TODO: check if VBD is attached before
try {
await this.call('VBD.unplug_force', vbd.$ref)
@@ -1902,21 +1914,21 @@ export default class Xapi extends XapiBase {
}
}
async disconnectVbd (vbdId) {
async disconnectVbd(vbdId) {
await this._disconnectVbd(this.getObject(vbdId))
}
async _deleteVbd (vbd) {
async _deleteVbd(vbd) {
await this._disconnectVbd(vbd)::ignoreErrors()
await this.call('VBD.destroy', vbd.$ref)
}
deleteVbd (vbdId) {
deleteVbd(vbdId) {
return this._deleteVbd(this.getObject(vbdId))
}
// TODO: remove when no longer used.
async destroyVbdsFromVm (vmId) {
async destroyVbdsFromVm(vmId) {
await Promise.all(
mapToArray(this.getObject(vmId).$VBDs, async vbd => {
await this.disconnectVbd(vbd.$ref)::ignoreErrors()
@@ -1925,25 +1937,25 @@ export default class Xapi extends XapiBase {
)
}
async deleteVdi (vdiId) {
async deleteVdi(vdiId) {
await this._deleteVdi(this.getObject(vdiId))
}
async resizeVdi (vdiId, size) {
async resizeVdi(vdiId, size) {
await this._resizeVdi(this.getObject(vdiId), size)
}
async ejectCdFromVm (vmId) {
async ejectCdFromVm(vmId) {
await this._ejectCdFromVm(this.getObject(vmId))
}
async insertCdIntoVm (cdId, vmId, opts = undefined) {
async insertCdIntoVm(cdId, vmId, opts = undefined) {
await this._insertCdIntoVm(this.getObject(cdId), this.getObject(vmId), opts)
}
// -----------------------------------------------------------------
async snapshotVdi (vdiId, nameLabel) {
async snapshotVdi(vdiId, nameLabel) {
const vdi = this.getObject(vdiId)
const snap = await this._getOrWaitObject(
@@ -1959,7 +1971,7 @@ export default class Xapi extends XapiBase {
@concurrency(12, stream => stream.then(stream => fromEvent(stream, 'end')))
@cancelable
_exportVdi ($cancelToken, vdi, base, format = VDI_FORMAT_VHD) {
_exportVdi($cancelToken, vdi, base, format = VDI_FORMAT_VHD) {
const query = {
format,
vdi: vdi.$ref,
@@ -1988,13 +2000,18 @@ export default class Xapi extends XapiBase {
}
@cancelable
exportVdiContent ($cancelToken, vdi, { format } = {}) {
exportVdiContent($cancelToken, vdi, { format } = {}) {
return this._exportVdi($cancelToken, this.getObject(vdi), undefined, format)
}
// -----------------------------------------------------------------
async _importVdiContent (vdi, body, format = VDI_FORMAT_VHD) {
async _importVdiContent(vdi, body, format = VDI_FORMAT_VHD) {
if (__DEV__ && body.length == null) {
throw new Error(
'Trying to import a VDI without a length field. Please report this error to Xen Orchestra.'
)
}
await Promise.all([
body.task,
body.checksumVerified,
@@ -2015,13 +2032,13 @@ export default class Xapi extends XapiBase {
})
}
importVdiContent (vdiId, body, { format } = {}) {
importVdiContent(vdiId, body, { format } = {}) {
return this._importVdiContent(this.getObject(vdiId), body, format)
}
// =================================================================
async _createVif (
async _createVif(
vm,
network,
{
@@ -2071,7 +2088,7 @@ export default class Xapi extends XapiBase {
return vifRef
}
async createVif (vmId, networkId, opts = undefined) {
async createVif(vmId, networkId, opts = undefined) {
return /* await */ this._getOrWaitObject(
await this._createVif(
this.getObject(vmId),
@@ -2081,7 +2098,7 @@ export default class Xapi extends XapiBase {
)
}
@deferrable
async createNetwork (
async createNetwork(
$defer,
{ name, description = 'Created with Xen Orchestra', pifId, mtu, vlan }
) {
@@ -2106,7 +2123,7 @@ export default class Xapi extends XapiBase {
return this._getOrWaitObject(networkRef)
}
async editPif (pifId, { vlan }) {
async editPif(pifId, { vlan }) {
const pif = this.getObject(pifId)
const physPif = find(
this.objects.all,
@@ -2153,7 +2170,7 @@ export default class Xapi extends XapiBase {
}
@deferrable
async createBondedNetwork ($defer, { bondMode, mac = '', pifIds, ...params }) {
async createBondedNetwork($defer, { bondMode, mac = '', pifIds, ...params }) {
const network = await this.createNetwork(params)
$defer.onFailure(() => this.deleteNetwork(network))
// TODO: test and confirm:
@@ -2170,7 +2187,7 @@ export default class Xapi extends XapiBase {
return network
}
async deleteNetwork (networkId) {
async deleteNetwork(networkId) {
const network = this.getObject(networkId)
const pifs = network.$PIFs
@@ -2192,7 +2209,7 @@ export default class Xapi extends XapiBase {
// =================================================================
async _doDockerAction (vmId, action, containerId) {
async _doDockerAction(vmId, action, containerId) {
const vm = this.getObject(vmId)
const host = vm.$resident_on || this.pool.$master
@@ -2208,35 +2225,35 @@ export default class Xapi extends XapiBase {
)
}
async registerDockerContainer (vmId) {
async registerDockerContainer(vmId) {
await this._doDockerAction(vmId, 'register')
}
async deregisterDockerContainer (vmId) {
async deregisterDockerContainer(vmId) {
await this._doDockerAction(vmId, 'deregister')
}
async startDockerContainer (vmId, containerId) {
async startDockerContainer(vmId, containerId) {
await this._doDockerAction(vmId, 'start', containerId)
}
async stopDockerContainer (vmId, containerId) {
async stopDockerContainer(vmId, containerId) {
await this._doDockerAction(vmId, 'stop', containerId)
}
async restartDockerContainer (vmId, containerId) {
async restartDockerContainer(vmId, containerId) {
await this._doDockerAction(vmId, 'restart', containerId)
}
async pauseDockerContainer (vmId, containerId) {
async pauseDockerContainer(vmId, containerId) {
await this._doDockerAction(vmId, 'pause', containerId)
}
async unpauseDockerContainer (vmId, containerId) {
async unpauseDockerContainer(vmId, containerId) {
await this._doDockerAction(vmId, 'unpause', containerId)
}
async getCloudInitConfig (templateId) {
async getCloudInitConfig(templateId) {
const template = this.getObject(templateId)
const host = this.pool.$master
@@ -2253,7 +2270,7 @@ export default class Xapi extends XapiBase {
}
// Specific CoreOS Config Drive
async createCoreOsCloudInitConfigDrive (vmId, srId, config) {
async createCoreOsCloudInitConfigDrive(vmId, srId, config) {
const vm = this.getObject(vmId)
const host = this.pool.$master
const sr = this.getObject(srId)
@@ -2274,7 +2291,7 @@ export default class Xapi extends XapiBase {
// Generic Config Drive
@deferrable
async createCloudInitConfigDrive ($defer, vmId, srId, config) {
async createCloudInitConfigDrive($defer, vmId, srId, config) {
const vm = this.getObject(vmId)
const sr = this.getObject(srId)
@@ -2310,7 +2327,7 @@ export default class Xapi extends XapiBase {
}
@deferrable
async createTemporaryVdiOnSr (
async createTemporaryVdiOnSr(
$defer,
stream,
sr,
@@ -2331,7 +2348,7 @@ export default class Xapi extends XapiBase {
}
// Create VDI on an adequate local SR
async createTemporaryVdiOnHost (stream, hostId, name_label, name_description) {
async createTemporaryVdiOnHost(stream, hostId, name_label, name_description) {
const pbd = find(this.getObject(hostId).$PBDs, pbd =>
canSrHaveNewVdiOfSize(pbd.$SR, stream.length)
)
@@ -2348,7 +2365,7 @@ export default class Xapi extends XapiBase {
)
}
findAvailableSharedSr (minSize) {
findAvailableSharedSr(minSize) {
return find(
this.objects.all,
obj =>
@@ -2356,16 +2373,15 @@ export default class Xapi extends XapiBase {
)
}
async _assertConsistentHostServerTime (hostRef) {
if (
Math.abs(
parseDateTime(
await this.call('host.get_servertime', hostRef)
).getTime() - Date.now()
) > 2e3
) {
async _assertConsistentHostServerTime(hostRef) {
const delta =
parseDateTime(await this.call('host.get_servertime', hostRef)).getTime() -
Date.now()
if (Math.abs(delta) > 30e3) {
throw new Error(
'host server time and XOA date are not consistent with each other'
`host server time and XOA date are not consistent with each other (${ms(
delta
)})`
)
}
}

View File

@@ -462,7 +462,9 @@ export default {
async _installAllPoolPatchesOnHost (host) {
const installableByUuid =
host.license_params.sku_type !== 'free'
? await this._listMissingPoolPatchesOnHost(host)
? pickBy(await this._listMissingPoolPatchesOnHost(host), {
upgrade: false,
})
: pickBy(await this._listMissingPoolPatchesOnHost(host), {
paid: false,
upgrade: false,
@@ -509,11 +511,10 @@ export default {
...(await Promise.all(
mapFilter(this.objects.all, host => {
if (host.$type === 'host') {
return this._listMissingPoolPatchesOnHost(host).then(
patches =>
host.license_params.sku_type !== 'free'
? patches
: pickBy(patches, { paid: false, upgrade: false })
return this._listMissingPoolPatchesOnHost(host).then(patches =>
host.license_params.sku_type !== 'free'
? pickBy(patches, { upgrade: false })
: pickBy(patches, { paid: false, upgrade: false })
)
}
})

View File

@@ -14,7 +14,7 @@ const XEN_VIDEORAM_VALUES = [1, 2, 4, 8, 16]
export default {
// TODO: clean up on error.
@deferrable
async createVm (
async createVm(
$defer,
templateId,
{
@@ -234,7 +234,7 @@ export default {
_editVm: makeEditObject({
affinityHost: {
get: 'affinity',
set (value, vm) {
set(value, vm) {
return this._setObjectProperty(
vm,
'affinity',
@@ -244,7 +244,7 @@ export default {
},
autoPoweron: {
set (value, vm) {
set(value, vm) {
return Promise.all([
this._updateObjectMapProperty(vm, 'other_config', {
autoPoweron: value ? 'true' : null,
@@ -257,8 +257,24 @@ export default {
},
},
virtualizationMode: {
set(virtualizationMode, vm) {
if (virtualizationMode !== 'pv' && virtualizationMode !== 'hvm') {
throw new Error(`The virtualization mode must be 'pv' or 'hvm'`)
}
return this._set('domain_type', virtualizationMode)::pCatch(
{ code: 'MESSAGE_METHOD_UNKNOWN' },
() =>
this._set(
'HVM_boot_policy',
virtualizationMode === 'hvm' ? 'Boot order' : ''
)
)
},
},
coresPerSocket: {
set (coresPerSocket, vm) {
set(coresPerSocket, vm) {
return this._updateObjectMapProperty(vm, 'platform', {
'cores-per-socket': coresPerSocket,
})
@@ -280,7 +296,7 @@ export default {
get: vm => +vm.VCPUs_at_startup,
set: [
'VCPUs_at_startup',
function (value, vm) {
function(value, vm) {
return isVmRunning(vm) && this._set('VCPUs_number_live', value)
},
],
@@ -288,7 +304,7 @@ export default {
cpuCap: {
get: vm => vm.VCPUs_params.cap && +vm.VCPUs_params.cap,
set (cap, vm) {
set(cap, vm) {
return this._updateObjectMapProperty(vm, 'VCPUs_params', { cap })
},
},
@@ -304,13 +320,13 @@ export default {
cpuWeight: {
get: vm => vm.VCPUs_params.weight && +vm.VCPUs_params.weight,
set (weight, vm) {
set(weight, vm) {
return this._updateObjectMapProperty(vm, 'VCPUs_params', { weight })
},
},
highAvailability: {
set (ha, vm) {
set(ha, vm) {
return this.call('VM.set_ha_restart_priority', vm.$ref, ha)
},
},
@@ -330,7 +346,7 @@ export default {
limitName: 'memory',
get: vm => +vm.memory_dynamic_max,
preprocess: parseSize,
set (dynamicMax, vm) {
set(dynamicMax, vm) {
const { $ref } = vm
const dynamicMin = Math.min(vm.memory_dynamic_min, dynamicMax)
@@ -383,8 +399,16 @@ export default {
hasVendorDevice: true,
expNestedHvm: {
set(expNestedHvm, vm) {
return this._updateObjectMapProperty(vm, 'platform', {
'exp-nested-hvm': expNestedHvm ? 'true' : null,
})
},
},
nicType: {
set (nicType, vm) {
set(nicType, vm) {
return this._updateObjectMapProperty(vm, 'platform', {
nic_type: nicType,
})
@@ -392,7 +416,7 @@ export default {
},
vga: {
set (vga, vm) {
set(vga, vm) {
if (!includes(XEN_VGA_VALUES, vga)) {
throw new Error(
`The different values that the VGA can take are: ${XEN_VGA_VALUES}`
@@ -403,7 +427,7 @@ export default {
},
videoram: {
set (videoram, vm) {
set(videoram, vm) {
if (!includes(XEN_VIDEORAM_VALUES, videoram)) {
throw new Error(
`The different values that the video RAM can take are: ${XEN_VIDEORAM_VALUES}`
@@ -414,32 +438,36 @@ export default {
},
}),
async editVm (id, props, checkLimits) {
async editVm(id, props, checkLimits) {
return /* await */ this._editVm(this.getObject(id), props, checkLimits)
},
async revertVm (snapshotId, snapshotBefore = true) {
async revertVm(snapshotId, snapshotBefore = true) {
const snapshot = this.getObject(snapshotId)
if (snapshotBefore) {
await this._snapshotVm(snapshot.$snapshot_of)
}
await this.call('VM.revert', snapshot.$ref)
if (snapshot.snapshot_info['power-state-at-snapshot'] === 'Running') {
const vm = snapshot.$snapshot_of
const vm = await this.barrier(snapshot.snapshot_of)
if (vm.power_state === 'Halted') {
;this.startVm(vm.$id)::ignoreErrors()
this.startVm(vm.$id)::ignoreErrors()
} else if (vm.power_state === 'Suspended') {
;this.resumeVm(vm.$id)::ignoreErrors()
this.resumeVm(vm.$id)::ignoreErrors()
}
}
},
async resumeVm (vmId) {
async resumeVm(vmId) {
// the force parameter is always true
return this.call('VM.resume', this.getObject(vmId).$ref, false, true)
},
shutdownVm (vmId, { hard = false } = {}) {
async unpauseVm(vmId) {
return this.call('VM.unpause', this.getObject(vmId).$ref)
},
shutdownVm(vmId, { hard = false } = {}) {
return this.call(
`VM.${hard ? 'hard' : 'clean'}_shutdown`,
this.getObject(vmId).$ref

View File

@@ -143,7 +143,18 @@ export const isHostRunning = host => {
// -------------------------------------------------------------------
export const isVmHvm = vm => Boolean(vm.HVM_boot_policy)
export const getVmDomainType = vm => {
const dt = vm.domain_type
if (
dt !== undefined && // XS < 7.5
dt !== 'unspecified' // detection failed
) {
return dt
}
return vm.HVM_boot_policy === '' ? 'pv' : 'hvm'
}
export const isVmHvm = vm => getVmDomainType(vm) === 'hvm'
const VM_RUNNING_POWER_STATES = {
Running: true,

View File

@@ -1,4 +1,4 @@
import checkAuthorization from 'xo-acl-resolver'
import aclResolver from 'xo-acl-resolver'
import { forEach, includes, map } from 'lodash'
import { ModelAlreadyExists } from '../collection'
@@ -102,6 +102,21 @@ export default class {
return permissions
}
async checkPermissions (userId, permissions) {
const user = await this._xo.getUser(userId)
// Special case for super XO administrators.
if (user.permission === 'admin') {
return true
}
aclResolver.assert(
await this.getPermissionsForUser(userId),
id => this._xo.getObject(id),
permissions
)
}
async hasPermissions (userId, permissions) {
const user = await this._xo.getUser(userId)
@@ -110,7 +125,7 @@ export default class {
return true
}
return checkAuthorization(
return aclResolver.check(
await this.getPermissionsForUser(userId),
id => this._xo.getObject(id),
permissions

View File

@@ -2,10 +2,11 @@ import createLogger from '@xen-orchestra/log'
import kindOf from 'kindof'
import ms from 'ms'
import schemaInspector from 'schema-inspector'
import { forEach, isArray, isFunction, map, mapValues } from 'lodash'
import { forEach, isFunction } from 'lodash'
import { MethodNotFound } from 'json-rpc-peer'
import * as methods from '../api'
import { MethodNotFound } from 'json-rpc-peer'
import * as sensitiveValues from '../sensitive-values'
import { noop, serializeError } from '../utils'
import * as errors from 'xo-common/api-errors'
@@ -82,7 +83,7 @@ function checkPermission (method) {
const { user } = this
if (!user) {
throw errors.unauthorized()
throw errors.unauthorized(permission)
}
// The only requirement is login.
@@ -91,11 +92,11 @@ function checkPermission (method) {
}
if (!hasPermission(user, permission)) {
throw errors.unauthorized()
throw errors.unauthorized(permission)
}
}
function resolveParams (method, params) {
async function resolveParams (method, params) {
const resolve = method.resolve
if (!resolve) {
return params
@@ -134,33 +135,13 @@ function resolveParams (method, params) {
}
})
return this.hasPermissions(userId, permissions).then(success => {
if (success) {
return params
}
await this.checkPermissions(userId, permissions)
throw errors.unauthorized()
})
return params
}
// -------------------------------------------------------------------
const removeSensitiveParams = (value, name) => {
if (name === 'password' && typeof value === 'string') {
return '* obfuscated *'
}
if (typeof value !== 'object' || value === null) {
return value
}
return isArray(value)
? map(value, removeSensitiveParams)
: mapValues(value, removeSensitiveParams)
}
// ===================================================================
export default class Api {
constructor (xo) {
this._logger = null
@@ -295,7 +276,7 @@ export default class Api {
const data = {
userId,
method: name,
params: removeSensitiveParams(params),
params: sensitiveValues.replace(params, '* obfuscated *'),
duration: Date.now() - startTime,
error: serializeError(error),
}

View File

@@ -181,7 +181,7 @@ export default class {
token = token.properties
if (!(token.expiration > Date.now())) {
;this._tokens.remove(id)::ignoreErrors()
this._tokens.remove(id)::ignoreErrors()
throw noSuchAuthenticationToken(id)
}

View File

@@ -129,6 +129,13 @@ export default {
data: data.data,
message,
})
} else if (event === 'task.info') {
const parent = started[data.taskId]
parent !== undefined &&
(parent.infos || (parent.infos = [])).push({
data: data.data,
message,
})
} else if (event === 'jobCall.start') {
const parent = started[data.runJobId]
if (parent !== undefined) {

View File

@@ -134,8 +134,8 @@ const getOldEntries = <T>(retention: number, entries?: T[]): T[] =>
entries === undefined
? []
: retention > 0
? entries.slice(0, -retention)
: entries
? entries.slice(0, -retention)
: entries
const defaultSettings: Settings = {
concurrency: 0,
@@ -147,12 +147,12 @@ const defaultSettings: Settings = {
timeout: 0,
vmTimeout: 0,
}
const getSetting = <T>(
const getSetting = <T, K: $Keys<Settings>>(
settings: $Dict<Settings>,
name: $Keys<Settings>,
name: K,
keys: string[],
defaultValue?: T
): T | any => {
): T | $ElementType<Settings, K> => {
for (let i = 0, n = keys.length; i < n; ++i) {
const objectSettings = settings[keys[i]]
if (objectSettings !== undefined) {
@@ -470,6 +470,28 @@ const extractIdsFromSimplePattern = (pattern: mixed) => {
// - copy in delta mode: `Continuous Replication`
// - copy in full mode: `Disaster Recovery`
// - imported from backup: `restored from backup`
//
// Task logs emitted in a backup execution:
//
// job.start(data: { mode: Mode, reportWhen: ReportWhen })
// ├─ task.info(message: 'vms', data: { vms: string[] })
// ├─ task.warning(message: 'missingVms', data: { vms: string[] })
// ├─ task.warning(message: string)
// ├─ task.start(data: { type: 'VM', id: string })
// │ ├─ task.warning(message: string)
// │ ├─ task.start(message: 'snapshot')
// │ │ └─ task.end
// │ ├─ task.start(message: 'export', data: { type: 'SR' | 'remote', id: string })
// │ │ ├─ task.warning(message: string)
// │ │ ├─ task.start(message: 'transfer')
// │ │ │ ├─ task.warning(message: string)
// │ │ │ └─ task.end(result: { size: number })
// │ │ ├─ task.start(message: 'merge')
// │ │ │ ├─ task.warning(message: string)
// │ │ │ └─ task.end(result: { size: number })
// │ │ └─ task.end
// │ └─ task.end
// └─ job.end
export default class BackupNg {
_app: {
createJob: ($Diff<BackupJob, {| id: string |}>) => Promise<BackupJob>,
@@ -521,31 +543,25 @@ export default class BackupNg {
vmsId !== undefined ||
(vmsId = extractIdsFromSimplePattern(vmsPattern)) !== undefined
) {
vms = vmsId
.map(id => {
try {
return app.getObject(id, 'VM')
} catch (error) {
const taskId: string = logger.notice(
`Starting backup of ${id}. (${job.id})`,
{
event: 'task.start',
parentId: runJobId,
data: {
type: 'VM',
id,
},
}
)
logger.error(`Backuping ${id} has failed. (${job.id})`, {
event: 'task.end',
taskId,
status: 'failure',
result: serializeError(error),
})
}
vms = {}
const missingVms = []
vmsId.forEach(id => {
try {
vms[id] = app.getObject(id, 'VM')
} catch (error) {
missingVms.push(id)
}
})
if (missingVms.length !== 0) {
logger.warning('missingVms', {
event: 'task.warning',
taskId: runJobId,
data: {
vms: missingVms,
},
})
.filter(vm => vm !== undefined)
}
} else {
vms = app.getObjects({
filter: createPredicate({
@@ -646,6 +662,13 @@ export default class BackupNg {
])
if (concurrency !== 0) {
handleVm = limitConcurrency(concurrency)(handleVm)
logger.notice('vms', {
event: 'task.info',
taskId: runJobId,
data: {
vms: Object.keys(vms),
},
})
}
await asyncMap(vms, handleVm)
}
@@ -711,6 +734,12 @@ export default class BackupNg {
}
}
// Task logs emitted in a restore execution:
//
// task.start(message: 'restore', data: { jobId: string, srId: string, time: number })
// ├─ task.start(message: 'transfer')
// │ └─ task.end(result: { id: string, size: number })
// └─ task.end
async importVmBackupNg (id: string, srId: string): Promise<string> {
const app = this._app
const { metadataFilename, remoteId } = parseVmBackupId(id)

View File

@@ -144,8 +144,8 @@ const listPartitions = (() => {
key === 'start' || key === 'size'
? +value
: key === 'type'
? TYPES[+value] || value
: value,
? TYPES[+value] || value
: value,
})
return device =>
@@ -445,17 +445,17 @@ export default class {
// Once done, (asynchronously) remove the (now obsolete) local
// base.
if (localBaseUuid) {
;promise.then(() => srcXapi.deleteVm(localBaseUuid))::ignoreErrors()
promise.then(() => srcXapi.deleteVm(localBaseUuid))::ignoreErrors()
}
if (toRemove !== undefined) {
;promise
promise
.then(() => asyncMap(toRemove, _ => targetXapi.deleteVm(_.$id)))
::ignoreErrors()
}
// (Asynchronously) Identify snapshot as future base.
;promise
promise
.then(() => {
return srcXapi._updateObjectMapProperty(srcVm, 'other_config', {
[TAG_LAST_BASE_DELTA]: delta.vm.uuid,
@@ -593,7 +593,7 @@ export default class {
base => base.snapshot_time
)
forEach(bases, base => {
;xapi.deleteVdi(base.$id)::ignoreErrors()
xapi.deleteVdi(base.$id)::ignoreErrors()
})
// Export full or delta backup.
@@ -652,7 +652,7 @@ export default class {
)
const baseVm = bases.pop()
forEach(bases, base => {
;xapi.deleteVm(base.$id)::ignoreErrors()
xapi.deleteVm(base.$id)::ignoreErrors()
})
// Check backup dirs.
@@ -780,7 +780,7 @@ export default class {
await this._removeOldDeltaVmBackups(xapi, { vm, handler, dir, retention })
if (baseVm) {
;xapi.deleteVm(baseVm.$id)::ignoreErrors()
xapi.deleteVm(baseVm.$id)::ignoreErrors()
}
return {

View File

@@ -56,8 +56,8 @@ const parsePartxLine = createPairsParser({
key === 'start' || key === 'size'
? +value
: key === 'type'
? PARTITION_TYPE_NAMES[+value] || value
: value,
? PARTITION_TYPE_NAMES[+value] || value
: value,
})
const listLvmLogicalVolumes = defer(

View File

@@ -328,7 +328,7 @@ export default class Jobs {
app.emit('job:terminated', undefined, job, schedule, runJobId)
throw error
} finally {
;this.updateJob({ id, runId: null })::ignoreErrors()
this.updateJob({ id, runId: null })::ignoreErrors()
delete runningJobs[id]
delete runs[runJobId]
if (session !== undefined) {

View File

@@ -44,7 +44,7 @@ export default class LevelDbLogger extends AbstractLogger {
return promise.then(() => key)
}
;promise::ignoreErrors()
promise::ignoreErrors()
return key
}

View File

@@ -1,8 +1,9 @@
import Ajv from 'ajv'
import createLogger from '@xen-orchestra/log'
import { PluginsMetadata } from '../models/plugin-metadata'
import { invalidParameters, noSuchObject } from 'xo-common/api-errors'
import * as sensitiveValues from '../sensitive-values'
import { PluginsMetadata } from '../models/plugin-metadata'
import { isFunction, mapToArray } from '../utils'
// ===================================================================
@@ -119,7 +120,7 @@ export default class {
loaded,
unloadable,
version,
configuration,
configuration: sensitiveValues.obfuscate(configuration),
configurationPresets,
configurationSchema,
testable,
@@ -165,6 +166,14 @@ export default class {
// save the new configuration.
async configurePlugin (id, configuration) {
const plugin = this._getRawPlugin(id)
const metadata = await this._getPluginMetadata()
if (metadata !== undefined) {
configuration = sensitiveValues.merge(
configuration,
metadata.configuration
)
}
await this._configurePlugin(plugin, configuration)

View File

@@ -1,16 +1,24 @@
import synchronized from 'decorator-synchronized'
import { format, parse } from 'xo-remote-parser'
import { getHandler } from '@xen-orchestra/fs'
import { noSuchObject } from 'xo-common/api-errors'
import { ignoreErrors } from 'promise-toolbox'
import { noSuchObject } from 'xo-common/api-errors'
import * as sensitiveValues from '../sensitive-values'
import patch from '../patch'
import { mapToArray } from '../utils'
import { Remotes } from '../models/remote'
// ===================================================================
const obfuscateRemote = ({ url, ...remote }) => {
remote.url = format(sensitiveValues.obfuscate(parse(url)))
return remote
}
export default class {
constructor (xo) {
constructor (xo, { remoteOptions }) {
this._remoteOptions = remoteOptions
this._remotes = new Remotes({
connection: xo._redis,
prefix: 'xo:remote',
@@ -29,7 +37,7 @@ export default class {
)
)
const remotes = await this.getAllRemotes()
const remotes = await this._remotes.get()
remotes.forEach(remote => {
ignoreErrors.call(this.updateRemote(remote.id, {}))
})
@@ -46,7 +54,7 @@ export default class {
async getRemoteHandler (remote) {
if (typeof remote === 'string') {
remote = await this.getRemote(remote)
remote = await this._getRemote(remote)
}
if (!remote.enabled) {
@@ -57,7 +65,7 @@ export default class {
const handlers = this._handlers
let handler = handlers[id]
if (handler === undefined) {
handler = handlers[id] = getHandler(remote)
handler = handlers[id] = getHandler(remote, this._remoteOptions)
}
try {
@@ -77,10 +85,10 @@ export default class {
}
async getAllRemotes () {
return this._remotes.get()
return (await this._remotes.get()).map(_ => obfuscateRemote(_))
}
async getRemote (id) {
async _getRemote (id) {
const remote = await this._remotes.first(id)
if (remote === undefined) {
throw noSuchObject(id, 'remote')
@@ -88,6 +96,10 @@ export default class {
return remote.properties
}
getRemote (id) {
return this._getRemote(id).then(obfuscateRemote)
}
async createRemote ({ name, url, options }) {
const params = {
name,
@@ -119,9 +131,16 @@ export default class {
}
@synchronized()
async _updateRemote (id, props) {
const remote = await this.getRemote(id)
async _updateRemote (id, { url, ...props }) {
const remote = await this._getRemote(id)
// url is handled separately to take care of obfuscated values
if (typeof url === 'string') {
remote.url = format(sensitiveValues.merge(parse(url), parse(remote.url)))
}
patch(remote, props)
return (await this._remotes.update(remote)).properties
}

View File

@@ -57,15 +57,13 @@ const normalize = set => ({
id: set.id,
ipPools: set.ipPools || [],
limits: set.limits
? map(
set.limits,
limit =>
isObject(limit)
? limit
: {
available: limit,
total: limit,
}
? map(set.limits, limit =>
isObject(limit)
? limit
: {
available: limit,
total: limit,
}
)
: {},
name: set.name || '',

View File

@@ -10,10 +10,8 @@ import { forEach, isFunction, promisify } from '../utils'
const _levelHas = function has (key, cb) {
if (cb) {
return this.get(
key,
(error, value) =>
error ? (error.notFound ? cb(null, false) : cb(error)) : cb(null, true)
return this.get(key, (error, value) =>
error ? (error.notFound ? cb(null, false) : cb(error)) : cb(null, true)
)
}

View File

@@ -104,7 +104,7 @@ export default class {
.getAuthenticationTokensForUser(id)
.then(tokens => {
forEach(tokens, token => {
;this._xo.deleteAuthenticationToken(id)::ignoreErrors()
this._xo.deleteAuthenticationToken(id)::ignoreErrors()
})
})
::ignoreErrors()
@@ -112,13 +112,13 @@ export default class {
// Remove ACLs for this user.
this._xo.getAclsForSubject(id).then(acls => {
forEach(acls, acl => {
;this._xo.removeAcl(id, acl.object, acl.action)::ignoreErrors()
this._xo.removeAcl(id, acl.object, acl.action)::ignoreErrors()
})
})
// Remove the user from all its groups.
forEach(user.groups, groupId => {
;this.getGroup(groupId)
this.getGroup(groupId)
.then(group => this._removeUserFromGroup(id, group))
::ignoreErrors()
})
@@ -264,13 +264,13 @@ export default class {
// Remove ACLs for this group.
this._xo.getAclsForSubject(id).then(acls => {
forEach(acls, acl => {
;this._xo.removeAcl(id, acl.object, acl.action)::ignoreErrors()
this._xo.removeAcl(id, acl.object, acl.action)::ignoreErrors()
})
})
// Remove the group from all its users.
forEach(group.users, userId => {
;this.getUser(userId)
this.getUser(userId)
.then(user => this._removeGroupFromUser(id, user))
::ignoreErrors()
})

View File

@@ -5,8 +5,10 @@ export default class Workers {
return this._worker
}
constructor (app) {
constructor (app, config) {
app.on('start', () => {
process.env.XO_CONFIG = JSON.stringify(config)
this._worker = new Worker(require.resolve('./worker'))
})
app.on('stop', () => this._worker.end())

View File

@@ -5,8 +5,13 @@ import { mergeVhd as mergeVhd_ } from 'vhd-lib'
// Use Bluebird for all promises as it provides better performance and
// less memory usage.
//
// $FlowFixMe
global.Promise = require('bluebird')
// $FlowFixMe
const config: Object = JSON.parse(process.env.XO_CONFIG)
export function mergeVhd (
parentRemote: Remote,
parentPath: string,
@@ -14,9 +19,9 @@ export function mergeVhd (
childPath: string
) {
return mergeVhd_(
getHandler(parentRemote),
getHandler(parentRemote, config.remoteOptions),
parentPath,
getHandler(childRemote),
getHandler(childRemote, config.remoteOptions),
childPath
)
}

View File

@@ -1,4 +1,5 @@
import createLogger from '@xen-orchestra/log'
import { findKey } from 'lodash'
import { ignoreErrors } from 'promise-toolbox'
import { noSuchObject } from 'xo-common/api-errors'
@@ -20,7 +21,7 @@ import { Servers } from '../models/server'
const log = createLogger('xo:xo-mixins:xen-servers')
export default class {
constructor (xo) {
constructor(xo, { xapiOptions }) {
this._objectConflicts = { __proto__: null } // TODO: clean when a server is disconnected.
const serversDb = (this._servers = new Servers({
connection: xo._redis,
@@ -28,6 +29,7 @@ export default class {
indexes: ['host'],
}))
this._stats = new XapiStats()
this._xapiOptions = xapiOptions
this._xapis = { __proto__: null }
this._xapisByPool = { __proto__: null }
this._xo = xo
@@ -57,7 +59,7 @@ export default class {
// TODO: disconnect servers on stop.
}
async registerXenServer ({
async registerXenServer({
allowUnauthorized,
host,
label,
@@ -81,15 +83,15 @@ export default class {
return server.properties
}
async unregisterXenServer (id) {
;this.disconnectXenServer(id)::ignoreErrors()
async unregisterXenServer(id) {
this.disconnectXenServer(id)::ignoreErrors()
if (!(await this._servers.remove(id))) {
throw noSuchObject(id, 'xenServer')
}
}
async updateXenServer (
async updateXenServer(
id,
{
allowUnauthorized,
@@ -149,7 +151,7 @@ export default class {
// TODO: this method will no longer be async when servers are
// integrated to the main collection.
async _getXenServer (id) {
async _getXenServer(id) {
const server = await this._servers.first(id)
if (server === undefined) {
throw noSuchObject(id, 'xenServer')
@@ -158,13 +160,28 @@ export default class {
return server
}
_onXenAdd (xapiObjects, xapiIdsToXo, toRetry, conId) {
_onXenAdd(
newXapiObjects,
xapiIdsToXo,
toRetry,
conId,
dependents,
xapiObjects
) {
const conflicts = this._objectConflicts
const objects = this._xo._objects
forEach(xapiObjects, (xapiObject, xapiId) => {
forEach(newXapiObjects, function handleObject(xapiObject, xapiId) {
const { $ref } = xapiObject
const dependent = dependents[$ref]
if (dependent !== undefined) {
delete dependents[$ref]
return handleObject(xapiObjects[dependent], dependent)
}
try {
const xoObject = xapiObjectToXo(xapiObject)
const xoObject = xapiObjectToXo(xapiObject, dependents)
if (!xoObject) {
return
}
@@ -173,7 +190,7 @@ export default class {
xapiIdsToXo[xapiId] = xoId
const previous = objects.get(xoId, undefined)
if (previous && previous._xapiRef !== xapiObject.$ref) {
if (previous && previous._xapiRef !== $ref) {
const conflicts_ =
conflicts[xoId] || (conflicts[xoId] = { __proto__: null })
conflicts_[conId] = xoObject
@@ -188,7 +205,7 @@ export default class {
})
}
_onXenRemove (xapiObjects, xapiIdsToXo, toRetry, conId) {
_onXenRemove(xapiObjects, xapiIdsToXo, toRetry, conId) {
const conflicts = this._objectConflicts
const objects = this._xo._objects
@@ -220,121 +237,138 @@ export default class {
})
}
async connectXenServer (id) {
async connectXenServer(id) {
const server = (await this._getXenServer(id)).properties
const xapi = (this._xapis[server.id] = new Xapi({
const xapi = new Xapi({
allowUnauthorized: Boolean(server.allowUnauthorized),
readOnly: Boolean(server.readOnly),
...this._xapiOptions,
auth: {
user: server.username,
password: server.password,
},
readOnly: Boolean(server.readOnly),
url: server.host,
}))
watchEvents: false,
})
xapi.xo = (() => {
const conId = server.id
// Maps ids of XAPI objects to ids of XO objects.
const xapiIdsToXo = { __proto__: null }
// Map of XAPI objects which failed to be transformed to XO
// objects.
//
// At each `finish` there will be another attempt to transform
// until they succeed.
let toRetry
let toRetryNext = { __proto__: null }
const onAddOrUpdate = objects => {
this._onXenAdd(objects, xapiIdsToXo, toRetryNext, conId)
}
const onRemove = objects => {
this._onXenRemove(objects, xapiIdsToXo, toRetry, conId)
}
try {
await xapi.connect()
const xapisByPool = this._xapisByPool
const onFinish = () => {
const { pool } = xapi
if (pool) {
xapisByPool[pool.$id] = xapi
}
if (!isEmpty(toRetry)) {
onAddOrUpdate(toRetry)
toRetry = null
}
if (!isEmpty(toRetryNext)) {
toRetry = toRetryNext
toRetryNext = { __proto__: null }
}
const [{ $id: poolId }] = await xapi.getAllRecords('pool')
if (xapisByPool[poolId] !== undefined) {
throw new Error("the server's pool is already connected")
}
const { objects } = xapi
this._xapis[server.id] = xapisByPool[poolId] = xapi
const addObject = object => {
// TODO: optimize.
onAddOrUpdate({ [object.$id]: object })
return xapiObjectToXo(object)
}
xapi.xo = (() => {
const conId = server.id
return {
httpRequest: this._xo.httpRequest.bind(this),
// Maps ids of XAPI objects to ids of XO objects.
const xapiIdsToXo = { __proto__: null }
install () {
objects.on('add', onAddOrUpdate)
objects.on('update', onAddOrUpdate)
objects.on('remove', onRemove)
objects.on('finish', onFinish)
// Map of XAPI objects which failed to be transformed to XO
// objects.
//
// At each `finish` there will be another attempt to transform
// until they succeed.
let toRetry
let toRetryNext = { __proto__: null }
onAddOrUpdate(objects.all)
},
uninstall () {
objects.removeListener('add', onAddOrUpdate)
objects.removeListener('update', onAddOrUpdate)
objects.removeListener('remove', onRemove)
objects.removeListener('finish', onFinish)
const dependents = { __proto__: null }
onRemove(objects.all)
},
addObject,
getData: (id, key) => {
const value = (typeof id === 'string' ? xapi.getObject(id) : id)
.other_config[`xo:${camelToSnakeCase(key)}`]
return value && JSON.parse(value)
},
setData: async (id, key, value) => {
await xapi._updateObjectMapProperty(
xapi.getObject(id),
'other_config',
{
[`xo:${camelToSnakeCase(key)}`]:
value !== null ? JSON.stringify(value) : value,
}
const onAddOrUpdate = objects => {
this._onXenAdd(
objects,
xapiIdsToXo,
toRetryNext,
conId,
dependents,
xapi.objects.all
)
}
const onRemove = objects => {
this._onXenRemove(objects, xapiIdsToXo, toRetry, conId, dependents)
}
// Register the updated object.
addObject(await xapi._waitObject(id))
},
}
})()
const onFinish = () => {
if (!isEmpty(toRetry)) {
onAddOrUpdate(toRetry)
toRetry = null
}
xapi.xo.install()
if (!isEmpty(toRetryNext)) {
toRetry = toRetryNext
toRetryNext = { __proto__: null }
}
}
await xapi.connect().then(
() => this.updateXenServer(id, { error: null }),
error => {
this.updateXenServer(id, { error: serializeError(error) })
const { objects } = xapi
throw error
}
)
const addObject = object => {
// TODO: optimize.
onAddOrUpdate({ [object.$id]: object })
return xapiObjectToXo(object, dependents)
}
return {
httpRequest: this._xo.httpRequest.bind(this),
install() {
objects.on('add', onAddOrUpdate)
objects.on('update', onAddOrUpdate)
objects.on('remove', onRemove)
objects.on('finish', onFinish)
onAddOrUpdate(objects.all)
},
uninstall() {
objects.removeListener('add', onAddOrUpdate)
objects.removeListener('update', onAddOrUpdate)
objects.removeListener('remove', onRemove)
objects.removeListener('finish', onFinish)
onRemove(objects.all)
},
addObject,
getData: (id, key) => {
const value = (typeof id === 'string' ? xapi.getObject(id) : id)
.other_config[`xo:${camelToSnakeCase(key)}`]
return value && JSON.parse(value)
},
setData: async (id, key, value) => {
await xapi._updateObjectMapProperty(
xapi.getObject(id),
'other_config',
{
[`xo:${camelToSnakeCase(key)}`]:
value !== null ? JSON.stringify(value) : value,
}
)
// Register the updated object.
addObject(await xapi._waitObject(id))
},
}
})()
xapi.xo.install()
xapi.watchEvents()
this.updateXenServer(id, { error: null })::ignoreErrors()
} catch (error) {
xapi.disconnect()::ignoreErrors()
this.updateXenServer(id, { error: serializeError(error) })::ignoreErrors()
throw error
}
}
async disconnectXenServer (id) {
async disconnectXenServer(id) {
const xapi = this._xapis[id]
if (!xapi) {
throw noSuchObject(id, 'xenServer')
@@ -343,20 +377,20 @@ export default class {
delete this._xapis[id]
const { pool } = xapi
if (pool) {
delete this._xapisByPool[pool.id]
if (pool != null) {
delete this._xapisByPool[pool.$id]
}
xapi.xo.uninstall()
return xapi.disconnect()
}
getAllXapis () {
getAllXapis() {
return this._xapis
}
// Returns the XAPI connection associated to an object.
getXapi (object, type) {
getXapi(object, type) {
if (isString(object)) {
object = this._xo.getObject(object, type)
}
@@ -374,7 +408,7 @@ export default class {
return xapi
}
async getAllXenServers () {
async getAllXenServers() {
const servers = await this._servers.get()
const xapis = this._xapis
forEach(servers, server => {
@@ -395,24 +429,24 @@ export default class {
return servers
}
getXapiVmStats (vmId, granularity) {
getXapiVmStats(vmId, granularity) {
return this._stats.getVmStats(this.getXapi(vmId), vmId, granularity)
}
getXapiHostStats (hostId, granularity) {
getXapiHostStats(hostId, granularity) {
return this._stats.getHostStats(this.getXapi(hostId), hostId, granularity)
}
getXapiSrStats (srId, granularity) {
getXapiSrStats(srId, granularity) {
return this._stats.getSrStats(this.getXapi(srId), srId, granularity)
}
async mergeXenPools (sourceId, targetId, force = false) {
const sourceXapi = this.getXapi(sourceId)
async mergeXenPools(sourcePoolId, targetPoolId, force = false) {
const sourceXapi = this.getXapi(sourcePoolId)
const {
_auth: { user, password },
_url: { hostname },
} = this.getXapi(targetId)
} = this.getXapi(targetPoolId)
// We don't want the events of the source XAPI to interfere with
// the events of the new XAPI.
@@ -426,6 +460,8 @@ export default class {
throw e
}
await this.unregisterXenServer(sourceId)
this.unregisterXenServer(
findKey(this._xapis, candidate => candidate === sourceXapi)
)::ignoreErrors()
}
}

View File

@@ -26,7 +26,7 @@
"child-process-promise": "^2.0.3",
"core-js": "3.0.0-beta.3",
"pipette": "^0.9.3",
"promise-toolbox": "^0.10.1",
"promise-toolbox": "^0.11.0",
"tmp": "^0.0.33",
"vhd-lib": "^0.4.0"
},

View File

@@ -10,7 +10,7 @@ const GRAIN_ADDRESS_OFFSET = 56
* the grain table is the array of LBAs (in byte, not in sector) ordered by their position in the VDMK file
* THIS CODE RUNS ON THE BROWSER
*/
export default async function readVmdkGrainTable (fileAccessor) {
export default async function readVmdkGrainTable(fileAccessor) {
const getLongLong = (buffer, offset, name) => {
if (buffer.length < offset + 8) {
throw new Error(

Some files were not shown because too many files have changed in this diff Show More