Compare commits

...

83 Commits

Author SHA1 Message Date
Mohamedox
d2951f617b fix label id 2019-06-18 11:34:31 +02:00
Mohamedox
0ea64bdca7 remove nfs version 3 2019-06-18 11:34:31 +02:00
Mohamedox
f06bee3737 fix 2019-06-18 11:34:30 +02:00
Mohamedox
2693598ac8 change key nfs label name 2019-06-18 11:34:30 +02:00
Mohamedox
19011ad372 change nfs label name 2019-06-18 11:34:29 +02:00
Mohamedox
86eb7744a1 fix 2019-06-18 11:34:29 +02:00
Mohamedox
fe13ef6ff9 fix 2019-06-18 11:34:28 +02:00
Mohamedox
5607d34719 Fix select
Fixes #3951
2019-06-18 11:34:27 +02:00
Mohamedox
4501018dd6 update changelog 2019-06-18 11:34:27 +02:00
Mohamedox
9be9007fde fix 2019-06-18 11:34:06 +02:00
Enishowk
2b4443f333 feat(xo-web/backup-ng/restore): display size for full VM backup (#4245)
Fixes #4009
2019-06-18 09:55:14 +02:00
Julien Fontanet
ab6548122f Revert "fix(xo-server/metadata-backups): missing pool/remote should emit failure tasks"
This reverts commit f81573d999.
2019-06-17 14:36:49 +02:00
badrAZ
f81573d999 fix(xo-server/metadata-backups): missing pool/remote should emit failure tasks
See #4281
2019-06-17 11:29:56 +02:00
Julien Fontanet
84ccebb858 fix(xo-server/vm.create): accept extra params
Fixes #4280
2019-06-15 23:37:46 +02:00
badrAZ
530bc50e7c chore(CHANGELOG): update next 2019-06-13 14:57:22 +02:00
badrAZ
57e490fc23 feat(xo-web): 5.43.0 2019-06-13 14:11:21 +02:00
badrAZ
61e902c094 feat(xo-server): 5.43.0 2019-06-13 14:11:08 +02:00
badrAZ
8378ba77d6 feat(xen-api): 0.25.2 2019-06-13 14:09:59 +02:00
Julien Fontanet
c9e30b74e2 fix(xo-server/api): only change id to namespace if necessary 2019-06-13 12:00:10 +02:00
HamadaBrest
af944fd2e3 feat(xo-server,xo-web): ability to set HVM boot firmware (#4268)
Fixes #4264
2019-06-12 17:41:58 +02:00
Nicolas Raynaud
bcc0e76f1d feat(xo-server/api): sr.probZfs and sr.createFile (#4258)
Server side of #4260

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

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

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

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

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

Unfortunately the result can be embedded into XML, you should either not use the result or add `.then(extractOpaquerRef)` if you are expecting an opaque ref.
2019-05-21 15:04:24 +02:00
Dustin B
c7c6e0e2ff chore(xo-web/messages): one shot job → onetime job (#4222) 2019-05-21 11:30:42 +02:00
Julien Fontanet
1e529c995a chore(xo-server/vm.reboot): move into Xapi#rebootVm 2019-05-21 10:37:56 +02:00
Julien Fontanet
7be1c7a47b fix(xo-server): always use Xapi#callAsync for migration 2019-05-21 10:24:14 +02:00
HamadaBrest
b17380443b chore(@xen-orchestra/fs): test truncate() (#4225)
Related to #4180
2019-05-21 10:20:31 +02:00
Pierre Donias
59e68682bd fix(xo-web): lock admin pages (#4220)
Related to xoa-support#1460
2019-05-20 17:31:26 +02:00
Julien Fontanet
b7a92cfe92 feat(xo-server/vif.set): rateLimit support
Server-side of #4215
2019-05-20 16:06:42 +02:00
HamadaBrest
5ebe27da49 feat(fs): add truncate method (#4180) 2019-05-20 14:03:54 +02:00
Julien Fontanet
42df6ba6fa chore: update dependencies 2019-05-17 16:31:39 +02:00
Pierre Donias
8210fddfab fix(xo-web/charts): ensure consistent series order (#4221)
Fixes support#1481
2019-05-16 13:54:27 +02:00
Pierre Donias
f55ed273c5 chore(xo-web): remove unused messages (#4219) 2019-05-16 13:28:39 +02:00
Dustin B
d67e95af7b fix(xo-web/messages): more verbiage and typo fixes, clarifications as well (#4218) 2019-05-15 17:36:00 +02:00
badrAZ
0b0f235252 feat(xo-web/new/metadata): ability to set the backup report when property (#4149) 2019-05-15 16:47:59 +02:00
Jon Sands
36a5f52068 fix(docu/interface) grammar fixes for interface messages (#4213)
* Grammar fixes and typo for messages
2019-05-15 15:10:43 +02:00
Julien Fontanet
31266728f7 feat(xo-server): add / mounts to vendor config 2019-05-15 14:28:20 +02:00
Rajaa.BARHTAOUI
8c79ea4ce3 feat(xo-web/vm/general): display 'Started... ago' for paused state (#4170)
Fixes #3750
2019-05-15 10:41:02 +02:00
Dustin B
c73a4204cb Verbiage change to align with main messages.js 2019-05-14 22:52:58 +02:00
Dustin B
0b3c2cc252 Verbiage changes (#4211)
* Verbiage changes
2019-05-14 22:03:50 +02:00
Dustin B
2bd3ca1d0b Grammar and typos adjustments (#4210)
* Grammar and typos adjustments
2019-05-14 20:55:50 +02:00
badrAZ
ce8649d991 fix(xo-web/backup-ng): handle improper "reportWhen" value (#4199)
Additional change to #4178 to actually fix #4092.
2019-05-14 17:12:53 +02:00
badrAZ
9bd563b111 chore(CHANGELOG): update next 2019-05-14 16:32:31 +02:00
badrAZ
6ceb924a85 feat(xo-web): 5.41.0 2019-05-14 16:20:31 +02:00
84 changed files with 2363 additions and 1902 deletions

View File

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

View File

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

View File

@@ -21,12 +21,12 @@
"node": ">=6"
},
"dependencies": {
"@marsaud/smb2": "^0.13.0",
"@marsaud/smb2": "^0.14.0",
"@sindresorhus/df": "^2.1.0",
"@xen-orchestra/async-map": "^0.0.0",
"decorator-synchronized": "^0.5.0",
"execa": "^1.0.0",
"fs-extra": "^7.0.0",
"fs-extra": "^8.0.1",
"get-stream": "^4.0.0",
"lodash": "^4.17.4",
"promise-toolbox": "^0.12.1",
@@ -45,7 +45,7 @@
"async-iterator-to-stream": "^1.1.0",
"babel-plugin-lodash": "^3.3.2",
"cross-env": "^5.1.3",
"dotenv": "^7.0.0",
"dotenv": "^8.0.0",
"index-modules": "^0.3.0",
"rimraf": "^2.6.2"
},

View File

@@ -400,6 +400,10 @@ export default class RemoteHandlerAbstract {
}
}
async truncate(file: string, len: number): Promise<void> {
await this._truncate(file, len)
}
async unlink(file: string, { checksum = true }: Object = {}): Promise<void> {
file = normalizePath(file)

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,60 @@
# ChangeLog
## **next**
### Enhancements
- [VM/Advanced] Ability to use UEFI instead of BIOS [#4264](https://github.com/vatesfr/xen-orchestra/issues/4264) (PR [#4268](https://github.com/vatesfr/xen-orchestra/pull/4268))
### Bug fixes
- [XOA] Don't require editing the _email_ field in case of re-registration (PR [#4259](https://github.com/vatesfr/xen-orchestra/pull/4259))
### Released packages
- xen-api v0.25.2
- xo-server v5.43.0
- xo-web v5.43.0
## **5.35.0** (2019-05-29)
![Channel: latest](https://badgen.net/badge/channel/latest/yellow)
### Enhancements
- [VM/general] Display 'Started... ago' instead of 'Halted... ago' for paused state [#3750](https://github.com/vatesfr/xen-orchestra/issues/3750) (PR [#4170](https://github.com/vatesfr/xen-orchestra/pull/4170))
- [Metadata backup] Ability to define when the backup report will be sent (PR [#4149](https://github.com/vatesfr/xen-orchestra/pull/4149))
- [XOA/Update] Ability to select release channel [#4200](https://github.com/vatesfr/xen-orchestra/issues/4200) (PR [#4202](https://github.com/vatesfr/xen-orchestra/pull/4202))
- [User] Forget connection tokens on password change or on demand [#4214](https://github.com/vatesfr/xen-orchestra/issues/4214) (PR [#4224](https://github.com/vatesfr/xen-orchestra/pull/4224))
- [Settings/Logs] LICENCE_RESTRICTION errors: suggest XCP-ng as an Open Source alternative [#3876](https://github.com/vatesfr/xen-orchestra/issues/3876) (PR [#4238](https://github.com/vatesfr/xen-orchestra/pull/4238))
- [VM/Migrate] Display VDI size on migrate modal [#2534](https://github.com/vatesfr/xen-orchestra/issues/2534) (PR [#4250](https://github.com/vatesfr/xen-orchestra/pull/4250))
- [Host] Display hyperthreading status on advanced tab [#4262](https://github.com/vatesfr/xen-orchestra/issues/4262) (PR [#4263](https://github.com/vatesfr/xen-orchestra/pull/4263))
### Bug fixes
- [Pool/Patches] Fix "an error has occurred" in "Applied patches" [#4192](https://github.com/vatesfr/xen-orchestra/issues/4192) (PR [#4193](https://github.com/vatesfr/xen-orchestra/pull/4193))
- [Backup NG] Fix report sent even though "Never" is selected [#4092](https://github.com/vatesfr/xen-orchestra/issues/4092) (PR [#4178](https://github.com/vatesfr/xen-orchestra/pull/4178))
- [Remotes] Fix issues after a config import (PR [#4197](https://github.com/vatesfr/xen-orchestra/pull/4197))
- [Charts] Fixed the chart lines sometimes changing order/color (PR [#4221](https://github.com/vatesfr/xen-orchestra/pull/4221))
- Prevent non-admin users to access admin pages with URL (PR [#4220](https://github.com/vatesfr/xen-orchestra/pull/4220))
- [Upgrade] Fix alert before upgrade while running backup jobs [#4164](https://github.com/vatesfr/xen-orchestra/issues/4164) (PR [#4235](https://github.com/vatesfr/xen-orchestra/pull/4235))
- [Import] Fix import OVA files (PR [#4232](https://github.com/vatesfr/xen-orchestra/pull/4232))
- [VM/network] Fix duplicate IPv4 (PR [#4239](https://github.com/vatesfr/xen-orchestra/pull/4239))
- [Remotes] Fix disconnected remotes which may appear to work
- [Host] Fix incorrect hypervisor name [#4246](https://github.com/vatesfr/xen-orchestra/issues/4246) (PR [#4248](https://github.com/vatesfr/xen-orchestra/pull/4248))
### Released packages
- xo-server-backup-reports v0.16.1
- @xen-orchestra/fs v0.9.0
- vhd-lib v0.7.0
- xo-server v5.42.1
- xo-web v5.42.1
## **5.34.0** (2019-04-30)
![Channel: stable](https://badgen.net/badge/channel/stable/green)
### Highlights
- [Self/New VM] Add network config box to custom cloud-init [#3872](https://github.com/vatesfr/xen-orchestra/issues/3872) (PR [#4150](https://github.com/vatesfr/xen-orchestra/pull/4150))

View File

@@ -2,16 +2,12 @@
### Enhancements
### Bug fixes
- [Backup-ng/restore] Display size for full VM backup [#4009](https://github.com/vatesfr/xen-orchestra/issues/4009) (PR [#4245](https://github.com/vatesfr/xen-orchestra/pull/4245))
- [Sr/new] Ability to select NFS version when creating NFS storage [#3951](https://github.com/vatesfr/xen-orchestra/issues/#3951) (PR [#4277](https://github.com/vatesfr/xen-orchestra/pull/4277))
- [Pool/Patches] Fix "an error has occurred" in "Applied patches" [#4192](https://github.com/vatesfr/xen-orchestra/issues/4192) (PR [#4193](https://github.com/vatesfr/xen-orchestra/pull/4193))
- [Backup NG] Fix report sent even though "Never" is selected [#4092](https://github.com/vatesfr/xen-orchestra/issues/4092) (PR [#4178](https://github.com/vatesfr/xen-orchestra/pull/4178))
- [Remotes] Fix issues after a config import (PR [#4197](https://github.com/vatesfr/xen-orchestra/pull/4197))
### Bug fixes
### Released packages
- xo-server-backup-reports v0.16.1
- @xen-orchestra/fs v0.9.0
- vhd-lib v0.7.0
- xo-server v5.41.0
- xo-web v5.41.0
- xo-server v5.44.0
- xo-web v5.44.0

View File

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

View File

@@ -12,12 +12,12 @@
"eslint-config-standard-jsx": "^6.0.2",
"eslint-plugin-eslint-comments": "^3.1.1",
"eslint-plugin-import": "^2.8.0",
"eslint-plugin-node": "^8.0.0",
"eslint-plugin-node": "^9.0.1",
"eslint-plugin-promise": "^4.0.0",
"eslint-plugin-react": "^7.6.1",
"eslint-plugin-standard": "^4.0.0",
"exec-promise": "^0.7.0",
"flow-bin": "^0.98.0",
"flow-bin": "^0.100.0",
"globby": "^9.0.0",
"husky": "^2.2.0",
"jest": "^24.1.0",

View File

@@ -24,7 +24,7 @@
"async-iterator-to-stream": "^1.0.2",
"core-js": "^3.0.0",
"from2": "^2.3.0",
"fs-extra": "^7.0.0",
"fs-extra": "^8.0.1",
"limit-concurrency-decorator": "^0.4.0",
"promise-toolbox": "^0.12.1",
"struct-fu": "^1.2.0",
@@ -40,7 +40,7 @@
"cross-env": "^5.1.3",
"execa": "^1.0.0",
"fs-promise": "^2.0.0",
"get-stream": "^4.0.0",
"get-stream": "^5.1.0",
"index-modules": "^0.3.0",
"readable-stream": "^3.0.6",
"rimraf": "^2.6.2",

View File

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

View File

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

View File

@@ -1059,9 +1059,14 @@ export class Xapi extends EventEmitter {
}
}
props[`add_to_${field}`] = function(...values) {
props[`add_${field}`] = function(value) {
return xapi
.call(`${type}.add_${field}`, this.$ref, values)
.call(`${type}.add_${field}`, this.$ref, value)
.then(noop)
}
props[`remove_${field}`] = function(value) {
return xapi
.call(`${type}.remove_${field}`, this.$ref, value)
.then(noop)
}
} else if (value !== null && typeof value === 'object') {

View File

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

View File

@@ -49,6 +49,11 @@ maxTokenValidity = '0.5 year'
# Delay for which backups listing on a remote is cached
listingDebounce = '1 min'
# Duration for which we can wait for the backup size before returning
#
# It should be short to avoid blocking the display of the available backups.
vmBackupSizeTimeout = '2 seconds'
# Helmet handles HTTP security via headers
#
# https://helmetjs.github.io/docs/
@@ -74,6 +79,7 @@ honorCipherOrder = true
secureOptions = 117440512
[http.mounts]
'/' = '../xo-web/dist'
[remoteOptions]
mountsDir = '/run/xo-server/mounts'

View File

@@ -1,7 +1,7 @@
{
"private": true,
"name": "xo-server",
"version": "5.41.0",
"version": "5.43.0",
"license": "AGPL-3.0",
"description": "Server part of Xen-Orchestra",
"keywords": [
@@ -42,7 +42,7 @@
"@xen-orchestra/log": "^0.1.4",
"@xen-orchestra/mixin": "^0.0.0",
"ajv": "^6.1.1",
"app-conf": "^0.6.1",
"app-conf": "^0.7.0",
"archiver": "^3.0.0",
"async-iterator-to-stream": "^1.0.1",
"base64url": "^3.0.0",
@@ -51,7 +51,7 @@
"body-parser": "^1.18.2",
"compression": "^1.7.3",
"connect-flash": "^0.1.1",
"cookie": "^0.3.1",
"cookie": "^0.4.0",
"cookie-parser": "^1.4.3",
"d3-time-format": "^2.1.1",
"debug": "^4.0.1",
@@ -65,7 +65,7 @@
"express-session": "^1.15.6",
"fatfs": "^0.10.4",
"from2": "^2.3.0",
"fs-extra": "^7.0.0",
"fs-extra": "^8.0.1",
"get-stream": "^4.0.0",
"golike-defer": "^0.4.1",
"hashy": "^0.7.1",
@@ -123,7 +123,7 @@
"value-matcher": "^0.2.0",
"vhd-lib": "^0.7.0",
"ws": "^6.0.0",
"xen-api": "^0.25.1",
"xen-api": "^0.25.2",
"xml2js": "^0.4.19",
"xo-acl-resolver": "^0.4.1",
"xo-collection": "^0.4.1",

View File

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

View File

@@ -183,6 +183,7 @@ getLogs.params = {
after: { type: ['number', 'string'], optional: true },
before: { type: ['number', 'string'], optional: true },
limit: { type: 'number', optional: true },
'*': { type: 'any' },
}
// -----------------------------------------------------------------------------

View File

@@ -4,23 +4,19 @@ import { format, JsonRpcError } from 'json-rpc-peer'
export async function set({
host,
multipathing,
// TODO: use camel case.
multipathing,
name_label: nameLabel,
name_description: nameDescription,
}) {
const xapi = this.getXapi(host)
const hostId = host._xapiId
host = this.getXapiObject(host)
if (multipathing !== undefined) {
await xapi.setHostMultipathing(hostId, multipathing)
}
return xapi.setHostProperties(hostId, {
nameLabel,
nameDescription,
})
await Promise.all([
nameDescription !== undefined && host.set_name_description(nameDescription),
nameLabel !== undefined && host.set_name_label(nameLabel),
multipathing !== undefined &&
host.$xapi.setHostMultipathing(host.$id, multipathing),
])
}
set.description = 'changes the properties of an host'

View File

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

View File

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

View File

@@ -15,7 +15,7 @@ export function getIpv6ConfigurationModes() {
async function delete_({ pif }) {
// TODO: check if PIF is attached before
await this.getXapi(pif).call('PIF.destroy', pif._xapiRef)
await this.getXapi(pif).callAsync('PIF.destroy', pif._xapiRef)
}
export { delete_ as delete }
@@ -32,7 +32,7 @@ delete_.resolve = {
export async function disconnect({ pif }) {
// TODO: check if PIF is attached before
await this.getXapi(pif).call('PIF.unplug', pif._xapiRef)
await this.getXapi(pif).callAsync('PIF.unplug', pif._xapiRef)
}
disconnect.params = {
@@ -47,7 +47,7 @@ disconnect.resolve = {
export async function connect({ pif }) {
// TODO: check if PIF is attached before
await this.getXapi(pif).call('PIF.plug', pif._xapiRef)
await this.getXapi(pif).callAsync('PIF.plug', pif._xapiRef)
}
connect.params = {

View File

@@ -5,14 +5,15 @@ import { format, JsonRPcError } from 'json-rpc-peer'
export async function set({
pool,
// TODO: use camel case.
name_description: nameDescription,
name_label: nameLabel,
}) {
await this.getXapi(pool).setPoolProperties({
nameDescription,
nameLabel,
})
pool = this.getXapiObject(pool)
await Promise.all([
nameDescription !== undefined && pool.set_name_description(nameDescription),
nameLabel !== undefined && pool.set_name_label(nameLabel),
])
}
set.params = {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -320,6 +320,11 @@ create.params = {
},
},
},
hvmBootFirmware: { type: 'string', optional: true },
// other params are passed to `editVm`
'*': { type: 'any' },
}
create.resolve = {
@@ -603,7 +608,7 @@ set.params = {
// Switch from Cirrus video adaptor to VGA adaptor
vga: { type: 'string', optional: true },
videoram: { type: ['string', 'number'], optional: true },
videoram: { type: 'number', optional: true },
coresPerSocket: { type: ['string', 'number', 'null'], optional: true },
@@ -621,6 +626,9 @@ set.params = {
// set the VM network interface controller
nicType: { type: ['string', 'null'], optional: true },
// set the VM boot firmware mode
hvmBootFirmware: { type: ['string', 'null'], optional: true },
}
set.resolve = {
@@ -630,13 +638,7 @@ set.resolve = {
// -------------------------------------------------------------------
export async function restart({ vm, force = false }) {
const xapi = this.getXapi(vm)
if (force) {
await xapi.call('VM.hard_reboot', vm._xapiRef)
} else {
await xapi.call('VM.clean_reboot', vm._xapiRef)
}
return this.getXapi(vm).rebootVm(vm._xapiId, { hard: force })
}
restart.params = {
@@ -737,7 +739,7 @@ export async function convertToTemplate({ vm }) {
// Convert to a template requires pool admin permission.
await this.checkPermissions(this.user.id, [[vm.$pool, 'administrate']])
await this.getXapi(vm).call('VM.set_is_a_template', vm._xapiRef, true)
await this.getXapiObject(vm).set_is_a_template(true)
}
convertToTemplate.params = {
@@ -1089,7 +1091,7 @@ stop.resolve = {
// -------------------------------------------------------------------
export async function suspend({ vm }) {
await this.getXapi(vm).call('VM.suspend', vm._xapiRef)
await this.getXapi(vm).callAsync('VM.suspend', vm._xapiRef)
}
suspend.params = {
@@ -1103,7 +1105,7 @@ suspend.resolve = {
// -------------------------------------------------------------------
export async function pause({ vm }) {
await this.getXapi(vm).call('VM.pause', vm._xapiRef)
await this.getXapi(vm).callAsync('VM.pause', vm._xapiRef)
}
pause.params = {
@@ -1366,9 +1368,7 @@ createInterface.resolve = {
// -------------------------------------------------------------------
export async function attachPci({ vm, pciId }) {
const xapi = this.getXapi(vm)
await xapi.call('VM.add_to_other_config', vm._xapiRef, 'pci', pciId)
await this.getXapiObject(vm).update_other_config('pci', pciId)
}
attachPci.params = {
@@ -1383,9 +1383,7 @@ attachPci.resolve = {
// -------------------------------------------------------------------
export async function detachPci({ vm }) {
const xapi = this.getXapi(vm)
await xapi.call('VM.remove_from_other_config', vm._xapiRef, 'pci')
await this.getXapiObject(vm).update_other_config('pci', null)
}
detachPci.params = {
@@ -1418,15 +1416,11 @@ stats.resolve = {
// -------------------------------------------------------------------
export async function setBootOrder({ vm, order }) {
const xapi = this.getXapi(vm)
order = { order }
if (vm.virtualizationMode === 'hvm') {
await xapi.call('VM.set_HVM_boot_params', vm._xapiRef, order)
return
if (vm.virtualizationMode !== 'hvm') {
throw invalidParameters('You can only set the boot order on a HVM guest')
}
throw invalidParameters('You can only set the boot order on a HVM guest')
await this.getXapiObject(vm).update_HVM_boot_params('order', order)
}
setBootOrder.params = {

View File

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

View File

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

View File

@@ -569,6 +569,16 @@ const TRANSFORMS = {
MAC: obj.MAC,
MTU: +obj.MTU,
// in kB/s
rateLimit: (() => {
if (obj.qos_algorithm_type === 'ratelimit') {
const { kbps } = obj.qos_algorithm_params
if (kbps !== undefined) {
return +kbps
}
}
})(),
$network: link(obj, 'network'),
$VM: link(obj, 'VM'),
}

View File

@@ -29,6 +29,7 @@ import {
groupBy,
includes,
isEmpty,
noop,
omit,
startsWith,
uniq,
@@ -228,14 +229,6 @@ export default class Xapi extends XapiBase {
// =================================================================
_setObjectProperty(object, name, value) {
return this.call(
`${object.$type}.set_${camelToSnakeCase(name)}`,
object.$ref,
prepareXapiParam(value)
)
}
_setObjectProperties(object, props) {
const { $ref: ref, $type: type } = object
@@ -254,101 +247,10 @@ export default class Xapi extends XapiBase {
)::ignoreErrors()
}
async _updateObjectMapProperty(object, prop, values) {
const { $ref: ref, $type: type } = object
prop = camelToSnakeCase(prop)
const add = `${type}.add_to_${prop}`
const remove = `${type}.remove_from_${prop}`
await Promise.all(
mapToArray(values, (value, name) => {
if (value !== undefined) {
name = camelToSnakeCase(name)
const removal = this.call(remove, ref, name)
return value === null
? removal
: removal
::ignoreErrors()
.then(() => this.call(add, ref, name, prepareXapiParam(value)))
}
})
)
}
async setHostProperties(id, { nameLabel, nameDescription }) {
await this._setObjectProperties(this.getObject(id), {
nameLabel,
nameDescription,
})
}
async setPoolProperties({ autoPoweron, nameLabel, nameDescription }) {
const { pool } = this
await Promise.all([
this._setObjectProperties(pool, {
nameLabel,
nameDescription,
}),
autoPoweron != null &&
this._updateObjectMapProperty(pool, 'other_config', {
autoPoweron: autoPoweron ? 'true' : null,
}),
])
}
async setSrProperties(id, { nameLabel, nameDescription }) {
await this._setObjectProperties(this.getObject(id), {
nameLabel,
nameDescription,
})
}
async setNetworkProperties(
id,
{ automatic, defaultIsLocked, nameDescription, nameLabel }
) {
let defaultLockingMode
if (defaultIsLocked != null) {
defaultLockingMode = defaultIsLocked ? 'disabled' : 'unlocked'
}
const network = this.getObject(id)
await Promise.all([
this._setObjectProperties(network, {
defaultLockingMode,
nameDescription,
nameLabel,
}),
this._updateObjectMapProperty(network, 'other_config', {
automatic:
automatic === undefined ? undefined : automatic ? 'true' : null,
}),
])
}
// =================================================================
async addTag(id, tag) {
const { $ref: ref, $type: type } = this.getObject(id)
await this.call(`${type}.add_tags`, ref, tag)
}
async removeTag(id, tag) {
const { $ref: ref, $type: type } = this.getObject(id)
await this.call(`${type}.remove_tags`, ref, tag)
}
// =================================================================
async setDefaultSr(srId) {
this._setObjectProperties(this.pool, {
default_SR: this.getObject(srId).$ref,
})
setDefaultSr(srId) {
return this.pool.set_default_SR(this.getObject(srId).$ref)
}
// =================================================================
@@ -377,12 +279,12 @@ export default class Xapi extends XapiBase {
await pSettle(
mapToArray(vms, vm => {
if (!vm.is_control_domain) {
return this.call('VM.suspend', vm.$ref)
return this.callAsync('VM.suspend', vm.$ref)
}
})
)
await this.call('host.disable', host.$ref)
await this.call('host.shutdown', host.$ref)
await this.callAsync('host.shutdown', host.$ref)
}
// =================================================================
@@ -395,7 +297,7 @@ export default class Xapi extends XapiBase {
await this.call('host.disable', ref)
try {
await this.call('host.evacuate', ref)
await this.callAsync('host.evacuate', ref)
} catch (error) {
if (!force) {
await this.call('host.enable', ref)
@@ -410,7 +312,7 @@ export default class Xapi extends XapiBase {
}
async forgetHost(hostId) {
await this.call('host.destroy', this.getObject(hostId).$ref)
await this.callAsync('host.destroy', this.getObject(hostId).$ref)
}
async ejectHostFromPool(hostId) {
@@ -445,9 +347,7 @@ export default class Xapi extends XapiBase {
$defer(() => this.plugPbd(ref))
})
return this._updateObjectMapProperty(
host,
'other_config',
return host.update_other_config(
multipathing
? {
multipathing: 'true',
@@ -460,23 +360,23 @@ export default class Xapi extends XapiBase {
}
async powerOnHost(hostId) {
await this.call('host.power_on', this.getObject(hostId).$ref)
await this.callAsync('host.power_on', this.getObject(hostId).$ref)
}
async rebootHost(hostId, force = false) {
const host = this.getObject(hostId)
await this._clearHost(host, force)
await this.call('host.reboot', host.$ref)
await this.callAsync('host.reboot', host.$ref)
}
async restartHostAgent(hostId) {
await this.call('host.restart_agent', this.getObject(hostId).$ref)
await this.callAsync('host.restart_agent', this.getObject(hostId).$ref)
}
async setRemoteSyslogHost(hostId, syslogDestination) {
const host = this.getObject(hostId)
await this.call('host.set_logging', host.$ref, {
await host.set_logging({
syslog_destination: syslogDestination,
})
await this.call('host.syslog_reconfigure', host.$ref)
@@ -486,7 +386,7 @@ export default class Xapi extends XapiBase {
const host = this.getObject(hostId)
await this._clearHost(host, force)
await this.call('host.shutdown', host.$ref)
await this.callAsync('host.shutdown', host.$ref)
}
// =================================================================
@@ -500,7 +400,7 @@ export default class Xapi extends XapiBase {
}`
)
return this.call('VM.clone', vm.$ref, nameLabel)
return this.callAsync('VM.clone', vm.$ref, nameLabel).then(extractOpaqueRef)
}
// Copy a VM: make a normal copy of a VM and all its VDIs.
@@ -571,12 +471,7 @@ export default class Xapi extends XapiBase {
stream = stream.pipe(sizeStream)
const onVmCreation =
nameLabel !== undefined
? vm =>
targetXapi._setObjectProperties(vm, {
nameLabel,
})
: null
nameLabel !== undefined ? vm => vm.set_name_label(nameLabel) : null
const vm = await targetXapi._getOrWaitObject(
await targetXapi._importVm(stream, sr, onVmCreation)
@@ -716,17 +611,13 @@ export default class Xapi extends XapiBase {
// It is necessary for suspended VMs to be shut down
// to be able to delete their VDIs.
if (vm.power_state !== 'Halted') {
await this.call('VM.hard_shutdown', $ref)
await this.callAsync('VM.hard_shutdown', $ref)
}
await Promise.all([
this.call('VM.set_is_a_template', vm.$ref, false),
this._updateObjectMapProperty(vm, 'blocked_operations', {
destroy: null,
}),
this._updateObjectMapProperty(vm, 'other_config', {
default_template: null,
}),
vm.set_is_a_template(false),
vm.update_blocked_operations('destroy', null),
vm.update_other_config('default_template', null),
])
// must be done before destroying the VM
@@ -734,7 +625,7 @@ export default class Xapi extends XapiBase {
// this cannot be done in parallel, otherwise disks and snapshots will be
// destroyed even if this fails
await this.call('VM.destroy', $ref)
await this.callAsync('VM.destroy', $ref)
return Promise.all([
asyncMap(vm.$snapshots, snapshot =>
@@ -1065,23 +956,21 @@ export default class Xapi extends XapiBase {
await this._createVmRecord({
...delta.vm,
affinity: null,
blocked_operations: {
...delta.vm.blocked_operations,
start: 'Importing…',
},
ha_always_run: false,
is_a_template: false,
name_label: `[Importing…] ${name_label}`,
other_config: {
...delta.vm.other_config,
[TAG_COPY_SRC]: delta.vm.uuid,
},
})
)
$defer.onFailure(() => this._deleteVm(vm))
await Promise.all([
this._setObjectProperties(vm, {
name_label: `[Importing…] ${name_label}`,
}),
this._updateObjectMapProperty(vm, 'blocked_operations', {
start: 'Importing…',
}),
this._updateObjectMapProperty(vm, 'other_config', {
[TAG_COPY_SRC]: delta.vm.uuid,
}),
])
// 2. Delete all VBDs which may have been created by the import.
await asyncMap(vm.$VBDs, vbd => this._deleteVbd(vbd))::ignoreErrors()
@@ -1103,9 +992,7 @@ export default class Xapi extends XapiBase {
newVdi = await this._getOrWaitObject(await this._cloneVdi(baseVdi))
$defer.onFailure(() => this._deleteVdi(newVdi.$ref))
await this._updateObjectMapProperty(newVdi, 'other_config', {
[TAG_COPY_SRC]: vdi.uuid,
})
await newVdi.update_other_config(TAG_COPY_SRC, vdi.uuid)
} else {
newVdi = await this.createVdi({
...vdi,
@@ -1200,15 +1087,15 @@ export default class Xapi extends XapiBase {
}
await Promise.all([
this._setObjectProperties(vm, {
name_label,
}),
delta.vm.ha_always_run && vm.set_ha_always_run(true),
vm.set_name_label(name_label),
// FIXME: move
this._updateObjectMapProperty(vm, 'blocked_operations', {
start: disableStartAfterImport
vm.update_blocked_operations(
'start',
disableStartAfterImport
? 'Do not start this VM, clone it if you want to use it.'
: null,
}),
: null
),
])
return { transferSize, vm }
@@ -1262,7 +1149,7 @@ export default class Xapi extends XapiBase {
)
const loop = () =>
this.call(
this.callAsync(
'VM.migrate_send',
vm.$ref,
token,
@@ -1276,7 +1163,7 @@ export default class Xapi extends XapiBase {
pDelay(1e4).then(loop)
)
return loop()
return loop().then(noop)
}
@synchronized()
@@ -1431,14 +1318,8 @@ export default class Xapi extends XapiBase {
$defer.onFailure(() => this._deleteVm(vm))
// Disable start and change the VM name label during import.
await Promise.all([
this.addForbiddenOperationToVm(
vm.$id,
'start',
'OVA import in progress...'
),
this._setObjectProperties(vm, {
name_label: `[Importing...] ${nameLabel}`,
}),
vm.update_blocked_operations('start', 'OVA import in progress...'),
vm.set_name_label(`[Importing...] ${nameLabel}`),
])
// 2. Create VDIs & Vifs.
@@ -1455,7 +1336,7 @@ export default class Xapi extends XapiBase {
$defer.onFailure(() => this._deleteVdi(vdi.$ref))
return this.createVbd({
userdevice: disk.position,
userdevice: String(disk.position),
vdi,
vm,
})
@@ -1498,8 +1379,8 @@ export default class Xapi extends XapiBase {
// Enable start and restore the VM name label after import.
await Promise.all([
this.removeForbiddenOperationFromVm(vm.$id, 'start'),
this._setObjectProperties(vm, { name_label: nameLabel }),
vm.update_blocked_operations('start', null),
vm.set_name_label(nameLabel),
])
return vm
}
@@ -1546,7 +1427,7 @@ export default class Xapi extends XapiBase {
})
} else {
try {
await this.call('VM.pool_migrate', vm.$ref, host.$ref, {
await this.callAsync('VM.pool_migrate', vm.$ref, host.$ref, {
force: 'true',
})
} catch (error) {
@@ -1631,19 +1512,11 @@ export default class Xapi extends XapiBase {
return /* await */ this._snapshotVm(this.getObject(vmId), nameLabel)
}
async setVcpuWeight(vmId, weight) {
weight = weight || null // Take all falsy values as a removal (0 included)
const vm = this.getObject(vmId)
await this._updateObjectMapProperty(vm, 'VCPUs_params', { weight })
}
async _startVm(vm, host, force) {
log.debug(`Starting VM ${vm.name_label}`)
if (force) {
await this._updateObjectMapProperty(vm, 'blocked_operations', {
start: null,
})
await vm.update_blocked_operations('start', null)
}
return host === undefined
@@ -1653,7 +1526,7 @@ export default class Xapi extends XapiBase {
false, // Start paused?
false // Skip pre-boot checks?
)
: this.call('VM.start_on', vm.$ref, host.$ref, false, false)
: this.callAsync('VM.start_on', vm.$ref, host.$ref, false, false)
}
async startVm(vmId, hostId, force) {
@@ -1682,16 +1555,12 @@ export default class Xapi extends XapiBase {
if (isVmHvm(vm)) {
const { order } = vm.HVM_boot_params
await this._updateObjectMapProperty(vm, 'HVM_boot_params', {
order: 'd',
})
await vm.update_HVM_boot_params('order', 'd')
try {
await this._startVm(vm)
} finally {
await this._updateObjectMapProperty(vm, 'HVM_boot_params', {
order,
})
await vm.update_HVM_boot_params('order', order)
}
} else {
// Find the original template by name (*sigh*).
@@ -1713,20 +1582,14 @@ export default class Xapi extends XapiBase {
const cdDrive = this._getVmCdDrive(vm)
forEach(vm.$VBDs, vbd => {
promises.push(
this._setObjectProperties(vbd, {
bootable: vbd === cdDrive,
})
)
promises.push(vbd.set_bootable(vbd === cdDrive))
bootables.push([vbd, Boolean(vbd.bootable)])
})
promises.push(
this._setObjectProperties(vm, {
PV_bootloader: 'eliloader',
}),
this._updateObjectMapProperty(vm, 'other_config', {
vm.set_PV_bootloader('eliloader'),
vm.update_other_config({
'install-distro':
template && template.other_config['install-distro'],
'install-repository': 'cdrom',
@@ -1737,35 +1600,15 @@ export default class Xapi extends XapiBase {
await this._startVm(vm)
} finally {
this._setObjectProperties(vm, {
PV_bootloader: bootloader,
})::ignoreErrors()
vm.set_PV_bootloader(bootloader)::ignoreErrors()
forEach(bootables, ([vbd, bootable]) => {
this._setObjectProperties(vbd, { bootable })::ignoreErrors()
vbd.set_bootable(bootable)::ignoreErrors()
})
}
}
}
// vm_operations: http://xapi-project.github.io/xen-api/classes/vm.html
async addForbiddenOperationToVm(vmId, operation, reason) {
await this.call(
'VM.add_to_blocked_operations',
this.getObject(vmId).$ref,
operation,
`[XO] ${reason}`
)
}
async removeForbiddenOperationFromVm(vmId, operation) {
await this.call(
'VM.remove_from_blocked_operations',
this.getObject(vmId).$ref,
operation
)
}
// =================================================================
async createVbd({
@@ -1826,14 +1669,14 @@ export default class Xapi extends XapiBase {
})
if (isVmRunning(vm)) {
await this.call('VBD.plug', vbdRef)
await this.callAsync('VBD.plug', vbdRef)
}
}
_cloneVdi(vdi) {
log.debug(`Cloning VDI ${vdi.name_label}`)
return this.call('VDI.clone', vdi.$ref)
return this.callAsync('VDI.clone', vdi.$ref).then(extractOpaqueRef)
}
async createVdi({
@@ -1856,7 +1699,7 @@ export default class Xapi extends XapiBase {
log.debug(`Creating VDI ${name_label} on ${sr.name_label}`)
return this._getOrWaitObject(
await this.call('VDI.create', {
await this.callAsync('VDI.create', {
name_description,
name_label,
other_config,
@@ -1868,7 +1711,7 @@ export default class Xapi extends XapiBase {
type,
virtual_size: size !== undefined ? parseSize(size) : virtual_size,
xenstore_data,
})
}).then(extractOpaqueRef)
)
}
@@ -1886,9 +1729,12 @@ export default class Xapi extends XapiBase {
}`
)
try {
await pRetry(() => this.call('VDI.pool_migrate', vdi.$ref, sr.$ref, {}), {
when: { code: 'TOO_MANY_STORAGE_MIGRATES' },
})
await pRetry(
() => this.callAsync('VDI.pool_migrate', vdi.$ref, sr.$ref, {}),
{
when: { code: 'TOO_MANY_STORAGE_MIGRATES' },
}
)
} catch (error) {
const { code } = error
if (
@@ -1899,7 +1745,9 @@ export default class Xapi extends XapiBase {
throw error
}
const newVdi = await this.barrier(
await this.call('VDI.copy', vdi.$ref, sr.$ref)
await this.callAsync('VDI.copy', vdi.$ref, sr.$ref).then(
extractOpaqueRef
)
)
await asyncMap(vdi.$VBDs, async vbd => {
await this.call('VBD.destroy', vbd.$ref)
@@ -1917,7 +1765,7 @@ export default class Xapi extends XapiBase {
log.debug(`Deleting VDI ${vdiRef}`)
try {
await this.call('VDI.destroy', vdiRef)
await this.callAsync('VDI.destroy', vdiRef)
} catch (error) {
if (error?.code !== 'HANDLE_INVALID') {
throw error
@@ -1930,7 +1778,7 @@ export default class Xapi extends XapiBase {
`Resizing VDI ${vdi.name_label} from ${vdi.virtual_size} to ${size}`
)
return this.call('VDI.resize', vdi.$ref, size)
return this.callAsync('VDI.resize', vdi.$ref, size)
}
_getVmCdDrive(vm) {
@@ -1944,7 +1792,7 @@ export default class Xapi extends XapiBase {
async _ejectCdFromVm(vm) {
const cdDrive = this._getVmCdDrive(vm)
if (cdDrive) {
await this.call('VBD.eject', cdDrive.$ref)
await this.callAsync('VBD.eject', cdDrive.$ref)
}
}
@@ -1952,20 +1800,20 @@ export default class Xapi extends XapiBase {
const cdDrive = await this._getVmCdDrive(vm)
if (cdDrive) {
try {
await this.call('VBD.insert', cdDrive.$ref, cd.$ref)
await this.callAsync('VBD.insert', cdDrive.$ref, cd.$ref)
} catch (error) {
if (!force || error.code !== 'VBD_NOT_EMPTY') {
throw error
}
await this.call('VBD.eject', cdDrive.$ref)::ignoreErrors()
await this.callAsync('VBD.eject', cdDrive.$ref)::ignoreErrors()
// Retry.
await this.call('VBD.insert', cdDrive.$ref, cd.$ref)
await this.callAsync('VBD.insert', cdDrive.$ref, cd.$ref)
}
if (bootable !== Boolean(cdDrive.bootable)) {
await this._setObjectProperties(cdDrive, { bootable })
await cdDrive.set_bootable(bootable)
}
} else {
await this.createVbd({
@@ -1978,7 +1826,7 @@ export default class Xapi extends XapiBase {
}
async connectVbd(vbdId) {
await this.call('VBD.plug', vbdId)
await this.callAsync('VBD.plug', vbdId)
}
async _disconnectVbd(vbd) {
@@ -1987,7 +1835,7 @@ export default class Xapi extends XapiBase {
await this.call('VBD.unplug_force', vbd.$ref)
} catch (error) {
if (error.code === 'VBD_NOT_UNPLUGGABLE') {
await this.call('VBD.set_unpluggable', vbd.$ref, true)
await vbd.set_unpluggable(true)
return this.call('VBD.unplug_force', vbd.$ref)
}
}
@@ -2038,11 +1886,11 @@ export default class Xapi extends XapiBase {
const vdi = this.getObject(vdiId)
const snap = await this._getOrWaitObject(
await this.call('VDI.snapshot', vdi.$ref)
await this.callAsync('VDI.snapshot', vdi.$ref).then(extractOpaqueRef)
)
if (nameLabel) {
await this.call('VDI.set_name_label', snap.$ref, nameLabel)
await snap.set_name_label(nameLabel)
}
return snap
@@ -2166,7 +2014,7 @@ export default class Xapi extends XapiBase {
)
if (currently_attached && isVmRunning(vm)) {
await this.call('VIF.plug', vifRef)
await this.callAsync('VIF.plug', vifRef)
}
return vifRef
@@ -2194,7 +2042,7 @@ export default class Xapi extends XapiBase {
// https://citrix.github.io/xenserver-sdk/#network
other_config: { automatic: 'false' },
})
$defer.onFailure(() => this.call('network.destroy', networkRef))
$defer.onFailure(() => this.callAsync('network.destroy', networkRef))
if (pifId) {
await this.call(
'pool.create_VLAN_from_PIF',
@@ -2233,7 +2081,7 @@ export default class Xapi extends XapiBase {
await Promise.all(
mapToArray(
vlans,
vlan => vlan !== NULL_REF && this.call('VLAN.destroy', vlan)
vlan => vlan !== NULL_REF && this.callAsync('VLAN.destroy', vlan)
)
)
@@ -2248,7 +2096,7 @@ export default class Xapi extends XapiBase {
newPifs,
pifRef =>
!wasAttached[this.getObject(pifRef).host] &&
this.call('PIF.unplug', pifRef)::ignoreErrors()
this.callAsync('PIF.unplug', pifRef)::ignoreErrors()
)
)
}
@@ -2279,7 +2127,7 @@ export default class Xapi extends XapiBase {
await Promise.all(
mapToArray(
vlans,
vlan => vlan !== NULL_REF && this.call('VLAN.destroy', vlan)
vlan => vlan !== NULL_REF && this.callAsync('VLAN.destroy', vlan)
)
)
@@ -2288,7 +2136,7 @@ export default class Xapi extends XapiBase {
mapToArray(bonds, bond => this.call('Bond.destroy', bond))
)
await this.call('network.destroy', network.$ref)
await this.callAsync('network.destroy', network.$ref)
}
// =================================================================

View File

@@ -68,18 +68,12 @@ declare export class Xapi {
sr?: XapiObject,
onVmCreation?: (XapiObject) => any
): Promise<string>;
_updateObjectMapProperty(
object: XapiObject,
property: string,
entries: $Dict<null | string>
): Promise<void>;
_setObjectProperties(
object: XapiObject,
properties: $Dict<mixed>
): Promise<void>;
_snapshotVm(cancelToken: mixed, vm: Vm, nameLabel?: string): Promise<Vm>;
addTag(object: Id, tag: string): Promise<void>;
barrier(): Promise<void>;
barrier(ref: string): Promise<XapiObject>;
deleteVm(vm: Id): Promise<void>;

View File

@@ -4,13 +4,13 @@ import { makeEditObject } from '../utils'
export default {
async _connectVif(vif) {
await this.call('VIF.plug', vif.$ref)
await this.callAsync('VIF.plug', vif.$ref)
},
async connectVif(vifId) {
await this._connectVif(this.getObject(vifId))
},
async _deleteVif(vif) {
await this.call('VIF.destroy', vif.$ref)
await this.callAsync('VIF.destroy', vif.$ref)
},
async deleteVif(vifId) {
const vif = this.getObject(vifId)
@@ -20,7 +20,7 @@ export default {
await this._deleteVif(vif)
},
async _disconnectVif(vif) {
await this.call('VIF.unplug_force', vif.$ref)
await this.callAsync('VIF.unplug_force', vif.$ref)
},
async disconnectVif(vifId) {
await this._disconnectVif(this.getObject(vifId))
@@ -37,7 +37,7 @@ export default {
: 'locked'
if (lockingMode !== vif.locking_mode) {
return this._set('locking_mode', lockingMode)
return vif.set_locking_mode(lockingMode)
}
},
],
@@ -53,10 +53,36 @@ export default {
: 'locked'
if (lockingMode !== vif.locking_mode) {
return this._set('locking_mode', lockingMode)
return vif.set_locking_mode(lockingMode)
}
},
],
},
// in kB/s
rateLimit: {
get: vif => {
if (vif.qos_algorithm_type === 'ratelimit') {
const { kbps } = vif.qos_algorithm_params
if (kbps !== undefined) {
return +kbps
}
}
// null is value used to remove the existing value
//
// we need to match this, to allow avoiding the `set` if the value is
// already missing.
return null
},
set: (value, vif) =>
Promise.all([
vif.set_qos_algorithm_type(value === null ? '' : 'ratelimit'),
vif.update_qos_algorithm_params(
'kbps',
value === null ? null : String(value)
),
]),
},
}),
}

View File

@@ -353,9 +353,10 @@ export default {
if (JSON.parse(update).exit !== 0) {
throw new Error('Update install failed')
} else {
await this._updateObjectMapProperty(host, 'other_config', {
rpm_patch_installation_time: String(Date.now() / 1000),
})
await host.update_other_config(
'rpm_patch_installation_time',
String(Date.now() / 1000)
)
}
})
},

View File

@@ -35,7 +35,7 @@ export default {
},
_plugPbd(pbd) {
return this.call('PBD.plug', pbd.$ref)
return this.callAsync('PBD.plug', pbd.$ref)
},
async plugPbd(id) {
@@ -43,7 +43,7 @@ export default {
},
_unplugPbd(pbd) {
return this.call('PBD.unplug', pbd.$ref)
return this.callAsync('PBD.unplug', pbd.$ref)
},
async unplugPbd(id) {
@@ -84,4 +84,32 @@ export default {
})
return unhealthyVdis
},
async createSr({
hostRef,
content_type = 'user', // recommended by Citrix
device_config = {},
name_description = '',
name_label,
shared = false,
physical_size = 0,
sm_config = {},
type,
}) {
const srRef = await this.call(
'SR.create',
hostRef,
device_config,
physical_size,
name_label,
name_description,
type,
content_type,
shared,
sm_config
)
return (await this.barrier(srRef)).uuid
},
}

View File

@@ -94,7 +94,7 @@ export default {
// Creates the VDIs and executes the initial steps of the
// installation.
await this.call('VM.provision', vmRef)
await this.callAsync('VM.provision', vmRef)
let vm = await this._getOrWaitObject(vmRef)
@@ -107,17 +107,12 @@ export default {
if (isHvm) {
if (!isEmpty(vdis) || installMethod === 'network') {
const { HVM_boot_params: bootParams } = vm
let order = bootParams.order
if (order) {
order = 'n' + order.replace('n', '')
} else {
order = 'ncd'
}
const { order } = vm.HVM_boot_params
this._setObjectProperties(vm, {
HVM_boot_params: { ...bootParams, order },
})
vm.update_HVM_boot_params(
'order',
order ? 'n' + order.replace('n', '') : 'ncd'
)
}
} else {
// PV
@@ -125,13 +120,12 @@ export default {
if (installMethod === 'network') {
// TODO: normalize RHEL URL?
await this._updateObjectMapProperty(vm, 'other_config', {
'install-repository': installRepository,
})
await vm.update_other_config(
'install-repository',
installRepository
)
} else if (installMethod === 'cd') {
await this._updateObjectMapProperty(vm, 'other_config', {
'install-repository': 'cdrom',
})
await vm.update_other_config('install-repository', 'cdrom')
}
}
}
@@ -264,25 +258,15 @@ export default {
_editVm: makeEditObject({
affinityHost: {
get: 'affinity',
set(value, vm) {
return this._setObjectProperty(
vm,
'affinity',
value ? this.getObject(value).$ref : NULL_REF
)
},
set: (value, vm) =>
vm.set_affinity(value ? this.getObject(value).$ref : NULL_REF),
},
autoPoweron: {
set(value, vm) {
return Promise.all([
this._updateObjectMapProperty(vm, 'other_config', {
autoPoweron: value ? 'true' : null,
}),
value &&
this.setPoolProperties({
autoPoweron: true,
}),
vm.update_other_config('auto_poweron', value ? 'true' : null),
value && vm.$pool.update_other_config('auto_poweron', 'true'),
])
},
},
@@ -292,23 +276,19 @@ export default {
if (virtualizationMode !== 'pv' && virtualizationMode !== 'hvm') {
throw new Error(`The virtualization mode must be 'pv' or 'hvm'`)
}
return this._set('domain_type', virtualizationMode)::pCatch(
{ code: 'MESSAGE_METHOD_UNKNOWN' },
() =>
this._set(
'HVM_boot_policy',
return vm
.set_domain_type(virtualizationMode)
::pCatch({ code: 'MESSAGE_METHOD_UNKNOWN' }, () =>
vm.set_HVM_boot_policy(
virtualizationMode === 'hvm' ? 'Boot order' : ''
)
)
)
},
},
coresPerSocket: {
set(coresPerSocket, vm) {
return this._updateObjectMapProperty(vm, 'platform', {
'cores-per-socket': coresPerSocket,
})
},
set: (coresPerSocket, vm) =>
vm.update_platform('cores-per-socket', String(coresPerSocket)),
},
CPUs: 'cpus',
@@ -326,26 +306,22 @@ export default {
get: vm => +vm.VCPUs_at_startup,
set: [
'VCPUs_at_startup',
function(value, vm) {
return isVmRunning(vm) && this._set('VCPUs_number_live', value)
},
(value, vm) => isVmRunning(vm) && vm.set_VCPUs_number_live(value),
],
},
cpuCap: {
get: vm => vm.VCPUs_params.cap && +vm.VCPUs_params.cap,
set(cap, vm) {
return this._updateObjectMapProperty(vm, 'VCPUs_params', { cap })
},
set: (cap, vm) => vm.update_VCPUs_params('cap', String(cap)),
},
cpuMask: {
get: vm => vm.VCPUs_params.mask && vm.VCPUs_params.mask.split(','),
set(cpuMask, vm) {
return this._updateObjectMapProperty(vm, 'VCPUs_params', {
mask: cpuMask == null ? cpuMask : cpuMask.join(','),
})
},
set: (cpuMask, vm) =>
vm.update_VCPUs_params(
'mask',
cpuMask == null ? cpuMask : cpuMask.join(',')
),
},
cpusMax: 'cpusStaticMax',
@@ -359,15 +335,15 @@ export default {
cpuWeight: {
get: vm => vm.VCPUs_params.weight && +vm.VCPUs_params.weight,
set(weight, vm) {
return this._updateObjectMapProperty(vm, 'VCPUs_params', { weight })
},
set: (weight, vm) =>
vm.update_VCPUs_params(
'weight',
weight === null ? null : String(weight)
),
},
highAvailability: {
set(ha, vm) {
return this.call('VM.set_ha_restart_priority', vm.$ref, ha)
},
set: (ha, vm) => vm.set_ha_restart_priority(ha),
},
memoryMin: {
@@ -439,19 +415,12 @@ export default {
hasVendorDevice: true,
expNestedHvm: {
set(expNestedHvm, vm) {
return this._updateObjectMapProperty(vm, 'platform', {
'exp-nested-hvm': expNestedHvm ? 'true' : null,
})
},
set: (expNestedHvm, vm) =>
vm.update_platform('exp-nested-hvm', expNestedHvm ? 'true' : null),
},
nicType: {
set(nicType, vm) {
return this._updateObjectMapProperty(vm, 'platform', {
nic_type: nicType,
})
},
set: (nicType, vm) => vm.update_platform('nic_type', nicType),
},
vga: {
@@ -461,7 +430,7 @@ export default {
`The different values that the VGA can take are: ${XEN_VGA_VALUES}`
)
}
return this._updateObjectMapProperty(vm, 'platform', { vga })
return vm.update_platform('vga', vga)
},
},
@@ -472,15 +441,17 @@ export default {
`The different values that the video RAM can take are: ${XEN_VIDEORAM_VALUES}`
)
}
return this._updateObjectMapProperty(vm, 'platform', { videoram })
return vm.update_platform('videoram', String(videoram))
},
},
startDelay: {
get: vm => +vm.start_delay,
set(startDelay, vm) {
return this.call('VM.set_start_delay', vm.$ref, startDelay)
},
set: (startDelay, vm) => vm.set_start_delay(startDelay),
},
hvmBootFirmware: {
set: (firmware, vm) => vm.update_HVM_boot_params('firmware', firmware),
},
}),
@@ -493,7 +464,7 @@ export default {
if (snapshotBefore) {
await this._snapshotVm(snapshot.$snapshot_of)
}
await this.call('VM.revert', snapshot.$ref)
await this.callAsync('VM.revert', snapshot.$ref)
if (snapshot.snapshot_info['power-state-at-snapshot'] === 'Running') {
const vm = await this.barrier(snapshot.snapshot_of)
if (vm.power_state === 'Halted') {
@@ -506,15 +477,22 @@ export default {
async resumeVm(vmId) {
// the force parameter is always true
return this.call('VM.resume', this.getObject(vmId).$ref, false, true)
await this.callAsync('VM.resume', this.getObject(vmId).$ref, false, true)
},
async unpauseVm(vmId) {
return this.call('VM.unpause', this.getObject(vmId).$ref)
await this.callAsync('VM.unpause', this.getObject(vmId).$ref)
},
rebootVm(vmId, { hard = false } = {}) {
return this.callAsync(
`VM.${hard ? 'hard' : 'clean'}_reboot`,
this.getObject(vmId).$ref
).then(noop)
},
shutdownVm(vmId, { hard = false } = {}) {
return this.call(
return this.callAsync(
`VM.${hard ? 'hard' : 'clean'}_shutdown`,
this.getObject(vmId).$ref
).then(noop)

View File

@@ -148,8 +148,8 @@ export const makeEditObject = specs => {
if (set === true) {
const prop = camelToSnakeCase(name)
return function(value) {
return this._set(prop, value)
return function(value, obj) {
return this.setField(obj.$type, obj.$ref, prop, value)
}
}
@@ -157,16 +157,22 @@ export const makeEditObject = specs => {
const index = set.indexOf('.')
if (index === -1) {
const prop = camelToSnakeCase(set)
return function(value) {
return this._set(prop, value)
return function(value, obj) {
return this.setField(obj.$type, obj.$ref, prop, value)
}
}
const map = set.slice(0, index)
const prop = set.slice(index + 1)
const field = set.slice(0, index)
const entry = set.slice(index + 1)
return function(value, object) {
return this._updateObjectMapProperty(object, map, { [prop]: value })
return this.setFieldEntry(
object.$type,
object.$ref,
field,
entry,
value
)
}
}
@@ -249,16 +255,6 @@ export const makeEditObject = specs => {
const limits = checkLimits && {}
const object = this.getObject(id)
const _objectRef = object.$ref
const _setMethodPrefix = `${object.$type}.set_`
// Context used to execute functions.
const context = {
__proto__: this,
_set: (prop, value) =>
this.call(_setMethodPrefix + prop, _objectRef, prepareXapiParam(value)),
}
const set = (value, name) => {
if (value === undefined) {
return
@@ -287,7 +283,7 @@ export const makeEditObject = specs => {
}
}
const cb = () => spec.set.call(context, value, object)
const cb = () => spec.set.call(this, value, object)
const { constraints } = spec
if (constraints) {

View File

@@ -60,8 +60,9 @@ function checkParams(method, params) {
const result = schemaInspector.validate(
{
type: 'object',
properties: schema,
strict: true,
type: 'object',
},
params
)
@@ -261,11 +262,15 @@ export default class Api {
//
// The goal here is to standardize the calls by always providing
// an id parameter when possible to simplify calls to the API.
if (params != null && params.id === undefined) {
if (params?.id === undefined) {
const namespace = name.slice(0, name.indexOf('.'))
const id = params[namespace]
if (typeof id === 'string') {
params.id = id
const spec = method.params
if (spec !== undefined && 'id' in spec && !(namespace in spec)) {
const id = params[namespace]
if (typeof id === 'string') {
delete params[namespace]
params.id = id
}
}
}

View File

@@ -1,4 +1,5 @@
import createLogger from '@xen-orchestra/log'
import { createPredicate } from 'value-matcher'
import { ignoreErrors } from 'promise-toolbox'
import { invalidCredentials, noSuchObject } from 'xo-common/api-errors'
@@ -193,6 +194,14 @@ export default class {
}
}
async deleteAuthenticationTokens({ filter }) {
return Promise.all(
(await this._tokens.get())
.filter(createPredicate(filter))
.map(({ id }) => this.deleteAuthenticationToken(id))
)
}
async getAuthenticationToken(id) {
let token = await this._tokens.first(id)
if (token === undefined) {

View File

@@ -29,6 +29,7 @@ import {
ignoreErrors,
pFinally,
pFromEvent,
timeout,
} from 'promise-toolbox'
import Vhd, {
chainVhd,
@@ -41,6 +42,7 @@ import { type CallJob, type Executor, type Job } from '../jobs'
import { type Schedule } from '../scheduling'
import createSizeStream from '../../size-stream'
import parseDuration from '../../_parseDuration'
import {
type DeltaVmExport,
type DeltaVmImport,
@@ -286,7 +288,7 @@ const importers: $Dict<
xapi.importVm(xva, { srId: sr.$id })
)
await Promise.all([
xapi.addTag(vm.$id, 'restored from backup'),
vm.add_tags('restored from backup'),
xapi.editVm(vm.$id, {
name_label: `${metadata.vm.name_label} (${safeDateFormat(
metadata.timestamp
@@ -449,10 +451,8 @@ const disableVmHighAvailability = async (xapi: Xapi, vm: Vm) => {
}
return Promise.all([
xapi._setObjectProperties(vm, {
haRestartPriority: '',
}),
xapi.addTag(vm.$ref, 'HA disabled'),
vm.set_ha_restart_priority(''),
vm.add_tags('HA disabled'),
])
}
@@ -509,9 +509,17 @@ const disableVmHighAvailability = async (xapi: Xapi, vm: Vm) => {
// │ │ ├─ task.start(message: 'transfer')
// │ │ │ ├─ task.warning(message: string)
// │ │ │ └─ task.end(result: { size: number })
// │ │ │
// │ │ │ // in case of full backup, DR and CR
// │ │ ├─ task.start(message: 'clean')
// │ │ │ ├─ task.warning(message: string)
// │ │ │ └─ task.end
// │ │ │
// │ │ │ // in case of delta backup
// │ │ ├─ task.start(message: 'merge')
// │ │ │ ├─ task.warning(message: string)
// │ │ │ └─ task.end(result: { size: number })
// │ │ │
// │ │ └─ task.end
// │ └─ task.end
// └─ job.end
@@ -538,10 +546,11 @@ export default class BackupNg {
return this._runningRestores
}
constructor(app: any) {
constructor(app: any, { backup }) {
this._app = app
this._logger = undefined
this._runningRestores = new Set()
this._backupOptions = backup
app.on('start', async () => {
this._logger = await app.getLogger('restore')
@@ -937,7 +946,7 @@ export default class BackupNg {
message: 'clean backup metadata on VM',
parentId: taskId,
},
xapi._updateObjectMapProperty(vm, 'other_config', {
vm.update_other_config({
'xo:backup:datetime': null,
'xo:backup:deltaChainLength': null,
'xo:backup:exported': null,
@@ -1051,7 +1060,7 @@ export default class BackupNg {
message: 'add metadata to snapshot',
parentId: taskId,
},
xapi._updateObjectMapProperty(snapshot, 'other_config', {
snapshot.update_other_config({
'xo:backup:datetime': snapshot.snapshot_time,
'xo:backup:job': jobId,
'xo:backup:schedule': scheduleId,
@@ -1193,11 +1202,20 @@ export default class BackupNg {
)
): any)
const deleteOldBackups = () =>
wrapTask(
{
logger,
message: 'clean',
parentId: taskId,
},
this._deleteFullVmBackups(handler, oldBackups)
)
const deleteFirst = getSetting(settings, 'deleteFirst', [
remoteId,
])
if (deleteFirst) {
await this._deleteFullVmBackups(handler, oldBackups)
await deleteOldBackups()
}
await wrapTask(
@@ -1213,7 +1231,7 @@ export default class BackupNg {
await handler.outputFile(metadataFilename, jsonMetadata)
if (!deleteFirst) {
await this._deleteFullVmBackups(handler, oldBackups)
await deleteOldBackups()
}
}
)
@@ -1244,9 +1262,18 @@ export default class BackupNg {
listReplicatedVms(xapi, scheduleId, srId, vmUuid)
)
const deleteOldBackups = () =>
wrapTask(
{
logger,
message: 'clean',
parentId: taskId,
},
this._deleteVms(xapi, oldVms)
)
const deleteFirst = getSetting(settings, 'deleteFirst', [srId])
if (deleteFirst) {
await this._deleteVms(xapi, oldVms)
await deleteOldBackups()
}
const vm = await xapi.barrier(
@@ -1258,29 +1285,27 @@ export default class BackupNg {
result: () => ({ size: xva.size }),
},
xapi._importVm($cancelToken, fork, sr, vm =>
xapi._setObjectProperties(vm, {
nameLabel: `${metadata.vm.name_label} - ${
vm.set_name_label(
`${metadata.vm.name_label} - ${
job.name
} - (${safeDateFormat(metadata.timestamp)})`,
})
} - (${safeDateFormat(metadata.timestamp)})`
)
)
)
)
await Promise.all([
xapi.addTag(vm.$ref, 'Disaster Recovery'),
vm.add_tags('Disaster Recovery'),
disableVmHighAvailability(xapi, vm),
xapi._updateObjectMapProperty(vm, 'blocked_operations', {
start:
'Start operation for this vm is blocked, clone it if you want to use it.',
}),
xapi._updateObjectMapProperty(vm, 'other_config', {
'xo:backup:sr': srId,
}),
vm.update_blocked_operations(
'start',
'Start operation for this vm is blocked, clone it if you want to use it.'
),
vm.update_other_config('xo:backup:sr', srId),
])
if (!deleteFirst) {
await this._deleteVms(xapi, oldVms)
await deleteOldBackups()
}
}
)
@@ -1606,9 +1631,19 @@ export default class BackupNg {
listReplicatedVms(xapi, scheduleId, srId, vmUuid)
)
const deleteOldBackups = () =>
wrapTask(
{
logger,
message: 'clean',
parentId: taskId,
},
this._deleteVms(xapi, oldVms)
)
const deleteFirst = getSetting(settings, 'deleteFirst', [srId])
if (deleteFirst) {
await this._deleteVms(xapi, oldVms)
await deleteOldBackups()
}
const { vm } = await wrapTask(
@@ -1628,19 +1663,17 @@ export default class BackupNg {
)
await Promise.all([
xapi.addTag(vm.$ref, 'Continuous Replication'),
vm.add_tags('Continuous Replication'),
disableVmHighAvailability(xapi, vm),
xapi._updateObjectMapProperty(vm, 'blocked_operations', {
start:
'Start operation for this vm is blocked, clone it if you want to use it.',
}),
xapi._updateObjectMapProperty(vm, 'other_config', {
'xo:backup:sr': srId,
}),
vm.update_blocked_operations(
'start',
'Start operation for this vm is blocked, clone it if you want to use it.'
),
vm.update_other_config('xo:backup:sr', srId),
])
if (!deleteFirst) {
await this._deleteVms(xapi, oldVms)
await deleteOldBackups()
}
}
)
@@ -1667,9 +1700,7 @@ export default class BackupNg {
message: 'set snapshot.other_config[xo:backup:exported]',
parentId: taskId,
},
xapi._updateObjectMapProperty(snapshot, 'other_config', {
'xo:backup:exported': 'true',
})
snapshot.update_other_config('xo:backup:exported', 'true')
)
}
@@ -1769,6 +1800,16 @@ export default class BackupNg {
const path = `${dir}/${file}`
try {
const metadata = JSON.parse(String(await handler.readFile(path)))
if (metadata.mode === 'full') {
metadata.size = await timeout
.call(
handler.getSize(resolveRelativeFromFile(path, metadata.xva)),
parseDuration(this._backupOptions.vmBackupSizeTimeout)
)
.catch(err => {
log.warn(`_listVmBackups, getSize`, { err })
})
}
if (predicate === undefined || predicate(metadata)) {
Object.defineProperty(metadata, '_filename', {
value: path,

View File

@@ -372,7 +372,7 @@ export default class {
const { datetime } = parseVmBackupPath(file)
await Promise.all([
xapi.addTag(vm.$id, 'restored from backup'),
vm.add_tags('restored from backup'),
xapi.editVm(vm.$id, {
name_label: `${vm.name_label} (${shortDate(datetime * 1e3)})`,
}),
@@ -456,11 +456,9 @@ export default class {
// (Asynchronously) Identify snapshot as future base.
promise
.then(() => {
return srcXapi._updateObjectMapProperty(srcVm, 'other_config', {
[TAG_LAST_BASE_DELTA]: delta.vm.uuid,
})
})
.then(() =>
srcVm.update_other_config(TAG_LAST_BASE_DELTA, delta.vm.uuid)
)
::ignoreErrors()
return promise
@@ -974,12 +972,13 @@ export default class {
nameLabel: copyName,
})
targetXapi._updateObjectMapProperty(data.vm, 'blocked_operations', {
start:
'Start operation for this vm is blocked, clone it if you want to use it.',
})
await targetXapi.addTag(data.vm.$id, 'Disaster Recovery')
await Promise.all([
data.vm.add_tags('Disaster Recovery'),
data.vm.update_blocked_operations(
'start',
'Start operation for this vm is blocked, clone it if you want to use it.'
),
])
if (!deleteOldBackupsFirst) {
await this._removeVms(targetXapi, vmsToRemove)

View File

@@ -67,7 +67,7 @@ export default class {
const handlers = this._handlers
let handler = handlers[id]
if (handler === undefined) {
handler = handlers[id] = getHandler(remote, this._remoteOptions)
handler = getHandler(remote, this._remoteOptions)
try {
await handler.sync()
@@ -76,6 +76,8 @@ export default class {
ignoreErrors.call(this._updateRemote(id, { error: error.message }))
throw error
}
handlers[id] = handler
}
return handler

View File

@@ -386,14 +386,12 @@ export default class {
return value && JSON.parse(value)
},
setData: async (id, key, value) => {
await xapi._updateObjectMapProperty(
xapi.getObject(id),
'other_config',
{
[`xo:${camelToSnakeCase(key)}`]:
value !== null ? JSON.stringify(value) : value,
}
)
await xapi
.getObject(id)
.update_other_config(
`xo:${camelToSnakeCase(key)}`,
value !== null ? JSON.stringify(value) : value
)
// Register the updated object.
addObject(await xapi._waitObject(id))
@@ -457,6 +455,11 @@ export default class {
return xapi
}
// returns the XAPI object corresponding to an XO object
getXapiObject(xoObject) {
return this.getXapi(xoObject).getObjectByRef(xoObject._xapiRef)
}
_getXenServerStatus(id) {
const xapi = this._xapis[id]
return xapi === undefined

View File

@@ -39,8 +39,8 @@
"cross-env": "^5.1.3",
"event-to-promise": "^0.8.0",
"execa": "^1.0.0",
"fs-extra": "^7.0.0",
"get-stream": "^4.0.0",
"fs-extra": "^8.0.1",
"get-stream": "^5.1.0",
"index-modules": "^0.3.0",
"rimraf": "^2.6.2"
},

View File

@@ -1,7 +1,7 @@
{
"private": true,
"name": "xo-web",
"version": "5.40.1",
"version": "5.43.0",
"license": "AGPL-3.0",
"description": "Web interface client for Xen-Orchestra",
"keywords": [
@@ -85,7 +85,7 @@
"immutable": "^4.0.0-rc.9",
"index-modules": "^0.3.0",
"is-ip": "^2.0.0",
"jsonrpc-websocket-client": "^0.4.1",
"jsonrpc-websocket-client": "^0.5.0",
"kindof": "^2.0.0",
"lodash": "^4.6.1",
"loose-envify": "^1.1.0",

View File

@@ -228,7 +228,7 @@ export default {
userPage: 'Utilisateur',
// Original text: "No support"
noSupport: 'Pas de support',
noSupport: 'Pas de support professionnel',
// Original text: "Free upgrade!"
freeUpgrade: 'Mise à jour gratuite !',

View File

@@ -11,8 +11,8 @@ const messages = {
statusDisconnected: 'Disconnected',
statusLoading: 'Loading…',
errorPageNotFound: 'Page not found',
errorNoSuchItem: 'no such item',
errorUnknownItem: 'unknown {type}',
errorNoSuchItem: 'No such item',
errorUnknownItem: 'Unknown {type}',
memoryFree: '{memoryFree} RAM free',
date: 'Date',
notifications: 'Notifications',
@@ -32,8 +32,8 @@ const messages = {
multipathingDisabled: 'Multipathing disabled',
enableMultipathing: 'Enable multipathing',
disableMultipathing: 'Disable multipathing',
enableAllHostsMultipathing: 'Enable all hosts multipathing',
disableAllHostsMultipathing: 'Disable all hosts multipathing',
enableAllHostsMultipathing: 'Enable multipathing for all hosts',
disableAllHostsMultipathing: 'Disable multipathing for all hosts',
paths: 'Paths',
pbdDisconnected: 'PBD disconnected',
hasInactivePath: 'Has an inactive path',
@@ -166,8 +166,8 @@ const messages = {
homeHelp: 'Want some help?',
homeAddServer: 'Add server',
homeConnectServer: 'Connect servers',
homeOnlineDoc: 'Online Doc',
homeProSupport: 'Pro Support',
homeOnlineDoc: 'Online doc',
homeProSupport: 'Pro support',
homeNoVms: 'There are no VMs!',
homeNoVmsOr: 'Or…',
homeImportVm: 'Import VM',
@@ -206,7 +206,6 @@ const messages = {
homeSortBySize: 'Size',
homeSortByType: 'Type',
homeSortByUsage: 'Usage',
homeSortByvCPUs: 'vCPUs',
homeSortVmsBySnapshots: 'Snapshots',
homeSortByContainer: 'Container',
homeSortByPool: 'Pool',
@@ -219,9 +218,8 @@ const messages = {
homeResourceSet: 'Resource set: {resourceSet}',
highAvailability: 'High Availability',
srSharedType: 'Shared {type}',
srNotSharedType: 'Not shared {type}',
// ----- Home snapshots -----
// ----- Home snapshots -----
snapshotVmsName: 'Name',
snapshotVmsDescription: 'Description',
@@ -266,14 +264,14 @@ const messages = {
item: 'Item',
noSelectedValue: 'No selected value',
selectSubjects: 'Choose user(s) and/or group(s)',
selectObjects: 'Select Object(s)…',
selectObjects: 'Select object(s)…',
selectRole: 'Choose a role',
selectHosts: 'Select Host(s)…',
selectHosts: 'Select host(s)…',
selectHostsVms: 'Select object(s)…',
selectNetworks: 'Select Network(s)…',
selectNetworks: 'Select network(s)…',
selectPifs: 'Select PIF(s)…',
selectPools: 'Select Pool(s)…',
selectRemotes: 'Select Remote(s)…',
selectPools: 'Select pool(s)…',
selectRemotes: 'Select remote(s)…',
selectResourceSets: 'Select resource set(s)…',
selectResourceSetsVmTemplate: 'Select template(s)…',
selectResourceSetsSr: 'Select SR(s)…',
@@ -290,10 +288,9 @@ const messages = {
selectIp: 'Select IP(s)…',
selectIpPool: 'Select IP pool(s)…',
selectVgpuType: 'Select VGPU type(s)…',
fillRequiredInformations: 'Fill required information.',
fillOptionalInformations: 'Fill information (optional)',
selectTableReset: 'Reset',
selectCloudConfigs: 'Select Cloud Config(s)…',
selectCloudConfigs: 'Select cloud config(s)…',
// --- Dates/Scheduler ---
@@ -307,13 +304,10 @@ const messages = {
selectTableAllDay: 'Every day',
selectTableAllHour: 'Every hour',
selectTableAllMinute: 'Every minute',
schedulingReset: 'Reset',
unknownSchedule: 'Unknown',
timezonePickerUseLocalTime: 'Web browser timezone',
serverTimezoneOption: 'Server timezone ({value})',
cronPattern: 'Cron Pattern:',
backupEditNotFoundTitle: 'Cannot edit backup',
backupEditNotFoundMessage: 'Missing required info for edition',
successfulJobCall: 'Successful',
failedJobCall: 'Failed',
jobCallSkipped: 'Skipped',
@@ -345,7 +339,7 @@ const messages = {
runJob: 'Run job',
cancelJob: 'Cancel job',
runJobConfirm: 'Are you sure you want to run {backupType} {id} ({tag})?',
runJobVerbose: 'One shot running started. See overview for logs.',
runJobVerbose: 'Onetime job started. See overview for logs.',
jobEdit: 'Edit job',
jobDelete: 'Delete',
jobFinished: 'Finished',
@@ -376,7 +370,7 @@ const messages = {
deleteSelectedJobs: 'Delete selected jobs',
scheduleEnableAfterCreation: 'Enable immediately after creation',
scheduleEditMessage:
'You are editing Schedule {name} ({id}). Saving will override previous schedule state.',
'You are editing schedule {name} ({id}). Saving will override previous schedule state.',
jobEditMessage:
'You are editing job {name} ({id}). Saving will override previous job state.',
scheduleEdit: 'Edit schedule',
@@ -390,7 +384,7 @@ const messages = {
missingRetentions:
'The modes need at least a schedule with retention higher than 0',
missingExportRetention:
'The Backup mode and The Delta Backup mode require backup retention to be higher than 0!',
'The Backup mode and the Delta Backup mode require backup retention to be higher than 0!',
missingCopyRetention:
'The CR mode and The DR mode require replication retention to be higher than 0!',
missingSnapshotRetention:
@@ -410,25 +404,24 @@ const messages = {
noScheduledJobs: 'No scheduled jobs.',
legacySnapshotsLink: 'You can delete all your legacy backup snapshots.',
newSchedule: 'New schedule',
noJobs: 'No jobs found.',
noSchedules: 'No schedules found',
jobActionPlaceHolder: 'Select a xo-server API command',
jobActionPlaceHolder: 'Select an xo-server API command',
jobTimeoutPlaceHolder:
'Timeout (number of seconds after which a VM is considered failed)',
jobSchedules: 'Schedules',
jobScheduleNamePlaceHolder: 'Name of your schedule',
jobScheduleJobPlaceHolder: 'Select a Job',
jobScheduleJobPlaceHolder: 'Select a job',
jobOwnerPlaceholder: 'Job owner',
jobUserNotFound: "This job's creator no longer exists",
backupUserNotFound: "This backup's creator no longer exists",
redirectToMatchingVms: 'Click here to see the matching VMs',
migrateToBackupNg: 'Migrate to backup NG',
migrateToBackupNg: 'Migrate to Backup NG',
noMatchingVms: 'There are no matching VMs!',
allMatchingVms: '{icon} See the matching VMs ({nMatchingVms, number})',
backupOwner: 'Backup owner',
migrateBackupSchedule: 'Migrate to backup NG',
migrateBackupSchedule: 'Migrate to Backup NG',
migrateBackupScheduleMessage:
'This will migrate this backup to a backup NG. This operation is not reversible. Do you want to continue?',
'This will convert the old backup job to a Backup NG job. This operation is not reversible. Do you want to continue?',
runBackupNgJobConfirm: 'Are you sure you want to run {name} ({id})?',
cancelJobConfirm: 'Are you sure you want to cancel {name} ({id})?',
scheduleDstWarning:
@@ -448,38 +441,38 @@ const messages = {
smartBackup: 'Smart backup',
snapshotRetention: 'Snapshot retention',
backupName: 'Name',
useDelta: 'Use delta',
offlineSnapshot: 'Offline snapshot',
offlineSnapshotInfo: 'Shutdown VMs before snapshotting them',
timeout: 'Timeout',
timeoutInfo: 'Number of hours after which a job is considered failed',
fullBackupInterval: 'Full backup interval',
timeoutUnit: 'in hours',
dbAndDrRequireEnterprisePlan: 'Delta Backup and DR require Enterprise plan',
crRequiresPremiumPlan: 'CR requires Premium plan',
timeoutUnit: 'In hours',
dbAndDrRequireEnterprisePlan:
'Delta Backup and DR require an Enterprise plan',
crRequiresPremiumPlan: 'CR requires a Premium plan',
smartBackupModeTitle: 'Smart mode',
backupTargetRemotes: 'Target remotes (for Export)',
backupTargetSrs: 'Target SRs (for Replication)',
backupTargetRemotes: 'Target remotes (for export)',
backupTargetSrs: 'Target SRs (for replication)',
localRemoteWarningTitle: 'Local remote selected',
crOnThickProvisionedSrWarning:
'Tip: using a thin-provisioned storage will consume less space. Please click on the icon to get more information',
'Tip: Using thin-provisioned storage will consume less space. Please click on the icon to get more information',
vmsOnThinProvisionedSrTip:
'Tip: creating VMs on a thin-provisioned storage will consume less space when backing them up. Please click on the icon to get more information',
'Tip: Creating VMs on thin-provisioned storage will consume less space. Please click on the icon to get more information',
deltaBackupOnOutdatedXenServerWarning:
'Delta Backup and Continuous Replication require at least XenServer 6.5.',
backupNgLinkToDocumentationMessage:
'Click for more information about the backup modes.',
'Click for more information about the backup methods.',
localRemoteWarningMessage:
'Warning: local remotes will use limited XOA disk space. Only for advanced users.',
'Warning: Local remotes will use limited XOA disk space. Only for advanced users.',
backupVersionWarning:
'Warning: this feature works only with XenServer 6.5 or newer.',
'Warning: This feature works only with XenServer 6.5 or newer.',
editBackupVmsTitle: 'VMs',
editBackupSmartStatusTitle: 'VMs statuses',
editBackupSmartResidentOn: 'Resident on',
editBackupSmartNotResidentOn: 'Not resident on',
editBackupSmartPools: 'Pools',
editBackupSmartTags: 'Tags',
sampleOfMatchingVms: 'Sample of matching Vms',
sampleOfMatchingVms: 'Sample of matching VMs',
backupReplicatedVmsInfo:
'Replicated VMs (VMs with Continuous Replication or Disaster Recovery tag) must be excluded!',
editBackupSmartTagsTitle: 'VMs Tags',
@@ -495,21 +488,19 @@ const messages = {
'Delete old backups before backing up the VMs. If the new backup fails, you will lose your old backups.',
// ------ New Remote -----
remoteList: 'Remote stores for backup',
newRemote: 'New File System Remote',
newRemote: 'New file system remote',
remoteTypeLocal: 'Local',
remoteTypeNfs: 'NFS',
remoteTypeSmb: 'SMB',
remoteType: 'Type',
remoteSmbWarningMessage:
'SMB remotes are meant to work on Windows Server. For other systems (Linux Samba, which means almost all NAS), please use NFS.',
'SMB remotes are meant to work with Windows Server. For other systems (Linux Samba, which means almost all NAS), please use NFS.',
remoteTestTip: 'Test your remote',
testRemote: 'Test Remote',
testRemote: 'Test remote',
remoteTestFailure: 'Test failed for {name}',
remoteTestSuccess: 'Test passed for {name}',
remoteTestError: 'Error',
remoteTestStep: 'Test Step',
remoteTestFile: 'Test file',
remoteTestStep: 'Test step',
remoteTestName: 'Test name',
remoteTestNameFailure: 'Remote name already exists!',
remoteTestSuccessMessage: 'The remote appears to work correctly',
@@ -532,33 +523,29 @@ const messages = {
remoteSpeedInfo: 'Read and write rate speed performed during latest test',
remoteOptions: 'Options',
remoteShare: 'Share',
remoteAction: 'Action',
remoteAuth: 'Auth',
remoteMounted: 'Mounted',
remoteUnmounted: 'Unmounted',
remoteDeleteTip: 'Delete',
remoteDeleteSelected: 'Delete selected remotes',
remoteNamePlaceHolder: 'remote name *',
remoteMyNamePlaceHolder: 'Name *',
remoteLocalPlaceHolderPath: '/path/to/backup',
remoteNfsPlaceHolderHost: 'host *',
remoteNfsPlaceHolderHost: 'Host *',
remoteNfsPlaceHolderPort: 'Port',
remoteNfsPlaceHolderPath: 'path/to/backup',
remoteNfsPlaceHolderOptions: 'Custom mount options. Default: vers=3',
remoteSmbPlaceHolderRemotePath: 'subfolder [path\\\\to\\\\backup]',
remoteSmbPlaceHolderRemotePath: 'Subfolder [path\\\\to\\\\backup]',
remoteSmbPlaceHolderUsername: 'Username',
remoteSmbPlaceHolderPassword: 'Password',
remoteSmbPlaceHolderDomain: 'Domain',
remoteSmbPlaceHolderAddressShare: '<address>\\\\<share> *',
remoteSmbPlaceHolderOptions: 'Custom mount options',
remotePlaceHolderPassword: 'password(fill to edit)',
remotePlaceHolderPassword: 'Password(fill to edit)',
// ------ New Storage -----
newSrTitle: 'Create a new SR',
newSrGeneral: 'General',
newSrTypeSelection: 'Select Storage Type:',
newSrTypeSelection: 'Select storage type:',
newSrSettings: 'Settings',
newSrUsage: 'Storage Usage',
newSrUsage: 'Storage usage',
newSrSummary: 'Summary',
newSrHost: 'Host',
newSrType: 'Type',
@@ -569,11 +556,11 @@ const messages = {
newSrIqn: 'IQN',
newSrLun: 'LUN',
newSrNoHba: 'No HBA devices',
newSrAuth: 'with auth.',
newSrUsername: 'User Name',
newSrAuth: 'With auth.',
newSrUsername: 'User name',
newSrPassword: 'Password',
newSrDevice: 'Device',
newSrInUse: 'in use',
newSrInUse: 'In use',
newSrSize: 'Size',
newSrCreate: 'Create',
newSrNamePlaceHolder: 'Storage name',
@@ -584,38 +571,39 @@ const messages = {
newSrPasswordPlaceHolder: 'Password',
newSrLvmDevicePlaceHolder: 'Device, e.g /dev/sda…',
newSrLocalPathPlaceHolder: '/path/to/directory',
newSrUseNfs4: 'Use NFSv4',
newSrNfsDefaultVersion: 'Default NFS version',
newSrNfsOptions: 'Comma delimited NFS options',
newSrNfs: 'NFS version',
reattachNewSrTooltip: 'Reattach SR',
// ------ New Newtork -----
createNewNetworkNoPermission: 'You have no permission to create a network',
// ------ New Network -----
createNewNetworkNoPermission:
'You do not have permission to create a network',
createNewNetworkOn: 'Create a new network on {select}',
// ----- Acls, Users, Groups ------
// ----- ACLs, Users, Groups ------
subjectName: 'Users/Groups',
objectName: 'Object',
aclNoneFound: 'No acls found',
roleName: 'Role',
aclCreate: 'Create',
newGroupName: 'New Group Name',
createGroup: 'Create Group',
newGroupName: 'New group name',
createGroup: 'Create group',
createGroupButton: 'Create',
deleteGroup: 'Delete Group',
deleteGroup: 'Delete group',
deleteGroupConfirm: 'Are you sure you want to delete this group?',
removeUserFromGroup: 'Remove user from Group',
removeUserFromGroup: 'Remove user from group',
deleteUserConfirm: 'Are you sure you want to delete this user?',
deleteUser: 'Delete user',
deleteSelectedUsers: 'Delete selected users',
deleteUsersModalTitle: 'Delete user{nUsers, plural, one {} other {s}}',
deleteUsersModalMessage:
'Are you sure you want to delete {nUsers, number} user{nUsers, plural, one {} other {s}}?',
noUser: 'no user',
unknownUser: 'unknown user',
noUser: 'No user',
unknownUser: 'Unknown user',
noGroupFound: 'No group found',
groupNameColumn: 'Name',
groupUsersColumn: 'Users',
addUserToGroupColumn: 'Add User',
addUserToGroupColumn: 'Add user',
userNameColumn: 'Username',
userGroupsColumn: 'Member of',
userCountGroups: '{nGroups, number} group{nGroups, plural, one {} other {s}}',
@@ -629,7 +617,7 @@ const messages = {
adminLabel: 'Admin',
noUserInGroup: 'No user in group',
countUsers: '{users, number} user{users, plural, one {} other {s}}',
selectPermission: 'Select Permission',
selectPermission: 'Select permission',
deleteAcl: 'Delete ACL',
deleteSelectedAcls: 'Delete selected ACLs',
deleteAclsModalTitle: 'Delete ACL{nAcls, plural, one {} other {s}}',
@@ -645,8 +633,7 @@ const messages = {
unknownPluginError: 'Unknown error',
purgePluginConfiguration: 'Purge plugin configuration',
purgePluginConfigurationQuestion:
'Are you sure you want to purge this configuration ?',
editPluginConfiguration: 'Edit',
'Are you sure you want to purge this configuration?',
cancelPluginEdition: 'Cancel',
pluginConfigurationSuccess: 'Plugin configuration',
pluginConfigurationChanges: 'Plugin configuration successfully saved!',
@@ -660,15 +647,12 @@ const messages = {
filterName: 'Name:',
filterValue: 'Value:',
saveNewFilterTitle: 'Save new filter',
setUserFiltersTitle: 'Set custom filters',
setUserFiltersBody: 'Are you sure you want to set custom filters?',
removeUserFilterTitle: 'Remove custom filter',
removeUserFilterBody: 'Are you sure you want to remove custom filter?',
removeUserFilterBody: 'Are you sure you want to remove the custom filter?',
defaultFilter: 'Default filter',
defaultFilters: 'Default filters',
customFilters: 'Custom filters',
customizeFilters: 'Customize filters',
saveCustomFilters: 'Save custom filters',
// ----- VM actions ------
startVmLabel: 'Start',
@@ -691,7 +675,6 @@ const messages = {
copyVmLabel: 'Copy',
cloneVmLabel: 'Clone',
fastCloneVmLabel: 'Fast clone',
convertVmToTemplateLabel: 'Convert to template',
vmConsoleLabel: 'Console',
// ----- SR advanced tab -----
@@ -727,19 +710,18 @@ const messages = {
displayAllVMs: 'Display all VMs of this pool',
licenseRestrictions: 'License restrictions',
licenseRestrictionsModalTitle:
'Warning: you are using a Free XenServer license',
actionsRestricted: 'Some actions will be restricted.',
'Warning: You are using a Free XenServer license',
actionsRestricted: 'Some functionality is restricted.',
counterRestrictionsOptions: 'You can:',
counterRestrictionsOptionsXcp:
'upgrade to XCP-ng for free to get rid of these restrictions',
counterRestrictionsOptionsXsLicense: 'or get a commercial Citrix license',
// ----- Pool tabs -----
hostsTabName: 'Hosts',
vmsTabName: 'Vms',
srsTabName: 'Srs',
vmsTabName: 'VMs',
srsTabName: 'SRs',
// ----- Pool advanced tab -----
poolEditAll: 'Edit all',
poolEditRemoteSyslog: 'Edit remote syslog for all hosts',
poolHaStatus: 'High Availability',
poolHaEnabled: 'Enabled',
poolHaDisabled: 'Disabled',
@@ -750,7 +732,6 @@ const messages = {
// ----- Pool host tab -----
hostNameLabel: 'Name',
hostDescription: 'Description',
hostMemory: 'Memory',
noHost: 'No hosts',
memoryLeftTooltip: '{used}% used ({free} free)',
// ----- Pool network tab -----
@@ -767,8 +748,6 @@ const messages = {
poolNetworkPifDetached: 'Disconnected',
showPifs: 'Show PIFs',
hidePifs: 'Hide PIFs',
showDetails: 'Show details',
hideDetails: 'Hide details',
// ----- Pool stats tab -----
poolNoStats: 'No stats',
poolAllHosts: 'All hosts',
@@ -777,9 +756,9 @@ const messages = {
addVmLabel: 'Add VM',
addHostLabel: 'Add Host',
missingPatchesPool:
'The pool needs to install {nMissingPatches, number} patch{nMissingPatches, plural, one {} other {es}}. This operation may be long.',
'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 be long.',
'This host needs 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',
@@ -796,7 +775,7 @@ const messages = {
rebootHostLabel: 'Reboot',
noHostsAvailableErrorTitle: 'Error while restarting host',
noHostsAvailableErrorMessage:
'Some VMs cannot be migrated before restarting this host. Please try force reboot.',
'Some VMs cannot be migrated without first rebooting this host. Please try force reboot.',
failHostBulkRestartTitle: 'Error while restarting hosts',
failHostBulkRestartMessage:
'{failedHosts, number}/{totalHosts, number} host{failedHosts, plural, one {} other {s}} could not be restarted.',
@@ -812,6 +791,7 @@ const messages = {
memoryHostState:
'RAM: {memoryUsed} used on {memoryTotal} ({memoryFree} free)',
hardwareHostSettingsLabel: 'Hardware',
hyperThreading: 'Hyper-threading (SMT)',
hostAddress: 'Address',
hostStatus: 'Status',
hostBuildNumber: 'Build number',
@@ -821,9 +801,9 @@ const messages = {
hostMultipathingPaths:
'{nActives, number} of {nPaths, number} path{nPaths, plural, one {} other {s}} ({ nSessions, number } iSCSI session{nSessions, plural, one {} other {s}})',
hostMultipathingRequiredState:
'This action will not be fulfilled if a VM is in a running state. Please ensure that all VMs are evacuated or stopped before doing this action!',
'This action will not be fulfilled if a VM is in a running state. Please ensure that all VMs are evacuated or stopped before performing this action!',
hostMultipathingWarning:
'The host{nHosts, plural, one {} other {s}} will lose the connection to the SRs. Do you want to continue?',
'The host{nHosts, plural, one {} other {s}} will lose their connection to the SRs. Do you want to continue?',
hostXenServerVersion: 'Version',
hostStatusEnabled: 'Enabled',
hostStatusDisabled: 'Disabled',
@@ -854,7 +834,6 @@ const messages = {
'Supplemental pack successfully installed.',
// ----- Host net tabs -----
networkCreateButton: 'Add a network',
networkCreateBondedButton: 'Add a bonded network',
pifDeviceLabel: 'Device',
pifNetworkLabel: 'Network',
pifVlanLabel: 'VLAN',
@@ -864,11 +843,7 @@ const messages = {
pifMtuLabel: 'MTU',
pifSpeedLabel: 'Speed',
pifStatusLabel: 'Status',
pifStatusConnected: 'Connected',
pifStatusDisconnected: 'Disconnected',
pifNoInterface: 'No physical interface detected',
pifInUse: 'This interface is currently in use',
pifAction: 'Action',
defaultLockingMode: 'Default locking mode',
pifConfigureIp: 'Configure IP address',
configIpErrorTitle: 'Invalid parameters',
@@ -879,9 +854,7 @@ const messages = {
gateway: 'Gateway',
// ----- Host storage tabs -----
addSrDeviceButton: 'Add a storage',
srNameLabel: 'Name',
srType: 'Type',
pbdAction: 'Action',
pbdStatus: 'Status',
pbdStatusConnected: 'Connected',
pbdStatusDisconnected: 'Disconnected',
@@ -898,13 +871,9 @@ const messages = {
patchVersion: 'Version',
patchApplied: 'Applied date',
patchSize: 'Size',
patchStatus: 'Status',
patchStatusApplied: 'Applied',
patchStatusNotApplied: 'Missing patches',
patchNothing: 'No patches detected',
patchReleaseDate: 'Release date',
patchGuidance: 'Guidance',
patchAction: 'Action',
hostAppliedPatches: 'Applied patches',
hostMissingPatches: 'Missing patches',
hostUpToDate: 'Host up-to-date!',
@@ -923,7 +892,6 @@ const messages = {
changelogDate: 'Date',
changelogDescription: 'Description',
// ----- Pool patch tabs -----
refreshPatches: 'Refresh patches',
install: 'Install',
installPatchesTitle: 'Install patch{nPatches, plural, one {} other {es}}',
installPatchesContent:
@@ -932,7 +900,6 @@ const messages = {
confirmPoolPatch:
'Are you sure you want to install all the patches on this pool?',
poolNeedsDefaultSr: 'The pool needs a default SR to install the patches.',
selectDefaultSr: 'Select a default SR',
vmsHaveCds:
'{nVms, number} VM{nVms, plural, one {} other {s}} {nVms, plural, one {has} other {have}} CDs',
ejectCds: 'Eject CDs',
@@ -952,10 +919,10 @@ const messages = {
networkTabName: 'Network',
disksTabName: 'Disk{disks, plural, one {} other {s}}',
powerStateHalted: 'halted',
powerStateRunning: 'running',
powerStateSuspended: 'suspended',
powerStatePaused: 'paused',
powerStateHalted: 'Halted',
powerStateRunning: 'Running',
powerStateSuspended: 'Suspended',
powerStatePaused: 'Paused',
// ----- VM home -----
vmCurrentStatus: 'Current status:',
@@ -989,8 +956,8 @@ const messages = {
copyToClipboardLabel: 'Copy',
ctrlAltDelButtonLabel: 'Ctrl+Alt+Del',
tipLabel: 'Tip:',
hideHeaderTooltip: 'Hide infos',
showHeaderTooltip: 'Show infos',
hideHeaderTooltip: 'Hide info',
showHeaderTooltip: 'Show info',
// ----- VM container tab -----
containerName: 'Name',
@@ -1006,14 +973,12 @@ const messages = {
containerRestart: 'Restart this container',
// ----- VM disk tab -----
vdiAction: 'Action',
vdiAttachDeviceButton: 'Attach disk',
vbdCreateDeviceButton: 'New disk',
vdiBootOrder: 'Boot order',
vdiNameLabel: 'Name',
vdiNameDescription: 'Description',
vdiPool: 'Pool',
vdiDisconnect: 'Disconnect',
vdiTags: 'Tags',
vdiSize: 'Size',
vdiSr: 'SR',
@@ -1025,18 +990,16 @@ const messages = {
vdiMigrateNoSrMessage: 'A target SR is required to migrate a VDI',
vdiForget: 'Forget',
vdiRemove: 'Remove VDI',
noControlDomainVdis: 'No VDIs attached to Control Domain',
noControlDomainVdis: 'No VDIs attached to control domain',
vbdBootableStatus: 'Boot flag',
vbdDevice: 'Device',
vbdStatus: 'Status',
vbdStatusConnected: 'Connected',
vbdStatusDisconnected: 'Disconnected',
vbdNoVbd: 'No disks',
vbdConnect: 'Connect VBD',
vbdDisconnect: 'Disconnect VBD',
vbdBootable: 'Bootable',
vbdReadonly: 'Readonly',
vbdAction: 'Action',
vbdCreate: 'Create',
vbdAttach: 'Attach',
vbdNamePlaceHolder: 'Disk name',
@@ -1056,11 +1019,10 @@ const messages = {
notEnoughSpaceInResourceSet:
'Not enough space in resource set {resourceSet} ({spaceLeft} left)',
warningVdiSr:
"The VDIs' SRs must either be shared or on the same host for the VM to be able to start.",
"The VDIs' SRs must either be shared or on the same host for the VM to be able to start.",
// ----- VM network tab -----
vifCreateDeviceButton: 'New device',
vifNoInterface: 'No interface',
vifDeviceLabel: 'Device',
vifMacLabel: 'MAC address',
vifMtuLabel: 'MTU',
@@ -1081,13 +1043,12 @@ const messages = {
'Network locked and no IPs are allowed for this interface',
vifUnLockedNetwork: 'Network not locked',
vifUnknownNetwork: 'Unknown network',
vifAction: 'Action',
vifCreate: 'Create',
// ----- VM snapshot tab -----
noSnapshots: 'No snapshots',
newSnapshotWithMemory: 'New snapshot with memory',
snapshotMemorySaved: 'memory saved',
snapshotMemorySaved: 'Memory saved',
snapshotCreateButton: 'New snapshot',
tipCreateSnapshotLabel: 'Just click on the snapshot button to create one!',
revertSnapshot: 'Revert VM to this snapshot',
@@ -1098,7 +1059,6 @@ const messages = {
snapshotDate: 'Creation date',
snapshotName: 'Name',
snapshotDescription: 'Description',
snapshotAction: 'Action',
snapshotQuiesce: 'Quiesced snapshot',
vmRevertSuccessfulTitle: 'Revert successful',
vmRevertSuccessfulMessage: 'VM successfully reverted',
@@ -1159,10 +1119,8 @@ const messages = {
vmCoresPerSocketIncorrectValue: 'Incorrect cores per socket value',
vmCoresPerSocketIncorrectValueSolution:
'Please change the selected value to fix it.',
vmHaDisabled: 'disabled',
vmHaDisabled: 'Disabled',
vmMemoryLimitsLabel: 'Memory limits (min/max)',
vmMaxVcpus: 'vCPUs max:',
vmMaxRam: 'Memory max:',
vmVgpu: 'vGPU',
vmVgpus: 'GPUs',
vmVgpuNone: 'None',
@@ -1174,13 +1132,15 @@ const messages = {
addAclsErrorMessage: 'User(s)/group(s) and role are required.',
removeAcl: 'Delete',
moreAcls: '{nAcls, number} more…',
vmBootFirmware: 'Boot firmware',
vmDefaultBootFirmwareLabel: 'default (bios)',
vmBootFirmwareWarningMessage:
"You're about to change your boot firmware. This is still experimental in CH/XCP-ng 8.0. Are you sure you want to continue?",
// ----- VM placeholders -----
vmHomeNamePlaceholder: 'Long click to add a name',
vmHomeDescriptionPlaceholder: 'Long click to add a description',
vmViewNamePlaceholder: 'Click to add a name',
vmViewDescriptionPlaceholder: 'Click to add a description',
// ----- Templates -----
@@ -1258,9 +1218,6 @@ const messages = {
statsDashboardSelectObjects: 'Select',
metricsLoading: 'Loading…',
// ----- Visualizations -----
comingSoon: 'Coming soon!',
// ----- Health -----
orphanedVdis: 'Orphaned snapshot VDIs',
orphanedVms: 'Orphaned VMs snapshot',
@@ -1281,7 +1238,6 @@ const messages = {
alarmContent: 'Content',
alarmObject: 'Issue on',
alarmPool: 'Pool',
alarmRemoveAll: 'Remove all alarms',
spaceLeftTooltip: '{used}% used ({free} left)',
// ----- New VM -----
@@ -1339,7 +1295,6 @@ const messages = {
newVmCreateVms: 'Create VMs',
newVmCreateVmsConfirm: 'Are you sure you want to create {nbVms, number} VMs?',
newVmMultipleVms: 'Multiple VMs:',
newVmSelectResourceSet: 'Select a resource set:',
newVmMultipleVmsPattern: 'Name pattern:',
newVmMultipleVmsPatternPlaceholder: 'e.g.: \\{name\\}_%',
newVmFirstIndex: 'First index:',
@@ -1379,9 +1334,6 @@ const messages = {
deleteResourceSetQuestion:
'Are you sure you want to delete this resource set?',
resourceSetMissingObjects: 'Missing objects:',
resourceSetVcpus: 'vCPUs',
resourceSetMemory: 'Memory',
resourceSetStorage: 'Storage',
unknownResourceSetValue: 'Unknown',
availableHosts: 'Available hosts',
excludedHosts: 'Excluded hosts',
@@ -1393,8 +1345,6 @@ const messages = {
maxDiskSpace: 'Maximum disk space',
ipPool: 'IP pool',
quantity: 'Quantity',
noResourceSetLimits: 'No limits.',
remainingResource: 'Remaining:',
usedResourceLabel: 'Used',
availableResourceLabel: 'Available',
resourceSetQuota: 'Used: {usage} (Total: {total})',
@@ -1457,7 +1407,6 @@ const messages = {
listRemote: 'List Remote',
simpleBackup: 'simple',
delta: 'delta',
restoreBackups: 'Restore Backups',
noBackups: 'There are no backups!',
restoreBackupsInfo: 'Click on a VM to display restore options',
restoreDeltaBackupsInfo:
@@ -1466,7 +1415,6 @@ const messages = {
remoteDisabled: 'Disabled',
enableRemote: 'Enable',
disableRemote: 'Disable',
remoteError: 'Error',
remoteErrorMessage:
'The URL ({url}) is invalid (colon in path). Click this button to change the URL to {newUrl}.',
backupVmNameColumn: 'VM Name',
@@ -1477,11 +1425,6 @@ const messages = {
availableBackupsColumn: 'Available Backups',
backupRestoreErrorTitle: 'Missing parameters',
backupRestoreErrorMessage: 'Choose a SR and a backup',
backupRestoreSelectDefaultSr: 'Select default SR…',
backupRestoreChooseSrForEachVdis: 'Choose a SR for each VDI',
backupRestoreVdiLabel: 'VDI',
backupRestoreSrLabel: 'SR',
displayBackup: 'Display backups',
importBackupTitle: 'Import VM',
importBackupMessage: 'Starting your backup import',
vmsToBackup: 'VMs to backup',
@@ -1515,7 +1458,6 @@ const messages = {
'Are you sure you want to delete all the backups from {nVms, number} VM{nVms, plural, one {} other {s}}?',
deleteVmBackupsBulkConfirmText:
'delete {nBackups} backup{nBackups, plural, one {} other {s}}',
unknownJob: 'Unknown job',
bulkDeleteMetadataBackupsTitle: 'Delete metadata backups',
bulkDeleteMetadataBackupsMessage:
'Are you sure you want to delete all the backups from {nMetadataBackups, number} metadata backup{nMetadataBackups, plural, one {} other {s}}?',
@@ -1530,9 +1472,7 @@ const messages = {
restoreFilesSelectBackup: 'Select a backup…',
restoreFilesSelectDisk: 'Select a disk…',
restoreFilesSelectPartition: 'Select a partition…',
restoreFilesSelectFolderPath: 'Folder path',
restoreFilesSelectFiles: 'Select a file…',
restoreFileContentNotFound: 'Content not found',
restoreFilesNoFilesSelected: 'No files selected',
restoreFilesSelectedFiles: 'Selected files ({files}):',
restoreFilesSelectedFilesAndFolders: 'Selected files/folders ({files}):',
@@ -1671,12 +1611,10 @@ const messages = {
deleteOrphanedVdisModalTitle: 'Delete orphaned snapshot VDIs',
deleteOrphanedVdisModalMessage:
'Are you sure you want to delete {nVdis, number} orphaned snapshot VDI{nVdis, plural, one {} other {s}}?',
removeAllLogsModalTitle: 'Remove all logs',
removeAllLogsModalWarning: 'Are you sure you want to remove all logs?',
definitiveMessageModal: 'This operation is definitive.',
existingLunModalTitle: 'Previous LUN Usage',
existingLunModalText:
'This LUN has been previously used as a Storage by a XenServer host. All data will be lost if you choose to continue the SR creation.',
'This LUN has been previously used as storage by a XenServer host. All data will be lost if you choose to continue with the SR creation.',
alreadyRegisteredModal: 'Replace current registration?',
alreadyRegisteredModalText:
'Your XO appliance is already registered to {email}, do you want to forget and replace this registration ?',
@@ -1707,7 +1645,6 @@ const messages = {
serverHost: 'Host',
serverUsername: 'Username',
serverPassword: 'Password',
serverAction: 'Action',
serverReadOnly: 'Read Only',
serverUnauthorizedCertificates: 'Unauthorized Certificates',
serverAllowUnauthorizedCertificates: 'Allow Unauthorized Certificates',
@@ -1723,7 +1660,6 @@ const messages = {
serverAddFailed: 'Adding server failed',
serverStatus: 'Status',
serverConnectionFailed: 'Connection failed. Click for more information.',
serverConnecting: 'Connecting…',
serverConnected: 'Connected',
serverDisconnected: 'Disconnected',
serverAuthFailed: 'Authentication error',
@@ -1735,7 +1671,6 @@ const messages = {
// ----- Copy VM -----
copyVm: 'Copy VM',
copyVmName: 'Name',
copyVmNamePattern: 'Name pattern',
copyVmNamePlaceholder: 'If empty: name of the copied VM',
copyVmNamePatternPlaceholder: 'e.g.: "\\{name\\}_COPY"',
copyVmSelectSr: 'Select SR',
@@ -1786,12 +1721,11 @@ const messages = {
addHostNoHostMessage: 'No host selected to be added',
// ----- About View -----
xenOrchestra: 'Xen Orchestra',
xenOrchestraServer: 'Xen Orchestra server',
xenOrchestraWeb: 'Xen Orchestra web client',
noProSupport: 'No pro support provided!',
noProductionUse: 'Use in production at your own risks',
downloadXoaFromWebsite: 'You can download our turnkey appliance at {website}',
noProSupport: 'Professional support missing!',
noProductionUse: 'Use in production at your own risk',
downloadXoaFromWebsite: 'You can download the turnkey appliance at {website}',
bugTracker: 'Bug Tracker',
bugTrackerText: 'Issues? Report it!',
community: 'Community',
@@ -1817,7 +1751,6 @@ const messages = {
'This feature is not available in your version, contact your administrator to know more.',
// ----- Updates View -----
updateTitle: 'Updates',
registration: 'Registration',
settings: 'Settings',
proxySettings: 'Proxy settings',
@@ -1861,6 +1794,11 @@ const messages = {
upgradeWarningTitle: 'Upgrade warning',
upgradeWarningMessage:
'You have some backup jobs in progress. If you upgrade now, these jobs will be interrupted! Are you sure you want to continue?',
releaseChannels: 'Release channels',
unlistedChannel: 'unlisted channel',
unlistedChannelName: 'Unlisted channel name',
selectChannel: 'Select channel',
changeChannel: 'Change channel',
// ----- OS Disclaimer -----
disclaimerTitle: 'Xen Orchestra from the sources',
@@ -1907,11 +1845,15 @@ const messages = {
pwdChangeErrorBody:
'The old password provided is incorrect. Your password has not been changed.',
changePasswordOk: 'OK',
forgetTokens: 'Forget all connection tokens',
forgetTokensExplained:
'This will prevent other clients from authenticating with existing tokens but will not kill active sessions',
forgetTokensSuccess: 'Successfully forgot connection tokens',
forgetTokensError: 'Error while forgetting connection tokens',
sshKeys: 'SSH keys',
newSshKey: 'New SSH key',
deleteSshKey: 'Delete',
deleteSshKeys: 'Delete selected SSH keys',
noSshKeys: 'No SSH keys',
newSshKeyModalTitle: 'New SSH key',
sshKeyErrorTitle: 'Invalid key',
sshKeyErrorMessage: 'An SSH key requires both a title and a key.',
@@ -1934,22 +1876,16 @@ const messages = {
others: 'Others',
// ----- Logs -----
loadingLogs: 'Loading logs…',
logUser: 'User',
logMethod: 'Method',
logParams: 'Params',
logMessage: 'Message',
logSuggestXcpNg: 'Use XCP-ng to get rid of restrictions',
logError: 'Error',
logTitle: 'Logs',
logDisplayDetails: 'Display details',
logDownload: 'Download log',
logTime: 'Date',
logNoStackTrace: 'No stack trace',
logNoParams: 'No params',
logDelete: 'Delete log',
logsDelete: 'Delete logs',
logsThreePerPage: '3 / page',
logsTenPerPage: '10 / page',
logsJobId: 'Job ID',
logsJobName: 'Job name',
logsBackupTime: 'Backup time',
@@ -1973,12 +1909,9 @@ const messages = {
// ----- IPs ------
ipPoolName: 'Name',
ipPoolIps: 'IPs',
ipPoolIpsPlaceholder: 'IPs (e.g.: 1.0.0.12-1.0.0.17;1.0.0.23)',
ipPoolNetworks: 'Networks',
ipsNoIpPool: 'No IP pools',
ipsCreate: 'Create',
ipsDeleteAllTitle: 'Delete all IP pools',
ipsDeleteAllMessage: 'Are you sure you want to delete all the IP pools?',
ipsVifs: 'VIFs',
ipsNotUsed: 'Not used',
ipPoolUnknownVif: 'unknown VIF',
@@ -2027,7 +1960,7 @@ const messages = {
// ----- Config -----
noConfigFile: 'No config file selected',
importTip:
'Try dropping a config file here, or click to select a config file to upload.',
'Try dropping a config file here or click to select a config file to upload.',
config: 'Config',
importConfig: 'Import',
importConfigSuccess: 'Config file successfully imported',
@@ -2039,8 +1972,6 @@ const messages = {
// ----- SR -----
srReconnectAllModalTitle: 'Reconnect all hosts',
srReconnectAllModalMessage: 'This will reconnect this SR to all its hosts.',
srsReconnectAllModalMessage:
'This will reconnect each selected SR to its host (local SR) or to every hosts of its pool (shared SR).',
srDisconnectAllModalTitle: 'Disconnect all hosts',
srDisconnectAllModalMessage:
'This will disconnect this SR from all its hosts.',
@@ -2058,8 +1989,6 @@ const messages = {
// ----- XOSAN -----
xosanTitle: 'XOSAN',
xosanSrTitle: 'Xen Orchestra SAN SR',
xosanAvailableSrsTitle: 'Select local SRs (lvm)',
xosanSuggestions: 'Suggestions',
xosanDisperseWarning:
'Warning: using disperse layout is not recommended right now. Please read {link}.',
@@ -2067,7 +1996,6 @@ const messages = {
xosanHost: 'Host',
xosanHosts: 'Connected Hosts',
xosanPool: 'Pool',
xosanVolumeId: 'Volume ID',
xosanSize: 'Size',
xosanUsedSpace: 'Used space',
xosanLicense: 'License',
@@ -2078,8 +2006,6 @@ const messages = {
xosanNeedRestart:
'Some hosts need their toolstack to be restarted before you can create an XOSAN',
xosanRestartAgents: 'Restart toolstacks',
xosanMasterOffline: 'Pool master is not running',
xosanInstallPackTitle: 'Install XOSAN pack on {pool}',
xosanSrOnSameHostMessage: 'Select no more than 1 SR per host',
xosanLayout: 'Layout',
xosanRedundancy: 'Redundancy',
@@ -2087,8 +2013,6 @@ const messages = {
xosanAvailableSpace: 'Available space',
xosanDiskLossLegend: '* Can fail without data loss',
xosanCreate: 'Create',
xosanAdd: 'Add',
xosanInstalling: 'Installing XOSAN. Please wait…',
xosanCommunity: 'No XOSAN available for Community Edition',
xosanNew: 'New',
xosanAdvanced: 'Advanced',
@@ -2104,7 +2028,7 @@ const messages = {
xosanUpdatePacks: 'Update packs',
xosanPackUpdateChecking: 'Checking for updates',
xosanPackUpdateError:
'Error while checking XOSAN packs. Please make sure that the Cloud plugin is installed and loaded and that the updater is reachable.',
'Error while checking XOSAN packs. Please make sure that the Cloud plugin is installed and loaded, and that the updater is reachable.',
xosanPackUpdateUnavailable: 'XOSAN resources are unavailable',
xosanPackUpdateUnregistered: 'Not registered for XOSAN resources',
xosanPackUpdateUpToDate: "✓ This pool's XOSAN packs are up to date!",
@@ -2122,9 +2046,6 @@ const messages = {
// Pack download modal
xosanInstallCloudPlugin: 'Install cloud plugin first',
xosanLoadCloudPlugin: 'Load cloud plugin first',
xosanRegister: 'Register your appliance first',
xosanLoading: 'Loading…',
xosanNotAvailable: 'XOSAN is not available at the moment',
xosanNoPackFound:
'No compatible XOSAN pack found for your XenServer versions.',
// SR tab XOSAN
@@ -2169,7 +2090,6 @@ const messages = {
'Will configure the host xosan network device with a static IP address and plug it in.',
// Licenses
licensesTitle: 'Licenses',
xosanUnregisteredDisclaimer:
'You are not registered and therefore will not be able to create or manage your XOSAN SRs. {link}',
xosanSourcesDisclaimer:

View File

@@ -283,7 +283,7 @@ export const Vdi = decorate([
sr: getSr(state, props),
})
}),
({ id, sr, vdi }) => {
({ id, showSize, showSr, sr, vdi }) => {
if (vdi === undefined) {
return unknowItem(id, 'VDI')
}
@@ -291,9 +291,12 @@ export const Vdi = decorate([
return (
<span>
<Icon icon='disk' /> {vdi.name_label}
{sr !== undefined && (
{sr !== undefined && showSr && (
<span className='text-muted'> - {sr.name_label}</span>
)}
{showSize && (
<span className='text-muted'> ({formatSize(vdi.size)})</span>
)}
</span>
)
},
@@ -302,10 +305,13 @@ export const Vdi = decorate([
Vdi.propTypes = {
id: PropTypes.string.isRequired,
self: PropTypes.bool,
showSize: PropTypes.bool,
}
Vdi.defaultProps = {
self: false,
showSize: false,
showSr: false,
}
// ===================================================================
@@ -432,8 +438,8 @@ const xoItemToRender = {
// XO objects.
pool: ({ id }) => <Pool id={id} />,
VDI: ({ id }) => <Vdi id={id} />,
'VDI-resourceSet': ({ id }) => <Vdi id={id} self />,
VDI: ({ id }) => <Vdi id={id} showSr />,
'VDI-resourceSet': ({ id }) => <Vdi id={id} self showSr />,
// Pool objects.
'VM-template': ({ id }) => <VmTemplate id={id} />,
@@ -498,6 +504,9 @@ const xoItemToRender = {
{backup.mode}
</span>{' '}
<span className='tag tag-warning'>{backup.remote.name}</span>{' '}
{backup.size !== undefined && (
<span className='tag tag-info'>{formatSize(backup.size)}</span>
)}{' '}
<FormattedDate
value={new Date(backup.timestamp)}
month='long'

View File

@@ -0,0 +1,53 @@
import _ from 'intl'
import PropTypes from 'prop-types'
import React from 'react'
import { confirm } from 'modal'
import { injectState, provideState } from 'reaclette'
import { noop } from 'utils'
// https://docs.citrix.com/en-us/citrix-hypervisor/whats-new/experimental.html
// XAPI values should be lowercased
const VM_BOOT_FIRMWARES = ['bios', 'uefi']
const withState = provideState({
effects: {
handleBootFirmwareChange(
__,
{
target: { value },
}
) {
if (value !== '') {
// TODO: Confirm should be removed once the feature is stabilized
confirm({
title: _('vmBootFirmware'),
body: _('vmBootFirmwareWarningMessage'),
}).then(() => this.props.onChange(value), noop)
} else {
this.props.onChange(value)
}
},
},
})
const SelectBootFirmware = ({ effects, value }) => (
<select
className='form-control'
onChange={effects.handleBootFirmwareChange}
value={value}
>
<option value=''>{_('vmDefaultBootFirmwareLabel')}</option>
{VM_BOOT_FIRMWARES.map(val => (
<option key={val} value={val}>
{val}
</option>
))}
</select>
)
SelectBootFirmware.propTypes = {
onChange: PropTypes.func.isRequired,
value: PropTypes.string.isRequired,
}
export default withState(injectState(SelectBootFirmware))

View File

@@ -59,7 +59,7 @@ export const constructSmartPattern = (
const valueToComplexMatcher = pattern => {
if (typeof pattern === 'string') {
return new CM.RegExpNode(`^${escapeRegExp(pattern)}$`)
return new CM.RegExpNode(`^${escapeRegExp(pattern)}$`, 'i')
}
if (Array.isArray(pattern)) {

View File

@@ -29,7 +29,7 @@ import _ from './intl'
import * as actions from './store/actions'
import invoke from './invoke'
import store from './store'
import { getObject } from './selectors'
import { getObject, isAdmin } from './selectors'
import { satisfies as versionSatisfies } from 'semver'
export const EMPTY_ARRAY = Object.freeze([])
@@ -650,3 +650,12 @@ export const hasLicenseRestrictions = host =>
host.productBrand !== 'XCP-ng' &&
versionSatisfies(host.version, '>=7.3.0') &&
host.license_params.sku_type === 'free'
// ===================================================================
export const adminOnly = Component =>
connectStore({
_isAdmin: isAdmin,
})(({ _isAdmin, ...props }) =>
_isAdmin ? <Component {...props} /> : <_NotFound />
)

View File

@@ -15,6 +15,7 @@ import {
max,
round,
size,
sortBy,
sum,
values,
} from 'lodash'
@@ -107,7 +108,7 @@ const makeLabelInterpolationFnc = (intl, nValues, endTimestamp, interval) => {
// Supported series: xvds, vifs, pifs.
const buildSeries = ({ stats, label, addSumSeries }) => {
const series = []
let series = []
for (const io in stats) {
const ioData = stats[io]
@@ -123,6 +124,8 @@ const buildSeries = ({ stats, label, addSumSeries }) => {
}
}
series = sortBy(series, 'name')
if (addSumSeries) {
series.push({
name: `All ${io}`,
@@ -588,10 +591,13 @@ PoolLoadLineChart.propTypes = {
}
const buildSrSeries = ({ stats, label, addSumSeries }) => {
const series = map(stats, (data, key) => ({
name: `${label} (${key})`,
data,
}))
const series = sortBy(
map(stats, (data, key) => ({
name: `${label} (${key})`,
data,
})),
'name'
)
if (addSumSeries) {
series.push({

View File

@@ -3,6 +3,7 @@ import Component from 'base-component'
import PropTypes from 'prop-types'
import React from 'react'
import { map } from 'lodash'
import { Vdi } from 'render-xo-item'
import _ from '../../intl'
import SingleLineRow from '../../single-line-row'
@@ -85,7 +86,9 @@ export default class ChooseSrForEachVdisModal extends Component {
</SingleLineRow>
{map(props.vdis, vdi => (
<SingleLineRow key={vdi.uuid}>
<Col size={6}>{vdi.name_label || vdi.name}</Col>
<Col size={6}>
<Vdi id={vdi.id} showSize />
</Col>
<Col size={6}>
<SelectSr
onChange={sr =>

View File

@@ -376,7 +376,7 @@ export const dismissNotification = id => {
export const subscribeNotifications = createSubscription(async () => {
const { user, xoaUpdaterState } = store.getState()
if (
process.env.XOA_PLAN === 5 ||
+process.env.XOA_PLAN === 5 ||
xoaUpdaterState === 'disconnected' ||
xoaUpdaterState === 'error'
) {
@@ -2508,14 +2508,25 @@ export const editUser = (user, { email, password, permission }) =>
subscribeUsers.forceRefresh
)
const _signOutFromEverywhereElse = () =>
_call('token.deleteAll', { except: cookies.get('token') })
export const signOutFromEverywhereElse = () =>
_signOutFromEverywhereElse().then(
() => success(_('forgetTokens'), _('forgetTokensSuccess')),
() => error(_('forgetTokens'), _('forgetTokensError'))
)
export const changePassword = (oldPassword, newPassword) =>
_call('user.changePassword', {
oldPassword,
newPassword,
}).then(
() => success(_('pwdChangeSuccess'), _('pwdChangeSuccessBody')),
() => error(_('pwdChangeError'), _('pwdChangeErrorBody'))
)
})
.then(_signOutFromEverywhereElse)
.then(
() => success(_('pwdChangeSuccess'), _('pwdChangeSuccessBody')),
() => error(_('pwdChangeError'), _('pwdChangeErrorBody'))
)
const _setUserPreferences = preferences =>
_call('user.set', {

View File

@@ -381,6 +381,13 @@ class XoaUpdater extends EventEmitter {
}
}
getReleaseChannels() {
return this._call('getReleaseChannels').catch(error => {
console.error('getReleaseChannels', error)
return {}
})
}
async _call(...args) {
const c = await this._open()
try {

View File

@@ -7,6 +7,7 @@ const DEFAULTS = {
concurrency: 0,
fullInterval: 0,
offlineSnapshot: false,
reportWhen: 'failure',
timeout: 0,
}

View File

@@ -12,9 +12,9 @@ import React from 'react'
import SortedTable from 'sorted-table'
import StateButton from 'state-button'
import Tooltip from 'tooltip'
import { adminOnly, connectStore, routes } from 'utils'
import { Card, CardHeader, CardBlock } from 'card'
import { confirm } from 'modal'
import { connectStore, routes } from 'utils'
import { constructQueryString } from 'smart-backup'
import { Container, Row, Col } from 'grid'
import { createGetLoneSnapshots, createSelector } from 'selectors'
@@ -430,8 +430,10 @@ export default routes('overview', {
'restore/metadata': RestoreMetadata,
'file-restore': FileRestore,
health: Health,
})(({ children }) => (
<Page header={HEADER} title='backupPage' formatTitle>
{children}
</Page>
))
})(
adminOnly(({ children }) => (
<Page header={HEADER} title='backupPage' formatTitle>
{children}
</Page>
))
)

View File

@@ -0,0 +1,70 @@
import _ from 'intl'
import decorate from 'apply-decorators'
import Icon from 'icon'
import Link from 'link'
import PropTypes from 'prop-types'
import React from 'react'
import Select from 'form/select'
import Tooltip from 'tooltip'
import { generateId } from 'reaclette-utils'
import { injectState, provideState } from 'reaclette'
import { FormGroup } from './../utils'
const REPORT_WHEN_FILTER_OPTIONS = [
{
label: 'reportWhenAlways',
value: 'always',
},
{
label: 'reportWhenFailure',
value: 'failure',
},
{
label: 'reportWhenNever',
value: 'never',
},
]
const getOptionRenderer = ({ label }) => <span>{_(label)}</span>
const ReportWhen = decorate([
provideState({
computed: {
idInput: generateId,
},
}),
injectState,
({ state, onChange, value, ...props }) => (
<FormGroup>
<label htmlFor={state.idInput}>
<strong>{_('reportWhen')}</strong>
</label>{' '}
<Tooltip content={_('pluginsWarning')}>
<Link
className='btn btn-primary btn-sm'
target='_blank'
to='/settings/plugins'
>
<Icon icon='menu-settings-plugins' />{' '}
<strong>{_('pluginsSettings')}</strong>
</Link>
</Tooltip>
<Select
id={state.idInput}
onChange={onChange}
optionRenderer={getOptionRenderer}
options={REPORT_WHEN_FILTER_OPTIONS}
value={value}
{...props}
/>
</FormGroup>
),
])
ReportWhen.propTypes = {
onChange: PropTypes.func.isRequired,
value: PropTypes.string,
}
export { ReportWhen as default }

View File

@@ -7,7 +7,6 @@ import Icon from 'icon'
import Link from 'link'
import moment from 'moment-timezone'
import React from 'react'
import Select from 'form/select'
import Tooltip from 'tooltip'
import Upgrade from 'xoa-upgrade'
import UserError from 'user-error'
@@ -42,6 +41,7 @@ import {
} from 'xo'
import NewSchedule from './new-schedule'
import ReportWhen from './_reportWhen'
import Schedules from './schedules'
import SmartBackup from './smart-backup'
import getSettingsWithNonDefaultValue from '../_getSettingsWithNonDefaultValue'
@@ -115,23 +115,6 @@ const destructVmsPattern = pattern =>
vms: destructPattern(pattern),
}
const REPORT_WHEN_FILTER_OPTIONS = [
{
label: 'reportWhenAlways',
value: 'always',
},
{
label: 'reportWhenFailure',
value: 'failure',
},
{
label: 'reportWhenNever',
value: 'never',
},
]
const getOptionRenderer = ({ label }) => <span>{_(label)}</span>
const createDoesRetentionExist = name => {
const predicate = setting => setting[name] > 0
return ({ propSettings, settings = propSettings }) => settings.some(predicate)
@@ -541,7 +524,6 @@ export default decorate([
formId: generateId,
inputConcurrencyId: generateId,
inputFullIntervalId: generateId,
inputReportWhenId: generateId,
inputTimeoutId: generateId,
vmsPattern: ({ _vmsPattern }, { job }) =>
@@ -884,31 +866,13 @@ export default decorate([
</ActionButton>
</CardHeader>
<CardBlock>
<FormGroup>
<label htmlFor={state.inputReportWhenId}>
<strong>{_('reportWhen')}</strong>
</label>{' '}
<Tooltip content={_('pluginsWarning')}>
<Link
className='btn btn-primary btn-sm'
target='_blank'
to='/settings/plugins'
>
<Icon icon='menu-settings-plugins' />{' '}
<strong>{_('pluginsSettings')}</strong>
</Link>
</Tooltip>
<Select
id={state.inputReportWhenId}
labelKey='label'
onChange={effects.setReportWhen}
optionRenderer={getOptionRenderer}
options={REPORT_WHEN_FILTER_OPTIONS}
required
value={reportWhen}
valueKey='value'
/>
</FormGroup>
<ReportWhen
onChange={effects.setReportWhen}
required
// Handle improper value introduced by:
// https://github.com/vatesfr/xen-orchestra/commit/753ee994f2948bbaca9d3161eaab82329a682773#diff-9c044ab8a42ed6576ea927a64c1ec3ebR105
value={reportWhen === 'Never' ? 'never' : reportWhen}
/>
{state.displayAdvancedSettings && (
<div>
<FormGroup>

View File

@@ -33,6 +33,7 @@ import {
Ul,
} from '../../utils'
import ReportWhen from '../_reportWhen'
import Schedules from '../_schedules'
// A retention can be:
@@ -51,6 +52,25 @@ const RETENTION_XO_METADATA = {
valuePath: 'retentionXoMetadata',
}
const GLOBAL_SETTING_KEY = ''
const setSettingsDefaultRetentions = (
settings,
{ modePoolMetadata, modeXoMetadata }
) =>
mapValues(settings, (setting, key) =>
key !== GLOBAL_SETTING_KEY
? {
retentionPoolMetadata: modePoolMetadata
? defined(setting.retentionPoolMetadata, DEFAULT_RETENTION)
: undefined,
retentionXoMetadata: modeXoMetadata
? defined(setting.retentionXoMetadata, DEFAULT_RETENTION)
: undefined,
}
: setting
)
const getInitialState = () => ({
_modePoolMetadata: undefined,
_modeXoMetadata: undefined,
@@ -79,28 +99,25 @@ export default decorate([
return { showErrors: true }
}
const {
modePoolMetadata,
modeXoMetadata,
name,
pools,
remotes,
schedules,
settings,
} = state
await createMetadataBackupJob({
name: state.name,
pools: state.modePoolMetadata
? constructPattern(state.pools)
: undefined,
remotes: constructPattern(state.remotes),
xoMetadata: state.modeXoMetadata,
schedules: mapValues(
state.schedules,
({ id, ...schedule }) => schedule
),
settings: mapValues(
state.settings,
({ retentionPoolMetadata, retentionXoMetadata }) => ({
retentionPoolMetadata: state.modePoolMetadata
? defined(retentionPoolMetadata, DEFAULT_RETENTION)
: undefined,
retentionXoMetadata: state.modeXoMetadata
? defined(retentionXoMetadata, DEFAULT_RETENTION)
: undefined,
})
),
name,
pools: modePoolMetadata ? constructPattern(pools) : undefined,
remotes: constructPattern(remotes),
schedules: mapValues(schedules, ({ id, ...schedule }) => schedule),
settings: setSettingsDefaultRetentions(settings, {
modePoolMetadata,
modeXoMetadata,
}),
xoMetadata: modeXoMetadata,
})
},
editJob: () => async (state, props) => {
@@ -138,23 +155,17 @@ export default decorate([
}),
])
const { modePoolMetadata, modeXoMetadata, name, pools, remotes } = state
await editMetadataBackupJob({
id: props.job.id,
name: state.name,
pools: state.modePoolMetadata ? constructPattern(state.pools) : null,
remotes: constructPattern(state.remotes),
xoMetadata: state.modeXoMetadata,
settings: mapValues(
settings,
({ retentionPoolMetadata, retentionXoMetadata }) => ({
retentionPoolMetadata: state.modePoolMetadata
? defined(retentionPoolMetadata, DEFAULT_RETENTION)
: undefined,
retentionXoMetadata: state.modeXoMetadata
? defined(retentionXoMetadata, DEFAULT_RETENTION)
: undefined,
})
),
name,
pools: modePoolMetadata ? constructPattern(pools) : null,
remotes: constructPattern(remotes),
settings: setSettingsDefaultRetentions(settings, {
modePoolMetadata,
modeXoMetadata,
}),
xoMetadata: modeXoMetadata,
})
},
@@ -169,6 +180,20 @@ export default decorate([
setSettings: (_, _settings) => () => ({
_settings,
}),
setGlobalSettings: ({ setSettings }, name, value) => ({
settings = {},
}) => {
setSettings({
...settings,
[GLOBAL_SETTING_KEY]: {
...settings[GLOBAL_SETTING_KEY],
[name]: value,
},
})
},
setReportWhen({ setGlobalSettings }, { value }) {
setGlobalSettings('reportWhen', value)
},
toggleMode: (_, { mode }) => state => ({
[`_${mode}`]: !state[mode],
}),
@@ -265,6 +290,11 @@ export default decorate([
missingSchedules,
} = state.showErrors ? state : {}
const { reportWhen = 'failure' } = defined(
() => state.settings[GLOBAL_SETTING_KEY],
{}
)
return (
<form id={state.idForm}>
<Container>
@@ -357,6 +387,16 @@ export default decorate([
)}
</CardBlock>
</Card>
<Card>
<CardHeader>{_('newBackupSettings')}</CardHeader>
<CardBlock>
<ReportWhen
onChange={effects.setReportWhen}
required
value={reportWhen}
/>
</CardBlock>
</Card>
</Col>
<Col mediumSize={6}>
{state.modePoolMetadata && (

View File

@@ -6,7 +6,7 @@ import Icon from 'icon'
import React from 'react'
import SortedTable from 'sorted-table'
import Upgrade from 'xoa-upgrade'
import { addSubscriptions, noop } from 'utils'
import { addSubscriptions, formatSize, noop } from 'utils'
import { confirm } from 'modal'
import { error } from 'notification'
import { FormattedDate } from 'react-intl'
@@ -87,6 +87,12 @@ const BACKUPS_COLUMNS = [
default: true,
sortOrder: 'desc',
},
{
name: _('labelSize'),
itemRenderer: ({ size }) =>
size !== undefined && size !== 0 && formatSize(size),
sortCriteria: 'size',
},
{
name: _('availableBackupsColumn'),
itemRenderer: ({ count }) =>
@@ -149,6 +155,7 @@ export default class Restore extends Component {
})
// TODO: perf
let first, last
let size = 0
forEach(backupDataByVm, (data, vmId) => {
first = { timestamp: Infinity }
last = { timestamp: 0 }
@@ -161,9 +168,13 @@ export default class Restore extends Component {
first = backup
}
count[backup.mode] = (count[backup.mode] || 0) + 1
if (backup.size !== undefined) {
size += backup.size
}
})
assign(data, { first, last, count, id: vmId })
assign(data, { first, last, count, id: vmId, size })
})
forEach(backupDataByVm, ({ backups }, vmId) => {

View File

@@ -3,7 +3,7 @@ import Icon from 'icon'
import Link from 'link'
import Page from '../page'
import React from 'react'
import { routes } from 'utils'
import { adminOnly, routes } from 'utils'
import { Container, Row, Col } from 'grid'
import { NavLink, NavTabs } from 'nav'
@@ -68,10 +68,12 @@ const Backup = routes('overview', {
overview: Overview,
restore: MovingRestoreMessage,
'file-restore': MovingFileRestoreMessage,
})(({ children }) => (
<Page header={HEADER} title='backupPage' formatTitle>
{children}
</Page>
))
})(
adminOnly(({ children }) => (
<Page header={HEADER} title='backupPage' formatTitle>
{children}
</Page>
))
)
export default Backup

View File

@@ -750,7 +750,8 @@ export default class Home extends Component {
new ComplexMatcher.Or(
map(
tags,
tag => new ComplexMatcher.RegExp(`^${escapeRegExp(tag.id)}$`)
tag =>
new ComplexMatcher.RegExp(`^${escapeRegExp(tag.id)}$`, 'i')
)
)
)

View File

@@ -112,6 +112,10 @@ export default class extends Component {
return uniqPacks
}
)
_isHtEnabled = createSelector(
() => this.props.host.CPUs.flags,
flags => /\bht\b/.test(flags)
)
_setRemoteSyslogHost = value => setRemoteSyslogHost(this.props.host, value)
render() {
@@ -272,6 +276,14 @@ export default class extends Component {
{host.cpus.cores} ({host.cpus.sockets})
</td>
</tr>
<tr>
<th>{_('hyperThreading')}</th>
<td>
{this._isHtEnabled()
? _('stateEnabled')
: _('stateDisabled')}
</td>
</tr>
<tr>
<th>{_('hostManufacturerinfo')}</th>
<Copiable tagName='td'>

View File

@@ -118,7 +118,9 @@ export default ({ statsOverview, host, nVms, vmController, vms }) => {
<Usage total={host.memory.size}>
<UsageElement
highlight
tooltip={`XenServer (${formatSize(vmController.memory.size)})`}
tooltip={`${host.productBrand} (${formatSize(
vmController.memory.size
)})`}
value={vmController.memory.size}
/>
{map(vms, vm => (

View File

@@ -4,7 +4,7 @@ import Page from '../page'
import React from 'react'
import { Container, Row, Col } from 'grid'
import { NavLink, NavTabs } from 'nav'
import { routes } from 'utils'
import { adminOnly, routes } from 'utils'
import Edit from './edit'
import New from './new'
@@ -43,10 +43,12 @@ const Jobs = routes('overview', {
overview: Overview,
schedules: Schedules,
'schedules/:id/edit': EditSchedule,
})(({ children }) => (
<Page header={HEADER} title='jobsPage' formatTitle>
{children}
</Page>
))
})(
adminOnly(({ children }) => (
<Page header={HEADER} title='jobsPage' formatTitle>
{children}
</Page>
))
)
export default Jobs

View File

@@ -10,6 +10,7 @@ import Link from 'link'
import Page from '../page'
import PropTypes from 'prop-types'
import React from 'react'
import SelectBootFirmware from 'select-boot-firmware'
import store from 'store'
import Tags from 'tags'
import Tooltip from 'tooltip'
@@ -334,6 +335,7 @@ export default class NewVm extends BaseComponent {
cpuWeight: '',
existingDisks: {},
fastClone: true,
hvmBootFirmware: '',
installMethod: 'noConfigDrive',
multipleVms: false,
name_label: '',
@@ -501,6 +503,8 @@ export default class NewVm extends BaseComponent {
tags: state.tags,
vgpuType: get(() => state.vgpuType.id),
gpuGroup: get(() => state.vgpuType.gpuGroup),
hvmBootFirmware:
state.hvmBootFirmware === '' ? undefined : state.hvmBootFirmware,
}
return state.multipleVms
@@ -576,6 +580,7 @@ export default class NewVm extends BaseComponent {
CPUs: template.CPUs.number,
cpuCap: '',
cpuWeight: '',
hvmBootFirmware: defined(() => template.boot.firmware, ''),
memoryDynamicMax: template.memory.dynamic[1],
// installation
installMethod:
@@ -752,6 +757,11 @@ export default class NewVm extends BaseComponent {
template => template && template.name_label === 'CoreOS'
)
_isHvm = createSelector(
() => this.state.template,
template => template && template.virtualizationMode === 'hvm'
)
// On change -------------------------------------------------------------------
_onChangeSshKeys = keys =>
@@ -900,6 +910,8 @@ export default class NewVm extends BaseComponent {
_getRedirectionUrl = id =>
this.state.state.multipleVms ? '/home' : `/vms/${id}`
_handleBootFirmware = value => this._setState({ hvmBootFirmware: value })
// MAIN ------------------------------------------------------------------------
_renderHeader = () => {
@@ -1630,6 +1642,7 @@ export default class NewVm extends BaseComponent {
bootAfterCreate,
cpuCap,
cpuWeight,
hvmBootFirmware,
memoryDynamicMin,
memoryDynamicMax,
memoryStaticMax,
@@ -1641,10 +1654,11 @@ export default class NewVm extends BaseComponent {
share,
showAdvanced,
tags,
template,
} = this.state.state
const { isAdmin } = this.props
const { formatMessage } = this.props.intl
const isHvm = this._isHvm()
return (
<Section
icon='new-vm-advanced'
@@ -1827,7 +1841,7 @@ export default class NewVm extends BaseComponent {
</Item>
</SectionContent>
),
template && template.virtualizationMode === 'hvm' && (
isHvm && (
<SectionContent>
<Item label={_('vmVgpu')}>
<SelectVgpuType
@@ -1837,6 +1851,16 @@ export default class NewVm extends BaseComponent {
</Item>
</SectionContent>
),
isHvm && (
<SectionContent>
<Item label={_('vmBootFirmware')}>
<SelectBootFirmware
onChange={this._handleBootFirmware}
value={hvmBootFirmware}
/>
</Item>
</SectionContent>
),
]}
</Section>
)

View File

@@ -15,10 +15,10 @@ import store from 'store'
import trim from 'lodash/trim'
import Wizard, { Section } from 'wizard'
import { confirm } from 'modal'
import { connectStore, formatSize } from 'utils'
import { adminOnly, connectStore, formatSize } from 'utils'
import { Container, Row, Col } from 'grid'
import { injectIntl } from 'react-intl'
import { Password, Select, Toggle } from 'form'
import { Password, Select } from 'form'
import { SelectHost } from 'select-objects'
import {
createFilter,
@@ -46,6 +46,10 @@ import {
// ===================================================================
const NFS_VERSIONS = ['4', '4.1']
// ===================================================================
class SelectScsiId extends Component {
static propTypes = {
onChange: PropTypes.func.isRequired,
@@ -216,6 +220,7 @@ const getSrPath = id => (id !== undefined ? `/srs/${id}` : undefined)
// ===================================================================
@adminOnly
@injectIntl
@connectStore(() => ({
hosts: createGetObjectsOfType('host'),
@@ -236,6 +241,7 @@ export default class New extends Component {
lockCreation: undefined,
lun: undefined,
luns: undefined,
nfsVersion: '',
hbaDevices: undefined,
name: undefined,
path: undefined,
@@ -266,7 +272,16 @@ export default class New extends Component {
server,
username,
} = this.refs
const { host, iqn, lun, path, type, scsiId, nfs4, nfsOptions } = this.state
const {
host,
iqn,
lun,
nfsOptions,
nfsVersion,
path,
scsiId,
type,
} = this.state
const createMethodFactories = {
nfs: () =>
@@ -276,7 +291,7 @@ export default class New extends Component {
description.value,
server.value,
path,
nfs4 ? '4' : undefined,
nfsVersion !== '' ? nfsVersion : undefined,
nfsOptions
),
hba: async () => {
@@ -521,6 +536,12 @@ export default class New extends Component {
}
}
_handleNfsVersion = ({ target: { value } }) => {
this.setState({
nfsVersion: value,
})
}
_reattach = async uuid => {
const { host, type } = this.state
@@ -565,6 +586,7 @@ export default class New extends Component {
lockCreation,
lun,
luns,
nfsVersion,
path,
paths,
summary,
@@ -656,10 +678,22 @@ export default class New extends Component {
</div>
</fieldset>,
<fieldset>
<label>{_('newSrUseNfs4')}</label>
<div>
<Toggle onChange={this.toggleState('nfs4')} />
</div>
<label htmlFor='selectNfsVersion'>{_('newSrNfs')}</label>
<select
className='form-control'
id='selectNfsVersion'
onChange={this._handleNfsVersion}
value={nfsVersion}
>
<option value=''>
{formatMessage(messages.newSrNfsDefaultVersion)}
</option>
{map(NFS_VERSIONS, option => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
</fieldset>,
<fieldset>
<label>{_('newSrNfsOptions')}</label>

View File

@@ -25,7 +25,12 @@ import Upgrade from 'xoa-upgrade'
import { Container, Row, Col } from 'grid'
import { injectIntl } from 'react-intl'
import { SizeInput } from 'form'
import { addSubscriptions, connectStore, resolveIds } from 'utils'
import {
addSubscriptions,
adminOnly,
connectStore,
resolveIds
} from 'utils'
import {
createGetObjectsOfType,
createSelector,
@@ -700,6 +705,7 @@ class ResourceSet extends Component {
const compareName = (a, b) => (a.name < b.name ? -1 : 1)
@adminOnly
@addSubscriptions({ resourceSets: subscribeResourceSets })
@connectStore({ resolvedResourceSets: getResolvedResourceSets })
export default class Self extends Component {

View File

@@ -2,7 +2,7 @@ import _ from 'intl'
import Icon from 'icon'
import Page from '../page'
import React from 'react'
import { routes } from 'utils'
import { adminOnly, routes } from 'utils'
import { Container, Row, Col } from 'grid'
import { NavLink, NavTabs } from 'nav'
@@ -74,10 +74,12 @@ const Settings = routes('servers', {
remotes: Remotes,
servers: Servers,
users: Users,
})(({ children }) => (
<Page header={HEADER} title='settingsPage' formatTitle>
{children}
</Page>
))
})(
adminOnly(({ children }) => (
<Page header={HEADER} title='settingsPage' formatTitle>
{children}
</Page>
))
)
export default Settings

View File

@@ -51,9 +51,20 @@ const COLUMNS = [
{
name: _('logMessage'),
itemRenderer: log => (
<pre className={styles.widthLimit}>
{log.data.error && log.data.error.message}
</pre>
<span>
<pre className={styles.widthLimit}>
{log.data.error && log.data.error.message}
</pre>
{log.data.error && log.data.error.code === 'LICENCE_RESTRICTION' && (
<a
href='https://xcp-ng.org/'
rel='noopener noreferrer'
target='_blank'
>
{_('logSuggestXcpNg')}
</a>
)}
</span>
),
sortCriteria: log => log.data.error && log.data.error.message,
},

View File

@@ -7,6 +7,7 @@ import Icon from 'icon'
import PropTypes from 'prop-types'
import React from 'react'
import SortedTable from 'sorted-table'
import Tooltip from 'tooltip'
import { Text } from 'editable'
import { alert } from 'modal'
import { Container, Row, Col } from 'grid'
@@ -24,6 +25,7 @@ import {
editCustomFilter,
removeCustomFilter,
setDefaultHomeFilter,
signOutFromEverywhereElse,
subscribeCurrentUser,
} from 'xo'
@@ -400,6 +402,20 @@ export default class User extends Component {
</Col>
</Row>
<br />
<Row>
<Col smallSize={10} offset={2}>
<Tooltip content={_('forgetTokensExplained')}>
<ActionButton
btnStyle='danger'
handler={signOutFromEverywhereElse}
icon='disconnect'
>
{_('forgetTokens')}
</ActionButton>
</Tooltip>
</Col>
</Row>
<br />
<Row>
<Col smallSize={2}>
<strong>{_('language')}</strong>

View File

@@ -8,6 +8,7 @@ import Icon from 'icon'
import Link from 'link'
import React from 'react'
import renderXoItem from 'render-xo-item'
import SelectBootFirmware from 'select-boot-firmware'
import TabButton from 'tab-button'
import Tooltip from 'tooltip'
import { error } from 'notification'
@@ -510,6 +511,11 @@ export default class TabAdvanced extends Component {
_onChangeCpuMask = cpuMask =>
editVm(this.props.vm, { cpuMask: map(cpuMask, 'value') })
_handleBootFirmware = value =>
editVm(this.props.vm, {
hvmBootFirmware: value !== '' ? value : null,
})
_onNicTypeChange = value =>
editVm(this.props.vm, { nicType: value === '' ? null : value })
@@ -833,6 +839,17 @@ export default class TabAdvanced extends Component {
</td>
</tr>
)}
{vm.virtualizationMode === 'hvm' && (
<tr>
<th>{_('vmBootFirmware')}</th>
<td>
<SelectBootFirmware
onChange={this._handleBootFirmware}
value={defined(() => vm.boot.firmware, '')}
/>
</td>
</tr>
)}
</tbody>
</table>
<br />

View File

@@ -59,160 +59,178 @@ export default connectStore(() => {
vgpuTypes,
vm,
vmTotalDiskSpace,
}) => (
<Container>
{/* TODO: use CSS style */}
<br />
<Row className='text-xs-center'>
<Col mediumSize={3}>
<h2>
<Number
value={vm.CPUs.number}
onChange={vcpus => editVm(vm, { CPUs: vcpus })}
/>
x <Icon icon='cpu' size='lg' />
</h2>
<BlockLink to={`/vms/${vm.id}/stats`}>
{statsOverview && <CpuSparkLines data={statsOverview} />}
</BlockLink>
</Col>
<Col mediumSize={3}>
<h2 className='form-inline'>
<Size
value={defined(vm.memory.dynamic[1], null)}
onChange={memory => editVm(vm, { memory })}
/>
&nbsp;
<span>
<Icon icon='memory' size='lg' />
</span>
</h2>
<BlockLink to={`/vms/${vm.id}/stats`}>
{statsOverview && <MemorySparkLines data={statsOverview} />}
</BlockLink>
</Col>
<Col mediumSize={3}>
<BlockLink to={`/vms/${vm.id}/network`}>
}) => {
const {
addresses,
CPUs: cpus,
current_operations: currentOperations,
id,
installTime,
memory,
os_version: osVersion,
power_state: powerState,
startTime,
tags,
VIFs: vifs,
xenTools,
} = vm
return (
<Container>
{/* TODO: use CSS style */}
<br />
<Row className='text-xs-center'>
<Col mediumSize={3}>
<h2>
{vm.VIFs.length}x <Icon icon='network' size='lg' />
<Number
value={cpus.number}
onChange={vcpus => editVm(vm, { CPUs: vcpus })}
/>
x <Icon icon='cpu' size='lg' />
</h2>
</BlockLink>
<BlockLink to={`/vms/${vm.id}/stats`}>
{statsOverview && <NetworkSparkLines data={statsOverview} />}
</BlockLink>
</Col>
<Col mediumSize={3}>
<BlockLink to={`/vms/${vm.id}/disks`}>
<h2>
{formatSize(vmTotalDiskSpace)} <Icon icon='disk' size='lg' />
<BlockLink to={`/vms/${id}/stats`}>
{statsOverview && <CpuSparkLines data={statsOverview} />}
</BlockLink>
</Col>
<Col mediumSize={3}>
<h2 className='form-inline'>
<Size
value={defined(memory.dynamic[1], null)}
onChange={memory => editVm(vm, { memory })}
/>
&nbsp;
<span>
<Icon icon='memory' size='lg' />
</span>
</h2>
</BlockLink>
<BlockLink to={`/vms/${vm.id}/stats`}>
{statsOverview && <XvdSparkLines data={statsOverview} />}
</BlockLink>
</Col>
</Row>
{/* TODO: use CSS style */}
<br />
<Row className='text-xs-center'>
<Col mediumSize={3}>
{vm.installTime !== null && (
<div className='text-xs-center'>
{_('created', {
date: (
<FormattedDate
day='2-digit'
month='long'
value={vm.installTime * 1000}
year='numeric'
/>
),
})}
</div>
)}
{vm.power_state === 'Running' ? (
<div>
<p className='text-xs-center'>
{_('started', {
ago: <FormattedRelative value={vm.startTime * 1000} />,
<BlockLink to={`/vms/${id}/stats`}>
{statsOverview && <MemorySparkLines data={statsOverview} />}
</BlockLink>
</Col>
<Col mediumSize={3}>
<BlockLink to={`/vms/${id}/network`}>
<h2>
{vifs.length}x <Icon icon='network' size='lg' />
</h2>
</BlockLink>
<BlockLink to={`/vms/${id}/stats`}>
{statsOverview && <NetworkSparkLines data={statsOverview} />}
</BlockLink>
</Col>
<Col mediumSize={3}>
<BlockLink to={`/vms/${id}/disks`}>
<h2>
{formatSize(vmTotalDiskSpace)} <Icon icon='disk' size='lg' />
</h2>
</BlockLink>
<BlockLink to={`/vms/${id}/stats`}>
{statsOverview && <XvdSparkLines data={statsOverview} />}
</BlockLink>
</Col>
</Row>
{/* TODO: use CSS style */}
<br />
<Row className='text-xs-center'>
<Col mediumSize={3}>
{installTime !== null && (
<div className='text-xs-center'>
{_('created', {
date: (
<FormattedDate
day='2-digit'
month='long'
value={installTime * 1000}
year='numeric'
/>
),
})}
</p>
</div>
) : (
<p className='text-xs-center'>
{lastShutdownTime
? _('vmHaltedSince', {
ago: <FormattedRelative value={lastShutdownTime * 1000} />,
})
: _('vmNotRunning')}
</p>
)}
</Col>
<Col mediumSize={3}>
<p>{_(getVirtualizationModeLabel(vm))}</p>
{vgpu !== undefined && (
<p>{renderXoItem(vgpuTypes[vgpu.vgpuType])}</p>
)}
</Col>
<Col mediumSize={3}>
<BlockLink to={`/vms/${vm.id}/network`}>
{vm.addresses && vm.addresses['0/ip'] ? (
<Copiable tagName='p'>{vm.addresses['0/ip']}</Copiable>
) : (
<p>{_('noIpv4Record')}</p>
</div>
)}
</BlockLink>
</Col>
<Col mediumSize={3}>
<BlockLink to={`/vms/${vm.id}/advanced`}>
<Tooltip
content={vm.os_version ? vm.os_version.name : _('unknownOsName')}
>
<h1>
<Icon
className='text-info'
icon={
vm.os_version &&
vm.os_version.distro &&
osFamily(vm.os_version.distro)
}
/>
</h1>
</Tooltip>
</BlockLink>
</Col>
</Row>
{!vm.xenTools && vm.power_state === 'Running' && (
<Row className='text-xs-center'>
<Col>
<Icon icon='error' />
<em> {_('noToolsDetected')}.</em>
{powerState === 'Running' || powerState === 'Paused' ? (
<div>
<p className='text-xs-center'>
{_('started', {
ago: <FormattedRelative value={startTime * 1000} />,
})}
</p>
</div>
) : (
<p className='text-xs-center'>
{lastShutdownTime
? _('vmHaltedSince', {
ago: (
<FormattedRelative value={lastShutdownTime * 1000} />
),
})
: _('vmNotRunning')}
</p>
)}
</Col>
<Col mediumSize={3}>
<p>{_(getVirtualizationModeLabel(vm))}</p>
{vgpu !== undefined && (
<p>{renderXoItem(vgpuTypes[vgpu.vgpuType])}</p>
)}
</Col>
<Col mediumSize={3}>
<BlockLink to={`/vms/${id}/network`}>
{addresses && addresses['0/ip'] ? (
<Copiable tagName='p'>{addresses['0/ip']}</Copiable>
) : (
<p>{_('noIpv4Record')}</p>
)}
</BlockLink>
</Col>
<Col mediumSize={3}>
<BlockLink to={`/vms/${id}/advanced`}>
<Tooltip
content={osVersion ? osVersion.name : _('unknownOsName')}
>
<h1>
<Icon
className='text-info'
icon={
osVersion &&
osVersion.distro &&
osFamily(osVersion.distro)
}
/>
</h1>
</Tooltip>
</BlockLink>
</Col>
</Row>
)}
{/* TODO: use CSS style */}
<br />
<Row>
<Col>
<h2 className='text-xs-center'>
<HomeTags
type='VM'
labels={vm.tags}
onDelete={tag => removeTag(vm.id, tag)}
onAdd={tag => addTag(vm.id, tag)}
/>
</h2>
</Col>
</Row>
{isEmpty(vm.current_operations) ? null : (
<Row className='text-xs-center'>
{!xenTools && powerState === 'Running' && (
<Row className='text-xs-center'>
<Col>
<Icon icon='error' />
<em> {_('noToolsDetected')}.</em>
</Col>
</Row>
)}
{/* TODO: use CSS style */}
<br />
<Row>
<Col>
<h4>
{_('vmCurrentStatus')} {map(vm.current_operations)[0]}
</h4>
<h2 className='text-xs-center'>
<HomeTags
type='VM'
labels={tags}
onDelete={tag => removeTag(id, tag)}
onAdd={tag => addTag(id, tag)}
/>
</h2>
</Col>
</Row>
)}
</Container>
)
{isEmpty(currentOperations) ? null : (
<Row className='text-xs-center'>
<Col>
<h4>
{_('vmCurrentStatus')} {map(currentOperations)[0]}
</h4>
</Col>
</Row>
)}
</Container>
)
}
)

View File

@@ -37,6 +37,8 @@ import {
map,
remove,
some,
uniq,
values,
} from 'lodash'
import {
@@ -515,6 +517,14 @@ export default class TabNetwork extends BaseComponent {
newVif: !this.state.newVif,
})
_getIpAddresses = createSelector(
() => this.props.vm.addresses,
// VM_guest_metrics.networks seems to always have 3 fields (ip, ipv4 and ipv6) for each interface
// http://xenbits.xenproject.org/docs/4.12-testing/misc/xenstore-paths.html#attrvifdevidipv4index-ipv4_address-w
// https://github.com/xapi-project/xen-api/blob/d650621ba7b64a82aeb77deca787acb059636eaf/ocaml/xapi/xapi_guest_agent.ml#L76-L79
addresses => uniq(values(addresses))
)
render() {
const { newVif } = this.state
const { pool, vm, vifs, networks } = this.props
@@ -551,8 +561,8 @@ export default class TabNetwork extends BaseComponent {
{!isEmpty(vm.addresses) ? (
<span>
<h4>{_('vifIpAddresses')}</h4>
{map(vm.addresses, (address, key) => (
<span key={key} className='tag tag-info tag-ip'>
{map(this._getIpAddresses(), address => (
<span key={address} className='tag tag-info tag-ip'>
{address}
</span>
))}

View File

@@ -6,7 +6,13 @@ import Link from 'link'
import React from 'react'
import renderXoItem from 'render-xo-item'
import SortedTable from 'sorted-table'
import { addSubscriptions, connectStore, getXoaPlan, ShortDate } from 'utils'
import {
addSubscriptions,
adminOnly,
connectStore,
getXoaPlan,
ShortDate,
} from 'utils'
import { Container, Row, Col } from 'grid'
import { createSelector, createGetObjectsOfType } from 'selectors'
import { find, forEach } from 'lodash'
@@ -77,6 +83,7 @@ const getBoundXosanRenderer = (boundObjectId, xosanSrs) => {
return () => <Link to={`srs/${sr.id}`}>{renderXoItem(sr)}</Link>
}
@adminOnly
@connectStore({
xosanSrs: createGetObjectsOfType('SR').filter([
({ SR_type }) => SR_type === 'xosan', // eslint-disable-line camelcase

View File

@@ -9,16 +9,17 @@ import Icon from 'icon'
import React from 'react'
import Tooltip from 'tooltip'
import xoaUpdater, { exposeTrial, isTrialRunning } from 'xoa-updater'
import { addSubscriptions, connectStore } from 'utils'
import { addSubscriptions, adminOnly, connectStore } from 'utils'
import { Card, CardBlock, CardHeader } from 'card'
import { confirm } from 'modal'
import { Container, Row, Col } from 'grid'
import { error } from 'notification'
import { generateId, linkState, toggleState } from 'reaclette-utils'
import { injectIntl } from 'react-intl'
import { injectState, provideState } from 'reaclette'
import { Input as DebounceInput } from 'debounce-input-decorator'
import { isEmpty, map, pick, some, zipObject } from 'lodash'
import { linkState, toggleState } from 'reaclette-utils'
import { Password } from 'form'
import { Password, Select } from 'form'
import { subscribeBackupNgJobs, subscribeJobs } from 'xo'
const ansiUp = new AnsiUp()
@@ -59,16 +60,22 @@ const LEVELS_TO_CLASSES = {
error: 'text-danger',
}
const UNLISTED_CHANNEL_VALUE = ''
const PROXY_ENTRIES = ['proxyHost', 'proxyPassword', 'proxyPort', 'proxyUser']
const initialProxyState = () => zipObject(PROXY_ENTRIES)
const REGISTRATION_ENTRIES = ['email', 'password']
const initialRegistrationState = () => zipObject(REGISTRATION_ENTRIES)
const CHANNEL_ENTRIES = ['channel']
const initialChannelState = () => zipObject(CHANNEL_ENTRIES)
const helper = (obj1, obj2, prop) =>
defined(() => obj1[prop], () => obj2[prop], '')
const Updates = decorate([
adminOnly,
addSubscriptions({
backupNgJobs: subscribeBackupNgJobs,
jobs: subscribeJobs,
@@ -82,6 +89,7 @@ const Updates = decorate([
]),
provideState({
initialState: () => ({
...initialChannelState(),
...initialProxyState(),
...initialRegistrationState(),
askRegisterAgain: false,
@@ -89,22 +97,33 @@ const Updates = decorate([
}),
effects: {
async configure() {
await xoaUpdater.configure(
pick(this.state, [
await xoaUpdater.configure({
...pick(this.state, [
'channel',
'proxyHost',
'proxyPassword',
'proxyPort',
'proxyUser',
])
)
return this.effects.resetProxyConfig()
]),
})
const { effects } = this
await Promise.all([
effects.resetChannel(),
effects.resetProxyConfig(),
effects.update(),
])
},
initialize() {
return this.effects.update()
},
linkState,
onChannelChange: (_, channel) => ({ channel }),
async register() {
const { state } = this
const {
props: { xoaRegisterState },
state,
} = this
const { isRegistered } = state
if (isRegistered) {
@@ -114,7 +133,7 @@ const Updates = decorate([
body: (
<p>
{_('alreadyRegisteredModalText', {
email: this.props.xoaRegisterState.email,
email: xoaRegisterState.email,
})}
</p>
),
@@ -128,11 +147,12 @@ const Updates = decorate([
}
state.askRegisterAgain = false
const { email, password } = state
const { email = xoaRegisterState.email, password } = state
await xoaUpdater.register(email, password, isRegistered)
return initialRegistrationState()
},
resetChannel: initialChannelState,
resetProxyConfig: initialProxyState,
async startTrial() {
try {
@@ -152,13 +172,41 @@ const Updates = decorate([
},
toggleState,
update: () => xoaUpdater.update(),
upgrade: () => xoaUpdater.upgrade(),
upgrade() {
return this.state.areJobsRunning
? confirm({
title: _('upgradeWarningTitle'),
body: <p>{_('upgradeWarningMessage')}</p>,
}).then(() => xoaUpdater.upgrade())
: xoaUpdater.upgrade()
},
},
computed: {
areJobsRunning: (_, { jobs, backupNgJobs }) =>
jobs !== undefined &&
backupNgJobs !== undefined &&
some(jobs.concat(backupNgJobs), job => job.runId !== undefined),
channelsFormId: generateId,
channels: () => xoaUpdater.getReleaseChannels(),
channelsOptions: ({ channels }) =>
channels === undefined
? undefined
: [
...Object.keys(channels)
.sort()
.map(channel => ({
label: channel,
value: channel,
})),
{
label: (
<span className='font-italic'>{_('unlistedChannel')}</span>
),
value: UNLISTED_CHANNEL_VALUE,
},
],
consolidatedChannel: ({ channel }, { xoaConfiguration }) =>
defined(channel, xoaConfiguration.channel),
async installedPackages() {
const { installer, updater, npm } = await xoaUpdater.getLocalManifest()
return { ...installer, ...updater, ...npm }
@@ -178,6 +226,8 @@ const Updates = decorate([
xoaTrialState.state === 'default' &&
!isTrialRunning(xoaTrialState.trial) &&
!exposeTrial(xoaTrialState.trial),
isUnlistedChannel: ({ consolidatedChannel, channels }) =>
consolidatedChannel !== undefined && !(consolidatedChannel in channels),
isUpdaterDown: (_, { xoaTrialState }) =>
isEmpty(xoaTrialState) || xoaTrialState.state === 'ERROR',
packagesList: ({ installedPackages }) =>
@@ -202,7 +252,7 @@ const Updates = decorate([
}) => (
<Container>
<Row>
<Col mediumSize={12}>
<Col mediumSize={6}>
<Card>
<CardHeader>
<UpdateTag /> {LABELS_BY_STATE[xoaUpdaterState]}
@@ -245,7 +295,6 @@ const Updates = decorate([
</ActionButton>{' '}
<ActionButton
btnStyle='success'
data-runningJobsExist={state.areJobsRunning}
disabled={
xoaUpdaterState !== 'upgradeNeeded' &&
xoaTrialState.state !== 'untrustedTrial'
@@ -276,6 +325,56 @@ const Updates = decorate([
</CardBlock>
</Card>
</Col>
<Col mediumSize={6}>
<Card>
<CardHeader>{_('releaseChannels')}</CardHeader>
<CardBlock>
<form id={state.channelsFormId} className='form'>
<div className='form-group'>
<Select
isLoading={state.channelsOptions === undefined}
onChange={effects.onChannelChange}
options={state.channelsOptions}
placeholder={formatMessage(messages.selectChannel)}
required
simpleValue
value={
state.isUnlistedChannel
? UNLISTED_CHANNEL_VALUE
: state.consolidatedChannel
}
/>
<br />
{state.isUnlistedChannel && (
<div className='form-group'>
<DebounceInput
autoFocus
className='form-control'
debounceTimeout={500}
name='channel'
onChange={effects.linkState}
placeholder={formatMessage(
messages.unlistedChannelName
)}
required
type='text'
value={state.consolidatedChannel}
/>
</div>
)}
</div>{' '}
<ActionButton
btnStyle='primary'
form={state.channelsFormId}
handler={effects.configure}
icon='success'
>
{_('changeChannel')}
</ActionButton>
</form>
</CardBlock>
</Card>
</Col>
</Row>
<Row>
<Col mediumSize={6}>
@@ -375,7 +474,7 @@ const Updates = decorate([
</div>{' '}
<div className='form-group'>
<Password
disabled={state.email === undefined}
disabled={helper(state, xoaRegisterState, 'email') === ''}
name='password'
onChange={effects.linkState}
placeholder={formatMessage(

View File

@@ -14,6 +14,7 @@ import { every, filter, find, flatten, forEach, isEmpty, map } from 'lodash'
import { get } from '@xen-orchestra/defined'
import {
addSubscriptions,
adminOnly,
connectStore,
cowSet,
formatSize,
@@ -211,6 +212,7 @@ const XOSAN_INDIVIDUAL_ACTIONS = [
},
]
@adminOnly
@connectStore(() => {
const getHosts = createGetObjectsOfType('host')
const getHostsByPool = getHosts.groupBy('$pool')

View File

@@ -301,7 +301,7 @@ export default class NewXosan extends Component {
}
render() {
if (process.env.XOA_PLAN === 5) {
if (+process.env.XOA_PLAN === 5) {
return (
<em>
{_('xosanSourcesDisclaimer', {

1707
yarn.lock

File diff suppressed because it is too large Load Diff