Compare commits
3 Commits
bugfix-rem
...
export-sna
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d7e4a65ba7 | ||
|
|
f26919cca7 | ||
|
|
78afe122a6 |
@@ -68,11 +68,6 @@ module.exports = {
|
||||
|
||||
'no-console': ['error', { allow: ['warn', 'error'] }],
|
||||
|
||||
// this rule can prevent race condition bugs like parallel `a += await foo()`
|
||||
//
|
||||
// as it has a lots of false positive, it is only enabled as a warning for now
|
||||
'require-atomic-updates': 'warn',
|
||||
|
||||
strict: 'error',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
"url": "https://vates.fr"
|
||||
},
|
||||
"license": "ISC",
|
||||
"version": "0.1.5",
|
||||
"version": "0.1.4",
|
||||
"engines": {
|
||||
"node": ">=8.10"
|
||||
},
|
||||
@@ -23,7 +23,7 @@
|
||||
"test": "node--test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vates/multi-key-map": "^0.2.0",
|
||||
"@vates/multi-key-map": "^0.1.0",
|
||||
"@xen-orchestra/async-map": "^0.1.2",
|
||||
"@xen-orchestra/log": "^0.6.0",
|
||||
"ensure-array": "^1.0.0"
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
"fuse-native": "^2.2.6",
|
||||
"lru-cache": "^7.14.0",
|
||||
"promise-toolbox": "^0.21.0",
|
||||
"vhd-lib": "^4.7.0"
|
||||
"vhd-lib": "^4.6.1"
|
||||
},
|
||||
"scripts": {
|
||||
"postversion": "npm publish --access public"
|
||||
|
||||
@@ -17,14 +17,4 @@ map.get(['foo', 'bar']) // 2
|
||||
map.get(['bar', 'foo']) // 3
|
||||
map.get([OBJ]) // 4
|
||||
map.get([{}]) // undefined
|
||||
|
||||
map.delete([])
|
||||
|
||||
for (const [key, value] of map.entries() {
|
||||
console.log(key, value)
|
||||
}
|
||||
|
||||
for (const value of map.values()) {
|
||||
console.log(value)
|
||||
}
|
||||
```
|
||||
|
||||
@@ -35,16 +35,6 @@ map.get(['foo', 'bar']) // 2
|
||||
map.get(['bar', 'foo']) // 3
|
||||
map.get([OBJ]) // 4
|
||||
map.get([{}]) // undefined
|
||||
|
||||
map.delete([])
|
||||
|
||||
for (const [key, value] of map.entries() {
|
||||
console.log(key, value)
|
||||
}
|
||||
|
||||
for (const value of map.values()) {
|
||||
console.log(value)
|
||||
}
|
||||
```
|
||||
|
||||
## Contributions
|
||||
|
||||
@@ -36,23 +36,6 @@ function del(node, i, keys) {
|
||||
return node
|
||||
}
|
||||
|
||||
function* entries(node, key) {
|
||||
if (node !== undefined) {
|
||||
if (node instanceof Node) {
|
||||
const { value } = node
|
||||
if (value !== undefined) {
|
||||
yield [key, node.value]
|
||||
}
|
||||
|
||||
for (const [childKey, child] of node.children.entries()) {
|
||||
yield* entries(child, key.concat(childKey))
|
||||
}
|
||||
} else {
|
||||
yield [key, node]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function get(node, i, keys) {
|
||||
return i === keys.length
|
||||
? node instanceof Node
|
||||
@@ -86,22 +69,6 @@ function set(node, i, keys, value) {
|
||||
return node
|
||||
}
|
||||
|
||||
function* values(node) {
|
||||
if (node !== undefined) {
|
||||
if (node instanceof Node) {
|
||||
const { value } = node
|
||||
if (value !== undefined) {
|
||||
yield node.value
|
||||
}
|
||||
for (const child of node.children.values()) {
|
||||
yield* values(child)
|
||||
}
|
||||
} else {
|
||||
yield node
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
exports.MultiKeyMap = class MultiKeyMap {
|
||||
constructor() {
|
||||
// each node is either a value or a Node if it contains children
|
||||
@@ -112,10 +79,6 @@ exports.MultiKeyMap = class MultiKeyMap {
|
||||
this._root = del(this._root, 0, keys)
|
||||
}
|
||||
|
||||
entries() {
|
||||
return entries(this._root, [])
|
||||
}
|
||||
|
||||
get(keys) {
|
||||
return get(this._root, 0, keys)
|
||||
}
|
||||
@@ -123,8 +86,4 @@ exports.MultiKeyMap = class MultiKeyMap {
|
||||
set(keys, value) {
|
||||
this._root = set(this._root, 0, keys, value)
|
||||
}
|
||||
|
||||
values() {
|
||||
return values(this._root)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ describe('MultiKeyMap', () => {
|
||||
// reverse composite key
|
||||
['bar', 'foo'],
|
||||
]
|
||||
const values = keys.map(() => Math.random())
|
||||
const values = keys.map(() => ({}))
|
||||
|
||||
// set all values first to make sure they are all stored and not only the
|
||||
// last one
|
||||
@@ -27,12 +27,6 @@ describe('MultiKeyMap', () => {
|
||||
map.set(key, values[i])
|
||||
})
|
||||
|
||||
assert.deepEqual(
|
||||
Array.from(map.entries()),
|
||||
keys.map((key, i) => [key, values[i]])
|
||||
)
|
||||
assert.deepEqual(Array.from(map.values()), values)
|
||||
|
||||
keys.forEach((key, i) => {
|
||||
// copy the key to make sure the array itself is not the key
|
||||
assert.strictEqual(map.get(key.slice()), values[i])
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
"url": "https://vates.fr"
|
||||
},
|
||||
"license": "ISC",
|
||||
"version": "0.2.0",
|
||||
"version": "0.1.0",
|
||||
"engines": {
|
||||
"node": ">=8.10"
|
||||
},
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"url": "https://vates.fr"
|
||||
},
|
||||
"license": "ISC",
|
||||
"version": "2.0.1",
|
||||
"version": "2.0.0",
|
||||
"engines": {
|
||||
"node": ">=14.0"
|
||||
},
|
||||
@@ -24,7 +24,7 @@
|
||||
"@xen-orchestra/async-map": "^0.1.2",
|
||||
"@xen-orchestra/log": "^0.6.0",
|
||||
"promise-toolbox": "^0.21.0",
|
||||
"xen-api": "^2.0.0"
|
||||
"xen-api": "^1.3.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"tap": "^16.3.0",
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
|
||||
"dependencies": {
|
||||
"@xen-orchestra/async-map": "^0.1.2",
|
||||
"@xen-orchestra/backups": "^0.44.2",
|
||||
"@xen-orchestra/fs": "^4.1.3",
|
||||
"@xen-orchestra/backups": "^0.43.2",
|
||||
"@xen-orchestra/fs": "^4.1.2",
|
||||
"filenamify": "^6.0.0",
|
||||
"getopts": "^2.2.5",
|
||||
"lodash": "^4.17.15",
|
||||
@@ -27,7 +27,7 @@
|
||||
"scripts": {
|
||||
"postversion": "npm publish --access public"
|
||||
},
|
||||
"version": "1.0.14",
|
||||
"version": "1.0.13",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"author": {
|
||||
"name": "Vates SAS",
|
||||
|
||||
@@ -4,14 +4,7 @@ import { formatFilenameDate } from './_filenameDate.mjs'
|
||||
import { importIncrementalVm } from './_incrementalVm.mjs'
|
||||
import { Task } from './Task.mjs'
|
||||
import { watchStreamSize } from './_watchStreamSize.mjs'
|
||||
import { VhdNegative, VhdSynthetic } from 'vhd-lib'
|
||||
import { decorateClass } from '@vates/decorate-with'
|
||||
import { createLogger } from '@xen-orchestra/log'
|
||||
import { dirname, join } from 'node:path'
|
||||
import pickBy from 'lodash/pickBy.js'
|
||||
import { defer } from 'golike-defer'
|
||||
|
||||
const { debug, info, warn } = createLogger('xo:backups:importVmBackup')
|
||||
async function resolveUuid(xapi, cache, uuid, type) {
|
||||
if (uuid == null) {
|
||||
return uuid
|
||||
@@ -23,199 +16,26 @@ async function resolveUuid(xapi, cache, uuid, type) {
|
||||
return cache.get(uuid)
|
||||
}
|
||||
export class ImportVmBackup {
|
||||
constructor({
|
||||
adapter,
|
||||
metadata,
|
||||
srUuid,
|
||||
xapi,
|
||||
settings: { additionnalVmTag, newMacAddresses, mapVdisSrs = {}, useDifferentialRestore = false } = {},
|
||||
}) {
|
||||
constructor({ adapter, metadata, srUuid, xapi, settings: { newMacAddresses, mapVdisSrs = {} } = {} }) {
|
||||
this._adapter = adapter
|
||||
this._importIncrementalVmSettings = { additionnalVmTag, newMacAddresses, mapVdisSrs, useDifferentialRestore }
|
||||
this._importIncrementalVmSettings = { newMacAddresses, mapVdisSrs }
|
||||
this._metadata = metadata
|
||||
this._srUuid = srUuid
|
||||
this._xapi = xapi
|
||||
}
|
||||
|
||||
async #getPathOfVdiSnapshot(snapshotUuid) {
|
||||
const metadata = this._metadata
|
||||
if (this._pathToVdis === undefined) {
|
||||
const backups = await this._adapter.listVmBackups(
|
||||
this._metadata.vm.uuid,
|
||||
({ mode, timestamp }) => mode === 'delta' && timestamp >= metadata.timestamp
|
||||
)
|
||||
const map = new Map()
|
||||
for (const backup of backups) {
|
||||
for (const [vdiRef, vdi] of Object.entries(backup.vdis)) {
|
||||
map.set(vdi.uuid, backup.vhds[vdiRef])
|
||||
}
|
||||
}
|
||||
this._pathToVdis = map
|
||||
}
|
||||
return this._pathToVdis.get(snapshotUuid)
|
||||
}
|
||||
|
||||
async _reuseNearestSnapshot($defer, ignoredVdis) {
|
||||
const metadata = this._metadata
|
||||
async #decorateIncrementalVmMetadata(backup) {
|
||||
const { mapVdisSrs } = this._importIncrementalVmSettings
|
||||
const { vbds, vhds, vifs, vm, vmSnapshot } = metadata
|
||||
const streams = {}
|
||||
const metdataDir = dirname(metadata._filename)
|
||||
const vdis = ignoredVdis === undefined ? metadata.vdis : pickBy(metadata.vdis, vdi => !ignoredVdis.has(vdi.uuid))
|
||||
|
||||
for (const [vdiRef, vdi] of Object.entries(vdis)) {
|
||||
const vhdPath = join(metdataDir, vhds[vdiRef])
|
||||
|
||||
let xapiDisk
|
||||
try {
|
||||
xapiDisk = await this._xapi.getRecordByUuid('VDI', vdi.$snapshot_of$uuid)
|
||||
} catch (err) {
|
||||
// if this disk is not present anymore, fall back to default restore
|
||||
warn(err)
|
||||
}
|
||||
|
||||
let snapshotCandidate, backupCandidate
|
||||
if (xapiDisk !== undefined) {
|
||||
debug('found disks, wlll search its snapshots', { snapshots: xapiDisk.snapshots })
|
||||
for (const snapshotRef of xapiDisk.snapshots) {
|
||||
const snapshot = await this._xapi.getRecord('VDI', snapshotRef)
|
||||
debug('handling snapshot', { snapshot })
|
||||
|
||||
// take only the first snapshot
|
||||
if (snapshotCandidate && snapshotCandidate.snapshot_time < snapshot.snapshot_time) {
|
||||
debug('already got a better candidate')
|
||||
continue
|
||||
}
|
||||
|
||||
// have a corresponding backup more recent than metadata ?
|
||||
const pathToSnapshotData = await this.#getPathOfVdiSnapshot(snapshot.uuid)
|
||||
if (pathToSnapshotData === undefined) {
|
||||
debug('no backup linked to this snaphot')
|
||||
continue
|
||||
}
|
||||
if (snapshot.$SR.uuid !== (mapVdisSrs[vdi.$snapshot_of$uuid] ?? this._srUuid)) {
|
||||
debug('not restored on the same SR', { snapshotSr: snapshot.$SR.uuid, mapVdisSrs, srUuid: this._srUuid })
|
||||
continue
|
||||
}
|
||||
|
||||
debug('got a candidate', pathToSnapshotData)
|
||||
|
||||
snapshotCandidate = snapshot
|
||||
backupCandidate = pathToSnapshotData
|
||||
}
|
||||
}
|
||||
|
||||
let stream
|
||||
const backupWithSnapshotPath = join(metdataDir, backupCandidate ?? '')
|
||||
if (vhdPath === backupWithSnapshotPath) {
|
||||
// all the data are already on the host
|
||||
debug('direct reuse of a snapshot')
|
||||
stream = null
|
||||
vdis[vdiRef].baseVdi = snapshotCandidate
|
||||
// go next disk , we won't use this stream
|
||||
continue
|
||||
}
|
||||
|
||||
let disposableDescendants
|
||||
|
||||
const disposableSynthetic = await VhdSynthetic.fromVhdChain(this._adapter._handler, vhdPath)
|
||||
|
||||
// this will also clean if another disk of this VM backup fails
|
||||
// if user really only need to restore non failing disks he can retry with ignoredVdis
|
||||
let disposed = false
|
||||
const disposeOnce = async () => {
|
||||
if (!disposed) {
|
||||
disposed = true
|
||||
try {
|
||||
await disposableDescendants?.dispose()
|
||||
await disposableSynthetic?.dispose()
|
||||
} catch (error) {
|
||||
warn('openVhd: failed to dispose VHDs', { error })
|
||||
}
|
||||
}
|
||||
}
|
||||
$defer.onFailure(() => disposeOnce())
|
||||
|
||||
const parentVhd = disposableSynthetic.value
|
||||
await parentVhd.readBlockAllocationTable()
|
||||
debug('got vhd synthetic of parents', parentVhd.length)
|
||||
|
||||
if (snapshotCandidate !== undefined) {
|
||||
try {
|
||||
debug('will try to use differential restore', {
|
||||
backupWithSnapshotPath,
|
||||
vhdPath,
|
||||
vdiRef,
|
||||
})
|
||||
|
||||
disposableDescendants = await VhdSynthetic.fromVhdChain(this._adapter._handler, backupWithSnapshotPath, {
|
||||
until: vhdPath,
|
||||
})
|
||||
const descendantsVhd = disposableDescendants.value
|
||||
await descendantsVhd.readBlockAllocationTable()
|
||||
debug('got vhd synthetic of descendants')
|
||||
const negativeVhd = new VhdNegative(parentVhd, descendantsVhd)
|
||||
debug('got vhd negative')
|
||||
|
||||
// update the stream with the negative vhd stream
|
||||
stream = await negativeVhd.stream()
|
||||
vdis[vdiRef].baseVdi = snapshotCandidate
|
||||
} catch (err) {
|
||||
// can be a broken VHD chain, a vhd chain with a key backup, ....
|
||||
// not an irrecuperable error, don't dispose parentVhd, and fallback to full restore
|
||||
warn(`can't use differential restore`, err)
|
||||
disposableDescendants?.dispose()
|
||||
}
|
||||
}
|
||||
// didn't make a negative stream : fallback to classic stream
|
||||
if (stream === undefined) {
|
||||
debug('use legacy restore')
|
||||
stream = await parentVhd.stream()
|
||||
}
|
||||
|
||||
stream.on('end', disposeOnce)
|
||||
stream.on('close', disposeOnce)
|
||||
stream.on('error', disposeOnce)
|
||||
info('everything is ready, will transfer', stream.length)
|
||||
streams[`${vdiRef}.vhd`] = stream
|
||||
}
|
||||
return {
|
||||
streams,
|
||||
vbds,
|
||||
vdis,
|
||||
version: '1.0.0',
|
||||
vifs,
|
||||
vm: { ...vm, suspend_VDI: vmSnapshot.suspend_VDI },
|
||||
}
|
||||
}
|
||||
|
||||
async #decorateIncrementalVmMetadata() {
|
||||
const { additionnalVmTag, mapVdisSrs, useDifferentialRestore } = this._importIncrementalVmSettings
|
||||
|
||||
const ignoredVdis = new Set(
|
||||
Object.entries(mapVdisSrs)
|
||||
.filter(([_, srUuid]) => srUuid === null)
|
||||
.map(([vdiUuid]) => vdiUuid)
|
||||
)
|
||||
let backup
|
||||
if (useDifferentialRestore) {
|
||||
backup = await this._reuseNearestSnapshot(ignoredVdis)
|
||||
} else {
|
||||
backup = await this._adapter.readIncrementalVmBackup(this._metadata, ignoredVdis)
|
||||
}
|
||||
const xapi = this._xapi
|
||||
|
||||
const cache = new Map()
|
||||
const mapVdisSrRefs = {}
|
||||
if (additionnalVmTag !== undefined) {
|
||||
backup.vm.tags.push(additionnalVmTag)
|
||||
}
|
||||
for (const [vdiUuid, srUuid] of Object.entries(mapVdisSrs)) {
|
||||
mapVdisSrRefs[vdiUuid] = await resolveUuid(xapi, cache, srUuid, 'SR')
|
||||
}
|
||||
const srRef = await resolveUuid(xapi, cache, this._srUuid, 'SR')
|
||||
const sr = await resolveUuid(xapi, cache, this._srUuid, 'SR')
|
||||
Object.values(backup.vdis).forEach(vdi => {
|
||||
vdi.SR = mapVdisSrRefs[vdi.uuid] ?? srRef
|
||||
vdi.SR = mapVdisSrRefs[vdi.uuid] ?? sr.$ref
|
||||
})
|
||||
return backup
|
||||
}
|
||||
@@ -226,7 +46,7 @@ export class ImportVmBackup {
|
||||
const isFull = metadata.mode === 'full'
|
||||
|
||||
const sizeContainer = { size: 0 }
|
||||
const { newMacAddresses } = this._importIncrementalVmSettings
|
||||
const { mapVdisSrs, newMacAddresses } = this._importIncrementalVmSettings
|
||||
let backup
|
||||
if (isFull) {
|
||||
backup = await adapter.readFullVmBackup(metadata)
|
||||
@@ -234,7 +54,12 @@ export class ImportVmBackup {
|
||||
} else {
|
||||
assert.strictEqual(metadata.mode, 'delta')
|
||||
|
||||
backup = await this.#decorateIncrementalVmMetadata()
|
||||
const ignoredVdis = new Set(
|
||||
Object.entries(mapVdisSrs)
|
||||
.filter(([_, srUuid]) => srUuid === null)
|
||||
.map(([vdiUuid]) => vdiUuid)
|
||||
)
|
||||
backup = await this.#decorateIncrementalVmMetadata(await adapter.readIncrementalVmBackup(metadata, ignoredVdis))
|
||||
Object.values(backup.streams).forEach(stream => watchStreamSize(stream, sizeContainer))
|
||||
}
|
||||
|
||||
@@ -276,5 +101,3 @@ export class ImportVmBackup {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
decorateClass(ImportVmBackup, { _reuseNearestSnapshot: defer })
|
||||
|
||||
@@ -250,10 +250,6 @@ export const importIncrementalVm = defer(async function importIncrementalVm(
|
||||
// Import VDI contents.
|
||||
cancelableMap(cancelToken, Object.entries(newVdis), async (cancelToken, [id, vdi]) => {
|
||||
for (let stream of ensureArray(streams[`${id}.vhd`])) {
|
||||
if (stream === null) {
|
||||
// we restore a backup and reuse completly a local snapshot
|
||||
continue
|
||||
}
|
||||
if (typeof stream === 'function') {
|
||||
stream = await stream()
|
||||
}
|
||||
|
||||
@@ -96,9 +96,6 @@ export const MixinRemoteWriter = (BaseClass = Object) =>
|
||||
metadata,
|
||||
srUuid,
|
||||
xapi,
|
||||
settings: {
|
||||
additionnalVmTag: 'xo:no-bak=Health Check',
|
||||
},
|
||||
}).run()
|
||||
const restoredVm = xapi.getObject(restoredId)
|
||||
try {
|
||||
|
||||
@@ -58,7 +58,7 @@ export const MixinXapiWriter = (BaseClass = Object) =>
|
||||
)
|
||||
}
|
||||
const healthCheckVm = xapi.getObject(healthCheckVmRef) ?? (await xapi.waitObject(healthCheckVmRef))
|
||||
await healthCheckVm.add_tag('xo:no-bak=Health Check')
|
||||
|
||||
await new HealthCheckVmBackup({
|
||||
restoredVm: healthCheckVm,
|
||||
xapi,
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"type": "git",
|
||||
"url": "https://github.com/vatesfr/xen-orchestra.git"
|
||||
},
|
||||
"version": "0.44.2",
|
||||
"version": "0.43.2",
|
||||
"engines": {
|
||||
"node": ">=14.18"
|
||||
},
|
||||
@@ -23,12 +23,12 @@
|
||||
"@vates/cached-dns.lookup": "^1.0.0",
|
||||
"@vates/compose": "^2.1.0",
|
||||
"@vates/decorate-with": "^2.0.0",
|
||||
"@vates/disposable": "^0.1.5",
|
||||
"@vates/disposable": "^0.1.4",
|
||||
"@vates/fuse-vhd": "^2.0.0",
|
||||
"@vates/nbd-client": "^2.0.1",
|
||||
"@vates/nbd-client": "^2.0.0",
|
||||
"@vates/parse-duration": "^0.1.1",
|
||||
"@xen-orchestra/async-map": "^0.1.2",
|
||||
"@xen-orchestra/fs": "^4.1.3",
|
||||
"@xen-orchestra/fs": "^4.1.2",
|
||||
"@xen-orchestra/log": "^0.6.0",
|
||||
"@xen-orchestra/template": "^0.1.0",
|
||||
"app-conf": "^2.3.0",
|
||||
@@ -44,8 +44,8 @@
|
||||
"proper-lockfile": "^4.1.2",
|
||||
"tar": "^6.1.15",
|
||||
"uuid": "^9.0.0",
|
||||
"vhd-lib": "^4.7.0",
|
||||
"xen-api": "^2.0.0",
|
||||
"vhd-lib": "^4.6.1",
|
||||
"xen-api": "^1.3.6",
|
||||
"yazl": "^2.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -56,7 +56,7 @@
|
||||
"tmp": "^0.2.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@xen-orchestra/xapi": "^4.0.0"
|
||||
"@xen-orchestra/xapi": "^3.3.0"
|
||||
},
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"author": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": false,
|
||||
"name": "@xen-orchestra/cr-seed-cli",
|
||||
"version": "1.0.0",
|
||||
"version": "0.2.0",
|
||||
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/cr-seed-cli",
|
||||
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
|
||||
"repository": {
|
||||
@@ -18,7 +18,7 @@
|
||||
"preferGlobal": true,
|
||||
"dependencies": {
|
||||
"golike-defer": "^0.5.1",
|
||||
"xen-api": "^2.0.0"
|
||||
"xen-api": "^1.3.6"
|
||||
},
|
||||
"scripts": {
|
||||
"postversion": "npm publish"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": false,
|
||||
"name": "@xen-orchestra/fs",
|
||||
"version": "4.1.3",
|
||||
"version": "4.1.2",
|
||||
"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",
|
||||
|
||||
@@ -33,7 +33,7 @@ import { pRetry } from 'promise-toolbox'
|
||||
const MAX_PART_SIZE = 1024 * 1024 * 1024 * 5 // 5GB
|
||||
const MAX_PART_NUMBER = 10000
|
||||
const MIN_PART_SIZE = 5 * 1024 * 1024
|
||||
const { debug, info, warn } = createLogger('xo:fs:s3')
|
||||
const { warn } = createLogger('xo:fs:s3')
|
||||
|
||||
export default class S3Handler extends RemoteHandlerAbstract {
|
||||
#bucket
|
||||
@@ -453,18 +453,10 @@ export default class S3Handler extends RemoteHandlerAbstract {
|
||||
if (res.ObjectLockConfiguration?.ObjectLockEnabled === 'Enabled') {
|
||||
// Workaround for https://github.com/aws/aws-sdk-js-v3/issues/2673
|
||||
// will automatically add the contentMD5 header to any upload to S3
|
||||
debug(`Object Lock is enable, enable content md5 header`)
|
||||
this.#s3.middlewareStack.use(getApplyMd5BodyChecksumPlugin(this.#s3.config))
|
||||
}
|
||||
} catch (error) {
|
||||
// maybe the account doesn't have enought privilege to query the object lock configuration
|
||||
// be defensive and apply the md5 just in case
|
||||
if (error.$metadata.httpStatusCode === 403) {
|
||||
info(`s3 user doesnt have enough privilege to check for Object Lock, enable content MD5 header`)
|
||||
this.#s3.middlewareStack.use(getApplyMd5BodyChecksumPlugin(this.#s3.config))
|
||||
} else if (error.Code === 'ObjectLockConfigurationNotFoundError' || error.$metadata.httpStatusCode === 501) {
|
||||
info(`Object lock is not available or not configured, don't add the content MD5 header`)
|
||||
} else {
|
||||
if (error.Code !== 'ObjectLockConfigurationNotFoundError' && error.$metadata.httpStatusCode !== 501) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,17 +2,10 @@
|
||||
|
||||
## **next**
|
||||
|
||||
- [VM/Action] Ability to migrate a VM from its view (PR [#7164](https://github.com/vatesfr/xen-orchestra/pull/7164))
|
||||
- Ability to override host address with `master` URL query param (PR [#7187](https://github.com/vatesfr/xen-orchestra/pull/7187))
|
||||
- Added tooltip on CPU provisioning warning icon (PR [#7223](https://github.com/vatesfr/xen-orchestra/pull/7223))
|
||||
|
||||
## **0.1.6** (2023-11-30)
|
||||
|
||||
- Explicit error if users attempt to connect from a slave host (PR [#7110](https://github.com/vatesfr/xen-orchestra/pull/7110))
|
||||
- More compact UI (PR [#7159](https://github.com/vatesfr/xen-orchestra/pull/7159))
|
||||
- Fix dashboard host patches list (PR [#7169](https://github.com/vatesfr/xen-orchestra/pull/7169))
|
||||
- Ability to export selected VMs (PR [#7174](https://github.com/vatesfr/xen-orchestra/pull/7174))
|
||||
- [VM/Action] Ability to export a VM from its view (PR [#7190](https://github.com/vatesfr/xen-orchestra/pull/7190))
|
||||
|
||||
## **0.1.5** (2023-11-07)
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@xen-orchestra/lite",
|
||||
"version": "0.1.6",
|
||||
"version": "0.1.5",
|
||||
"scripts": {
|
||||
"dev": "GIT_HEAD=$(git rev-parse HEAD) vite",
|
||||
"build": "run-p type-check build-only",
|
||||
@@ -10,55 +10,57 @@
|
||||
"test": "yarn run type-check",
|
||||
"type-check": "vue-tsc --noEmit"
|
||||
},
|
||||
"devDependencies": {
|
||||
"dependencies": {
|
||||
"@fontsource/poppins": "^5.0.8",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.4.2",
|
||||
"@fortawesome/free-regular-svg-icons": "^6.4.2",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.4.2",
|
||||
"@fortawesome/vue-fontawesome": "^3.0.5",
|
||||
"@intlify/unplugin-vue-i18n": "^1.5.0",
|
||||
"@limegrass/eslint-plugin-import-alias": "^1.1.0",
|
||||
"@novnc/novnc": "^1.4.0",
|
||||
"@rushstack/eslint-patch": "^1.5.1",
|
||||
"@tsconfig/node18": "^18.2.2",
|
||||
"@types/d3-time-format": "^4.0.3",
|
||||
"@types/file-saver": "^2.0.7",
|
||||
"@types/lodash-es": "^4.17.11",
|
||||
"@types/node": "^18.18.9",
|
||||
"@vitejs/plugin-vue": "^4.4.0",
|
||||
"@vue/eslint-config-prettier": "^8.0.0",
|
||||
"@vue/eslint-config-typescript": "^12.0.0",
|
||||
"@vue/tsconfig": "^0.4.0",
|
||||
"@vueuse/core": "^10.5.0",
|
||||
"@vueuse/math": "^10.5.0",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.1.1",
|
||||
"@fortawesome/free-regular-svg-icons": "^6.2.0",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.2.0",
|
||||
"@fortawesome/vue-fontawesome": "^3.0.1",
|
||||
"@novnc/novnc": "^1.3.0",
|
||||
"@types/d3-time-format": "^4.0.0",
|
||||
"@types/file-saver": "^2.0.5",
|
||||
"@types/lodash-es": "^4.17.6",
|
||||
"@types/marked": "^4.0.8",
|
||||
"@vueuse/core": "^10.1.2",
|
||||
"@vueuse/math": "^10.1.2",
|
||||
"complex-matcher": "^0.7.1",
|
||||
"d3-time-format": "^4.1.0",
|
||||
"decorator-synchronized": "^0.6.0",
|
||||
"echarts": "^5.4.3",
|
||||
"eslint-plugin-vue": "^9.18.1",
|
||||
"echarts": "^5.3.3",
|
||||
"file-saver": "^2.0.5",
|
||||
"highlight.js": "^11.9.0",
|
||||
"human-format": "^1.2.0",
|
||||
"highlight.js": "^11.6.0",
|
||||
"human-format": "^1.1.0",
|
||||
"iterable-backoff": "^0.1.0",
|
||||
"json-rpc-2.0": "^1.7.0",
|
||||
"json5": "^2.2.3",
|
||||
"json-rpc-2.0": "^1.3.0",
|
||||
"json5": "^2.2.1",
|
||||
"limit-concurrency-decorator": "^0.5.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"make-error": "^1.3.6",
|
||||
"marked": "^9.1.5",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"pinia": "^2.1.7",
|
||||
"marked": "^4.2.12",
|
||||
"pinia": "^2.1.2",
|
||||
"placement.js": "^1.0.0-beta.5",
|
||||
"postcss": "^8.4.31",
|
||||
"postcss-custom-media": "^10.0.2",
|
||||
"postcss-nested": "^6.0.1",
|
||||
"typescript": "^5.2.2",
|
||||
"vite": "^4.5.0",
|
||||
"vue": "^3.3.8",
|
||||
"vue-echarts": "^6.6.1",
|
||||
"vue-i18n": "^9.6.5",
|
||||
"vue-router": "^4.2.5",
|
||||
"vue-tsc": "^1.8.22"
|
||||
"vue": "^3.3.4",
|
||||
"vue-echarts": "^6.2.3",
|
||||
"vue-i18n": "^9.2.2",
|
||||
"vue-router": "^4.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@intlify/unplugin-vue-i18n": "^0.10.0",
|
||||
"@limegrass/eslint-plugin-import-alias": "^1.0.5",
|
||||
"@rushstack/eslint-patch": "^1.1.0",
|
||||
"@types/node": "^16.11.41",
|
||||
"@vitejs/plugin-vue": "^4.2.3",
|
||||
"@vue/eslint-config-prettier": "^8.0.0",
|
||||
"@vue/eslint-config-typescript": "^11.0.0",
|
||||
"@vue/tsconfig": "^0.1.3",
|
||||
"eslint-plugin-vue": "^9.0.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"postcss": "^8.4.19",
|
||||
"postcss-custom-media": "^9.0.1",
|
||||
"postcss-nested": "^6.0.0",
|
||||
"typescript": "^4.9.3",
|
||||
"vite": "^4.3.8",
|
||||
"vue-tsc": "^1.6.5"
|
||||
},
|
||||
"private": true,
|
||||
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/lite",
|
||||
@@ -74,6 +76,6 @@
|
||||
},
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
"node": ">=8.10"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
</RouterLink>
|
||||
<slot />
|
||||
<div class="right">
|
||||
<PoolOverrideWarning as-tooltip />
|
||||
<AccountButton />
|
||||
</div>
|
||||
</header>
|
||||
@@ -20,7 +19,6 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import AccountButton from "@/components/AccountButton.vue";
|
||||
import PoolOverrideWarning from "@/components/PoolOverrideWarning.vue";
|
||||
import TextLogo from "@/components/TextLogo.vue";
|
||||
import UiIcon from "@/components/ui/icon/UiIcon.vue";
|
||||
import { useNavigationStore } from "@/stores/navigation.store";
|
||||
@@ -53,10 +51,6 @@ const { trigger: navigationTrigger } = storeToRefs(navigationStore);
|
||||
margin-left: 1rem;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.warning-not-current-pool {
|
||||
font-size: 2.4rem;
|
||||
}
|
||||
}
|
||||
|
||||
.right {
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
<div class="app-login form-container">
|
||||
<form @submit.prevent="handleSubmit">
|
||||
<img alt="XO Lite" src="../assets/logo-title.svg" />
|
||||
<PoolOverrideWarning />
|
||||
<p v-if="isHostIsSlaveErr(error)" class="error">
|
||||
<UiIcon :icon="faExclamationCircle" />
|
||||
{{ $t("login-only-on-master") }}
|
||||
@@ -46,7 +45,6 @@ import FormCheckbox from "@/components/form/FormCheckbox.vue";
|
||||
import FormInput from "@/components/form/FormInput.vue";
|
||||
import FormInputWrapper from "@/components/form/FormInputWrapper.vue";
|
||||
import LoginError from "@/components/LoginError.vue";
|
||||
import PoolOverrideWarning from "@/components/PoolOverrideWarning.vue";
|
||||
import UiButton from "@/components/ui/UiButton.vue";
|
||||
import UiIcon from "@/components/ui/icon/UiIcon.vue";
|
||||
import type { XenApiError } from "@/libs/xen-api/xen-api.types";
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="xenApi.isPoolOverridden"
|
||||
class="warning-not-current-pool"
|
||||
@click="xenApi.resetPoolMasterIp"
|
||||
v-tooltip="
|
||||
asTooltip && {
|
||||
placement: 'right',
|
||||
content: `
|
||||
${$t('you-are-currently-on', [masterSessionStorage])}.
|
||||
${$t('click-to-return-default-pool')}
|
||||
`,
|
||||
}
|
||||
"
|
||||
>
|
||||
<div class="wrapper">
|
||||
<UiIcon :icon="faWarning" />
|
||||
<p v-if="!asTooltip">
|
||||
<i18n-t keypath="you-are-currently-on">
|
||||
<strong>{{ masterSessionStorage }}</strong>
|
||||
</i18n-t>
|
||||
<br />
|
||||
{{ $t("click-to-return-default-pool") }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { faWarning } from "@fortawesome/free-solid-svg-icons";
|
||||
import { useSessionStorage } from "@vueuse/core";
|
||||
|
||||
import UiIcon from "@/components/ui/icon/UiIcon.vue";
|
||||
import { useXenApiStore } from "@/stores/xen-api.store";
|
||||
import { vTooltip } from "@/directives/tooltip.directive";
|
||||
|
||||
defineProps<{
|
||||
asTooltip?: boolean;
|
||||
}>();
|
||||
|
||||
const xenApi = useXenApiStore();
|
||||
const masterSessionStorage = useSessionStorage("master", null);
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.warning-not-current-pool {
|
||||
color: var(--color-orange-world-base);
|
||||
cursor: pointer;
|
||||
|
||||
.wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
svg {
|
||||
margin: auto 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -3,14 +3,8 @@
|
||||
<UiCardTitle>
|
||||
{{ $t("cpu-provisioning") }}
|
||||
<template v-if="!hasError" #right>
|
||||
<UiStatusIcon
|
||||
v-if="state !== 'success'"
|
||||
v-tooltip="{
|
||||
content: $t('cpu-provisioning-warning'),
|
||||
placement: 'left',
|
||||
}"
|
||||
:state="state"
|
||||
/>
|
||||
<!-- TODO: add a tooltip for the warning icon -->
|
||||
<UiStatusIcon v-if="state !== 'success'" :state="state" />
|
||||
</template>
|
||||
</UiCardTitle>
|
||||
<NoDataError v-if="hasError" />
|
||||
@@ -43,12 +37,11 @@ import UiCard from "@/components/ui/UiCard.vue";
|
||||
import UiCardFooter from "@/components/ui/UiCardFooter.vue";
|
||||
import UiCardSpinner from "@/components/ui/UiCardSpinner.vue";
|
||||
import UiCardTitle from "@/components/ui/UiCardTitle.vue";
|
||||
import { vTooltip } from "@/directives/tooltip.directive";
|
||||
import { useHostCollection } from "@/stores/xen-api/host.store";
|
||||
import { useVmCollection } from "@/stores/xen-api/vm.store";
|
||||
import { useVmMetricsCollection } from "@/stores/xen-api/vm-metrics.store";
|
||||
import { percent } from "@/libs/utils";
|
||||
import { VM_POWER_STATE } from "@/libs/xen-api/xen-api.enums";
|
||||
import { useHostCollection } from "@/stores/xen-api/host.store";
|
||||
import { useVmMetricsCollection } from "@/stores/xen-api/vm-metrics.store";
|
||||
import { useVmCollection } from "@/stores/xen-api/vm.store";
|
||||
import { logicAnd } from "@vueuse/math";
|
||||
import { computed } from "vue";
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import { useContext } from "@/composables/context.composable";
|
||||
import { ColorContext, DisabledContext } from "@/context";
|
||||
import type { Color } from "@/types";
|
||||
import { IK_MODAL } from "@/types/injection-keys";
|
||||
import { useMagicKeys, whenever } from "@vueuse/core";
|
||||
import { useMagicKeys, whenever } from "@vueuse/core/index";
|
||||
import { inject } from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
v-tooltip="
|
||||
vmRefs.length > 0 &&
|
||||
!isSomeExportable &&
|
||||
$t(isSingleAction ? 'vm-is-running' : 'no-selected-vm-can-be-exported')
|
||||
$t('no-selected-vm-can-be-exported')
|
||||
"
|
||||
:icon="faDisplay"
|
||||
:disabled="isDisabled"
|
||||
@click="openModal"
|
||||
>
|
||||
{{ $t(isSingleAction ? "export-vm" : "export-vms") }}
|
||||
{{ $t("export-vms") }}
|
||||
</MenuItem>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
@@ -26,10 +26,7 @@ import { vTooltip } from "@/directives/tooltip.directive";
|
||||
|
||||
import type { XenApiVm } from "@/libs/xen-api/xen-api.types";
|
||||
|
||||
const props = defineProps<{
|
||||
vmRefs: XenApiVm["$ref"][];
|
||||
isSingleAction?: boolean;
|
||||
}>();
|
||||
const props = defineProps<{ vmRefs: XenApiVm["$ref"][] }>();
|
||||
|
||||
const { getByOpaqueRefs, areSomeOperationAllowed } = useVmCollection();
|
||||
|
||||
|
||||
@@ -3,11 +3,7 @@
|
||||
v-tooltip="
|
||||
selectedRefs.length > 0 &&
|
||||
!isMigratable &&
|
||||
$t(
|
||||
isSingleAction
|
||||
? 'this-vm-cant-be-migrated'
|
||||
: 'no-selected-vm-can-be-migrated'
|
||||
)
|
||||
$t('no-selected-vm-can-be-migrated')
|
||||
"
|
||||
:busy="isMigrating"
|
||||
:disabled="isParentDisabled || !isMigratable"
|
||||
@@ -32,7 +28,6 @@ import { computed } from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
selectedRefs: XenApiVm["$ref"][];
|
||||
isSingleAction?: boolean;
|
||||
}>();
|
||||
|
||||
const { getByOpaqueRefs, isOperationPending, areSomeOperationAllowed } =
|
||||
|
||||
@@ -26,9 +26,7 @@
|
||||
/>
|
||||
</template>
|
||||
<VmActionCopyItem :selected-refs="[vm.$ref]" is-single-action />
|
||||
<VmActionExportItem :vm-refs="[vm.$ref]" is-single-action />
|
||||
<VmActionSnapshotItem :vm-refs="[vm.$ref]" />
|
||||
<VmActionMigrateItem :selected-refs="[vm.$ref]" is-single-action />
|
||||
</AppMenu>
|
||||
</template>
|
||||
</TitleBar>
|
||||
@@ -39,11 +37,9 @@ import AppMenu from "@/components/menu/AppMenu.vue";
|
||||
import TitleBar from "@/components/TitleBar.vue";
|
||||
import UiIcon from "@/components/ui/icon/UiIcon.vue";
|
||||
import UiButton from "@/components/ui/UiButton.vue";
|
||||
import VmActionMigrateItem from "@/components/vm/VmActionItems/VmActionMigrateItem.vue";
|
||||
import VmActionPowerStateItems from "@/components/vm/VmActionItems/VmActionPowerStateItems.vue";
|
||||
import VmActionSnapshotItem from "@/components/vm/VmActionItems/VmActionSnapshotItem.vue";
|
||||
import VmActionCopyItem from "@/components/vm/VmActionItems/VmActionCopyItem.vue";
|
||||
import VmActionExportItem from "@/components/vm/VmActionItems/VmActionExportItem.vue";
|
||||
import { useVmCollection } from "@/stores/xen-api/vm.store";
|
||||
import { vTooltip } from "@/directives/tooltip.directive";
|
||||
import type { XenApiVm } from "@/libs/xen-api/xen-api.types";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { HighlightResult } from "highlight.js";
|
||||
import type { HighlightResult, Language } from "highlight.js";
|
||||
import HLJS from "highlight.js/lib/core";
|
||||
import cssLang from "highlight.js/lib/languages/css";
|
||||
import jsonLang from "highlight.js/lib/languages/json";
|
||||
@@ -19,6 +19,10 @@ export const highlight: (
|
||||
ignoreIllegals?: boolean
|
||||
) => HighlightResult = HLJS.highlight;
|
||||
|
||||
export const getLanguage: (
|
||||
languageName: AcceptedLanguage
|
||||
) => Language | undefined = HLJS.getLanguage;
|
||||
|
||||
export type AcceptedLanguage =
|
||||
| "xml"
|
||||
| "css"
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { type AcceptedLanguage, highlight } from "@/libs/highlight";
|
||||
import HLJS from "highlight.js/lib/core";
|
||||
import {
|
||||
type AcceptedLanguage,
|
||||
getLanguage,
|
||||
highlight,
|
||||
} from "@/libs/highlight";
|
||||
import { marked } from "marked";
|
||||
|
||||
enum VUE_TAG {
|
||||
@@ -8,26 +11,15 @@ enum VUE_TAG {
|
||||
STYLE = "vue-style",
|
||||
}
|
||||
|
||||
function extractLang(lang: string | undefined): AcceptedLanguage | VUE_TAG {
|
||||
if (lang === undefined) {
|
||||
return "plaintext";
|
||||
}
|
||||
|
||||
if (Object.values(VUE_TAG).includes(lang as VUE_TAG)) {
|
||||
return lang as VUE_TAG;
|
||||
}
|
||||
|
||||
if (HLJS.getLanguage(lang) !== undefined) {
|
||||
return lang as AcceptedLanguage;
|
||||
}
|
||||
|
||||
return "plaintext";
|
||||
}
|
||||
|
||||
marked.use({
|
||||
renderer: {
|
||||
code(str, lang) {
|
||||
const code = customHighlight(str, extractLang(lang));
|
||||
code(str: string, lang: AcceptedLanguage) {
|
||||
const code = customHighlight(
|
||||
str,
|
||||
Object.values(VUE_TAG).includes(lang as VUE_TAG) || getLanguage(lang)
|
||||
? lang
|
||||
: "plaintext"
|
||||
);
|
||||
return `<pre class="hljs"><button class="copy-button" type="button">Copy</button><code class="hljs-code">${code}</code></pre>`;
|
||||
},
|
||||
},
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
"cancel": "Cancel",
|
||||
"change-state": "Change state",
|
||||
"click-to-display-alarms": "Click to display alarms:",
|
||||
"click-to-return-default-pool": "Click here to return to the default pool",
|
||||
"close": "Close",
|
||||
"coming-soon": "Coming soon!",
|
||||
"community": "Community",
|
||||
@@ -38,7 +37,6 @@
|
||||
"console-unavailable": "Console unavailable",
|
||||
"copy": "Copy",
|
||||
"cpu-provisioning": "CPU provisioning",
|
||||
"cpu-provisioning-warning": "The number of vCPUs allocated exceeds the number of physical CPUs available. System performance could be affected",
|
||||
"cpu-usage": "CPU usage",
|
||||
"dashboard": "Dashboard",
|
||||
"delete": "Delete",
|
||||
@@ -57,7 +55,6 @@
|
||||
"export-n-vms": "Export 1 VM | Export {n} VMs",
|
||||
"export-n-vms-manually": "Export 1 VM manually | Export {n} VMs manually",
|
||||
"export-table-to": "Export table to {type}",
|
||||
"export-vm": "Export VM",
|
||||
"export-vms": "Export VMs",
|
||||
"export-vms-manually-information": "Some VM exports were not able to start automatically, probably due to your browser settings. To export them, you should click on each one. (Alternatively, copy the link as well.)",
|
||||
"fetching-fresh-data": "Fetching fresh data",
|
||||
@@ -179,7 +176,6 @@
|
||||
"theme-auto": "Auto",
|
||||
"theme-dark": "Dark",
|
||||
"theme-light": "Light",
|
||||
"this-vm-cant-be-migrated": "This VM can't be migrated",
|
||||
"top-#": "Top {n}",
|
||||
"total-cpus": "Total CPUs",
|
||||
"total-free": "Total free",
|
||||
@@ -192,6 +188,5 @@
|
||||
"vm-is-running": "The VM is running",
|
||||
"vms": "VMs",
|
||||
"xo-lite-under-construction": "XOLite is under construction",
|
||||
"you-are-currently-on": "You are currently on: {0}",
|
||||
"zstd": "zstd"
|
||||
}
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
"cancel": "Annuler",
|
||||
"change-state": "Changer l'état",
|
||||
"click-to-display-alarms": "Cliquer pour afficher les alarmes :",
|
||||
"click-to-return-default-pool": "Cliquer ici pour revenir au pool par défaut",
|
||||
"close": "Fermer",
|
||||
"coming-soon": "Bientôt disponible !",
|
||||
"community": "Communauté",
|
||||
@@ -38,7 +37,6 @@
|
||||
"console-unavailable": "Console indisponible",
|
||||
"copy": "Copier",
|
||||
"cpu-provisioning": "Provisionnement CPU",
|
||||
"cpu-provisioning-warning": "Le nombre de vCPU alloués dépasse le nombre de CPU physique disponible. Les performances du système pourraient être affectées",
|
||||
"cpu-usage": "Utilisation CPU",
|
||||
"dashboard": "Tableau de bord",
|
||||
"delete": "Supprimer",
|
||||
@@ -57,7 +55,6 @@
|
||||
"export-n-vms": "Exporter 1 VM | Exporter {n} VMs",
|
||||
"export-n-vms-manually": "Exporter 1 VM manuellement | Exporter {n} VMs manuellement",
|
||||
"export-table-to": "Exporter le tableau en {type}",
|
||||
"export-vm": "Exporter la VM",
|
||||
"export-vms": "Exporter les VMs",
|
||||
"export-vms-manually-information": "Certaines exportations de VMs n'ont pas pu démarrer automatiquement, peut-être en raison des paramètres du navigateur. Pour les exporter, vous devrez cliquer sur chacune d'entre elles. (Ou copier le lien.)",
|
||||
"fetching-fresh-data": "Récupération de données à jour",
|
||||
@@ -179,7 +176,6 @@
|
||||
"theme-auto": "Auto",
|
||||
"theme-dark": "Sombre",
|
||||
"theme-light": "Clair",
|
||||
"this-vm-cant-be-migrated": "Cette VM ne peut pas être migrée",
|
||||
"top-#": "Top {n}",
|
||||
"total-cpus": "Total CPUs",
|
||||
"total-free": "Total libre",
|
||||
@@ -192,6 +188,5 @@
|
||||
"vm-is-running": "La VM est en cours d'exécution",
|
||||
"vms": "VMs",
|
||||
"xo-lite-under-construction": "XOLite est en construction",
|
||||
"you-are-currently-on": "Vous êtes actuellement sur : {0}",
|
||||
"zstd": "zstd"
|
||||
}
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import XapiStats from "@/libs/xapi-stats";
|
||||
import XenApi from "@/libs/xen-api/xen-api";
|
||||
import { useLocalStorage, useSessionStorage, whenever } from "@vueuse/core";
|
||||
import { useLocalStorage } from "@vueuse/core";
|
||||
import { defineStore } from "pinia";
|
||||
import { computed, ref, watchEffect } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useRoute } from "vue-router";
|
||||
|
||||
const HOST_URL = import.meta.env.PROD
|
||||
? window.origin
|
||||
@@ -17,27 +15,7 @@ enum STATUS {
|
||||
}
|
||||
|
||||
export const useXenApiStore = defineStore("xen-api", () => {
|
||||
// undefined not correctly handled. See https://github.com/vueuse/vueuse/issues/3595
|
||||
const masterSessionStorage = useSessionStorage<null | string>("master", null);
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
whenever(
|
||||
() => route.query.master,
|
||||
async (newMaster) => {
|
||||
masterSessionStorage.value = newMaster as string;
|
||||
await router.replace({ query: { ...route.query, master: undefined } });
|
||||
window.location.reload();
|
||||
}
|
||||
);
|
||||
|
||||
const hostUrl = new URL(HOST_URL);
|
||||
if (masterSessionStorage.value !== null) {
|
||||
hostUrl.hostname = masterSessionStorage.value;
|
||||
}
|
||||
|
||||
const isPoolOverridden = hostUrl.origin !== new URL(HOST_URL).origin;
|
||||
const xenApi = new XenApi(hostUrl.origin);
|
||||
const xenApi = new XenApi(HOST_URL);
|
||||
const xapiStats = new XapiStats(xenApi);
|
||||
const storedSessionId = useLocalStorage<string | undefined>(
|
||||
"sessionId",
|
||||
@@ -97,21 +75,14 @@ export const useXenApiStore = defineStore("xen-api", () => {
|
||||
status.value = STATUS.DISCONNECTED;
|
||||
}
|
||||
|
||||
function resetPoolMasterIp() {
|
||||
masterSessionStorage.value = null;
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
return {
|
||||
isConnected,
|
||||
isConnecting,
|
||||
isPoolOverridden,
|
||||
connect,
|
||||
reconnect,
|
||||
disconnect,
|
||||
getXapi,
|
||||
getXapiStats,
|
||||
currentSessionId,
|
||||
resetPoolMasterIp,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -135,15 +135,23 @@
|
||||
</UiCard>
|
||||
<UiCard class="group">
|
||||
<UiCardTitle>{{ $t("language") }}</UiCardTitle>
|
||||
<FormSelect :before="faEarthAmericas" v-model="$i18n.locale">
|
||||
<option
|
||||
:value="locale"
|
||||
v-for="locale in $i18n.availableLocales"
|
||||
:key="locale"
|
||||
>
|
||||
{{ locales[locale].name ?? locale }}
|
||||
</option>
|
||||
</FormSelect>
|
||||
<UiKeyValueList>
|
||||
<UiKeyValueRow>
|
||||
<template #value>
|
||||
<FormWidget class="full-length" :before="faEarthAmericas">
|
||||
<select v-model="$i18n.locale">
|
||||
<option
|
||||
:value="locale"
|
||||
v-for="locale in $i18n.availableLocales"
|
||||
:key="locale"
|
||||
>
|
||||
{{ locales[locale].name ?? locale }}
|
||||
</option>
|
||||
</select>
|
||||
</FormWidget>
|
||||
</template>
|
||||
</UiKeyValueRow>
|
||||
</UiKeyValueList>
|
||||
</UiCard>
|
||||
</div>
|
||||
</template>
|
||||
@@ -166,7 +174,7 @@ import {
|
||||
faGear,
|
||||
faCheck,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import FormSelect from "@/components/form/FormSelect.vue";
|
||||
import FormWidget from "@/components/FormWidget.vue";
|
||||
import TitleBar from "@/components/TitleBar.vue";
|
||||
import UiCard from "@/components/ui/UiCard.vue";
|
||||
import UiKeyValueList from "@/components/ui/UiKeyValueList.vue";
|
||||
@@ -241,4 +249,8 @@ h5 {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.full-length {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"extends": ["@tsconfig/node18/tsconfig.json", "@vue/tsconfig/tsconfig.json"],
|
||||
"extends": "@vue/tsconfig/tsconfig.node.json",
|
||||
"include": ["vite.config.*", "vitest.config.*", "cypress.config.*"],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||
"extends": "@vue/tsconfig/tsconfig.web.json",
|
||||
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
|
||||
"exclude": ["src/stories/**/*"],
|
||||
"compilerOptions": {
|
||||
"experimentalDecorators": true,
|
||||
"lib": ["ES2019", "ES2020.Intl", "dom"],
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "@xen-orchestra/proxy",
|
||||
"version": "0.26.41",
|
||||
"version": "0.26.38",
|
||||
"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.5",
|
||||
"@vates/disposable": "^0.1.4",
|
||||
"@xen-orchestra/async-map": "^0.1.2",
|
||||
"@xen-orchestra/backups": "^0.44.2",
|
||||
"@xen-orchestra/fs": "^4.1.3",
|
||||
"@xen-orchestra/backups": "^0.43.2",
|
||||
"@xen-orchestra/fs": "^4.1.2",
|
||||
"@xen-orchestra/log": "^0.6.0",
|
||||
"@xen-orchestra/mixin": "^0.1.0",
|
||||
"@xen-orchestra/mixins": "^0.14.0",
|
||||
"@xen-orchestra/self-signed": "^0.1.3",
|
||||
"@xen-orchestra/xapi": "^4.0.0",
|
||||
"@xen-orchestra/xapi": "^3.3.0",
|
||||
"ajv": "^8.0.3",
|
||||
"app-conf": "^2.3.0",
|
||||
"async-iterator-to-stream": "^1.1.0",
|
||||
@@ -60,7 +60,7 @@
|
||||
"source-map-support": "^0.5.16",
|
||||
"stoppable": "^1.0.6",
|
||||
"xdg-basedir": "^5.1.0",
|
||||
"xen-api": "^2.0.0",
|
||||
"xen-api": "^1.3.6",
|
||||
"xo-common": "^0.8.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
"pw": "^0.0.4",
|
||||
"xdg-basedir": "^4.0.0",
|
||||
"xo-lib": "^0.11.1",
|
||||
"xo-vmdk-to-vhd": "^2.5.7"
|
||||
"xo-vmdk-to-vhd": "^2.5.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.0.0",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"license": "ISC",
|
||||
"private": false,
|
||||
"version": "0.3.1",
|
||||
"version": "0.3.0",
|
||||
"name": "@xen-orchestra/vmware-explorer",
|
||||
"dependencies": {
|
||||
"@vates/node-vsphere-soap": "^2.0.0",
|
||||
@@ -10,7 +10,7 @@
|
||||
"@xen-orchestra/log": "^0.6.0",
|
||||
"lodash": "^4.17.21",
|
||||
"node-fetch": "^3.3.0",
|
||||
"vhd-lib": "^4.7.0"
|
||||
"vhd-lib": "^4.6.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@xen-orchestra/xapi",
|
||||
"version": "4.0.0",
|
||||
"version": "3.3.0",
|
||||
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/xapi",
|
||||
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
|
||||
"repository": {
|
||||
@@ -16,7 +16,7 @@
|
||||
},
|
||||
"main": "./index.mjs",
|
||||
"peerDependencies": {
|
||||
"xen-api": "^2.0.0"
|
||||
"xen-api": "^1.3.6"
|
||||
},
|
||||
"scripts": {
|
||||
"postversion": "npm publish --access public",
|
||||
@@ -25,7 +25,7 @@
|
||||
"dependencies": {
|
||||
"@vates/async-each": "^1.0.0",
|
||||
"@vates/decorate-with": "^2.0.0",
|
||||
"@vates/nbd-client": "^2.0.1",
|
||||
"@vates/nbd-client": "^2.0.0",
|
||||
"@xen-orchestra/async-map": "^0.1.2",
|
||||
"@xen-orchestra/log": "^0.6.0",
|
||||
"d3-time-format": "^4.1.0",
|
||||
@@ -34,7 +34,7 @@
|
||||
"json-rpc-protocol": "^0.13.2",
|
||||
"lodash": "^4.17.15",
|
||||
"promise-toolbox": "^0.21.0",
|
||||
"vhd-lib": "^4.7.0",
|
||||
"vhd-lib": "^4.6.1",
|
||||
"xo-common": "^0.8.0"
|
||||
},
|
||||
"private": false,
|
||||
|
||||
@@ -200,6 +200,18 @@ class Vm {
|
||||
}
|
||||
}
|
||||
|
||||
_safeSetIsATemplate(ref) {
|
||||
return pCatch.call(
|
||||
this.setField('VM', ref, 'is_a_template', false),
|
||||
|
||||
// Ignore if this fails due to license restriction
|
||||
//
|
||||
// see https://bugs.xenserver.org/browse/XSO-766
|
||||
{ code: 'LICENSE_RESTRICTION' },
|
||||
noop
|
||||
)
|
||||
}
|
||||
|
||||
async assertHealthyVdiChains(vmRef, tolerance = this._maxUncoalescedVdis) {
|
||||
const vdiRefs = {}
|
||||
;(await this.getRecords('VBD', await this.getField('VM', vmRef, 'VBDs'))).forEach(({ VDI: ref }) => {
|
||||
@@ -486,9 +498,10 @@ class Vm {
|
||||
if (useSnapshot === undefined) {
|
||||
useSnapshot = isVmRunning(vm)
|
||||
}
|
||||
let exportedVmRef, destroySnapshot
|
||||
let exportedVmRef, destroySnapshot, isSnapshot
|
||||
if (useSnapshot) {
|
||||
exportedVmRef = await this.VM_snapshot(vmRef, { cancelToken, name_label: `[XO Export] ${vm.name_label}` })
|
||||
isSnapshot = true
|
||||
destroySnapshot = () =>
|
||||
this.VM_destroy(exportedVmRef).catch(error => {
|
||||
warn('VM_export: failed to destroy snapshot', {
|
||||
@@ -500,8 +513,13 @@ class Vm {
|
||||
$defer.onFailure(destroySnapshot)
|
||||
} else {
|
||||
exportedVmRef = vmRef
|
||||
isSnapshot = vm.is_a_snapshot
|
||||
}
|
||||
try {
|
||||
// VM snapshots are marked as templates, unfortunately it does not play well with XVA export/import
|
||||
// which will import them as templates and not VM snapshots or plain VMs
|
||||
await this._safeSetIsATemplate(exportedVmRef, false)
|
||||
|
||||
const stream = await this.getResource(cancelToken, '/export/', {
|
||||
query: {
|
||||
ref: exportedVmRef,
|
||||
@@ -510,6 +528,16 @@ class Vm {
|
||||
task: taskRef,
|
||||
})
|
||||
|
||||
if (isSnapshot) {
|
||||
// FIXME: VM_IS_SNAPSHOT(OpaqueRef:757d6cfd-a185-4114-bfc8-fb9fdd279bf2, make_into_template)
|
||||
this._safeSetIsATemplate(exportedVmRef, true).catch(error => {
|
||||
warn('VM_export: failed to reset is_a_template on snapshot', {
|
||||
error,
|
||||
snapshotRef: exportedVmRef,
|
||||
vmRef,
|
||||
})
|
||||
})
|
||||
}
|
||||
if (useSnapshot) {
|
||||
stream.once('end', destroySnapshot).once('error', destroySnapshot)
|
||||
}
|
||||
@@ -665,18 +693,6 @@ class Vm {
|
||||
// detached async
|
||||
this._httpHook(vm, '/post-sync').catch(noop)
|
||||
|
||||
// VM snapshots are marked as templates, unfortunately it does not play well with XVA export/import
|
||||
// which will import them as templates and not VM snapshots or plain VMs
|
||||
await pCatch.call(
|
||||
this.setField('VM', ref, 'is_a_template', false),
|
||||
|
||||
// Ignore if this fails due to license restriction
|
||||
//
|
||||
// see https://bugs.xenserver.org/browse/XSO-766
|
||||
{ code: 'LICENSE_RESTRICTION' },
|
||||
noop
|
||||
)
|
||||
|
||||
if (destroyNobakVdis) {
|
||||
await asyncMap(await listNobakVbds(this, await this.getField('VM', ref, 'VBDs')), async vbd => {
|
||||
try {
|
||||
|
||||
58
CHANGELOG.md
58
CHANGELOG.md
@@ -1,61 +1,7 @@
|
||||
# ChangeLog
|
||||
|
||||
## **5.89.0** (2023-11-30)
|
||||
|
||||
<img id="latest" src="https://badgen.net/badge/channel/latest/yellow" alt="Channel: latest" />
|
||||
|
||||
### Highlights
|
||||
|
||||
- [Restore] Show source remote and restoration time on a restored VM (PR [#7186](https://github.com/vatesfr/xen-orchestra/pull/7186))
|
||||
- [Backup/Import] Show disk import status during Incremental Replication or restoration of Incremental Backup (PR [#7171](https://github.com/vatesfr/xen-orchestra/pull/7171))
|
||||
- [VM/Console] Add a message to indicate that the console view has been [disabled](https://support.citrix.com/article/CTX217766/how-to-disable-the-console-for-the-vm-in-xencenter) for this VM [#6319](https://github.com/vatesfr/xen-orchestra/issues/6319) (PR [#7161](https://github.com/vatesfr/xen-orchestra/pull/7161))
|
||||
- [REST API] `tags` property can be updated (PR [#7196](https://github.com/vatesfr/xen-orchestra/pull/7196))
|
||||
- [REST API] A VDI export can now be imported in an existing VDI (PR [#7199](https://github.com/vatesfr/xen-orchestra/pull/7199))
|
||||
- [REST API] Support VM import using the XVA format
|
||||
- [File Restore] API method `backupNg.mountPartition` to manually mount a backup disk on the XOA
|
||||
- [Backup] Implement differential restore (PR [#7202](https://github.com/vatesfr/xen-orchestra/pull/7202))
|
||||
- [VM/Disks] Display task information when importing VDIs (PR [#7197](https://github.com/vatesfr/xen-orchestra/pull/7197))
|
||||
- [VM Creation] Added ISO option in new VM form when creating from template with a disk [#3464](https://github.com/vatesfr/xen-orchestra/issues/3464) (PR [#7166](https://github.com/vatesfr/xen-orchestra/pull/7166))
|
||||
- [Task] Show the related SR on the Garbage Collector Task ( vdi coalescing) (PR [#7189](https://github.com/vatesfr/xen-orchestra/pull/7189))
|
||||
|
||||
### Enhancements
|
||||
|
||||
- [Netbox] Ability to synchronize XO users as Netbox tenants (PR [#7158](https://github.com/vatesfr/xen-orchestra/pull/7158))
|
||||
- [Backup] Don't backup VM with tag xo:no-bak (PR [#7173](https://github.com/vatesfr/xen-orchestra/pull/7173))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- [Backup/Restore] In case of snapshot with memory, create the suspend VDI on the correct SR instead of the default one
|
||||
- [Import/ESXi] Handle `Cannot read properties of undefined (reading 'perDatastoreUsage')` error when importing VM without storage (PR [#7168](https://github.com/vatesfr/xen-orchestra/pull/7168))
|
||||
- [Export/OVA] Handle export with resulting disk larger than 8.2GB (PR [#7183](https://github.com/vatesfr/xen-orchestra/pull/7183))
|
||||
- [Self Service] Fix error displayed after adding a VM to a resource set (PR [#7144](https://github.com/vatesfr/xen-orchestra/pull/7144))
|
||||
- [Backup/HealthCheck] Don't backup VM created by health check when using smart mode (PR [#7173](https://github.com/vatesfr/xen-orchestra/pull/7173))
|
||||
|
||||
### Released packages
|
||||
|
||||
- vhd-lib 4.7.0
|
||||
- @vates/multi-key-map 0.2.0
|
||||
- @vates/disposable 0.1.5
|
||||
- @xen-orchestra/fs 4.1.3
|
||||
- xen-api 2.0.0
|
||||
- @vates/nbd-client 2.0.1
|
||||
- @xen-orchestra/xapi 4.0.0
|
||||
- @xen-orchestra/backups 0.44.2
|
||||
- @xen-orchestra/backups-cli 1.0.14
|
||||
- @xen-orchestra/cr-seed-cli 1.0.0
|
||||
- @xen-orchestra/proxy 0.26.41
|
||||
- xo-vmdk-to-vhd 2.5.7
|
||||
- @xen-orchestra/vmware-explorer 0.3.1
|
||||
- xapi-explore-sr 0.4.2
|
||||
- xo-cli 0.22.0
|
||||
- xo-server 5.129.0
|
||||
- xo-server-netbox 1.4.0
|
||||
- xo-web 5.130.0
|
||||
|
||||
## **5.88.2** (2023-11-13)
|
||||
|
||||
<img id="stable" src="https://badgen.net/badge/channel/stable/green" alt="Channel: stable" />
|
||||
|
||||
### Enhancement
|
||||
|
||||
- [REST API] Add `users` collection
|
||||
@@ -67,6 +13,8 @@
|
||||
|
||||
## **5.88.1** (2023-11-07)
|
||||
|
||||
<img id="latest" src="https://badgen.net/badge/channel/latest/yellow" alt="Channel: latest" />
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- [Netbox] Fix VMs' `site` property being unnecessarily updated on some versions of Netbox (PR [#7145](https://github.com/vatesfr/xen-orchestra/pull/7145))
|
||||
@@ -135,6 +83,8 @@
|
||||
|
||||
## **5.87.0** (2023-09-29)
|
||||
|
||||
<img id="stable" src="https://badgen.net/badge/channel/stable/green" alt="Channel: stable" />
|
||||
|
||||
### Highlights
|
||||
|
||||
- [Patches] Support new XenServer Updates system. See [our documentation](https://xen-orchestra.com/docs/updater.html#xenserver-updates). (PR [#7044](https://github.com/vatesfr/xen-orchestra/pull/7044))
|
||||
|
||||
@@ -7,13 +7,21 @@
|
||||
|
||||
> Users must be able to say: “Nice enhancement, I'm eager to test it”
|
||||
|
||||
- [Forget SR] Changed the modal message and added a confirmation text to be sure the action is understood by the user [#7148](https://github.com/vatesfr/xen-orchestra/issues/7148) (PR [#7155](https://github.com/vatesfr/xen-orchestra/pull/7155))
|
||||
- [Netbox] Ability to synchronize XO users as Netbox tenants (PR [#7158](https://github.com/vatesfr/xen-orchestra/pull/7158))
|
||||
- [VM/Console] Add a message to indicate that the console view has been [disabled](https://support.citrix.com/article/CTX217766/how-to-disable-the-console-for-the-vm-in-xencenter) for this VM [#6319](https://github.com/vatesfr/xen-orchestra/issues/6319) (PR [#7161](https://github.com/vatesfr/xen-orchestra/pull/7161))
|
||||
- [Restore] Show source remote and restoration time on a restored VM (PR [#7186](https://github.com/vatesfr/xen-orchestra/pull/7186))
|
||||
- [Backup/Import] Show disk import status during Incremental Replication or restoration of Incremental Backup (PR [#7171](https://github.com/vatesfr/xen-orchestra/pull/7171))
|
||||
- [VM Creation] Added ISO option in new VM form when creating from template with a disk [#3464](https://github.com/vatesfr/xen-orchestra/issues/3464) (PR [#7166](https://github.com/vatesfr/xen-orchestra/pull/7166))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
> Users must be able to say: “I had this issue, happy to know it's fixed”
|
||||
|
||||
- [Remotes] Prevents the "connection failed" alert from continuing to appear after successfull connection
|
||||
- [Backup/Restore] In case of snapshot with memory, create the suspend VDI on the correct SR instead of the default one
|
||||
- [Import/ESXi] Handle `Cannot read properties of undefined (reading 'perDatastoreUsage')` error when importing VM without storage (PR [#7168](https://github.com/vatesfr/xen-orchestra/pull/7168))
|
||||
- [Export/OVA] Handle export with resulting disk larger than 8.2GB (PR [#7183](https://github.com/vatesfr/xen-orchestra/pull/7183))
|
||||
- [Self Service] Fix error displayed after adding a VM to a resource set (PR [#7144](https://github.com/vatesfr/xen-orchestra/pull/7144))
|
||||
- VMs snapshotted with XO will no longer appear as regular VMs in other clients like `xe`
|
||||
|
||||
### Packages to release
|
||||
|
||||
@@ -31,6 +39,15 @@
|
||||
|
||||
<!--packages-start-->
|
||||
|
||||
- xo-remote-parser patch
|
||||
- @vates/nbd-client patch
|
||||
- @xen-orchestra/backups minor
|
||||
- @xen-orchestra/cr-seed-cli major
|
||||
- @xen-orchestra/vmware-explorer patch
|
||||
- @xen-orchestra/xapi patch
|
||||
- xen-api major
|
||||
- xo-server patch
|
||||
- xo-server-netbox minor
|
||||
- xo-vmdk-to-vhd patch
|
||||
- xo-web minor
|
||||
|
||||
<!--packages-end-->
|
||||
|
||||
@@ -106,13 +106,13 @@ XO needs the following packages to be installed. Redis is used as a database by
|
||||
For example, on Debian/Ubuntu:
|
||||
|
||||
```sh
|
||||
apt-get install build-essential redis-server libpng-dev git python3-minimal libvhdi-utils lvm2 cifs-utils nfs-common ntfs-3g
|
||||
apt-get install build-essential redis-server libpng-dev git python3-minimal libvhdi-utils lvm2 cifs-utils nfs-common
|
||||
```
|
||||
|
||||
On Fedora/CentOS like:
|
||||
|
||||
```sh
|
||||
dnf install redis libpng-devel git lvm2 cifs-utils make automake gcc gcc-c++ nfs-utils ntfs-3g
|
||||
dnf install redis libpng-devel git libvhdi-tools lvm2 cifs-utils make automake gcc gcc-c++
|
||||
```
|
||||
|
||||
### Make sure Redis is running
|
||||
|
||||
@@ -123,7 +123,7 @@ Content-Type: application/x-ndjson
|
||||
|
||||
## Properties update
|
||||
|
||||
> This feature is restricted to `name_label`, `name_description` and `tags` at the moment.
|
||||
> This feature is restricted to `name_label` and `name_description` at the moment.
|
||||
|
||||
```sh
|
||||
curl \
|
||||
@@ -135,30 +135,6 @@ curl \
|
||||
'https://xo.example.org/rest/v0/vms/770aa52a-fd42-8faf-f167-8c5c4a237cac'
|
||||
```
|
||||
|
||||
### Collections
|
||||
|
||||
For collection properties, like `tags`, it can be more practical to touch a single item without impacting the others.
|
||||
|
||||
An item can be created with `PUT <collection>/<item id>` and can be destroyed with `DELETE <collection>/<item id>`.
|
||||
|
||||
Adding a tag:
|
||||
|
||||
```sh
|
||||
curl \
|
||||
-X PUT \
|
||||
-b authenticationToken=KQxQdm2vMiv7jBIK0hgkmgxKzemd8wSJ7ugFGKFkTbs \
|
||||
'https://xo.example.org/rest/v0/vms/770aa52a-fd42-8faf-f167-8c5c4a237cac/tags/My%20tag'
|
||||
```
|
||||
|
||||
Removing a tag:
|
||||
|
||||
```sh
|
||||
curl \
|
||||
-X DELETE \
|
||||
-b authenticationToken=KQxQdm2vMiv7jBIK0hgkmgxKzemd8wSJ7ugFGKFkTbs \
|
||||
'https://xo.example.org/rest/v0/vms/770aa52a-fd42-8faf-f167-8c5c4a237cac/tags/My%20tag'
|
||||
```
|
||||
|
||||
## VM and VDI destruction
|
||||
|
||||
For a VM:
|
||||
@@ -199,45 +175,9 @@ curl \
|
||||
> myDisk.vhd
|
||||
```
|
||||
|
||||
## VM Import
|
||||
|
||||
A VM can be imported by posting to `/rest/v0/pools/:id/vms`.
|
||||
|
||||
```sh
|
||||
curl \
|
||||
-X POST \
|
||||
-b authenticationToken=KQxQdm2vMiv7jBIK0hgkmgxKzemd8wSJ7ugFGKFkTbs \
|
||||
-T myDisk.raw \
|
||||
'https://xo.example.org/rest/v0/pools/355ee47d-ff4c-4924-3db2-fd86ae629676/vms?sr=357bd56c-71f9-4b2a-83b8-3451dec04b8f' \
|
||||
| cat
|
||||
```
|
||||
|
||||
The `sr` query parameter can be used to specify on which SR the VM should be imported, if not specified, the default SR will be used.
|
||||
|
||||
> Note: the final `| cat` ensures cURL's standard output is not a TTY, which is necessary for upload stats to be dislayed.
|
||||
|
||||
## VDI Import
|
||||
|
||||
### Existing VDI
|
||||
|
||||
A VHD or a raw export can be imported in an existing VDI respectively at `/rest/v0/vdis/<uuid>.vhd` and `/rest/v0/vdis/<uuid>.raw`.
|
||||
|
||||
> Note: the size of the VDI must match exactly the size of VDI that was previously exported.
|
||||
|
||||
```sh
|
||||
curl \
|
||||
-X PUT \
|
||||
-b authenticationToken=KQxQdm2vMiv7jBIK0hgkmgxKzemd8wSJ7ugFGKFkTbs \
|
||||
-T myDisk.vhd \
|
||||
'https://xo.example.org/rest/v0/vdis/1a269782-ea93-4c4c-897a-475365f7b674.vhd' \
|
||||
| cat
|
||||
```
|
||||
|
||||
> Note: the final `| cat` ensures cURL's standard output is not a TTY, which is necessary for upload stats to be dislayed.
|
||||
|
||||
### New VDI
|
||||
|
||||
An export can also be imported on an SR to create a new VDI at `/rest/v0/srs/<sr uuid>/vdis`.
|
||||
A VHD or a raw export can be imported on an SR to create a new VDI at `/rest/v0/srs/<sr uuid>/vdis`.
|
||||
|
||||
```sh
|
||||
curl \
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
"node": ">=10"
|
||||
},
|
||||
"dependencies": {
|
||||
"@xen-orchestra/fs": "^4.1.3",
|
||||
"@xen-orchestra/fs": "^4.1.2",
|
||||
"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.7.0"
|
||||
"vhd-lib": "^4.6.1"
|
||||
},
|
||||
"scripts": {
|
||||
"postversion": "npm publish",
|
||||
|
||||
@@ -1,184 +0,0 @@
|
||||
'use strict'
|
||||
|
||||
const { VhdAbstract, VhdNegative } = require('..')
|
||||
|
||||
const { describe, it } = require('test')
|
||||
const assert = require('assert/strict')
|
||||
const { unpackHeader, unpackFooter } = require('./_utils')
|
||||
const { createHeader, createFooter } = require('../_createFooterHeader')
|
||||
const _computeGeometryForSize = require('../_computeGeometryForSize')
|
||||
const { FOOTER_SIZE, DISK_TYPES } = require('../_constants')
|
||||
|
||||
const VHD_BLOCK_LENGTH = 2 * 1024 * 1024
|
||||
class VhdMock extends VhdAbstract {
|
||||
#blockUsed
|
||||
#header
|
||||
#footer
|
||||
get header() {
|
||||
return this.#header
|
||||
}
|
||||
get footer() {
|
||||
return this.#footer
|
||||
}
|
||||
|
||||
constructor(header, footer, blockUsed = new Set()) {
|
||||
super()
|
||||
this.#header = header
|
||||
this.#footer = footer
|
||||
this.#blockUsed = blockUsed
|
||||
}
|
||||
containsBlock(blockId) {
|
||||
return this.#blockUsed.has(blockId)
|
||||
}
|
||||
readBlock(blockId, onlyBitmap = false) {
|
||||
const bitmap = Buffer.alloc(512, 255) // bitmap are full of bit 1
|
||||
|
||||
const data = Buffer.alloc(2 * 1024 * 1024, 0) // empty are full of bit 0
|
||||
data.writeUint8(blockId)
|
||||
return {
|
||||
id: blockId,
|
||||
bitmap,
|
||||
data,
|
||||
buffer: Buffer.concat([bitmap, data]),
|
||||
}
|
||||
}
|
||||
|
||||
readBlockAllocationTable() {}
|
||||
readHeaderAndFooter() {}
|
||||
_readParentLocatorData(id) {}
|
||||
}
|
||||
|
||||
describe('vhd negative', async () => {
|
||||
it(`throws when uid aren't chained `, () => {
|
||||
const length = 10e8
|
||||
|
||||
let header = unpackHeader(createHeader(length / VHD_BLOCK_LENGTH))
|
||||
const geometry = _computeGeometryForSize(length)
|
||||
let footer = unpackFooter(
|
||||
createFooter(length, Math.floor(Date.now() / 1000), geometry, FOOTER_SIZE, DISK_TYPES.DIFFERENCING)
|
||||
)
|
||||
const parent = new VhdMock(header, footer)
|
||||
|
||||
header = unpackHeader(createHeader(length / VHD_BLOCK_LENGTH))
|
||||
footer = unpackFooter(
|
||||
createFooter(length, Math.floor(Date.now() / 1000), geometry, FOOTER_SIZE, DISK_TYPES.DIFFERENCING)
|
||||
)
|
||||
|
||||
const child = new VhdMock(header, footer)
|
||||
assert.throws(() => new VhdNegative(parent, child), { message: 'NOT_CHAINED' })
|
||||
})
|
||||
|
||||
it('throws when size changed', () => {
|
||||
const childLength = 10e8
|
||||
const parentLength = 10e8 + 1
|
||||
|
||||
let header = unpackHeader(createHeader(parentLength / VHD_BLOCK_LENGTH))
|
||||
let geometry = _computeGeometryForSize(parentLength)
|
||||
let footer = unpackFooter(
|
||||
createFooter(parentLength, Math.floor(Date.now() / 1000), geometry, FOOTER_SIZE, DISK_TYPES.DIFFERENCING)
|
||||
)
|
||||
const parent = new VhdMock(header, footer)
|
||||
|
||||
header = unpackHeader(createHeader(childLength / VHD_BLOCK_LENGTH))
|
||||
geometry = _computeGeometryForSize(childLength)
|
||||
header.parentUuid = footer.uuid
|
||||
footer = unpackFooter(
|
||||
createFooter(childLength, Math.floor(Date.now() / 1000), geometry, FOOTER_SIZE, DISK_TYPES.DIFFERENCING)
|
||||
)
|
||||
const child = new VhdMock(header, footer)
|
||||
assert.throws(() => new VhdNegative(parent, child), { message: 'GEOMETRY_CHANGED' })
|
||||
})
|
||||
it('throws when child is not differencing', () => {
|
||||
const length = 10e8
|
||||
|
||||
let header = unpackHeader(createHeader(length / VHD_BLOCK_LENGTH))
|
||||
const geometry = _computeGeometryForSize(length)
|
||||
let footer = unpackFooter(
|
||||
createFooter(length, Math.floor(Date.now() / 1000), geometry, FOOTER_SIZE, DISK_TYPES.DIFFERENCING)
|
||||
)
|
||||
const parent = new VhdMock(header, footer)
|
||||
|
||||
header = unpackHeader(createHeader(length / VHD_BLOCK_LENGTH))
|
||||
header.parentUuid = footer.uuid
|
||||
footer = unpackFooter(
|
||||
createFooter(length, Math.floor(Date.now() / 1000), geometry, FOOTER_SIZE, DISK_TYPES.DYNAMIC)
|
||||
)
|
||||
|
||||
const child = new VhdMock(header, footer)
|
||||
assert.throws(() => new VhdNegative(parent, child), { message: 'CHILD_NOT_DIFFERENCING' })
|
||||
})
|
||||
|
||||
it(`throws when writing into vhd negative `, async () => {
|
||||
const length = 10e8
|
||||
|
||||
let header = unpackHeader(createHeader(length / VHD_BLOCK_LENGTH))
|
||||
const geometry = _computeGeometryForSize(length)
|
||||
let footer = unpackFooter(
|
||||
createFooter(length, Math.floor(Date.now() / 1000), geometry, FOOTER_SIZE, DISK_TYPES.DIFFERENCING)
|
||||
)
|
||||
const parent = new VhdMock(header, footer)
|
||||
const parentUuid = footer.uuid
|
||||
header = unpackHeader(createHeader(length / VHD_BLOCK_LENGTH))
|
||||
header.parentUuid = parentUuid
|
||||
footer = unpackFooter(
|
||||
createFooter(length, Math.floor(Date.now() / 1000), geometry, FOOTER_SIZE, DISK_TYPES.DIFFERENCING)
|
||||
)
|
||||
|
||||
const child = new VhdMock(header, footer)
|
||||
|
||||
const vhd = new VhdNegative(parent, child)
|
||||
|
||||
// await assert.rejects( ()=> vhd.writeFooter())
|
||||
assert.throws(() => vhd.writeHeader())
|
||||
assert.throws(() => vhd.writeBlockAllocationTable())
|
||||
assert.throws(() => vhd.writeEntireBlock())
|
||||
assert.throws(() => vhd.mergeBlock(), { message: `can't coalesce block into a vhd negative` })
|
||||
})
|
||||
|
||||
it('normal case', async () => {
|
||||
const length = 10e8
|
||||
|
||||
let header = unpackHeader(createHeader(length / VHD_BLOCK_LENGTH))
|
||||
let geometry = _computeGeometryForSize(length)
|
||||
let footer = unpackFooter(
|
||||
createFooter(length, Math.floor(Date.now() / 1000), geometry, FOOTER_SIZE, DISK_TYPES.DYNAMIC)
|
||||
)
|
||||
const parent = new VhdMock(header, footer, new Set([1, 3]))
|
||||
const parentUuid = footer.uuid
|
||||
header = unpackHeader(createHeader(length / VHD_BLOCK_LENGTH))
|
||||
header.parentUuid = parentUuid
|
||||
geometry = _computeGeometryForSize(length)
|
||||
footer = unpackFooter(
|
||||
createFooter(length, Math.floor(Date.now() / 1000), geometry, FOOTER_SIZE, DISK_TYPES.DIFFERENCING)
|
||||
)
|
||||
|
||||
const childUuid = footer.uuid
|
||||
const child = new VhdMock(header, footer, new Set([2, 3]))
|
||||
|
||||
const vhd = new VhdNegative(parent, child)
|
||||
assert.equal(vhd.header.parentUuid.equals(childUuid), true)
|
||||
assert.equal(vhd.footer.diskType, DISK_TYPES.DIFFERENCING)
|
||||
await vhd.readBlockAllocationTable()
|
||||
await vhd.readHeaderAndFooter()
|
||||
await vhd.readParentLocator(0)
|
||||
assert.equal(vhd.header.parentUuid, childUuid)
|
||||
assert.equal(vhd.footer.diskType, DISK_TYPES.DIFFERENCING)
|
||||
assert.equal(vhd.containsBlock(1), false)
|
||||
assert.equal(vhd.containsBlock(2), true)
|
||||
assert.equal(vhd.containsBlock(3), true)
|
||||
assert.equal(vhd.containsBlock(4), false)
|
||||
|
||||
const expected = [0, 1, 0, 3, 0]
|
||||
const expectedBitmap = Buffer.alloc(512, 255) // bitmap must always be full of bit 1
|
||||
for (let index = 0; index < 5; index++) {
|
||||
if (vhd.containsBlock(index)) {
|
||||
const { id, data, bitmap } = await vhd.readBlock(index)
|
||||
assert.equal(index, id)
|
||||
assert.equal(expectedBitmap.equals(bitmap), true)
|
||||
assert.equal(data.readUInt8(0), expected[index])
|
||||
} else {
|
||||
assert.equal([2, 3].includes(index), false)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -1,84 +0,0 @@
|
||||
'use strict'
|
||||
|
||||
const UUID = require('uuid')
|
||||
const { DISK_TYPES } = require('../_constants')
|
||||
const { VhdAbstract } = require('./VhdAbstract')
|
||||
const { computeBlockBitmapSize } = require('./_utils')
|
||||
const assert = require('node:assert')
|
||||
/**
|
||||
* Build an incremental VHD which can be applied to a child to revert to the state of its parent.
|
||||
* @param {*} parent
|
||||
* @param {*} descendant
|
||||
*/
|
||||
|
||||
class VhdNegative extends VhdAbstract {
|
||||
#parent
|
||||
#child
|
||||
|
||||
get header() {
|
||||
// we want to have parent => child => negative
|
||||
// where => means " is the parent of "
|
||||
return {
|
||||
...this.#parent.header,
|
||||
parentUuid: this.#child.footer.uuid,
|
||||
}
|
||||
}
|
||||
|
||||
get footer() {
|
||||
// by construct a negative vhd is differencing disk
|
||||
return {
|
||||
...this.#parent.footer,
|
||||
diskType: DISK_TYPES.DIFFERENCING,
|
||||
}
|
||||
}
|
||||
|
||||
constructor(parent, child) {
|
||||
super()
|
||||
this.#parent = parent
|
||||
this.#child = child
|
||||
|
||||
assert.strictEqual(UUID.stringify(child.header.parentUuid), UUID.stringify(parent.footer.uuid), 'NOT_CHAINED')
|
||||
assert.strictEqual(child.footer.diskType, DISK_TYPES.DIFFERENCING, 'CHILD_NOT_DIFFERENCING')
|
||||
// we don't want to handle alignment and missing block for now
|
||||
// last block may contains partly empty data when changing size
|
||||
assert.strictEqual(child.footer.currentSize, parent.footer.currentSize, 'GEOMETRY_CHANGED')
|
||||
}
|
||||
|
||||
async readBlockAllocationTable() {
|
||||
return Promise.all([this.#parent.readBlockAllocationTable(), this.#child.readBlockAllocationTable()])
|
||||
}
|
||||
|
||||
containsBlock(blockId) {
|
||||
return this.#child.containsBlock(blockId)
|
||||
}
|
||||
|
||||
async readHeaderAndFooter() {
|
||||
return Promise.all([this.#parent.readHeaderAndFooter(), this.#child.readHeaderAndFooter()])
|
||||
}
|
||||
|
||||
async readBlock(blockId, onlyBitmap = false) {
|
||||
// only read the content of the first vhd containing this block
|
||||
if (this.#parent.containsBlock(blockId)) {
|
||||
return this.#parent.readBlock(blockId, onlyBitmap)
|
||||
}
|
||||
|
||||
const bitmap = Buffer.alloc(computeBlockBitmapSize(this.header.blockSize), 255) // bitmap are full of bit 1
|
||||
const data = Buffer.alloc(this.header.blockSize, 0) // empty are full of bit 0
|
||||
return {
|
||||
id: blockId,
|
||||
bitmap,
|
||||
data,
|
||||
buffer: Buffer.concat([bitmap, data]),
|
||||
}
|
||||
}
|
||||
|
||||
mergeBlock(child, blockId) {
|
||||
throw new Error(`can't coalesce block into a vhd negative`)
|
||||
}
|
||||
|
||||
_readParentLocatorData(id) {
|
||||
return this.#parent._readParentLocatorData(id)
|
||||
}
|
||||
}
|
||||
|
||||
exports.VhdNegative = VhdNegative
|
||||
@@ -120,8 +120,7 @@ const VhdSynthetic = class VhdSynthetic extends VhdAbstract {
|
||||
}
|
||||
|
||||
// add decorated static method
|
||||
// until is not included in the result , the chain will stop at its child
|
||||
VhdSynthetic.fromVhdChain = Disposable.factory(async function* fromVhdChain(handler, childPath, { until } = {}) {
|
||||
VhdSynthetic.fromVhdChain = Disposable.factory(async function* fromVhdChain(handler, childPath) {
|
||||
let vhdPath = childPath
|
||||
let vhd
|
||||
const vhds = []
|
||||
@@ -129,11 +128,8 @@ VhdSynthetic.fromVhdChain = Disposable.factory(async function* fromVhdChain(hand
|
||||
vhd = yield openVhd(handler, vhdPath)
|
||||
vhds.unshift(vhd) // from oldest to most recent
|
||||
vhdPath = resolveRelativeFromFile(vhdPath, vhd.header.parentUnicodeName)
|
||||
} while (vhd.footer.diskType !== DISK_TYPES.DYNAMIC && vhdPath !== until)
|
||||
} while (vhd.footer.diskType !== DISK_TYPES.DYNAMIC)
|
||||
|
||||
if (until !== undefined && vhdPath !== until) {
|
||||
throw new Error(`Didn't find ${until} as a parent of ${childPath}`)
|
||||
}
|
||||
const synthetic = new VhdSynthetic(vhds)
|
||||
await synthetic.readHeaderAndFooter()
|
||||
yield synthetic
|
||||
|
||||
@@ -2,10 +2,6 @@
|
||||
|
||||
const { dirname, resolve } = require('path')
|
||||
|
||||
const resolveRelativeFromFile = (file, path) => {
|
||||
if (file.startsWith('/')) {
|
||||
return resolve(dirname(file), path)
|
||||
}
|
||||
return resolve('/', dirname(file), path).slice(1)
|
||||
}
|
||||
const resolveRelativeFromFile = (file, path) => resolve('/', dirname(file), path).slice(1)
|
||||
|
||||
module.exports = resolveRelativeFromFile
|
||||
|
||||
@@ -13,5 +13,4 @@ exports.VhdAbstract = require('./Vhd/VhdAbstract').VhdAbstract
|
||||
exports.VhdDirectory = require('./Vhd/VhdDirectory').VhdDirectory
|
||||
exports.VhdFile = require('./Vhd/VhdFile').VhdFile
|
||||
exports.VhdSynthetic = require('./Vhd/VhdSynthetic').VhdSynthetic
|
||||
exports.VhdNegative = require('./Vhd/VhdNegative').VhdNegative
|
||||
exports.Constants = require('./_constants')
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": false,
|
||||
"name": "vhd-lib",
|
||||
"version": "4.7.0",
|
||||
"version": "4.6.1",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"description": "Primitives for VHD file handling",
|
||||
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/packages/vhd-lib",
|
||||
@@ -20,7 +20,7 @@
|
||||
"@vates/read-chunk": "^1.2.0",
|
||||
"@vates/stream-reader": "^0.1.0",
|
||||
"@xen-orchestra/async-map": "^0.1.2",
|
||||
"@xen-orchestra/fs": "^4.1.3",
|
||||
"@xen-orchestra/fs": "^4.1.2",
|
||||
"@xen-orchestra/log": "^0.6.0",
|
||||
"async-iterator-to-stream": "^1.0.2",
|
||||
"decorator-synchronized": "^0.6.0",
|
||||
@@ -33,7 +33,7 @@
|
||||
"uuid": "^9.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@xen-orchestra/fs": "^4.1.3",
|
||||
"@xen-orchestra/fs": "^4.1.2",
|
||||
"execa": "^5.0.0",
|
||||
"get-stream": "^6.0.0",
|
||||
"rimraf": "^5.0.1",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": false,
|
||||
"name": "xapi-explore-sr",
|
||||
"version": "0.4.2",
|
||||
"version": "0.4.1",
|
||||
"license": "ISC",
|
||||
"description": "Display the list of VDIs (unmanaged and snapshots included) of a SR",
|
||||
"keywords": [
|
||||
@@ -40,7 +40,7 @@
|
||||
"human-format": "^1.0.0",
|
||||
"lodash": "^4.17.4",
|
||||
"pw": "^0.0.4",
|
||||
"xen-api": "^2.0.0"
|
||||
"xen-api": "^1.3.6"
|
||||
},
|
||||
"scripts": {
|
||||
"postversion": "npm publish"
|
||||
|
||||
@@ -10,6 +10,6 @@
|
||||
"readable-stream": "^4.4.2",
|
||||
"source-map-support": "^0.5.21",
|
||||
"throttle": "^1.0.3",
|
||||
"vhd-lib": "^4.7.0"
|
||||
"vhd-lib": "^4.6.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": false,
|
||||
"name": "xen-api",
|
||||
"version": "2.0.0",
|
||||
"version": "1.3.6",
|
||||
"license": "ISC",
|
||||
"description": "Connector to the Xen API",
|
||||
"keywords": [
|
||||
|
||||
@@ -50,7 +50,6 @@ Usage:
|
||||
|
||||
Examples:
|
||||
xo-cli rest del tasks/<task id>
|
||||
xo-cli rest del vms/<vm id>/tags/<tag>
|
||||
|
||||
xo-cli rest get <collection> [fields=<fields>] [filter=<filter>] [limit=<limit>]
|
||||
List objects in a REST API collection.
|
||||
@@ -123,18 +122,6 @@ Usage:
|
||||
Examples:
|
||||
xo-cli rest post tasks/<task id>/actions/abort
|
||||
xo-cli rest post vms/<VM UUID>/actions/snapshot name_label='My snapshot'
|
||||
|
||||
xo-cli rest put <collection>/<item id> <name>=<value>...
|
||||
Put a item in a collection
|
||||
|
||||
<collection>/<item id>
|
||||
Full path of the item to add
|
||||
|
||||
<name>=<value>...
|
||||
Properties of the item
|
||||
|
||||
Examples:
|
||||
xo-cli rest put vms/<vm id>/tags/<tag>
|
||||
```
|
||||
|
||||
#### Register your XO instance
|
||||
|
||||
@@ -68,7 +68,6 @@ Usage:
|
||||
|
||||
Examples:
|
||||
xo-cli rest del tasks/<task id>
|
||||
xo-cli rest del vms/<vm id>/tags/<tag>
|
||||
|
||||
xo-cli rest get <collection> [fields=<fields>] [filter=<filter>] [limit=<limit>]
|
||||
List objects in a REST API collection.
|
||||
@@ -141,18 +140,6 @@ Usage:
|
||||
Examples:
|
||||
xo-cli rest post tasks/<task id>/actions/abort
|
||||
xo-cli rest post vms/<VM UUID>/actions/snapshot name_label='My snapshot'
|
||||
|
||||
xo-cli rest put <collection>/<item id> <name>=<value>...
|
||||
Put a item in a collection
|
||||
|
||||
<collection>/<item id>
|
||||
Full path of the item to add
|
||||
|
||||
<name>=<value>...
|
||||
Properties of the item
|
||||
|
||||
Examples:
|
||||
xo-cli rest put vms/<vm id>/tags/<tag>
|
||||
```
|
||||
|
||||
#### Register your XO instance
|
||||
|
||||
@@ -274,7 +274,6 @@ const help = wrap(
|
||||
|
||||
Examples:
|
||||
$name rest del tasks/<task id>
|
||||
$name rest del vms/<vm id>/tags/<tag>
|
||||
|
||||
$name rest get <collection> [fields=<fields>] [filter=<filter>] [limit=<limit>]
|
||||
List objects in a REST API collection.
|
||||
@@ -348,18 +347,6 @@ const help = wrap(
|
||||
$name rest post tasks/<task id>/actions/abort
|
||||
$name rest post vms/<VM UUID>/actions/snapshot name_label='My snapshot'
|
||||
|
||||
$name rest put <collection>/<item id> <name>=<value>...
|
||||
Put a item in a collection
|
||||
|
||||
<collection>/<item id>
|
||||
Full path of the item to add
|
||||
|
||||
<name>=<value>...
|
||||
Properties of the item
|
||||
|
||||
Examples:
|
||||
$name rest put vms/<vm id>/tags/<tag>
|
||||
|
||||
$name v$version`.replace(/<([^>]+)>|\$(\w+)/g, function (_, arg, key) {
|
||||
if (arg) {
|
||||
return '<' + chalk.yellow(arg) + '>'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": false,
|
||||
"name": "xo-cli",
|
||||
"version": "0.22.0",
|
||||
"version": "0.21.0",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"description": "Basic CLI for Xen-Orchestra",
|
||||
"keywords": [
|
||||
|
||||
@@ -112,18 +112,6 @@ const COMMANDS = {
|
||||
|
||||
return stripPrefix(await response.text())
|
||||
},
|
||||
|
||||
async put([path, ...params]) {
|
||||
const response = await this.exec(path, {
|
||||
body: JSON.stringify(parseParams(params)),
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
method: 'PUT',
|
||||
})
|
||||
|
||||
return stripPrefix(await response.text())
|
||||
},
|
||||
}
|
||||
|
||||
export async function rest(args) {
|
||||
|
||||
@@ -21,7 +21,6 @@
|
||||
"node": ">=6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@xen-orchestra/log": "^0.6.0",
|
||||
"lodash": "^4.13.1",
|
||||
"url-parse": "^1.4.7"
|
||||
},
|
||||
|
||||
@@ -4,9 +4,6 @@ import trim from 'lodash/trim'
|
||||
import trimStart from 'lodash/trimStart'
|
||||
import queryString from 'querystring'
|
||||
import urlParser from 'url-parse'
|
||||
import { createLogger } from '@xen-orchestra/log'
|
||||
|
||||
const { warn } = createLogger('xo:xo-remote-parser')
|
||||
|
||||
const NFS_RE = /^([^:]+):(?:(\d+):)?([^:?]+)(\?[^?]*)?$/
|
||||
const SMB_RE = /^([^:]+):(.+)@([^@]+)\\\\([^\0?]+)(?:\0([^?]*))?(\?[^?]*)?$/
|
||||
@@ -18,17 +15,6 @@ const parseOptionList = (optionList = '') => {
|
||||
optionList = optionList.substring(1)
|
||||
}
|
||||
const parsed = queryString.parse(optionList)
|
||||
|
||||
// bugfix for persisting error notification
|
||||
if ('error' in parsed) {
|
||||
warn('Deleting "error" value in url query, resave your remote to clear this values', { error: parsed.error })
|
||||
delete parsed.error
|
||||
}
|
||||
if ('name' in parsed) {
|
||||
warn('Deleting "name" value in url query, resave your remote to clear this values', { name: parsed.name })
|
||||
delete parsed.name
|
||||
}
|
||||
|
||||
Object.keys(parsed).forEach(key => {
|
||||
const val = parsed[key]
|
||||
// some incorrect values have been saved in users database (introduced by #6270)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "xo-server-netbox",
|
||||
"version": "1.4.0",
|
||||
"version": "1.3.3",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"description": "Synchronizes pools managed by Xen Orchestra with Netbox",
|
||||
"keywords": [
|
||||
|
||||
@@ -381,7 +381,6 @@ class Netbox {
|
||||
await this.#request(`/virtualization/clusters/?type_id=${nbClusterType.id}`),
|
||||
'custom_fields.uuid'
|
||||
)
|
||||
delete allNbClusters.null
|
||||
const nbClusters = pick(allNbClusters, xoPools)
|
||||
|
||||
const clustersToCreate = []
|
||||
@@ -550,9 +549,7 @@ class Netbox {
|
||||
// Then make them objects to map the Netbox VMs to their XO VMs
|
||||
// { VM UUID → Netbox VM }
|
||||
const allNbVms = keyBy(allNbVmsList, 'custom_fields.uuid')
|
||||
delete allNbVms.null
|
||||
const nbVms = keyBy(nbVmsList, 'custom_fields.uuid')
|
||||
delete nbVms.null
|
||||
|
||||
// Used for name deduplication
|
||||
// Start by storing the names of the VMs that have been created manually in
|
||||
@@ -670,7 +667,6 @@ class Netbox {
|
||||
const nbIfsList = await this.#request(`/virtualization/interfaces/?${clusterFilter}`)
|
||||
// { ID → Interface }
|
||||
const nbIfs = keyBy(nbIfsList, 'custom_fields.uuid')
|
||||
delete nbIfs.null
|
||||
|
||||
const ifsToDelete = []
|
||||
const ifsToUpdate = []
|
||||
@@ -914,10 +910,7 @@ class Netbox {
|
||||
}
|
||||
}
|
||||
|
||||
Object.assign(
|
||||
nbVms,
|
||||
keyBy(await this.#request('/virtualization/virtual-machines/', 'PATCH', vmsToUpdate2), 'custom_fields.uuid')
|
||||
)
|
||||
Object.assign(nbVms, keyBy(await this.#request('/virtualization/virtual-machines/', 'PATCH', vmsToUpdate2)))
|
||||
|
||||
log.info(`Done synchronizing ${xoPools.length} pools with Netbox`, { pools: xoPools })
|
||||
}
|
||||
|
||||
@@ -76,8 +76,6 @@ defaultSignInPage = '/signin'
|
||||
throttlingDelay = '2 seconds'
|
||||
|
||||
[backups]
|
||||
autoUnmountPartitionDelay = '24h'
|
||||
|
||||
disableMergeWorker = false
|
||||
|
||||
# Mode to use for newly created backup directories
|
||||
|
||||
@@ -119,7 +119,7 @@ Content-Type: application/x-ndjson
|
||||
|
||||
## Properties update
|
||||
|
||||
> This feature is restricted to `name_label`, `name_description` and `tags` at the moment.
|
||||
> This feature is restricted to `name_label` and `name_description` at the moment.
|
||||
|
||||
```sh
|
||||
curl \
|
||||
@@ -131,30 +131,6 @@ curl \
|
||||
'https://xo.company.lan/rest/v0/vms/770aa52a-fd42-8faf-f167-8c5c4a237cac'
|
||||
```
|
||||
|
||||
### Collections
|
||||
|
||||
For collection properties, like `tags`, it can be more practical to touch a single item without impacting the others.
|
||||
|
||||
An item can be created with `PUT <collection>/<item id>` and can be destroyed with `DELETE <collection>/<item id>`.
|
||||
|
||||
Adding a tag:
|
||||
|
||||
```sh
|
||||
curl \
|
||||
-X PUT \
|
||||
-b authenticationToken=KQxQdm2vMiv7jBIK0hgkmgxKzemd8wSJ7ugFGKFkTbs \
|
||||
'https://xo.company.lan/rest/v0/vms/770aa52a-fd42-8faf-f167-8c5c4a237cac/tags/My%20tag'
|
||||
```
|
||||
|
||||
Removing a tag:
|
||||
|
||||
```sh
|
||||
curl \
|
||||
-X DELETE \
|
||||
-b authenticationToken=KQxQdm2vMiv7jBIK0hgkmgxKzemd8wSJ7ugFGKFkTbs \
|
||||
'https://xo.company.lan/rest/v0/vms/770aa52a-fd42-8faf-f167-8c5c4a237cac/tags/My%20tag'
|
||||
```
|
||||
|
||||
## VM destruction
|
||||
|
||||
```sh
|
||||
@@ -180,23 +156,6 @@ curl \
|
||||
> myVM.xva
|
||||
```
|
||||
|
||||
## VM Import
|
||||
|
||||
A VM can be imported by posting to `/rest/v0/pools/:id/vms`.
|
||||
|
||||
```sh
|
||||
curl \
|
||||
-X POST \
|
||||
-b authenticationToken=KQxQdm2vMiv7jBIK0hgkmgxKzemd8wSJ7ugFGKFkTbs \
|
||||
-T myDisk.raw \
|
||||
'https://xo.company.lan/rest/v0/pools/355ee47d-ff4c-4924-3db2-fd86ae629676/vms?sr=357bd56c-71f9-4b2a-83b8-3451dec04b8f' \
|
||||
| cat
|
||||
```
|
||||
|
||||
The `sr` query parameter can be used to specify on which SR the VM should be imported, if not specified, the default SR will be used.
|
||||
|
||||
> Note: the final `| cat` ensures cURL's standard output is not a TTY, which is necessary for upload stats to be dislayed.
|
||||
|
||||
## VDI destruction
|
||||
|
||||
```sh
|
||||
@@ -219,26 +178,7 @@ curl \
|
||||
|
||||
## VDI Import
|
||||
|
||||
### Existing VDI
|
||||
|
||||
A VHD or a raw export can be imported in an existing VDI respectively at `/rest/v0/vdis/<uuid>.vhd` and `/rest/v0/vdis/<uuid>.raw`.
|
||||
|
||||
> Note: the size of the VDI must match exactly the size of VDI that was previously exported.
|
||||
|
||||
```sh
|
||||
curl \
|
||||
-X PUT \
|
||||
-b authenticationToken=KQxQdm2vMiv7jBIK0hgkmgxKzemd8wSJ7ugFGKFkTbs \
|
||||
-T myDisk.vhd \
|
||||
'https://xo.company.lan/rest/v0/vdis/1a269782-ea93-4c4c-897a-475365f7b674.vhd' \
|
||||
| cat
|
||||
```
|
||||
|
||||
> Note: the final `| cat` ensures cURL's standard output is not a TTY, which is necessary for upload stats to be dislayed.
|
||||
|
||||
### New VDI
|
||||
|
||||
An export can also be imported on an SR to create a new VDI at `/rest/v0/srs/<sr uuid>/vdis`.
|
||||
A VHD or a raw export can be imported on an SR to create a new VDI at `/rest/v0/srs/<sr uuid>/vdis`.
|
||||
|
||||
```sh
|
||||
curl \
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "xo-server",
|
||||
"version": "5.129.0",
|
||||
"version": "5.126.0",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"description": "Server part of Xen-Orchestra",
|
||||
"keywords": [
|
||||
@@ -33,26 +33,26 @@
|
||||
"@vates/cached-dns.lookup": "^1.0.0",
|
||||
"@vates/compose": "^2.1.0",
|
||||
"@vates/decorate-with": "^2.0.0",
|
||||
"@vates/disposable": "^0.1.5",
|
||||
"@vates/disposable": "^0.1.4",
|
||||
"@vates/event-listeners-manager": "^1.0.1",
|
||||
"@vates/multi-key-map": "^0.2.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.2.0",
|
||||
"@xen-orchestra/async-map": "^0.1.2",
|
||||
"@xen-orchestra/backups": "^0.44.2",
|
||||
"@xen-orchestra/backups": "^0.43.2",
|
||||
"@xen-orchestra/cron": "^1.0.6",
|
||||
"@xen-orchestra/defined": "^0.0.1",
|
||||
"@xen-orchestra/emit-async": "^1.0.0",
|
||||
"@xen-orchestra/fs": "^4.1.3",
|
||||
"@xen-orchestra/fs": "^4.1.2",
|
||||
"@xen-orchestra/log": "^0.6.0",
|
||||
"@xen-orchestra/mixin": "^0.1.0",
|
||||
"@xen-orchestra/mixins": "^0.14.0",
|
||||
"@xen-orchestra/self-signed": "^0.1.3",
|
||||
"@xen-orchestra/template": "^0.1.0",
|
||||
"@xen-orchestra/vmware-explorer": "^0.3.1",
|
||||
"@xen-orchestra/xapi": "^4.0.0",
|
||||
"@xen-orchestra/vmware-explorer": "^0.3.0",
|
||||
"@xen-orchestra/xapi": "^3.3.0",
|
||||
"ajv": "^8.0.3",
|
||||
"app-conf": "^2.3.0",
|
||||
"async-iterator-to-stream": "^1.0.1",
|
||||
@@ -127,15 +127,15 @@
|
||||
"unzipper": "^0.10.5",
|
||||
"uuid": "^9.0.0",
|
||||
"value-matcher": "^0.2.0",
|
||||
"vhd-lib": "^4.7.0",
|
||||
"vhd-lib": "^4.6.1",
|
||||
"ws": "^8.2.3",
|
||||
"xdg-basedir": "^5.1.0",
|
||||
"xen-api": "^2.0.0",
|
||||
"xen-api": "^1.3.6",
|
||||
"xo-acl-resolver": "^0.4.1",
|
||||
"xo-collection": "^0.5.0",
|
||||
"xo-common": "^0.8.0",
|
||||
"xo-remote-parser": "^0.9.2",
|
||||
"xo-vmdk-to-vhd": "^2.5.7"
|
||||
"xo-vmdk-to-vhd": "^2.5.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.0.0",
|
||||
|
||||
@@ -277,10 +277,6 @@ importVmBackup.params = {
|
||||
sr: {
|
||||
type: 'string',
|
||||
},
|
||||
useDifferentialRestore: {
|
||||
type: 'boolean',
|
||||
optional: true
|
||||
}
|
||||
}
|
||||
|
||||
export function checkBackup({ id, settings, sr }) {
|
||||
@@ -384,47 +380,3 @@ fetchFiles.params = {
|
||||
type: 'string',
|
||||
},
|
||||
}
|
||||
|
||||
export function listMountedPartitions() {
|
||||
return this.listMountedPartitions()
|
||||
}
|
||||
|
||||
listMountedPartitions.permission = 'admin'
|
||||
|
||||
export function mountPartition({ remote, disk, partition }) {
|
||||
return this.mountPartition(remote, disk, partition)
|
||||
}
|
||||
|
||||
mountPartition.permission = 'admin'
|
||||
|
||||
mountPartition.params = {
|
||||
disk: {
|
||||
type: 'string',
|
||||
},
|
||||
partition: {
|
||||
optional: true,
|
||||
type: 'string',
|
||||
},
|
||||
remote: {
|
||||
type: 'string',
|
||||
},
|
||||
}
|
||||
|
||||
export function unmountPartition({ remote, disk, partition }) {
|
||||
return this.unmountPartition(remote, disk, partition)
|
||||
}
|
||||
|
||||
unmountPartition.permission = 'admin'
|
||||
|
||||
unmountPartition.params = {
|
||||
disk: {
|
||||
type: 'string',
|
||||
},
|
||||
partition: {
|
||||
optional: true,
|
||||
type: 'string',
|
||||
},
|
||||
remote: {
|
||||
type: 'string',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ export async function copyVm({ vm, sr }) {
|
||||
const input = await srcXapi.VM_export(vm._xapiRef)
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('import full VM...')
|
||||
await tgtXapi.VM_destroy(await tgtXapi.VM_import(input, sr._xapiRef))
|
||||
await tgtXapi.VM_destroy((await tgtXapi.importVm(input, { srId: sr })).$ref)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1297,8 +1297,7 @@ async function import_({ data, sr, type = 'xva', url }) {
|
||||
throw invalidParameters('URL import is only compatible with XVA')
|
||||
}
|
||||
|
||||
const ref = await xapi.VM_import(await hrp(url), sr._xapiRef)
|
||||
return xapi.call('VM.get_by_uuid', ref)
|
||||
return (await xapi.importVm(await hrp(url), { srId, type })).$id
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -965,7 +965,10 @@ async function _importGlusterVM(xapi, template, lvmsrId) {
|
||||
namespace: 'xosan',
|
||||
version: template.version,
|
||||
})
|
||||
const newVM = await xapi.VM_import(templateStream, this.getObject(lvmsrId, 'SR')._xapiRef)
|
||||
const newVM = await xapi.importVm(templateStream, {
|
||||
srId: lvmsrId,
|
||||
type: 'xva',
|
||||
})
|
||||
await xapi.editVm(newVM, {
|
||||
autoPoweron: true,
|
||||
name_label: 'XOSAN imported VM',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { asyncEach } from '@vates/async-each'
|
||||
import { createLogger } from '@xen-orchestra/log'
|
||||
import { defer } from 'golike-defer'
|
||||
import { Task } from '@xen-orchestra/mixins/Tasks.mjs'
|
||||
|
||||
const ENUM_PROVISIONING = {
|
||||
Thin: 'thin',
|
||||
@@ -9,48 +9,35 @@ const ENUM_PROVISIONING = {
|
||||
const LV_NAME = 'thin_device'
|
||||
const PROVISIONING = Object.values(ENUM_PROVISIONING)
|
||||
const VG_NAME = 'linstor_group'
|
||||
const XOSTOR_DEPENDENCIES = ['xcp-ng-release-linstor', 'xcp-ng-linstor']
|
||||
const _XOSTOR_DEPENDENCIES = ['xcp-ng-release-linstor', 'xcp-ng-linstor']
|
||||
const XOSTOR_DEPENDENCIES = _XOSTOR_DEPENDENCIES.join(',')
|
||||
|
||||
const log = createLogger('xo:api:pool')
|
||||
|
||||
function pluginCall(xapi, host, plugin, fnName, args) {
|
||||
return Task.run(
|
||||
{ properties: { name: `call plugin on: ${host.name_label}`, objectId: host.uuid, plugin, fnName, args } },
|
||||
() => xapi.call('host.call_plugin', host._xapiRef, plugin, fnName, args)
|
||||
)
|
||||
return xapi.call('host.call_plugin', host._xapiRef, plugin, fnName, args)
|
||||
}
|
||||
|
||||
async function destroyVolumeGroup(xapi, host, force) {
|
||||
return Task.run(
|
||||
{ properties: { name: `destroy volume group on ${host.name_label}`, objectId: host.uuid, vgName: VG_NAME } },
|
||||
() =>
|
||||
pluginCall(xapi, host, 'lvm.py', 'destroy_volume_group', {
|
||||
vg_name: VG_NAME,
|
||||
force: String(force),
|
||||
})
|
||||
)
|
||||
log.info(`Trying to delete the ${VG_NAME} volume group.`, { hostId: host.id })
|
||||
return pluginCall(xapi, host, 'lvm.py', 'destroy_volume_group', {
|
||||
vg_name: VG_NAME,
|
||||
force: String(force),
|
||||
})
|
||||
}
|
||||
|
||||
async function installOrUpdateDependencies(host, method = 'install') {
|
||||
return Task.run(
|
||||
{
|
||||
properties: {
|
||||
dependencies: XOSTOR_DEPENDENCIES,
|
||||
name: `${method} XOSTOR dependencies on ${host.name_label}`,
|
||||
objectId: host.uuid,
|
||||
},
|
||||
},
|
||||
async () => {
|
||||
if (method !== 'install' && method !== 'update') {
|
||||
throw new Error('Invalid method')
|
||||
}
|
||||
if (method !== 'install' && method !== 'update') {
|
||||
throw new Error('Invalid method')
|
||||
}
|
||||
|
||||
const xapi = this.getXapi(host)
|
||||
for (const _package of XOSTOR_DEPENDENCIES) {
|
||||
await pluginCall(xapi, host, 'updater.py', method, {
|
||||
packages: _package,
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
const xapi = this.getXapi(host)
|
||||
log.info(`Trying to ${method} XOSTOR dependencies (${XOSTOR_DEPENDENCIES})`, { hostId: host.id })
|
||||
for (const _package of _XOSTOR_DEPENDENCIES) {
|
||||
await pluginCall(xapi, host, 'updater.py', method, {
|
||||
packages: _package,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export function installDependencies({ host }) {
|
||||
@@ -79,57 +66,53 @@ updateDependencies.resolve = {
|
||||
|
||||
export async function formatDisks({ disks, force, host, ignoreFileSystems, provisioning }) {
|
||||
const rawDisks = disks.join(',')
|
||||
return Task.run(
|
||||
{ properties: { disks, name: `format disks on ${host.name_label}`, objectId: host.uuid } },
|
||||
async () => {
|
||||
const xapi = this.getXapi(host)
|
||||
const xapi = this.getXapi(host)
|
||||
|
||||
const lvmPlugin = (fnName, args) => pluginCall(xapi, host, 'lvm.py', fnName, args)
|
||||
const lvmPlugin = (fnName, args) => pluginCall(xapi, host, 'lvm.py', fnName, args)
|
||||
log.info(`Format disks (${rawDisks}) with force: ${force}`, { hostId: host.id })
|
||||
|
||||
if (force) {
|
||||
await destroyVolumeGroup(xapi, host, force)
|
||||
}
|
||||
if (force) {
|
||||
await destroyVolumeGroup(xapi, host, force)
|
||||
}
|
||||
|
||||
// ATM we are unable to correctly identify errors (error.code can be used for multiple errors.)
|
||||
// so we are just adding some suggestion of "why there is this error"
|
||||
// Error handling will be improved as errors are discovered and understood
|
||||
try {
|
||||
await lvmPlugin('create_physical_volume', {
|
||||
devices: rawDisks,
|
||||
ignore_existing_filesystems: String(ignoreFileSystems),
|
||||
force: String(force),
|
||||
})
|
||||
} catch (error) {
|
||||
if (error.code === 'LVM_ERROR(5)') {
|
||||
error.params = error.params.concat([
|
||||
"[XO] This error can be triggered if one of the disks is a 'tapdevs' disk.",
|
||||
'[XO] This error can be triggered if one of the disks have children',
|
||||
])
|
||||
}
|
||||
throw error
|
||||
}
|
||||
try {
|
||||
await lvmPlugin('create_volume_group', {
|
||||
devices: rawDisks,
|
||||
vg_name: VG_NAME,
|
||||
})
|
||||
} catch (error) {
|
||||
if (error.code === 'LVM_ERROR(5)') {
|
||||
error.params = error.params.concat([
|
||||
"[XO] This error can be triggered if a VG 'linstor_group' is already present on the host.",
|
||||
])
|
||||
}
|
||||
throw error
|
||||
}
|
||||
|
||||
if (provisioning === ENUM_PROVISIONING.Thin) {
|
||||
await lvmPlugin('create_thin_pool', {
|
||||
lv_name: LV_NAME,
|
||||
vg_name: VG_NAME,
|
||||
})
|
||||
}
|
||||
// ATM we are unable to correctly identify errors (error.code can be used for multiple errors.)
|
||||
// so we are just adding some suggestion of "why there is this error"
|
||||
// Error handling will be improved as errors are discovered and understood
|
||||
try {
|
||||
await lvmPlugin('create_physical_volume', {
|
||||
devices: rawDisks,
|
||||
ignore_existing_filesystems: String(ignoreFileSystems),
|
||||
force: String(force),
|
||||
})
|
||||
} catch (error) {
|
||||
if (error.code === 'LVM_ERROR(5)') {
|
||||
error.params = error.params.concat([
|
||||
"[XO] This error can be triggered if one of the disks is a 'tapdevs' disk.",
|
||||
'[XO] This error can be triggered if one of the disks have children',
|
||||
])
|
||||
}
|
||||
)
|
||||
throw error
|
||||
}
|
||||
try {
|
||||
await lvmPlugin('create_volume_group', {
|
||||
devices: rawDisks,
|
||||
vg_name: VG_NAME,
|
||||
})
|
||||
} catch (error) {
|
||||
if (error.code === 'LVM_ERROR(5)') {
|
||||
error.params = error.params.concat([
|
||||
"[XO] This error can be triggered if a VG 'linstor_group' is already present on the host.",
|
||||
])
|
||||
}
|
||||
throw error
|
||||
}
|
||||
|
||||
if (provisioning === ENUM_PROVISIONING.Thin) {
|
||||
await lvmPlugin('create_thin_pool', {
|
||||
lv_name: LV_NAME,
|
||||
vg_name: VG_NAME,
|
||||
})
|
||||
}
|
||||
}
|
||||
formatDisks.description = 'Format disks for a XOSTOR use'
|
||||
formatDisks.permission = 'admin'
|
||||
@@ -148,91 +131,83 @@ export const create = defer(async function (
|
||||
$defer,
|
||||
{ description, disksByHost, force, ignoreFileSystems, name, provisioning, replication }
|
||||
) {
|
||||
const task = await this.tasks.create({ name: `creation of XOSTOR: ${name}`, type: 'xo:xostor:create' })
|
||||
return task.run(async () => {
|
||||
const hostIds = Object.keys(disksByHost)
|
||||
const hostIds = Object.keys(disksByHost)
|
||||
|
||||
const tmpBoundObjectId = `tmp_${hostIds.join(',')}_${Math.random().toString(32).slice(2)}`
|
||||
const tmpBoundObjectId = `tmp_${hostIds.join(',')}_${Math.random().toString(32).slice(2)}`
|
||||
|
||||
const license = await Task.run({ properties: { name: 'license check' } }, async () => {
|
||||
const xostorLicenses = await this.getLicenses({ productType: 'xostor' })
|
||||
const xostorLicenses = await this.getLicenses({ productType: 'xostor' })
|
||||
|
||||
const now = Date.now()
|
||||
const availableLicenses = xostorLicenses.filter(
|
||||
({ boundObjectId, expires }) => boundObjectId === undefined && (expires === undefined || expires > now)
|
||||
)
|
||||
const now = Date.now()
|
||||
const availableLicenses = xostorLicenses.filter(
|
||||
({ boundObjectId, expires }) => boundObjectId === undefined && (expires === undefined || expires > now)
|
||||
)
|
||||
|
||||
let _license = availableLicenses.find(({ productId }) => productId === 'xostor')
|
||||
let license = availableLicenses.find(license => license.productId === 'xostor')
|
||||
|
||||
if (_license === undefined) {
|
||||
_license = availableLicenses.find(({ productId }) => productId === 'xostor.trial')
|
||||
}
|
||||
if (license === undefined) {
|
||||
license = availableLicenses.find(license => license.productId === 'xostor.trial')
|
||||
}
|
||||
|
||||
if (_license === undefined) {
|
||||
_license = await this.createBoundXostorTrialLicense({
|
||||
boundObjectId: tmpBoundObjectId,
|
||||
})
|
||||
} else {
|
||||
await this.bindLicense({
|
||||
licenseId: _license.id,
|
||||
boundObjectId: tmpBoundObjectId,
|
||||
})
|
||||
}
|
||||
$defer.onFailure(() =>
|
||||
this.unbindLicense({
|
||||
licenseId: _license.id,
|
||||
productId: _license.productId,
|
||||
boundObjectId: tmpBoundObjectId,
|
||||
})
|
||||
)
|
||||
|
||||
return _license
|
||||
if (license === undefined) {
|
||||
license = await this.createBoundXostorTrialLicense({
|
||||
boundObjectId: tmpBoundObjectId,
|
||||
})
|
||||
|
||||
const hosts = hostIds.map(hostId => this.getObject(hostId, 'host'))
|
||||
if (!hosts.every(host => host.$pool === hosts[0].$pool)) {
|
||||
// we need to do this test to ensure it won't create a partial LV group with only the host of the pool of the first master
|
||||
throw new Error('All hosts must be in the same pool')
|
||||
}
|
||||
|
||||
const boundInstallDependencies = installDependencies.bind(this)
|
||||
await asyncEach(hosts, host => boundInstallDependencies({ host }), { stopOnError: false })
|
||||
const boundFormatDisks = formatDisks.bind(this)
|
||||
await asyncEach(
|
||||
hosts,
|
||||
host => boundFormatDisks({ disks: disksByHost[host.id], host, force, ignoreFileSystems, provisioning }),
|
||||
{
|
||||
stopOnError: false,
|
||||
}
|
||||
)
|
||||
|
||||
const host = hosts[0]
|
||||
const xapi = this.getXapi(host)
|
||||
|
||||
const srUuid = await Task.run({ properties: { name: 'creation of the storage' } }, async () => {
|
||||
const srRef = await xapi.SR_create({
|
||||
device_config: {
|
||||
'group-name': 'linstor_group/' + LV_NAME,
|
||||
redundancy: String(replication),
|
||||
provisioning,
|
||||
},
|
||||
host: host.id,
|
||||
name_description: description,
|
||||
name_label: name,
|
||||
shared: true,
|
||||
type: 'linstor',
|
||||
})
|
||||
return xapi.getField('SR', srRef, 'uuid')
|
||||
})
|
||||
|
||||
await this.rebindLicense({
|
||||
} else {
|
||||
await this.bindLicense({
|
||||
licenseId: license.id,
|
||||
oldBoundObjectId: tmpBoundObjectId,
|
||||
newBoundObjectId: srUuid,
|
||||
boundObjectId: tmpBoundObjectId,
|
||||
})
|
||||
}
|
||||
$defer.onFailure(() =>
|
||||
this.unbindLicense({
|
||||
licenseId: license.id,
|
||||
productId: license.productId,
|
||||
boundObjectId: tmpBoundObjectId,
|
||||
})
|
||||
)
|
||||
|
||||
return srUuid
|
||||
const hosts = hostIds.map(hostId => this.getObject(hostId, 'host'))
|
||||
if (!hosts.every(host => host.$pool === hosts[0].$pool)) {
|
||||
// we need to do this test to ensure it won't create a partial LV group with only the host of the pool of the first master
|
||||
throw new Error('All hosts must be in the same pool')
|
||||
}
|
||||
|
||||
const boundInstallDependencies = installDependencies.bind(this)
|
||||
await asyncEach(hosts, host => boundInstallDependencies({ host }), { stopOnError: false })
|
||||
const boundFormatDisks = formatDisks.bind(this)
|
||||
await asyncEach(
|
||||
hosts,
|
||||
host => boundFormatDisks({ disks: disksByHost[host.id], host, force, ignoreFileSystems, provisioning }),
|
||||
{
|
||||
stopOnError: false,
|
||||
}
|
||||
)
|
||||
|
||||
const host = hosts[0]
|
||||
const xapi = this.getXapi(host)
|
||||
|
||||
log.info(`Create XOSTOR (${name}) with provisioning: ${provisioning}`)
|
||||
const srRef = await xapi.SR_create({
|
||||
device_config: {
|
||||
'group-name': 'linstor_group/' + LV_NAME,
|
||||
redundancy: String(replication),
|
||||
provisioning,
|
||||
},
|
||||
host: host.id,
|
||||
name_description: description,
|
||||
name_label: name,
|
||||
shared: true,
|
||||
type: 'linstor',
|
||||
})
|
||||
const srUuid = await xapi.getField('SR', srRef, 'uuid')
|
||||
|
||||
await this.rebindLicense({
|
||||
licenseId: license.id,
|
||||
oldBoundObjectId: tmpBoundObjectId,
|
||||
newBoundObjectId: srUuid,
|
||||
})
|
||||
|
||||
return srUuid
|
||||
})
|
||||
|
||||
create.description = 'Create a XOSTOR storage'
|
||||
@@ -249,34 +224,19 @@ create.params = {
|
||||
|
||||
// Also called by sr.destroy if sr.SR_type === 'linstor'
|
||||
export async function destroy({ sr }) {
|
||||
const task = this.tasks.create({
|
||||
name: `deletion of XOSTOR: ${sr.name_label}`,
|
||||
objectId: sr.uuid,
|
||||
type: 'xo:xostor:destroy',
|
||||
})
|
||||
return task.run(async () => {
|
||||
if (sr.SR_type !== 'linstor') {
|
||||
throw new Error('Not a XOSTOR storage')
|
||||
}
|
||||
const xapi = this.getXapi(sr)
|
||||
const hosts = Object.values(xapi.objects.indexes.type.host).map(host => this.getObject(host.uuid, 'host'))
|
||||
if (sr.SR_type !== 'linstor') {
|
||||
throw new Error('Not a XOSTOR storage')
|
||||
}
|
||||
const xapi = this.getXapi(sr)
|
||||
const hosts = Object.values(xapi.objects.indexes.type.host).map(host => this.getObject(host.uuid, 'host'))
|
||||
|
||||
await Task.run({ properties: { name: 'deletion of the storage', objectId: sr.uuid } }, () =>
|
||||
xapi.destroySr(sr._xapiId)
|
||||
)
|
||||
const license = (await this.getLicenses({ productType: 'xostor' })).find(
|
||||
license => license.boundObjectId === sr.uuid
|
||||
)
|
||||
await Task.run({ properties: { name: 'unbind the attached license' } }, () =>
|
||||
this.unbindLicense({
|
||||
boundObjectId: license.boundObjectId,
|
||||
productId: license.productId,
|
||||
})
|
||||
)
|
||||
await Task.run({ properties: { name: `destroy volume group on ${hosts.length} hosts` } }, () =>
|
||||
asyncEach(hosts, host => destroyVolumeGroup(xapi, host, true), { stopOnError: false })
|
||||
)
|
||||
await xapi.destroySr(sr._xapiId)
|
||||
const license = (await this.getLicenses({ productType: 'xostor' })).find(license => license.boundObjectId === sr.uuid)
|
||||
await this.unbindLicense({
|
||||
boundObjectId: license.boundObjectId,
|
||||
productId: license.productId,
|
||||
})
|
||||
return asyncEach(hosts, host => destroyVolumeGroup(xapi, host, true), { stopOnError: false })
|
||||
}
|
||||
destroy.description = 'Destroy a XOSTOR storage'
|
||||
destroy.permission = 'admin'
|
||||
|
||||
@@ -697,10 +697,6 @@ const TRANSFORMS = {
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
task(obj) {
|
||||
let applies_to
|
||||
if (obj.other_config.applies_to) {
|
||||
applies_to = obj.$xapi.getObject(obj.other_config.applies_to, undefined).uuid
|
||||
}
|
||||
return {
|
||||
allowedOperations: obj.allowed_operations,
|
||||
created: toTimestamp(obj.created),
|
||||
@@ -712,7 +708,7 @@ const TRANSFORMS = {
|
||||
result: obj.result,
|
||||
status: obj.status,
|
||||
xapiRef: obj.$ref,
|
||||
applies_to,
|
||||
|
||||
$host: link(obj, 'resident_on'),
|
||||
}
|
||||
},
|
||||
|
||||
@@ -389,7 +389,7 @@ export default class Xapi extends XapiBase {
|
||||
|
||||
const onVmCreation = nameLabel !== undefined ? vm => vm.set_name_label(nameLabel) : null
|
||||
|
||||
const vm = await targetXapi._getOrWaitObject(await targetXapi.VM_import(stream, sr.$ref, onVmCreation))
|
||||
const vm = await targetXapi._getOrWaitObject(await targetXapi._importVm(stream, sr, onVmCreation))
|
||||
|
||||
return {
|
||||
vm,
|
||||
@@ -674,6 +674,36 @@ export default class Xapi extends XapiBase {
|
||||
)
|
||||
}
|
||||
|
||||
@cancelable
|
||||
async _importVm($cancelToken, stream, sr, onVmCreation = undefined) {
|
||||
const taskRef = await this.task_create('VM import')
|
||||
const query = {}
|
||||
|
||||
if (sr != null) {
|
||||
query.sr_id = sr.$ref
|
||||
}
|
||||
|
||||
if (onVmCreation != null) {
|
||||
this.waitObject(
|
||||
obj => obj != null && obj.current_operations != null && taskRef in obj.current_operations,
|
||||
onVmCreation
|
||||
)
|
||||
}
|
||||
|
||||
const vmRef = await this.putResource($cancelToken, stream, '/import/', {
|
||||
query,
|
||||
task: taskRef,
|
||||
}).then(extractOpaqueRef, error => {
|
||||
// augment the error with as much relevant info as possible
|
||||
error.pool_master = this.pool.$master
|
||||
error.SR = sr
|
||||
|
||||
throw error
|
||||
})
|
||||
|
||||
return vmRef
|
||||
}
|
||||
|
||||
@decorateWith(deferrable)
|
||||
async _importOvaVm($defer, stream, { descriptionLabel, disks, memory, nameLabel, networks, nCpus, tables }, sr) {
|
||||
// 1. Create VM.
|
||||
@@ -782,7 +812,7 @@ export default class Xapi extends XapiBase {
|
||||
const sr = srId && this.getObject(srId)
|
||||
|
||||
if (type === 'xva') {
|
||||
return /* await */ this._getOrWaitObject(await this.VM_import(stream, sr?.$ref))
|
||||
return /* await */ this._getOrWaitObject(await this._importVm(stream, sr))
|
||||
}
|
||||
|
||||
if (type === 'ova') {
|
||||
|
||||
@@ -129,9 +129,7 @@ export default class BackupNg {
|
||||
isMatchingVm(obj) &&
|
||||
// don't match replicated VMs created by this very job otherwise
|
||||
// they will be replicated again and again
|
||||
!('start' in obj.blockedOperations && obj.other['xo:backup:job'] === job.id) &&
|
||||
// handle xo:no-bak and xo:no-bak=reason tags. For example : VMs from Health Check
|
||||
!obj.tags.some(t => t.split('=', 1)[0] === 'xo:no-bak')
|
||||
!('start' in obj.blockedOperations && obj.other['xo:backup:job'] === job.id)
|
||||
})(),
|
||||
})
|
||||
)
|
||||
@@ -626,10 +624,7 @@ export default class BackupNg {
|
||||
.run(async () => {
|
||||
const app = this._app
|
||||
const xapi = app.getXapi(srId)
|
||||
const restoredId = await this.importVmBackupNg(backupId, srId, {
|
||||
...settings,
|
||||
additionnalVmTag: 'xo:no-bak=Health Check',
|
||||
})
|
||||
const restoredId = await this.importVmBackupNg(backupId, srId, settings)
|
||||
|
||||
const restoredVm = xapi.getObject(restoredId)
|
||||
try {
|
||||
|
||||
@@ -1,12 +1,5 @@
|
||||
import Disposable from 'promise-toolbox/Disposable'
|
||||
import isPromise from 'promise-toolbox/isPromise'
|
||||
import { asyncEach } from '@vates/async-each'
|
||||
import { createLogger } from '@xen-orchestra/log'
|
||||
import { decorateWith } from '@vates/decorate-with'
|
||||
import { execa } from 'execa'
|
||||
import { MultiKeyMap } from '@vates/multi-key-map'
|
||||
|
||||
const { warn } = createLogger('xo:mixins:file-restore-ng')
|
||||
|
||||
// - [x] list partitions
|
||||
// - [x] list files in a partition
|
||||
@@ -29,8 +22,6 @@ const { warn } = createLogger('xo:mixins:file-restore-ng')
|
||||
// - [ ] getMountedPartitions
|
||||
// - [ ] unmountPartition
|
||||
export default class BackupNgFileRestore {
|
||||
#mounts = new MultiKeyMap()
|
||||
|
||||
constructor(app) {
|
||||
this._app = app
|
||||
|
||||
@@ -40,16 +31,6 @@ export default class BackupNgFileRestore {
|
||||
await Promise.all([execa('losetup', ['-D']), execa('vgchange', ['-an'])])
|
||||
await execa('pvscan', ['--cache'])
|
||||
})
|
||||
|
||||
app.hooks.on('stop', () =>
|
||||
asyncEach(
|
||||
this.#mounts.values(),
|
||||
async pDisposable => {
|
||||
await (await pDisposable).dispose()
|
||||
},
|
||||
{ stopOnError: false }
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
async fetchBackupNgPartitionFiles(remoteId, diskId, partitionId, paths, format) {
|
||||
@@ -127,77 +108,4 @@ export default class BackupNgFileRestore {
|
||||
adapter.listPartitionFiles(diskId, partitionId, path)
|
||||
)
|
||||
}
|
||||
|
||||
listMountedPartitions() {
|
||||
const mounts = []
|
||||
for (const [key, disposable] of this.#mounts.entries()) {
|
||||
if (!isPromise(disposable)) {
|
||||
const [remote, disk, partition] = key
|
||||
mounts.push({ remote, disk, partition, path: disposable.value })
|
||||
}
|
||||
}
|
||||
return mounts
|
||||
}
|
||||
|
||||
@decorateWith(Disposable.factory)
|
||||
*_mountPartition(remoteId, diskId, partitionId) {
|
||||
const adapter = yield this._app.getBackupsRemoteAdapter(remoteId)
|
||||
|
||||
// yield(2) the disposable to use it
|
||||
// yield(1) the value to make it available
|
||||
yield yield adapter.getPartition(diskId, partitionId)
|
||||
}
|
||||
|
||||
async mountPartition(remoteId, diskId, partitionId) {
|
||||
const mounts = this.#mounts
|
||||
const key = [remoteId, diskId, partitionId]
|
||||
|
||||
let pDisposable = mounts.get(key)
|
||||
if (pDisposable !== undefined) {
|
||||
return (await pDisposable).value
|
||||
}
|
||||
|
||||
pDisposable = this._mountPartition(remoteId, diskId, partitionId)
|
||||
mounts.set(key, pDisposable)
|
||||
pDisposable.catch(() => mounts.delete(key))
|
||||
|
||||
const disposable = await pDisposable
|
||||
|
||||
// replace the promise by it's value so that it can be used directly in
|
||||
// listMountedPartitions without breaking other uses
|
||||
mounts.set(key, disposable)
|
||||
|
||||
const delay = await this._app.config.getDuration('backups.autoUnmountPartitionDelay')
|
||||
if (delay !== 0) {
|
||||
const dispose = disposable.dispose.bind(disposable)
|
||||
|
||||
const handle = setTimeout(
|
||||
() =>
|
||||
disposable.dispose().catch(error => {
|
||||
warn('unmounting partition', { error })
|
||||
}),
|
||||
delay
|
||||
)
|
||||
disposable.dispose = () => {
|
||||
clearTimeout(handle)
|
||||
return dispose()
|
||||
}
|
||||
}
|
||||
|
||||
return disposable.value
|
||||
}
|
||||
|
||||
async unmountPartition(remoteId, diskId, partitionId) {
|
||||
const mounts = this.#mounts
|
||||
const key = [remoteId, diskId, partitionId]
|
||||
|
||||
const pDisposable = mounts.get(key)
|
||||
if (pDisposable === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
mounts.delete(key)
|
||||
|
||||
await (await pDisposable).dispose()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -271,13 +271,13 @@ export default class Proxy {
|
||||
[namespace]: { xva },
|
||||
} = await app.getResourceCatalog()
|
||||
const xapi = app.getXapi(srId)
|
||||
const vm = await xapi.VM_import(
|
||||
const vm = await xapi.importVm(
|
||||
await app.requestResource({
|
||||
id: xva.id,
|
||||
namespace,
|
||||
version: xva.version,
|
||||
}),
|
||||
srId && this.getObject(srId, 'SR')._xapiRef
|
||||
{ srId }
|
||||
)
|
||||
$defer.onFailure(() => xapi.VM_destroy(vm.$ref))
|
||||
|
||||
|
||||
@@ -453,15 +453,6 @@ export default class RestApi {
|
||||
await pipeline(stream, res)
|
||||
})
|
||||
)
|
||||
api.put(
|
||||
'/:collection(vdis|vdi-snapshots)/:object.:format(vhd|raw)',
|
||||
wrap(async (req, res) => {
|
||||
req.length = +req.headers['content-length']
|
||||
await req.xapiObject.$importContent(req, { format: req.params.format })
|
||||
|
||||
res.sendStatus(204)
|
||||
})
|
||||
)
|
||||
api.get(
|
||||
'/:collection(vms|vm-snapshots|vm-templates)/:object.xva',
|
||||
wrap(async (req, res) => {
|
||||
@@ -488,43 +479,24 @@ export default class RestApi {
|
||||
|
||||
res.json(result)
|
||||
})
|
||||
api
|
||||
.patch(
|
||||
'/:collection/:object',
|
||||
json(),
|
||||
wrap(async (req, res) => {
|
||||
const obj = req.xapiObject
|
||||
api.patch(
|
||||
'/:collection/:object',
|
||||
json(),
|
||||
wrap(async (req, res) => {
|
||||
const obj = req.xapiObject
|
||||
|
||||
const promises = []
|
||||
const { body } = req
|
||||
|
||||
for (const key of ['name_description', 'name_label', 'tags']) {
|
||||
const value = body[key]
|
||||
if (value !== undefined) {
|
||||
promises.push(obj['set_' + key](value))
|
||||
}
|
||||
const promises = []
|
||||
const { body } = req
|
||||
for (const key of ['name_description', 'name_label']) {
|
||||
const value = body[key]
|
||||
if (value !== undefined) {
|
||||
promises.push(obj['set_' + key](value))
|
||||
}
|
||||
|
||||
await promises
|
||||
res.sendStatus(204)
|
||||
})
|
||||
)
|
||||
.delete(
|
||||
'/:collection/:object/tags/:tag',
|
||||
wrap(async (req, res) => {
|
||||
await req.xapiObject.$call('remove_tags', req.params.tag)
|
||||
|
||||
res.sendStatus(204)
|
||||
})
|
||||
)
|
||||
.put(
|
||||
'/:collection/:object/tags/:tag',
|
||||
wrap(async (req, res) => {
|
||||
await req.xapiObject.$call('add_tags', req.params.tag)
|
||||
|
||||
res.sendStatus(204)
|
||||
})
|
||||
)
|
||||
}
|
||||
await promises
|
||||
res.sendStatus(204)
|
||||
})
|
||||
)
|
||||
|
||||
api.get(
|
||||
'/:collection/:object/tasks',
|
||||
@@ -576,22 +548,6 @@ export default class RestApi {
|
||||
})
|
||||
)
|
||||
|
||||
api.post(
|
||||
'/:collection(pools)/:object/vms',
|
||||
wrap(async (req, res) => {
|
||||
let srRef
|
||||
const { sr } = req.params
|
||||
if (sr !== undefined) {
|
||||
srRef = app.getXapiObject(sr, 'SR').$ref
|
||||
}
|
||||
|
||||
const { $xapi } = req.xapiObject
|
||||
const ref = await $xapi.VM_import(req, srRef)
|
||||
|
||||
res.end(await $xapi.getField('VM', ref, 'uuid'))
|
||||
})
|
||||
)
|
||||
|
||||
api.post(
|
||||
'/:collection(srs)/:object/vdis',
|
||||
wrap(async (req, res) => {
|
||||
|
||||
@@ -7,7 +7,6 @@ import isEmpty from 'lodash/isEmpty.js'
|
||||
import iteratee from 'lodash/iteratee.js'
|
||||
import mixin from '@xen-orchestra/mixin'
|
||||
import mixinLegacy from '@xen-orchestra/mixin/legacy.js'
|
||||
import once from 'lodash/once.js'
|
||||
import stubTrue from 'lodash/stubTrue.js'
|
||||
import SslCertificate from '@xen-orchestra/mixins/SslCertificate.mjs'
|
||||
import Tasks from '@xen-orchestra/mixins/Tasks.mjs'
|
||||
@@ -127,16 +126,16 @@ export default class Xo extends EventEmitter {
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
_handleHttpRequest(req, res, next) {
|
||||
const { path } = req
|
||||
const { url } = req
|
||||
|
||||
const { _httpRequestWatchers: watchers } = this
|
||||
const watcher = watchers[path]
|
||||
const watcher = watchers[url]
|
||||
if (!watcher) {
|
||||
next()
|
||||
return
|
||||
}
|
||||
if (!watcher.persistent) {
|
||||
delete watchers[path]
|
||||
delete watchers[url]
|
||||
}
|
||||
|
||||
const { fn, data } = watcher
|
||||
@@ -172,35 +171,35 @@ export default class Xo extends EventEmitter {
|
||||
|
||||
async registerHttpRequest(fn, data, { suffix = '' } = {}) {
|
||||
const { _httpRequestWatchers: watchers } = this
|
||||
let path
|
||||
let url
|
||||
|
||||
do {
|
||||
path = `/api/${await generateToken()}${suffix}`
|
||||
} while (path in watchers)
|
||||
url = `/api/${await generateToken()}${suffix}`
|
||||
} while (url in watchers)
|
||||
|
||||
watchers[path] = {
|
||||
watchers[url] = {
|
||||
data,
|
||||
fn,
|
||||
}
|
||||
return path
|
||||
return url
|
||||
}
|
||||
|
||||
async registerHttpRequestHandler(path, fn, { data = undefined, persistent = true } = {}) {
|
||||
async registerHttpRequestHandler(url, fn, { data = undefined, persistent = true } = {}) {
|
||||
const { _httpRequestWatchers: watchers } = this
|
||||
|
||||
if (path in watchers) {
|
||||
throw new Error(`a handler is already registered for ${path}`)
|
||||
if (url in watchers) {
|
||||
throw new Error(`a handler is already registered for ${url}`)
|
||||
}
|
||||
|
||||
watchers[path] = {
|
||||
watchers[url] = {
|
||||
data,
|
||||
fn,
|
||||
persistent,
|
||||
}
|
||||
}
|
||||
|
||||
return once(() => {
|
||||
delete this._httpRequestWatchers[path]
|
||||
})
|
||||
async unregisterHttpRequestHandler(url) {
|
||||
delete this._httpRequestWatchers[url]
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": false,
|
||||
"name": "xo-vmdk-to-vhd",
|
||||
"version": "2.5.7",
|
||||
"version": "2.5.6",
|
||||
"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": "^3.1.6",
|
||||
"vhd-lib": "^4.7.0",
|
||||
"vhd-lib": "^4.6.1",
|
||||
"xml2js": "^0.4.23"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "xo-web",
|
||||
"version": "5.130.0",
|
||||
"version": "5.127.2",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"description": "Web interface client for Xen-Orchestra",
|
||||
"keywords": [
|
||||
@@ -137,7 +137,7 @@
|
||||
"xo-common": "^0.8.0",
|
||||
"xo-lib": "^0.11.1",
|
||||
"xo-remote-parser": "^0.9.2",
|
||||
"xo-vmdk-to-vhd": "^2.5.7"
|
||||
"xo-vmdk-to-vhd": "^2.5.6"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "GIT_HEAD=$(git rev-parse HEAD) NODE_ENV=production gulp build",
|
||||
|
||||
@@ -572,8 +572,6 @@ const messages = {
|
||||
editBackupSmartNotResidentOn: 'Not resident on',
|
||||
editBackupSmartPools: 'Pools',
|
||||
editBackupSmartTags: 'Tags',
|
||||
editBackupSmartTagsInfo:
|
||||
"VMs with tags in the form of <b>xo:no-bak</b> or <b>xo:no-bak=Reason</b>won't be included in any backup.For example, ephemeral VMs created by health check have this tag",
|
||||
sampleOfMatchingVms: 'Sample of matching VMs',
|
||||
backupReplicatedVmsInfo:
|
||||
'Replicated VMs (VMs with Continuous Replication or Disaster Recovery tag) must be excluded!',
|
||||
@@ -864,7 +862,6 @@ const messages = {
|
||||
srDisconnectAll: 'Disconnect from all hosts',
|
||||
srForget: 'Forget this SR',
|
||||
srsForget: 'Forget SRs',
|
||||
nSrsForget: 'Forget {nSrs, number} SR{nSrs, plural, one {} other{s}}',
|
||||
srRemoveButton: 'Remove this SR',
|
||||
srNoVdis: 'No VDIs in this storage',
|
||||
srReclaimSpace: 'Reclaim freed space',
|
||||
@@ -1241,7 +1238,6 @@ const messages = {
|
||||
vdiNameDescription: 'Description',
|
||||
vdiPool: 'Pool',
|
||||
vdiTags: 'Tags',
|
||||
vdiTasks: 'VDI tasks',
|
||||
vdiSize: 'Size',
|
||||
vdiSr: 'SR',
|
||||
vdiVms: 'VMs',
|
||||
@@ -1803,7 +1799,6 @@ const messages = {
|
||||
latest: 'latest',
|
||||
restoreVmBackupsStart: 'Start VM{nVms, plural, one {} other {s}} after restore',
|
||||
restoreVmBackupsBulkErrorTitle: 'Multi-restore error',
|
||||
restoreVmUseDifferentialRestore: 'Use differential restore',
|
||||
restoreMetadataBackupTitle: 'Restore {item}',
|
||||
bulkRestoreMetadataBackupTitle:
|
||||
'Restore {nMetadataBackups, number} metadata backup{nMetadataBackups, plural, one {} other {s}}',
|
||||
@@ -2376,9 +2371,11 @@ const messages = {
|
||||
srDisconnectAllModalMessage: 'This will disconnect this SR from all its hosts.',
|
||||
srsDisconnectAllModalMessage:
|
||||
'This will disconnect each selected SR from its host (local SR) or from every hosts of its pool (shared SR).',
|
||||
forgetNSrsModalMessage: 'Are you sure you want to forget {nSrs, number} SR{nSrs, plural, one {} other{s}}?',
|
||||
srForgetModalWarning:
|
||||
'You will lose all the metadata, meaning all the links between the VDIs (disks) and their respective VMs. This operation cannot be undone.',
|
||||
srForgetModalTitle: 'Forget SR',
|
||||
srsForgetModalTitle: 'Forget selected SRs',
|
||||
srForgetModalMessage: "Are you sure you want to forget this SR? VDIs on this storage won't be removed.",
|
||||
srsForgetModalMessage:
|
||||
"Are you sure you want to forget all the selected SRs? VDIs on these storages won't be removed.",
|
||||
srAllDisconnected: 'Disconnected',
|
||||
srSomeConnected: 'Partially connected',
|
||||
srAllConnected: 'Connected',
|
||||
|
||||
@@ -2268,20 +2268,15 @@ export const deleteSr = sr =>
|
||||
|
||||
export const fetchSrStats = (sr, granularity) => _call('sr.stats', { id: resolveId(sr), granularity })
|
||||
|
||||
export const forgetSr = sr => forgetSrs([sr])
|
||||
|
||||
export const forgetSr = sr =>
|
||||
confirm({
|
||||
title: _('srForgetModalTitle'),
|
||||
body: _('srForgetModalMessage'),
|
||||
}).then(() => _call('sr.forget', { id: resolveId(sr) }), noop)
|
||||
export const forgetSrs = srs =>
|
||||
confirm({
|
||||
title: _('nSrsForget', { nSrs: srs.length }),
|
||||
body: (
|
||||
<p className='text-warning font-weight-bold'>
|
||||
{_('forgetNSrsModalMessage', { nSrs: srs.length })} {_('srForgetModalWarning')}
|
||||
</p>
|
||||
),
|
||||
strongConfirm: {
|
||||
messageId: 'nSrsForget',
|
||||
values: { nSrs: srs.length },
|
||||
},
|
||||
title: _('srsForgetModalTitle'),
|
||||
body: _('srsForgetModalMessage'),
|
||||
}).then(() => Promise.all(map(resolveIds(srs), id => _call('sr.forget', { id }))), noop)
|
||||
|
||||
export const reconnectAllHostsSr = sr =>
|
||||
@@ -2589,11 +2584,11 @@ export const listVmBackups = remotes => _call('backupNg.listVmBackups', { remote
|
||||
export const restoreBackup = (
|
||||
backup,
|
||||
sr,
|
||||
{ generateNewMacAddresses = false, mapVdisSrs = {}, startOnRestore = false, useDifferentialRestore= false } = {}
|
||||
{ generateNewMacAddresses = false, mapVdisSrs = {}, startOnRestore = false } = {}
|
||||
) => {
|
||||
const promise = _call('backupNg.importVmBackup', {
|
||||
id: resolveId(backup),
|
||||
settings: { mapVdisSrs: resolveIds(mapVdisSrs), newMacAddresses: generateNewMacAddresses, useDifferentialRestore },
|
||||
settings: { mapVdisSrs: resolveIds(mapVdisSrs), newMacAddresses: generateNewMacAddresses },
|
||||
sr: resolveId(sr),
|
||||
})
|
||||
|
||||
|
||||
@@ -97,12 +97,7 @@ const SmartBackup = decorate([
|
||||
</label>
|
||||
<SelectPool multi onChange={effects.setPoolNotValues} value={state.pools.notValues} />
|
||||
</FormGroup>
|
||||
<h3>
|
||||
{_('editBackupSmartTags')}
|
||||
<Tooltip content={_('editBackupSmartTagsInfo')}>
|
||||
<Icon icon='info' />
|
||||
</Tooltip>{' '}
|
||||
</h3>
|
||||
<h3>{_('editBackupSmartTags')}</h3>
|
||||
<hr />
|
||||
<FormGroup>
|
||||
<label>
|
||||
|
||||
@@ -184,16 +184,16 @@ export default class Restore extends Component {
|
||||
body: <RestoreBackupsModalBody data={data} />,
|
||||
icon: 'restore',
|
||||
})
|
||||
.then(({ backup, generateNewMacAddresses, targetSrs: { mainSr, mapVdisSrs }, start, useDifferentialRestore }) => {
|
||||
.then(({ backup, generateNewMacAddresses, targetSrs: { mainSr, mapVdisSrs }, start }) => {
|
||||
if (backup == null || mainSr == null) {
|
||||
error(_('backupRestoreErrorTitle'), _('backupRestoreErrorMessage'))
|
||||
return
|
||||
}
|
||||
|
||||
return restoreBackup(backup, mainSr, {
|
||||
generateNewMacAddresses,
|
||||
mapVdisSrs,
|
||||
startOnRestore: start,
|
||||
useDifferentialRestore,
|
||||
})
|
||||
}, noop)
|
||||
.then(() => this._refreshBackupList())
|
||||
|
||||
@@ -12,11 +12,7 @@ import { SelectSr } from 'select-objects'
|
||||
const BACKUP_RENDERER = getRenderXoItemOfType('backup')
|
||||
|
||||
export default class RestoreBackupsModalBody extends Component {
|
||||
state = {
|
||||
generateNewMacAddresses: false,
|
||||
targetSrs: { mainSr: undefined, mapVdisSrs: undefined },
|
||||
useDifferentialRestore: false,
|
||||
}
|
||||
state = { generateNewMacAddresses: false, targetSrs: { mainSr: undefined, mapVdisSrs: undefined } }
|
||||
|
||||
get value() {
|
||||
return this.state
|
||||
@@ -83,17 +79,6 @@ export default class RestoreBackupsModalBody extends Component {
|
||||
{_('generateNewMacAddress')}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{this.state.backup.mode === 'delta' && (
|
||||
<div>
|
||||
<Toggle
|
||||
iconSize={1}
|
||||
value={this.state.useDifferentialRestore}
|
||||
onChange={this.toggleState('useDifferentialRestore')}
|
||||
/>{' '}
|
||||
{_('restoreVmUseDifferentialRestore')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -66,22 +66,15 @@ const FILTERS = {
|
||||
|
||||
@connectStore(() => ({
|
||||
host: createGetObject((_, props) => props.item.$host),
|
||||
appliesTo: createGetObject((_, props) => props.item.applies_to),
|
||||
}))
|
||||
export class TaskItem extends Component {
|
||||
render() {
|
||||
const { appliesTo, host, item: task } = this.props
|
||||
// garbage collection task has an uuid in the desc
|
||||
const showDesc = task.name_description && task.name_label !== 'Garbage Collection'
|
||||
const { host, item: task } = this.props
|
||||
|
||||
return (
|
||||
<div>
|
||||
{task.name_label} ({showDesc && `${task.name_description} `}
|
||||
{task.name_label} ({task.name_description && `${task.name_description} `}
|
||||
on {host ? <Link to={`/hosts/${host.id}`}>{host.name_label}</Link> : `unknown host − ${task.$host}`})
|
||||
{appliesTo !== undefined && (
|
||||
<span>
|
||||
, applies to <Link to={`/srs/${appliesTo.id}`}>{appliesTo.name_label}</Link>
|
||||
</span>
|
||||
)}
|
||||
{task.disappeared === undefined && ` ${Math.round(task.progress * 100)}%`}
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -11,19 +11,19 @@ import React from 'react'
|
||||
import StateButton from 'state-button'
|
||||
import SortedTable from 'sorted-table'
|
||||
import TabButton from 'tab-button'
|
||||
import { compact, every, filter, find, forEach, get, map, reduce, some, sortedUniq, uniq } from 'lodash'
|
||||
import { Sr, Vdi } from 'render-xo-item'
|
||||
import { compact, every, filter, find, forEach, get, map, some, sortedUniq, uniq } from 'lodash'
|
||||
import { Sr } from 'render-xo-item'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import {
|
||||
createCollectionWrapper,
|
||||
createGetObjectsOfType,
|
||||
createSelector,
|
||||
createFilter,
|
||||
createFinder,
|
||||
getCheckPermissions,
|
||||
getResolvedResourceSet,
|
||||
isAdmin,
|
||||
} from 'selectors'
|
||||
import { injectIntl } from 'react-intl'
|
||||
import {
|
||||
addSubscriptions,
|
||||
connectStore,
|
||||
@@ -57,8 +57,6 @@ import {
|
||||
setBootableVbd,
|
||||
subscribeResourceSets,
|
||||
} from 'xo'
|
||||
import { Card, CardHeader, CardBlock } from 'card'
|
||||
import { FormattedRelative, injectIntl } from 'react-intl'
|
||||
import { SelectResourceSetsSr, SelectSr as SelectAnySr, SelectVdi } from 'select-objects'
|
||||
|
||||
const compareSrs = createCompare([isSrShared])
|
||||
@@ -170,55 +168,6 @@ const COLUMNS_VM_PV = [
|
||||
|
||||
const COLUMNS = filter(COLUMNS_VM_PV, col => col.id !== 'vbdBootableStatus')
|
||||
|
||||
const PROGRESS_STYLES = { margin: 0 }
|
||||
|
||||
const COLUMNS_VDI_TASKS = [
|
||||
{
|
||||
itemRenderer: task => task.name_label,
|
||||
name: _('name'),
|
||||
sortCriteria: 'name_label',
|
||||
},
|
||||
{
|
||||
itemRenderer: task => <Vdi id={task.details.vdiId} />,
|
||||
name: _('object'),
|
||||
sortCriteria: 'details.vdiName',
|
||||
},
|
||||
{
|
||||
itemRenderer: task => task.details.action,
|
||||
name: _('action'),
|
||||
sortCriteria: 'details.action',
|
||||
},
|
||||
{
|
||||
itemRenderer: task => formatSize(task.details.length),
|
||||
name: _('size'),
|
||||
sortCriteria: 'details.length',
|
||||
},
|
||||
{
|
||||
itemRenderer: task => (
|
||||
<progress style={PROGRESS_STYLES} className='progress' value={task.progress * 100} max='100' />
|
||||
),
|
||||
name: _('progress'),
|
||||
sortCriteria: 'progress',
|
||||
},
|
||||
{
|
||||
itemRenderer: task => <FormattedRelative value={task.created * 1000} />,
|
||||
name: _('taskStarted'),
|
||||
sortCriteria: 'created',
|
||||
},
|
||||
{
|
||||
itemRenderer: task => {
|
||||
const started = task.created * 1000
|
||||
const { progress } = task
|
||||
|
||||
if (progress === 0 || progress === 1) {
|
||||
return // not yet started or already finished
|
||||
}
|
||||
return <FormattedRelative value={started + (Date.now() - started) / progress} />
|
||||
},
|
||||
name: _('taskEstimatedEnd'),
|
||||
},
|
||||
]
|
||||
|
||||
const INDIVIDUAL_ACTIONS = [
|
||||
...(process.env.XOA_PLAN > 1
|
||||
? [
|
||||
@@ -505,39 +454,10 @@ class AttachDisk extends Component {
|
||||
}))
|
||||
@connectStore(() => {
|
||||
const getAllVbds = createGetObjectsOfType('VBD')
|
||||
const getTasks = createGetObjectsOfType('task')
|
||||
|
||||
const getDetailedImportVdiTasks = createSelector(
|
||||
getTasks,
|
||||
createFilter((state, props) => props.vdis, [vdi => vdi.other_config['xo:import:task'] !== undefined]),
|
||||
createCollectionWrapper((tasks, vdis) =>
|
||||
reduce(
|
||||
vdis,
|
||||
(acc, vdi) => {
|
||||
const task = tasks[vdi.other_config['xo:import:task']]
|
||||
const length = vdi.other_config['xo:import:length']
|
||||
|
||||
acc.push({
|
||||
...task,
|
||||
details: {
|
||||
action: 'import',
|
||||
length: Number(length),
|
||||
vdiId: vdi.uuid,
|
||||
vdiName: vdi.name_label,
|
||||
},
|
||||
})
|
||||
|
||||
return acc
|
||||
},
|
||||
[]
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
return (state, props) => ({
|
||||
allVbds: getAllVbds(state, props),
|
||||
checkPermissions: getCheckPermissions(state, props),
|
||||
detailedImportVdiTasks: getDetailedImportVdiTasks(state, props),
|
||||
isAdmin: isAdmin(state, props),
|
||||
resolvedResourceSet: getResolvedResourceSet(state, props, !props.isAdmin && props.resourceSet !== undefined),
|
||||
})
|
||||
@@ -792,20 +712,6 @@ export default class TabDisks extends Component {
|
||||
<IsoDevice vm={vm} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row className='mt-1'>
|
||||
<Col>
|
||||
<Card>
|
||||
<CardHeader>{_('vdiTasks')}</CardHeader>
|
||||
<CardBlock>
|
||||
<SortedTable
|
||||
collection={this.props.detailedImportVdiTasks}
|
||||
columns={COLUMNS_VDI_TASKS}
|
||||
stateUrlParam='t'
|
||||
/>
|
||||
</CardBlock>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -123,10 +123,6 @@ async function readPackagesFromChangelog(toRelease) {
|
||||
}
|
||||
|
||||
const { name, releaseType } = match.groups
|
||||
if (name in toRelease) {
|
||||
throw new Error('duplicate package to release in CHANGELOG.unreleased.md: ' + name)
|
||||
}
|
||||
|
||||
toRelease[name] = releaseType
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user