Compare commits

...

95 Commits

Author SHA1 Message Date
Florent Beauchamp
737b40e668 fix: don't fail when there is no snapshot 2022-12-23 10:05:36 +01:00
Florent Beauchamp
c5e8bee5f0 fix(vmdk-explorer): hide TLS warning 2022-12-23 10:05:03 +01:00
Florent Beauchamp
67fda0bd42 fix import 2022-12-19 16:45:28 +01:00
Florent Beauchamp
f9cd8d1f2b feat: add published package 2022-12-19 15:50:38 +01:00
Florent Beauchamp
f996feb9cb feat(@xen-orchestra/vmware-explorer): 0.0.2 2022-12-19 15:42:38 +01:00
Florent Beauchamp
1caef5f7fc feat:force channel for proxy 2022-12-19 15:25:18 +01:00
Florent Beauchamp
12ad942ff9 debug 2022-12-19 14:42:16 +01:00
Florent Beauchamp
7314521fcb remove debug 2022-12-19 14:42:16 +01:00
Florent Beauchamp
405f1d2bbf fix handle multiple disks per vm 2022-12-19 14:42:16 +01:00
Florent Beauchamp
ac8b03bc11 missing files 2022-12-19 14:42:16 +01:00
Florent Beauchamp
eb06c8a0be debug 2022-12-19 14:42:16 +01:00
Florent Beauchamp
8a8757072b feat: reuse session 2022-12-19 14:42:16 +01:00
Florent Beauchamp
d0c7284d3b stop before deltas 2022-12-19 14:42:16 +01:00
Florent Beauchamp
37b38a5af1 fix 2022-12-19 14:42:16 +01:00
Florent Beauchamp
a609772415 cleanup 2022-12-19 14:42:16 +01:00
Florent Beauchamp
0876de77f5 wip2 2022-12-19 14:42:16 +01:00
Florent Beauchamp
19b0d5f584 wip 2022-12-19 14:42:16 +01:00
Mathieu
29a38cdf1a feat: technical release (#6586) 2022-12-19 14:30:41 +01:00
Julien Fontanet
960c569e86 fix(CHANGELOG): add missing backups changes
Introduced by f95a20173
2022-12-19 11:40:06 +01:00
Julien Fontanet
fa183fc97e fix(CHANGELOG): add missing Kubernetes changes
Introduced by a1d63118c
2022-12-19 10:42:53 +01:00
Gabriel Gunullu
a1d63118c0 feat(xo-web/recipes/kubernetes): CIDR is no longer necessary (#6583)
Related to 6227349725
2022-12-19 09:42:56 +01:00
Julien Fontanet
f95a20173c fix(backups/{Delta,Full}BackupWriter}): fix this._vmBackupDir access
May fix #6584

Introduced by 45dcb914b.
2022-12-17 10:57:49 +01:00
Mathieu
b82d0fadc3 feat: technical release (#6585) 2022-12-16 16:13:07 +01:00
Julien Fontanet
0635b3316e feat(xo-server/backups): remove merge limitations
Since 30fe9764a, merging range of VHDs is supported via synthetic VHD which limits the perf impact.

It's no longer necessary to limit the number of VHDs per run to merge.
2022-12-16 14:42:05 +01:00
Thierry Goettelmann
113235aec3 feat(lite): new useArrayRemovedItemsHistory composable (#6546) 2022-12-16 11:43:50 +01:00
Mathieu
3921401e96 fix(lite): fix 'not connected to xapi' (#6522)
Introduced by 1c3cad9235
2022-12-16 09:54:43 +01:00
Julien Fontanet
2e514478a4 fix(xo-server/remotes): always remove handler from cache when forgetting 2022-12-15 17:58:14 +01:00
Julien Fontanet
b3d53b230e fix(fs/abstract): use standard naming for logger 2022-12-15 17:58:14 +01:00
Julien Fontanet
45dcb914ba chore(backups/{Mixin,Delta,Full}BackupWriter}): mutualize VM backup dir computation 2022-12-15 17:58:14 +01:00
Mathieu
711087b686 feat(lite): feedback on login page (#6464) 2022-12-15 15:00:46 +01:00
Julien Fontanet
b100a59d1d feat(xapi/VM_snapshot): use ignore_vdis param 2022-12-14 23:36:03 +01:00
Mathieu
109b2b0055 feat(lite): not found views (page/object) (#6410) 2022-12-14 16:47:40 +01:00
Julien Fontanet
9dda99eb20 fix(xo-server/_handleBackupLog): fix sendPassiveCheck condition
Introduced by ba782d269

Fixes https://xcp-ng.org/forum/post/56175
2022-12-14 16:26:43 +01:00
Thierry Goettelmann
fa0f75b474 feat(lite): New UiCardTitle component (#6558) 2022-12-12 15:19:43 +01:00
Julien Fontanet
2d93e0d4be feat(xapi/waitObjectState): better timeout error stacktrace
Create the error synchronously for better stacktrace and debuggability.
2022-12-12 15:11:10 +01:00
Julien Fontanet
fe6406336d feat: release 5.77.2 2022-12-12 11:49:55 +01:00
Julien Fontanet
1037d44089 feat(xo-server): 5.107.3 2022-12-12 11:27:18 +01:00
Julien Fontanet
a8c3669f43 feat(@xen-orchestra/proxy): 0.26.7 2022-12-12 11:26:55 +01:00
Julien Fontanet
d91753aa82 feat(@xen-orchestra/backups): 0.29.3 2022-12-12 11:26:26 +01:00
Julien Fontanet
b548514d44 fix(backups): wait for cache to be updated before running cleanVm (#6580)
Introduced by 191c12413
2022-12-12 09:30:08 +01:00
Julien Fontanet
ba782d2698 fix(xo-server/_handleBackupLog): bail instead of failing if Nagios plugin is not loaded
Introduced by ed34d9cbc
2022-12-08 17:17:31 +01:00
Julien Fontanet
0552dc23a5 chore(CHANGELOG.unreleased): clarify format description 2022-12-08 17:17:31 +01:00
Cécile Morange
574bbbf5ff docs(manage infrastructure): add how to remove a host from pool (#6574)
Co-authored-by: Jon Sands <fohdeesha@gmail.com>
2022-12-08 15:38:02 +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
113 changed files with 2399 additions and 573 deletions

View File

@@ -14,7 +14,7 @@
"url": "https://vates.fr"
},
"license": "ISC",
"version": "0.1.2",
"version": "0.1.3",
"engines": {
"node": ">=8.10"
},
@@ -25,7 +25,7 @@
"dependencies": {
"@vates/multi-key-map": "^0.1.0",
"@xen-orchestra/async-map": "^0.1.2",
"@xen-orchestra/log": "^0.4.0",
"@xen-orchestra/log": "^0.5.0",
"ensure-array": "^1.0.0"
},
"devDependencies": {

View File

@@ -21,7 +21,7 @@
"fuse-native": "^2.2.6",
"lru-cache": "^7.14.0",
"promise-toolbox": "^0.21.0",
"vhd-lib": "^4.1.1"
"vhd-lib": "^4.2.0"
},
"scripts": {
"postversion": "npm publish --access public"

View File

@@ -7,7 +7,7 @@
"type": "git",
"url": "https://github.com/vatesfr/xen-orchestra.git"
},
"version": "0.2.1",
"version": "0.2.2",
"engines": {
"node": ">=14"
},
@@ -17,7 +17,7 @@
},
"dependencies": {
"@vates/decorate-with": "^2.0.0",
"@xen-orchestra/log": "^0.4.0",
"@xen-orchestra/log": "^0.5.0",
"golike-defer": "^0.5.1",
"object-hash": "^2.0.1"
},

View File

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

@@ -232,21 +232,23 @@ class RemoteAdapter {
return promise
}
#removeVmBackupsFromCache(backups) {
for (const [dir, filenames] of Object.entries(
groupBy(
backups.map(_ => _._filename),
dirname
)
)) {
// detached async action, will not reject
this._updateCache(dir + '/cache.json.gz', backups => {
for (const filename of filenames) {
debug('removing cache entry', { entry: filename })
delete backups[filename]
}
})
}
async #removeVmBackupsFromCache(backups) {
await asyncEach(
Object.entries(
groupBy(
backups.map(_ => _._filename),
dirname
)
),
([dir, filenames]) =>
// will not reject
this._updateCache(dir + '/cache.json.gz', backups => {
for (const filename of filenames) {
debug('removing cache entry', { entry: filename })
delete backups[filename]
}
})
)
}
async deleteDeltaVmBackups(backups) {
@@ -255,7 +257,7 @@ class RemoteAdapter {
// this will delete the json, unused VHDs will be detected by `cleanVm`
await asyncMapSettled(backups, ({ _filename }) => handler.unlink(_filename))
this.#removeVmBackupsFromCache(backups)
await this.#removeVmBackupsFromCache(backups)
}
async deleteMetadataBackup(backupId) {
@@ -284,7 +286,7 @@ class RemoteAdapter {
Promise.all([handler.unlink(_filename), handler.unlink(resolveRelativeFromFile(_filename, xva))])
)
this.#removeVmBackupsFromCache(backups)
await this.#removeVmBackupsFromCache(backups)
}
deleteVmBackup(file) {
@@ -508,7 +510,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 +523,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) {
@@ -577,7 +579,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
@@ -590,7 +592,7 @@ class RemoteAdapter {
}
// detached async action, will not reject
this.#writeCache(path, backups)
this._writeCache(path, backups)
return backups
}
@@ -641,7 +643,7 @@ class RemoteAdapter {
})
// will not throw
this._updateCache(this.#getVmBackupsCache(vmUuid), backups => {
await this._updateCache(this.#getVmBackupsCache(vmUuid), backups => {
debug('adding cache entry', { entry: path })
backups[path] = {
...metadata,

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.4",
"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.6.0"
},
"license": "AGPL-3.0-or-later",
"author": {

View File

@@ -11,7 +11,6 @@ const { dirname } = require('path')
const { formatFilenameDate } = require('../_filenameDate.js')
const { getOldEntries } = require('../_getOldEntries.js')
const { getVmBackupDir } = require('../_getVmBackupDir.js')
const { Task } = require('../Task.js')
const { MixinBackupWriter } = require('./_MixinBackupWriter.js')
@@ -29,8 +28,7 @@ exports.DeltaBackupWriter = class DeltaBackupWriter extends MixinBackupWriter(Ab
const backup = this._backup
const adapter = this._adapter
const backupDir = getVmBackupDir(backup.vm.uuid)
const vdisDir = `${backupDir}/vdis/${backup.job.id}`
const vdisDir = `${this._vmBackupDir}/vdis/${backup.job.id}`
await asyncMap(baseUuidToSrcVdi, async ([baseUuid, srcVdi]) => {
let found = false
@@ -143,7 +141,6 @@ exports.DeltaBackupWriter = class DeltaBackupWriter extends MixinBackupWriter(Ab
const jobId = job.id
const handler = adapter.handler
const backupDir = getVmBackupDir(vm.uuid)
// TODO: clean VM backup directory
@@ -177,7 +174,7 @@ exports.DeltaBackupWriter = class DeltaBackupWriter extends MixinBackupWriter(Ab
const { size } = await Task.run({ name: 'transfer' }, async () => {
await Promise.all(
map(deltaExport.vdis, async (vdi, id) => {
const path = `${backupDir}/${vhds[id]}`
const path = `${this._vmBackupDir}/${vhds[id]}`
const isDelta = vdi.other_config['xo:base_delta'] !== undefined
let parentPath

View File

@@ -2,7 +2,6 @@
const { formatFilenameDate } = require('../_filenameDate.js')
const { getOldEntries } = require('../_getOldEntries.js')
const { getVmBackupDir } = require('../_getVmBackupDir.js')
const { Task } = require('../Task.js')
const { MixinBackupWriter } = require('./_MixinBackupWriter.js')
@@ -34,7 +33,6 @@ exports.FullBackupWriter = class FullBackupWriter extends MixinBackupWriter(Abst
const { job, scheduleId, vm } = backup
const adapter = this._adapter
const backupDir = getVmBackupDir(vm.uuid)
// TODO: clean VM backup directory
@@ -47,7 +45,7 @@ exports.FullBackupWriter = class FullBackupWriter extends MixinBackupWriter(Abst
const basename = formatFilenameDate(timestamp)
const dataBasename = basename + '.xva'
const dataFilename = backupDir + '/' + dataBasename
const dataFilename = this._vmBackupDir + '/' + dataBasename
const metadata = {
jobId: job.id,

View File

@@ -16,7 +16,6 @@ const { info, warn } = createLogger('xo:backups:MixinBackupWriter')
exports.MixinBackupWriter = (BaseClass = Object) =>
class MixinBackupWriter extends BaseClass {
#lock
#vmBackupDir
constructor({ remoteId, ...rest }) {
super(rest)
@@ -24,13 +23,13 @@ exports.MixinBackupWriter = (BaseClass = Object) =>
this._adapter = rest.backup.remoteAdapters[remoteId]
this._remoteId = remoteId
this.#vmBackupDir = getVmBackupDir(this._backup.vm.uuid)
this._vmBackupDir = getVmBackupDir(this._backup.vm.uuid)
}
async _cleanVm(options) {
try {
return await Task.run({ name: 'clean-vm' }, () => {
return this._adapter.cleanVm(this.#vmBackupDir, {
return this._adapter.cleanVm(this._vmBackupDir, {
...options,
fixMetadata: true,
logInfo: info,
@@ -50,7 +49,7 @@ exports.MixinBackupWriter = (BaseClass = Object) =>
async beforeBackup() {
const { handler } = this._adapter
const vmBackupDir = this.#vmBackupDir
const vmBackupDir = this._vmBackupDir
await handler.mktree(vmBackupDir)
this.#lock = await handler.lock(vmBackupDir)
}

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

@@ -14,7 +14,7 @@ import { basename, dirname, normalize as normalizePath } from './path'
import { createChecksumStream, validChecksumOfReadStream } from './checksum'
import { DEFAULT_ENCRYPTION_ALGORITHM, _getEncryptor } from './_encryptor'
const { info, warn } = createLogger('@xen-orchestra:fs')
const { info, warn } = createLogger('xo:fs:abstract')
const checksumFile = file => file + '.checksum'
const computeRate = (hrtime, size) => {

View File

@@ -6,6 +6,7 @@
- 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))
- Implement not found page (PR [#6410](https://github.com/vatesfr/xen-orchestra/pull/6410))
## **0.1.0**

View File

@@ -13,6 +13,12 @@
<a :href="url.href" target="_blank" rel="noopener">{{ url.href }}</a>
</li>
</ul>
<template #buttons>
<UiButton color="success" @click="reload">{{
$t("unreachable-hosts-reload-page")
}}</UiButton>
<UiButton @click="clearUnreachableHostsUrls">{{ $t("cancel") }}</UiButton>
</template>
</UiModal>
<div v-if="!xenApiStore.isConnected">
<AppLogin />
@@ -43,6 +49,7 @@ import AppHeader from "@/components/AppHeader.vue";
import AppLogin from "@/components/AppLogin.vue";
import AppTooltips from "@/components/AppTooltips.vue";
import InfraPoolList from "@/components/infra/InfraPoolList.vue";
import UiButton from "@/components/ui/UiButton.vue";
import UiModal from "@/components/ui/UiModal.vue";
import { useChartTheme } from "@/composables/chart-theme.composable";
import { useHostStore } from "@/stores/host.store";
@@ -105,6 +112,7 @@ watch(
);
const isSslModalOpen = computed(() => unreachableHostsUrls.value.length > 0);
const reload = () => window.location.reload();
</script>
<style lang="postcss">

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.8 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -2,15 +2,24 @@
<div class="app-login form-container">
<form @submit.prevent="handleSubmit">
<img alt="XO Lite" src="../assets/logo-title.svg" />
<input v-model="login" name="login" readonly type="text" />
<input
v-model="password"
:readonly="isConnecting"
name="password"
:placeholder="$t('password')"
type="password"
/>
<UiButton :busy="isConnecting" type="submit">
<FormInputWrapper>
<FormInput v-model="login" name="login" readonly type="text" />
</FormInputWrapper>
<FormInputWrapper :error="error">
<FormInput
name="password"
ref="passwordRef"
type="password"
v-model="password"
:placeholder="$t('password')"
:readonly="isConnecting"
/>
</FormInputWrapper>
<UiButton
type="submit"
:busy="isConnecting"
:disabled="password.trim().length < 1"
>
{{ $t("login") }}
</UiButton>
</form>
@@ -19,21 +28,47 @@
<script lang="ts" setup>
import { storeToRefs } from "pinia";
import { onMounted, ref } from "vue";
import { onMounted, ref, watch } from "vue";
import { useI18n } from "vue-i18n";
import FormInput from "@/components/form/FormInput.vue";
import FormInputWrapper from "@/components/form/FormInputWrapper.vue";
import UiButton from "@/components/ui/UiButton.vue";
import { useXenApiStore } from "@/stores/xen-api.store";
const { t } = useI18n();
const xenApiStore = useXenApiStore();
const { isConnecting } = storeToRefs(xenApiStore);
const login = ref("root");
const password = ref("");
const error = ref<string>();
const passwordRef = ref<InstanceType<typeof FormInput>>();
const isInvalidPassword = ref(false);
const focusPasswordInput = () => passwordRef.value?.focus();
onMounted(() => {
xenApiStore.reconnect();
focusPasswordInput();
});
watch(password, () => {
isInvalidPassword.value = false;
error.value = undefined;
});
async function handleSubmit() {
await xenApiStore.connect(login.value, password.value);
try {
await xenApiStore.connect(login.value, password.value);
} catch (err) {
if ((err as Error).message === "SESSION_AUTHENTICATION_FAILED") {
focusPasswordInput();
isInvalidPassword.value = true;
error.value = t("password-invalid");
} else {
error.value = t("error-occured");
console.error(err);
}
}
}
</script>
@@ -50,6 +85,7 @@ async function handleSubmit() {
form {
display: flex;
font-size: 2rem;
min-width: 30em;
max-width: 100%;
align-items: center;
@@ -72,12 +108,6 @@ img {
margin-bottom: 5rem;
}
label {
font-size: 120%;
font-weight: bold;
margin: 1.5rem 0 0.5rem 0;
}
input {
width: 45rem;
max-width: 100%;
@@ -89,6 +119,6 @@ input {
}
button {
margin-top: 3rem;
margin-top: 2rem;
}
</style>

View File

@@ -0,0 +1,47 @@
<template>
<div class="wrapper-spinner" v-if="!store.isReady">
<UiSpinner class="spinner" />
</div>
<ObjectNotFoundView :id="id" v-else-if="isRecordNotFound" />
<slot v-else />
</template>
<script lang="ts" setup>
import ObjectNotFoundView from "@/views/ObjectNotFoundView.vue";
import UiSpinner from "@/components/ui/UiSpinner.vue";
import { useHostStore } from "@/stores/host.store";
import { useVmStore } from "@/stores/vm.store";
import { computed } from "vue";
import { useRouter } from "vue-router";
const storeByType = {
vm: useVmStore,
host: useHostStore,
};
const props = defineProps<{ objectType: "vm" | "host"; id?: string }>();
const store = storeByType[props.objectType]();
const { currentRoute } = useRouter();
const id = computed(
() => props.id ?? (currentRoute.value.params.uuid as string)
);
const isRecordNotFound = computed(
() => store.isReady && !store.hasRecordByUuid(id.value)
);
</script>
<style scoped>
.wrapper-spinner {
display: flex;
height: 100%;
}
.spinner {
color: var(--color-extra-blue-base);
display: flex;
margin: auto;
width: 10rem;
height: 10rem;
}
</style>

View File

@@ -1,68 +0,0 @@
<template>
<div class="progress-bar-component">
<div class="progress-bar">
<div class="progress-bar-fill" />
</div>
<div class="legend" v-if="label !== undefined">
<span class="circle" />
{{ label }}
<UiBadge class="badge">{{ 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>
.legend {
text-align: right;
margin: 1.6em 0;
}
.badge {
font-size: 0.9em;
font-weight: 700;
}
.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

@@ -1,18 +1,22 @@
<template>
<div>
<div class="header">
<slot name="header" />
</div>
<template v-if="data !== undefined">
<ProgressBar
<div
v-for="item in computedData.sortedArray"
:key="item.id"
:value="item.value"
:label="item.label"
:badge-label="item.badgeLabel"
/>
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>
<div class="footer">
<slot name="footer" :total-percent="computedData.totalPercentUsage" />
<slot :total-percent="computedData.totalPercentUsage" name="footer" />
</div>
</template>
<UiSpinner v-else class="spinner" />
@@ -20,8 +24,9 @@
</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 +38,7 @@ interface Data {
}
interface Props {
data?: Array<Data>;
data?: Data[];
nItems?: number;
}
@@ -60,15 +65,6 @@ const computedData = computed(() => {
</script>
<style scoped>
.header {
color: var(--color-extra-blue-base);
display: flex;
justify-content: space-between;
border-bottom: 1px solid var(--color-extra-blue-base);
margin-bottom: 2rem;
font-size: 16px;
font-weight: 700;
}
.footer {
display: flex;
justify-content: space-between;
@@ -84,23 +80,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

@@ -6,6 +6,7 @@
:class="inputClass"
:disabled="disabled || isLabelDisabled"
class="input"
ref="inputElement"
v-bind="$attrs"
/>
<template v-else>
@@ -14,6 +15,7 @@
:class="inputClass"
:disabled="disabled || isLabelDisabled"
class="select"
ref="inputElement"
v-bind="$attrs"
>
<slot />
@@ -70,6 +72,8 @@ interface Props extends Omit<InputHTMLAttributes, ""> {
const props = withDefaults(defineProps<Props>(), { color: "info" });
const inputElement = ref();
const emit = defineEmits<{
(event: "update:modelValue", value: any): void;
}>();
@@ -78,6 +82,10 @@ const value = useVModel(props, "modelValue", emit);
const empty = computed(() => isEmpty(props.modelValue));
const isSelect = inject("isSelect", false);
const isLabelDisabled = inject("isLabelDisabled", ref(false));
const color = inject(
"color",
computed(() => undefined)
);
const wrapperClass = computed(() => [
isSelect ? "form-select" : "form-input",
@@ -88,13 +96,19 @@ const wrapperClass = computed(() => [
]);
const inputClass = computed(() => [
props.color,
color.value ?? props.color,
{
right: props.right,
"has-before": props.before !== undefined,
"has-after": props.after !== undefined,
},
]);
const focus = () => inputElement.value.focus();
defineExpose({
focus,
});
</script>
<style lang="postcss" scoped>

View File

@@ -0,0 +1,96 @@
<template>
<div class="wrapper">
<label
v-if="$slots.label"
class="form-label"
:class="{ disabled, ...formInputWrapperClass }"
>
<slot />
</label>
<slot />
<p v-if="hasError || hasWarning" :class="formInputWrapperClass">
<UiIcon :icon="faCircleExclamation" v-if="hasError" />{{
error ?? warning
}}
</p>
</div>
</template>
<script lang="ts" setup>
import { computed, provide, useSlots } from "vue";
import { faCircleExclamation } from "@fortawesome/free-solid-svg-icons";
import UiIcon from "@/components/ui/UiIcon.vue";
const slots = useSlots();
const props = defineProps<{
disabled?: boolean;
error?: string;
warning?: string;
}>();
provide("hasLabel", slots.label !== undefined);
provide(
"isLabelDisabled",
computed(() => props.disabled)
);
const hasError = computed(
() => props.error !== undefined && props.error.trim() !== ""
);
const hasWarning = computed(
() => props.warning !== undefined && props.warning.trim() !== ""
);
provide(
"color",
computed(() =>
hasError.value ? "error" : hasWarning.value ? "warning" : undefined
)
);
const formInputWrapperClass = computed(() => ({
error: hasError.value,
warning: !hasError.value && hasWarning.value,
}));
</script>
<style lang="postcss" scoped>
.wrapper {
display: flex;
flex-direction: column;
}
.wrapper :deep(.input) {
margin-bottom: 1rem;
}
.form-label {
font-size: 1.6rem;
display: inline-flex;
align-items: center;
gap: 0.625em;
&.disabled {
cursor: not-allowed;
color: var(--color-blue-scale-300);
}
}
p.error,
p.warning {
font-size: 0.65em;
margin-bottom: 1rem;
}
.error {
color: var(--color-red-vates-base);
}
.warning {
color: var(--color-orange-world-base);
}
p svg {
margin-right: 0.4em;
}
</style>

View File

@@ -1,33 +0,0 @@
<template>
<label :class="{ disabled }" class="form-label">
<slot />
</label>
</template>
<script lang="ts" setup>
import { computed, provide } from "vue";
const props = defineProps<{
disabled?: boolean;
}>();
provide("hasLabel", true);
provide(
"isLabelDisabled",
computed(() => props.disabled)
);
</script>
<style lang="postcss" scoped>
.form-label {
font-size: 1.6rem;
display: inline-flex;
align-items: center;
gap: 0.625em;
&.disabled {
cursor: not-allowed;
color: var(--color-blue-scale-300);
}
}
</style>

View File

@@ -1,6 +1,6 @@
<template>
<UiCard>
<UiTitle type="h4">{{ $t("cpu-usage") }}</UiTitle>
<UiCardTitle>{{ $t("cpu-usage") }}</UiCardTitle>
<HostsCpuUsage />
<VmsCpuUsage />
</UiCard>
@@ -9,5 +9,5 @@
import HostsCpuUsage from "@/components/pool/dashboard/cpuUsage/HostsCpuUsage.vue";
import VmsCpuUsage from "@/components/pool/dashboard/cpuUsage/VmsCpuUsage.vue";
import UiCard from "@/components/ui/UiCard.vue";
import UiTitle from "@/components/ui/UiTitle.vue";
import UiCardTitle from "@/components/ui/UiCardTitle.vue";
</script>

View File

@@ -1,6 +1,6 @@
<template>
<UiCard>
<UiTitle type="h4">{{ $t("ram-usage") }}</UiTitle>
<UiCardTitle>{{ $t("ram-usage") }}</UiCardTitle>
<HostsRamUsage />
<VmsRamUsage />
</UiCard>
@@ -10,5 +10,5 @@
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";
import UiCardTitle from "@/components/ui/UiCardTitle.vue";
</script>

View File

@@ -1,17 +1,17 @@
<template>
<UiCard>
<UiTitle type="h4">{{ $t("status") }}</UiTitle>
<UiCardTitle>{{ $t("status") }}</UiCardTitle>
<template v-if="isReady">
<PoolDashboardStatusItem
:active="activeHostsCount"
:total="totalHostsCount"
:label="$t('hosts')"
:total="totalHostsCount"
/>
<UiSeparator />
<PoolDashboardStatusItem
:active="activeVmsCount"
:total="totalVmsCount"
:label="$t('vms')"
:total="totalVmsCount"
/>
</template>
<UiSpinner v-else class="spinner" />
@@ -19,14 +19,14 @@
</template>
<script lang="ts" setup>
import { computed } from "vue";
import PoolDashboardStatusItem from "@/components/pool/dashboard/PoolDashboardStatusItem.vue";
import UiCard from "@/components/ui/UiCard.vue";
import UiCardTitle from "@/components/ui/UiCardTitle.vue";
import UiSeparator from "@/components/ui/UiSeparator.vue";
import UiSpinner from "@/components/ui/UiSpinner.vue";
import UiTitle from "@/components/ui/UiTitle.vue";
import { useHostMetricsStore } from "@/stores/host-metrics.store";
import { useVmStore } from "@/stores/vm.store";
import { computed } from "vue";
const vmStore = useVmStore();
const hostMetricsStore = useHostMetricsStore();

View File

@@ -1,11 +1,13 @@
<template>
<UiCard>
<UiTitle type="h4">{{ $t("storage-usage") }}</UiTitle>
<UsageBar :data="srStore.isReady ? data.result : undefined" :nItems="N_ITEMS">
<template #header>
<span>{{ $t("storage") }}</span>
<span>{{ $t("top-#", { n: N_ITEMS }) }}</span>
</template>
<UiCardTitle
:left="$t('storage-usage')"
:right="$t('top-#', { n: N_ITEMS })"
/>
<UsageBar
:data="srStore.isReady ? data.result : undefined"
:nItems="N_ITEMS"
>
<template #footer v-if="showFooter">
<div class="footer-card">
<p>{{ $t("total-used") }}:</p>
@@ -31,10 +33,10 @@
</template>
<script lang="ts" setup>
import UiCardTitle from "@/components/ui/UiCardTitle.vue";
import { computed } from "vue";
import UsageBar from "@/components/UsageBar.vue";
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";

View File

@@ -1,19 +1,20 @@
<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>
<UiCardTitle
subtitle
:left="$t('hosts')"
:right="$t('top-#', { n: N_ITEMS })"
/>
<UsageBar :data="statFetched ? data : undefined" :n-items="N_ITEMS" />
</template>
<script lang="ts" setup>
import { type ComputedRef, computed, inject } from "vue";
import UiCardTitle from "@/components/ui/UiCardTitle.vue";
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";
import { computed, type ComputedRef, inject } from "vue";
const stats = inject<ComputedRef<Stat<HostStats>[]>>(
"hostStats",

View File

@@ -1,13 +1,14 @@
<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>
<UiCardTitle
subtitle
:left="$t('vms')"
:right="$t('top-#', { n: N_ITEMS })"
/>
<UsageBar :data="statFetched ? data : undefined" :n-items="N_ITEMS" />
</template>
<script lang="ts" setup>
import UiCardTitle from "@/components/ui/UiCardTitle.vue";
import { type ComputedRef, computed, inject } from "vue";
import UsageBar from "@/components/UsageBar.vue";
import type { Stat } from "@/composables/fetch-stats.composable";

View File

@@ -1,13 +1,14 @@
<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>
<UiCardTitle
subtitle
:left="$t('hosts')"
:right="$t('top-#', { n: N_ITEMS })"
/>
<UsageBar :data="statFetched ? data : undefined" :n-items="N_ITEMS" />
</template>
<script lang="ts" setup>
import UiCardTitle from "@/components/ui/UiCardTitle.vue";
import { type ComputedRef, computed, inject } from "vue";
import UsageBar from "@/components/UsageBar.vue";
import type { Stat } from "@/composables/fetch-stats.composable";

View File

@@ -1,13 +1,14 @@
<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>
<UiCardTitle
subtitle
:left="$t('vms')"
:right="$t('top-#', { n: N_ITEMS })"
/>
<UsageBar :data="statFetched ? data : undefined" :n-items="N_ITEMS" />
</template>
<script lang="ts" setup>
import UiCardTitle from "@/components/ui/UiCardTitle.vue";
import { type ComputedRef, computed, inject } from "vue";
import UsageBar from "@/components/UsageBar.vue";
import type { Stat } from "@/composables/fetch-stats.composable";

View File

@@ -0,0 +1,65 @@
<template>
<div :class="{ subtitle }" class="ui-section-title">
<component
:is="subtitle ? 'h5' : 'h4'"
v-if="$slots.default || left"
class="left"
>
<slot>{{ left }}</slot>
</component>
<component
:is="subtitle ? 'h6' : 'h5'"
v-if="$slots.right || right"
class="right"
>
<slot name="right">{{ right }}</slot>
</component>
</div>
</template>
<script lang="ts" setup>
defineProps<{
subtitle?: boolean;
left?: string;
right?: string;
}>();
</script>
<style lang="postcss" scoped>
.ui-section-title {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 2rem;
--section-title-left-size: 2rem;
--section-title-left-color: var(--color-blue-scale-100);
--section-title-left-weight: 500;
--section-title-right-size: 1.6rem;
--section-title-right-color: var(--color-extra-blue-base);
--section-title-right-weight: 700;
&.subtitle {
border-bottom: 1px solid var(--color-extra-blue-base);
--section-title-left-size: 1.6rem;
--section-title-left-color: var(--color-extra-blue-base);
--section-title-left-weight: 700;
--section-title-right-size: 1.4rem;
--section-title-right-color: var(--color-extra-blue-base);
--section-title-right-weight: 400;
}
}
.left {
font-size: var(--section-title-left-size);
font-weight: var(--section-title-left-weight);
color: var(--section-title-left-color);
}
.right {
font-size: var(--section-title-right-size);
font-weight: var(--section-title-right-weight);
color: var(--section-title-right-color);
}
</style>

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

@@ -0,0 +1,41 @@
# useArrayRemovedItemsHistory composable
This composable allows you to keep a history of each removed item of an array.
## Usage
```typescript
const myArray = ref([]);
const history = useArrayRemovedItemsHistory(myArray)
myArray.push('A'); // myArray = ['A']; history = []
myArray.push('B'); // myArray = ['A', 'B']; history = []
myArray.shift(); // myArray = ['B']; history = ['A']
```
You can limit the number of items to keep in history:
```typescript
const myArray = ref([]);
const history = useArrayRemovedItemsHistory(myArray, 30);
```
Be careful when using an array of objects which is likely to be replaced (instead of being altered):
```typescript
const myArray = ref([]);
const history = useArrayRemovedItemsHistory(myArray);
myArray.value = [{ id: 'foo' }, { id: 'bar' }];
myArray.value = [{ id: 'bar' }, { id: 'baz' }]; // history = [{ id: 'foo' }, { id: 'bar' }]
```
In this case, `{ id: 'bar' }` is detected as removed since in JavaScript `{ id: 'bar' } !== { id: 'bar' }`.
You must therefore use an identity function as third parameter to return the value to be used to detect deletion:
```typescript
const myArray = ref<{ id: string }[]>([]);
const history = useArrayRemovedItemsHistory(myArray, undefined, (item) => item.id);
myArray.value = [{ id: 'foo' }, { id: 'bar' }];
myArray.value = [{ id: 'bar' }, { id: 'baz' }]; // history = [{ id: 'foo' }]
```

View File

@@ -0,0 +1,30 @@
import { differenceBy } from "lodash-es";
import { type Ref, ref, unref, watch } from "vue";
export default function useArrayRemovedItemsHistory<T>(
list: Ref<T[]>,
limit = Infinity,
iteratee: (item: T) => unknown = (item) => item
) {
const currentList: Ref<T[]> = ref([]);
const history: Ref<T[]> = ref([]);
watch(
list,
(updatedList) => {
currentList.value = [...updatedList];
},
{ deep: true }
);
watch(currentList, (nextList, previousList) => {
const removedItems = differenceBy(previousList, nextList, iteratee);
history.value.push(...removedItems);
const currentLimit = unref(limit);
if (history.value.length > currentLimit) {
history.value.slice(-currentLimit);
}
});
return history;
}

View File

@@ -38,11 +38,12 @@ export default function useFetchStats<T extends XenApiHost | XenApiVm, S>(
const newStats = (await STORES_BY_OBJECT_TYPE[type]().getStats(
object.uuid,
granularity
)) as XapiStatsResponse<S>;
)) as XapiStatsResponse<S> | undefined;
stats.value.get(object.uuid)!.stats = newStats.stats;
await promiseTimeout(newStats.interval * 1000);
if (newStats !== undefined) {
stats.value.get(object.uuid)!.stats = newStats.stats;
await promiseTimeout(newStats.interval * 1000);
}
},
0,
{ immediate: true }

View File

@@ -9,6 +9,7 @@
"ascending": "ascending",
"available-properties-for-advanced-filter": "Available properties for advanced filter:",
"backup": "Backup",
"back-pool-dashboard": "Go back to your Pool dashboard",
"cancel": "Cancel",
"change-power-state": "Change power state",
"community": "Community",
@@ -23,6 +24,7 @@
"descending": "descending",
"display": "Display",
"edit-config": "Edit config",
"error-occured": "An error has occurred",
"export": "Export",
"export-table-to": "Export table to {type}",
"export-vms": "Export VMs",
@@ -36,8 +38,11 @@
"network": "Network",
"news": "News",
"news-name": "{name} news",
"object-not-found": "Object {id} can't be found…",
"or": "Or",
"page-not-found": "This page is not to be found…",
"password": "Password",
"password-invalid": "Password invalid",
"property": "Property",
"ram-usage":"RAM usage",
"send-us-feedback": "Send us feedback",
@@ -55,6 +60,7 @@
"total-free": "Total free",
"total-used": "Total used",
"unreachable-hosts": "Unreachable hosts",
"unreachable-hosts-reload-page": "Done, reload the page",
"version": "Version",
"vms": "VMs"
}

View File

@@ -9,6 +9,7 @@
"ascending": "ascendant",
"available-properties-for-advanced-filter": "Propriétés disponibles pour le filtrage avancé :",
"backup": "Sauvegarde",
"back-pool-dashboard": "Revenez au tableau de bord de votre pool",
"cancel": "Annuler",
"change-power-state": "Changer l'état d'alimentation",
"community": "Communauté",
@@ -23,6 +24,7 @@
"descending": "descendant",
"display": "Affichage",
"edit-config": "Modifier config",
"error-occured": "Une erreur est survenue",
"export": "Exporter",
"export-table-to": "Exporter le tableau en {type}",
"export-vms": "Exporter les VMs",
@@ -36,8 +38,11 @@
"network": "Réseau",
"news": "Actualités",
"news-name": "Actualités {name}",
"object-not-found": "L'objet {id} est introuvable…",
"or": "Ou",
"page-not-found": "Cette page est introuvable…",
"password": "Mot de passe",
"password-invalid": "Mot de passe incorrect",
"property": "Propriété",
"ram-usage":"Utilisation de la RAM",
"send-us-feedback": "Envoyez-nous vos commentaires",
@@ -55,6 +60,7 @@
"total-free": "Total libre",
"total-used": "Total utilisé",
"unreachable-hosts": "Hôtes inaccessibles",
"unreachable-hosts-reload-page": "C'est fait. Rafraîchir la page",
"version": "Version",
"vms": "VMs"
}

View File

@@ -1,6 +1,7 @@
import { createRouter, createWebHashHistory } from "vue-router";
import pool from "@/router/pool";
import HomeView from "@/views/HomeView.vue";
import PageNotFoundView from "@/views/PageNotFoundView.vue";
import HostDashboardView from "@/views/host/HostDashboardView.vue";
import HostRootView from "@/views/host/HostRootView.vue";
import SettingsView from "@/views/settings/SettingsView.vue";
@@ -43,6 +44,11 @@ const router = createRouter({
},
],
},
{
path: "/:pathMatch(.*)*",
name: "notFound",
component: PageNotFoundView,
},
],
});

View File

@@ -1,3 +1,4 @@
import { computed } from "vue";
import { sortRecordsByNameLabel } from "@/libs/utils";
import type { GRANULARITY } from "@/libs/xapi-stats";
import type { XenApiHost } from "@/libs/xen-api";
@@ -6,6 +7,10 @@ import { useXenApiStore } from "@/stores/xen-api.store";
import { defineStore } from "pinia";
export const useHostStore = defineStore("host", () => {
const xenApiStore = useXenApiStore();
const xapiStats = computed(() =>
xenApiStore.isConnected ? xenApiStore.getXapiStats() : undefined
);
const recordContext = createRecordContext<XenApiHost>("host", {
sort: sortRecordsByNameLabel,
});
@@ -15,7 +20,7 @@ export const useHostStore = defineStore("host", () => {
if (host === undefined) {
throw new Error(`Host ${id} could not be found.`);
}
return useXenApiStore().getXapiStats()._getAndUpdateStats({
return xapiStats.value?._getAndUpdateStats({
host,
uuid: host.uuid,
granularity,

View File

@@ -62,6 +62,9 @@ export function createRecordContext<T extends XenApiRecord>(
const getRecordByUuid = (uuid: string) =>
useRecordsStore().getRecordByUuid<T>(uuid);
const hasRecordByUuid = (uuid: string) =>
useRecordsStore().hasRecordByUuid(uuid);
return {
init,
opaqueRefs,
@@ -69,5 +72,6 @@ export function createRecordContext<T extends XenApiRecord>(
getRecordByUuid,
isReady,
allRecords,
hasRecordByUuid,
};
}

View File

@@ -77,6 +77,10 @@ export const useRecordsStore = defineStore("records", () => {
return opaqueRefsByObjectType.get(objectType) || new Set();
}
function hasRecordByUuid(uuid: string): boolean {
return uuidToOpaqueRefMapping.has(uuid);
}
return {
loadRecords,
addOrReplaceRecord,
@@ -84,5 +88,6 @@ export const useRecordsStore = defineStore("records", () => {
getRecord,
getRecordsOpaqueRefs,
getRecordByUuid,
hasRecordByUuid,
};
});

View File

@@ -8,7 +8,11 @@ import { defineStore } from "pinia";
import { computed } from "vue";
export const useVmStore = defineStore("vm", () => {
const xenApiStore = useXenApiStore();
const hostStore = useHostStore();
const xapiStats = computed(() =>
xenApiStore.isConnected ? xenApiStore.getXapiStats() : undefined
);
const baseVmContext = createRecordContext<XenApiVm>("VM", {
filter: (vm) =>
!vm.is_a_snapshot && !vm.is_a_template && !vm.is_control_domain,
@@ -41,7 +45,7 @@ export const useVmStore = defineStore("vm", () => {
throw new Error(`VM ${id} is halted or host could not be found.`);
}
return useXenApiStore().getXapiStats()._getAndUpdateStats({
return xapiStats.value?._getAndUpdateStats({
host,
uuid: vm.uuid,
granularity,

View File

@@ -91,6 +91,7 @@ export const useXenApiStore = defineStore("xen-api", () => {
}
async function connect(username: string, password: string) {
isConnecting.value = true;
try {
currentSessionId.value = await xenApi.connectWithPassword(
username,

View File

@@ -0,0 +1,44 @@
<template>
<div>
<img alt="Not found" src="../assets/object-not-found.svg" />
<p class="text">{{ $t("object-not-found", { id }) }}</p>
<UiButton @click="router.push({ name: 'home' })">{{
$t("back-pool-dashboard")
}}</UiButton>
</div>
</template>
<script setup lang="ts">
import { useRouter } from "vue-router";
import UiButton from "@/components/ui/UiButton.vue";
defineProps<{
id: string;
}>();
const router = useRouter();
</script>
<style lang="postcss" scoped>
div {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
img {
width: 30%;
}
.text {
color: var(--color-extra-blue-base);
font-size: 36px;
font-weight: 400;
line-height: 150%;
margin: 0.5em;
text-align: center;
}
</style>

View File

@@ -0,0 +1,49 @@
<template>
<div>
<img alt="Not found" src="../assets/page-not-found.svg" />
<p class="numeric">404</p>
<p class="text">{{ $t("page-not-found") }}</p>
<UiButton @click="router.push({ name: 'home' })">{{
$t("back-pool-dashboard")
}}</UiButton>
</div>
</template>
<script setup lang="ts">
import { useRouter } from "vue-router";
import UiButton from "@/components/ui/UiButton.vue";
const router = useRouter();
</script>
<style lang="postcss" scoped>
div {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
img {
width: 30%;
}
.numeric {
color: var(--color-extra-blue-base);
font-size: 96px;
font-weight: 900;
letter-spacing: 1em;
line-height: 100%;
margin-right: -1em;
text-align: center;
}
.text {
color: var(--color-extra-blue-base);
font-size: 36px;
font-weight: 400;
line-height: 150%;
margin: 0.5em;
text-align: center;
}
</style>

View File

@@ -1,5 +1,9 @@
<template>
<RouterView />
<ObjectNotFoundWrapper object-type="host">
<RouterView />
</ObjectNotFoundWrapper>
</template>
<script lang="ts" setup></script>
<script lang="ts" setup>
import ObjectNotFoundWrapper from "@/components/ObjectNotFoundWrapper.vue";
</script>

View File

@@ -2,7 +2,7 @@
<TitleBar :icon="faGear">{{ $t("settings") }}</TitleBar>
<div class="card-view">
<UiCard class="group">
<UiTitle type="h4">Xen Orchestra Lite</UiTitle>
<UiCardTitle>Xen Orchestra Lite</UiCardTitle>
<UiKeyValueList>
<UiKeyValueRow>
<template #key>{{ $t("version") }}</template>
@@ -50,24 +50,22 @@
</UiKeyValueList>
</UiCard>
<UiCard class="group">
<UiTitle type="h4">{{ $t("display") }}</UiTitle>
<UiCardTitle>{{ $t("display") }}</UiCardTitle>
<UiKeyValueList>
<UiKeyValueRow>
<template #key>{{ $t("appearance") }}</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>
<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>
</template>
</UiKeyValueRow>
</UiKeyValueList>
</UiCard>
<UiCard class="group">
<UiTitle type="h4">{{ $t("language") }}</UiTitle>
<UiCardTitle>{{ $t("language") }}</UiCardTitle>
<UiKeyValueList>
<UiKeyValueRow>
<template #value>
@@ -91,6 +89,7 @@
<script lang="ts" setup>
import FormSelect from "@/components/form/FormSelect.vue";
import UiCardTitle from "@/components/ui/UiCardTitle.vue";
import { useUiStore } from "@/stores/ui.store";
import { storeToRefs } from "pinia";
import { watch } from "vue";
@@ -99,11 +98,9 @@ import { locales } from "@/i18n";
import { faEarthAmericas, faGear } from "@fortawesome/free-solid-svg-icons";
import FormWidget from "@/components/FormWidget.vue";
import TitleBar from "@/components/TitleBar.vue";
import FormLabel from "@/components/form/FormLabel.vue";
import UiCard from "@/components/ui/UiCard.vue";
import UiKeyValueList from "@/components/ui/UiKeyValueList.vue";
import UiKeyValueRow from "@/components/ui/UiKeyValueRow.vue";
import UiTitle from "@/components/ui/UiTitle.vue";
const version = XO_LITE_VERSION;
const gitHead = XO_LITE_GIT_HEAD;

View File

@@ -1,8 +1,11 @@
<template>
<RouterView />
<ObjectNotFoundWrapper object-type="vm">
<RouterView />
</ObjectNotFoundWrapper>
</template>
<script lang="ts" setup>
import ObjectNotFoundWrapper from "@/components/ObjectNotFoundWrapper.vue";
import { watchEffect } from "vue";
import { useRoute } from "vue-router";
import { useUiStore } from "@/stores/ui.store";

View File

@@ -1,7 +1,7 @@
{
"private": false,
"name": "@xen-orchestra/log",
"version": "0.4.0",
"version": "0.5.0",
"license": "ISC",
"description": "Logging system with decoupled producers/consumer",
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/log",

View File

@@ -14,7 +14,7 @@
"url": "https://vates.fr"
},
"license": "AGPL-3.0-or-later",
"version": "0.8.1",
"version": "0.8.2",
"engines": {
"node": ">=15.6"
},
@@ -22,7 +22,7 @@
"@vates/event-listeners-manager": "^1.0.1",
"@vates/parse-duration": "^0.1.1",
"@xen-orchestra/emit-async": "^1.0.0",
"@xen-orchestra/log": "^0.4.0",
"@xen-orchestra/log": "^0.5.0",
"acme-client": "^5.0.0",
"app-conf": "^2.3.0",
"lodash": "^4.17.21",

View File

@@ -1,7 +1,7 @@
{
"private": true,
"name": "@xen-orchestra/proxy",
"version": "0.26.4",
"version": "0.26.9",
"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.4",
"@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.6.0",
"ajv": "^8.0.3",
"app-conf": "^2.3.0",
"async-iterator-to-stream": "^1.1.0",

View File

@@ -43,7 +43,7 @@
"pw": "^0.0.4",
"xdg-basedir": "^4.0.0",
"xo-lib": "^0.11.1",
"xo-vmdk-to-vhd": "^2.4.3"
"xo-vmdk-to-vhd": "^2.5.0"
},
"devDependencies": {
"@babel/cli": "^7.0.0",

View File

@@ -0,0 +1 @@
../../scripts/npmignore

View File

@@ -0,0 +1,154 @@
import { notEqual, strictEqual } from 'node:assert'
import { VhdAbstract } from 'vhd-lib'
import { createFooter, createHeader } from 'vhd-lib/_createFooterHeader.js'
import _computeGeometryForSize from 'vhd-lib/_computeGeometryForSize.js'
import { DISK_TYPES, FOOTER_SIZE } from 'vhd-lib/_constants.js'
import { unpackFooter, unpackHeader } from 'vhd-lib/Vhd/_utils.js'
export default class VhdCowd extends VhdAbstract {
#esxi
#datastore
#parentFileName
#path
#header
#footer
#grainDirectory
static async open(esxi, datastore, path) {
const vhd = new VhdCowd(esxi, datastore, path)
await vhd.readHeaderAndFooter()
return vhd
}
constructor(esxi, datastore, path, parentFileName) {
super()
this.#esxi = esxi
this.#path = path
this.#datastore = datastore
this.#parentFileName = parentFileName
}
get header() {
return this.#header
}
get footer() {
return this.#footer
}
containsBlock(blockId) {
notEqual(this.#grainDirectory, undefined, "bat must be loaded to use contain blocks'")
// only check if a grain table exist for on of the sector of the block
// the great news is that a grain size has 4096 entries of 512B = 2M
// and a vhd block is also 2M
// so we only need to check if a grain table exists (it's not created without data)
return this.#grainDirectory.readInt32LE(blockId * 4) !== 0
}
async #read(start, end) {
return (await this.#esxi.download(this.#datastore, this.#path, `${start}-${end}`)).buffer()
}
async readHeaderAndFooter(checkSecondFooter = true) {
const buffer = await this.#read(0, 2048)
strictEqual(buffer.slice(0, 4).toString('ascii'), 'COWD')
strictEqual(buffer.readInt32LE(4), 1) // version
strictEqual(buffer.readInt32LE(8), 3) // flags
const sectorCapacity = buffer.readInt32LE(12)
// const sectorGrainNumber = buffer.readInt32LE(16)
strictEqual(buffer.readInt32LE(20), 4) // grain directory position in sectors
// const nbGrainDirectoryEntries = buffer.readInt32LE(24)
// const nextFreeSector = buffer.readInt32LE(28)
const size = sectorCapacity * 512
// a grain directory entry represent a grain table
// a grain table can adresse, at most 4096 grain of 512 B
this.#header = unpackHeader(createHeader(Math.ceil(size / (4096 * 512))))
this.#header.parentUnicodeName = this.#parentFileName
const geometry = _computeGeometryForSize(size)
const actualSize = geometry.actualSize
this.#footer = unpackFooter(
createFooter(
actualSize,
Math.floor(Date.now() / 1000),
geometry,
FOOTER_SIZE,
this.#parentFileName ? DISK_TYPES.DIFFERENCING : DISK_TYPES.DYNAMIC
)
)
}
async readBlockAllocationTable() {
const nbBlocks = this.header.maxTableEntries
this.#grainDirectory = await this.#read(2048, 2048 + nbBlocks * 4 - 1)
}
async readBlock(blockId) {
const sectorOffset = this.#grainDirectory.readInt32LE(blockId * 4)
if (sectorOffset === 1) {
return Promise.resolve(Buffer.alloc(4096 * 512, 0))
}
const offset = sectorOffset * 512
const graintable = await this.#read(offset, offset + 2048 - 1)
const buf = Buffer.concat([
Buffer.alloc(512, 255), // vhd block bitmap,
Buffer.alloc(512 * 4096, 0), // empty data
])
// we have no guaranty that data are order or contiguous
// let's construct ranges to limit the number of queries
const fileOffsetToIndexInGrainTable = {}
let nbNonEmptyGrain = 0
for (let i = 0; i < graintable.length / 4; i++) {
const grainOffset = graintable.readInt32LE(i * 4)
if (grainOffset !== 0) {
// non empty grain
fileOffsetToIndexInGrainTable[grainOffset] = i
nbNonEmptyGrain++
}
}
// grain table exists but only contains empty grains
if (nbNonEmptyGrain === 0) {
return {
id: blockId,
bitmap: buf.slice(0, this.bitmapSize),
data: buf.slice(this.bitmapSize),
buffer: buf,
}
}
const offsets = Object.keys(fileOffsetToIndexInGrainTable).map(offset => parseInt(offset))
offsets.sort((a, b) => a - b)
let startOffset = offsets[0]
const ranges = []
const OVERPROVISION = 3
for (let i = 1; i < offsets.length; i++) {
if (offsets[i - 1] + OVERPROVISION < offsets[i]) {
ranges.push({ startOffset, endOffset: offsets[i - 1] })
startOffset = offsets[i]
}
}
ranges.push({ startOffset, endOffset: offsets[offsets.length - 1] })
for (const { startOffset, endOffset } of ranges) {
const startIndex = fileOffsetToIndexInGrainTable[startOffset]
const startInBlock = startIndex * 512 + 512 /* block bitmap */
const sectors = await this.#read(startOffset * 512, endOffset * 512 - 1)
// @todo : if overprovision > 1 , it may copy random data from the vmdk
sectors.copy(buf, startInBlock)
}
return {
id: blockId,
bitmap: buf.slice(0, 512),
data: buf.slice(512),
buffer: buf,
}
}
}

View File

@@ -0,0 +1,131 @@
import _computeGeometryForSize from 'vhd-lib/_computeGeometryForSize.js'
import { createFooter, createHeader } from 'vhd-lib/_createFooterHeader.js'
import { DISK_TYPES, FOOTER_SIZE } from 'vhd-lib/_constants.js'
import { readChunk } from '@vates/read-chunk'
import { unpackFooter, unpackHeader } from 'vhd-lib/Vhd/_utils.js'
import { VhdAbstract } from 'vhd-lib'
import assert from 'node:assert'
const VHD_BLOCK_LENGTH = 2 * 1024 * 1024
export default class VhdEsxiRaw extends VhdAbstract {
#esxi
#datastore
#path
#bat
#header
#footer
#stream
#bytesRead = 0
static async open(esxi, datastore, path) {
const vhd = new VhdEsxiRaw(esxi, datastore, path)
await vhd.readHeaderAndFooter()
return vhd
}
get header() {
return this.#header
}
get footer() {
return this.#footer
}
constructor(esxi, datastore, path) {
super()
this.#esxi = esxi
this.#path = path
this.#datastore = datastore
}
async readHeaderAndFooter(checkSecondFooter = true) {
const res = await this.#esxi.download(this.#datastore, this.#path)
const length = res.headers.get('content-length')
this.#header = unpackHeader(createHeader(length / VHD_BLOCK_LENGTH))
const geometry = _computeGeometryForSize(length)
const actualSize = geometry.actualSize
this.#footer = unpackFooter(
createFooter(actualSize, Math.floor(Date.now() / 1000), geometry, FOOTER_SIZE, DISK_TYPES.DYNAMIC)
)
}
containsBlock(blockId) {
assert.notEqual(this.#bat, undefined, "bat is not loaded")
return this.#bat.has(blockId)
}
async readBlock(blockId) {
const start = blockId * VHD_BLOCK_LENGTH
if (!this.#stream) {
this.#stream = (await this.#esxi.download(this.#datastore, this.#path)).body
this.#bytesRead = 0
}
if (this.#bytesRead > start) {
this.#stream.destroy()
this.#stream = (
await this.#esxi.download(this.#datastore, this.#path, `${start}-${this.footer.currentSize}`)
).body
this.#bytesRead = start
}
if (start - this.#bytesRead > 0) {
this.#stream.destroy()
this.#stream = (
await this.#esxi.download(this.#datastore, this.#path, `${start}-${this.footer.currentSize}`)
).body
this.#bytesRead = start
}
const data = await readChunk(this.#stream, VHD_BLOCK_LENGTH)
this.#bytesRead += data.length
const bitmap = Buffer.alloc(512, 255)
return {
id: blockId,
bitmap,
data,
buffer: Buffer.concat([bitmap, data]),
}
}
async readBlockAllocationTable() {
const res = await this.#esxi.download(this.#datastore, this.#path)
const length = res.headers.get('content-length')
const stream = res.body
const empty = Buffer.alloc(VHD_BLOCK_LENGTH, 0)
let pos = 0
this.#bat = new Set()
let nextChunkLength = Math.min(VHD_BLOCK_LENGTH, length)
const progress = setInterval(() => {
console.log("reading blocks", pos / VHD_BLOCK_LENGTH, "/", length/ VHD_BLOCK_LENGTH)
}, 30 * 1000)
while (nextChunkLength > 0) {
try{
const chunk = await readChunk(stream, nextChunkLength)
let isEmpty
if (nextChunkLength === VHD_BLOCK_LENGTH) {
isEmpty = empty.equals(chunk)
} else {
// last block can be smaller
isEmpty = Buffer.alloc(nextChunkLength, 0).equals(chunk)
}
if (!isEmpty) {
this.#bat.add(pos / VHD_BLOCK_LENGTH)
}
pos += VHD_BLOCK_LENGTH
nextChunkLength = Math.min(VHD_BLOCK_LENGTH, length - pos)
}catch(error){
clearInterval(progress)
throw error
}
}
console.log("BAT reading done, remaining ", this.#bat.size, "/", Math.ceil(length / VHD_BLOCK_LENGTH))
clearInterval(progress)
}
}

View File

@@ -0,0 +1,315 @@
import { Client } from 'node-vsphere-soap'
import { dirname } from 'node:path'
import { EventEmitter } from 'node:events'
import { strictEqual } from 'node:assert'
import fetch from 'node-fetch'
import parseVmdk from './parsers/vmdk.mjs'
import parseVmsd from './parsers/vmsd.mjs'
import parseVmx from './parsers/vmx.mjs'
import VhdCowd from './VhdEsxiCowd.mjs'
import VhdEsxiRaw from './VhdEsxiRaw.mjs'
const MAX_SCSI = 9
const MAX_ETHERNET = 9
export default class Esxi extends EventEmitter {
#client
#cookies
#host
#user
#password
#ready = false
constructor(host, user, password, sslVerify = true) {
super()
this.#host = host
this.#user = user
this.#password = password
this.#client = new Client(host, user, password, sslVerify)
process.on('warning', this.#eatTlsWarning )
this.#client.once('ready', () => {
process.off('warning', this.#eatTlsWarning )
this.#ready = true
this.emit('ready')
})
this.#client.on('error', err => {
process.off('warning', this.#eatTlsWarning )
console.error({
in:'ERROR',
code: err.code,
message: err.message
})
this.emit('error', err)
})
}
#eatTlsWarning (/* err */){
// console.log('yummy', err.code, err.message)
}
#exec(cmd, args) {
strictEqual(this.#ready, true)
const client = this.#client
return new Promise(function (resolve, reject) {
client.once('error', function (error) {
client.off('result', resolve)
reject(error)
})
client.runCommand(cmd, args).once('result', function () {
client.off('error', reject)
resolve(...arguments)
})
})
}
async download(dataStore, path, range) {
strictEqual(this.#ready, true)
const url = `https://${this.#host}/folder/${path}?dsName=${dataStore}`
const headers = {}
if(this.#cookies){
headers.cookie= this.#cookies
} else {
headers.Authorization = 'Basic ' + Buffer.from(this.#user + ':' + this.#password).toString('base64')
}
if (range) {
headers['content-type'] = 'multipart/byteranges'
headers.Range = 'bytes=' + range
}
const res = await fetch(url, {
method: 'GET',
headers,
highWaterMark: 10 * 1024 * 1024,
})
if (res.status < 200 || res.status >= 300) {
const error = new Error(res.status + ' ' + res.statusText + ' ' + url)
error.cause = res
throw error
}
if(res.headers.raw()['set-cookie']){
this.#cookies = res.headers.raw()['set-cookie']
}
return res
}
async search(type, properties) {
// get property collector
const propertyCollector = this.#client.serviceContent.propertyCollector
// get view manager
const viewManager = this.#client.serviceContent.viewManager
// get root folder
const rootFolder = this.#client.serviceContent.rootFolder
let result = await this.#exec('CreateContainerView', {
_this: viewManager,
container: rootFolder,
type: [type],
recursive: true,
})
// build all the data structures needed to query all the vm names
const containerView = result.returnval
const objectSpec = {
attributes: { 'xsi:type': 'ObjectSpec' }, // setting attributes xsi:type is important or else the server may mis-recognize types!
obj: containerView,
skip: true,
selectSet: [
{
attributes: { 'xsi:type': 'TraversalSpec' },
name: 'traverseEntities',
type: 'ContainerView',
path: 'view',
skip: false,
},
],
}
const propertyFilterSpec = {
attributes: { 'xsi:type': 'PropertyFilterSpec' },
propSet: properties.map(p => ({
attributes: { 'xsi:type': 'PropertySpec' },
type,
pathSet: [p],
})),
objectSet: [objectSpec],
}
result = await this.#exec('RetrievePropertiesEx', {
_this: propertyCollector,
specSet: [propertyFilterSpec],
options: { attributes: { type: 'RetrieveOptions' } },
})
const objects = {}
const returnObj = Array.isArray(result.returnval.objects) ? result.returnval.objects : [result.returnval.objects]
returnObj.forEach(({ obj, propSet }) => {
objects[obj.$value] = {}
propSet = Array.isArray(propSet) ? propSet : [propSet]
propSet.forEach(({ name, val }) => {
// don't care about the type for now
delete val.attributes
// a scalar value : simplify it
if (val.$value) {
objects[obj.$value][name] = val.$value
} else {
objects[obj.$value][name] = val
}
})
})
return objects
}
async #inspectVmdk(dataStores, currentDataStore, currentPath, filePath) {
let diskDataStore, diskPath
if (filePath.startsWith('/')) {
// disk is on another datastore
Object.keys(dataStores).forEach(dataStoreUrl => {
if (filePath.startsWith(dataStoreUrl)) {
diskDataStore = dataStores[dataStoreUrl].name
diskPath = filePath.substring(dataStoreUrl.length + 1)
}
})
} else {
diskDataStore = currentDataStore
diskPath = currentPath + '/' + filePath
}
const vmdkRes = await this.download(diskDataStore, diskPath)
const text = await vmdkRes.text()
const parsed = parseVmdk(text)
const { fileName, parentFileName, capacity } = parsed
return {
...parsed,
datastore: diskDataStore,
path: dirname(diskPath),
descriptionLabel: ' from esxi',
vhd: async () => {
if (fileName.endsWith('-flat.vmdk')) {
const vhd = await VhdEsxiRaw.open(this, diskDataStore, dirname(diskPath) + '/' + fileName)
await vhd.readBlockAllocationTable()
return vhd.stream()
}
// last snasphot only works when vm is powered off
const vhd = await VhdCowd.open(this, diskDataStore, dirname(diskPath) + '/' + fileName, parentFileName)
await vhd.readBlockAllocationTable()
return vhd.stream()
},
rawStream: async () => {
if (!fileName.endsWith('-flat.vmdk')) {
return
}
// @todo : only if vm is powered off
const stream = (await this.download(diskDataStore, dirname(diskPath) + '/' + fileName)).body
stream.length = capacity
return stream
},
}
}
async getTransferableVmMetadata(vmId) {
const search = await this.search('VirtualMachine', ['name', 'config', 'storage', 'runtime', 'snapshot'])
if (search[vmId] === undefined) {
throw new Error(`VM ${vmId} not found `)
}
const { config, runtime } = search[vmId]
const [, dataStore, vmxPath] = config.files.vmPathName.match(/^\[(.*)\] (.+.vmx)$/)
const res = await this.download(dataStore, vmxPath)
const vmx = parseVmx(await res.text())
// list datastores
const dataStores = {}
Object.values(await this.search('Datastore', ['summary'])).forEach(({ summary }) => {
dataStores[summary.url] = summary
})
const disks = []
for (let scsiIndex = 0; scsiIndex < MAX_SCSI; scsiIndex++) {
const scsiChannel = vmx[`scsi${scsiIndex}`]
if (scsiChannel === undefined) {
continue
}
for (const diskIndex in Object.values(scsiChannel)) {
const disk = scsiChannel[diskIndex]
if (typeof disk !== 'object' || disk.deviceType !== 'scsi-hardDisk') {
continue
}
disks.push({
...(await this.#inspectVmdk(dataStores, dataStore, dirname(vmxPath), disk.fileName)),
node: `scsi${scsiIndex}:${diskIndex}`,
})
}
}
const networks = []
for (let ethernetIndex = 0; ethernetIndex < MAX_ETHERNET; ethernetIndex++) {
const ethernet = vmx[`ethernet${ethernetIndex}`]
if (ethernet === undefined) {
continue
}
networks.push({
label: ethernet.networkName,
macAddress: ethernet.generatedAddress,
isGenerated: ethernet.addressType === 'generated',
})
}
const vmsd = await (await this.download(dataStore, vmxPath.replace('.vmx', '.vmsd'))).text()
let snapshots
if(vmsd){
snapshots = parseVmsd(vmsd)
for (const snapshotIndex in snapshots?.snapshots) {
const snapshot = snapshots.snapshots[snapshotIndex]
for (const diskIndex in snapshot.disks) {
const fileName = snapshot.disks[diskIndex].fileName
snapshot.disks[diskIndex] = {
node: snapshot.disks[diskIndex]?.node, // 'scsi0:0',
...(await this.#inspectVmdk(dataStores, dataStore, dirname(vmxPath), fileName)),
}
}
}
}
return {
name_label: config.name,
memory: parseInt(config.hardware.memoryMB) * 1024 * 1024,
numCpu: parseInt(config.hardware.numCPU),
guestToolsInstalled: false,
firmware: config.firmware, // bios or uefi
powerState: runtime.powerState,
snapshots,
disks: disks.map(({ fileName, rawDiskFileName, datastore, path, parentFileName, ...other }) => {
return {
...other,
vhd: async () => {
if (fileName.endsWith('-flat.vmdk')) {
const vhd = await VhdEsxiRaw.open(this, datastore, path + '/' + fileName)
await vhd.readBlockAllocationTable()
return vhd.stream()
}
// last snasphot only works when vm is powered off
const vhd = await VhdCowd.open(this, datastore, path + '/' + fileName, parentFileName)
await vhd.readBlockAllocationTable()
return vhd.stream()
},
rawStream: async () => {
if (fileName.endsWith('-flat.vmdk')) {
return
}
// @todo : only if vm is powered off
const stream = (await this.download(datastore, path + '/' + fileName)).body
stream.length = other.capacity
return stream
},
}
}),
networks,
}
}
}

View File

@@ -0,0 +1,14 @@
import Esxi from './esxi.mjs'
const host = '10.10.0.62'
const user = 'root'
const password = ''
const sslVerify = false
console.log(Esxi)
const esxi = new Esxi(host, user, password, sslVerify)
console.log(esxi)
esxi.on('ready', async function (){
const metadata = await esxi.getTransferableVmMetadata('4')
console.log('metadata', metadata)
})

View File

@@ -0,0 +1,30 @@
{
"license": "ISC",
"private": false,
"version": "0.0.2",
"name": "@xen-orchestra/vmware-explorer",
"dependencies": {
"@vates/read-chunk": "^1.0.1",
"lodash": "^4.17.21",
"node-fetch": "^3.3.0",
"node-vsphere-soap": "^0.0.2-5",
"vhd-lib": "^4.2.0"
},
"engines": {
"node": ">=18"
},
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/vmware-explorer",
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
"repository": {
"directory": "@xen-orchestra/vmware-explorer",
"type": "git",
"url": "https://github.com/vatesfr/xen-orchestra.git"
},
"author": {
"name": "Vates SAS",
"url": "https://vates.fr"
},
"scripts": {
"postversion": "npm publish --access public"
}
}

View File

@@ -0,0 +1,45 @@
import { strictEqual } from 'node:assert'
// this file contains the disk metadata
export function parseDescriptor(text) {
const descriptorText = text.toString('ascii').replace(/\x00+$/, '') // eslint-disable-line no-control-regex
strictEqual(descriptorText.substr(0, 21), '# Disk DescriptorFile')
const descriptorDict = {}
const extentList = []
const lines = descriptorText.split(/\r?\n/).filter(line => {
return line.trim().length > 0 && line[0] !== '#'
})
for (const line of lines) {
const defLine = line.split('=')
// the wonky quote test is to avoid having an equal sign in the name of an extent
if (defLine.length === 2 && defLine[0].indexOf('"') === -1) {
descriptorDict[defLine[0].toLowerCase()] = defLine[1].replace(/['"]+/g, '')
} else {
const [, access, sizeSectors, type, name, offset] = line.match(/([A-Z]+) ([0-9]+) ([A-Z]+) "(.*)" ?(.*)$/)
extentList.push({ access, sizeSectors, type, name, offset })
}
}
strictEqual(extentList.length, 1, 'only one extent per vmdk is supported')
return { ...descriptorDict, extent: extentList[0] }
}
// https://github.com/libyal/libvmdk/blob/main/documentation/VMWare%20Virtual%20Disk%20Format%20(VMDK).asciidoc#5-the-cowd-sparse-extent-data-file
// vmdk file can be only a descriptor, or a
export default function parseVmdk(raw) {
strictEqual(typeof raw, 'string')
const descriptor = parseDescriptor(raw)
const isFull = !descriptor.parentfilenamehint
return {
capacity: descriptor.extent.sizeSectors * 512,
isFull,
uid: descriptor.cid,
fileName: descriptor.extent.name,
parentId: descriptor.parentcid,
parentFileName: descriptor.parentfilenamehint,
vmdFormat: descriptor.extent.type,
nameLabel: descriptor.extent.name,
}
}

View File

@@ -0,0 +1,53 @@
// these files contains the snapshot history of the VM
function set(obj, keyPath, val) {
const [key, ...other] = keyPath
const match = key.match(/^(.+)([0-9])$/)
if (match) {
// an array
let [, label, index] = match
label += 's'
if (!obj[label]) {
obj[label] = []
}
if (other.length) {
if (!obj[label][index]) {
obj[label][parseInt(index)] = {}
}
set(obj[label][index], other, val)
} else {
obj[label][index] = val
}
} else {
if (other.length) {
// an object
if (!obj[key]) {
// first time
obj[key] = {}
}
set(obj[key], other, val)
} else {
// a scalar
obj[key] = val
}
}
}
export default function parseVmsd(text) {
const parsed = {}
text.split('\n').forEach(line => {
const [key, val] = line.split(' = ')
if (!key.startsWith('snapshot')) {
return
}
set(parsed, key.split('.'), val?.substring(1, val.length - 1))
})
return {
lastUID: parsed.snapshot.current,
current: parsed.snapshot.current,
numSnapshots: parsed.snapshot.numSnapshots,
snapshots: Object.values(parsed.snapshots) || [],
}
}

View File

@@ -0,0 +1,48 @@
function set(obj, keyPath, val) {
let [key, ...other] = keyPath
if (key.includes(':')) {
// it's an array
let index
;[key, index] = key.split(':')
index = parseInt(index)
if (!obj[key]) {
// first time on this array
obj[key] = []
}
if (!other.length) {
// without descendant
obj[key][index] = val
} else {
// with descendant
if (!obj[key][index]) {
// first time on this descendant
obj[key][index] = {}
}
set(obj[key][index], other, val)
}
} else {
// it's an object
if (!other.length) {
// wihtout descendant
obj[key] = val
} else {
// with descendant
if (obj[key] === undefined) {
// first time
obj[key] = {}
}
set(obj[key], other, val)
}
}
}
// this file contains the vm configuration
export default function parseVmx(text) {
const vmx = {}
text.split('\n').forEach(line => {
const [key, val] = line.split(' = ')
set(vmx, key.split('.'), val?.substring(1, val.length - 1))
})
return vmx
}

View File

@@ -211,9 +211,10 @@ class Xapi extends Base {
})
if (timeout !== undefined) {
const error = new Error(`waitObjectState: timeout reached before ${refOrUuid} in expected state`)
timeoutHandle = setTimeout(() => {
stop()
reject(new Error(`waitObjectState: timeout reached before ${refOrUuid} in expected state`))
reject(error)
}, timeout)
}
})

View File

@@ -1,6 +1,6 @@
{
"name": "@xen-orchestra/xapi",
"version": "1.5.2",
"version": "1.6.0",
"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

@@ -7,7 +7,6 @@ const ignoreErrors = require('promise-toolbox/ignoreErrors')
const pickBy = require('lodash/pickBy.js')
const omit = require('lodash/omit.js')
const pCatch = require('promise-toolbox/catch')
const pRetry = require('promise-toolbox/retry')
const { asyncMap } = require('@xen-orchestra/async-map')
const { createLogger } = require('@xen-orchestra/log')
const { decorateClass } = require('@vates/decorate-with')
@@ -637,92 +636,44 @@ class Vm {
}
}
let destroyNobakVdis = false
let ignoredVbds
if (ignoreNobakVdis) {
if (isHalted) {
await asyncMap(await listNobakVbds(this, vm.VBDs), async vbd => {
await this.VBD_destroy(vbd.$ref)
$defer.call(this, 'VBD_create', vbd)
})
} else {
// cannot unplug VBDs on Running, Paused and Suspended VMs
destroyNobakVdis = true
}
ignoredVbds = await listNobakVbds(this, vm.VBDs)
ignoreNobakVdis = ignoredVbds.length !== 0
}
if (name_label === undefined) {
name_label = vm.name_label
const params = [cancelToken, 'VM.snapshot', vmRef, name_label ?? vm.name_label]
if (ignoreNobakVdis) {
params.push(ignoredVbds.map(_ => _.VDI))
}
let ref
do {
if (!vm.tags.includes('xo-disable-quiesce')) {
try {
let { snapshots } = vm
ref = await pRetry(
async () => {
try {
return await this.callAsync(cancelToken, 'VM.snapshot_with_quiesce', vmRef, name_label)
} catch (error) {
if (error == null || error.code !== 'VM_SNAPSHOT_WITH_QUIESCE_FAILED') {
throw pRetry.bail(error)
}
// detect and remove new broken snapshots
//
// see https://github.com/vatesfr/xen-orchestra/issues/3936
const prevSnapshotRefs = new Set(snapshots)
const snapshotNameLabelPrefix = `Snapshot of ${vm.uuid} [`
snapshots = await this.getField('VM', vm.$ref, 'snapshots')
const createdSnapshots = (
await this.getRecords(
'VM',
snapshots.filter(_ => !prevSnapshotRefs.has(_))
)
).filter(_ => _.name_label.startsWith(snapshotNameLabelPrefix))
// be safe: only delete if there was a single match
if (createdSnapshots.length === 1) {
const snapshotRef = createdSnapshots[0]
this.VM_destroy(snapshotRef).catch(error => {
warn('VM_sapshot: failed to destroy broken snapshot', {
error,
snapshotRef,
vmRef,
})
})
}
throw error
}
},
{
delay: 60e3,
tries: 3,
}
).then(extractOpaqueRef)
this.call('VM.add_tags', ref, 'quiesce').catch(error => {
warn('VM_snapshot: failed to add quiesce tag', {
vmRef,
snapshotRef: ref,
error,
})
let destroyNobakVdis = false
let result
try {
result = await this.callAsync(...params)
} catch (error) {
if (error.code !== 'MESSAGE_PARAMETER_COUNT_MISMATCH') {
throw error
}
if (ignoreNobakVdis) {
if (isHalted) {
await asyncMap(ignoredVbds, async vbd => {
await this.VBD_destroy(vbd.$ref)
$defer.call(this, 'VBD_create', vbd)
})
break
} catch (error) {
const { code } = error
if (
// removed in CH 8.1
code !== 'MESSAGE_REMOVED' &&
code !== 'VM_SNAPSHOT_WITH_QUIESCE_NOT_SUPPORTED' &&
// quiesce only work on a running VM
code !== 'VM_BAD_POWER_STATE' &&
// quiesce failed, fallback on standard snapshot
// TODO: emit warning
code !== 'VM_SNAPSHOT_WITH_QUIESCE_FAILED'
) {
throw error
}
} else {
// cannot unplug VBDs on Running, Paused and Suspended VMs
destroyNobakVdis = true
}
}
ref = await this.callAsync(cancelToken, 'VM.snapshot', vmRef, name_label).then(extractOpaqueRef)
} while (false)
params.pop()
result = await this.callAsync(...params)
}
const ref = extractOpaqueRef(result)
// detached async
this._httpHook(vm, '/post-sync').catch(noop)

View File

@@ -1,8 +1,105 @@
# ChangeLog
## **next**
### Enhancements
- [Snapshot] Use the new [`ignore_vdis` feature](https://github.com/xapi-project/xen-api/pull/4563) of XCP-ng/XenServer 8.3
- [Hub/Recipes/Kubernetes] Now use the [Flannel](https://github.com/flannel-io/flannel) Container Network Interface plugin to handle network
### Bug fixes
- [Nagios] Fix reporting, broken in 5.77.2
### Released packages
- @xen-orchestra/xapi 1.6.0
- @xen-orchestra/backups 0.29.4
- @xen-orchestra/proxy 0.26.9
- xo-server 5.107.5
- xo-web 5.109.0
## **5.77.2** (2022-12-12)
<img id="latest" src="https://badgen.net/badge/channel/latest/yellow" alt="Channel: latest" />
### Bug fixes
- [Backups] Fixes most of the _unexpected number of entries in backup cache_ errors
### Released packages
- @xen-orchestra/backups 0.29.3
- @xen-orchestra/proxy 0.26.7
- xo-server 5.107.3
## **5.77.1** (2022-12-07)
### 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 +179,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,25 +7,15 @@
> Users must be able to say: “Nice enhancement, I'm eager to test it”
- [Remotes] Prevent remote path from ending with `xo-vm-backups` as it's usually a mistake
- [OVA export] Speed up OVA generation by 2. Generated file will be bigger (as big as uncompressed XVA) (PR [#6487](https://github.com/vatesfr/xen-orchestra/pull/6487))
- [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 beckup VMs invidually with the VM name label used as *host* and backup job name used as *service*
### Bug fixes
> Users must be able to say: “I had this issue, happy to know it's fixed”
- [Dashboard/Health] Fix `Unknown SR` and `Unknown VDI` in Unhealthy VDIs (PR [#6519](https://github.com/vatesfr/xen-orchestra/pull/6519))
- [Delta Backup] Can now recover VHD merge when failed at the begining
- [Delta Backup] Fix `ENOENT` errors when merging a VHD directory on non-S3 remote
- [Remote] Prevent the browser from auto-completing the encryption key field
### Packages to release
> When modifying a package, add it here with its release type.
>
> The format is the following: - `$packageName` `$releaseType`
> The format is the following: `- $packageName $releaseType`
>
> Where `$releaseType` is
>
@@ -37,14 +27,4 @@
<!--packages-start-->
- @xen-orchestra/backups-cli major
- @xen-orchestra/fs minor
- @xen-orchestra/log minor
- vhd-lib minor
- xo-cli patch
- xo-server minor
- xo-server-transport-nagios major
- xo-vmdk-to-vhd minor
- xo-web minor
<!--packages-end-->

BIN
docs/assets/detach-host.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View File

@@ -570,6 +570,17 @@ If your hosts are already in a pool you only need to add your pool master host t
Don't add pool slaves to your XOA server list! XOA will automatically find them from the master you add.
:::
### Remove a host from an existing pool
To remove one host from a pool, you can go to the "Advanced" tab of the host page for the host you wish to remove, and click on "Detach"
![](./assets/detach-host.png)
:::warning
- Detaching a host will remove all the VM disks stored on the Local Storage of this host, and reboot the host.
- The host you want to remove must be a slave, not the master!
:::
## Visualizations
Visualizations can help you to understand your XenServer infrastructure, as well as correlate events and detect bottlenecks.

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",
@@ -28,6 +27,7 @@
"promise-toolbox": "^0.21.0",
"semver": "^7.3.6",
"sorted-object": "^2.0.1",
"vue": "^2.7.14",
"vuepress": "^1.4.1"
},
"engines": {
@@ -84,10 +84,10 @@
"build": "scripts/run-script.js --parallel --concurrency 2 build",
"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",
@@ -98,5 +98,6 @@
"workspaces": [
"@*/*",
"packages/*"
]
],
"packageManager": "yarn@1.22.19"
}

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

@@ -8,7 +8,7 @@ const tmp = require('tmp')
const { getSyncedHandler } = require('@xen-orchestra/fs')
const { pFromCallback, Disposable } = require('promise-toolbox')
const { VhdFile, chainVhd, openVhd } = require('./index')
const { VhdFile, chainVhd, openVhd, VhdAbstract } = require('./index')
const { mergeVhdChain } = require('./merge')
const { checkFile, createRandomFile, convertFromRawToVhd } = require('./tests/utils')
@@ -163,25 +163,29 @@ test('it can resume a simple merge ', async () => {
}
})
test('it can resume a failed renaming ', 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}/parent.vhd`)
const parentVhd = new VhdFile(handler, 'parent.vhd')
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}/child1.vhd`)
await chainVhd(handler, 'parent.vhd', handler, 'child1.vhd', true)
const childVhd = new VhdFile(handler, 'child1.vhd')
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(
'.parent.vhd.merge.json',
`.${parentName}.merge.json`,
JSON.stringify({
parent: {
header: parentVhd.header.checksum,
@@ -192,15 +196,20 @@ test('it can resume a failed renaming ', async () => {
step: 'cleanupVhds',
})
)
// expect merge to succed
await mergeVhdChain(handler, ['parent.vhd', 'child1.vhd'])
// parent have been renamed
expect(await fs.exists(`${tempDir}/parent.vhd`)).toBeFalsy()
expect(await fs.exists(`${tempDir}/.parent.vhd.merge.json`)).toBeFalsy()
// expect merge to succeed
await mergeVhdChain(handler, [parentName, childName])
Disposable.use(openVhd(handler, 'child1.vhd'), async mergedVhd => {
// 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 contians parent data
// 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')
@@ -208,16 +217,14 @@ test('it can resume a failed renaming ', async () => {
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(
'.parent.vhd.merge.json',
`.${parentName}.merge.json`,
JSON.stringify({
parent: {
header: parentVhd.header.checksum,
@@ -228,10 +235,13 @@ test('it can resume a failed renaming ', async () => {
step: 'cleanupVhds',
})
)
await mergeVhdChain(handler, ['parent.vhd', 'child1.vhd'])
expect(await fs.exists(`${tempDir}/parent.vhd`)).toBeFalsy()
expect(await fs.exists(`${tempDir}/child1.vhd`)).toBeTruthy()
expect(await fs.exists(`${tempDir}/.parent.vhd.merge.json`)).toBeFalsy()
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 () => {

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')
@@ -253,10 +254,16 @@ class Merger {
// 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
await VhdAbstract.unlink(handler, mergeTargetChild)
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) {
@@ -265,6 +272,8 @@ class Merger {
assert.strictEqual(vhd.header.checksum, this.#state.parent.header)
})
this.#logInfo(`the VHD parent was already renamed`, { parent, mergeTargetChild })
} else {
throw error
}
}

View File

@@ -1,7 +1,7 @@
{
"private": false,
"name": "vhd-lib",
"version": "4.1.1",
"version": "4.2.0",
"license": "AGPL-3.0-or-later",
"description": "Primitives for VHD file handling",
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/packages/vhd-lib",
@@ -18,8 +18,8 @@
"@vates/async-each": "^1.0.0",
"@vates/read-chunk": "^1.0.1",
"@xen-orchestra/async-map": "^0.1.2",
"@xen-orchestra/fs": "^3.2.0",
"@xen-orchestra/log": "^0.4.0",
"@xen-orchestra/fs": "^3.3.0",
"@xen-orchestra/log": "^0.5.0",
"async-iterator-to-stream": "^1.0.2",
"decorator-synchronized": "^0.6.0",
"fs-extra": "^10.0.0",
@@ -31,7 +31,7 @@
"uuid": "^9.0.0"
},
"devDependencies": {
"@xen-orchestra/fs": "^3.2.0",
"@xen-orchestra/fs": "^3.3.0",
"execa": "^5.0.0",
"get-stream": "^6.0.0",
"rimraf": "^3.0.2",

View File

@@ -8,6 +8,6 @@
"promise-toolbox": "^0.19.2",
"readable-stream": "^3.1.1",
"throttle": "^1.0.3",
"vhd-lib": "^4.1.1"
"vhd-lib": "^4.2.0"
}
}

View File

@@ -1,7 +1,7 @@
{
"private": false,
"name": "xo-cli",
"version": "0.14.1",
"version": "0.14.2",
"license": "AGPL-3.0-or-later",
"description": "Basic CLI for Xen-Orchestra",
"keywords": [

View File

@@ -1,6 +1,6 @@
{
"name": "xo-server-audit",
"version": "0.10.1",
"version": "0.10.2",
"license": "AGPL-3.0-or-later",
"description": "Audit plugin for XO-Server",
"keywords": [
@@ -44,9 +44,9 @@
"prepublishOnly": "yarn run build"
},
"dependencies": {
"@xen-orchestra/audit-core": "^0.2.1",
"@xen-orchestra/audit-core": "^0.2.2",
"@xen-orchestra/cron": "^1.0.6",
"@xen-orchestra/log": "^0.4.0",
"@xen-orchestra/log": "^0.5.0",
"async-iterator-to-stream": "^1.1.0",
"promise-toolbox": "^0.21.0",
"readable-stream": "^4.1.0",

View File

@@ -1,6 +1,6 @@
{
"name": "xo-server-auth-ldap",
"version": "0.10.5",
"version": "0.10.6",
"license": "AGPL-3.0-or-later",
"description": "LDAP authentication plugin for XO-Server",
"keywords": [
@@ -31,7 +31,7 @@
"node": ">=12"
},
"dependencies": {
"@xen-orchestra/log": "^0.4.0",
"@xen-orchestra/log": "^0.5.0",
"ensure-array": "^1.0.0",
"exec-promise": "^0.7.0",
"inquirer": "^8.0.0",

View File

@@ -1,6 +1,6 @@
{
"name": "xo-server-backup-reports",
"version": "0.17.1",
"version": "0.17.2",
"license": "AGPL-3.0-or-later",
"description": "Backup reports plugin for XO-Server",
"keywords": [
@@ -33,7 +33,7 @@
},
"dependencies": {
"@xen-orchestra/defined": "^0.0.1",
"@xen-orchestra/log": "^0.4.0",
"@xen-orchestra/log": "^0.5.0",
"human-format": "^1.0.0",
"lodash": "^4.13.1",
"moment-timezone": "^0.5.13"

View File

@@ -1,6 +1,6 @@
{
"name": "xo-server-load-balancer",
"version": "0.7.1",
"version": "0.7.2",
"license": "AGPL-3.0-or-later",
"description": "Load balancer for XO-Server",
"keywords": [
@@ -28,7 +28,7 @@
},
"dependencies": {
"@xen-orchestra/cron": "^1.0.6",
"@xen-orchestra/log": "^0.4.0",
"@xen-orchestra/log": "^0.5.0",
"lodash": "^4.16.2"
},
"devDependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "xo-server-netbox",
"version": "0.3.4",
"version": "0.3.5",
"license": "AGPL-3.0-or-later",
"description": "Synchronizes pools managed by Xen Orchestra with Netbox",
"keywords": [
@@ -29,7 +29,7 @@
"node": ">=14.6"
},
"dependencies": {
"@xen-orchestra/log": "^0.4.0",
"@xen-orchestra/log": "^0.5.0",
"ipaddr.js": "^2.0.1",
"lodash": "^4.17.21",
"semver": "^7.3.5"

View File

@@ -16,7 +16,7 @@
"predev": "yarn run prebuild",
"prepublishOnly": "yarn run build"
},
"version": "1.0.6",
"version": "1.0.7",
"engines": {
"node": ">=10"
},
@@ -28,7 +28,7 @@
},
"dependencies": {
"@vates/coalesce-calls": "^0.1.0",
"@xen-orchestra/log": "^0.4.0",
"@xen-orchestra/log": "^0.5.0",
"@xen-orchestra/openflow": "^0.1.2",
"ipaddr.js": "^2.0.1",
"lodash": "^4.17.11",

View File

@@ -1,6 +1,6 @@
{
"name": "xo-server-transport-nagios",
"version": "0.1.2",
"version": "1.0.0",
"license": "AGPL-3.0-or-later",
"description": "Send backup runs statuses to Nagios",
"keywords": [
@@ -28,7 +28,8 @@
"node": ">=6"
},
"dependencies": {
"@xen-orchestra/log": "^0.4.0",
"@xen-orchestra/log": "^0.5.0",
"@xen-orchestra/template": "^0.1.0",
"buffer-crc32": "^0.2.13"
},
"devDependencies": {

View File

@@ -2,11 +2,24 @@ import crc32 from 'buffer-crc32'
import net from 'net'
import { Buffer } from 'buffer'
import { createLogger } from '@xen-orchestra/log'
import { compileTemplate } from '@xen-orchestra/template'
const { debug, warn } = createLogger('xo:server:transport:nagios')
// ===================================================================
const hostDescription = `Host name on Nagios.
Leave empty if the host name equals the vm name (the default configuration).
Otherwise, you could choose a custom name but the template \`{vm.name_label}\` must be included. For example: \`xo-backup-{vm.name_label}\`.`
const serviceDescription = `Service name on Nagios.
Leave empty if the host name equals the backup job name (the default configuration).
Otherwise, you could choose a custom name but the template \`{job.name}\` must e included. For example: \`{job.name}-Xen Orchestra\`.`
export const configurationSchema = {
type: 'object',
@@ -23,6 +36,16 @@ export const configurationSchema = {
type: 'string',
description: 'The encryption key',
},
host: {
default: '{vm.name_label}',
description: hostDescription,
type: 'string',
},
service: {
default: '{job.name}',
description: serviceDescription,
type: 'string',
},
},
additionalProperties: false,
required: ['server', 'port', 'key'],
@@ -31,16 +54,18 @@ export const configurationSchema = {
export const testSchema = {
type: 'object',
properties: {
host: {
description: 'Nagios host',
VmNameLabel: {
title: 'VM Name Label',
description: 'Name of a VM',
type: 'string',
},
service: {
description: 'Nagios service',
jobName: {
title: 'Job Name',
description: 'Name of a backup job',
type: 'string',
},
},
required: ['host', 'service'],
required: ['VmNameLabel', 'jobName'],
}
// ===================================================================
@@ -96,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() {
@@ -109,21 +142,24 @@ class XoServerNagios {
this._unset()
}
test({ host, service }) {
test({ VmNameLabel, jobName }) {
return this._sendPassiveCheck(
{
message: 'The server-nagios plugin for Xen Orchestra server seems to be working fine, nicely done :)',
status: OK,
},
host,
service
VmNameLabel,
jobName
)
}
_sendPassiveCheck({ message, status }, host, service) {
_sendPassiveCheck({ message, status }, vmNameLabel, jobName) {
return new Promise((resolve, reject) => {
this._conf.host = host
this._conf.service = service
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 })
@@ -139,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')
})
@@ -147,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

@@ -103,7 +103,7 @@ writeBlockConcurrency = 16
# This is a work-around.
#
# See https://github.com/vatesfr/xen-orchestra/pull/4674
maxMergedDeltasPerRun = 2
maxMergedDeltasPerRun = inf
# https://github.com/naugtur/blocked-at#params-and-return-value
[blockedAtOptions]
@@ -175,7 +175,7 @@ poolMarkingPrefix = 'xo:clientInfo:'
[xo-proxy]
callTimeout = '1 min'
channel = 'xo-proxy-appliance-{xoChannel}'
channel = 'xo-proxy-appliance-latest'
namespace = 'xoProxyAppliance'

View File

@@ -1,7 +1,7 @@
{
"private": true,
"name": "xo-server",
"version": "5.106.1",
"version": "5.107.5",
"license": "AGPL-3.0-or-later",
"description": "Server part of Xen-Orchestra",
"keywords": [
@@ -33,25 +33,26 @@
"@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",
"@vates/otp": "^1.0.0",
"@vates/parse-duration": "^0.1.1",
"@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.4",
"@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/vmware-explorer": "^0.0.2",
"@xen-orchestra/xapi": "^1.6.0",
"ajv": "^8.0.3",
"app-conf": "^2.3.0",
"async-iterator-to-stream": "^1.0.1",
@@ -127,7 +128,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 +136,7 @@
"xo-collection": "^0.5.0",
"xo-common": "^0.8.0",
"xo-remote-parser": "^0.9.2",
"xo-vmdk-to-vhd": "^2.4.3"
"xo-vmdk-to-vhd": "^2.5.0"
},
"devDependencies": {
"@babel/cli": "^7.0.0",

View File

@@ -5,6 +5,11 @@ import { createLogger } from '@xen-orchestra/log'
const { warn } = createLogger('xo:server:handleBackupLog')
async function sendToNagios(app, jobName, vmBackupInfo) {
if (app.sendPassiveCheck === undefined) {
// Nagios plugin is not loaded
return
}
try {
const messageToNagios = {
id: vmBackupInfo.id,

View File

@@ -596,8 +596,8 @@ migrate.resolve = {
migrationNetwork: ['migrationNetwork', 'network', 'administrate'],
}
export async function warmMigration({ vm, sr, startVm, deleteSource }) {
await this.warmMigrateVm(vm, sr, startVm, deleteSource)
export async function warmMigration({ vm, sr, startDestinationVm, deleteSourceVm }) {
await this.warmMigrateVm(vm, sr, startDestinationVm, deleteSourceVm)
}
warmMigration.permission = 'admin'
@@ -1298,6 +1298,25 @@ import_.resolve = {
export { import_ as import }
export async function importFomEsxi({host, user, password, sslVerify=true, sr, network, vm, thin=false}){
return await this.migrationfromEsxi({host, user, password, sslVerify, thin, vm, sr, network})
}
importFomEsxi.params = {
host: { type: 'string' },
network: { type: 'string' },
password: { type: 'string' },
user: { type: 'string' },
sr: { type: 'string' },
sslVerify: {type: 'boolean', optional: true},
vm:{type: 'string'},
thin:{type: 'boolean', optional: true}
}
// -------------------------------------------------------------------
// FIXME: if position is used, all other disks after this position

View File

@@ -1,5 +1,10 @@
import { Backup } from '@xen-orchestra/backups/Backup.js'
import { v4 as generateUuid } from 'uuid'
import Esxi from '@xen-orchestra/vmware-explorer/esxi.mjs'
import OTHER_CONFIG_TEMPLATE from '../xapi/other-config-template.mjs'
import asyncMapSettled from '@xen-orchestra/async-map/legacy.js'
import { fromEvent } from 'promise-toolbox'
import { VDI_FORMAT_RAW, VDI_FORMAT_VHD } from '@xen-orchestra/xapi'
export default class MigrateVm {
constructor(app) {
@@ -66,7 +71,7 @@ export default class MigrateVm {
// run the transfer again to transfer the changed parts
// since the source is stopped, there won't be any new change after
backup = this.#createWarmBackup(sourceVmId, srId)
backup = this.#createWarmBackup(sourceVmId, srId, jobId)
await backup.run()
// find the destination Vm
const targets = Object.keys(
@@ -106,4 +111,116 @@ export default class MigrateVm {
}
}
}
async migrationfromEsxi({ host, user, password, sslVerify, sr: srId, network: networkId, vm: vmId, thin }) {
const esxi = new Esxi(host, user, password, sslVerify)
const app = this._app
const sr = app.getXapiObject(srId)
const xapi = sr.$xapi
await fromEvent(esxi, 'ready')
const esxiVmMetadata = await esxi.getTransferableVmMetadata(vmId)
const { memory, name_label, networks, numCpu } = esxiVmMetadata
const vm = await xapi._getOrWaitObject(
await xapi.VM_create({
...OTHER_CONFIG_TEMPLATE,
memory_dynamic_max: memory,
memory_dynamic_min: memory,
memory_static_max: memory,
memory_static_min: memory,
name_description: 'from esxi',
name_label,
VCPUs_at_startup: numCpu,
VCPUs_max: numCpu,
})
)
await Promise.all([
asyncMapSettled(['start', 'start_on'], op => vm.update_blocked_operations(op, 'OVA import in progress...')),
vm.set_name_label(`[Importing...] ${name_label}`),
])
const vifDevices = await xapi.call('VM.get_allowed_VIF_devices', vm.$ref)
await Promise.all(
networks.map((network, i) =>
xapi.VIF_create({
device: vifDevices[i],
network: xapi.getObject(networkId).$ref,
VM: vm.$ref,
})
)
)
// get the snapshot to migrate
const snapshots = esxiVmMetadata.snapshots
let chain =[]
if(snapshots && snapshots.current){
const currentSnapshotId = snapshots.current
let currentSnapshot = snapshots.snapshots.find(({ uid }) => uid === currentSnapshotId)
chain = [currentSnapshot.disks]
while ((currentSnapshot = snapshots.snapshots.find(({ uid }) => uid === currentSnapshot.parent))) {
chain.push(currentSnapshot.disks)
}
chain.reverse()
}
chain.push(esxiVmMetadata.disks)
const chainsByNodes = {}
chain.forEach(disks => {
disks.forEach(disk => {
chainsByNodes[disk.node] = chainsByNodes[disk.node] || []
chainsByNodes[disk.node].push(disk)
})
})
let userdevice = 0
for (const node in chainsByNodes) {
const chainByNode = chainsByNodes[node]
const vdi = await xapi._getOrWaitObject(
await xapi.VDI_create({
name_description: 'fromESXI' + chainByNode[0].descriptionLabel,
name_label: '[ESXI]' + chainByNode[0].nameLabel,
SR: sr.$ref,
virtual_size: chainByNode[0].capacity,
})
)
console.log('vdi created')
await xapi.VBD_create({
userdevice: String(userdevice),
VDI: vdi.$ref,
VM: vm.$ref,
})
console.log('vbd created')
for (const disk of chainByNode) {
// the first one is a RAW disk ( full )
console.log('will import ', { disk })
let format = VDI_FORMAT_VHD
let stream
if (!thin) {
stream = await disk.rawStream()
format = VDI_FORMAT_RAW
}
if (!stream) {
stream = await disk.vhd()
}
console.log('will import in format ', { disk, format })
await vdi.$importContent(stream, { format })
// for now we don't handle snapshots
break
}
userdevice ++
}
console.log('disks created')
// remove the importing in label
await vm.set_name_label(esxiVmMetadata.name_label)
// remove lock on start
await asyncMapSettled(['start', 'start_on'], op => vm.update_blocked_operations(op, null))
return vm.uuid
}
}

View File

@@ -1,4 +1,5 @@
import Ajv from 'ajv'
import cloneDeep from 'lodash/cloneDeep.js'
import mapToArray from 'lodash/map.js'
import noop from 'lodash/noop.js'
import { createLogger } from '@xen-orchestra/log'
@@ -153,22 +154,18 @@ export default class {
}
const validate = this._ajv.compile(configurationSchema)
// deep clone the configuration to avoid modifying the parameter
configuration = cloneDeep(configuration)
if (!validate(configuration)) {
throw invalidParameters(validate.errors)
}
// Sets the plugin configuration.
await plugin.instance.configure(
{
// Shallow copy of the configuration object to avoid most of the
// errors when the plugin is altering the configuration object
// which is handed over to it.
...configuration,
},
{
loaded: plugin.loaded,
}
)
await plugin.instance.configure(configuration, {
loaded: plugin.loaded,
})
plugin.configured = true
}

View File

@@ -58,6 +58,7 @@ export default class {
const handlers = this._handlers
for (const id in handlers) {
try {
delete handlers[id]
await handlers[id].forget()
} catch (_) {}
}
@@ -251,8 +252,10 @@ export default class {
}
async removeRemote(id) {
const handler = this._handlers[id]
const handlers = this._handlers
const handler = handlers[id]
if (handler !== undefined) {
delete handlers[id]
ignoreErrors.call(handler.forget())
}

View File

@@ -1,7 +1,7 @@
{
"private": false,
"name": "xo-vmdk-to-vhd",
"version": "2.4.3",
"version": "2.5.0",
"license": "AGPL-3.0-or-later",
"description": "JS lib reading and writing .vmdk and .ova files",
"keywords": [
@@ -26,7 +26,7 @@
"pako": "^2.0.4",
"promise-toolbox": "^0.21.0",
"tar-stream": "^2.2.0",
"vhd-lib": "^4.1.1",
"vhd-lib": "^4.2.0",
"xml2js": "^0.4.23"
},
"devDependencies": {

View File

@@ -1,7 +1,7 @@
{
"private": true,
"name": "xo-web",
"version": "5.107.0",
"version": "5.109.0",
"license": "AGPL-3.0-or-later",
"description": "Web interface client for Xen-Orchestra",
"keywords": [
@@ -40,7 +40,7 @@
"@nraynaud/novnc": "0.6.1",
"@xen-orchestra/cron": "^1.0.6",
"@xen-orchestra/defined": "^0.0.1",
"@xen-orchestra/log": "^0.4.0",
"@xen-orchestra/log": "^0.5.0",
"@xen-orchestra/template": "^0.1.0",
"ansi_up": "^4.0.3",
"asap": "^2.0.6",
@@ -138,7 +138,7 @@
"xo-common": "^0.8.0",
"xo-lib": "^0.11.1",
"xo-remote-parser": "^0.9.2",
"xo-vmdk-to-vhd": "^2.4.3"
"xo-vmdk-to-vhd": "^2.5.0"
},
"scripts": {
"build": "GIT_HEAD=$(git rev-parse HEAD) NODE_ENV=production gulp build",

View File

@@ -245,7 +245,7 @@ export default {
// Original text: "Add your XCP-ng hosts or pools"
homeWelcomeText: 'Añade tus hosts/pools de XenServer',
// Original text: 'Some XenServers have been registered but are not connected'
// Original text: 'Some XCP-ng hosts have been registered but are not connected'
homeConnectServerText: undefined,
// Original text: "Want some help?"

View File

@@ -246,10 +246,10 @@ export default {
homeWelcome: 'Bienvenue sur Xen Orchestra !',
// Original text: "Add your XCP-ng hosts or pools"
homeWelcomeText: 'Ajouter vos serveurs ou pools XenServer',
homeWelcomeText: 'Ajouter vos serveurs ou pools XCP-ng',
// Original text: "Some XenServers have been registered but are not connected"
homeConnectServerText: "Des XenServers sont enregistrés mais aucun n'est connecté",
// Original text: "Some XCP-ng hosts have been registered but are not connected"
homeConnectServerText: "Des hôtes XCP-ng sont enregistrés mais aucun n'est connecté",
// Original text: "Want some help?"
homeHelp: "Besoin d'aide ?",

View File

@@ -228,10 +228,10 @@ export default {
homeWelcome: 'Üdvözöljük a Felhőben!',
// Original text: "Add your XCP-ng hosts or pools"
homeWelcomeText: 'Hozzáadása your XenServer kiszolgálók or pools',
homeWelcomeText: 'Hozzáadása your XCP-ng kiszolgálók or pools',
// Original text: "Some XenServers have been registered but are not connected"
homeConnectServerText: 'Some XenServers have been registered but are not Kapcsolódva',
// Original text: "Some XCP-ng hosts have been registered but are not connected"
homeConnectServerText: 'Some XCP-ng hosts have been registered but are not connected',
// Original text: "Want some help?"
homeHelp: 'Segítségre van szüksége?',

View File

@@ -516,10 +516,10 @@ export default {
homeWelcome: 'Benvenuti in Xen Orchestra!',
// Original text: 'Add your XCP-ng hosts or pools'
homeWelcomeText: 'Aggiungi i tuoi hosts o pools XenServer',
homeWelcomeText: 'Aggiungi i tuoi hosts o pools XCP-ng',
// Original text: 'Some XenServers have been registered but are not connected'
homeConnectServerText: 'Alcuni XenServers sono stati registrati ma non sono collegati',
// Original text: 'Some XCP-ng hosts have been registered but are not connected'
homeConnectServerText: 'Alcuni XCP-ng hosts sono stati registrati ma non sono collegati',
// Original text: 'Want some help?'
homeHelp: 'Vuoi un aiuto?',
@@ -5671,9 +5671,6 @@ export default {
// Original text: 'SSH key'
recipeSshKeyLabel: 'Chiave SSH',
// Original text: 'Network CIDR'
recipeNetworkCidr: 'Rete CIDR',
// Original text: 'Action/Event'
auditActionEvent: 'Azione/Evento',

View File

@@ -299,7 +299,7 @@ export default {
// Original text: "Add your XCP-ng hosts or pools"
homeWelcomeText: 'XenServer sunucu veya havuzunu ekle',
// Original text: "Some XenServers have been registered but are not connected"
// Original text: "Some XCP-ng hosts have been registered but are not connected"
homeConnectServerText: "Bazı XenServer'lar kayıtlı ama bağlı değil",
// Original text: "Want some help?"

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