Compare commits

..

56 Commits

Author SHA1 Message Date
Florent Beauchamp
4722a62a6a fix(xo-server): increase timeout for ova import 2022-12-08 08:47:49 +01:00
Julien Fontanet
1f32557743 fix(scripts/gen-deps-list): fix packages order (#6564)
The release order computation is now uncoupled of the packages to release computation, and is now done for all packages so that transitive dependencies are still correctly ordered.
2022-11-30 14:52:46 +01:00
Julien Fontanet
e95aae2129 feat: release 5.77.0 2022-11-30 14:05:38 +01:00
Pierre Donias
9176171f20 feat: technical release (#6566) 2022-11-30 11:18:33 +01:00
Florent BEAUCHAMP
d4f2249a4d fix(xo-server/vm.warmMigration): use same job id in subsequent run (#6565)
Introduced by 72c69d7
2022-11-30 11:00:42 +01:00
Julien Fontanet
e0b4069c17 fix(scripts/bump-pkg): don't call git add --patch twice 2022-11-29 18:56:03 +01:00
Julien Fontanet
6b25a21151 feat(scripts/bump-pkg): ignore yarn.lock changes 2022-11-29 18:56:03 +01:00
Julien Fontanet
716dc45d85 chore(CHANGELOG): integrate released changes 2022-11-29 18:56:03 +01:00
Julien Fontanet
57850230c8 feat(xo-web): 5.108.0 2022-11-29 18:47:33 +01:00
Julien Fontanet
362d597031 feat(xo-server-web-hooks): 0.3.2 2022-11-29 18:47:14 +01:00
Julien Fontanet
e89b84b37b feat(xo-server-usage-report): 0.10.2 2022-11-29 18:46:54 +01:00
Julien Fontanet
ae6f6bf536 feat(xo-server-transport-nagios): 1.0.0 2022-11-29 18:46:27 +01:00
Julien Fontanet
6f765bdd6f feat(xo-server-sdn-controller): 1.0.7 2022-11-29 18:45:50 +01:00
Julien Fontanet
1982c6e6e6 feat(xo-server-netbox): 0.3.5 2022-11-29 18:45:30 +01:00
Julien Fontanet
527dceb43f feat(xo-server-load-balancer): 0.7.2 2022-11-29 18:44:12 +01:00
Julien Fontanet
f5a3d68d07 feat(xo-server-backup-reports): 0.17.2 2022-11-29 18:43:50 +01:00
Julien Fontanet
6c904fbc96 feat(xo-server-auth-ldap): 0.10.6 2022-11-29 18:43:22 +01:00
Julien Fontanet
295036a1e3 feat(xo-server-audit): 0.10.2 2022-11-29 18:42:30 +01:00
Julien Fontanet
5601d61b49 feat(xo-server): 5.107.0 2022-11-29 18:32:04 +01:00
Julien Fontanet
1c35c1a61a feat(xo-cli): 0.14.2 2022-11-29 18:31:24 +01:00
Julien Fontanet
4143014466 feat(xo-vmdk-to-vhd): 2.5.0 2022-11-29 18:29:33 +01:00
Julien Fontanet
90fea69b7e feat(@xen-orchestra/proxy): 0.26.5 2022-11-29 18:21:01 +01:00
Julien Fontanet
625663d619 feat(@xen-orchestra/xapi): 1.5.3 2022-11-29 18:18:09 +01:00
Julien Fontanet
403afc7aaf feat(@xen-orchestra/mixins): 0.8.2 2022-11-29 17:50:43 +01:00
Julien Fontanet
d295524c3c feat(@xen-orchestra/backups-cli): 1.0.0 2022-11-29 17:48:21 +01:00
Julien Fontanet
5eb4294e70 feat(@xen-orchestra/backups): 0.29.1 2022-11-29 17:48:21 +01:00
Julien Fontanet
90598522a6 feat(@xen-orchestra/audit-core): 0.2.2 2022-11-29 17:48:21 +01:00
Julien Fontanet
519fa1bcf8 feat(vhd-lib): 4.2.0 2022-11-29 17:48:21 +01:00
Julien Fontanet
7b0e5afe37 feat(@xen-orchestra/fs): 3.3.0 2022-11-29 17:48:21 +01:00
Julien Fontanet
0b6b3a47a2 feat(@vates/disposable): 0.1.3 2022-11-29 17:48:21 +01:00
Julien Fontanet
75db810508 feat(@xen-orchestra/log): 0.5.0 2022-11-29 17:48:21 +01:00
Julien Fontanet
2f52c564f5 chore(backups-cli): format package.json 2022-11-29 17:48:21 +01:00
Florent Beauchamp
011d582b80 fix(vhd-lib/merge): delete old data AFTER the alias has been overwritten 2022-11-29 16:42:57 +01:00
Julien Fontanet
32d21b2308 chore: use caret range for @vates/async-each
Introduced by 08298d328
2022-11-29 16:31:41 +01:00
Pierre Donias
45971ca622 fix(xo-web): remove duplicated imports (#6562) 2022-11-29 16:17:40 +01:00
Mathieu
f3a09f2dad feat(xo-web/VM/advanced): add button for warm migration (#6533)
See #6549
2022-11-29 15:14:41 +01:00
Mathieu
552a9c7b9f feat(xo-web/proxy): register an existing proxy (#6556) 2022-11-29 14:44:51 +01:00
Gabriel Gunullu
ed34d9cbc0 feat(xo-server-transport-nagios): make host and service configurable (#6560) 2022-11-29 14:34:41 +01:00
Julien Fontanet
187ee99931 fix(xo-server/plugin.configure): don't save injected defaults
Default values injected by Ajv from the configuration schema should not be saved.
2022-11-29 12:43:17 +01:00
Cécile Morange
ff78dd8f7c feat(xo-web/i18n): "XenServer" → "XCP-ng" (#6462)
See #6439
2022-11-29 11:47:16 +01:00
Julien Fontanet
b0eadb8ea4 fix: remove concurrency limit for dev script
Introduced by 9d5bc8af6

Limited concurrency (which is the default) is not compatible with never-ending commands.
2022-11-29 11:35:01 +01:00
Julien Fontanet
a95754715a fix: use --verbose for dev script
Introduced by 9d5bc8af6

Silent mode is not compatible (i.e. does not show a meaningful output) with never-ending commands.
2022-11-29 11:14:44 +01:00
Julien Fontanet
18ece4b90c fix(xo-server/MigrateVm): fix uuid import
Introduced by 72c69d791

Fixes #6561
2022-11-29 10:30:09 +01:00
Florent Beauchamp
3862fb2664 fix(fs/rename): throw ENOENT when source file is missing 2022-11-28 17:33:57 +01:00
Florent BEAUCHAMP
72c69d791a feat(xo-server): implement warm migration backend (#6549) 2022-11-28 17:28:19 +01:00
Julien Fontanet
d6192a4a7a chore: remove unused travis-tests.js 2022-11-28 15:51:47 +01:00
Julien Fontanet
0f824ffa70 lint(vhd-lib): remove unused var and fix formatting
Introduced by f6c227e7f
2022-11-26 10:10:08 +01:00
Florent BEAUCHAMP
f6c227e7f5 feat(vhd-lib): merge resume can resume when rename fails (#6530) 2022-11-25 20:51:33 +01:00
Julien Fontanet
9d5bc8af6e feat: run-script.js now only shows output on error by default 2022-11-25 15:45:52 +01:00
Julien Fontanet
9480079770 feat: script test-unit now bails on first error 2022-11-25 15:45:08 +01:00
Julien Fontanet
54fe9147ac chore: only enable Babel debug on prod builds
The output was making test results hard to see.
2022-11-25 14:43:36 +01:00
Gabriel Gunullu
b6a0477232 feat(xo-server-transport-nagios): report backed up VM individually (#6534) 2022-11-25 14:36:41 +01:00
Julien Fontanet
c60644c578 chore(lite): merge lint with the root config 2022-11-25 11:23:04 +01:00
Thierry Goettelmann
abdce94c5f feat(lite): type check on test (#6547) 2022-11-25 11:19:58 +01:00
Mathieu
d7dee04013 feat(xo-web/settings/users): remove OTP of users in admin panel (#6541)
See https://xcp-ng.org/forum/topic/6521
2022-11-25 11:15:07 +01:00
Julien Fontanet
dfc62132b7 fix(xo-web/remote): prevent browser from autocompleting encryption key 2022-11-24 18:48:45 +01:00
61 changed files with 1008 additions and 365 deletions

View File

@@ -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": {

View File

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

View File

@@ -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"
},

View File

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

View File

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

View File

@@ -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": {

View File

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

View File

@@ -297,6 +297,7 @@ export default class RemoteHandlerAbstract {
await this._mktree(dirname(newPath))
return this.#rename(oldPath, newPath, { checksum }, false)
}
throw error
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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": [
"@*/*",

View File

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

View File

@@ -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')
})

View File

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

View File

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

View File

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

View File

@@ -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": [

View File

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

View File

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

View File

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

View File

@@ -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(' ')}`,
})
}
}

View File

@@ -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": {

View File

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

View File

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

View File

@@ -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": {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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 ?
}
}
}
}

View File

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

View File

@@ -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": {

View File

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

View File

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

View File

@@ -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 ?",

View File

@@ -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?',

View File

@@ -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?',

View File

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

View File

@@ -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}}',

View File

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

View 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>
)
}
}

View 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>
)
}
}

View File

@@ -488,6 +488,10 @@
@extend .fa;
@extend .fa-list-alt;
}
&-warm-migration {
@extend .fa;
@extend .fa-fire;
}
}
// Generic states

View File

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

View File

@@ -495,6 +495,7 @@ export default decorate([
<li>{_('remoteEncryptionBackupSize')}</li>
</ul>
<input
autoComplete='new-password'
className='form-control'
name='encryptionKey'
onChange={effects.linkState}

View File

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

View File

@@ -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' && (

View 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)
}

View File

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

View File

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

View File

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

View File

@@ -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']))
}