Compare commits
56 Commits
ci-prepare
...
feat_incre
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4722a62a6a | ||
|
|
1f32557743 | ||
|
|
e95aae2129 | ||
|
|
9176171f20 | ||
|
|
d4f2249a4d | ||
|
|
e0b4069c17 | ||
|
|
6b25a21151 | ||
|
|
716dc45d85 | ||
|
|
57850230c8 | ||
|
|
362d597031 | ||
|
|
e89b84b37b | ||
|
|
ae6f6bf536 | ||
|
|
6f765bdd6f | ||
|
|
1982c6e6e6 | ||
|
|
527dceb43f | ||
|
|
f5a3d68d07 | ||
|
|
6c904fbc96 | ||
|
|
295036a1e3 | ||
|
|
5601d61b49 | ||
|
|
1c35c1a61a | ||
|
|
4143014466 | ||
|
|
90fea69b7e | ||
|
|
625663d619 | ||
|
|
403afc7aaf | ||
|
|
d295524c3c | ||
|
|
5eb4294e70 | ||
|
|
90598522a6 | ||
|
|
519fa1bcf8 | ||
|
|
7b0e5afe37 | ||
|
|
0b6b3a47a2 | ||
|
|
75db810508 | ||
|
|
2f52c564f5 | ||
|
|
011d582b80 | ||
|
|
32d21b2308 | ||
|
|
45971ca622 | ||
|
|
f3a09f2dad | ||
|
|
552a9c7b9f | ||
|
|
ed34d9cbc0 | ||
|
|
187ee99931 | ||
|
|
ff78dd8f7c | ||
|
|
b0eadb8ea4 | ||
|
|
a95754715a | ||
|
|
18ece4b90c | ||
|
|
3862fb2664 | ||
|
|
72c69d791a | ||
|
|
d6192a4a7a | ||
|
|
0f824ffa70 | ||
|
|
f6c227e7f5 | ||
|
|
9d5bc8af6e | ||
|
|
9480079770 | ||
|
|
54fe9147ac | ||
|
|
b6a0477232 | ||
|
|
c60644c578 | ||
|
|
abdce94c5f | ||
|
|
d7dee04013 | ||
|
|
dfc62132b7 |
@@ -14,7 +14,7 @@
|
||||
"url": "https://vates.fr"
|
||||
},
|
||||
"license": "ISC",
|
||||
"version": "0.1.2",
|
||||
"version": "0.1.3",
|
||||
"engines": {
|
||||
"node": ">=8.10"
|
||||
},
|
||||
@@ -25,7 +25,7 @@
|
||||
"dependencies": {
|
||||
"@vates/multi-key-map": "^0.1.0",
|
||||
"@xen-orchestra/async-map": "^0.1.2",
|
||||
"@xen-orchestra/log": "^0.4.0",
|
||||
"@xen-orchestra/log": "^0.5.0",
|
||||
"ensure-array": "^1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
"fuse-native": "^2.2.6",
|
||||
"lru-cache": "^7.14.0",
|
||||
"promise-toolbox": "^0.21.0",
|
||||
"vhd-lib": "^4.1.1"
|
||||
"vhd-lib": "^4.2.0"
|
||||
},
|
||||
"scripts": {
|
||||
"postversion": "npm publish --access public"
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"type": "git",
|
||||
"url": "https://github.com/vatesfr/xen-orchestra.git"
|
||||
},
|
||||
"version": "0.2.1",
|
||||
"version": "0.2.2",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
@@ -17,7 +17,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@vates/decorate-with": "^2.0.0",
|
||||
"@xen-orchestra/log": "^0.4.0",
|
||||
"@xen-orchestra/log": "^0.5.0",
|
||||
"golike-defer": "^0.5.1",
|
||||
"object-hash": "^2.0.1"
|
||||
},
|
||||
|
||||
@@ -5,7 +5,6 @@ const PRESETS_RE = /^@babel\/preset-.+$/
|
||||
|
||||
const NODE_ENV = process.env.NODE_ENV || 'development'
|
||||
const __PROD__ = NODE_ENV === 'production'
|
||||
const __TEST__ = NODE_ENV === 'test'
|
||||
|
||||
const configs = {
|
||||
'@babel/plugin-proposal-decorators': {
|
||||
@@ -15,7 +14,7 @@ const configs = {
|
||||
proposal: 'minimal',
|
||||
},
|
||||
'@babel/preset-env': {
|
||||
debug: !__TEST__,
|
||||
debug: __PROD__,
|
||||
|
||||
// disabled until https://github.com/babel/babel/issues/8323 is resolved
|
||||
// loose: true,
|
||||
|
||||
@@ -7,12 +7,12 @@
|
||||
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
|
||||
"dependencies": {
|
||||
"@xen-orchestra/async-map": "^0.1.2",
|
||||
"@xen-orchestra/backups": "^0.29.0",
|
||||
"@xen-orchestra/fs": "^3.2.0",
|
||||
"@xen-orchestra/backups": "^0.29.1",
|
||||
"@xen-orchestra/fs": "^3.3.0",
|
||||
"filenamify": "^4.1.0",
|
||||
"getopts": "^2.2.5",
|
||||
"lodash": "^4.17.15",
|
||||
"promise-toolbox":"^0.21.0"
|
||||
"promise-toolbox": "^0.21.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
@@ -27,7 +27,7 @@
|
||||
"scripts": {
|
||||
"postversion": "npm publish --access public"
|
||||
},
|
||||
"version": "0.7.8",
|
||||
"version": "1.0.0",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"author": {
|
||||
"name": "Vates SAS",
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"type": "git",
|
||||
"url": "https://github.com/vatesfr/xen-orchestra.git"
|
||||
},
|
||||
"version": "0.29.0",
|
||||
"version": "0.29.1",
|
||||
"engines": {
|
||||
"node": ">=14.6"
|
||||
},
|
||||
@@ -21,13 +21,13 @@
|
||||
"@vates/cached-dns.lookup": "^1.0.0",
|
||||
"@vates/compose": "^2.1.0",
|
||||
"@vates/decorate-with": "^2.0.0",
|
||||
"@vates/disposable": "^0.1.2",
|
||||
"@vates/disposable": "^0.1.3",
|
||||
"@vates/fuse-vhd": "^1.0.0",
|
||||
"@vates/nbd-client": "*",
|
||||
"@vates/parse-duration": "^0.1.1",
|
||||
"@xen-orchestra/async-map": "^0.1.2",
|
||||
"@xen-orchestra/fs": "^3.2.0",
|
||||
"@xen-orchestra/log": "^0.4.0",
|
||||
"@xen-orchestra/fs": "^3.3.0",
|
||||
"@xen-orchestra/log": "^0.5.0",
|
||||
"@xen-orchestra/template": "^0.1.0",
|
||||
"compare-versions": "^5.0.1",
|
||||
"d3-time-format": "^3.0.0",
|
||||
@@ -42,7 +42,7 @@
|
||||
"promise-toolbox": "^0.21.0",
|
||||
"proper-lockfile": "^4.1.2",
|
||||
"uuid": "^9.0.0",
|
||||
"vhd-lib": "^4.1.1",
|
||||
"vhd-lib": "^4.2.0",
|
||||
"yazl": "^2.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -52,7 +52,7 @@
|
||||
"tmp": "^0.2.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@xen-orchestra/xapi": "^1.5.2"
|
||||
"@xen-orchestra/xapi": "^1.5.3"
|
||||
},
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"author": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": false,
|
||||
"name": "@xen-orchestra/fs",
|
||||
"version": "3.2.0",
|
||||
"version": "3.3.0",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"description": "The File System for Xen Orchestra backups.",
|
||||
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/fs",
|
||||
@@ -30,7 +30,7 @@
|
||||
"@vates/decorate-with": "^2.0.0",
|
||||
"@vates/read-chunk": "^1.0.1",
|
||||
"@xen-orchestra/async-map": "^0.1.2",
|
||||
"@xen-orchestra/log": "^0.4.0",
|
||||
"@xen-orchestra/log": "^0.5.0",
|
||||
"bind-property-descriptor": "^2.0.0",
|
||||
"decorator-synchronized": "^0.6.0",
|
||||
"execa": "^5.0.0",
|
||||
|
||||
@@ -297,6 +297,7 @@ export default class RemoteHandlerAbstract {
|
||||
await this._mktree(dirname(newPath))
|
||||
return this.#rename(oldPath, newPath, { checksum }, false)
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
"preview": "vite preview --port 4173",
|
||||
"build-only": "GIT_HEAD=$(git rev-parse HEAD) vite build",
|
||||
"deploy": "./scripts/deploy.sh",
|
||||
"type-check": "vue-tsc --noEmit",
|
||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore"
|
||||
"test": "yarn run type-check",
|
||||
"type-check": "vue-tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^6.1.1",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": false,
|
||||
"name": "@xen-orchestra/log",
|
||||
"version": "0.4.0",
|
||||
"version": "0.5.0",
|
||||
"license": "ISC",
|
||||
"description": "Logging system with decoupled producers/consumer",
|
||||
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/log",
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
"url": "https://vates.fr"
|
||||
},
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"version": "0.8.1",
|
||||
"version": "0.8.2",
|
||||
"engines": {
|
||||
"node": ">=15.6"
|
||||
},
|
||||
@@ -22,7 +22,7 @@
|
||||
"@vates/event-listeners-manager": "^1.0.1",
|
||||
"@vates/parse-duration": "^0.1.1",
|
||||
"@xen-orchestra/emit-async": "^1.0.0",
|
||||
"@xen-orchestra/log": "^0.4.0",
|
||||
"@xen-orchestra/log": "^0.5.0",
|
||||
"acme-client": "^5.0.0",
|
||||
"app-conf": "^2.3.0",
|
||||
"lodash": "^4.17.21",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "@xen-orchestra/proxy",
|
||||
"version": "0.26.4",
|
||||
"version": "0.26.5",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"description": "XO Proxy used to remotely execute backup jobs",
|
||||
"keywords": [
|
||||
@@ -30,15 +30,15 @@
|
||||
"@vates/cached-dns.lookup": "^1.0.0",
|
||||
"@vates/compose": "^2.1.0",
|
||||
"@vates/decorate-with": "^2.0.0",
|
||||
"@vates/disposable": "^0.1.2",
|
||||
"@vates/disposable": "^0.1.3",
|
||||
"@xen-orchestra/async-map": "^0.1.2",
|
||||
"@xen-orchestra/backups": "^0.29.0",
|
||||
"@xen-orchestra/fs": "^3.2.0",
|
||||
"@xen-orchestra/log": "^0.4.0",
|
||||
"@xen-orchestra/backups": "^0.29.1",
|
||||
"@xen-orchestra/fs": "^3.3.0",
|
||||
"@xen-orchestra/log": "^0.5.0",
|
||||
"@xen-orchestra/mixin": "^0.1.0",
|
||||
"@xen-orchestra/mixins": "^0.8.1",
|
||||
"@xen-orchestra/mixins": "^0.8.2",
|
||||
"@xen-orchestra/self-signed": "^0.1.3",
|
||||
"@xen-orchestra/xapi": "^1.5.2",
|
||||
"@xen-orchestra/xapi": "^1.5.3",
|
||||
"ajv": "^8.0.3",
|
||||
"app-conf": "^2.3.0",
|
||||
"async-iterator-to-stream": "^1.1.0",
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
"pw": "^0.0.4",
|
||||
"xdg-basedir": "^4.0.0",
|
||||
"xo-lib": "^0.11.1",
|
||||
"xo-vmdk-to-vhd": "^2.4.3"
|
||||
"xo-vmdk-to-vhd": "^2.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.0.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@xen-orchestra/xapi",
|
||||
"version": "1.5.2",
|
||||
"version": "1.5.3",
|
||||
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/xapi",
|
||||
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
|
||||
"repository": {
|
||||
@@ -23,14 +23,14 @@
|
||||
"dependencies": {
|
||||
"@vates/decorate-with": "^2.0.0",
|
||||
"@xen-orchestra/async-map": "^0.1.2",
|
||||
"@xen-orchestra/log": "^0.4.0",
|
||||
"@xen-orchestra/log": "^0.5.0",
|
||||
"d3-time-format": "^3.0.0",
|
||||
"golike-defer": "^0.5.1",
|
||||
"http-request-plus": "^0.14.0",
|
||||
"json-rpc-protocol": "^0.13.2",
|
||||
"lodash": "^4.17.15",
|
||||
"promise-toolbox": "^0.21.0",
|
||||
"vhd-lib": "^4.1.1",
|
||||
"vhd-lib": "^4.2.0",
|
||||
"xo-common": "^0.8.0"
|
||||
},
|
||||
"private": false,
|
||||
|
||||
54
CHANGELOG.md
54
CHANGELOG.md
@@ -1,8 +1,58 @@
|
||||
# ChangeLog
|
||||
|
||||
## **5.77.0** (2022-11-30)
|
||||
|
||||
<img id="latest" src="https://badgen.net/badge/channel/latest/yellow" alt="Channel: latest" />
|
||||
|
||||
### Highlights
|
||||
|
||||
- [Proxies] Ability to register an existing proxy (PR [#6556](https://github.com/vatesfr/xen-orchestra/pull/6556))
|
||||
- [VM] [Warm migration](https://xen-orchestra.com/blog/warm-migration-with-xen-orchestra/) support (PRs [6549](https://github.com/vatesfr/xen-orchestra/pull/6549) & [6549](https://github.com/vatesfr/xen-orchestra/pull/6549))
|
||||
|
||||
### Enhancements
|
||||
|
||||
- [Remotes] Prevent remote path from ending with `xo-vm-backups` as it's usually a mistake
|
||||
- [OVA export] Speed up OVA generation by 2. Generated file will be bigger (as big as uncompressed XVA) (PR [#6487](https://github.com/vatesfr/xen-orchestra/pull/6487))
|
||||
- [Settings/Users] Add `Remove` button to delete OTP of users from the admin panel [Forum#6521](https://xcp-ng.org/forum/topic/6521/remove-totp-on-a-user-account) (PR [#6541](https://github.com/vatesfr/xen-orchestra/pull/6541))
|
||||
- [Plugin/transport-nagios] XO now reports backed up VMs invidually with the VM name label used as _host_ and backup job name used as _service_
|
||||
- [VM/Advanced] Add warm migration button (PR [#6533](https://github.com/vatesfr/xen-orchestra/pull/6533))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- [Dashboard/Health] Fix `Unknown SR` and `Unknown VDI` in Unhealthy VDIs (PR [#6519](https://github.com/vatesfr/xen-orchestra/pull/6519))
|
||||
- [Delta Backup] Can now recover VHD merge when failed at the begining
|
||||
- [Delta Backup] Fix `ENOENT` errors when merging a VHD directory on non-S3 remote
|
||||
- [Remote] Prevent the browser from auto-completing the encryption key field
|
||||
|
||||
### Released packages
|
||||
|
||||
- @xen-orchestra/log 0.5.0
|
||||
- @vates/disposable 0.1.3
|
||||
- @xen-orchestra/fs 3.3.0
|
||||
- vhd-lib 4.2.0
|
||||
- @xen-orchestra/audit-core 0.2.2
|
||||
- @xen-orchestra/backups 0.29.1
|
||||
- @xen-orchestra/backups-cli 1.0.0
|
||||
- @xen-orchestra/mixins 0.8.2
|
||||
- @xen-orchestra/xapi 1.5.3
|
||||
- @xen-orchestra/proxy 0.26.5
|
||||
- xo-vmdk-to-vhd 2.5.0
|
||||
- xo-cli 0.14.2
|
||||
- xo-server 5.107.1
|
||||
- xo-server-audit 0.10.2
|
||||
- xo-server-auth-ldap 0.10.6
|
||||
- xo-server-backup-reports 0.17.2
|
||||
- xo-server-load-balancer 0.7.2
|
||||
- xo-server-netbox 0.3.5
|
||||
- xo-server-sdn-controller 1.0.7
|
||||
- xo-server-transport-nagios 1.0.0
|
||||
- xo-server-usage-report 0.10.2
|
||||
- xo-server-web-hooks 0.3.2
|
||||
- xo-web 5.108.0
|
||||
|
||||
## **5.76.2** (2022-11-14)
|
||||
|
||||
<img id="latest" src="https://badgen.net/badge/channel/latest/yellow" alt="Channel: latest" />
|
||||
<img id="stable" src="https://badgen.net/badge/channel/stable/green" alt="Channel: stable" />
|
||||
|
||||
### Bug fixes
|
||||
|
||||
@@ -82,8 +132,6 @@
|
||||
|
||||
## **5.75.0** (2022-09-30)
|
||||
|
||||
<img id="stable" src="https://badgen.net/badge/channel/stable/green" alt="Channel: stable" />
|
||||
|
||||
### Enhancements
|
||||
|
||||
- [Backup/Restore file] Implement File level restore for s3 and encrypted backups (PR [#6409](https://github.com/vatesfr/xen-orchestra/pull/6409))
|
||||
|
||||
@@ -7,17 +7,10 @@
|
||||
|
||||
> Users must be able to say: “Nice enhancement, I'm eager to test it”
|
||||
|
||||
- [Remotes] Prevent remote path from ending with `xo-vm-backups` as it's usually a mistake
|
||||
- [OVA export] Speed up OVA generation by 2. Generated file will be bigger (as big as uncompressed XVA) (PR [#6487](https://github.com/vatesfr/xen-orchestra/pull/6487))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
> Users must be able to say: “I had this issue, happy to know it's fixed”
|
||||
|
||||
- [Dashboard/Health] Fix `Unknown SR` and `Unknown VDI` in Unhealthy VDIs (PR [#6519](https://github.com/vatesfr/xen-orchestra/pull/6519))
|
||||
- [Delta Backup] Can now recover VHD merge when failed at the begining
|
||||
- [Delta Backup] Fix `ENOENT` errors when merging a VHD directory on non-S3 remote
|
||||
|
||||
### Packages to release
|
||||
|
||||
> When modifying a package, add it here with its release type.
|
||||
@@ -33,14 +26,4 @@
|
||||
> Keep this list alphabetically ordered to avoid merge conflicts
|
||||
|
||||
<!--packages-start-->
|
||||
|
||||
- @xen-orchestra/backups-cli major
|
||||
- @xen-orchestra/fs minor
|
||||
- @xen-orchestra/log minor
|
||||
- vhd-lib minor
|
||||
- xo-cli patch
|
||||
- xo-server minor
|
||||
- xo-vmdk-to-vhd minor
|
||||
- xo-web minor
|
||||
|
||||
<!--packages-end-->
|
||||
|
||||
12
package.json
12
package.json
@@ -3,10 +3,9 @@
|
||||
"@babel/core": "^7.0.0",
|
||||
"@babel/eslint-parser": "^7.13.8",
|
||||
"@babel/register": "^7.0.0",
|
||||
"@vates/async-each": "1.0.0",
|
||||
"@vates/async-each": "^1.0.0",
|
||||
"babel-jest": "^29.0.3",
|
||||
"benchmark": "^2.1.4",
|
||||
"deptree": "^1.0.0",
|
||||
"eslint": "^8.7.0",
|
||||
"eslint-config-prettier": "^8.1.0",
|
||||
"eslint-config-standard": "^17.0.0",
|
||||
@@ -74,7 +73,7 @@
|
||||
"scripts/run-changed-pkgs.js test",
|
||||
"prettier --ignore-unknown --write"
|
||||
],
|
||||
"*.{{,c,m}j,t}s{,x}": [
|
||||
"*.{{{,c,m}j,t}s{,x},vue}": [
|
||||
"eslint --ignore-pattern '!*'",
|
||||
"jest --testRegex='^(?!.*.integ.spec.js$).*.spec.js$' --findRelatedTests --passWithNoTests"
|
||||
]
|
||||
@@ -82,9 +81,9 @@
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "scripts/run-script.js --parallel --concurrency 2 build",
|
||||
"ci": "yarn && scripts/run-script.js --parallel prepare && yarn test-lint && yarn test-integration",
|
||||
"ci": "yarn && yarn build && yarn test-lint && yarn test-integration",
|
||||
"clean": "scripts/run-script.js --parallel clean",
|
||||
"dev": "scripts/run-script.js --parallel dev",
|
||||
"dev": "scripts/run-script.js --parallel --concurrency 0 --verbose dev",
|
||||
"dev-test": "jest --bail --watch \"^(?!.*\\.integ\\.spec\\.js$)\"",
|
||||
"docs:dev": "vuepress dev docs",
|
||||
"docs:build": "vuepress build docs",
|
||||
@@ -93,8 +92,7 @@
|
||||
"test": "npm run test-lint && npm run test-unit",
|
||||
"test-integration": "jest \".integ\\.spec\\.js$\"",
|
||||
"test-lint": "eslint --ignore-path .gitignore --ignore-pattern packages/xo-web .",
|
||||
"test-unit": "jest \"^(?!.*\\.integ\\.spec\\.js$)\" && scripts/run-script.js test",
|
||||
"travis-tests": "scripts/travis-tests.js"
|
||||
"test-unit": "jest \"^(?!.*\\.integ\\.spec\\.js$)\" && scripts/run-script.js --bail test"
|
||||
},
|
||||
"workspaces": [
|
||||
"@*/*",
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
"node": ">=10"
|
||||
},
|
||||
"dependencies": {
|
||||
"@xen-orchestra/fs": "^3.2.0",
|
||||
"@xen-orchestra/fs": "^3.3.0",
|
||||
"cli-progress": "^3.1.0",
|
||||
"exec-promise": "^0.7.0",
|
||||
"getopts": "^2.2.3",
|
||||
@@ -31,7 +31,7 @@
|
||||
"lodash": "^4.17.21",
|
||||
"promise-toolbox": "^0.21.0",
|
||||
"uuid": "^9.0.0",
|
||||
"vhd-lib": "^4.1.1"
|
||||
"vhd-lib": "^4.2.0"
|
||||
},
|
||||
"scripts": {
|
||||
"postversion": "npm publish"
|
||||
|
||||
@@ -6,10 +6,10 @@ const fs = require('fs-extra')
|
||||
const rimraf = require('rimraf')
|
||||
const tmp = require('tmp')
|
||||
const { getSyncedHandler } = require('@xen-orchestra/fs')
|
||||
const { pFromCallback } = require('promise-toolbox')
|
||||
const { pFromCallback, Disposable } = require('promise-toolbox')
|
||||
|
||||
const { VhdFile, chainVhd } = require('./index')
|
||||
const { _cleanupVhds: cleanupVhds, mergeVhdChain } = require('./merge')
|
||||
const { VhdFile, chainVhd, openVhd, VhdAbstract } = require('./index')
|
||||
const { mergeVhdChain } = require('./merge')
|
||||
|
||||
const { checkFile, createRandomFile, convertFromRawToVhd } = require('./tests/utils')
|
||||
|
||||
@@ -163,6 +163,87 @@ test('it can resume a simple merge ', async () => {
|
||||
}
|
||||
})
|
||||
|
||||
test('it can resume a failed renaming', async () => {
|
||||
const mbOfFather = 8
|
||||
const mbOfChildren = 4
|
||||
const parentRandomFileName = `${tempDir}/randomfile`
|
||||
|
||||
const parentName = 'parentvhd.alias.vhd'
|
||||
const childName = 'childvhd.alias.vhd'
|
||||
|
||||
await createRandomFile(`${tempDir}/randomfile`, mbOfFather)
|
||||
await convertFromRawToVhd(`${tempDir}/randomfile`, `${tempDir}/parentdata.vhd`)
|
||||
VhdAbstract.createAlias(handler, parentName, 'parentdata.vhd')
|
||||
const parentVhd = new VhdFile(handler, 'parentdata.vhd')
|
||||
await parentVhd.readHeaderAndFooter()
|
||||
|
||||
await createRandomFile(`${tempDir}/small_randomfile`, mbOfChildren)
|
||||
await convertFromRawToVhd(`${tempDir}/small_randomfile`, `${tempDir}/childdata.vhd`)
|
||||
await chainVhd(handler, 'parentdata.vhd', handler, 'childdata.vhd', true)
|
||||
VhdAbstract.createAlias(handler, childName, 'childdata.vhd')
|
||||
const childVhd = new VhdFile(handler, 'childdata.vhd')
|
||||
await childVhd.readHeaderAndFooter()
|
||||
|
||||
await handler.writeFile(
|
||||
`.${parentName}.merge.json`,
|
||||
JSON.stringify({
|
||||
parent: {
|
||||
header: parentVhd.header.checksum,
|
||||
},
|
||||
child: {
|
||||
header: childVhd.header.checksum,
|
||||
},
|
||||
step: 'cleanupVhds',
|
||||
})
|
||||
)
|
||||
// expect merge to succeed
|
||||
await mergeVhdChain(handler, [parentName, childName])
|
||||
|
||||
// parent have been renamed
|
||||
expect(await fs.exists(`${tempDir}/${parentName}`)).toBeFalsy()
|
||||
expect(await fs.exists(`${tempDir}/${childName}`)).toBeTruthy()
|
||||
expect(await fs.exists(`${tempDir}/.${parentName}.merge.json`)).toBeFalsy()
|
||||
// we shouldn't have moved the data, but the child data should have been merged into parent
|
||||
expect(await fs.exists(`${tempDir}/parentdata.vhd`)).toBeTruthy()
|
||||
expect(await fs.exists(`${tempDir}/childdata.vhd`)).toBeFalsy()
|
||||
|
||||
Disposable.use(openVhd(handler, childName), async mergedVhd => {
|
||||
await mergedVhd.readBlockAllocationTable()
|
||||
// the resume is at the step 'cleanupVhds' it should not have merged blocks and should still contains parent data
|
||||
|
||||
let offset = 0
|
||||
const fd = await fs.open(parentRandomFileName, 'r')
|
||||
for await (const block of mergedVhd.blocks()) {
|
||||
const blockContent = block.data
|
||||
const buffer = Buffer.alloc(blockContent.length)
|
||||
await fs.read(fd, buffer, 0, buffer.length, offset)
|
||||
expect(buffer.equals(blockContent)).toEqual(true)
|
||||
offset += childVhd.header.blockSize
|
||||
}
|
||||
})
|
||||
|
||||
// merge succeed if renaming was already done
|
||||
await handler.writeFile(
|
||||
`.${parentName}.merge.json`,
|
||||
JSON.stringify({
|
||||
parent: {
|
||||
header: parentVhd.header.checksum,
|
||||
},
|
||||
child: {
|
||||
header: childVhd.header.checksum,
|
||||
},
|
||||
step: 'cleanupVhds',
|
||||
})
|
||||
)
|
||||
await mergeVhdChain(handler, [parentName, childName])
|
||||
expect(await fs.exists(`${tempDir}/${parentName}`)).toBeFalsy()
|
||||
expect(await fs.exists(`${tempDir}/${childName}`)).toBeTruthy()
|
||||
// we shouldn't have moved the data, but the child data should have been merged into parent
|
||||
expect(await fs.exists(`${tempDir}/parentdata.vhd`)).toBeTruthy()
|
||||
expect(await fs.exists(`${tempDir}/childdata.vhd`)).toBeFalsy()
|
||||
expect(await fs.exists(`${tempDir}/.${parentName}.merge.json`)).toBeFalsy()
|
||||
})
|
||||
|
||||
test('it can resume a multiple merge ', async () => {
|
||||
const mbOfFather = 8
|
||||
const mbOfChildren = 6
|
||||
@@ -226,7 +307,11 @@ test('it can resume a multiple merge ', async () => {
|
||||
})
|
||||
)
|
||||
// it should succeed
|
||||
await mergeVhdChain(handler, ['parent.vhd', 'child.vhd', 'grandchild.vhd'])
|
||||
await mergeVhdChain(handler, ['parent.vhd', 'child.vhd', 'grandchild.vhd'], { removeUnused: true })
|
||||
expect(await fs.exists(`${tempDir}/parent.vhd`)).toBeFalsy()
|
||||
expect(await fs.exists(`${tempDir}/child.vhd`)).toBeFalsy()
|
||||
expect(await fs.exists(`${tempDir}/grandchild.vhd`)).toBeTruthy()
|
||||
expect(await fs.exists(`${tempDir}/.parent.vhd.merge.json`)).toBeFalsy()
|
||||
})
|
||||
|
||||
test('it merge multiple child in one pass ', async () => {
|
||||
@@ -278,18 +363,3 @@ test('it merge multiple child in one pass ', async () => {
|
||||
offset += parentVhd.header.blockSize
|
||||
}
|
||||
})
|
||||
|
||||
test('it cleans vhd mergedfiles', async () => {
|
||||
await handler.writeFile('parent', 'parentData')
|
||||
await handler.writeFile('child1', 'child1Data')
|
||||
await handler.writeFile('child2', 'child2Data')
|
||||
await handler.writeFile('child3', 'child3Data')
|
||||
|
||||
await cleanupVhds(handler, ['parent', 'child1', 'child2', 'child3'], { merge: true, removeUnused: true })
|
||||
|
||||
// only child3 should stay, with the data of parent
|
||||
const [child3, ...other] = await handler.list('.')
|
||||
expect(other.length).toEqual(0)
|
||||
expect(child3).toEqual('child3')
|
||||
expect((await handler.readFile('child3')).toString('utf8')).toEqual('parentData')
|
||||
})
|
||||
|
||||
@@ -18,6 +18,7 @@ const { VhdAbstract } = require('./Vhd/VhdAbstract')
|
||||
const { VhdDirectory } = require('./Vhd/VhdDirectory')
|
||||
const { VhdSynthetic } = require('./Vhd/VhdSynthetic')
|
||||
const { asyncMap } = require('@xen-orchestra/async-map')
|
||||
const { isVhdAlias, resolveVhdAlias } = require('./aliases')
|
||||
|
||||
const { warn } = createLogger('vhd-lib:merge')
|
||||
|
||||
@@ -41,91 +42,97 @@ const { warn } = createLogger('vhd-lib:merge')
|
||||
// | |
|
||||
// \_____________rename_____________/
|
||||
|
||||
// write the merge progress file at most every `delay` seconds
|
||||
function makeThrottledWriter(handler, path, delay) {
|
||||
let lastWrite = 0
|
||||
return async json => {
|
||||
class Merger {
|
||||
#chain
|
||||
#childrenPaths
|
||||
#handler
|
||||
#isResuming = false
|
||||
#lastStateWrittenAt = 0
|
||||
#logInfo
|
||||
#mergeBlockConcurrency
|
||||
#onProgress
|
||||
#parentPath
|
||||
#removeUnused
|
||||
#state
|
||||
#statePath
|
||||
|
||||
constructor(handler, chain, { onProgress, logInfo, removeUnused, mergeBlockConcurrency }) {
|
||||
this.#chain = chain
|
||||
this.#handler = handler
|
||||
this.#parentPath = chain[0]
|
||||
this.#childrenPaths = chain.slice(1)
|
||||
this.#logInfo = logInfo
|
||||
this.#onProgress = onProgress
|
||||
this.#removeUnused = removeUnused
|
||||
this.#mergeBlockConcurrency = mergeBlockConcurrency
|
||||
|
||||
this.#statePath = dirname(this.#parentPath) + '/.' + basename(this.#parentPath) + '.merge.json'
|
||||
}
|
||||
|
||||
async #writeState() {
|
||||
await this.#handler.writeFile(this.#statePath, JSON.stringify(this.#state), { flags: 'w' }).catch(warn)
|
||||
}
|
||||
|
||||
async #writeStateThrottled() {
|
||||
const delay = 10e3
|
||||
const now = Date.now()
|
||||
if (now - lastWrite > delay) {
|
||||
lastWrite = now
|
||||
await handler.writeFile(path, JSON.stringify(json), { flags: 'w' }).catch(warn)
|
||||
if (now - this.#lastStateWrittenAt > delay) {
|
||||
this.#lastStateWrittenAt = now
|
||||
await this.#writeState()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// make the rename / delete part of the merge process
|
||||
// will fail if parent and children are in different remote
|
||||
|
||||
async function cleanupVhds(handler, chain, { logInfo = noop, removeUnused = false } = {}) {
|
||||
const parent = chain[0]
|
||||
const children = chain.slice(1, -1)
|
||||
const mergeTargetChild = chain[chain.length - 1]
|
||||
|
||||
await handler.rename(parent, mergeTargetChild)
|
||||
|
||||
return asyncMap(children, child => {
|
||||
logInfo(`the VHD child is already merged`, { child })
|
||||
if (removeUnused) {
|
||||
logInfo(`deleting merged VHD child`, { child })
|
||||
return VhdAbstract.unlink(handler, child)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
module.exports._cleanupVhds = cleanupVhds
|
||||
|
||||
// Merge a chain of VHDs into a single VHD
|
||||
module.exports.mergeVhdChain = limitConcurrency(2)(async function mergeVhdChain(
|
||||
handler,
|
||||
chain,
|
||||
{ onProgress = noop, logInfo = noop, removeUnused = false, mergeBlockConcurrency = 2 } = {}
|
||||
) {
|
||||
assert(chain.length >= 2)
|
||||
|
||||
const parentPath = chain[0]
|
||||
const childrenPaths = chain.slice(1)
|
||||
|
||||
const mergeStatePath = dirname(parentPath) + '/.' + basename(parentPath) + '.merge.json'
|
||||
|
||||
return await Disposable.use(async function* () {
|
||||
let mergeState
|
||||
let isResuming = false
|
||||
async merge() {
|
||||
try {
|
||||
const mergeStateContent = await handler.readFile(mergeStatePath)
|
||||
mergeState = JSON.parse(mergeStateContent)
|
||||
const mergeStateContent = await this.#handler.readFile(this.#statePath)
|
||||
this.#state = JSON.parse(mergeStateContent)
|
||||
|
||||
// work-around a bug introduce in 97d94b795
|
||||
//
|
||||
// currentBlock could be `null` due to the JSON.stringify of a `NaN` value
|
||||
if (mergeState.currentBlock === null) {
|
||||
mergeState.currentBlock = 0
|
||||
if (this.#state.currentBlock === null) {
|
||||
this.#state.currentBlock = 0
|
||||
}
|
||||
this.#isResuming = true
|
||||
} catch (error) {
|
||||
if (error.code !== 'ENOENT') {
|
||||
warn('problem while checking the merge state', { error })
|
||||
}
|
||||
}
|
||||
/* eslint-disable no-fallthrough */
|
||||
switch (this.#state?.step ?? 'mergeBlocks') {
|
||||
case 'mergeBlocks':
|
||||
await this.#step_mergeBlocks()
|
||||
case 'cleanupVhds':
|
||||
await this.#step_cleanVhds()
|
||||
return this.#cleanup()
|
||||
default:
|
||||
warn(`Step ${this.#state.step} is unknown`, { state: this.#state })
|
||||
}
|
||||
/* eslint-enable no-fallthrough */
|
||||
}
|
||||
|
||||
async *#openVhds() {
|
||||
// during merging, the end footer of the parent can be overwritten by new blocks
|
||||
// we should use it as a way to check vhd health
|
||||
const parentVhd = yield openVhd(handler, parentPath, {
|
||||
const parentVhd = yield openVhd(this.#handler, this.#parentPath, {
|
||||
flags: 'r+',
|
||||
checkSecondFooter: mergeState === undefined,
|
||||
checkSecondFooter: this.#state === undefined,
|
||||
})
|
||||
let childVhd
|
||||
const parentIsVhdDirectory = parentVhd instanceof VhdDirectory
|
||||
let childIsVhdDirectory
|
||||
if (childrenPaths.length !== 1) {
|
||||
childVhd = yield VhdSynthetic.open(handler, childrenPaths)
|
||||
if (this.#childrenPaths.length !== 1) {
|
||||
childVhd = yield VhdSynthetic.open(this.#handler, this.#childrenPaths)
|
||||
childIsVhdDirectory = childVhd.checkVhdsClass(VhdDirectory)
|
||||
} else {
|
||||
childVhd = yield openVhd(handler, childrenPaths[0])
|
||||
childVhd = yield openVhd(this.#handler, this.#childrenPaths[0])
|
||||
childIsVhdDirectory = childVhd instanceof VhdDirectory
|
||||
}
|
||||
|
||||
// merging vhdFile must not be concurrently with the potential block reordering after a change
|
||||
const concurrency = parentIsVhdDirectory && childIsVhdDirectory ? mergeBlockConcurrency : 1
|
||||
if (mergeState === undefined) {
|
||||
this.#mergeBlockConcurrency = parentIsVhdDirectory && childIsVhdDirectory ? this.#mergeBlockConcurrency : 1
|
||||
if (this.#state === undefined) {
|
||||
// merge should be along a vhd chain
|
||||
assert.strictEqual(UUID.stringify(childVhd.header.parentUuid), UUID.stringify(parentVhd.footer.uuid))
|
||||
const parentDiskType = parentVhd.footer.diskType
|
||||
@@ -133,70 +140,86 @@ module.exports.mergeVhdChain = limitConcurrency(2)(async function mergeVhdChain(
|
||||
assert.strictEqual(childVhd.footer.diskType, DISK_TYPES.DIFFERENCING)
|
||||
assert.strictEqual(childVhd.header.blockSize, parentVhd.header.blockSize)
|
||||
} else {
|
||||
isResuming = true
|
||||
// vhd should not have changed to resume
|
||||
assert.strictEqual(parentVhd.header.checksum, mergeState.parent.header)
|
||||
assert.strictEqual(childVhd.header.checksum, mergeState.child.header)
|
||||
assert.strictEqual(parentVhd.header.checksum, this.#state.parent.header)
|
||||
assert.strictEqual(childVhd.header.checksum, this.#state.child.header)
|
||||
}
|
||||
|
||||
// Read allocation table of child/parent.
|
||||
await Promise.all([parentVhd.readBlockAllocationTable(), childVhd.readBlockAllocationTable()])
|
||||
|
||||
return { childVhd, parentVhd }
|
||||
}
|
||||
|
||||
async #step_mergeBlocks() {
|
||||
const self = this
|
||||
await Disposable.use(async function* () {
|
||||
const { childVhd, parentVhd } = yield* self.#openVhds()
|
||||
const { maxTableEntries } = childVhd.header
|
||||
|
||||
if (self.#state === undefined) {
|
||||
await parentVhd.ensureBatSize(childVhd.header.maxTableEntries)
|
||||
|
||||
self.#state = {
|
||||
child: { header: childVhd.header.checksum },
|
||||
parent: { header: parentVhd.header.checksum },
|
||||
currentBlock: 0,
|
||||
mergedDataSize: 0,
|
||||
step: 'mergeBlocks',
|
||||
chain: self.#chain.map(vhdPath => handlerPath.relativeFromFile(self.#statePath, vhdPath)),
|
||||
}
|
||||
|
||||
// finds first allocated block for the 2 following loops
|
||||
while (self.#state.currentBlock < maxTableEntries && !childVhd.containsBlock(self.#state.currentBlock)) {
|
||||
++self.#state.currentBlock
|
||||
}
|
||||
await self.#writeState()
|
||||
}
|
||||
await self.#mergeBlocks(parentVhd, childVhd)
|
||||
await self.#updateHeaders(parentVhd, childVhd)
|
||||
})
|
||||
}
|
||||
|
||||
async #mergeBlocks(parentVhd, childVhd) {
|
||||
const { maxTableEntries } = childVhd.header
|
||||
|
||||
if (mergeState === undefined) {
|
||||
await parentVhd.ensureBatSize(childVhd.header.maxTableEntries)
|
||||
|
||||
mergeState = {
|
||||
child: { header: childVhd.header.checksum },
|
||||
parent: { header: parentVhd.header.checksum },
|
||||
currentBlock: 0,
|
||||
mergedDataSize: 0,
|
||||
chain: chain.map(vhdPath => handlerPath.relativeFromFile(mergeStatePath, vhdPath)),
|
||||
}
|
||||
|
||||
// finds first allocated block for the 2 following loops
|
||||
while (mergeState.currentBlock < maxTableEntries && !childVhd.containsBlock(mergeState.currentBlock)) {
|
||||
++mergeState.currentBlock
|
||||
}
|
||||
}
|
||||
|
||||
// counts number of allocated blocks
|
||||
const toMerge = []
|
||||
for (let block = mergeState.currentBlock; block < maxTableEntries; block++) {
|
||||
for (let block = this.#state.currentBlock; block < maxTableEntries; block++) {
|
||||
if (childVhd.containsBlock(block)) {
|
||||
toMerge.push(block)
|
||||
}
|
||||
}
|
||||
const nBlocks = toMerge.length
|
||||
onProgress({ total: nBlocks, done: 0 })
|
||||
this.#onProgress({ total: nBlocks, done: 0 })
|
||||
|
||||
const merging = new Set()
|
||||
let counter = 0
|
||||
|
||||
const mergeStateWriter = makeThrottledWriter(handler, mergeStatePath, 10e3)
|
||||
await mergeStateWriter(mergeState)
|
||||
await asyncEach(
|
||||
toMerge,
|
||||
async blockId => {
|
||||
merging.add(blockId)
|
||||
mergeState.mergedDataSize += await parentVhd.mergeBlock(childVhd, blockId, isResuming)
|
||||
this.#state.mergedDataSize += await parentVhd.mergeBlock(childVhd, blockId, this.#isResuming)
|
||||
|
||||
mergeState.currentBlock = Math.min(...merging)
|
||||
this.#state.currentBlock = Math.min(...merging)
|
||||
merging.delete(blockId)
|
||||
|
||||
onProgress({
|
||||
this.#onProgress({
|
||||
total: nBlocks,
|
||||
done: counter + 1,
|
||||
})
|
||||
counter++
|
||||
mergeStateWriter(mergeState)
|
||||
this.#writeStateThrottled()
|
||||
},
|
||||
{
|
||||
concurrency,
|
||||
concurrency: this.#mergeBlockConcurrency,
|
||||
}
|
||||
)
|
||||
onProgress({ total: nBlocks, done: nBlocks })
|
||||
// ensure data size is correct
|
||||
await this.#writeState()
|
||||
this.#onProgress({ total: nBlocks, done: nBlocks })
|
||||
}
|
||||
|
||||
async #updateHeaders(parentVhd, childVhd) {
|
||||
// some blocks could have been created or moved in parent : write bat
|
||||
await parentVhd.writeBlockAllocationTable()
|
||||
|
||||
@@ -212,19 +235,78 @@ module.exports.mergeVhdChain = limitConcurrency(2)(async function mergeVhdChain(
|
||||
// necessary to update values and to recreate the footer after block
|
||||
// creation
|
||||
await parentVhd.writeFooter()
|
||||
}
|
||||
|
||||
await cleanupVhds(handler, chain, { logInfo, removeUnused })
|
||||
// make the rename / delete part of the merge process
|
||||
// will fail if parent and children are in different remote
|
||||
async #step_cleanVhds() {
|
||||
assert.notEqual(this.#state, undefined)
|
||||
this.#state.step = 'cleanupVhds'
|
||||
await this.#writeState()
|
||||
|
||||
// should be a disposable
|
||||
handler.unlink(mergeStatePath).catch(warn)
|
||||
const chain = this.#chain
|
||||
const handler = this.#handler
|
||||
|
||||
return mergeState.mergedDataSize
|
||||
}).catch(error => {
|
||||
const parent = chain[0]
|
||||
const children = chain.slice(1, -1)
|
||||
const mergeTargetChild = chain[chain.length - 1]
|
||||
|
||||
// in the case is an alias, renaming parent to mergeTargetChild will keep the real data
|
||||
// of mergeTargetChild in the data folder
|
||||
// mergeTargetChild is already in an incomplete state, its blocks have been transferred to parent
|
||||
let oldTarget
|
||||
if (isVhdAlias(mergeTargetChild)) {
|
||||
oldTarget = await resolveVhdAlias(handler, mergeTargetChild)
|
||||
}
|
||||
|
||||
try {
|
||||
await handler.rename(parent, mergeTargetChild)
|
||||
if (oldTarget !== undefined) {
|
||||
await VhdAbstract.unlink(handler, oldTarget).catch(warn)
|
||||
}
|
||||
} catch (error) {
|
||||
// maybe the renaming was already successfull during merge
|
||||
if (error.code === 'ENOENT' && this.#isResuming) {
|
||||
Disposable.use(openVhd(handler, mergeTargetChild), vhd => {
|
||||
// we are sure that mergeTargetChild is the right one
|
||||
assert.strictEqual(vhd.header.checksum, this.#state.parent.header)
|
||||
})
|
||||
this.#logInfo(`the VHD parent was already renamed`, { parent, mergeTargetChild })
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
await asyncMap(children, child => {
|
||||
this.#logInfo(`the VHD child is already merged`, { child })
|
||||
if (this.#removeUnused) {
|
||||
this.#logInfo(`deleting merged VHD child`, { child })
|
||||
return VhdAbstract.unlink(handler, child)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async #cleanup() {
|
||||
const mergedSize = this.#state?.mergedDataSize ?? 0
|
||||
await this.#handler.unlink(this.#statePath).catch(warn)
|
||||
return mergedSize
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.mergeVhdChain = limitConcurrency(2)(async function mergeVhdChain(
|
||||
handler,
|
||||
chain,
|
||||
{ onProgress = noop, logInfo = noop, removeUnused = false, mergeBlockConcurrency = 2 } = {}
|
||||
) {
|
||||
const merger = new Merger(handler, chain, { onProgress, logInfo, removeUnused, mergeBlockConcurrency })
|
||||
try {
|
||||
return merger.merge()
|
||||
} catch (error) {
|
||||
try {
|
||||
error.chain = chain
|
||||
} finally {
|
||||
// eslint-disable-next-line no-unsafe-finally
|
||||
throw error
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": false,
|
||||
"name": "vhd-lib",
|
||||
"version": "4.1.1",
|
||||
"version": "4.2.0",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"description": "Primitives for VHD file handling",
|
||||
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/packages/vhd-lib",
|
||||
@@ -18,8 +18,8 @@
|
||||
"@vates/async-each": "^1.0.0",
|
||||
"@vates/read-chunk": "^1.0.1",
|
||||
"@xen-orchestra/async-map": "^0.1.2",
|
||||
"@xen-orchestra/fs": "^3.2.0",
|
||||
"@xen-orchestra/log": "^0.4.0",
|
||||
"@xen-orchestra/fs": "^3.3.0",
|
||||
"@xen-orchestra/log": "^0.5.0",
|
||||
"async-iterator-to-stream": "^1.0.2",
|
||||
"decorator-synchronized": "^0.6.0",
|
||||
"fs-extra": "^10.0.0",
|
||||
@@ -31,7 +31,7 @@
|
||||
"uuid": "^9.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@xen-orchestra/fs": "^3.2.0",
|
||||
"@xen-orchestra/fs": "^3.3.0",
|
||||
"execa": "^5.0.0",
|
||||
"get-stream": "^6.0.0",
|
||||
"rimraf": "^3.0.2",
|
||||
|
||||
@@ -8,6 +8,6 @@
|
||||
"promise-toolbox": "^0.19.2",
|
||||
"readable-stream": "^3.1.1",
|
||||
"throttle": "^1.0.3",
|
||||
"vhd-lib": "^4.1.1"
|
||||
"vhd-lib": "^4.2.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": false,
|
||||
"name": "xo-cli",
|
||||
"version": "0.14.1",
|
||||
"version": "0.14.2",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"description": "Basic CLI for Xen-Orchestra",
|
||||
"keywords": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "xo-server-audit",
|
||||
"version": "0.10.1",
|
||||
"version": "0.10.2",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"description": "Audit plugin for XO-Server",
|
||||
"keywords": [
|
||||
@@ -44,9 +44,9 @@
|
||||
"prepublishOnly": "yarn run build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@xen-orchestra/audit-core": "^0.2.1",
|
||||
"@xen-orchestra/audit-core": "^0.2.2",
|
||||
"@xen-orchestra/cron": "^1.0.6",
|
||||
"@xen-orchestra/log": "^0.4.0",
|
||||
"@xen-orchestra/log": "^0.5.0",
|
||||
"async-iterator-to-stream": "^1.1.0",
|
||||
"promise-toolbox": "^0.21.0",
|
||||
"readable-stream": "^4.1.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "xo-server-auth-ldap",
|
||||
"version": "0.10.5",
|
||||
"version": "0.10.6",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"description": "LDAP authentication plugin for XO-Server",
|
||||
"keywords": [
|
||||
@@ -31,7 +31,7 @@
|
||||
"node": ">=12"
|
||||
},
|
||||
"dependencies": {
|
||||
"@xen-orchestra/log": "^0.4.0",
|
||||
"@xen-orchestra/log": "^0.5.0",
|
||||
"ensure-array": "^1.0.0",
|
||||
"exec-promise": "^0.7.0",
|
||||
"inquirer": "^8.0.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "xo-server-backup-reports",
|
||||
"version": "0.17.1",
|
||||
"version": "0.17.2",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"description": "Backup reports plugin for XO-Server",
|
||||
"keywords": [
|
||||
@@ -33,7 +33,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@xen-orchestra/defined": "^0.0.1",
|
||||
"@xen-orchestra/log": "^0.4.0",
|
||||
"@xen-orchestra/log": "^0.5.0",
|
||||
"human-format": "^1.0.0",
|
||||
"lodash": "^4.13.1",
|
||||
"moment-timezone": "^0.5.13"
|
||||
|
||||
@@ -284,8 +284,6 @@ class BackupReportsXoPlugin {
|
||||
getErrorMarkdown(log),
|
||||
]
|
||||
|
||||
const nagiosText = []
|
||||
|
||||
// body
|
||||
for (const status of STATUS) {
|
||||
const tasks = tasksByStatus[status]
|
||||
@@ -310,10 +308,6 @@ class BackupReportsXoPlugin {
|
||||
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) {
|
||||
@@ -335,10 +329,6 @@ class BackupReportsXoPlugin {
|
||||
subject: `[Xen Orchestra] ${log.status} − Metadata backup report for ${log.jobName} ${STATUS_ICON[log.status]}`,
|
||||
markdown: toMarkdown(markdown),
|
||||
success: log.status === 'success',
|
||||
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(' ')}`,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -369,9 +359,6 @@ class BackupReportsXoPlugin {
|
||||
mailReceivers,
|
||||
markdown: toMarkdown(markdown),
|
||||
success: false,
|
||||
nagiosMarkdown: `[Xen Orchestra] [${log.status}] Backup report for ${jobName}${
|
||||
log.result?.message !== undefined ? ` - Error : ${log.result.message}` : ''
|
||||
}`,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -379,7 +366,6 @@ class BackupReportsXoPlugin {
|
||||
const skippedVmsText = []
|
||||
const successfulVmsText = []
|
||||
const interruptedVmsText = []
|
||||
const nagiosText = []
|
||||
|
||||
let globalMergeSize = 0
|
||||
let globalTransferSize = 0
|
||||
@@ -401,16 +387,13 @@ class BackupReportsXoPlugin {
|
||||
if (type === 'SR') {
|
||||
const { name_label: name, uuid } = xo.getObject(id)
|
||||
failedTasksText.push(`### ${name}`, '', `- **UUID**: ${uuid}`)
|
||||
nagiosText.push(`[(${type} failed) ${name} : ${taskLog.result.message} ]`)
|
||||
} else {
|
||||
const { name } = await xo.getRemote(id)
|
||||
failedTasksText.push(`### ${name}`, '', `- **UUID**: ${id}`)
|
||||
nagiosText.push(`[(${type} failed) ${name} : ${taskLog.result.message} ]`)
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn(error)
|
||||
failedTasksText.push(`### ${UNKNOWN_ITEM}`, '', `- **UUID**: ${id}`)
|
||||
nagiosText.push(`[(${type} failed) ${id} : ${taskLog.result.message} ]`)
|
||||
}
|
||||
|
||||
failedTasksText.push(
|
||||
@@ -553,22 +536,17 @@ class BackupReportsXoPlugin {
|
||||
: taskLog.result.message
|
||||
}`
|
||||
)
|
||||
nagiosText.push(`[(Skipped) ${vm !== undefined ? vm.name_label : 'undefined'} : ${taskLog.result.message} ]`)
|
||||
} else {
|
||||
++nFailures
|
||||
failedTasksText.push(...text, `- **Error**: ${taskLog.result.message}`)
|
||||
|
||||
nagiosText.push(`[(Failed) ${vm !== undefined ? vm.name_label : 'undefined'} : ${taskLog.result.message} ]`)
|
||||
}
|
||||
} else {
|
||||
if (taskLog.status === 'failure') {
|
||||
++nFailures
|
||||
failedTasksText.push(...text, ...subText)
|
||||
nagiosText.push(`[${vm !== undefined ? vm.name_label : 'undefined'}: (failed)[${failedSubTasks.toString()}]]`)
|
||||
} else if (taskLog.status === 'interrupted') {
|
||||
++nInterrupted
|
||||
interruptedVmsText.push(...text, ...subText)
|
||||
nagiosText.push(`[(Interrupted) ${vm !== undefined ? vm.name_label : 'undefined'}]`)
|
||||
} else {
|
||||
++nSuccesses
|
||||
successfulVmsText.push(...text, ...subText)
|
||||
@@ -614,16 +592,10 @@ class BackupReportsXoPlugin {
|
||||
markdown: toMarkdown(markdown),
|
||||
subject: `[Xen Orchestra] ${log.status} − Backup report for ${jobName} ${STATUS_ICON[log.status]}`,
|
||||
success: log.status === 'success',
|
||||
nagiosMarkdown:
|
||||
log.status === 'success'
|
||||
? `[Xen Orchestra] [Success] Backup report for ${jobName}`
|
||||
: `[Xen Orchestra] [${
|
||||
nFailures !== 0 ? 'Failure' : 'Skipped'
|
||||
}] Backup report for ${jobName} - VMs : ${nagiosText.join(' ')}`,
|
||||
})
|
||||
}
|
||||
|
||||
_sendReport({ mailReceivers, markdown, nagiosMarkdown, subject, success }) {
|
||||
_sendReport({ mailReceivers, markdown, subject, success }) {
|
||||
if (mailReceivers === undefined || mailReceivers.length === 0) {
|
||||
mailReceivers = this._mailsReceivers
|
||||
}
|
||||
@@ -645,11 +617,6 @@ class BackupReportsXoPlugin {
|
||||
xo.sendSlackMessage({
|
||||
message: markdown,
|
||||
}),
|
||||
xo.sendPassiveCheck !== undefined &&
|
||||
xo.sendPassiveCheck({
|
||||
status: success ? 0 : 2,
|
||||
message: nagiosMarkdown,
|
||||
}),
|
||||
xo.sendIcinga2Status !== undefined &&
|
||||
xo.sendIcinga2Status({
|
||||
status: success ? 'OK' : 'CRITICAL',
|
||||
@@ -683,7 +650,6 @@ class BackupReportsXoPlugin {
|
||||
subject: `[Xen Orchestra] ${globalStatus} ${icon}`,
|
||||
markdown,
|
||||
success: false,
|
||||
nagiosMarkdown: `[Xen Orchestra] [${globalStatus}] Error : ${error.message}`,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -720,7 +686,6 @@ class BackupReportsXoPlugin {
|
||||
let nSkipped = 0
|
||||
|
||||
const failedBackupsText = []
|
||||
const nagiosText = []
|
||||
const skippedBackupsText = []
|
||||
const successfulBackupText = []
|
||||
|
||||
@@ -754,13 +719,9 @@ class BackupReportsXoPlugin {
|
||||
`- **Reason**: ${message === UNHEALTHY_VDI_CHAIN_ERROR ? UNHEALTHY_VDI_CHAIN_MESSAGE : message}`,
|
||||
''
|
||||
)
|
||||
|
||||
nagiosText.push(`[(Skipped) ${vm !== undefined ? vm.name_label : 'undefined'} : ${message} ]`)
|
||||
} else {
|
||||
++nFailures
|
||||
failedBackupsText.push(...text, `- **Error**: ${message}`, '')
|
||||
|
||||
nagiosText.push(`[(Failed) ${vm !== undefined ? vm.name_label : 'undefined'} : ${message} ]`)
|
||||
}
|
||||
} else if (!reportOnFailure) {
|
||||
const { returnedValue } = call
|
||||
@@ -835,11 +796,6 @@ class BackupReportsXoPlugin {
|
||||
globalSuccess ? ICON_SUCCESS : nFailures !== 0 ? ICON_FAILURE : ICON_SKIPPED
|
||||
}`,
|
||||
success: globalSuccess,
|
||||
nagiosMarkdown: globalSuccess
|
||||
? `[Xen Orchestra] [Success] Backup report for ${tag}`
|
||||
: `[Xen Orchestra] [${
|
||||
nFailures !== 0 ? 'Failure' : 'Skipped'
|
||||
}] Backup report for ${tag} - VMs : ${nagiosText.join(' ')}`,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "xo-server-load-balancer",
|
||||
"version": "0.7.1",
|
||||
"version": "0.7.2",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"description": "Load balancer for XO-Server",
|
||||
"keywords": [
|
||||
@@ -28,7 +28,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@xen-orchestra/cron": "^1.0.6",
|
||||
"@xen-orchestra/log": "^0.4.0",
|
||||
"@xen-orchestra/log": "^0.5.0",
|
||||
"lodash": "^4.16.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "xo-server-netbox",
|
||||
"version": "0.3.4",
|
||||
"version": "0.3.5",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"description": "Synchronizes pools managed by Xen Orchestra with Netbox",
|
||||
"keywords": [
|
||||
@@ -29,7 +29,7 @@
|
||||
"node": ">=14.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@xen-orchestra/log": "^0.4.0",
|
||||
"@xen-orchestra/log": "^0.5.0",
|
||||
"ipaddr.js": "^2.0.1",
|
||||
"lodash": "^4.17.21",
|
||||
"semver": "^7.3.5"
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"predev": "yarn run prebuild",
|
||||
"prepublishOnly": "yarn run build"
|
||||
},
|
||||
"version": "1.0.6",
|
||||
"version": "1.0.7",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
@@ -28,7 +28,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@vates/coalesce-calls": "^0.1.0",
|
||||
"@xen-orchestra/log": "^0.4.0",
|
||||
"@xen-orchestra/log": "^0.5.0",
|
||||
"@xen-orchestra/openflow": "^0.1.2",
|
||||
"ipaddr.js": "^2.0.1",
|
||||
"lodash": "^4.17.11",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "xo-server-transport-nagios",
|
||||
"version": "0.1.2",
|
||||
"version": "1.0.0",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"description": "Send backup runs statuses to Nagios",
|
||||
"keywords": [
|
||||
@@ -28,7 +28,8 @@
|
||||
"node": ">=6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@xen-orchestra/log": "^0.4.0",
|
||||
"@xen-orchestra/log": "^0.5.0",
|
||||
"@xen-orchestra/template": "^0.1.0",
|
||||
"buffer-crc32": "^0.2.13"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -2,11 +2,24 @@ import crc32 from 'buffer-crc32'
|
||||
import net from 'net'
|
||||
import { Buffer } from 'buffer'
|
||||
import { createLogger } from '@xen-orchestra/log'
|
||||
import { compileTemplate } from '@xen-orchestra/template'
|
||||
|
||||
const { debug, warn } = createLogger('xo:server:transport:nagios')
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const hostDescription = `Host name on Nagios.
|
||||
|
||||
Leave empty if the host name equals the vm name (the default configuration).
|
||||
|
||||
Otherwise, you could choose a custom name but the template \`{vm.name_label}\` must be included. For example: \`xo-backup-{vm.name_label}\`.`
|
||||
|
||||
const serviceDescription = `Service name on Nagios.
|
||||
|
||||
Leave empty if the host name equals the backup job name (the default configuration).
|
||||
|
||||
Otherwise, you could choose a custom name but the template \`{job.name}\` must e included. For example: \`{job.name}-Xen Orchestra\`.`
|
||||
|
||||
export const configurationSchema = {
|
||||
type: 'object',
|
||||
|
||||
@@ -24,16 +37,35 @@ export const configurationSchema = {
|
||||
description: 'The encryption key',
|
||||
},
|
||||
host: {
|
||||
default: '{vm.name_label}',
|
||||
description: hostDescription,
|
||||
type: 'string',
|
||||
description: 'The host name in Nagios',
|
||||
},
|
||||
service: {
|
||||
default: '{job.name}',
|
||||
description: serviceDescription,
|
||||
type: 'string',
|
||||
description: 'The service description in Nagios',
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
required: ['server', 'port', 'key', 'host', 'service'],
|
||||
required: ['server', 'port', 'key'],
|
||||
}
|
||||
|
||||
export const testSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
VmNameLabel: {
|
||||
title: 'VM Name Label',
|
||||
description: 'Name of a VM',
|
||||
type: 'string',
|
||||
},
|
||||
jobName: {
|
||||
title: 'Job Name',
|
||||
description: 'Name of a backup job',
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
required: ['VmNameLabel', 'jobName'],
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
@@ -89,9 +121,17 @@ class XoServerNagios {
|
||||
this._key = null
|
||||
}
|
||||
|
||||
configure(configuration) {
|
||||
configure({ host, service, ...configuration }) {
|
||||
this._conf = configuration
|
||||
this._key = Buffer.from(configuration.key, ENCODING)
|
||||
|
||||
const templateRules = {
|
||||
'{vm.name_label}': vmNameLabel => vmNameLabel,
|
||||
'{job.name}': (vmNameLabel, jobName) => jobName,
|
||||
}
|
||||
|
||||
this._getHost = compileTemplate(host, templateRules)
|
||||
this._getService = compileTemplate(service, templateRules)
|
||||
}
|
||||
|
||||
load() {
|
||||
@@ -102,15 +142,25 @@ class XoServerNagios {
|
||||
this._unset()
|
||||
}
|
||||
|
||||
test() {
|
||||
return this._sendPassiveCheck({
|
||||
message: 'The server-nagios plugin for Xen Orchestra server seems to be working fine, nicely done :)',
|
||||
status: OK,
|
||||
})
|
||||
test({ VmNameLabel, jobName }) {
|
||||
return this._sendPassiveCheck(
|
||||
{
|
||||
message: 'The server-nagios plugin for Xen Orchestra server seems to be working fine, nicely done :)',
|
||||
status: OK,
|
||||
},
|
||||
VmNameLabel,
|
||||
jobName
|
||||
)
|
||||
}
|
||||
|
||||
_sendPassiveCheck({ message, status }) {
|
||||
_sendPassiveCheck({ message, status }, vmNameLabel, jobName) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const conf = {
|
||||
...this._conf,
|
||||
host: this._getHost(vmNameLabel, jobName),
|
||||
service: this._getService(vmNameLabel, jobName),
|
||||
}
|
||||
|
||||
if (/\r|\n/.test(message)) {
|
||||
warn('the message must not contain a line break', { message })
|
||||
for (let i = 0, n = message.length; i < n; ++i) {
|
||||
@@ -125,7 +175,7 @@ class XoServerNagios {
|
||||
|
||||
const client = new net.Socket()
|
||||
|
||||
client.connect(this._conf.port, this._conf.server, () => {
|
||||
client.connect(conf.port, conf.server, () => {
|
||||
debug('Successful connection')
|
||||
})
|
||||
|
||||
@@ -133,7 +183,7 @@ class XoServerNagios {
|
||||
const timestamp = data.readInt32BE(128)
|
||||
const iv = data.slice(0, 128) // initialization vector
|
||||
const packet = nscaPacketBuilder({
|
||||
...this._conf,
|
||||
...conf,
|
||||
iv,
|
||||
message,
|
||||
status,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "xo-server-usage-report",
|
||||
"version": "0.10.1",
|
||||
"version": "0.10.2",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"description": "Report resources usage with their evolution",
|
||||
"keywords": [
|
||||
@@ -31,7 +31,7 @@
|
||||
"dependencies": {
|
||||
"@xen-orchestra/async-map": "^0.1.2",
|
||||
"@xen-orchestra/cron": "^1.0.6",
|
||||
"@xen-orchestra/log": "^0.4.0",
|
||||
"@xen-orchestra/log": "^0.5.0",
|
||||
"csv-stringify": "^6.0.0",
|
||||
"handlebars": "^4.0.6",
|
||||
"html-minifier": "^4.0.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "xo-server-web-hooks",
|
||||
"version": "0.3.1",
|
||||
"version": "0.3.2",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"description": "Sends HTTP requests on XO-Server API calls",
|
||||
"keywords": [
|
||||
@@ -29,7 +29,7 @@
|
||||
"node": ">=8.10"
|
||||
},
|
||||
"dependencies": {
|
||||
"@xen-orchestra/log": "^0.4.0"
|
||||
"@xen-orchestra/log": "^0.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.7.0",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "xo-server",
|
||||
"version": "5.106.1",
|
||||
"version": "5.107.1",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"description": "Server part of Xen-Orchestra",
|
||||
"keywords": [
|
||||
@@ -33,7 +33,7 @@
|
||||
"@vates/cached-dns.lookup": "^1.0.0",
|
||||
"@vates/compose": "^2.1.0",
|
||||
"@vates/decorate-with": "^2.0.0",
|
||||
"@vates/disposable": "^0.1.2",
|
||||
"@vates/disposable": "^0.1.3",
|
||||
"@vates/event-listeners-manager": "^1.0.1",
|
||||
"@vates/otp": "^1.0.0",
|
||||
"@vates/multi-key-map": "^0.1.0",
|
||||
@@ -41,17 +41,17 @@
|
||||
"@vates/predicates": "^1.1.0",
|
||||
"@vates/read-chunk": "^1.0.1",
|
||||
"@xen-orchestra/async-map": "^0.1.2",
|
||||
"@xen-orchestra/backups": "^0.29.0",
|
||||
"@xen-orchestra/backups": "^0.29.1",
|
||||
"@xen-orchestra/cron": "^1.0.6",
|
||||
"@xen-orchestra/defined": "^0.0.1",
|
||||
"@xen-orchestra/emit-async": "^1.0.0",
|
||||
"@xen-orchestra/fs": "^3.2.0",
|
||||
"@xen-orchestra/log": "^0.4.0",
|
||||
"@xen-orchestra/fs": "^3.3.0",
|
||||
"@xen-orchestra/log": "^0.5.0",
|
||||
"@xen-orchestra/mixin": "^0.1.0",
|
||||
"@xen-orchestra/mixins": "^0.8.1",
|
||||
"@xen-orchestra/mixins": "^0.8.2",
|
||||
"@xen-orchestra/self-signed": "^0.1.3",
|
||||
"@xen-orchestra/template": "^0.1.0",
|
||||
"@xen-orchestra/xapi": "^1.5.2",
|
||||
"@xen-orchestra/xapi": "^1.5.3",
|
||||
"ajv": "^8.0.3",
|
||||
"app-conf": "^2.3.0",
|
||||
"async-iterator-to-stream": "^1.0.1",
|
||||
@@ -127,7 +127,7 @@
|
||||
"unzipper": "^0.10.5",
|
||||
"uuid": "^9.0.0",
|
||||
"value-matcher": "^0.2.0",
|
||||
"vhd-lib": "^4.1.1",
|
||||
"vhd-lib": "^4.2.0",
|
||||
"ws": "^8.2.3",
|
||||
"xdg-basedir": "^5.1.0",
|
||||
"xen-api": "^1.2.2",
|
||||
@@ -135,7 +135,7 @@
|
||||
"xo-collection": "^0.5.0",
|
||||
"xo-common": "^0.8.0",
|
||||
"xo-remote-parser": "^0.9.2",
|
||||
"xo-vmdk-to-vhd": "^2.4.3"
|
||||
"xo-vmdk-to-vhd": "^2.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.0.0",
|
||||
|
||||
@@ -1,3 +1,31 @@
|
||||
import humanFormat from 'human-format'
|
||||
import ms from 'ms'
|
||||
import { createLogger } from '@xen-orchestra/log'
|
||||
|
||||
const { warn } = createLogger('xo:server:handleBackupLog')
|
||||
|
||||
async function sendToNagios(app, jobName, vmBackupInfo) {
|
||||
try {
|
||||
const messageToNagios = {
|
||||
id: vmBackupInfo.id,
|
||||
result: vmBackupInfo.result,
|
||||
size: humanFormat.bytes(vmBackupInfo.size),
|
||||
duration: ms(vmBackupInfo.end - vmBackupInfo.start),
|
||||
}
|
||||
|
||||
await app.sendPassiveCheck(
|
||||
{
|
||||
message: JSON.stringify(messageToNagios),
|
||||
status: 0,
|
||||
},
|
||||
app.getObject(messageToNagios.id).name_label,
|
||||
jobName
|
||||
)
|
||||
} catch (error) {
|
||||
warn('sendToNagios:', error)
|
||||
}
|
||||
}
|
||||
|
||||
function forwardResult(log) {
|
||||
if (log.status === 'failure') {
|
||||
throw log.result
|
||||
@@ -6,8 +34,40 @@ function forwardResult(log) {
|
||||
}
|
||||
|
||||
// it records logs generated by `@xen-orchestra/backups/Task#run`
|
||||
export const handleBackupLog = (log, { logger, localTaskIds, rootTaskId, runJobId = rootTaskId, handleRootTaskId }) => {
|
||||
const { event, message, taskId } = log
|
||||
export const handleBackupLog = (
|
||||
log,
|
||||
{ vmBackupInfo, app, jobName, logger, localTaskIds, rootTaskId, runJobId = rootTaskId, handleRootTaskId }
|
||||
) => {
|
||||
const { event, message, parentId, taskId } = log
|
||||
|
||||
if (app !== undefined && jobName !== undefined) {
|
||||
if (event === 'start') {
|
||||
if (log.data?.type === 'VM') {
|
||||
vmBackupInfo.set('vm-' + taskId, {
|
||||
id: log.data.id,
|
||||
start: log.timestamp,
|
||||
})
|
||||
} else if (vmBackupInfo.has('vm-' + parentId) && log.message === 'export') {
|
||||
vmBackupInfo.set('export-' + taskId, {
|
||||
parentId: 'vm-' + parentId,
|
||||
})
|
||||
} else if (vmBackupInfo.has('export-' + parentId) && log.message === 'transfer') {
|
||||
vmBackupInfo.set('transfer-' + taskId, {
|
||||
parentId: 'export-' + parentId,
|
||||
})
|
||||
}
|
||||
} else if (event === 'end') {
|
||||
if (vmBackupInfo.has('vm-' + taskId)) {
|
||||
const data = vmBackupInfo.get('vm-' + taskId)
|
||||
data.result = log.status
|
||||
data.end = log.timestamp
|
||||
sendToNagios(app, jobName, data)
|
||||
} else if (vmBackupInfo.has('transfer-' + taskId)) {
|
||||
vmBackupInfo.get(vmBackupInfo.get(vmBackupInfo.get('transfer-' + taskId).parentId).parentId).size =
|
||||
log.result.size
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If `runJobId` is defined, it means that the root task is already handled by `runJob`
|
||||
if (runJobId !== undefined) {
|
||||
|
||||
@@ -596,6 +596,22 @@ migrate.resolve = {
|
||||
migrationNetwork: ['migrationNetwork', 'network', 'administrate'],
|
||||
}
|
||||
|
||||
export async function warmMigration({ vm, sr, startVm, deleteSource }) {
|
||||
await this.warmMigrateVm(vm, sr, startVm, deleteSource)
|
||||
}
|
||||
warmMigration.permission = 'admin'
|
||||
|
||||
warmMigration.params = {
|
||||
vm: {
|
||||
type: 'string',
|
||||
},
|
||||
sr: {
|
||||
type: 'string',
|
||||
},
|
||||
startDestinationVm: { type: 'boolean' },
|
||||
deleteSourceVm: { type: 'boolean' },
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export const set = defer(async function ($defer, params) {
|
||||
@@ -1168,7 +1184,9 @@ export { export_ as export }
|
||||
async function handleVmImport(req, res, { data, srId, type, xapi }) {
|
||||
// Timeout seems to be broken in Node 4.
|
||||
// See https://github.com/nodejs/node/issues/3319
|
||||
req.setTimeout(43200000) // 12 hours
|
||||
req.setTimeout(24 * 60 * 60 * 1000, () => {
|
||||
log.warn('Import timeout reached', { data, srId, type })
|
||||
}) // 24 hours
|
||||
|
||||
// expect "multipart/form-data; boundary=something"
|
||||
const contentType = req.headers['content-type']
|
||||
|
||||
@@ -149,11 +149,15 @@ export default class BackupNg {
|
||||
try {
|
||||
if (!useXoProxy && backupsConfig.disableWorkers) {
|
||||
const localTaskIds = { __proto__: null }
|
||||
const vmBackupInfo = new Map()
|
||||
return await Task.run(
|
||||
{
|
||||
name: 'backup run',
|
||||
onLog: log =>
|
||||
handleBackupLog(log, {
|
||||
vmBackupInfo,
|
||||
app: this._app,
|
||||
jobName: job.name,
|
||||
localTaskIds,
|
||||
logger,
|
||||
runJobId,
|
||||
@@ -279,8 +283,12 @@ export default class BackupNg {
|
||||
|
||||
const localTaskIds = { __proto__: null }
|
||||
let result
|
||||
const vmBackupInfo = new Map()
|
||||
for await (const log of logsStream) {
|
||||
result = handleBackupLog(log, {
|
||||
vmBackupInfo,
|
||||
app: this._app,
|
||||
jobName: job.name,
|
||||
logger,
|
||||
localTaskIds,
|
||||
runJobId,
|
||||
@@ -296,6 +304,7 @@ export default class BackupNg {
|
||||
}
|
||||
} else {
|
||||
const localTaskIds = { __proto__: null }
|
||||
const vmBackupInfo = new Map()
|
||||
return await runBackupWorker(
|
||||
{
|
||||
config: backupsConfig,
|
||||
@@ -306,6 +315,9 @@ export default class BackupNg {
|
||||
},
|
||||
log =>
|
||||
handleBackupLog(log, {
|
||||
vmBackupInfo,
|
||||
app: this._app,
|
||||
jobName: job.name,
|
||||
logger,
|
||||
localTaskIds,
|
||||
runJobId,
|
||||
|
||||
109
packages/xo-server/src/xo-mixins/migrate-vm.mjs
Normal file
109
packages/xo-server/src/xo-mixins/migrate-vm.mjs
Normal file
@@ -0,0 +1,109 @@
|
||||
import { Backup } from '@xen-orchestra/backups/Backup.js'
|
||||
import { v4 as generateUuid } from 'uuid'
|
||||
|
||||
export default class MigrateVm {
|
||||
constructor(app) {
|
||||
this._app = app
|
||||
}
|
||||
|
||||
// Backup should be reinstentiated each time
|
||||
#createWarmBackup(sourceVmId, srId, jobId) {
|
||||
const app = this._app
|
||||
const config = {
|
||||
snapshotNameLabelTpl: '[XO warm migration {job.name}] {vm.name_label}',
|
||||
}
|
||||
const job = {
|
||||
type: 'backup',
|
||||
id: jobId,
|
||||
mode: 'delta',
|
||||
vms: { id: sourceVmId },
|
||||
name: `Warm migration`,
|
||||
srs: { id: srId },
|
||||
settings: {
|
||||
'': {
|
||||
// mandatory for delta replication writer
|
||||
copyRetention: 1,
|
||||
},
|
||||
},
|
||||
}
|
||||
const schedule = { id: 'one-time' }
|
||||
|
||||
// for now we only support this from the main OA, no proxy
|
||||
return new Backup({
|
||||
config,
|
||||
job,
|
||||
schedule,
|
||||
getAdapter: async remoteId => app.getBackupsRemoteAdapter(await app.getRemoteWithCredentials(remoteId)),
|
||||
|
||||
// `@xen-orchestra/backups/Backup` expect that `getConnectedRecord` returns a promise
|
||||
getConnectedRecord: async (xapiType, uuid) => app.getXapiObject(uuid),
|
||||
})
|
||||
}
|
||||
|
||||
async warmMigrateVm(sourceVmId, srId, startDestVm = true, deleteSource = false) {
|
||||
// we'll use a one time use continuous replication job with the VM to migrate
|
||||
const jobId = generateUuid()
|
||||
const app = this._app
|
||||
const sourceVm = app.getXapiObject(sourceVmId)
|
||||
let backup = this.#createWarmBackup(sourceVmId, srId, jobId)
|
||||
await backup.run()
|
||||
const xapi = sourceVm.$xapi
|
||||
const ref = sourceVm.$ref
|
||||
|
||||
// stop the source VM before
|
||||
try {
|
||||
await xapi.callAsync('VM.clean_shutdown', ref)
|
||||
} catch (error) {
|
||||
await xapi.callAsync('VM.hard_shutdown', ref)
|
||||
}
|
||||
// make it so it can't be restarted by error
|
||||
const message =
|
||||
'This VM has been migrated somewhere else and might not be up to date, check twice before starting it.'
|
||||
await sourceVm.update_blocked_operations({
|
||||
start: message,
|
||||
start_on: message,
|
||||
})
|
||||
|
||||
// run the transfer again to transfer the changed parts
|
||||
// since the source is stopped, there won't be any new change after
|
||||
backup = this.#createWarmBackup(sourceVmId, srId, jobId)
|
||||
await backup.run()
|
||||
// find the destination Vm
|
||||
const targets = Object.keys(
|
||||
app.getObjects({
|
||||
filter: obj => {
|
||||
return (
|
||||
'other' in obj &&
|
||||
obj.other['xo:backup:job'] === jobId &&
|
||||
obj.other['xo:backup:sr'] === srId &&
|
||||
obj.other['xo:backup:vm'] === sourceVm.uuid &&
|
||||
'start' in obj.blockedOperations
|
||||
)
|
||||
},
|
||||
})
|
||||
)
|
||||
if (targets.length === 0) {
|
||||
throw new Error(`Vm target of warm migration not found for ${sourceVmId} on SR ${srId} `)
|
||||
}
|
||||
if (targets.length > 1) {
|
||||
throw new Error(`Multiple target of warm migration found for ${sourceVmId} on SR ${srId} `)
|
||||
}
|
||||
const targetVm = app.getXapiObject(targets[0])
|
||||
|
||||
// new vm is ready to start
|
||||
// delta replication writer as set this as blocked
|
||||
await targetVm.update_blocked_operations({ start: null, start_on: null })
|
||||
|
||||
if (startDestVm) {
|
||||
// boot it
|
||||
await targetVm.$xapi.startVm(targetVm.$ref)
|
||||
// wait for really started
|
||||
// delete source
|
||||
if (deleteSource) {
|
||||
sourceVm.$xapi.VM_destroy(sourceVm.$ref)
|
||||
} else {
|
||||
// @todo should we delete the snapshot if we keep the source vm ?
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import Ajv from 'ajv'
|
||||
import cloneDeep from 'lodash/cloneDeep.js'
|
||||
import mapToArray from 'lodash/map.js'
|
||||
import noop from 'lodash/noop.js'
|
||||
import { createLogger } from '@xen-orchestra/log'
|
||||
@@ -153,22 +154,18 @@ export default class {
|
||||
}
|
||||
|
||||
const validate = this._ajv.compile(configurationSchema)
|
||||
|
||||
// deep clone the configuration to avoid modifying the parameter
|
||||
configuration = cloneDeep(configuration)
|
||||
|
||||
if (!validate(configuration)) {
|
||||
throw invalidParameters(validate.errors)
|
||||
}
|
||||
|
||||
// Sets the plugin configuration.
|
||||
await plugin.instance.configure(
|
||||
{
|
||||
// Shallow copy of the configuration object to avoid most of the
|
||||
// errors when the plugin is altering the configuration object
|
||||
// which is handed over to it.
|
||||
...configuration,
|
||||
},
|
||||
{
|
||||
loaded: plugin.loaded,
|
||||
}
|
||||
)
|
||||
await plugin.instance.configure(configuration, {
|
||||
loaded: plugin.loaded,
|
||||
})
|
||||
plugin.configured = true
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": false,
|
||||
"name": "xo-vmdk-to-vhd",
|
||||
"version": "2.4.3",
|
||||
"version": "2.5.0",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"description": "JS lib reading and writing .vmdk and .ova files",
|
||||
"keywords": [
|
||||
@@ -26,7 +26,7 @@
|
||||
"pako": "^2.0.4",
|
||||
"promise-toolbox": "^0.21.0",
|
||||
"tar-stream": "^2.2.0",
|
||||
"vhd-lib": "^4.1.1",
|
||||
"vhd-lib": "^4.2.0",
|
||||
"xml2js": "^0.4.23"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "xo-web",
|
||||
"version": "5.107.0",
|
||||
"version": "5.108.0",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"description": "Web interface client for Xen-Orchestra",
|
||||
"keywords": [
|
||||
@@ -40,7 +40,7 @@
|
||||
"@nraynaud/novnc": "0.6.1",
|
||||
"@xen-orchestra/cron": "^1.0.6",
|
||||
"@xen-orchestra/defined": "^0.0.1",
|
||||
"@xen-orchestra/log": "^0.4.0",
|
||||
"@xen-orchestra/log": "^0.5.0",
|
||||
"@xen-orchestra/template": "^0.1.0",
|
||||
"ansi_up": "^4.0.3",
|
||||
"asap": "^2.0.6",
|
||||
@@ -138,7 +138,7 @@
|
||||
"xo-common": "^0.8.0",
|
||||
"xo-lib": "^0.11.1",
|
||||
"xo-remote-parser": "^0.9.2",
|
||||
"xo-vmdk-to-vhd": "^2.4.3"
|
||||
"xo-vmdk-to-vhd": "^2.5.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "GIT_HEAD=$(git rev-parse HEAD) NODE_ENV=production gulp build",
|
||||
|
||||
@@ -245,7 +245,7 @@ export default {
|
||||
// Original text: "Add your XCP-ng hosts or pools"
|
||||
homeWelcomeText: 'Añade tus hosts/pools de XenServer',
|
||||
|
||||
// Original text: 'Some XenServers have been registered but are not connected'
|
||||
// Original text: 'Some XCP-ng hosts have been registered but are not connected'
|
||||
homeConnectServerText: undefined,
|
||||
|
||||
// Original text: "Want some help?"
|
||||
|
||||
@@ -246,10 +246,10 @@ export default {
|
||||
homeWelcome: 'Bienvenue sur Xen Orchestra !',
|
||||
|
||||
// Original text: "Add your XCP-ng hosts or pools"
|
||||
homeWelcomeText: 'Ajouter vos serveurs ou pools XenServer',
|
||||
homeWelcomeText: 'Ajouter vos serveurs ou pools XCP-ng',
|
||||
|
||||
// Original text: "Some XenServers have been registered but are not connected"
|
||||
homeConnectServerText: "Des XenServers sont enregistrés mais aucun n'est connecté",
|
||||
// Original text: "Some XCP-ng hosts have been registered but are not connected"
|
||||
homeConnectServerText: "Des hôtes XCP-ng sont enregistrés mais aucun n'est connecté",
|
||||
|
||||
// Original text: "Want some help?"
|
||||
homeHelp: "Besoin d'aide ?",
|
||||
|
||||
@@ -228,10 +228,10 @@ export default {
|
||||
homeWelcome: 'Üdvözöljük a Felhőben!',
|
||||
|
||||
// Original text: "Add your XCP-ng hosts or pools"
|
||||
homeWelcomeText: 'Hozzáadása your XenServer kiszolgálók or pools',
|
||||
homeWelcomeText: 'Hozzáadása your XCP-ng kiszolgálók or pools',
|
||||
|
||||
// Original text: "Some XenServers have been registered but are not connected"
|
||||
homeConnectServerText: 'Some XenServers have been registered but are not Kapcsolódva',
|
||||
// Original text: "Some XCP-ng hosts have been registered but are not connected"
|
||||
homeConnectServerText: 'Some XCP-ng hosts have been registered but are not connected',
|
||||
|
||||
// Original text: "Want some help?"
|
||||
homeHelp: 'Segítségre van szüksége?',
|
||||
|
||||
@@ -516,10 +516,10 @@ export default {
|
||||
homeWelcome: 'Benvenuti in Xen Orchestra!',
|
||||
|
||||
// Original text: 'Add your XCP-ng hosts or pools'
|
||||
homeWelcomeText: 'Aggiungi i tuoi hosts o pools XenServer',
|
||||
homeWelcomeText: 'Aggiungi i tuoi hosts o pools XCP-ng',
|
||||
|
||||
// Original text: 'Some XenServers have been registered but are not connected'
|
||||
homeConnectServerText: 'Alcuni XenServers sono stati registrati ma non sono collegati',
|
||||
// Original text: 'Some XCP-ng hosts have been registered but are not connected'
|
||||
homeConnectServerText: 'Alcuni XCP-ng hosts sono stati registrati ma non sono collegati',
|
||||
|
||||
// Original text: 'Want some help?'
|
||||
homeHelp: 'Vuoi un aiuto?',
|
||||
|
||||
@@ -299,7 +299,7 @@ export default {
|
||||
// Original text: "Add your XCP-ng hosts or pools"
|
||||
homeWelcomeText: 'XenServer sunucu veya havuzunu ekle',
|
||||
|
||||
// Original text: "Some XenServers have been registered but are not connected"
|
||||
// Original text: "Some XCP-ng hosts have been registered but are not connected"
|
||||
homeConnectServerText: "Bazı XenServer'lar kayıtlı ama bağlı değil",
|
||||
|
||||
// Original text: "Want some help?"
|
||||
|
||||
@@ -7,6 +7,7 @@ const messages = {
|
||||
alpha: 'Alpha',
|
||||
creation: 'Creation',
|
||||
description: 'Description',
|
||||
deleteSourceVm: 'Delete source VM',
|
||||
expiration: 'Expiration',
|
||||
keyValue: '{key}: {value}',
|
||||
|
||||
@@ -19,6 +20,7 @@ const messages = {
|
||||
errorUnknownItem: 'Unknown {type}',
|
||||
generateNewMacAddress: 'Generate new MAC addresses',
|
||||
memoryFree: '{memoryFree} RAM free',
|
||||
notConfigured: 'Not configured',
|
||||
utcDate: 'UTC date',
|
||||
utcTime: 'UTC time',
|
||||
date: 'Date',
|
||||
@@ -107,8 +109,10 @@ const messages = {
|
||||
replaceExistingCertificate: 'Replace existing certificate',
|
||||
customFields: 'Custom fields',
|
||||
addCustomField: 'Add custom field',
|
||||
availableXoaPremium: 'Available in XOA Premium',
|
||||
editCustomField: 'Edit custom field',
|
||||
deleteCustomField: 'Delete custom field',
|
||||
onlyAvailableXoaUsers: 'Only available to XOA users',
|
||||
|
||||
// ----- Modals -----
|
||||
alertOk: 'OK',
|
||||
@@ -232,7 +236,7 @@ const messages = {
|
||||
homeFetchingData: 'Fetching data…',
|
||||
homeWelcome: 'Welcome to Xen Orchestra!',
|
||||
homeWelcomeText: 'Add your XCP-ng hosts or pools',
|
||||
homeConnectServerText: 'Some XenServers have been registered but are not connected',
|
||||
homeConnectServerText: 'Some XCP-ng hosts have been registered but are not connected',
|
||||
homeHelp: 'Want some help?',
|
||||
homeAddServer: 'Add server',
|
||||
homeConnectServer: 'Connect servers',
|
||||
@@ -769,8 +773,12 @@ const messages = {
|
||||
cloneVmLabel: 'Clone',
|
||||
cleanVm: 'Clean VM directory',
|
||||
fastCloneVmLabel: 'Fast clone',
|
||||
startMigratedVm: 'Start the migrated VM',
|
||||
vmConsoleLabel: 'Console',
|
||||
vmExportUrlValidity: 'The URL is valid once for a short period of time.',
|
||||
vmWarmMigration: 'Warm migration',
|
||||
vmWarmMigrationProcessInfo:
|
||||
'Warm migration process will first create a copy of the VM on the destination while the source VM is still running, then shutdown the source VM and send the changes that happened during the migration to the destination to minimize downtime.',
|
||||
backupLabel: 'Backup',
|
||||
|
||||
// ----- SR general tab -----
|
||||
@@ -1331,6 +1339,7 @@ const messages = {
|
||||
vmCoresPerSocketExceedsSocketsLimit: 'The selected value exceeds the sockets limit ({maxSockets, number})',
|
||||
vmHaDisabled: 'Disabled',
|
||||
vmMemoryLimitsLabel: 'Memory limits (min/max)',
|
||||
vmUuid: 'VM UUID',
|
||||
vmVgpu: 'vGPU',
|
||||
vmVgpus: 'GPUs',
|
||||
vmVgpuNone: 'None',
|
||||
@@ -2470,7 +2479,6 @@ const messages = {
|
||||
proxyUnknownVm: 'Unknown proxy VM.',
|
||||
|
||||
// ----- proxies -----
|
||||
deployProxyDisabled: 'Only available to XOA users',
|
||||
forgetProxyApplianceTitle: 'Forget prox{n, plural, one {y} other {ies}}',
|
||||
forgetProxyApplianceMessage: 'Are you sure you want to forget {n, number} prox{n, plural, one {y} other {ies}}?',
|
||||
forgetProxies: 'Forget proxy(ies)',
|
||||
@@ -2481,11 +2489,16 @@ const messages = {
|
||||
redeployProxy: 'Redeploy proxy',
|
||||
redeployProxyAction: 'Redeploy this proxy',
|
||||
redeployProxyWarning: 'This action will destroy the old proxy VM',
|
||||
registerProxy: 'Register a proxy',
|
||||
noProxiesAvailable: 'No proxies available',
|
||||
checkProxyHealth: 'Test your proxy',
|
||||
updateProxyApplianceSettings: 'Update appliance settings',
|
||||
urlNotFound: 'URL not found',
|
||||
proxyAuthToken: 'Authentication token',
|
||||
proxyConnectionFailedAfterRegistrationMessage: 'Unable to connect to this proxy. Do you want to forget it?',
|
||||
proxyCopyUrl: 'Copy proxy URL',
|
||||
proxyError: 'Proxy error',
|
||||
proxyOptionalVmUuid: 'VM UUID is optional but recommended.',
|
||||
proxyTestSuccess: 'Test passed for {name}',
|
||||
proxyTestSuccessMessage: 'The proxy appears to work correctly',
|
||||
proxyTestFailed: 'Test failed for {name}',
|
||||
@@ -2504,6 +2517,7 @@ const messages = {
|
||||
'The upgrade will interrupt {nJobs, number} running backup job{nJobs, plural, one {} other {s}}. Do you want to continue?',
|
||||
proxiesNeedUpgrade: 'Some proxies need to be upgraded.',
|
||||
upgradeNeededForProxies: 'Some proxies need to be upgraded. Click here to get more information.',
|
||||
xoProxyConcreteGuide: 'XO Proxy: a concrete guide',
|
||||
|
||||
// ----- Utils -----
|
||||
secondsFormat: '{seconds, plural, one {# second} other {# seconds}}',
|
||||
|
||||
@@ -26,8 +26,10 @@ import invoke from '../invoke'
|
||||
import Icon from '../icon'
|
||||
import logError from '../log-error'
|
||||
import NewAuthTokenModal from './new-auth-token-modal'
|
||||
import RegisterProxyModal from './register-proxy-modal'
|
||||
import renderXoItem, { renderXoItemFromId, Vm } from '../render-xo-item'
|
||||
import store from 'store'
|
||||
import WarmMigrationModal from './warm-migration-modal'
|
||||
import { alert, chooseAction, confirm } from '../modal'
|
||||
import { error, info, success } from '../notification'
|
||||
import { getObject } from 'selectors'
|
||||
@@ -1879,6 +1881,20 @@ export const shareVm = async (vm, resourceSet) =>
|
||||
}),
|
||||
}).then(() => editVm(vm, { share: true }), noop)
|
||||
|
||||
export const vmWarmMigration = async vm => {
|
||||
const { sr, deleteSourceVm, startDestinationVm } = await confirm({
|
||||
body: <WarmMigrationModal />,
|
||||
title: _('vmWarmMigration'),
|
||||
icon: 'vm-warm-migration',
|
||||
})
|
||||
return _call('vm.warmMigration', {
|
||||
deleteSourceVm,
|
||||
sr: resolveId(sr),
|
||||
startDestinationVm,
|
||||
vm: resolveId(vm),
|
||||
})
|
||||
}
|
||||
|
||||
// DISK ---------------------------------------------------------------
|
||||
|
||||
export const createDisk = (name, size, sr, { vm, bootable, mode, position }) =>
|
||||
@@ -2860,9 +2876,9 @@ export const changePassword = (oldPassword, newPassword) =>
|
||||
() => error(_('pwdChangeError'), _('pwdChangeErrorBody'))
|
||||
)
|
||||
|
||||
const _setUserPreferences = preferences =>
|
||||
const _setUserPreferences = (preferences, userId) =>
|
||||
_call('user.set', {
|
||||
id: xo.user.id,
|
||||
id: userId ?? xo.user.id,
|
||||
preferences,
|
||||
})::tap(subscribeCurrentUser.forceRefresh)
|
||||
|
||||
@@ -2923,15 +2939,18 @@ export const addOtp = secret =>
|
||||
noop
|
||||
)
|
||||
|
||||
export const removeOtp = () =>
|
||||
export const removeOtp = user =>
|
||||
confirm({
|
||||
title: _('removeOtpConfirm'),
|
||||
body: _('removeOtpConfirmMessage'),
|
||||
}).then(
|
||||
() =>
|
||||
_setUserPreferences({
|
||||
otp: null,
|
||||
}),
|
||||
_setUserPreferences(
|
||||
{
|
||||
otp: null,
|
||||
},
|
||||
resolveId(user)
|
||||
),
|
||||
noop
|
||||
)
|
||||
|
||||
@@ -3303,6 +3322,37 @@ export const deployProxyAppliance = (license, sr, { network, proxy, ...props } =
|
||||
...props,
|
||||
})::tap(subscribeProxies.forceRefresh)
|
||||
|
||||
export const registerProxy = async () => {
|
||||
const getStringOrUndefined = string => (string.trim() === '' ? undefined : string)
|
||||
|
||||
const { address, authenticationToken, name, vmUuid } = await confirm({
|
||||
body: <RegisterProxyModal />,
|
||||
icon: 'connect',
|
||||
title: _('registerProxy'),
|
||||
})
|
||||
|
||||
const proxyId = await registerProxyApplicance({
|
||||
address: getStringOrUndefined(address),
|
||||
authenticationToken: getStringOrUndefined(authenticationToken),
|
||||
name: getStringOrUndefined(name),
|
||||
vmUuid: getStringOrUndefined(vmUuid),
|
||||
})
|
||||
const _isProxyWorking = await isProxyWorking(proxyId).catch(err => {
|
||||
console.error('isProxyWorking error:', err)
|
||||
return false
|
||||
})
|
||||
if (!_isProxyWorking) {
|
||||
await confirm({
|
||||
body: _('proxyConnectionFailedAfterRegistrationMessage'),
|
||||
title: _('proxyError'),
|
||||
})
|
||||
await forgetProxyAppliances([proxyId])
|
||||
}
|
||||
}
|
||||
|
||||
export const registerProxyApplicance = proxyInfo =>
|
||||
_call('proxy.register', proxyInfo)::tap(subscribeProxies.forceRefresh)
|
||||
|
||||
export const editProxyAppliance = (proxy, { vm, ...props }) =>
|
||||
_call('proxy.update', {
|
||||
id: resolveId(proxy),
|
||||
@@ -3362,6 +3412,8 @@ export const checkProxyHealth = async proxy => {
|
||||
)
|
||||
}
|
||||
|
||||
export const isProxyWorking = async proxy => (await _call('proxy.checkHealth', { id: resolveId(proxy) })).success
|
||||
|
||||
// Audit plugin ---------------------------------------------------------
|
||||
|
||||
const METHOD_NOT_FOUND_CODE = -32601
|
||||
|
||||
68
packages/xo-web/src/common/xo/register-proxy-modal/index.js
Normal file
68
packages/xo-web/src/common/xo/register-proxy-modal/index.js
Normal file
@@ -0,0 +1,68 @@
|
||||
import _ from 'intl'
|
||||
import Component from 'base-component'
|
||||
import Icon from 'icon'
|
||||
import React from 'react'
|
||||
import SingleLineRow from 'single-line-row'
|
||||
import { Col, Container } from 'grid'
|
||||
import { Input as DebounceInput } from 'debounce-input-decorator'
|
||||
|
||||
export default class RegisterProxyModal extends Component {
|
||||
state = {
|
||||
address: '',
|
||||
authenticationToken: '',
|
||||
name: '',
|
||||
vmUuid: '',
|
||||
}
|
||||
get value() {
|
||||
return this.state
|
||||
}
|
||||
|
||||
render() {
|
||||
const { address, authenticationToken, name, vmUuid } = this.state
|
||||
return (
|
||||
<Container>
|
||||
<a href='https://xen-orchestra.com/blog/xo-proxy-a-concrete-guide/' rel='noopener noreferrer'>
|
||||
<Icon icon='info' /> {_('xoProxyConcreteGuide')}
|
||||
</a>
|
||||
<SingleLineRow className='mt-1'>
|
||||
<Col size={6}>{_('proxyAuthToken')}</Col>
|
||||
<Col size={6}>
|
||||
<DebounceInput
|
||||
className='form-control'
|
||||
onChange={this.linkState('authenticationToken')}
|
||||
value={authenticationToken}
|
||||
/>
|
||||
</Col>
|
||||
</SingleLineRow>
|
||||
<SingleLineRow className='mt-1'>
|
||||
<Col size={6}>{_('name')}</Col>
|
||||
<Col size={6}>
|
||||
<DebounceInput className='form-control' onChange={this.linkState('name')} value={name} />
|
||||
</Col>
|
||||
</SingleLineRow>
|
||||
<SingleLineRow className='mt-1'>
|
||||
<Col size={6}>{_('address')}</Col>
|
||||
<Col size={6}>
|
||||
<DebounceInput
|
||||
className='form-control'
|
||||
onChange={this.linkState('address')}
|
||||
placeholder='192.168.2.20[:4343]'
|
||||
value={address}
|
||||
/>
|
||||
</Col>
|
||||
</SingleLineRow>
|
||||
<SingleLineRow className='mt-1'>
|
||||
<Col size={6}>{_('vmUuid')}</Col>
|
||||
<Col size={6}>
|
||||
<DebounceInput className='form-control' onChange={this.linkState('vmUuid')} value={vmUuid} />
|
||||
</Col>
|
||||
</SingleLineRow>
|
||||
<SingleLineRow className='mt-1'>
|
||||
<Col className='text-info'>
|
||||
<Icon icon='info' /> {_('proxyOptionalVmUuid')}
|
||||
</Col>
|
||||
</SingleLineRow>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
}
|
||||
48
packages/xo-web/src/common/xo/warm-migration-modal/index.js
Normal file
48
packages/xo-web/src/common/xo/warm-migration-modal/index.js
Normal file
@@ -0,0 +1,48 @@
|
||||
import _ from 'intl'
|
||||
import Component from 'base-component'
|
||||
import Icon from 'icon'
|
||||
import React from 'react'
|
||||
import SingleLineRow from 'single-line-row'
|
||||
import { Col, Container } from 'grid'
|
||||
import { SelectSr } from 'select-objects'
|
||||
import { Toggle } from 'form'
|
||||
|
||||
export default class WarmMigrationModal extends Component {
|
||||
state = {
|
||||
deleteSourceVm: false,
|
||||
sr: undefined,
|
||||
startDestinationVm: false,
|
||||
}
|
||||
get value() {
|
||||
return this.state
|
||||
}
|
||||
|
||||
render() {
|
||||
const { deleteSourceVm, sr, startDestinationVm } = this.state
|
||||
return (
|
||||
<Container>
|
||||
<div className='text-info'>
|
||||
<Icon icon='info' /> <i>{_('vmWarmMigrationProcessInfo')}</i>
|
||||
</div>
|
||||
<SingleLineRow className='mt-1'>
|
||||
<Col size={6}>{_('destinationSR')}</Col>
|
||||
<Col size={6}>
|
||||
<SelectSr onChange={this.linkState('sr')} value={sr} />
|
||||
</Col>
|
||||
</SingleLineRow>
|
||||
<SingleLineRow className='mt-1'>
|
||||
<Col size={6}>{_('deleteSourceVm')}</Col>
|
||||
<Col size={6}>
|
||||
<Toggle onChange={this.toggleState('deleteSourceVm')} value={deleteSourceVm} />
|
||||
</Col>
|
||||
</SingleLineRow>
|
||||
<SingleLineRow className='mt-1'>
|
||||
<Col size={6}>{_('startMigratedVm')}</Col>
|
||||
<Col size={6}>
|
||||
<Toggle onChange={this.toggleState('startDestinationVm')} value={startDestinationVm} />
|
||||
</Col>
|
||||
</SingleLineRow>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -488,6 +488,10 @@
|
||||
@extend .fa;
|
||||
@extend .fa-list-alt;
|
||||
}
|
||||
&-warm-migration {
|
||||
@extend .fa;
|
||||
@extend .fa-fire;
|
||||
}
|
||||
}
|
||||
|
||||
// Generic states
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
forgetProxyAppliances,
|
||||
getLicenses,
|
||||
getProxyApplianceUpdaterState,
|
||||
registerProxy,
|
||||
subscribeProxies,
|
||||
upgradeProxyAppliance,
|
||||
EXPIRES_SOON_DELAY,
|
||||
@@ -322,10 +323,21 @@ const Proxies = decorate([
|
||||
handler={effects.deployProxy}
|
||||
icon='proxy'
|
||||
size='large'
|
||||
tooltip={state.isFromSource ? _('deployProxyDisabled') : undefined}
|
||||
tooltip={state.isFromSource ? _('onlyAvailableXoaUsers') : undefined}
|
||||
>
|
||||
{_('deployProxy')}
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
className='ml-1'
|
||||
btnStyle='success'
|
||||
disabled={state.isFromSource}
|
||||
handler={registerProxy}
|
||||
icon='connect'
|
||||
size='large'
|
||||
tooltip={state.isFromSource ? _('onlyAvailableXoaUsers') : undefined}
|
||||
>
|
||||
{_('registerProxy')}
|
||||
</ActionButton>
|
||||
</div>
|
||||
<NoObjects
|
||||
actions={ACTIONS}
|
||||
|
||||
@@ -495,6 +495,7 @@ export default decorate([
|
||||
<li>{_('remoteEncryptionBackupSize')}</li>
|
||||
</ul>
|
||||
<input
|
||||
autoComplete='new-password'
|
||||
className='form-control'
|
||||
name='encryptionKey'
|
||||
onChange={effects.linkState}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import * as Editable from 'editable'
|
||||
import _, { messages } from 'intl'
|
||||
import ActionButton from 'action-button'
|
||||
import Button from 'button'
|
||||
import Component from 'base-component'
|
||||
import Icon from 'icon'
|
||||
import isEmpty from 'lodash/isEmpty'
|
||||
import keyBy from 'lodash/keyBy'
|
||||
import map from 'lodash/map'
|
||||
@@ -14,7 +16,7 @@ import { get } from '@xen-orchestra/defined'
|
||||
import { injectIntl } from 'react-intl'
|
||||
import { Password, Select } from 'form'
|
||||
|
||||
import { createUser, deleteUser, deleteUsers, editUser, subscribeGroups, subscribeUsers } from 'xo'
|
||||
import { createUser, deleteUser, deleteUsers, editUser, removeOtp, subscribeGroups, subscribeUsers } from 'xo'
|
||||
|
||||
const permissions = {
|
||||
none: {
|
||||
@@ -78,6 +80,17 @@ const USER_COLUMNS = [
|
||||
itemRenderer: user =>
|
||||
isEmpty(user.authProviders) && <Editable.Password onChange={password => editUser(user, { password })} value='' />,
|
||||
},
|
||||
{
|
||||
name: 'OTP',
|
||||
itemRenderer: user =>
|
||||
user.preferences.otp !== undefined ? (
|
||||
<Button btnStyle='danger' onClick={() => removeOtp(user)} size='small'>
|
||||
<Icon icon='remove' /> {_('remove')}
|
||||
</Button>
|
||||
) : (
|
||||
_('notConfigured')
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
const USER_ACTIONS = [
|
||||
|
||||
@@ -45,11 +45,13 @@ import {
|
||||
subscribeResourceSets,
|
||||
subscribeUsers,
|
||||
suspendVm,
|
||||
vmWarmMigration,
|
||||
XEN_DEFAULT_CPU_CAP,
|
||||
XEN_DEFAULT_CPU_WEIGHT,
|
||||
XEN_VIDEORAM_VALUES,
|
||||
} from 'xo'
|
||||
import { createGetObject, createGetObjectsOfType, createSelector, isAdmin } from 'selectors'
|
||||
import { getXoaPlan, PREMIUM } from 'xoa-plans'
|
||||
import { SelectSuspendSr } from 'select-suspend-sr'
|
||||
|
||||
import BootOrder from './boot-order'
|
||||
@@ -450,6 +452,7 @@ export default class TabAdvanced extends Component {
|
||||
|
||||
render() {
|
||||
const { container, isAdmin, vgpus, vm, vmPool } = this.props
|
||||
const isWarmMigrationAvailable = getXoaPlan().value >= PREMIUM.value
|
||||
return (
|
||||
<Container>
|
||||
<Row>
|
||||
@@ -484,6 +487,15 @@ export default class TabAdvanced extends Component {
|
||||
icon='vm-force-shutdown'
|
||||
labelId='forceShutdownVmLabel'
|
||||
/>
|
||||
<TabButton
|
||||
btnStyle='warning'
|
||||
disabled={!isWarmMigrationAvailable}
|
||||
handler={vmWarmMigration}
|
||||
handlerParam={vm}
|
||||
icon='vm-warm-migration'
|
||||
labelId='vmWarmMigration'
|
||||
tooltip={isWarmMigrationAvailable ? undefined : _('availableXoaPremium')}
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
{vm.power_state === 'Halted' && (
|
||||
|
||||
44
scripts/_computeDepOrder.js
Normal file
44
scripts/_computeDepOrder.js
Normal file
@@ -0,0 +1,44 @@
|
||||
'use strict'
|
||||
|
||||
function addPkgDepsToTree(deps, internalDeps) {
|
||||
if (deps !== undefined) {
|
||||
for (const depName of Object.keys(deps)) {
|
||||
const dep = this.pkgs[depName]
|
||||
if (dep !== undefined) {
|
||||
internalDeps.push(depName)
|
||||
addPkgToTree.call(this, dep)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function addPkgToTree(pkg) {
|
||||
const { name, package: pkgJson } = pkg
|
||||
|
||||
if (!(name in this.tree)) {
|
||||
const internalDeps = (this.tree[name] = [])
|
||||
|
||||
addPkgDepsToTree.call(this, pkgJson.dependencies, internalDeps)
|
||||
addPkgDepsToTree.call(this, pkgJson.devDependencies, internalDeps)
|
||||
addPkgDepsToTree.call(this, pkgJson.optionalDependencies, internalDeps)
|
||||
}
|
||||
}
|
||||
|
||||
function addPkgToResolution(name) {
|
||||
if (!this.resolution.has(name)) {
|
||||
this.tree[name].sort().forEach(addPkgToResolution, this)
|
||||
this.resolution.add(name)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string[]} package names in the order they should be released
|
||||
*/
|
||||
module.exports = function computeDepOrder(pkgsByName) {
|
||||
const tree = { __proto__: null }
|
||||
Object.values(pkgsByName).forEach(addPkgToTree, { pkgs: pkgsByName, tree })
|
||||
|
||||
const resolution = new Set()
|
||||
Object.keys(tree).sort().forEach(addPkgToResolution, { resolution, tree })
|
||||
return Array.from(resolution)
|
||||
}
|
||||
@@ -24,8 +24,7 @@ esac
|
||||
if [ $# -ge 2 ]
|
||||
then
|
||||
npm version "$2"
|
||||
|
||||
git add --patch
|
||||
git checkout HEAD :/yarn.lock
|
||||
fi
|
||||
# if version is not passed, simply update other packages
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#!/usr/bin/env node
|
||||
'use strict'
|
||||
|
||||
const DepTree = require('deptree')
|
||||
const fs = require('fs').promises
|
||||
const joinPath = require('path').join
|
||||
const semver = require('semver')
|
||||
@@ -10,6 +9,8 @@ const escapeRegExp = require('lodash/escapeRegExp')
|
||||
const invert = require('lodash/invert')
|
||||
const keyBy = require('lodash/keyBy')
|
||||
|
||||
const computeDepOrder = require('./_computeDepOrder.js')
|
||||
|
||||
const changelogConfig = {
|
||||
path: joinPath(__dirname, '../CHANGELOG.unreleased.md'),
|
||||
startTag: '<!--packages-start-->',
|
||||
@@ -19,14 +20,6 @@ const changelogConfig = {
|
||||
const RELEASE_WEIGHT = { PATCH: 1, MINOR: 2, MAJOR: 3 }
|
||||
const RELEASE_TYPE = invert(RELEASE_WEIGHT)
|
||||
|
||||
const releaseGraph = { __proto__: null }
|
||||
function addToGraph(name, depName) {
|
||||
const deps = releaseGraph[name] ?? (releaseGraph[name] = [])
|
||||
if (depName !== undefined) {
|
||||
deps.push(depName)
|
||||
}
|
||||
}
|
||||
|
||||
/** @type {Map<string, int>} A mapping of package names to their release weight */
|
||||
const packagesToRelease = new Map()
|
||||
|
||||
@@ -64,6 +57,7 @@ async function main(args, scriptName) {
|
||||
}
|
||||
|
||||
allPackages = keyBy(await getPackages(true), 'name')
|
||||
const releaseOrder = computeDepOrder(allPackages)
|
||||
|
||||
Object.entries(toRelease).forEach(([packageName, releaseType]) => {
|
||||
const rootPackage = allPackages[packageName]
|
||||
@@ -74,7 +68,6 @@ async function main(args, scriptName) {
|
||||
|
||||
const rootReleaseWeight = releaseTypeToWeight(releaseType)
|
||||
registerPackageToRelease(packageName, rootReleaseWeight)
|
||||
addToGraph(rootPackage.name)
|
||||
|
||||
handlePackageDependencies(rootPackage.name, getNextVersion(rootPackage.package.version, rootReleaseWeight))
|
||||
})
|
||||
@@ -82,20 +75,15 @@ async function main(args, scriptName) {
|
||||
const commandsToExecute = ['', 'Commands to execute:', '']
|
||||
const releasedPackages = ['', '### Released packages', '']
|
||||
|
||||
const tree = new DepTree()
|
||||
Object.keys(releaseGraph)
|
||||
.sort()
|
||||
.forEach(name => {
|
||||
tree.add(name, releaseGraph[name])
|
||||
})
|
||||
|
||||
tree.resolve().forEach(dependencyName => {
|
||||
const releaseWeight = packagesToRelease.get(dependencyName)
|
||||
const {
|
||||
package: { version },
|
||||
} = allPackages[dependencyName]
|
||||
commandsToExecute.push(`./scripts/bump-pkg ${dependencyName} ${RELEASE_TYPE[releaseWeight].toLocaleLowerCase()}`)
|
||||
releasedPackages.push(`- ${dependencyName} ${getNextVersion(version, releaseWeight)}`)
|
||||
releaseOrder.forEach(name => {
|
||||
if (packagesToRelease.has(name)) {
|
||||
const releaseWeight = packagesToRelease.get(name)
|
||||
const {
|
||||
package: { version },
|
||||
} = allPackages[name]
|
||||
commandsToExecute.push(`./scripts/bump-pkg ${name} ${RELEASE_TYPE[releaseWeight].toLocaleLowerCase()}`)
|
||||
releasedPackages.push(`- ${name} ${getNextVersion(version, releaseWeight)}`)
|
||||
}
|
||||
})
|
||||
|
||||
console.log(commandsToExecute.join('\n'))
|
||||
@@ -153,7 +141,6 @@ function handlePackageDependencies(packageName, packageNextVersion) {
|
||||
|
||||
if (releaseWeight !== undefined) {
|
||||
registerPackageToRelease(name, releaseWeight)
|
||||
addToGraph(name, packageName)
|
||||
handlePackageDependencies(name, getNextVersion(version, releaseWeight))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,16 +11,44 @@ const { getPackages } = require('./utils')
|
||||
|
||||
const { env } = process
|
||||
|
||||
async function run(command, opts, verbose) {
|
||||
const child = spawn(command, {
|
||||
...opts,
|
||||
shell: true,
|
||||
stdio: verbose ? 'inherit' : 'pipe',
|
||||
})
|
||||
|
||||
const output = []
|
||||
if (!verbose) {
|
||||
function onData(chunk) {
|
||||
output.push(chunk)
|
||||
}
|
||||
child.stderr.on('data', onData)
|
||||
child.stdout.on('data', onData)
|
||||
}
|
||||
|
||||
const code = await fromEvent(child, 'exit')
|
||||
if (code !== 0) {
|
||||
for (const chunk of output) {
|
||||
process.stderr.write(chunk)
|
||||
}
|
||||
|
||||
throw code
|
||||
}
|
||||
}
|
||||
|
||||
// run a script for each package (also run pre and post)
|
||||
//
|
||||
// TODO: https://docs.npmjs.com/misc/scripts#environment
|
||||
require('exec-promise')(args => {
|
||||
const {
|
||||
bail,
|
||||
concurrency,
|
||||
parallel,
|
||||
verbose,
|
||||
_: [script],
|
||||
} = getopts(args, {
|
||||
boolean: ['parallel'],
|
||||
boolean: ['bail', 'parallel', 'verbose'],
|
||||
string: ['concurrency'],
|
||||
})
|
||||
|
||||
@@ -37,15 +65,18 @@ require('exec-promise')(args => {
|
||||
env: Object.assign({}, env, {
|
||||
PATH: `${dir}/node_modules/.bin${delimiter}${env.PATH}`,
|
||||
}),
|
||||
shell: true,
|
||||
stdio: 'inherit',
|
||||
}
|
||||
return forEach.call([`pre${script}`, script, `post${script}`], script => {
|
||||
const command = scripts[script]
|
||||
if (command !== undefined) {
|
||||
console.log(`* ${name}:${script} −`, command)
|
||||
return fromEvent(spawn(command, spawnOpts), 'exit').then(code => {
|
||||
return run(command, spawnOpts, verbose).catch(code => {
|
||||
if (code !== 0) {
|
||||
if (bail) {
|
||||
// eslint-disable-next-line no-throw-literal
|
||||
throw `${name}:${script} − Error: ` + code
|
||||
}
|
||||
|
||||
++errors
|
||||
console.log(`* ${name}:${script} − Error:`, code)
|
||||
}
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
'use strict'
|
||||
|
||||
const { execFileSync, spawnSync } = require('child_process')
|
||||
|
||||
const run = (command, args) => spawnSync(command, args, { stdio: 'inherit' }).status
|
||||
|
||||
const getFiles = () =>
|
||||
execFileSync('git', ['diff-index', '--diff-filter=AM', '--ignore-submodules', '--name-only', 'master'], {
|
||||
encoding: 'utf8',
|
||||
})
|
||||
.split('\n')
|
||||
.filter(_ => _ !== '')
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
// Travis vars : https://docs.travis-ci.com/user/environment-variables#default-environment-variables.
|
||||
if (process.env.TRAVIS_PULL_REQUEST !== 'false') {
|
||||
const files = getFiles().filter(_ => _.endsWith('.cjs') || _.endsWith('.js') || _.endsWith('.mjs'))
|
||||
if (files.length !== 0) {
|
||||
process.exit(run('./node_modules/.bin/jest', ['--findRelatedTests', '--passWithNoTests'].concat(files)))
|
||||
}
|
||||
} else {
|
||||
process.exit(run('yarn', ['test-lint']) + run('yarn', ['test-unit']) + run('yarn', ['test-integration']))
|
||||
}
|
||||
Reference in New Issue
Block a user