Compare commits

...

96 Commits

Author SHA1 Message Date
Mohamedox
cbdfccb716 fix 2019-05-27 10:00:44 +02:00
Mohamedox
d605e9740d update CHANGELOG.release 2019-05-23 15:11:08 +02:00
Mohamedox
00d2a88da3 fix(xo-web/new-vm): Automatic networks behaviour Fixes: #4081 2019-05-23 15:10:22 +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
badrAZ
c2ef0ded43 feat(xo-server): 5.41.0 2019-05-14 16:20:09 +02:00
badrAZ
6081a6f6db feat(vhd-lib): 0.7.0 2019-05-14 16:19:15 +02:00
badrAZ
a0d92a0b1d feat(@xen-orchestra/fs): 0.9.0 2019-05-14 16:17:19 +02:00
badrAZ
3cf1f7ede2 feat(xo-server-backup-reports): 0.16.1 2019-05-14 16:15:29 +02:00
Julien Fontanet
5757afa1d8 fix(xo-server/proxies): use Host of the config
Use the hostname of the URL in the config instead of the one from the incoming request.
2019-05-14 14:08:04 +02:00
Julien Fontanet
86e9b9c1b8 fix(xo-server/api/*): wrap with JsonRpcError 2019-05-14 10:21:43 +02:00
Danp2
1cdd1fa00e Switch forum link to new XCP subforum (#4203)
* Fix usage of "backuping"

* Switch to XCP sub-forum
2019-05-10 17:49:26 +02:00
Julien Fontanet
9d12759c68 fix(xo-server/VM import): forward error message 2019-05-10 17:32:23 +02:00
Julien Fontanet
594341fab6 fix(xo-server/remotes): dont fail is benchmarks JSON is broken 2019-05-09 14:06:50 +02:00
Julien Fontanet
4e88125cbe fix(xo-server/remotes): always JSON encode benchmarks (#4197)
See xoa-support#1464

Fix an issue when restoring from config.
2019-05-09 13:58:36 +02:00
badrAZ
13237180a2 fix(Backup-ng): report sent even though "Never" is selected (#4178)
Fixes #4092
2019-05-09 09:58:35 +02:00
Olivier Lambert
f64d7e0b6e fix README title and also update installation doc (#4198) 2019-05-09 09:34:36 +02:00
Olivier Lambert
040a6930a4 improving support page (#4196)
* improving support page
2019-05-09 09:27:26 +02:00
Julien Fontanet
c54b9189a6 feat(scripts/normalize-package): add version if missing 2019-05-09 09:23:49 +02:00
Julien Fontanet
8882f1b019 chore(bump-pkg): stop on errors 2019-05-07 14:39:17 +02:00
Julien Fontanet
ae6416c4d2 feat(xo-server): add XenServers from XenStore (#4194)
XenStore can be used to pass XenServers to connect to to xo-server:

`vm-data/xen-servers` entry:

```json
[
  {
    "allowUnauthorized": true,
    "host": "xs1.company.tld",
    "label": "My XenServer",
    "password": "super%secret+password",
    "readOnly": false,
    "username": "root"
  }
]
```
2019-05-07 10:25:00 +02:00
Dustin B
8faed87656 Documentation typo
Typo "on" to "one" in support page
2019-05-06 22:41:37 +02:00
Dustin B
0983f05969 Update support documentation 2019-05-06 22:27:57 +02:00
Julien Fontanet
d43e2544a1 feat(xo-server): admin account from XenStore (#4184)
XenStore can be used to pass the credentials of the default admin account to xo-server:

`vm-data/admin-account` entry:

```json
{ "email": "admin@admin.net", "password": "admin" }
```
2019-05-06 17:40:02 +02:00
Pierre Donias
ca83d11ac8 fix(xo-server/xapi-object-to-xo): cast patch size to number (#4193)
Fixes #4192
2019-05-06 16:55:54 +02:00
badrAZ
1cdcdd9b5f chore(reaclette): v0.8.0 (#4188) 2019-05-06 15:03:08 +02:00
Julien Fontanet
cc7806e35b chore(xo-server plugins): are not published as well 2019-05-06 11:45:00 +02:00
Julien Fontanet
0ee48b6623 chore(xo-{server,web}): mark as private
Explicit that these packages are not published on the npm registry.
2019-05-06 11:32:55 +02:00
Julien Fontanet
8c02e0efbd chore(scripts): publish on version bumping 2019-05-06 11:32:55 +02:00
marcpezin
34d3ca82bc adding free limited support section (#4186) 2019-05-06 10:21:18 +02:00
HamadaBrest
43822d3667 chore(vhd-lib): use write instead of createOutputStream (#4179)
See #4156
2019-05-03 14:07:10 +02:00
Pierre Donias
f4ac73b3b4 fix(xo-web/new-vm): cloud init should only be available in Premium (#4174) 2019-05-02 17:01:06 +02:00
HamadaBrest
f084b6def9 feat(fs#write): write buffer at specific position (#4169)
Fixes #4156
2019-05-02 16:20:57 +02:00
Julien Fontanet
a00d101ff7 chore: update dependencies 2019-05-02 10:58:38 +02:00
badrAZ
9d5900d9b6 chore(CHANGELOG): 5.34.0 2019-04-30 11:39:03 +02:00
badrAZ
28fb4e8216 feat(xo-web): 5.40.1 2019-04-30 11:21:14 +02:00
Julien Fontanet
bec4dbe652 feat(xo-web): unlock basic stats for all editions (#4172)
Fixes #4166
2019-04-30 10:17:31 +02:00
Pierre Donias
72cc14f508 feat(xo-web): add banner to clarify support conditions (#4167)
Fixes #4165
2019-04-30 09:54:40 +02:00
Julien Fontanet
d20941cc2c chore(CHANGELOG): Highlights sections 2019-04-29 14:10:27 +02:00
Julien Fontanet
9cb8a05316 chore(CHANGELOG): update next 2019-04-26 16:31:26 +02:00
Julien Fontanet
dccd799f6d feat(xo-web): 5.40.0 2019-04-26 16:27:25 +02:00
Julien Fontanet
b42b3d1b01 feat(xo-server): 5.40.0 2019-04-26 16:27:15 +02:00
Julien Fontanet
a40d6f772a feat(complex-matcher): 0.6.0 2019-04-26 16:25:47 +02:00
Julien Fontanet
6e9bfd18d9 feat(xo-server-backup-reports): 0.16.0 2019-04-26 16:24:22 +02:00
Julien Fontanet
3b92dd0139 feat(scripts): bump-pkg 2019-04-26 16:17:28 +02:00
HamadaBrest
564d53610a fix(xo-web/editable): notify user when undo fails (#4157)
Fixes #3799
2019-04-26 11:25:23 +02:00
Pierre Donias
b4c7b8ac7f fix(xo-web/new-vm): typos (#4158)
Introduced by 7acd90307b
2019-04-25 14:44:25 +02:00
HamadaBrest
7acd90307b feat(xo-web/new-vm): network config box for cloud-init (#4150)
Fixes #3872
2019-04-24 17:04:54 +02:00
Julien Fontanet
d3ec76c19f feat(lint): add eslint-comments plugin 2019-04-19 16:27:11 +02:00
HamadaBrest
688cb20674 feat(xo-web/self): remove ID from end user resource sets and add it to Self UI (#4151)
Fixes #4100
2019-04-18 16:45:52 +02:00
HamadaBrest
c63be20bea fix(xo-web/home): J/K navigation loop (#4152)
Fixes #2793
2019-04-18 16:15:25 +02:00
Rajaa.BARHTAOUI
df36633223 feat(xo-web/vm/attach disk): warning if VDI is on another local SR (#4117)
See #3911

Show a warning message if the VM has a VDI sitting on a local SR and the user
select a VDI sitting on a local SR on a different host since the VM won't be
able to start
2019-04-18 16:00:24 +02:00
badrAZ
3597621d88 feat(xo-server-backup-reports): metadata report implementation (#4084) 2019-04-18 09:49:18 +02:00
Pierre Donias
8387684839 fix(xo-web/migrateVm): don't pass SR if same-pool migration (#4146)
Fixes #4145

Introduced by f581e93b88
2019-04-17 16:04:05 +02:00
Pierre Donias
f261f395f1 fix(xo-web/migrateVm): typo (#4147) 2019-04-17 15:31:20 +02:00
Rajaa.BARHTAOUI
f27170ff0e feat(xo-web/vm/disk): notify user before breaking action (#4035)
See #3911

- New disk: warning if the selected SR is local to another host than another VDI
- Migrate VDI (row action only): warning if the selected SR is local to another host than another VDI
2019-04-16 11:04:12 +02:00
Enishowk
d82c951db6 feat(home): use regexp for tags filtering (#4112)
Avoid substring false positives.

Fixes #4087
2019-04-16 10:31:39 +02:00
Rajaa.BARHTAOUI
41ca853e03 feat(xo-web/new-vm): warning on high resource consumption (#4127)
Fixes #4044
2019-04-15 14:26:17 +02:00
Julien Fontanet
a08d098265 chore: update dependencies 2019-04-15 09:54:58 +02:00
Rajaa.BARHTAOUI
875681b8ce fix(xo-web/New VM): template selector won't load (#3565)
Fixes #3265
2019-04-12 14:51:13 +02:00
Julien Fontanet
a03dcbbf55 feat(xo-server): make Helmet configurable (#4141) 2019-04-12 13:49:51 +02:00
badrAZ
97cabbbc69 chore(CHANGELOG): update next 2019-04-11 17:42:52 +02:00
badrAZ
13725a9e21 feat(xo-web): v5.39.1 2019-04-11 17:22:34 +02:00
badrAZ
f47df961f7 fix(xo-web/backup-ng): transfer/merge tasks not displayed in the logs (#4140)
Introduced by 865d2df124
2019-04-11 17:12:47 +02:00
badrAZ
2f644d5eeb chore(CHANGELOG): update next 2019-04-11 16:19:29 +02:00
badrAZ
4b292bb78c feat(xo-web): v5.39.0 2019-04-11 16:02:57 +02:00
badrAZ
804891cc81 feat(xo-server): v5.39.0 2019-04-11 16:00:36 +02:00
127 changed files with 3400 additions and 2555 deletions

View File

@@ -1,5 +1,7 @@
module.exports = {
extends: [
'plugin:eslint-comments/recommended',
'standard',
'standard-jsx',
'prettier',

View File

@@ -46,6 +46,7 @@
"prebuild": "yarn run clean",
"predev": "yarn run prebuild",
"prepare": "yarn run build",
"prepublishOnly": "yarn run build"
"prepublishOnly": "yarn run build",
"postversion": "npm publish"
}
}

View File

@@ -17,5 +17,8 @@
"dependencies": {
"golike-defer": "^0.4.1",
"xen-api": "^0.25.1"
},
"scripts": {
"postversion": "npm publish"
}
}

View File

@@ -55,6 +55,7 @@
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
"prebuild": "yarn run clean",
"predev": "yarn run clean",
"prepublishOnly": "yarn run build"
"prepublishOnly": "yarn run build",
"postversion": "npm publish"
}
}

View File

@@ -43,6 +43,7 @@
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
"prebuild": "yarn run clean",
"predev": "yarn run prebuild",
"prepublishOnly": "yarn run build"
"prepublishOnly": "yarn run build",
"postversion": "npm publish"
}
}

View File

@@ -42,6 +42,7 @@
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
"prebuild": "yarn run clean",
"predev": "yarn run prebuild",
"prepublishOnly": "yarn run build"
"prepublishOnly": "yarn run build",
"postversion": "npm publish"
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@xen-orchestra/fs",
"version": "0.8.0",
"version": "0.9.0",
"license": "AGPL-3.0",
"description": "The File System for Xen Orchestra backups.",
"keywords": [],
@@ -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"
},
@@ -55,6 +55,7 @@
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
"prebuild": "yarn run clean",
"predev": "yarn run clean",
"prepare": "yarn run build"
"prepare": "yarn run build",
"postversion": "npm publish"
}
}

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)
@@ -410,6 +414,18 @@ export default class RemoteHandlerAbstract {
await this._unlink(file).catch(ignoreEnoent)
}
async write(
file: File,
buffer: Buffer,
position: number
): Promise<{| bytesWritten: number, buffer: Buffer |}> {
await this._write(
typeof file === 'string' ? normalizePath(file) : file,
buffer,
position
)
}
async writeFile(
file: string,
data: Data,
@@ -546,6 +562,28 @@ export default class RemoteHandlerAbstract {
throw new Error('Not implemented')
}
async _write(file: File, buffer: Buffer, position: number): Promise<void> {
const isPath = typeof file === 'string'
if (isPath) {
file = await this.openFile(file, 'r+')
}
try {
return await this._writeFd(file, buffer, position)
} finally {
if (isPath) {
await this.closeFile(file)
}
}
}
async _writeFd(
fd: FileDescriptor,
buffer: Buffer,
position: number
): Promise<void> {
throw new Error('Not implemented')
}
async _writeFile(
file: string,
data: Data,

View File

@@ -3,9 +3,9 @@
import 'dotenv/config'
import asyncIteratorToStream from 'async-iterator-to-stream'
import getStream from 'get-stream'
import { forOwn, random } from 'lodash'
import { fromCallback } from 'promise-toolbox'
import { pipeline } from 'readable-stream'
import { random } from 'lodash'
import { tmpdir } from 'os'
import { getHandler } from '.'
@@ -310,5 +310,70 @@ handlers.forEach(url => {
await handler.unlink('file')
})
})
describe('#write()', () => {
beforeEach(() => handler.outputFile('file', TEST_DATA))
const PATCH_DATA_LEN = Math.ceil(TEST_DATA_LEN / 2)
const PATCH_DATA = unsecureRandomBytes(PATCH_DATA_LEN)
forOwn(
{
'dont increase file size': (() => {
const offset = random(0, TEST_DATA_LEN - PATCH_DATA_LEN)
const expected = Buffer.from(TEST_DATA)
PATCH_DATA.copy(expected, offset)
return { offset, expected }
})(),
'increase file size': (() => {
const offset = random(
TEST_DATA_LEN - PATCH_DATA_LEN + 1,
TEST_DATA_LEN
)
const expected = Buffer.alloc(offset + PATCH_DATA_LEN)
TEST_DATA.copy(expected)
PATCH_DATA.copy(expected, offset)
return { offset, expected }
})(),
},
({ offset, expected }, title) => {
describe(title, () => {
testWithFileDescriptor('file', 'r+', async ({ file }) => {
await handler.write(file, PATCH_DATA, offset)
await expect(await handler.readFile('file')).toEqual(expected)
})
})
}
)
})
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,10 +106,18 @@ 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))
}
_writeFd(file, buffer, position) {
return fs.write(file.fd, buffer, 0, buffer.length, position)
}
_writeFile(file, data, { flags }) {
return fs.writeFile(this._getFilePath(file), data, { flag: flags })
}

View File

@@ -155,10 +155,20 @@ 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)
}
_writeFd(file, buffer, position) {
return this._client.write(file.fd, buffer, 0, buffer.length, position)
}
_writeFile(file, data, options) {
return this._client
.writeFile(this._getFilePath(file), data, options)

View File

@@ -48,6 +48,7 @@
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
"prebuild": "yarn run clean",
"predev": "yarn run prebuild",
"prepublishOnly": "yarn run build"
"prepublishOnly": "yarn run build",
"postversion": "npm publish"
}
}

View File

@@ -45,6 +45,7 @@
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
"prebuild": "yarn run clean",
"predev": "yarn run prebuild",
"prepublishOnly": "yarn run build"
"prepublishOnly": "yarn run build",
"postversion": "npm publish"
}
}

View File

@@ -1,5 +1,64 @@
# ChangeLog
## **next** (2019-05-14)
### Enhancements
### 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))
### 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
## **5.34.0** (2019-04-30)
### 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))
- [Metadata backup] Detailed logs [#4005](https://github.com/vatesfr/xen-orchestra/issues/4005) (PR [#4014](https://github.com/vatesfr/xen-orchestra/pull/4014))
- [Backup reports] Support metadata backups (PR [#4084](https://github.com/vatesfr/xen-orchestra/pull/4084))
- [VM migration] Auto select default SR and collapse optional actions [#3326](https://github.com/vatesfr/xen-orchestra/issues/3326) (PR [#4121](https://github.com/vatesfr/xen-orchestra/pull/4121))
- Unlock basic stats on all editions [#4166](https://github.com/vatesfr/xen-orchestra/issues/4166) (PR [#4172](https://github.com/vatesfr/xen-orchestra/pull/4172))
### Enhancements
- [Settings/remotes] Expose mount options field for SMB [#4063](https://github.com/vatesfr/xen-orchestra/issues/4063) (PR [#4067](https://github.com/vatesfr/xen-orchestra/pull/4067))
- [Backup/Schedule] Add warning regarding DST when you add a schedule [#4042](https://github.com/vatesfr/xen-orchestra/issues/4042) (PR [#4056](https://github.com/vatesfr/xen-orchestra/pull/4056))
- [Import] Avoid blocking the UI when dropping a big OVA file on the UI (PR [#4018](https://github.com/vatesfr/xen-orchestra/pull/4018))
- [Backup NG/Overview] Make backup list title clearer [#4111](https://github.com/vatesfr/xen-orchestra/issues/4111) (PR [#4129](https://github.com/vatesfr/xen-orchestra/pull/4129))
- [Dashboard] Hide "Report" section for non-admins [#4123](https://github.com/vatesfr/xen-orchestra/issues/4123) (PR [#4126](https://github.com/vatesfr/xen-orchestra/pull/4126))
- [Self/New VM] Display confirmation modal when user will use a large amount of resources [#4044](https://github.com/vatesfr/xen-orchestra/issues/4044) (PR [#4127](https://github.com/vatesfr/xen-orchestra/pull/4127))
- [VDI migration, New disk] Warning when SR host is different from the other disks [#3911](https://github.com/vatesfr/xen-orchestra/issues/3911) (PR [#4035](https://github.com/vatesfr/xen-orchestra/pull/4035))
- [Attach disk] Display warning message when VDI SR is on different host from the other disks [#3911](https://github.com/vatesfr/xen-orchestra/issues/3911) (PR [#4117](https://github.com/vatesfr/xen-orchestra/pull/4117))
- [Editable] Notify user when editable undo fails [#3799](https://github.com/vatesfr/xen-orchestra/issues/3799) (PR [#4150](https://github.com/vatesfr/xen-orchestra/pull/4157))
- [XO] Add banner for sources users to clarify support conditions [#4165](https://github.com/vatesfr/xen-orchestra/issues/4165) (PR [#4167](https://github.com/vatesfr/xen-orchestra/pull/4167))
### Bug fixes
- [Continuous Replication] Fix VHD size guess for empty files [#4105](https://github.com/vatesfr/xen-orchestra/issues/4105) (PR [#4107](https://github.com/vatesfr/xen-orchestra/pull/4107))
- [Backup NG] Only display full backup interval in case of a delta backup (PR [#4125](https://github.com/vatesfr/xen-orchestra/pull/4107))
- [Dashboard/Health] fix 'an error has occurred' on the storage state table [#4128](https://github.com/vatesfr/xen-orchestra/issues/4128) (PR [#4132](https://github.com/vatesfr/xen-orchestra/pull/4132))
- [Menu] XOA: Fixed empty slot when menu is collapsed [#4012](https://github.com/vatesfr/xen-orchestra/issues/4012) (PR [#4068](https://github.com/vatesfr/xen-orchestra/pull/4068)
- [Self/New VM] Fix missing templates when refreshing page [#3265](https://github.com/vatesfr/xen-orchestra/issues/3265) (PR [#3565](https://github.com/vatesfr/xen-orchestra/pull/3565))
- [Home] No more false positives when select Tag on Home page [#4087](https://github.com/vatesfr/xen-orchestra/issues/4087) (PR [#4112](https://github.com/vatesfr/xen-orchestra/pull/4112))
### Released packages
- xo-server-backup-reports v0.16.0
- complex-matcher v0.6.0
- xo-vmdk-to-vhd v0.1.7
- vhd-lib v0.6.1
- xo-server v5.40.0
- xo-web v5.40.1
## **5.33.1** (2019-04-04)
### Bug fix

View File

@@ -2,24 +2,19 @@
### Enhancements
- [Settings/remotes] Expose mount options field for SMB [#4063](https://github.com/vatesfr/xen-orchestra/issues/4063) (PR [#4067](https://github.com/vatesfr/xen-orchestra/pull/4067))
- [Backup/Schedule] Add warning regarding DST when you add a schedule [#4042](https://github.com/vatesfr/xen-orchestra/issues/4042) (PR [#4056](https://github.com/vatesfr/xen-orchestra/pull/4056))
- [Import] Avoid blocking the UI when dropping a big OVA file on the UI (PR [#4018](https://github.com/vatesfr/xen-orchestra/pull/4018))
- [Backup NG/Overview] Make backup list title clearer [#4111](https://github.com/vatesfr/xen-orchestra/issues/4111) (PR [#4129](https://github.com/vatesfr/xen-orchestra/pull/4129))
- [Dashboard] Hide "Report" section for non-admins [#4123](https://github.com/vatesfr/xen-orchestra/issues/4123) (PR [#4126](https://github.com/vatesfr/xen-orchestra/pull/4126))
- [VM migration] Auto select default SR and collapse optional actions [#3326](https://github.com/vatesfr/xen-orchestra/issues/3326) (PR [#4121](https://github.com/vatesfr/xen-orchestra/pull/4121))
- [Metadata backup] Logs [#4005](https://github.com/vatesfr/xen-orchestra/issues/4005) (PR [#4014](https://github.com/vatesfr/xen-orchestra/pull/4014))
- [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))
### Bug fixes
- [Continuous Replication] Fix VHD size guess for empty files [#4105](https://github.com/vatesfr/xen-orchestra/issues/4105) (PR [#4107](https://github.com/vatesfr/xen-orchestra/pull/4107))
- [Backup NG] Only display full backup interval in case of a delta backup (PR [#4125](https://github.com/vatesfr/xen-orchestra/pull/4107))
- [Dashboard/Health] fix 'an error has occurred' on the storage state table [#4128](https://github.com/vatesfr/xen-orchestra/issues/4128) (PR [#4132](https://github.com/vatesfr/xen-orchestra/pull/4132))
- [Menu] XOA: Fixed empty slot when menu is collapsed [#4012](https://github.com/vatesfr/xen-orchestra/issues/4012) (PR [#4068](https://github.com/vatesfr/xen-orchestra/pull/4068)
- [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
- [Upgrade] Fix alert before upgrade while running backup jobs (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))
### Released packages
- xo-vmdk-to-vhd v0.1.7
- vhd-lib v0.6.1
- xo-server v5.39.0
- xo-web v5.39.0
- xo-server v5.42.0
- xo-web v5.42.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

@@ -1,4 +1,4 @@
# Xen Orchestra [![Chat with us](https://storage.crisp.im/plugins/images/936925df-f37b-4ba8-bab0-70cd2edcb0be/badge.svg)](https://go.crisp.im/chat/embed/?website_id=-JzqzzwddSV7bKGtEyAQ) [![Build Status](https://travis-ci.org/vatesfr/xen-orchestra.png?branch=master)](https://travis-ci.org/vatesfr/xen-orchestra)
# Xen Orchestra [![Build Status](https://travis-ci.org/vatesfr/xen-orchestra.png?branch=master)](https://travis-ci.org/vatesfr/xen-orchestra)
![](http://i.imgur.com/tRffA5y.png)

View File

@@ -1,13 +1,13 @@
# Installation
SSH to your XenServer and execute the following:
SSH to your XenServer/XCP-ng host and execute the following:
```
bash -c "$(curl -s http://xoa.io/deploy)"
```
This will automatically download/import/start the XOA appliance. Nothing is changed on your XenServer host itself, it's 100% safe.
This will automatically download/import/start the XOA appliance. Nothing is changed on your host itself, it's 100% safe.
## [More on XOA](xoa.md)

View File

@@ -1,24 +1,33 @@
# Support
You can access our pro support if you subscribe to any of these plans:
Xen Orchestra will run in a controlled/tested environment thanks to XOA ([Xen Orchestra virtual Appliance](https://xen-orchestra.com/#!/xoa)). **This is the way to get pro support**. Any account with a registered XOA can access a [dedicated support panel](https://xen-orchestra.com/#!/member/support).
XOA is available in multiple plans:
* Free
* Starter
* Enterprise
* Premium
The better the plan, the faster the support will be with higher priority.
Higher tier support plans include faster ticket response times (and cover more features). Paid support plans and response times are based on the plan you have, plans can be [reviewed here](https://xen-orchestra.com/#!/xo-pricing).
## XOA Free support
With the free version of the Xen Orchestra Appliance (XOA free), you can open support tickets and we will do our best to assist you, however, this support is limited and is not guaranteed in regards to response times or resolutions offered.
## Community support
If you are using Xen Orchestra via the sources, you can ask questions and try to recieve help two different ways:
If you are using Xen Orchestra via the source and not XOA, you can ask questions and try to recieve help through a number of different ways:
* In our [forum](https://xen-orchestra.com/forum/)
* In our [forum](https://xcp-ng.org/forum/category/12/xen-orchestra)
* In our IRC - `#xen-orchestra` on `Freenode`
However, there's no guarantee you will receive an answer and no guaranteed response time. If you are using XO from sources, we encourage you to give back to the community by assisting other users via these two avenues as well.
We encourage you to give back to the community by assisting other users via these two avenues as well.
If you are using Xen Orchestra in production, please subscribe to a plan.
Lastly while Xen Orchestra is free and Open Source software, supporting and developing it takes a lot of effort. If you are considering using Xen Orchestra in production, please subscribe for one of our [professional support plans](https://xen-orchestra.com/#!/xo-pricing).
> Note: support from the sources is harder, because Xen Orchestra can potentially run on any Linux distro (or even FreeBSD and Windows!). Always try to double check that you followed our guide on how to [install it from the sources](https://xen-orchestra.com/docs/from_the_sources.html) before going further.
## Open a ticket
If you have a subscription, you can open a ticket describing your issue directly from your personal account page [here](https://xen-orchestra.com/#!/member/support)
If you have a subscription (or at least a registered free XOA), you can open a ticket describing your issue directly from your personal account page [here](https://xen-orchestra.com/#!/member/support)

View File

@@ -10,15 +10,16 @@
"eslint-config-prettier": "^4.1.0",
"eslint-config-standard": "12.0.0",
"eslint-config-standard-jsx": "^6.0.2",
"eslint-plugin-eslint-comments": "^3.1.1",
"eslint-plugin-import": "^2.8.0",
"eslint-plugin-node": "^8.0.0",
"eslint-plugin-node": "^9.0.1",
"eslint-plugin-promise": "^4.0.0",
"eslint-plugin-react": "^7.6.1",
"eslint-plugin-standard": "^4.0.0",
"exec-promise": "^0.7.0",
"flow-bin": "^0.96.0",
"flow-bin": "^0.98.0",
"globby": "^9.0.0",
"husky": "^1.2.1",
"husky": "^2.2.0",
"jest": "^24.1.0",
"lodash": "^4.17.4",
"prettier": "^1.10.2",

View File

@@ -1,6 +1,6 @@
{
"name": "complex-matcher",
"version": "0.5.0",
"version": "0.6.0",
"license": "ISC",
"description": "",
"keywords": [],
@@ -44,6 +44,7 @@
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
"prebuild": "yarn run clean",
"predev": "yarn run prebuild",
"prepublishOnly": "yarn run build"
"prepublishOnly": "yarn run build",
"postversion": "npm publish"
}
}

View File

@@ -599,6 +599,13 @@ export const parse = parser.parse.bind(parser)
// -------------------------------------------------------------------
const _extractStringFromRegexp = child => {
const unescapedRegexp = child.re.source.replace(/^(\^)|\\|\$$/g, '')
if (child.re.source === `^${escapeRegExp(unescapedRegexp)}$`) {
return unescapedRegexp
}
}
const _getPropertyClauseStrings = ({ child }) => {
if (child instanceof Or) {
const strings = []
@@ -606,6 +613,12 @@ const _getPropertyClauseStrings = ({ child }) => {
if (child instanceof StringNode) {
strings.push(child.value)
}
if (child instanceof RegExpNode) {
const unescapedRegexp = _extractStringFromRegexp(child)
if (unescapedRegexp !== undefined) {
strings.push(unescapedRegexp)
}
}
})
return strings
}
@@ -613,6 +626,12 @@ const _getPropertyClauseStrings = ({ child }) => {
if (child instanceof StringNode) {
return [child.value]
}
if (child instanceof RegExpNode) {
const unescapedRegexp = _extractStringFromRegexp(child)
if (unescapedRegexp !== undefined) {
return [unescapedRegexp]
}
}
return []
}

View File

@@ -12,10 +12,13 @@ import {
} from './'
it('getPropertyClausesStrings', () => {
const tmp = getPropertyClausesStrings(parse('foo bar:baz baz:|(foo bar)'))
const tmp = getPropertyClausesStrings(
parse('foo bar:baz baz:|(foo bar /^boo$/ /^far$/) foo:/^bar$/')
)
expect(tmp).toEqual({
bar: ['baz'],
baz: ['foo', 'bar'],
baz: ['foo', 'bar', 'boo', 'far'],
foo: ['bar'],
})
})

View File

@@ -43,6 +43,7 @@
"prebuild": "yarn run clean",
"predev": "yarn run prebuild",
"prepare": "yarn run build",
"prepublishOnly": "yarn run build"
"prepublishOnly": "yarn run build",
"postversion": "npm publish"
}
}

View File

@@ -27,12 +27,12 @@
"node": ">=6"
},
"dependencies": {
"@xen-orchestra/fs": "^0.8.0",
"@xen-orchestra/fs": "^0.9.0",
"cli-progress": "^2.0.0",
"exec-promise": "^0.7.0",
"getopts": "^2.2.3",
"struct-fu": "^1.2.0",
"vhd-lib": "^0.6.1"
"vhd-lib": "^0.7.0"
},
"devDependencies": {
"@babel/cli": "^7.0.0",
@@ -51,6 +51,7 @@
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
"prebuild": "rimraf dist/ && index-modules --cjs-lazy src/commands",
"predev": "yarn run prebuild",
"prepare": "yarn run build"
"prepare": "yarn run build",
"postversion": "npm publish"
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "vhd-lib",
"version": "0.6.1",
"version": "0.7.0",
"license": "AGPL-3.0",
"description": "Primitives for VHD file handling",
"keywords": [],
@@ -22,9 +22,9 @@
},
"dependencies": {
"async-iterator-to-stream": "^1.0.2",
"core-js": "3.0.0",
"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",
@@ -35,7 +35,7 @@
"@babel/core": "^7.0.0",
"@babel/preset-env": "^7.0.0",
"@babel/preset-flow": "^7.0.0",
"@xen-orchestra/fs": "^0.8.0",
"@xen-orchestra/fs": "^0.9.0",
"babel-plugin-lodash": "^3.3.2",
"cross-env": "^5.1.3",
"execa": "^1.0.0",
@@ -52,6 +52,7 @@
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
"prebuild": "yarn run clean",
"predev": "yarn run clean",
"prepare": "yarn run build"
"prepare": "yarn run build",
"postversion": "npm publish"
}
}

View File

@@ -1,9 +1,7 @@
import assert from 'assert'
import { fromEvent } from 'promise-toolbox'
import checkFooter from './_checkFooter'
import checkHeader from './_checkHeader'
import constantStream from './_constant-stream'
import getFirstAndLastBlocks from './_getFirstAndLastBlocks'
import { fuFooter, fuHeader, checksumStruct, unpackField } from './_structs'
import { set as mapSetBit, test as mapTestBit } from './_bitmap'
@@ -232,24 +230,11 @@ export default class Vhd {
// Write functions.
// =================================================================
// Write a buffer/stream at a given position in a vhd file.
// Write a buffer at a given position in a vhd file.
async _write(data, offset) {
debug(
`_write offset=${offset} size=${
Buffer.isBuffer(data) ? data.length : '???'
}`
)
// TODO: could probably be merged in remote handlers.
const stream = await this._handler.createOutputStream(this._path, {
flags: 'r+',
start: offset,
})
return Buffer.isBuffer(data)
? new Promise((resolve, reject) => {
stream.on('error', reject)
stream.end(data, resolve)
})
: fromEvent(data.pipe(stream), 'finish')
assert(Buffer.isBuffer(data))
debug(`_write offset=${offset} size=${data.length}`)
return this._handler.write(this._path, data, offset)
}
async _freeFirstBlockSpace(spaceNeededBytes) {
@@ -306,7 +291,7 @@ export default class Vhd {
`ensureBatSize: extend BAT ${prevMaxTableEntries} -> ${maxTableEntries}`
)
await this._write(
constantStream(BUF_BLOCK_UNUSED, maxTableEntries - prevMaxTableEntries),
Buffer.alloc(maxTableEntries - prevMaxTableEntries, BUF_BLOCK_UNUSED),
header.tableOffset + prevBat.length
)
await this.writeHeader()
@@ -331,10 +316,7 @@ export default class Vhd {
await Promise.all([
// Write an empty block and addr in vhd file.
this._write(
constantStream([0], this.fullBlockSize),
sectorsToBytes(blockAddr)
),
this._write(Buffer.alloc(this.fullBlockSize), sectorsToBytes(blockAddr)),
this._setBatEntry(blockId, blockAddr),
])

View File

@@ -56,6 +56,7 @@
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
"prebuild": "rimraf dist/",
"predev": "yarn run prebuild",
"prepublishOnly": "yarn run build"
"prepublishOnly": "yarn run build",
"postversion": "npm publish"
}
}

View File

@@ -69,6 +69,7 @@
"plot": "gnuplot -p memory-test.gnu",
"prebuild": "rimraf dist/",
"predev": "yarn run prebuild",
"prepublishOnly": "yarn run build"
"prepublishOnly": "yarn run build",
"postversion": "npm publish"
}
}

View File

@@ -25,5 +25,8 @@
},
"dependencies": {
"xo-common": "^0.2.0"
},
"scripts": {
"postversion": "npm publish"
}
}

View File

@@ -64,6 +64,7 @@
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
"prebuild": "rimraf dist/",
"predev": "yarn run prebuild",
"prepublishOnly": "yarn run build"
"prepublishOnly": "yarn run build",
"postversion": "npm publish"
}
}

View File

@@ -43,6 +43,7 @@
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
"prebuild": "rimraf dist/",
"predev": "yarn run prebuild",
"prepublishOnly": "yarn run build"
"prepublishOnly": "yarn run build",
"postversion": "npm publish"
}
}

View File

@@ -45,6 +45,7 @@
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
"prebuild": "yarn run clean",
"predev": "yarn run prebuild",
"prepublishOnly": "yarn run build"
"prepublishOnly": "yarn run build",
"postversion": "npm publish"
}
}

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",
@@ -55,6 +55,7 @@
"lint": "tslint 'src/*.ts'",
"posttest": "yarn run lint",
"prepublishOnly": "yarn run build",
"start": "node dist/index.js"
"start": "node dist/index.js",
"postversion": "npm publish"
}
}

View File

@@ -32,7 +32,7 @@
"node": ">=6"
},
"dependencies": {
"jsonrpc-websocket-client": "^0.4.1",
"jsonrpc-websocket-client": "^0.5.0",
"lodash": "^4.17.2",
"make-error": "^1.0.4"
},
@@ -49,6 +49,7 @@
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
"prebuild": "rimraf dist/",
"predev": "yarn run prebuild",
"prepublishOnly": "yarn run build"
"prepublishOnly": "yarn run build",
"postversion": "npm publish"
}
}

View File

@@ -41,6 +41,7 @@
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
"prebuild": "rimraf dist/",
"predev": "yarn run prebuild",
"prepare": "yarn run build"
"prepare": "yarn run build",
"postversion": "npm publish"
}
}

View File

@@ -41,5 +41,6 @@
"build": "NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
"dev": "NODE_DEV=development babel --watch --source-maps --out-dir=dist/ src/",
"prepublishOnly": "yarn run build"
}
},
"private": true
}

View File

@@ -49,5 +49,6 @@
"prebuild": "rimraf dist/",
"predev": "yarn run prebuild",
"prepublishOnly": "yarn run build"
}
},
"private": true
}

View File

@@ -55,5 +55,6 @@
"prebuild": "rimraf dist/",
"predev": "yarn run prebuild",
"prepublishOnly": "yarn run build"
}
},
"private": true
}

View File

@@ -50,5 +50,6 @@
"prebuild": "yarn run clean",
"predev": "yarn run prebuild",
"prepublishOnly": "yarn run build"
}
},
"private": true
}

View File

@@ -1,6 +1,6 @@
{
"name": "xo-server-backup-reports",
"version": "0.15.0",
"version": "0.16.1",
"license": "AGPL-3.0",
"description": "Backup reports plugin for XO-Server",
"keywords": [
@@ -36,6 +36,7 @@
"node": ">=6"
},
"dependencies": {
"@xen-orchestra/log": "^0.1.4",
"human-format": "^0.10.0",
"lodash": "^4.13.1",
"moment-timezone": "^0.5.13"
@@ -43,6 +44,8 @@
"devDependencies": {
"@babel/cli": "^7.0.0",
"@babel/core": "^7.0.0",
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.4.3",
"@babel/plugin-proposal-optional-chaining": "^7.2.0",
"@babel/preset-env": "^7.0.0",
"babel-plugin-lodash": "^3.3.2",
"cross-env": "^5.1.3",
@@ -55,5 +58,6 @@
"prebuild": "yarn run clean",
"predev": "yarn run prebuild",
"prepublishOnly": "yarn run build"
}
},
"private": true
}

View File

@@ -1,8 +1,11 @@
import createLogger from '@xen-orchestra/log'
import humanFormat from 'human-format'
import moment from 'moment-timezone'
import { forEach, get, startCase } from 'lodash'
import { forEach, groupBy, startCase } from 'lodash'
import pkg from '../package'
const logger = createLogger('xo:xo-server-backup-reports')
export const configurationSchema = {
type: 'object',
@@ -46,6 +49,9 @@ export const testSchema = {
// ===================================================================
const INDENT = ' '
const UNKNOWN_ITEM = 'Unknown'
const ICON_FAILURE = '🚨'
const ICON_INTERRUPTED = '⚠️'
const ICON_SKIPPED = '⏩'
@@ -60,7 +66,7 @@ const STATUS_ICON = {
}
const DATE_FORMAT = 'dddd, MMMM Do YYYY, h:mm:ss a'
const createDateFormater = timezone =>
const createDateFormatter = timezone =>
timezone !== undefined
? timestamp =>
moment(timestamp)
@@ -86,10 +92,6 @@ const formatSpeed = (bytes, milliseconds) =>
})
: 'N/A'
const logError = e => {
console.error('backup report error:', e)
}
const NO_VMS_MATCH_THIS_PATTERN = 'no VMs match this pattern'
const NO_SUCH_OBJECT_ERROR = 'no such object'
const UNHEALTHY_VDI_CHAIN_ERROR = 'unhealthy VDI chain'
@@ -100,40 +102,114 @@ const isSkippedError = error =>
error.message === UNHEALTHY_VDI_CHAIN_ERROR ||
error.message === NO_SUCH_OBJECT_ERROR
const INDENT = ' '
const createGetTemporalDataMarkdown = formatDate => (
start,
end,
nbIndent = 0
) => {
const indent = INDENT.repeat(nbIndent)
// ===================================================================
const markdown = [`${indent}- **Start time**: ${formatDate(start)}`]
const STATUS = ['failure', 'interrupted', 'skipped', 'success']
const TITLE_BY_STATUS = {
failure: n => `## ${n} Failure${n === 1 ? '' : 's'}`,
interrupted: n => `## ${n} Interrupted`,
skipped: n => `## ${n} Skipped`,
success: n => `## ${n} Success${n === 1 ? '' : 'es'}`,
}
const getTemporalDataMarkdown = (end, start, formatDate) => {
const markdown = [`- **Start time**: ${formatDate(start)}`]
if (end !== undefined) {
markdown.push(`${indent}- **End time**: ${formatDate(end)}`)
markdown.push(`- **End time**: ${formatDate(end)}`)
const duration = end - start
if (duration >= 1) {
markdown.push(`${indent}- **Duration**: ${formatDuration(duration)}`)
markdown.push(`- **Duration**: ${formatDuration(duration)}`)
}
}
return markdown
}
const addWarnings = (text, warnings, nbIndent = 0) => {
if (warnings === undefined) {
const getWarningsMarkdown = (warnings = []) =>
warnings.map(({ message }) => `- **${ICON_WARNING} ${message}**`)
const getErrorMarkdown = task => {
let message
if (
task.status === 'success' ||
(message = task.result?.message ?? task.result?.code) === undefined
) {
return
}
const indent = INDENT.repeat(nbIndent)
warnings.forEach(({ message }) => {
text.push(`${indent}- **${ICON_WARNING} ${message}**`)
})
const label = task.status === 'skipped' ? 'Reason' : 'Error'
return `- **${label}**: ${message}`
}
const MARKDOWN_BY_TYPE = {
pool(task, { formatDate }) {
const { pool, poolMaster = {} } = task.data
const name = pool.name_label || poolMaster.name_label || UNKNOWN_ITEM
return {
body: [
`- **UUID**: ${pool.uuid}`,
...getTemporalDataMarkdown(task.end, task.start, formatDate),
getErrorMarkdown(task),
],
title: `[pool] ${name}`,
}
},
xo(task, { formatDate, jobName }) {
return {
body: [
...getTemporalDataMarkdown(task.end, task.start, formatDate),
getErrorMarkdown(task),
],
title: `[XO] ${jobName}`,
}
},
async remote(task, { formatDate, xo }) {
const id = task.data.id
const name = await xo.getRemote(id).then(
({ name }) => name,
error => {
logger.warn(error)
return UNKNOWN_ITEM
}
)
return {
body: [
`- **ID**: ${id}`,
...getTemporalDataMarkdown(task.end, task.start, formatDate),
getErrorMarkdown(task),
],
title: `[remote] ${name}`,
}
},
}
const getMarkdown = (task, props) =>
MARKDOWN_BY_TYPE[(task.data?.type)]?.(task, props)
const toMarkdown = parts => {
const lines = []
let indentLevel = 0
const helper = part => {
if (typeof part === 'string') {
lines.push(`${INDENT.repeat(indentLevel)}${part}`)
} else if (Array.isArray(part)) {
++indentLevel
part.forEach(helper)
--indentLevel
}
}
helper(parts)
return lines.join('\n')
}
// ===================================================================
class BackupReportsXoPlugin {
constructor(xo) {
this._xo = xo
this._report = this._wrapper.bind(this)
this._report = this._report.bind(this)
}
configure({ toMails, toXmpp }) {
@@ -146,72 +222,174 @@ class BackupReportsXoPlugin {
}
test({ runId }) {
return this._backupNgListener(undefined, undefined, undefined, runId)
return this._report(runId, undefined, true)
}
unload() {
this._xo.removeListener('job:terminated', this._report)
}
_wrapper(status, job, schedule, runJobId) {
if (job.type === 'metadataBackup') {
return
}
async _report(runJobId, { type, status } = {}, force) {
const xo = this._xo
try {
if (type === 'call') {
return this._legacyVmHandler(status)
}
return new Promise(resolve =>
resolve(
job.type === 'backup'
? this._backupNgListener(status, job, schedule, runJobId)
: this._listener(status, job, schedule, runJobId)
)
).catch(logError)
const log = await xo.getBackupNgLogs(runJobId)
if (log === undefined) {
throw new Error(`no log found with runId=${JSON.stringify(runJobId)}`)
}
const reportWhen = log.data.reportWhen
if (
!force &&
(reportWhen === 'never' ||
// Handle improper value introduced by:
// https://github.com/vatesfr/xen-orchestra/commit/753ee994f2948bbaca9d3161eaab82329a682773#diff-9c044ab8a42ed6576ea927a64c1ec3ebR105
reportWhen === 'Never' ||
(reportWhen === 'failure' && log.status === 'success'))
) {
return
}
const [job, schedule] = await Promise.all([
await xo.getJob(log.jobId),
await xo.getSchedule(log.scheduleId).catch(error => {
logger.warn(error)
}),
])
if (job.type === 'backup') {
return this._ngVmHandler(log, job, schedule, force)
} else if (job.type === 'metadataBackup') {
return this._metadataHandler(log, job, schedule, force)
}
throw new Error(`Unknown backup job type: ${job.type}`)
} catch (error) {
logger.warn(error)
}
}
async _backupNgListener(_1, _2, schedule, runJobId) {
async _metadataHandler(log, { name: jobName }, schedule, force) {
const xo = this._xo
const log = await xo.getBackupNgLogs(runJobId)
if (log === undefined) {
throw new Error(`no log found with runId=${JSON.stringify(runJobId)}`)
const formatDate = createDateFormatter(schedule?.timezone)
const tasksByStatus = groupBy(log.tasks, 'status')
const n = log.tasks?.length ?? 0
const nSuccesses = tasksByStatus.success?.length ?? 0
if (!force && log.data.reportWhen === 'failure') {
delete tasksByStatus.success
}
// header
const markdown = [
`## Global status: ${log.status}`,
'',
`- **Job ID**: ${log.jobId}`,
`- **Job name**: ${jobName}`,
`- **Run ID**: ${log.id}`,
...getTemporalDataMarkdown(log.end, log.start, formatDate),
n !== 0 && `- **Successes**: ${nSuccesses} / ${n}`,
...getWarningsMarkdown(log.warnings),
getErrorMarkdown(log),
]
const nagiosText = []
// body
for (const status of STATUS) {
const tasks = tasksByStatus[status]
if (tasks === undefined) {
continue
}
// tasks header
markdown.push('---', '', TITLE_BY_STATUS[status](tasks.length))
// tasks body
for (const task of tasks) {
const taskMarkdown = await getMarkdown(task, {
formatDate,
jobName: log.jobName,
})
if (taskMarkdown === undefined) {
continue
}
const { title, body } = taskMarkdown
const subMarkdown = [...body, ...getWarningsMarkdown(task.warnings)]
if (task.status !== 'success') {
nagiosText.push(`[${task.status}] ${title}`)
}
for (const subTask of task.tasks ?? []) {
const taskMarkdown = await getMarkdown(subTask, { formatDate, xo })
if (taskMarkdown === undefined) {
continue
}
const icon = STATUS_ICON[subTask.status]
const { title, body } = taskMarkdown
subMarkdown.push([
`- **${title}** ${icon}`,
[...body, ...getWarningsMarkdown(subTask.warnings)],
])
}
markdown.push('', '', `### ${title}`, ...subMarkdown)
}
}
// footer
markdown.push('---', '', `*${pkg.name} v${pkg.version}*`)
return this._sendReport({
subject: `[Xen Orchestra] ${log.status} Metadata backup report for ${
log.jobName
} ${STATUS_ICON[log.status]}`,
markdown: toMarkdown(markdown),
nagiosStatus: log.status === 'success' ? 0 : 2,
nagiosMarkdown:
log.status === 'success'
? `[Xen Orchestra] [Success] Metadata backup report for ${
log.jobName
}`
: `[Xen Orchestra] [${log.status}] Metadata backup report for ${
log.jobName
} - ${nagiosText.join(' ')}`,
})
}
async _ngVmHandler(log, { name: jobName }, schedule, force) {
const xo = this._xo
const { reportWhen, mode } = log.data || {}
if (
reportWhen === 'never' ||
(log.status === 'success' && reportWhen === 'failure')
) {
return
}
if (schedule === undefined) {
schedule = await xo.getSchedule(log.scheduleId)
}
const formatDate = createDateFormatter(schedule?.timezone)
const jobName = (await xo.getJob(log.jobId, 'backup')).name
const formatDate = createDateFormater(schedule.timezone)
const getTemporalDataMarkdown = createGetTemporalDataMarkdown(formatDate)
if (
(log.status === 'failure' || log.status === 'skipped') &&
log.result !== undefined
) {
let markdown = [
if (log.tasks === undefined) {
const markdown = [
`## Global status: ${log.status}`,
'',
`- **Job ID**: ${log.jobId}`,
`- **Run ID**: ${runJobId}`,
`- **Run ID**: ${log.id}`,
`- **mode**: ${mode}`,
...getTemporalDataMarkdown(log.start, log.end),
`- **Error**: ${log.result.message}`,
...getTemporalDataMarkdown(log.end, log.start, formatDate),
getErrorMarkdown(log),
...getWarningsMarkdown(log.warnings),
'---',
'',
`*${pkg.name} v${pkg.version}*`,
]
addWarnings(markdown, log.warnings)
markdown.push('---', '', `*${pkg.name} v${pkg.version}*`)
markdown = markdown.join('\n')
return this._sendReport({
subject: `[Xen Orchestra] ${
log.status
} Backup report for ${jobName} ${STATUS_ICON[log.status]}`,
markdown,
markdown: toMarkdown(markdown),
nagiosStatus: 2,
nagiosMarkdown: `[Xen Orchestra] [${
log.status
@@ -231,7 +409,7 @@ class BackupReportsXoPlugin {
let nSkipped = 0
let nInterrupted = 0
for (const taskLog of log.tasks) {
if (taskLog.status === 'success' && reportWhen === 'failure') {
if (!force && taskLog.status === 'success' && reportWhen === 'failure') {
continue
}
@@ -244,16 +422,16 @@ class BackupReportsXoPlugin {
`### ${vm !== undefined ? vm.name_label : 'VM not found'}`,
'',
`- **UUID**: ${vm !== undefined ? vm.uuid : vmId}`,
...getTemporalDataMarkdown(taskLog.start, taskLog.end),
...getTemporalDataMarkdown(taskLog.end, taskLog.start, formatDate),
...getWarningsMarkdown(taskLog.warnings),
]
addWarnings(text, taskLog.warnings)
const failedSubTasks = []
const snapshotText = []
const srsText = []
const remotesText = []
for (const subTaskLog of taskLog.tasks || []) {
for (const subTaskLog of taskLog.tasks ?? []) {
if (
subTaskLog.message !== 'export' &&
subTaskLog.message !== 'snapshot'
@@ -262,29 +440,36 @@ class BackupReportsXoPlugin {
}
const icon = STATUS_ICON[subTaskLog.status]
const errorMessage = ` - **Error**: ${get(
subTaskLog.result,
'message'
)}`
const type = subTaskLog.data?.type
const errorMarkdown = getErrorMarkdown(subTaskLog)
if (subTaskLog.message === 'snapshot') {
snapshotText.push(
`- **Snapshot** ${icon}`,
...getTemporalDataMarkdown(subTaskLog.start, subTaskLog.end, 1)
)
} else if (subTaskLog.data.type === 'remote') {
snapshotText.push(`- **Snapshot** ${icon}`, [
...getTemporalDataMarkdown(
subTaskLog.end,
subTaskLog.start,
formatDate
),
])
} else if (type === 'remote') {
const id = subTaskLog.data.id
const remote = await xo.getRemote(id).catch(() => {})
remotesText.push(
` - **${
remote !== undefined ? remote.name : `Remote Not found`
}** (${id}) ${icon}`,
...getTemporalDataMarkdown(subTaskLog.start, subTaskLog.end, 2)
)
addWarnings(remotesText, subTaskLog.warnings, 2)
const remote = await xo.getRemote(id).catch(error => {
logger.warn(error)
})
const title = remote !== undefined ? remote.name : `Remote Not found`
remotesText.push(`- **${title}** (${id}) ${icon}`, [
...getTemporalDataMarkdown(
subTaskLog.end,
subTaskLog.start,
formatDate
),
...getWarningsMarkdown(subTaskLog.warnings),
errorMarkdown,
])
if (subTaskLog.status === 'failure') {
failedSubTasks.push(remote !== undefined ? remote.name : id)
remotesText.push('', errorMessage)
}
} else {
const id = subTaskLog.data.id
@@ -294,14 +479,17 @@ class BackupReportsXoPlugin {
} catch (e) {}
const [srName, srUuid] =
sr !== undefined ? [sr.name_label, sr.uuid] : [`SR Not found`, id]
srsText.push(
` - **${srName}** (${srUuid}) ${icon}`,
...getTemporalDataMarkdown(subTaskLog.start, subTaskLog.end, 2)
)
addWarnings(srsText, subTaskLog.warnings, 2)
srsText.push(`- **${srName}** (${srUuid}) ${icon}`, [
...getTemporalDataMarkdown(
subTaskLog.end,
subTaskLog.start,
formatDate
),
...getWarningsMarkdown(subTaskLog.warnings),
errorMarkdown,
])
if (subTaskLog.status === 'failure') {
failedSubTasks.push(sr !== undefined ? sr.name_label : id)
srsText.push('', errorMessage)
}
}
@@ -313,53 +501,48 @@ class BackupReportsXoPlugin {
return
}
const operationInfoText = []
addWarnings(operationInfoText, operationLog.warnings, 3)
if (operationLog.status === 'success') {
const size = operationLog.result.size
const size = operationLog.result?.size
if (size > 0) {
if (operationLog.message === 'merge') {
globalMergeSize += size
} else {
globalTransferSize += size
}
}
operationInfoText.push(
` - **Size**: ${formatSize(size)}`,
` - **Speed**: ${formatSpeed(
size,
operationLog.end - operationLog.start
)}`
)
} else if (get(operationLog.result, 'message') !== undefined) {
operationInfoText.push(
` - **Error**: ${get(operationLog.result, 'message')}`
)
}
const operationText = [
` - **${operationLog.message}** ${
STATUS_ICON[operationLog.status]
}`,
...getTemporalDataMarkdown(operationLog.start, operationLog.end, 3),
...operationInfoText,
].join('\n')
if (get(subTaskLog, 'data.type') === 'remote') {
`- **${operationLog.message}** ${STATUS_ICON[operationLog.status]}`,
[
...getTemporalDataMarkdown(
operationLog.end,
operationLog.start,
formatDate
),
size > 0 && `- **Size**: ${formatSize(size)}`,
size > 0 &&
`- **Speed**: ${formatSpeed(
size,
operationLog.end - operationLog.start
)}`,
...getWarningsMarkdown(operationLog.warnings),
getErrorMarkdown(operationLog),
],
]
if (type === 'remote') {
remotesText.push(operationText)
remotesText.join('\n')
}
if (get(subTaskLog, 'data.type') === 'SR') {
} else if (type === 'SR') {
srsText.push(operationText)
srsText.join('\n')
}
})
}
if (srsText.length !== 0) {
srsText.unshift(`- **SRs**`)
}
if (remotesText.length !== 0) {
remotesText.unshift(`- **Remotes**`)
}
const subText = [...snapshotText, '', ...srsText, '', ...remotesText]
const subText = [
...snapshotText,
srsText.length !== 0 && `- **SRs**`,
srsText,
remotesText.length !== 0 && `- **Remotes**`,
remotesText,
]
if (taskLog.result !== undefined) {
if (taskLog.status === 'skipped') {
++nSkipped
@@ -369,8 +552,7 @@ class BackupReportsXoPlugin {
taskLog.result.message === UNHEALTHY_VDI_CHAIN_ERROR
? UNHEALTHY_VDI_CHAIN_MESSAGE
: taskLog.result.message
}`,
''
}`
)
nagiosText.push(
`[(Skipped) ${vm !== undefined ? vm.name_label : 'undefined'} : ${
@@ -379,11 +561,7 @@ class BackupReportsXoPlugin {
)
} else {
++nFailures
failedVmsText.push(
...text,
`- **Error**: ${taskLog.result.message}`,
''
)
failedVmsText.push(...text, `- **Error**: ${taskLog.result.message}`)
nagiosText.push(
`[(Failed) ${vm !== undefined ? vm.name_label : 'undefined'} : ${
@@ -394,7 +572,7 @@ class BackupReportsXoPlugin {
} else {
if (taskLog.status === 'failure') {
++nFailures
failedVmsText.push(...text, '', '', ...subText, '')
failedVmsText.push(...text, ...subText)
nagiosText.push(
`[${
vm !== undefined ? vm.name_label : 'undefined'
@@ -402,37 +580,34 @@ class BackupReportsXoPlugin {
)
} else if (taskLog.status === 'interrupted') {
++nInterrupted
interruptedVmsText.push(...text, '', '', ...subText, '')
interruptedVmsText.push(...text, ...subText)
nagiosText.push(
`[(Interrupted) ${vm !== undefined ? vm.name_label : 'undefined'}]`
)
} else {
successfulVmsText.push(...text, '', '', ...subText, '')
successfulVmsText.push(...text, ...subText)
}
}
}
const nVms = log.tasks.length
const nSuccesses = nVms - nFailures - nSkipped - nInterrupted
let markdown = [
const markdown = [
`## Global status: ${log.status}`,
'',
`- **Job ID**: ${log.jobId}`,
`- **Run ID**: ${runJobId}`,
`- **Run ID**: ${log.id}`,
`- **mode**: ${mode}`,
...getTemporalDataMarkdown(log.start, log.end),
...getTemporalDataMarkdown(log.end, log.start, formatDate),
`- **Successes**: ${nSuccesses} / ${nVms}`,
globalTransferSize !== 0 &&
`- **Transfer size**: ${formatSize(globalTransferSize)}`,
globalMergeSize !== 0 &&
`- **Merge size**: ${formatSize(globalMergeSize)}`,
...getWarningsMarkdown(log.warnings),
'',
]
if (globalTransferSize !== 0) {
markdown.push(`- **Transfer size**: ${formatSize(globalTransferSize)}`)
}
if (globalMergeSize !== 0) {
markdown.push(`- **Merge size**: ${formatSize(globalMergeSize)}`)
}
addWarnings(markdown, log.warnings)
markdown.push('')
if (nFailures !== 0) {
markdown.push(
'---',
@@ -457,7 +632,7 @@ class BackupReportsXoPlugin {
)
}
if (nSuccesses !== 0 && reportWhen !== 'failure') {
if (nSuccesses !== 0 && (force || reportWhen !== 'failure')) {
markdown.push(
'---',
'',
@@ -468,9 +643,8 @@ class BackupReportsXoPlugin {
}
markdown.push('---', '', `*${pkg.name} v${pkg.version}*`)
markdown = markdown.join('\n')
return this._sendReport({
markdown,
markdown: toMarkdown(markdown),
subject: `[Xen Orchestra] ${log.status} Backup report for ${jobName} ${
STATUS_ICON[log.status]
}`,
@@ -510,9 +684,9 @@ class BackupReportsXoPlugin {
])
}
_listener(status) {
_legacyVmHandler(status) {
const { calls, timezone, error } = status
const formatDate = createDateFormater(timezone)
const formatDate = createDateFormatter(timezone)
if (status.error !== undefined) {
const [globalStatus, icon] =

View File

@@ -33,7 +33,7 @@
},
"dependencies": {
"http-request-plus": "^0.8.0",
"jsonrpc-websocket-client": "^0.4.1"
"jsonrpc-websocket-client": "^0.5.0"
},
"devDependencies": {
"@babel/cli": "^7.0.0",
@@ -49,5 +49,6 @@
"prebuild": "yarn run clean",
"predev": "yarn run prebuild",
"prepublishOnly": "yarn run build"
}
},
"private": true
}

View File

@@ -44,5 +44,6 @@
"build": "NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
"dev": "NODE_DEV=development babel --watch --source-maps --out-dir=dist/ src/",
"prepublishOnly": "yarn run build"
}
},
"private": true
}

View File

@@ -42,5 +42,6 @@
"prebuild": "yarn run clean",
"predev": "yarn run prebuild",
"prepublishOnly": "yarn run build"
}
},
"private": true
}

View File

@@ -50,5 +50,6 @@
"prebuild": "rimraf dist/",
"predev": "yarn run prebuild",
"prepublishOnly": "yarn run build"
}
},
"private": true
}

View File

@@ -49,5 +49,6 @@
"prebuild": "yarn run clean",
"predev": "yarn run prebuild",
"prepublishOnly": "yarn run build"
}
},
"private": true
}

View File

@@ -50,5 +50,6 @@
"prebuild": "yarn run clean",
"predev": "yarn run prebuild",
"prepublishOnly": "yarn run build"
}
},
"private": true
}

View File

@@ -50,5 +50,6 @@
"prebuild": "yarn run clean",
"predev": "yarn run prebuild",
"prepublishOnly": "yarn run build"
}
},
"private": true
}

View File

@@ -59,5 +59,6 @@
"prebuild": "yarn run clean",
"predev": "yarn run prebuild",
"prepublishOnly": "yarn run build"
}
},
"private": true
}

View File

@@ -49,6 +49,12 @@ maxTokenValidity = '0.5 year'
# Delay for which backups listing on a remote is cached
listingDebounce = '1 min'
# Helmet handles HTTP security via headers
#
# https://helmetjs.github.io/docs/
#[http.helmet.hsts]
#includeSubDomains = false
[[http.listen]]
port = 80
@@ -68,6 +74,7 @@ honorCipherOrder = true
secureOptions = 117440512
[http.mounts]
'/' = '../xo-web/dist'
[remoteOptions]
mountsDir = '/run/xo-server/mounts'

View File

@@ -1,6 +1,7 @@
{
"private": true,
"name": "xo-server",
"version": "5.38.2",
"version": "5.41.0",
"license": "AGPL-3.0",
"description": "Server part of Xen-Orchestra",
"keywords": [
@@ -37,11 +38,11 @@
"@xen-orchestra/cron": "^1.0.3",
"@xen-orchestra/defined": "^0.0.0",
"@xen-orchestra/emit-async": "^0.0.0",
"@xen-orchestra/fs": "^0.8.0",
"@xen-orchestra/fs": "^0.9.0",
"@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",
@@ -50,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",
@@ -64,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",
@@ -109,7 +110,7 @@
"readable-stream": "^3.2.0",
"redis": "^2.8.0",
"schema-inspector": "^1.6.8",
"semver": "^5.4.1",
"semver": "^6.0.0",
"serve-static": "^1.13.1",
"split-lines": "^2.0.0",
"stack-chain": "^2.0.0",
@@ -120,7 +121,7 @@
"tmp": "^0.1.0",
"uuid": "^3.0.1",
"value-matcher": "^0.2.0",
"vhd-lib": "^0.6.1",
"vhd-lib": "^0.7.0",
"ws": "^6.0.0",
"xen-api": "^0.25.1",
"xml2js": "^0.4.19",

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

@@ -0,0 +1,5 @@
import fromCallback from 'promise-toolbox/fromCallback'
import { execFile } from 'child_process'
export const read = key =>
fromCallback(cb => execFile('xenstore-read', [key], cb))

View File

@@ -1,6 +1,6 @@
import createLogger from '@xen-orchestra/log'
import pump from 'pump'
import { format } from 'json-rpc-peer'
import { format, JsonRpcError } from 'json-rpc-peer'
import { noSuchObject } from 'xo-common/api-errors'
import { parseSize } from '../utils'
@@ -128,7 +128,7 @@ async function handleImportContent(req, res, { xapi, id }) {
res.end(format.response(0, true))
} catch (e) {
res.writeHead(500)
res.end(format.error(0, new Error(e.message)))
res.end(format.error(0, new JsonRpcError(e.message)))
}
}

View File

@@ -1,4 +1,4 @@
import { format } from 'json-rpc-peer'
import { format, JsonRpcError } from 'json-rpc-peer'
// ===================================================================
@@ -248,7 +248,7 @@ async function handleInstallSupplementalPack(req, res, { hostId }) {
res.end(format.response(0))
} catch (e) {
res.writeHead(500)
res.end(format.error(0, new Error(e.message)))
res.end(format.error(0, new JsonRpcError(e.message)))
}
}

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

@@ -1,4 +1,4 @@
import { format } from 'json-rpc-peer'
import { format, JsonRPcError } from 'json-rpc-peer'
// ===================================================================
@@ -234,7 +234,7 @@ async function handleInstallSupplementalPack(req, res, { poolId }) {
res.end(format.response(0))
} catch (e) {
res.writeHead(500)
res.end(format.error(0, new Error(e.message)))
res.end(format.error(0, new JsonRPcError(e.message)))
}
}

View File

@@ -35,7 +35,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 = {

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

@@ -1,5 +1,5 @@
import defer from 'golike-defer'
import { format } from 'json-rpc-peer'
import { format, JsonRpcError } from 'json-rpc-peer'
import { ignoreErrors } from 'promise-toolbox'
import { assignWith, concat } from 'lodash'
import {
@@ -193,6 +193,11 @@ create.params = {
optional: true,
},
networkConfig: {
type: 'string',
optional: true,
},
coreOs: {
type: 'boolean',
optional: true,
@@ -625,13 +630,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 = {
@@ -732,7 +731,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 = {
@@ -1084,7 +1083,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 = {
@@ -1098,7 +1097,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 = {
@@ -1198,7 +1197,7 @@ async function handleVmImport(req, res, { data, srId, type, xapi }) {
res.end(format.response(0, vm.$id))
} catch (e) {
res.writeHead(500)
res.end(format.error(0, new Error(e.message)))
res.end(format.error(0, new JsonRpcError(e.message)))
}
}
@@ -1413,15 +1412,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).set_HVM_boot_params({ order })
}
setBootOrder.params = {

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,7 +884,7 @@ 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, {
@@ -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
@@ -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

@@ -93,7 +93,7 @@ async function loadConfiguration() {
function createExpressApp(config) {
const app = createExpress()
app.use(helmet())
app.use(helmet(config.http.helmet))
app.use(compression())
@@ -417,6 +417,7 @@ const setUpProxies = (express, opts, xo) => {
}
const proxy = createProxyServer({
changeOrigin: true,
ignorePath: true,
}).on('error', error => console.error(error))

View File

@@ -2,6 +2,8 @@ import Collection from '../collection/redis'
import Model from '../model'
import { forEach } from '../utils'
import { parseProp } from './utils'
// ===================================================================
export default class Remote extends Model {}
@@ -14,12 +16,21 @@ export class Remotes extends Collection {
async get(properties) {
const remotes = await super.get(properties)
forEach(remotes, remote => {
remote.benchmarks =
remote.benchmarks !== undefined
? JSON.parse(remote.benchmarks)
: undefined
remote.benchmarks = parseProp('remote', remote, 'benchmarks')
remote.enabled = remote.enabled === 'true'
})
return remotes
}
_update(remotes) {
return super._update(
remotes.map(remote => {
const { benchmarks } = remote
if (benchmarks !== undefined) {
remote.benchmarks = JSON.stringify(benchmarks)
}
return remote
})
)
}
}

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'),
}
@@ -633,7 +643,7 @@ const TRANSFORMS = {
description: poolPatch.name_description,
name: poolPatch.name_label,
pool_patch: poolPatch.$ref,
size: poolPatch.size,
size: +poolPatch.size,
guidance: poolPatch.after_apply_guidance,
time: toTimestamp(obj.timestamp_applied),

View File

@@ -1,3 +1,4 @@
/* eslint eslint-comments/disable-enable-pair: [error, {allowWholeFile: true}] */
/* eslint-disable camelcase */
import asyncMap from '@xen-orchestra/async-map'
import concurrency from 'limit-concurrency-decorator'
@@ -28,6 +29,7 @@ import {
groupBy,
includes,
isEmpty,
noop,
omit,
startsWith,
uniq,
@@ -227,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
@@ -253,57 +247,33 @@ 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,
})
const host = this.getObject(id)
await Promise.all([
nameDescription !== undefined &&
host.set_name_description(nameDescription),
nameLabel !== undefined && host.set_name_label(nameLabel),
])
}
async setPoolProperties({ autoPoweron, nameLabel, nameDescription }) {
const { pool } = this
await Promise.all([
this._setObjectProperties(pool, {
nameLabel,
nameDescription,
}),
nameDescription !== undefined &&
pool.set_name_description(nameDescription),
nameLabel !== undefined && pool.set_name_label(nameLabel),
autoPoweron != null &&
this._updateObjectMapProperty(pool, 'other_config', {
autoPoweron: autoPoweron ? 'true' : null,
}),
pool.update_other_config('autoPoweron', autoPoweron ? 'true' : null),
])
}
async setSrProperties(id, { nameLabel, nameDescription }) {
await this._setObjectProperties(this.getObject(id), {
nameLabel,
nameDescription,
})
const sr = this.getObject(id)
await Promise.all([
nameDescription !== undefined && sr.set_name_description(nameDescription),
nameLabel !== undefined && sr.set_name_label(nameLabel),
])
}
async setNetworkProperties(
@@ -316,15 +286,13 @@ export default class Xapi extends XapiBase {
}
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,
}),
defaultLockingMode !== undefined &&
network.set_default_locking_mode(defaultLockingMode),
nameDescription !== undefined &&
network.set_name_description(nameDescription),
nameLabel !== undefined && network.set_name_label(nameLabel),
automatic !== undefined &&
network.update_other_config('automatic', automatic ? 'true' : null),
])
}
@@ -344,10 +312,8 @@ export default class Xapi extends XapiBase {
// =================================================================
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)
}
// =================================================================
@@ -376,12 +342,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)
}
// =================================================================
@@ -394,7 +360,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)
@@ -409,7 +375,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) {
@@ -444,9 +410,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',
@@ -459,23 +423,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)
@@ -485,7 +449,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)
}
// =================================================================
@@ -499,7 +463,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.
@@ -570,12 +534,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)
@@ -715,17 +674,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
@@ -733,7 +688,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 =>
@@ -1070,15 +1025,9 @@ export default class Xapi extends XapiBase {
$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,
}),
vm.set_name_label(`[Importing…] ${name_label}`),
vm.update_blocked_operations('start', 'Importing…'),
vm.update_other_config(TAG_COPY_SRC, delta.vm.uuid),
])
// 2. Delete all VBDs which may have been created by the import.
@@ -1102,9 +1051,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,
@@ -1199,15 +1146,14 @@ export default class Xapi extends XapiBase {
}
await Promise.all([
this._setObjectProperties(vm, {
name_label,
}),
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 }
@@ -1261,7 +1207,7 @@ export default class Xapi extends XapiBase {
)
const loop = () =>
this.call(
this.callAsync(
'VM.migrate_send',
vm.$ref,
token,
@@ -1275,7 +1221,7 @@ export default class Xapi extends XapiBase {
pDelay(1e4).then(loop)
)
return loop()
return loop().then(noop)
}
@synchronized()
@@ -1435,9 +1381,7 @@ export default class Xapi extends XapiBase {
'start',
'OVA import in progress...'
),
this._setObjectProperties(vm, {
name_label: `[Importing...] ${nameLabel}`,
}),
vm.set_name_label(`[Importing...] ${nameLabel}`),
])
// 2. Create VDIs & Vifs.
@@ -1454,7 +1398,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,7 +1442,7 @@ 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.set_name_label(nameLabel),
])
return vm
}
@@ -1545,7 +1489,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,18 +1575,17 @@ export default class Xapi extends XapiBase {
}
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 })
await this.getObject(vmId).update_VCPUs_params(
'weight',
weight || null // Take all falsy values as a removal (0 included)
)
}
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
@@ -1652,7 +1595,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) {
@@ -1681,16 +1624,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*).
@@ -1712,20 +1651,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',
@@ -1736,12 +1669,10 @@ 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()
})
}
}
@@ -1825,14 +1756,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({
@@ -1855,7 +1786,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,
@@ -1867,7 +1798,7 @@ export default class Xapi extends XapiBase {
type,
virtual_size: size !== undefined ? parseSize(size) : virtual_size,
xenstore_data,
})
}).then(extractOpaqueRef)
)
}
@@ -1885,9 +1816,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 (
@@ -1898,7 +1832,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)
@@ -1916,7 +1852,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
@@ -1929,7 +1865,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) {
@@ -1943,7 +1879,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)
}
}
@@ -1951,20 +1887,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({
@@ -1977,7 +1913,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) {
@@ -1986,7 +1922,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)
}
}
@@ -2037,11 +1973,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
@@ -2165,7 +2101,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
@@ -2193,7 +2129,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',
@@ -2232,7 +2168,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)
)
)
@@ -2247,7 +2183,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()
)
)
}
@@ -2278,7 +2214,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)
)
)
@@ -2287,7 +2223,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,11 +68,6 @@ 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>

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) {

View File

@@ -52,6 +52,7 @@ export default {
coreOs = false,
cloudConfig = undefined,
networkConfig = undefined,
vgpuType = undefined,
gpuGroup = undefined,
@@ -93,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)
@@ -114,9 +115,7 @@ export default {
order = 'ncd'
}
this._setObjectProperties(vm, {
HVM_boot_params: { ...bootParams, order },
})
vm.set_HVM_boot_params({ ...bootParams, order })
}
} else {
// PV
@@ -124,13 +123,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')
}
}
}
@@ -241,10 +239,16 @@ export default {
}
})
const method = coreOs
? 'createCoreOsCloudInitConfigDrive'
: 'createCloudInitConfigDrive'
await this[method](vm.$id, srRef, cloudConfig)
if (coreOs) {
await this.createCoreOsCloudInitConfigDrive(vm.$id, srRef, cloudConfig)
} else {
await this.createCloudInitConfigDrive(
vm.$id,
srRef,
cloudConfig,
networkConfig
)
}
}
// wait for the record with all the VBDs and VIFs
@@ -257,21 +261,14 @@ 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,
}),
vm.update_other_config('autoPoweron', value ? 'true' : null),
value &&
this.setPoolProperties({
autoPoweron: true,
@@ -285,23 +282,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', coresPerSocket),
},
CPUs: 'cpus',
@@ -319,26 +312,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', 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',
@@ -352,15 +341,11 @@ 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),
},
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: {
@@ -432,19 +417,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: {
@@ -454,7 +432,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)
},
},
@@ -465,15 +443,13 @@ 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', 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),
},
}),
@@ -486,7 +462,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') {
@@ -499,15 +475,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

@@ -449,9 +449,7 @@ const disableVmHighAvailability = async (xapi: Xapi, vm: Vm) => {
}
return Promise.all([
xapi._setObjectProperties(vm, {
haRestartPriority: '',
}),
vm.set_ha_restart_priority(''),
xapi.addTag(vm.$ref, 'HA disabled'),
])
}
@@ -937,7 +935,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 +1049,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,
@@ -1258,11 +1256,11 @@ 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)})`
)
)
)
)
@@ -1270,13 +1268,11 @@ export default class BackupNg {
await Promise.all([
xapi.addTag(vm.$ref, '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) {
@@ -1630,13 +1626,11 @@ export default class BackupNg {
await Promise.all([
xapi.addTag(vm.$ref, '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) {
@@ -1667,9 +1661,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')
)
}

View File

@@ -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,10 +972,10 @@ 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.',
})
data.vm.update_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')

View File

@@ -164,11 +164,11 @@ export default class Jobs {
xo.emit(
'job:terminated',
undefined,
job,
undefined,
// This cast can be removed after merging the PR: https://github.com/vatesfr/xen-orchestra/pull/3209
String(job.runId)
String(job.runId),
{
type: job.type,
}
)
return this.updateJob({ id: job.id, runId: null })
})
@@ -319,7 +319,10 @@ export default class Jobs {
true
)
app.emit('job:terminated', status, job, schedule, runJobId)
app.emit('job:terminated', runJobId, {
type: job.type,
status,
})
} catch (error) {
await logger.error(
`The execution of ${id} has failed.`,
@@ -330,7 +333,9 @@ export default class Jobs {
},
true
)
app.emit('job:terminated', undefined, job, schedule, runJobId)
app.emit('job:terminated', runJobId, {
type: job.type,
})
throw error
} finally {
this.updateJob({ id, runId: null })::ignoreErrors()

View File

@@ -168,7 +168,7 @@ export default class {
}
@synchronized()
async _updateRemote(id, { benchmarks, url, ...props }) {
async _updateRemote(id, { url, ...props }) {
const remote = await this._getRemote(id)
// url is handled separately to take care of obfuscated values
@@ -176,13 +176,6 @@ export default class {
remote.url = format(sensitiveValues.merge(parse(url), parse(remote.url)))
}
if (
benchmarks !== undefined ||
(benchmarks = remote.benchmarks) !== undefined
) {
remote.benchmarks = JSON.stringify(benchmarks)
}
patch(remote, props)
return (await this._remotes.update(remote)).properties

View File

@@ -4,6 +4,7 @@ import { ignoreErrors } from 'promise-toolbox'
import { hash, needsRehash, verify } from 'hashy'
import { invalidCredentials, noSuchObject } from 'xo-common/api-errors'
import * as XenStore from '../_XenStore'
import { Groups } from '../models/group'
import { Users } from '../models/user'
import { forEach, isEmpty, lightSet, mapToArray } from '../utils'
@@ -68,8 +69,12 @@ export default class {
)
if (!(await usersDb.exists())) {
const email = 'admin@admin.net'
const password = 'admin'
const {
email = 'admin@admin.net',
password = 'admin',
} = await XenStore.read('vm-data/admin-account')
.then(JSON.parse)
.catch(() => ({}))
await this.createUser({ email, password, permission: 'admin' })
log.info(`Default user created: ${email} with password ${password}`)

View File

@@ -4,6 +4,7 @@ import { fibonacci } from 'iterable-backoff'
import { noSuchObject } from 'xo-common/api-errors'
import { pDelay, ignoreErrors } from 'promise-toolbox'
import * as XenStore from '../_XenStore'
import Xapi from '../xapi'
import xapiObjectToXo from '../xapi-object-to-xo'
import XapiStats from '../xapi-stats'
@@ -64,8 +65,19 @@ export default class {
servers => serversDb.update(servers)
)
// Connects to existing servers.
const servers = await serversDb.get()
// Add servers in XenStore
if (servers.length === 0) {
const xenStoreServers = await XenStore.read('vm-data/xen-servers')
.then(JSON.parse)
.catch(() => [])
for (const server of xenStoreServers) {
servers.push(await this.registerXenServer(server))
}
}
// Connects to existing servers.
for (const server of servers) {
if (server.enabled) {
this.connectXenServer(server.id).catch(error => {
@@ -374,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))
@@ -445,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

@@ -25,11 +25,11 @@
},
"dependencies": {
"child-process-promise": "^2.0.3",
"core-js": "3.0.0",
"core-js": "^3.0.0",
"pipette": "^0.9.3",
"promise-toolbox": "^0.12.1",
"tmp": "^0.1.0",
"vhd-lib": "^0.6.1"
"vhd-lib": "^0.7.0"
},
"devDependencies": {
"@babel/cli": "^7.0.0",
@@ -39,7 +39,7 @@
"cross-env": "^5.1.3",
"event-to-promise": "^0.8.0",
"execa": "^1.0.0",
"fs-extra": "^7.0.0",
"fs-extra": "^8.0.1",
"get-stream": "^4.0.0",
"index-modules": "^0.3.0",
"rimraf": "^2.6.2"
@@ -50,6 +50,7 @@
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
"prebuild": "yarn run clean",
"predev": "yarn run clean",
"prepare": "yarn run build"
"prepare": "yarn run build",
"postversion": "npm publish"
}
}

View File

@@ -1,7 +1,7 @@
{
"private": false,
"private": true,
"name": "xo-web",
"version": "5.38.0",
"version": "5.41.0",
"license": "AGPL-3.0",
"description": "Web interface client for Xen-Orchestra",
"keywords": [
@@ -58,7 +58,7 @@
"chartist-plugin-legend": "^0.6.1",
"chartist-plugin-tooltip": "0.0.11",
"classnames": "^2.2.3",
"complex-matcher": "^0.5.0",
"complex-matcher": "^0.6.0",
"cookies-js": "^1.2.2",
"copy-to-clipboard": "^3.0.8",
"d3": "^5.0.0",
@@ -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",
@@ -100,7 +100,7 @@
"prop-types": "^15.6.0",
"qrcode": "^1.3.2",
"random-password": "^0.1.2",
"reaclette": "^0.7.0",
"reaclette": "^0.8.0",
"react": "^15.4.1",
"react-addons-shallow-compare": "^15.6.2",
"react-addons-test-utils": "^15.6.2",

View File

@@ -21,6 +21,37 @@ const showAvailableTemplateVars = () =>
</ul>
)
const showNetworkConfigInfo = () =>
alert(
_('newVmNetworkConfigLabel'),
<div>
<p>
{_('newVmNetworkConfigInfo', {
noCloudDatasourceLink: (
<a
href='https://cloudinit.readthedocs.io/en/latest/topics/datasources/nocloud.html#datasource-nocloud'
target='_blank'
>
{_('newVmNoCloudDatasource')}
</a>
),
})}
</p>
<p>
{_('newVmNetworkConfigDocLink', {
networkConfigDocLink: (
<a
href='https://cloudinit.readthedocs.io/en/latest/topics/network-config-format-v1.html'
target='_blank'
>
{_('newVmNetworkConfigDoc')}
</a>
),
})}
</p>
</div>
)
export const AvailableTemplateVars = () => (
<Tooltip content={_('availableTemplateVarsInfo')}>
<a
@@ -33,5 +64,28 @@ export const AvailableTemplateVars = () => (
</Tooltip>
)
export const NetworkConfigInfo = () => (
<Tooltip content={_('newVmNetworkConfigTooltip')}>
<a
className='text-info'
style={{ cursor: 'pointer' }}
onClick={showNetworkConfigInfo}
>
<Icon icon='info' />
</a>
</Tooltip>
)
export const DEFAULT_CLOUD_CONFIG_TEMPLATE =
'#cloud-config\n#hostname: {name}%\n#ssh_authorized_keys:\n# - ssh-rsa <myKey>\n#packages:\n# - htop\n'
// SOURCE: https://cloudinit.readthedocs.io/en/latest/topics/network-config-format-v1.html
export const DEFAULT_NETWORK_CONFIG_TEMPLATE = `#network:
# version: 1
# config:
# - type: physical
# name: eth0
# subnets:
# - type: dhcp`
export const CAN_CLOUD_INIT = +process.env.XOA_PLAN > 3

View File

@@ -150,6 +150,17 @@ class Editable extends Component {
render() {
const { state, props } = this
const { error, saving } = state
const ErrorTooltip = props =>
props.error != null && (
<span>
{' '}
<Tooltip content={error}>
<Icon icon='error' />
</Tooltip>
</span>
)
if (!state.editing) {
const { onUndo, previous } = state
@@ -184,12 +195,11 @@ class Editable extends Component {
) : (
success
))}
<ErrorTooltip error={error} />
</span>
)
}
const { error, saving } = state
return (
<span>
{this._renderEdition()}
@@ -199,14 +209,7 @@ class Editable extends Component {
<Icon icon='loading' />
</span>
)}
{error != null && (
<span>
{' '}
<Tooltip content={error}>
<Icon icon='error' />
</Tooltip>
</span>
)}
<ErrorTooltip error={error} />
</span>
)
}
@@ -340,7 +343,9 @@ class SimpleSelect_ extends Editable {
return this.state.value === undefined ? this.props.value : this.state.value
}
_onChange = value => this.setState({ value }, this._save)
_onChange = value => {
this.setState({ value }, this._save)
}
_renderDisplay() {
const { children, optionRenderer, value } = this.props
@@ -450,7 +455,9 @@ export class XoSelect extends Editable {
)
}
_onChange = object => this.setState({ value: object }, object && this._save)
_onChange = object => {
this.setState({ value: object }, object && this._save)
}
_renderEdition() {
const { saving, xoType, ...props } = this.props

View File

@@ -3411,7 +3411,7 @@ export default {
// Original text: "This version is not bundled with any support nor updates. Use it with caution for critical tasks."
disclaimerText3:
'Esta versión no está creada para recibir soporte ni actualizaciones. Úsala con precaución para tareas críticas.',
'Esta versión no está creada para recibir soporte ni actualizaciones. Úsala con precaución.',
// Original text: "Connect PIF"
connectPif: 'Conectar PIF',

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 !',
@@ -3497,7 +3497,7 @@ export default {
// Original text: "This version is not bundled with any support nor updates. Use it with caution for critical tasks."
disclaimerText3:
"Cette version n'est fournie avec aucun support ni aucune mise à jour. Soyez prudent en cas d'utilisation pour des tâches importantes.",
"Cette version n'est fournie avec aucun support ni aucune mise à jour. Utilisez-la avec précaution.",
// Original text: "Connect PIF"
connectPif: 'Connecter la PIF',

View File

@@ -3248,7 +3248,7 @@ export default {
// Original text: "This version is not bundled with any support nor updates. Use it with caution for critical tasks."
disclaimerText3:
'This Verzió is not bundled with any support nor upDates. Use it with caution for critical tasks.',
'This Verzió is not bundled with any support nor upDates. Use it with caution.',
// Original text: "Connect PIF"
connectPif: 'Csatlakozás PIF',

View File

@@ -2958,7 +2958,7 @@ export default {
// Original text: "This version is not bundled with any support nor updates. Use it with caution for critical tasks."
disclaimerText3:
'This version is not bundled with any support nor updates. Use it with caution for critical tasks.',
'This version is not bundled with any support nor updates. Use it with caution.',
// Original text: "Connect PIF"
connectPif: 'Connect PIF',

View File

@@ -2944,7 +2944,7 @@ export default {
// Original text: "This version is not bundled with any support nor updates. Use it with caution for critical tasks."
disclaimerText3:
'Esta versão não está vinculada a qualquer tipo de suporte nem atualizações. Use-a com cuidado em se tratando de tarefas críticas.',
'Esta versão não está vinculada a qualquer tipo de suporte nem atualizações. Use-a com cuidado.',
// Original text: "Connect PIF"
connectPif: 'Conectar PIF',

View File

@@ -4294,7 +4294,7 @@ export default {
// Original text: "This version is not bundled with any support nor updates. Use it with caution for critical tasks."
disclaimerText3:
'Bu sürüm herhangi bir destek veya güncellemeyle birlikte verilmez. Kritik görevler için dikkatli kullanın.',
'Bu sürüm herhangi bir destek veya güncellemeyle birlikte verilmez. Dikkatli kullanın.',
// Original text: "Connect PIF"
connectPif: "PIF'e bağlan",

View File

@@ -2236,7 +2236,7 @@ export default {
disclaimerText2: '如果你是一个公司,建议使用我们的设备结合专业的支持',
// Original text: "This version is not bundled with any support nor updates. Use it with caution for critical tasks."
disclaimerText3: '这个版本没有绑定任何支持或更新,在紧急任务下,请谨慎使用',
disclaimerText3: '这个版本没有绑定任何支持或更新,请谨慎使用',
// Original text: "Connect PIF"
connectPif: '连接物理网卡',

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',
@@ -588,34 +575,34 @@ const messages = {
newSrNfsOptions: 'Comma delimited NFS options',
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 +616,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 +632,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 +646,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 +674,6 @@ const messages = {
copyVmLabel: 'Copy',
cloneVmLabel: 'Clone',
fastCloneVmLabel: 'Fast clone',
convertVmToTemplateLabel: 'Convert to template',
vmConsoleLabel: 'Console',
// ----- SR advanced tab -----
@@ -727,19 +709,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 +731,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 +747,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 +755,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 +774,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.',
@@ -821,9 +799,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 +832,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 +841,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 +852,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 +869,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 +890,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 +898,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 +917,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 +954,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 +971,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 +988,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',
@@ -1051,16 +1012,15 @@ const messages = {
importVdi: 'Import VDI content',
importVdiNoFile: 'No file selected',
selectVdiMessage: 'Drop VHD file here',
srsNotOnSameHost:
'The SRs must either be shared or on the same host for the VM to be able to start.',
useQuotaWarning:
'Creating this disk will use the disk space quota from the resource set {resourceSet} ({spaceLeft} left)',
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.",
// ----- VM network tab -----
vifCreateDeviceButton: 'New device',
vifNoInterface: 'No interface',
vifDeviceLabel: 'Device',
vifMacLabel: 'MAC address',
vifMtuLabel: 'MTU',
@@ -1081,13 +1041,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 +1057,6 @@ const messages = {
snapshotDate: 'Creation date',
snapshotName: 'Name',
snapshotDescription: 'Description',
snapshotAction: 'Action',
snapshotQuiesce: 'Quiesced snapshot',
vmRevertSuccessfulTitle: 'Revert successful',
vmRevertSuccessfulMessage: 'VM successfully reverted',
@@ -1159,10 +1117,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',
@@ -1179,8 +1135,6 @@ const messages = {
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 +1212,6 @@ const messages = {
statsDashboardSelectObjects: 'Select',
metricsLoading: 'Loading…',
// ----- Visualizations -----
comingSoon: 'Coming soon!',
// ----- Health -----
orphanedVdis: 'Orphaned snapshot VDIs',
orphanedVms: 'Orphaned VMs snapshot',
@@ -1281,10 +1232,12 @@ const messages = {
alarmContent: 'Content',
alarmObject: 'Issue on',
alarmPool: 'Pool',
alarmRemoveAll: 'Remove all alarms',
spaceLeftTooltip: '{used}% used ({free} left)',
// ----- New VM -----
createVmModalTitle: 'Create VM',
createVmModalWarningMessage:
"You're about to use a large amount of resources available on the resource set. Are you sure you want to continue?",
newVmCreateNewVmOn: 'Create a new VM on {select}',
newVmCreateNewVmNoPermission: 'You have no permission to create a VM',
newVmInfoPanel: 'Infos',
@@ -1318,6 +1271,7 @@ const messages = {
newVmSshKey: 'SSH key',
noConfigDrive: 'No config drive',
newVmCustomConfig: 'Custom config',
premiumOnly: 'Only available in Premium',
availableTemplateVarsInfo:
'Click here to see the available template variables',
availableTemplateVarsTitle: 'Available template variables',
@@ -1335,7 +1289,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:',
@@ -1347,6 +1300,15 @@ const messages = {
newVmHideAdvanced: 'Hide advanced settings',
newVmShare: 'Share this VM',
newVmSrsNotOnSameHost: 'The SRs must either be on the same host or shared',
newVmNetworkConfigLabel: 'Network config',
newVmNetworkConfigInfo:
'Network configuration is only compatible with the {noCloudDatasourceLink}.',
newVmNetworkConfigDocLink: 'See {networkConfigDocLink}.',
newVmNetworkConfigTooltip:
'Click here to get more information about network config',
newVmUserConfigLabel: 'User config',
newVmNoCloudDatasource: 'NoCloud datasource',
newVmNetworkConfigDoc: 'Network config documentation',
// ----- Self -----
resourceSets: 'Resource sets',
@@ -1366,9 +1328,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',
@@ -1380,8 +1339,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})',
@@ -1444,7 +1401,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:
@@ -1453,7 +1409,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',
@@ -1464,11 +1419,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',
@@ -1502,7 +1452,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}}?',
@@ -1517,9 +1466,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}):',
@@ -1658,12 +1605,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 ?',
@@ -1694,7 +1639,6 @@ const messages = {
serverHost: 'Host',
serverUsername: 'Username',
serverPassword: 'Password',
serverAction: 'Action',
serverReadOnly: 'Read Only',
serverUnauthorizedCertificates: 'Unauthorized Certificates',
serverAllowUnauthorizedCertificates: 'Allow Unauthorized Certificates',
@@ -1710,7 +1654,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',
@@ -1722,7 +1665,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',
@@ -1773,12 +1715,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',
@@ -1804,7 +1745,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',
@@ -1848,6 +1788,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',
@@ -1856,7 +1801,7 @@ const messages = {
disclaimerText2:
"If you are a company, it's better to use it with our appliance + pro support included:",
disclaimerText3:
'This version is not bundled with any support nor updates. Use it with caution for critical tasks.',
'This version is not bundled with any support nor updates. Use it with caution.',
notRegisteredDisclaimerInfo:
'You are not registered. Your XOA may not be up to date.',
notRegisteredDisclaimerCreateAccount: 'Click here to create an account.',
@@ -1898,7 +1843,6 @@ const messages = {
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.',
@@ -1921,22 +1865,15 @@ const messages = {
others: 'Others',
// ----- Logs -----
loadingLogs: 'Loading logs…',
logUser: 'User',
logMethod: 'Method',
logParams: 'Params',
logMessage: 'Message',
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',
@@ -1960,12 +1897,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',
@@ -2014,7 +1948,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',
@@ -2026,8 +1960,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.',
@@ -2045,8 +1977,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}.',
@@ -2054,7 +1984,6 @@ const messages = {
xosanHost: 'Host',
xosanHosts: 'Connected Hosts',
xosanPool: 'Pool',
xosanVolumeId: 'Volume ID',
xosanSize: 'Size',
xosanUsedSpace: 'Used space',
xosanLicense: 'License',
@@ -2065,8 +1994,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',
@@ -2074,8 +2001,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',
@@ -2091,7 +2016,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!",
@@ -2109,9 +2034,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
@@ -2156,7 +2078,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

@@ -409,8 +409,7 @@ const xoItemToRender = {
<span>
<strong>
<Icon icon='resource-set' /> {resourceSet.name}
</strong>{' '}
({resourceSet.id})
</strong>
</span>
),
sshKey: key => (

View File

@@ -275,13 +275,13 @@ const _getPermissionsPredicate = invoke(() => {
}
)
return state => {
return (state, props, useResourceSet) => {
const user = getUser(state)
if (!user) {
return false
}
if (user.permission === 'admin') {
if (user.permission === 'admin' || useResourceSet) {
return // No predicate means no filtering.
}

View File

@@ -37,7 +37,9 @@ export class TooltipViewer extends Component {
return (
<div
className={classNames(
show ? styles.tooltipEnabled : styles.tooltipDisabled,
show && content !== undefined
? styles.tooltipEnabled
: styles.tooltipDisabled,
className
)}
style={{

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

@@ -177,8 +177,8 @@ export default class MigrateVmModalBody extends BaseComponent {
host,
intraPool,
mapVifsNetworks: undefined,
migrationNetwork: undefined,
targetSrs: { mainSr: pools[host.$pool].default_SR },
migrationNetworkId: undefined,
targetSrs: {},
})
return
}
@@ -210,7 +210,7 @@ export default class MigrateVmModalBody extends BaseComponent {
intraPool,
mapVifsNetworks: defaultNetworksForVif,
migrationNetworkId: defaultMigrationNetworkId,
targetSrs: {},
targetSrs: { mainSr: pools[host.$pool].default_SR },
})
}

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

@@ -88,7 +88,7 @@ export default class About extends Component {
<p className='text-muted'>{_('bugTrackerText')}</p>
</Col>
<Col mediumSize={6}>
<a href='https://xen-orchestra.com/forum/'>
<a href='https://xcp-ng.org/forum/category/12/xen-orchestra'>
<Icon icon='group' size={4} />
<h4>{_('community')}</h4>
</a>

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