Compare commits

..

317 Commits

Author SHA1 Message Date
Pierre Donias
0fad24d757 Handle clone and copy 2019-07-23 17:05:02 +02:00
Pierre Donias
3f0878940f WiP: VM owner 2019-07-23 17:05:01 +02:00
badrAZ
a00e3e6f41 feat(xo-web#copyVm): only show zstd option when it's supported (#4326)
See #3892
2019-07-23 15:00:55 +02:00
BenjiReis
82ba02b4f3 chore(xo-server-sdn-controller): rework logs (#4365) 2019-07-23 11:45:51 +02:00
badrAZ
d70ae6ebe3 feat(xo-web/backup-ng/new): create schedule without mode (#4357)
Fixes #4098
2019-07-22 15:22:01 +02:00
BenjiReis
f6c411a261 fix(xo-server-sdn-controller): better monitor of host life cycle (#4314) 2019-07-22 14:53:42 +02:00
badrAZ
b606eaf9ee chore(CHANGELOG): update next 2019-07-22 10:42:04 +02:00
badrAZ
516edd1b09 feat(xo-web): 5.46.0 2019-07-22 10:29:05 +02:00
badrAZ
e31c3b1f27 feat(xo-server): 5.46.0 2019-07-22 10:28:54 +02:00
Pierre Donias
619818f968 feat(xo-web/{host,vm}): state icons improvements (#4363)
- Always use .xo-status-* classes
- Show host busy state in the host view as well (previously only in home/hosts view)
- Differentiate "disabled" state from "busy" state
- Host view state icon tooltip
- Homogenize state display between hosts and VMs
2019-07-19 17:07:47 +02:00
Julien Fontanet
79a80a1adf fix(xo-server/vm.set): setting VCPUs_live (#4360)
Caused by 3196c7ca09

Fixes xoa-support#1632
2019-07-18 17:00:01 +02:00
Julien Fontanet
7cef48b995 fix(xo-server/vm.set): setting affinity (#4361)
Caused by 3196c7ca09

Fixes xoa-support#1625
2019-07-18 16:41:02 +02:00
Rajaa.BARHTAOUI
7d3d1b1544 feat(xo-web/vm/console): copy/paste multiple lines (#4341)
Fixes #4261
2019-07-18 16:31:20 +02:00
Rajaa.BARHTAOUI
3f935f271d feat(xo-web/pool): ability to add multiple hosts on the pool (#3716)
Fixes #2402
2019-07-18 14:24:56 +02:00
HamadaBrest
89935a1517 feat(xo-web/backup-ng): ability to bypass VDI chain check (#4340)
Fixes #4324
2019-07-18 10:46:22 +02:00
Rajaa.BARHTAOUI
c67af4fb2f feat(xo-web/host/advanced): ability to edit host ISCSI IQN (#4208)
Fixes #4048
2019-07-17 17:32:05 +02:00
Julien Fontanet
0b4adc36a0 chore(xo-server-test): remove unused deps 2019-07-17 16:49:53 +02:00
Julien Fontanet
44776b795f fix(xo-server-test/.babelrc.js): look into pro deps 2019-07-17 16:49:10 +02:00
Julien Fontanet
bec73a1c43 feat(normalize-packages): further clean package.json 2019-07-17 16:31:34 +02:00
Julien Fontanet
6ce35fdfa8 fix(import-packages): force merge (--no-ff) 2019-07-17 16:30:54 +02:00
Julien Fontanet
dabc2d0442 fix(xo-server-test): adapt to monorepo 2019-07-17 15:54:26 +02:00
Julien Fontanet
0527d3bc2b Merge branch 'xo-server-test/master' 2019-07-17 15:54:10 +02:00
Julien Fontanet
a7cfb71070 feat(xo-server-test): move all files to packages/xo-server-test 2019-07-17 15:54:10 +02:00
Pierre Donias
52003bedb4 feat(xo-web/Usage): use same color for "Others" block (#4347)
Fixes #3608

And show number of others in tooltip
2019-07-17 15:41:40 +02:00
Rajaa.BARHTAOUI
a02fb8e739 fix(xo-web/task): fix hidden notification by search field (#4305)
Fixes #3874
2019-07-16 17:07:51 +02:00
badrAZ
60fad187a2 feat(xo-web/backup-ng/new): generate default schedule if no sch… (#4183)
…edules specified

Fixes #4036
2019-07-16 16:20:43 +02:00
BenjiReis
e8cd1e070f fixme(xo-server/xapi): missing vgpu_map param to VM.migrate_send (#4351) 2019-07-16 16:15:25 +02:00
badrAZ
de6620be12 chore(CHANGELOG): update next 2019-07-16 16:05:25 +02:00
badrAZ
72dee73faa fix(xo-server): 5.45.3 2019-07-16 16:01:54 +02:00
badrAZ
d8ce27907d fix(@xen-orchestra/fs): 0.10.1 2019-07-16 15:57:00 +02:00
badrAZ
3d8891d518 chore(CHANGELOG): update next 2019-07-16 14:50:02 +02:00
badrAZ
97742ccdc2 fix(xo-server): 5.45.2 2019-07-16 14:00:06 +02:00
badrAZ
82fec86179 fix(xen-api): 0.27.1 2019-07-16 13:39:16 +02:00
badrAZ
be83b53875 chore(CHANGELOG): update next 2019-07-16 11:12:05 +02:00
badrAZ
85fda0c18b feat(xo-web): 5.45.1 2019-07-16 10:50:17 +02:00
badrAZ
a89f8fbd9c feat(xo-server): 5.45.1 2019-07-16 10:50:06 +02:00
Julien Fontanet
efdfa1f2f7 chore: update dependencies
Main goal is to update Babel to latest fix release which fix object rest spread on Node 6
2019-07-16 09:59:03 +02:00
badrAZ
5bd61e3fb0 fix(xo-server/vm#set): fix virtualization switch (#4349) 2019-07-15 14:57:16 +02:00
badrAZ
a45f83b646 fix(README): fix test command 2019-07-12 16:59:49 +02:00
Rajaa.BARHTAOUI
16135b8e37 feat(xo-web/sr/general): improve SR usage graph (#3830)
See #3608
2019-07-12 16:56:06 +02:00
heafalan
b011e8656f fix(user#user.changePassword): delete useless tests (#118) 2019-07-12 11:00:05 +02:00
badrAZ
215432be6c chore(CHANGELOG): update next 2019-07-11 16:12:48 +02:00
badrAZ
d373760412 feat(xo-web): 5.45.0 2019-07-11 16:12:48 +02:00
badrAZ
a1de04e285 feat(xo-server): 5.45.0 2019-07-11 16:12:48 +02:00
badrAZ
23e16732fd feat(xen-api): 0.27.0 2019-07-11 16:12:48 +02:00
badrAZ
5efac84b8b feat(xo-server-sdn-controller): 0.1.1 2019-07-11 16:12:48 +02:00
badrAZ
2cbc7b7d7d feat(@xen-orchestra/fs): 0.10.0 2019-07-11 16:12:48 +02:00
Julien Fontanet
b1acbaecc2 fix(log): remove some lints 2019-07-10 15:40:25 +02:00
Julien Fontanet
6d61e8efff chore: use native String#{end,start}sWith
Supported since Node 4
2019-07-09 16:00:16 +02:00
Julien Fontanet
482e6b3cb3 feat(xo-server/backupNg.runJob): new param settings (#4321)
Can be used to override certain job settings, the format is the same as the `settings` field in the job.

Related to xoa-support#1583
2019-07-08 14:54:09 +02:00
heafalan
445b13ec29 feat: create temp remote with remote configuration (#117) 2019-07-08 13:01:40 +02:00
HamadaBrest
116af372dc fix(fs): EIO errors due to massive parallel operations (#4330)
Fixes #4323
2019-07-08 11:42:25 +02:00
badrAZ
970952783c fix(xo-web/vm/advanced): fix CPUs/max CPUs edition (#4337) 2019-07-08 10:44:16 +02:00
Pierre Donias
e59cf13456 feat(xo-web/VM): show related pending tasks (#3982)
And their progress.

Fixes #3811
2019-07-05 17:15:44 +02:00
BenjiReis
d0cfddce19 chore(xo-server-sdn-controller): _getOrWaitObject → getObjectByRef (#4336)
When possible.
2019-07-05 13:48:37 +02:00
Julien Fontanet
30b2a8dd8d feat(log/README): document data param 2019-07-05 12:13:54 +02:00
BenjiReis
b811ee7e7e chore(xo-server-sdn-controller): use optionnal chaining (#4335) 2019-07-05 11:54:07 +02:00
Julien Fontanet
ebe7f6784a chore: re-format with Prettier
Related to c8aa058ede

Mainly due to https://prettier.io/blog/2019/06/06/1.18.0.html#stop-breaking-simple-template-literals-5979-by-jwbay
2019-07-05 11:34:15 +02:00
BenjiReis
e40792378f chore(xo-server-sdn-controller): simplify for loops (#4334) 2019-07-05 11:08:57 +02:00
Julien Fontanet
cc9c8fb891 feat(CHANGELOG.unreleased): add instructions for devs (#4333) 2019-07-05 10:05:02 +02:00
Enishowk
ca06c4d403 fix(xo-web/backup-ng/restore): display correct size of full VM backup (#4332)
Fixes #4316
2019-07-05 09:31:32 +02:00
Julien Fontanet
c8aa058ede chore: update dependencies 2019-07-04 16:01:07 +02:00
badrAZ
34169d685e fix(xo-server/xen-servers): handle pool UUID change (#4302)
Fixes #4299
2019-07-04 14:52:30 +02:00
HamadaBrest
d5a9d36815 feat(xo-server/vm): VM operators can revert any snapshot (#4247)
Fixes #3928

- VM operators were only able to revert the snapshots they created. They can now revert any snapshot
- VM operators can still only delete the snapshots they created
- VM admins still have full control over the snapshots
2019-07-04 11:21:54 +02:00
HamadaBrest
c7aaeca530 feat(xo-web/settings/servers): display connection issues (#4310)
Fixes #4300
2019-07-04 10:19:44 +02:00
Julien Fontanet
863e4f0c19 feat(xo-server/backup NG): new setting bypassVdiChainsCheck (#4320)
Related to xoa-support#1583
2019-07-04 10:07:28 +02:00
BenjiReis
0226e0553d fix(xo-server-sdn-controller): dont pick PIF if ip_configuration_mode = None (#4319)
Fixes xoa-support#1572
2019-07-03 17:07:30 +02:00
Julien Fontanet
02995d278f fix(xo-server/server.set): declare readOnly in params
Related to b8524732ce
2019-07-02 17:31:44 +02:00
Julien Fontanet
78a2104bcc fix(xo-server/api/server): use correct types
Booleans should be booleans, not strings.
2019-07-02 17:09:57 +02:00
heafalan
4e9d143996 fix: remove VM ids from snapshots (#116) 2019-07-01 09:39:41 +02:00
badrAZ
0811e5c765 feat(xo-web/stats): ability to display last day stats (#4168)
Fixes #4160
2019-06-28 14:35:50 +02:00
heafalan
b2cf2edd43 fix(backup-ng): update snapshots (#115) 2019-06-28 09:34:48 +02:00
heafalan
db493f6887 fix(backup-ng): defaultSr -> default (#114) 2019-06-28 09:09:38 +02:00
heafalan
2cd0dec480 fix: delete_disks -> deleteDisks (#113) 2019-06-27 16:27:07 +02:00
badrAZ
29024888fb chore(CHANGELOG): v5.36.0 2019-06-27 15:07:29 +02:00
badrAZ
dbcaab2bc1 chore(CHANGELOG): update next 2019-06-27 14:52:16 +02:00
badrAZ
28d445ae1c feat(xo-web): 5.44.0 2019-06-27 14:08:37 +02:00
badrAZ
530360f859 feat(xo-server): 5.44.0 2019-06-27 14:08:26 +02:00
badrAZ
738c55bad0 feat(xo-server-backup-reports): 0.16.2 2019-06-27 14:08:11 +02:00
badrAZ
4b09bc85f5 feat(xo-server-auth-saml): 0.6.0 2019-06-27 14:07:44 +02:00
badrAZ
5bc67d3570 feat(xo-server-sdn-controller): 0.1.0 2019-06-27 14:07:23 +02:00
badrAZ
f7ae6222b7 feat(xen-api): 0.26.0 2019-06-27 14:06:14 +02:00
badrAZ
1e50dab093 feat(xo-server-auth-ldap): 0.6.5 2019-06-27 14:05:20 +02:00
Julien Fontanet
d1935bf778 feat(xo-server-auth-ldap/cli): display the whole entry on success
This can help while debugging filters.
2019-06-27 11:31:24 +02:00
Julien Fontanet
70a346d11e feat(CHANGELOG.unreleased): add xen-api v0.26.0 2019-06-27 11:12:23 +02:00
Julien Fontanet
fd39a2063d fix(xen-api): refresh pool on new session 2019-06-27 11:08:45 +02:00
Julien Fontanet
682512fffe fix(xen-api/events): full refresh on new session 2019-06-27 11:08:45 +02:00
HamadaBrest
b13f91ec8d fix(xo-web/new-vm): tooltips not showing up on disabled inputs (#4309)
Fixes #4304

mouseenter event not supported on disabled inputs in some browsers
https://devdocs.io/dom_events/mouseenter
2019-06-27 10:24:47 +02:00
BenjiReis
a140fc09ac feat(xo-server-sdn-controller): new plugin wich enables private pool-wide networkks (#4269)
See xcp-ng/xcp#175
2019-06-27 09:49:58 +02:00
badrAZ
f403a7e753 fix(xo-web): fix fibre channel paths display (#4303)
Fixes #4291
2019-06-26 16:42:56 +02:00
heafalan
dfe5f412eb feat: config.toml -> sample.config.toml and update documentation (#111) 2019-06-26 14:52:34 +02:00
Rajaa.BARHTAOUI
033d784c52 feat(xo-web/vm/network): set VIF bandwidth rate limit (#4293)
Fixes #4215
2019-06-26 12:18:17 +02:00
Rajaa.BARHTAOUI
62c3fa13ca feat(xo-web/{sr/hosts,host/storage}): PBD details (#4284)
Fixes #4161
2019-06-25 15:50:26 +02:00
HamadaBrest
ce338cb6ca fix(xo-web/host): host CPU hyperthreading detection (#4285)
Fixes #4262
2019-06-25 09:43:00 +02:00
heafalan
003eadc8fd fix(backup-ng): use generic test for retentions (#103) 2019-06-24 14:15:24 +02:00
Rajaa.BARHTAOUI
8782151c5d feat(xo-web/host): warning when host and XOA's time differ too much (#4173)
Fixes #4113
2019-06-21 16:33:32 +02:00
HamadaBrest
b22c74c5a8 feat(xo-web/new-sr): list ZFS pools at SR creation (#4266)
Fixes #4260
2019-06-21 15:27:57 +02:00
badrAZ
254fa36c01 chore: structured "config.toml" (#110)
See #108
2019-06-21 15:25:29 +02:00
HamadaBrest
a3e4253005 feat(xo-web/home/vm): add "Create backup" bulk action (#4257)
Fixes #2573
2019-06-21 10:36:00 +02:00
Rajaa.BARHTAOUI
2388593b8a feat(xo-web): warning for 'express' license (#4279)
Fixes #4251
2019-06-21 10:31:45 +02:00
Julien Fontanet
cdced63c1b feat(xo-server-auth-saml): improve Azure AD compat (#4294)
See xoa-support#1543
2019-06-20 17:11:39 +02:00
Julien Fontanet
45e1d1ecef fix(CHANGELOG.unreleased): fix issue link 2019-06-19 15:36:21 +02:00
badrAZ
f44447ce71 fix(xo-server/metadata): missing Xapis should trigger a failure job (#4283)
Fixes #4281
2019-06-19 15:20:26 +02:00
HamadaBrest
238e9cd8cc feat(xo-web/new/sr): ability to select NFS version (#4277)
Fixes #3951
2019-06-19 10:40:35 +02:00
Julien Fontanet
e171d8ed0e fix(xo-server/vm.set): add auto_poweron to params
Related to b8524732ce

Fixes #4295
2019-06-18 17:24:54 +02:00
HamadaBrest
bd3399e04b chore(xen-api/README): document --au flag (#4292) 2019-06-18 14:32:30 +02:00
Enishowk
2b4443f333 feat(xo-web/backup-ng/restore): display size for full VM backup (#4245)
Fixes #4009
2019-06-18 09:55:14 +02:00
Julien Fontanet
ab6548122f Revert "fix(xo-server/metadata-backups): missing pool/remote should emit failure tasks"
This reverts commit f81573d999.
2019-06-17 14:36:49 +02:00
badrAZ
f81573d999 fix(xo-server/metadata-backups): missing pool/remote should emit failure tasks
See #4281
2019-06-17 11:29:56 +02:00
Julien Fontanet
84ccebb858 fix(xo-server/vm.create): accept extra params
Fixes #4280
2019-06-15 23:37:46 +02:00
badrAZ
530bc50e7c chore(CHANGELOG): update next 2019-06-13 14:57:22 +02:00
badrAZ
57e490fc23 feat(xo-web): 5.43.0 2019-06-13 14:11:21 +02:00
badrAZ
61e902c094 feat(xo-server): 5.43.0 2019-06-13 14:11:08 +02:00
badrAZ
8378ba77d6 feat(xen-api): 0.25.2 2019-06-13 14:09:59 +02:00
Julien Fontanet
c9e30b74e2 fix(xo-server/api): only change id to namespace if necessary 2019-06-13 12:00:10 +02:00
HamadaBrest
af944fd2e3 feat(xo-server,xo-web): ability to set HVM boot firmware (#4268)
Fixes #4264
2019-06-12 17:41:58 +02:00
Nicolas Raynaud
bcc0e76f1d feat(xo-server/api): sr.probZfs and sr.createFile (#4258)
Server side of #4260

Related to xcp-ng/xcp-ng-xapi-plugins#5
2019-06-10 17:38:58 +02:00
badrAZ
95078d250a feat(xo-server/backup-ng): clean task in logs for full, DR and CR (#4236)
This is equivalent to the `merge` task for delta backups.
2019-06-10 15:42:38 +02:00
Julien Fontanet
4b16a2c0c5 fix(xo-server/xo.getAllObjects): correctly validate ndjson param
Fixes https://xcp-ng.org/forum/topic/1478/fresh-install-can-t-create-vms-or-see-vms-after-refresh
2019-06-10 14:32:12 +02:00
Julien Fontanet
b8524732ce fix(xo-server/api): throw on unexpected parameters 2019-06-06 18:11:12 +02:00
Julien Fontanet
814fee4f47 chore(package): update deps 2019-06-06 09:47:04 +02:00
Julien Fontanet
d641d35d5c fix(xo-server/importDeltaVm): disable HA during import
Fixes xoa-support#1525

This is necessary because HA does not respect `blocked_operations.start`.
2019-06-05 17:38:36 +02:00
Julien Fontanet
7464d95b57 chore(xo-server/importDeltaVm): merge some attributes changes into VM record creation
Limits race conditions (especially for the `blocked_operations.start`.
2019-06-05 17:27:16 +02:00
heafalan
8924a64622 chore: delete useless test (#105) 2019-06-05 16:59:10 +02:00
Julien Fontanet
3d6aa667fe chore(xo-server): use xen-api setters for tags 2019-06-04 18:21:47 +02:00
Julien Fontanet
147c3d2e7b fix(xo-server): dont touch other entries when changing HVM_boot_params.order 2019-06-04 14:55:45 +02:00
Julien Fontanet
ac298c3be3 chore(xo-server): use more Xapi's field entries updaters 2019-06-04 14:37:12 +02:00
Julien Fontanet
e88848c44a fix(xo-web/xoa): fix email quirk (#4259) 2019-06-03 11:12:01 +02:00
Julien Fontanet
cd518e3e4c chore: update dependencies 2019-06-03 10:51:14 +02:00
heafalan
114d521636 feat: test backupNg.runJob with srs without copyRetention (#87)
See #76
2019-06-03 08:58:34 +02:00
Pierre Donias
24d4fad394 chore(CHANGELOG): 5.35.0 2019-05-29 17:28:02 +02:00
Julien Fontanet
6d8785e689 feat(xo-web): 5.42.1 2019-05-29 17:20:59 +02:00
HamadaBrest
508cbf0a82 feat(xo-web/host): display hyperthreading status in advanced tab (#4263)
Fixes #2573
2019-05-29 17:20:06 +02:00
Julien Fontanet
c83f56166d feat(xo-server): 5.42.1 2019-05-29 17:18:29 +02:00
Julien Fontanet
7199e1a214 feat(CHANGELOG): add channel badges 2019-05-29 16:48:07 +02:00
heafalan
85d55e97e7 feat: test run backup job with remotes without export retention (#86)
See #76
2019-05-29 16:47:50 +02:00
Julien Fontanet
cc2c71c076 chore(xo-server): remove unnecessary methods 2019-05-28 18:53:57 +02:00
Pierre Donias
9ca273b2c4 chore(CHANGELOG.unreleased): add some missing links (#4253) 2019-05-28 14:09:19 +02:00
Julien Fontanet
b85c2f35b6 fix(xo-server/vm.set): autoPoweronauto_poweron in XAPI
Related to 0e1e32d241
2019-05-28 11:48:00 +02:00
HamadaBrest
fdd79885f9 feat(xo-web/VM): display VDI size in migrate modal (#4250)
Fixes #2534
2019-05-27 16:56:45 +02:00
Julien Fontanet
b2eb970796 fix(xo-server/vm.set): cast weight to string
Follow-up of 49e1b0ba7
2019-05-27 16:23:38 +02:00
Julien Fontanet
3ee9c1b550 chore(xo-server/Xapi): remove unused setVcpuWeight 2019-05-27 16:23:37 +02:00
HamadaBrest
2566c24753 fix(xo-web/host): incorrect hypervisor name in RAM usage tooltip (#4248)
Fixes #4246
2019-05-27 15:44:59 +02:00
Julien Fontanet
49e1b0ba7e fix(xo-server/vm.set): cast logical numbers to XAPI string values 2019-05-27 11:10:19 +02:00
Julien Fontanet
453c329f14 fix(xo-server/vm.set): videoram is strictly a number 2019-05-27 11:04:44 +02:00
heafalan
abad2944fb feat: test backupNg.runJob 3 times with retention 2 and revert (#93)
See #76
2019-05-27 10:53:34 +02:00
badrAZ
27193f38f3 feat(xo-web): 5.42.0 2019-05-24 15:32:16 +02:00
badrAZ
d3dc94e210 feat(xo-server): 5.42.0 2019-05-24 15:31:54 +02:00
Julien Fontanet
6dad860635 fix(xo-server/getRemoteHandler): only cache on success
Otherwise subsequent calls will use an invalid handler.

Related to xoa-support#1498
2019-05-23 18:13:58 +02:00
Julien Fontanet
0362ac8909 feat(xo-web/home): case-sensitive filtering 2019-05-23 17:34:19 +02:00
Pierre Donias
e7b79f83d1 fix(xo-web): cast XOA_PLAN for strict equality tests (#4241) 2019-05-23 17:17:48 +02:00
Pierre Donias
62379c1e41 feat(xo-web/settings/logs): suggest XCP-ng when LICENCE_RESTRICTION (#4238)
Fixes #3876
2019-05-23 17:16:39 +02:00
Pierre Donias
23b422e3df feat(xo-server,xo-web/user): forget all authentication tokens (#4224)
Fixes #4214
2019-05-23 17:13:27 +02:00
Pierre Donias
f8e6dee635 fix(xo-web/vm/networks/addresses): avoid duplicate IP addresses (#4239)
Fixes support#1227
2019-05-23 14:33:32 +02:00
Nicolas Raynaud
c8e9b287f4 fix(xo-server/Xapi#_importOvaVm): userdevice must be string not a number (#4232)
Fixes xoa-support#1479
2019-05-22 16:12:19 +02:00
HamadaBrest
c9412dbcd0 fix(xo-web/xoa): ask confirm on upgrade if running jobs (#4235)
Fixes #4164
2019-05-22 15:19:31 +02:00
Julien Fontanet
77222e9e6b Update PULL_REQUEST_TEMPLATE.md 2019-05-22 14:50:57 +02:00
heafalan
2827544409 chore: backupNg.create() -> backupNg.createJob() (#96) 2019-05-22 11:55:30 +02:00
HamadaBrest
9d0f24eae1 feat(xo-web/xoa): release channels support (#4202)
Fixes #4200
2019-05-22 09:32:01 +02:00
heafalan
db0a399da1 chore: use getSchedule() (#99) 2019-05-22 08:58:01 +02:00
Pierre Donias
6e527947be fix(xo-web): adminOnly breaks the routes (#4231)
Introduced by 59e68682bd

`@routes` must always be on top because it decorates add a `routes` property to the component which will be used by the parent.
2019-05-21 17:45:27 +02:00
Julien Fontanet
e7051c1129 fix(xo-server/network.set): dont pass undefined to update_other_config()
Related to 0e1e32d241
2019-05-21 16:47:22 +02:00
Julien Fontanet
3196c7ca09 chore(xo-server): use Xapi's setters (#4229) 2019-05-21 15:44:10 +02:00
Julien Fontanet
0e1e32d241 chore(xo-server): use Xapi's field entries updaters (#4230) 2019-05-21 15:25:37 +02:00
Julien Fontanet
a34912fb0d chore(xo-server): move some calls to Xapi#callAsync (#4227)
Fixes #4226

`.callAsync` is more robust to disconnections than `.call` and should be used for all non-instantaneous calls.

Unfortunately the result can be embedded into XML, you should either not use the result or add `.then(extractOpaquerRef)` if you are expecting an opaque ref.
2019-05-21 15:04:24 +02:00
Dustin B
c7c6e0e2ff chore(xo-web/messages): one shot job → onetime job (#4222) 2019-05-21 11:30:42 +02:00
Julien Fontanet
1e529c995a chore(xo-server/vm.reboot): move into Xapi#rebootVm 2019-05-21 10:37:56 +02:00
Julien Fontanet
7be1c7a47b fix(xo-server): always use Xapi#callAsync for migration 2019-05-21 10:24:14 +02:00
HamadaBrest
b17380443b chore(@xen-orchestra/fs): test truncate() (#4225)
Related to #4180
2019-05-21 10:20:31 +02:00
Pierre Donias
59e68682bd fix(xo-web): lock admin pages (#4220)
Related to xoa-support#1460
2019-05-20 17:31:26 +02:00
Julien Fontanet
b7a92cfe92 feat(xo-server/vif.set): rateLimit support
Server-side of #4215
2019-05-20 16:06:42 +02:00
HamadaBrest
5ebe27da49 feat(fs): add truncate method (#4180) 2019-05-20 14:03:54 +02:00
Julien Fontanet
42df6ba6fa chore: update dependencies 2019-05-17 16:31:39 +02:00
Pierre Donias
8210fddfab fix(xo-web/charts): ensure consistent series order (#4221)
Fixes support#1481
2019-05-16 13:54:27 +02:00
Pierre Donias
f55ed273c5 chore(xo-web): remove unused messages (#4219) 2019-05-16 13:28:39 +02:00
Dustin B
d67e95af7b fix(xo-web/messages): more verbiage and typo fixes, clarifications as well (#4218) 2019-05-15 17:36:00 +02:00
badrAZ
0b0f235252 feat(xo-web/new/metadata): ability to set the backup report when property (#4149) 2019-05-15 16:47:59 +02:00
Jon Sands
36a5f52068 fix(docu/interface) grammar fixes for interface messages (#4213)
* Grammar fixes and typo for messages
2019-05-15 15:10:43 +02:00
Julien Fontanet
31266728f7 feat(xo-server): add / mounts to vendor config 2019-05-15 14:28:20 +02:00
heafalan
87d2096ed7 chore: createUser() -> createTempUser() (#100) 2019-05-15 14:21:11 +02:00
Rajaa.BARHTAOUI
8c79ea4ce3 feat(xo-web/vm/general): display 'Started... ago' for paused state (#4170)
Fixes #3750
2019-05-15 10:41:02 +02:00
Dustin B
c73a4204cb Verbiage change to align with main messages.js 2019-05-14 22:52:58 +02:00
Dustin B
0b3c2cc252 Verbiage changes (#4211)
* Verbiage changes
2019-05-14 22:03:50 +02:00
Dustin B
2bd3ca1d0b Grammar and typos adjustments (#4210)
* Grammar and typos adjustments
2019-05-14 20:55:50 +02:00
badrAZ
ce8649d991 fix(xo-web/backup-ng): handle improper "reportWhen" value (#4199)
Additional change to #4178 to actually fix #4092.
2019-05-14 17:12:53 +02:00
badrAZ
9bd563b111 chore(CHANGELOG): update next 2019-05-14 16:32:31 +02:00
badrAZ
6ceb924a85 feat(xo-web): 5.41.0 2019-05-14 16:20:31 +02:00
badrAZ
c2ef0ded43 feat(xo-server): 5.41.0 2019-05-14 16:20:09 +02:00
heafalan
d47f66548d chore: use snapshot property matchers instead of 'omit' (#98) 2019-05-10 13:23:47 +02:00
heafalan
fb9425e503 feat(backupNg): test running a job with a VM without disks (#92)
See #76
2019-04-18 17:18:15 +02:00
heafalan
d75580e11d feat: add doc for tests (#90) 2019-04-15 13:55:06 +02:00
badrAZ
a64960ddd0 chore(xoConnection): various changes (#95) 2019-04-15 09:22:53 +02:00
heafalan
876850a7a7 chore(_xoWithTestHelpers): refactoring (#94) 2019-04-10 15:23:45 +02:00
heafalan
0b689d99fa feat: test backupNg.runJob with non-existent vm (#91)
See #76
2019-03-26 16:39:06 +01:00
heafalan
cd0064d19c feat: fails trying to test backupNg.runJob with no matching VMs (#84)
See #76
2019-03-21 14:06:06 +01:00
heafalan
b4baa6cd7b feat: test backupNg.runJob without schedule (#83)
See #76
2019-03-15 10:18:42 +01:00
heafalan
1ab2cdeed3 feat: test backupNg.deleteJob (#82)
See #76
2019-03-12 09:44:45 +01:00
badrAZ
83c0281a33 chore: various changes (#89) 2019-03-07 16:58:54 +01:00
heafalan
437b0b0240 feat: test backupNg.createJob with schedules (#81)
See #76
2019-03-05 13:27:02 +01:00
heafalan
5c48697eda feat: improve test job.runSequence() (#71)
Fixes #65
2019-03-01 12:13:21 +01:00
heafalan
0feea5b7a6 feat: test "backupNg.createJob" without defining schedules (#77)
See #76
2019-02-27 16:27:15 +01:00
heafalan
9eb27fdd5e Chore(old-tests/user): delete old file (#80) 2019-02-18 09:32:52 +01:00
heafalan
6e4a64232a fix(job.create): job not properly deleted after the tests (#79) 2019-02-18 09:31:06 +01:00
heafalan
4bbedeeea9 fix: improve test job.delete (#75)
See #65
2019-02-15 13:43:14 +01:00
heafalan
b5c004e870 fix: change vmIdXoTest (#78) 2019-02-14 17:13:28 +01:00
heafalan
a0ef1ab4f4 fix: improve test job.set (#73)
See #65
2019-02-11 15:32:39 +01:00
heafalan
c9172a11a8 fix: improve test job.get (#72)
See #65
2019-02-07 16:44:32 +01:00
badrAZ
a0feee912e chore(util): remove useless console.logs 2019-02-07 16:24:44 +01:00
heafalan
8e42b7b891 fix: improve test job.getAll (#69)
See #65
2019-02-07 16:16:10 +01:00
heafalan
147d7e773f fix: improve test job.create (#68)
See #65
2019-02-07 14:23:59 +01:00
heafalan
759ab1c5ee fix: all-uppercase format for global constants (#70) 2019-02-04 16:04:17 +01:00
heafalan
4c1581d845 feat: improve test user.changePassword (#66)
Fixes #53
2019-02-01 15:23:01 +01:00
heafalan
e1c6e4347a fix(user.delete): create a new admin user for tests (#67)
Introduced by #59
2019-01-29 17:25:17 +01:00
heafalan
256f117bbf feat: improve test user.set (#63)
See #53
2019-01-22 15:11:58 +01:00
heafalan
3b0acf82c7 fix: change the titles of tests (#61) 2019-01-11 16:35:52 +01:00
heafalan
3a12f3d6c7 feat: improve test user.getAll (#62)
See #53
2019-01-11 15:54:59 +01:00
heafalan
335ac5a595 chore(package): remove jest-extended (#60) 2019-01-11 09:35:17 +01:00
heafalan
d0e2e97007 fix: improve test user.delete (#59)
Fixes #53
2019-01-10 17:03:05 +01:00
heafalan
85e1baa2dc fix: improve test user.create (#56)
See #53
2019-01-08 12:02:59 +01:00
Julien Fontanet
0c66c39211 WiP: chore: use app-conf (#58)
chore: use app-conf

Fixes #54
2018-12-14 14:44:09 +01:00
Julien Fontanet
250afa38ca chore(package): lint-staged integration 2018-12-12 11:25:47 +01:00
Julien Fontanet
b7e58eeb3f chore(eslint): use plugin:node/recommended 2018-12-12 11:17:42 +01:00
Julien Fontanet
6f024d78a6 chore(package): update dependencies 2018-12-12 10:58:06 +01:00
Julien Fontanet
1e48096f36 chore(package): remove incorrect posttest script 2018-12-07 09:48:35 +01:00
Julien Fontanet
ccf6a1bedb chore(package): ESLint on commit 2018-12-04 11:22:12 +01:00
Julien Fontanet
3639edb4db chore(jest): some config 2018-12-04 11:20:47 +01:00
badrAZ
d3bbe0b3b6 chore: move old tests to a dedicated directory (#52) 2018-11-29 16:10:14 +01:00
Julien Fontanet
e8ab101993 chore: update dependencies 2018-11-26 15:59:47 +01:00
badrAZ
ef98b10063 feat: PCI (#47) 2017-03-10 16:13:54 +01:00
badrAZ
84943e7fe6 feat: VM insert/eject CD (#34) 2017-03-10 16:10:08 +01:00
badrAZ
d0fa5ff385 feat: snapshotting (#40) 2017-03-10 16:09:31 +01:00
badrAZ
3609559ced feat: VM lifecycle (#36) 2017-03-10 16:08:11 +01:00
badrAZ
950c780122 fix(package): register jasmine as a global (#51) 2017-03-10 14:56:28 +01:00
greenkeeper[bot]
32b510ef40 fix(package): update husky to version 0.13.2 (#50)
https://greenkeeper.io/
2017-03-02 22:04:59 +01:00
greenkeeper[bot]
4cc33ed29b fix(package): update standard to version 9.0.0 (#49)
https://greenkeeper.io/
2017-03-01 00:19:00 +01:00
badrAZ
d72906a6ba fix(package): fix for Jest 19 (#46) 2017-02-21 11:12:28 +01:00
greenkeeper[bot]
d577b51a86 fix(package): update jest to version 19.0.0 (#45)
https://greenkeeper.io/
2017-02-21 10:54:43 +01:00
badrAZ
63d4865427 fix: server tests (#31) 2017-01-13 14:35:16 +01:00
badrAZ
1355477e37 chore(user): use rejectionOf() (#32) 2017-01-13 11:30:50 +01:00
badrAZ
d50e1b4e02 fix: role tests (#30) 2017-01-12 17:07:29 +01:00
badrAZ
606ae41698 fix(user): use utils/testConnection (#29) 2017-01-12 14:20:54 +01:00
badrAZ
b6ee5ae779 fix: token tests (#28) 2017-01-12 13:58:12 +01:00
badrAZ
aeb1b2c30f fix: group tests (#26) 2017-01-11 15:50:51 +01:00
badrAZ
35ace281cc fix: user tests (#27) 2017-01-11 11:47:59 +01:00
badrAZ
6cd056eee5 fix: user tests (#25) 2017-01-02 13:42:49 +01:00
Julien Fontanet
6c664bfaa7 chore: update yarn.lock 2016-12-23 17:52:47 +01:00
Julien Fontanet
8890d445dc feat(utils): export xo connection 2016-12-23 17:51:17 +01:00
Julien Fontanet
7a7db1ea08 fix: various fixes 2016-12-23 11:55:38 +01:00
Julien Fontanet
e585a3e5c4 chore(package): update all dependencies 2016-12-22 17:20:21 +01:00
greenkeeper[bot]
7336032009 chore(package): update dependencies (#24)
https://greenkeeper.io/
2016-11-28 15:27:36 +01:00
Greenkeeper
d29bc63b24 chore: drop support for Node.js 0.10 (#22)
BREAKING CHANGE: This module no longer supports Node.js 0.10
2016-11-01 15:59:11 +01:00
Greenkeeper
2a9bd1d4cb chore(package): update babel-eslint to version 7.0.0 (#6)
https://greenkeeper.io/
2016-09-27 23:27:01 +02:00
Greenkeeper
6578c14292 chore(package): update dependencies (#2)
https://greenkeeper.io/
2016-09-22 07:49:04 +02:00
Julien Fontanet
ceee93883f Remove only specifiers. 2015-08-26 15:26:21 +02:00
Julien Fontanet
dae8fd2370 Remove console.log(). 2015-08-26 15:16:44 +02:00
Julien Fontanet
48f8322390 Coding style fixes. 2015-08-26 14:38:26 +02:00
Julien Fontanet
7df833bd9f Upgrade standard & babel-eslint. 2015-08-26 14:32:47 +02:00
Varchar38
2d639e191a Add functions on host 2015-08-12 16:33:26 +02:00
Varchar38
db758c6806 Add test on vm 2015-08-12 16:33:12 +02:00
Varchar38
6822e4ac0c Add function on disk 2015-08-11 10:08:03 +02:00
Varchar38
14b1b07ecd Add test on server.spec 2015-08-11 10:06:41 +02:00
Varchar38
3c71a20bb2 Add tests and move functions to util 2015-07-29 15:25:33 +02:00
Varchar38
8f73619ba1 Add tests on job and schedule 2015-07-28 17:09:09 +02:00
Varchar38
0ee6e5a35f Tests job.spec 2015-07-27 17:13:25 +02:00
Varchar38
22692757e6 Add test on server.spec 2015-07-24 11:00:35 +02:00
Varchar38
ed9584270d Add getIsoId 2015-07-24 10:59:46 +02:00
Varchar38
5a5c35a1c9 Add getPoolId 2015-07-24 10:45:35 +02:00
Varchar38
1f842e4fe4 Add tests 2015-07-22 17:08:56 +02:00
Varchar38
9275c4a6d6 getMainConnection 2015-07-21 11:49:03 +02:00
Varchar38
9c7e61cbf3 Reduce timeout 2015-07-17 17:13:22 +02:00
Varchar38
69a6066fd8 Add vif's tests 2015-07-16 15:36:13 +02:00
Varchar38
47d2d09e50 Add tests on pool.spec and vif.spec 2015-07-15 17:22:52 +02:00
Varchar38
da648e0a78 Move a function from vm.spc to util 2015-07-15 17:22:31 +02:00
Varchar38
9e1c526d51 Reorganization of user.spec and group.spec's code 2015-07-15 10:42:23 +02:00
Varchar38
d81998f91c Add test on VM.spec 2015-07-13 16:22:06 +02:00
Varchar38
a717d9b8f3 Reorganization of server.spec and host.spec code 2015-07-13 16:21:46 +02:00
Varchar38
31d1243a14 Add tests on vbd.spec 2015-07-13 15:00:07 +02:00
Varchar38
2424222964 Add vm.migrate test 2015-07-09 15:49:26 +02:00
Varchar38
370b245d65 Add test to vm.spec 2015-07-08 16:42:40 +02:00
Varchar38
c4dfcc27e3 Add tests and reorganization of vm.spec code 2015-07-07 16:39:11 +02:00
Varchar38
dfa870a777 Add vm test 2015-07-06 17:51:31 +02:00
Varchar38
572375fff4 Add test on vm.spec 2015-07-03 14:52:59 +02:00
Varchar38
ed1caee9f8 Change host IP 2015-07-03 13:13:34 +02:00
Varchar38
6f7757c81b Tests vbd.spec 2015-07-03 13:13:14 +02:00
Varchar38
4c92965313 Reorganization host.spec code 2015-07-02 10:41:45 +02:00
Varchar38
bbce96eb67 Add pvVm to getConfig 2015-06-30 17:06:20 +02:00
Varchar38
e3cb7bd4c7 Reorganization disk.spec code 2015-06-30 16:07:38 +02:00
Varchar38
79599bf831 Add test on VM 2015-06-26 16:56:15 +02:00
Varchar38
1ab67bc225 Tests disk 2015-06-26 10:58:03 +02:00
Varchar38
37df213771 Tests VM 2015-06-24 17:27:18 +02:00
Varchar38
d48ffdb14f Add getConfig 2015-06-24 17:26:44 +02:00
Varchar38
766cdc9f59 Tests group (delete group) 2015-06-24 17:25:54 +02:00
Varchar38
21a40c9d14 Add tests on VM 2015-06-23 14:02:33 +02:00
Varchar38
9275e9d006 console.log delete 2015-06-22 17:15:09 +02:00
Varchar38
ef9fe025e0 Tests VM 2015-06-22 17:14:03 +02:00
Varchar38
05694a8cda Delete items created for test modified 2015-06-19 17:21:40 +02:00
Varchar38
e6304cb028 Variables nerver used in comment 2015-06-19 17:20:46 +02:00
Varchar38
b2d00784a4 Add missing event-to-promise 2015-06-18 17:04:48 +02:00
Varchar38
ae31ebdc33 Tests host and vm 2015-06-18 17:03:48 +02:00
Varchar38
a2d50b380f Modified presentation in token.spec 2015-06-18 17:03:23 +02:00
Varchar38
654e8fd13f Add waitObjet function 2015-06-18 17:02:30 +02:00
Varchar38
bcd44e4b2d Tests for disk and docker 2015-06-18 16:58:01 +02:00
Varchar38
5200793744 Test host 2015-06-17 17:22:28 +02:00
Varchar38
abcb29391c Read me 2015-06-17 15:04:06 +02:00
Varchar38
6a682dc143 Tests server 2015-06-17 14:07:48 +02:00
Varchar38
d93d30537f Coding style fix 2015-06-16 16:00:04 +02:00
Varchar38
377e88ff36 Code mutualisation 2015-06-16 15:58:17 +02:00
Varchar38
1733290c02 Tests role 2015-06-16 11:10:22 +02:00
Varchar38
e702ccc48a Tests for groups 2015-06-16 10:37:26 +02:00
Varchar38
ba729c493b Tests for token.*() 2015-06-15 13:20:07 +02:00
Varchar38
1c55950b7e Fix tests on Windows. 2015-06-15 13:19:43 +02:00
Olivier Lambert
18c8282bac initial work on VM tests 2015-06-12 21:37:45 +02:00
Julien Fontanet
1d20456853 Initial test suite for vm.*(). 2015-06-12 20:54:42 +02:00
Varchar38
7e32d0ae10 More tests for user.*(). 2015-06-12 17:32:07 +02:00
Julien Fontanet
5d33e45eae Spec for another test. 2015-06-12 11:37:31 +02:00
Julien Fontanet
1590930ef9 First test! 2015-06-12 11:34:59 +02:00
Julien Fontanet
8186d34f4e Initial commit. 2015-06-11 11:01:46 +02:00
198 changed files with 11506 additions and 3367 deletions

View File

@@ -35,6 +35,9 @@ module.exports = {
},
},
rules: {
// disabled because XAPI objects are using camel case
camelcase: ['off'],
'no-console': ['error', { allow: ['warn', 'error'] }],
'no-var': 'error',
'node/no-extraneous-import': 'error',

View File

@@ -16,7 +16,7 @@
},
"dependencies": {
"golike-defer": "^0.4.1",
"xen-api": "^0.25.1"
"xen-api": "^0.27.1"
},
"scripts": {
"postversion": "npm publish"

View File

@@ -1,6 +1,6 @@
{
"name": "@xen-orchestra/fs",
"version": "0.9.0",
"version": "0.10.1",
"license": "AGPL-3.0",
"description": "The File System for Xen Orchestra backups.",
"keywords": [],
@@ -21,15 +21,16 @@
"node": ">=6"
},
"dependencies": {
"@marsaud/smb2": "^0.13.0",
"@marsaud/smb2": "^0.14.0",
"@sindresorhus/df": "^2.1.0",
"@xen-orchestra/async-map": "^0.0.0",
"decorator-synchronized": "^0.5.0",
"execa": "^1.0.0",
"fs-extra": "^7.0.0",
"fs-extra": "^8.0.1",
"get-stream": "^4.0.0",
"limit-concurrency-decorator": "^0.4.0",
"lodash": "^4.17.4",
"promise-toolbox": "^0.12.1",
"promise-toolbox": "^0.13.0",
"readable-stream": "^3.0.6",
"through2": "^3.0.0",
"tmp": "^0.1.0",
@@ -40,12 +41,13 @@
"@babel/core": "^7.0.0",
"@babel/plugin-proposal-decorators": "^7.1.6",
"@babel/plugin-proposal-function-bind": "^7.0.0",
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.4.4",
"@babel/preset-env": "^7.0.0",
"@babel/preset-flow": "^7.0.0",
"async-iterator-to-stream": "^1.1.0",
"babel-plugin-lodash": "^3.3.2",
"cross-env": "^5.1.3",
"dotenv": "^7.0.0",
"dotenv": "^8.0.0",
"index-modules": "^0.3.0",
"rimraf": "^2.6.2"
},

View File

@@ -4,6 +4,7 @@
import getStream from 'get-stream'
import asyncMap from '@xen-orchestra/async-map'
import limit from 'limit-concurrency-decorator'
import path from 'path'
import synchronized from 'decorator-synchronized'
import { fromCallback, fromEvent, ignoreErrors, timeout } from 'promise-toolbox'
@@ -31,6 +32,7 @@ const computeRate = (hrtime: number[], size: number) => {
}
const DEFAULT_TIMEOUT = 6e5 // 10 min
const DEFAULT_MAX_PARALLEL_OPERATIONS = 10
const ignoreEnoent = error => {
if (error == null || error.code !== 'ENOENT') {
@@ -83,6 +85,25 @@ export default class RemoteHandlerAbstract {
}
}
;({ timeout: this._timeout = DEFAULT_TIMEOUT } = options)
const sharedLimit = limit(
options.maxParallelOperations ?? DEFAULT_MAX_PARALLEL_OPERATIONS
)
this.closeFile = sharedLimit(this.closeFile)
this.getInfo = sharedLimit(this.getInfo)
this.getSize = sharedLimit(this.getSize)
this.list = sharedLimit(this.list)
this.mkdir = sharedLimit(this.mkdir)
this.openFile = sharedLimit(this.openFile)
this.outputFile = sharedLimit(this.outputFile)
this.read = sharedLimit(this.read)
this.readFile = sharedLimit(this.readFile)
this.rename = sharedLimit(this.rename)
this.rmdir = sharedLimit(this.rmdir)
this.truncate = sharedLimit(this.truncate)
this.unlink = sharedLimit(this.unlink)
this.write = sharedLimit(this.write)
this.writeFile = sharedLimit(this.writeFile)
}
// Public members
@@ -400,6 +421,10 @@ export default class RemoteHandlerAbstract {
}
}
async truncate(file: string, len: number): Promise<void> {
await this._truncate(file, len)
}
async unlink(file: string, { checksum = true }: Object = {}): Promise<void> {
file = normalizePath(file)

View File

@@ -350,5 +350,30 @@ handlers.forEach(url => {
}
)
})
describe('#truncate()', () => {
forOwn(
{
'shrinks file': (() => {
const length = random(0, TEST_DATA_LEN)
const expected = TEST_DATA.slice(0, length)
return { length, expected }
})(),
'grows file': (() => {
const length = random(TEST_DATA_LEN, TEST_DATA_LEN * 2)
const expected = Buffer.alloc(length)
TEST_DATA.copy(expected)
return { length, expected }
})(),
},
({ length, expected }, title) => {
it(title, async () => {
await handler.outputFile('file', TEST_DATA)
await handler.truncate('file', length)
await expect(await handler.readFile('file')).toEqual(expected)
})
}
)
})
})
})

View File

@@ -106,6 +106,10 @@ export default class LocalHandler extends RemoteHandlerAbstract {
await fs.access(path, fs.R_OK | fs.W_OK)
}
_truncate(file, len) {
return fs.truncate(this._getFilePath(file), len)
}
async _unlink(file) {
return fs.unlink(this._getFilePath(file))
}

View File

@@ -155,6 +155,12 @@ export default class SmbHandler extends RemoteHandlerAbstract {
return this.list('.')
}
_truncate(file, len) {
return this._client
.truncate(this._getFilePath(file), len)
.catch(normalizeError)
}
_unlink(file) {
return this._client.unlink(this._getFilePath(file)).catch(normalizeError)
}

View File

@@ -24,6 +24,19 @@ log.info('this information is relevant to the user')
log.warn('something went wrong but did not prevent current action')
log.error('something went wrong')
log.fatal('service/app is going down')
// you can add contextual info
log.debug('new API request', {
method: 'foo',
params: [ 'bar', 'baz' ]
user: 'qux'
})
// by convention, errors go into the `error` field
log.error('could not join server', {
error,
server: 'example.org',
})
```
Then, at application level, configure the logs are handled:

View File

@@ -31,7 +31,7 @@
},
"dependencies": {
"lodash": "^4.17.4",
"promise-toolbox": "^0.12.1"
"promise-toolbox": "^0.13.0"
},
"devDependencies": {
"@babel/cli": "^7.0.0",

View File

@@ -1,10 +1,12 @@
import LEVELS, { NAMES } from '../levels'
// Bind console methods (necessary for browsers)
/* eslint-disable no-console */
const debugConsole = console.log.bind(console)
const infoConsole = console.info.bind(console)
const warnConsole = console.warn.bind(console)
const errorConsole = console.error.bind(console)
/* eslint-enable no-console */
const { ERROR, INFO, WARN } = LEVELS

View File

@@ -1,7 +1,6 @@
import fromCallback from 'promise-toolbox/fromCallback'
import splitHost from 'split-host' // eslint-disable-line node/no-extraneous-import node/no-missing-import
import startsWith from 'lodash/startsWith'
import { createClient, Facility, Severity, Transport } from 'syslog-client' // eslint-disable-line node/no-extraneous-import node/no-missing-import
import splitHost from 'split-host'
import { createClient, Facility, Severity, Transport } from 'syslog-client'
import LEVELS from '../levels'
@@ -19,10 +18,10 @@ const facility = Facility.User
export default target => {
const opts = {}
if (target !== undefined) {
if (startsWith(target, 'tcp://')) {
if (target.startsWith('tcp://')) {
target = target.slice(6)
opts.transport = Transport.Tcp
} else if (startsWith(target, 'udp://')) {
} else if (target.startsWith('udp://')) {
target = target.slice(6)
opts.transport = Transport.Udp
}

View File

@@ -1,5 +1,116 @@
# ChangeLog
## **next**
### Enhancements
- [Stats] Ability to display last day stats [#4160](https://github.com/vatesfr/xen-orchestra/issues/4160) (PR [#4168](https://github.com/vatesfr/xen-orchestra/pull/4168))
- [Settings/servers] Display servers connection issues [#4300](https://github.com/vatesfr/xen-orchestra/issues/4300) (PR [#4310](https://github.com/vatesfr/xen-orchestra/pull/4310))
- [VM] Permission to revert to any snapshot for VM operators [#3928](https://github.com/vatesfr/xen-orchestra/issues/3928) (PR [#4247](https://github.com/vatesfr/xen-orchestra/pull/4247))
- [VM] Show current operations and progress [#3811](https://github.com/vatesfr/xen-orchestra/issues/3811) (PR [#3982](https://github.com/vatesfr/xen-orchestra/pull/3982))
- [SR/General] Improve SR usage graph [#3608](https://github.com/vatesfr/xen-orchestra/issues/3608) (PR [#3830](https://github.com/vatesfr/xen-orchestra/pull/3830))
- [Backup NG/New] Generate default schedule if no schedule is specified [#4036](https://github.com/vatesfr/xen-orchestra/issues/4036) (PR [#4183](https://github.com/vatesfr/xen-orchestra/pull/4183))
- [Host/Advanced] Ability to edit iSCSI IQN [#4048](https://github.com/vatesfr/xen-orchestra/issues/4048) (PR [#4208](https://github.com/vatesfr/xen-orchestra/pull/4208))
- [Backup NG] Ability to bypass unhealthy VDI chains check [#4324](https://github.com/vatesfr/xen-orchestra/issues/4324) (PR [#4340](https://github.com/vatesfr/xen-orchestra/pull/4340))
- [Pool] Ability to add multiple hosts on the pool [#2402](https://github.com/vatesfr/xen-orchestra/issues/2402) (PR [#3716](https://github.com/vatesfr/xen-orchestra/pull/3716))
- [VM/console] Multiline copy/pasting [#4261](https://github.com/vatesfr/xen-orchestra/issues/4261) (PR [#4341](https://github.com/vatesfr/xen-orchestra/pull/4341))
- [VM,host] Improved state icons/pills (colors and tooltips) (PR [#4363](https://github.com/vatesfr/xen-orchestra/pull/4363))
### Bug fixes
- [Settings/Servers] Fix read-only setting toggling
- [SDN Controller] Do not choose physical PIF without IP configuration for tunnels. (PR [#4319](https://github.com/vatesfr/xen-orchestra/pull/4319))
- [Xen servers] Fix `no connection found for object` error if pool master is reinstalled [#4299](https://github.com/vatesfr/xen-orchestra/issues/4299) (PR [#4302](https://github.com/vatesfr/xen-orchestra/pull/4302))
- [Backup-ng/restore] Display correct size for full VM backup [#4316](https://github.com/vatesfr/xen-orchestra/issues/4316) (PR [#4332](https://github.com/vatesfr/xen-orchestra/pull/4332))
- [VM/tab-advanced] Fix CPU limits edition (PR [#4337](https://github.com/vatesfr/xen-orchestra/pull/4337))
- [Remotes] Fix `EIO` errors due to massive parallel fs operations [#4323](https://github.com/vatesfr/xen-orchestra/issues/4323) (PR [#4330](https://github.com/vatesfr/xen-orchestra/pull/4330))
- [VM/Advanced] Fix virtualization mode switch (PV/HVM) (PR [#4349](https://github.com/vatesfr/xen-orchestra/pull/4349))
- [Task] fix hidden notification by search field [#3874](https://github.com/vatesfr/xen-orchestra/issues/3874) (PR [#4305](https://github.com/vatesfr/xen-orchestra/pull/4305)
- [VM] Fail to change affinity (PR [#4361](https://github.com/vatesfr/xen-orchestra/pull/4361)
- [VM] Number of CPUs not correctly changed on running VMs (PR [#4360](https://github.com/vatesfr/xen-orchestra/pull/4360)
### Released packages
- @xen-orchestra/fs v0.10.1
- xo-server-sdn-controller v0.1.1
- xen-api v0.27.1
- xo-server v5.46.0
- xo-web v5.46.0
## **5.36.0** (2019-06-27)
![Channel: latest](https://badgen.net/badge/channel/latest/yellow)
### Highlights
- [SR/new] Create ZFS storage [#4260](https://github.com/vatesfr/xen-orchestra/issues/4260) (PR [#4266](https://github.com/vatesfr/xen-orchestra/pull/4266))
- [Host/advanced] Fix host CPU hyperthreading detection [#4262](https://github.com/vatesfr/xen-orchestra/issues/4262) (PR [#4285](https://github.com/vatesfr/xen-orchestra/pull/4285))
- [VM/Advanced] Ability to use UEFI instead of BIOS [#4264](https://github.com/vatesfr/xen-orchestra/issues/4264) (PR [#4268](https://github.com/vatesfr/xen-orchestra/pull/4268))
- [Backup-ng/restore] Display size for full VM backup [#4009](https://github.com/vatesfr/xen-orchestra/issues/4009) (PR [#4245](https://github.com/vatesfr/xen-orchestra/pull/4245))
- [Sr/new] Ability to select NFS version when creating NFS storage [#3951](https://github.com/vatesfr/xen-orchestra/issues/3951) (PR [#4277](https://github.com/vatesfr/xen-orchestra/pull/4277))
- [Host/storages, SR/hosts] Display PBD details [#4264](https://github.com/vatesfr/xen-orchestra/issues/4161) (PR [#4268](https://github.com/vatesfr/xen-orchestra/pull/4284))
- [auth-saml] Improve compatibility with Microsoft Azure Active Directory (PR [#4294](https://github.com/vatesfr/xen-orchestra/pull/4294))
### Enhancements
- [Host] Display warning when "Citrix Hypervisor" license has restrictions [#4251](https://github.com/vatesfr/xen-orchestra/issues/4164) (PR [#4235](https://github.com/vatesfr/xen-orchestra/pull/4279))
- [VM/Backup] Create backup bulk action [#2573](https://github.com/vatesfr/xen-orchestra/issues/2573) (PR [#4257](https://github.com/vatesfr/xen-orchestra/pull/4257))
- [Host] Display warning when host's time differs too much from XOA's time [#4113](https://github.com/vatesfr/xen-orchestra/issues/4113) (PR [#4173](https://github.com/vatesfr/xen-orchestra/pull/4173))
- [VM/network] Display and set bandwidth rate-limit of a VIF [#4215](https://github.com/vatesfr/xen-orchestra/issues/4215) (PR [#4293](https://github.com/vatesfr/xen-orchestra/pull/4293))
- [SDN Controller] New plugin which enables creating pool-wide private networks [xcp-ng/xcp#175](https://github.com/xcp-ng/xcp/issues/175) (PR [#4269](https://github.com/vatesfr/xen-orchestra/pull/4269))
### Bug fixes
- [XOA] Don't require editing the _email_ field in case of re-registration (PR [#4259](https://github.com/vatesfr/xen-orchestra/pull/4259))
- [Metadata backup] Missing XAPIs should trigger a failure job [#4281](https://github.com/vatesfr/xen-orchestra/issues/4281) (PR [#4283](https://github.com/vatesfr/xen-orchestra/pull/4283))
- [iSCSI] Fix fibre channel paths display [#4291](https://github.com/vatesfr/xen-orchestra/issues/4291) (PR [#4303](https://github.com/vatesfr/xen-orchestra/pull/4303))
- [New VM] Fix tooltips not displayed on disabled elements in some browsers (e.g. Google Chrome) [#4304](https://github.com/vatesfr/xen-orchestra/issues/4304) (PR [#4309](https://github.com/vatesfr/xen-orchestra/pull/4309))
### Released packages
- xo-server-auth-ldap v0.6.5
- xen-api v0.26.0
- xo-server-sdn-controller v0.1
- xo-server-auth-saml v0.6.0
- xo-server-backup-reports v0.16.2
- xo-server v5.44.0
- xo-web v5.44.0
## **5.35.0** (2019-05-29)
![Channel: stable](https://badgen.net/badge/channel/stable/green)
### Enhancements
- [VM/general] Display 'Started... ago' instead of 'Halted... ago' for paused state [#3750](https://github.com/vatesfr/xen-orchestra/issues/3750) (PR [#4170](https://github.com/vatesfr/xen-orchestra/pull/4170))
- [Metadata backup] Ability to define when the backup report will be sent (PR [#4149](https://github.com/vatesfr/xen-orchestra/pull/4149))
- [XOA/Update] Ability to select release channel [#4200](https://github.com/vatesfr/xen-orchestra/issues/4200) (PR [#4202](https://github.com/vatesfr/xen-orchestra/pull/4202))
- [User] Forget connection tokens on password change or on demand [#4214](https://github.com/vatesfr/xen-orchestra/issues/4214) (PR [#4224](https://github.com/vatesfr/xen-orchestra/pull/4224))
- [Settings/Logs] LICENCE_RESTRICTION errors: suggest XCP-ng as an Open Source alternative [#3876](https://github.com/vatesfr/xen-orchestra/issues/3876) (PR [#4238](https://github.com/vatesfr/xen-orchestra/pull/4238))
- [VM/Migrate] Display VDI size on migrate modal [#2534](https://github.com/vatesfr/xen-orchestra/issues/2534) (PR [#4250](https://github.com/vatesfr/xen-orchestra/pull/4250))
- [Host] Display hyperthreading status on advanced tab [#4262](https://github.com/vatesfr/xen-orchestra/issues/4262) (PR [#4263](https://github.com/vatesfr/xen-orchestra/pull/4263))
### Bug fixes
- [Pool/Patches] Fix "an error has occurred" in "Applied patches" [#4192](https://github.com/vatesfr/xen-orchestra/issues/4192) (PR [#4193](https://github.com/vatesfr/xen-orchestra/pull/4193))
- [Backup NG] Fix report sent even though "Never" is selected [#4092](https://github.com/vatesfr/xen-orchestra/issues/4092) (PR [#4178](https://github.com/vatesfr/xen-orchestra/pull/4178))
- [Remotes] Fix issues after a config import (PR [#4197](https://github.com/vatesfr/xen-orchestra/pull/4197))
- [Charts] Fixed the chart lines sometimes changing order/color (PR [#4221](https://github.com/vatesfr/xen-orchestra/pull/4221))
- Prevent non-admin users to access admin pages with URL (PR [#4220](https://github.com/vatesfr/xen-orchestra/pull/4220))
- [Upgrade] Fix alert before upgrade while running backup jobs [#4164](https://github.com/vatesfr/xen-orchestra/issues/4164) (PR [#4235](https://github.com/vatesfr/xen-orchestra/pull/4235))
- [Import] Fix import OVA files (PR [#4232](https://github.com/vatesfr/xen-orchestra/pull/4232))
- [VM/network] Fix duplicate IPv4 (PR [#4239](https://github.com/vatesfr/xen-orchestra/pull/4239))
- [Remotes] Fix disconnected remotes which may appear to work
- [Host] Fix incorrect hypervisor name [#4246](https://github.com/vatesfr/xen-orchestra/issues/4246) (PR [#4248](https://github.com/vatesfr/xen-orchestra/pull/4248))
### Released packages
- xo-server-backup-reports v0.16.1
- @xen-orchestra/fs v0.9.0
- vhd-lib v0.7.0
- xo-server v5.42.1
- xo-web v5.42.1
## **5.34.0** (2019-04-30)
### Highlights

View File

@@ -1,17 +1,27 @@
> This file contains all changes that have not been released yet.
>
> Keep in mind the changelog is addressed to **users** and should be
> understandable by them.
### Enhancements
> Users must be able to say: “Nice enhancement, I'm eager to test it”
- [VM/copy] Only show zstd option when it's supported [#3892](https://github.com/vatesfr/xen-orchestra/issues/3892) (PR [#4326](https://github.com/vatesfr/xen-orchestra/pull/4326))
### Bug fixes
- [Pool/Patches] Fix "an error has occurred" in "Applied patches" [#4192](https://github.com/vatesfr/xen-orchestra/issues/4192) (PR [#4193](https://github.com/vatesfr/xen-orchestra/pull/4193))
- [Backup NG] Fix report sent even though "Never" is selected [#4092](https://github.com/vatesfr/xen-orchestra/issues/4092) (PR [#4178](https://github.com/vatesfr/xen-orchestra/pull/4178))
- [Remotes] Fix issues after a config import (PR [#4197](https://github.com/vatesfr/xen-orchestra/pull/4197))
> Users must be able to say: “I had this issue, happy to know it's fixed”
- [SDN Controller] Better detect host shutting down to adapt network topology (PR [#4314](https://github.com/vatesfr/xen-orchestra/pull/4314))
### Released packages
- xo-server-backup-reports v0.16.1
- @xen-orchestra/fs v0.9.0
- vhd-lib v0.7.0
- xo-server v5.41.0
- xo-web v5.41.0
> Packages will be released in the order they are here, therefore, they should
> be listed by inverse order of dependency.
>
> Rule of thumb: add packages on top.
- xo-server-sdn-controller v0.1.2
- xo-server v5.47.0
- xo-web v5.47.0

View File

@@ -14,5 +14,5 @@
1. create a PR as soon as possible
1. mark it as `WiP:` (Work in Progress) if not ready to be merged
1. when you want a review, add a reviewer
1. when you want a review, add a reviewer (and only one)
1. if necessary, update your PR, and re- add a reviewer

View File

@@ -6,24 +6,24 @@
"babel-eslint": "^10.0.1",
"babel-jest": "^24.1.0",
"benchmark": "^2.1.4",
"eslint": "^5.1.0",
"eslint-config-prettier": "^4.1.0",
"eslint": "^6.0.1",
"eslint-config-prettier": "^6.0.0",
"eslint-config-standard": "12.0.0",
"eslint-config-standard-jsx": "^6.0.2",
"eslint-plugin-eslint-comments": "^3.1.1",
"eslint-plugin-import": "^2.8.0",
"eslint-plugin-node": "^8.0.0",
"eslint-plugin-node": "^9.0.1",
"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.98.0",
"globby": "^9.0.0",
"husky": "^2.2.0",
"flow-bin": "^0.102.0",
"globby": "^10.0.0",
"husky": "^3.0.0",
"jest": "^24.1.0",
"lodash": "^4.17.4",
"prettier": "^1.10.2",
"promise-toolbox": "^0.12.1",
"promise-toolbox": "^0.13.0",
"sorted-object": "^2.0.1"
},
"engines": {

View File

@@ -27,7 +27,7 @@
"node": ">=6"
},
"dependencies": {
"@xen-orchestra/fs": "^0.9.0",
"@xen-orchestra/fs": "^0.10.1",
"cli-progress": "^2.0.0",
"exec-promise": "^0.7.0",
"getopts": "^2.2.3",
@@ -40,9 +40,9 @@
"@babel/preset-env": "^7.0.0",
"babel-plugin-lodash": "^3.3.2",
"cross-env": "^5.1.3",
"execa": "^1.0.0",
"execa": "^2.0.2",
"index-modules": "^0.3.0",
"promise-toolbox": "^0.12.1",
"promise-toolbox": "^0.13.0",
"rimraf": "^2.6.1",
"tmp": "^0.1.0"
},

View File

@@ -24,9 +24,9 @@
"async-iterator-to-stream": "^1.0.2",
"core-js": "^3.0.0",
"from2": "^2.3.0",
"fs-extra": "^7.0.0",
"fs-extra": "^8.0.1",
"limit-concurrency-decorator": "^0.4.0",
"promise-toolbox": "^0.12.1",
"promise-toolbox": "^0.13.0",
"struct-fu": "^1.2.0",
"uuid": "^3.0.1"
},
@@ -35,12 +35,12 @@
"@babel/core": "^7.0.0",
"@babel/preset-env": "^7.0.0",
"@babel/preset-flow": "^7.0.0",
"@xen-orchestra/fs": "^0.9.0",
"@xen-orchestra/fs": "^0.10.1",
"babel-plugin-lodash": "^3.3.2",
"cross-env": "^5.1.3",
"execa": "^1.0.0",
"execa": "^2.0.2",
"fs-promise": "^2.0.0",
"get-stream": "^4.0.0",
"get-stream": "^5.1.0",
"index-modules": "^0.3.0",
"readable-stream": "^3.0.6",
"rimraf": "^2.6.2",

View File

@@ -364,9 +364,7 @@ export default class Vhd {
const offset = blockAddr + this.sectorsOfBitmap + beginSectorId
debug(
`writeBlockSectors at ${offset} block=${
block.id
}, sectors=${beginSectorId}...${endSectorId}`
`writeBlockSectors at ${offset} block=${block.id}, sectors=${beginSectorId}...${endSectorId}`
)
for (let i = beginSectorId; i < endSectorId; ++i) {

View File

@@ -41,7 +41,7 @@
"human-format": "^0.10.0",
"lodash": "^4.17.4",
"pw": "^0.0.4",
"xen-api": "^0.25.1"
"xen-api": "^0.27.1"
},
"devDependencies": {
"@babel/cli": "^7.1.5",

View File

@@ -82,7 +82,7 @@ console.log(xapi.pool.$master.$resident_VMs[0].name_label)
A CLI is provided to help exploration and discovery of the XAPI.
```
> xen-api https://xen1.company.net root
> xen-api xen1.company.net root
Password: ******
root@xen1.company.net> xapi.status
'connected'
@@ -92,6 +92,14 @@ root@xen1.company.net> xapi.pool.$master.name_label
'xen1'
```
You can optionally prefix the address by a protocol: `https://` (default) or `http://`.
In case of error due to invalid or self-signed certificates you can use the `--allow-unauthorized` flag (or `--au`):
```
> xen-api --au xen1.company.net root
```
To ease searches, `find()` and `findAll()` functions are available:
```

View File

@@ -1,6 +1,6 @@
{
"name": "xen-api",
"version": "0.25.1",
"version": "0.27.1",
"license": "ISC",
"description": "Connector to the Xen API",
"keywords": [
@@ -46,7 +46,7 @@
"make-error": "^1.3.0",
"minimist": "^1.2.0",
"ms": "^2.1.1",
"promise-toolbox": "^0.12.1",
"promise-toolbox": "^0.13.0",
"pw": "0.0.4",
"xmlrpc": "^1.3.2",
"xo-collection": "^0.4.1"

View File

@@ -99,6 +99,9 @@ export class Xapi extends EventEmitter {
this._sessionId = undefined
this._status = DISCONNECTED
this._watchEventsError = undefined
this._lastEventFetchedTimestamp = undefined
this._debounce = opts.debounce ?? 200
this._objects = new Collection()
this._objectsByRef = { __proto__: null }
@@ -168,22 +171,6 @@ export class Xapi extends EventEmitter {
try {
await this._sessionOpen()
// Uses introspection to list available types.
const types = (this._types = (await this._interruptOnDisconnect(
this._call('system.listMethods')
))
.filter(isGetAllRecordsMethod)
.map(method => method.slice(0, method.indexOf('.'))))
this._lcToTypes = { __proto__: null }
types.forEach(type => {
const lcType = type.toLowerCase()
if (lcType !== type) {
this._lcToTypes[lcType] = type
}
})
this._pool = (await this.getAllRecords('pool'))[0]
debug('%s: connected', this._humanId)
this._status = CONNECTED
this._resolveConnected()
@@ -495,6 +482,14 @@ export class Xapi extends EventEmitter {
return this._objectsFetched
}
get lastEventFetchedTimestamp() {
return this._lastEventFetchedTimestamp
}
get watchEventsError() {
return this._watchEventsError
}
// ensure we have received all events up to this call
//
// optionally returns the up to date object for the given ref
@@ -739,6 +734,28 @@ export class Xapi extends EventEmitter {
},
}
)
const oldPoolRef = this._pool?.$ref
this._pool = (await this.getAllRecords('pool'))[0]
// if the pool ref has changed, it means that the XAPI has been restarted or
// it's not the same XAPI, we need to refetch the available types and reset
// the event loop in that case
if (this._pool.$ref !== oldPoolRef) {
// Uses introspection to list available types.
const types = (this._types = (await this._interruptOnDisconnect(
this._call('system.listMethods')
))
.filter(isGetAllRecordsMethod)
.map(method => method.slice(0, method.indexOf('.'))))
this._lcToTypes = { __proto__: null }
types.forEach(type => {
const lcType = type.toLowerCase()
if (lcType !== type) {
this._lcToTypes[lcType] = type
}
})
}
}
_setUrl(url) {
@@ -936,21 +953,28 @@ export class Xapi extends EventEmitter {
let result
try {
result = await this._sessionCall(
// don't use _sessionCall because a session failure should break the
// loop and trigger a complete refetch
result = await this._call(
'event.from',
[
this._sessionId,
types,
fromToken,
EVENT_TIMEOUT + 0.1, // must be float for XML-RPC transport
],
EVENT_TIMEOUT * 1e3 * 1.1
)
this._lastEventFetchedTimestamp = Date.now()
this._watchEventsError = undefined
} catch (error) {
if (error?.code === 'EVENTS_LOST') {
const code = error?.code
if (code === 'EVENTS_LOST' || code === 'SESSION_INVALID') {
// eslint-disable-next-line no-labels
continue mainLoop
}
this._watchEventsError = error
console.warn('_watchEvents', error)
await pDelay(this._eventPollDelay)
continue
@@ -1059,9 +1083,14 @@ export class Xapi extends EventEmitter {
}
}
props[`add_to_${field}`] = function(...values) {
props[`add_${field}`] = function(value) {
return xapi
.call(`${type}.add_${field}`, this.$ref, values)
.call(`${type}.add_${field}`, this.$ref, value)
.then(noop)
}
props[`remove_${field}`] = function(value) {
return xapi
.call(`${type}.remove_${field}`, this.$ref, value)
.then(noop)
}
} else if (value !== null && typeof value === 'object') {

View File

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

View File

@@ -24,7 +24,6 @@ const nicePipe = require('nice-pipe')
const pairs = require('lodash/toPairs')
const pick = require('lodash/pick')
const pump = require('pump')
const startsWith = require('lodash/startsWith')
const prettyMs = require('pretty-ms')
const progressStream = require('progress-stream')
const pw = require('pw')
@@ -81,7 +80,7 @@ function parseParameters(args) {
const name = matches[1]
let value = matches[2]
if (startsWith(value, 'json:')) {
if (value.startsWith('json:')) {
value = JSON.parse(value.slice(5))
}

View File

@@ -43,7 +43,7 @@
"xo-lib": "^0.9.0"
},
"devDependencies": {
"@types/node": "^11.11.4",
"@types/node": "^12.0.2",
"@types/through2": "^2.0.31",
"tslint": "^5.9.1",
"tslint-config-standard": "^8.0.1",

View File

@@ -1,6 +1,5 @@
import JsonRpcWebSocketClient, { OPEN, CLOSED } from 'jsonrpc-websocket-client'
import { BaseError } from 'make-error'
import { startsWith } from 'lodash'
// ===================================================================
@@ -35,7 +34,7 @@ export default class Xo extends JsonRpcWebSocketClient {
}
call(method, args, i) {
if (startsWith(method, 'session.')) {
if (method.startsWith('session.')) {
return Promise.reject(
new XoError('session.*() methods are disabled from this interface')
)

View File

@@ -1,6 +1,6 @@
{
"name": "xo-server-auth-ldap",
"version": "0.6.4",
"version": "0.6.5",
"license": "AGPL-3.0",
"description": "LDAP authentication plugin for XO-Server",
"keywords": [
@@ -39,7 +39,7 @@
"inquirer": "^6.0.0",
"ldapjs": "^1.0.1",
"lodash": "^4.17.4",
"promise-toolbox": "^0.12.1"
"promise-toolbox": "^0.13.0"
},
"devDependencies": {
"@babel/cli": "^7.0.0",

View File

@@ -230,10 +230,9 @@ class AuthLdap {
logger(`attempting to bind as ${entry.objectName}`)
await bind(entry.objectName, password)
logger(
`successfully bound as ${
entry.objectName
} => ${username} authenticated`
`successfully bound as ${entry.objectName} => ${username} authenticated`
)
logger(JSON.stringify(entry, null, 2))
return { username }
} catch (error) {
logger(`failed to bind as ${entry.objectName}: ${error.message}`)

View File

@@ -1,6 +1,6 @@
{
"name": "xo-server-auth-saml",
"version": "0.5.3",
"version": "0.6.0",
"license": "AGPL-3.0",
"description": "SAML authentication plugin for XO-Server",
"keywords": [
@@ -33,7 +33,7 @@
"node": ">=6"
},
"dependencies": {
"passport-saml": "^1.0.0"
"passport-saml": "^1.1.0"
},
"devDependencies": {
"@babel/cli": "^7.0.0",

View File

@@ -24,7 +24,10 @@ export const configurationSchema = {
},
usernameField: {
title: 'Username field',
description: 'Field to use as the XO username',
description: `Field to use as the XO username
You should try \`http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress\` if you are using Microsoft Azure Active Directory.
`,
type: 'string',
},
},

View File

@@ -1,6 +1,6 @@
{
"name": "xo-server-backup-reports",
"version": "0.16.1",
"version": "0.16.2",
"license": "AGPL-3.0",
"description": "Backup reports plugin for XO-Server",
"keywords": [

View File

@@ -142,12 +142,14 @@ const getErrorMarkdown = task => {
const MARKDOWN_BY_TYPE = {
pool(task, { formatDate }) {
const { pool, poolMaster = {} } = task.data
const { id, pool = {}, poolMaster = {} } = task.data
const name = pool.name_label || poolMaster.name_label || UNKNOWN_ITEM
return {
body: [
`- **UUID**: ${pool.uuid}`,
pool.uuid !== undefined
? `- **UUID**: ${pool.uuid}`
: `- **ID**: ${id}`,
...getTemporalDataMarkdown(task.end, task.start, formatDate),
getErrorMarkdown(task),
],
@@ -355,9 +357,7 @@ class BackupReportsXoPlugin {
nagiosStatus: log.status === 'success' ? 0 : 2,
nagiosMarkdown:
log.status === 'success'
? `[Xen Orchestra] [Success] Metadata backup report for ${
log.jobName
}`
? `[Xen Orchestra] [Success] Metadata backup report for ${log.jobName}`
: `[Xen Orchestra] [${log.status}] Metadata backup report for ${
log.jobName
} - ${nagiosText.join(' ')}`,
@@ -391,9 +391,7 @@ class BackupReportsXoPlugin {
} Backup report for ${jobName} ${STATUS_ICON[log.status]}`,
markdown: toMarkdown(markdown),
nagiosStatus: 2,
nagiosMarkdown: `[Xen Orchestra] [${
log.status
}] Backup report for ${jobName} - Error : ${log.result.message}`,
nagiosMarkdown: `[Xen Orchestra] [${log.status}] Backup report for ${jobName} - Error : ${log.result.message}`,
})
}
@@ -711,9 +709,7 @@ class BackupReportsXoPlugin {
subject: `[Xen Orchestra] ${globalStatus} ${icon}`,
markdown,
nagiosStatus: 2,
nagiosMarkdown: `[Xen Orchestra] [${globalStatus}] Error : ${
error.message
}`,
nagiosMarkdown: `[Xen Orchestra] [${globalStatus}] Error : ${error.message}`,
})
}

View File

@@ -189,9 +189,7 @@ export default class DensityPlan extends Plan {
const { vm, destination } = move
const xapiDest = this.xo.getXapi(destination)
debug(
`Migrate VM (${vm.id}) to Host (${destination.id}) from Host (${
vm.$container
}).`
`Migrate VM (${vm.id}) to Host (${destination.id}) from Host (${vm.$container}).`
)
return xapiDest.migrateVm(
vm._xapiId,

View File

@@ -126,9 +126,7 @@ export default class PerformancePlan extends Plan {
destinationAverages.memoryFree -= vmAverages.memory
debug(
`Migrate VM (${vm.id}) to Host (${destination.id}) from Host (${
exceededHost.id
}).`
`Migrate VM (${vm.id}) to Host (${destination.id}) from Host (${exceededHost.id}).`
)
optimizationsCount++
@@ -143,9 +141,7 @@ export default class PerformancePlan extends Plan {
await Promise.all(promises)
debug(
`Performance mode: ${optimizationsCount} optimizations for Host (${
exceededHost.id
}).`
`Performance mode: ${optimizationsCount} optimizations for Host (${exceededHost.id}).`
)
}
}

View File

@@ -183,9 +183,7 @@ export const configurationSchema = {
description: Object.keys(HOST_FUNCTIONS)
.map(
k =>
` * ${k} (${HOST_FUNCTIONS[k].unit}): ${
HOST_FUNCTIONS[k].description
}`
` * ${k} (${HOST_FUNCTIONS[k].unit}): ${HOST_FUNCTIONS[k].description}`
)
.join('\n'),
type: 'string',
@@ -233,9 +231,7 @@ export const configurationSchema = {
description: Object.keys(VM_FUNCTIONS)
.map(
k =>
` * ${k} (${VM_FUNCTIONS[k].unit}): ${
VM_FUNCTIONS[k].description
}`
` * ${k} (${VM_FUNCTIONS[k].unit}): ${VM_FUNCTIONS[k].description}`
)
.join('\n'),
type: 'string',
@@ -284,9 +280,7 @@ export const configurationSchema = {
description: Object.keys(SR_FUNCTIONS)
.map(
k =>
` * ${k} (${SR_FUNCTIONS[k].unit}): ${
SR_FUNCTIONS[k].description
}`
` * ${k} (${SR_FUNCTIONS[k].unit}): ${SR_FUNCTIONS[k].description}`
)
.join('\n'),
type: 'string',
@@ -414,9 +408,7 @@ ${monitorBodies.join('\n')}`
}
_parseDefinition(definition) {
const alarmId = `${definition.objectType}|${definition.variableName}|${
definition.alarmTriggerLevel
}`
const alarmId = `${definition.objectType}|${definition.variableName}|${definition.alarmTriggerLevel}`
const typeFunction =
TYPE_FUNCTION_MAP[definition.objectType][definition.variableName]
const parseData = (result, uuid) => {
@@ -468,9 +460,7 @@ ${monitorBodies.join('\n')}`
...definition,
alarmId,
vmFunction: typeFunction,
title: `${typeFunction.name} ${definition.comparator} ${
definition.alarmTriggerLevel
}${typeFunction.unit}`,
title: `${typeFunction.name} ${definition.comparator} ${definition.alarmTriggerLevel}${typeFunction.unit}`,
snapshot: async () => {
return Promise.all(
map(definition.uuids, async uuid => {
@@ -664,9 +654,7 @@ ${entry.listItem}
subject: `[Xen Orchestra] Performance Alert ${subjectSuffix}`,
markdown:
markdownBody +
`\n\n\nSent from Xen Orchestra [perf-alert plugin](${
this._configuration.baseUrl
}#/settings/plugins)\n`,
`\n\n\nSent from Xen Orchestra [perf-alert plugin](${this._configuration.baseUrl}#/settings/plugins)\n`,
})
} else {
throw new Error('The email alert system has a configuration issue.')

View File

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

View File

@@ -0,0 +1,43 @@
# xo-server-sdn-controller [![Build Status](https://travis-ci.org/vatesfr/xen-orchestra.png?branch=master)](https://travis-ci.org/vatesfr/xen-orchestra)
XO Server plugin that allows the creation of pool-wide private networks.
## Install
For installing XO and the plugins from the sources, please take a look at [the documentation](https://xen-orchestra.com/docs/from_the_sources.html).
## Usage
### Network creation
In the network creation view, select a `pool` and `Private network`.
Create the network.
Choice is offer between `GRE` and `VxLAN`, if `VxLAN` is chosen, then the port 4789 must be open for UDP traffic.
The following line needs to be added, if not already present, in `/etc/sysconfig/iptables` of all the hosts where `VxLAN` is wanted:
`-A xapi-INPUT -p udp -m conntrack --ctstate NEW -m udp --dport 4789 -j ACCEPT`
### Configuration
Like all other xo-server plugins, it can be configured directly via
the web interface, see [the plugin documentation](https://xen-orchestra.com/docs/plugins.html).
The plugin's configuration contains:
- `cert-dir`: A path where to find the certificates to create SSL connections with the hosts.
If none is provided, the plugin will create its own self-signed certificates.
- `override-certs:` Whether or not to uninstall an already existing SDN controller CA certificate in order to replace it by the plugin's one.
## 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
AGPL3 © [Vates SAS](http://vates.fr)

View File

@@ -0,0 +1,36 @@
{
"name": "xo-server-sdn-controller",
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/packages/xo-server-sdn-controller",
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
"repository": {
"directory": "packages/xo-server-sdn-controller",
"type": "git",
"url": "https://github.com/vatesfr/xen-orchestra.git"
},
"main": "./dist",
"scripts": {
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
"prebuild": "rimraf dist/",
"predev": "yarn run prebuild",
"prepublishOnly": "yarn run build"
},
"version": "0.1.1",
"engines": {
"node": ">=6"
},
"devDependencies": {
"@babel/cli": "^7.4.4",
"@babel/core": "^7.4.4",
"@babel/plugin-proposal-optional-chaining": "^7.2.0",
"@babel/preset-env": "^7.4.4",
"cross-env": "^5.2.0"
},
"dependencies": {
"@xen-orchestra/log": "^0.1.4",
"lodash": "^4.17.11",
"node-openssl-cert": "^0.0.84",
"promise-toolbox": "^0.13.0"
},
"private": true
}

View File

@@ -0,0 +1,862 @@
import assert from 'assert'
import createLogger from '@xen-orchestra/log'
import NodeOpenssl from 'node-openssl-cert'
import { access, constants, readFile, writeFile } from 'fs'
import { EventEmitter } from 'events'
import { filter, find, forOwn, map } from 'lodash'
import { fromCallback, fromEvent } from 'promise-toolbox'
import { join } from 'path'
import { OvsdbClient } from './ovsdb-client'
// =============================================================================
const log = createLogger('xo:xo-server:sdn-controller')
const PROTOCOL = 'pssl'
const CA_CERT = 'ca-cert.pem'
const CLIENT_KEY = 'client-key.pem'
const CLIENT_CERT = 'client-cert.pem'
const SDN_CONTROLLER_CERT = 'sdn-controller-ca.pem'
const NB_DAYS = 9999
// =============================================================================
export const configurationSchema = {
type: 'object',
properties: {
'cert-dir': {
description: `Full path to a directory where to find: \`client-cert.pem\`,
\`client-key.pem\` and \`ca-cert.pem\` to create ssl connections with hosts.
If none is provided, the plugin will create its own self-signed certificates.`,
type: 'string',
},
'override-certs': {
description: `Replace already existing SDN controller CA certificate`,
type: 'boolean',
default: false,
},
},
}
// =============================================================================
async function fileWrite(path, data) {
await fromCallback(writeFile, path, data)
}
async function fileRead(path) {
const result = await fromCallback(readFile, path)
return result
}
async function fileExists(path) {
try {
await fromCallback(access, path, constants.F_OK)
} catch (error) {
if (error.code === 'ENOENT') {
return false
}
throw error
}
return true
}
// =============================================================================
class SDNController extends EventEmitter {
constructor({ xo, getDataDir }) {
super()
this._xo = xo
this._getDataDir = getDataDir
this._clientKey = null
this._clientCert = null
this._caCert = null
this._poolNetworks = []
this._ovsdbClients = []
this._newHosts = []
this._networks = new Map()
this._starCenters = new Map()
this._cleaners = []
this._objectsAdded = this._objectsAdded.bind(this)
this._objectsUpdated = this._objectsUpdated.bind(this)
this._overrideCerts = false
this._unsetApiMethod = null
}
// ---------------------------------------------------------------------------
async configure(configuration) {
this._overrideCerts = configuration['override-certs']
let certDirectory = configuration['cert-dir']
if (certDirectory == null) {
log.debug(`No cert-dir provided, using default self-signed certificates`)
certDirectory = await this._getDataDir()
if (!(await fileExists(join(certDirectory, CA_CERT)))) {
// If one certificate doesn't exist, none should
assert(
!(await fileExists(join(certDirectory, CLIENT_KEY))),
`${CLIENT_KEY} should not exist`
)
assert(
!(await fileExists(join(certDirectory, CLIENT_CERT))),
`${CLIENT_CERT} should not exist`
)
log.debug(`No default self-signed certificates exists, creating them`)
await this._generateCertificatesAndKey(certDirectory)
}
}
// TODO: verify certificates and create new certificates if needed
;[this._clientKey, this._clientCert, this._caCert] = await Promise.all([
fileRead(join(certDirectory, CLIENT_KEY)),
fileRead(join(certDirectory, CLIENT_CERT)),
fileRead(join(certDirectory, CA_CERT)),
])
this._ovsdbClients.forEach(client => {
client.updateCertificates(this._clientKey, this._clientCert, this._caCert)
})
const updatedPools = []
for (const poolNetwork of this._poolNetworks) {
if (updatedPools.includes(poolNetwork.pool)) {
continue
}
const xapi = this._xo.getXapi(poolNetwork.pool)
await this._installCaCertificateIfNeeded(xapi)
updatedPools.push(poolNetwork.pool)
}
}
async load() {
const createPrivateNetwork = this._createPrivateNetwork.bind(this)
createPrivateNetwork.description =
'Creates a pool-wide private network on a selected pool'
createPrivateNetwork.params = {
poolId: { type: 'string' },
networkName: { type: 'string' },
networkDescription: { type: 'string' },
encapsulation: { type: 'string' },
}
createPrivateNetwork.resolve = {
xoPool: ['poolId', 'pool', ''],
}
this._unsetApiMethod = this._xo.addApiMethod(
'plugin.SDNController.createPrivateNetwork',
createPrivateNetwork
)
// FIXME: we should monitor when xapis are added/removed
forOwn(this._xo.getAllXapis(), async xapi => {
await xapi.objectsFetched
if (this._setControllerNeeded(xapi) === false) {
this._cleaners.push(await this._manageXapi(xapi))
const hosts = filter(xapi.objects.all, { $type: 'host' })
await Promise.all(
map(hosts, async host => {
this._createOvsdbClient(host)
})
)
// Add already existing pool-wide private networks
const networks = filter(xapi.objects.all, { $type: 'network' })
forOwn(networks, async network => {
if (network.other_config.private_pool_wide === 'true') {
log.debug('Adding network to managed networks', {
network: network.name_label,
pool: network.$pool.name_label,
})
const center = await this._electNewCenter(network, true)
this._poolNetworks.push({
pool: network.$pool.$ref,
network: network.$ref,
starCenter: center?.$ref,
})
this._networks.set(network.$id, network.$ref)
if (center != null) {
this._starCenters.set(center.$id, center.$ref)
}
}
})
}
})
}
async unload() {
this._ovsdbClients = []
this._poolNetworks = []
this._newHosts = []
this._networks.clear()
this._starCenters.clear()
this._cleaners.forEach(cleaner => cleaner())
this._cleaners = []
this._unsetApiMethod()
}
// ===========================================================================
async _createPrivateNetwork({
xoPool,
networkName,
networkDescription,
encapsulation,
}) {
const pool = this._xo.getXapiObject(xoPool)
await this._setPoolControllerIfNeeded(pool)
// Create the private network
const privateNetworkRef = await pool.$xapi.call('network.create', {
name_label: networkName,
name_description: networkDescription,
MTU: 0,
other_config: {
automatic: 'false',
private_pool_wide: 'true',
encapsulation: encapsulation,
},
})
const privateNetwork = await pool.$xapi._getOrWaitObject(privateNetworkRef)
log.info('New private network created', {
network: privateNetwork.name_label,
pool: pool.name_label,
})
// For each pool's host, create a tunnel to the private network
const hosts = filter(pool.$xapi.objects.all, { $type: 'host' })
await Promise.all(
map(hosts, async host => {
await this._createTunnel(host, privateNetwork)
this._createOvsdbClient(host)
})
)
const center = await this._electNewCenter(privateNetwork, false)
this._poolNetworks.push({
pool: pool.$ref,
network: privateNetwork.$ref,
starCenter: center?.$ref,
encapsulation: encapsulation,
})
this._networks.set(privateNetwork.$id, privateNetwork.$ref)
if (center != null) {
this._starCenters.set(center.$id, center.$ref)
}
}
// ---------------------------------------------------------------------------
async _manageXapi(xapi) {
const { objects } = xapi
const objectsRemovedXapi = this._objectsRemoved.bind(this, xapi)
objects.on('add', this._objectsAdded)
objects.on('update', this._objectsUpdated)
objects.on('remove', objectsRemovedXapi)
await this._installCaCertificateIfNeeded(xapi)
return () => {
objects.removeListener('add', this._objectsAdded)
objects.removeListener('update', this._objectsUpdated)
objects.removeListener('remove', objectsRemovedXapi)
}
}
async _objectsAdded(objects) {
await Promise.all(
map(objects, async object => {
const { $type } = object
if ($type === 'host') {
log.debug('New host', {
host: object.name_label,
pool: object.$pool.name_label,
})
if (find(this._newHosts, { $ref: object.$ref }) == null) {
this._newHosts.push(object)
}
this._createOvsdbClient(object)
}
})
)
}
async _objectsUpdated(objects) {
await Promise.all(
map(objects, async (object, id) => {
const { $type } = object
if ($type === 'PIF') {
await this._pifUpdated(object)
} else if ($type === 'host') {
await this._hostUpdated(object)
} else if ($type === 'host_metrics') {
await this._hostMetricsUpdated(object)
}
})
)
}
async _objectsRemoved(xapi, objects) {
await Promise.all(
map(objects, async (object, id) => {
const client = find(this._ovsdbClients, { id: id })
if (client != null) {
this._ovsdbClients.splice(this._ovsdbClients.indexOf(client), 1)
}
// If a Star center host is removed: re-elect a new center where needed
const starCenterRef = this._starCenters.get(id)
if (starCenterRef != null) {
this._starCenters.delete(id)
const poolNetworks = filter(this._poolNetworks, {
starCenter: starCenterRef,
})
for (const poolNetwork of poolNetworks) {
const network = xapi.getObjectByRef(poolNetwork.network)
const newCenter = await this._electNewCenter(network, true)
poolNetwork.starCenter = newCenter?.$ref
if (newCenter != null) {
this._starCenters.set(newCenter.$id, newCenter.$ref)
}
}
return
}
// If a network is removed, clean this._poolNetworks from it
const networkRef = this._networks.get(id)
if (networkRef != null) {
this._networks.delete(id)
const poolNetwork = find(this._poolNetworks, {
network: networkRef,
})
if (poolNetwork != null) {
this._poolNetworks.splice(
this._poolNetworks.indexOf(poolNetwork),
1
)
}
}
})
)
}
async _pifUpdated(pif) {
// Only if PIF is in a private network
const poolNetwork = find(this._poolNetworks, { network: pif.network })
if (poolNetwork == null) {
return
}
if (!pif.currently_attached) {
if (poolNetwork.starCenter !== pif.host) {
return
}
log.debug(
'PIF of star-center host has been unplugged, electing a new star-center',
{
pif: pif.device,
network: pif.$network.name_label,
host: pif.$host.name_label,
pool: pif.$pool.name_label,
}
)
const newCenter = await this._electNewCenter(pif.$network, true)
poolNetwork.starCenter = newCenter?.$ref
this._starCenters.delete(pif.$host.$id)
if (newCenter != null) {
this._starCenters.set(newCenter.$id, newCenter.$ref)
}
} else {
if (poolNetwork.starCenter == null) {
const host = pif.$host
log.debug('First available host becomes star center of network', {
host: host.name_label,
network: pif.$network.name_label,
pool: pif.$pool.name_label,
})
poolNetwork.starCenter = pif.host
this._starCenters.set(host.$id, host.$ref)
}
log.debug('PIF plugged', {
pif: pif.device,
network: pif.$network.name_label,
host: pif.$host.name_label,
pool: pif.$pool.name_label,
})
const starCenter = pif.$xapi.getObjectByRef(poolNetwork.starCenter)
await this._addHostToNetwork(pif.$host, pif.$network, starCenter)
}
}
async _hostUpdated(host) {
const xapi = host.$xapi
if (host.enabled) {
if (host.PIFs.length === 0) {
return
}
const newHost = find(this._newHosts, { $ref: host.$ref })
if (newHost != null) {
this._newHosts.splice(this._newHosts.indexOf(newHost), 1)
try {
await xapi.call('pool.certificate_sync')
} catch (error) {
log.error('Error while syncing SDN controller CA certificate', {
error,
pool: host.$pool.name_label,
})
}
}
}
}
async _hostMetricsUpdated(hostMetrics) {
const ovsdbClient = find(this._ovsdbClients, {
hostMetricsRef: hostMetrics.$ref,
})
const host = ovsdbClient._host
if (hostMetrics.live) {
await this._addHostToPoolNetworks(host)
} else {
await this._hostUnreachable(host)
}
}
// ---------------------------------------------------------------------------
async _setPoolControllerIfNeeded(pool) {
if (!this._setControllerNeeded(pool.$xapi)) {
// Nothing to do
return
}
const controller = find(pool.$xapi.objects.all, { $type: 'SDN_controller' })
if (controller != null) {
await pool.$xapi.call('SDN_controller.forget', controller.$ref)
log.debug('Old SDN controller removed', {
pool: pool.name_label,
})
}
await pool.$xapi.call('SDN_controller.introduce', PROTOCOL)
log.debug('SDN controller has been set', {
pool: pool.name_label,
})
this._cleaners.push(await this._manageXapi(pool.$xapi))
}
_setControllerNeeded(xapi) {
const controller = find(xapi.objects.all, { $type: 'SDN_controller' })
return !(
controller != null &&
controller.protocol === PROTOCOL &&
controller.address === '' &&
controller.port === 0
)
}
// ---------------------------------------------------------------------------
async _installCaCertificateIfNeeded(xapi) {
let needInstall = false
try {
const result = await xapi.call('pool.certificate_list')
if (!result.includes(SDN_CONTROLLER_CERT)) {
needInstall = true
} else if (this._overrideCerts) {
await xapi.call('pool.certificate_uninstall', SDN_CONTROLLER_CERT)
log.debug('Old SDN controller CA certificate uninstalled', {
pool: xapi.pool.name_label,
})
needInstall = true
}
} catch (error) {
log.error('Error while retrieving certificate list', {
error,
pool: xapi.pool.name_label,
})
}
if (!needInstall) {
return
}
try {
await xapi.call(
'pool.certificate_install',
SDN_CONTROLLER_CERT,
this._caCert.toString()
)
await xapi.call('pool.certificate_sync')
log.debug('SDN controller CA certficate installed', {
pool: xapi.pool.name_label,
})
} catch (error) {
log.error('Error while installing SDN controller CA certificate', {
error,
pool: xapi.pool.name_label,
})
}
}
// ---------------------------------------------------------------------------
async _electNewCenter(network, resetNeeded) {
const pool = network.$pool
let newCenter = null
const hosts = filter(pool.$xapi.objects.all, { $type: 'host' })
for (const host of hosts) {
const pif = find(host.$PIFs, { network: network.$ref })
if (pif !== undefined && pif.currently_attached && host.$metrics.live) {
newCenter = host
}
}
await Promise.all(
map(hosts, async host => {
if (!resetNeeded) {
return
}
// Clean old ports and interfaces
const hostClient = find(this._ovsdbClients, { host: host.$ref })
if (hostClient != null) {
try {
await hostClient.resetForNetwork(network.uuid, network.name_label)
} catch (error) {
log.error('Error while resetting private network', {
error,
network: network.name_label,
host: host.name_label,
pool: network.$pool.name_label,
})
}
}
})
)
if (newCenter == null) {
log.error('No available host to elect new star-center', {
network: network.name_label,
pool: network.$pool.name_label,
})
return null
}
// Recreate star topology
await Promise.all(
await map(hosts, async host => {
await this._addHostToNetwork(host, network, newCenter)
})
)
log.info('New star-center elected', {
center: newCenter.name_label,
network: network.name_label,
pool: network.$pool.name_label,
})
return newCenter
}
async _createTunnel(host, network) {
const pif = host.$PIFs.find(
pif => pif.physical && pif.ip_configuration_mode !== 'None'
)
if (pif == null) {
log.error('No PIF found to create tunnel', {
host: host.name_label,
network: network.name_label,
})
return
}
await host.$xapi.call('tunnel.create', pif.$ref, network.$ref)
log.debug('New tunnel added', {
network: network.name_label,
host: host.name_label,
pool: host.$pool.name_label,
})
}
async _addHostToNetwork(host, network, starCenter) {
if (host.$ref === starCenter.$ref) {
// Nothing to do
return
}
const hostClient = find(this._ovsdbClients, {
host: host.$ref,
})
if (hostClient == null) {
log.error('No OVSDB client found', {
host: host.name_label,
pool: host.$pool.name_label,
})
return
}
const starCenterClient = find(this._ovsdbClients, {
host: starCenter.$ref,
})
if (starCenterClient == null) {
log.error('No OVSDB client found for star-center', {
host: starCenter.name_label,
pool: starCenter.$pool.name_label,
})
return
}
const encapsulation =
network.other_config.encapsulation != null
? network.other_config.encapsulation
: 'gre'
try {
await hostClient.addInterfaceAndPort(
network.uuid,
network.name_label,
starCenterClient.address,
encapsulation
)
await starCenterClient.addInterfaceAndPort(
network.uuid,
network.name_label,
hostClient.address,
encapsulation
)
} catch (error) {
log.error('Error while connection host to private network', {
error,
network: network.name_label,
host: host.name_label,
pool: host.$pool.name_label,
})
}
}
async _addHostToPoolNetworks(host) {
const xapi = host.$xapi
const tunnels = filter(xapi.objects.all, { $type: 'tunnel' })
for (const tunnel of tunnels) {
const accessPif = xapi.getObjectByRef(tunnel.access_PIF)
if (accessPif.host !== host.$ref) {
continue
}
const poolNetwork = find(this._poolNetworks, {
network: accessPif.network,
})
if (poolNetwork == null || accessPif.currently_attached) {
continue
}
try {
await xapi.call('PIF.plug', accessPif.$ref)
} catch (error) {
log.error('Error while plugging PIF', {
error,
pif: accessPif.device,
network: accessPif.$network.name_label,
host: host.name_label,
pool: host.$pool.name_label,
})
continue
}
log.debug('PIF plugged', {
pif: accessPif.device,
network: accessPif.$network.name_label,
host: host.name_label,
pool: host.$pool.name_label,
})
const starCenter = xapi.getObjectByRef(poolNetwork.starCenter)
await this._addHostToNetwork(host, accessPif.$network, starCenter)
}
}
async _hostUnreachable(host) {
const poolNetworks = filter(this._poolNetworks, { starCenter: host.$ref })
for (const poolNetwork of poolNetworks) {
const network = host.$xapi.getObjectByRef(poolNetwork.network)
log.debug('Unreachable star-center, electing a new one', {
network: network.name_label,
center: host.name_label,
pool: host.$pool.name_label,
})
const newCenter = await this._electNewCenter(network, true)
poolNetwork.starCenter = newCenter?.$ref
this._starCenters.delete(host.$id)
if (newCenter !== null) {
this._starCenters.set(newCenter.$id, newCenter.$ref)
}
}
}
// ---------------------------------------------------------------------------
_createOvsdbClient(host) {
const foundClient = find(this._ovsdbClients, { host: host.$ref })
if (foundClient != null) {
return foundClient
}
const client = new OvsdbClient(
host,
this._clientKey,
this._clientCert,
this._caCert
)
this._ovsdbClients.push(client)
return client
}
// ---------------------------------------------------------------------------
async _generateCertificatesAndKey(dataDir) {
const openssl = new NodeOpenssl()
const rsakeyoptions = {
rsa_keygen_bits: 4096,
format: 'PKCS8',
}
const subject = {
countryName: 'XX',
localityName: 'Default City',
organizationName: 'Default Company LTD',
}
const csroptions = {
hash: 'sha256',
startdate: new Date('1984-02-04 00:00:00'),
enddate: new Date('2143-06-04 04:16:23'),
subject: subject,
}
const cacsroptions = {
hash: 'sha256',
days: NB_DAYS,
subject: subject,
}
openssl.generateRSAPrivateKey(rsakeyoptions, (error, cakey, cmd) => {
if (error !== undefined) {
log.error('Error while generating CA private key', {
error,
})
return
}
openssl.generateCSR(cacsroptions, cakey, null, (error, csr, cmd) => {
if (error !== undefined) {
log.error('Error while generating CA certificate', {
error,
})
return
}
openssl.selfSignCSR(
csr,
cacsroptions,
cakey,
null,
async (error, cacrt, cmd) => {
if (error !== undefined) {
log.error('Error while signing CA certificate', {
error,
})
return
}
await fileWrite(join(dataDir, CA_CERT), cacrt)
openssl.generateRSAPrivateKey(
rsakeyoptions,
async (error, key, cmd) => {
if (error !== undefined) {
log.error('Error while generating private key', {
error,
})
return
}
await fileWrite(join(dataDir, CLIENT_KEY), key)
openssl.generateCSR(
csroptions,
key,
null,
(error, csr, cmd) => {
if (error !== undefined) {
log.error('Error while generating certificate', {
error,
})
return
}
openssl.CASignCSR(
csr,
cacsroptions,
false,
cacrt,
cakey,
null,
async (error, crt, cmd) => {
if (error !== undefined) {
log.error('Error while signing certificate', {
error,
})
return
}
await fileWrite(join(dataDir, CLIENT_CERT), crt)
this.emit('certWritten')
}
)
}
)
}
)
}
)
})
})
await fromEvent(this, 'certWritten', {})
log.debug('All certificates have been successfully written')
}
}
export default opts => new SDNController(opts)

View File

@@ -0,0 +1,513 @@
import assert from 'assert'
import createLogger from '@xen-orchestra/log'
import forOwn from 'lodash/forOwn'
import fromEvent from 'promise-toolbox/fromEvent'
import { connect } from 'tls'
const log = createLogger('xo:xo-server:sdn-controller:ovsdb-client')
const OVSDB_PORT = 6640
// =============================================================================
export class OvsdbClient {
constructor(host, clientKey, clientCert, caCert) {
this._host = host
this._numberOfPortAndInterface = 0
this._requestID = 0
this.updateCertificates(clientKey, clientCert, caCert)
log.debug('New OVSDB client', {
host: this._host.name_label,
})
}
// ---------------------------------------------------------------------------
get address() {
return this._host.address
}
get host() {
return this._host.$ref
}
get id() {
return this._host.$id
}
get hostMetricsRef() {
return this._host.metrics
}
updateCertificates(clientKey, clientCert, caCert) {
this._clientKey = clientKey
this._clientCert = clientCert
this._caCert = caCert
log.debug('Certificates have been updated', {
host: this._host.name_label,
})
}
// ---------------------------------------------------------------------------
async addInterfaceAndPort(
networkUuid,
networkName,
remoteAddress,
encapsulation
) {
const socket = await this._connect()
const index = this._numberOfPortAndInterface
++this._numberOfPortAndInterface
const [bridgeUuid, bridgeName] = await this._getBridgeUuidForNetwork(
networkUuid,
networkName,
socket
)
if (bridgeUuid == null) {
socket.destroy()
return
}
const alreadyExist = await this._interfaceAndPortAlreadyExist(
bridgeUuid,
bridgeName,
remoteAddress,
socket
)
if (alreadyExist) {
socket.destroy()
return
}
const interfaceName = 'tunnel_iface' + index
const portName = 'tunnel_port' + index
// Add interface and port to the bridge
const options = ['map', [['remote_ip', remoteAddress]]]
const addInterfaceOperation = {
op: 'insert',
table: 'Interface',
row: {
type: encapsulation,
options: options,
name: interfaceName,
other_config: ['map', [['private_pool_wide', 'true']]],
},
'uuid-name': 'new_iface',
}
const addPortOperation = {
op: 'insert',
table: 'Port',
row: {
name: portName,
interfaces: ['set', [['named-uuid', 'new_iface']]],
other_config: ['map', [['private_pool_wide', 'true']]],
},
'uuid-name': 'new_port',
}
const mutateBridgeOperation = {
op: 'mutate',
table: 'Bridge',
where: [['_uuid', '==', ['uuid', bridgeUuid]]],
mutations: [['ports', 'insert', ['set', [['named-uuid', 'new_port']]]]],
}
const params = [
'Open_vSwitch',
addInterfaceOperation,
addPortOperation,
mutateBridgeOperation,
]
const jsonObjects = await this._sendOvsdbTransaction(params, socket)
if (jsonObjects == null) {
socket.destroy()
return
}
let error
let details
let i = 0
let opResult
do {
opResult = jsonObjects[0].result[i]
if (opResult != null && opResult.error != null) {
error = opResult.error
details = opResult.details
}
++i
} while (opResult && !error)
if (error != null) {
log.error('Error while adding port and interface to bridge', {
error,
details,
port: portName,
interface: interfaceName,
bridge: bridgeName,
network: networkName,
host: this._host.name_label,
})
socket.destroy()
return
}
log.debug('Port and interface added to bridge', {
port: portName,
interface: interfaceName,
bridge: bridgeName,
network: networkName,
host: this._host.name_label,
})
socket.destroy()
}
async resetForNetwork(networkUuid, networkName) {
const socket = await this._connect()
const [bridgeUuid, bridgeName] = await this._getBridgeUuidForNetwork(
networkUuid,
networkName,
socket
)
if (bridgeUuid == null) {
socket.destroy()
return
}
// Delete old ports created by a SDN controller
const ports = await this._getBridgePorts(bridgeUuid, bridgeName, socket)
if (ports == null) {
socket.destroy()
return
}
const portsToDelete = []
for (const port of ports) {
const portUuid = port[1]
const where = [['_uuid', '==', ['uuid', portUuid]]]
const selectResult = await this._select(
'Port',
['name', 'other_config'],
where,
socket
)
if (selectResult == null) {
continue
}
forOwn(selectResult.other_config[1], config => {
if (config[0] === 'private_pool_wide' && config[1] === 'true') {
portsToDelete.push(['uuid', portUuid])
}
})
}
if (portsToDelete.length === 0) {
// Nothing to do
socket.destroy()
return
}
const mutateBridgeOperation = {
op: 'mutate',
table: 'Bridge',
where: [['_uuid', '==', ['uuid', bridgeUuid]]],
mutations: [['ports', 'delete', ['set', portsToDelete]]],
}
const params = ['Open_vSwitch', mutateBridgeOperation]
const jsonObjects = await this._sendOvsdbTransaction(params, socket)
if (jsonObjects == null) {
socket.destroy()
return
}
if (jsonObjects[0].error != null) {
log.error('Error while deleting ports from bridge', {
error: jsonObjects.error,
bridge: bridgeName,
host: this._host.name_label,
})
socket.destroy()
return
}
log.debug('Ports deleted from bridge', {
nPorts: jsonObjects[0].result[0].count,
bridge: bridgeName,
host: this._host.name_label,
})
socket.destroy()
}
// ===========================================================================
_parseJson(chunk) {
let data = chunk.toString()
let buffer = ''
let depth = 0
let pos = 0
const objects = []
for (let i = pos; i < data.length; ++i) {
const c = data.charAt(i)
if (c === '{') {
depth++
} else if (c === '}') {
depth--
if (depth === 0) {
const object = JSON.parse(buffer + data.substr(0, i + 1))
objects.push(object)
buffer = ''
data = data.substr(i + 1)
pos = 0
i = -1
}
}
}
buffer += data
return objects
}
// ---------------------------------------------------------------------------
async _getBridgeUuidForNetwork(networkUuid, networkName, socket) {
const where = [
[
'external_ids',
'includes',
['map', [['xs-network-uuids', networkUuid]]],
],
]
const selectResult = await this._select(
'Bridge',
['_uuid', 'name'],
where,
socket
)
if (selectResult == null) {
log.error('No bridge found for network', {
network: networkName,
host: this._host.name_label,
})
return [null, null]
}
const bridgeUuid = selectResult._uuid[1]
const bridgeName = selectResult.name
return [bridgeUuid, bridgeName]
}
async _interfaceAndPortAlreadyExist(
bridgeUuid,
bridgeName,
remoteAddress,
socket
) {
const ports = await this._getBridgePorts(bridgeUuid, bridgeName, socket)
if (ports == null) {
return
}
for (const port of ports) {
const portUuid = port[1]
const interfaces = await this._getPortInterfaces(portUuid, socket)
if (interfaces == null) {
continue
}
for (const iface of interfaces) {
const interfaceUuid = iface[1]
const hasRemote = await this._interfaceHasRemote(
interfaceUuid,
remoteAddress,
socket
)
if (hasRemote === true) {
return true
}
}
}
return false
}
async _getBridgePorts(bridgeUuid, bridgeName, socket) {
const where = [['_uuid', '==', ['uuid', bridgeUuid]]]
const selectResult = await this._select('Bridge', ['ports'], where, socket)
if (selectResult == null) {
return null
}
return selectResult.ports[0] === 'set'
? selectResult.ports[1]
: [selectResult.ports]
}
async _getPortInterfaces(portUuid, socket) {
const where = [['_uuid', '==', ['uuid', portUuid]]]
const selectResult = await this._select(
'Port',
['name', 'interfaces'],
where,
socket
)
if (selectResult == null) {
return null
}
return selectResult.interfaces[0] === 'set'
? selectResult.interfaces[1]
: [selectResult.interfaces]
}
async _interfaceHasRemote(interfaceUuid, remoteAddress, socket) {
const where = [['_uuid', '==', ['uuid', interfaceUuid]]]
const selectResult = await this._select(
'Interface',
['name', 'options'],
where,
socket
)
if (selectResult == null) {
return false
}
for (const option of selectResult.options[1]) {
if (option[0] === 'remote_ip' && option[1] === remoteAddress) {
return true
}
}
return false
}
// ---------------------------------------------------------------------------
async _select(table, columns, where, socket) {
const selectOperation = {
op: 'select',
table: table,
columns: columns,
where: where,
}
const params = ['Open_vSwitch', selectOperation]
const jsonObjects = await this._sendOvsdbTransaction(params, socket)
if (jsonObjects == null) {
return
}
const jsonResult = jsonObjects[0].result[0]
if (jsonResult.error != null) {
log.error('Error while selecting columns', {
error: jsonResult.error,
details: jsonResult.details,
columns,
table,
where,
host: this._host.name_label,
})
return null
}
if (jsonResult.rows.length === 0) {
log.error('No result for select', {
columns,
table,
where,
host: this._host.name_label,
})
return null
}
// For now all select operations should return only 1 row
assert(
jsonResult.rows.length === 1,
`[${this._host.name_label}] There should exactly 1 row when searching: '${columns}' in: '${table}' where: '${where}'`
)
return jsonResult.rows[0]
}
async _sendOvsdbTransaction(params, socket) {
const stream = socket
const requestId = this._requestID
++this._requestID
const req = {
id: requestId,
method: 'transact',
params: params,
}
try {
stream.write(JSON.stringify(req))
} catch (error) {
log.error('Error while writing into stream', {
error,
host: this._host.name_label,
})
return null
}
let result
let jsonObjects
let resultRequestId
do {
try {
result = await fromEvent(stream, 'data', {})
} catch (error) {
log.error('Error while waiting for stream data', {
error,
host: this._host.name_label,
})
return null
}
jsonObjects = this._parseJson(result)
resultRequestId = jsonObjects[0].id
} while (resultRequestId !== requestId)
return jsonObjects
}
// ---------------------------------------------------------------------------
async _connect() {
const options = {
ca: this._caCert,
key: this._clientKey,
cert: this._clientCert,
host: this._host.address,
port: OVSDB_PORT,
rejectUnauthorized: false,
requestCert: false,
}
const socket = connect(options)
try {
await fromEvent(socket, 'secureConnect', {})
} catch (error) {
log.error('TLS connection failed', {
error,
code: error.code,
host: this._host.name_label,
})
throw error
}
socket.on('error', error => {
log.error('Socket error', {
error,
code: error.code,
host: this._host.name_label,
})
})
return socket
}
}

View File

@@ -0,0 +1,8 @@
const pkg = require('./package.json')
// `xo-server-test` is a special package which has no dev dependencies but our
// babel config generator only looks in `devDependencies`.
require('assert').strictEqual(pkg.devDependencies, undefined)
pkg.devDependencies = pkg.dependencies
module.exports = require('../../@xen-orchestra/babel-config')(pkg)

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,136 @@
# xo-server-test
> Test client for Xo-Server
## Adding a test
### Organization
```
src
├─ user
| ├─ __snapshots__
| | └─ index.spec.js.snap
| └─ index.spec.js
├─ job
¦ └─ index.spec.js
¦
¦
├─ _xoConnection.js
└─ util.js
```
The tests can describe xo methods or scenarios:
```javascript
import xo from "../_xoConnection";
describe("user", () => {
// testing a method
describe(".set()", () => {
it("sets an email", async () => {
// some tests using xo methods and helpers from _xoConnection.js
const id = await xo.createTempUser(SIMPLE_USER);
expect(await xo.call("user.set", params)).toBe(true);
expect(await xo.getUser(id)).toMatchSnapshot({
id: expect.any(String),
});
});
});
// testing a scenario
test("create two users, modify a user email to be the same with the other and fail trying to connect them", () => {
/* some tests */
});
});
```
### Best practices
- The test environment must remain the same before and after each test:
* each resource created must be deleted
* existing resources should not be altered
- Make a sentence for the title of the test. It must be clear and consistent.
- If the feature you want to test is not implemented : write it and skip it, using `it.skip()`.
- Take values that cover the maximum of testing possibilities.
- If you make tests which keep track of large object, it is better to use snapshots.
- `_xoConnection.js` contains helpers to create temporary resources and to interface with XO.
You can use it if you need to create resources which will be automatically deleted after the test:
```javascript
import xo from "../_xoConnection";
describe(".create()", () => {
it("creates a user without permission", async () => {
// The user will be deleted automatically at the end of the test
const userId = await xo.createTempUser({
email: "wayne1@vates.fr",
password: "batman1",
});
expect(await xo.getUser(userId)).toMatchSnapshot({
id: expect.any(String),
});
});
});
```
The available helpers:
* `createTempUser(params)`
* `getUser(id)`
* `createTempJob(params)`
* `createTempBackupNgJob(params)`
* `createTempVm(params)`
* `getSchedule(predicate)`
## Usage
- Before running the tests, you have to create a config file for xo-server-test.
```
> cp sample.config.toml ~/.config/xo-server-test/config.toml
```
And complete it.
- To run the tests:
```
> npm ci
> yarn test
```
You get all the test suites passed (`PASS`) or failed (`FAIL`).
```
> yarn test
yarn run v1.9.4
$ jest
PASS src/user/user.spec.js
PASS src/job/job.spec.js
PASS src/backupNg/backupNg.spec.js
Test Suites: 3 passed, 3 total
Tests: 2 skipped, 36 passed, 38 total
Snapshots: 35 passed, 35 total
Time: 7.257s, estimated 8s
Ran all test suites.
Done in 7.92s.
```
- You can run only tests related to changed files, and review the failed output by using: `> yarn test --watch`
## 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,54 @@
{
"private": true,
"name": "xo-server-test",
"version": "0.0.0",
"license": "ISC",
"description": "Test client for Xo-Server",
"keywords": [],
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/packages/xo-server-test",
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
"repository": {
"directory": "packages/xo-server-test",
"type": "git",
"url": "https://github.com/vatesfr/xen-orchestra.git"
},
"author": {
"name": "Julien Fontanet",
"email": "julien.fontanet@isonoe.net"
},
"preferGlobal": false,
"main": "dist/",
"bin": {},
"files": [
"dist/"
],
"engines": {
"node": ">=6"
},
"dependencies": {
"@babel/cli": "^7.1.5",
"@babel/core": "^7.1.6",
"@babel/plugin-proposal-decorators": "^7.4.0",
"@babel/preset-env": "^7.1.6",
"@iarna/toml": "^2.2.1",
"app-conf": "^0.7.0",
"babel-plugin-lodash": "^3.2.11",
"golike-defer": "^0.4.1",
"jest": "^24.8.0",
"lodash": "^4.17.11",
"xo-collection": "^0.4.1",
"xo-common": "^0.2.0",
"xo-lib": "^0.9.0"
},
"scripts": {
"dev-test": "jest --bail --watch",
"test": "jest"
},
"jest": {
"modulePathIgnorePatterns": [
"<rootDir>/src/old-tests"
],
"testEnvironment": "node",
"testRegex": "\\.spec\\.js$"
}
}

View File

@@ -0,0 +1,18 @@
[xoConnection]
url = ''
email = ''
password = ''
[vms]
default = ''
[templates]
default = ''
[srs]
default = ''
# resources created before all tests and deleted at the end.
[preCreatedResources]
[preCreatedResources.remotes]
default = { name = '', url = '' }

View File

@@ -0,0 +1,13 @@
import appConf from 'app-conf'
import path from 'path'
/* eslint-env jest */
let config
export { config as default }
beforeAll(async () => {
config = await appConf.load('xo-server-test', {
appDir: path.join(__dirname, '..'),
})
})

View File

@@ -0,0 +1,6 @@
const randomId = () =>
Math.random()
.toString(36)
.slice(2)
export { randomId as default }

View File

@@ -0,0 +1,205 @@
/* eslint-env jest */
import defer from 'golike-defer'
import Xo from 'xo-lib'
import XoCollection from 'xo-collection'
import { find, forOwn } from 'lodash'
import config from './_config'
const ARGS_BY_TYPE = {
remotes: {
getCreationArgs: conf => ['remote.create', conf],
getDeletionArgs: res => ['remote.delete', { id: res.id }],
},
}
const getDefaultCredentials = () => {
const { email, password } = config.xoConnection
return { email, password }
}
class XoConnection extends Xo {
constructor(opts) {
super(opts)
const objects = (this._objects = new XoCollection())
const watchers = (this._watchers = {})
this._tempResourceDisposers = []
this._durableResourceDisposers = []
this.on('notification', ({ method, params }) => {
if (method !== 'all') {
return
}
const fn = params.type === 'exit' ? objects.unset : objects.set
forOwn(params.items, (item, id) => {
fn.call(objects, id, item)
const watcher = watchers[id]
if (watcher !== undefined) {
watcher(item)
delete watchers[id]
}
})
})
}
get objects() {
return this._objects
}
async _fetchObjects() {
const { _objects: objects, _watchers: watchers } = this
forOwn(await this.call('xo.getAllObjects'), (object, id) => {
objects.set(id, object)
const watcher = watchers[id]
if (watcher !== undefined) {
watcher(object)
delete watchers[id]
}
})
}
// TODO: integrate in xo-lib.
waitObject(id) {
return new Promise(resolve => {
this._watchers[id] = resolve
}) // FIXME: work with multiple listeners.
}
async getOrWaitObject(id) {
const object = this._objects.all[id]
if (object !== undefined) {
return object
}
return this.waitObject(id)
}
@defer
async connect($defer, credentials = getDefaultCredentials()) {
await this.open()
$defer.onFailure(() => this.close())
await this.signIn(credentials)
await this._fetchObjects()
return this
}
async waitObjectState(id, predicate) {
let obj = this._objects.all[id]
while (true) {
try {
await predicate(obj)
return
} catch (_) {}
// If failed, wait for next object state/update and retry.
obj = await this.waitObject(id)
}
}
async createTempUser(params) {
const id = await this.call('user.create', params)
this._tempResourceDisposers.push('user.delete', { id })
return id
}
async getUser(id) {
return find(await super.call('user.getAll'), { id })
}
async createTempJob(params) {
const id = await this.call('job.create', { job: params })
this._tempResourceDisposers.push('job.delete', { id })
return id
}
async createTempBackupNgJob(params) {
const job = await this.call('backupNg.createJob', params)
this._tempResourceDisposers.push('backupNg.deleteJob', { id: job.id })
return job
}
async createTempVm(params) {
const id = await this.call('vm.create', params)
this._tempResourceDisposers.push('vm.delete', { id })
await this.waitObjectState(id, vm => {
if (vm.type !== 'VM') throw new Error('retry')
})
return id
}
async createRequiredResources() {
const requiredResources = {}
const resourcesToCreate = config.preCreatedResources
for (const typeOfResources in resourcesToCreate) {
const { getCreationArgs, getDeletionArgs } = ARGS_BY_TYPE[typeOfResources]
const resources = resourcesToCreate[typeOfResources]
for (const resource in resources) {
const result = await this.call(...getCreationArgs(resources[resource]))
this._durableResourceDisposers.push(...getDeletionArgs(result))
requiredResources[typeOfResources] = {
...requiredResources[typeOfResources],
[resource]: result,
}
}
}
return requiredResources
}
async getSchedule(predicate) {
return find(await this.call('schedule.getAll'), predicate)
}
async _cleanDisposers(disposers) {
for (let n = disposers.length - 1; n > 0; ) {
const params = disposers[n--]
const method = disposers[n--]
await this.call(method, params).catch(error => {
console.warn('_cleanDisposers', method, params, error)
})
}
disposers.length = 0
}
async deleteTempResources() {
await this._cleanDisposers(this._tempResourceDisposers)
}
async deleteDurableResources() {
await this._cleanDisposers(this._durableResourceDisposers)
}
}
const getConnection = credentials => {
const xo = new XoConnection({ url: config.xoConnection.url })
return xo.connect(credentials)
}
let xo
let resources
beforeAll(async () => {
xo = await getConnection()
resources = await xo.createRequiredResources()
})
afterAll(async () => {
await xo.deleteDurableResources()
await xo.close()
xo = null
})
afterEach(() => xo.deleteTempResources())
export { xo as default, resources }
export const testConnection = ({ credentials }) =>
getConnection(credentials).then(connection => connection.close())
export const testWithOtherConnection = defer(
async ($defer, credentials, functionToExecute) => {
const xoUser = await getConnection(credentials)
$defer(() => xoUser.close())
await functionToExecute(xoUser)
}
)

View File

@@ -0,0 +1,170 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`backupNg .createJob() : creates a new backup job with schedules 1`] = `
Object {
"id": Any<String>,
"mode": "full",
"name": "default-backupNg",
"settings": Any<Object>,
"type": "backup",
"userId": Any<String>,
"vms": Any<Object>,
}
`;
exports[`backupNg .createJob() : creates a new backup job with schedules 2`] = `
Object {
"cron": "0 * * * * *",
"enabled": false,
"id": Any<String>,
"jobId": Any<String>,
"name": "scheduleTest",
}
`;
exports[`backupNg .createJob() : creates a new backup job without schedules 1`] = `
Object {
"id": Any<String>,
"mode": "full",
"name": "default-backupNg",
"settings": Object {
"": Object {
"reportWhen": "never",
},
},
"type": "backup",
"userId": Any<String>,
"vms": Any<Object>,
}
`;
exports[`backupNg .runJob() : fails trying to run a backup job with a VM without disks 1`] = `
Object {
"data": Object {
"mode": "full",
"reportWhen": "never",
},
"end": Any<Number>,
"id": Any<String>,
"jobId": Any<String>,
"jobName": "default-backupNg",
"message": "backup",
"scheduleId": Any<String>,
"start": Any<Number>,
"status": "skipped",
}
`;
exports[`backupNg .runJob() : fails trying to run a backup job with a VM without disks 2`] = `
Object {
"data": Object {
"id": Any<String>,
"type": "VM",
},
"end": Any<Number>,
"id": Any<String>,
"message": Any<String>,
"result": Object {
"message": "no disks found",
"name": "Error",
"stack": Any<String>,
},
"start": Any<Number>,
"status": "skipped",
}
`;
exports[`backupNg .runJob() : fails trying to run a backup job with no matching VMs 1`] = `[JsonRpcError: unknown error from the peer]`;
exports[`backupNg .runJob() : fails trying to run a backup job with non-existent vm 1`] = `
Array [
Object {
"data": Object {
"vms": Array [
"non-existent-id",
],
},
"message": "missingVms",
},
]
`;
exports[`backupNg .runJob() : fails trying to run a backup job without schedule 1`] = `[JsonRpcError: invalid parameters]`;
exports[`backupNg .runJob() : fails trying to run backup job without retentions 1`] = `
Object {
"data": Object {
"mode": "full",
"reportWhen": "never",
},
"end": Any<Number>,
"id": Any<String>,
"jobId": Any<String>,
"jobName": "default-backupNg",
"message": "backup",
"scheduleId": Any<String>,
"start": Any<Number>,
"status": "failure",
}
`;
exports[`backupNg .runJob() : fails trying to run backup job without retentions 2`] = `
Object {
"data": Object {
"id": Any<String>,
"type": "VM",
},
"end": Any<Number>,
"id": Any<String>,
"message": Any<String>,
"result": Object {
"message": "copy, export and snapshot retentions cannot both be 0",
"name": "Error",
"stack": Any<String>,
},
"start": Any<Number>,
"status": "failure",
}
`;
exports[`backupNg execute three times a rolling snapshot with 2 as retention & revert to an old state 1`] = `
Object {
"data": Object {
"mode": "full",
"reportWhen": "never",
},
"end": Any<Number>,
"id": Any<String>,
"jobId": Any<String>,
"jobName": "default-backupNg",
"message": "backup",
"scheduleId": Any<String>,
"start": Any<Number>,
"status": "success",
}
`;
exports[`backupNg execute three times a rolling snapshot with 2 as retention & revert to an old state 2`] = `
Object {
"end": Any<Number>,
"id": Any<String>,
"message": "snapshot",
"result": Any<String>,
"start": Any<Number>,
"status": "success",
}
`;
exports[`backupNg execute three times a rolling snapshot with 2 as retention & revert to an old state 3`] = `
Object {
"data": Object {
"id": Any<String>,
"type": "VM",
},
"end": Any<Number>,
"id": Any<String>,
"message": Any<String>,
"start": Any<Number>,
"status": "success",
}
`;

View File

@@ -0,0 +1,385 @@
/* eslint-env jest */
import { noSuchObject } from 'xo-common/api-errors'
import config from '../_config'
import randomId from '../_randomId'
import xo, { resources } from '../_xoConnection'
const DEFAULT_SCHEDULE = {
name: 'scheduleTest',
cron: '0 * * * * *',
}
describe('backupNg', () => {
let defaultBackupNg
beforeAll(() => {
defaultBackupNg = {
name: 'default-backupNg',
mode: 'full',
vms: {
id: config.vms.default,
},
settings: {
'': {
reportWhen: 'never',
},
},
}
})
describe('.createJob() :', () => {
it('creates a new backup job without schedules', async () => {
const backupNg = await xo.createTempBackupNgJob(defaultBackupNg)
expect(backupNg).toMatchSnapshot({
id: expect.any(String),
userId: expect.any(String),
vms: expect.any(Object),
})
expect(backupNg.vms).toEqual(defaultBackupNg.vms)
expect(backupNg.userId).toBe(xo._user.id)
})
it('creates a new backup job with schedules', async () => {
const scheduleTempId = randomId()
const { id: jobId } = await xo.createTempBackupNgJob({
...defaultBackupNg,
schedules: {
[scheduleTempId]: DEFAULT_SCHEDULE,
},
settings: {
...defaultBackupNg.settings,
[scheduleTempId]: { snapshotRetention: 1 },
},
})
const backupNgJob = await xo.call('backupNg.getJob', { id: jobId })
expect(backupNgJob).toMatchSnapshot({
id: expect.any(String),
userId: expect.any(String),
settings: expect.any(Object),
vms: expect.any(Object),
})
expect(backupNgJob.vms).toEqual(defaultBackupNg.vms)
expect(backupNgJob.userId).toBe(xo._user.id)
expect(Object.keys(backupNgJob.settings).length).toBe(2)
const schedule = await xo.getSchedule({ jobId })
expect(typeof schedule).toBe('object')
expect(backupNgJob.settings[schedule.id]).toEqual({
snapshotRetention: 1,
})
expect(schedule).toMatchSnapshot({
id: expect.any(String),
jobId: expect.any(String),
})
})
})
describe('.delete() :', () => {
it('deletes a backup job', async () => {
const scheduleTempId = randomId()
const { id: jobId } = await xo.call('backupNg.createJob', {
...defaultBackupNg,
schedules: {
[scheduleTempId]: DEFAULT_SCHEDULE,
},
settings: {
...defaultBackupNg.settings,
[scheduleTempId]: { snapshotRetention: 1 },
},
})
const schedule = await xo.getSchedule({ jobId })
expect(typeof schedule).toBe('object')
await xo.call('backupNg.deleteJob', { id: jobId })
let isRejectedJobErrorValid = false
await xo.call('backupNg.getJob', { id: jobId }).catch(error => {
isRejectedJobErrorValid = noSuchObject.is(error)
})
expect(isRejectedJobErrorValid).toBe(true)
let isRejectedScheduleErrorValid = false
await xo.call('schedule.get', { id: schedule.id }).catch(error => {
isRejectedScheduleErrorValid = noSuchObject.is(error)
})
expect(isRejectedScheduleErrorValid).toBe(true)
})
})
describe('.runJob() :', () => {
it('fails trying to run a backup job without schedule', async () => {
const { id } = await xo.createTempBackupNgJob(defaultBackupNg)
await expect(xo.call('backupNg.runJob', { id })).rejects.toMatchSnapshot()
})
it('fails trying to run a backup job with no matching VMs', async () => {
const scheduleTempId = randomId()
const { id: jobId } = await xo.createTempBackupNgJob({
...defaultBackupNg,
schedules: {
[scheduleTempId]: DEFAULT_SCHEDULE,
},
settings: {
[scheduleTempId]: { snapshotRetention: 1 },
},
vms: {
id: config.vms.default,
name: 'test-vm-backupNg',
},
})
const schedule = await xo.getSchedule({ jobId })
expect(typeof schedule).toBe('object')
await expect(
xo.call('backupNg.runJob', { id: jobId, schedule: schedule.id })
).rejects.toMatchSnapshot()
})
it('fails trying to run a backup job with non-existent vm', async () => {
const scheduleTempId = randomId()
const { id: jobId } = await xo.createTempBackupNgJob({
...defaultBackupNg,
schedules: {
[scheduleTempId]: DEFAULT_SCHEDULE,
},
settings: {
[scheduleTempId]: { snapshotRetention: 1 },
},
vms: {
id: 'non-existent-id',
},
})
const schedule = await xo.getSchedule({ jobId })
expect(typeof schedule).toBe('object')
await xo.call('backupNg.runJob', { id: jobId, schedule: schedule.id })
const [log] = await xo.call('backupNg.getLogs', {
scheduleId: schedule.id,
})
expect(log.warnings).toMatchSnapshot()
})
it('fails trying to run a backup job with a VM without disks', async () => {
const vmIdWithoutDisks = await xo.createTempVm({
name_label: 'XO Test Without Disks',
name_description: 'Creating a vm without disks',
template: config.templates.default,
})
const scheduleTempId = randomId()
const { id: jobId } = await xo.createTempBackupNgJob({
...defaultBackupNg,
schedules: {
[scheduleTempId]: DEFAULT_SCHEDULE,
},
settings: {
...defaultBackupNg.settings,
[scheduleTempId]: { snapshotRetention: 1 },
},
vms: {
id: vmIdWithoutDisks,
},
})
const schedule = await xo.getSchedule({ jobId })
expect(typeof schedule).toBe('object')
await xo.call('backupNg.runJob', { id: jobId, schedule: schedule.id })
const [
{
tasks: [vmTask],
...log
},
] = await xo.call('backupNg.getLogs', {
jobId,
scheduleId: schedule.id,
})
expect(log).toMatchSnapshot({
end: expect.any(Number),
id: expect.any(String),
jobId: expect.any(String),
scheduleId: expect.any(String),
start: expect.any(Number),
})
expect(vmTask).toMatchSnapshot({
end: expect.any(Number),
data: {
id: expect.any(String),
},
id: expect.any(String),
message: expect.any(String),
result: {
stack: expect.any(String),
},
start: expect.any(Number),
})
expect(vmTask.data.id).toBe(vmIdWithoutDisks)
})
it('fails trying to run backup job without retentions', async () => {
const scheduleTempId = randomId()
const { id: jobId } = await xo.createTempBackupNgJob({
...defaultBackupNg,
remotes: {
id: resources.remotes.default.id,
},
schedules: {
[scheduleTempId]: DEFAULT_SCHEDULE,
},
settings: {
...defaultBackupNg.settings,
[scheduleTempId]: {},
},
srs: {
id: config.srs.default,
},
})
const schedule = await xo.getSchedule({ jobId })
expect(typeof schedule).toBe('object')
await xo.call('backupNg.runJob', { id: jobId, schedule: schedule.id })
const [
{
tasks: [task],
...log
},
] = await xo.call('backupNg.getLogs', {
jobId,
scheduleId: schedule.id,
})
expect(log).toMatchSnapshot({
end: expect.any(Number),
id: expect.any(String),
jobId: expect.any(String),
scheduleId: expect.any(String),
start: expect.any(Number),
})
expect(task).toMatchSnapshot({
data: {
id: expect.any(String),
},
end: expect.any(Number),
id: expect.any(String),
message: expect.any(String),
result: {
stack: expect.any(String),
},
start: expect.any(Number),
})
expect(task.data.id).toBe(config.vms.default)
})
})
test('execute three times a rolling snapshot with 2 as retention & revert to an old state', async () => {
jest.setTimeout(7e4)
const vmId = await xo.createTempVm({
name_label: 'XO Test Temp',
name_description: 'Creating a temporary vm',
template: config.templates.default,
VDIs: [
{
size: 1,
SR: config.srs.default,
type: 'user',
},
],
})
const scheduleTempId = randomId()
const { id: jobId } = await xo.createTempBackupNgJob({
...defaultBackupNg,
vms: {
id: vmId,
},
schedules: {
[scheduleTempId]: DEFAULT_SCHEDULE,
},
settings: {
...defaultBackupNg.settings,
[scheduleTempId]: { snapshotRetention: 2 },
},
})
const schedule = await xo.getSchedule({ jobId })
expect(typeof schedule).toBe('object')
for (let i = 0; i < 3; i++) {
const oldSnapshots = xo.objects.all[vmId].snapshots
await xo.call('backupNg.runJob', { id: jobId, schedule: schedule.id })
await xo.waitObjectState(vmId, ({ snapshots }) => {
// Test on updating snapshots.
expect(snapshots).not.toEqual(oldSnapshots)
})
}
const { snapshots, videoram: oldVideoram } = xo.objects.all[vmId]
// Test on the retention, how many snapshots should be saved.
expect(snapshots.length).toBe(2)
const newVideoram = 16
await xo.call('vm.set', { id: vmId, videoram: newVideoram })
await xo.waitObjectState(vmId, ({ videoram }) => {
expect(videoram).toBe(newVideoram.toString())
})
await xo.call('vm.revert', {
snapshot: snapshots[0],
})
await xo.waitObjectState(vmId, ({ videoram }) => {
expect(videoram).toBe(oldVideoram)
})
const [
{
tasks: [{ tasks: subTasks, ...vmTask }],
...log
},
] = await xo.call('backupNg.getLogs', {
jobId,
scheduleId: schedule.id,
})
expect(log).toMatchSnapshot({
end: expect.any(Number),
id: expect.any(String),
jobId: expect.any(String),
scheduleId: expect.any(String),
start: expect.any(Number),
})
const subTaskSnapshot = subTasks.find(
({ message }) => message === 'snapshot'
)
expect(subTaskSnapshot).toMatchSnapshot({
end: expect.any(Number),
id: expect.any(String),
result: expect.any(String),
start: expect.any(Number),
})
expect(vmTask).toMatchSnapshot({
data: {
id: expect.any(String),
},
end: expect.any(Number),
id: expect.any(String),
message: expect.any(String),
start: expect.any(Number),
})
expect(vmTask.data.id).toBe(vmId)
})
})

View File

@@ -0,0 +1,76 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`job .create() : creates a new job 1`] = `
Object {
"id": Any<String>,
"key": "snapshot",
"method": "vm.snapshot",
"name": "jobTest",
"paramsVector": Any<Object>,
"timeout": 2000,
"type": "call",
"userId": Any<String>,
}
`;
exports[`job .create() : fails trying to create a job without job params 1`] = `[JsonRpcError: invalid parameters]`;
exports[`job .delete() : deletes an existing job 1`] = `[JsonRpcError: no such job [object Object]]`;
exports[`job .delete() : deletes an existing job 2`] = `[JsonRpcError: no such schedule [object Object]]`;
exports[`job .get() : fails trying to get a job with a non existent id 1`] = `[JsonRpcError: no such job [object Object]]`;
exports[`job .get() : gets an existing job 1`] = `
Object {
"id": Any<String>,
"key": "snapshot",
"method": "vm.snapshot",
"name": "jobTest",
"paramsVector": Any<Object>,
"timeout": 2000,
"type": "call",
"userId": Any<String>,
}
`;
exports[`job .getAll() : gets all available jobs 1`] = `
Object {
"id": Any<String>,
"key": "snapshot",
"method": "vm.snapshot",
"name": "jobTest",
"paramsVector": Any<Object>,
"timeout": 2000,
"type": "call",
"userId": Any<String>,
}
`;
exports[`job .getAll() : gets all available jobs 2`] = `
Object {
"id": Any<String>,
"key": "snapshot",
"method": "vm.snapshot",
"name": "jobTest2",
"paramsVector": Any<Object>,
"timeout": 2000,
"type": "call",
"userId": Any<String>,
}
`;
exports[`job .set() : fails trying to set a job without job.id 1`] = `[JsonRpcError: invalid parameters]`;
exports[`job .set() : sets a job 1`] = `
Object {
"id": Any<String>,
"key": "snapshot",
"method": "vm.clone",
"name": "jobTest",
"paramsVector": Any<Object>,
"timeout": 2000,
"type": "call",
"userId": Any<String>,
}
`;

View File

@@ -0,0 +1,223 @@
/* eslint-env jest */
import { difference, keyBy } from 'lodash'
import config from '../_config'
import xo, { testWithOtherConnection } from '../_xoConnection'
const ADMIN_USER = {
email: 'admin2@admin.net',
password: 'admin',
permission: 'admin',
}
describe('job', () => {
let defaultJob
beforeAll(() => {
defaultJob = {
name: 'jobTest',
timeout: 2000,
type: 'call',
key: 'snapshot',
method: 'vm.snapshot',
paramsVector: {
type: 'crossProduct',
items: [
{
type: 'set',
values: [
{
id: config.vms.default,
name: 'test-snapshot',
},
],
},
],
},
}
})
describe('.create() :', () => {
it('creates a new job', async () => {
const userId = await xo.createTempUser(ADMIN_USER)
const { email, password } = ADMIN_USER
await testWithOtherConnection({ email, password }, async xo => {
const id = await xo.call('job.create', { job: defaultJob })
expect(typeof id).toBe('string')
const job = await xo.call('job.get', { id })
expect(job).toMatchSnapshot({
id: expect.any(String),
paramsVector: expect.any(Object),
userId: expect.any(String),
})
expect(job.paramsVector).toEqual(defaultJob.paramsVector)
expect(job.userId).toBe(userId)
await xo.call('job.delete', { id })
})
})
it('creates a job with a userId', async () => {
const userId = await xo.createTempUser(ADMIN_USER)
const id = await xo.createTempJob({ ...defaultJob, userId })
const { userId: expectedUserId } = await xo.call('job.get', { id })
expect(userId).toBe(expectedUserId)
})
it('fails trying to create a job without job params', async () => {
await expect(xo.createTempJob({})).rejects.toMatchSnapshot()
})
})
describe('.getAll() :', () => {
it('gets all available jobs', async () => {
const jobId1 = await xo.createTempJob(defaultJob)
const job2 = {
...defaultJob,
name: 'jobTest2',
paramsVector: {
type: 'crossProduct',
items: [
{
type: 'set',
values: [
{
id: config.vms.default,
name: 'test2-snapshot',
},
],
},
],
},
}
const jobId2 = await xo.createTempJob(job2)
let jobs = await xo.call('job.getAll')
expect(Array.isArray(jobs)).toBe(true)
jobs = keyBy(jobs, 'id')
const newJob1 = jobs[jobId1]
expect(newJob1).toMatchSnapshot({
id: expect.any(String),
paramsVector: expect.any(Object),
userId: expect.any(String),
})
expect(newJob1.paramsVector).toEqual(defaultJob.paramsVector)
const newJob2 = jobs[jobId2]
expect(newJob2).toMatchSnapshot({
id: expect.any(String),
paramsVector: expect.any(Object),
userId: expect.any(String),
})
expect(newJob2.paramsVector).toEqual(job2.paramsVector)
})
})
describe('.get() :', () => {
it('gets an existing job', async () => {
const id = await xo.createTempJob(defaultJob)
const job = await xo.call('job.get', { id })
expect(job).toMatchSnapshot({
id: expect.any(String),
paramsVector: expect.any(Object),
userId: expect.any(String),
})
expect(job.paramsVector).toEqual(defaultJob.paramsVector)
})
it('fails trying to get a job with a non existent id', async () => {
await expect(
xo.call('job.get', { id: 'non-existent-id' })
).rejects.toMatchSnapshot()
})
})
describe('.set() :', () => {
it('sets a job', async () => {
const id = await xo.createTempJob(defaultJob)
const job = {
id,
type: 'call',
key: 'snapshot',
method: 'vm.clone',
paramsVector: {
type: 'crossProduct',
items: [
{
type: 'set',
values: [
{
id: config.vms.default,
name: 'clone',
full_copy: true,
},
],
},
],
},
}
await xo.call('job.set', {
job,
})
const newJob = await xo.call('job.get', { id })
expect(newJob).toMatchSnapshot({
id: expect.any(String),
paramsVector: expect.any(Object),
userId: expect.any(String),
})
expect(newJob.paramsVector).toEqual(job.paramsVector)
})
it('fails trying to set a job without job.id', async () => {
await expect(xo.call('job.set', defaultJob)).rejects.toMatchSnapshot()
})
})
describe('.delete() :', () => {
it('deletes an existing job', async () => {
const id = await xo.call('job.create', { job: defaultJob })
const { id: scheduleId } = await xo.call('schedule.create', {
jobId: id,
cron: '* * * * * *',
enabled: false,
})
await xo.call('job.delete', { id })
await expect(xo.call('job.get', { id })).rejects.toMatchSnapshot()
await expect(
xo.call('schedule.get', { id: scheduleId })
).rejects.toMatchSnapshot()
})
it.skip('fails trying to delete a job with a non existent id', async () => {
await expect(
xo.call('job.delete', { id: 'non-existent-id' })
).rejects.toMatchSnapshot()
})
})
describe('.runSequence() :', () => {
let id
afterEach(async () => {
await xo
.call('vm.delete', { id, deleteDisks: true })
.catch(error => console.error(error))
})
it('runs a job', async () => {
const jobId = await xo.createTempJob(defaultJob)
const snapshots = xo.objects.all[config.vms.default].snapshots
await xo.call('job.runSequence', { idSequence: [jobId] })
await xo.waitObjectState(
config.vms.default,
({ snapshots: actualSnapshots }) => {
expect(actualSnapshots.length).toBe(snapshots.length + 1)
id = difference(actualSnapshots, snapshots)[0]
}
)
})
})
})

View File

@@ -0,0 +1,156 @@
/* eslint-env jest */
// Doc: https://github.com/moll/js-must/blob/master/doc/API.md#must
import expect from 'must'
// ===================================================================
import { getConfig, getMainConnection, getSrId, waitObjectState } from './util'
import { map, assign } from 'lodash'
import eventToPromise from 'event-to-promise'
// ===================================================================
describe('disk', () => {
let diskId
let diskIds = []
let serverId
let srId
let xo
// -----------------------------------------------------------------
beforeAll(async () => {
jest.setTimeout(10e3)
xo = await getMainConnection()
const config = await getConfig()
serverId = await xo.call(
'server.add',
assign({ autoConnect: false }, config.xenServer1)
)
await xo.call('server.connect', { id: serverId })
await eventToPromise(xo.objects, 'finish')
srId = await getSrId(xo)
})
// -----------------------------------------------------------------
afterEach(async () => {
await Promise.all(
map(diskIds, diskId => xo.call('vdi.delete', { id: diskId }))
)
diskIds = []
})
// -----------------------------------------------------------------
afterAll(async () => {
await xo.call('server.remove', { id: serverId })
})
// -----------------------------------------------------------------
async function createDisk(params) {
const id = await xo.call('disk.create', params)
diskIds.push(id)
return id
}
async function createDiskTest() {
const id = await createDisk({
name: 'diskTest',
size: '1GB',
sr: srId,
})
return id
}
// ===================================================================
describe('.create()', () => {
it('create a new disk on a SR', async () => {
diskId = await createDisk({
name: 'diskTest',
size: '1GB',
sr: srId,
})
await Promise.all([
waitObjectState(xo, diskId, disk => {
expect(disk.type).to.be.equal('VDI')
expect(disk.name_label).to.be.equal('diskTest')
// TODO: should not test an exact value but around 10%
expect(disk.size).to.be.equal(1000341504)
expect(disk.$SR).to.be.equal(srId)
}),
waitObjectState(xo, srId, sr => {
expect(sr.VDIs).include(diskId)
}),
])
})
})
// -------------------------------------------------------------------
describe('.delete()', () => {
beforeEach(async () => {
diskId = await createDiskTest()
})
it('deletes a disk', async () => {
await Promise.all([
xo.call('vdi.delete', { id: diskId }),
waitObjectState(xo, diskId, disk => {
expect(disk).to.be.undefined()
}),
waitObjectState(xo, srId, sr => {
expect(sr.VDIs).not.include(diskId)
}),
])
diskIds = []
})
})
// ---------------------------------------------------------------------
describe('.set()', () => {
beforeEach(async () => {
diskId = await createDiskTest()
})
it('set the name of the disk', async () => {
await xo.call('vdi.set', {
id: diskId,
name_label: 'disk2',
})
await waitObjectState(xo, diskId, disk => {
expect(disk.name_label).to.be.equal('disk2')
})
})
it('set the description of the disk', async () => {
await xo.call('vdi.set', {
id: diskId,
name_description: 'description',
})
await waitObjectState(xo, diskId, disk => {
expect(disk.name_description).to.be.equal('description')
})
})
it.skip('set the size of the disk', async () => {
await xo.getOrWaitObject(diskId)
await xo.call('vdi.set', {
id: diskId,
size: '5MB',
})
await waitObjectState(xo, diskId, disk => {
expect(disk.size).to.be.equal(6291456)
})
})
})
})

View File

@@ -0,0 +1,59 @@
/* eslint-env jest */
// Doc: https://github.com/moll/js-must/blob/master/doc/API.md#must
// import expect from 'must'
// ===================================================================
// import {getConnection} from './util'
// ===================================================================
describe('docker', () => {
// let xo
// beforeAll(async () => {
// xo = await getConnection()
// })
// ===================================================================
describe('.register()', async () => {
it('registers the VM for Docker management')
})
// -------------------------------------------------------------------
describe('.deregister()', async () => {
it('deregister the VM for Docker management')
})
// -------------------------------------------------------------------
describe('.start()', async () => {
it('starts the Docker')
})
// -------------------------------------------------------------------
describe('.stop()', async () => {
it('stops the Docker')
})
// -------------------------------------------------------------------
describe('.restart()', async () => {
it('restarts the Docker')
})
// -------------------------------------------------------------------
describe('.pause()', async () => {
it('pauses the Docker')
})
// -------------------------------------------------------------------
describe('.unpause()', async () => {
it('unpauses the Docker')
})
})

View File

@@ -0,0 +1,377 @@
/* eslint-env jest */
import { find, map } from 'lodash'
import { createUser, deleteUsers, getUser, xo } from './util.js'
// ===================================================================
describe('group', () => {
const userIds = []
const groupIds = []
// -----------------------------------------------------------------
afterEach(async () => {
await Promise.all(map(groupIds, id => xo.call('group.delete', { id })))
// Deleting users must be done AFTER deleting the group
// because there is a race condition in xo-server
// which cause some users to not be properly deleted.
// The test “delete the group with its users” highlight this issue.
await deleteUsers(xo, userIds)
userIds.length = groupIds.length = 0
})
// -----------------------------------------------------------------
async function createGroup(params) {
const groupId = await xo.call('group.create', params)
groupIds.push(groupId)
return groupId
}
// ----------------------------------------------------------------
function compareGroup(actual, expected) {
expect(actual.name).toEqual(expected.name)
expect(actual.id).toEqual(expected.id)
expect(actual.users).toEqual(expected.users)
}
// ----------------------------------------------------------------
function getAllGroups() {
return xo.call('group.getAll')
}
// ---------------------------------------------------------------
async function getGroup(id) {
const groups = await getAllGroups()
return find(groups, { id: id })
}
// =================================================================
describe('.create()', () => {
it('creates a group and return its id', async () => {
const groupId = await createGroup({
name: 'Avengers',
})
const group = await getGroup(groupId)
compareGroup(group, {
id: groupId,
name: 'Avengers',
users: [],
})
})
it.skip('does not create two groups with the same name', async () => {
await createGroup({
name: 'Avengers',
})
await createGroup({
name: 'Avengers',
}).then(
() => {
throw new Error('createGroup() should have thrown')
},
function(error) {
expect(error.message).to.match(/duplicate group/i)
}
)
})
})
// ------------------------------------------------------------------
describe('.delete()', () => {
let groupId
let userId1
let userId2
let userId3
beforeEach(async () => {
groupId = await xo.call('group.create', {
name: 'Avengers',
})
})
it('delete a group', async () => {
await xo.call('group.delete', {
id: groupId,
})
const group = await getGroup(groupId)
expect(group).toBeUndefined()
})
it.skip("erase the group from user's groups list", async () => {
// create user and add it to the group
const userId = await createUser(xo, userIds, {
email: 'tony.stark@stark_industry.com',
password: 'IronMan',
})
await xo.call('group.addUser', {
id: groupId,
userId: userId,
})
// delete the group
await xo.call('group.delete', { id: groupId })
const user = await getUser(userId)
expect(user.groups).toEqual([])
})
it.skip("erase the user from group's users list", async () => {
// create user and add it to the group
const userId = await createUser(xo, userIds, {
email: 'tony.stark@stark_industry.com',
password: 'IronMan',
})
await xo.call('group.addUser', {
id: groupId,
userId: userId,
})
// delete the group
await xo.call('user.delete', { id: userId })
const group = await getGroup(groupId)
expect(group.users).toEqual([])
})
// FIXME: some users are not properly deleted because of a race condition with group deletion.
it.skip('delete the group with its users', async () => {
// create users
;[userId1, userId2, userId3] = await Promise.all([
xo.call('user.create', {
email: 'tony.stark@stark_industry.com',
password: 'IronMan',
}),
xo.call('user.create', {
email: 'natasha.romanov@shield.com',
password: 'BlackWidow',
}),
xo.call('user.create', {
email: 'pietro.maximoff@shield.com',
password: 'QickSilver',
}),
])
await xo.call('group.setUsers', {
id: groupId,
userIds: [userId1, userId2, userId3],
})
// delete the group with his users
await Promise.all([
xo.call('group.delete', {
id: groupId,
}),
deleteUsers(xo, [userId1, userId2, userId3]),
])
const [group, user1, user2, user3] = await Promise.all([
getGroup(groupId),
getUser(xo, userId1),
getUser(xo, userId2),
getUser(xo, userId3),
])
expect(group).toBeUndefined()
expect(user1).toBeUndefined()
expect(user2).toBeUndefined()
expect(user3).toBeUndefined()
})
})
// -------------------------------------------------------------------
describe('.getAll()', () => {
it('returns an array', async () => {
const groups = await xo.call('group.getAll')
expect(groups).toBeInstanceOf(Array)
})
})
// -------------------------------------------------------------------
describe('.setUsers ()', () => {
let groupId
let userId1
let userId2
let userId3
beforeEach(async () => {
;[groupId, userId1, userId2, userId3] = await Promise.all([
createGroup({
name: 'Avengers',
}),
createUser(xo, userIds, {
email: 'tony.stark@stark_industry.com',
password: 'IronMan',
}),
createUser(xo, userIds, {
email: 'natasha.romanov@shield.com',
password: 'BlackWidow',
}),
createUser(xo, userIds, {
email: 'pietro.maximoff@shield.com',
password: 'QickSilver',
}),
])
})
it('can set users of a group', async () => {
// add two users on the group
await xo.call('group.setUsers', {
id: groupId,
userIds: [userId1, userId2],
})
{
const [group, user1, user2, user3] = await Promise.all([
getGroup(groupId),
getUser(xo, userId1),
getUser(xo, userId2),
getUser(xo, userId3),
])
compareGroup(group, {
id: groupId,
name: 'Avengers',
users: [userId1, userId2],
})
expect(user1.groups).toEqual([groupId])
expect(user2.groups).toEqual([groupId])
expect(user3.groups).toEqual([])
}
// change users of the group
await xo.call('group.setUsers', {
id: groupId,
userIds: [userId1, userId3],
})
{
const [group, user1, user2, user3] = await Promise.all([
getGroup(groupId),
getUser(xo, userId1),
getUser(xo, userId2),
getUser(xo, userId3),
])
compareGroup(group, {
id: groupId,
name: 'Avengers',
users: [userId1, userId3],
})
expect(user1.groups).toEqual([groupId])
expect(user2.groups).toEqual([])
expect(user3.groups).toEqual([groupId])
}
})
})
// -------------------------------------------------------------------
describe('.addUser()', () => {
let groupId
let userId
beforeEach(async () => {
;[groupId, userId] = await Promise.all([
createGroup({
name: 'Avengers',
}),
createUser(xo, userIds, {
email: 'tony.stark@stark_industry.com',
password: 'IronMan',
}),
])
})
it('adds a user id to a group', async () => {
await xo.call('group.addUser', {
id: groupId,
userId: userId,
})
const [group, user] = await Promise.all([
getGroup(groupId),
getUser(xo, userId),
])
compareGroup(group, {
id: groupId,
name: 'Avengers',
users: [userId],
})
expect(user.groups).toEqual([groupId])
})
})
// -------------------------------------------------------------------
describe('removeUser()', () => {
let groupId
let userId
beforeEach(async () => {
;[groupId, userId] = await Promise.all([
createGroup({
name: 'Avengers',
}),
createUser(xo, userIds, {
email: 'tony.stark@stark_industry.com',
password: 'IronMan',
}),
])
await xo.call('group.addUser', {
id: groupId,
userId: userId,
})
})
it('removes a user to a group', async () => {
await xo.call('group.removeUser', {
id: groupId,
userId: userId,
})
const [group, user] = await Promise.all([
getGroup(groupId),
getUser(xo, userId),
])
compareGroup(group, {
id: groupId,
name: 'Avengers',
users: [],
})
expect(user.groups).toEqual([])
})
})
// -------------------------------------------------------------------
describe('set()', () => {
let groupId
beforeEach(async () => {
groupId = await createGroup({
name: 'Avengers',
})
})
it('changes name of a group', async () => {
await xo.call('group.set', {
id: groupId,
name: 'Guardians of the Galaxy',
})
const group = await getGroup(groupId)
compareGroup(group, {
id: groupId,
name: 'Guardians of the Galaxy',
users: [],
})
})
})
})

View File

@@ -0,0 +1,239 @@
/* eslint-env jest */
// Doc: https://github.com/moll/js-must/blob/master/doc/API.md#must
import expect from 'must'
import eventToPromise from 'event-to-promise'
import {
getAllHosts,
getConfig,
getMainConnection,
getVmToMigrateId,
waitObjectState,
} from './util'
import { find, forEach } from 'lodash'
// ===================================================================
describe('host', () => {
let xo
let serverId
let hostId
// -----------------------------------------------------------------
beforeAll(async () => {
jest.setTimeout(10e3)
let config
;[xo, config] = await Promise.all([getMainConnection(), getConfig()])
serverId = await xo.call('server.add', config.xenServer2).catch(() => {})
await eventToPromise(xo.objects, 'finish')
hostId = getHost(config.host1)
})
// -------------------------------------------------------------------
afterAll(async () => {
await xo.call('server.remove', {
id: serverId,
})
})
// -------------------------------------------------------------------
function getHost(nameLabel) {
const hosts = getAllHosts(xo)
const host = find(hosts, { name_label: nameLabel })
return host.id
}
// ===================================================================
describe('.set()', () => {
let nameLabel
let nameDescription
beforeEach(async () => {
// get values to set them at the end of the test
const host = xo.objects.all[hostId]
nameLabel = host.name_label
nameDescription = host.name_description
})
afterEach(async () => {
await xo.call('host.set', {
id: hostId,
name_label: nameLabel,
name_description: nameDescription,
})
})
it('changes properties of the host', async () => {
await xo.call('host.set', {
id: hostId,
name_label: 'labTest',
name_description: 'description',
})
await waitObjectState(xo, hostId, host => {
expect(host.name_label).to.be.equal('labTest')
expect(host.name_description).to.be.equal('description')
})
})
})
// ------------------------------------------------------------------
describe('.restart()', () => {
jest.setTimeout(330e3)
it('restart the host', async () => {
await xo.call('host.restart', { id: hostId })
await waitObjectState(xo, hostId, host => {
expect(host.current_operations)
})
await waitObjectState(xo, hostId, host => {
expect(host.power_state).to.be.equal('Halted')
})
await waitObjectState(xo, hostId, host => {
expect(host.power_state).to.be.equal('Running')
})
})
})
// ------------------------------------------------------------------
describe('.restartAgent()', () => {
it('restart a Xen agent on the host')
})
// ------------------------------------------------------------------
describe('.start()', () => {
jest.setTimeout(300e3)
beforeEach(async () => {
try {
await xo.call('host.stop', { id: hostId })
} catch (_) {}
// test if the host is shutdown
await waitObjectState(xo, hostId, host => {
expect(host.power_state).to.be.equal('Halted')
})
})
it('start the host', async () => {
await xo.call('host.start', { id: hostId })
await waitObjectState(xo, hostId, host => {
expect(host.power_state).to.be.equal('Running')
})
})
})
// ------------------------------------------------------------------
describe('.stop()', () => {
jest.setTimeout(300e3)
let vmId
beforeAll(async () => {
vmId = await getVmToMigrateId(xo)
try {
await xo.call('vm.start', { id: vmId })
} catch (_) {}
try {
await xo.call('vm.migrate', {
vm: vmId,
host: hostId,
})
} catch (_) {}
})
afterEach(async () => {
await xo.call('host.start', { id: hostId })
})
it('stop the host and shutdown its VMs', async () => {
await xo.call('host.stop', { id: hostId })
await Promise.all([
waitObjectState(xo, vmId, vm => {
expect(vm.$container).not.to.be.equal(hostId)
expect(vm.power_state).to.be.equal('Halted')
}),
waitObjectState(xo, hostId, host => {
expect(host.power_state).to.be.equal('Halted')
}),
])
})
})
// ------------------------------------------------------------------
describe('.detach()', () => {
it('ejects the host of a pool')
})
// ------------------------------------------------------------------
describe('.disable(), ', () => {
afterEach(async () => {
await xo.call('host.enable', {
id: hostId,
})
})
it('disables to create VM on the host', async () => {
await xo.call('host.disable', { id: hostId })
await waitObjectState(xo, hostId, host => {
expect(host.enabled).to.be.false()
})
})
})
// ------------------------------------------------------------------
describe('.enable()', async () => {
beforeEach(async () => {
await xo.call('host.disable', { id: hostId })
})
it('enables to create VM on the host', async () => {
await xo.call('host.enable', { id: hostId })
await waitObjectState(xo, hostId, host => {
expect(host.enabled).to.be.true()
})
})
})
// -----------------------------------------------------------------
describe('.createNetwork()', () => {
it('create a network')
})
// -----------------------------------------------------------------
describe('.listMissingPatches()', () => {
it('returns an array of missing patches in the host')
it('returns a empty array if up-to-date')
})
// ------------------------------------------------------------------
describe('.installPatch()', () => {
it('installs a patch patch on the host')
})
// ------------------------------------------------------------------
describe('.stats()', () => {
it('returns an array with statistics of the host', async () => {
const stats = await xo.call('host.stats', {
host: hostId,
})
expect(stats).to.be.an.object()
forEach(stats, function(array, key) {
expect(array).to.be.an.array()
})
})
})
})

View File

@@ -0,0 +1,79 @@
/* eslint-env jest */
// Doc: https://github.com/moll/js-must/blob/master/doc/API.md#must
import expect from 'must'
// ===================================================================
import { getConfig, getMainConnection, waitObjectState } from './util'
import eventToPromise from 'event-to-promise'
import { find } from 'lodash'
// ===================================================================
describe('pool', () => {
let xo
let serverId
let poolId
let config
beforeAll(async () => {
jest.setTimeout(10e3)
;[xo, config] = await Promise.all([getMainConnection(), getConfig()])
serverId = await xo.call('server.add', config.xenServer1).catch(() => {})
await eventToPromise(xo.objects, 'finish')
poolId = getPoolId()
})
// -------------------------------------------------------------------
afterAll(async () => {
await xo.call('server.remove', {
id: serverId,
})
})
// -----------------------------------------------------------------
function getPoolId() {
const pools = xo.objects.indexes.type.pool
const pool = find(pools, { name_label: config.pool.name_label })
return pool.id
}
// ===================================================================
describe('.set()', () => {
afterEach(async () => {
await xo.call('pool.set', {
id: poolId,
name_label: config.pool.name_label,
name_description: '',
})
})
it.skip('set pool parameters', async () => {
await xo.call('pool.set', {
id: poolId,
name_label: 'nameTest',
name_description: 'description',
})
await waitObjectState(xo, poolId, pool => {
expect(pool.name_label).to.be.equal('nameTest')
expect(pool.name_description).to.be.equal('description')
})
})
})
// ------------------------------------------------------------------
describe('.installPatch()', () => {
it('install a patch on the pool')
})
// -----------------------------------------------------------------
describe('handlePatchUpload()', () => {
it('')
})
})

View File

@@ -0,0 +1,33 @@
/* eslint-env jest */
import { xo } from './util'
// ===================================================================
describe('role', () => {
describe('.getAll()', () => {
it(' returns all the roles', async () => {
const role = await xo.call('role.getAll')
// FIXME: use permutationOf but figure out how not to compare objects by
// equality.
expect(role).toEqual([
{
id: 'viewer',
name: 'Viewer',
permissions: ['view'],
},
{
id: 'operator',
name: 'Operator',
permissions: ['view', 'operate'],
},
{
id: 'admin',
name: 'Admin',
permissions: ['view', 'operate', 'administrate'],
},
])
})
})
})

View File

@@ -0,0 +1,149 @@
/* eslint-env jest */
// Doc: https://github.com/moll/js-must/blob/master/doc/API.md#must
import expect from 'must'
// ===================================================================
import {
getConfig,
getMainConnection,
getSchedule,
jobTest,
scheduleTest,
} from './util'
import eventToPromise from 'event-to-promise'
import { map } from 'lodash'
// ===================================================================
describe('schedule', () => {
let xo
let serverId
let scheduleIds = []
let jobId
beforeAll(async () => {
jest.setTimeout(10e3)
let config
;[xo, config] = await Promise.all([getMainConnection(), getConfig()])
serverId = await xo.call('server.add', config.xenServer1).catch(() => {})
await eventToPromise(xo.objects, 'finish')
jobId = await jobTest(xo)
})
// -----------------------------------------------------------------
afterAll(async () => {
await Promise.all([
xo.call('job.delete', { id: jobId }),
xo.call('server.remove', { id: serverId }),
])
})
// -----------------------------------------------------------------
afterEach(async () => {
await Promise.all(
map(scheduleIds, scheduleId =>
xo.call('schedule.delete', { id: scheduleId })
)
)
scheduleIds = []
})
// -----------------------------------------------------------------
async function createSchedule(params) {
const schedule = await xo.call('schedule.create', params)
scheduleIds.push(schedule.id)
return schedule
}
async function createScheduleTest() {
const schedule = await scheduleTest(xo, jobId)
scheduleIds.push(schedule.id)
return schedule
}
// =================================================================
describe('.getAll()', () => {
it('gets all existing schedules', async () => {
const schedules = await xo.call('schedule.getAll')
expect(schedules).to.be.an.array()
})
})
// -----------------------------------------------------------------
describe('.get()', () => {
let scheduleId
beforeAll(async () => {
scheduleId = (await createScheduleTest()).id
})
it('gets an existing schedule', async () => {
const schedule = await xo.call('schedule.get', { id: scheduleId })
expect(schedule.job).to.be.equal(jobId)
expect(schedule.cron).to.be.equal('* * * * * *')
expect(schedule.enabled).to.be.false()
})
})
// -----------------------------------------------------------------
describe('.create()', () => {
it('creates a new schedule', async () => {
const schedule = await createSchedule({
jobId: jobId,
cron: '* * * * * *',
enabled: true,
})
expect(schedule.job).to.be.equal(jobId)
expect(schedule.cron).to.be.equal('* * * * * *')
expect(schedule.enabled).to.be.true()
})
})
// -----------------------------------------------------------------
describe('.set()', () => {
let scheduleId
beforeAll(async () => {
scheduleId = (await createScheduleTest()).id
})
it('modifies an existing schedule', async () => {
await xo.call('schedule.set', {
id: scheduleId,
cron: '2 * * * * *',
})
const schedule = await getSchedule(xo, scheduleId)
expect(schedule.cron).to.be.equal('2 * * * * *')
})
})
// -----------------------------------------------------------------
describe('.delete()', () => {
let scheduleId
beforeEach(async () => {
scheduleId = (await createScheduleTest()).id
})
it('deletes an existing schedule', async () => {
await xo.call('schedule.delete', { id: scheduleId })
await getSchedule(xo, scheduleId).then(
() => {
throw new Error('getSchedule() should have thrown')
},
function(error) {
expect(error.message).to.match(/no such object/)
}
)
scheduleIds = []
})
})
})

View File

@@ -0,0 +1,82 @@
/* eslint-env jest */
// Doc: https://github.com/moll/js-must/blob/master/doc/API.md#must
import expect from 'must'
// ===================================================================
import {
jobTest,
scheduleTest,
getConfig,
getMainConnection,
getSchedule,
} from './util'
import eventToPromise from 'event-to-promise'
// ===================================================================
describe('scheduler', () => {
let xo
let serverId
let jobId
let scheduleId
beforeAll(async () => {
jest.setTimeout(10e3)
let config
;[xo, config] = await Promise.all([getMainConnection(), getConfig()])
serverId = await xo.call('server.add', config.xenServer1).catch(() => {})
await eventToPromise(xo.objects, 'finish')
jobId = await jobTest(xo)
scheduleId = (await scheduleTest(xo, jobId)).id
})
// -----------------------------------------------------------------
afterAll(async () => {
await Promise.all([
xo.call('schedule.delete', { id: scheduleId }),
xo.call('job.delete', { id: jobId }),
xo.call('server.remove', { id: serverId }),
])
})
// =================================================================
describe('.enable()', () => {
afterEach(async () => {
await xo.call('scheduler.disable', { id: scheduleId })
})
it.skip("enables a schedule to run it's job as scheduled", async () => {
await xo.call('scheduler.enable', { id: scheduleId })
const schedule = await getSchedule(xo, scheduleId)
expect(schedule.enabled).to.be.true()
})
})
// -----------------------------------------------------------------
describe('.disable()', () => {
beforeEach(async () => {
await xo.call('schedule.enable', { id: scheduleId })
})
it.skip('disables a schedule', async () => {
await xo.call('schedule.disable', { id: scheduleId })
const schedule = await getSchedule(xo, scheduleId)
expect(schedule.enabled).to.be.false()
})
})
// -----------------------------------------------------------------
describe('.getScheduleTable()', () => {
it('get a map of existing schedules', async () => {
const table = await xo.call('scheduler.getScheduleTable')
expect(table).to.be.an.object()
expect(table).to.match(scheduleId)
})
})
})

View File

@@ -0,0 +1,208 @@
/* eslint-env jest */
import { assign, find, map } from 'lodash'
import { config, rejectionOf, xo } from './util'
// ===================================================================
describe('server', () => {
let serverIds = []
afterEach(async () => {
await Promise.all(
map(serverIds, serverId => xo.call('server.remove', { id: serverId }))
)
serverIds = []
})
async function addServer(params) {
const serverId = await xo.call('server.add', params)
serverIds.push(serverId)
return serverId
}
function getAllServers() {
return xo.call('server.getAll')
}
async function getServer(id) {
const servers = await getAllServers()
return find(servers, { id: id })
}
// ==================================================================
describe('.add()', () => {
it('add a Xen server and return its id', async () => {
const serverId = await addServer({
host: 'xen1.example.org',
username: 'root',
password: 'password',
autoConnect: false,
})
const server = await getServer(serverId)
expect(typeof server.id).toBe('string')
expect(server).toEqual({
id: serverId,
host: 'xen1.example.org',
username: 'root',
status: 'disconnected',
})
})
it('does not add two servers with the same host', async () => {
await addServer({
host: 'xen1.example.org',
username: 'root',
password: 'password',
autoConnect: false,
})
expect(
(await rejectionOf(
addServer({
host: 'xen1.example.org',
username: 'root',
password: 'password',
autoConnect: false,
})
)).message
).toBe('unknown error from the peer')
})
it('set autoConnect true by default', async () => {
const serverId = await addServer(config.xenServer1)
const server = await getServer(serverId)
expect(server.id).toBe(serverId)
expect(server.host).toBe('192.168.100.3')
expect(server.username).toBe('root')
expect(server.status).toMatch(/^connect(?:ed|ing)$/)
})
})
// -----------------------------------------------------------------
describe('.remove()', () => {
let serverId
beforeEach(async () => {
serverId = await addServer({
host: 'xen1.example.org',
username: 'root',
password: 'password',
autoConnect: false,
})
})
it('remove a Xen server', async () => {
await xo.call('server.remove', {
id: serverId,
})
const server = await getServer(serverId)
expect(server).toBeUndefined()
})
})
// -----------------------------------------------------------------
describe('.getAll()', () => {
it('returns an array', async () => {
const servers = await xo.call('server.getAll')
expect(servers).toBeInstanceOf(Array)
})
})
// -----------------------------------------------------------------
describe('.set()', () => {
let serverId
beforeEach(async () => {
serverId = await addServer({
host: 'xen1.example.org',
username: 'root',
password: 'password',
autoConnect: false,
})
})
it('changes attributes of an existing server', async () => {
await xo.call('server.set', {
id: serverId,
username: 'root2',
})
const server = await getServer(serverId)
expect(server).toEqual({
id: serverId,
host: 'xen1.example.org',
username: 'root2',
status: 'disconnected',
})
})
})
// -----------------------------------------------------------------
describe('.connect()', () => {
jest.setTimeout(5e3)
it('connects to a Xen server', async () => {
const serverId = await addServer(
assign({ autoConnect: false }, config.xenServer1)
)
await xo.call('server.connect', {
id: serverId,
})
const server = await getServer(serverId)
expect(server).toEqual({
enabled: 'true',
id: serverId,
host: '192.168.100.3',
username: 'root',
status: 'connected',
})
})
it.skip('connect to a Xen server on a slave host', async () => {
const serverId = await addServer(config.slaveServer)
await xo.call('server.connect', { id: serverId })
const server = await getServer(serverId)
expect(server.status).toBe('connected')
})
})
// -----------------------------------------------------------------
describe('.disconnect()', () => {
jest.setTimeout(5e3)
let serverId
beforeEach(async () => {
serverId = await addServer(
assign({ autoConnect: false }, config.xenServer1)
)
await xo.call('server.connect', {
id: serverId,
})
})
it('disconnects to a Xen server', async () => {
await xo.call('server.disconnect', {
id: serverId,
})
const server = await getServer(serverId)
expect(server).toEqual({
id: serverId,
host: '192.168.100.3',
username: 'root',
status: 'disconnected',
})
})
})
})

View File

@@ -0,0 +1,53 @@
/* eslint-env jest */
import defer from 'golike-defer'
import { map } from 'lodash'
import { getConnection, rejectionOf, testConnection, xo } from './util.js'
// ===================================================================
describe('token', () => {
const tokens = []
afterAll(async () => {
await Promise.all(map(tokens, token => xo.call('token.delete', { token })))
})
async function createToken() {
const token = await xo.call('token.create')
tokens.push(token)
return token
}
// =================================================================
describe('.create()', () => {
it('creates a token string which can be used to sign in', async () => {
const token = await createToken()
await testConnection({ credentials: { token } })
})
})
// -------------------------------------------------------------------
describe('.delete()', () => {
it(
'deletes a token',
defer(async $defer => {
const token = await createToken()
const xo2 = await getConnection({ credentials: { token } })
$defer(() => xo2.close())
await xo2.call('token.delete', {
token,
})
expect(
(await rejectionOf(testConnection({ credentials: { token } }))).code
).toBe(3)
})
)
})
})

View File

@@ -0,0 +1,169 @@
/* eslint-env jest */
// Doc: https://github.com/moll/js-must/blob/master/doc/API.md#must
import expect from 'must'
// ===================================================================
import {
getConfig,
getMainConnection,
getVmXoTestPvId,
getOneHost,
waitObjectState,
} from './util'
import { assign, map } from 'lodash'
import eventToPromise from 'event-to-promise'
// ===================================================================
describe('vbd', () => {
let xo
let vbdId
let diskIds = []
let serverId
let vmId
// ------------------------------------------------------------------
beforeAll(async () => {
jest.setTimeout(10e3)
let config
;[xo, config] = await Promise.all([getMainConnection(), getConfig()])
serverId = await xo.call(
'server.add',
assign({ autoConnect: false }, config.xenServer1)
)
await xo.call('server.connect', { id: serverId })
await eventToPromise(xo.objects, 'finish')
vmId = await getVmXoTestPvId(xo)
try {
await xo.call('vm.start', { id: vmId })
} catch (_) {}
})
// -----------------------------------------------------------------
beforeEach(async () => {
jest.setTimeout(10e3)
vbdId = await createVbd()
})
// ------------------------------------------------------------------
afterEach(async () => {
await Promise.all(
map(diskIds, diskId => xo.call('vdi.delete', { id: diskId }))
)
diskIds = []
})
// ------------------------------------------------------------------
afterAll(async () => {
jest.setTimeout(5e3)
await Promise.all([
xo.call('vm.stop', { id: vmId }),
xo.call('server.remove', { id: serverId }),
])
})
// ------------------------------------------------------------------
async function createVbd() {
// Create disk
const pool = await xo.getOrWaitObject(getOneHost(xo).$poolId)
const diskId = await xo.call('disk.create', {
name: 'diskTest',
size: '1MB',
sr: pool.default_SR,
})
diskIds.push(diskId)
// Create VBD
await xo.call('vm.attachDisk', {
vm: vmId,
vdi: diskId,
})
const disk = await xo.waitObject(diskId)
return disk.$VBDs[0]
}
// =====================================================================
describe('.delete()', () => {
it('delete the VBD', async () => {
await xo.call('vbd.disconnect', { id: vbdId })
await xo.call('vbd.delete', { id: vbdId })
await waitObjectState(xo, vbdId, vbd => {
expect(vbd).to.be.undefined()
})
})
it('deletes the VBD only if it is deconnected', async () => {
await xo.call('vbd.delete', { id: vbdId }).then(
() => {
throw new Error('vbd.delete() should have thrown')
},
function(error) {
// TODO: check with Julien if it is ok
expect(error.message).to.match('unknown error from the peer')
}
)
await xo.call('vbd.disconnect', { id: vbdId })
})
})
// --------------------------------------------------------------------
describe('.disconnect()', () => {
it('disconnect the VBD', async () => {
await xo.call('vbd.disconnect', { id: vbdId })
await waitObjectState(xo, vbdId, vbd => {
expect(vbd.attached).to.be.false()
})
})
})
// -------------------------------------------------------------------
describe('.connect()', () => {
beforeEach(async () => {
await xo.call('vbd.disconnect', { id: vbdId })
})
afterEach(async () => {
await xo.call('vbd.disconnect', { id: vbdId })
})
it('connect the VBD', async () => {
await xo.call('vbd.connect', { id: vbdId })
await waitObjectState(xo, vbdId, vbd => {
expect(vbd.attached).to.be.true()
})
})
})
// ----------------------------------------------------------------
describe('.set()', () => {
afterEach(async () => {
await xo.call('vbd.disconnect', { id: vbdId })
})
// TODO: resolve problem with disconnect
it.skip('set the position of the VBD', async () => {
await xo.call('vbd.set', {
id: vbdId,
position: '10',
})
await waitObjectState(xo, vbdId, vbd => {
expect(vbd.position).to.be.equal('10')
})
})
})
})

View File

@@ -0,0 +1,133 @@
/* eslint-env jest */
// Doc: https://github.com/moll/js-must/blob/master/doc/API.md#must
import expect from 'must'
// ===================================================================
import {
getConfig,
getMainConnection,
getNetworkId,
waitObjectState,
getVmXoTestPvId,
} from './util'
import eventToPromise from 'event-to-promise'
import { map } from 'lodash'
// ===================================================================
describe('vif', () => {
let xo
let serverId
let vifIds = []
let vmId
let vifId
beforeAll(async () => {
jest.setTimeout(10e3)
let config
;[xo, config] = await Promise.all([getMainConnection(), getConfig()])
serverId = await xo.call('server.add', config.xenServer1).catch(() => {})
await eventToPromise(xo.objects, 'finish')
vmId = await getVmXoTestPvId(xo)
try {
await xo.call('vm.start', { id: vmId })
} catch (_) {}
})
// -------------------------------------------------------------------
beforeEach(async () => {
vifId = await createVif()
})
// -------------------------------------------------------------------
afterEach(async () => {
await Promise.all(
map(vifIds, vifId => xo.call('vif.delete', { id: vifId }))
)
vifIds = []
})
// -------------------------------------------------------------------
afterAll(async () => {
jest.setTimeout(5e3)
await xo.call('vm.stop', { id: vmId, force: true })
await xo.call('server.remove', { id: serverId })
})
// -------------------------------------------------------------------
async function createVif() {
const networkId = await getNetworkId(xo)
const vifId = await xo.call('vm.createInterface', {
vm: vmId,
network: networkId,
position: '1',
})
vifIds.push(vifId)
return vifId
}
// ===================================================================
describe('.delete()', () => {
it('deletes a VIF', async () => {
await xo.call('vif.disconnect', { id: vifId })
await xo.call('vif.delete', { id: vifId })
await waitObjectState(xo, vifId, vif => {
expect(vif).to.be.undefined()
})
vifIds = []
})
it('can not delete a VIF if it is connected', async () => {
await xo.call('vif.delete', { id: vifId }).then(
() => {
throw new Error('vif.delete() should have thrown')
},
function(error) {
expect(error.message).to.be.equal('unknown error from the peer')
}
)
await xo.call('vif.disconnect', { id: vifId })
})
})
// ----------------------------------------------------------------
describe('.disconnect()', () => {
it('disconnects a VIF', async () => {
await xo.call('vif.disconnect', { id: vifId })
await waitObjectState(xo, vifId, vif => {
expect(vif.attached).to.be.false()
})
})
})
// ----------------------------------------------------------------
describe('.connect()', () => {
beforeEach(async () => {
await xo.call('vif.disconnect', { id: vifId })
})
afterEach(async () => {
await xo.call('vif.disconnect', { id: vifId })
})
it('connects a VIF', async () => {
await xo.call('vif.connect', { id: vifId })
await waitObjectState(xo, vifId, vif => {
expect(vif.attached).to.be.true()
})
})
})
})

View File

@@ -0,0 +1,666 @@
/* eslint-env jest */
// Doc: https://github.com/moll/js-must/blob/master/doc/API.md#must
import expect from 'must'
// ===================================================================
import {
almostEqual,
getAllHosts,
getConfig,
getMainConnection,
getNetworkId,
getOneHost,
getSrId,
getVmToMigrateId,
getVmXoTestPvId,
waitObjectState,
} from './util'
import { map, find } from 'lodash'
import eventToPromise from 'event-to-promise'
// ===================================================================
describe('vm', () => {
let xo
let vmId
let vmIds = []
let serverId
let config
// ----------------------------------------------------------------------
beforeAll(async () => {
jest.setTimeout(10e3)
;[xo, config] = await Promise.all([getMainConnection(), getConfig()])
serverId = await xo.call('server.add', config.xenServer1).catch(() => {})
await eventToPromise(xo.objects, 'finish')
})
// ----------------------------------------------------------------------
afterEach(async () => {
jest.setTimeout(15e3)
await Promise.all(
map(vmIds, vmId => xo.call('vm.delete', { id: vmId, delete_disks: true }))
)
vmIds = []
})
// ---------------------------------------------------------------------
afterAll(async () => {
await xo.call('server.remove', {
id: serverId,
})
})
// ---------------------------------------------------------------------
async function createVm(params) {
const vmId = await xo.call('vm.create', params)
vmIds.push(vmId)
return vmId
}
async function createVmTest() {
const templateId = getTemplateId(config.templates.debian)
const vmId = await createVm({
name_label: 'vmTest',
template: templateId,
VIFs: [],
})
return vmId
}
// ------------------------------------------------------------------
// eslint-disable-next-line no-unused-vars
async function getCdVbdPosition(vmId) {
const vm = await xo.getOrWaitObject(vmId)
for (let i = 0; i < vm.$VBDs.length; i++) {
const vbd = await xo.getOrWaitObject(vm.$VBDs[i])
if (vbd.is_cd_drive === true) {
return vbd.id
}
}
}
function getHostOtherPool(vm) {
const hosts = getAllHosts(xo)
for (const id in hosts) {
if (hosts[id].$poolId !== vm.$poolId) {
return id
}
}
}
// eslint-disable-next-line no-unused-vars
function getIsoId() {
const vdis = xo.objects.indexes.type.VDI
const iso = find(vdis, { name_label: config.iso })
return iso.id
}
function getOtherHost(vm) {
const hosts = getAllHosts(xo)
for (const id in hosts) {
if (hosts[id].$poolId === vm.poolId) {
if (id !== vm.$container) {
return id
}
}
}
}
function getTemplateId(nameTemplate) {
const templates = xo.objects.indexes.type['VM-template']
const template = find(templates, { name_label: nameTemplate })
return template.id
}
// =================================================================
describe('.create()', () => {
it('creates a VM with only a name and a template', async () => {
const templateId = getTemplateId(config.templates.debian)
vmId = await createVm({
name_label: 'vmTest',
template: templateId,
VIFs: [],
})
await waitObjectState(xo, vmId, vm => {
expect(vm.id).to.be.a.string()
expect(vm).to.be.an.object()
})
})
describe('.createHVM()', () => {
let srId
let templateId
beforeAll(async () => {
srId = await getSrId(xo)
templateId = getTemplateId(config.templates.otherConfig)
})
it.skip('creates a VM with the Other Config template, three disks, two interfaces and a ISO mounted', async () => {
jest.setTimeout(30e3)
const networkId = await getNetworkId(xo)
vmId = await createVm({
name_label: 'vmTest',
template: templateId,
VIFs: [{ network: networkId }, { network: networkId }],
VDIs: [
{ device: '0', size: 1, SR: srId, type: 'user' },
{ device: '1', size: 1, SR: srId, type: 'user' },
{ device: '2', size: 1, SR: srId, type: 'user' },
],
})
await waitObjectState(xo, vmId, vm => {
expect(vm.name_label).to.be.equal('vmTest')
expect(vm.other.base_template_name).to.be.equal(
config.templates.otherConfig
)
expect(vm.VIFs).to.have.length(2)
expect(vm.$VBDs).to.have.length(3)
})
})
it.skip('creates a VM with the Other Config template, no disk, no network and a ISO mounted', async () => {
vmId = await createVm({
name_label: 'vmTest',
template: templateId,
VIFs: [],
})
await waitObjectState(xo, vmId, vm => {
expect(vm.other.base_template_name).to.be.equal(
config.templates.otherConfig
)
expect(vm.VIFs).to.have.length(0)
expect(vm.$VBDs).to.have.length(0)
})
})
})
describe('.createPV()', () => {
let srId
let templateId
let networkId
beforeAll(async () => {
;[networkId, srId] = await Promise.all([getNetworkId(xo), getSrId(xo)])
})
it.skip('creates a VM with the Debian 7 64 bits template, network install, one disk, one network', async () => {
templateId = getTemplateId(config.templates.debian)
vmId = await createVm({
name_label: 'vmTest',
template: templateId,
VIFs: [{ network: networkId }],
VDIs: [
{
device: '0',
size: 1,
SR: srId,
type: 'user',
},
],
})
await waitObjectState(xo, vmId, vm => {
expect(vm.other.base_template_name).to.be.equal(
config.templates.debian
)
expect(vm.VIFs).to.have.length(1)
expect(vm.$VBDs).to.have.length(1)
})
})
it('creates a VM with the CentOS 7 64 bits template, two disks, two networks and a ISO mounted', async () => {
jest.setTimeout(10e3)
templateId = getTemplateId(config.templates.centOS)
vmId = await createVm({
name_label: 'vmTest',
template: templateId,
VIFs: [{ network: networkId }, { network: networkId }],
VDIs: [
{ device: '0', size: 1, SR: srId, type: 'user' },
{ device: '1', size: 1, SR: srId, type: 'user' },
],
})
await waitObjectState(xo, vmId, vm => {
expect(vm.other.base_template_name).to.be.equal(
config.templates.centOS
)
expect(vm.VIFs).to.have.length(2)
expect(vm.$VBDs).to.have.length(2)
})
})
})
})
// ------------------------------------------------------------------
describe('.delete()', () => {
let snapshotIds = []
let diskIds = []
beforeEach(async () => {
vmId = await createVmTest()
})
afterAll(async () => {
await Promise.all(
map(snapshotIds, snapshotId =>
xo.call('vm.delete', { id: snapshotId })
),
map(diskIds, diskId => xo.call('vdi.delete', { id: diskId }))
)
})
it('deletes a VM', async () => {
await xo.call('vm.delete', {
id: vmId,
delete_disks: true,
})
await waitObjectState(xo, vmId, vm => {
expect(vm).to.be.undefined()
})
vmIds = []
})
it('deletes a VM and its snapshots', async () => {
const snapshotId = await xo.call('vm.snapshot', {
id: vmId,
name: 'snapshot',
})
snapshotIds.push(snapshotId)
await xo.call('vm.delete', {
id: vmId,
delete_disks: true,
})
vmIds = []
await waitObjectState(xo, snapshotId, snapshot => {
expect(snapshot).to.be.undefined()
})
snapshotIds = []
})
it('deletes a VM and its disks', async () => {
jest.setTimeout(5e3)
// create disk
const host = getOneHost(xo)
const pool = await xo.getOrWaitObject(host.$poolId)
const diskId = await xo.call('disk.create', {
name: 'diskTest',
size: '1GB',
sr: pool.default_SR,
})
diskIds.push(diskId)
// attach the disk on the VM
await xo.call('vm.attachDisk', {
vm: vmId,
vdi: diskId,
})
// delete the VM
await xo.call('vm.delete', {
id: vmId,
delete_disks: true,
})
vmIds = []
await waitObjectState(xo, diskId, disk => {
expect(disk).to.be.undefined()
})
diskIds = []
})
// TODO: do a copy of the ISO
it.skip('deletes a vm but not delete its ISO', async () => {
vmId = await createVmTest()
await xo.call('vm.insertCd', {
id: vmId,
cd_id: '1169eb8a-d43f-4daf-a0ca-f3434a4bf301',
force: false,
})
await xo.call('vm.delete', {
id: vmId,
delete_disks: true,
})
waitObjectState(xo, '1169eb8a-d43f-4daf-a0ca-f3434a4bf301', iso => {
expect(iso).not.to.be.undefined()
})
})
})
// -------------------------------------------------------------------
describe('.migrate', () => {
jest.setTimeout(15e3)
let secondServerId
let startHostId
let hostId
beforeAll(async () => {
secondServerId = await xo
.call('server.add', config.xenServer2)
.catch(() => {})
await eventToPromise(xo.objects, 'finish')
vmId = await getVmToMigrateId(xo)
try {
await xo.call('vm.start', { id: vmId })
} catch (_) {}
})
beforeEach(async () => {
const vm = await xo.getOrWaitObject(vmId)
startHostId = vm.$container
hostId = getOtherHost(vm)
})
afterEach(async () => {
await xo.call('vm.migrate', {
id: vmId,
host_id: startHostId,
})
})
afterAll(async () => {
await xo.call('server.remove', {
id: secondServerId,
})
})
it('migrates the VM on an other host', async () => {
await xo.call('vm.migrate', {
id: vmId,
host_id: hostId,
})
await waitObjectState(xo, vmId, vm => {
expect(vm.$container).to.be.equal(hostId)
})
})
})
// -------------------------------------------------------------------
describe('.migratePool()', () => {
jest.setTimeout(100e3)
let hostId
let secondServerId
let startHostId
beforeAll(async () => {
secondServerId = await xo
.call('server.add', config.xenServer2)
.catch(() => {})
await eventToPromise(xo.objects, 'finish')
vmId = await getVmToMigrateId(xo)
try {
await xo.call('vm.start', { id: vmId })
} catch (_) {}
})
afterAll(async () => {
await xo.call('server.remove', { id: secondServerId })
})
beforeEach(async () => {
const vm = await xo.getOrWaitObject(vmId)
startHostId = vm.$container
hostId = getHostOtherPool(xo, vm)
})
afterEach(async () => {
// TODO: try to get the vmId
vmId = await getVmToMigrateId(xo)
await xo.call('vm.migrate_pool', {
id: vmId,
target_host_id: startHostId,
})
})
it.skip('migrates the VM on an other host which is in an other pool', async () => {
await xo.call('vm.migrate_pool', {
id: vmId,
target_host_id: hostId,
})
await waitObjectState(xo, vmId, vm => {
expect(vm).to.be.undefined()
})
})
})
// --------------------------------------------------------------------
describe('.clone()', () => {
beforeEach(async () => {
vmId = await createVmTest()
})
it('clones a VM', async () => {
const cloneId = await xo.call('vm.clone', {
id: vmId,
name: 'clone',
full_copy: true,
})
// push cloneId in vmIds array to delete the VM after test
vmIds.push(cloneId)
const [vm, clone] = await Promise.all([
xo.getOrWaitObject(vmId),
xo.getOrWaitObject(cloneId),
])
expect(clone.type).to.be.equal('VM')
expect(clone.name_label).to.be.equal('clone')
almostEqual(clone, vm, ['name_label', 'ref', 'id', 'other.mac_seed'])
})
})
// --------------------------------------------------------------------
describe('.convert()', () => {
beforeEach(async () => {
vmId = await createVmTest()
})
it('converts a VM', async () => {
await xo.call('vm.convert', { id: vmId })
await waitObjectState(xo, vmId, vm => {
expect(vm.type).to.be.equal('VM-template')
})
})
})
// ---------------------------------------------------------------------
describe('.revert()', () => {
jest.setTimeout(5e3)
let snapshotId
beforeEach(async () => {
vmId = await createVmTest()
snapshotId = await xo.call('vm.snapshot', {
id: vmId,
name: 'snapshot',
})
})
afterEach(async () => {
await xo.call('vm.delete', { id: snapshotId })
})
it('reverts a snapshot to its parent VM', async () => {
const revert = await xo.call('vm.revert', { id: snapshotId })
expect(revert).to.be.true()
})
})
// ---------------------------------------------------------------------
describe('.handleExport()', () => {
it('')
})
// --------------------------------------------------------------------
describe('.import()', () => {
it('')
})
// ---------------------------------------------------------------------
describe('.attachDisk()', () => {
jest.setTimeout(5e3)
let diskId
beforeEach(async () => {
vmId = await createVmTest()
const srId = await getSrId(xo)
diskId = await xo.call('disk.create', {
name: 'diskTest',
size: '1GB',
sr: srId,
})
})
afterEach(async () => {
await xo.call('vdi.delete', { id: diskId })
})
it('attaches the disk to the VM with attributes by default', async () => {
await xo.call('vm.attachDisk', {
vm: vmId,
vdi: diskId,
})
const vm = await xo.waitObject(vmId)
await waitObjectState(xo, diskId, disk => {
expect(disk.$VBDs).to.be.eql(vm.$VBDs)
})
await waitObjectState(xo, vm.$VBDs, vbd => {
expect(vbd.type).to.be.equal('VBD')
// expect(vbd.attached).to.be.true()
expect(vbd.bootable).to.be.false()
expect(vbd.is_cd_drive).to.be.false()
expect(vbd.position).to.be.equal('0')
expect(vbd.read_only).to.be.false()
expect(vbd.VDI).to.be.equal(diskId)
expect(vbd.VM).to.be.equal(vmId)
expect(vbd.$poolId).to.be.equal(vm.$poolId)
})
})
it('attaches the disk to the VM with specified attributes', async () => {
await xo.call('vm.attachDisk', {
vm: vmId,
vdi: diskId,
bootable: true,
mode: 'RO',
position: '2',
})
const vm = await xo.waitObject(vmId)
await waitObjectState(xo, vm.$VBDs, vbd => {
expect(vbd.type).to.be.equal('VBD')
// expect(vbd.attached).to.be.true()
expect(vbd.bootable).to.be.true()
expect(vbd.is_cd_drive).to.be.false()
expect(vbd.position).to.be.equal('2')
expect(vbd.read_only).to.be.true()
expect(vbd.VDI).to.be.equal(diskId)
expect(vbd.VM).to.be.equal(vmId)
expect(vbd.$poolId).to.be.equal(vm.$poolId)
})
})
})
// ---------------------------------------------------------------------
describe('.createInterface()', () => {
let vifId
let networkId
beforeAll(async () => {
vmId = await getVmXoTestPvId(xo)
networkId = await getNetworkId(xo)
})
afterEach(async () => {
await xo.call('vif.delete', { id: vifId })
})
it('create a VIF between the VM and the network', async () => {
vifId = await xo.call('vm.createInterface', {
vm: vmId,
network: networkId,
position: '1',
})
await waitObjectState(xo, vifId, vif => {
expect(vif.type).to.be.equal('VIF')
// expect(vif.attached).to.be.true()
expect(vif.$network).to.be.equal(networkId)
expect(vif.$VM).to.be.equal(vmId)
expect(vif.device).to.be.equal('1')
})
})
it('can not create two interfaces on the same device', async () => {
vifId = await xo.call('vm.createInterface', {
vm: vmId,
network: networkId,
position: '1',
})
await xo
.call('vm.createInterface', {
vm: vmId,
network: networkId,
position: '1',
})
.then(
() => {
throw new Error('createInterface() sould have trown')
},
function(error) {
expect(error.message).to.be.equal('unknown error from the peer')
}
)
})
})
// ---------------------------------------------------------------------
describe('.stats()', () => {
jest.setTimeout(20e3)
beforeAll(async () => {
vmId = await getVmXoTestPvId(xo)
})
beforeEach(async () => {
await xo.call('vm.start', { id: vmId })
})
afterEach(async () => {
await xo.call('vm.stop', {
id: vmId,
force: true,
})
})
it('returns an array with statistics of the VM', async () => {
const stats = await xo.call('vm.stats', { id: vmId })
expect(stats).to.be.an.object()
})
})
// ---------------------------------------------------------------------
describe('.bootOrder()', () => {
it('')
})
})

View File

@@ -0,0 +1,126 @@
/* eslint-env jest */
import {
config,
getOrWaitCdVbdPosition,
rejectionOf,
waitObjectState,
xo,
} from './../util'
// ===================================================================
beforeAll(async () => {
jest.setTimeout(20e3)
})
describe('cd', () => {
let vmId
// ----------------------------------------------------------------------
beforeAll(async () => {
vmId = await xo.call('vm.create', {
name_label: 'vmTest',
template: config.templatesId.debian,
})
await waitObjectState(xo, vmId, vm => {
if (vm.type !== 'VM') throw new Error('retry')
})
})
afterAll(() => xo.call('vm.delete', { id: vmId }))
// ===================================================================
describe('.insertCd()', () => {
afterEach(() => xo.call('vm.ejectCd', { id: vmId }))
it('mount an ISO on the VM (force: false)', async () => {
await xo.call('vm.insertCd', {
id: vmId,
cd_id: config.windowsIsoId,
force: false,
})
const vbdId = await getOrWaitCdVbdPosition(vmId)
await waitObjectState(xo, vbdId, vbd => {
expect(vbd.VDI).toBe(config.windowsIsoId)
expect(vbd.is_cd_drive).toBeTruthy()
expect(vbd.position).toBe('3')
})
})
it('mount an ISO on the VM (force: false) which has already a CD in the VBD', async () => {
await xo.call('vm.insertCd', {
id: vmId,
cd_id: config.windowsIsoId,
force: false,
})
await getOrWaitCdVbdPosition(vmId)
expect(
(await rejectionOf(
xo.call('vm.insertCd', {
id: vmId,
cd_id: config.ubuntuIsoId,
force: false,
})
)).message
).toBe('unknown error from the peer')
})
it('mount an ISO on the VM (force: true) which has already a CD in the VBD', async () => {
await xo.call('vm.insertCd', {
id: vmId,
cd_id: config.windowsIsoId,
force: true,
})
const vbdId = await getOrWaitCdVbdPosition(vmId)
await xo.call('vm.insertCd', {
id: vmId,
cd_id: config.ubuntuIsoId,
force: true,
})
await waitObjectState(xo, vbdId, vbd => {
expect(vbd.VDI).toBe(config.ubuntuIsoId)
expect(vbd.is_cd_drive).toBeTruthy()
expect(vbd.position).toBe('3')
})
})
it("mount an ISO on a VM which do not have already cd's VBD", async () => {
await xo.call('vm.insertCd', {
id: vmId,
cd_id: config.windowsIsoId,
force: false,
})
await waitObjectState(xo, vmId, async vm => {
expect(vm.$VBDs).toHaveLength(1)
const vbd = await xo.getOrWaitObject(vm.$VBDs)
expect(vbd.is_cd_drive).toBeTruthy()
expect(vbd.position).toBe('3')
})
})
})
describe('.ejectCd()', () => {
it('ejects an ISO', async () => {
await xo.call('vm.insertCd', {
id: vmId,
cd_id: config.windowsIsoId,
force: false,
})
const vbdId = await getOrWaitCdVbdPosition(vmId)
await xo.call('vm.ejectCd', { id: vmId })
await waitObjectState(xo, vbdId, vbd => {
expect(vbd.VDI).toBeNull()
})
})
})
})

View File

@@ -0,0 +1,268 @@
/* eslint-env jest */
import { map, size } from 'lodash'
import { config, rejectionOf, waitObjectState, xo } from './../util'
// ===================================================================
beforeAll(async () => {
jest.setTimeout(150e3)
})
describe('the VM life cyle', () => {
const vmsToDelete = []
// hvm with tools behave like pv vm
let hvmWithToolsId
let hvmWithoutToolsId
// ----------------------------------------------------------------------
beforeAll(async () => {
hvmWithToolsId = await xo.call('vm.create', {
name_label: 'vmTest-updateState',
template: config.templatesId.debianCloud,
VIFs: [{ network: config.labPoolNetworkId }],
VDIs: [
{
device: '0',
size: 1,
SR: config.labPoolSrId,
type: 'user',
},
],
})
vmsToDelete.push(hvmWithToolsId)
await waitObjectState(xo, hvmWithToolsId, vm => {
if (vm.type !== 'VM') throw new Error('retry')
})
hvmWithoutToolsId = await xo.call('vm.create', {
name_label: 'vmTest-updateState',
template: config.templatesId.centOS,
VIFs: [{ network: config.labPoolNetworkId }],
VDIs: [
{
device: '0',
size: 1,
SR: config.labPoolSrId,
type: 'user',
},
],
})
vmsToDelete.push(hvmWithoutToolsId)
await waitObjectState(xo, hvmWithoutToolsId, vm => {
if (vm.type !== 'VM') throw new Error('retry')
})
})
afterAll(async () => {
await Promise.all(
map(vmsToDelete, id =>
xo
.call('vm.delete', { id, delete_disks: true })
.catch(error => console.error(error))
)
)
vmsToDelete.length = 0
})
// =================================================================
describe('.start()', () => {
it('starts a VM', async () => {
await xo.call('vm.start', { id: hvmWithToolsId })
await waitObjectState(xo, hvmWithToolsId, vm => {
expect(size(vm.current_operations)).toBe(0)
expect(vm.power_state).toBe('Running')
expect(vm.startTime).not.toBe(0)
expect(vm.xenTools).not.toBeFalsy()
})
})
})
describe('.sets() on a running VM', () => {
it('sets VM parameters', async () => {
await xo.call('vm.set', {
id: hvmWithToolsId,
name_label: 'startedVmRenamed',
name_description: 'test started vm',
high_availability: true,
CPUs: 1,
memoryMin: 260e6,
})
await waitObjectState(xo, hvmWithToolsId, vm => {
expect(vm.name_label).toBe('startedVmRenamed')
expect(vm.name_description).toBe('test started vm')
expect(vm.high_availability).toBeTruthy()
expect(vm.CPUs.number).toBe(1)
expect(vm.memory.dynamic[0]).toBe(260e6)
})
})
})
describe('.restart()', () => {
it('restarts a VM (clean reboot)', async () => {
await xo.call('vm.restart', {
id: hvmWithToolsId,
force: false,
})
await waitObjectState(xo, hvmWithToolsId, vm => {
expect(size(vm.current_operations)).toBe(0)
expect(vm.power_state).toBe('Running')
expect(vm.startTime).not.toBe(0)
expect(vm.xenTools).not.toBeFalsy()
})
})
it('restarts a VM without PV drivers(clean reboot)', async () => {
await xo.call('vm.start', { id: hvmWithoutToolsId })
await waitObjectState(xo, hvmWithoutToolsId, vm => {
if (size(vm.current_operations) !== 0 || vm.power_state !== 'Running')
throw new Error('retry')
})
expect(
(await rejectionOf(
xo.call('vm.restart', {
id: hvmWithoutToolsId,
force: false,
})
)).message
).toBe('VM lacks feature shutdown')
})
it('restarts a VM (hard reboot)', async () => {
await xo.call('vm.restart', {
id: hvmWithToolsId,
force: true,
})
await waitObjectState(xo, hvmWithToolsId, vm => {
expect(size(vm.current_operations)).toBe(0)
expect(vm.power_state).toBe('Running')
expect(vm.startTime).not.toBe(0)
expect(vm.xenTools).not.toBeFalsy()
})
})
})
describe('.suspend()', () => {
it('suspends a VM', async () => {
await xo.call('vm.suspend', { id: hvmWithToolsId })
await waitObjectState(xo, hvmWithToolsId, vm => {
expect(size(vm.current_operations)).toBe(0)
expect(vm.power_state).toBe('Suspended')
})
})
})
describe('.resume()', () => {
it('resumes a VM', async () => {
await xo.call('vm.resume', { id: hvmWithToolsId })
await waitObjectState(xo, hvmWithToolsId, vm => {
expect(size(vm.current_operations)).toBe(0)
expect(vm.power_state).toBe('Running')
expect(vm.startTime).not.toBe(0)
expect(vm.xenTools).not.toBeFalsy()
})
})
})
describe('.stop()', () => {
it('stops a VM (clean shutdown)', async () => {
await xo.call('vm.stop', {
id: hvmWithToolsId,
force: false,
})
await waitObjectState(xo, hvmWithToolsId, vm => {
expect(size(vm.current_operations)).toBe(0)
expect(vm.power_state).toBe('Halted')
expect(vm.startTime).toBe(0)
})
})
it('stops a VM without PV drivers (clean shutdown)', async () => {
await xo.call('vm.start', { id: hvmWithoutToolsId })
await waitObjectState(xo, hvmWithoutToolsId, vm => {
if (size(vm.current_operations) !== 0 || vm.power_state !== 'Running')
throw new Error('retry')
})
expect(
(await rejectionOf(
xo.call('vm.stop', {
id: hvmWithoutToolsId,
force: false,
})
)).message
).toBe('clean shutdown requires PV drivers')
})
it('stops a VM (hard shutdown)', async () => {
await xo.call('vm.start', { id: hvmWithToolsId })
await waitObjectState(xo, hvmWithToolsId, vm => {
if (size(vm.current_operations) !== 0 || vm.startTime === 0)
throw new Error('retry')
})
await xo.call('vm.stop', {
id: hvmWithToolsId,
force: true,
})
await waitObjectState(xo, hvmWithToolsId, vm => {
expect(size(vm.current_operations)).toBe(0)
expect(vm.power_state).toBe('Halted')
expect(vm.startTime).toBe(0)
})
})
})
describe('.sets() on a halted VM', () => {
it('sets VM parameters', async () => {
await xo.call('vm.set', {
id: hvmWithToolsId,
name_label: 'haltedVmRenamed',
name_description: 'test halted vm',
high_availability: true,
CPUs: 1,
memoryMin: 20e8,
memoryMax: 90e8,
memoryStaticMax: 100e8,
})
await waitObjectState(xo, hvmWithToolsId, vm => {
expect(vm.name_label).toBe('haltedVmRenamed')
expect(vm.name_description).toBe('test halted vm')
expect(vm.high_availability).toBeTruthy()
expect(vm.CPUs.number).toBe(1)
expect(vm.memory.dynamic[0]).toBe(20e8)
expect(vm.memory.dynamic[1]).toBe(90e8)
expect(vm.memory.static[1]).toBe(100e8)
})
})
})
describe('.recoveryStart()', () => {
it('start a VM in recovery state', async () => {
await xo.call('vm.recoveryStart', { id: hvmWithToolsId })
await waitObjectState(xo, hvmWithToolsId, vm => {
expect(vm.boot.order).toBe('d')
})
await waitObjectState(xo, hvmWithToolsId, vm => {
expect(size(vm.current_operations)).toBe(0)
expect(vm.power_state).toBe('Running')
expect(vm.boot.order).not.toBe('d')
})
})
})
})

View File

@@ -0,0 +1,57 @@
/* eslint-env jest */
import { config, waitObjectState, xo } from './../util'
// ===================================================================
beforeAll(async () => {
jest.setTimeout(30e3)
})
describe('pci', () => {
let vmId
// ----------------------------------------------------------------------
beforeAll(async () => {
vmId = await xo.call('vm.create', {
name_label: 'vmTest',
template: config.templatesId.debianCloud,
VIFs: [{ network: config.labPoolNetworkId }],
VDIs: [
{
device: '0',
size: 1,
SR: config.labPoolSrId,
type: 'user',
},
],
})
await waitObjectState(xo, vmId, vm => {
if (vm.type !== 'VM') throw new Error('retry')
})
})
afterAll(() => xo.call('vm.delete', { id: vmId, delete_disks: true }))
// =================================================================
it('attaches the pci to the VM', async () => {
await xo.call('vm.attachPci', {
vm: vmId,
pciId: config.pciId,
})
await waitObjectState(xo, vmId, vm => {
expect(vm.other.pci).toBe(config.pciId)
})
})
it('detaches the pci from the VM', async () => {
await xo.call('vm.detachPci', { vm: vmId })
await waitObjectState(xo, vmId, vm => {
expect(vm.other.pci).toBeUndefined()
})
})
})

View File

@@ -0,0 +1,121 @@
/* eslint-env jest */
import { map, size } from 'lodash'
import { almostEqual, config, waitObjectState, xo } from './../util'
// ===================================================================
beforeAll(async () => {
jest.setTimeout(100e3)
})
describe('snapshotting', () => {
let snapshotId
let vmId
// ----------------------------------------------------------------------
beforeAll(async () => {
vmId = await xo.call('vm.create', {
name_label: 'vmTest',
name_description: 'creating a vm',
template: config.templatesId.centOS,
VIFs: [
{ network: config.labPoolNetworkId },
{ network: config.labPoolNetworkId },
],
VDIs: [
{
device: '0',
size: 1,
SR: config.labPoolSrId,
type: 'user',
},
{
device: '1',
size: 1,
SR: config.labPoolSrId,
type: 'user',
},
{
device: '2',
size: 1,
SR: config.labPoolSrId,
type: 'user',
},
],
})
await waitObjectState(xo, vmId, vm => {
if (vm.type !== 'VM') throw new Error('retry')
})
})
afterAll(() => xo.call('vm.delete', { id: vmId, delete_disks: true }))
// =================================================================
describe('.snapshot()', () => {
let $vm
it('snapshots a VM', async () => {
snapshotId = await xo.call('vm.snapshot', {
id: vmId,
name: 'snapshot',
})
const [, snapshot] = await Promise.all([
waitObjectState(xo, vmId, vm => {
$vm = vm
expect(vm.snapshots[0]).toBe(snapshotId)
}),
xo.getOrWaitObject(snapshotId),
])
expect(snapshot.type).toBe('VM-snapshot')
expect(snapshot.name_label).toBe('snapshot')
expect(snapshot.$snapshot_of).toBe(vmId)
almostEqual(snapshot, $vm, [
'$snapshot_of',
'$VBDs',
'id',
'installTime',
'name_label',
'snapshot_time',
'snapshots',
'type',
'uuid',
'VIFs',
])
})
})
describe('.revert()', () => {
let createdSnapshotId
it('reverts a snapshot to its parent VM', async () => {
await xo.call('vm.set', {
id: vmId,
name_label: 'vmRenamed',
})
await waitObjectState(xo, vmId, vm => {
if (vm.name_label !== 'vmRenamed') throw new Error('retry')
})
await xo.call('vm.revert', { id: snapshotId })
await waitObjectState(xo, vmId, vm => {
expect(size(vm.current_operations)).toBe(0)
expect(vm.name_label).toBe('vmTest')
expect(size(vm.snapshots)).toBe(2)
map(vm.snapshots, snapshot => {
if (snapshot !== snapshotId) createdSnapshotId = snapshot
})
})
const createdSnapshot = await xo.getOrWaitObject(createdSnapshotId)
expect(createdSnapshot.name_label).toBe('vmRenamed')
})
})
})

View File

@@ -0,0 +1,114 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`user .changePassword() : changes the actual user password 1`] = `true`;
exports[`user .changePassword() : changes the actual user password 2`] = `[JsonRpcError: invalid credentials]`;
exports[`user .changePassword() : fails trying to change the password with invalid oldPassword 1`] = `[JsonRpcError: invalid credentials]`;
exports[`user .create() : creates a user with permission 1`] = `
Object {
"email": "wayne2@vates.fr",
"groups": Array [],
"id": Any<String>,
"permission": "user",
"preferences": Object {},
}
`;
exports[`user .create() : creates a user without permission 1`] = `
Object {
"email": "wayne1@vates.fr",
"groups": Array [],
"id": Any<String>,
"preferences": Object {},
}
`;
exports[`user .create() : fails trying to create a user with an email already used 1`] = `[JsonRpcError: unknown error from the peer]`;
exports[`user .create() : fails trying to create a user without email 1`] = `[JsonRpcError: invalid parameters]`;
exports[`user .create() : fails trying to create a user without password 1`] = `[JsonRpcError: invalid parameters]`;
exports[`user .delete() : fails trying to delete a user with a nonexistent user 1`] = `[JsonRpcError: no such user nonexistentId]`;
exports[`user .delete() : fails trying to delete itself 1`] = `[JsonRpcError: a user cannot delete itself]`;
exports[`user .getAll() : gets all the users created 1`] = `
Object {
"email": "wayne4@vates.fr",
"groups": Array [],
"id": Any<String>,
"permission": "user",
"preferences": Object {},
}
`;
exports[`user .getAll() : gets all the users created 2`] = `
Object {
"email": "wayne5@vates.fr",
"groups": Array [],
"id": Any<String>,
"permission": "user",
"preferences": Object {},
}
`;
exports[`user .set() : fails trying to set a password with a non admin user connection 1`] = `[JsonRpcError: this properties can only changed by an administrator]`;
exports[`user .set() : fails trying to set a permission with a non admin user connection 1`] = `[JsonRpcError: this properties can only changed by an administrator]`;
exports[`user .set() : fails trying to set a property of a nonexistant user 1`] = `[JsonRpcError: no such user non-existent-id]`;
exports[`user .set() : fails trying to set an email with a non admin user connection 1`] = `[JsonRpcError: this properties can only changed by an administrator]`;
exports[`user .set() : fails trying to set its own permission as a non admin user 1`] = `[JsonRpcError: this properties can only changed by an administrator]`;
exports[`user .set() : fails trying to set its own permission as an admin 1`] = `[JsonRpcError: a user cannot change its own permission]`;
exports[`user .set() : sets a password 1`] = `
Object {
"email": "wayne3@vates.fr",
"groups": Array [],
"id": Any<String>,
"permission": "none",
"preferences": Object {},
}
`;
exports[`user .set() : sets a permission 1`] = `
Object {
"email": "wayne3@vates.fr",
"groups": Array [],
"id": Any<String>,
"permission": "user",
"preferences": Object {},
}
`;
exports[`user .set() : sets a preference 1`] = `
Object {
"email": "wayne3@vates.fr",
"groups": Array [],
"id": Any<String>,
"permission": "none",
"preferences": Object {
"filters": Object {
"VM": Object {
"test": "name_label: test",
},
},
},
}
`;
exports[`user .set() : sets an email 1`] = `
Object {
"email": "wayne_modified@vates.fr",
"groups": Array [],
"id": Any<String>,
"permission": "none",
"preferences": Object {},
}
`;

View File

@@ -0,0 +1,261 @@
/* eslint-env jest */
import { forOwn, keyBy } from 'lodash'
import xo, { testConnection, testWithOtherConnection } from '../_xoConnection'
const SIMPLE_USER = {
email: 'wayne3@vates.fr',
password: 'batman',
}
const ADMIN_USER = {
email: 'admin2@admin.net',
password: 'admin',
permission: 'admin',
}
const withData = (data, fn) =>
forOwn(data, (data, title) => {
it(title, () => fn(data))
})
describe('user', () => {
describe('.create() :', () => {
withData(
{
'creates a user without permission': {
email: 'wayne1@vates.fr',
password: 'batman1',
},
'creates a user with permission': {
email: 'wayne2@vates.fr',
password: 'batman2',
permission: 'user',
},
},
async data => {
const userId = await xo.createTempUser(data)
expect(typeof userId).toBe('string')
expect(await xo.getUser(userId)).toMatchSnapshot({
id: expect.any(String),
})
await testConnection({
credentials: {
email: data.email,
password: data.password,
},
})
}
)
withData(
{
'fails trying to create a user without email': { password: 'batman' },
'fails trying to create a user without password': {
email: 'wayne@vates.fr',
},
},
async data => {
await expect(xo.createTempUser(data)).rejects.toMatchSnapshot()
}
)
it('fails trying to create a user with an email already used', async () => {
await xo.createTempUser(SIMPLE_USER)
await expect(xo.createTempUser(SIMPLE_USER)).rejects.toMatchSnapshot()
})
})
describe('.changePassword() :', () => {
it('changes the actual user password', async () => {
const user = {
email: 'wayne7@vates.fr',
password: 'batman',
}
const newPassword = 'newpwd'
await xo.createTempUser(user)
await testWithOtherConnection(user, xo =>
expect(
xo.call('user.changePassword', {
oldPassword: user.password,
newPassword,
})
).resolves.toMatchSnapshot()
)
await testConnection({
credentials: {
email: user.email,
password: newPassword,
},
})
await expect(
testConnection({
credentials: user,
})
).rejects.toMatchSnapshot()
})
it('fails trying to change the password with invalid oldPassword', async () => {
await xo.createTempUser(SIMPLE_USER)
await testWithOtherConnection(SIMPLE_USER, xo =>
expect(
xo.call('user.changePassword', {
oldPassword: 'falsepwd',
newPassword: 'newpwd',
})
).rejects.toMatchSnapshot()
)
})
})
describe('.getAll() :', () => {
it('gets all the users created', async () => {
const userId1 = await xo.createTempUser({
email: 'wayne4@vates.fr',
password: 'batman',
permission: 'user',
})
const userId2 = await xo.createTempUser({
email: 'wayne5@vates.fr',
password: 'batman',
permission: 'user',
})
let users = await xo.call('user.getAll')
expect(Array.isArray(users)).toBe(true)
users = keyBy(users, 'id')
expect(users[userId1]).toMatchSnapshot({ id: expect.any(String) })
expect(users[userId2]).toMatchSnapshot({ id: expect.any(String) })
})
})
describe('.set() :', () => {
withData(
{
'sets an email': { email: 'wayne_modified@vates.fr' },
'sets a password': { password: 'newPassword' },
'sets a permission': { permission: 'user' },
'sets a preference': {
preferences: {
filters: {
VM: {
test: 'name_label: test',
},
},
},
},
},
async data => {
data.id = await xo.createTempUser(SIMPLE_USER)
expect(await xo.call('user.set', data)).toBe(true)
expect(await xo.getUser(data.id)).toMatchSnapshot({
id: expect.any(String),
})
await testConnection({
credentials: {
email: data.email === undefined ? SIMPLE_USER.email : data.email,
password:
data.password === undefined
? SIMPLE_USER.password
: data.password,
},
})
}
)
withData(
{
'fails trying to set an email with a non admin user connection': {
email: 'wayne_modified@vates.fr',
},
'fails trying to set a password with a non admin user connection': {
password: 'newPassword',
},
'fails trying to set a permission with a non admin user connection': {
permission: 'user',
},
},
async data => {
data.id = await xo.createTempUser({
email: 'wayne8@vates.fr',
password: 'batman8',
})
await xo.createTempUser(SIMPLE_USER)
await testWithOtherConnection(SIMPLE_USER, xo =>
expect(xo.call('user.set', data)).rejects.toMatchSnapshot()
)
}
)
withData(
{
'fails trying to set its own permission as a non admin user': SIMPLE_USER,
'fails trying to set its own permission as an admin': {
email: 'admin2@admin.net',
password: 'batman',
permission: 'admin',
},
},
async data => {
const id = await xo.createTempUser(data)
const { email, password } = data
await testWithOtherConnection({ email, password }, xo =>
expect(
xo.call('user.set', { id, permission: 'user' })
).rejects.toMatchSnapshot()
)
}
)
it('fails trying to set a property of a nonexistant user', async () => {
await expect(
xo.call('user.set', {
id: 'non-existent-id',
password: SIMPLE_USER.password,
})
).rejects.toMatchSnapshot()
})
it.skip('fails trying to set an email already used', async () => {
await xo.createTempUser(SIMPLE_USER)
const userId2 = await xo.createTempUser({
email: 'wayne6@vates.fr',
password: 'batman',
})
await expect(
xo.call('user.set', {
id: userId2,
email: SIMPLE_USER.email,
})
).rejects.toMatchSnapshot()
})
})
describe('.delete() :', () => {
it('deletes a user successfully with id', async () => {
const userId = await xo.call('user.create', SIMPLE_USER)
expect(await xo.call('user.delete', { id: userId })).toBe(true)
expect(await xo.getUser(userId)).toBe(undefined)
})
it('fails trying to delete a user with a nonexistent user', async () => {
await expect(
xo.call('user.delete', { id: 'nonexistentId' })
).rejects.toMatchSnapshot()
})
it('fails trying to delete itself', async () => {
const id = await xo.createTempUser(ADMIN_USER)
const { email, password } = ADMIN_USER
await testWithOtherConnection({ email, password }, xo =>
expect(xo.call('user.delete', { id })).rejects.toMatchSnapshot()
)
})
})
})

View File

@@ -0,0 +1,146 @@
import expect from 'must'
import { find, forEach, map, cloneDeep } from 'lodash'
import config from './_config'
export const rejectionOf = promise =>
promise.then(
value => {
throw value
},
reason => reason
)
// =================================================================
async function getAllUsers(xo) {
return xo.call('user.getAll')
}
export async function getUser(xo, id) {
const users = await getAllUsers(xo)
return find(users, { id })
}
export async function createUser(xo, userIds, params) {
const userId = await xo.call('user.create', params)
userIds.push(userId)
return userId
}
export async function deleteUsers(xo, userIds) {
await Promise.all(
map(userIds, userId => xo.call('user.delete', { id: userId }))
)
}
// ==================================================================
export function getAllHosts(xo) {
return xo.objects.indexes.type.host
}
export function getOneHost(xo) {
const hosts = getAllHosts(xo)
for (const id in hosts) {
return hosts[id]
}
throw new Error('no hosts found')
}
// ==================================================================
export async function getNetworkId(xo) {
const networks = xo.objects.indexes.type.network
const network = find(networks, { name_label: config.network })
return network.id
}
// ==================================================================
export async function getVmXoTestPvId(xo) {
const vms = xo.objects.indexes.type.VM
const vm = find(vms, { name_label: config.pvVm })
return vm.id
}
export async function getVmToMigrateId(xo) {
const vms = xo.objects.indexes.type.VM
const vm = find(vms, { name_label: config.vmToMigrate })
return vm.id
}
// ==================================================================
export async function getSrId(xo) {
const host = getOneHost(xo)
const pool = await xo.getOrWaitObject(host.$poolId)
return pool.default_SR
}
// ==================================================================
export async function jobTest(xo) {
const vmId = await getVmXoTestPvId(xo)
const jobId = await xo.call('job.create', {
job: {
type: 'call',
key: 'snapshot',
method: 'vm.snapshot',
paramsVector: {
type: 'cross product',
items: [
{
type: 'set',
values: [
{
id: vmId,
name: 'snapshot',
},
],
},
],
},
},
})
return jobId
}
export async function scheduleTest(xo, jobId) {
const schedule = await xo.call('schedule.create', {
jobId: jobId,
cron: '* * * * * *',
enabled: false,
})
return schedule
}
export async function getSchedule(xo, id) {
const schedule = xo.call('schedule.get', { id: id })
return schedule
}
// ==================================================================
export function deepDelete(obj, path) {
const lastIndex = path.length - 1
for (let i = 0; i < lastIndex; i++) {
obj = obj[path[i]]
if (typeof obj !== 'object' || obj === null) {
return
}
}
delete obj[path[lastIndex]]
}
export function almostEqual(actual, expected, ignoredAttributes) {
actual = cloneDeep(actual)
expected = cloneDeep(expected)
forEach(ignoredAttributes, ignoredAttribute => {
deepDelete(actual, ignoredAttribute.split('.'))
deepDelete(expected, ignoredAttribute.split('.'))
})
expect(actual).to.be.eql(expected)
}

View File

@@ -34,7 +34,7 @@
"dependencies": {
"nodemailer": "^6.1.0",
"nodemailer-markdown": "^1.0.1",
"promise-toolbox": "^0.12.1"
"promise-toolbox": "^0.13.0"
},
"devDependencies": {
"@babel/cli": "^7.0.0",

View File

@@ -33,7 +33,7 @@
"node": ">=6"
},
"dependencies": {
"promise-toolbox": "^0.12.1",
"promise-toolbox": "^0.13.0",
"slack-node": "^0.1.8"
},
"devDependencies": {

View File

@@ -42,7 +42,7 @@
"html-minifier": "^4.0.0",
"human-format": "^0.10.0",
"lodash": "^4.17.4",
"promise-toolbox": "^0.12.1"
"promise-toolbox": "^0.13.0"
},
"devDependencies": {
"@babel/cli": "^7.0.0",

View File

@@ -29,6 +29,9 @@ guessVhdSizeOnImport = false
# be turned for investigation by the administrator.
verboseApiLogsOnErrors = false
# if no events could be fetched during this delay, the server will be marked as disconnected
xapiMarkDisconnectedDelay = '5 minutes'
# https:#github.com/websockets/ws#websocket-compression
[apiWebSocketOptions]
perMessageDeflate = { threshold = 524288 } # 512kiB
@@ -49,6 +52,11 @@ maxTokenValidity = '0.5 year'
# Delay for which backups listing on a remote is cached
listingDebounce = '1 min'
# Duration for which we can wait for the backup size before returning
#
# It should be short to avoid blocking the display of the available backups.
vmBackupSizeTimeout = '2 seconds'
# Helmet handles HTTP security via headers
#
# https://helmetjs.github.io/docs/
@@ -74,6 +82,7 @@ honorCipherOrder = true
secureOptions = 117440512
[http.mounts]
'/' = '../xo-web/dist'
[remoteOptions]
mountsDir = '/run/xo-server/mounts'

View File

@@ -1,7 +1,7 @@
{
"private": true,
"name": "xo-server",
"version": "5.40.0",
"version": "5.46.0",
"license": "AGPL-3.0",
"description": "Server part of Xen-Orchestra",
"keywords": [
@@ -38,11 +38,11 @@
"@xen-orchestra/cron": "^1.0.3",
"@xen-orchestra/defined": "^0.0.0",
"@xen-orchestra/emit-async": "^0.0.0",
"@xen-orchestra/fs": "^0.9.0",
"@xen-orchestra/fs": "^0.10.1",
"@xen-orchestra/log": "^0.1.4",
"@xen-orchestra/mixin": "^0.0.0",
"ajv": "^6.1.1",
"app-conf": "^0.6.1",
"app-conf": "^0.7.0",
"archiver": "^3.0.0",
"async-iterator-to-stream": "^1.0.1",
"base64url": "^3.0.0",
@@ -51,7 +51,7 @@
"body-parser": "^1.18.2",
"compression": "^1.7.3",
"connect-flash": "^0.1.1",
"cookie": "^0.3.1",
"cookie": "^0.4.0",
"cookie-parser": "^1.4.3",
"d3-time-format": "^2.1.1",
"debug": "^4.0.1",
@@ -65,7 +65,7 @@
"express-session": "^1.15.6",
"fatfs": "^0.10.4",
"from2": "^2.3.0",
"fs-extra": "^7.0.0",
"fs-extra": "^8.0.1",
"get-stream": "^4.0.0",
"golike-defer": "^0.4.1",
"hashy": "^0.7.1",
@@ -102,7 +102,7 @@
"passport": "^0.4.0",
"passport-local": "^1.0.0",
"pretty-format": "^24.0.0",
"promise-toolbox": "^0.12.1",
"promise-toolbox": "^0.13.0",
"proxy-agent": "^3.0.0",
"pug": "^2.0.0-rc.4",
"pump": "^3.0.0",
@@ -123,7 +123,7 @@
"value-matcher": "^0.2.0",
"vhd-lib": "^0.7.0",
"ws": "^6.0.0",
"xen-api": "^0.25.1",
"xen-api": "^0.27.1",
"xml2js": "^0.4.19",
"xo-acl-resolver": "^0.4.1",
"xo-collection": "^0.4.1",

View File

@@ -117,7 +117,7 @@ port = 80
# List of files/directories which will be served.
[http.mounts]
#'/' = '/path/to/xo-web/dist/'
#'/any/url' = '/path/to/directory'
# List of proxied URLs (HTTP & WebSockets).
[http.proxies]

View File

@@ -123,10 +123,14 @@ getJob.params = {
export async function runJob({
id,
schedule,
settings,
vm,
vms = vm !== undefined ? [vm] : undefined,
}) {
return this.runJobSequence([id], await this.getSchedule(schedule), vms)
return this.runJobSequence([id], await this.getSchedule(schedule), {
settings,
vms,
})
}
runJob.permission = 'admin'
@@ -138,6 +142,13 @@ runJob.params = {
schedule: {
type: 'string',
},
settings: {
type: 'object',
properties: {
'*': { type: 'object' },
},
optional: true,
},
vm: {
type: 'string',
optional: true,
@@ -183,6 +194,7 @@ getLogs.params = {
after: { type: ['number', 'string'], optional: true },
before: { type: ['number', 'string'], optional: true },
limit: { type: 'number', optional: true },
'*': { type: 'any' },
}
// -----------------------------------------------------------------------------

View File

@@ -4,29 +4,34 @@ import { format, JsonRpcError } from 'json-rpc-peer'
export async function set({
host,
multipathing,
// TODO: use camel case.
iscsiIqn,
multipathing,
name_label: nameLabel,
name_description: nameDescription,
}) {
const xapi = this.getXapi(host)
const hostId = host._xapiId
host = this.getXapiObject(host)
if (multipathing !== undefined) {
await xapi.setHostMultipathing(hostId, multipathing)
}
return xapi.setHostProperties(hostId, {
nameLabel,
nameDescription,
})
await Promise.all([
iscsiIqn !== undefined &&
(host.iscsi_iqn !== undefined
? host.set_iscsi_iqn(iscsiIqn)
: host.update_other_config(
'iscsi_iqn',
iscsiIqn === '' ? null : iscsiIqn
)),
nameDescription !== undefined && host.set_name_description(nameDescription),
nameLabel !== undefined && host.set_name_label(nameLabel),
multipathing !== undefined &&
host.$xapi.setHostMultipathing(host.$id, multipathing),
])
}
set.description = 'changes the properties of an host'
set.params = {
id: { type: 'string' },
iscsiIqn: { type: 'string', optional: true },
name_label: {
type: 'string',
optional: true,
@@ -215,6 +220,25 @@ emergencyShutdownHost.resolve = {
// -------------------------------------------------------------------
export async function isHostServerTimeConsistent({ host }) {
try {
await this.getXapi(host).assertConsistentHostServerTime(host._xapiRef)
return true
} catch (e) {
return false
}
}
isHostServerTimeConsistent.params = {
host: { type: 'string' },
}
isHostServerTimeConsistent.resolve = {
host: ['host', 'host', 'administrate'],
}
// -------------------------------------------------------------------
export function stats({ host, granularity }) {
return this.getXapiHostStats(host._xapiId, granularity)
}
@@ -269,3 +293,19 @@ installSupplementalPack.params = {
installSupplementalPack.resolve = {
host: ['host', 'host', 'admin'],
}
// -------------------------------------------------------------------
export function isHyperThreadingEnabled({ host }) {
return this.getXapi(host).isHyperThreadingEnabled(host._xapiId)
}
isHyperThreadingEnabled.description = 'get hyper-threading information'
isHyperThreadingEnabled.params = {
id: { type: 'string' },
}
isHyperThreadingEnabled.resolve = {
host: ['id', 'host', 'administrate'],
}

View File

@@ -85,18 +85,26 @@ createBonded.description =
// ===================================================================
export async function set({
network,
automatic,
defaultIsLocked,
name_description: nameDescription,
name_label: nameLabel,
network,
}) {
await this.getXapi(network).setNetworkProperties(network._xapiId, {
automatic,
defaultIsLocked,
nameDescription,
nameLabel,
})
network = this.getXapiObject(network)
await Promise.all([
automatic !== undefined &&
network.update_other_config('automatic', automatic ? 'true' : null),
defaultIsLocked !== undefined &&
network.set_default_locking_mode(
defaultIsLocked ? 'disabled' : 'unlocked'
),
nameDescription !== undefined &&
network.set_name_description(nameDescription),
nameLabel !== undefined && network.set_name_label(nameLabel),
])
}
set.params = {

View File

@@ -5,7 +5,7 @@
async function delete_({ PBD }) {
// TODO: check if PBD is attached before
await this.getXapi(PBD).call('PBD.destroy', PBD._xapiRef)
await this.getXapi(PBD).callAsync('PBD.destroy', PBD._xapiRef)
}
export { delete_ as delete }
@@ -37,7 +37,7 @@ disconnect.resolve = {
export async function connect({ PBD }) {
// TODO: check if PBD is attached before
await this.getXapi(PBD).call('PBD.plug', PBD._xapiRef)
await this.getXapi(PBD).callAsync('PBD.plug', PBD._xapiRef)
}
connect.params = {

View File

@@ -1,5 +1,7 @@
// TODO: too low level, move into host.
import { filter, find } from 'lodash'
import { IPV4_CONFIG_MODES, IPV6_CONFIG_MODES } from '../xapi'
export function getIpv4ConfigurationModes() {
@@ -15,7 +17,17 @@ export function getIpv6ConfigurationModes() {
async function delete_({ pif }) {
// TODO: check if PIF is attached before
await this.getXapi(pif).call('PIF.destroy', pif._xapiRef)
const xapi = this.getXapi(pif)
const tunnels = filter(xapi.objects.all, { $type: 'tunnel' })
const tunnel = find(tunnels, { access_PIF: pif._xapiRef })
if (tunnel != null) {
await xapi.callAsync('PIF.unplug', pif._xapiRef)
await xapi.callAsync('tunnel.destroy', tunnel.$ref)
return
}
await xapi.callAsync('PIF.destroy', pif._xapiRef)
}
export { delete_ as delete }
@@ -32,7 +44,7 @@ delete_.resolve = {
export async function disconnect({ pif }) {
// TODO: check if PIF is attached before
await this.getXapi(pif).call('PIF.unplug', pif._xapiRef)
await this.getXapi(pif).callAsync('PIF.unplug', pif._xapiRef)
}
disconnect.params = {
@@ -47,7 +59,7 @@ disconnect.resolve = {
export async function connect({ pif }) {
// TODO: check if PIF is attached before
await this.getXapi(pif).call('PIF.plug', pif._xapiRef)
await this.getXapi(pif).callAsync('PIF.plug', pif._xapiRef)
}
connect.params = {

View File

@@ -5,14 +5,15 @@ import { format, JsonRPcError } from 'json-rpc-peer'
export async function set({
pool,
// TODO: use camel case.
name_description: nameDescription,
name_label: nameLabel,
}) {
await this.getXapi(pool).setPoolProperties({
nameDescription,
nameLabel,
})
pool = this.getXapiObject(pool)
await Promise.all([
nameDescription !== undefined && pool.set_name_description(nameDescription),
nameLabel !== undefined && pool.set_name_label(nameLabel),
])
}
set.params = {
@@ -161,45 +162,30 @@ getPatchesDifference.resolve = {
// -------------------------------------------------------------------
export async function mergeInto({ source, target, force }) {
const sourceHost = this.getObject(source.master)
const targetHost = this.getObject(target.master)
if (sourceHost.productBrand !== targetHost.productBrand) {
throw new Error(
`a ${sourceHost.productBrand} pool cannot be merged into a ${
targetHost.productBrand
} pool`
)
}
const counterDiff = this.getPatchesDifference(source.master, target.master)
if (counterDiff.length > 0) {
const targetXapi = this.getXapi(target)
await targetXapi.installPatches({
patches: await targetXapi.findPatches(counterDiff),
})
}
const diff = this.getPatchesDifference(target.master, source.master)
if (diff.length > 0) {
const sourceXapi = this.getXapi(source)
await sourceXapi.installPatches({
patches: await sourceXapi.findPatches(diff),
})
}
await this.mergeXenPools(source._xapiId, target._xapiId, force)
export async function mergeInto({ source, sources = [source], target, force }) {
await this.checkPermissions(
this.user.id,
sources.map(source => [source, 'administrate'])
)
return this.mergeInto({
force,
sources,
target,
})
}
mergeInto.params = {
force: { type: 'boolean', optional: true },
source: { type: 'string' },
source: { type: 'string', optional: true },
sources: {
type: 'array',
items: { type: 'string' },
optional: true,
},
target: { type: 'string' },
}
mergeInto.resolve = {
source: ['source', 'pool', 'administrate'],
target: ['target', 'pool', 'administrate'],
}

View File

@@ -100,20 +100,24 @@ set.params = {
optional: true,
type: 'boolean',
},
readOnly: {
optional: true,
type: 'boolean',
},
}
// -------------------------------------------------------------------
export async function connect({ id }) {
export async function enable({ id }) {
this.updateXenServer(id, { enabled: true })::ignoreErrors()
await this.connectXenServer(id)
}
connect.description = 'connect a Xen server'
enable.description = 'enable a Xen server'
connect.permission = 'admin'
enable.permission = 'admin'
connect.params = {
enable.params = {
id: {
type: 'string',
},
@@ -121,16 +125,16 @@ connect.params = {
// -------------------------------------------------------------------
export async function disconnect({ id }) {
export async function disable({ id }) {
this.updateXenServer(id, { enabled: false })::ignoreErrors()
await this.disconnectXenServer(id)
}
disconnect.description = 'disconnect a Xen server'
disable.description = 'disable a Xen server'
disconnect.permission = 'admin'
disable.permission = 'admin'
disconnect.params = {
disable.params = {
id: {
type: 'string',
},

View File

@@ -10,14 +10,15 @@ import { forEach, parseXml } from '../utils'
export async function set({
sr,
// TODO: use camel case.
name_description: nameDescription,
name_label: nameLabel,
}) {
await this.getXapi(sr).setSrProperties(sr._xapiId, {
nameDescription,
nameLabel,
})
sr = this.getXapiObject(sr)
await Promise.all([
nameDescription !== undefined && sr.set_name_description(nameDescription),
nameLabel !== undefined && sr.set_name_label(nameLabel),
])
}
set.params = {
@@ -35,7 +36,7 @@ set.resolve = {
// -------------------------------------------------------------------
export async function scan({ SR }) {
await this.getXapi(SR).call('SR.scan', SR._xapiRef)
await this.getXapi(SR).callAsync('SR.scan', SR._xapiRef)
}
scan.params = {
@@ -179,6 +180,35 @@ createIso.resolve = {
host: ['host', 'host', 'administrate'],
}
// -------------------------------------------------------------------
export async function createFile({
host,
nameLabel,
nameDescription,
location,
}) {
const xapi = this.getXapi(host)
return xapi.createSr({
hostRef: host._xapiRef,
name_label: nameLabel,
name_description: nameDescription,
type: 'file',
device_config: { location },
})
}
createFile.params = {
host: { type: 'string' },
nameLabel: { type: 'string' },
nameDescription: { type: 'string' },
location: { type: 'string' },
}
createFile.resolve = {
host: ['host', 'host', 'administrate'],
}
// -------------------------------------------------------------------
// NFS SR
@@ -361,6 +391,58 @@ createExt.resolve = {
host: ['host', 'host', 'administrate'],
}
// -------------------------------------------------------------------
// This function helps to detect all ZFS pools
// Return a dict of pools with their parameters { <poolname>: {<paramdict>}}
// example output (the parameter mountpoint is of interest):
// {"tank":
// {
// "setuid": "on", "relatime": "off", "referenced": "24K", "written": "24K", "zoned": "off", "primarycache": "all",
// "logbias": "latency", "creation": "Mon May 27 17:24 2019", "sync": "standard", "snapdev": "hidden",
// "dedup": "off", "sharenfs": "off", "usedbyrefreservation": "0B", "sharesmb": "off", "createtxg": "1",
// "canmount": "on", "mountpoint": "/tank", "casesensitivity": "sensitive", "utf8only": "off", "xattr": "on",
// "dnodesize": "legacy", "mlslabel": "none", "objsetid": "54", "defcontext": "none", "rootcontext": "none",
// "mounted": "yes", "compression": "off", "overlay": "off", "logicalused": "47K", "usedbysnapshots": "0B",
// "filesystem_count": "none", "copies": "1", "snapshot_limit": "none", "aclinherit": "restricted",
// "compressratio": "1.00x", "readonly": "off", "version": "5", "normalization": "none", "filesystem_limit": "none",
// "type": "filesystem", "secondarycache": "all", "refreservation": "none", "available": "17.4G", "used": "129K",
// "exec": "on", "refquota": "none", "refcompressratio": "1.00x", "quota": "none", "keylocation": "none",
// "snapshot_count": "none", "fscontext": "none", "vscan": "off", "reservation": "none", "atime": "on",
// "recordsize": "128K", "usedbychildren": "105K", "usedbydataset": "24K", "guid": "656061077639704004",
// "pbkdf2iters": "0", "checksum": "on", "special_small_blocks": "0", "redundant_metadata": "all",
// "volmode": "default", "devices": "on", "keyformat": "none", "logicalreferenced": "12K", "acltype": "off",
// "nbmand": "off", "context": "none", "encryption": "off", "snapdir": "hidden"}}
export async function probeZfs({ host }) {
const xapi = this.getXapi(host)
try {
const result = await xapi.call(
'host.call_plugin',
host._xapiRef,
'zfs.py',
'list_zfs_pools',
{}
)
return JSON.parse(result)
} catch (error) {
if (
error.code === 'XENAPI_MISSING_PLUGIN' ||
error.code === 'UNKNOWN_XENAPI_PLUGIN_FUNCTION'
) {
return {}
} else {
throw error
}
}
}
probeZfs.params = {
host: { type: 'string' },
}
probeZfs.resolve = {
host: ['host', 'host', 'administrate'],
}
// -------------------------------------------------------------------
// This function helps to detect all NFS shares (exports) on a NFS server
// Return a table of exports with their paths and ACLs

View File

@@ -1,5 +1,5 @@
export async function add({ tag, object }) {
await this.getXapi(object).addTag(object._xapiId, tag)
await this.getXapiObject(object).add_tags(tag)
}
add.description = 'add a new tag to an object'
@@ -16,7 +16,7 @@ add.params = {
// -------------------------------------------------------------------
export async function remove({ tag, object }) {
await this.getXapi(object).removeTag(object._xapiId, tag)
await this.getXapiObject(object).remove_tags(tag)
}
remove.description = 'remove an existing tag from an object'

View File

@@ -1,3 +1,6 @@
import assert from 'assert'
import { fromEvent } from 'promise-toolbox'
export function getPermissionsForUser({ userId }) {
return this.getPermissionsForUser(userId)
}
@@ -86,3 +89,35 @@ copyVm.resolve = {
vm: ['vm', 'VM'],
sr: ['sr', 'SR'],
}
// -------------------------------------------------------------------
export async function changeConnectedXapiHostname({
hostname,
newObject,
oldObject,
}) {
const xapi = this.getXapi(oldObject)
const { pool: currentPool } = xapi
xapi._setUrl({ ...xapi._url, hostname })
await fromEvent(xapi.objects, 'finish')
if (xapi.pool.$id === currentPool.$id) {
await fromEvent(xapi.objects, 'finish')
}
assert(xapi.pool.$id !== currentPool.$id)
assert.doesNotThrow(() => this.getXapi(newObject))
assert.throws(() => this.getXapi(oldObject))
}
changeConnectedXapiHostname.description =
'change the connected XAPI hostname and check if the pool and the local cache are updated'
changeConnectedXapiHostname.permission = 'admin'
changeConnectedXapiHostname.params = {
hostname: { type: 'string' },
newObject: { type: 'string', description: "new connection's XO object" },
oldObject: { type: 'string', description: "current connection's XO object" },
}

View File

@@ -34,3 +34,25 @@ delete_.permission = 'admin'
delete_.params = {
token: { type: 'string' },
}
// -------------------------------------------------------------------
export async function deleteAll({ except }) {
await this.deleteAuthenticationTokens({
filter: {
user_id: this.session.get('user_id'),
id: {
__not: except,
},
},
})
}
deleteAll.description =
'delete all tokens of the current user except the current one'
deleteAll.permission = ''
deleteAll.params = {
except: { type: 'string', optional: true },
}

View File

@@ -48,8 +48,7 @@ connect.resolve = {
export async function set({ position, vbd }) {
if (position !== undefined) {
const xapi = this.getXapi(vbd)
await xapi.call('VBD.set_userdevice', vbd._xapiRef, String(position))
await this.getXapiObject(vbd).set_userdevice(String(position))
}
}
@@ -67,9 +66,7 @@ set.resolve = {
// -------------------------------------------------------------------
export async function setBootable({ vbd, bootable }) {
const xapi = this.getXapi(vbd)
await xapi.call('VBD.set_bootable', vbd._xapiRef, bootable)
await this.getXapiObject(vbd).set_bootable(bootable)
}
setBootable.params = {

View File

@@ -64,6 +64,7 @@ export async function set({
allowedIpv4Addresses,
allowedIpv6Addresses,
attached,
rateLimit,
}) {
const oldIpAddresses = vif.allowedIpv4Addresses.concat(
vif.allowedIpv6Addresses
@@ -91,6 +92,9 @@ export async function set({
mac,
currently_attached: attached,
ipv4_allowed: newIpAddresses,
qos_algorithm_type: rateLimit != null ? 'ratelimit' : undefined,
qos_algorithm_params:
rateLimit != null ? { kbps: String(rateLimit) } : undefined,
})
await this.allocIpAddresses(newVif.$id, newIpAddresses)
@@ -107,6 +111,7 @@ export async function set({
return this.getXapi(vif).editVif(vif._xapiId, {
ipv4Allowed: allowedIpv4Addresses,
ipv6Allowed: allowedIpv6Addresses,
rateLimit,
})
}
@@ -129,6 +134,11 @@ set.params = {
optional: true,
},
attached: { type: 'boolean', optional: true },
rateLimit: {
description: 'in kilobytes per seconds',
optional: true,
type: ['number', 'null'],
},
}
set.resolve = {

View File

@@ -150,6 +150,8 @@ export async function create(params) {
}
const xapiVm = await xapi.createVm(template._xapiId, params, checkLimits)
await xapiVm.update_other_config('owner', user.id)
const vm = xapi.xo.addObject(xapiVm)
if (resourceSet) {
@@ -320,6 +322,11 @@ create.params = {
},
},
},
hvmBootFirmware: { type: 'string', optional: true },
// other params are passed to `editVm`
'*': { type: 'any' },
}
create.resolve = {
@@ -560,6 +567,8 @@ set.params = {
// Identifier of the VM to update.
id: { type: 'string' },
auto_poweron: { type: 'boolean', optional: true },
name_label: { type: 'string', optional: true },
name_description: { type: 'string', optional: true },
@@ -603,7 +612,7 @@ set.params = {
// Switch from Cirrus video adaptor to VGA adaptor
vga: { type: 'string', optional: true },
videoram: { type: ['string', 'number'], optional: true },
videoram: { type: 'number', optional: true },
coresPerSocket: { type: ['string', 'number', 'null'], optional: true },
@@ -621,6 +630,11 @@ set.params = {
// set the VM network interface controller
nicType: { type: ['string', 'null'], optional: true },
// set the VM boot firmware mode
hvmBootFirmware: { type: ['string', 'null'], optional: true },
virtualizationMode: { type: 'string', optional: true },
}
set.resolve = {
@@ -630,13 +644,7 @@ set.resolve = {
// -------------------------------------------------------------------
export async function restart({ vm, force = false }) {
const xapi = this.getXapi(vm)
if (force) {
await xapi.call('VM.hard_reboot', vm._xapiRef)
} else {
await xapi.call('VM.clean_reboot', vm._xapiRef)
}
return this.getXapi(vm).rebootVm(vm._xapiId, { hard: force })
}
restart.params = {
@@ -657,15 +665,16 @@ export const clone = defer(async function(
await checkPermissionOnSrs.call(this, vm)
const xapi = this.getXapi(vm)
const { $id: cloneId } = await xapi.cloneVm(vm._xapiRef, {
const xapiVm = await xapi.cloneVm(vm._xapiRef, {
nameLabel: name,
fast: !fullCopy,
})
$defer.onFailure(() => xapi.deleteVm(cloneId))
$defer.onFailure(() => xapi.deleteVm(xapiVm.$id))
await xapiVm.update_other_config('owner', this.user.id)
const isAdmin = this.user.permission === 'admin'
if (!isAdmin) {
await this.addAcl(this.user.id, cloneId, 'admin')
await this.addAcl(this.user.id, xapiVm.$id, 'admin')
}
if (vm.resourceSet !== undefined) {
@@ -676,7 +685,7 @@ export const clone = defer(async function(
)
}
return cloneId
return xapiVm.$id
})
clone.params = {
@@ -698,19 +707,26 @@ export async function copy({ compress, name: nameLabel, sr, vm }) {
await checkPermissionOnSrs.call(this, vm)
}
return this.getXapi(vm)
.copyVm(vm._xapiId, sr._xapiId, {
nameLabel,
})
.then(vm => vm.$id)
}
return this.getXapi(vm)
.remoteCopyVm(vm._xapiId, this.getXapi(sr), sr._xapiId, {
compress,
const xapiVm = await this.getXapi(vm).copyVm(vm._xapiId, sr._xapiId, {
nameLabel,
})
.then(({ vm }) => vm.$id)
await xapiVm.update_other_config('owner', this.user.id)
return xapiVm.$id
}
const { vm: xapiVm } = await this.getXapi(vm).remoteCopyVm(
vm._xapiId,
this.getXapi(sr),
sr._xapiId,
{
compress,
nameLabel,
}
)
await xapiVm.update_other_config('owner', this.user.id)
return xapiVm.$id
}
copy.params = {
@@ -737,7 +753,7 @@ export async function convertToTemplate({ vm }) {
// Convert to a template requires pool admin permission.
await this.checkPermissions(this.user.id, [[vm.$pool, 'administrate']])
await this.getXapi(vm).call('VM.set_is_a_template', vm._xapiRef, true)
await this.getXapiObject(vm).set_is_a_template(true)
}
convertToTemplate.params = {
@@ -1089,7 +1105,7 @@ stop.resolve = {
// -------------------------------------------------------------------
export async function suspend({ vm }) {
await this.getXapi(vm).call('VM.suspend', vm._xapiRef)
await this.getXapi(vm).callAsync('VM.suspend', vm._xapiRef)
}
suspend.params = {
@@ -1103,7 +1119,7 @@ suspend.resolve = {
// -------------------------------------------------------------------
export async function pause({ vm }) {
await this.getXapi(vm).call('VM.pause', vm._xapiRef)
await this.getXapi(vm).callAsync('VM.pause', vm._xapiRef)
}
pause.params = {
@@ -1130,7 +1146,10 @@ resume.resolve = {
// -------------------------------------------------------------------
export function revert({ snapshot, snapshotBefore }) {
export async function revert({ snapshot, snapshotBefore }) {
await this.checkPermissions(this.user.id, [
[snapshot.$snapshot_of, 'operate'],
])
return this.getXapi(snapshot).revertVm(snapshot._xapiId, snapshotBefore)
}
@@ -1140,7 +1159,7 @@ revert.params = {
}
revert.resolve = {
snapshot: ['snapshot', 'VM-snapshot', 'administrate'],
snapshot: ['snapshot', 'VM-snapshot', 'view'],
}
// -------------------------------------------------------------------
@@ -1366,9 +1385,7 @@ createInterface.resolve = {
// -------------------------------------------------------------------
export async function attachPci({ vm, pciId }) {
const xapi = this.getXapi(vm)
await xapi.call('VM.add_to_other_config', vm._xapiRef, 'pci', pciId)
await this.getXapiObject(vm).update_other_config('pci', pciId)
}
attachPci.params = {
@@ -1383,9 +1400,7 @@ attachPci.resolve = {
// -------------------------------------------------------------------
export async function detachPci({ vm }) {
const xapi = this.getXapi(vm)
await xapi.call('VM.remove_from_other_config', vm._xapiRef, 'pci')
await this.getXapiObject(vm).update_other_config('pci', null)
}
detachPci.params = {
@@ -1418,15 +1433,11 @@ stats.resolve = {
// -------------------------------------------------------------------
export async function setBootOrder({ vm, order }) {
const xapi = this.getXapi(vm)
order = { order }
if (vm.virtualizationMode === 'hvm') {
await xapi.call('VM.set_HVM_boot_params', vm._xapiRef, order)
return
if (vm.virtualizationMode !== 'hvm') {
throw invalidParameters('You can only set the boot order on a HVM guest')
}
throw invalidParameters('You can only set the boot order on a HVM guest')
await this.getXapiObject(vm).update_HVM_boot_params('order', order)
}
setBootOrder.params = {

View File

@@ -55,6 +55,7 @@ getAllObjects.description = 'Returns all XO objects'
getAllObjects.params = {
filter: { type: 'object', optional: true },
limit: { type: 'number', optional: true },
ndjson: { type: 'boolean', optional: true },
}
// -------------------------------------------------------------------

View File

@@ -269,10 +269,10 @@ export async function fixHostNotInNetwork({ xosanSr, host }) {
if (pif) {
const newIP = _findIPAddressOutsideList(usedAddresses, HOST_FIRST_NUMBER)
reconfigurePifIP(xapi, pif, newIP)
await xapi.call('PIF.plug', pif.$ref)
await xapi.callAsync('PIF.plug', pif.$ref)
const PBD = find(xosanSr.$PBDs, pbd => pbd.$host.$id === host)
if (PBD) {
await xapi.call('PBD.plug', PBD.$ref)
await xapi.callAsync('PBD.plug', PBD.$ref)
}
const sshKey = await getOrCreateSshKey(xapi)
await callPlugin(xapi, host, 'receive_ssh_keys', {
@@ -446,9 +446,7 @@ const createNetworkAndInsertHosts = defer(async function(
})
if (result.exit !== 0) {
throw invalidParameters(
`Could not ping ${master.name_label}->${
address.pif.$host.name_label
} (${address.address}) \n${result.stdout}`
`Could not ping ${master.name_label}->${address.pif.$host.name_label} (${address.address}) \n${result.stdout}`
)
}
})
@@ -809,7 +807,7 @@ export const createSR = defer(async function(
})
CURRENT_POOL_OPERATIONS[poolId] = { ...OPERATION_OBJECT, state: 6 }
log.debug('scanning new SR')
await xapi.call('SR.scan', xosanSrRef)
await xapi.callAsync('SR.scan', xosanSrRef)
await this.rebindLicense({
licenseId: license.id,
oldBoundObjectId: tmpBoundObjectId,
@@ -884,13 +882,13 @@ async function createVDIOnLVMWithoutSizeLimit(xapi, lvmSr, diskSize) {
if (result.exit !== 0) {
throw Error('Could not create volume ->' + result.stdout)
}
await xapi.call('SR.scan', xapi.getObject(lvmSr).$ref)
await xapi.callAsync('SR.scan', xapi.getObject(lvmSr).$ref)
const vdi = find(xapi.getObject(lvmSr).$VDIs, vdi => vdi.uuid === uuid)
if (vdi != null) {
await xapi.setSrProperties(vdi.$ref, {
nameLabel: 'xosan_data',
nameDescription: 'Created by XO',
})
await Promise.all([
vdi.set_name_description('Created by XO'),
vdi.set_name_label('xosan_data'),
])
return vdi
}
}
@@ -989,7 +987,7 @@ async function replaceBrickOnSameVM(
await xapi.disconnectVbd(previousVBD)
await xapi.deleteVdi(previousVBD.VDI)
CURRENT_POOL_OPERATIONS[poolId] = { ...OPERATION_OBJECT, state: 4 }
await xapi.call('SR.scan', xapi.getObject(xosansr).$ref)
await xapi.callAsync('SR.scan', xapi.getObject(xosansr).$ref)
} finally {
delete CURRENT_POOL_OPERATIONS[poolId]
}
@@ -1050,9 +1048,7 @@ export async function replaceBrick({
CURRENT_POOL_OPERATIONS[poolId] = { ...OPERATION_OBJECT, state: 1 }
await glusterCmd(
glusterEndpoint,
`volume replace-brick xosan ${previousBrick} ${
addressAndHost.brickName
} commit force`
`volume replace-brick xosan ${previousBrick} ${addressAndHost.brickName} commit force`
)
await glusterCmd(glusterEndpoint, 'peer detach ' + previousIp)
data.nodes.splice(nodeIndex, 1, {
@@ -1068,7 +1064,7 @@ export async function replaceBrick({
await xapi.deleteVm(previousVMEntry.vm, true)
}
CURRENT_POOL_OPERATIONS[poolId] = { ...OPERATION_OBJECT, state: 3 }
await xapi.call('SR.scan', xapi.getObject(xosansr).$ref)
await xapi.callAsync('SR.scan', xapi.getObject(xosansr).$ref)
} finally {
delete CURRENT_POOL_OPERATIONS[poolId]
}
@@ -1115,7 +1111,7 @@ async function _prepareGlusterVm(
const firstVif = newVM.$VIFs[0]
if (xosanNetwork.$id !== firstVif.$network.$id) {
try {
await xapi.call('VIF.move', firstVif.$ref, xosanNetwork.$ref)
await xapi.callAsync('VIF.move', firstVif.$ref, xosanNetwork.$ref)
} catch (error) {
if (error.code === 'MESSAGE_METHOD_UNKNOWN') {
// VIF.move has been introduced in xenserver 7.0
@@ -1124,15 +1120,13 @@ async function _prepareGlusterVm(
}
}
}
await xapi.addTag(newVM.$id, 'XOSAN')
await newVM.add_tags('XOSAN')
await xapi.editVm(newVM, {
name_label: `XOSAN - ${lvmSr.name_label} - ${
host.name_label
} ${labelSuffix}`,
name_label: `XOSAN - ${lvmSr.name_label} - ${host.name_label} ${labelSuffix}`,
name_description: 'Xosan VM storage',
memory: memorySize,
})
await xapi.call('VM.set_xenstore_data', newVM.$ref, xenstoreData)
await newVM.set_xenstore_data(xenstoreData)
const rootDisk = newVM.$VBDs
.map(vbd => vbd && vbd.$VDI)
.find(vdi => vdi && vdi.name_label === 'xosan_root')
@@ -1330,7 +1324,7 @@ export const addBricks = defer(async function(
data.nodes = data.nodes.concat(newNodes)
await xapi.xo.setData(xosansr, 'xosan_config', data)
CURRENT_POOL_OPERATIONS[poolId] = { ...OPERATION_OBJECT, state: 2 }
await xapi.call('SR.scan', xapi.getObject(xosansr).$ref)
await xapi.callAsync('SR.scan', xapi.getObject(xosansr).$ref)
} finally {
delete CURRENT_POOL_OPERATIONS[poolId]
}
@@ -1382,7 +1376,7 @@ export const removeBricks = defer(async function($defer, { xosansr, bricks }) {
)
remove(data.nodes, node => ips.includes(node.vm.ip))
await xapi.xo.setData(xosansr.id, 'xosan_config', data)
await xapi.call('SR.scan', xapi.getObject(xosansr._xapiId).$ref)
await xapi.callAsync('SR.scan', xapi.getObject(xosansr._xapiId).$ref)
await asyncMap(brickVMs, vm => xapi.deleteVm(vm.vm, true))
} finally {
delete CURRENT_POOL_OPERATIONS[xapi.pool.$id]
@@ -1542,9 +1536,10 @@ export async function downloadAndInstallXosanPack({ id, version, pool }) {
const res = await this.requestResource('xosan', id, version)
await xapi.installSupplementalPackOnAllHosts(res)
await xapi._updateObjectMapProperty(xapi.pool, 'other_config', {
xosan_pack_installation_time: String(Math.floor(Date.now() / 1e3)),
})
await xapi.pool.update_other_config(
'xosan_pack_installation_time',
String(Math.floor(Date.now() / 1e3))
)
}
downloadAndInstallXosanPack.description = 'Register a resource via cloud plugin'

View File

@@ -13,7 +13,6 @@ import includes from 'lodash/includes'
import proxyConsole from './proxy-console'
import pw from 'pw'
import serveStatic from 'serve-static'
import startsWith from 'lodash/startsWith'
import stoppable from 'stoppable'
import WebServer from 'http-server-plus'
import WebSocket from 'ws'
@@ -332,7 +331,7 @@ async function registerPluginsInPath(path) {
await Promise.all(
mapToArray(files, name => {
if (startsWith(name, PLUGIN_PREFIX)) {
if (name.startsWith(PLUGIN_PREFIX)) {
return registerPluginWrapper.call(
this,
`${path}/${name}`,
@@ -428,7 +427,7 @@ const setUpProxies = (express, opts, xo) => {
const { url } = req
for (const prefix in opts) {
if (startsWith(url, prefix)) {
if (url.startsWith(prefix)) {
const target = opts[prefix]
proxy.web(req, res, {
@@ -452,7 +451,7 @@ const setUpProxies = (express, opts, xo) => {
const { url } = req
for (const prefix in opts) {
if (startsWith(url, prefix)) {
if (url.startsWith(prefix)) {
const target = opts[prefix]
proxy.ws(req, socket, head, {

View File

@@ -1,6 +1,6 @@
import Collection from '../collection/redis'
import Model from '../model'
import { forEach } from '../utils'
import { forEach, serializeError } from '../utils'
import { parseProp } from './utils'
@@ -30,13 +30,28 @@ export class Servers extends Collection {
// Deserializes
forEach(servers, server => {
server.allowUnauthorized = server.allowUnauthorized === 'true'
server.enabled = server.enabled === 'true'
if (server.error) {
server.error = parseProp('server', server, 'error', '')
} else {
delete server.error
}
server.readOnly = server.readOnly === 'true'
})
return servers
}
_update(servers) {
servers.map(server => {
server.allowUnauthorized = server.allowUnauthorized ? 'true' : undefined
server.enabled = server.enabled ? 'true' : undefined
const { error } = server
server.error =
error != null ? JSON.stringify(serializeError(error)) : undefined
server.readOnly = server.readOnly ? 'true' : undefined
})
return super._update(servers)
}
}

View File

@@ -13,9 +13,7 @@ export default function proxyConsole(ws, vmConsole, sessionId) {
hostname = address
log.warn(
`host is missing in console (${vmConsole.uuid}) URI (${
vmConsole.location
}) using host address (${address}) as fallback`
`host is missing in console (${vmConsole.uuid}) URI (${vmConsole.location}) using host address (${address}) as fallback`
)
}

View File

@@ -1,5 +1,3 @@
import { startsWith } from 'lodash'
import ensureArray from './_ensureArray'
import {
extractProperty,
@@ -78,6 +76,7 @@ const TRANSFORMS = {
cores: cpuInfo && +cpuInfo.cpu_count,
sockets: cpuInfo && +cpuInfo.socket_count,
},
zstdSupported: obj.restrictions.restrict_zstd_export === 'false',
// TODO
// - ? networks = networksByPool.items[pool.id] (network.$pool.id)
@@ -119,7 +118,7 @@ const TRANSFORMS = {
size: update.installation_size,
}
if (startsWith(update.name_label, 'XS')) {
if (update.name_label.startsWith('XS')) {
// It's a patch update but for homogeneity, we're still using pool_patches
} else {
supplementalPacks.push(formattedUpdate)
@@ -143,7 +142,8 @@ const TRANSFORMS = {
},
current_operations: obj.current_operations,
hostname: obj.hostname,
iSCSI_name: otherConfig.iscsi_iqn || null,
iscsiIqn: obj.iscsi_iqn ?? otherConfig.iscsi_iqn ?? '',
zstdSupported: obj.license_params.restrict_zstd_export === 'false',
license_params: obj.license_params,
license_server: obj.license_server,
license_expiry: toTimestamp(obj.license_params.expiry),
@@ -265,6 +265,17 @@ const TRANSFORMS = {
}
}
// Build a { taskId → operation } map instead of forwarding the
// { taskRef → operation } map directly
const currentOperations = {}
const { $xapi } = obj
forEach(obj.current_operations, (operation, ref) => {
const task = $xapi.getObjectByRef(ref, undefined)
if (task !== undefined) {
currentOperations[task.$id] = operation
}
})
const vm = {
// type is redefined after for controllers/, templates &
// snapshots.
@@ -281,7 +292,7 @@ const TRANSFORMS = {
? +metrics.VCPUs_number
: +obj.VCPUs_at_startup,
},
current_operations: obj.current_operations,
current_operations: currentOperations,
docker: (function() {
const monitor = otherConfig['xscontainer-monitor']
if (!monitor) {
@@ -519,6 +530,7 @@ const TRANSFORMS = {
name_description: obj.name_description,
name_label: obj.name_label,
parent: obj.sm_config['vhd-parent'],
size: +obj.virtual_size,
snapshots: link(obj, 'snapshots'),
tags: obj.tags,
@@ -569,6 +581,16 @@ const TRANSFORMS = {
MAC: obj.MAC,
MTU: +obj.MTU,
// in kB/s
rateLimit: (() => {
if (obj.qos_algorithm_type === 'ratelimit') {
const { kbps } = obj.qos_algorithm_params
if (kbps !== undefined) {
return +kbps
}
}
})(),
$network: link(obj, 'network'),
$VM: link(obj, 'VM'),
}

View File

@@ -4,7 +4,6 @@ import synchronized from 'decorator-synchronized'
import { BaseError } from 'make-error'
import {
defaults,
endsWith,
findKey,
forEach,
identity,
@@ -184,7 +183,7 @@ const STATS = {
transformValue: value => value * 1024,
},
memory: {
test: metricType => endsWith(metricType, 'memory'),
test: metricType => metricType.endsWith('memory'),
},
cpus: {
test: /^cpu(\d+)$/,

View File

@@ -22,15 +22,15 @@ import { forbiddenOperation } from 'xo-common/api-errors'
import { Xapi as XapiBase, NULL_REF } from 'xen-api'
import {
every,
find,
filter,
find,
flatMap,
flatten,
groupBy,
includes,
isEmpty,
noop,
omit,
startsWith,
uniq,
} from 'lodash'
import { satisfies as versionSatisfies } from 'semver'
@@ -228,14 +228,6 @@ export default class Xapi extends XapiBase {
// =================================================================
_setObjectProperty(object, name, value) {
return this.call(
`${object.$type}.set_${camelToSnakeCase(name)}`,
object.$ref,
prepareXapiParam(value)
)
}
_setObjectProperties(object, props) {
const { $ref: ref, $type: type } = object
@@ -254,101 +246,10 @@ export default class Xapi extends XapiBase {
)::ignoreErrors()
}
async _updateObjectMapProperty(object, prop, values) {
const { $ref: ref, $type: type } = object
prop = camelToSnakeCase(prop)
const add = `${type}.add_to_${prop}`
const remove = `${type}.remove_from_${prop}`
await Promise.all(
mapToArray(values, (value, name) => {
if (value !== undefined) {
name = camelToSnakeCase(name)
const removal = this.call(remove, ref, name)
return value === null
? removal
: removal
::ignoreErrors()
.then(() => this.call(add, ref, name, prepareXapiParam(value)))
}
})
)
}
async setHostProperties(id, { nameLabel, nameDescription }) {
await this._setObjectProperties(this.getObject(id), {
nameLabel,
nameDescription,
})
}
async setPoolProperties({ autoPoweron, nameLabel, nameDescription }) {
const { pool } = this
await Promise.all([
this._setObjectProperties(pool, {
nameLabel,
nameDescription,
}),
autoPoweron != null &&
this._updateObjectMapProperty(pool, 'other_config', {
autoPoweron: autoPoweron ? 'true' : null,
}),
])
}
async setSrProperties(id, { nameLabel, nameDescription }) {
await this._setObjectProperties(this.getObject(id), {
nameLabel,
nameDescription,
})
}
async setNetworkProperties(
id,
{ automatic, defaultIsLocked, nameDescription, nameLabel }
) {
let defaultLockingMode
if (defaultIsLocked != null) {
defaultLockingMode = defaultIsLocked ? 'disabled' : 'unlocked'
}
const network = this.getObject(id)
await Promise.all([
this._setObjectProperties(network, {
defaultLockingMode,
nameDescription,
nameLabel,
}),
this._updateObjectMapProperty(network, 'other_config', {
automatic:
automatic === undefined ? undefined : automatic ? 'true' : null,
}),
])
}
// =================================================================
async addTag(id, tag) {
const { $ref: ref, $type: type } = this.getObject(id)
await this.call(`${type}.add_tags`, ref, tag)
}
async removeTag(id, tag) {
const { $ref: ref, $type: type } = this.getObject(id)
await this.call(`${type}.remove_tags`, ref, tag)
}
// =================================================================
async setDefaultSr(srId) {
this._setObjectProperties(this.pool, {
default_SR: this.getObject(srId).$ref,
})
setDefaultSr(srId) {
return this.pool.set_default_SR(this.getObject(srId).$ref)
}
// =================================================================
@@ -377,12 +278,12 @@ export default class Xapi extends XapiBase {
await pSettle(
mapToArray(vms, vm => {
if (!vm.is_control_domain) {
return this.call('VM.suspend', vm.$ref)
return this.callAsync('VM.suspend', vm.$ref)
}
})
)
await this.call('host.disable', host.$ref)
await this.call('host.shutdown', host.$ref)
await this.callAsync('host.shutdown', host.$ref)
}
// =================================================================
@@ -395,7 +296,7 @@ export default class Xapi extends XapiBase {
await this.call('host.disable', ref)
try {
await this.call('host.evacuate', ref)
await this.callAsync('host.evacuate', ref)
} catch (error) {
if (!force) {
await this.call('host.enable', ref)
@@ -410,7 +311,7 @@ export default class Xapi extends XapiBase {
}
async forgetHost(hostId) {
await this.call('host.destroy', this.getObject(hostId).$ref)
await this.callAsync('host.destroy', this.getObject(hostId).$ref)
}
async ejectHostFromPool(hostId) {
@@ -445,9 +346,7 @@ export default class Xapi extends XapiBase {
$defer(() => this.plugPbd(ref))
})
return this._updateObjectMapProperty(
host,
'other_config',
return host.update_other_config(
multipathing
? {
multipathing: 'true',
@@ -460,23 +359,23 @@ export default class Xapi extends XapiBase {
}
async powerOnHost(hostId) {
await this.call('host.power_on', this.getObject(hostId).$ref)
await this.callAsync('host.power_on', this.getObject(hostId).$ref)
}
async rebootHost(hostId, force = false) {
const host = this.getObject(hostId)
await this._clearHost(host, force)
await this.call('host.reboot', host.$ref)
await this.callAsync('host.reboot', host.$ref)
}
async restartHostAgent(hostId) {
await this.call('host.restart_agent', this.getObject(hostId).$ref)
await this.callAsync('host.restart_agent', this.getObject(hostId).$ref)
}
async setRemoteSyslogHost(hostId, syslogDestination) {
const host = this.getObject(hostId)
await this.call('host.set_logging', host.$ref, {
await host.set_logging({
syslog_destination: syslogDestination,
})
await this.call('host.syslog_reconfigure', host.$ref)
@@ -486,7 +385,7 @@ export default class Xapi extends XapiBase {
const host = this.getObject(hostId)
await this._clearHost(host, force)
await this.call('host.shutdown', host.$ref)
await this.callAsync('host.shutdown', host.$ref)
}
// =================================================================
@@ -500,7 +399,7 @@ export default class Xapi extends XapiBase {
}`
)
return this.call('VM.clone', vm.$ref, nameLabel)
return this.callAsync('VM.clone', vm.$ref, nameLabel).then(extractOpaqueRef)
}
// Copy a VM: make a normal copy of a VM and all its VDIs.
@@ -571,12 +470,7 @@ export default class Xapi extends XapiBase {
stream = stream.pipe(sizeStream)
const onVmCreation =
nameLabel !== undefined
? vm =>
targetXapi._setObjectProperties(vm, {
nameLabel,
})
: null
nameLabel !== undefined ? vm => vm.set_name_label(nameLabel) : null
const vm = await targetXapi._getOrWaitObject(
await targetXapi._importVm(stream, sr, onVmCreation)
@@ -716,17 +610,13 @@ export default class Xapi extends XapiBase {
// 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)
await this.callAsync('VM.hard_shutdown', $ref)
}
await Promise.all([
this.call('VM.set_is_a_template', vm.$ref, false),
this._updateObjectMapProperty(vm, 'blocked_operations', {
destroy: null,
}),
this._updateObjectMapProperty(vm, 'other_config', {
default_template: null,
}),
vm.set_is_a_template(false),
vm.update_blocked_operations('destroy', null),
vm.update_other_config('default_template', null),
])
// must be done before destroying the VM
@@ -734,7 +624,7 @@ export default class Xapi extends XapiBase {
// this cannot be done in parallel, otherwise disks and snapshots will be
// destroyed even if this fails
await this.call('VM.destroy', $ref)
await this.callAsync('VM.destroy', $ref)
return Promise.all([
asyncMap(vm.$snapshots, snapshot =>
@@ -939,7 +829,7 @@ export default class Xapi extends XapiBase {
}
// If the VDI name start with `[NOBAK]`, do not export it.
if (startsWith(vdi.name_label, '[NOBAK]')) {
if (vdi.name_label.startsWith('[NOBAK]')) {
// FIXME: find a way to not create the VDI snapshot in the
// first time.
//
@@ -1065,23 +955,21 @@ export default class Xapi extends XapiBase {
await this._createVmRecord({
...delta.vm,
affinity: null,
blocked_operations: {
...delta.vm.blocked_operations,
start: 'Importing…',
},
ha_always_run: false,
is_a_template: false,
name_label: `[Importing…] ${name_label}`,
other_config: {
...delta.vm.other_config,
[TAG_COPY_SRC]: delta.vm.uuid,
},
})
)
$defer.onFailure(() => this._deleteVm(vm))
await Promise.all([
this._setObjectProperties(vm, {
name_label: `[Importing…] ${name_label}`,
}),
this._updateObjectMapProperty(vm, 'blocked_operations', {
start: 'Importing…',
}),
this._updateObjectMapProperty(vm, 'other_config', {
[TAG_COPY_SRC]: delta.vm.uuid,
}),
])
// 2. Delete all VBDs which may have been created by the import.
await asyncMap(vm.$VBDs, vbd => this._deleteVbd(vbd))::ignoreErrors()
@@ -1103,9 +991,7 @@ export default class Xapi extends XapiBase {
newVdi = await this._getOrWaitObject(await this._cloneVdi(baseVdi))
$defer.onFailure(() => this._deleteVdi(newVdi.$ref))
await this._updateObjectMapProperty(newVdi, 'other_config', {
[TAG_COPY_SRC]: vdi.uuid,
})
await newVdi.update_other_config(TAG_COPY_SRC, vdi.uuid)
} else {
newVdi = await this.createVdi({
...vdi,
@@ -1200,15 +1086,15 @@ export default class Xapi extends XapiBase {
}
await Promise.all([
this._setObjectProperties(vm, {
name_label,
}),
delta.vm.ha_always_run && vm.set_ha_always_run(true),
vm.set_name_label(name_label),
// FIXME: move
this._updateObjectMapProperty(vm, 'blocked_operations', {
start: disableStartAfterImport
vm.update_blocked_operations(
'start',
disableStartAfterImport
? 'Do not start this VM, clone it if you want to use it.'
: null,
}),
: null
),
])
return { transferSize, vm }
@@ -1262,7 +1148,7 @@ export default class Xapi extends XapiBase {
)
const loop = () =>
this.call(
this.callAsync(
'VM.migrate_send',
vm.$ref,
token,
@@ -1272,11 +1158,14 @@ export default class Xapi extends XapiBase {
{
force: 'true',
}
// FIXME: missing param `vgu_map`, it does not cause issues ATM but it
// might need to be changed one day.
// {},
)::pCatch({ code: 'TOO_MANY_STORAGE_MIGRATES' }, () =>
pDelay(1e4).then(loop)
)
return loop()
return loop().then(noop)
}
@synchronized()
@@ -1431,14 +1320,8 @@ export default class Xapi extends XapiBase {
$defer.onFailure(() => this._deleteVm(vm))
// Disable start and change the VM name label during import.
await Promise.all([
this.addForbiddenOperationToVm(
vm.$id,
'start',
'OVA import in progress...'
),
this._setObjectProperties(vm, {
name_label: `[Importing...] ${nameLabel}`,
}),
vm.update_blocked_operations('start', 'OVA import in progress...'),
vm.set_name_label(`[Importing...] ${nameLabel}`),
])
// 2. Create VDIs & Vifs.
@@ -1455,7 +1338,7 @@ export default class Xapi extends XapiBase {
$defer.onFailure(() => this._deleteVdi(vdi.$ref))
return this.createVbd({
userdevice: disk.position,
userdevice: String(disk.position),
vdi,
vm,
})
@@ -1498,8 +1381,8 @@ export default class Xapi extends XapiBase {
// Enable start and restore the VM name label after import.
await Promise.all([
this.removeForbiddenOperationFromVm(vm.$id, 'start'),
this._setObjectProperties(vm, { name_label: nameLabel }),
vm.update_blocked_operations('start', null),
vm.set_name_label(nameLabel),
])
return vm
}
@@ -1546,7 +1429,7 @@ export default class Xapi extends XapiBase {
})
} else {
try {
await this.call('VM.pool_migrate', vm.$ref, host.$ref, {
await this.callAsync('VM.pool_migrate', vm.$ref, host.$ref, {
force: 'true',
})
} catch (error) {
@@ -1631,19 +1514,11 @@ export default class Xapi extends XapiBase {
return /* await */ this._snapshotVm(this.getObject(vmId), nameLabel)
}
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) {
log.debug(`Starting VM ${vm.name_label}`)
if (force) {
await this._updateObjectMapProperty(vm, 'blocked_operations', {
start: null,
})
await vm.update_blocked_operations('start', null)
}
return host === undefined
@@ -1653,7 +1528,7 @@ export default class Xapi extends XapiBase {
false, // Start paused?
false // Skip pre-boot checks?
)
: this.call('VM.start_on', vm.$ref, host.$ref, false, false)
: this.callAsync('VM.start_on', vm.$ref, host.$ref, false, false)
}
async startVm(vmId, hostId, force) {
@@ -1682,16 +1557,12 @@ export default class Xapi extends XapiBase {
if (isVmHvm(vm)) {
const { order } = vm.HVM_boot_params
await this._updateObjectMapProperty(vm, 'HVM_boot_params', {
order: 'd',
})
await vm.update_HVM_boot_params('order', 'd')
try {
await this._startVm(vm)
} finally {
await this._updateObjectMapProperty(vm, 'HVM_boot_params', {
order,
})
await vm.update_HVM_boot_params('order', order)
}
} else {
// Find the original template by name (*sigh*).
@@ -1713,20 +1584,14 @@ export default class Xapi extends XapiBase {
const cdDrive = this._getVmCdDrive(vm)
forEach(vm.$VBDs, vbd => {
promises.push(
this._setObjectProperties(vbd, {
bootable: vbd === cdDrive,
})
)
promises.push(vbd.set_bootable(vbd === cdDrive))
bootables.push([vbd, Boolean(vbd.bootable)])
})
promises.push(
this._setObjectProperties(vm, {
PV_bootloader: 'eliloader',
}),
this._updateObjectMapProperty(vm, 'other_config', {
vm.set_PV_bootloader('eliloader'),
vm.update_other_config({
'install-distro':
template && template.other_config['install-distro'],
'install-repository': 'cdrom',
@@ -1737,35 +1602,15 @@ export default class Xapi extends XapiBase {
await this._startVm(vm)
} finally {
this._setObjectProperties(vm, {
PV_bootloader: bootloader,
})::ignoreErrors()
vm.set_PV_bootloader(bootloader)::ignoreErrors()
forEach(bootables, ([vbd, bootable]) => {
this._setObjectProperties(vbd, { bootable })::ignoreErrors()
vbd.set_bootable(bootable)::ignoreErrors()
})
}
}
}
// vm_operations: http://xapi-project.github.io/xen-api/classes/vm.html
async addForbiddenOperationToVm(vmId, operation, reason) {
await this.call(
'VM.add_to_blocked_operations',
this.getObject(vmId).$ref,
operation,
`[XO] ${reason}`
)
}
async removeForbiddenOperationFromVm(vmId, operation) {
await this.call(
'VM.remove_from_blocked_operations',
this.getObject(vmId).$ref,
operation
)
}
// =================================================================
async createVbd({
@@ -1826,14 +1671,14 @@ export default class Xapi extends XapiBase {
})
if (isVmRunning(vm)) {
await this.call('VBD.plug', vbdRef)
await this.callAsync('VBD.plug', vbdRef)
}
}
_cloneVdi(vdi) {
log.debug(`Cloning VDI ${vdi.name_label}`)
return this.call('VDI.clone', vdi.$ref)
return this.callAsync('VDI.clone', vdi.$ref).then(extractOpaqueRef)
}
async createVdi({
@@ -1856,7 +1701,7 @@ export default class Xapi extends XapiBase {
log.debug(`Creating VDI ${name_label} on ${sr.name_label}`)
return this._getOrWaitObject(
await this.call('VDI.create', {
await this.callAsync('VDI.create', {
name_description,
name_label,
other_config,
@@ -1868,7 +1713,7 @@ export default class Xapi extends XapiBase {
type,
virtual_size: size !== undefined ? parseSize(size) : virtual_size,
xenstore_data,
})
}).then(extractOpaqueRef)
)
}
@@ -1881,14 +1726,15 @@ export default class Xapi extends XapiBase {
}
log.debug(
`Moving VDI ${vdi.name_label} from ${vdi.$SR.name_label} to ${
sr.name_label
}`
`Moving VDI ${vdi.name_label} from ${vdi.$SR.name_label} to ${sr.name_label}`
)
try {
await pRetry(() => this.call('VDI.pool_migrate', vdi.$ref, sr.$ref, {}), {
when: { code: 'TOO_MANY_STORAGE_MIGRATES' },
})
await pRetry(
() => this.callAsync('VDI.pool_migrate', vdi.$ref, sr.$ref, {}),
{
when: { code: 'TOO_MANY_STORAGE_MIGRATES' },
}
)
} catch (error) {
const { code } = error
if (
@@ -1899,7 +1745,9 @@ export default class Xapi extends XapiBase {
throw error
}
const newVdi = await this.barrier(
await this.call('VDI.copy', vdi.$ref, sr.$ref)
await this.callAsync('VDI.copy', vdi.$ref, sr.$ref).then(
extractOpaqueRef
)
)
await asyncMap(vdi.$VBDs, async vbd => {
await this.call('VBD.destroy', vbd.$ref)
@@ -1917,7 +1765,7 @@ export default class Xapi extends XapiBase {
log.debug(`Deleting VDI ${vdiRef}`)
try {
await this.call('VDI.destroy', vdiRef)
await this.callAsync('VDI.destroy', vdiRef)
} catch (error) {
if (error?.code !== 'HANDLE_INVALID') {
throw error
@@ -1930,7 +1778,7 @@ export default class Xapi extends XapiBase {
`Resizing VDI ${vdi.name_label} from ${vdi.virtual_size} to ${size}`
)
return this.call('VDI.resize', vdi.$ref, size)
return this.callAsync('VDI.resize', vdi.$ref, size)
}
_getVmCdDrive(vm) {
@@ -1944,7 +1792,7 @@ export default class Xapi extends XapiBase {
async _ejectCdFromVm(vm) {
const cdDrive = this._getVmCdDrive(vm)
if (cdDrive) {
await this.call('VBD.eject', cdDrive.$ref)
await this.callAsync('VBD.eject', cdDrive.$ref)
}
}
@@ -1952,20 +1800,20 @@ export default class Xapi extends XapiBase {
const cdDrive = await this._getVmCdDrive(vm)
if (cdDrive) {
try {
await this.call('VBD.insert', cdDrive.$ref, cd.$ref)
await this.callAsync('VBD.insert', cdDrive.$ref, cd.$ref)
} catch (error) {
if (!force || error.code !== 'VBD_NOT_EMPTY') {
throw error
}
await this.call('VBD.eject', cdDrive.$ref)::ignoreErrors()
await this.callAsync('VBD.eject', cdDrive.$ref)::ignoreErrors()
// Retry.
await this.call('VBD.insert', cdDrive.$ref, cd.$ref)
await this.callAsync('VBD.insert', cdDrive.$ref, cd.$ref)
}
if (bootable !== Boolean(cdDrive.bootable)) {
await this._setObjectProperties(cdDrive, { bootable })
await cdDrive.set_bootable(bootable)
}
} else {
await this.createVbd({
@@ -1978,7 +1826,7 @@ export default class Xapi extends XapiBase {
}
async connectVbd(vbdId) {
await this.call('VBD.plug', vbdId)
await this.callAsync('VBD.plug', vbdId)
}
async _disconnectVbd(vbd) {
@@ -1987,7 +1835,7 @@ export default class Xapi extends XapiBase {
await this.call('VBD.unplug_force', vbd.$ref)
} catch (error) {
if (error.code === 'VBD_NOT_UNPLUGGABLE') {
await this.call('VBD.set_unpluggable', vbd.$ref, true)
await vbd.set_unpluggable(true)
return this.call('VBD.unplug_force', vbd.$ref)
}
}
@@ -2038,11 +1886,11 @@ export default class Xapi extends XapiBase {
const vdi = this.getObject(vdiId)
const snap = await this._getOrWaitObject(
await this.call('VDI.snapshot', vdi.$ref)
await this.callAsync('VDI.snapshot', vdi.$ref).then(extractOpaqueRef)
)
if (nameLabel) {
await this.call('VDI.set_name_label', snap.$ref, nameLabel)
await snap.set_name_label(nameLabel)
}
return snap
@@ -2166,7 +2014,7 @@ export default class Xapi extends XapiBase {
)
if (currently_attached && isVmRunning(vm)) {
await this.call('VIF.plug', vifRef)
await this.callAsync('VIF.plug', vifRef)
}
return vifRef
@@ -2194,7 +2042,7 @@ export default class Xapi extends XapiBase {
// https://citrix.github.io/xenserver-sdk/#network
other_config: { automatic: 'false' },
})
$defer.onFailure(() => this.call('network.destroy', networkRef))
$defer.onFailure(() => this.callAsync('network.destroy', networkRef))
if (pifId) {
await this.call(
'pool.create_VLAN_from_PIF',
@@ -2233,7 +2081,7 @@ export default class Xapi extends XapiBase {
await Promise.all(
mapToArray(
vlans,
vlan => vlan !== NULL_REF && this.call('VLAN.destroy', vlan)
vlan => vlan !== NULL_REF && this.callAsync('VLAN.destroy', vlan)
)
)
@@ -2248,7 +2096,7 @@ export default class Xapi extends XapiBase {
newPifs,
pifRef =>
!wasAttached[this.getObject(pifRef).host] &&
this.call('PIF.unplug', pifRef)::ignoreErrors()
this.callAsync('PIF.unplug', pifRef)::ignoreErrors()
)
)
}
@@ -2279,7 +2127,7 @@ export default class Xapi extends XapiBase {
await Promise.all(
mapToArray(
vlans,
vlan => vlan !== NULL_REF && this.call('VLAN.destroy', vlan)
vlan => vlan !== NULL_REF && this.callAsync('VLAN.destroy', vlan)
)
)
@@ -2288,7 +2136,17 @@ export default class Xapi extends XapiBase {
mapToArray(bonds, bond => this.call('Bond.destroy', bond))
)
await this.call('network.destroy', network.$ref)
const tunnels = filter(this.objects.all, { $type: 'tunnel' })
await Promise.all(
map(pifs, async pif => {
const tunnel = find(tunnels, { access_PIF: pif.$ref })
if (tunnel != null) {
await this.callAsync('tunnel.destroy', tunnel.$ref)
}
})
)
await this.callAsync('network.destroy', network.$ref)
}
// =================================================================
@@ -2488,7 +2346,7 @@ export default class Xapi extends XapiBase {
)
}
async _assertConsistentHostServerTime(hostRef) {
async assertConsistentHostServerTime(hostRef) {
const delta =
parseDateTime(await this.call('host.get_servertime', hostRef)).getTime() -
Date.now()
@@ -2500,4 +2358,27 @@ export default class Xapi extends XapiBase {
)
}
}
async isHyperThreadingEnabled(hostId) {
try {
return (
(await this.call(
'host.call_plugin',
this.getObject(hostId).$ref,
'hyperthreading.py',
'get_hyperthreading',
{}
)) !== 'false'
)
} catch (error) {
if (
error.code === 'XENAPI_MISSING_PLUGIN' ||
error.code === 'UNKNOWN_XENAPI_PLUGIN_FUNCTION'
) {
return null
} else {
throw error
}
}
}
}

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