Compare commits

...

212 Commits

Author SHA1 Message Date
heafalan
50704b8404 feat(xo-server-test/backupNg.spec.js): test on 'backupNg.importVmBackup'
See #4307
2019-08-12 15:04:03 +02:00
heafalan
341cdf6cba delete snapshot at the end of the test 2019-08-12 14:04:01 +02:00
heafalan
ced02de429 fixes tests for QA 2019-08-09 12:58:01 +02:00
heafalan
1745d61c1d requested changes 2019-08-08 14:08:23 +02:00
heafalan
fac2e6845c requested changes 2019-08-08 10:21:25 +02:00
heafalan
fd7eddb68c various changes 2019-08-07 13:09:08 +02:00
heafalan
23fb486a40 various changes 2019-08-05 16:27:15 +02:00
heafalan
9114aa5b11 add server 2019-08-02 12:39:50 +02:00
heafalan
fdb3368b44 Merge branch 'master' of http://github.com/vatesfr/xen-orchestra into deltaBackup-with2remotes 2019-08-02 12:09:42 +02:00
heafalan
c0949e9aef Merge branch 'master' of http://github.com/vatesfr/xen-orchestra into deltaBackup-with2remotes 2019-08-02 12:09:22 +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
heafalan
ae9b4126b1 feat(xo-server-test): delta backup test
See #4307
2019-07-31 15:52:37 +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
a45f83b646 fix(README): fix test command 2019-07-12 16:59:49 +02:00
heafalan
b011e8656f fix(user#user.changePassword): delete useless tests (#118) 2019-07-12 11:00:05 +02:00
heafalan
445b13ec29 feat: create temp remote with remote configuration (#117) 2019-07-08 13:01:40 +02:00
heafalan
4e9d143996 fix: remove VM ids from snapshots (#116) 2019-07-01 09:39:41 +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
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
80 changed files with 6785 additions and 679 deletions

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@xen-orchestra/fs",
"version": "0.10.0",
"version": "0.10.1",
"license": "AGPL-3.0",
"description": "The File System for Xen Orchestra backups.",
"keywords": [],

View File

@@ -4,10 +4,33 @@
### Enhancements
### Bug fixes
### Released packages
- xo-server v5.47.0
- xo-web v5.47.0
## **5.37.0** (2019-07-25)
![Channel: latest](https://badgen.net/badge/channel/latest/yellow)
### 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] Permission to revert to any snapshot for VM operators [#3928](https://github.com/vatesfr/xen-orchestra/issues/3928) (PR [#4247](https://github.com/vatesfr/xen-orchestra/pull/4247))
- [VM] Show current operations and progress [#3811](https://github.com/vatesfr/xen-orchestra/issues/3811) (PR [#3982](https://github.com/vatesfr/xen-orchestra/pull/3982))
- [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
@@ -17,18 +40,22 @@
- [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.0
- @xen-orchestra/fs v0.10.1
- xo-server-sdn-controller v0.1.1
- xen-api v0.27.0
- xo-server v5.45.0
- xo-web v5.45.0
- xen-api v0.27.1
- xo-server v5.46.0
- xo-web v5.46.0
## **5.36.0** (2019-06-27)
![Channel: latest](https://badgen.net/badge/channel/latest/yellow)
![Channel: stable](https://badgen.net/badge/channel/stable/green)
### Highlights
@@ -67,8 +94,6 @@
## **5.35.0** (2019-05-29)
![Channel: stable](https://badgen.net/badge/channel/stable/green)
### Enhancements
- [VM/general] Display 'Started... ago' instead of 'Halted... ago' for paused state [#3750](https://github.com/vatesfr/xen-orchestra/issues/3750) (PR [#4170](https://github.com/vatesfr/xen-orchestra/pull/4170))

View File

@@ -7,13 +7,16 @@
> Users must be able to say: “Nice enhancement, I'm eager to test it”
- [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/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))
- [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
> Users must be able to say: “I had this issue, happy to know it's fixed”
- [VM/Advanced] Fix virtualization mode switch (PV/HVM) (PR [#4349](https://github.com/vatesfr/xen-orchestra/pull/4349))
- [SDN Controller] Better detect host shutting down to adapt network topology (PR [#4314](https://github.com/vatesfr/xen-orchestra/pull/4314))
- [SR/General] Display VDI VM name in SR usage graph (PR [#4370](https://github.com/vatesfr/xen-orchestra/pull/4370))
- [SDN Controller] Add new hosts to pool's private networks (PR [#4382](https://github.com/vatesfr/xen-orchestra/pull/4382))
### Released packages
@@ -22,5 +25,7 @@
>
> Rule of thumb: add packages on top.
- xo-server v5.46.0
- xo-web v5.46.0
- xo-server-usage-report v0.7.3
- xo-server-sdn-controller v0.1.2
- xo-server v5.47.0
- xo-web v5.47.0

View File

@@ -42,6 +42,7 @@
"testEnvironment": "node",
"testPathIgnorePatterns": [
"/dist/",
"/xo-server-test/",
"/xo-web/"
],
"testRegex": "\\.spec\\.js$",

View File

@@ -27,7 +27,7 @@
"node": ">=6"
},
"dependencies": {
"@xen-orchestra/fs": "^0.10.0",
"@xen-orchestra/fs": "^0.10.1",
"cli-progress": "^2.0.0",
"exec-promise": "^0.7.0",
"getopts": "^2.2.3",

View File

@@ -35,7 +35,7 @@
"@babel/core": "^7.0.0",
"@babel/preset-env": "^7.0.0",
"@babel/preset-flow": "^7.0.0",
"@xen-orchestra/fs": "^0.10.0",
"@xen-orchestra/fs": "^0.10.1",
"babel-plugin-lodash": "^3.3.2",
"cross-env": "^5.1.3",
"execa": "^2.0.2",

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "xen-api",
"version": "0.27.0",
"version": "0.27.1",
"license": "ISC",
"description": "Connector to the Xen API",
"keywords": [

View File

@@ -3,7 +3,7 @@ import createLogger from '@xen-orchestra/log'
import NodeOpenssl from 'node-openssl-cert'
import { access, constants, readFile, writeFile } from 'fs'
import { EventEmitter } from 'events'
import { filter, find, forOwn, map } from 'lodash'
import { filter, find, forEach, map } from 'lodash'
import { fromCallback, fromEvent } from 'promise-toolbox'
import { join } from 'path'
@@ -48,7 +48,6 @@ export const configurationSchema = {
async function fileWrite(path, data) {
await fromCallback(writeFile, path, data)
log.debug(`${path} successfully written`)
}
async function fileRead(path) {
@@ -80,10 +79,6 @@ class SDNController extends EventEmitter {
this._getDataDir = getDataDir
this._clientKey = null
this._clientCert = null
this._caCert = null
this._poolNetworks = []
this._ovsdbClients = []
this._newHosts = []
@@ -96,8 +91,6 @@ class SDNController extends EventEmitter {
this._objectsUpdated = this._objectsUpdated.bind(this)
this._overrideCerts = false
this._unsetApiMethod = null
}
// ---------------------------------------------------------------------------
@@ -106,7 +99,7 @@ class SDNController extends EventEmitter {
this._overrideCerts = configuration['override-certs']
let certDirectory = configuration['cert-dir']
if (certDirectory == null) {
if (certDirectory === undefined) {
log.debug(`No cert-dir provided, using default self-signed certificates`)
certDirectory = await this._getDataDir()
@@ -148,7 +141,7 @@ class SDNController extends EventEmitter {
}
}
async load() {
load() {
const createPrivateNetwork = this._createPrivateNetwork.bind(this)
createPrivateNetwork.description =
'Creates a pool-wide private network on a selected pool'
@@ -157,9 +150,11 @@ class SDNController extends EventEmitter {
networkName: { type: 'string' },
networkDescription: { type: 'string' },
encapsulation: { type: 'string' },
pifId: { type: 'string' },
}
createPrivateNetwork.resolve = {
xoPool: ['poolId', 'pool', ''],
xoPif: ['pifId', 'PIF', ''],
}
this._unsetApiMethod = this._xo.addApiMethod(
'plugin.SDNController.createPrivateNetwork',
@@ -167,40 +162,58 @@ class SDNController extends EventEmitter {
)
// FIXME: we should monitor when xapis are added/removed
forOwn(this._xo.getAllXapis(), async xapi => {
await xapi.objectsFetched
return Promise.all(
map(this._xo.getAllXapis(), async xapi => {
await xapi.objectsFetched
if (this._setControllerNeeded(xapi)) {
return
}
if (this._setControllerNeeded(xapi) === false) {
this._cleaners.push(await this._manageXapi(xapi))
const hosts = filter(xapi.objects.all, { $type: 'host' })
await Promise.all(
map(hosts, async host => {
this._createOvsdbClient(host)
})
)
for (const host of hosts) {
this._createOvsdbClient(host)
}
// Add already existing pool-wide private networks
const networks = filter(xapi.objects.all, { $type: 'network' })
forOwn(networks, async network => {
if (network.other_config.private_pool_wide === 'true') {
log.debug(
`Adding network: '${network.name_label}' for pool: '${network.$pool.name_label}' to managed networks`
)
await Promise.all(
map(networks, async network => {
if (network.other_config.private_pool_wide !== 'true') {
return
}
log.debug('Adding network to managed networks', {
network: network.name_label,
pool: network.$pool.name_label,
})
const center = await this._electNewCenter(network, true)
// Previously created network didn't store `pif_device`
if (network.other_config.pif_device === undefined) {
const tunnel = this._getHostTunnelForNetwork(center, network.$ref)
const pif = xapi.getObjectByRef(tunnel.transport_PIF)
await xapi.call(
'network.add_to_other_config',
network.$ref,
'pif_device',
pif.device
)
}
this._poolNetworks.push({
pool: network.$pool.$ref,
network: network.$ref,
starCenter: center?.$ref,
})
this._networks.set(network.$id, network.$ref)
if (center != null) {
if (center !== undefined) {
this._starCenters.set(center.$id, center.$ref)
}
}
})
}
})
})
)
})
)
}
async unload() {
@@ -224,10 +237,13 @@ class SDNController extends EventEmitter {
networkName,
networkDescription,
encapsulation,
xoPif,
}) {
const pool = this._xo.getXapiObject(xoPool)
await this._setPoolControllerIfNeeded(pool)
const pif = this._xo.getXapiObject(xoPif)
// Create the private network
const privateNetworkRef = await pool.$xapi.call('network.create', {
name_label: networkName,
@@ -237,20 +253,22 @@ class SDNController extends EventEmitter {
automatic: 'false',
private_pool_wide: 'true',
encapsulation: encapsulation,
pif_device: pif.device,
},
})
const privateNetwork = await pool.$xapi._getOrWaitObject(privateNetworkRef)
log.info(
`Private network '${privateNetwork.name_label}' has been created for pool '${pool.name_label}'`
)
log.info('New private network created', {
network: privateNetwork.name_label,
pool: pool.name_label,
})
// For each pool's host, create a tunnel to the private network
const hosts = filter(pool.$xapi.objects.all, { $type: 'host' })
await Promise.all(
map(hosts, async host => {
await this._createTunnel(host, privateNetwork)
await this._createTunnel(host, privateNetwork, pif.device)
this._createOvsdbClient(host)
})
)
@@ -260,10 +278,9 @@ class SDNController extends EventEmitter {
pool: pool.$ref,
network: privateNetwork.$ref,
starCenter: center?.$ref,
encapsulation: encapsulation,
})
this._networks.set(privateNetwork.$id, privateNetwork.$ref)
if (center != null) {
if (center !== undefined) {
this._starCenters.set(center.$id, center.$ref)
}
}
@@ -287,50 +304,52 @@ class SDNController extends EventEmitter {
}
}
async _objectsAdded(objects) {
await Promise.all(
map(objects, async object => {
const { $type } = object
_objectsAdded(objects) {
forEach(objects, object => {
const { $type } = object
if ($type === 'host') {
log.debug(
`New host: '${object.name_label}' in pool: '${object.$pool.name_label}'`
)
if ($type === 'host') {
log.debug('New host', {
host: object.name_label,
pool: object.$pool.name_label,
})
if (find(this._newHosts, { $ref: object.$ref }) == null) {
this._newHosts.push(object)
}
this._createOvsdbClient(object)
if (find(this._newHosts, { $ref: object.$ref }) === undefined) {
this._newHosts.push(object)
}
})
)
this._createOvsdbClient(object)
}
})
}
async _objectsUpdated(objects) {
await Promise.all(
map(objects, async (object, id) => {
_objectsUpdated(objects) {
return Promise.all(
map(objects, object => {
const { $type } = object
if ($type === 'PIF') {
await this._pifUpdated(object)
} else if ($type === 'host') {
await this._hostUpdated(object)
return this._pifUpdated(object)
}
if ($type === 'host') {
return this._hostUpdated(object)
}
if ($type === 'host_metrics') {
return this._hostMetricsUpdated(object)
}
})
)
}
async _objectsRemoved(xapi, objects) {
await Promise.all(
_objectsRemoved(xapi, objects) {
return Promise.all(
map(objects, async (object, id) => {
const client = find(this._ovsdbClients, { id: id })
if (client != null) {
this._ovsdbClients.splice(this._ovsdbClients.indexOf(client), 1)
}
this._ovsdbClients = this._ovsdbClients.filter(
client => client.host.$id !== id
)
// If a Star center host is removed: re-elect a new center where needed
const starCenterRef = this._starCenters.get(id)
if (starCenterRef != null) {
if (starCenterRef !== undefined) {
this._starCenters.delete(id)
const poolNetworks = filter(this._poolNetworks, {
starCenter: starCenterRef,
@@ -339,7 +358,7 @@ class SDNController extends EventEmitter {
const network = xapi.getObjectByRef(poolNetwork.network)
const newCenter = await this._electNewCenter(network, true)
poolNetwork.starCenter = newCenter?.$ref
if (newCenter != null) {
if (newCenter !== undefined) {
this._starCenters.set(newCenter.$id, newCenter.$ref)
}
}
@@ -348,17 +367,11 @@ class SDNController extends EventEmitter {
// If a network is removed, clean this._poolNetworks from it
const networkRef = this._networks.get(id)
if (networkRef != null) {
if (networkRef !== undefined) {
this._networks.delete(id)
const poolNetwork = find(this._poolNetworks, {
network: networkRef,
})
if (poolNetwork != null) {
this._poolNetworks.splice(
this._poolNetworks.indexOf(poolNetwork),
1
)
}
this._poolNetworks = this._poolNetworks.filter(
poolNetwork => poolNetwork.network !== networkRef
)
}
})
)
@@ -367,37 +380,53 @@ class SDNController extends EventEmitter {
async _pifUpdated(pif) {
// Only if PIF is in a private network
const poolNetwork = find(this._poolNetworks, { network: pif.network })
if (poolNetwork == null) {
if (poolNetwork === undefined) {
return
}
if (!pif.currently_attached) {
const tunnel = this._getHostTunnelForNetwork(pif.$host, pif.network)
await pif.$xapi.call('tunnel.set_status', tunnel.$ref, {
active: 'false',
})
if (poolNetwork.starCenter !== pif.host) {
return
}
log.debug(
`PIF: '${pif.device}' of network: '${pif.$network.name_label}' star-center host: '${pif.$host.name_label}' has been unplugged, electing a new host`
'PIF of star-center host has been unplugged, electing a new star-center',
{
pif: pif.device,
network: pif.$network.name_label,
host: pif.$host.name_label,
pool: pif.$pool.name_label,
}
)
const newCenter = await this._electNewCenter(pif.$network, true)
poolNetwork.starCenter = newCenter?.$ref
this._starCenters.delete(pif.$host.$id)
if (newCenter != null) {
if (newCenter !== undefined) {
this._starCenters.set(newCenter.$id, newCenter.$ref)
}
} else {
if (poolNetwork.starCenter == null) {
if (poolNetwork.starCenter === undefined) {
const host = pif.$host
log.debug(
`First available host: '${host.name_label}' becomes star center of network: '${pif.$network.name_label}'`
)
log.debug('First available host becomes star center of network', {
host: host.name_label,
network: pif.$network.name_label,
pool: pif.$pool.name_label,
})
poolNetwork.starCenter = pif.host
this._starCenters.set(host.$id, host.$ref)
}
log.debug(
`PIF: '${pif.device}' of network: '${pif.$network.name_label}' host: '${pif.$host.name_label}' has been plugged`
)
log.debug('PIF plugged', {
pif: pif.device,
network: pif.$network.name_label,
host: pif.$host.name_label,
pool: pif.$pool.name_label,
})
const starCenter = pif.$xapi.getObjectByRef(poolNetwork.starCenter)
await this._addHostToNetwork(pif.$host, pif.$network, starCenter)
@@ -405,74 +434,66 @@ class SDNController extends EventEmitter {
}
async _hostUpdated(host) {
const xapi = host.$xapi
if (host.enabled) {
if (host.PIFs.length === 0) {
return
}
const tunnels = filter(xapi.objects.all, { $type: 'tunnel' })
const newHost = find(this._newHosts, { $ref: host.$ref })
if (newHost != null) {
this._newHosts.splice(this._newHosts.indexOf(newHost), 1)
try {
await xapi.call('pool.certificate_sync')
} catch (error) {
log.error(
`Couldn't sync SDN controller ca certificate in pool: '${host.$pool.name_label}' because: ${error}`
)
}
}
for (const tunnel of tunnels) {
const accessPIF = xapi.getObjectByRef(tunnel.access_PIF)
if (accessPIF.host !== host.$ref) {
continue
}
if (newHost !== undefined) {
this._newHosts = this._newHosts.slice(
this._newHosts.indexOf(newHost),
1
)
const poolNetwork = find(this._poolNetworks, {
network: accessPIF.network,
log.debug('Sync pool certificates', {
newHost: host.name_label,
pool: host.$pool.name_label,
})
if (poolNetwork == null) {
continue
}
if (accessPIF.currently_attached) {
continue
}
log.debug(
`Pluging PIF: '${accessPIF.device}' for host: '${host.name_label}' on network: '${accessPIF.$network.name_label}'`
)
try {
await xapi.call('PIF.plug', accessPIF.$ref)
await host.$xapi.call('pool.certificate_sync')
} catch (error) {
log.error(
`XAPI error while pluging PIF: '${accessPIF.device}' on host: '${host.name_label}' for network: '${accessPIF.$network.name_label}'`
log.error('Error while syncing SDN controller CA certificate', {
error,
pool: host.$pool.name_label,
})
}
const poolNetworks = filter(this._poolNetworks, {
pool: host.$pool.$ref,
})
for (const poolNetwork of poolNetworks) {
const tunnel = this._getHostTunnelForNetwork(
host,
poolNetwork.network
)
if (tunnel !== undefined) {
continue
}
const network = host.$xapi.getObjectByRef(poolNetwork.network)
const pifDevice = network.other_config.pif_device || 'eth0'
this._createTunnel(host, network, pifDevice)
}
const starCenter = host.$xapi.getObjectByRef(poolNetwork.starCenter)
await this._addHostToNetwork(host, accessPIF.$network, starCenter)
}
} else {
const poolNetworks = filter(this._poolNetworks, { starCenter: host.$ref })
for (const poolNetwork of poolNetworks) {
const network = host.$xapi.getObjectByRef(poolNetwork.network)
log.debug(
`Star center host: '${host.name_label}' of network: '${network.name_label}' in pool: '${host.$pool.name_label}' is no longer reachable, electing a new host`
)
const newCenter = await this._electNewCenter(network, true)
poolNetwork.starCenter = newCenter?.$ref
this._starCenters.delete(host.$id)
if (newCenter != null) {
this._starCenters.set(newCenter.$id, newCenter.$ref)
}
this._addHostToPoolNetworks(host)
}
}
}
_hostMetricsUpdated(hostMetrics) {
const ovsdbClient = find(
this._ovsdbClients,
client => client.host.metrics === hostMetrics.$ref
)
if (hostMetrics.live) {
return this._addHostToPoolNetworks(ovsdbClient.host)
}
return this._hostUnreachable(ovsdbClient.host)
}
// ---------------------------------------------------------------------------
async _setPoolControllerIfNeeded(pool) {
@@ -482,20 +503,24 @@ class SDNController extends EventEmitter {
}
const controller = find(pool.$xapi.objects.all, { $type: 'SDN_controller' })
if (controller != null) {
if (controller !== undefined) {
await pool.$xapi.call('SDN_controller.forget', controller.$ref)
log.debug(`Remove old SDN controller from pool: '${pool.name_label}'`)
log.debug('Old SDN controller removed', {
pool: pool.name_label,
})
}
await pool.$xapi.call('SDN_controller.introduce', PROTOCOL)
log.debug(`Set SDN controller of pool: '${pool.name_label}'`)
log.debug('SDN controller has been set', {
pool: pool.name_label,
})
this._cleaners.push(await this._manageXapi(pool.$xapi))
}
_setControllerNeeded(xapi) {
const controller = find(xapi.objects.all, { $type: 'SDN_controller' })
return !(
controller != null &&
controller !== undefined &&
controller.protocol === PROTOCOL &&
controller.address === '' &&
controller.port === 0
@@ -512,15 +537,16 @@ class SDNController extends EventEmitter {
needInstall = true
} else if (this._overrideCerts) {
await xapi.call('pool.certificate_uninstall', SDN_CONTROLLER_CERT)
log.debug(
`Old SDN Controller CA certificate uninstalled on pool: '${xapi.pool.name_label}'`
)
log.debug('Old SDN controller CA certificate uninstalled', {
pool: xapi.pool.name_label,
})
needInstall = true
}
} catch (error) {
log.error(
`Couldn't retrieve certificate list of pool: '${xapi.pool.name_label}'`
)
log.error('Error while retrieving certificate list', {
error,
pool: xapi.pool.name_label,
})
}
if (!needInstall) {
return
@@ -533,13 +559,14 @@ class SDNController extends EventEmitter {
this._caCert.toString()
)
await xapi.call('pool.certificate_sync')
log.debug(
`SDN controller CA certificate install in pool: '${xapi.pool.name_label}'`
)
log.debug('SDN controller CA certficate installed', {
pool: xapi.pool.name_label,
})
} catch (error) {
log.error(
`Couldn't install SDN controller CA certificate in pool: '${xapi.pool.name_label}' because: ${error}`
)
log.error('Error while installing SDN controller CA certificate', {
error,
pool: xapi.pool.name_label,
})
}
}
@@ -548,72 +575,95 @@ class SDNController extends EventEmitter {
async _electNewCenter(network, resetNeeded) {
const pool = network.$pool
let newCenter = null
let newCenter
const hosts = filter(pool.$xapi.objects.all, { $type: 'host' })
for (const host of hosts) {
const pif = find(host.$PIFs, { network: network.$ref })
if (pif !== undefined && pif.currently_attached && host.$metrics.live) {
newCenter = host
}
}
await Promise.all(
map(hosts, async host => {
if (resetNeeded) {
// Clean old ports and interfaces
const hostClient = find(this._ovsdbClients, { host: host.$ref })
if (hostClient != null) {
try {
await hostClient.resetForNetwork(network.uuid, network.name_label)
} catch (error) {
log.error(
`Couldn't reset network: '${network.name_label}' for host: '${host.name_label}' in pool: '${network.$pool.name_label}' because: ${error}`
)
return
}
}
}
if (newCenter != null) {
if (!resetNeeded) {
return
}
const pif = find(host.$PIFs, { network: network.$ref })
if (pif != null && pif.currently_attached && host.enabled) {
newCenter = host
// Clean old ports and interfaces
const hostClient = find(
this._ovsdbClients,
client => client.host.$ref === host.$ref
)
if (hostClient !== undefined) {
try {
await hostClient.resetForNetwork(network.uuid, network.name_label)
} catch (error) {
log.error('Error while resetting private network', {
error,
network: network.name_label,
host: host.name_label,
pool: network.$pool.name_label,
})
}
}
})
)
if (newCenter == null) {
log.error(
`Unable to elect a new star-center host to network: '${network.name_label}' for pool: '${network.$pool.name_label}' because there's no available host`
)
return null
if (newCenter === undefined) {
log.error('No available host to elect new star-center', {
network: network.name_label,
pool: network.$pool.name_label,
})
return
}
// Recreate star topology
await Promise.all(
await map(hosts, async host => {
await this._addHostToNetwork(host, network, newCenter)
})
map(hosts, host => this._addHostToNetwork(host, network, newCenter))
)
log.info(
`New star center host elected: '${newCenter.name_label}' in network: '${network.name_label}'`
)
log.info('New star-center elected', {
center: newCenter.name_label,
network: network.name_label,
pool: network.$pool.name_label,
})
return newCenter
}
async _createTunnel(host, network) {
const pif = host.$PIFs.find(
pif => pif.physical && pif.ip_configuration_mode !== 'None'
)
if (pif == null) {
log.error(
`No PIF found to create tunnel on host: '${host.name_label}' for network: '${network.name_label}'`
)
async _createTunnel(host, network, pifDevice) {
const hostPif = find(host.$PIFs, { device: pifDevice })
if (hostPif === undefined) {
log.error("Can't create tunnel: no available PIF", {
pif: pifDevice,
network: network.name_label,
host: host.name_label,
pool: host.$pool.name_label,
})
return
}
await host.$xapi.call('tunnel.create', pif.$ref, network.$ref)
log.debug(
`Tunnel added on host '${host.name_label}' for network '${network.name_label}'`
)
try {
await host.$xapi.call('tunnel.create', hostPif.$ref, network.$ref)
} catch (error) {
log.error('Error while creating tunnel', {
error,
pif: pifDevice,
network: network.name_label,
host: host.name_label,
pool: host.$pool.name_label,
})
return
}
log.debug('New tunnel added', {
pif: pifDevice,
network: network.name_label,
host: host.name_label,
pool: host.$pool.name_label,
})
}
async _addHostToNetwork(host, network, starCenter) {
@@ -622,54 +672,163 @@ class SDNController extends EventEmitter {
return
}
const hostClient = find(this._ovsdbClients, {
host: host.$ref,
})
if (hostClient == null) {
log.error(`No OVSDB client found for host: '${host.name_label}'`)
const xapi = host.$xapi
const tunnel = this._getHostTunnelForNetwork(host, network.$ref)
const starCenterTunnel = this._getHostTunnelForNetwork(
starCenter,
network.$ref
)
await xapi.call('tunnel.set_status', tunnel.$ref, { active: 'false' })
const hostClient = find(
this._ovsdbClients,
client => client.host.$ref === host.$ref
)
if (hostClient === undefined) {
log.error('No OVSDB client found', {
host: host.name_label,
pool: host.$pool.name_label,
})
return
}
const starCenterClient = find(this._ovsdbClients, {
host: starCenter.$ref,
})
if (starCenterClient == null) {
log.error(
`No OVSDB client found for star-center host: '${starCenter.name_label}'`
)
const starCenterClient = find(
this._ovsdbClients,
client => client.host.$ref === starCenter.$ref
)
if (starCenterClient === undefined) {
log.error('No OVSDB client found for star-center', {
host: starCenter.name_label,
pool: starCenter.$pool.name_label,
})
return
}
const encapsulation =
network.other_config.encapsulation != null
? network.other_config.encapsulation
: 'gre'
const encapsulation = network.other_config.encapsulation || 'gre'
let bridgeName
try {
await hostClient.addInterfaceAndPort(
bridgeName = await hostClient.addInterfaceAndPort(
network.uuid,
network.name_label,
starCenterClient.address,
starCenterClient.host.address,
encapsulation
)
await starCenterClient.addInterfaceAndPort(
network.uuid,
network.name_label,
hostClient.address,
hostClient.host.address,
encapsulation
)
} catch (error) {
log.error(
`Couldn't add host: '${host.name_label}' to network: '${network.name_label}' in pool: '${host.$pool.name_label}' because: ${error}`
)
log.error('Error while connecting host to private network', {
error,
network: network.name_label,
host: host.name_label,
pool: host.$pool.name_label,
})
}
if (bridgeName !== undefined) {
const activeStatus = { active: 'true', key: bridgeName }
await Promise.all([
xapi.call('tunnel.set_status', tunnel.$ref, activeStatus),
xapi.call('tunnel.set_status', starCenterTunnel.$ref, activeStatus),
])
}
}
async _addHostToPoolNetworks(host) {
const xapi = host.$xapi
const tunnels = filter(xapi.objects.all, { $type: 'tunnel' })
for (const tunnel of tunnels) {
const accessPif = xapi.getObjectByRef(tunnel.access_PIF)
if (accessPif.host !== host.$ref) {
continue
}
const poolNetwork = find(this._poolNetworks, {
network: accessPif.network,
})
if (poolNetwork === undefined || accessPif.currently_attached) {
continue
}
try {
await xapi.call('PIF.plug', accessPif.$ref)
} catch (error) {
log.error('Error while plugging PIF', {
error,
pif: accessPif.device,
network: accessPif.$network.name_label,
host: host.name_label,
pool: host.$pool.name_label,
})
continue
}
log.debug('PIF plugged', {
pif: accessPif.device,
network: accessPif.$network.name_label,
host: host.name_label,
pool: host.$pool.name_label,
})
const starCenter = xapi.getObjectByRef(poolNetwork.starCenter)
await this._addHostToNetwork(host, accessPif.$network, starCenter)
}
}
async _hostUnreachable(host) {
const poolNetworks = filter(this._poolNetworks, { starCenter: host.$ref })
for (const poolNetwork of poolNetworks) {
const network = host.$xapi.getObjectByRef(poolNetwork.network)
log.debug('Unreachable star-center, electing a new one', {
network: network.name_label,
center: host.name_label,
pool: host.$pool.name_label,
})
const newCenter = await this._electNewCenter(network, true)
poolNetwork.starCenter = newCenter?.$ref
this._starCenters.delete(host.$id)
if (newCenter !== undefined) {
this._starCenters.set(newCenter.$id, newCenter.$ref)
}
}
for (const poolNetwork of this._poolNetworks) {
const tunnel = this._getHostTunnelForNetwork(host, poolNetwork.network)
await host.$xapi.call('tunnel.set_status', tunnel.$ref, {
active: 'false',
})
}
}
// ---------------------------------------------------------------------------
_getHostTunnelForNetwork(host, networkRef) {
const pif = find(host.$PIFs, { network: networkRef })
if (pif === undefined) {
return
}
const tunnel = find(host.$xapi.objects.all, {
$type: 'tunnel',
access_PIF: pif.$ref,
})
return tunnel
}
// ---------------------------------------------------------------------------
_createOvsdbClient(host) {
const foundClient = find(this._ovsdbClients, { host: host.$ref })
if (foundClient != null) {
const foundClient = find(
this._ovsdbClients,
client => client.host.$ref === host.$ref
)
if (foundClient !== undefined) {
return foundClient
}
@@ -709,15 +868,22 @@ class SDNController extends EventEmitter {
subject: subject,
}
openssl.generateRSAPrivateKey(rsakeyoptions, (err, cakey, cmd) => {
if (err) {
log.error(`Error while generating CA private key: ${err}`)
// In all the following callbacks, `error` is:
// - either an error object if there was an error
// - or a boolean set to `false` if no error occurred
openssl.generateRSAPrivateKey(rsakeyoptions, (error, cakey, cmd) => {
if (error !== false) {
log.error('Error while generating CA private key', {
error,
})
return
}
openssl.generateCSR(cacsroptions, cakey, null, (err, csr, cmd) => {
if (err) {
log.error(`Error while generating CA certificate: ${err}`)
openssl.generateCSR(cacsroptions, cakey, null, (error, csr, cmd) => {
if (error !== false) {
log.error('Error while generating CA certificate', {
error,
})
return
}
@@ -726,45 +892,58 @@ class SDNController extends EventEmitter {
cacsroptions,
cakey,
null,
async (err, cacrt, cmd) => {
if (err) {
log.error(`Error while signing CA certificate: ${err}`)
async (error, cacrt, cmd) => {
if (error !== false) {
log.error('Error while signing CA certificate', {
error,
})
return
}
await fileWrite(join(dataDir, CA_CERT), cacrt)
openssl.generateRSAPrivateKey(
rsakeyoptions,
async (err, key, cmd) => {
if (err) {
log.error(`Error while generating private key: ${err}`)
async (error, key, cmd) => {
if (error !== false) {
log.error('Error while generating private key', {
error,
})
return
}
await fileWrite(join(dataDir, CLIENT_KEY), key)
openssl.generateCSR(csroptions, key, null, (err, csr, cmd) => {
if (err) {
log.error(`Error while generating certificate: ${err}`)
return
}
openssl.CASignCSR(
csr,
cacsroptions,
false,
cacrt,
cakey,
null,
async (err, crt, cmd) => {
if (err) {
log.error(`Error while signing certificate: ${err}`)
return
}
await fileWrite(join(dataDir, CLIENT_CERT), crt)
this.emit('certWritten')
openssl.generateCSR(
csroptions,
key,
null,
(error, csr, cmd) => {
if (error !== false) {
log.error('Error while generating certificate', {
error,
})
return
}
)
})
openssl.CASignCSR(
csr,
cacsroptions,
false,
cacrt,
cakey,
null,
async (error, crt, cmd) => {
if (error !== false) {
log.error('Error while signing certificate', {
error,
})
return
}
await fileWrite(join(dataDir, CLIENT_CERT), crt)
this.emit('certWritten')
}
)
}
)
}
)
}

View File

@@ -12,35 +12,30 @@ const OVSDB_PORT = 6640
export class OvsdbClient {
constructor(host, clientKey, clientCert, caCert) {
this._host = host
this._numberOfPortAndInterface = 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,
})
}
// ---------------------------------------------------------------------------
@@ -51,6 +46,16 @@ export class OvsdbClient {
remoteAddress,
encapsulation
) {
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 index = this._numberOfPortAndInterface
++this._numberOfPortAndInterface
@@ -60,8 +65,9 @@ export class OvsdbClient {
networkName,
socket
)
if (bridgeUuid == null) {
if (bridgeUuid === undefined) {
socket.destroy()
this._adding = this._adding.slice(this._adding.indexOf(adding), 1)
return
}
@@ -73,7 +79,8 @@ export class OvsdbClient {
)
if (alreadyExist) {
socket.destroy()
return
this._adding = this._adding.slice(this._adding.indexOf(adding), 1)
return bridgeName
}
const interfaceName = 'tunnel_iface' + index
@@ -115,7 +122,9 @@ export class OvsdbClient {
mutateBridgeOperation,
]
const jsonObjects = await this._sendOvsdbTransaction(params, socket)
if (jsonObjects == null) {
this._adding = this._adding.slice(this._adding.indexOf(adding), 1)
if (jsonObjects === undefined) {
socket.destroy()
return
}
@@ -126,25 +135,36 @@ export class OvsdbClient {
let opResult
do {
opResult = jsonObjects[0].result[i]
if (opResult != null && opResult.error != null) {
if (opResult !== undefined && 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) {
@@ -154,14 +174,14 @@ export class OvsdbClient {
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
}
@@ -176,15 +196,12 @@ 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}'`
)
portsToDelete.push(['uuid', portUuid])
}
})
@@ -205,21 +222,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()
}
@@ -269,15 +290,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]
}
@@ -289,14 +311,14 @@ export class OvsdbClient {
socket
) {
const ports = await this._getBridgePorts(bridgeUuid, bridgeName, socket)
if (ports == null) {
return
if (ports === undefined) {
return false
}
for (const port of ports) {
const portUuid = port[1]
const interfaces = await this._getPortInterfaces(portUuid, socket)
if (interfaces == null) {
if (interfaces === undefined) {
continue
}
@@ -319,8 +341,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'
@@ -336,8 +358,8 @@ export class OvsdbClient {
where,
socket
)
if (selectResult == null) {
return null
if (selectResult === undefined) {
return
}
return selectResult.interfaces[0] === 'set'
@@ -353,7 +375,7 @@ export class OvsdbClient {
where,
socket
)
if (selectResult == null) {
if (selectResult === undefined) {
return false
}
@@ -378,28 +400,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]
@@ -419,10 +449,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
@@ -432,10 +463,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)
@@ -452,7 +484,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,
@@ -462,18 +494,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,144 @@
# 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 is due to an inadvertently modified snapshots.
## 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,28 @@
[xoConnection]
url = ''
email = ''
password = ''
[servers]
[servers.default]
username = ''
password = ''
host = ''
[vms]
default = ''
# vmToBackup = ''
[templates]
default = ''
templateWithoutDisks = ''
[srs]
default = ''
localSr = ''
sharedSr = ''
[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,254 @@
/* 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)
}
}
)
})
})
for (const id in this.objects.all) {
if (this.objects.all[id].other) {
const { 'xo:backup:schedule': snapshotSchedule } = this.objects.all[
id
].other
if (snapshotSchedule === scheduleId) {
this._tempResourceDisposers.push('vm.delete', {
id,
})
}
}
}
}
return backups
}
async importVmBackup(params) {
const id = await xo.call('backupNg.importVmBackup', params)
this._tempResourceDisposers.push('vm.delete', { id })
return id
}
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,600 @@
/* 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(6e4)
const {
vms: { default: defaultVm, vmToBackup = defaultVm },
remotes: { default: defaultRemote, remote1, remote2 = defaultRemote },
srs: { localSr, sharedSr },
servers: { default: defaultServer },
} = config
expect(vmToBackup).not.toBe(undefined)
expect(remote1).not.toBe(undefined)
expect(remote2).not.toBe(undefined)
expect(localSr).not.toBe(undefined)
expect(sharedSr).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,
})
})
const vmBackupOnLocalSr = await xo.importVmBackup({
id: backupsByRemote[remoteId1][0],
sr: localSr,
})
const vmBackupOnSharedSr = await xo.importVmBackup({
id: backupsByRemote[remoteId2][0],
sr: sharedSr,
})
expect(xo.objects.all[vmBackupOnLocalSr]).not.toBe(undefined)
expect(xo.objects.all[vmBackupOnSharedSr]).not.toBe(undefined)
await xo.call('vm.start', { id: vmBackupOnLocalSr })
await xo.call('vm.start', { id: vmBackupOnSharedSr })
})
})

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

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

@@ -1,7 +1,7 @@
{
"private": true,
"name": "xo-server",
"version": "5.45.1",
"version": "5.46.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.10.0",
"@xen-orchestra/fs": "^0.10.1",
"@xen-orchestra/log": "^0.1.4",
"@xen-orchestra/mixin": "^0.0.0",
"ajv": "^6.1.1",
@@ -123,7 +123,7 @@
"value-matcher": "^0.2.0",
"vhd-lib": "^0.7.0",
"ws": "^6.0.0",
"xen-api": "^0.27.0",
"xen-api": "^0.27.1",
"xml2js": "^0.4.19",
"xo-acl-resolver": "^0.4.1",
"xo-collection": "^0.4.1",

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,43 +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

@@ -76,6 +76,7 @@ const TRANSFORMS = {
cores: cpuInfo && +cpuInfo.cpu_count,
sockets: cpuInfo && +cpuInfo.socket_count,
},
zstdSupported: obj.restrictions.restrict_zstd_export === 'false',
// TODO
// - ? networks = networksByPool.items[pool.id] (network.$pool.id)
@@ -141,7 +142,8 @@ const TRANSFORMS = {
},
current_operations: obj.current_operations,
hostname: obj.hostname,
iSCSI_name: otherConfig.iscsi_iqn || null,
iscsiIqn: obj.iscsi_iqn ?? otherConfig.iscsi_iqn ?? '',
zstdSupported: obj.license_params.restrict_zstd_export === 'false',
license_params: obj.license_params,
license_server: obj.license_server,
license_expiry: toTimestamp(obj.license_params.expiry),

View File

@@ -1158,6 +1158,9 @@ export default class Xapi extends XapiBase {
{
force: 'true',
}
// FIXME: missing param `vgu_map`, it does not cause issues ATM but it
// might need to be changed one day.
// {},
)::pCatch({ code: 'TOO_MANY_STORAGE_MIGRATES' }, () =>
pDelay(1e4).then(loop)
)

View File

@@ -255,7 +255,7 @@ export default {
)) !== undefined
) {
if (getAll) {
log(
log.debug(
`patch ${patch.name} (${id}) conflicts with installed patch ${conflictId}`
)
return
@@ -271,7 +271,7 @@ export default {
)) !== undefined
) {
if (getAll) {
log(`patches ${id} and ${conflictId} conflict with eachother`)
log.debug(`patches ${id} and ${conflictId} conflict with eachother`)
return
}
throw new Error(

View File

@@ -259,7 +259,7 @@ export default {
affinityHost: {
get: 'affinity',
set: (value, vm) =>
vm.set_affinity(value ? this.getObject(value).$ref : NULL_REF),
vm.set_affinity(value ? vm.$xapi.getObject(value).$ref : NULL_REF),
},
autoPoweron: {
@@ -306,7 +306,9 @@ export default {
get: vm => +vm.VCPUs_at_startup,
set: [
'VCPUs_at_startup',
(value, vm) => isVmRunning(vm) && vm.set_VCPUs_number_live(value),
(value, vm) =>
isVmRunning(vm) &&
vm.$xapi.call('VM.set_VCPUs_number_live', vm.$ref, String(value)),
],
},

View File

@@ -0,0 +1,99 @@
import { difference, flatten, isEmpty, uniq } from 'lodash'
export default class Pools {
constructor(xo) {
this._xo = xo
}
async mergeInto({ sources: sourceIds, target, force }) {
const { _xo } = this
const targetHost = _xo.getObject(target.master)
const sources = []
const sourcePatches = {}
// Check hosts compatibility.
for (const sourceId of sourceIds) {
const source = _xo.getObject(sourceId)
const sourceHost = _xo.getObject(source.master)
if (sourceHost.productBrand !== targetHost.productBrand) {
throw new Error(
`a ${sourceHost.productBrand} pool cannot be merged into a ${targetHost.productBrand} pool`
)
}
if (sourceHost.version !== targetHost.version) {
throw new Error('The hosts are not compatible')
}
sources.push(source)
sourcePatches[sourceId] = sourceHost.patches
}
// Find missing patches on the target.
const targetRequiredPatches = uniq(
flatten(
await Promise.all(
sources.map(({ master }) =>
_xo.getPatchesDifference(master, target.master)
)
)
)
)
// Find missing patches on the sources.
const allRequiredPatches = targetRequiredPatches.concat(
targetHost.patches.map(patchId => _xo.getObject(patchId).name)
)
const sourceRequiredPatches = {}
for (const sourceId of sourceIds) {
const _sourcePatches = sourcePatches[sourceId].map(
patchId => _xo.getObject(patchId).name
)
const requiredPatches = difference(allRequiredPatches, _sourcePatches)
if (requiredPatches.length > 0) {
sourceRequiredPatches[sourceId] = requiredPatches
}
}
// On XCP-ng, "installPatches" installs *all* the patches
// whatever the patches argument is.
// So we must not call it if there are no patches to install.
if (targetRequiredPatches.length > 0 || !isEmpty(sourceRequiredPatches)) {
// Find patches in parallel.
const findPatchesPromises = []
const sourceXapis = {}
const targetXapi = _xo.getXapi(target)
for (const sourceId of sourceIds) {
const sourceXapi = (sourceXapis[sourceId] = _xo.getXapi(sourceId))
findPatchesPromises.push(
sourceXapi.findPatches(sourceRequiredPatches[sourceId] ?? [])
)
}
const patchesName = await Promise.all([
targetXapi.findPatches(targetRequiredPatches),
...findPatchesPromises,
])
// Install patches in parallel.
const installPatchesPromises = []
installPatchesPromises.push(
targetXapi.installPatches({
patches: patchesName[0],
})
)
let i = 1
for (const sourceId of sourceIds) {
installPatchesPromises.push(
sourceXapis[sourceId].installPatches({
patches: patchesName[i++],
})
)
}
await Promise.all(installPatchesPromises)
}
// Merge the sources into the target sequentially to be safe.
for (const source of sources) {
await _xo.mergeXenPools(source._xapiId, target._xapiId, force)
}
}
}

View File

@@ -1,7 +1,7 @@
{
"private": true,
"name": "xo-web",
"version": "5.45.1",
"version": "5.46.0",
"license": "AGPL-3.0",
"description": "Web interface client for Xen-Orchestra",
"keywords": [

View File

@@ -770,14 +770,14 @@ const messages = {
// ----- Pool actions ------
addSrLabel: 'Add SR',
addVmLabel: 'Add VM',
addHostLabel: 'Add Host',
addHostsLabel: 'Add hosts',
missingPatchesPool:
'The pool needs to install {nMissingPatches, number} patch{nMissingPatches, plural, one {} other {es}}. This operation may take a while.',
missingPatchesHost:
'This host needs to install {nMissingPatches, number} patch{nMissingPatches, plural, one {} other {es}}. This operation may take a while.',
'The selected host{nHosts, plural, one {} other {s}} need{nHosts, plural, one {s} other {}} to install {nMissingPatches, number} patch{nMissingPatches, plural, one {} other {es}}. This operation may take a while.',
patchUpdateNoInstall:
'This host cannot be added to the pool because the patches are not homogeneous.',
addHostErrorTitle: 'Adding host failed',
'The selected host{nHosts, plural, one {} other {s}} cannot be added to the pool because the patches are not homogeneous.',
addHostsErrorTitle: 'Adding host{nHosts, plural, one {} other {s}} failed',
addHostNotHomogeneousErrorMessage: 'Host patches could not be homogenized.',
disconnectServer: 'Disconnect',
@@ -803,6 +803,9 @@ const messages = {
// ----- host stat tab -----
statLoad: 'Load average',
// ----- host advanced tab -----
editHostIscsiIqnTitle: 'Edit iSCSI IQN',
editHostIscsiIqnMessage:
'Are you sure you want to edit the iSCSI IQN? This may result in failures connecting to existing SRs if the host is attached to iSCSI SRs.',
hostTitleRamUsage: 'Host RAM usage:',
memoryHostState:
'RAM: {memoryUsed} used on {memoryTotal} ({memoryFree} free)',
@@ -813,7 +816,7 @@ const messages = {
hostAddress: 'Address',
hostStatus: 'Status',
hostBuildNumber: 'Build number',
hostIscsiName: 'iSCSI name',
hostIscsiIqn: 'iSCSI IQN',
hostNoIscsiSr: 'Not connected to an iSCSI SR',
hostMultipathingSrs: 'Click to see concerned SRs',
hostMultipathingPaths:
@@ -850,6 +853,7 @@ const messages = {
supplementalPackInstallSuccessTitle: 'Installation success',
supplementalPackInstallSuccessMessage:
'Supplemental pack successfully installed.',
uniqueHostIscsiIqnInfo: 'The iSCSI IQN must be unique. ',
// ----- Host net tabs -----
networkCreateButton: 'Add a network',
pifDeviceLabel: 'Device',
@@ -942,6 +946,8 @@ const messages = {
powerStateRunning: 'Running',
powerStateSuspended: 'Suspended',
powerStatePaused: 'Paused',
powerStateDisabled: 'Disabled',
powerStateBusy: 'Busy',
// ----- VM home -----
vmCurrentStatus: 'Current status:',
@@ -975,9 +981,11 @@ const messages = {
// ----- VM console tab -----
copyToClipboardLabel: 'Copy',
ctrlAltDelButtonLabel: 'Ctrl+Alt+Del',
multilineCopyToClipboard: 'Multiline copy',
tipLabel: 'Tip:',
hideHeaderTooltip: 'Hide info',
showHeaderTooltip: 'Show info',
sendToClipboard: 'Send to clipboard',
// ----- VM container tab -----
containerName: 'Name',
@@ -1740,7 +1748,7 @@ const messages = {
privateNetwork: 'Private network',
// ----- Add host -----
addHostSelectHost: 'Host',
hosts: 'Hosts',
addHostNoHost: 'No host',
addHostNoHostMessage: 'No host selected to be added',
@@ -1897,7 +1905,7 @@ const messages = {
OtpAuthentication: 'OTP authentication',
// ----- Usage -----
others: 'Others',
others: '{nOthers, number} other{nOthers, plural, one {} other {s}}',
// ----- Logs -----
logUser: 'User',
@@ -1927,7 +1935,9 @@ const messages = {
reportBug: 'Report a bug',
unhealthyVdiChainError: 'Job canceled to protect the VDI chain',
backupRestartVm: "Restart VM's backup",
backupForceRestartVm: "Force restart VM's backup",
backupRestartFailedVms: "Restart failed VMs' backup",
backupForceRestartFailedVms: "Force restart failed VMs' backup",
clickForMoreInformation: 'Click for more information',
// ----- IPs ------

View File

@@ -1,7 +1,11 @@
import _ from 'intl'
import PropTypes from 'prop-types'
import React from 'react'
import Select from 'form/select'
import { injectState, provideState } from 'reaclette'
import { omit } from 'lodash'
import decorate from './apply-decorators'
import { Select } from './form'
const OPTIONS = [
{
@@ -12,27 +16,40 @@ const OPTIONS = [
label: _('chooseCompressionGzipOption'),
value: 'native',
},
]
const OPTIONS_WITH_ZSTD = [
...OPTIONS,
{
label: _('chooseCompressionZstdOption'),
value: 'zstd',
},
]
const SelectCompression = ({ onChange, value, ...props }) => (
<Select
labelKey='label'
onChange={onChange}
options={OPTIONS}
required
simpleValue
value={value}
{...props}
/>
)
SelectCompression.propTypes = {
onChange: PropTypes.func.isRequired,
value: PropTypes.string,
const SELECT_COMPRESSION_PROP_TYPES = {
showZstd: PropTypes.bool,
}
const SelectCompression = decorate([
provideState({
computed: {
options: (_, { showZstd }) => (showZstd ? OPTIONS_WITH_ZSTD : OPTIONS),
selectProps: (_, props) =>
omit(props, Object.keys(SELECT_COMPRESSION_PROP_TYPES)),
},
}),
injectState,
({ onChange, state, value }) => (
<Select
labelKey='label'
options={state.options}
required
simpleValue
{...state.selectProps}
/>
),
])
SelectCompression.defaultProps = { showZstd: true }
SelectCompression.propTypes = SELECT_COMPRESSION_PROP_TYPES
export { SelectCompression as default }

View File

@@ -2,17 +2,20 @@ import _ from 'intl'
import classNames from 'classnames'
import PropTypes from 'prop-types'
import React, { cloneElement } from 'react'
import sum from 'lodash/sum'
import { compact, sum } from 'lodash'
import Tooltip from '../tooltip'
const Usage = ({ total, children }) => {
const limit = total / 400
const othersValues = React.Children.map(children, child => {
const { value } = child.props
return value < limit && value
})
const othersValues = compact(
React.Children.map(children, child => {
const { value } = child.props
return value < limit && value
})
)
const othersTotal = sum(othersValues)
const nOthers = othersValues.length
return (
<span className='usage'>
{React.Children.map(
@@ -20,7 +23,12 @@ const Usage = ({ total, children }) => {
(child, index) =>
child.props.value > limit && cloneElement(child, { total })
)}
<Element others tooltip={_('others')} total={total} value={othersTotal} />
<Element
others
tooltip={_('others', { nOthers })}
total={total}
value={othersTotal}
/>
</span>
)
}

View File

@@ -10,7 +10,7 @@ import {
createGetObjectsOfType,
createSelector,
} from 'selectors'
import { forEach } from 'lodash'
import { flatten, forEach, isEmpty, map, uniq } from 'lodash'
import { getPatchesDifference } from 'xo'
import { SelectHost } from 'select-objects'
@@ -26,7 +26,7 @@ import { SelectHost } from 'select-objects'
const { $pool } = host
if ($pool !== poolId) {
const previousHost = visitedPools[$pool]
if (previousHost) {
if (previousHost !== undefined) {
delete singleHosts[previousHost]
} else {
const { id } = host
@@ -41,17 +41,17 @@ import { SelectHost } from 'select-objects'
}),
{ withRef: true }
)
export default class AddHostModal extends BaseComponent {
export default class AddHostsModal extends BaseComponent {
get value() {
const { nHostMissingPatches, nPoolMissingPatches } = this.state
const { nHostsMissingPatches, nPoolMissingPatches } = this.state
if (
process.env.XOA_PLAN < 2 &&
(nHostMissingPatches > 0 || nPoolMissingPatches > 0)
(nHostsMissingPatches > 0 || nPoolMissingPatches > 0)
) {
return {}
}
return { host: this.state.host }
return { hosts: this.state.hosts }
}
_getHostPredicate = createSelector(
@@ -59,44 +59,61 @@ export default class AddHostModal extends BaseComponent {
singleHosts => host => singleHosts[host.id]
)
_onChangeHost = async host => {
if (host === null) {
_onChangeHosts = async hosts => {
if (isEmpty(hosts)) {
this.setState({
host,
nHostMissingPatches: undefined,
hosts,
nHostsMissingPatches: undefined,
nPoolMissingPatches: undefined,
})
return
}
const { master } = this.props.pool
const hostMissingPatches = await getPatchesDifference(host.id, master)
const poolMissingPatches = await getPatchesDifference(master, host.id)
this.setState({
host,
nHostMissingPatches: hostMissingPatches.length,
nPoolMissingPatches: poolMissingPatches.length,
hosts,
nHostsMissingPatches: uniq(
flatten(
await Promise.all(
map(hosts, ({ id: hostId }) => getPatchesDifference(hostId, master))
)
)
).length,
nPoolMissingPatches: uniq(
flatten(
await Promise.all(
map(hosts, ({ id: hostId }) => getPatchesDifference(master, hostId))
)
)
).length,
})
}
render() {
const { nHostMissingPatches, nPoolMissingPatches } = this.state
const { hosts, nHostsMissingPatches, nPoolMissingPatches } = this.state
const canMulti = +process.env.XOA_PLAN > 3
return (
<div>
<SingleLineRow>
<Col size={6}>{_('addHostSelectHost')}</Col>
<Col size={6}>{_('hosts')}</Col>
<Col size={6}>
<SelectHost
onChange={this._onChangeHost}
multi={canMulti}
onChange={
canMulti
? this._onChangeHosts
: host =>
this._onChangeHosts(host !== null ? [host] : undefined)
}
predicate={this._getHostPredicate()}
value={this.state.host}
value={
canMulti ? hosts : hosts !== undefined ? hosts[0] : undefined
}
/>
</Col>
</SingleLineRow>
<br />
{(nHostMissingPatches > 0 || nPoolMissingPatches > 0) && (
{(nHostsMissingPatches > 0 || nPoolMissingPatches > 0) && (
<div>
{process.env.XOA_PLAN > 1 ? (
<div>
@@ -112,13 +129,14 @@ export default class AddHostModal extends BaseComponent {
</Col>
</SingleLineRow>
)}
{nHostMissingPatches > 0 && (
{nHostsMissingPatches > 0 && (
<SingleLineRow>
<Col>
<span className='text-danger'>
<Icon icon='error' />{' '}
{_('missingPatchesHost', {
nMissingPatches: nHostMissingPatches,
nHosts: hosts.length,
nMissingPatches: nHostsMissingPatches,
})}
</span>
</Col>
@@ -126,7 +144,9 @@ export default class AddHostModal extends BaseComponent {
)}
</div>
) : (
_('patchUpdateNoInstall')
_('patchUpdateNoInstall', {
nHosts: hosts.length,
})
)}
</div>
)}

View File

@@ -1,14 +1,25 @@
import BaseComponent from 'base-component'
import React from 'react'
import Upgrade from 'xoa-upgrade'
import { injectIntl } from 'react-intl'
import _, { messages } from '../../intl'
import SelectCompression from '../../select-compression'
import SingleLineRow from '../../single-line-row'
import Upgrade from 'xoa-upgrade'
import { Col } from '../../grid'
import { connectStore } from '../../utils'
import { createGetObject, createSelector } from '../../selectors'
import { SelectSr } from '../../select-objects'
import { injectIntl } from 'react-intl'
@connectStore(
{
isZstdSupported: createSelector(
createGetObject((_, { vm }) => vm.$container),
container => container === undefined || container.zstdSupported
),
},
{ withRef: true }
)
class CopyVmModalBody extends BaseComponent {
state = {
compression: '',
@@ -27,7 +38,10 @@ class CopyVmModalBody extends BaseComponent {
}
render() {
const { formatMessage } = this.props.intl
const {
intl: { formatMessage },
isZstdSupported,
} = this.props
const { compression, copyMode, name, sr } = this.state
return process.env.XOA_PLAN > 2 ? (
@@ -81,6 +95,7 @@ class CopyVmModalBody extends BaseComponent {
<SelectCompression
disabled={copyMode !== 'fullCopy'}
onChange={this.linkState('compression')}
showZstd={isZstdSupported}
value={compression}
/>
</Col>

View File

@@ -1,10 +1,22 @@
import BaseComponent from 'base-component'
import PropTypes from 'prop-types'
import React from 'react'
import _ from '../../intl'
import SelectCompression from '../../select-compression'
import { connectStore } from '../../utils'
import { Container, Row, Col } from '../../grid'
import { createGetObject, createSelector } from '../../selectors'
@connectStore(
{
isZstdSupported: createSelector(
createGetObject((_, { vm }) => vm.$container),
container => container === undefined || container.zstdSupported
),
},
{ withRef: true }
)
export default class ExportVmModalBody extends BaseComponent {
state = {
compression: '',
@@ -25,6 +37,7 @@ export default class ExportVmModalBody extends BaseComponent {
<Col mediumSize={6}>
<SelectCompression
onChange={this.linkState('compression')}
showZstd={this.props.isZstdSupported}
value={this.state.compression}
/>
</Col>
@@ -33,3 +46,7 @@ export default class ExportVmModalBody extends BaseComponent {
)
}
}
ExportVmModalBody.propTypes = {
vm: PropTypes.object.isRequired,
}

View File

@@ -562,7 +562,6 @@ export const getPatchesDifference = (source, target) =>
target: resolveId(target),
})
import AddHostModalBody from './add-host-modal' // eslint-disable-line import/first
export const addHostToPool = (pool, host) => {
if (host) {
return confirm({
@@ -580,18 +579,23 @@ export const addHostToPool = (pool, host) => {
})
)
}
}
return confirm({
import AddHostsModalBody from './add-hosts-modal' // eslint-disable-line import/first
export const addHostsToPool = pool =>
confirm({
icon: 'add',
title: _('addHostModalTitle'),
body: <AddHostModalBody pool={pool} />,
title: _('addHostsLabel'),
body: <AddHostsModalBody pool={pool} />,
}).then(params => {
if (!params.host) {
const { hosts } = params
if (isEmpty(hosts)) {
error(_('addHostNoHost'), _('addHostNoHostMessage'))
return
}
return _call('pool.mergeInto', {
source: params.host.$pool,
sources: map(hosts, '$pool'),
target: pool.id,
force: true,
}).catch(error => {
@@ -599,10 +603,12 @@ export const addHostToPool = (pool, host) => {
throw error
}
error(_('addHostErrorTitle'), _('addHostNotHomogeneousErrorMessage'))
error(
_('addHostsErrorTitle', { nHosts: hosts.length }),
_('addHostNotHomogeneousErrorMessage')
)
})
}, noop)
}
})
export const detachHost = host =>
confirm({
@@ -1451,7 +1457,7 @@ export const importVms = (vms, sr) =>
import ExportVmModalBody from './export-vm-modal' // eslint-disable-line import/first
export const exportVm = vm =>
confirm({
body: <ExportVmModalBody />,
body: <ExportVmModalBody vm={vm} />,
icon: 'export',
title: _('exportVmLabel'),
}).then(compress => {
@@ -2000,7 +2006,16 @@ export const editBackupNgJob = props =>
export const getBackupNgJob = id => _call('backupNg.getJob', { id })
export const runBackupNgJob = params => _call('backupNg.runJob', params)
export const runBackupNgJob = ({ force, ...params }) => {
if (force) {
params.settings = {
'': {
bypassVdiChainsCheck: true,
},
}
}
return _call('backupNg.runJob', params)
}
export const listVmBackups = remotes =>
_call('backupNg.listVmBackups', { remotes: resolveIds(remotes) })

View File

@@ -140,6 +140,10 @@
@extend .fa;
@extend .fa-play;
}
&-force-restart {
@extend .fa;
@extend .fa-forward;
}
&-ssh-key {
@extend .fa;
@extend .fa-key;
@@ -324,27 +328,27 @@
&-running {
@extend .fa;
@extend .fa-desktop;
@extend .text-success;
@extend .xo-status-running;
}
&-suspended {
@extend .fa;
@extend .fa-desktop;
@extend .text-primary;
@extend .xo-status-suspended;
}
&-paused {
@extend .fa;
@extend .fa-desktop;
@extend .text-muted;
@extend .xo-status-paused;
}
&-halted {
@extend .fa;
@extend .fa-desktop;
@extend .text-danger;
@extend .xo-status-halted;
}
&-busy {
@extend .fa;
@extend .fa-desktop;
@extend .text-warning;
@extend .xo-status-busy;
}
// Actions
@@ -453,7 +457,7 @@
&-disabled {
@extend .fa;
@extend .fa-circle;
@extend .xo-status-busy;
@extend .xo-status-disabled;
}
&-all-connected {
@@ -526,27 +530,27 @@
&-running {
@extend .fa;
@extend .fa-server;
@extend .text-success;
@extend .xo-status-running;
}
&-halted {
@extend .fa;
@extend .fa-server;
@extend .text-danger;
@extend .xo-status-halted;
}
&-disabled {
@extend .fa;
@extend .fa-server;
@extend .text-warning;
@extend .xo-status-disabled;
}
&-busy {
@extend .fa;
@extend .fa-server;
@extend .xo-status-busy;
}
&-forget {
@extend .fa;
@extend .fa-ban;
}
&-working {
@extend .fa;
@extend .fa-circle;
@extend .text-warning;
}
// Actions
&-enable {
@@ -728,6 +732,11 @@
@extend .fa-sign-out;
}
&-multiline-clipboard {
@extend .fa;
@extend .fa-file-text-o;
}
// Menu
&-menu-collapse {
@extend .fa;

View File

@@ -109,7 +109,7 @@ $select-input-height: 40px; // Bootstrap input height
@extend .text-info;
}
.xo-status-unknown, .xo-status-paused {
.xo-status-unknown, .xo-status-paused, .xo-status-disabled {
@extend .text-muted;
}
@@ -213,6 +213,12 @@ $select-input-height: 40px; // Bootstrap input height
border: 1px solid black;
margin: 5px 10px;
width: 250px;
// Workaround to prevent some bootstrap elements from hiding the notifications.
// In bootstrap, ".input-group .form-control" and ".input-group > .input-group-btn > .btn"
// have "z-index: 2" and "z-index: 3" if they are hovered, focused or active.
// (https://github.com/twbs/bootstrap/blob/v4.0.0-alpha.5/scss/_input-group.scss#L18-L37)
// (https://github.com/twbs/bootstrap/blob/v4.0.0-alpha.5/scss/_input-group.scss#L177-L187)
z-index: 3;
&.success {
background: $alert-success-bg;
border-color: $alert-success-border;

View File

@@ -22,7 +22,7 @@
}
.usage-element-others {
background-color: $brand-info;
background-color: #5cb85c75;
}
.usage-element:hover {

View File

@@ -186,20 +186,40 @@ export default decorate([
}
}
await createBackupNgJob({
name: state.name,
mode: state.isDelta ? 'delta' : 'full',
compression: state.compression,
schedules: mapValues(
let schedules, settings
if (!isEmpty(state.schedules)) {
schedules = mapValues(
state.schedules,
({ id, ...schedule }) => schedule
),
settings: normalizeSettings({
)
settings = normalizeSettings({
settings: state.settings,
exportMode: state.exportMode,
copyMode: state.copyMode,
snapshotMode: state.snapshotMode,
}).toObject(),
}).toObject()
} else {
const id = generateId()
schedules = {
[id]: DEFAULT_SCHEDULE,
}
settings = {
[id]: {
copyRetention: state.copyMode ? DEFAULT_RETENTION : undefined,
exportRetention: state.exportMode ? DEFAULT_RETENTION : undefined,
snapshotRetention: state.snapshotMode
? DEFAULT_RETENTION
: undefined,
},
}
}
await createBackupNgJob({
name: state.name,
mode: state.isDelta ? 'delta' : 'full',
compression: state.compression,
schedules,
settings,
remotes:
state.deltaMode || state.backupMode
? constructPattern(state.remotes)
@@ -375,7 +395,7 @@ export default decorate([
{ saveSchedule },
storedSchedule = DEFAULT_SCHEDULE
) => async (
{ copyMode, exportMode, snapshotMode },
{ copyMode, exportMode, missingBackupMode, snapshotMode },
{ intl: { formatMessage } }
) => {
const schedule = await form({
@@ -395,6 +415,7 @@ export default decorate([
handler: value => {
if (
!(
missingBackupMode ||
(exportMode && value.exportRetention > 0) ||
(copyMode && value.copyRetention > 0) ||
(snapshotMode && value.snapshotRetention > 0)
@@ -557,13 +578,16 @@ export default decorate([
missingRemotes: state =>
(state.backupMode || state.deltaMode) && isEmpty(state.remotes),
missingSrs: state => (state.drMode || state.crMode) && isEmpty(state.srs),
missingSchedules: state => isEmpty(state.schedules),
missingExportRetention: state =>
state.exportMode && !state.exportRetentionExists,
missingCopyRetention: state =>
state.copyMode && !state.copyRetentionExists,
missingSnapshotRetention: state =>
state.snapshotMode && !state.snapshotRetentionExists,
missingSchedules: (state, { job }) =>
job !== undefined && isEmpty(state.schedules),
missingExportRetention: (state, { job }) =>
job !== undefined && state.exportMode && !state.exportRetentionExists,
missingCopyRetention: (state, { job }) =>
job !== undefined && state.copyMode && !state.copyRetentionExists,
missingSnapshotRetention: (state, { job }) =>
job !== undefined &&
state.snapshotMode &&
!state.snapshotRetentionExists,
exportMode: state => state.backupMode || state.deltaMode,
copyMode: state => state.drMode || state.crMode,
exportRetentionExists: createDoesRetentionExist('exportRetention'),

View File

@@ -6,7 +6,7 @@ import SortedTable from 'sorted-table'
import StateButton from 'state-button'
import { Card, CardBlock, CardHeader } from 'card'
import { injectState, provideState } from 'reaclette'
import { isEmpty, find, size } from 'lodash'
import { isEmpty, find } from 'lodash'
import { FormFeedback } from './../utils'
@@ -23,10 +23,9 @@ export default decorate([
injectState,
provideState({
computed: {
disabledDeletion: state => size(state.schedules) <= 1,
error: state => find(FEEDBACK_ERRORS, error => state[error]),
individualActions: (
{ disabledDeletion, disabledEdition },
{ disabledEdition },
{ effects: { deleteSchedule, showScheduleModal } }
) => [
{
@@ -37,7 +36,6 @@ export default decorate([
level: 'primary',
},
{
disabled: disabledDeletion,
handler: deleteSchedule,
icon: 'delete',
label: _('scheduleDelete'),

View File

@@ -65,12 +65,22 @@ export default class HostItem extends Component {
_toggleExpanded = () => this.setState({ expanded: !this.state.expanded })
_onSelect = () => this.props.onSelect(this.props.item.id)
_getHostState = createSelector(
() => this.props.item.power_state,
() => this.props.item.enabled,
() => this.props.item.current_operations,
(powerState, enabled, operations) =>
!isEmpty(operations)
? 'Busy'
: powerState === 'Running' && !enabled
? 'Disabled'
: powerState
)
render() {
const { item: host, container, expandAll, selected, nVms } = this.props
const toolTipContent =
host.power_state === `Running` && !host.enabled
? `disabled`
: _(`powerState${host.power_state}`)
const state = this._getHostState()
return (
<div className={styles.item}>
<BlockLink to={`/hosts/${host.id}`}>
@@ -86,25 +96,19 @@ export default class HostItem extends Component {
&nbsp;&nbsp;
<Tooltip
content={
isEmpty(host.current_operations) ? (
toolTipContent
) : (
<div>
{toolTipContent}
{' ('}
{map(host.current_operations)[0]}
{')'}
</div>
)
<span>
{_(`powerState${state}`)}
{state === 'Busy' && (
<span>
{' ('}
{map(host.current_operations)[0]}
{')'}
</span>
)}
</span>
}
>
{!isEmpty(host.current_operations) ? (
<Icon icon='busy' />
) : host.power_state === 'Running' && !host.enabled ? (
<Icon icon='disabled' />
) : (
<Icon icon={`${host.power_state.toLowerCase()}`} />
)}
<Icon icon={state.toLowerCase()} />
</Tooltip>
&nbsp;&nbsp;
<Ellipsis>

View File

@@ -80,9 +80,16 @@ export default class VmItem extends Component {
_toggleExpanded = () => this.setState({ expanded: !this.state.expanded })
_onSelect = () => this.props.onSelect(this.props.item.id)
_getVmState = createSelector(
() => this.props.item.power_state,
() => this.props.item.current_operations,
(powerState, operations) => (!isEmpty(operations) ? 'Busy' : powerState)
)
render() {
const { item: vm, container, expandAll, selected } = this.props
const resourceSet = this._getResourceSet()
const state = this._getVmState()
return (
<div className={styles.item}>
@@ -99,23 +106,19 @@ export default class VmItem extends Component {
&nbsp;&nbsp;
<Tooltip
content={
isEmpty(vm.current_operations) ? (
_(`powerState${vm.power_state}`)
) : (
<div>
{_(`powerState${vm.power_state}`)}
{' ('}
{map(vm.current_operations)[0]}
{')'}
</div>
)
<span>
{_(`powerState${state}`)}
{state === 'Busy' && (
<span>
{' ('}
{map(vm.current_operations)[0]}
{')'}
</span>
)}
</span>
}
>
{isEmpty(vm.current_operations) ? (
<Icon icon={`${vm.power_state.toLowerCase()}`} />
) : (
<Icon icon='busy' />
)}
<Icon icon={state.toLowerCase()} />
</Tooltip>
&nbsp;&nbsp;
<Ellipsis>

View File

@@ -228,9 +228,22 @@ export default class Host extends Component {
_setNameLabel = nameLabel =>
editHost(this.props.host, { name_label: nameLabel })
_getHostState = createSelector(
() => this.props.host.power_state,
() => this.props.host.enabled,
() => this.props.host.current_operations,
(powerState, enabled, operations) =>
!isEmpty(operations)
? 'Busy'
: powerState === 'Running' && !enabled
? 'Disabled'
: powerState
)
header() {
const { host, pool } = this.props
const { missingPatches } = this.state || {}
const state = this._getHostState()
if (!host) {
return <Icon icon='loading' />
}
@@ -240,13 +253,22 @@ export default class Host extends Component {
<Col mediumSize={6} className='header-title'>
{pool !== undefined && <Pool id={pool.id} link />}
<h2>
<Icon
icon={
host.power_state === 'Running' && !host.enabled
? 'host-disabled'
: `host-${host.power_state.toLowerCase()}`
<Tooltip
content={
<span>
{_(`powerState${state}`)}
{state === 'Busy' && (
<span>
{' ('}
{map(host.current_operations)[0]}
{')'}
</span>
)}
</span>
}
/>{' '}
>
<Icon icon={`host-${state.toLowerCase()}`} />
</Tooltip>{' '}
<Text value={host.name_label} onChange={this._setNameLabel} />
{this.props.needsRestart && (
<Tooltip content={_('rebootUpdateHostLabel')}>

View File

@@ -2,6 +2,7 @@ import _ from 'intl'
import Component from 'base-component'
import Copiable from 'copiable'
import decorate from 'apply-decorators'
import Icon from 'icon'
import PropTypes from 'prop-types'
import React from 'react'
import SelectFiles from 'select-files'
@@ -9,6 +10,7 @@ import StateButton from 'state-button'
import TabButton from 'tab-button'
import Upgrade from 'xoa-upgrade'
import { compareVersions, connectStore, getIscsiPaths } from 'utils'
import { confirm } from 'modal'
import { Container, Row, Col } from 'grid'
import { createGetObjectsOfType, createSelector } from 'selectors'
import { forEach, map, noop, isEmpty } from 'lodash'
@@ -19,6 +21,7 @@ import { Toggle } from 'form'
import {
detachHost,
disableHost,
editHost,
enableHost,
forgetHost,
isHyperThreadingEnabledHost,
@@ -121,6 +124,20 @@ export default class extends Component {
}
)
_setHostIscsiIqn = iscsiIqn =>
confirm({
icon: 'alarm',
title: _('editHostIscsiIqnTitle'),
body: (
<div>
<p>{_('editHostIscsiIqnMessage')}</p>
<p className='text-muted'>
<Icon icon='info' /> {_('uniqueHostIscsiIqnInfo')}
</p>
</div>
),
}).then(() => editHost(this.props.host, { iscsiIqn }), noop)
_setRemoteSyslogHost = value => setRemoteSyslogHost(this.props.host, value)
render() {
@@ -231,8 +248,13 @@ export default class extends Component {
<Copiable tagName='td'>{host.build}</Copiable>
</tr>
<tr>
<th>{_('hostIscsiName')}</th>
<Copiable tagName='td'>{host.iSCSI_name}</Copiable>
<th>{_('hostIscsiIqn')}</th>
<td>
<Text
onChange={this._setHostIscsiIqn}
value={host.iscsiIqn}
/>
</td>
</tr>
<tr>
<th>{_('multipathing')}</th>

View File

@@ -11,7 +11,7 @@ import Shortcuts from 'shortcuts'
import themes from 'themes'
import _, { IntlProvider } from 'intl'
import { blockXoaAccess } from 'xoa-updater'
import { connectStore, routes } from 'utils'
import { connectStore, getXoaPlan, routes } from 'utils'
import { Notification } from 'notification'
import { ShortcutManager } from 'react-shortcuts'
import { ThemeProvider } from 'styled-components'
@@ -132,6 +132,8 @@ export default class XoApp extends Component {
}
}
dismissSourceBanner = () => this.setState({ dismissedSourceBanner: true })
componentDidMount() {
this.refs.bodyWrapper.style.minHeight =
this.refs.menu.getWrappedInstance().height + 'px'
@@ -201,13 +203,14 @@ export default class XoApp extends Component {
render() {
const { signedUp, trial, registerNeeded } = this.props
const blocked = signedUp && blockXoaAccess(trial) // If we are under expired or unstable trial (signed up only)
const plan = getXoaPlan()
return (
<IntlProvider>
<ThemeProvider theme={themes.base}>
<DocumentTitle title='Xen Orchestra'>
<div>
{process.env.XOA_PLAN < 5 && registerNeeded && (
{plan !== 'Community' && registerNeeded && (
<div className='alert alert-danger mb-0'>
{_('notRegisteredDisclaimerInfo')}{' '}
<a
@@ -222,7 +225,7 @@ export default class XoApp extends Component {
</Link>
</div>
)}
{+process.env.XOA_PLAN === 5 && (
{plan === 'Community' && !this.state.dismissedSourceBanner && (
<div className='alert alert-danger mb-0'>
<a
href='https://xen-orchestra.com/#!/xoa?pk_campaign=xo_source_banner'
@@ -231,6 +234,9 @@ export default class XoApp extends Component {
>
{_('disclaimerText3')}
</a>
<button className='close' onClick={this.dismissSourceBanner}>
&times;
</button>
</div>
)}
<div style={CONTAINER_STYLE}>

View File

@@ -1,5 +1,6 @@
import _, { FormattedDuration } from 'intl'
import ActionButton from 'action-button'
import ButtonGroup from 'button-group'
import decorate from 'apply-decorators'
import defined, { get } from '@xen-orchestra/defined'
import Icon from 'icon'
@@ -136,13 +137,24 @@ const VmTask = ({ children, restartVmJob, task }) => (
<div>
<Vm id={task.data.id} link newTab /> <TaskStateInfos status={task.status} />{' '}
{restartVmJob !== undefined && hasTaskFailed(task) && (
<ActionButton
handler={restartVmJob}
icon='run'
size='small'
tooltip={_('backupRestartVm')}
data-vm={task.data.id}
/>
<ButtonGroup>
<ActionButton
data-vm={task.data.id}
handler={restartVmJob}
icon='run'
size='small'
tooltip={_('backupRestartVm')}
/>
<ActionButton
btnStyle='warning'
data-force
data-vm={task.data.id}
handler={restartVmJob}
icon='force-restart'
size='small'
tooltip={_('backupForceRestartVm')}
/>
</ButtonGroup>
)}
<Warnings warnings={task.warnings} />
{children}
@@ -316,14 +328,15 @@ export default decorate([
setFilter: (_, filter) => () => ({
filter,
}),
restartVmJob: (_, { vm }) => async (
restartVmJob: (_, params) => async (
_,
{ log: { scheduleId, jobId } }
) => {
await runBackupNgJob({
force: get(() => params.force),
id: jobId,
vm,
schedule: scheduleId,
vm: get(() => params.vm),
})
},
},

View File

@@ -31,7 +31,7 @@ export default decorate([
effects: {
_downloadLog: () => ({ formattedLog }, { log }) =>
downloadLog({ log: formattedLog, date: log.start, type: 'backup NG' }),
restartFailedVms: () => async (
restartFailedVms: (_, params) => async (
_,
{ log: { jobId: id, scheduleId: schedule, tasks, infos } }
) => {
@@ -54,8 +54,8 @@ export default decorate([
})
}
}
await runBackupNgJob({
force: get(() => params.force),
id,
schedule,
vms,
@@ -97,12 +97,22 @@ export default decorate([
/>
)}
{state.jobFailed && log.scheduleId !== undefined && (
<ActionButton
handler={effects.restartFailedVms}
icon='run'
size='small'
tooltip={_('backupRestartFailedVms')}
/>
<ButtonGroup>
<ActionButton
handler={effects.restartFailedVms}
icon='run'
size='small'
tooltip={_('backupRestartFailedVms')}
/>
<ActionButton
btnStyle='warning'
data-force
handler={effects.restartFailedVms}
icon='force-restart'
size='small'
tooltip={_('backupForceRestartFailedVms')}
/>
</ButtonGroup>
)}
</ButtonGroup>
</span>

View File

@@ -84,6 +84,10 @@ const NewNetwork = decorate([
: [],
pifPredicate: (_, { pool }) => pif =>
pif.vlan === -1 && pif.$host === (pool && pool.master),
pifPredicateSdnController: (_, { pool }) => pif =>
pif.physical &&
pif.ip_configuration_mode !== 'None' &&
pif.$host === (pool && pool.master),
isSdnControllerLoaded: (state, { plugins = [] }) =>
plugins.some(
plugin => plugin.name === 'sdn-controller' && plugin.loaded
@@ -126,6 +130,7 @@ const NewNetwork = decorate([
networkName: name,
networkDescription: description,
encapsulation: encapsulation,
pifId: pif.id,
})
: createNetwork({
description,
@@ -179,6 +184,7 @@ const NewNetwork = decorate([
name,
pif,
pifPredicate,
pifPredicateSdnController,
pifs,
vlan,
isSdnControllerLoaded,
@@ -204,102 +210,89 @@ const NewNetwork = decorate([
</div>
</Section>
<Section icon='info' title='newNetworkInfo'>
{isPrivate ? (
<div className='form-group'>
<label>{_('newNetworkName')}</label>
<input
className='form-control'
name='name'
onChange={effects.linkState}
required
type='text'
value={name}
/>
<label>{_('newNetworkDescription')}</label>
<input
className='form-control'
name='description'
onChange={effects.linkState}
type='text'
value={description}
/>
<label>{_('newNetworkEncapsulation')}</label>
<Select
className='form-control'
name='encapsulation'
onChange={effects.onChangeEncapsulation}
options={[
{ label: 'GRE', value: 'gre' },
{ label: 'VxLAN', value: 'vxlan' },
]}
value={encapsulation}
/>
</div>
) : (
<div className='form-group'>
<label>{_('newNetworkInterface')}</label>
<SelectPif
multi={bonded}
onChange={effects.onChangePif}
predicate={pifPredicate}
required={bonded}
value={bonded ? pifs : pif}
/>
<label>{_('newNetworkName')}</label>
<input
className='form-control'
name='name'
onChange={effects.linkState}
required
type='text'
value={name}
/>
<label>{_('newNetworkDescription')}</label>
<input
className='form-control'
name='description'
onChange={effects.linkState}
type='text'
value={description}
/>
<label>{_('newNetworkMtu')}</label>
<input
className='form-control'
name='mtu'
onChange={effects.linkState}
placeholder={formatMessage(
messages.newNetworkDefaultMtu
<div className='form-group'>
<label>{_('newNetworkInterface')}</label>
<SelectPif
multi={bonded}
onChange={effects.onChangePif}
predicate={
isPrivate ? pifPredicateSdnController : pifPredicate
}
required={bonded || isPrivate}
value={bonded ? pifs : pif}
/>
<label>{_('newNetworkName')}</label>
<input
className='form-control'
name='name'
onChange={effects.linkState}
required
type='text'
value={name}
/>
<label>{_('newNetworkDescription')}</label>
<input
className='form-control'
name='description'
onChange={effects.linkState}
type='text'
value={description}
/>
{isPrivate ? (
<div>
<label>{_('newNetworkEncapsulation')}</label>
<Select
className='form-control'
name='encapsulation'
onChange={effects.onChangeEncapsulation}
options={[
{ label: 'GRE', value: 'gre' },
{ label: 'VxLAN', value: 'vxlan' },
]}
value={encapsulation}
/>
</div>
) : (
<div>
<label>{_('newNetworkMtu')}</label>
<input
className='form-control'
name='mtu'
onChange={effects.linkState}
placeholder={formatMessage(
messages.newNetworkDefaultMtu
)}
type='text'
value={mtu}
/>
{bonded ? (
<div>
<label>{_('newNetworkBondMode')}</label>
<Select
onChange={effects.onChangeMode}
options={modeOptions}
required
value={bondMode}
/>
</div>
) : (
<div>
<label>{_('newNetworkVlan')}</label>
<input
className='form-control'
name='vlan'
onChange={effects.linkState}
placeholder={formatMessage(
messages.newNetworkDefaultVlan
)}
type='text'
value={vlan}
/>
</div>
)}
type='text'
value={mtu}
/>
{bonded ? (
<div>
<label>{_('newNetworkBondMode')}</label>
<Select
onChange={effects.onChangeMode}
options={modeOptions}
required
value={bondMode}
/>
</div>
) : (
<div>
<label>{_('newNetworkVlan')}</label>
<input
className='form-control'
name='vlan'
onChange={effects.linkState}
placeholder={formatMessage(
messages.newNetworkDefaultVlan
)}
type='text'
value={vlan}
/>
</div>
)}
</div>
)}
</div>
)}
</div>
</Section>
</Wizard>
<div className='form-group pull-right'>

View File

@@ -5,7 +5,7 @@ import React from 'react'
import { createGetObjectsOfType, createSelector } from 'selectors'
import { find } from 'lodash'
import { addSubscriptions, connectStore, noop } from 'utils'
import { addHostToPool, disconnectServer, subscribeServers } from 'xo'
import { addHostsToPool, disconnectServer, subscribeServers } from 'xo'
@connectStore({
hosts: createGetObjectsOfType('host'),
@@ -50,9 +50,9 @@ export default class PoolActionBar extends Component {
redirectOnSuccess={`vms/new?pool=${pool.id}`}
/>
<Action
handler={addHostToPool}
handler={addHostsToPool}
icon='add-host'
label={_('addHostLabel')}
label={_('addHostsLabel')}
/>
<Action
handler={this._disconnectServer}

View File

@@ -63,7 +63,7 @@ const UsageTooltip = decorate([
snapshotsUsage: (_, { group: { snapshots } }) =>
formatSize(sumBy(snapshots, 'usage')),
vmNamesByVdi: createCollectionWrapper(({ vdis }, { vbds, vms }) =>
mapValues(vdis, vdi => get(() => vms[vbds[vdi.VBD].VM]))
mapValues(vdis, vdi => get(() => vms[vbds[vdi.$VBDs[0]].VM].name_label))
),
},
}),
@@ -183,7 +183,7 @@ export default class TabGeneral extends Component {
)
}
// search root base copy for each VDI
const vdisInfo = vdis.map(({ id, parent, name_label, usage }) => {
const vdisInfo = vdis.map(({ id, parent, name_label, usage, $VBDs }) => {
const baseCopies = new Set()
let baseCopy
let root = id
@@ -212,6 +212,7 @@ export default class TabGeneral extends Component {
root,
snapshots: snapshots === undefined ? [] : snapshots,
usage,
$VBDs,
}
})
// group VDIs by their root base copy.

View File

@@ -168,14 +168,19 @@ export default class Vm extends BaseComponent {
_setNameLabel = nameLabel => editVm(this.props.vm, { name_label: nameLabel })
_migrateVm = host => migrateVm(this.props.vm, host)
_getVmState = createSelector(
() => this.props.vm.power_state,
() => this.props.vm.current_operations,
(powerState, operations) => (!isEmpty(operations) ? 'Busy' : powerState)
)
header() {
const { vm, container, pool } = this.props
if (!vm) {
return <Icon icon='loading' />
}
const state = isEmpty(vm.current_operations)
? vm.power_state.toLowerCase()
: 'busy'
const state = this._getVmState()
return (
<Container>
<Row>
@@ -207,8 +212,21 @@ export default class Vm extends BaseComponent {
</span>
</span>
<h2>
<Tooltip content={state}>
<Icon icon={`vm-${state}`} />
<Tooltip
content={
<span>
{_(`powerState${state}`)}
{state === 'Busy' && (
<span>
{' ('}
{map(vm.current_operations)[0]}
{')'}
</span>
)}
</span>
}
>
<Icon icon={`vm-${state.toLowerCase()}`} />
</Tooltip>{' '}
<Text value={vm.name_label} onChange={this._setNameLabel} />
</h2>{' '}

View File

@@ -1,16 +1,19 @@
import _ from 'intl'
import ActionButton from 'action-button'
import Button from 'button'
import Component from 'base-component'
import CopyToClipboard from 'react-copy-to-clipboard'
import debounce from 'lodash/debounce'
import getEventValue from 'get-event-value'
import Icon from 'icon'
import invoke from 'invoke'
import IsoDevice from 'iso-device'
import NoVnc from 'react-novnc'
import React from 'react'
import Tooltip from 'tooltip'
import { resolveUrl, isVmRunning } from 'xo'
import { Container, Row, Col } from 'grid'
import { isVmRunning, resolveUrl } from 'xo'
import { Col, Container, Row } from 'grid'
import { confirm } from 'modal'
import {
CpuSparkLines,
MemorySparkLines,
@@ -18,8 +21,36 @@ import {
XvdSparkLines,
} from 'xo-sparklines'
class SendToClipboard extends Component {
state = { value: this.props.clipboard }
get value() {
return this.state.value
}
_selectContent = ref => {
if (ref !== null) {
ref.select()
}
}
render() {
return (
<div>
<textarea
className='form-control'
onChange={this.linkState('value')}
ref={this._selectContent}
rows={10}
value={this.state.value}
/>
</div>
)
}
}
export default class TabConsole extends Component {
state = { scale: 1 }
state = { clipboard: '', scale: 1 }
componentWillReceiveProps(props) {
if (
@@ -36,17 +67,24 @@ export default class TabConsole extends Component {
_getRemoteClipboard = clipboard => {
this.setState({ clipboard })
this.refs.clipboard.value = clipboard
}
_setRemoteClipboard = invoke(() => {
const setRemoteClipboard = debounce(value => {
this.setState({ clipboard: value })
this.refs.noVnc.setClipboard(value)
}, 200)
return event => setRemoteClipboard(event.target.value)
return event => setRemoteClipboard(getEventValue(event))
})
_getClipboardContent = () => this.refs.clipboard && this.refs.clipboard.value
_openClipboardModal = async () =>
this._setRemoteClipboard(
await confirm({
icon: 'multiline-clipboard',
title: _('sendToClipboard'),
body: <SendToClipboard clipboard={this.state.clipboard} />,
})
)
_toggleMinimalLayout = () => {
this.props.toggleHeader()
@@ -101,14 +139,21 @@ export default class TabConsole extends Component {
</Col>
<Col mediumSize={3}>
<div className='input-group'>
<span className='input-group-btn'>
<ActionButton
handler={this._openClipboardModal}
icon='multiline-clipboard'
tooltip={_('multilineCopyToClipboard')}
/>
</span>
<input
type='text'
className='form-control'
ref='clipboard'
onChange={this._setRemoteClipboard}
type='text'
value={this.state.clipboard}
/>
<span className='input-group-btn'>
<CopyToClipboard text={this.state.clipboard || ''}>
<CopyToClipboard text={this.state.clipboard}>
<Button>
<Icon icon='clipboard' /> {_('copyToClipboardLabel')}
</Button>

View File

@@ -78,6 +78,6 @@ do
move_"$move" "$pkg" "packages/$pkg"
# merge and delete master branch
git merge --allow-unrelated-histories "$pkg/master"
git merge --no-ff --allow-unrelated-histories "$pkg/master"
git branch -d "$pkg/master"
done

View File

@@ -32,7 +32,9 @@ require('exec-promise')(() =>
pkg.version = '0.0.0'
}
delete pkg.husky
delete pkg.standard
delete pkg['lint-staged']
deleteProperties(pkg, 'config', ['commitizen'])
deleteProperties(pkg, 'devDependencies', [
@@ -43,6 +45,7 @@ require('exec-promise')(() =>
'cz-conventional-changelog',
'dependency-check',
'eslint',
'eslint-config-prettier',
'eslint-config-standard',
'eslint-plugin-import',
'eslint-plugin-node',

View File

@@ -26,7 +26,7 @@
dependencies:
"@babel/highlight" "^7.0.0"
"@babel/core@^7.0.0", "@babel/core@^7.1.0", "@babel/core@^7.1.5", "@babel/core@^7.4.4":
"@babel/core@^7.0.0", "@babel/core@^7.1.0", "@babel/core@^7.1.5", "@babel/core@^7.1.6", "@babel/core@^7.4.4":
version "7.5.4"
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.5.4.tgz#4c32df7ad5a58e9ea27ad025c11276324e0b4ddd"
integrity sha512-+DaeBEpYq6b2+ZmHx3tHspC+ZRflrvLqwfv8E3hNr5LVQoyBnL8RPKSBCg+rK2W2My9PWlujBiqd0ZPsR9Q6zQ==
@@ -264,7 +264,7 @@
"@babel/helper-create-class-features-plugin" "^7.5.0"
"@babel/helper-plugin-utils" "^7.0.0"
"@babel/plugin-proposal-decorators@^7.0.0", "@babel/plugin-proposal-decorators@^7.1.6":
"@babel/plugin-proposal-decorators@^7.0.0", "@babel/plugin-proposal-decorators@^7.1.6", "@babel/plugin-proposal-decorators@^7.4.0":
version "7.4.4"
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.4.4.tgz#de9b2a1a8ab0196f378e2a82f10b6e2a36f21cc0"
integrity sha512-z7MpQz3XC/iQJWXH9y+MaWcLPNSMY9RQSthrLzak8R8hCj0fuyNk+Dzi9kfNe/JxxlWQ2g7wkABbgWjW36MTcw==
@@ -730,7 +730,7 @@
core-js "^2.6.5"
regenerator-runtime "^0.13.2"
"@babel/preset-env@^7.0.0", "@babel/preset-env@^7.1.5", "@babel/preset-env@^7.4.4":
"@babel/preset-env@^7.0.0", "@babel/preset-env@^7.1.5", "@babel/preset-env@^7.1.6", "@babel/preset-env@^7.4.4":
version "7.5.4"
resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.5.4.tgz#64bc15041a3cbb0798930319917e70fcca57713d"
integrity sha512-hFnFnouyRNiH1rL8YkX1ANCNAUVC8Djwdqfev8i1415tnAG+7hlA5zhZ0Q/3Q5gkop4HioIPbCEWAalqcbxRoQ==
@@ -8164,7 +8164,7 @@ jest-worker@^24.0.0, jest-worker@^24.6.0:
merge-stream "^1.0.1"
supports-color "^6.1.0"
jest@^24.1.0:
jest@^24.1.0, jest@^24.8.0:
version "24.8.0"
resolved "https://registry.yarnpkg.com/jest/-/jest-24.8.0.tgz#d5dff1984d0d1002196e9b7f12f75af1b2809081"
integrity sha512-o0HM90RKFRNWmAWvlyV8i5jGZ97pFwkeVoGvPW1EtLTgJc2+jcuqcbbqcSZLE/3f2S5pt0y2ZBETuhpWNl1Reg==