Compare commits

...

88 Commits

Author SHA1 Message Date
Thierry Goettelmann
539056efd1 feat(lite/tests): First implementation of tests (Vitest + Playwright) 2022-12-12 12:03:15 +01:00
Julien Fontanet
df11a92cdb feat(scripts/gen-deps-list.js): add debug logs 2022-12-07 14:35:49 +01:00
Julien Fontanet
33ae59adf7 feat: release 5.77.1 2022-12-07 13:41:17 +01:00
Julien Fontanet
e0a115b41d feat(xo-server): 5.107.2 2022-12-07 13:19:15 +01:00
Julien Fontanet
f838d6c179 feat(@xen-orchestra/proxy): 0.26.6 2022-12-07 13:16:51 +01:00
Julien Fontanet
6c3229f517 feat(@xen-orchestra/backups): 0.29.2 2022-12-07 13:16:50 +01:00
Julien Fontanet
6973928b1a feat(backups/cleanVm): detect and fix cache inconsistencies (#6575) 2022-12-07 13:06:03 +01:00
Julien Fontanet
a5daba2a4d fix: work-around VuePress issues #2 2022-12-06 14:43:15 +01:00
Julien Fontanet
40ef83416e fix: work-around VuePress issues 2022-12-06 14:35:00 +01:00
Julien Fontanet
8518146455 fix: force classic Yarn 2022-12-06 10:53:35 +01:00
Florent BEAUCHAMP
d58f563de5 fix(xo-server/vm.warmMigration): fix start/delete params handling (#6570) 2022-12-06 10:42:51 +01:00
Thierry Goettelmann
ad2454adab feat(lite): replace ProgressBar with UiProgressBar & update UsageBar (#6544)
`ProgressBar` component handled too much logic (a progress bar + a circle icon + a label + a badge)

Since at various places we need a simple progress bar, all the additional logic should be handled by `UsageBar`.

- Move usage-specific logic from `ProgressBar` to `UsageBar`
- Removed `ProgressBar` component
- Created `ui/UiProgressBar` component containing only the bar itself
- Updated `UsageBar` to use `UiProgressBar` then adapting its style
2022-12-06 09:50:50 +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
Julien Fontanet
36f7f193aa feat: run linter in CI 2022-11-24 17:00:59 +01:00
Julien Fontanet
ca4a82ec38 fix: make test-lint script ignore xo-web
Too many errors in this legacy package.
2022-11-24 16:26:40 +01:00
Julien Fontanet
37aea1888d chore: fix lint issues 2022-11-24 16:26:40 +01:00
Julien Fontanet
92f3b4ddd7 chore(backups/RemoteAdapter): remove unused invalidateVmBackupListCache 2022-11-24 16:26:40 +01:00
Mathieu
647995428c feat(lite/pool/dashboard): top 5 RAM usage (#6419) 2022-11-24 15:57:11 +01:00
Mathieu
407e9c25f3 feat(xo-web/licenses): text to explicit where to bind xcp-ng licenses (#6551)
See zammad#11037
2022-11-24 15:42:16 +01:00
Julien Fontanet
1612ab7335 fix(backups-cli/clean-vms): remove incorrect console.log
Introduced by 94c755b10
2022-11-23 23:03:46 +01:00
Julien Fontanet
b952c36210 fix(vhd-lib/merge): VhdAbstract.rename → handler.rename
Missing changed from c5b3acfce
2022-11-23 15:02:56 +01:00
Florent BEAUCHAMP
96b5cb2c61 feat(xo-vmdk-to-vhd): overprovision vmdk size to generate ova in one pass (#6487) 2022-11-23 14:48:18 +01:00
Florent Beauchamp
c5b3acfce2 fix(vhd-lib): remove unsafe VhdAbstract.rename implementation
actual implementation was deleting the target vhd even if the source did not exist, leading to ptential data loss
2022-11-23 14:31:37 +01:00
Julien Fontanet
20a01bf266 feat(lint-staged): format all files with Prettier 2022-11-22 18:20:01 +01:00
Julien Fontanet
a33b88cf1c chore: format with Prettier 2022-11-22 17:30:14 +01:00
Julien Fontanet
09a2f45ada feat: run test script for all pkgs with changed files 2022-11-22 17:30:14 +01:00
Julien Fontanet
83a7dd7ea1 chore: remove custom scripts/lint-staged 2022-11-22 17:30:14 +01:00
Julien Fontanet
afc1b6a5c0 Revert "feat: run pre-commit script for all packages"
This reverts commit f5b91cd45d.
2022-11-22 17:30:14 +01:00
Thierry Goettelmann
7f4f860735 feat(lite/color mode): "auto" mode + "D" shortcut to toggle (#6536)
The shortcut is only enabled in dev environment
2022-11-22 15:35:31 +01:00
Julien Fontanet
d789e3aa0d chore: update to husky@8 2022-11-22 15:33:43 +01:00
Julien Fontanet
f5b91cd45d feat: run pre-commit script for all packages 2022-11-22 11:37:40 +01:00
Julien Fontanet
92ab4b3309 chore(lite): format with Prettier (#6545) 2022-11-22 11:33:03 +01:00
Florent Beauchamp
2c456e4c89 fix(vhd-lib): create directory for merged blocks 2022-11-22 11:05:51 +01:00
Florent Beauchamp
1460e63449 fix(vhd-lib): write state at the begining 2022-11-22 11:05:51 +01:00
135 changed files with 2607 additions and 839 deletions

1
.gitignore vendored
View File

@@ -7,6 +7,7 @@
/@vates/*/node_modules/
/@xen-orchestra/*/dist/
/@xen-orchestra/*/node_modules/
/@xen-orchestra/*/.tests-output/
/packages/*/dist/
/packages/*/node_modules/

4
.husky/pre-commit Executable file
View File

@@ -0,0 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx lint-staged

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

@@ -30,6 +30,7 @@ if (args.length === 0) {
${name} v${version}
`)
// eslint-disable-next-line n/no-process-exit
process.exit()
}

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

@@ -22,7 +22,6 @@ export default async function cleanVms(args) {
await asyncMap(_, vmDir =>
Disposable.use(getSyncedHandler({ url: pathToFileURL(dirname(vmDir)).href }), async handler => {
console.log(handler, basename(vmDir))
try {
await new RemoteAdapter(handler).cleanVm(basename(vmDir), {
fixMetadata: fix,

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

@@ -508,7 +508,7 @@ class RemoteAdapter {
return `${BACKUP_DIR}/${vmUuid}/cache.json.gz`
}
async #readCache(path) {
async _readCache(path) {
try {
return JSON.parse(await fromCallback(zlib.gunzip, await this.handler.readFile(path)))
} catch (error) {
@@ -521,15 +521,15 @@ class RemoteAdapter {
_updateCache = synchronized.withKey()(this._updateCache)
// eslint-disable-next-line no-dupe-class-members
async _updateCache(path, fn) {
const cache = await this.#readCache(path)
const cache = await this._readCache(path)
if (cache !== undefined) {
fn(cache)
await this.#writeCache(path, cache)
await this._writeCache(path, cache)
}
}
async #writeCache(path, data) {
async _writeCache(path, data) {
try {
await this.handler.writeFile(path, await fromCallback(zlib.gzip, JSON.stringify(data)), { flags: 'w' })
} catch (error) {
@@ -537,10 +537,6 @@ class RemoteAdapter {
}
}
async invalidateVmBackupListCache(vmUuid) {
await this.handler.unlink(this.#getVmBackupsCache(vmUuid))
}
async #getCachabledDataListVmBackups(dir) {
debug('generating cache', { path: dir })
@@ -581,7 +577,7 @@ class RemoteAdapter {
async _readCacheListVmBackups(vmUuid) {
const path = this.#getVmBackupsCache(vmUuid)
const cache = await this.#readCache(path)
const cache = await this._readCache(path)
if (cache !== undefined) {
debug('found VM backups cache, using it', { path })
return cache
@@ -594,7 +590,7 @@ class RemoteAdapter {
}
// detached async action, will not reject
this.#writeCache(path, backups)
this._writeCache(path, backups)
return backups
}

View File

@@ -311,7 +311,6 @@ exports.cleanVm = async function cleanVm(
}
const jsons = new Set()
let mustInvalidateCache = false
const xvas = new Set()
const xvaSums = []
const entries = await handler.list(vmDir, {
@@ -327,6 +326,20 @@ exports.cleanVm = async function cleanVm(
}
})
const cachePath = vmDir + '/cache.json.gz'
let mustRegenerateCache
{
const cache = await this._readCache(cachePath)
const actual = cache === undefined ? 0 : Object.keys(cache).length
const expected = jsons.size
mustRegenerateCache = actual !== expected
if (mustRegenerateCache) {
logWarn('unexpected number of entries in backup cache', { path: cachePath, actual, expected })
}
}
await asyncMap(xvas, async path => {
// check is not good enough to delete the file, the best we can do is report
// it
@@ -338,6 +351,8 @@ exports.cleanVm = async function cleanVm(
const unusedVhds = new Set(vhds)
const unusedXvas = new Set(xvas)
const backups = new Map()
// compile the list of unused XVAs and VHDs, and remove backup metadata which
// reference a missing XVA/VHD
await asyncMap(jsons, async json => {
@@ -350,19 +365,16 @@ exports.cleanVm = async function cleanVm(
return
}
let isBackupComplete
const { mode } = metadata
if (mode === 'full') {
const linkedXva = resolve('/', vmDir, metadata.xva)
if (xvas.has(linkedXva)) {
isBackupComplete = xvas.has(linkedXva)
if (isBackupComplete) {
unusedXvas.delete(linkedXva)
} else {
logWarn('the XVA linked to the backup is missing', { backup: json, xva: linkedXva })
if (remove) {
logInfo('deleting incomplete backup', { path: json })
jsons.delete(json)
mustInvalidateCache = true
await handler.unlink(json)
}
}
} else if (mode === 'delta') {
const linkedVhds = (() => {
@@ -371,22 +383,28 @@ exports.cleanVm = async function cleanVm(
})()
const missingVhds = linkedVhds.filter(_ => !vhds.has(_))
isBackupComplete = missingVhds.length === 0
// FIXME: find better approach by keeping as much of the backup as
// possible (existing disks) even if one disk is missing
if (missingVhds.length === 0) {
if (isBackupComplete) {
linkedVhds.forEach(_ => unusedVhds.delete(_))
linkedVhds.forEach(path => {
vhdsToJSons[path] = json
})
} else {
logWarn('some VHDs linked to the backup are missing', { backup: json, missingVhds })
if (remove) {
logInfo('deleting incomplete backup', { path: json })
mustInvalidateCache = true
jsons.delete(json)
await handler.unlink(json)
}
}
}
if (isBackupComplete) {
backups.set(json, metadata)
} else {
jsons.delete(json)
if (remove) {
logInfo('deleting incomplete backup', { backup: json })
mustRegenerateCache = true
await handler.unlink(json)
}
}
})
@@ -496,7 +514,7 @@ exports.cleanVm = async function cleanVm(
// check for the other that the size is the same as the real file size
await asyncMap(jsons, async metadataPath => {
const metadata = JSON.parse(await handler.readFile(metadataPath))
const metadata = backups.get(metadataPath)
let fileSystemSize
const merged = metadataWithMergedVhd[metadataPath] !== undefined
@@ -538,6 +556,7 @@ exports.cleanVm = async function cleanVm(
// systematically update size after a merge
if ((merged || fixMetadata) && size !== fileSystemSize) {
metadata.size = fileSystemSize
mustRegenerateCache = true
try {
await handler.writeFile(metadataPath, JSON.stringify(metadata), { flags: 'w' })
} catch (error) {
@@ -546,9 +565,16 @@ exports.cleanVm = async function cleanVm(
}
})
// purge cache if a metadata file has been deleted
if (mustInvalidateCache) {
await handler.unlink(vmDir + '/cache.json.gz')
if (mustRegenerateCache) {
const cache = {}
for (const [path, content] of backups.entries()) {
cache[path] = {
_filename: path,
id: path,
...content,
}
}
await this._writeCache(cachePath, cache)
}
return {

View File

@@ -8,7 +8,7 @@
"type": "git",
"url": "https://github.com/vatesfr/xen-orchestra.git"
},
"version": "0.29.0",
"version": "0.29.2",
"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

@@ -49,7 +49,6 @@ exports.FullBackupWriter = class FullBackupWriter extends MixinBackupWriter(Abst
const dataBasename = basename + '.xva'
const dataFilename = backupDir + '/' + dataBasename
const metadataFilename = `${backupDir}/${basename}.json`
const metadata = {
jobId: job.id,
mode: job.mode,

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

@@ -284,15 +284,25 @@ export default class RemoteHandlerAbstract {
return this._encryptor.decryptData(data)
}
async rename(oldPath, newPath, { checksum = false } = {}) {
oldPath = normalizePath(oldPath)
newPath = normalizePath(newPath)
let p = timeout.call(this._rename(oldPath, newPath), this._timeout)
if (checksum) {
p = Promise.all([p, this._rename(checksumFile(oldPath), checksumFile(newPath))])
async #rename(oldPath, newPath, { checksum }, createTree = true) {
try {
let p = timeout.call(this._rename(oldPath, newPath), this._timeout)
if (checksum) {
p = Promise.all([p, this._rename(checksumFile(oldPath), checksumFile(newPath))])
}
await p
} catch (error) {
// ENOENT can be a missing target directory OR a missing source
if (error.code === 'ENOENT' && createTree) {
await this._mktree(dirname(newPath))
return this.#rename(oldPath, newPath, { checksum }, false)
}
throw error
}
return p
}
rename(oldPath, newPath, { checksum = false } = {}) {
return this.#rename(normalizePath(oldPath), normalizePath(newPath), { checksum })
}
async copy(oldPath, newPath, { checksum = false } = {}) {

View File

@@ -228,6 +228,17 @@ handlers.forEach(url => {
expect(await handler.list('.')).toEqual(['file2'])
expect(await handler.readFile(`file2`)).toEqual(TEST_DATA)
})
it(`should rename the file and create dest directory`, async () => {
await handler.outputFile('file', TEST_DATA)
await handler.rename('file', `sub/file2`)
expect(await handler.list('sub')).toEqual(['file2'])
expect(await handler.readFile(`sub/file2`)).toEqual(TEST_DATA)
})
it(`should fail with enoent if source file is missing`, async () => {
const error = await rejectionOf(handler.rename('file', `sub/file2`))
expect(error.code).toBe('ENOENT')
})
})
describe('#rmdir()', () => {

View File

@@ -22,7 +22,7 @@ module.exports = {
"@typescript-eslint/no-explicit-any": "off",
"@limegrass/import-alias/import-alias": [
"error",
{ aliasConfigPath: require("path").join(__dirname, "tsconfig.json") },
{ aliasConfigPath: require("path").join(__dirname, "tsconfig.app.json") },
],
},
};

View File

@@ -5,6 +5,7 @@
- Invalidate sessionId token after logout (PR [#6480](https://github.com/vatesfr/xen-orchestra/pull/6480))
- Settings page (PR [#6418](https://github.com/vatesfr/xen-orchestra/pull/6418))
- Uncollapse hosts in the tree by default (PR [#6428](https://github.com/vatesfr/xen-orchestra/pull/6428))
- Display RAM usage in pool dashboard (PR [#6419](https://github.com/vatesfr/xen-orchestra/pull/6419))
## **0.1.0**

View File

@@ -105,7 +105,7 @@ Use the `busy` prop to display a loader icon.
</template>
<script lang="ts" setup>
import UiIcon from "@/components/ui/UiIcon.vue"
import UiIcon from "@/components/ui/UiIcon.vue";
import { faDisplay } from "@fortawesome/free-solid-svg-icons";
</script>
```

View File

@@ -0,0 +1,4 @@
{
"extends": "@vue/tsconfig/tsconfig.node.json",
"include": ["./**/*"]
}

View File

@@ -0,0 +1,8 @@
import { test, expect } from "@playwright/test";
// See here how to get started:
// https://playwright.dev/docs/intro
test("visits the app root url", async ({ page }) => {
await page.goto("/");
await expect(page.getByTestId("login-form")).toBeTruthy();
});

View File

@@ -3,12 +3,16 @@
"version": "0.1.0",
"scripts": {
"dev": "GIT_HEAD=$(git rev-parse HEAD) vite",
"build": "run-p type-check build-only",
"build": "run-p test:type-check build-only",
"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": "run-p test:once test:type-check",
"test:once": "vitest run",
"test:watch": "vitest",
"test:e2e": "playwright test",
"test:coverage": "vitest run --coverage",
"test:type-check": "vue-tsc --noEmit"
},
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^6.1.1",
@@ -19,6 +23,7 @@
"@types/d3-time-format": "^4.0.0",
"@types/lodash-es": "^4.17.6",
"@vueuse/core": "^9.5.0",
"@vueuse/math": "^9.5.0",
"complex-matcher": "^0.7.0",
"d3-time-format": "^4.1.0",
"decorator-synchronized": "^0.6.0",
@@ -39,18 +44,23 @@
"devDependencies": {
"@intlify/vite-plugin-vue-i18n": "^6.0.1",
"@limegrass/eslint-plugin-import-alias": "^1.0.5",
"@playwright/test": "^1.28.1",
"@rushstack/eslint-patch": "^1.1.0",
"@testing-library/vue": "^6.6.1",
"@types/node": "^16.11.41",
"@vitejs/plugin-vue": "^3.2.0",
"@vitest/coverage-c8": "^0.25.3",
"@vue/eslint-config-prettier": "^7.0.0",
"@vue/eslint-config-typescript": "^11.0.0",
"@vue/tsconfig": "^0.1.3",
"eslint-plugin-vue": "^9.0.0",
"happy-dom": "^7.7.0",
"npm-run-all": "^4.1.5",
"postcss": "^8.4.19",
"postcss-nested": "^6.0.0",
"typescript": "^4.9.3",
"vite": "^3.2.4",
"vitest": "^0.25.3",
"vue-tsc": "^1.0.9"
},
"private": true,

View File

@@ -0,0 +1,114 @@
import type { PlaywrightTestConfig } from "@playwright/test";
import { devices } from "@playwright/test";
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// require('dotenv').config();
/**
* See https://playwright.dev/docs/test-configuration.
*/
const config: PlaywrightTestConfig = {
testDir: "./e2e",
/* Maximum time one test can run for. */
timeout: 30 * 1000,
expect: {
/**
* Maximum time expect() should wait for the condition to be met.
* For example in `await expect(locator).toHaveText();`
*/
timeout: 5000,
},
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: [["html", { outputFolder: ".tests-output/e2e-report" }]],
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */
actionTimeout: 0,
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL: "http://localhost:3000",
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: "on-first-retry",
/* Only on CI systems run the tests headless */
headless: !!process.env.CI,
screenshot: "only-on-failure",
},
/* Configure projects for major browsers */
projects: [
{
name: "chromium",
use: {
...devices["Desktop Chrome"],
},
},
{
name: "firefox",
use: {
...devices["Desktop Firefox"],
},
},
{
name: "webkit",
use: {
...devices["Desktop Safari"],
},
},
/* Test against mobile viewports. */
// {
// name: 'Mobile Chrome',
// use: {
// ...devices['Pixel 5'],
// },
// },
// {
// name: 'Mobile Safari',
// use: {
// ...devices['iPhone 12'],
// },
// },
/* Test against branded browsers. */
// {
// name: 'Microsoft Edge',
// use: {
// channel: 'msedge',
// },
// },
// {
// name: 'Google Chrome',
// use: {
// channel: 'chrome',
// },
// },
],
/* Folder for test artifacts such as screenshots, videos, traces, etc. */
outputDir: ".tests-output/e2e-result",
/* Run your local dev server before starting the tests */
webServer: {
/**
* Use the dev server by default for faster feedback loop.
* Use the preview server on CI for more realistic testing.
Playwright will re-use the local server if there is already a dev-server running.
*/
command: process.env.CI ? "vite preview --port 3000" : "vite dev",
port: 5173,
reuseExistingServer: !process.env.CI,
},
};
export default config;

View File

@@ -32,6 +32,9 @@
</template>
<script lang="ts" setup>
import { useUiStore } from "@/stores/ui.store";
import { useActiveElement, useMagicKeys, whenever } from "@vueuse/core";
import { logicAnd } from "@vueuse/math";
import { difference } from "lodash";
import { computed, ref, watch, watchEffect } from "vue";
import favicon from "@/assets/favicon.svg";
@@ -58,13 +61,28 @@ link.href = favicon;
document.title = "XO Lite";
if (window.localStorage?.getItem("colorMode") !== "light") {
document.documentElement.classList.add("dark");
}
const xenApiStore = useXenApiStore();
const hostStore = useHostStore();
useChartTheme();
const uiStore = useUiStore();
if (import.meta.env.DEV) {
const activeElement = useActiveElement();
const { D } = useMagicKeys();
const canToggleDarkMode = computed(() => {
if (activeElement.value == null) {
return true;
}
return !["INPUT", "TEXTAREA"].includes(activeElement.value.tagName);
});
whenever(
logicAnd(D, canToggleDarkMode),
() => (uiStore.colorMode = uiStore.colorMode === "dark" ? "light" : "dark")
);
}
watchEffect(() => {
if (xenApiStore.isConnected) {

View File

@@ -11,7 +11,7 @@
</template>
<script lang="ts" setup>
import AccountButton from '@/components/AccountButton.vue'
import AccountButton from "@/components/AccountButton.vue";
</script>
<style lang="postcss" scoped>

View File

@@ -1,6 +1,6 @@
<template>
<div class="app-login form-container">
<form @submit.prevent="handleSubmit">
<form @submit.prevent="handleSubmit" data-testid="login-form">
<img alt="XO Lite" src="../assets/logo-title.svg" />
<input v-model="login" name="login" readonly type="text" />
<input

View File

@@ -43,14 +43,14 @@
<template #buttons>
<UiButton transparent @click="addNewFilter">
{{ $t("add-or") }}
</UiButton>
{{ $t("add-or") }}
</UiButton>
<UiButton :disabled="!isFilterValid" type="submit">
{{ $t(editedFilter ? "update" : "add") }}
</UiButton>
<UiButton outlined @click="handleCancel">
{{ $t("cancel") }}
</UiButton>
{{ $t("cancel") }}
</UiButton>
</template>
</UiModal>
</template>

View File

@@ -41,8 +41,8 @@
<template #buttons>
<UiButton type="submit">{{ $t("add") }}</UiButton>
<UiButton outlined @click="handleCancel">
{{ $t("cancel") }}
</UiButton>
{{ $t("cancel") }}
</UiButton>
</template>
</UiModal>
</template>

View File

@@ -0,0 +1,27 @@
import { mount } from "@vue/test-utils";
import { describe, expect, it } from "vitest";
import PowerStateIcon from "@/components/PowerStateIcon.vue";
describe("PowerStateIcon.vue", () => {
it("should render correctly", async () => {
const wrapper = mount(PowerStateIcon, {
props: {
state: "Running",
},
});
expect(wrapper.element.classList.contains("state-running")).toBeTruthy();
await wrapper.setProps({
state: "Paused",
});
expect(wrapper.element.classList.contains("state-paused")).toBeTruthy();
await wrapper.setProps({
state: "not-exists",
});
expect(wrapper.element.classList.contains("state-not-exists")).toBeTruthy();
});
});

View File

@@ -3,6 +3,7 @@
</template>
<script lang="ts" setup>
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { computed } from "vue";
import {
faMoon,

View File

@@ -1,63 +0,0 @@
<template>
<div class="progress-bar-component">
<div class="progress-bar">
<div class="progress-bar-fill" />
</div>
<div class="badge" v-if="label !== undefined">
<span class="circle" />
{{ label }}
<UiBadge>{{ badgeLabel ?? progressWithUnit }}</UiBadge>
</div>
</div>
</template>
<script lang="ts" setup>
import { computed } from "vue";
import UiBadge from "@/components/ui/UiBadge.vue";
interface Props {
value: number;
badgeLabel?: string | number;
label?: string;
maxValue?: number;
}
const props = withDefaults(defineProps<Props>(), {
maxValue: 100,
});
const progressWithUnit = computed(() => {
const progress = Math.round((props.value / props.maxValue) * 100);
return `${progress}%`;
});
</script>
<style lang="postcss" scoped>
.badge {
text-align: right;
margin: 1rem 0;
}
.circle {
display: inline-block;
height: 10px;
width: 10px;
background-color: #716ac6;
border-radius: 1rem;
}
.progress-bar {
overflow: hidden;
height: 1.2rem;
border-radius: 0.4rem;
background-color: var(--color-blue-scale-400);
margin: 1rem 0;
}
.progress-bar-fill {
transition: width 1s ease-in-out;
width: v-bind(progressWithUnit);
height: 1.2rem;
background-color: var(--color-extra-blue-d20);
}
</style>

View File

@@ -3,25 +3,33 @@
<div class="header">
<slot name="header" />
</div>
<template v-if="data !== undefined">
<ProgressBar
<div v-if="data !== undefined">
<div
v-for="item in computedData.sortedArray"
:key="item.id"
:value="item.value"
:label="item.label"
:badge-label="item.badgeLabel"
/>
<div class="footer">
<slot name="footer" :total-percent="computedData.totalPercentUsage" />
class="progress-item"
>
<UiProgressBar :value="item.value" color="custom" />
<div class="legend">
<span class="circle" />
{{ item.label }}
<UiBadge class="badge">{{
item.badgeLabel ?? `${item.value}%`
}}</UiBadge>
</div>
</div>
</template>
<div class="footer">
<slot :total-percent="computedData.totalPercentUsage" name="footer" />
</div>
</div>
<UiSpinner v-else class="spinner" />
</div>
</template>
<script lang="ts" setup>
import UiBadge from "@/components/ui/UiBadge.vue";
import UiProgressBar from "@/components/ui/UiProgressBar.vue";
import { computed } from "vue";
import ProgressBar from "@/components/ProgressBar.vue";
import UiSpinner from "@/components/ui/UiSpinner.vue";
interface Data {
@@ -33,7 +41,7 @@ interface Data {
}
interface Props {
data?: Array<Data>;
data?: Data[];
nItems?: number;
}
@@ -84,23 +92,43 @@ const computedData = computed(() => {
width: 40px;
height: 40px;
}
</style>
<style>
.progress-bar-component:nth-of-type(2) .progress-bar-fill,
.progress-bar-component:nth-of-type(2) .circle {
background-color: var(--color-extra-blue-d60);
.legend {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 0.5rem;
margin: 1.6em 0;
}
.progress-bar-component:nth-of-type(3) .progress-bar-fill,
.progress-bar-component:nth-of-type(3) .circle {
background-color: var(--color-extra-blue-d40);
.badge {
font-size: 0.9em;
font-weight: 700;
}
.progress-bar-component:nth-of-type(4) .progress-bar-fill,
.progress-bar-component:nth-of-type(4) .circle {
background-color: var(--color-extra-blue-d20);
.progress-item {
--progress-bar-height: 1.2rem;
--progress-bar-color: var(--color-extra-blue-l20);
--progress-bar-background-color: var(--color-blue-scale-400);
}
.progress-bar-component .progress-bar-fill,
.progress-bar-component .circle {
background-color: var(--color-extra-blue-l20);
.progress-item:nth-child(1) {
--progress-bar-color: var(--color-extra-blue-d60);
}
.progress-item:nth-child(2) {
--progress-bar-color: var(--color-extra-blue-d40);
}
.progress-item:nth-child(3) {
--progress-bar-color: var(--color-extra-blue-d20);
}
.circle {
display: inline-block;
width: 1rem;
height: 1rem;
border-radius: 0.5rem;
background-color: var(--progress-bar-color);
}
</style>

View File

@@ -12,7 +12,7 @@
:icon="faServer"
:route="{ name: 'host.dashboard', params: { uuid: host.uuid } }"
>
{{ host.name_label || '(Host)' }}
{{ host.name_label || "(Host)" }}
<template #actions>
<InfraAction
:icon="isExpanded ? faAngleDown : faAngleUp"

View File

@@ -12,7 +12,7 @@
:icon="faDisplay"
:route="{ name: 'vm.console', params: { uuid: vm.uuid } }"
>
{{ vm.name_label || '(VM)' }}
{{ vm.name_label || "(VM)" }}
<template #actions>
<InfraAction>
<PowerStateIcon :state="vm?.power_state" />

View File

@@ -0,0 +1,14 @@
<template>
<UiCard>
<UiTitle type="h4">{{ $t("ram-usage") }}</UiTitle>
<HostsRamUsage />
<VmsRamUsage />
</UiCard>
</template>
<script setup lang="ts">
import HostsRamUsage from "@/components/pool/dashboard/ramUsage/HostsRamUsage.vue";
import VmsRamUsage from "@/components/pool/dashboard/ramUsage/VmsRamUsage.vue";
import UiCard from "@/components/ui/UiCard.vue";
import UiTitle from "@/components/ui/UiTitle.vue";
</script>

View File

@@ -1,10 +1,10 @@
<template>
<UiCard>
<UiTitle type="h4">{{ $t("storage-usage") }}</UiTitle>
<UsageBar :data="srStore.isReady ? data.result : undefined" :nItems="5">
<UsageBar :data="srStore.isReady ? data.result : undefined" :nItems="N_ITEMS">
<template #header>
<span>{{ $t("storage") }}</span>
<span>{{ $t("top-#", { n: 5 }) }}</span>
<span>{{ $t("top-#", { n: N_ITEMS }) }}</span>
</template>
<template #footer v-if="showFooter">
<div class="footer-card">
@@ -37,6 +37,7 @@ import UiCard from "@/components/ui/UiCard.vue";
import UiTitle from "@/components/ui/UiTitle.vue";
import { formatSize, percent } from "@/libs/utils";
import { useSrStore } from "@/stores/storage.store";
import { N_ITEMS } from "@/views/pool/PoolDashboardView.vue";
const srStore = useSrStore();

View File

@@ -1,8 +1,8 @@
<template>
<UsageBar :data="statFetched ? data : undefined" :n-items="5">
<UsageBar :data="statFetched ? data : undefined" :n-items="N_ITEMS">
<template #header>
<span>{{ $t("hosts") }}</span>
<span>{{ $t("top-#", { n: 5 }) }}</span>
<span>{{ $t("top-#", { n: N_ITEMS }) }}</span>
</template>
</UsageBar>
</template>
@@ -13,6 +13,7 @@ import UsageBar from "@/components/UsageBar.vue";
import type { Stat } from "@/composables/fetch-stats.composable";
import { getAvgCpuUsage } from "@/libs/utils";
import type { HostStats } from "@/libs/xapi-stats";
import { N_ITEMS } from "@/views/pool/PoolDashboardView.vue";
const stats = inject<ComputedRef<Stat<HostStats>[]>>(
"hostStats",

View File

@@ -1,8 +1,8 @@
<template>
<UsageBar :data="statFetched ? data : undefined" :n-items="5">
<UsageBar :data="statFetched ? data : undefined" :n-items="N_ITEMS">
<template #header>
<span>{{ $t("vms") }}</span>
<span>{{ $t("top-#", { n: 5 }) }}</span>
<span>{{ $t("top-#", { n: N_ITEMS }) }}</span>
</template>
</UsageBar>
</template>
@@ -13,6 +13,7 @@ import UsageBar from "@/components/UsageBar.vue";
import type { Stat } from "@/composables/fetch-stats.composable";
import { getAvgCpuUsage } from "@/libs/utils";
import type { VmStats } from "@/libs/xapi-stats";
import { N_ITEMS } from "@/views/pool/PoolDashboardView.vue";
const stats = inject<ComputedRef<Stat<VmStats>[]>>(
"vmStats",

View File

@@ -0,0 +1,52 @@
<template>
<UsageBar :data="statFetched ? data : undefined" :n-items="N_ITEMS">
<template #header>
<span>{{ $t("hosts") }}</span>
<span>{{ $t("top-#", { n: N_ITEMS }) }}</span>
</template>
</UsageBar>
</template>
<script lang="ts" setup>
import { type ComputedRef, computed, inject } from "vue";
import UsageBar from "@/components/UsageBar.vue";
import type { Stat } from "@/composables/fetch-stats.composable";
import { formatSize, parseRamUsage } from "@/libs/utils";
import type { HostStats } from "@/libs/xapi-stats";
import { N_ITEMS } from "@/views/pool/PoolDashboardView.vue";
const stats = inject<ComputedRef<Stat<HostStats>[]>>(
"hostStats",
computed(() => [])
);
const data = computed(() => {
const result: {
id: string;
label: string;
value: number;
badgeLabel: string;
}[] = [];
stats.value.forEach((stat) => {
if (stat.stats === undefined) {
return;
}
const { percentUsed, total, used } = parseRamUsage(stat.stats);
result.push({
id: stat.id,
label: stat.name,
value: percentUsed,
badgeLabel: `${formatSize(used)}/${formatSize(total)}`,
});
});
return result;
});
const statFetched: ComputedRef<boolean> = computed(
() =>
statFetched.value ||
(stats.value.length > 0 && stats.value.length === data.value.length)
);
</script>

View File

@@ -0,0 +1,52 @@
<template>
<UsageBar :data="statFetched ? data : undefined" :n-items="N_ITEMS">
<template #header>
<span>{{ $t("vms") }}</span>
<span>{{ $t("top-#", { n: N_ITEMS }) }}</span>
</template>
</UsageBar>
</template>
<script lang="ts" setup>
import { type ComputedRef, computed, inject } from "vue";
import UsageBar from "@/components/UsageBar.vue";
import type { Stat } from "@/composables/fetch-stats.composable";
import { formatSize, parseRamUsage } from "@/libs/utils";
import type { VmStats } from "@/libs/xapi-stats";
import { N_ITEMS } from "@/views/pool/PoolDashboardView.vue";
const stats = inject<ComputedRef<Stat<VmStats>[]>>(
"vmStats",
computed(() => [])
);
const data = computed(() => {
const result: {
id: string;
label: string;
value: number;
badgeLabel: string;
}[] = [];
stats.value.forEach((stat) => {
if (stat.stats === undefined) {
return;
}
const { percentUsed, total, used } = parseRamUsage(stat.stats);
result.push({
id: stat.id,
label: stat.name,
value: percentUsed,
badgeLabel: `${formatSize(used)}/${formatSize(total)}`,
});
});
return result;
});
const statFetched: ComputedRef<boolean> = computed(
() =>
statFetched.value ||
(stats.value.length > 0 && stats.value.length === data.value.length)
);
</script>

View File

@@ -0,0 +1,27 @@
import UiBadge from "@/components/ui/UiBadge.vue";
import { faDisplay } from "@fortawesome/free-solid-svg-icons";
import { render } from "@testing-library/vue";
describe("UiBadge", () => {
it("should render with no icon", () => {
const { getByText, queryByTestId } = render(UiBadge, {
slots: {
default: "3456",
},
});
getByText("3456");
expect(queryByTestId("ui-icon")).toBeNull();
});
it("should render with icon", () => {
const { getByTestId } = render(UiBadge, {
props: {
icon: faDisplay,
},
});
getByTestId("ui-icon");
});
});

View File

@@ -22,7 +22,7 @@ defineProps<{
font-size: 1.4rem;
font-weight: 500;
padding: 0 0.8rem;
height: 2.4rem;
height: 1.8em;
color: var(--color-blue-scale-500);
border-radius: 9.6rem;
background-color: var(--color-blue-scale-300);

View File

@@ -5,6 +5,7 @@
:spin="busy"
class="ui-icon"
:fixed-width="fixedWidth"
data-testid="ui-icon"
/>
</template>
@@ -12,6 +13,7 @@
import { computed } from "vue";
import type { IconDefinition } from "@fortawesome/fontawesome-common-types";
import { faSpinner } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
const props = withDefaults(
defineProps<{

View File

@@ -0,0 +1,60 @@
<template>
<div class="ui-progress-bar" :class="`color-${color}`">
<div class="fill" />
</div>
</template>
<script lang="ts" setup>
import type { Color } from "@/types";
import { computed } from "vue";
const props = withDefaults(
defineProps<{
value: number;
color?: Color | "custom";
maxValue?: number;
}>(),
{ color: "info", maxValue: 100 }
);
const progressWithUnit = computed(() => {
const progress = (props.value / props.maxValue) * 100;
return `${progress}%`;
});
</script>
<style lang="postcss" scoped>
.ui-progress-bar {
overflow: hidden;
height: var(--progress-bar-height, 0.4rem);
margin: 1rem 0;
border-radius: 0.4rem;
background-color: var(
--progress-bar-background-color,
var(--background-color-extra-blue)
);
&.color-info {
--progress-bar-color: var(--color-extra-blue-base);
}
&.color-success {
--progress-bar-color: var(--color-green-infra-base);
}
&.color-warning {
--progress-bar-color: var(--color-orange-world-base);
}
&.color-error {
--progress-bar-color: var(--color-red-vates-base);
}
}
.fill {
width: v-bind(progressWithUnit);
height: var(--progress-bar-height, 0.4rem);
transition: width 1s ease-in-out;
background-color: var(--progress-bar-color);
}
</style>

View File

@@ -1,23 +1,22 @@
# useBusy composable
```vue
<template>
<span class="error" v-if="error">{{ error }}</span>
<button @click="run" :disabled="isBusy">Do something</button>
</template>
<script lang="ts" setup>
import useBusy from '@/composables/busy.composable';
import useBusy from "@/composables/busy.composable";
async function doSomething() {
try {
// Doing some async work
} catch (e) {
throw "Something bad happened";
}
async function doSomething() {
try {
// Doing some async work
} catch (e) {
throw "Something bad happened";
}
}
const { isBusy, error, run } = useBusy(doSomething)
const { isBusy, error, run } = useBusy(doSomething);
</script>
```

View File

@@ -0,0 +1,35 @@
import useBusy from "@/composables/busy.composable";
import { expect, vi } from "vitest";
describe("Busy Composable", () => {
it("should work", async () => {
let resolve: (value: unknown) => void = () => undefined;
const promise = new Promise((r) => (resolve = r));
const func = vi.fn(() => promise);
const { isBusy, run, error } = useBusy(func);
expect(isBusy.value).toBeFalsy();
const runPromise = run();
expect(isBusy.value).toBeTruthy();
resolve(null);
await runPromise;
expect(isBusy.value).toBeFalsy();
expect(error.value).toBeUndefined();
});
it("should handle error", async () => {
const errorMessage = "SOMETHING BAD HAPPENED";
const promise = Promise.reject(errorMessage);
const func = vi.fn(() => promise);
const { isBusy, run, error } = useBusy(func);
try {
await run();
} catch (e) {
/**/
}
expect(isBusy.value).toBeFalsy();
expect(error.value).toBe(errorMessage);
});
});

View File

@@ -13,19 +13,23 @@ const filteredCollection = myCollection.filter(predicate);
By default, when adding/removing filters, the URL will update automatically.
```typescript
addFilter('name:/^foo/i'); // Will update the URL with ?filter=name:/^foo/i
addFilter("name:/^foo/i"); // Will update the URL with ?filter=name:/^foo/i
```
### Change the URL query string parameter name
```typescript
const { /* ... */ } = useCollectionFilter({ queryStringParam: 'f' }); // ?f=name:/^foo/i
const {
/* ... */
} = useCollectionFilter({ queryStringParam: "f" }); // ?f=name:/^foo/i
```
### Disable the usage of URL query string
```typescript
const { /* ... */ } = useCollectionFilter({ queryStringParam: undefined });
const {
/* ... */
} = useCollectionFilter({ queryStringParam: undefined });
```
## Example of using the composable with the `CollectionFilter` component
@@ -38,32 +42,32 @@ const { /* ... */ } = useCollectionFilter({ queryStringParam: undefined });
@add-filter="addFilter"
@remove-filter="removeFilter"
/>
<div v-for="item in filteredCollection">...</div>
</template>
<script lang="ts" setup>
import CollectionFilter from "@/components/CollectionFilter.vue";
import useCollectionFilter from "@/composables/collection-filter.composable";
import { computed } from "vue";
import CollectionFilter from "@/components/CollectionFilter.vue";
import useCollectionFilter from "@/composables/collection-filter.composable";
import { computed } from "vue";
const collection = [
{ name: "Foo", age: 5, registered: true },
{ name: "Bar", age: 12, registered: false },
{ name: "Foo Bar", age: 2, registered: true },
{ name: "Bar Baz", age: 45, registered: false },
{ name: "Foo Baz", age: 32, registered: false },
{ name: "Foo Bar Baz", age: 32, registered: true },
];
const collection = [
{ name: "Foo", age: 5, registered: true },
{ name: "Bar", age: 12, registered: false },
{ name: "Foo Bar", age: 2, registered: true },
{ name: "Bar Baz", age: 45, registered: false },
{ name: "Foo Baz", age: 32, registered: false },
{ name: "Foo Bar Baz", age: 32, registered: true },
];
const availableFilters: AvailableFilter[] = [
{ property: "name", label: "Name", type: "string" },
{ property: "age", label: "Age", type: "number" },
{ property: "registered", label: "Registered", type: "boolean", icon: faKey },
];
const availableFilters: AvailableFilter[] = [
{ property: "name", label: "Name", type: "string" },
{ property: "age", label: "Age", type: "number" },
{ property: "registered", label: "Registered", type: "boolean", icon: faKey },
];
const { filters, addFilter, removeFilter, predicate } = useCollectionFilter();
const filteredCollection = computed(() => collection.filter(predicate));
const { filters, addFilter, removeFilter, predicate } = useCollectionFilter();
const filteredCollection = computed(() => collection.filter(predicate));
</script>
```

View File

@@ -0,0 +1,97 @@
import useCollectionFilter from "@/composables/collection-filter.composable";
import { expect } from "vitest";
import { nextTick } from "vue";
import { useRoute, useRouter } from "vue-router";
vi.mock("vue-router", () => ({
useRoute: vi.fn(() => ({
query: {},
})),
useRouter: vi.fn(() => ({
replace: () => undefined,
})),
}));
describe("Collection Filter Composable", () => {
afterEach(() => {
vi.clearAllMocks();
});
const items = [{ name: "Foo" }, { name: "Bar" }, { name: "Baz" }];
it("should add filters", () => {
const { filters, addFilter } = useCollectionFilter();
expect(filters.value).toHaveLength(0);
addFilter("foo");
addFilter("bar");
expect(filters.value).toHaveLength(2);
expect(filters.value).toEqual(["foo", "bar"]);
});
it("should remove filters", () => {
const { filters, addFilter, removeFilter } = useCollectionFilter();
addFilter("foo");
addFilter("bar");
removeFilter("foo");
expect(filters.value).toHaveLength(1);
expect(filters.value).toEqual(["bar"]);
});
it("should filter an array correctly", () => {
const { addFilter, predicate } = useCollectionFilter();
addFilter("name:/^Ba/");
const filtered = items.filter(predicate.value);
expect(filtered).toHaveLength(2);
expect(filtered).toEqual([{ name: "Bar" }, { name: "Baz" }]);
});
it("should not use router when disabled", async () => {
const replace = vi.fn();
vi.mocked(useRouter, { partial: true }).mockImplementation(() => ({
replace,
}));
const { addFilter } = useCollectionFilter({ queryStringParam: "" });
addFilter("name:/^Ba/");
await nextTick();
expect(replace).not.toHaveBeenCalled();
});
it("should use router when enabled", async () => {
const replace = vi.fn();
vi.mocked(useRouter, { partial: true }).mockImplementation(() => ({
replace,
}));
const { addFilter } = useCollectionFilter();
addFilter("name:/^Ba/");
await nextTick();
expect(replace).toHaveBeenCalledOnce();
});
it("should load single initial filter from query string", () => {
vi.mocked(useRoute, { partial: true }).mockImplementation(() => ({
query: {
filter: "name:Foo",
},
}));
const { filters } = useCollectionFilter();
expect(filters.value).toHaveLength(1);
});
it("should load multiple initial filters from query string", () => {
vi.mocked(useRoute, { partial: true }).mockImplementation(() => ({
query: {
filter: "name:Foo age:20",
},
}));
const { filters } = useCollectionFilter();
expect(filters.value).toHaveLength(2);
});
});

View File

@@ -2,14 +2,17 @@
```vue
<script lang="ts" setup>
import useFilteredCollection from './filtered-collection.composable';
import useFilteredCollection from "./filtered-collection.composable";
const players = [
{ name: "Foo", team: "Blue" },
{ name: "Bar", team: "Red" },
{ name: "Baz", team: "Blue" },
]
const bluePlayers = useFilteredCollection(players, (player) => player.team === "Blue");
const players = [
{ name: "Foo", team: "Blue" },
{ name: "Bar", team: "Red" },
{ name: "Baz", team: "Blue" },
];
const bluePlayers = useFilteredCollection(
players,
(player) => player.team === "Blue"
);
</script>
```

View File

@@ -5,27 +5,28 @@
<div v-for="item in items">
{{ item.name }} <button @click="openRemoveModal(item)">Delete</button>
</div>
<UiModal v-if="isRemoveModalOpen">
Are you sure you want to delete {{ removeModalPayload.name }}
<button @click="handleRemove">Yes</button> <button @click="closeRemoveModal">No</button>
<button @click="handleRemove">Yes</button>
<button @click="closeRemoveModal">No</button>
</UiModal>
</template>
<script lang="ts" setup>
import useModal from '@/composables/modal.composable';
import useModal from "@/composables/modal.composable";
const {
payload: removeModalPayload,
isOpen: isRemoveModalOpen,
open: openRemoveModal,
close: closeRemoveModal,
} = useModal()
async function handleRemove() {
await removeItem(removeModalPayload.id);
closeRemoveModal()
}
const {
payload: removeModalPayload,
isOpen: isRemoveModalOpen,
open: openRemoveModal,
close: closeRemoveModal,
} = useModal();
async function handleRemove() {
await removeItem(removeModalPayload.id);
closeRemoveModal();
}
</script>
```

View File

@@ -4,34 +4,30 @@
<template>
<table>
<thead>
<tr>
<th>
<input type="checkbox" v-model="areAllSelected">
</th>
<th>Name</th>
</tr>
<tr>
<th>
<input type="checkbox" v-model="areAllSelected" />
</th>
<th>Name</th>
</tr>
</thead>
<tbody>
<tr v-for="item in items">
<td>
<input type="checkbox" :value="item.id" v-model="selected" />
</td>
<td>{{ item.name }}</td>
</tr>
<tr v-for="item in items">
<td>
<input type="checkbox" :value="item.id" v-model="selected" />
</td>
<td>{{ item.name }}</td>
</tr>
</tbody>
</table>
<!-- You can use something else than a "Select All" checkbox -->
<button @click="areAllSelected = !areAllSelected">Toggle all selected</button>
</template>
<script lang="ts" setup>
import useMultiSelect from './multi-select.composable';
import useMultiSelect from "./multi-select.composable";
const {
selected,
areAllSelected,
} = useMultiSelect()
const { selected, areAllSelected } = useMultiSelect();
</script>
```

View File

@@ -123,3 +123,37 @@ export const buildXoObject = (
...record,
$ref: params.opaqueRef,
});
export function parseRamUsage(
{
memory,
memoryFree,
}: {
memory: number[];
memoryFree?: number[];
},
{ nSequence = 4 } = {}
) {
const _nSequence = Math.min(memory.length, nSequence);
let total = 0;
let used = 0;
memory = memory.slice(memory.length - _nSequence);
memoryFree = memoryFree?.slice(memoryFree.length - _nSequence);
memory.forEach((ram, key) => {
total += ram;
used += ram - (memoryFree?.[key] ?? 0);
});
const percentUsed = percent(used, total);
return {
// In case `memoryFree` is not given by the xapi,
// we won't be able to calculate the percentage of used memory properly.
percentUsed:
memoryFree === undefined || isNaN(percentUsed) ? 0 : percentUsed,
total: total / _nSequence,
used: memoryFree === undefined ? 0 : used / _nSequence,
};
}

View File

@@ -259,7 +259,7 @@ export type VmStats = {
w: Record<string, number[]>;
};
memory: number[];
memoryFree: number[];
memoryFree?: number[];
vifs: {
rx: Record<string, number[]>;
tx: Record<string, number[]>;

View File

@@ -15,7 +15,9 @@
"community-name": "{name} community",
"copy": "Copy",
"cpu-usage":"CPU usage",
"dark-mode": "Dark mode",
"theme-dark": "Dark",
"theme-light": "Light",
"theme-auto": "Auto",
"dashboard": "Dashboard",
"delete": "Delete",
"descending": "descending",
@@ -37,6 +39,7 @@
"or": "Or",
"password": "Password",
"property": "Property",
"ram-usage":"RAM usage",
"send-us-feedback": "Send us feedback",
"settings": "Settings",
"snapshot": "Snapshot",

View File

@@ -15,7 +15,9 @@
"community-name": "Communauté {name}",
"copy": "Copier",
"cpu-usage":"Utilisation CPU",
"dark-mode": "Mode sombre",
"theme-dark": "Sombre",
"theme-light": "Clair",
"theme-auto": "Auto",
"dashboard": "Tableau de bord",
"delete": "Supprimer",
"descending": "descendant",
@@ -37,6 +39,7 @@
"or": "Ou",
"password": "Mot de passe",
"property": "Propriété",
"ram-usage":"Utilisation de la RAM",
"send-us-feedback": "Envoyez-nous vos commentaires",
"settings": "Paramètres",
"snapshot": "Instantané",

View File

@@ -1,10 +1,14 @@
import { useColorMode } from "@vueuse/core";
import { defineStore } from "pinia";
import { ref } from "vue";
export const useUiStore = defineStore("ui", () => {
const currentHostOpaqueRef = ref();
const colorMode = useColorMode({ emitAuto: true, initialValue: "dark" });
return {
colorMode,
currentHostOpaqueRef,
};
});

View File

@@ -3,13 +3,18 @@
<PoolDashboardStatus class="item" />
<PoolDashboardStorageUsage class="item" />
<PoolDashboardCpuUsage class="item" />
<PoolDashboardRamUsage class="item" />
</div>
</template>
<script lang="ts">
export const N_ITEMS = 5;
</script>
<script lang="ts" setup>
import { differenceBy } from "lodash-es";
import { computed, onMounted, provide, watch } from "vue";
import PoolDashboardCpuUsage from "@/components/pool/dashboard/PoolDashboardCpuUsage.vue";
import PoolDashboardRamUsage from "@/components/pool/dashboard/PoolDashboardRamUsage.vue";
import PoolDashboardStatus from "@/components/pool/dashboard/PoolDashboardStatus.vue";
import PoolDashboardStorageUsage from "@/components/pool/dashboard/PoolDashboardStorageUsage.vue";
import useFetchStats from "@/composables/fetch-stats.composable";

View File

@@ -19,7 +19,9 @@
rel="noopener noreferrer"
href="https://xcp-ng.org/blog/"
>{{ $t("news-name", { name: "XCP-ng" }) }}</a
> - <a
>
-
<a
target="_blank"
rel="noopener noreferrer"
href="https://xen-orchestra.com/blog/"
@@ -35,7 +37,9 @@
rel="noopener noreferrer"
href="https://xcp-ng.org/forum"
>{{ $t("community-name", { name: "XCP-ng" }) }}</a
> - <a
>
-
<a
target="_blank"
rel="noopener noreferrer"
href="https://xcp-ng.org/forum/category/12/xen-orchestra"
@@ -50,14 +54,15 @@
<UiKeyValueList>
<UiKeyValueRow>
<template #key>{{ $t("appearance") }}</template>
<template #value
><FormLabel>
<FormToggle
:modelValue="darkMode"
@update:modelValue="setDarkMode"
/>{{ $t("dark-mode") }}</FormLabel
></template
>
<template #value>
<FormLabel>
<FormSelect v-model="colorMode">
<option value="auto">{{ $t("theme-auto") }}</option>
<option value="dark">{{ $t("theme-dark") }}</option>
<option value="light">{{ $t("theme-light") }}</option>
</FormSelect>
</FormLabel>
</template>
</UiKeyValueRow>
</UiKeyValueList>
</UiCard>
@@ -85,15 +90,16 @@
</template>
<script lang="ts" setup>
import { computed, watch } from "vue";
import FormSelect from "@/components/form/FormSelect.vue";
import { useUiStore } from "@/stores/ui.store";
import { storeToRefs } from "pinia";
import { watch } from "vue";
import { useI18n } from "vue-i18n";
import { locales } from "@/i18n";
import { faEarthAmericas, faGear } from "@fortawesome/free-solid-svg-icons";
import { useLocalStorage } from "@vueuse/core";
import FormWidget from "@/components/FormWidget.vue";
import TitleBar from "@/components/TitleBar.vue";
import FormLabel from "@/components/form/FormLabel.vue";
import FormToggle from "@/components/form/FormToggle.vue";
import UiCard from "@/components/ui/UiCard.vue";
import UiKeyValueList from "@/components/ui/UiKeyValueList.vue";
import UiKeyValueRow from "@/components/ui/UiKeyValueRow.vue";
@@ -105,12 +111,7 @@ const { locale } = useI18n();
watch(locale, (newLocale) => localStorage.setItem("lang", newLocale));
const colorMode = useLocalStorage<string>("colorMode", "dark");
const darkMode = computed(() => colorMode.value !== "light");
const setDarkMode = (enabled: boolean) => {
colorMode.value = enabled ? "dark" : "light";
document.documentElement.classList[enabled ? "add" : "remove"]("dark");
};
const { colorMode } = storeToRefs(useUiStore());
</script>
<style lang="postcss" scoped>

View File

@@ -0,0 +1,19 @@
{
"extends": "@vue/tsconfig/tsconfig.web.json",
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
"compilerOptions": {
"experimentalDecorators": true,
"lib": ["ES2019", "dom"],
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
},
"types": ["vitest/globals"]
},
"references": [
{
"path": "./tsconfig.config.json"
}
]
}

View File

@@ -1,21 +1,14 @@
{
"extends": "@vue/tsconfig/tsconfig.web.json",
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
"compilerOptions": {
"experimentalDecorators": true,
"lib": [
"ES2019",
"dom"
],
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"files": [],
"references": [
{
"path": "./tsconfig.config.json"
},
{
"path": "./tsconfig.app.json"
},
{
"path": "./tsconfig.vitest.json"
}
]
}

View File

@@ -0,0 +1,8 @@
{
"extends": "./tsconfig.app.json",
"exclude": [],
"compilerOptions": {
"composite": true,
"types": ["node", "vitest/globals"]
}
}

View File

@@ -1,3 +1,4 @@
/// <reference types="vitest" />
import vueI18n from "@intlify/vite-plugin-vue-i18n";
import vue from "@vitejs/plugin-vue";
import { fileURLToPath, URL } from "url";
@@ -29,4 +30,13 @@ export default defineConfig({
optimizeDeps: {
include: ["complex-matcher"],
},
test: {
globals: true,
environment: "happy-dom",
exclude: ["e2e"],
coverage: {
reportsDirectory: ".tests-output/coverage",
},
},
root: ".",
});

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

@@ -1,9 +1,9 @@
'use strict'
const fromCallback = require('promise-toolbox/fromCallback')
// eslint-disable-next-line n/no-missing-require
// eslint-disable-next-line n/no-extraneous-require
const splitHost = require('split-host')
// eslint-disable-next-line n/no-missing-require
// eslint-disable-next-line n/no-extraneous-require
const { createClient, Facility, Severity, Transport } = require('syslog-client')
const LEVELS = require('../levels')

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.6",
"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.2",
"@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

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,74 @@
# ChangeLog
## **5.77.1** (2022-12-07)
<img id="latest" src="https://badgen.net/badge/channel/latest/yellow" alt="Channel: latest" />
### Enhancements
- [Backups] Automatically detect, report and fix cache inconsistencies
### Bug fixes
- [Warm migration] Fix start and delete VMs after a warm migration [#6568](https://github.com/vatesfr/xen-orchestra/issues/6568)
### Released packages
- @xen-orchestra/backups 0.29.2
- @xen-orchestra/proxy 0.26.6
- xo-server 0.107.2
## **5.77.0** (2022-11-30)
### 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 +148,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,14 +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
### 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))
### Packages to release
> When modifying a package, add it here with its release type.
@@ -31,10 +27,4 @@
<!--packages-start-->
- @xen-orchestra/backups-cli major
- @xen-orchestra/log minor
- xo-cli patch
- xo-server 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",
@@ -20,7 +19,7 @@
"getopts": "^2.3.0",
"globby": "^13.1.1",
"handlebars": "^4.7.6",
"husky": "^4.2.5",
"husky": "^8.0.2",
"jest": "^29.0.3",
"lint-staged": "^13.0.3",
"lodash": "^4.17.4",
@@ -28,17 +27,13 @@
"promise-toolbox": "^0.21.0",
"semver": "^7.3.6",
"sorted-object": "^2.0.1",
"vue": "^2.7.14",
"vuepress": "^1.4.1"
},
"engines": {
"node": ">=14",
"yarn": "^1.7.0"
},
"husky": {
"hooks": {
"pre-commit": "lint-staged && scripts/lint-staged.js"
}
},
"jest": {
"moduleNameMapper": {
"^(@vates/[^/]+)$": [
@@ -75,26 +70,34 @@
"testRegex": "\\.spec\\.js$"
},
"lint-staged": {
"*.{md,ts,ts}": "prettier --write"
"*": [
"scripts/run-changed-pkgs.js test",
"prettier --ignore-unknown --write"
],
"*.{{{,c,m}j,t}s{,x},vue}": [
"eslint --ignore-pattern '!*'",
"jest --testRegex='^(?!.*.integ.spec.js$).*.spec.js$' --findRelatedTests --passWithNoTests"
]
},
"private": true,
"scripts": {
"build": "scripts/run-script.js --parallel --concurrency 2 build",
"ci": "yarn && yarn build && 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",
"docs:dev": "NODE_OPTIONS=--openssl-legacy-provider vuepress dev docs",
"docs:build": "NODE_OPTIONS=--openssl-legacy-provider vuepress build docs",
"prepare": "husky install",
"prettify": "prettier --ignore-path .gitignore --write '**/*.{cjs,js,jsx,md,mjs,ts,tsx}'",
"test": "npm run test-lint && npm run test-unit",
"test-integration": "jest \".integ\\.spec\\.js$\"",
"test-lint": "eslint --ignore-path .gitignore .",
"test-unit": "jest \"^(?!.*\\.integ\\.spec\\.js$)\" && scripts/run-script.js test",
"travis-tests": "scripts/travis-tests.js"
"test-lint": "eslint --ignore-path .gitignore --ignore-pattern packages/xo-web .",
"test-unit": "jest \"^(?!.*\\.integ\\.spec\\.js$)\" && scripts/run-script.js --bail test"
},
"workspaces": [
"@*/*",
"packages/*"
]
],
"packageManager": "yarn@1.22.19"
}

View File

@@ -94,5 +94,5 @@ describe('setPropertyClause', () => {
})
it('toString', () => {
assert.equal(ast.toString(), pattern)
assert.equal(ast.toString(), pattern)
})

View File

@@ -2,7 +2,9 @@
// This file has been generated by [index-modules](https://npmjs.com/index-modules)
//
var d = Object.defineProperty
'use strict'
const d = Object.defineProperty
function de(o, n, v) {
d(o, n, { enumerable: true, value: v })
return v
@@ -17,7 +19,7 @@ function dl(o, n, g, a) {
})
}
function r(p) {
var v = require(p)
const v = require(p)
return v && v.__esModule
? v
: typeof v === 'object' || typeof v === 'function'
@@ -32,7 +34,7 @@ function e(p, i) {
}
d(exports, '__esModule', { value: true })
var defaults = de(exports, 'default', {})
const defaults = de(exports, 'default', {})
e('./check.js', 'check')
e('./compare.js', 'compare')
e('./copy.js', 'copy')

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

@@ -95,15 +95,9 @@ test('It rename and unlink a VHDFile', async () => {
await convertFromRawToVhd(rawFileName, vhdFileName)
await Disposable.use(async function* () {
const handler = yield getSyncedHandler({ url: 'file:///' })
const { size } = await fs.stat(vhdFileName)
const targetFileName = `${tempDir}/renamed.vhd`
await VhdAbstract.rename(handler, vhdFileName, targetFileName)
await VhdAbstract.unlink(handler, vhdFileName)
expect(await fs.exists(vhdFileName)).toEqual(false)
const { size: renamedSize } = await fs.stat(targetFileName)
expect(size).toEqual(renamedSize)
await VhdAbstract.unlink(handler, targetFileName)
expect(await fs.exists(targetFileName)).toEqual(false)
})
})
@@ -122,12 +116,8 @@ test('It rename and unlink a VhdDirectory', async () => {
// it should clean an existing directory
await fs.mkdir(targetFileName)
await fs.writeFile(`${targetFileName}/dummy`, 'I exists')
await VhdAbstract.rename(handler, vhdDirectory, targetFileName)
expect(await fs.exists(vhdDirectory)).toEqual(false)
expect(await fs.exists(targetFileName)).toEqual(true)
await VhdAbstract.unlink(handler, `${targetFileName}/dummy`)
expect(await fs.exists(`${targetFileName}/dummy`)).toEqual(false)
await VhdAbstract.unlink(handler, targetFileName)
expect(await fs.exists(targetFileName)).toEqual(false)
})
})
@@ -138,7 +128,6 @@ test('It create , rename and unlink alias', async () => {
const vhdFileName = `${tempDir}/randomfile.vhd`
await convertFromRawToVhd(rawFileName, vhdFileName)
const aliasFileName = `${tempDir}/aliasFileName.alias.vhd`
const aliasFileNameRenamed = `${tempDir}/aliasFileNameRenamed.alias.vhd`
await Disposable.use(async function* () {
const handler = yield getSyncedHandler({ url: 'file:///' })
@@ -146,15 +135,9 @@ test('It create , rename and unlink alias', async () => {
expect(await fs.exists(aliasFileName)).toEqual(true)
expect(await fs.exists(vhdFileName)).toEqual(true)
await VhdAbstract.rename(handler, aliasFileName, aliasFileNameRenamed)
expect(await fs.exists(aliasFileName)).toEqual(false)
expect(await fs.exists(vhdFileName)).toEqual(true)
expect(await fs.exists(aliasFileNameRenamed)).toEqual(true)
await VhdAbstract.unlink(handler, aliasFileNameRenamed)
await VhdAbstract.unlink(handler, aliasFileName)
expect(await fs.exists(aliasFileName)).toEqual(false)
expect(await fs.exists(vhdFileName)).toEqual(false)
expect(await fs.exists(aliasFileNameRenamed)).toEqual(false)
})
})

View File

@@ -200,14 +200,6 @@ exports.VhdAbstract = class VhdAbstract {
}
}
static async rename(handler, sourcePath, targetPath) {
try {
// delete target if it already exists
await VhdAbstract.unlink(handler, targetPath)
} catch (e) {}
await handler.rename(sourcePath, targetPath)
}
static async unlink(handler, path) {
const resolved = await resolveVhdAlias(handler, path)
try {

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 = Date.now()
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 VhdAbstract.rename(handler, 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,69 +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 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()
@@ -211,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

@@ -0,0 +1,5 @@
'use strict'
module.exports = {
ignorePatterns: ['*'],
}

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

@@ -55,3 +55,4 @@ setTimeout(function () {
name: 'Steve',
})
}, 10)
/* eslint-enable no-console */

View File

@@ -1,3 +1,5 @@
/* eslint-disable no-console */
'use strict'
process.on('unhandledRejection', function (error) {
@@ -59,3 +61,5 @@ xo.open()
.then(function () {
return xo.close()
})
/* eslint-enable no-console */

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,3 +1,5 @@
/* eslint-disable no-console */
'use strict'
// This is one of the simplest xo-server's plugin than can be created.
@@ -78,3 +80,5 @@ exports.default = function (opts) {
},
}
}
/* eslint-enable no-console */

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

@@ -6,6 +6,7 @@ function handleHook(type, data) {
const hooks = this._hooks[data.method]?.[type]
if (hooks !== undefined) {
return Promise.all(
// eslint-disable-next-line array-callback-return
hooks.map(({ url, waitForResponse = false }) => {
const promise = this._makeRequest(url, type, data).catch(error => {
log.error('web hook failed', {

View File

@@ -1,7 +1,7 @@
{
"private": true,
"name": "xo-server",
"version": "5.106.1",
"version": "5.107.2",
"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.2",
"@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",

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