Compare commits

...

307 Commits

Author SHA1 Message Date
badrAZ
802a7a4463 feat(xo-web): 5.49.0 2019-09-05 16:17:00 +02:00
badrAZ
3b3d6ba13c feat(xo-server): 5.49.0 2019-09-05 16:16:52 +02:00
badrAZ
7350bf58e2 feat(xo-server-sdn-controller): 0.2.1 2019-09-05 16:16:00 +02:00
badrAZ
d37e29afc6 fix(xo-web/home): wait initial fetch before state.objects.fetched (#4456)
Fixes #4420
2019-09-05 15:42:13 +02:00
Julien Fontanet
40de8c9e23 fix(xo-server/createVdi): ignore sm_config (#4484)
Fixes #4482
2019-09-05 14:42:30 +02:00
badrAZ
c81eac13c8 fix(xo-server/cr): handle undefined vdis (#4417)
Fixes #4416
2019-09-05 14:31:12 +02:00
badrAZ
a6e1860f0d fix(xo-web/network): fix inability to create bonded network (#4489)
Fixes xoa-support#1725
2019-09-05 13:39:31 +02:00
Nicolas Raynaud
03eb2d81f0 feat(xo-server/patching): fewer XCP-ng updater plugin requests (#4477)
Fixes #4358
2019-09-05 11:58:54 +02:00
badrAZ
171710b5e8 feat(xo-web/backup-ng/new): warning if zstd is not supported (#4411)
Fixes #3892
2019-09-05 11:44:48 +02:00
BenjiReis
bed76429c2 fix(xo-server-sdn-controller):don't add host to network when no tunnel available (#4480) 2019-09-05 08:50:42 +02:00
Pierre Donias
d19f9b5062 fix(xo-web/build): disable uglify inline optimization (#4485)
Fixes #4377

See mishoo/UglifyJS2#2842
2019-09-04 17:49:55 +02:00
badrAZ
38081d9822 fix(xo-server/api/xosan): missing params definition (#4478)
Fixes xoa-support #1724
2019-09-04 16:23:14 +02:00
BenjiReis
54e278d3f7 doc(xo-server-sdn-controller): enhance documentation (#4461) 2019-09-04 08:48:27 +02:00
BenjiReis
181ed1b1a5 chore(xo-server-sdn-controller): namespace 'other_config' entries (#4473) 2019-09-03 14:39:44 +02:00
Pierre Donias
fb2d325ccb fix(xo-web/xo): missing Promise.alls (#4469) 2019-09-02 16:34:23 +02:00
badrAZ
5f94a52537 fix(xo-server/xapi-object-to-xo): fix incorrect PBD entry name (#4466)
Fixes #4465

Introduced by 77c62d6e7d
2019-09-02 10:33:40 +02:00
Julien Fontanet
c69b50c5d2 chore: update dependencies 2019-09-02 09:45:38 +02:00
BenjiReis
1c72f89178 fix(xo-server-sdn-controller): same VNI for nodes of cross pool network (#4464) 2019-08-30 14:30:28 +02:00
HamadaBrest
14bd16da14 feat(xo-web/new/sr): clarify address formats (#4460)
Fixes #4450
2019-08-30 11:08:37 +02:00
badrAZ
11a57f4618 chore(CHANGELOG): 5.38.0 2019-08-29 17:06:56 +02:00
badrAZ
57f35aff90 chore(CHANGELOG): update next 2019-08-29 13:39:26 +02:00
badrAZ
60e63a307f feat(xo-web): 5.48.1 2019-08-29 13:16:10 +02:00
badrAZ
175e878ea6 chore(CHANGELOG): update next 2019-08-28 17:27:10 +02:00
badrAZ
5c960a3213 feat(xo-server-sdn-controller): 0.2.0 2019-08-28 16:59:28 +02:00
BenjiReis
5dfb299e37 chore(xo-server-sdn-controller): use ?. to simplify tests (#4459) 2019-08-28 16:49:38 +02:00
BenjiReis
3890d4d9d1 feat(xo-server-sdn-controller): cross-pool private networks (#4405) 2019-08-28 16:31:33 +02:00
badrAZ
77c62d6e7d fix(xo-server/xapi-object-to-xo): obfuscate cifs password from PBD device_config (#4401)
Fixes #4384
2019-08-27 17:37:57 +02:00
badrAZ
ba54b53194 fix(xo-server-test/backup-ng): reduce timeout for delta backup scenario (#4454)
Introduced by 1ce3368530
2019-08-27 10:08:18 +02:00
BenjiReis
b4ef7352f2 chore(xo-server-sdn-controller): Do not specify test when comparing boolean (#4452) 2019-08-27 09:48:53 +02:00
badrAZ
1ce3368530 fix(xo-server-test/backup-ng): large timeout for delta backup scenario (#4453) 2019-08-27 09:45:32 +02:00
BenjiReis
a4b32f3cb7 chore(xo-server-sdn-controller): better name for port & iface in OVS (#4451) 2019-08-27 09:03:55 +02:00
badrAZ
ee9cc05ae0 feat(PULL_REQUEST_TEMPLATE): xo-server-test integration (#4447) 2019-08-26 13:59:40 +02:00
BenjiReis
b8ccf2b0d6 chore(xo-server-sdn-controller): parallelize adding port and interface (#4448) 2019-08-26 13:15:36 +02:00
badrAZ
886b499b94 feat(xo-server-test): update documentation 2019-08-26 11:54:48 +02:00
heafalan
07924d5621 feat(xo-server-test): delta backup test (#4393)
See #4307
2019-08-26 11:20:39 +02:00
BenjiReis
43f3367ae4 chore(xo-server-sdn-controller): use xen-api helpers (#4445) 2019-08-26 09:23:45 +02:00
Julien Fontanet
454c73f42f chore(xo-server/registerHttpRequest): simplify code 2019-08-23 17:54:54 +02:00
BenjiReis
041df698d5 chore(xo-server-sdn-controller): unify (in/de)crement style (#4444) 2019-08-23 10:42:51 +02:00
BenjiReis
97081f1219 chore(xo-server-sdn-controller): use destructuring (#4443) 2019-08-23 10:03:04 +02:00
Pierre Donias
f6792bf080 fix(CHANGELOG): entry indentation 2019-08-22 14:41:32 +02:00
Pierre Donias
88635f31d6 chore(CHANGELOG): update next 2019-08-22 14:36:30 +02:00
Pierre Donias
abd0f115fc feat(xo-web): 5.48.0 2019-08-22 14:28:39 +02:00
Pierre Donias
e9766c76c1 feat(xo-server): 5.48.0 2019-08-22 14:28:30 +02:00
Pierre Donias
570506b324 feat(xo-server-sdn-controller): 0.1.3 2019-08-22 14:27:50 +02:00
BenjiReis
11889880eb feat(xo-server-sdn-controller): use VNI (#4435) 2019-08-22 14:22:01 +02:00
Pierre Donias
a86abde893 fix(xo-web/createBondedNetwork): remove vlan param (#4429)
Fixes #4425
Introduced by 7a2a88b7ad
2019-08-22 09:25:19 +02:00
Julien Fontanet
2cfe3360d8 feat(xo-server/config): enable guessVhdSizeOnImport by default (#4436) 2019-08-21 17:29:43 +02:00
Pierre Donias
60d75cb8ee chore(xo-web): improve display of XOA related pages for sources users (#4434)
- XOSAN
- Updater
2019-08-21 14:51:26 +02:00
BenjiReis
68838e310a feat(xo-web,sdn-controller): tip on network creation (#4431)
"Warning: a pool can have 1 GRE and 1 VxLAN network max"
2019-08-21 09:41:05 +02:00
Jon Sands
161de6cb7c fix(docs/troubleshooting): fix sudo usage (#4427) 2019-08-20 17:28:36 +02:00
Pierre Donias
af5a9b644b feat(xo-web/about): XOA support message (#4433) 2019-08-20 16:22:51 +02:00
Pierre Donias
785426eab5 feat(xo-web/config): unlock import/export config for everyone (#4432) 2019-08-20 12:21:31 +02:00
Jon Sands
9267aef498 clarify that ssl proxy changes require sudo (#4424) 2019-08-17 01:28:11 +02:00
Rajaa.BARHTAOUI
ae27a07578 feat(xo-web/usage): show the item if there's only 1 'other' (#4419)
Fixes #4392
2019-08-14 09:45:01 +02:00
Rajaa.BARHTAOUI
131b2a35aa fix(xo-server/vm/revert): set admin ACL on created snapshot (#4391)
Fixes #4331
2019-08-13 16:39:52 +02:00
Olivier Lambert
5a89601b24 update doc for web deploy 2019-08-11 19:58:24 +02:00
badrAZ
2528bbc552 chore(CHANGELOG): update next 2019-08-08 16:26:04 +02:00
badrAZ
7c3a480003 feat(xo-web): 5.47.0 2019-08-08 16:08:16 +02:00
badrAZ
80eac8443d feat(xo-server): 5.47.0 2019-08-08 16:07:38 +02:00
badrAZ
a97234c48d feat(xo-server-usage-report): 0.7.3 2019-08-08 16:06:39 +02:00
badrAZ
53ea58c2f6 chore(CHANGELOG): v5.37.1 2019-08-06 14:22:11 +02:00
badrAZ
d867524c6b feat(xo-server-sdn-controller): 0.1.2 2019-08-06 13:51:43 +02:00
badrAZ
5edf9bde78 fix(xo-web/copyVms): handle undefined container (#4402)
Introduced by 770ea55872
2019-08-02 17:01:06 +02:00
badrAZ
770ea55872 feat(xo-web#copyVms): warning if some VMs don't support ZSTD (#4346)
See #3892
2019-08-02 15:51:08 +02:00
Rajaa.BARHTAOUI
4eb0101c5b feat(xo-web/vm/attachDisk): warning when VDI already attached (#4366)
Fixes #3381
2019-08-02 15:43:15 +02:00
badrAZ
5d7af94abf fix(xo-server/xen-servers): error on connecting a connected server (#4396) 2019-08-02 14:38:49 +02:00
Rajaa.BARHTAOUI
b729b8f7c8 fix(xo-web/vm/attachDisk): VBDs mode check (#4373) 2019-08-02 14:37:15 +02:00
BenjiReis
064e69d943 chore(xo-server-sdn-controller): simplify _objectsUpdated method (#4394) 2019-08-02 11:58:05 +02:00
heafalan
d880931951 feat(xo-server-test/xoConnection): add a TOFIX (#4399) 2019-08-02 11:56:07 +02:00
BenjiReis
f24741cd32 chore(sdn-controller): use simpler syntax to clearify the code (#4398) 2019-08-02 11:26:42 +02:00
BenjiReis
45c7017e83 chore(xo-server-sdn-controller): keep arrays immutable (#4397) 2019-08-02 11:05:23 +02:00
heafalan
7cfb891e6b feat(xo-server-test): add required servers specified in the config (#4381)
Fixes #4355
2019-08-02 09:37:02 +02:00
BenjiReis
fc8604e896 fix(sdn-controller): add new hosts to pool's private networks (#4382) 2019-08-01 11:57:43 +02:00
badrAZ
6b5e94103d fix(usage-report): throw error when transport-email is disabled (#4389)
See support#1652
2019-07-31 14:25:08 +02:00
badrAZ
aee4679ae5 fix(xo-web/exportVm,copyVm): handle undefined container (#4386)
Introduced by a00e3e6f41 and 10d5228eb2
2019-07-30 17:11:16 +02:00
Pierre Donias
2c2c930fce fix(xo-server/patching): log → log.debug (#4387)
See support#1652
2019-07-30 16:36:18 +02:00
BenjiReis
3f309e4db5 fix(xo-server-sdn-controller): fix certificate generator (#4383) 2019-07-30 15:42:04 +02:00
BenjiReis
d26be402db feat(xo-server-sdn-controller/new network): choose PIF (#4379) 2019-07-30 15:05:17 +02:00
Pierre Donias
a571e83005 feat(xo-web): make source disclaimer banner dismissible (#4278) 2019-07-30 14:27:36 +02:00
badrAZ
10d5228eb2 feat(xo-web#exportVm): only show zstd option when it's supported (#4368)
See #3892
2019-07-30 10:20:42 +02:00
Rajaa.BARHTAOUI
7ed49b476f fix(xo-web/sr/general): display VDI VM name in SR usage graph (#4370) 2019-07-30 10:14:30 +02:00
BenjiReis
5396b90695 chore(xo-server-sdn-controller): unify and specify tests (#4376) 2019-07-26 11:22:03 +02:00
BenjiReis
a6983d4e7b feat(xo-server-sdn-controller): use tunnel status (#4322)
Use XAPI `tunnel.status` as described here:
https://xapi-project.github.io/xapi/design/tunnelling.html
2019-07-25 15:32:44 +02:00
heafalan
a3d1c76f67 fix(xo-server-test): extend the tests timeouts (#4374)
See #4356
2019-07-25 15:31:52 +02:00
badrAZ
15fab226b7 chore(CHANGELOG): 5.37.0 2019-07-25 14:21:58 +02:00
BenjiReis
5a065d5a05 chore(xo-server-sdn-controller): expose ovsdbClient's host (#4369) 2019-07-25 10:03:17 +02:00
badrAZ
de81f3ffbb chore(xo-server-test/README): add known issue 2019-07-24 14:20:43 +02:00
heafalan
9103369cf6 chore(xo-server-test/README.md): add warning (#4371) 2019-07-24 13:49:52 +02:00
heafalan
7be36e6d0d feat(xo-server-test): use temp resources instead of create resources for each test suite (#4359) 2019-07-24 11:31:17 +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
heafalan
dfe5f412eb feat: config.toml -> sample.config.toml and update documentation (#111) 2019-06-26 14:52:34 +02:00
heafalan
003eadc8fd fix(backup-ng): use generic test for retentions (#103) 2019-06-24 14:15:24 +02:00
badrAZ
254fa36c01 chore: structured "config.toml" (#110)
See #108
2019-06-21 15:25:29 +02:00
Julien Fontanet
814fee4f47 chore(package): update deps 2019-06-06 09:47:04 +02:00
heafalan
8924a64622 chore: delete useless test (#105) 2019-06-05 16:59:10 +02:00
heafalan
114d521636 feat: test backupNg.runJob with srs without copyRetention (#87)
See #76
2019-06-03 08:58:34 +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
heafalan
abad2944fb feat: test backupNg.runJob 3 times with retention 2 and revert (#93)
See #76
2019-05-27 10:53:34 +02:00
heafalan
2827544409 chore: backupNg.create() -> backupNg.createJob() (#96) 2019-05-22 11:55:30 +02:00
heafalan
db0a399da1 chore: use getSchedule() (#99) 2019-05-22 08:58:01 +02:00
heafalan
87d2096ed7 chore: createUser() -> createTempUser() (#100) 2019-05-15 14:21:11 +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
191 changed files with 10973 additions and 3361 deletions

View File

@@ -38,6 +38,8 @@ module.exports = {
// disabled because XAPI objects are using camel case
camelcase: ['off'],
'react/jsx-handler-names': 'off',
'no-console': ['error', { allow: ['warn', 'error'] }],
'no-var': 'error',
'node/no-extraneous-import': 'error',

View File

@@ -37,7 +37,7 @@
"@babel/preset-flow": "^7.0.0",
"babel-plugin-lodash": "^3.3.2",
"cross-env": "^5.1.3",
"rimraf": "^2.6.2"
"rimraf": "^3.0.0"
},
"scripts": {
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",

View File

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

View File

@@ -47,7 +47,7 @@
"@babel/preset-env": "^7.0.0",
"@babel/preset-flow": "^7.0.0",
"cross-env": "^5.1.3",
"rimraf": "^2.6.2"
"rimraf": "^3.0.0"
},
"scripts": {
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",

View File

@@ -35,7 +35,7 @@
"@babel/preset-flow": "^7.0.0",
"babel-plugin-lodash": "^3.3.2",
"cross-env": "^5.1.3",
"rimraf": "^2.6.2"
"rimraf": "^3.0.0"
},
"scripts": {
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",

View File

@@ -34,7 +34,7 @@
"@babel/preset-env": "^7.0.0",
"babel-plugin-lodash": "^3.3.2",
"cross-env": "^5.1.3",
"rimraf": "^2.6.2"
"rimraf": "^3.0.0"
},
"scripts": {
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",

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": [],
@@ -28,8 +28,9 @@
"execa": "^1.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,6 +41,7 @@
"@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",
@@ -47,7 +49,7 @@
"cross-env": "^5.1.3",
"dotenv": "^8.0.0",
"index-modules": "^0.3.0",
"rimraf": "^2.6.2"
"rimraf": "^3.0.0"
},
"scripts": {
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",

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

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",
@@ -40,7 +40,7 @@
"babel-plugin-lodash": "^3.3.2",
"cross-env": "^5.1.3",
"index-modules": "^0.3.0",
"rimraf": "^2.6.2"
"rimraf": "^3.0.0"
},
"scripts": {
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",

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

@@ -37,7 +37,7 @@
"babel-plugin-dev": "^1.0.0",
"babel-plugin-lodash": "^3.3.2",
"cross-env": "^5.1.3",
"rimraf": "^2.6.2"
"rimraf": "^3.0.0"
},
"scripts": {
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",

View File

@@ -4,22 +4,137 @@
### Enhancements
### Bug fixes
### Released packages
- xo-server v5.49.0
- xo-web v5.49.0
## **5.38.0** (2019-08-29)
![Channel: latest](https://badgen.net/badge/channel/latest/yellow)
### Enhancements
- [VM/Attach disk] Display confirmation modal when VDI is already attached [#3381](https://github.com/vatesfr/xen-orchestra/issues/3381) (PR [#4366](https://github.com/vatesfr/xen-orchestra/pull/4366))
- [Zstd]
- [VM/copy, VM/export] Only show zstd option when it's supported [#3892](https://github.com/vatesfr/xen-orchestra/issues/3892) (PRs [#4326](https://github.com/vatesfr/xen-orchestra/pull/4326) [#4368](https://github.com/vatesfr/xen-orchestra/pull/4368))
- [VM/Bulk copy] Show warning if zstd compression is not supported on a VM [#3892](https://github.com/vatesfr/xen-orchestra/issues/3892) (PR [#4346](https://github.com/vatesfr/xen-orchestra/pull/4346))
- [VM import & Continuous Replication] Enable `guessVhdSizeOnImport` by default, this fix some `VDI_IO_ERROR` with XenServer 7.1 and XCP-ng 8.0 (PR [#4436](https://github.com/vatesfr/xen-orchestra/pull/4436))
- [SDN Controller] Add possibility to create multiple GRE networks and VxLAN networks within a same pool (PR [#4435](https://github.com/vatesfr/xen-orchestra/pull/4435))
- [SDN Controller] Add possibility to create cross-pool private networks (PR [#4405](https://github.com/vatesfr/xen-orchestra/pull/4405))
### Bug fixes
- [SR/General] Display VDI VM name in SR usage graph (PR [#4370](https://github.com/vatesfr/xen-orchestra/pull/4370))
- [VM/Attach disk] Fix checking VDI mode (PR [#4373](https://github.com/vatesfr/xen-orchestra/pull/4373))
- [VM revert] Snapshot before: add admin ACLs on created snapshot [#4331](https://github.com/vatesfr/xen-orchestra/issues/4331) (PR [#4391](https://github.com/vatesfr/xen-orchestra/pull/4391))
- [Network] Fixed "invalid parameters" error when creating bonded network [#4425](https://github.com/vatesfr/xen-orchestra/issues/4425) (PR [#4429](https://github.com/vatesfr/xen-orchestra/pull/4429))
### Released packages
- xo-server-sdn-controller v0.2.0
- xo-server-usage-report v0.7.3
- xo-server v5.48.0
- xo-web v5.48.1
## **5.37.1** (2019-08-06)
![Channel: stable](https://badgen.net/badge/channel/stable/green)
### Enhancements
- [SDN Controller] Let the user choose on which PIF to create a private network (PR [#4379](https://github.com/vatesfr/xen-orchestra/pull/4379))
### Bug fixes
- [SDN Controller] Better detect host shutting down to adapt network topology (PR [#4314](https://github.com/vatesfr/xen-orchestra/pull/4314))
- [SDN Controller] Add new hosts to pool's private networks (PR [#4382](https://github.com/vatesfr/xen-orchestra/pull/4382))
### Released packages
- xo-server-sdn-controller v0.1.2
## **5.37.0** (2019-07-25)
### Highlights
- [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))
- [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))
- [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))
- [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))
- [VM/console] Multiline copy/pasting [#4261](https://github.com/vatesfr/xen-orchestra/issues/4261) (PR [#4341](https://github.com/vatesfr/xen-orchestra/pull/4341))
### 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] Show current operations and progress [#3811](https://github.com/vatesfr/xen-orchestra/issues/3811) (PR [#3982](https://github.com/vatesfr/xen-orchestra/pull/3982))
- [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))
- [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)
### 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
- xen-api v0.25.2
- xo-server v5.43.0
- xo-web v5.43.0
- 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: latest](https://badgen.net/badge/channel/latest/yellow)
### 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))
@@ -53,8 +168,6 @@
## **5.34.0** (2019-04-30)
![Channel: stable](https://badgen.net/badge/channel/stable/green)
### Highlights
- [Self/New VM] Add network config box to custom cloud-init [#3872](https://github.com/vatesfr/xen-orchestra/issues/3872) (PR [#4150](https://github.com/vatesfr/xen-orchestra/pull/4150))

View File

@@ -1,32 +1,33 @@
> 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
- [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))
- [auth-saml] Improve compatibility with Microsoft Azure Active Directory (PR [#4294](https://github.com/vatesfr/xen-orchestra/pull/4294))
- [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))
- [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))
- [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] 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))
- [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))
- [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))
> Users must be able to say: “Nice enhancement, I'm eager to test it”
- [SR/new] Clarify address formats [#4450](https://github.com/vatesfr/xen-orchestra/issues/4450) (PR [#4460](https://github.com/vatesfr/xen-orchestra/pull/4460))
- [Backup NG/New] Show warning if zstd compression is not supported on a VM [#3892](https://github.com/vatesfr/xen-orchestra/issues/3892) (PRs [#4411](https://github.com/vatesfr/xen-orchestra/pull/4411))
### Bug fixes
- [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))
- [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))
- [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))
- [PBD] Obfuscate cifs password from device config [#4384](https://github.com/vatesfr/xen-orchestra/issues/4384) (PR [#4401](https://github.com/vatesfr/xen-orchestra/pull/4401))
- [XOSAN] Fix "invalid parameters" error on creating a SR (PR [#4478](https://github.com/vatesfr/xen-orchestra/pull/4478))
- [Patching] Avoid overloading XCP-ng by reducing the frequency of yum update checks [#4358](https://github.com/vatesfr/xen-orchestra/issues/4358) (PR [#4477](https://github.com/vatesfr/xen-orchestra/pull/4477))
- [Network] Fix inability to create a bonded network (PR [#4489](https://github.com/vatesfr/xen-orchestra/pull/4489))
- [Backup restore & Replication] Don't copy `sm_config` to new VDIs which might leads to useless coalesces [#4482](https://github.com/vatesfr/xen-orchestra/issues/4482) (PR [#4484](https://github.com/vatesfr/xen-orchestra/pull/4484))
- [Home] Fix intermediary "no results" display showed on filtering items [#4420](https://github.com/vatesfr/xen-orchestra/issues/4420) (PR [#4456](https://github.com/vatesfr/xen-orchestra/pull/4456)
> Users must be able to say: “I had this issue, happy to know it's fixed”
### 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
> 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.2.2
- xo-server v5.49.0
- xo-web v5.49.0

View File

@@ -4,6 +4,7 @@
- [ ] PR reference the relevant issue (e.g. `Fixes #007`)
- [ ] if UI changes, a screenshot has been added to the PR
- [ ] if `xo-server` API changes, the corresponding test has been added to/updated on [`xo-server-test`](https://github.com/vatesfr/xen-orchestra/tree/master/packages/xo-server-test)
- [ ] `CHANGELOG.unreleased.md`:
- enhancement/bug fix entry added
- list of packages to release updated (`${name} v${new version}`)

View File

@@ -55,6 +55,7 @@
* [Emergency Shutdown](emergency_shutdown.md)
* [Auto scalability](auto_scalability.md)
* [Forecaster](forecaster.md)
* [SDN Controller](sdn_controller.md)
* [Recipes](recipes.md)
* [Reverse proxy](reverse_proxy.md)
* [How to contribute?](contributing.md)

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

View File

@@ -15,5 +15,6 @@ We've made multiple categories to help you to find what you need:
* [Job Manager](scheduler.html)
* [Alerts](alerts.html)
* [Load balancing](load_balancing.html)
* [SDN Controller](sdn_controller.html)
![](./assets/xo5tablet.jpg)

44
docs/sdn_controller.md Normal file
View File

@@ -0,0 +1,44 @@
# SDN Controller
> SDN Controller is available in XOA 5.44 and higher
The SDN Controller enables a user to **create pool-wide and cross-pool** (since XOA 5.48.1) **private networks**.
![](./assets/sdn-controller.png)
## How does it work?
Please read the [dedicated devblog on the SDN Controller](https://xen-orchestra.com/blog/xo-sdn-controller/) and its [extension for cross-pool private networks](https://xen-orchestra.com/blog/devblog-3-extending-the-sdn-controller/).
## Usage
### Network creation
In the network creation view:
- Select a `pool`
- Select `Private network`
- Select an interface on which to create the network's tunnels
- Select the encapsulation: a choice is offered between `GRE` and `VxLAN`, if `VxLAN` is chosen, then port 4789 must be open for UDP traffic on all the network's hosts (see [the requirements](#requirements))
- Select other `pool`s to add them to the network if desired
- For each added `pool`: select an interface on which to create the tunnels
- Create the network
- Have fun! ☺
### 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`: The path where the plugin will look for the certificates to create SSL connections with the hosts.
If none is provided, the plugin will create its own self-signed certificates.
- `override-certs`: Enable to uninstall the existing SDN controller CA certificate in order to replace it with the plugin's one.
## Requirements
> All requirements are met by running up to date XCP-ng hosts.
>
> On older XCP-ng hosts, or hosts running Citrix Hypervisor, changes might have to be done manually.
To be able to use `VxLAN`, 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`

View File

@@ -110,16 +110,17 @@ $ systemctl restart xo-server
### Behind a transparent proxy
If your are behind a transparent proxy, you'll probably have issues with the updater (SSL/TLS issues).
If you're behind a transparent proxy, you'll probably have issues with the updater (SSL/TLS issues).
First, run the following commands:
Run the following commands to allow the updater to work:
```
$ sudo -s
$ echo NODE_TLS_REJECT_UNAUTHORIZED=0 >> /etc/xo-appliance/env
$ npm config -g set strict-ssl=false
$ systemctl restart xoa-updater
```
Then, restart the updater with `systemctl restart xoa-updater`.
Now try running an update again.
### Updating SSL self-signed certificate

View File

@@ -22,26 +22,34 @@ For use on huge infrastructure (more than 500+ VMs), feel free to increase the R
### The quickest way
The fastest way to install Xen Orchestra is to use our appliance deploy script. You can deploy it by connecting to your XenServer host and executing the following:
The **fastest and most secure way** to install Xen Orchestra is to use our web deploy page. Go on https://xen-orchestra.com/#!/xoa and follow instructions.
> **Note:** no data will be sent to our servers, it's running only between your browser and your host!
### Via a bash script
Alternatively, you can deploy it by connecting to your XenServer host and executing the following:
```
bash -c "$(curl -s http://xoa.io/deploy)"
```
**Note:** This won't write or modify anything on your XenServer host: it will just import the XOA VM into your default storage repository.
Now follow the instructions:
> **Note:** This won't write or modify anything on your XenServer host: it will just import the XOA VM into your default storage repository.
Follow the instructions:
* Your IP configuration will be requested: it's set to **DHCP by default**, otherwise you can enter a fixed IP address (eg `192.168.0.10`)
* If DHCP is selected, the script will continue automatically. Otherwise a netmask, gateway, and DNS should be provided.
* XOA will be deployed on your default storage repository. You can move it elsewhere anytime after.
### The alternative
### Via download the XVA
Download XOA from xen-orchestra.com. Once you've got the XVA file, you can import it with `xe vm-import filename=xoa_unified.xva` or via XenCenter.
After the VM is imported, you just need to start it with `xe vm-start vm="XOA"` or with XenCenter.
## First Login
Once you have started the VM, you can access the web UI by putting the IP you configured during deployment into your web browser. If you did not configure an IP or are unsure, try one of the following methods to find it:
* Run `xe vm-list params=name-label,networks | grep -A 1 XOA` on your host

View File

@@ -6,10 +6,10 @@
"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-config-standard": "12.0.0",
"eslint-config-standard-jsx": "^6.0.2",
"eslint": "^6.0.1",
"eslint-config-prettier": "^6.0.0",
"eslint-config-standard": "14.1.0",
"eslint-config-standard-jsx": "^8.1.0",
"eslint-plugin-eslint-comments": "^3.1.1",
"eslint-plugin-import": "^2.8.0",
"eslint-plugin-node": "^9.0.1",
@@ -17,13 +17,13 @@
"eslint-plugin-react": "^7.6.1",
"eslint-plugin-standard": "^4.0.0",
"exec-promise": "^0.7.0",
"flow-bin": "^0.100.0",
"globby": "^9.0.0",
"husky": "^2.2.0",
"flow-bin": "^0.106.3",
"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": {
@@ -42,6 +42,7 @@
"testEnvironment": "node",
"testPathIgnorePatterns": [
"/dist/",
"/xo-server-test/",
"/xo-web/"
],
"testRegex": "\\.spec\\.js$",

View File

@@ -36,7 +36,7 @@
"@babel/preset-env": "^7.0.0",
"babel-plugin-lodash": "^3.3.2",
"cross-env": "^5.1.1",
"rimraf": "^2.6.2"
"rimraf": "^3.0.0"
},
"scripts": {
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",

View File

@@ -34,7 +34,7 @@
"@babel/preset-env": "^7.0.0",
"@babel/preset-flow": "^7.0.0",
"cross-env": "^5.1.3",
"rimraf": "^2.6.2"
"rimraf": "^3.0.0"
},
"scripts": {
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",

View File

@@ -27,8 +27,8 @@
"node": ">=6"
},
"dependencies": {
"@xen-orchestra/fs": "^0.9.0",
"cli-progress": "^2.0.0",
"@xen-orchestra/fs": "^0.10.1",
"cli-progress": "^3.1.0",
"exec-promise": "^0.7.0",
"getopts": "^2.2.3",
"struct-fu": "^1.2.0",
@@ -40,10 +40,10 @@
"@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",
"rimraf": "^2.6.1",
"promise-toolbox": "^0.13.0",
"rimraf": "^3.0.0",
"tmp": "^0.1.0"
},
"scripts": {

View File

@@ -26,7 +26,7 @@
"from2": "^2.3.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,15 +35,15 @@
"@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": "^5.1.0",
"index-modules": "^0.3.0",
"readable-stream": "^3.0.6",
"rimraf": "^2.6.2",
"rimraf": "^3.0.0",
"tmp": "^0.1.0"
},
"scripts": {

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.26.0"
"xen-api": "^0.27.1"
},
"devDependencies": {
"@babel/cli": "^7.1.5",
@@ -49,7 +49,7 @@
"@babel/preset-env": "^7.1.5",
"babel-plugin-lodash": "^3.2.11",
"cross-env": "^5.1.4",
"rimraf": "^2.6.1"
"rimraf": "^3.0.0"
},
"scripts": {
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",

View File

@@ -1,6 +1,6 @@
{
"name": "xen-api",
"version": "0.26.0",
"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"
@@ -61,7 +61,7 @@
"@babel/preset-env": "^7.0.0",
"babel-plugin-lodash": "^3.3.2",
"cross-env": "^5.1.3",
"rimraf": "^2.6.1"
"rimraf": "^3.0.0"
},
"scripts": {
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",

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 }
@@ -479,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
@@ -954,6 +965,8 @@ export class Xapi extends EventEmitter {
],
EVENT_TIMEOUT * 1e3 * 1.1
)
this._lastEventFetchedTimestamp = Date.now()
this._watchEventsError = undefined
} catch (error) {
const code = error?.code
if (code === 'EVENTS_LOST' || code === 'SESSION_INVALID') {
@@ -961,6 +974,7 @@ export class Xapi extends EventEmitter {
continue mainLoop
}
this._watchEventsError = error
console.warn('_watchEvents', error)
await pDelay(this._eventPollDelay)
continue

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",
@@ -57,7 +57,7 @@
"@babel/preset-flow": "^7.0.0",
"babel-plugin-lodash": "^3.3.2",
"cross-env": "^5.1.3",
"rimraf": "^2.6.2"
"rimraf": "^3.0.0"
},
"scripts": {
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",

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

@@ -36,7 +36,7 @@
"@babel/preset-env": "^7.0.0",
"cross-env": "^5.1.3",
"event-to-promise": "^0.8.0",
"rimraf": "^2.6.1"
"rimraf": "^3.0.0"
},
"scripts": {
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",

View File

@@ -37,7 +37,7 @@
"@babel/preset-env": "^7.0.0",
"babel-plugin-lodash": "^3.3.2",
"cross-env": "^5.1.3",
"rimraf": "^2.6.1"
"rimraf": "^3.0.0"
},
"scripts": {
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",

View File

@@ -42,7 +42,7 @@
"@babel/preset-env": "^7.0.0",
"babel-plugin-lodash": "^3.3.2",
"cross-env": "^5.1.3",
"rimraf": "^2.6.1"
"rimraf": "^3.0.0"
},
"scripts": {
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",

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

@@ -34,7 +34,7 @@
"babel-plugin-lodash": "^3.3.2",
"cross-env": "^5.1.3",
"deep-freeze": "^0.0.1",
"rimraf": "^2.6.1"
"rimraf": "^3.0.0"
},
"scripts": {
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",

View File

@@ -41,7 +41,7 @@
"@babel/preset-env": "^7.0.0",
"babel-plugin-lodash": "^3.3.2",
"cross-env": "^5.1.3",
"rimraf": "^2.6.1"
"rimraf": "^3.0.0"
},
"scripts": {
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",

View File

@@ -36,10 +36,10 @@
"dependencies": {
"event-to-promise": "^0.8.0",
"exec-promise": "^0.7.0",
"inquirer": "^6.0.0",
"inquirer": "^7.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",
@@ -47,7 +47,7 @@
"@babel/preset-env": "^7.0.0",
"babel-plugin-lodash": "^3.3.2",
"cross-env": "^5.1.3",
"rimraf": "^2.6.1"
"rimraf": "^3.0.0"
},
"scripts": {
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",

View File

@@ -230,9 +230,7 @@ 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 }

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": [
@@ -41,7 +41,7 @@
"@babel/preset-env": "^7.0.0",
"babel-preset-env": "^1.6.1",
"cross-env": "^5.1.3",
"rimraf": "^2.6.1"
"rimraf": "^3.0.0"
},
"scripts": {
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",

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": [
@@ -49,7 +49,7 @@
"@babel/preset-env": "^7.0.0",
"babel-plugin-lodash": "^3.3.2",
"cross-env": "^5.1.3",
"rimraf": "^2.6.1"
"rimraf": "^3.0.0"
},
"scripts": {
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",

View File

@@ -357,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(' ')}`,
@@ -393,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}`,
})
}
@@ -713,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

@@ -40,7 +40,7 @@
"@babel/core": "^7.0.0",
"@babel/preset-env": "^7.0.0",
"cross-env": "^5.1.3",
"rimraf": "^2.5.4"
"rimraf": "^3.0.0"
},
"scripts": {
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",

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

@@ -33,7 +33,7 @@
"@babel/preset-flow": "^7.0.0",
"babel-plugin-lodash": "^3.3.2",
"cross-env": "^5.1.3",
"rimraf": "^2.6.2"
"rimraf": "^3.0.0"
},
"scripts": {
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",

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

@@ -1,31 +1,14 @@
# 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.
XO Server plugin that allows the creation of pool-wide and cross-pool 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
## Documentation
### 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.
Please see the plugin's [official documentation](https://xen-orchestra.com/docs/sdn_controller.html).
## Contributions

View File

@@ -15,21 +15,24 @@
"predev": "yarn run prebuild",
"prepublishOnly": "yarn run build"
},
"version": "0.1.0",
"version": "0.2.1",
"engines": {
"node": ">=6"
},
"devDependencies": {
"@babel/cli": "^7.4.4",
"@babel/core": "^7.4.4",
"@babel/plugin-proposal-nullish-coalescing-operator": "^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.81",
"promise-toolbox": "^0.13.0"
"node-openssl-cert": "^0.0.97",
"promise-toolbox": "^0.13.0",
"uuid": "^3.3.2"
},
"private": true
}

File diff suppressed because it is too large Load Diff

View File

@@ -11,36 +11,50 @@ const OVSDB_PORT = 6640
// =============================================================================
export class OvsdbClient {
/*
Create an SSL connection to an XCP-ng host.
Interact with the host's OpenVSwitch (OVS) daemon to create and manage the virtual bridges
corresponding to the private networks with OVSDB (OpenVSwitch DataBase) Protocol.
See:
- OVSDB Protocol: https://tools.ietf.org/html/rfc7047
- OVS Tunneling : http://docs.openvswitch.org/en/latest/howto/tunneling/
Attributes on created OVS ports (corresponds to a XAPI `PIF` or `VIF`):
- `other_config`:
- `xo:sdn-controller:cross-pool` : UUID of the remote network connected by the tunnel
- `xo:sdn-controller:private-pool-wide`: `true` if created (and managed) by a SDN Controller
Attributes on created OVS interfaces:
- `options`:
- `key` : Network's VNI
- `remote_ip`: Remote IP of the tunnel
*/
constructor(host, clientKey, clientCert, caCert) {
this._host = host
this._numberOfPortAndInterface = 0
this._requestID = 0
this._requestId = 0
this._adding = []
this.host = host
this.updateCertificates(clientKey, clientCert, caCert)
log.debug(`[${this._host.name_label}] New OVSDB client`)
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
}
updateCertificates(clientKey, clientCert, caCert) {
this._clientKey = clientKey
this._clientCert = clientCert
this._caCert = caCert
log.debug(`[${this._host.name_label}] Certificates have been updated`)
log.debug('Certificates have been updated', {
host: this.host.name_label,
})
}
// ---------------------------------------------------------------------------
@@ -49,19 +63,31 @@ export class OvsdbClient {
networkUuid,
networkName,
remoteAddress,
encapsulation
encapsulation,
key,
remoteNetwork
) {
const socket = await this._connect()
const index = this._numberOfPortAndInterface
++this._numberOfPortAndInterface
if (
this._adding.find(
elem => elem.id === networkUuid && elem.addr === remoteAddress
) !== undefined
) {
return
}
const adding = { id: networkUuid, addr: remoteAddress }
this._adding.push(adding)
const socket = await this._connect()
const [bridgeUuid, bridgeName] = await this._getBridgeUuidForNetwork(
networkUuid,
networkName,
socket
)
if (bridgeUuid == null) {
if (bridgeUuid === undefined) {
socket.destroy()
this._adding = this._adding.filter(
elem => elem.id !== networkUuid || elem.addr !== remoteAddress
)
return
}
@@ -73,14 +99,23 @@ export class OvsdbClient {
)
if (alreadyExist) {
socket.destroy()
return
this._adding = this._adding.filter(
elem => elem.id !== networkUuid || elem.addr !== remoteAddress
)
return bridgeName
}
const interfaceName = 'tunnel_iface' + index
const portName = 'tunnel_port' + index
const index = ++this._numberOfPortAndInterface
const interfaceName = bridgeName + '_iface' + index
const portName = bridgeName + '_port' + index
// Add interface and port to the bridge
const options = ['map', [['remote_ip', remoteAddress]]]
const options = ['map', [['remote_ip', remoteAddress], ['key', key]]]
const otherConfig =
remoteNetwork !== undefined
? ['map', [['xo:sdn-controller:cross-pool', remoteNetwork]]]
: ['map', [['xo:sdn-controller:private-pool-wide', 'true']]]
const addInterfaceOperation = {
op: 'insert',
table: 'Interface',
@@ -88,7 +123,7 @@ export class OvsdbClient {
type: encapsulation,
options: options,
name: interfaceName,
other_config: ['map', [['private_pool_wide', 'true']]],
other_config: otherConfig,
},
'uuid-name': 'new_iface',
}
@@ -98,7 +133,7 @@ export class OvsdbClient {
row: {
name: portName,
interfaces: ['set', [['named-uuid', 'new_iface']]],
other_config: ['map', [['private_pool_wide', 'true']]],
other_config: otherConfig,
},
'uuid-name': 'new_port',
}
@@ -115,7 +150,11 @@ export class OvsdbClient {
mutateBridgeOperation,
]
const jsonObjects = await this._sendOvsdbTransaction(params, socket)
if (jsonObjects == null) {
this._adding = this._adding.filter(
elem => elem.id !== networkUuid || elem.addr !== remoteAddress
)
if (jsonObjects === undefined) {
socket.destroy()
return
}
@@ -126,52 +165,64 @@ export class OvsdbClient {
let opResult
do {
opResult = jsonObjects[0].result[i]
if (opResult != null && opResult.error != null) {
if (opResult?.error !== undefined) {
error = opResult.error
details = opResult.details
}
++i
} while (opResult && !error)
} while (opResult !== undefined && error === undefined)
if (error != null) {
log.error(
`[${
this._host.name_label
}] Error while adding port: '${portName}' and interface: '${interfaceName}' to bridge: '${bridgeName}' on network: '${networkName}' because: ${error}: ${details}`
)
if (error !== undefined) {
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(
`[${
this._host.name_label
}] Port: '${portName}' and interface: '${interfaceName}' added to bridge: '${bridgeName}' on network: '${networkName}'`
)
log.debug('Port and interface added to bridge', {
port: portName,
interface: interfaceName,
bridge: bridgeName,
network: networkName,
host: this.host.name_label,
})
socket.destroy()
return bridgeName
}
async resetForNetwork(networkUuid, networkName) {
async resetForNetwork(
networkUuid,
networkName,
crossPoolOnly,
remoteNetwork
) {
const socket = await this._connect()
const [bridgeUuid, bridgeName] = await this._getBridgeUuidForNetwork(
networkUuid,
networkName,
socket
)
if (bridgeUuid == null) {
if (bridgeUuid === undefined) {
socket.destroy()
return
}
// Delete old ports created by a SDN controller
const ports = await this._getBridgePorts(bridgeUuid, bridgeName, socket)
if (ports == null) {
if (ports === undefined) {
socket.destroy()
return
}
const portsToDelete = []
for (let i = 0; i < ports.length; ++i) {
const portUuid = ports[i][1]
for (const port of ports) {
const portUuid = port[1]
const where = [['_uuid', '==', ['uuid', portUuid]]]
const selectResult = await this._select(
@@ -180,17 +231,25 @@ export class OvsdbClient {
where,
socket
)
if (selectResult == null) {
if (selectResult === undefined) {
continue
}
forOwn(selectResult.other_config[1], config => {
if (config[0] === 'private_pool_wide' && config[1] === 'true') {
log.debug(
`[${this._host.name_label}] Adding port: '${
selectResult.name
}' to delete list from bridge: '${bridgeName}'`
)
// 2019-09-03
// Compatibility code, to be removed in 1 year.
const oldShouldDelete =
(config[0] === 'private_pool_wide' && !crossPoolOnly) ||
(config[0] === 'cross_pool' &&
(remoteNetwork === undefined || remoteNetwork === config[1]))
const shouldDelete =
(config[0] === 'xo:sdn-controller:private-pool-wide' &&
!crossPoolOnly) ||
(config[0] === 'xo:sdn-controller:cross-pool' &&
(remoteNetwork === undefined || remoteNetwork === config[1]))
if (shouldDelete || oldShouldDelete) {
portsToDelete.push(['uuid', portUuid])
}
})
@@ -211,27 +270,25 @@ export class OvsdbClient {
const params = ['Open_vSwitch', mutateBridgeOperation]
const jsonObjects = await this._sendOvsdbTransaction(params, socket)
if (jsonObjects == null) {
if (jsonObjects === undefined) {
socket.destroy()
return
}
if (jsonObjects[0].error != null) {
log.error(
`[${
this._host.name_label
}] Couldn't delete ports from bridge: '${bridgeName}' because: ${
jsonObjects.error
}`
)
log.error('Error while deleting ports from bridge', {
error: jsonObjects[0].error,
bridge: bridgeName,
host: this.host.name_label,
})
socket.destroy()
return
}
log.debug(
`[${this._host.name_label}] Deleted ${
jsonObjects[0].result[0].count
} ports from bridge: '${bridgeName}'`
)
log.debug('Ports deleted from bridge', {
nPorts: jsonObjects[0].result[0].count,
bridge: bridgeName,
host: this.host.name_label,
})
socket.destroy()
}
@@ -247,9 +304,9 @@ export class OvsdbClient {
for (let i = pos; i < data.length; ++i) {
const c = data.charAt(i)
if (c === '{') {
depth++
++depth
} else if (c === '}') {
depth--
--depth
if (depth === 0) {
const object = JSON.parse(buffer + data.substr(0, i + 1))
objects.push(object)
@@ -281,17 +338,16 @@ export class OvsdbClient {
where,
socket
)
if (selectResult == null) {
return [null, null]
if (selectResult === undefined) {
log.error('No bridge found for network', {
network: networkName,
host: this.host.name_label,
})
return []
}
const bridgeUuid = selectResult._uuid[1]
const bridgeName = selectResult.name
log.debug(
`[${
this._host.name_label
}] Found bridge: '${bridgeName}' for network: '${networkName}'`
)
return [bridgeUuid, bridgeName]
}
@@ -303,26 +359,25 @@ export class OvsdbClient {
socket
) {
const ports = await this._getBridgePorts(bridgeUuid, bridgeName, socket)
if (ports == null) {
return
if (ports === undefined) {
return false
}
for (let i = 0; i < ports.length; ++i) {
const portUuid = ports[i][1]
for (const port of ports) {
const portUuid = port[1]
const interfaces = await this._getPortInterfaces(portUuid, socket)
if (interfaces == null) {
if (interfaces === undefined) {
continue
}
let j
for (j = 0; j < interfaces.length; ++j) {
const interfaceUuid = interfaces[j][1]
for (const iface of interfaces) {
const interfaceUuid = iface[1]
const hasRemote = await this._interfaceHasRemote(
interfaceUuid,
remoteAddress,
socket
)
if (hasRemote === true) {
if (hasRemote) {
return true
}
}
@@ -334,8 +389,8 @@ export class OvsdbClient {
async _getBridgePorts(bridgeUuid, bridgeName, socket) {
const where = [['_uuid', '==', ['uuid', bridgeUuid]]]
const selectResult = await this._select('Bridge', ['ports'], where, socket)
if (selectResult == null) {
return null
if (selectResult === undefined) {
return
}
return selectResult.ports[0] === 'set'
@@ -351,8 +406,8 @@ export class OvsdbClient {
where,
socket
)
if (selectResult == null) {
return null
if (selectResult === undefined) {
return
}
return selectResult.interfaces[0] === 'set'
@@ -368,12 +423,11 @@ export class OvsdbClient {
where,
socket
)
if (selectResult == null) {
if (selectResult === undefined) {
return false
}
for (let i = 0; i < selectResult.options[1].length; ++i) {
const option = selectResult.options[1][i]
for (const option of selectResult.options[1]) {
if (option[0] === 'remote_ip' && option[1] === remoteAddress) {
return true
}
@@ -394,36 +448,36 @@ export class OvsdbClient {
const params = ['Open_vSwitch', selectOperation]
const jsonObjects = await this._sendOvsdbTransaction(params, socket)
if (jsonObjects == null) {
if (jsonObjects === undefined) {
return
}
const jsonResult = jsonObjects[0].result[0]
if (jsonResult.error != null) {
log.error(
`[${
this._host.name_label
}] Couldn't retrieve: '${columns}' in: '${table}' because: ${
jsonResult.error
}: ${jsonResult.details}`
)
return null
if (jsonResult.error !== undefined) {
log.error('Error while selecting columns', {
error: jsonResult.error,
details: jsonResult.details,
columns,
table,
where,
host: this.host.name_label,
})
return
}
if (jsonResult.rows.length === 0) {
log.error(
`[${
this._host.name_label
}] No '${columns}' found in: '${table}' where: '${where}'`
)
return null
log.error('No result for select', {
columns,
table,
where,
host: this.host.name_label,
})
return
}
// 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}'`
`[${this.host.name_label}] There should be exactly 1 row when searching: '${columns}' in: '${table}' where: '${where}'`
)
return jsonResult.rows[0]
@@ -431,9 +485,7 @@ export class OvsdbClient {
async _sendOvsdbTransaction(params, socket) {
const stream = socket
const requestId = this._requestID
++this._requestID
const requestId = ++this._requestId
const req = {
id: requestId,
method: 'transact',
@@ -443,10 +495,11 @@ export class OvsdbClient {
try {
stream.write(JSON.stringify(req))
} catch (error) {
log.error(
`[${this._host.name_label}] Error while writing into stream: ${error}`
)
return null
log.error('Error while writing into stream', {
error,
host: this.host.name_label,
})
return
}
let result
@@ -456,12 +509,11 @@ export class OvsdbClient {
try {
result = await fromEvent(stream, 'data', {})
} catch (error) {
log.error(
`[${
this._host.name_label
}] Error while waiting for stream data: ${error}`
)
return null
log.error('Error while waiting for stream data', {
error,
host: this.host.name_label,
})
return
}
jsonObjects = this._parseJson(result)
@@ -478,7 +530,7 @@ export class OvsdbClient {
ca: this._caCert,
key: this._clientKey,
cert: this._clientCert,
host: this._host.address,
host: this.host.address,
port: OVSDB_PORT,
rejectUnauthorized: false,
requestCert: false,
@@ -488,22 +540,20 @@ export class OvsdbClient {
try {
await fromEvent(socket, 'secureConnect', {})
} catch (error) {
log.error(
`[${this._host.name_label}] TLS connection failed because: ${error}: ${
error.code
}`
)
log.error('TLS connection failed', {
error,
code: error.code,
host: this.host.name_label,
})
throw error
}
log.debug(`[${this._host.name_label}] TLS connection successful`)
socket.on('error', error => {
log.error(
`[${
this._host.name_label
}] OVSDB client socket error: ${error} with code: ${error.code}`
)
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,145 @@
# xo-server-test
> Test client for Xo-Server
Tests are ran sequentially to avoid concurrency issues.
## 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`
- ⚠ Warning: snapshots ⚠
After each run of the tests, check that snapshots are not inadvertently modified.
- ⚠ Jest known issue ⚠
If a test timeout is triggered the next async tests can fail, it's due to an inadvertently modified snapshots.
As a workaround, you can clean your git working tree and re-run jest using a large timeout: `> yarn test --testTimeout=100000`
## 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,56 @@
{
"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",
"promise-toolbox": "^0.13.0",
"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$",
"maxConcurrency": 1
}
}

View File

@@ -0,0 +1,26 @@
[xoConnection]
url = ''
email = ''
password = ''
[servers]
[servers.default]
username = ''
password = ''
host = ''
[vms]
default = ''
# vmToBackup = ''
[templates]
default = ''
templateWithoutDisks = ''
[srs]
default = ''
[remotes]
default = { name = '', url = '' }
remote1 = { name = '', url = '' }
# remote2 = { 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,248 @@
/* eslint-env jest */
import defer from 'golike-defer'
import Xo from 'xo-lib'
import XoCollection from 'xo-collection'
import { find, forOwn } from 'lodash'
import { fromEvent } from 'promise-toolbox'
import config from './_config'
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 createTempRemote(params) {
const remote = await this.call('remote.create', params)
this._tempResourceDisposers.push('remote.delete', { id: remote.id })
return remote
}
async createTempServer(params) {
const servers = await this.call('server.getAll')
const server = servers.find(server => server.host === params.host)
if (server !== undefined) {
if (server.status === 'disconnected') {
await this.call('server.enable', { id: server.id })
this._durableResourceDisposers.push('server.disable', { id: server.id })
await fromEvent(this._objects, 'finish')
}
return
}
const id = await this.call('server.add', {
...params,
allowUnauthorized: true,
autoConnect: false,
})
this._durableResourceDisposers.push('server.remove', { id })
await this.call('server.enable', { id })
await fromEvent(this._objects, 'finish')
}
async getSchedule(predicate) {
return find(await this.call('schedule.getAll'), predicate)
}
async runBackupJob(jobId, scheduleId, { remotes, nExecutions = 1 }) {
for (let i = 0; i < nExecutions; i++) {
await xo.call('backupNg.runJob', { id: jobId, schedule: scheduleId })
}
const backups = {}
if (remotes !== undefined) {
const backupsByRemote = await xo.call('backupNg.listVmBackups', {
remotes,
})
forOwn(backupsByRemote, (backupsByVm, remoteId) => {
backups[remoteId] = []
forOwn(backupsByVm, vmBackups => {
vmBackups.forEach(
({ jobId: backupJobId, scheduleId: backupScheduleId, id }) => {
if (jobId === backupJobId && scheduleId === backupScheduleId) {
this._tempResourceDisposers.push('backupNg.deleteVmBackup', {
id,
})
backups[remoteId].push(id)
}
}
)
})
})
}
forOwn(this.objects.all, (obj, id) => {
if (
obj.other !== undefined &&
obj.other['xo:backup:job'] === jobId &&
obj.other['xo:backup:schedule'] === scheduleId
) {
this._tempResourceDisposers.push('vm.delete', {
id,
})
}
})
return backups
}
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('deleteTempResources', 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
beforeAll(async () => {
// TOFIX: stop tests if the connection is not established properly and show the error
xo = await getConnection()
})
afterAll(async () => {
await xo.deleteDurableResources()
await xo.close()
xo = null
})
afterEach(() => xo.deleteTempResources())
export { xo as default }
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,539 @@
// 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 delta backup with 2 remotes, 2 as retention and 2 as fullInterval 1`] = `
Object {
"data": Object {
"mode": "delta",
"reportWhen": "never",
},
"end": Any<Number>,
"id": Any<String>,
"jobId": Any<String>,
"message": "backup",
"scheduleId": Any<String>,
"start": Any<Number>,
"status": "success",
}
`;
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 2`] = `
Object {
"data": Object {
"id": Any<String>,
"type": "VM",
},
"end": Any<Number>,
"id": Any<String>,
"message": Any<String>,
"start": Any<Number>,
"status": "success",
}
`;
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 3`] = `
Object {
"end": Any<Number>,
"id": Any<String>,
"message": "snapshot",
"result": Any<String>,
"start": Any<Number>,
"status": "success",
}
`;
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 4`] = `
Object {
"data": Object {
"id": Any<String>,
"isFull": true,
"type": "remote",
},
"end": Any<Number>,
"id": Any<String>,
"message": Any<String>,
"start": Any<Number>,
"status": "success",
}
`;
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 5`] = `
Object {
"end": Any<Number>,
"id": Any<String>,
"message": Any<String>,
"result": Object {
"size": Any<Number>,
},
"start": Any<Number>,
"status": "success",
}
`;
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 6`] = `
Object {
"end": Any<Number>,
"id": Any<String>,
"message": Any<String>,
"result": Object {
"size": Any<Number>,
},
"start": Any<Number>,
"status": "success",
}
`;
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 7`] = `
Object {
"data": Object {
"id": Any<String>,
"isFull": true,
"type": "remote",
},
"end": Any<Number>,
"id": Any<String>,
"message": Any<String>,
"start": Any<Number>,
"status": "success",
}
`;
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 8`] = `
Object {
"end": Any<Number>,
"id": Any<String>,
"message": Any<String>,
"result": Object {
"size": Any<Number>,
},
"start": Any<Number>,
"status": "success",
}
`;
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 9`] = `
Object {
"end": Any<Number>,
"id": Any<String>,
"message": Any<String>,
"result": Object {
"size": Any<Number>,
},
"start": Any<Number>,
"status": "success",
}
`;
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 10`] = `
Object {
"data": Object {
"mode": "delta",
"reportWhen": "never",
},
"end": Any<Number>,
"id": Any<String>,
"jobId": Any<String>,
"message": "backup",
"scheduleId": Any<String>,
"start": Any<Number>,
"status": "success",
}
`;
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 11`] = `
Object {
"data": Object {
"id": Any<String>,
"type": "VM",
},
"end": Any<Number>,
"id": Any<String>,
"message": Any<String>,
"start": Any<Number>,
"status": "success",
}
`;
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 12`] = `
Object {
"end": Any<Number>,
"id": Any<String>,
"message": "snapshot",
"result": Any<String>,
"start": Any<Number>,
"status": "success",
}
`;
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 13`] = `
Object {
"data": Object {
"id": Any<String>,
"isFull": false,
"type": "remote",
},
"end": Any<Number>,
"id": Any<String>,
"message": Any<String>,
"start": Any<Number>,
"status": "success",
}
`;
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 14`] = `
Object {
"end": Any<Number>,
"id": Any<String>,
"message": Any<String>,
"result": Object {
"size": Any<Number>,
},
"start": Any<Number>,
"status": "success",
}
`;
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 15`] = `
Object {
"end": Any<Number>,
"id": Any<String>,
"message": Any<String>,
"result": Object {
"size": Any<Number>,
},
"start": Any<Number>,
"status": "success",
}
`;
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 16`] = `
Object {
"data": Object {
"id": Any<String>,
"isFull": false,
"type": "remote",
},
"end": Any<Number>,
"id": Any<String>,
"message": Any<String>,
"start": Any<Number>,
"status": "success",
}
`;
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 17`] = `
Object {
"end": Any<Number>,
"id": Any<String>,
"message": Any<String>,
"result": Object {
"size": Any<Number>,
},
"start": Any<Number>,
"status": "success",
}
`;
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 18`] = `
Object {
"end": Any<Number>,
"id": Any<String>,
"message": Any<String>,
"result": Object {
"size": Any<Number>,
},
"start": Any<Number>,
"status": "success",
}
`;
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 19`] = `
Object {
"data": Object {
"mode": "delta",
"reportWhen": "never",
},
"end": Any<Number>,
"id": Any<String>,
"jobId": Any<String>,
"message": "backup",
"scheduleId": Any<String>,
"start": Any<Number>,
"status": "success",
}
`;
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 20`] = `
Object {
"data": Object {
"id": Any<String>,
"type": "VM",
},
"end": Any<Number>,
"id": Any<String>,
"message": Any<String>,
"start": Any<Number>,
"status": "success",
}
`;
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 21`] = `
Object {
"end": Any<Number>,
"id": Any<String>,
"message": "snapshot",
"result": Any<String>,
"start": Any<Number>,
"status": "success",
}
`;
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 22`] = `
Object {
"data": Object {
"id": Any<String>,
"isFull": true,
"type": "remote",
},
"end": Any<Number>,
"id": Any<String>,
"message": Any<String>,
"start": Any<Number>,
"status": "success",
}
`;
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 23`] = `
Object {
"end": Any<Number>,
"id": Any<String>,
"message": Any<String>,
"result": Object {
"size": Any<Number>,
},
"start": Any<Number>,
"status": "success",
}
`;
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 24`] = `
Object {
"end": Any<Number>,
"id": Any<String>,
"message": Any<String>,
"result": Object {
"size": Any<Number>,
},
"start": Any<Number>,
"status": "success",
}
`;
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 25`] = `
Object {
"data": Object {
"id": Any<String>,
"isFull": true,
"type": "remote",
},
"end": Any<Number>,
"id": Any<String>,
"message": Any<String>,
"start": Any<Number>,
"status": "success",
}
`;
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 26`] = `
Object {
"end": Any<Number>,
"id": Any<String>,
"message": Any<String>,
"result": Object {
"size": Any<Number>,
},
"start": Any<Number>,
"status": "success",
}
`;
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 27`] = `
Object {
"end": Any<Number>,
"id": Any<String>,
"message": Any<String>,
"result": Object {
"size": Any<Number>,
},
"start": Any<Number>,
"status": "success",
}
`;
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,582 @@
/* eslint-env jest */
import { forOwn } from 'lodash'
import { noSuchObject } from 'xo-common/api-errors'
import config from '../_config'
import randomId from '../_randomId'
import xo from '../_xoConnection'
const DEFAULT_SCHEDULE = {
name: 'scheduleTest',
cron: '0 * * * * *',
}
const validateRootTask = (log, props) =>
expect(log).toMatchSnapshot({
end: expect.any(Number),
id: expect.any(String),
jobId: expect.any(String),
scheduleId: expect.any(String),
start: expect.any(Number),
...props,
})
const validateVmTask = (task, vmId, props) => {
expect(task).toMatchSnapshot({
data: {
id: expect.any(String),
},
end: expect.any(Number),
id: expect.any(String),
message: expect.any(String),
start: expect.any(Number),
...props,
})
expect(task.data.id).toBe(vmId)
}
const validateSnapshotTask = (task, props) =>
expect(task).toMatchSnapshot({
end: expect.any(Number),
id: expect.any(String),
result: expect.any(String),
start: expect.any(Number),
...props,
})
const validateExportTask = (task, srOrRemoteIds, props) => {
expect(task).toMatchSnapshot({
end: expect.any(Number),
id: expect.any(String),
message: expect.any(String),
start: expect.any(Number),
...props,
})
expect(srOrRemoteIds).toContain(task.data.id)
}
const validateOperationTask = (task, props) => {
expect(task).toMatchSnapshot({
end: expect.any(Number),
id: expect.any(String),
message: expect.any(String),
start: expect.any(Number),
...props,
})
}
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 () => {
jest.setTimeout(7e3)
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 () => {
jest.setTimeout(8e3)
await xo.createTempServer(config.servers.default)
const vmIdWithoutDisks = await xo.createTempVm({
name_label: 'XO Test Without Disks',
name_description: 'Creating a vm without disks',
template: config.templates.templateWithoutDisks,
})
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 () => {
jest.setTimeout(7e3)
const scheduleTempId = randomId()
await xo.createTempServer(config.servers.default)
const { id: remoteId } = await xo.createTempRemote(config.remotes.default)
const { id: jobId } = await xo.createTempBackupNgJob({
...defaultBackupNg,
remotes: {
id: remoteId,
},
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(6e4)
await xo.createTempServer(config.servers.default)
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)
})
test('execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval', async () => {
jest.setTimeout(12e5)
const {
vms: { default: defaultVm, vmToBackup = defaultVm },
remotes: { default: defaultRemote, remote1, remote2 = defaultRemote },
servers: { default: defaultServer },
} = config
expect(vmToBackup).not.toBe(undefined)
expect(remote1).not.toBe(undefined)
expect(remote2).not.toBe(undefined)
await xo.createTempServer(defaultServer)
const { id: remoteId1 } = await xo.createTempRemote(remote1)
const { id: remoteId2 } = await xo.createTempRemote(remote2)
const remotes = [remoteId1, remoteId2]
const exportRetention = 2
const fullInterval = 2
const scheduleTempId = randomId()
const { id: jobId } = await xo.createTempBackupNgJob({
mode: 'delta',
remotes: {
id: {
__or: remotes,
},
},
schedules: {
[scheduleTempId]: DEFAULT_SCHEDULE,
},
settings: {
'': {
reportWhen: 'never',
fullInterval,
},
[remoteId1]: { deleteFirst: true },
[scheduleTempId]: { exportRetention },
},
vms: {
id: vmToBackup,
},
})
const schedule = await xo.getSchedule({ jobId })
expect(typeof schedule).toBe('object')
const nExecutions = 3
const backupsByRemote = await xo.runBackupJob(jobId, schedule.id, {
remotes,
nExecutions,
})
forOwn(backupsByRemote, backups =>
expect(backups.length).toBe(exportRetention)
)
const backupLogs = await xo.call('backupNg.getLogs', {
jobId,
scheduleId: schedule.id,
})
expect(backupLogs.length).toBe(nExecutions)
backupLogs.forEach(({ tasks = [], ...log }, key) => {
validateRootTask(log, {
data: {
mode: 'delta',
reportWhen: 'never',
},
message: 'backup',
status: 'success',
})
const numberOfTasks = {
export: 0,
merge: 0,
snapshot: 0,
transfer: 0,
vm: 0,
}
tasks.forEach(({ tasks = [], ...vmTask }) => {
if (vmTask.data !== undefined && vmTask.data.type === 'VM') {
validateVmTask(vmTask, vmToBackup, { status: 'success' })
numberOfTasks.vm++
tasks.forEach(({ tasks = [], ...subTask }) => {
if (subTask.message === 'snapshot') {
validateSnapshotTask(subTask, { status: 'success' })
numberOfTasks.snapshot++
}
if (subTask.message === 'export') {
validateExportTask(subTask, remotes, {
data: {
id: expect.any(String),
isFull: key % fullInterval === 0,
type: 'remote',
},
status: 'success',
})
numberOfTasks.export++
let mergeTaskKey, transferTaskKey
tasks.forEach((operationTask, key) => {
if (
operationTask.message === 'transfer' ||
operationTask.message === 'merge'
) {
validateOperationTask(operationTask, {
result: { size: expect.any(Number) },
status: 'success',
})
if (operationTask.message === 'transfer') {
mergeTaskKey = key
numberOfTasks.merge++
} else {
transferTaskKey = key
numberOfTasks.transfer++
}
}
})
expect(
subTask.data.id === remoteId1
? mergeTaskKey > transferTaskKey
: mergeTaskKey < transferTaskKey
).toBe(true)
}
})
}
})
expect(numberOfTasks).toEqual({
export: 2,
merge: 2,
snapshot: 1,
transfer: 2,
vm: 1,
})
})
})
})

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,226 @@
/* 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 () => {
jest.setTimeout(6e3)
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 () => {
jest.setTimeout(7e4)
await xo.createTempServer(config.servers.default)
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,264 @@
/* 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 => {
jest.setTimeout(6e3)
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 () => {
jest.setTimeout(7e3)
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 => {
jest.setTimeout(6e3)
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",
@@ -42,7 +42,7 @@
"@babel/preset-env": "^7.0.0",
"babel-plugin-lodash": "^3.3.2",
"cross-env": "^5.1.3",
"rimraf": "^2.6.1"
"rimraf": "^3.0.0"
},
"scripts": {
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",

View File

@@ -40,7 +40,7 @@
"@babel/preset-env": "^7.0.0",
"babel-preset-env": "^1.5.2",
"cross-env": "^5.1.3",
"rimraf": "^2.6.1"
"rimraf": "^3.0.0"
},
"scripts": {
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",

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": {
@@ -41,7 +41,7 @@
"@babel/core": "^7.0.0",
"@babel/preset-env": "^7.0.0",
"cross-env": "^5.1.3",
"rimraf": "^2.5.4"
"rimraf": "^3.0.0"
},
"scripts": {
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",

View File

@@ -41,7 +41,7 @@
"@babel/core": "^7.0.0",
"@babel/preset-env": "^7.0.0",
"cross-env": "^5.1.3",
"rimraf": "^2.6.1"
"rimraf": "^3.0.0"
},
"scripts": {
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",

View File

@@ -1,6 +1,6 @@
{
"name": "xo-server-usage-report",
"version": "0.7.2",
"version": "0.7.3",
"license": "AGPL-3.0",
"description": "",
"keywords": [
@@ -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",
@@ -50,7 +50,7 @@
"@babel/preset-env": "^7.0.0",
"babel-plugin-lodash": "^3.3.2",
"cross-env": "^5.1.3",
"rimraf": "^2.6.1"
"rimraf": "^3.0.0"
},
"scripts": {
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",

View File

@@ -19,7 +19,7 @@ import {
values,
zipObject,
} from 'lodash'
import { promisify } from 'promise-toolbox'
import { ignoreErrors, promisify } from 'promise-toolbox'
import { readFile, writeFile } from 'fs'
// ===================================================================
@@ -759,14 +759,22 @@ class UsageReportPlugin {
}
async _sendReport(storeData) {
const xo = this._xo
if (xo.sendEmail === undefined) {
ignoreErrors.call(xo.unloadPlugin('usage-report'))
throw new Error(
'The plugin usage-report requires the plugin transport-email to be loaded'
)
}
const data = await dataBuilder({
xo: this._xo,
xo,
storedStatsPath: this._storedStatsPath,
all: this._conf.all,
})
await Promise.all([
this._xo.sendEmail({
xo.sendEmail({
to: this._conf.emails,
subject: `[Xen Orchestra] Xo Report - ${currDate}`,
markdown: `Hi there,

View File

@@ -17,9 +17,11 @@ createUserOnFirstSignin = true
# their size just by looking at the beginning of the stream.
#
# But it is a guess, not a certainty, it depends on how the VHDs are formatted
# by XenServer, therefore it's disabled for the moment but can be enabled
# specifically for a user if necessary.
guessVhdSizeOnImport = false
# by XenServer.
#
# This has been tested for 5 months, therefore it's enabled by default but can
# be disabled specifically for a user if necessary.
guessVhdSizeOnImport = true
# Whether API logs should contains the full request/response on
# errors.
@@ -29,6 +31,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

View File

@@ -1,7 +1,7 @@
{
"private": true,
"name": "xo-server",
"version": "5.43.0",
"version": "5.49.0",
"license": "AGPL-3.0",
"description": "Server part of Xen-Orchestra",
"keywords": [
@@ -38,7 +38,7 @@
"@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",
@@ -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.26.0",
"xen-api": "^0.27.1",
"xml2js": "^0.4.19",
"xo-acl-resolver": "^0.4.1",
"xo-collection": "^0.4.1",
@@ -149,7 +149,7 @@
"babel-plugin-transform-dev": "^2.0.1",
"cross-env": "^5.1.3",
"index-modules": "^0.3.0",
"rimraf": "^2.6.2"
"rimraf": "^3.0.0"
},
"scripts": {
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",

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,

View File

@@ -5,6 +5,7 @@ import { format, JsonRpcError } from 'json-rpc-peer'
export async function set({
host,
iscsiIqn,
multipathing,
name_label: nameLabel,
name_description: nameDescription,
@@ -12,6 +13,13 @@ export async function set({
host = this.getXapiObject(host)
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 &&
@@ -23,6 +31,7 @@ set.description = 'changes the properties of an host'
set.params = {
id: { type: 'string' },
iscsiIqn: { type: 'string', optional: true },
name_label: {
type: 'string',
optional: true,

View File

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

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

@@ -631,6 +631,8 @@ set.params = {
// set the VM boot firmware mode
hvmBootFirmware: { type: ['string', 'null'], optional: true },
virtualizationMode: { type: 'string', optional: true },
}
set.resolve = {
@@ -1134,8 +1136,16 @@ resume.resolve = {
// -------------------------------------------------------------------
export function revert({ snapshot, snapshotBefore }) {
return this.getXapi(snapshot).revertVm(snapshot._xapiId, snapshotBefore)
export async function revert({ snapshot, snapshotBefore }) {
const { id: userId, permission } = this.user
await this.checkPermissions(userId, [[snapshot.$snapshot_of, 'operate']])
const newSnapshot = await this.getXapi(snapshot).revertVm(
snapshot._xapiId,
snapshotBefore
)
if (snapshotBefore && permission !== 'admin') {
await this.addAcl(userId, newSnapshot.$id, 'admin')
}
}
revert.params = {
@@ -1144,7 +1154,7 @@ revert.params = {
}
revert.resolve = {
snapshot: ['snapshot', 'VM-snapshot', 'administrate'],
snapshot: ['snapshot', 'VM-snapshot', 'view'],
}
// -------------------------------------------------------------------

View File

@@ -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}`
)
}
})
@@ -823,12 +821,14 @@ export const createSR = defer(async function(
createSR.description = 'create gluster VM'
createSR.permission = 'admin'
createSR.params = {
brickSize: { type: 'number', optional: true },
srs: {
type: 'array',
items: {
type: 'string',
},
},
template: { type: 'object' },
pif: {
type: 'string',
},
@@ -1050,9 +1050,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, {
@@ -1126,9 +1124,7 @@ async function _prepareGlusterVm(
}
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,
})

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