Compare commits

..

1 Commits

Author SHA1 Message Date
Julien Fontanet
00c5641ca3 feat(xen-api): rewrite from scratch
- never stop event watching no matter what's happening
- never disconnect unless explicit requested
- better handling of critical sections
2019-03-29 16:44:16 +01:00
165 changed files with 4583 additions and 5040 deletions

View File

@@ -1,7 +1,5 @@
module.exports = {
extends: [
'plugin:eslint-comments/recommended',
'standard',
'standard-jsx',
'prettier',
@@ -21,7 +19,7 @@ module.exports = {
overrides: [
{
files: ['cli.js', '*-cli.js', 'packages/*cli*/**/*.js'],
files: ['packages/*cli*/**/*.js', '*-cli.js'],
rules: {
'no-console': 'off',
},

View File

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

View File

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

View File

@@ -55,7 +55,6 @@
"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",
"postversion": "npm publish"
"prepublishOnly": "yarn run build"
}
}

View File

@@ -43,7 +43,6 @@
"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",
"postversion": "npm publish"
"prepublishOnly": "yarn run build"
}
}

View File

@@ -42,7 +42,6 @@
"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",
"postversion": "npm publish"
"prepublishOnly": "yarn run build"
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@xen-orchestra/fs",
"version": "0.9.0",
"version": "0.8.0",
"license": "AGPL-3.0",
"description": "The File System for Xen Orchestra backups.",
"keywords": [],
@@ -21,18 +21,18 @@
"node": ">=6"
},
"dependencies": {
"@marsaud/smb2": "^0.14.0",
"@marsaud/smb2": "^0.13.0",
"@sindresorhus/df": "^2.1.0",
"@xen-orchestra/async-map": "^0.0.0",
"decorator-synchronized": "^0.5.0",
"execa": "^1.0.0",
"fs-extra": "^8.0.1",
"fs-extra": "^7.0.0",
"get-stream": "^4.0.0",
"lodash": "^4.17.4",
"promise-toolbox": "^0.12.1",
"readable-stream": "^3.0.6",
"through2": "^3.0.0",
"tmp": "^0.1.0",
"tmp": "^0.0.33",
"xo-remote-parser": "^0.5.0"
},
"devDependencies": {
@@ -45,7 +45,7 @@
"async-iterator-to-stream": "^1.1.0",
"babel-plugin-lodash": "^3.3.2",
"cross-env": "^5.1.3",
"dotenv": "^8.0.0",
"dotenv": "^7.0.0",
"index-modules": "^0.3.0",
"rimraf": "^2.6.2"
},
@@ -55,7 +55,6 @@
"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",
"postversion": "npm publish"
"prepare": "yarn run build"
}
}

View File

@@ -400,10 +400,6 @@ 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)
@@ -414,18 +410,6 @@ 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,
@@ -562,28 +546,6 @@ 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,70 +310,5 @@ 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,18 +106,10 @@ export default class LocalHandler extends RemoteHandlerAbstract {
await fs.access(path, fs.R_OK | fs.W_OK)
}
_truncate(file, len) {
return fs.truncate(this._getFilePath(file), len)
}
async _unlink(file) {
return fs.unlink(this._getFilePath(file))
}
_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,20 +155,10 @@ 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

@@ -27,7 +27,7 @@
">2%"
],
"engines": {
"node": ">=6"
"node": ">=4"
},
"dependencies": {
"lodash": "^4.17.4",
@@ -48,7 +48,6 @@
"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",
"postversion": "npm publish"
"prepublishOnly": "yarn run build"
}
}

View File

@@ -45,7 +45,6 @@
"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",
"postversion": "npm publish"
"prepublishOnly": "yarn run build"
}
}

View File

@@ -1,79 +1,5 @@
# 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)
![Channel: latest](https://badgen.net/badge/channel/latest/yellow)
### 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)
![Channel: stable](https://badgen.net/badge/channel/stable/green)
### Bug fix
- Fix major memory leak [2563be4](https://github.com/vatesfr/xen-orchestra/commit/2563be472bfd84c6ed867efd21c4aeeb824d387f)
### Released packages
- xen-api v0.25.1
- xo-server v5.38.2
## **5.33.0** (2019-03-29)
### Enhancements
@@ -102,6 +28,15 @@
- never disconnect by itself even in case of errors
- never stop watching events
### Released packages
- xen-api v0.25.0
- vhd-lib v0.6.0
- @xen-orchestra/fs v0.8.0
- xo-server-usage-report v0.7.2
- xo-server v5.38.1
- xo-web v5.38.0
### Bug fixes
- [New network] PIF was wrongly required which prevented from creating a private network (PR [#4010](https://github.com/vatesfr/xen-orchestra/pull/4010))
@@ -121,15 +56,6 @@
- Safely install a subset of patches on a pool [#3777](https://github.com/vatesfr/xen-orchestra/issues/3777)
- XCP-ng: no longer requires to run `yum install xcp-ng-updater` when it's already installed [#3934](https://github.com/vatesfr/xen-orchestra/issues/3934)
### Released packages
- xen-api v0.25.0
- vhd-lib v0.6.0
- @xen-orchestra/fs v0.8.0
- xo-server-usage-report v0.7.2
- xo-server v5.38.1
- xo-web v5.38.0
## **5.32.2** (2019-02-28)
### Bug fixes

View File

@@ -2,25 +2,9 @@
### Enhancements
- [VM/general] Display 'Started... ago' instead of 'Halted... ago' for paused state [#3750](https://github.com/vatesfr/xen-orchestra/issues/3750) (PR [#4170](https://github.com/vatesfr/xen-orchestra/pull/4170))
- [Metadata backup] Ability to define when the backup report will be sent (PR [#4149](https://github.com/vatesfr/xen-orchestra/pull/4149))
- [XOA/Update] Ability to select release channel [#4200](https://github.com/vatesfr/xen-orchestra/issues/4200) (PR [#4202](https://github.com/vatesfr/xen-orchestra/pull/4202))
- [User] Forget connection tokens on password change or on demand [#4214](https://github.com/vatesfr/xen-orchestra/issues/4214) (PR [#4224](https://github.com/vatesfr/xen-orchestra/pull/4224))
- [Settings/Logs] LICENCE_RESTRICTION errors: suggest XCP-ng as an Open Source alternative [#3876](https://github.com/vatesfr/xen-orchestra/issues/3876) (PR [#4238](https://github.com/vatesfr/xen-orchestra/pull/4238))
- [VM/Migrate] Display VDI size on migrate modal [#2534](https://github.com/vatesfr/xen-orchestra/issues/2534) (PR [#4250](https://github.com/vatesfr/xen-orchestra/pull/4250))
- [Host] Display hyperthreading status on advanced tab [#4262](https://github.com/vatesfr/xen-orchestra/issues/4262) (PR [#4263](https://github.com/vatesfr/xen-orchestra/pull/4263))
### Bug fixes
- [Charts] Fixed the chart lines sometimes changing order/color (PR [#4221](https://github.com/vatesfr/xen-orchestra/pull/4221))
- Prevent non-admin users to access admin pages with URL (PR [#4220](https://github.com/vatesfr/xen-orchestra/pull/4220))
- [Upgrade] Fix alert before upgrade while running backup jobs [#4164](https://github.com/vatesfr/xen-orchestra/issues/4164) (PR [#4235](https://github.com/vatesfr/xen-orchestra/pull/4235))
- [Import] Fix import OVA files (PR [#4232](https://github.com/vatesfr/xen-orchestra/pull/4232))
- [VM/network] Fix duplicate IPv4 (PR [#4239](https://github.com/vatesfr/xen-orchestra/pull/4239))
- [Remotes] Fix disconnected remotes which may appear to work
- [Host] Fix incorrect hypervisor name [#4246](https://github.com/vatesfr/xen-orchestra/issues/4246) (PR [#4248](https://github.com/vatesfr/xen-orchestra/pull/4248))
### Released packages
- xo-server v5.42.0
- xo-web v5.42.0
- xo-server v5.39.0
- xo-web v5.39.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 (and only one)
1. when you want a review, add a reviewer
1. if necessary, update your PR, and re- add a reviewer

View File

@@ -1,4 +1,4 @@
# Xen Orchestra [![Build Status](https://travis-ci.org/vatesfr/xen-orchestra.png?branch=master)](https://travis-ci.org/vatesfr/xen-orchestra)
# 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)
![](http://i.imgur.com/tRffA5y.png)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -1,13 +1,13 @@
# Installation
SSH to your XenServer/XCP-ng host and execute the following:
SSH to your XenServer 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 host itself, it's 100% safe.
This will automatically download/import/start the XOA appliance. Nothing is changed on your XenServer host itself, it's 100% safe.
## [More on XOA](xoa.md)

View File

@@ -1,6 +1,6 @@
# Metadata backup
> WARNING: Metadata backup is an experimental feature. Unexpected issues are possible, but unlikely.
> WARNING: Metadata backup is an experimental feature. Restore is not yet available and some unexpected issues may occur.
## Introduction
@@ -11,38 +11,21 @@ In Xen Orchestra, Metadata backup is divided into two different options:
* Pool metadata backup
* XO configuration backup
### Performing a backup
### How to use metadata backup
In the backup job section, when creating a new backup job, you will now have a choice between backing up VMs and backing up Metadata:
![](./assets/metadata-1.png)
In the backup job section, when creating a new backup job, you will now have a choice between backing up VMs and backing up Metadata.
![](https://user-images.githubusercontent.com/21563339/53413921-bd636f00-39cd-11e9-8a3c-d4f893135fa4.png)
When you select Metadata backup, you will have a new backup job screen, letting you choose between a pool metadata backup and an XO configuration backup (or both at the same time):
![](./assets/metadata-2.png)
![](https://user-images.githubusercontent.com/21563339/52416838-d2de2b00-2aea-11e9-8da0-340fcb2767db.png)
Define the name and retention for the job.
![](./assets/metadata-3.png)
![](https://user-images.githubusercontent.com/21563339/52471527-65390a00-2b91-11e9-8019-600a4d9eeafb.png)
Once created, the job is displayed with the other classic jobs.
![](./assets/metadata-4.png)
![](https://user-images.githubusercontent.com/21563339/52416802-c0fc8800-2aea-11e9-8ef0-b0c1bd0e48b8.png)
### Performing a restore
> WARNING: restoring pool metadata completely overwrites the XAPI database of a host. Only perform a metadata restore if it is a new server with nothing running on it (eg replacing a host with new hardware).
If you browse to the Backup NG Restore panel, you will now notice a Metadata filter button:
![](./assets/metadata-5.png)
If you click this button, it will show you Metadata backups available for restore:
![](./assets/metadata-6.png)
You can see both our Xen Orchestra config backup, and our pool metadata backup. To restore one, simply click the blue restore arrow, choose a backup date to restore, and click OK:
![](./assets/metadata-7.png)
That's it!
> Restore for metadata backup jobs should be available in XO 5.33

View File

@@ -1,33 +1,24 @@
# Support
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).
You can access our pro support if you subscribe to any of these plans:
XOA is available in multiple plans:
* Free
* Starter
* Enterprise
* Premium
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.
The better the plan, the faster the support will be with higher priority.
## Community support
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:
If you are using Xen Orchestra via the sources, you can ask questions and try to recieve help two different ways:
* In our [forum](https://xcp-ng.org/forum/category/12/xen-orchestra)
* In our [forum](https://xen-orchestra.com/forum/)
* In our IRC - `#xen-orchestra` on `Freenode`
We encourage you to give back to the community by assisting other users via these two avenues as well.
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.
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.
If you are using Xen Orchestra in production, please subscribe to a plan.
## Open a ticket
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)
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)

View File

@@ -10,16 +10,15 @@
"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": "^9.0.1",
"eslint-plugin-node": "^8.0.0",
"eslint-plugin-promise": "^4.0.0",
"eslint-plugin-react": "^7.6.1",
"eslint-plugin-standard": "^4.0.0",
"exec-promise": "^0.7.0",
"flow-bin": "^0.98.0",
"flow-bin": "^0.95.1",
"globby": "^9.0.0",
"husky": "^2.2.0",
"husky": "^1.2.1",
"jest": "^24.1.0",
"lodash": "^4.17.4",
"prettier": "^1.10.2",

View File

@@ -1,6 +1,6 @@
{
"name": "complex-matcher",
"version": "0.6.0",
"version": "0.5.0",
"license": "ISC",
"description": "",
"keywords": [],
@@ -25,7 +25,7 @@
">2%"
],
"engines": {
"node": ">=6"
"node": ">=4"
},
"dependencies": {
"lodash": "^4.17.4"
@@ -44,7 +44,6 @@
"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",
"postversion": "npm publish"
"prepublishOnly": "yarn run build"
}
}

View File

@@ -599,13 +599,6 @@ 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 = []
@@ -613,12 +606,6 @@ 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
}
@@ -626,12 +613,6 @@ 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,13 +12,10 @@ import {
} from './'
it('getPropertyClausesStrings', () => {
const tmp = getPropertyClausesStrings(
parse('foo bar:baz baz:|(foo bar /^boo$/ /^far$/) foo:/^bar$/')
)
const tmp = getPropertyClausesStrings(parse('foo bar:baz baz:|(foo bar)'))
expect(tmp).toEqual({
bar: ['baz'],
baz: ['foo', 'bar', 'boo', 'far'],
foo: ['bar'],
baz: ['foo', 'bar'],
})
})

View File

@@ -25,7 +25,7 @@
">2%"
],
"engines": {
"node": ">=6"
"node": ">=4"
},
"dependencies": {},
"devDependencies": {
@@ -43,7 +43,6 @@
"prebuild": "yarn run clean",
"predev": "yarn run prebuild",
"prepare": "yarn run build",
"prepublishOnly": "yarn run build",
"postversion": "npm publish"
"prepublishOnly": "yarn run build"
}
}

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "vhd-lib",
"version": "0.7.0",
"version": "0.6.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": "^8.0.1",
"fs-extra": "^7.0.0",
"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.9.0",
"@xen-orchestra/fs": "^0.8.0",
"babel-plugin-lodash": "^3.3.2",
"cross-env": "^5.1.3",
"execa": "^1.0.0",
@@ -44,7 +44,7 @@
"index-modules": "^0.3.0",
"readable-stream": "^3.0.6",
"rimraf": "^2.6.2",
"tmp": "^0.1.0"
"tmp": "^0.0.33"
},
"scripts": {
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
@@ -52,7 +52,6 @@
"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",
"postversion": "npm publish"
"prepare": "yarn run build"
}
}

View File

@@ -19,7 +19,9 @@ export default bat => {
j += 4
if (j === n) {
return
const error = new Error('no allocated block found')
error.noBlock = true
throw error
}
}
lastSector = firstSector

View File

@@ -23,110 +23,71 @@ afterEach(async () => {
await pFromCallback(cb => rimraf(tempDir, cb))
})
const RAW = 'raw'
const VHD = 'vpc'
const convert = (inputFormat, inputFile, outputFormat, outputFile) =>
execa('qemu-img', [
'convert',
'-f',
inputFormat,
'-O',
outputFormat,
inputFile,
outputFile,
])
const createRandomStream = asyncIteratorToStream(function*(size) {
let requested = Math.min(size, yield)
while (size > 0) {
const buf = Buffer.allocUnsafe(requested)
for (let i = 0; i < requested; ++i) {
buf[i] = Math.floor(Math.random() * 256)
}
requested = Math.min((size -= requested), yield buf)
}
})
async function convertFromRawToVhd(rawName, vhdName) {
await execa('qemu-img', ['convert', '-f', 'raw', '-Ovpc', rawName, vhdName])
}
async function createRandomFile(name, size) {
const createRandomStream = asyncIteratorToStream(function*(size) {
while (size-- > 0) {
yield Buffer.from([Math.floor(Math.random() * 256)])
}
})
const input = await createRandomStream(size)
await pFromCallback(cb => pipeline(input, fs.createWriteStream(name), cb))
}
const forOwn = (object, cb) =>
Object.keys(object).forEach(key => cb(object[key], key, object))
test('createVhdStreamWithLength can extract length', async () => {
const initialSize = 4 * 1024
const rawFileName = `${tempDir}/randomfile`
const vhdName = `${tempDir}/randomfile.vhd`
const outputVhdName = `${tempDir}/output.vhd`
await createRandomFile(rawFileName, initialSize)
await convertFromRawToVhd(rawFileName, vhdName)
const vhdSize = fs.statSync(vhdName).size
const result = await createVhdStreamWithLength(
await createReadStream(vhdName)
)
expect(result.length).toEqual(vhdSize)
const outputFileStream = await createWriteStream(outputVhdName)
await pFromCallback(cb => pipeline(result, outputFileStream, cb))
const outputSize = fs.statSync(outputVhdName).size
expect(outputSize).toEqual(vhdSize)
})
describe('createVhdStreamWithLength', () => {
forOwn(
{
// qemu-img requires this length or it fill with null bytes which breaks
// the test
'can extract length': 34816,
'can handle empty file': 0,
},
(size, title) =>
it(title, async () => {
const inputRaw = `${tempDir}/input.raw`
await createRandomFile(inputRaw, size)
const inputVhd = `${tempDir}/input.vhd`
await convert(RAW, inputRaw, VHD, inputVhd)
const result = await createVhdStreamWithLength(
await createReadStream(inputVhd)
)
const { length } = result
const outputVhd = `${tempDir}/output.vhd`
await pFromCallback(
pipeline.bind(undefined, result, await createWriteStream(outputVhd))
)
// ensure the guessed length correspond to the stream length
const { size: outputSize } = await fs.stat(outputVhd)
expect(length).toEqual(outputSize)
// ensure the generated VHD is correct and contains the same data
const outputRaw = `${tempDir}/output.raw`
await convert(VHD, outputVhd, RAW, outputRaw)
await execa('cmp', [inputRaw, outputRaw])
})
test('createVhdStreamWithLength can skip blank after last block and before footer', async () => {
const initialSize = 4 * 1024
const rawFileName = `${tempDir}/randomfile`
const vhdName = `${tempDir}/randomfile.vhd`
const outputVhdName = `${tempDir}/output.vhd`
await createRandomFile(rawFileName, initialSize)
await convertFromRawToVhd(rawFileName, vhdName)
const vhdSize = fs.statSync(vhdName).size
// read file footer
const footer = await getStream.buffer(
createReadStream(vhdName, { start: vhdSize - FOOTER_SIZE })
)
it('can skip blank after the last block and before the footer', async () => {
const initialSize = 4 * 1024
const rawFileName = `${tempDir}/randomfile`
const vhdName = `${tempDir}/randomfile.vhd`
const outputVhdName = `${tempDir}/output.vhd`
await createRandomFile(rawFileName, initialSize)
await convert(RAW, rawFileName, VHD, vhdName)
const { size: vhdSize } = await fs.stat(vhdName)
// read file footer
const footer = await getStream.buffer(
createReadStream(vhdName, { start: vhdSize - FOOTER_SIZE })
)
// we'll override the footer
const endOfFile = await createWriteStream(vhdName, {
flags: 'r+',
start: vhdSize - FOOTER_SIZE,
})
// write a blank over the previous footer
await pFromCallback(cb => endOfFile.write(Buffer.alloc(FOOTER_SIZE), cb))
// write the footer after the new blank
await pFromCallback(cb => endOfFile.end(footer, cb))
const { size: longerSize } = await fs.stat(vhdName)
// check input file has been lengthened
expect(longerSize).toEqual(vhdSize + FOOTER_SIZE)
const result = await createVhdStreamWithLength(
await createReadStream(vhdName)
)
expect(result.length).toEqual(vhdSize)
const outputFileStream = await createWriteStream(outputVhdName)
await pFromCallback(cb => pipeline(result, outputFileStream, cb))
const { size: outputSize } = await fs.stat(outputVhdName)
// check out file has been shortened again
expect(outputSize).toEqual(vhdSize)
await execa('qemu-img', ['compare', outputVhdName, vhdName])
// we'll override the footer
const endOfFile = await createWriteStream(vhdName, {
flags: 'r+',
start: vhdSize - FOOTER_SIZE,
})
// write a blank over the previous footer
await pFromCallback(cb => endOfFile.write(Buffer.alloc(FOOTER_SIZE), cb))
// write the footer after the new blank
await pFromCallback(cb => endOfFile.end(footer, cb))
const longerSize = fs.statSync(vhdName).size
// check input file has been lengthened
expect(longerSize).toEqual(vhdSize + FOOTER_SIZE)
const result = await createVhdStreamWithLength(
await createReadStream(vhdName)
)
expect(result.length).toEqual(vhdSize)
const outputFileStream = await createWriteStream(outputVhdName)
await pFromCallback(cb => pipeline(result, outputFileStream, cb))
const outputSize = fs.statSync(outputVhdName).size
// check out file has been shortened again
expect(outputSize).toEqual(vhdSize)
await execa('qemu-img', ['compare', outputVhdName, vhdName])
})

View File

@@ -63,14 +63,10 @@ export default async function createVhdStreamWithLength(stream) {
stream.unshift(buf)
}
const firstAndLastBlocks = getFirstAndLastBlocks(table)
const footerOffset =
firstAndLastBlocks !== undefined
? firstAndLastBlocks.lastSector * SECTOR_SIZE +
Math.ceil(header.blockSize / SECTOR_SIZE / 8 / SECTOR_SIZE) *
SECTOR_SIZE +
header.blockSize
: Math.ceil(streamPosition / SECTOR_SIZE) * SECTOR_SIZE
getFirstAndLastBlocks(table).lastSector * SECTOR_SIZE +
Math.ceil(header.blockSize / SECTOR_SIZE / 8 / SECTOR_SIZE) * SECTOR_SIZE +
header.blockSize
// ignore any data after footerOffset and push footerBuffer
//

View File

@@ -1,7 +1,9 @@
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'
@@ -230,45 +232,64 @@ export default class Vhd {
// Write functions.
// =================================================================
// Write a buffer at a given position in a vhd file.
// Write a buffer/stream at a given position in a vhd file.
async _write(data, offset) {
assert(Buffer.isBuffer(data))
debug(`_write offset=${offset} size=${data.length}`)
return this._handler.write(this._path, 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')
}
async _freeFirstBlockSpace(spaceNeededBytes) {
const firstAndLastBlocks = getFirstAndLastBlocks(this.blockTable)
if (firstAndLastBlocks === undefined) {
return
}
const { first, firstSector, lastSector } = firstAndLastBlocks
const tableOffset = this.header.tableOffset
const { batSize } = this
const newMinSector = Math.ceil(
(tableOffset + batSize + spaceNeededBytes) / SECTOR_SIZE
)
if (
tableOffset + batSize + spaceNeededBytes >=
sectorsToBytes(firstSector)
) {
const { fullBlockSize } = this
const newFirstSector = Math.max(
lastSector + fullBlockSize / SECTOR_SIZE,
newMinSector
try {
const { first, firstSector, lastSector } = getFirstAndLastBlocks(
this.blockTable
)
debug(
`freeFirstBlockSpace: move first block ${firstSector} -> ${newFirstSector}`
const tableOffset = this.header.tableOffset
const { batSize } = this
const newMinSector = Math.ceil(
(tableOffset + batSize + spaceNeededBytes) / SECTOR_SIZE
)
// copy the first block at the end
const block = await this._read(sectorsToBytes(firstSector), fullBlockSize)
await this._write(block, sectorsToBytes(newFirstSector))
await this._setBatEntry(first, newFirstSector)
await this.writeFooter(true)
spaceNeededBytes -= this.fullBlockSize
if (spaceNeededBytes > 0) {
return this._freeFirstBlockSpace(spaceNeededBytes)
if (
tableOffset + batSize + spaceNeededBytes >=
sectorsToBytes(firstSector)
) {
const { fullBlockSize } = this
const newFirstSector = Math.max(
lastSector + fullBlockSize / SECTOR_SIZE,
newMinSector
)
debug(
`freeFirstBlockSpace: move first block ${firstSector} -> ${newFirstSector}`
)
// copy the first block at the end
const block = await this._read(
sectorsToBytes(firstSector),
fullBlockSize
)
await this._write(block, sectorsToBytes(newFirstSector))
await this._setBatEntry(first, newFirstSector)
await this.writeFooter(true)
spaceNeededBytes -= this.fullBlockSize
if (spaceNeededBytes > 0) {
return this._freeFirstBlockSpace(spaceNeededBytes)
}
}
} catch (e) {
if (!e.noBlock) {
throw e
}
}
}
@@ -291,7 +312,7 @@ export default class Vhd {
`ensureBatSize: extend BAT ${prevMaxTableEntries} -> ${maxTableEntries}`
)
await this._write(
Buffer.alloc(maxTableEntries - prevMaxTableEntries, BUF_BLOCK_UNUSED),
constantStream(BUF_BLOCK_UNUSED, maxTableEntries - prevMaxTableEntries),
header.tableOffset + prevBat.length
)
await this.writeHeader()
@@ -316,7 +337,10 @@ export default class Vhd {
await Promise.all([
// Write an empty block and addr in vhd file.
this._write(Buffer.alloc(this.fullBlockSize), sectorsToBytes(blockAddr)),
this._write(
constantStream([0], this.fullBlockSize),
sectorsToBytes(blockAddr)
),
this._setBatEntry(blockId, blockAddr),
])

View File

@@ -41,7 +41,7 @@
"human-format": "^0.10.0",
"lodash": "^4.17.4",
"pw": "^0.0.4",
"xen-api": "^0.25.1"
"xen-api": "^0.25.0"
},
"devDependencies": {
"@babel/cli": "^7.1.5",
@@ -56,7 +56,6 @@
"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",
"postversion": "npm publish"
"prepublishOnly": "yarn run build"
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "xen-api",
"version": "0.25.1",
"version": "0.25.0",
"license": "ISC",
"description": "Connector to the Xen API",
"keywords": [
@@ -33,7 +33,6 @@
"node": ">=6"
},
"dependencies": {
"bind-property-descriptor": "^1.0.0",
"blocked": "^1.2.1",
"debug": "^4.0.1",
"event-to-promise": "^0.8.0",
@@ -69,7 +68,6 @@
"plot": "gnuplot -p memory-test.gnu",
"prebuild": "rimraf dist/",
"predev": "yarn run prebuild",
"prepublishOnly": "yarn run build",
"postversion": "npm publish"
"prepublishOnly": "yarn run build"
}
}

View File

@@ -9,7 +9,6 @@ import minimist from 'minimist'
import pw from 'pw'
import { asCallback, fromCallback } from 'promise-toolbox'
import { filter, find, isArray } from 'lodash'
import { getBoundPropertyDescriptor } from 'bind-property-descriptor'
import { start as createRepl } from 'repl'
import { createClient } from './'
@@ -26,20 +25,6 @@ function askPassword(prompt = 'Password: ') {
})
}
const { getPrototypeOf, ownKeys } = Reflect
function getAllBoundDescriptors(object) {
const descriptors = { __proto__: null }
let current = object
do {
ownKeys(current).forEach(key => {
if (!(key in descriptors)) {
descriptors[key] = getBoundPropertyDescriptor(current, key, object)
}
})
} while ((current = getPrototypeOf(current)) !== null)
return descriptors
}
// ===================================================================
const usage = 'Usage: xen-api <url> [<user> [<password>]]'
@@ -93,17 +78,11 @@ const main = async args => {
const repl = createRepl({
prompt: `${xapi._humanId}> `,
})
repl.context.xapi = xapi
{
const ctx = repl.context
ctx.xapi = xapi
ctx.diff = (a, b) => console.log('%s', diff(a, b))
ctx.find = predicate => find(xapi.objects.all, predicate)
ctx.findAll = predicate => filter(xapi.objects.all, predicate)
Object.defineProperties(ctx, getAllBoundDescriptors(xapi))
}
repl.context.diff = (a, b) => console.log('%s', diff(a, b))
repl.context.find = predicate => find(xapi.objects.all, predicate)
repl.context.findAll = predicate => filter(xapi.objects.all, predicate)
// Make the REPL waits for promise completion.
repl.eval = (evaluate => (cmd, context, filename, cb) => {

View File

@@ -34,7 +34,7 @@ const EVENT_TIMEOUT = 60
// ===================================================================
const { defineProperties, defineProperty, freeze, keys: getKeys } = Object
const { defineProperties, freeze, keys: getKeys } = Object
// -------------------------------------------------------------------
@@ -672,47 +672,35 @@ export class Xapi extends EventEmitter {
}
_interruptOnDisconnect(promise) {
let listener
const pWrapper = new Promise((resolve, reject) => {
promise.then(resolve, reject)
this.on(
DISCONNECTED,
(listener = () => {
reject(new Error('disconnected'))
})
)
})
const clean = () => {
this.removeListener(DISCONNECTED, listener)
}
pWrapper.then(clean, clean)
return pWrapper
return Promise.race([
promise,
this._disconnected.then(() => {
throw new Error('disconnected')
}),
])
}
_sessionCallRetryOptions = {
tries: 2,
when: error =>
this._status !== DISCONNECTED && error?.code === 'SESSION_INVALID',
onRetry: () => this._sessionOpen(),
}
_sessionCall(method, args, timeout) {
async _sessionCall(method, args, timeout) {
if (method.startsWith('session.')) {
return Promise.reject(
new Error('session.*() methods are disabled from this interface')
)
throw new Error('session.*() methods are disabled from this interface')
}
return pRetry(() => {
const sessionId = this._sessionId
assert.notStrictEqual(sessionId, undefined)
const sessionId = this._sessionId
assert.notStrictEqual(sessionId, undefined)
const newArgs = [sessionId]
if (args !== undefined) {
newArgs.push.apply(newArgs, args)
const newArgs = [sessionId]
if (args !== undefined) {
newArgs.push.apply(newArgs, args)
}
return pRetry(
() => this._interruptOnDisconnect(this._call(method, newArgs, timeout)),
{
tries: 2,
when: { code: 'SESSION_INVALID' },
onRetry: () => this._sessionOpen(),
}
return this._call(method, newArgs, timeout)
}, this._sessionCallRetryOptions)
)
}
// FIXME: (probably rare) race condition leading to unnecessary login when:
@@ -1023,23 +1011,17 @@ export class Xapi extends EventEmitter {
const getObjectByRef = ref => this._objectsByRef[ref]
Record = defineProperty(
function(ref, data) {
defineProperties(this, {
$id: { value: data.uuid ?? ref },
$ref: { value: ref },
$xapi: { value: xapi },
})
for (let i = 0; i < nFields; ++i) {
const field = fields[i]
this[field] = data[field]
}
},
'name',
{
value: type,
Record = function(ref, data) {
defineProperties(this, {
$id: { value: data.uuid ?? ref },
$ref: { value: ref },
$xapi: { value: xapi },
})
for (let i = 0; i < nFields; ++i) {
const field = fields[i]
this[field] = data[field]
}
)
}
const getters = { $pool: getPool }
const props = { $type: type }

File diff suppressed because it is too large Load Diff

View File

@@ -4,33 +4,31 @@ import { pDelay } from 'promise-toolbox'
import { createClient } from './'
async function main([url]) {
const xapi = createClient({
allowUnauthorized: true,
const xapi = (() => {
const [, , url, user, password] = process.argv
return createClient({
auth: { user, password },
url,
watchEvents: false,
})
await xapi.connect()
})()
let loop = true
process.on('SIGINT', () => {
loop = false
xapi
.connect()
// Get the pool record's ref.
.then(() => xapi.call('pool.get_all'))
// Injects lots of events.
.then(([poolRef]) => {
const loop = () =>
pDelay
.call(
xapi.call('event.inject', 'pool', poolRef),
10 // A small delay is required to avoid overloading the Xen API.
)
.then(loop)
return loop()
})
const { pool } = xapi
// eslint-disable-next-line no-unmodified-loop-condition
while (loop) {
await pool.update_other_config(
'xo:injectEvents',
Math.random()
.toString(36)
.slice(2)
)
await pDelay(1e2)
}
await pool.update_other_config('xo:injectEvents', null)
await xapi.disconnect()
}
main(process.argv.slice(2)).catch(console.error)

View File

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

View File

@@ -64,7 +64,6 @@
"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",
"postversion": "npm publish"
"prepublishOnly": "yarn run build"
}
}

View File

@@ -43,7 +43,6 @@
"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",
"postversion": "npm publish"
"prepublishOnly": "yarn run build"
}
}

View File

@@ -45,7 +45,6 @@
"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",
"postversion": "npm publish"
"prepublishOnly": "yarn run build"
}
}

View File

@@ -32,7 +32,7 @@
"dist/"
],
"engines": {
"node": ">=6"
"node": ">=4"
},
"dependencies": {
"csv-parser": "^2.1.0",
@@ -43,7 +43,7 @@
"xo-lib": "^0.9.0"
},
"devDependencies": {
"@types/node": "^12.0.2",
"@types/node": "^11.11.4",
"@types/through2": "^2.0.31",
"tslint": "^5.9.1",
"tslint-config-standard": "^8.0.1",
@@ -55,7 +55,6 @@
"lint": "tslint 'src/*.ts'",
"posttest": "yarn run lint",
"prepublishOnly": "yarn run build",
"start": "node dist/index.js",
"postversion": "npm publish"
"start": "node dist/index.js"
}
}

View File

@@ -32,7 +32,7 @@
"node": ">=6"
},
"dependencies": {
"jsonrpc-websocket-client": "^0.5.0",
"jsonrpc-websocket-client": "^0.4.1",
"lodash": "^4.17.2",
"make-error": "^1.0.4"
},
@@ -49,7 +49,6 @@
"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",
"postversion": "npm publish"
"prepublishOnly": "yarn run build"
}
}

View File

@@ -41,7 +41,6 @@
"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",
"postversion": "npm publish"
"prepare": "yarn run build"
}
}

View File

@@ -41,6 +41,5 @@
"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,6 +49,5 @@
"prebuild": "rimraf dist/",
"predev": "yarn run prebuild",
"prepublishOnly": "yarn run build"
},
"private": true
}
}

View File

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

View File

@@ -50,6 +50,5 @@
"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.16.1",
"version": "0.15.0",
"license": "AGPL-3.0",
"description": "Backup reports plugin for XO-Server",
"keywords": [
@@ -36,7 +36,6 @@
"node": ">=6"
},
"dependencies": {
"@xen-orchestra/log": "^0.1.4",
"human-format": "^0.10.0",
"lodash": "^4.13.1",
"moment-timezone": "^0.5.13"
@@ -44,8 +43,6 @@
"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",
@@ -58,6 +55,5 @@
"prebuild": "yarn run clean",
"predev": "yarn run prebuild",
"prepublishOnly": "yarn run build"
},
"private": true
}
}

View File

@@ -1,11 +1,8 @@
import createLogger from '@xen-orchestra/log'
import humanFormat from 'human-format'
import moment from 'moment-timezone'
import { forEach, groupBy, startCase } from 'lodash'
import { forEach, get, startCase } from 'lodash'
import pkg from '../package'
const logger = createLogger('xo:xo-server-backup-reports')
export const configurationSchema = {
type: 'object',
@@ -49,9 +46,6 @@ export const testSchema = {
// ===================================================================
const INDENT = ' '
const UNKNOWN_ITEM = 'Unknown'
const ICON_FAILURE = '🚨'
const ICON_INTERRUPTED = '⚠️'
const ICON_SKIPPED = '⏩'
@@ -66,7 +60,7 @@ const STATUS_ICON = {
}
const DATE_FORMAT = 'dddd, MMMM Do YYYY, h:mm:ss a'
const createDateFormatter = timezone =>
const createDateFormater = timezone =>
timezone !== undefined
? timestamp =>
moment(timestamp)
@@ -92,6 +86,10 @@ 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'
@@ -102,114 +100,40 @@ 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 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)}`]
const markdown = [`${indent}- **Start time**: ${formatDate(start)}`]
if (end !== undefined) {
markdown.push(`- **End time**: ${formatDate(end)}`)
markdown.push(`${indent}- **End time**: ${formatDate(end)}`)
const duration = end - start
if (duration >= 1) {
markdown.push(`- **Duration**: ${formatDuration(duration)}`)
markdown.push(`${indent}- **Duration**: ${formatDuration(duration)}`)
}
}
return markdown
}
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
) {
const addWarnings = (text, warnings, nbIndent = 0) => {
if (warnings === undefined) {
return
}
const label = task.status === 'skipped' ? 'Reason' : 'Error'
return `- **${label}**: ${message}`
const indent = INDENT.repeat(nbIndent)
warnings.forEach(({ message }) => {
text.push(`${indent}- **${ICON_WARNING} ${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._report.bind(this)
this._report = this._wrapper.bind(this)
}
configure({ toMails, toXmpp }) {
@@ -222,174 +146,72 @@ class BackupReportsXoPlugin {
}
test({ runId }) {
return this._report(runId, undefined, true)
return this._backupNgListener(undefined, undefined, undefined, runId)
}
unload() {
this._xo.removeListener('job:terminated', this._report)
}
async _report(runJobId, { type, status } = {}, force) {
const xo = this._xo
try {
if (type === 'call') {
return this._legacyVmHandler(status)
}
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)
_wrapper(status, job, schedule, runJobId) {
if (job.type === 'metadataBackup') {
return
}
return new Promise(resolve =>
resolve(
job.type === 'backup'
? this._backupNgListener(status, job, schedule, runJobId)
: this._listener(status, job, schedule, runJobId)
)
).catch(logError)
}
async _metadataHandler(log, { name: jobName }, schedule, force) {
async _backupNgListener(_1, _2, schedule, runJobId) {
const xo = this._xo
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
const log = await xo.getBackupNgLogs(runJobId)
if (log === undefined) {
throw new Error(`no log found with runId=${JSON.stringify(runJobId)}`)
}
// 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
}
const formatDate = createDateFormatter(schedule?.timezone)
if (schedule === undefined) {
schedule = await xo.getSchedule(log.scheduleId)
}
if (log.tasks === undefined) {
const markdown = [
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 = [
`## Global status: ${log.status}`,
'',
`- **Job ID**: ${log.jobId}`,
`- **Run ID**: ${log.id}`,
`- **Run ID**: ${runJobId}`,
`- **mode**: ${mode}`,
...getTemporalDataMarkdown(log.end, log.start, formatDate),
getErrorMarkdown(log),
...getWarningsMarkdown(log.warnings),
'---',
'',
`*${pkg.name} v${pkg.version}*`,
...getTemporalDataMarkdown(log.start, log.end),
`- **Error**: ${log.result.message}`,
]
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: toMarkdown(markdown),
markdown,
nagiosStatus: 2,
nagiosMarkdown: `[Xen Orchestra] [${
log.status
@@ -409,7 +231,7 @@ class BackupReportsXoPlugin {
let nSkipped = 0
let nInterrupted = 0
for (const taskLog of log.tasks) {
if (!force && taskLog.status === 'success' && reportWhen === 'failure') {
if (taskLog.status === 'success' && reportWhen === 'failure') {
continue
}
@@ -422,16 +244,16 @@ class BackupReportsXoPlugin {
`### ${vm !== undefined ? vm.name_label : 'VM not found'}`,
'',
`- **UUID**: ${vm !== undefined ? vm.uuid : vmId}`,
...getTemporalDataMarkdown(taskLog.end, taskLog.start, formatDate),
...getWarningsMarkdown(taskLog.warnings),
...getTemporalDataMarkdown(taskLog.start, taskLog.end),
]
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'
@@ -440,36 +262,29 @@ class BackupReportsXoPlugin {
}
const icon = STATUS_ICON[subTaskLog.status]
const type = subTaskLog.data?.type
const errorMarkdown = getErrorMarkdown(subTaskLog)
const errorMessage = ` - **Error**: ${get(
subTaskLog.result,
'message'
)}`
if (subTaskLog.message === 'snapshot') {
snapshotText.push(`- **Snapshot** ${icon}`, [
...getTemporalDataMarkdown(
subTaskLog.end,
subTaskLog.start,
formatDate
),
])
} else if (type === 'remote') {
snapshotText.push(
`- **Snapshot** ${icon}`,
...getTemporalDataMarkdown(subTaskLog.start, subTaskLog.end, 1)
)
} else if (subTaskLog.data.type === 'remote') {
const id = subTaskLog.data.id
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,
])
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)
if (subTaskLog.status === 'failure') {
failedSubTasks.push(remote !== undefined ? remote.name : id)
remotesText.push('', errorMessage)
}
} else {
const id = subTaskLog.data.id
@@ -479,17 +294,14 @@ 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.end,
subTaskLog.start,
formatDate
),
...getWarningsMarkdown(subTaskLog.warnings),
errorMarkdown,
])
srsText.push(
` - **${srName}** (${srUuid}) ${icon}`,
...getTemporalDataMarkdown(subTaskLog.start, subTaskLog.end, 2)
)
addWarnings(srsText, subTaskLog.warnings, 2)
if (subTaskLog.status === 'failure') {
failedSubTasks.push(sr !== undefined ? sr.name_label : id)
srsText.push('', errorMessage)
}
}
@@ -501,48 +313,53 @@ class BackupReportsXoPlugin {
return
}
const size = operationLog.result?.size
if (size > 0) {
const operationInfoText = []
addWarnings(operationInfoText, operationLog.warnings, 3)
if (operationLog.status === 'success') {
const size = operationLog.result.size
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.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') {
` - **${operationLog.message}** ${
STATUS_ICON[operationLog.status]
}`,
...getTemporalDataMarkdown(operationLog.start, operationLog.end, 3),
...operationInfoText,
].join('\n')
if (get(subTaskLog, 'data.type') === 'remote') {
remotesText.push(operationText)
} else if (type === 'SR') {
remotesText.join('\n')
}
if (get(subTaskLog, 'data.type') === 'SR') {
srsText.push(operationText)
srsText.join('\n')
}
})
}
const subText = [
...snapshotText,
srsText.length !== 0 && `- **SRs**`,
srsText,
remotesText.length !== 0 && `- **Remotes**`,
remotesText,
]
if (srsText.length !== 0) {
srsText.unshift(`- **SRs**`)
}
if (remotesText.length !== 0) {
remotesText.unshift(`- **Remotes**`)
}
const subText = [...snapshotText, '', ...srsText, '', ...remotesText]
if (taskLog.result !== undefined) {
if (taskLog.status === 'skipped') {
++nSkipped
@@ -552,7 +369,8 @@ 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'} : ${
@@ -561,7 +379,11 @@ 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'} : ${
@@ -572,7 +394,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'
@@ -580,34 +402,37 @@ 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
const markdown = [
let markdown = [
`## Global status: ${log.status}`,
'',
`- **Job ID**: ${log.jobId}`,
`- **Run ID**: ${log.id}`,
`- **Run ID**: ${runJobId}`,
`- **mode**: ${mode}`,
...getTemporalDataMarkdown(log.end, log.start, formatDate),
...getTemporalDataMarkdown(log.start, log.end),
`- **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(
'---',
@@ -632,7 +457,7 @@ class BackupReportsXoPlugin {
)
}
if (nSuccesses !== 0 && (force || reportWhen !== 'failure')) {
if (nSuccesses !== 0 && reportWhen !== 'failure') {
markdown.push(
'---',
'',
@@ -643,8 +468,9 @@ class BackupReportsXoPlugin {
}
markdown.push('---', '', `*${pkg.name} v${pkg.version}*`)
markdown = markdown.join('\n')
return this._sendReport({
markdown: toMarkdown(markdown),
markdown,
subject: `[Xen Orchestra] ${log.status} Backup report for ${jobName} ${
STATUS_ICON[log.status]
}`,
@@ -684,9 +510,9 @@ class BackupReportsXoPlugin {
])
}
_legacyVmHandler(status) {
_listener(status) {
const { calls, timezone, error } = status
const formatDate = createDateFormatter(timezone)
const formatDate = createDateFormater(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.5.0"
"jsonrpc-websocket-client": "^0.4.1"
},
"devDependencies": {
"@babel/cli": "^7.0.0",
@@ -49,6 +49,5 @@
"prebuild": "yarn run clean",
"predev": "yarn run prebuild",
"prepublishOnly": "yarn run build"
},
"private": true
}
}

View File

@@ -44,6 +44,5 @@
"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,6 +42,5 @@
"prebuild": "yarn run clean",
"predev": "yarn run prebuild",
"prepublishOnly": "yarn run build"
},
"private": true
}
}

View File

@@ -32,7 +32,7 @@
"node": ">=6"
},
"dependencies": {
"nodemailer": "^6.1.0",
"nodemailer": "^5.0.0",
"nodemailer-markdown": "^1.0.1",
"promise-toolbox": "^0.12.1"
},
@@ -50,6 +50,5 @@
"prebuild": "rimraf dist/",
"predev": "yarn run prebuild",
"prepublishOnly": "yarn run build"
},
"private": true
}
}

View File

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

View File

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

View File

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

View File

@@ -39,7 +39,7 @@
"@xen-orchestra/cron": "^1.0.3",
"@xen-orchestra/log": "^0.1.4",
"handlebars": "^4.0.6",
"html-minifier": "^4.0.0",
"html-minifier": "^3.5.8",
"human-format": "^0.10.0",
"lodash": "^4.17.4",
"promise-toolbox": "^0.12.1"
@@ -59,6 +59,5 @@
"prebuild": "yarn run clean",
"predev": "yarn run prebuild",
"prepublishOnly": "yarn run build"
},
"private": true
}
}

View File

@@ -49,12 +49,6 @@ 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
@@ -74,7 +68,6 @@ honorCipherOrder = true
secureOptions = 117440512
[http.mounts]
'/' = '../xo-web/dist'
[remoteOptions]
mountsDir = '/run/xo-server/mounts'

View File

@@ -1,7 +1,6 @@
{
"private": true,
"name": "xo-server",
"version": "5.42.1",
"version": "5.38.1",
"license": "AGPL-3.0",
"description": "Server part of Xen-Orchestra",
"keywords": [
@@ -38,11 +37,11 @@
"@xen-orchestra/cron": "^1.0.3",
"@xen-orchestra/defined": "^0.0.0",
"@xen-orchestra/emit-async": "^0.0.0",
"@xen-orchestra/fs": "^0.9.0",
"@xen-orchestra/fs": "^0.8.0",
"@xen-orchestra/log": "^0.1.4",
"@xen-orchestra/mixin": "^0.0.0",
"ajv": "^6.1.1",
"app-conf": "^0.7.0",
"app-conf": "^0.6.1",
"archiver": "^3.0.0",
"async-iterator-to-stream": "^1.0.1",
"base64url": "^3.0.0",
@@ -51,7 +50,7 @@
"body-parser": "^1.18.2",
"compression": "^1.7.3",
"connect-flash": "^0.1.1",
"cookie": "^0.4.0",
"cookie": "^0.3.1",
"cookie-parser": "^1.4.3",
"d3-time-format": "^2.1.1",
"debug": "^4.0.1",
@@ -65,7 +64,7 @@
"express-session": "^1.15.6",
"fatfs": "^0.10.4",
"from2": "^2.3.0",
"fs-extra": "^8.0.1",
"fs-extra": "^7.0.0",
"get-stream": "^4.0.0",
"golike-defer": "^0.4.1",
"hashy": "^0.7.1",
@@ -110,7 +109,7 @@
"readable-stream": "^3.2.0",
"redis": "^2.8.0",
"schema-inspector": "^1.6.8",
"semver": "^6.0.0",
"semver": "^5.4.1",
"serve-static": "^1.13.1",
"split-lines": "^2.0.0",
"stack-chain": "^2.0.0",
@@ -118,18 +117,18 @@
"struct-fu": "^1.2.0",
"tar-stream": "^2.0.1",
"through2": "^3.0.0",
"tmp": "^0.1.0",
"tmp": "^0.0.33",
"uuid": "^3.0.1",
"value-matcher": "^0.2.0",
"vhd-lib": "^0.7.0",
"vhd-lib": "^0.6.0",
"ws": "^6.0.0",
"xen-api": "^0.25.1",
"xen-api": "^0.25.0",
"xml2js": "^0.4.19",
"xo-acl-resolver": "^0.4.1",
"xo-collection": "^0.4.1",
"xo-common": "^0.2.0",
"xo-remote-parser": "^0.5.0",
"xo-vmdk-to-vhd": "^0.1.7",
"xo-vmdk-to-vhd": "^0.1.6",
"yazl": "^2.4.3"
},
"devDependencies": {

View File

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

View File

@@ -1,5 +0,0 @@
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, JsonRpcError } from 'json-rpc-peer'
import { format } 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 JsonRpcError(e.message)))
res.end(format.error(0, new Error(e.message)))
}
}

View File

@@ -1,22 +1,26 @@
import { format, JsonRpcError } from 'json-rpc-peer'
import { format } from 'json-rpc-peer'
// ===================================================================
export async function set({
host,
multipathing,
// TODO: use camel case.
name_label: nameLabel,
name_description: nameDescription,
}) {
host = this.getXapiObject(host)
const xapi = this.getXapi(host)
const hostId = host._xapiId
await Promise.all([
nameDescription !== undefined && host.set_name_description(nameDescription),
nameLabel !== undefined && host.set_name_label(nameLabel),
multipathing !== undefined &&
host.$xapi.setHostMultipathing(host.$id, multipathing),
])
if (multipathing !== undefined) {
await xapi.setHostMultipathing(hostId, multipathing)
}
return xapi.setHostProperties(hostId, {
nameLabel,
nameDescription,
})
}
set.description = 'changes the properties of an host'
@@ -244,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 JsonRpcError(e.message)))
res.end(format.error(0, new Error(e.message)))
}
}

View File

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

View File

@@ -5,7 +5,7 @@
async function delete_({ PBD }) {
// TODO: check if PBD is attached before
await this.getXapi(PBD).callAsync('PBD.destroy', PBD._xapiRef)
await this.getXapi(PBD).call('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).callAsync('PBD.plug', PBD._xapiRef)
await this.getXapi(PBD).call('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).callAsync('PIF.destroy', pif._xapiRef)
await this.getXapi(pif).call('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).callAsync('PIF.unplug', pif._xapiRef)
await this.getXapi(pif).call('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).callAsync('PIF.plug', pif._xapiRef)
await this.getXapi(pif).call('PIF.plug', pif._xapiRef)
}
connect.params = {

View File

@@ -1,19 +1,18 @@
import { format, JsonRPcError } from 'json-rpc-peer'
import { format } from 'json-rpc-peer'
// ===================================================================
export async function set({
pool,
// TODO: use camel case.
name_description: nameDescription,
name_label: nameLabel,
}) {
pool = this.getXapiObject(pool)
await Promise.all([
nameDescription !== undefined && pool.set_name_description(nameDescription),
nameLabel !== undefined && pool.set_name_label(nameLabel),
])
await this.getXapi(pool).setPoolProperties({
nameDescription,
nameLabel,
})
}
set.params = {
@@ -235,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 JsonRPcError(e.message)))
res.end(format.error(0, new Error(e.message)))
}
}

View File

@@ -10,15 +10,14 @@ import { forEach, parseXml } from '../utils'
export async function set({
sr,
// TODO: use camel case.
name_description: nameDescription,
name_label: nameLabel,
}) {
sr = this.getXapiObject(sr)
await Promise.all([
nameDescription !== undefined && sr.set_name_description(nameDescription),
nameLabel !== undefined && sr.set_name_label(nameLabel),
])
await this.getXapi(sr).setSrProperties(sr._xapiId, {
nameDescription,
nameLabel,
})
}
set.params = {
@@ -36,7 +35,7 @@ set.resolve = {
// -------------------------------------------------------------------
export async function scan({ SR }) {
await this.getXapi(SR).callAsync('SR.scan', SR._xapiRef)
await this.getXapi(SR).call('SR.scan', SR._xapiRef)
}
scan.params = {

View File

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

View File

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

View File

@@ -64,7 +64,6 @@ export async function set({
allowedIpv4Addresses,
allowedIpv6Addresses,
attached,
rateLimit,
}) {
const oldIpAddresses = vif.allowedIpv4Addresses.concat(
vif.allowedIpv6Addresses
@@ -92,9 +91,6 @@ 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)
@@ -111,7 +107,6 @@ export async function set({
return this.getXapi(vif).editVif(vif._xapiId, {
ipv4Allowed: allowedIpv4Addresses,
ipv6Allowed: allowedIpv6Addresses,
rateLimit,
})
}
@@ -134,11 +129,6 @@ 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, JsonRpcError } from 'json-rpc-peer'
import { format } from 'json-rpc-peer'
import { ignoreErrors } from 'promise-toolbox'
import { assignWith, concat } from 'lodash'
import {
@@ -193,11 +193,6 @@ create.params = {
optional: true,
},
networkConfig: {
type: 'string',
optional: true,
},
coreOs: {
type: 'boolean',
optional: true,
@@ -603,7 +598,7 @@ set.params = {
// Switch from Cirrus video adaptor to VGA adaptor
vga: { type: 'string', optional: true },
videoram: { type: 'number', optional: true },
videoram: { type: ['string', 'number'], optional: true },
coresPerSocket: { type: ['string', 'number', 'null'], optional: true },
@@ -630,7 +625,13 @@ set.resolve = {
// -------------------------------------------------------------------
export async function restart({ vm, force = false }) {
return this.getXapi(vm).rebootVm(vm._xapiId, { hard: force })
const xapi = this.getXapi(vm)
if (force) {
await xapi.call('VM.hard_reboot', vm._xapiRef)
} else {
await xapi.call('VM.clean_reboot', vm._xapiRef)
}
}
restart.params = {
@@ -731,7 +732,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.getXapiObject(vm).set_is_a_template(true)
await this.getXapi(vm).call('VM.set_is_a_template', vm._xapiRef, true)
}
convertToTemplate.params = {
@@ -1083,7 +1084,7 @@ stop.resolve = {
// -------------------------------------------------------------------
export async function suspend({ vm }) {
await this.getXapi(vm).callAsync('VM.suspend', vm._xapiRef)
await this.getXapi(vm).call('VM.suspend', vm._xapiRef)
}
suspend.params = {
@@ -1097,7 +1098,7 @@ suspend.resolve = {
// -------------------------------------------------------------------
export async function pause({ vm }) {
await this.getXapi(vm).callAsync('VM.pause', vm._xapiRef)
await this.getXapi(vm).call('VM.pause', vm._xapiRef)
}
pause.params = {
@@ -1197,7 +1198,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 JsonRpcError(e.message)))
res.end(format.error(0, new Error(e.message)))
}
}
@@ -1412,11 +1413,15 @@ stats.resolve = {
// -------------------------------------------------------------------
export async function setBootOrder({ vm, order }) {
if (vm.virtualizationMode !== 'hvm') {
throw invalidParameters('You can only set the boot order on a HVM guest')
const xapi = this.getXapi(vm)
order = { order }
if (vm.virtualizationMode === 'hvm') {
await xapi.call('VM.set_HVM_boot_params', vm._xapiRef, order)
return
}
await this.getXapiObject(vm).set_HVM_boot_params({ order })
throw invalidParameters('You can only set the boot order on a HVM guest')
}
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.callAsync('PIF.plug', pif.$ref)
await xapi.call('PIF.plug', pif.$ref)
const PBD = find(xosanSr.$PBDs, pbd => pbd.$host.$id === host)
if (PBD) {
await xapi.callAsync('PBD.plug', PBD.$ref)
await xapi.call('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.callAsync('SR.scan', xosanSrRef)
await xapi.call('SR.scan', xosanSrRef)
await this.rebindLicense({
licenseId: license.id,
oldBoundObjectId: tmpBoundObjectId,
@@ -884,13 +884,13 @@ async function createVDIOnLVMWithoutSizeLimit(xapi, lvmSr, diskSize) {
if (result.exit !== 0) {
throw Error('Could not create volume ->' + result.stdout)
}
await xapi.callAsync('SR.scan', xapi.getObject(lvmSr).$ref)
await xapi.call('SR.scan', xapi.getObject(lvmSr).$ref)
const vdi = find(xapi.getObject(lvmSr).$VDIs, vdi => vdi.uuid === uuid)
if (vdi != null) {
await Promise.all([
vdi.set_name_description('Created by XO'),
vdi.set_name_label('xosan_data'),
])
await xapi.setSrProperties(vdi.$ref, {
nameLabel: 'xosan_data',
nameDescription: 'Created by XO',
})
return vdi
}
}
@@ -989,7 +989,7 @@ async function replaceBrickOnSameVM(
await xapi.disconnectVbd(previousVBD)
await xapi.deleteVdi(previousVBD.VDI)
CURRENT_POOL_OPERATIONS[poolId] = { ...OPERATION_OBJECT, state: 4 }
await xapi.callAsync('SR.scan', xapi.getObject(xosansr).$ref)
await xapi.call('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.callAsync('SR.scan', xapi.getObject(xosansr).$ref)
await xapi.call('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.callAsync('VIF.move', firstVif.$ref, xosanNetwork.$ref)
await xapi.call('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 newVM.set_xenstore_data(xenstoreData)
await xapi.call('VM.set_xenstore_data', newVM.$ref, 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.callAsync('SR.scan', xapi.getObject(xosansr).$ref)
await xapi.call('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.callAsync('SR.scan', xapi.getObject(xosansr._xapiId).$ref)
await xapi.call('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,10 +1542,9 @@ export async function downloadAndInstallXosanPack({ id, version, pool }) {
const res = await this.requestResource('xosan', id, version)
await xapi.installSupplementalPackOnAllHosts(res)
await xapi.pool.update_other_config(
'xosan_pack_installation_time',
String(Math.floor(Date.now() / 1e3))
)
await xapi._updateObjectMapProperty(xapi.pool, '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(config.http.helmet))
app.use(helmet())
app.use(compression())
@@ -417,7 +417,6 @@ const setUpProxies = (express, opts, xo) => {
}
const proxy = createProxyServer({
changeOrigin: true,
ignorePath: true,
}).on('error', error => console.error(error))

View File

@@ -2,8 +2,6 @@ import Collection from '../collection/redis'
import Model from '../model'
import { forEach } from '../utils'
import { parseProp } from './utils'
// ===================================================================
export default class Remote extends Model {}
@@ -16,21 +14,12 @@ export class Remotes extends Collection {
async get(properties) {
const remotes = await super.get(properties)
forEach(remotes, remote => {
remote.benchmarks = parseProp('remote', remote, 'benchmarks')
remote.benchmarks =
remote.benchmarks !== undefined
? JSON.parse(remote.benchmarks)
: undefined
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,16 +569,6 @@ 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'),
}
@@ -643,7 +633,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,4 +1,3 @@
/* 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'
@@ -29,7 +28,6 @@ import {
groupBy,
includes,
isEmpty,
noop,
omit,
startsWith,
uniq,
@@ -229,6 +227,14 @@ 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
@@ -247,6 +253,81 @@ export default class Xapi extends XapiBase {
)::ignoreErrors()
}
async _updateObjectMapProperty(object, prop, values) {
const { $ref: ref, $type: type } = object
prop = camelToSnakeCase(prop)
const add = `${type}.add_to_${prop}`
const remove = `${type}.remove_from_${prop}`
await Promise.all(
mapToArray(values, (value, name) => {
if (value !== undefined) {
name = camelToSnakeCase(name)
const removal = this.call(remove, ref, name)
return value === null
? removal
: removal
::ignoreErrors()
.then(() => this.call(add, ref, name, prepareXapiParam(value)))
}
})
)
}
async setHostProperties(id, { nameLabel, nameDescription }) {
await this._setObjectProperties(this.getObject(id), {
nameLabel,
nameDescription,
})
}
async setPoolProperties({ autoPoweron, nameLabel, nameDescription }) {
const { pool } = this
await Promise.all([
this._setObjectProperties(pool, {
nameLabel,
nameDescription,
}),
autoPoweron != null &&
this._updateObjectMapProperty(pool, 'other_config', {
autoPoweron: autoPoweron ? 'true' : null,
}),
])
}
async setSrProperties(id, { nameLabel, nameDescription }) {
await this._setObjectProperties(this.getObject(id), {
nameLabel,
nameDescription,
})
}
async setNetworkProperties(
id,
{ automatic, defaultIsLocked, nameDescription, nameLabel }
) {
let defaultLockingMode
if (defaultIsLocked != null) {
defaultLockingMode = defaultIsLocked ? 'disabled' : 'unlocked'
}
const network = this.getObject(id)
await Promise.all([
this._setObjectProperties(network, {
defaultLockingMode,
nameDescription,
nameLabel,
}),
this._updateObjectMapProperty(network, 'other_config', {
automatic:
automatic === undefined ? undefined : automatic ? 'true' : null,
}),
])
}
// =================================================================
async addTag(id, tag) {
@@ -263,8 +344,10 @@ export default class Xapi extends XapiBase {
// =================================================================
setDefaultSr(srId) {
return this.pool.set_default_SR(this.getObject(srId).$ref)
async setDefaultSr(srId) {
this._setObjectProperties(this.pool, {
default_SR: this.getObject(srId).$ref,
})
}
// =================================================================
@@ -293,12 +376,12 @@ export default class Xapi extends XapiBase {
await pSettle(
mapToArray(vms, vm => {
if (!vm.is_control_domain) {
return this.callAsync('VM.suspend', vm.$ref)
return this.call('VM.suspend', vm.$ref)
}
})
)
await this.call('host.disable', host.$ref)
await this.callAsync('host.shutdown', host.$ref)
await this.call('host.shutdown', host.$ref)
}
// =================================================================
@@ -311,7 +394,7 @@ export default class Xapi extends XapiBase {
await this.call('host.disable', ref)
try {
await this.callAsync('host.evacuate', ref)
await this.call('host.evacuate', ref)
} catch (error) {
if (!force) {
await this.call('host.enable', ref)
@@ -326,7 +409,7 @@ export default class Xapi extends XapiBase {
}
async forgetHost(hostId) {
await this.callAsync('host.destroy', this.getObject(hostId).$ref)
await this.call('host.destroy', this.getObject(hostId).$ref)
}
async ejectHostFromPool(hostId) {
@@ -361,7 +444,9 @@ export default class Xapi extends XapiBase {
$defer(() => this.plugPbd(ref))
})
return host.update_other_config(
return this._updateObjectMapProperty(
host,
'other_config',
multipathing
? {
multipathing: 'true',
@@ -374,23 +459,23 @@ export default class Xapi extends XapiBase {
}
async powerOnHost(hostId) {
await this.callAsync('host.power_on', this.getObject(hostId).$ref)
await this.call('host.power_on', this.getObject(hostId).$ref)
}
async rebootHost(hostId, force = false) {
const host = this.getObject(hostId)
await this._clearHost(host, force)
await this.callAsync('host.reboot', host.$ref)
await this.call('host.reboot', host.$ref)
}
async restartHostAgent(hostId) {
await this.callAsync('host.restart_agent', this.getObject(hostId).$ref)
await this.call('host.restart_agent', this.getObject(hostId).$ref)
}
async setRemoteSyslogHost(hostId, syslogDestination) {
const host = this.getObject(hostId)
await host.set_logging({
await this.call('host.set_logging', host.$ref, {
syslog_destination: syslogDestination,
})
await this.call('host.syslog_reconfigure', host.$ref)
@@ -400,7 +485,7 @@ export default class Xapi extends XapiBase {
const host = this.getObject(hostId)
await this._clearHost(host, force)
await this.callAsync('host.shutdown', host.$ref)
await this.call('host.shutdown', host.$ref)
}
// =================================================================
@@ -414,7 +499,7 @@ export default class Xapi extends XapiBase {
}`
)
return this.callAsync('VM.clone', vm.$ref, nameLabel).then(extractOpaqueRef)
return this.call('VM.clone', vm.$ref, nameLabel)
}
// Copy a VM: make a normal copy of a VM and all its VDIs.
@@ -485,7 +570,12 @@ export default class Xapi extends XapiBase {
stream = stream.pipe(sizeStream)
const onVmCreation =
nameLabel !== undefined ? vm => vm.set_name_label(nameLabel) : null
nameLabel !== undefined
? vm =>
targetXapi._setObjectProperties(vm, {
nameLabel,
})
: null
const vm = await targetXapi._getOrWaitObject(
await targetXapi._importVm(stream, sr, onVmCreation)
@@ -625,13 +715,17 @@ 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.callAsync('VM.hard_shutdown', $ref)
await this.call('VM.hard_shutdown', $ref)
}
await Promise.all([
vm.set_is_a_template(false),
vm.update_blocked_operations('destroy', null),
vm.update_other_config('default_template', null),
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,
}),
])
// must be done before destroying the VM
@@ -639,7 +733,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.callAsync('VM.destroy', $ref)
await this.call('VM.destroy', $ref)
return Promise.all([
asyncMap(vm.$snapshots, snapshot =>
@@ -775,13 +869,7 @@ export default class Xapi extends XapiBase {
_assertHealthyVdiChains(vm) {
const cache = { __proto__: null }
forEach(vm.$VBDs, ({ $VDI }) => {
try {
this._assertHealthyVdiChain($VDI, cache)
} catch (error) {
error.VDI = $VDI
error.VM = vm
throw error
}
this._assertHealthyVdiChain($VDI, cache)
})
}
@@ -976,9 +1064,15 @@ export default class Xapi extends XapiBase {
$defer.onFailure(() => this._deleteVm(vm))
await Promise.all([
vm.set_name_label(`[Importing…] ${name_label}`),
vm.update_blocked_operations('start', 'Importing…'),
vm.update_other_config(TAG_COPY_SRC, delta.vm.uuid),
this._setObjectProperties(vm, {
name_label: `[Importing…] ${name_label}`,
}),
this._updateObjectMapProperty(vm, 'blocked_operations', {
start: 'Importing…',
}),
this._updateObjectMapProperty(vm, 'other_config', {
[TAG_COPY_SRC]: delta.vm.uuid,
}),
])
// 2. Delete all VBDs which may have been created by the import.
@@ -1002,7 +1096,9 @@ export default class Xapi extends XapiBase {
newVdi = await this._getOrWaitObject(await this._cloneVdi(baseVdi))
$defer.onFailure(() => this._deleteVdi(newVdi.$ref))
await newVdi.update_other_config(TAG_COPY_SRC, vdi.uuid)
await this._updateObjectMapProperty(newVdi, 'other_config', {
[TAG_COPY_SRC]: vdi.uuid,
})
} else {
newVdi = await this.createVdi({
...vdi,
@@ -1097,14 +1193,15 @@ export default class Xapi extends XapiBase {
}
await Promise.all([
vm.set_name_label(name_label),
this._setObjectProperties(vm, {
name_label,
}),
// FIXME: move
vm.update_blocked_operations(
'start',
disableStartAfterImport
this._updateObjectMapProperty(vm, 'blocked_operations', {
start: disableStartAfterImport
? 'Do not start this VM, clone it if you want to use it.'
: null
),
: null,
}),
])
return { transferSize, vm }
@@ -1158,7 +1255,7 @@ export default class Xapi extends XapiBase {
)
const loop = () =>
this.callAsync(
this.call(
'VM.migrate_send',
vm.$ref,
token,
@@ -1172,7 +1269,7 @@ export default class Xapi extends XapiBase {
pDelay(1e4).then(loop)
)
return loop().then(noop)
return loop()
}
@synchronized()
@@ -1332,7 +1429,9 @@ export default class Xapi extends XapiBase {
'start',
'OVA import in progress...'
),
vm.set_name_label(`[Importing...] ${nameLabel}`),
this._setObjectProperties(vm, {
name_label: `[Importing...] ${nameLabel}`,
}),
])
// 2. Create VDIs & Vifs.
@@ -1349,7 +1448,7 @@ export default class Xapi extends XapiBase {
$defer.onFailure(() => this._deleteVdi(vdi.$ref))
return this.createVbd({
userdevice: String(disk.position),
userdevice: disk.position,
vdi,
vm,
})
@@ -1393,7 +1492,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'),
vm.set_name_label(nameLabel),
this._setObjectProperties(vm, { name_label: nameLabel }),
])
return vm
}
@@ -1440,7 +1539,7 @@ export default class Xapi extends XapiBase {
})
} else {
try {
await this.callAsync('VM.pool_migrate', vm.$ref, host.$ref, {
await this.call('VM.pool_migrate', vm.$ref, host.$ref, {
force: 'true',
})
} catch (error) {
@@ -1525,11 +1624,19 @@ export default class Xapi extends XapiBase {
return /* await */ this._snapshotVm(this.getObject(vmId), nameLabel)
}
async setVcpuWeight(vmId, weight) {
weight = weight || null // Take all falsy values as a removal (0 included)
const vm = this.getObject(vmId)
await this._updateObjectMapProperty(vm, 'VCPUs_params', { weight })
}
async _startVm(vm, host, force) {
log.debug(`Starting VM ${vm.name_label}`)
if (force) {
await vm.update_blocked_operations('start', null)
await this._updateObjectMapProperty(vm, 'blocked_operations', {
start: null,
})
}
return host === undefined
@@ -1539,7 +1646,7 @@ export default class Xapi extends XapiBase {
false, // Start paused?
false // Skip pre-boot checks?
)
: this.callAsync('VM.start_on', vm.$ref, host.$ref, false, false)
: this.call('VM.start_on', vm.$ref, host.$ref, false, false)
}
async startVm(vmId, hostId, force) {
@@ -1568,12 +1675,16 @@ export default class Xapi extends XapiBase {
if (isVmHvm(vm)) {
const { order } = vm.HVM_boot_params
await vm.update_HVM_boot_params('order', 'd')
await this._updateObjectMapProperty(vm, 'HVM_boot_params', {
order: 'd',
})
try {
await this._startVm(vm)
} finally {
await vm.update_HVM_boot_params('order', order)
await this._updateObjectMapProperty(vm, 'HVM_boot_params', {
order,
})
}
} else {
// Find the original template by name (*sigh*).
@@ -1595,14 +1706,20 @@ export default class Xapi extends XapiBase {
const cdDrive = this._getVmCdDrive(vm)
forEach(vm.$VBDs, vbd => {
promises.push(vbd.set_bootable(vbd === cdDrive))
promises.push(
this._setObjectProperties(vbd, {
bootable: vbd === cdDrive,
})
)
bootables.push([vbd, Boolean(vbd.bootable)])
})
promises.push(
vm.set_PV_bootloader('eliloader'),
vm.update_other_config({
this._setObjectProperties(vm, {
PV_bootloader: 'eliloader',
}),
this._updateObjectMapProperty(vm, 'other_config', {
'install-distro':
template && template.other_config['install-distro'],
'install-repository': 'cdrom',
@@ -1613,10 +1730,12 @@ export default class Xapi extends XapiBase {
await this._startVm(vm)
} finally {
vm.set_PV_bootloader(bootloader)::ignoreErrors()
this._setObjectProperties(vm, {
PV_bootloader: bootloader,
})::ignoreErrors()
forEach(bootables, ([vbd, bootable]) => {
vbd.set_bootable(bootable)::ignoreErrors()
this._setObjectProperties(vbd, { bootable })::ignoreErrors()
})
}
}
@@ -1700,14 +1819,14 @@ export default class Xapi extends XapiBase {
})
if (isVmRunning(vm)) {
await this.callAsync('VBD.plug', vbdRef)
await this.call('VBD.plug', vbdRef)
}
}
_cloneVdi(vdi) {
log.debug(`Cloning VDI ${vdi.name_label}`)
return this.callAsync('VDI.clone', vdi.$ref).then(extractOpaqueRef)
return this.call('VDI.clone', vdi.$ref)
}
async createVdi({
@@ -1730,7 +1849,7 @@ export default class Xapi extends XapiBase {
log.debug(`Creating VDI ${name_label} on ${sr.name_label}`)
return this._getOrWaitObject(
await this.callAsync('VDI.create', {
await this.call('VDI.create', {
name_description,
name_label,
other_config,
@@ -1742,7 +1861,7 @@ export default class Xapi extends XapiBase {
type,
virtual_size: size !== undefined ? parseSize(size) : virtual_size,
xenstore_data,
}).then(extractOpaqueRef)
})
)
}
@@ -1760,12 +1879,9 @@ export default class Xapi extends XapiBase {
}`
)
try {
await pRetry(
() => this.callAsync('VDI.pool_migrate', vdi.$ref, sr.$ref, {}),
{
when: { code: 'TOO_MANY_STORAGE_MIGRATES' },
}
)
await pRetry(() => this.call('VDI.pool_migrate', vdi.$ref, sr.$ref, {}), {
when: { code: 'TOO_MANY_STORAGE_MIGRATES' },
})
} catch (error) {
const { code } = error
if (
@@ -1776,9 +1892,7 @@ export default class Xapi extends XapiBase {
throw error
}
const newVdi = await this.barrier(
await this.callAsync('VDI.copy', vdi.$ref, sr.$ref).then(
extractOpaqueRef
)
await this.call('VDI.copy', vdi.$ref, sr.$ref)
)
await asyncMap(vdi.$VBDs, async vbd => {
await this.call('VBD.destroy', vbd.$ref)
@@ -1796,7 +1910,7 @@ export default class Xapi extends XapiBase {
log.debug(`Deleting VDI ${vdiRef}`)
try {
await this.callAsync('VDI.destroy', vdiRef)
await this.call('VDI.destroy', vdiRef)
} catch (error) {
if (error?.code !== 'HANDLE_INVALID') {
throw error
@@ -1809,7 +1923,7 @@ export default class Xapi extends XapiBase {
`Resizing VDI ${vdi.name_label} from ${vdi.virtual_size} to ${size}`
)
return this.callAsync('VDI.resize', vdi.$ref, size)
return this.call('VDI.resize', vdi.$ref, size)
}
_getVmCdDrive(vm) {
@@ -1823,7 +1937,7 @@ export default class Xapi extends XapiBase {
async _ejectCdFromVm(vm) {
const cdDrive = this._getVmCdDrive(vm)
if (cdDrive) {
await this.callAsync('VBD.eject', cdDrive.$ref)
await this.call('VBD.eject', cdDrive.$ref)
}
}
@@ -1831,20 +1945,20 @@ export default class Xapi extends XapiBase {
const cdDrive = await this._getVmCdDrive(vm)
if (cdDrive) {
try {
await this.callAsync('VBD.insert', cdDrive.$ref, cd.$ref)
await this.call('VBD.insert', cdDrive.$ref, cd.$ref)
} catch (error) {
if (!force || error.code !== 'VBD_NOT_EMPTY') {
throw error
}
await this.callAsync('VBD.eject', cdDrive.$ref)::ignoreErrors()
await this.call('VBD.eject', cdDrive.$ref)::ignoreErrors()
// Retry.
await this.callAsync('VBD.insert', cdDrive.$ref, cd.$ref)
await this.call('VBD.insert', cdDrive.$ref, cd.$ref)
}
if (bootable !== Boolean(cdDrive.bootable)) {
await cdDrive.set_bootable(bootable)
await this._setObjectProperties(cdDrive, { bootable })
}
} else {
await this.createVbd({
@@ -1857,7 +1971,7 @@ export default class Xapi extends XapiBase {
}
async connectVbd(vbdId) {
await this.callAsync('VBD.plug', vbdId)
await this.call('VBD.plug', vbdId)
}
async _disconnectVbd(vbd) {
@@ -1866,7 +1980,7 @@ export default class Xapi extends XapiBase {
await this.call('VBD.unplug_force', vbd.$ref)
} catch (error) {
if (error.code === 'VBD_NOT_UNPLUGGABLE') {
await vbd.set_unpluggable(true)
await this.call('VBD.set_unpluggable', vbd.$ref, true)
return this.call('VBD.unplug_force', vbd.$ref)
}
}
@@ -1917,11 +2031,11 @@ export default class Xapi extends XapiBase {
const vdi = this.getObject(vdiId)
const snap = await this._getOrWaitObject(
await this.callAsync('VDI.snapshot', vdi.$ref).then(extractOpaqueRef)
await this.call('VDI.snapshot', vdi.$ref)
)
if (nameLabel) {
await snap.set_name_label(nameLabel)
await this.call('VDI.set_name_label', snap.$ref, nameLabel)
}
return snap
@@ -2045,7 +2159,7 @@ export default class Xapi extends XapiBase {
)
if (currently_attached && isVmRunning(vm)) {
await this.callAsync('VIF.plug', vifRef)
await this.call('VIF.plug', vifRef)
}
return vifRef
@@ -2073,7 +2187,7 @@ export default class Xapi extends XapiBase {
// https://citrix.github.io/xenserver-sdk/#network
other_config: { automatic: 'false' },
})
$defer.onFailure(() => this.callAsync('network.destroy', networkRef))
$defer.onFailure(() => this.call('network.destroy', networkRef))
if (pifId) {
await this.call(
'pool.create_VLAN_from_PIF',
@@ -2112,7 +2226,7 @@ export default class Xapi extends XapiBase {
await Promise.all(
mapToArray(
vlans,
vlan => vlan !== NULL_REF && this.callAsync('VLAN.destroy', vlan)
vlan => vlan !== NULL_REF && this.call('VLAN.destroy', vlan)
)
)
@@ -2127,7 +2241,7 @@ export default class Xapi extends XapiBase {
newPifs,
pifRef =>
!wasAttached[this.getObject(pifRef).host] &&
this.callAsync('PIF.unplug', pifRef)::ignoreErrors()
this.call('PIF.unplug', pifRef)::ignoreErrors()
)
)
}
@@ -2158,7 +2272,7 @@ export default class Xapi extends XapiBase {
await Promise.all(
mapToArray(
vlans,
vlan => vlan !== NULL_REF && this.callAsync('VLAN.destroy', vlan)
vlan => vlan !== NULL_REF && this.call('VLAN.destroy', vlan)
)
)
@@ -2167,7 +2281,7 @@ export default class Xapi extends XapiBase {
mapToArray(bonds, bond => this.call('Bond.destroy', bond))
)
await this.callAsync('network.destroy', network.$ref)
await this.call('network.destroy', network.$ref)
}
// =================================================================

View File

@@ -68,6 +68,11 @@ 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.callAsync('VIF.plug', vif.$ref)
await this.call('VIF.plug', vif.$ref)
},
async connectVif(vifId) {
await this._connectVif(this.getObject(vifId))
},
async _deleteVif(vif) {
await this.callAsync('VIF.destroy', vif.$ref)
await this.call('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.callAsync('VIF.unplug_force', vif.$ref)
await this.call('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 vif.set_locking_mode(lockingMode)
return this._set('locking_mode', lockingMode)
}
},
],
@@ -53,36 +53,10 @@ export default {
: 'locked'
if (lockingMode !== vif.locking_mode) {
return vif.set_locking_mode(lockingMode)
return this._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,10 +353,9 @@ export default {
if (JSON.parse(update).exit !== 0) {
throw new Error('Update install failed')
} else {
await host.update_other_config(
'rpm_patch_installation_time',
String(Date.now() / 1000)
)
await this._updateObjectMapProperty(host, 'other_config', {
rpm_patch_installation_time: String(Date.now() / 1000),
})
}
})
},

View File

@@ -35,7 +35,7 @@ export default {
},
_plugPbd(pbd) {
return this.callAsync('PBD.plug', pbd.$ref)
return this.call('PBD.plug', pbd.$ref)
},
async plugPbd(id) {
@@ -43,7 +43,7 @@ export default {
},
_unplugPbd(pbd) {
return this.callAsync('PBD.unplug', pbd.$ref)
return this.call('PBD.unplug', pbd.$ref)
},
async unplugPbd(id) {

View File

@@ -52,7 +52,6 @@ export default {
coreOs = false,
cloudConfig = undefined,
networkConfig = undefined,
vgpuType = undefined,
gpuGroup = undefined,
@@ -94,7 +93,7 @@ export default {
// Creates the VDIs and executes the initial steps of the
// installation.
await this.callAsync('VM.provision', vmRef)
await this.call('VM.provision', vmRef)
let vm = await this._getOrWaitObject(vmRef)
@@ -115,7 +114,9 @@ export default {
order = 'ncd'
}
vm.set_HVM_boot_params({ ...bootParams, order })
this._setObjectProperties(vm, {
HVM_boot_params: { ...bootParams, order },
})
}
} else {
// PV
@@ -123,12 +124,13 @@ export default {
if (installMethod === 'network') {
// TODO: normalize RHEL URL?
await vm.update_other_config(
'install-repository',
installRepository
)
await this._updateObjectMapProperty(vm, 'other_config', {
'install-repository': installRepository,
})
} else if (installMethod === 'cd') {
await vm.update_other_config('install-repository', 'cdrom')
await this._updateObjectMapProperty(vm, 'other_config', {
'install-repository': 'cdrom',
})
}
}
}
@@ -239,16 +241,10 @@ export default {
}
})
if (coreOs) {
await this.createCoreOsCloudInitConfigDrive(vm.$id, srRef, cloudConfig)
} else {
await this.createCloudInitConfigDrive(
vm.$id,
srRef,
cloudConfig,
networkConfig
)
}
const method = coreOs
? 'createCoreOsCloudInitConfigDrive'
: 'createCloudInitConfigDrive'
await this[method](vm.$id, srRef, cloudConfig)
}
// wait for the record with all the VBDs and VIFs
@@ -261,15 +257,25 @@ export default {
_editVm: makeEditObject({
affinityHost: {
get: 'affinity',
set: (value, vm) =>
vm.set_affinity(value ? this.getObject(value).$ref : NULL_REF),
set(value, vm) {
return this._setObjectProperty(
vm,
'affinity',
value ? this.getObject(value).$ref : NULL_REF
)
},
},
autoPoweron: {
set(value, vm) {
return Promise.all([
vm.update_other_config('auto_poweron', value ? 'true' : null),
value && vm.$pool.update_other_config('auto_poweron', 'true'),
this._updateObjectMapProperty(vm, 'other_config', {
autoPoweron: value ? 'true' : null,
}),
value &&
this.setPoolProperties({
autoPoweron: true,
}),
])
},
},
@@ -279,19 +285,23 @@ export default {
if (virtualizationMode !== 'pv' && virtualizationMode !== 'hvm') {
throw new Error(`The virtualization mode must be 'pv' or 'hvm'`)
}
return vm
.set_domain_type(virtualizationMode)
::pCatch({ code: 'MESSAGE_METHOD_UNKNOWN' }, () =>
vm.set_HVM_boot_policy(
return this._set('domain_type', virtualizationMode)::pCatch(
{ code: 'MESSAGE_METHOD_UNKNOWN' },
() =>
this._set(
'HVM_boot_policy',
virtualizationMode === 'hvm' ? 'Boot order' : ''
)
)
)
},
},
coresPerSocket: {
set: (coresPerSocket, vm) =>
vm.update_platform('cores-per-socket', String(coresPerSocket)),
set(coresPerSocket, vm) {
return this._updateObjectMapProperty(vm, 'platform', {
'cores-per-socket': coresPerSocket,
})
},
},
CPUs: 'cpus',
@@ -309,22 +319,26 @@ export default {
get: vm => +vm.VCPUs_at_startup,
set: [
'VCPUs_at_startup',
(value, vm) => isVmRunning(vm) && vm.set_VCPUs_number_live(value),
function(value, vm) {
return isVmRunning(vm) && this._set('VCPUs_number_live', value)
},
],
},
cpuCap: {
get: vm => vm.VCPUs_params.cap && +vm.VCPUs_params.cap,
set: (cap, vm) => vm.update_VCPUs_params('cap', String(cap)),
set(cap, vm) {
return this._updateObjectMapProperty(vm, 'VCPUs_params', { cap })
},
},
cpuMask: {
get: vm => vm.VCPUs_params.mask && vm.VCPUs_params.mask.split(','),
set: (cpuMask, vm) =>
vm.update_VCPUs_params(
'mask',
cpuMask == null ? cpuMask : cpuMask.join(',')
),
set(cpuMask, vm) {
return this._updateObjectMapProperty(vm, 'VCPUs_params', {
mask: cpuMask == null ? cpuMask : cpuMask.join(','),
})
},
},
cpusMax: 'cpusStaticMax',
@@ -338,15 +352,15 @@ export default {
cpuWeight: {
get: vm => vm.VCPUs_params.weight && +vm.VCPUs_params.weight,
set: (weight, vm) =>
vm.update_VCPUs_params(
'weight',
weight === null ? null : String(weight)
),
set(weight, vm) {
return this._updateObjectMapProperty(vm, 'VCPUs_params', { weight })
},
},
highAvailability: {
set: (ha, vm) => vm.set_ha_restart_priority(ha),
set(ha, vm) {
return this.call('VM.set_ha_restart_priority', vm.$ref, ha)
},
},
memoryMin: {
@@ -418,12 +432,19 @@ export default {
hasVendorDevice: true,
expNestedHvm: {
set: (expNestedHvm, vm) =>
vm.update_platform('exp-nested-hvm', expNestedHvm ? 'true' : null),
set(expNestedHvm, vm) {
return this._updateObjectMapProperty(vm, 'platform', {
'exp-nested-hvm': expNestedHvm ? 'true' : null,
})
},
},
nicType: {
set: (nicType, vm) => vm.update_platform('nic_type', nicType),
set(nicType, vm) {
return this._updateObjectMapProperty(vm, 'platform', {
nic_type: nicType,
})
},
},
vga: {
@@ -433,7 +454,7 @@ export default {
`The different values that the VGA can take are: ${XEN_VGA_VALUES}`
)
}
return vm.update_platform('vga', vga)
return this._updateObjectMapProperty(vm, 'platform', { vga })
},
},
@@ -444,13 +465,15 @@ export default {
`The different values that the video RAM can take are: ${XEN_VIDEORAM_VALUES}`
)
}
return vm.update_platform('videoram', String(videoram))
return this._updateObjectMapProperty(vm, 'platform', { videoram })
},
},
startDelay: {
get: vm => +vm.start_delay,
set: (startDelay, vm) => vm.set_start_delay(startDelay),
set(startDelay, vm) {
return this.call('VM.set_start_delay', vm.$ref, startDelay)
},
},
}),
@@ -463,7 +486,7 @@ export default {
if (snapshotBefore) {
await this._snapshotVm(snapshot.$snapshot_of)
}
await this.callAsync('VM.revert', snapshot.$ref)
await this.call('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') {
@@ -476,22 +499,15 @@ export default {
async resumeVm(vmId) {
// the force parameter is always true
await this.callAsync('VM.resume', this.getObject(vmId).$ref, false, true)
return this.call('VM.resume', this.getObject(vmId).$ref, false, true)
},
async unpauseVm(vmId) {
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)
return this.call('VM.unpause', this.getObject(vmId).$ref)
},
shutdownVm(vmId, { hard = false } = {}) {
return this.callAsync(
return this.call(
`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, obj) {
return this.setField(obj.$type, obj.$ref, prop, value)
return function(value) {
return this._set(prop, value)
}
}
@@ -157,22 +157,16 @@ export const makeEditObject = specs => {
const index = set.indexOf('.')
if (index === -1) {
const prop = camelToSnakeCase(set)
return function(value, obj) {
return this.setField(obj.$type, obj.$ref, prop, value)
return function(value) {
return this._set(prop, value)
}
}
const field = set.slice(0, index)
const entry = set.slice(index + 1)
const map = set.slice(0, index)
const prop = set.slice(index + 1)
return function(value, object) {
return this.setFieldEntry(
object.$type,
object.$ref,
field,
entry,
value
)
return this._updateObjectMapProperty(object, map, { [prop]: value })
}
}
@@ -255,6 +249,16 @@ 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
@@ -283,7 +287,7 @@ export const makeEditObject = specs => {
}
}
const cb = () => spec.set.call(this, value, object)
const cb = () => spec.set.call(context, value, object)
const { constraints } = spec
if (constraints) {

View File

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

View File

@@ -449,7 +449,9 @@ const disableVmHighAvailability = async (xapi: Xapi, vm: Vm) => {
}
return Promise.all([
vm.set_ha_restart_priority(''),
xapi._setObjectProperties(vm, {
haRestartPriority: '',
}),
xapi.addTag(vm.$ref, 'HA disabled'),
])
}
@@ -935,7 +937,7 @@ export default class BackupNg {
message: 'clean backup metadata on VM',
parentId: taskId,
},
vm.update_other_config({
xapi._updateObjectMapProperty(vm, 'other_config', {
'xo:backup:datetime': null,
'xo:backup:deltaChainLength': null,
'xo:backup:exported': null,
@@ -1049,7 +1051,7 @@ export default class BackupNg {
message: 'add metadata to snapshot',
parentId: taskId,
},
snapshot.update_other_config({
xapi._updateObjectMapProperty(snapshot, 'other_config', {
'xo:backup:datetime': snapshot.snapshot_time,
'xo:backup:job': jobId,
'xo:backup:schedule': scheduleId,
@@ -1256,11 +1258,11 @@ export default class BackupNg {
result: () => ({ size: xva.size }),
},
xapi._importVm($cancelToken, fork, sr, vm =>
vm.set_name_label(
`${metadata.vm.name_label} - ${
xapi._setObjectProperties(vm, {
nameLabel: `${metadata.vm.name_label} - ${
job.name
} - (${safeDateFormat(metadata.timestamp)})`
)
} - (${safeDateFormat(metadata.timestamp)})`,
})
)
)
)
@@ -1268,11 +1270,13 @@ export default class BackupNg {
await Promise.all([
xapi.addTag(vm.$ref, 'Disaster Recovery'),
disableVmHighAvailability(xapi, vm),
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),
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,
}),
])
if (!deleteFirst) {
@@ -1626,11 +1630,13 @@ export default class BackupNg {
await Promise.all([
xapi.addTag(vm.$ref, 'Continuous Replication'),
disableVmHighAvailability(xapi, vm),
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),
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,
}),
])
if (!deleteFirst) {
@@ -1661,7 +1667,9 @@ export default class BackupNg {
message: 'set snapshot.other_config[xo:backup:exported]',
parentId: taskId,
},
snapshot.update_other_config('xo:backup:exported', 'true')
xapi._updateObjectMapProperty(snapshot, 'other_config', {
'xo:backup:exported': 'true',
})
)
}

View File

@@ -456,9 +456,11 @@ export default class {
// (Asynchronously) Identify snapshot as future base.
promise
.then(() =>
srcVm.update_other_config(TAG_LAST_BASE_DELTA, delta.vm.uuid)
)
.then(() => {
return srcXapi._updateObjectMapProperty(srcVm, 'other_config', {
[TAG_LAST_BASE_DELTA]: delta.vm.uuid,
})
})
::ignoreErrors()
return promise
@@ -972,10 +974,10 @@ export default class {
nameLabel: copyName,
})
data.vm.update_blocked_operations(
'start',
'Start operation for this vm is blocked, clone it if you want to use it.'
)
targetXapi._updateObjectMapProperty(data.vm, 'blocked_operations', {
start:
'Start operation for this vm is blocked, clone it if you want to use it.',
})
await targetXapi.addTag(data.vm.$id, 'Disaster Recovery')

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),
{
type: job.type,
}
String(job.runId)
)
return this.updateJob({ id: job.id, runId: null })
})
@@ -266,11 +266,6 @@ export default class Jobs {
reportWhen: (settings && settings.reportWhen) || 'failure',
}
}
if (type === 'metadataBackup') {
data = {
reportWhen: job.settings['']?.reportWhen ?? 'failure',
}
}
const logger = this._logger
const runJobId = logger.notice(`Starting execution of ${id}.`, {
@@ -319,10 +314,7 @@ export default class Jobs {
true
)
app.emit('job:terminated', runJobId, {
type: job.type,
status,
})
app.emit('job:terminated', status, job, schedule, runJobId)
} catch (error) {
await logger.error(
`The execution of ${id} has failed.`,
@@ -333,9 +325,7 @@ export default class Jobs {
},
true
)
app.emit('job:terminated', runJobId, {
type: job.type,
})
app.emit('job:terminated', undefined, job, schedule, runJobId)
throw error
} finally {
this.updateJob({ id, runId: null })::ignoreErrors()

View File

@@ -1,6 +1,7 @@
// @flow
import asyncMap from '@xen-orchestra/async-map'
import createLogger from '@xen-orchestra/log'
import defer from 'golike-defer'
import { fromEvent, ignoreErrors } from 'promise-toolbox'
import debounceWithKey from '../_pDebounceWithKey'
@@ -24,14 +25,9 @@ const METADATA_BACKUP_JOB_TYPE = 'metadataBackup'
const compareTimestamp = (a, b) => a.timestamp - b.timestamp
const DEFAULT_RETENTION = 0
type ReportWhen = 'always' | 'failure' | 'never'
type Settings = {|
reportWhen?: ReportWhen,
retentionPoolMetadata?: number,
retentionXoMetadata?: number,
retentionPoolMetadata?: number,
|}
type MetadataBackupJob = {
@@ -51,22 +47,6 @@ const createSafeReaddir = (handler, methodName) => (path, options) =>
return []
})
const deleteOldBackups = (handler, dir, retention, handleError) =>
handler.list(dir).then(list => {
list.sort()
list = list
.filter(timestamp => /^\d{8}T\d{6}Z$/.test(timestamp))
.slice(0, -retention)
return Promise.all(
list.map(timestamp => {
const backupDir = `${dir}/${timestamp}`
return handler
.rmtree(backupDir)
.catch(error => handleError(error, backupDir))
})
)
}, handleError)
// metadata.json
//
// {
@@ -93,19 +73,6 @@ const deleteOldBackups = (handler, dir, retention, handleError) =>
// └─ <YYYYMMDD>T<HHmmss>
// ├─ metadata.json
// └─ data
//
// Task logs emitted in a metadata backup execution:
//
// job.start(data: { reportWhen: ReportWhen })
// ├─ task.start(data: { type: 'pool', id: string, pool: <Pool />, poolMaster: <Host /> })
// │ ├─ task.start(data: { type: 'remote', id: string })
// │ │ └─ task.end
// │ └─ task.end
// ├─ task.start(data: { type: 'xo' })
// │ ├─ task.start(data: { type: 'remote', id: string })
// │ │ └─ task.end
// │ └─ task.end
// └─ job.end
export default class metadataBackup {
_app: {
createJob: (
@@ -156,293 +123,7 @@ export default class metadataBackup {
})
}
async _backupXo({ handlers, job, logger, retention, runJobId, schedule }) {
const app = this._app
const timestamp = Date.now()
const taskId = logger.notice(`Starting XO metadata backup. (${job.id})`, {
data: {
type: 'xo',
},
event: 'task.start',
parentId: runJobId,
})
try {
const scheduleDir = `${DIR_XO_CONFIG_BACKUPS}/${schedule.id}`
const dir = `${scheduleDir}/${safeDateFormat(timestamp)}`
const data = JSON.stringify(await app.exportConfig(), null, 2)
const fileName = `${dir}/data.json`
const metadata = JSON.stringify(
{
jobId: job.id,
jobName: job.name,
scheduleId: schedule.id,
scheduleName: schedule.name,
timestamp,
},
null,
2
)
const metaDataFileName = `${dir}/metadata.json`
await asyncMap(handlers, async (handler, remoteId) => {
const subTaskId = logger.notice(
`Starting XO metadata backup for the remote (${remoteId}). (${
job.id
})`,
{
data: {
id: remoteId,
type: 'remote',
},
event: 'task.start',
parentId: taskId,
}
)
try {
await Promise.all([
handler.outputFile(fileName, data),
handler.outputFile(metaDataFileName, metadata),
])
await deleteOldBackups(
handler,
scheduleDir,
retention,
(error, backupDir) => {
logger.warning(
backupDir !== undefined
? `unable to delete the folder ${backupDir}`
: `unable to list backups for the remote (${remoteId})`,
{
event: 'task.warning',
taskId: subTaskId,
data: {
error,
},
}
)
}
)
logger.notice(
`Backuping XO metadata for the remote (${remoteId}) is a success. (${
job.id
})`,
{
event: 'task.end',
status: 'success',
taskId: subTaskId,
}
)
} catch (error) {
await handler.rmtree(dir).catch(error => {
logger.warning(`unable to delete the folder ${dir}`, {
event: 'task.warning',
taskId: subTaskId,
data: {
error,
},
})
})
logger.error(
`Backuping XO metadata for the remote (${remoteId}) has failed. (${
job.id
})`,
{
event: 'task.end',
result: serializeError(error),
status: 'failure',
taskId: subTaskId,
}
)
}
})
logger.notice(`Backuping XO metadata is a success. (${job.id})`, {
event: 'task.end',
status: 'success',
taskId,
})
} catch (error) {
logger.error(`Backuping XO metadata has failed. (${job.id})`, {
event: 'task.end',
result: serializeError(error),
status: 'failure',
taskId,
})
}
}
async _backupPool(
poolId,
{ cancelToken, handlers, job, logger, retention, runJobId, schedule, xapi }
) {
const poolMaster = await xapi
.getRecord('host', xapi.pool.master)
::ignoreErrors()
const timestamp = Date.now()
const taskId = logger.notice(
`Starting metadata backup for the pool (${poolId}). (${job.id})`,
{
data: {
id: poolId,
pool: xapi.pool,
poolMaster,
type: 'pool',
},
event: 'task.start',
parentId: runJobId,
}
)
try {
const poolDir = `${DIR_XO_POOL_METADATA_BACKUPS}/${schedule.id}/${poolId}`
const dir = `${poolDir}/${safeDateFormat(timestamp)}`
// TODO: export the metadata only once then split the stream between remotes
const stream = await xapi.exportPoolMetadata(cancelToken)
const fileName = `${dir}/data`
const metadata = JSON.stringify(
{
jobId: job.id,
jobName: job.name,
pool: xapi.pool,
poolMaster,
scheduleId: schedule.id,
scheduleName: schedule.name,
timestamp,
},
null,
2
)
const metaDataFileName = `${dir}/metadata.json`
await asyncMap(handlers, async (handler, remoteId) => {
const subTaskId = logger.notice(
`Starting metadata backup for the pool (${poolId}) for the remote (${remoteId}). (${
job.id
})`,
{
data: {
id: remoteId,
type: 'remote',
},
event: 'task.start',
parentId: taskId,
}
)
let outputStream
try {
await Promise.all([
(async () => {
outputStream = await handler.createOutputStream(fileName)
// 'readable-stream/pipeline' not call the callback when an error throws
// from the readable stream
stream.pipe(outputStream)
return fromEvent(stream, 'end').catch(error => {
if (error.message !== 'aborted') {
throw error
}
})
})(),
handler.outputFile(metaDataFileName, metadata),
])
await deleteOldBackups(
handler,
poolDir,
retention,
(error, backupDir) => {
logger.warning(
backupDir !== undefined
? `unable to delete the folder ${backupDir}`
: `unable to list backups for the remote (${remoteId})`,
{
event: 'task.warning',
taskId: subTaskId,
data: {
error,
},
}
)
}
)
logger.notice(
`Backuping pool metadata (${poolId}) for the remote (${remoteId}) is a success. (${
job.id
})`,
{
event: 'task.end',
status: 'success',
taskId: subTaskId,
}
)
} catch (error) {
if (outputStream !== undefined) {
outputStream.destroy()
}
await handler.rmtree(dir).catch(error => {
logger.warning(`unable to delete the folder ${dir}`, {
event: 'task.warning',
taskId: subTaskId,
data: {
error,
},
})
})
logger.error(
`Backuping pool metadata (${poolId}) for the remote (${remoteId}) has failed. (${
job.id
})`,
{
event: 'task.end',
result: serializeError(error),
status: 'failure',
taskId: subTaskId,
}
)
}
})
logger.notice(
`Backuping pool metadata (${poolId}) is a success. (${job.id})`,
{
event: 'task.end',
status: 'success',
taskId,
}
)
} catch (error) {
logger.error(
`Backuping pool metadata (${poolId}) has failed. (${job.id})`,
{
event: 'task.end',
result: serializeError(error),
status: 'failure',
taskId,
}
)
}
}
async _executor({
cancelToken,
job: job_,
logger,
runJobId,
schedule,
}): Executor {
async _executor({ cancelToken, job: job_, schedule }): Executor {
if (schedule === undefined) {
throw new Error('backup job cannot run without a schedule')
}
@@ -459,103 +140,133 @@ export default class metadataBackup {
throw new Error('no metadata mode found')
}
let { retentionXoMetadata, retentionPoolMetadata } =
job.settings[schedule.id] || {}
const app = this._app
const { retentionXoMetadata, retentionPoolMetadata } =
job?.settings[schedule.id] || {}
// it also replaces null retentions introduced by the commit
// https://github.com/vatesfr/xen-orchestra/commit/fea5117ed83b58d3a57715b32d63d46e3004a094#diff-c02703199db2a4c217943cf8e02b91deR40
if (retentionXoMetadata == null) {
retentionXoMetadata = DEFAULT_RETENTION
}
if (retentionPoolMetadata == null) {
retentionPoolMetadata = DEFAULT_RETENTION
const timestamp = Date.now()
const formattedTimestamp = safeDateFormat(timestamp)
const commonMetadata = {
jobId: job.id,
jobName: job.name,
scheduleId: schedule.id,
scheduleName: schedule.name,
timestamp,
}
if (
(retentionPoolMetadata === 0 && retentionXoMetadata === 0) ||
(!job.xoMetadata && retentionPoolMetadata === 0) ||
(isEmptyPools && retentionXoMetadata === 0)
) {
const files = []
if (job.xoMetadata && retentionXoMetadata > 0) {
const xoMetadataDir = `${DIR_XO_CONFIG_BACKUPS}/${schedule.id}`
const dir = `${xoMetadataDir}/${formattedTimestamp}`
const data = JSON.stringify(await app.exportConfig(), null, 2)
const fileName = `${dir}/data.json`
const metadata = JSON.stringify(commonMetadata, null, 2)
const metaDataFileName = `${dir}/metadata.json`
files.push({
executeBackup: defer(($defer, handler) => {
$defer.onFailure(() => handler.rmtree(dir))
return Promise.all([
handler.outputFile(fileName, data),
handler.outputFile(metaDataFileName, metadata),
])
}),
dir: xoMetadataDir,
retention: retentionXoMetadata,
})
}
if (!isEmptyPools && retentionPoolMetadata > 0) {
files.push(
...(await Promise.all(
poolIds.map(async id => {
const poolMetadataDir = `${DIR_XO_POOL_METADATA_BACKUPS}/${
schedule.id
}/${id}`
const dir = `${poolMetadataDir}/${formattedTimestamp}`
// TODO: export the metadata only once then split the stream between remotes
const stream = await app.getXapi(id).exportPoolMetadata(cancelToken)
const fileName = `${dir}/data`
const xapi = this._app.getXapi(id)
const metadata = JSON.stringify(
{
...commonMetadata,
pool: xapi.pool,
poolMaster: await xapi.getRecord('host', xapi.pool.master),
},
null,
2
)
const metaDataFileName = `${dir}/metadata.json`
return {
executeBackup: defer(($defer, handler) => {
$defer.onFailure(() => handler.rmtree(dir))
return Promise.all([
(async () => {
const outputStream = await handler.createOutputStream(
fileName
)
$defer.onFailure(() => outputStream.destroy())
// 'readable-stream/pipeline' not call the callback when an error throws
// from the readable stream
stream.pipe(outputStream)
return fromEvent(stream, 'end').catch(error => {
if (error.message !== 'aborted') {
throw error
}
})
})(),
handler.outputFile(metaDataFileName, metadata),
])
}),
dir: poolMetadataDir,
retention: retentionPoolMetadata,
}
})
))
)
}
if (files.length === 0) {
throw new Error('no retentions corresponding to the metadata modes found')
}
cancelToken.throwIfRequested()
const app = this._app
const timestampReg = /^\d{8}T\d{6}Z$/
return asyncMap(
// TODO: emit a warning task if a remote is broken
asyncMap(remoteIds, id => app.getRemoteHandler(id)::ignoreErrors()),
async handler => {
if (handler === undefined) {
return
}
const handlers = {}
await Promise.all(
remoteIds.map(id =>
app.getRemoteHandler(id).then(
handler => {
handlers[id] = handler
},
error => {
logger.warning(`unable to get the handler for the remote (${id})`, {
event: 'task.warning',
taskId: runJobId,
data: {
error,
},
await Promise.all(
files.map(async ({ executeBackup, dir, retention }) => {
await executeBackup(handler)
// deleting old backups
await handler.list(dir).then(list => {
list.sort()
list = list
.filter(timestampDir => timestampReg.test(timestampDir))
.slice(0, -retention)
return Promise.all(
list.map(timestampDir =>
handler.rmtree(`${dir}/${timestampDir}`)
)
)
})
}
})
)
)
}
)
if (Object.keys(handlers).length === 0) {
return
}
const promises = []
if (job.xoMetadata && retentionXoMetadata !== 0) {
promises.push(
this._backupXo({
handlers,
job,
logger,
retention: retentionXoMetadata,
runJobId,
schedule,
})
)
}
if (!isEmptyPools && retentionPoolMetadata !== 0) {
poolIds.forEach(id => {
let xapi
try {
xapi = this._app.getXapi(id)
} catch (error) {
logger.warning(
`unable to get the xapi associated to the pool (${id})`,
{
event: 'task.warning',
taskId: runJobId,
data: {
error,
},
}
)
}
if (xapi !== undefined) {
promises.push(
this._backupPool(id, {
cancelToken,
handlers,
job,
logger,
retention: retentionPoolMetadata,
runJobId,
schedule,
xapi,
})
)
}
})
}
return Promise.all(promises)
}
async createMetadataBackupJob(

View File

@@ -67,7 +67,7 @@ export default class {
const handlers = this._handlers
let handler = handlers[id]
if (handler === undefined) {
handler = getHandler(remote, this._remoteOptions)
handler = handlers[id] = getHandler(remote, this._remoteOptions)
try {
await handler.sync()
@@ -76,8 +76,6 @@ export default class {
ignoreErrors.call(this._updateRemote(id, { error: error.message }))
throw error
}
handlers[id] = handler
}
return handler
@@ -170,7 +168,7 @@ export default class {
}
@synchronized()
async _updateRemote(id, { url, ...props }) {
async _updateRemote(id, { benchmarks, url, ...props }) {
const remote = await this._getRemote(id)
// url is handled separately to take care of obfuscated values
@@ -178,6 +176,13 @@ 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,7 +4,6 @@ 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'
@@ -69,12 +68,8 @@ export default class {
)
if (!(await usersDb.exists())) {
const {
email = 'admin@admin.net',
password = 'admin',
} = await XenStore.read('vm-data/admin-account')
.then(JSON.parse)
.catch(() => ({}))
const email = 'admin@admin.net'
const password = 'admin'
await this.createUser({ email, password, permission: 'admin' })
log.info(`Default user created: ${email} with password ${password}`)

View File

@@ -4,7 +4,6 @@ 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'
@@ -65,19 +64,8 @@ export default class {
servers => serversDb.update(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.
const servers = await serversDb.get()
for (const server of servers) {
if (server.enabled) {
this.connectXenServer(server.id).catch(error => {
@@ -386,12 +374,14 @@ export default class {
return value && JSON.parse(value)
},
setData: async (id, key, value) => {
await xapi
.getObject(id)
.update_other_config(
`xo:${camelToSnakeCase(key)}`,
value !== null ? JSON.stringify(value) : value
)
await xapi._updateObjectMapProperty(
xapi.getObject(id),
'other_config',
{
[`xo:${camelToSnakeCase(key)}`]:
value !== null ? JSON.stringify(value) : value,
}
)
// Register the updated object.
addObject(await xapi._waitObject(id))
@@ -455,11 +445,6 @@ 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

@@ -1,6 +1,6 @@
{
"name": "xo-vmdk-to-vhd",
"version": "0.1.7",
"version": "0.1.6",
"license": "AGPL-3.0",
"description": "JS lib streaming a vmdk file to a vhd",
"keywords": [
@@ -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.7.0"
"tmp": "^0.0.33",
"vhd-lib": "^0.6.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": "^8.0.1",
"fs-extra": "^7.0.0",
"get-stream": "^4.0.0",
"index-modules": "^0.3.0",
"rimraf": "^2.6.2"
@@ -50,7 +50,6 @@
"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",
"postversion": "npm publish"
"prepare": "yarn run build"
}
}

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