Compare commits
43 Commits
xen-api-2
...
bugfix-rem
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c11ed381c7 | ||
|
|
265b545c0c | ||
|
|
86ccdd8f72 | ||
|
|
f0da94081b | ||
|
|
cd44a6e28c | ||
|
|
70b09839c7 | ||
|
|
12140143d2 | ||
|
|
e68236c9f2 | ||
|
|
8a1a0d76f7 | ||
|
|
4a5bc5dccc | ||
|
|
0ccdfbd6f4 | ||
|
|
75af7668b5 | ||
|
|
0b454fa670 | ||
|
|
2dcb5cb7cd | ||
|
|
a5aeeceb7f | ||
|
|
b2f2c3cbc4 | ||
|
|
0f7ac004ad | ||
|
|
7faa82a9c8 | ||
|
|
4b3f60b280 | ||
|
|
b29d5ba95c | ||
|
|
408fc5af84 | ||
|
|
2748aea4e9 | ||
|
|
a5acc7d267 | ||
|
|
87a9fbe237 | ||
|
|
9d0b7242f0 | ||
|
|
20ec44c3b3 | ||
|
|
6f68456bae | ||
|
|
b856c1a6b4 | ||
|
|
61e1f83a9f | ||
|
|
5820e19731 | ||
|
|
cdb51f8fe3 | ||
|
|
57940e0a52 | ||
|
|
6cc95efe51 | ||
|
|
b0ff2342ab | ||
|
|
0f67692be4 | ||
|
|
865461bfb9 | ||
|
|
e108cb0990 | ||
|
|
c4535c6bae | ||
|
|
ad8eaaa771 | ||
|
|
9419cade3d | ||
|
|
272e6422bd | ||
|
|
547908a8f9 | ||
|
|
8abfaa0bd5 |
@@ -68,6 +68,11 @@ 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.4",
|
||||
"version": "0.1.5",
|
||||
"engines": {
|
||||
"node": ">=8.10"
|
||||
},
|
||||
@@ -23,7 +23,7 @@
|
||||
"test": "node--test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vates/multi-key-map": "^0.1.0",
|
||||
"@vates/multi-key-map": "^0.2.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.6.1"
|
||||
"vhd-lib": "^4.7.0"
|
||||
},
|
||||
"scripts": {
|
||||
"postversion": "npm publish --access public"
|
||||
|
||||
@@ -17,4 +17,14 @@ 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,6 +35,16 @@ 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,6 +36,23 @@ 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
|
||||
@@ -69,6 +86,22 @@ 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
|
||||
@@ -79,6 +112,10 @@ exports.MultiKeyMap = class MultiKeyMap {
|
||||
this._root = del(this._root, 0, keys)
|
||||
}
|
||||
|
||||
entries() {
|
||||
return entries(this._root, [])
|
||||
}
|
||||
|
||||
get(keys) {
|
||||
return get(this._root, 0, keys)
|
||||
}
|
||||
@@ -86,4 +123,8 @@ 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(() => ({}))
|
||||
const values = keys.map(() => Math.random())
|
||||
|
||||
// set all values first to make sure they are all stored and not only the
|
||||
// last one
|
||||
@@ -27,6 +27,12 @@ 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.1.0",
|
||||
"version": "0.2.0",
|
||||
"engines": {
|
||||
"node": ">=8.10"
|
||||
},
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"url": "https://vates.fr"
|
||||
},
|
||||
"license": "ISC",
|
||||
"version": "2.0.0",
|
||||
"version": "2.0.1",
|
||||
"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": "^1.3.6"
|
||||
"xen-api": "^2.0.0"
|
||||
},
|
||||
"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.43.2",
|
||||
"@xen-orchestra/fs": "^4.1.2",
|
||||
"@xen-orchestra/backups": "^0.44.2",
|
||||
"@xen-orchestra/fs": "^4.1.3",
|
||||
"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.13",
|
||||
"version": "1.0.14",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"author": {
|
||||
"name": "Vates SAS",
|
||||
|
||||
@@ -4,7 +4,14 @@ 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
|
||||
@@ -16,26 +23,199 @@ async function resolveUuid(xapi, cache, uuid, type) {
|
||||
return cache.get(uuid)
|
||||
}
|
||||
export class ImportVmBackup {
|
||||
constructor({ adapter, metadata, srUuid, xapi, settings: { newMacAddresses, mapVdisSrs = {} } = {} }) {
|
||||
constructor({
|
||||
adapter,
|
||||
metadata,
|
||||
srUuid,
|
||||
xapi,
|
||||
settings: { additionnalVmTag, newMacAddresses, mapVdisSrs = {}, useDifferentialRestore = false } = {},
|
||||
}) {
|
||||
this._adapter = adapter
|
||||
this._importIncrementalVmSettings = { newMacAddresses, mapVdisSrs }
|
||||
this._importIncrementalVmSettings = { additionnalVmTag, newMacAddresses, mapVdisSrs, useDifferentialRestore }
|
||||
this._metadata = metadata
|
||||
this._srUuid = srUuid
|
||||
this._xapi = xapi
|
||||
}
|
||||
|
||||
async #decorateIncrementalVmMetadata(backup) {
|
||||
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
|
||||
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 sr = await resolveUuid(xapi, cache, this._srUuid, 'SR')
|
||||
const srRef = await resolveUuid(xapi, cache, this._srUuid, 'SR')
|
||||
Object.values(backup.vdis).forEach(vdi => {
|
||||
vdi.SR = mapVdisSrRefs[vdi.uuid] ?? sr.$ref
|
||||
vdi.SR = mapVdisSrRefs[vdi.uuid] ?? srRef
|
||||
})
|
||||
return backup
|
||||
}
|
||||
@@ -46,7 +226,7 @@ export class ImportVmBackup {
|
||||
const isFull = metadata.mode === 'full'
|
||||
|
||||
const sizeContainer = { size: 0 }
|
||||
const { mapVdisSrs, newMacAddresses } = this._importIncrementalVmSettings
|
||||
const { newMacAddresses } = this._importIncrementalVmSettings
|
||||
let backup
|
||||
if (isFull) {
|
||||
backup = await adapter.readFullVmBackup(metadata)
|
||||
@@ -54,12 +234,7 @@ export class ImportVmBackup {
|
||||
} else {
|
||||
assert.strictEqual(metadata.mode, 'delta')
|
||||
|
||||
const ignoredVdis = new Set(
|
||||
Object.entries(mapVdisSrs)
|
||||
.filter(([_, srUuid]) => srUuid === null)
|
||||
.map(([vdiUuid]) => vdiUuid)
|
||||
)
|
||||
backup = await this.#decorateIncrementalVmMetadata(await adapter.readIncrementalVmBackup(metadata, ignoredVdis))
|
||||
backup = await this.#decorateIncrementalVmMetadata()
|
||||
Object.values(backup.streams).forEach(stream => watchStreamSize(stream, sizeContainer))
|
||||
}
|
||||
|
||||
@@ -101,3 +276,5 @@ export class ImportVmBackup {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
decorateClass(ImportVmBackup, { _reuseNearestSnapshot: defer })
|
||||
|
||||
@@ -250,6 +250,10 @@ 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,6 +96,9 @@ 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.43.2",
|
||||
"version": "0.44.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.4",
|
||||
"@vates/disposable": "^0.1.5",
|
||||
"@vates/fuse-vhd": "^2.0.0",
|
||||
"@vates/nbd-client": "^2.0.0",
|
||||
"@vates/nbd-client": "^2.0.1",
|
||||
"@vates/parse-duration": "^0.1.1",
|
||||
"@xen-orchestra/async-map": "^0.1.2",
|
||||
"@xen-orchestra/fs": "^4.1.2",
|
||||
"@xen-orchestra/fs": "^4.1.3",
|
||||
"@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.6.1",
|
||||
"xen-api": "^1.3.6",
|
||||
"vhd-lib": "^4.7.0",
|
||||
"xen-api": "^2.0.0",
|
||||
"yazl": "^2.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -56,7 +56,7 @@
|
||||
"tmp": "^0.2.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@xen-orchestra/xapi": "^3.3.0"
|
||||
"@xen-orchestra/xapi": "^4.0.0"
|
||||
},
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"author": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": false,
|
||||
"name": "@xen-orchestra/cr-seed-cli",
|
||||
"version": "0.2.0",
|
||||
"version": "1.0.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": "^1.3.6"
|
||||
"xen-api": "^2.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"postversion": "npm publish"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": false,
|
||||
"name": "@xen-orchestra/fs",
|
||||
"version": "4.1.2",
|
||||
"version": "4.1.3",
|
||||
"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 { warn } = createLogger('xo:fs:s3')
|
||||
const { debug, info, warn } = createLogger('xo:fs:s3')
|
||||
|
||||
export default class S3Handler extends RemoteHandlerAbstract {
|
||||
#bucket
|
||||
@@ -453,10 +453,18 @@ 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) {
|
||||
if (error.Code !== 'ObjectLockConfigurationNotFoundError' && error.$metadata.httpStatusCode !== 501) {
|
||||
// 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 {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,17 @@
|
||||
|
||||
## **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.5",
|
||||
"version": "0.1.6",
|
||||
"scripts": {
|
||||
"dev": "GIT_HEAD=$(git rev-parse HEAD) vite",
|
||||
"build": "run-p type-check build-only",
|
||||
@@ -10,57 +10,55 @@
|
||||
"test": "yarn run type-check",
|
||||
"type-check": "vue-tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"devDependencies": {
|
||||
"@fontsource/poppins": "^5.0.8",
|
||||
"@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",
|
||||
"@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",
|
||||
"complex-matcher": "^0.7.1",
|
||||
"d3-time-format": "^4.1.0",
|
||||
"decorator-synchronized": "^0.6.0",
|
||||
"echarts": "^5.3.3",
|
||||
"echarts": "^5.4.3",
|
||||
"eslint-plugin-vue": "^9.18.1",
|
||||
"file-saver": "^2.0.5",
|
||||
"highlight.js": "^11.6.0",
|
||||
"human-format": "^1.1.0",
|
||||
"highlight.js": "^11.9.0",
|
||||
"human-format": "^1.2.0",
|
||||
"iterable-backoff": "^0.1.0",
|
||||
"json-rpc-2.0": "^1.3.0",
|
||||
"json5": "^2.2.1",
|
||||
"json-rpc-2.0": "^1.7.0",
|
||||
"json5": "^2.2.3",
|
||||
"limit-concurrency-decorator": "^0.5.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"make-error": "^1.3.6",
|
||||
"marked": "^4.2.12",
|
||||
"pinia": "^2.1.2",
|
||||
"placement.js": "^1.0.0-beta.5",
|
||||
"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",
|
||||
"marked": "^9.1.5",
|
||||
"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"
|
||||
"pinia": "^2.1.7",
|
||||
"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"
|
||||
},
|
||||
"private": true,
|
||||
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/lite",
|
||||
@@ -76,6 +74,6 @@
|
||||
},
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"engines": {
|
||||
"node": ">=8.10"
|
||||
"node": ">=18"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
</RouterLink>
|
||||
<slot />
|
||||
<div class="right">
|
||||
<PoolOverrideWarning as-tooltip />
|
||||
<AccountButton />
|
||||
</div>
|
||||
</header>
|
||||
@@ -19,6 +20,7 @@
|
||||
|
||||
<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";
|
||||
@@ -51,6 +53,10 @@ const { trigger: navigationTrigger } = storeToRefs(navigationStore);
|
||||
margin-left: 1rem;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.warning-not-current-pool {
|
||||
font-size: 2.4rem;
|
||||
}
|
||||
}
|
||||
|
||||
.right {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
<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") }}
|
||||
@@ -45,6 +46,7 @@ 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";
|
||||
|
||||
59
@xen-orchestra/lite/src/components/PoolOverrideWarning.vue
Normal file
59
@xen-orchestra/lite/src/components/PoolOverrideWarning.vue
Normal file
@@ -0,0 +1,59 @@
|
||||
<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,8 +3,14 @@
|
||||
<UiCardTitle>
|
||||
{{ $t("cpu-provisioning") }}
|
||||
<template v-if="!hasError" #right>
|
||||
<!-- TODO: add a tooltip for the warning icon -->
|
||||
<UiStatusIcon v-if="state !== 'success'" :state="state" />
|
||||
<UiStatusIcon
|
||||
v-if="state !== 'success'"
|
||||
v-tooltip="{
|
||||
content: $t('cpu-provisioning-warning'),
|
||||
placement: 'left',
|
||||
}"
|
||||
:state="state"
|
||||
/>
|
||||
</template>
|
||||
</UiCardTitle>
|
||||
<NoDataError v-if="hasError" />
|
||||
@@ -37,11 +43,12 @@ 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 { 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 { vTooltip } from "@/directives/tooltip.directive";
|
||||
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/index";
|
||||
import { useMagicKeys, whenever } from "@vueuse/core";
|
||||
import { inject } from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
v-tooltip="
|
||||
vmRefs.length > 0 &&
|
||||
!isSomeExportable &&
|
||||
$t('no-selected-vm-can-be-exported')
|
||||
$t(isSingleAction ? 'vm-is-running' : 'no-selected-vm-can-be-exported')
|
||||
"
|
||||
:icon="faDisplay"
|
||||
:disabled="isDisabled"
|
||||
@click="openModal"
|
||||
>
|
||||
{{ $t("export-vms") }}
|
||||
{{ $t(isSingleAction ? "export-vm" : "export-vms") }}
|
||||
</MenuItem>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
@@ -26,7 +26,10 @@ import { vTooltip } from "@/directives/tooltip.directive";
|
||||
|
||||
import type { XenApiVm } from "@/libs/xen-api/xen-api.types";
|
||||
|
||||
const props = defineProps<{ vmRefs: XenApiVm["$ref"][] }>();
|
||||
const props = defineProps<{
|
||||
vmRefs: XenApiVm["$ref"][];
|
||||
isSingleAction?: boolean;
|
||||
}>();
|
||||
|
||||
const { getByOpaqueRefs, areSomeOperationAllowed } = useVmCollection();
|
||||
|
||||
|
||||
@@ -3,7 +3,11 @@
|
||||
v-tooltip="
|
||||
selectedRefs.length > 0 &&
|
||||
!isMigratable &&
|
||||
$t('no-selected-vm-can-be-migrated')
|
||||
$t(
|
||||
isSingleAction
|
||||
? 'this-vm-cant-be-migrated'
|
||||
: 'no-selected-vm-can-be-migrated'
|
||||
)
|
||||
"
|
||||
:busy="isMigrating"
|
||||
:disabled="isParentDisabled || !isMigratable"
|
||||
@@ -28,6 +32,7 @@ import { computed } from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
selectedRefs: XenApiVm["$ref"][];
|
||||
isSingleAction?: boolean;
|
||||
}>();
|
||||
|
||||
const { getByOpaqueRefs, isOperationPending, areSomeOperationAllowed } =
|
||||
|
||||
@@ -26,7 +26,9 @@
|
||||
/>
|
||||
</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>
|
||||
@@ -37,9 +39,11 @@ 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, Language } from "highlight.js";
|
||||
import type { HighlightResult } 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,10 +19,6 @@ export const highlight: (
|
||||
ignoreIllegals?: boolean
|
||||
) => HighlightResult = HLJS.highlight;
|
||||
|
||||
export const getLanguage: (
|
||||
languageName: AcceptedLanguage
|
||||
) => Language | undefined = HLJS.getLanguage;
|
||||
|
||||
export type AcceptedLanguage =
|
||||
| "xml"
|
||||
| "css"
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import {
|
||||
type AcceptedLanguage,
|
||||
getLanguage,
|
||||
highlight,
|
||||
} from "@/libs/highlight";
|
||||
import { type AcceptedLanguage, highlight } from "@/libs/highlight";
|
||||
import HLJS from "highlight.js/lib/core";
|
||||
import { marked } from "marked";
|
||||
|
||||
enum VUE_TAG {
|
||||
@@ -11,15 +8,26 @@ 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: string, lang: AcceptedLanguage) {
|
||||
const code = customHighlight(
|
||||
str,
|
||||
Object.values(VUE_TAG).includes(lang as VUE_TAG) || getLanguage(lang)
|
||||
? lang
|
||||
: "plaintext"
|
||||
);
|
||||
code(str, lang) {
|
||||
const code = customHighlight(str, extractLang(lang));
|
||||
return `<pre class="hljs"><button class="copy-button" type="button">Copy</button><code class="hljs-code">${code}</code></pre>`;
|
||||
},
|
||||
},
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
"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",
|
||||
@@ -37,6 +38,7 @@
|
||||
"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",
|
||||
@@ -55,6 +57,7 @@
|
||||
"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",
|
||||
@@ -176,6 +179,7 @@
|
||||
"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",
|
||||
@@ -188,5 +192,6 @@
|
||||
"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,6 +27,7 @@
|
||||
"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é",
|
||||
@@ -37,6 +38,7 @@
|
||||
"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",
|
||||
@@ -55,6 +57,7 @@
|
||||
"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",
|
||||
@@ -176,6 +179,7 @@
|
||||
"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",
|
||||
@@ -188,5 +192,6 @@
|
||||
"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,8 +1,10 @@
|
||||
import XapiStats from "@/libs/xapi-stats";
|
||||
import XenApi from "@/libs/xen-api/xen-api";
|
||||
import { useLocalStorage } from "@vueuse/core";
|
||||
import { useLocalStorage, useSessionStorage, whenever } 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
|
||||
@@ -15,7 +17,27 @@ enum STATUS {
|
||||
}
|
||||
|
||||
export const useXenApiStore = defineStore("xen-api", () => {
|
||||
const xenApi = new XenApi(HOST_URL);
|
||||
// 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 xapiStats = new XapiStats(xenApi);
|
||||
const storedSessionId = useLocalStorage<string | undefined>(
|
||||
"sessionId",
|
||||
@@ -75,14 +97,21 @@ 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,23 +135,15 @@
|
||||
</UiCard>
|
||||
<UiCard class="group">
|
||||
<UiCardTitle>{{ $t("language") }}</UiCardTitle>
|
||||
<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>
|
||||
<FormSelect :before="faEarthAmericas" v-model="$i18n.locale">
|
||||
<option
|
||||
:value="locale"
|
||||
v-for="locale in $i18n.availableLocales"
|
||||
:key="locale"
|
||||
>
|
||||
{{ locales[locale].name ?? locale }}
|
||||
</option>
|
||||
</FormSelect>
|
||||
</UiCard>
|
||||
</div>
|
||||
</template>
|
||||
@@ -174,7 +166,7 @@ import {
|
||||
faGear,
|
||||
faCheck,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import FormWidget from "@/components/FormWidget.vue";
|
||||
import FormSelect from "@/components/form/FormSelect.vue";
|
||||
import TitleBar from "@/components/TitleBar.vue";
|
||||
import UiCard from "@/components/ui/UiCard.vue";
|
||||
import UiKeyValueList from "@/components/ui/UiKeyValueList.vue";
|
||||
@@ -249,8 +241,4 @@ h5 {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.full-length {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.node.json",
|
||||
"extends": ["@tsconfig/node18/tsconfig.json", "@vue/tsconfig/tsconfig.json"],
|
||||
"include": ["vite.config.*", "vitest.config.*", "cypress.config.*"],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.web.json",
|
||||
"extends": "@vue/tsconfig/tsconfig.dom.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.38",
|
||||
"version": "0.26.41",
|
||||
"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.4",
|
||||
"@vates/disposable": "^0.1.5",
|
||||
"@xen-orchestra/async-map": "^0.1.2",
|
||||
"@xen-orchestra/backups": "^0.43.2",
|
||||
"@xen-orchestra/fs": "^4.1.2",
|
||||
"@xen-orchestra/backups": "^0.44.2",
|
||||
"@xen-orchestra/fs": "^4.1.3",
|
||||
"@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": "^3.3.0",
|
||||
"@xen-orchestra/xapi": "^4.0.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": "^1.3.6",
|
||||
"xen-api": "^2.0.0",
|
||||
"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.6"
|
||||
"xo-vmdk-to-vhd": "^2.5.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.0.0",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"license": "ISC",
|
||||
"private": false,
|
||||
"version": "0.3.0",
|
||||
"version": "0.3.1",
|
||||
"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.6.1"
|
||||
"vhd-lib": "^4.7.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@xen-orchestra/xapi",
|
||||
"version": "3.3.0",
|
||||
"version": "4.0.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": "^1.3.6"
|
||||
"xen-api": "^2.0.0"
|
||||
},
|
||||
"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.0",
|
||||
"@vates/nbd-client": "^2.0.1",
|
||||
"@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.6.1",
|
||||
"vhd-lib": "^4.7.0",
|
||||
"xo-common": "^0.8.0"
|
||||
},
|
||||
"private": false,
|
||||
|
||||
@@ -491,7 +491,7 @@ class Vm {
|
||||
exportedVmRef = await this.VM_snapshot(vmRef, { cancelToken, name_label: `[XO Export] ${vm.name_label}` })
|
||||
destroySnapshot = () =>
|
||||
this.VM_destroy(exportedVmRef).catch(error => {
|
||||
warn('VM_export: failed to destroy snapshots', {
|
||||
warn('VM_export: failed to destroy snapshot', {
|
||||
error,
|
||||
snapshotRef: exportedVmRef,
|
||||
vmRef,
|
||||
|
||||
58
CHANGELOG.md
58
CHANGELOG.md
@@ -1,7 +1,61 @@
|
||||
# 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
|
||||
@@ -13,8 +67,6 @@
|
||||
|
||||
## **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))
|
||||
@@ -83,8 +135,6 @@
|
||||
|
||||
## **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,20 +7,13 @@
|
||||
|
||||
> Users must be able to say: “Nice enhancement, I'm eager to test it”
|
||||
|
||||
- [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))
|
||||
- [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))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
> Users must be able to say: “I had this issue, happy to know it's fixed”
|
||||
|
||||
- [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))
|
||||
- [Remotes] Prevents the "connection failed" alert from continuing to appear after successfull connection
|
||||
|
||||
### Packages to release
|
||||
|
||||
@@ -38,14 +31,6 @@
|
||||
|
||||
<!--packages-start-->
|
||||
|
||||
- @vates/nbd-client patch
|
||||
- @xen-orchestra/backups minor
|
||||
- @xen-orchestra/cr-seed-cli major
|
||||
- @xen-orchestra/vmware-explorer patch
|
||||
- xen-api major
|
||||
- xo-server patch
|
||||
- xo-server-netbox minor
|
||||
- xo-vmdk-to-vhd patch
|
||||
- xo-web minor
|
||||
- xo-remote-parser patch
|
||||
|
||||
<!--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
|
||||
apt-get install build-essential redis-server libpng-dev git python3-minimal libvhdi-utils lvm2 cifs-utils nfs-common ntfs-3g
|
||||
```
|
||||
|
||||
On Fedora/CentOS like:
|
||||
|
||||
```sh
|
||||
dnf install redis libpng-devel git libvhdi-tools lvm2 cifs-utils make automake gcc gcc-c++
|
||||
dnf install redis libpng-devel git lvm2 cifs-utils make automake gcc gcc-c++ nfs-utils ntfs-3g
|
||||
```
|
||||
|
||||
### Make sure Redis is running
|
||||
|
||||
@@ -123,7 +123,7 @@ Content-Type: application/x-ndjson
|
||||
|
||||
## Properties update
|
||||
|
||||
> This feature is restricted to `name_label` and `name_description` at the moment.
|
||||
> This feature is restricted to `name_label`, `name_description` and `tags` at the moment.
|
||||
|
||||
```sh
|
||||
curl \
|
||||
@@ -135,6 +135,30 @@ 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:
|
||||
@@ -175,9 +199,45 @@ 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
|
||||
|
||||
A VHD or a raw export can be imported on an SR to create a new VDI at `/rest/v0/srs/<sr uuid>/vdis`.
|
||||
### 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`.
|
||||
|
||||
```sh
|
||||
curl \
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
"node": ">=10"
|
||||
},
|
||||
"dependencies": {
|
||||
"@xen-orchestra/fs": "^4.1.2",
|
||||
"@xen-orchestra/fs": "^4.1.3",
|
||||
"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.6.1"
|
||||
"vhd-lib": "^4.7.0"
|
||||
},
|
||||
"scripts": {
|
||||
"postversion": "npm publish",
|
||||
|
||||
184
packages/vhd-lib/Vhd/VhdNegative.integ.js
Normal file
184
packages/vhd-lib/Vhd/VhdNegative.integ.js
Normal file
@@ -0,0 +1,184 @@
|
||||
'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)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
84
packages/vhd-lib/Vhd/VhdNegative.js
Normal file
84
packages/vhd-lib/Vhd/VhdNegative.js
Normal file
@@ -0,0 +1,84 @@
|
||||
'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,7 +120,8 @@ const VhdSynthetic = class VhdSynthetic extends VhdAbstract {
|
||||
}
|
||||
|
||||
// add decorated static method
|
||||
VhdSynthetic.fromVhdChain = Disposable.factory(async function* fromVhdChain(handler, childPath) {
|
||||
// until is not included in the result , the chain will stop at its child
|
||||
VhdSynthetic.fromVhdChain = Disposable.factory(async function* fromVhdChain(handler, childPath, { until } = {}) {
|
||||
let vhdPath = childPath
|
||||
let vhd
|
||||
const vhds = []
|
||||
@@ -128,8 +129,11 @@ 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)
|
||||
} while (vhd.footer.diskType !== DISK_TYPES.DYNAMIC && vhdPath !== until)
|
||||
|
||||
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,6 +2,10 @@
|
||||
|
||||
const { dirname, resolve } = require('path')
|
||||
|
||||
const resolveRelativeFromFile = (file, path) => resolve('/', dirname(file), path).slice(1)
|
||||
|
||||
const resolveRelativeFromFile = (file, path) => {
|
||||
if (file.startsWith('/')) {
|
||||
return resolve(dirname(file), path)
|
||||
}
|
||||
return resolve('/', dirname(file), path).slice(1)
|
||||
}
|
||||
module.exports = resolveRelativeFromFile
|
||||
|
||||
@@ -13,4 +13,5 @@ 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.6.1",
|
||||
"version": "4.7.0",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"description": "Primitives for VHD file handling",
|
||||
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/packages/vhd-lib",
|
||||
@@ -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.2",
|
||||
"@xen-orchestra/fs": "^4.1.3",
|
||||
"@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.2",
|
||||
"@xen-orchestra/fs": "^4.1.3",
|
||||
"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.1",
|
||||
"version": "0.4.2",
|
||||
"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": "^1.3.6"
|
||||
"xen-api": "^2.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"postversion": "npm publish"
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
```js
|
||||
import { Xapi } from 'xen-api'
|
||||
|
||||
// bare-bones XAPI client
|
||||
const xapi = new Xapi({
|
||||
// URL to a host belonging to the XCP-ng/XenServer pool we want to connect to
|
||||
url: 'https://xen1.company.net',
|
||||
|
||||
// credentials used to connect to this XAPI
|
||||
auth: {
|
||||
user: 'root',
|
||||
password: 'important secret password',
|
||||
},
|
||||
|
||||
// if true, only side-effects free calls will be allowed
|
||||
readOnly: false,
|
||||
})
|
||||
|
||||
// ensure that the connection is working
|
||||
await xapi.checkConnection()
|
||||
|
||||
// call a XAPI method
|
||||
//
|
||||
// see available methods there: https://xapi-project.github.io/xen-api/
|
||||
const result = await xapi.call(
|
||||
// name of the method
|
||||
'VM.snapshot',
|
||||
|
||||
// list of params
|
||||
[vm.$ref, 'My new snapshot'],
|
||||
|
||||
// options
|
||||
{
|
||||
// AbortSignal that can be used to stop the call
|
||||
//
|
||||
// Note: this will not stop/rollback the side-effects of the call
|
||||
signal,
|
||||
}
|
||||
)
|
||||
|
||||
// after a call (or checkConnection) has succeed, the following properties are available
|
||||
|
||||
// list of classes available on this XAPI
|
||||
xapi.classes
|
||||
|
||||
// timestamp of the last reply from XAPI
|
||||
xapi.lastReply
|
||||
|
||||
// pool record of this XAPI
|
||||
xapi.pool
|
||||
|
||||
// secret identifier of the current session
|
||||
//
|
||||
// it might become obsolete, in that case, it will be automatically renewed by the next call
|
||||
xapi.sessionId
|
||||
|
||||
// invalidate the session identifier
|
||||
await xapi.logOut()
|
||||
```
|
||||
|
||||
```js
|
||||
import { Proxy } from 'xen-api/proxy'
|
||||
|
||||
const proxy = new Proxy(xapi)
|
||||
|
||||
await proxy.VM.snapshot()
|
||||
```
|
||||
|
||||
```js
|
||||
import { Events } from 'xen-api/events'
|
||||
|
||||
const events = new Events(xapi)
|
||||
|
||||
// ensure that all events until now have been received and processed
|
||||
await events.barrier()
|
||||
|
||||
// watch events on tasks and wait for a task to finish
|
||||
const task = await events.waitTask(taskRef, { signal })
|
||||
|
||||
// for long running actions, it's better to use an async call which will are based on tasks
|
||||
const result = await events.asyncCall(method)
|
||||
|
||||
const stop = events.watch(
|
||||
// class that we are interested in
|
||||
//
|
||||
// use `*` for all classes
|
||||
'pool',
|
||||
|
||||
// called each time a new event for this class has been received
|
||||
//
|
||||
// https://xapi-project.github.io/xen-api/classes/event.html
|
||||
event => {
|
||||
stop()
|
||||
}
|
||||
)
|
||||
|
||||
// when wanting to really stop watching all events, simply remove all watchers
|
||||
events.clear()
|
||||
```
|
||||
|
||||
```js
|
||||
import { Cache } from 'xen-api/events'
|
||||
|
||||
const cache = new Cache(watcher)
|
||||
|
||||
const host = await cache.get('host', 'OpaqueRef:1c3f19c8-f80a-464d-9c48-a2c19d4e4fc3')
|
||||
|
||||
const vm = await cache.getByUuid('VM', '355ee47d-ff4c-4924-3db2-fd86ae629676')
|
||||
|
||||
cache.clear()
|
||||
```
|
||||
32
packages/xen-api/_Ref.mjs
Normal file
32
packages/xen-api/_Ref.mjs
Normal file
@@ -0,0 +1,32 @@
|
||||
const EMPTY = 'OpaqueRef:NULL'
|
||||
const PREFIX = 'OpaqueRef:'
|
||||
|
||||
export default {
|
||||
// Reference to use to indicate it's not pointing to an object
|
||||
EMPTY,
|
||||
|
||||
// Whether this value is a reference (probably) pointing to an object
|
||||
isNotEmpty(val) {
|
||||
return val !== EMPTY && typeof val === 'string' && val.startsWith(PREFIX)
|
||||
},
|
||||
|
||||
// Whether this value looks like a reference
|
||||
is(val) {
|
||||
return (
|
||||
typeof val === 'string' &&
|
||||
(val.startsWith(PREFIX) ||
|
||||
// 2019-02-07 - JFT: even if `value` should not be an empty string for
|
||||
// a ref property, an user had the case on XenServer 7.0 on the CD VBD
|
||||
// of a VM created by XenCenter
|
||||
val === '' ||
|
||||
// 2021-03-08 - JFT: there is an bug in XCP-ng/XenServer which leads to
|
||||
// some refs to be `Ref:*` instead of being rewritten
|
||||
//
|
||||
// We'll consider them as empty refs in this lib to avoid issues with
|
||||
// _wrapRecord.
|
||||
//
|
||||
// See https://github.com/xapi-project/xen-api/issues/4338
|
||||
val.startsWith('Ref:'))
|
||||
)
|
||||
},
|
||||
}
|
||||
30
packages/xen-api/_XapiError.mjs
Normal file
30
packages/xen-api/_XapiError.mjs
Normal file
@@ -0,0 +1,30 @@
|
||||
import { BaseError } from 'make-error'
|
||||
|
||||
export default class XapiError extends BaseError {
|
||||
static wrap(error) {
|
||||
let code, params
|
||||
if (Array.isArray(error)) {
|
||||
// < XenServer 7.3
|
||||
;[code, ...params] = error
|
||||
} else {
|
||||
code = error.message
|
||||
params = error.data
|
||||
if (!Array.isArray(params)) {
|
||||
params = []
|
||||
}
|
||||
}
|
||||
return new XapiError(code, params)
|
||||
}
|
||||
|
||||
constructor(code, params) {
|
||||
super(`${code}(${params.join(', ')})`)
|
||||
|
||||
this.code = code
|
||||
this.params = params
|
||||
|
||||
// slots than can be assigned later
|
||||
this.call = undefined
|
||||
this.url = undefined
|
||||
this.task = undefined
|
||||
}
|
||||
}
|
||||
3
packages/xen-api/_debug.mjs
Normal file
3
packages/xen-api/_debug.mjs
Normal file
@@ -0,0 +1,3 @@
|
||||
import debug from 'debug'
|
||||
|
||||
export default debug('xen-api')
|
||||
22
packages/xen-api/_getTaskResult.mjs
Normal file
22
packages/xen-api/_getTaskResult.mjs
Normal file
@@ -0,0 +1,22 @@
|
||||
import { Cancel } from 'promise-toolbox'
|
||||
|
||||
import XapiError from './_XapiError.mjs'
|
||||
|
||||
export default task => {
|
||||
const { status } = task
|
||||
if (status === 'cancelled') {
|
||||
return Promise.reject(new Cancel('task canceled'))
|
||||
}
|
||||
if (status === 'failure') {
|
||||
const error = XapiError.wrap(task.error_info)
|
||||
error.task = task
|
||||
return Promise.reject(error)
|
||||
}
|
||||
if (status === 'success') {
|
||||
// the result might be:
|
||||
// - empty string
|
||||
// - an opaque reference
|
||||
// - an XML-RPC value
|
||||
return Promise.resolve(task.result)
|
||||
}
|
||||
}
|
||||
3
packages/xen-api/_isGetAllRecordsMethod.mjs
Normal file
3
packages/xen-api/_isGetAllRecordsMethod.mjs
Normal file
@@ -0,0 +1,3 @@
|
||||
const SUFFIX = '.get_all_records'
|
||||
|
||||
export default method => method.endsWith(SUFFIX)
|
||||
6
packages/xen-api/_isReadOnlyCall.mjs
Normal file
6
packages/xen-api/_isReadOnlyCall.mjs
Normal file
@@ -0,0 +1,6 @@
|
||||
const RE = /^[^.]+\.get_/
|
||||
|
||||
export default function isReadOnlyCall(method, args) {
|
||||
const n = args.length
|
||||
return (n === 0 || (n === 1 && typeof args[0] === 'string')) && RE.test(method)
|
||||
}
|
||||
8
packages/xen-api/_makeCallSetting.mjs
Normal file
8
packages/xen-api/_makeCallSetting.mjs
Normal file
@@ -0,0 +1,8 @@
|
||||
export default (setting, defaultValue) =>
|
||||
setting === undefined
|
||||
? () => defaultValue
|
||||
: typeof setting === 'function'
|
||||
? setting
|
||||
: typeof setting === 'object'
|
||||
? method => setting[method] ?? setting['*'] ?? defaultValue
|
||||
: () => setting
|
||||
26
packages/xen-api/_parseUrl.mjs
Normal file
26
packages/xen-api/_parseUrl.mjs
Normal file
@@ -0,0 +1,26 @@
|
||||
const URL_RE = /^(?:(https?:)\/*)?(?:(([^:]*)(?::([^@]*))?)@)?(\[[^\]]+\]|[^:/]+)(?::([0-9]+))?(\/[^?#]*)?$/
|
||||
|
||||
export default url => {
|
||||
const matches = URL_RE.exec(url)
|
||||
if (matches === null) {
|
||||
throw new Error('invalid URL: ' + url)
|
||||
}
|
||||
|
||||
const [, protocol = 'https:', auth, username = '', password = '', hostname, port, pathname = '/'] = matches
|
||||
const parsedUrl = {
|
||||
protocol,
|
||||
hostname,
|
||||
port,
|
||||
pathname,
|
||||
|
||||
// compat with url.parse
|
||||
auth,
|
||||
}
|
||||
if (username !== '') {
|
||||
parsedUrl.username = decodeURIComponent(username)
|
||||
}
|
||||
if (password !== '') {
|
||||
parsedUrl.password = decodeURIComponent(password)
|
||||
}
|
||||
return parsedUrl
|
||||
}
|
||||
50
packages/xen-api/_parseUrl.spec.mjs
Normal file
50
packages/xen-api/_parseUrl.spec.mjs
Normal file
@@ -0,0 +1,50 @@
|
||||
import t from 'tap'
|
||||
|
||||
import parseUrl from './_parseUrl.mjs'
|
||||
|
||||
const data = {
|
||||
'xcp.company.lan': {
|
||||
hostname: 'xcp.company.lan',
|
||||
pathname: '/',
|
||||
protocol: 'https:',
|
||||
},
|
||||
'[::1]': {
|
||||
hostname: '[::1]',
|
||||
pathname: '/',
|
||||
protocol: 'https:',
|
||||
},
|
||||
'http://username:password@xcp.company.lan': {
|
||||
auth: 'username:password',
|
||||
hostname: 'xcp.company.lan',
|
||||
password: 'password',
|
||||
pathname: '/',
|
||||
protocol: 'http:',
|
||||
username: 'username',
|
||||
},
|
||||
'https://username@xcp.company.lan': {
|
||||
auth: 'username',
|
||||
hostname: 'xcp.company.lan',
|
||||
pathname: '/',
|
||||
protocol: 'https:',
|
||||
username: 'username',
|
||||
},
|
||||
}
|
||||
|
||||
t.test('invalid url', function (t) {
|
||||
t.throws(() => parseUrl(''))
|
||||
t.end()
|
||||
})
|
||||
|
||||
for (const url of Object.keys(data)) {
|
||||
t.test(url, function (t) {
|
||||
const parsed = parseUrl(url)
|
||||
for (const key of Object.keys(parsed)) {
|
||||
if (parsed[key] === undefined) {
|
||||
delete parsed[key]
|
||||
}
|
||||
}
|
||||
|
||||
t.same(parsed, data[url])
|
||||
t.end()
|
||||
})
|
||||
}
|
||||
17
packages/xen-api/_replaceSensitiveValues.mjs
Normal file
17
packages/xen-api/_replaceSensitiveValues.mjs
Normal file
@@ -0,0 +1,17 @@
|
||||
import mapValues from 'lodash/mapValues.js'
|
||||
|
||||
export default function replaceSensitiveValues(value, replacement) {
|
||||
function helper(value, name) {
|
||||
if (name === 'password' && typeof value === 'string') {
|
||||
return replacement
|
||||
}
|
||||
|
||||
if (typeof value !== 'object' || value === null) {
|
||||
return value
|
||||
}
|
||||
|
||||
return Array.isArray(value) ? value.map(helper) : mapValues(value, helper)
|
||||
}
|
||||
|
||||
return helper(value)
|
||||
}
|
||||
130
packages/xen-api/cli-lib.mjs
Normal file
130
packages/xen-api/cli-lib.mjs
Normal file
@@ -0,0 +1,130 @@
|
||||
/* eslint-disable no-console */
|
||||
import blocked from 'blocked'
|
||||
import createDebug from 'debug'
|
||||
import filter from 'lodash/filter.js'
|
||||
import find from 'lodash/find.js'
|
||||
import L from 'lodash'
|
||||
import minimist from 'minimist'
|
||||
import pw from 'pw'
|
||||
import { asCallback, fromCallback, fromEvent } from 'promise-toolbox'
|
||||
import { diff } from 'jest-diff'
|
||||
import { getBoundPropertyDescriptor } from 'bind-property-descriptor'
|
||||
import { start as createRepl } from 'repl'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
function askPassword(prompt = 'Password: ') {
|
||||
if (prompt) {
|
||||
process.stdout.write(prompt)
|
||||
}
|
||||
|
||||
return new Promise(resolve => {
|
||||
pw(resolve)
|
||||
})
|
||||
}
|
||||
|
||||
const { getPrototypeOf, ownKeys } = Reflect
|
||||
function getAllBoundDescriptors(object) {
|
||||
const descriptors = { __proto__: null }
|
||||
let current = object
|
||||
do {
|
||||
ownKeys(current).forEach(key => {
|
||||
if (!(key in descriptors)) {
|
||||
descriptors[key] = getBoundPropertyDescriptor(current, key, object)
|
||||
}
|
||||
})
|
||||
} while ((current = getPrototypeOf(current)) !== null)
|
||||
return descriptors
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const usage = 'Usage: xen-api <url> [<user> [<password>]]'
|
||||
|
||||
export async function main(createClient) {
|
||||
const opts = minimist(process.argv.slice(2), {
|
||||
string: ['proxy', 'session-id', 'transport'],
|
||||
boolean: ['allow-unauthorized', 'help', 'read-only', 'verbose'],
|
||||
|
||||
alias: {
|
||||
'allow-unauthorized': 'au',
|
||||
debounce: 'd',
|
||||
help: 'h',
|
||||
proxy: 'p',
|
||||
'read-only': 'ro',
|
||||
verbose: 'v',
|
||||
transport: 't',
|
||||
},
|
||||
})
|
||||
|
||||
if (opts.help) {
|
||||
return usage
|
||||
}
|
||||
|
||||
if (opts.verbose) {
|
||||
// Does not work perfectly.
|
||||
//
|
||||
// https://github.com/visionmedia/debug/pull/156
|
||||
createDebug.enable('xen-api,xen-api:*')
|
||||
}
|
||||
|
||||
let auth
|
||||
if (opts._.length > 1) {
|
||||
const [, user, password = await askPassword()] = opts._
|
||||
auth = { user, password }
|
||||
} else if (opts['session-id'] !== undefined) {
|
||||
auth = { sessionId: opts['session-id'] }
|
||||
}
|
||||
|
||||
{
|
||||
const debug = createDebug('xen-api:perf')
|
||||
blocked(ms => {
|
||||
debug('blocked for %sms', ms | 0)
|
||||
})
|
||||
}
|
||||
|
||||
const xapi = createClient({
|
||||
url: opts._[0],
|
||||
allowUnauthorized: opts.au,
|
||||
auth,
|
||||
debounce: opts.debounce != null ? +opts.debounce : null,
|
||||
httpProxy: opts.proxy,
|
||||
readOnly: opts.ro,
|
||||
syncStackTraces: true,
|
||||
transport: opts.transport || undefined,
|
||||
})
|
||||
await xapi.connect()
|
||||
|
||||
const repl = createRepl({
|
||||
prompt: `${xapi._humanId}> `,
|
||||
})
|
||||
|
||||
{
|
||||
const ctx = repl.context
|
||||
ctx.xapi = xapi
|
||||
|
||||
ctx.diff = (a, b) => console.log('%s', diff(a, b))
|
||||
ctx.find = predicate => find(xapi.objects.all, predicate)
|
||||
ctx.findAll = predicate => filter(xapi.objects.all, predicate)
|
||||
ctx.L = L
|
||||
|
||||
Object.defineProperties(ctx, getAllBoundDescriptors(xapi))
|
||||
}
|
||||
|
||||
// Make the REPL waits for promise completion.
|
||||
repl.eval = (evaluate => (cmd, context, filename, cb) => {
|
||||
asCallback.call(
|
||||
fromCallback(cb => {
|
||||
evaluate.call(repl, cmd, context, filename, cb)
|
||||
}).then(value => (Array.isArray(value) ? Promise.all(value) : value)),
|
||||
cb
|
||||
)
|
||||
})(repl.eval)
|
||||
|
||||
await fromEvent(repl, 'exit')
|
||||
|
||||
try {
|
||||
await xapi.disconnect()
|
||||
} catch (error) {}
|
||||
}
|
||||
/* eslint-enable no-console */
|
||||
6
packages/xen-api/cli.mjs
Executable file
6
packages/xen-api/cli.mjs
Executable file
@@ -0,0 +1,6 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { createClient } from './index.mjs'
|
||||
import { main } from './cli-lib.mjs'
|
||||
|
||||
main(createClient).catch(console.error.bind(console, 'FATAL'))
|
||||
@@ -1,115 +0,0 @@
|
||||
const EVENT_TIMEOUT = 60e3
|
||||
|
||||
export class Watcher {
|
||||
#abortController
|
||||
#typeWatchers = new Map()
|
||||
classes = new Map()
|
||||
xapi
|
||||
|
||||
constructor(xapi) {
|
||||
this.xapi = xapi
|
||||
}
|
||||
|
||||
async asyncCall(method, params, { signal }) {
|
||||
const taskRef = await this.xapi.call('Async.' + method, params, { signal })
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const stop = this.watch(
|
||||
'task',
|
||||
taskRef,
|
||||
task => {
|
||||
const { status } = task
|
||||
if (status === 'success') {
|
||||
stop()
|
||||
resolve(task.status)
|
||||
} else if (status === 'cancelled' || status === 'failure') {
|
||||
stop()
|
||||
reject(task.error_info)
|
||||
}
|
||||
},
|
||||
{ signal }
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
async #start() {
|
||||
const { xapi } = this
|
||||
const { signal } = this.#abortController
|
||||
const watchers = this.#typeWatchers
|
||||
|
||||
let token = await xapi.call('event.inject', 'pool', xapi.pool.$ref)
|
||||
|
||||
while (true) {
|
||||
signal.throwIfRequested()
|
||||
|
||||
const result = await xapi.call({ signal }, 'event.from', this.classes, token, EVENT_TIMEOUT)
|
||||
|
||||
for (const event of result.events) {
|
||||
}
|
||||
}
|
||||
this.#abortController = undefined
|
||||
}
|
||||
|
||||
start() {
|
||||
if (this.#abortController !== undefined) {
|
||||
throw new Error('already started')
|
||||
}
|
||||
|
||||
this.#abortController = new AbortController()
|
||||
this.#start()
|
||||
}
|
||||
|
||||
stop() {
|
||||
if (this.#abortController === undefined) {
|
||||
throw new Error('already stopped')
|
||||
}
|
||||
|
||||
this.#abortController.abort()
|
||||
}
|
||||
}
|
||||
|
||||
export class Cache {
|
||||
// contains records indexed by type + ref
|
||||
//
|
||||
// plain records when retrieved by events
|
||||
//
|
||||
// promises to record when retrieved by a get_record call (might be a rejection if the record does not exist)
|
||||
#recordCache = new Map()
|
||||
#watcher
|
||||
|
||||
constructor(watcher) {
|
||||
this.#watcher = watcher
|
||||
}
|
||||
|
||||
async #get(type, ref) {
|
||||
let record
|
||||
try {
|
||||
record = await this.#watcher.xapi.call(`${type}.get_record`, ref)
|
||||
} catch (error) {
|
||||
if (error.code !== 'HANDLE_INVALID') {
|
||||
throw error
|
||||
}
|
||||
record = Promise.reject(error)
|
||||
}
|
||||
this.#recordCache.set(type, Promise.resolve(record))
|
||||
return record
|
||||
}
|
||||
|
||||
async get(type, ref) {
|
||||
const cache = this.#recordCache
|
||||
const key = type + ref
|
||||
|
||||
let record = cache.get(key)
|
||||
if (record === undefined) {
|
||||
record = this.#get(type, ref)
|
||||
cache.set(key, record)
|
||||
}
|
||||
|
||||
return record
|
||||
}
|
||||
|
||||
async getByUuid(type, uuid) {
|
||||
return this.get(type, await this.#watcher.xapi.call(`${type}.get_by_uuid`, uuid))
|
||||
}
|
||||
}
|
||||
exports.Cache = Cache
|
||||
5
packages/xen-api/examples/.eslintrc.js
Normal file
5
packages/xen-api/examples/.eslintrc.js
Normal file
@@ -0,0 +1,5 @@
|
||||
'use strict'
|
||||
|
||||
module.exports = {
|
||||
ignorePatterns: ['*'],
|
||||
}
|
||||
3
packages/xen-api/examples/env.mjs
Normal file
3
packages/xen-api/examples/env.mjs
Normal file
@@ -0,0 +1,3 @@
|
||||
if (process.env.DEBUG === undefined) {
|
||||
process.env.DEBUG = 'xen-api'
|
||||
}
|
||||
67
packages/xen-api/examples/export-vdi.mjs
Executable file
67
packages/xen-api/examples/export-vdi.mjs
Executable file
@@ -0,0 +1,67 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import './env.mjs'
|
||||
|
||||
import createProgress from 'progress-stream'
|
||||
import createTop from 'process-top'
|
||||
import getopts from 'getopts'
|
||||
import { defer } from 'golike-defer'
|
||||
import { CancelToken } from 'promise-toolbox'
|
||||
|
||||
import { createClient } from '../index.mjs'
|
||||
|
||||
import { createOutputStream, formatProgress, pipeline, resolveRecord, throttle } from './utils.mjs'
|
||||
|
||||
defer(async ($defer, rawArgs) => {
|
||||
const {
|
||||
raw,
|
||||
throttle: bps,
|
||||
_: args,
|
||||
} = getopts(rawArgs, {
|
||||
boolean: 'raw',
|
||||
alias: {
|
||||
raw: 'r',
|
||||
throttle: 't',
|
||||
},
|
||||
})
|
||||
|
||||
if (args.length < 2) {
|
||||
return console.log('Usage: export-vdi [--raw] <XS URL> <VDI identifier> [<VHD file>]')
|
||||
}
|
||||
|
||||
const xapi = createClient({
|
||||
allowUnauthorized: true,
|
||||
url: args[0],
|
||||
watchEvents: false,
|
||||
})
|
||||
|
||||
await xapi.connect()
|
||||
$defer(() => xapi.disconnect())
|
||||
|
||||
const { cancel, token } = CancelToken.source()
|
||||
process.on('SIGINT', cancel)
|
||||
|
||||
const vdi = await resolveRecord(xapi, 'VDI', args[1])
|
||||
|
||||
// https://xapi-project.github.io/xen-api/snapshots.html#downloading-a-disk-or-snapshot
|
||||
const exportStream = await xapi.getResource(token, '/export_raw_vdi/', {
|
||||
query: {
|
||||
format: raw ? 'raw' : 'vhd',
|
||||
vdi: vdi.$ref,
|
||||
},
|
||||
})
|
||||
|
||||
console.warn('Export task:', exportStream.headers['task-id'])
|
||||
|
||||
const top = createTop()
|
||||
const progressStream = createProgress()
|
||||
|
||||
$defer(
|
||||
clearInterval,
|
||||
setInterval(() => {
|
||||
console.warn('\r %s | %s', top.toString(), formatProgress(progressStream.progress()))
|
||||
}, 1e3)
|
||||
)
|
||||
|
||||
await pipeline(exportStream, progressStream, throttle(bps), createOutputStream(args[2]))
|
||||
})(process.argv.slice(2)).catch(console.error.bind(console, 'error'))
|
||||
54
packages/xen-api/examples/export-vm.mjs
Executable file
54
packages/xen-api/examples/export-vm.mjs
Executable file
@@ -0,0 +1,54 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import './env.mjs'
|
||||
|
||||
import createProgress from 'progress-stream'
|
||||
import getopts from 'getopts'
|
||||
import { defer } from 'golike-defer'
|
||||
import { CancelToken } from 'promise-toolbox'
|
||||
|
||||
import { createClient } from '../index.mjs'
|
||||
|
||||
import { createOutputStream, formatProgress, pipeline, resolveRecord } from './utils.mjs'
|
||||
|
||||
defer(async ($defer, rawArgs) => {
|
||||
const {
|
||||
gzip,
|
||||
zstd,
|
||||
_: args,
|
||||
} = getopts(rawArgs, {
|
||||
boolean: ['gzip', 'zstd'],
|
||||
})
|
||||
|
||||
if (args.length < 2) {
|
||||
return console.log('Usage: export-vm <XS URL> <VM identifier> [<XVA file>]')
|
||||
}
|
||||
|
||||
const xapi = createClient({
|
||||
allowUnauthorized: true,
|
||||
url: args[0],
|
||||
watchEvents: false,
|
||||
})
|
||||
|
||||
await xapi.connect()
|
||||
$defer(() => xapi.disconnect())
|
||||
|
||||
const { cancel, token } = CancelToken.source()
|
||||
process.on('SIGINT', cancel)
|
||||
|
||||
// https://xapi-project.github.io/xen-api/importexport.html
|
||||
const exportStream = await xapi.getResource(token, '/export/', {
|
||||
query: {
|
||||
ref: (await resolveRecord(xapi, 'VM', args[1])).$ref,
|
||||
use_compression: zstd ? 'zstd' : gzip ? 'true' : 'false',
|
||||
},
|
||||
})
|
||||
|
||||
console.warn('Export task:', exportStream.headers['task-id'])
|
||||
|
||||
await pipeline(
|
||||
exportStream,
|
||||
createProgress({ time: 1e3 }, p => console.warn(formatProgress(p))),
|
||||
createOutputStream(args[2])
|
||||
)
|
||||
})(process.argv.slice(2)).catch(console.error.bind(console, 'error'))
|
||||
88
packages/xen-api/examples/import-vdi.mjs
Executable file
88
packages/xen-api/examples/import-vdi.mjs
Executable file
@@ -0,0 +1,88 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import './env.mjs'
|
||||
|
||||
import getopts from 'getopts'
|
||||
import { defer } from 'golike-defer'
|
||||
import { CancelToken } from 'promise-toolbox'
|
||||
import { createVhdStreamWithLength } from 'vhd-lib'
|
||||
|
||||
import { createClient } from '../index.mjs'
|
||||
|
||||
import { createInputStream, resolveRef } from './utils.mjs'
|
||||
|
||||
defer(async ($defer, argv) => {
|
||||
const opts = getopts(argv, { boolean: ['events', 'raw', 'remove-length'], string: ['sr', 'vdi'] })
|
||||
|
||||
const url = opts._[0]
|
||||
|
||||
if (url === undefined) {
|
||||
return console.log(
|
||||
'Usage: import-vdi [--events] [--raw] [--sr <SR identifier>] [--vdi <VDI identifier>] <XS URL> [<VHD file>]'
|
||||
)
|
||||
}
|
||||
|
||||
const { raw, sr, vdi } = opts
|
||||
|
||||
const createVdi = vdi === ''
|
||||
if (createVdi) {
|
||||
if (sr === '') {
|
||||
throw 'requires either --vdi or --sr'
|
||||
}
|
||||
if (!raw) {
|
||||
throw 'creating a VDI requires --raw'
|
||||
}
|
||||
} else if (sr !== '') {
|
||||
throw '--vdi and --sr are mutually exclusive'
|
||||
}
|
||||
|
||||
const xapi = createClient({
|
||||
allowUnauthorized: true,
|
||||
url,
|
||||
watchEvents: opts.events && ['task'],
|
||||
})
|
||||
|
||||
await xapi.connect()
|
||||
$defer(() => xapi.disconnect())
|
||||
|
||||
const { cancel, token } = CancelToken.source()
|
||||
process.on('SIGINT', cancel)
|
||||
|
||||
let input = createInputStream(opts._[1])
|
||||
$defer.onFailure(() => input.destroy())
|
||||
|
||||
let vdiRef
|
||||
if (createVdi) {
|
||||
vdiRef = await xapi.call('VDI.create', {
|
||||
name_label: 'xen-api/import-vdi',
|
||||
other_config: {},
|
||||
read_only: false,
|
||||
sharable: false,
|
||||
SR: await resolveRef(xapi, 'SR', sr),
|
||||
type: 'user',
|
||||
virtual_size: input.length,
|
||||
})
|
||||
$defer.onFailure(() => xapi.call('VDI.destroy', vdiRef))
|
||||
} else {
|
||||
vdiRef = await resolveRef(xapi, 'VDI', vdi)
|
||||
}
|
||||
|
||||
if (opts['remove-length']) {
|
||||
delete input.length
|
||||
console.log('length removed')
|
||||
} else if (!raw && input.length === undefined) {
|
||||
input = await createVhdStreamWithLength(input)
|
||||
}
|
||||
|
||||
// https://xapi-project.github.io/xen-api/snapshots.html#uploading-a-disk-or-snapshot
|
||||
const result = await xapi.putResource(token, input, '/import_raw_vdi/', {
|
||||
query: {
|
||||
format: raw ? 'raw' : 'vhd',
|
||||
vdi: vdiRef,
|
||||
},
|
||||
})
|
||||
|
||||
if (result !== undefined) {
|
||||
console.log(result)
|
||||
}
|
||||
})(process.argv.slice(2)).catch(console.error.bind(console, 'Fatal:'))
|
||||
33
packages/xen-api/examples/import-vm.mjs
Executable file
33
packages/xen-api/examples/import-vm.mjs
Executable file
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import './env.mjs'
|
||||
|
||||
import { defer } from 'golike-defer'
|
||||
import { CancelToken } from 'promise-toolbox'
|
||||
|
||||
import { createClient } from '../index.mjs'
|
||||
|
||||
import { createInputStream, resolveRef } from './utils.mjs'
|
||||
|
||||
defer(async ($defer, args) => {
|
||||
if (args.length < 1) {
|
||||
return console.log('Usage: import-vm <XS URL> [<XVA file>] [<SR identifier>]')
|
||||
}
|
||||
|
||||
const xapi = createClient({
|
||||
allowUnauthorized: true,
|
||||
url: args[0],
|
||||
watchEvents: false,
|
||||
})
|
||||
|
||||
await xapi.connect()
|
||||
$defer(() => xapi.disconnect())
|
||||
|
||||
const { cancel, token } = CancelToken.source()
|
||||
process.on('SIGINT', cancel)
|
||||
|
||||
// https://xapi-project.github.io/xen-api/importexport.html
|
||||
await xapi.putResource(token, createInputStream(args[1]), '/import/', {
|
||||
query: args[2] && { sr_id: await resolveRef(xapi, 'SR', args[2]) },
|
||||
})
|
||||
})(process.argv.slice(2)).catch(console.error.bind(console, 'error'))
|
||||
59
packages/xen-api/examples/log-events.mjs
Executable file
59
packages/xen-api/examples/log-events.mjs
Executable file
@@ -0,0 +1,59 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import 'source-map-support/register.js'
|
||||
|
||||
import forEach from 'lodash/forEach.js'
|
||||
import size from 'lodash/size.js'
|
||||
|
||||
import { createClient } from '../index.mjs'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
if (process.argv.length < 3) {
|
||||
throw new Error('Usage: log-events <XS URL>')
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// Creation
|
||||
|
||||
const xapi = createClient({
|
||||
allowUnauthorized: true,
|
||||
url: process.argv[2],
|
||||
})
|
||||
|
||||
// ===================================================================
|
||||
// Method call
|
||||
|
||||
xapi.connect().then(() => {
|
||||
xapi
|
||||
.call('VM.get_all_records')
|
||||
.then(function (vms) {
|
||||
console.log('%s VMs fetched', size(vms))
|
||||
})
|
||||
.catch(function (error) {
|
||||
console.error(error)
|
||||
})
|
||||
})
|
||||
|
||||
// ===================================================================
|
||||
// Objects
|
||||
|
||||
const objects = xapi.objects
|
||||
|
||||
objects.on('add', objects => {
|
||||
forEach(objects, object => {
|
||||
console.log('+ %s: %s', object.$type, object.$id)
|
||||
})
|
||||
})
|
||||
|
||||
objects.on('update', objects => {
|
||||
forEach(objects, object => {
|
||||
console.log('± %s: %s', object.$type, object.$id)
|
||||
})
|
||||
})
|
||||
|
||||
objects.on('remove', objects => {
|
||||
forEach(objects, (value, id) => {
|
||||
console.log('- %s', id)
|
||||
})
|
||||
})
|
||||
2647
packages/xen-api/examples/package-lock.json
generated
Normal file
2647
packages/xen-api/examples/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
15
packages/xen-api/examples/package.json
Normal file
15
packages/xen-api/examples/package.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"getopts": "^2.2.3",
|
||||
"golike-defer": "^0.5.1",
|
||||
"human-format": "^0.11.0",
|
||||
"lodash": "^4.17.21",
|
||||
"process-top": "^1.2.0",
|
||||
"progress-stream": "^2.0.0",
|
||||
"promise-toolbox": "^0.19.2",
|
||||
"readable-stream": "^4.4.2",
|
||||
"source-map-support": "^0.5.21",
|
||||
"throttle": "^1.0.3",
|
||||
"vhd-lib": "^4.7.0"
|
||||
}
|
||||
}
|
||||
75
packages/xen-api/examples/utils.mjs
Normal file
75
packages/xen-api/examples/utils.mjs
Normal file
@@ -0,0 +1,75 @@
|
||||
import { createReadStream, createWriteStream, statSync } from 'fs'
|
||||
import { fromCallback } from 'promise-toolbox'
|
||||
import { PassThrough, pipeline as Pipeline } from 'readable-stream'
|
||||
import humanFormat from 'human-format'
|
||||
import Throttle from 'throttle'
|
||||
|
||||
import Ref from '../_Ref.mjs'
|
||||
|
||||
export const createInputStream = path => {
|
||||
if (path === undefined || path === '-') {
|
||||
return process.stdin
|
||||
}
|
||||
|
||||
const { size } = statSync(path)
|
||||
|
||||
const stream = createReadStream(path)
|
||||
stream.length = size
|
||||
return stream
|
||||
}
|
||||
|
||||
export const createOutputStream = path => {
|
||||
if (path !== undefined && path !== '-') {
|
||||
return createWriteStream(path)
|
||||
}
|
||||
|
||||
// introduce a through stream because stdout is not a normal stream!
|
||||
const stream = new PassThrough()
|
||||
stream.pipe(process.stdout)
|
||||
return stream
|
||||
}
|
||||
|
||||
const formatSizeOpts = { scale: 'binary', unit: 'B' }
|
||||
const formatSize = bytes => humanFormat(bytes, formatSizeOpts)
|
||||
|
||||
export const formatProgress = p => {
|
||||
return [
|
||||
formatSize(p.transferred),
|
||||
' / ',
|
||||
formatSize(p.length),
|
||||
' | ',
|
||||
p.runtime,
|
||||
's / ',
|
||||
p.eta,
|
||||
's | ',
|
||||
formatSize(p.speed),
|
||||
'/s',
|
||||
].join('')
|
||||
}
|
||||
|
||||
export const pipeline = (...streams) => {
|
||||
return fromCallback(cb => {
|
||||
streams = streams.filter(_ => _ != null)
|
||||
streams.push(cb)
|
||||
Pipeline.apply(undefined, streams)
|
||||
})
|
||||
}
|
||||
|
||||
const resolveRef = (xapi, type, refOrUuidOrNameLabel) =>
|
||||
Ref.is(refOrUuidOrNameLabel)
|
||||
? refOrUuidOrNameLabel
|
||||
: xapi.call(`${type}.get_by_uuid`, refOrUuidOrNameLabel).catch(() =>
|
||||
xapi.call(`${type}.get_by_name_label`, refOrUuidOrNameLabel).then(refs => {
|
||||
if (refs.length === 1) {
|
||||
return refs[0]
|
||||
}
|
||||
throw new Error(`no single match for ${type} with name label ${refOrUuidOrNameLabel}`)
|
||||
})
|
||||
)
|
||||
|
||||
export const resolveRecord = async (xapi, type, refOrUuidOrNameLabel) =>
|
||||
xapi.getRecord(type, await resolveRef(xapi, type, refOrUuidOrNameLabel))
|
||||
|
||||
export { resolveRef }
|
||||
|
||||
export const throttle = opts => (opts != null ? new Throttle(opts) : undefined)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": false,
|
||||
"name": "xen-api",
|
||||
"version": "1.3.6",
|
||||
"version": "2.0.0",
|
||||
"license": "ISC",
|
||||
"description": "Connector to the Xen API",
|
||||
"keywords": [
|
||||
|
||||
3
packages/xen-api/transports/_UnsupportedTransport.mjs
Normal file
3
packages/xen-api/transports/_UnsupportedTransport.mjs
Normal file
@@ -0,0 +1,3 @@
|
||||
import makeError from 'make-error'
|
||||
|
||||
export default makeError('UnsupportedTransport')
|
||||
25
packages/xen-api/transports/_prepareXmlRpcParams.mjs
Normal file
25
packages/xen-api/transports/_prepareXmlRpcParams.mjs
Normal file
@@ -0,0 +1,25 @@
|
||||
// Prepare values before passing them to the XenAPI:
|
||||
//
|
||||
// - cast integers to strings
|
||||
export default function prepare(param) {
|
||||
if (Number.isInteger(param)) {
|
||||
return String(param)
|
||||
}
|
||||
|
||||
if (typeof param !== 'object' || param === null) {
|
||||
return param
|
||||
}
|
||||
|
||||
if (Array.isArray(param)) {
|
||||
return param.map(prepare)
|
||||
}
|
||||
|
||||
const values = {}
|
||||
Object.keys(param).forEach(key => {
|
||||
const value = param[key]
|
||||
if (value !== undefined) {
|
||||
values[key] = prepare(value)
|
||||
}
|
||||
})
|
||||
return values
|
||||
}
|
||||
35
packages/xen-api/transports/auto.mjs
Normal file
35
packages/xen-api/transports/auto.mjs
Normal file
@@ -0,0 +1,35 @@
|
||||
import jsonRpc from './json-rpc.mjs'
|
||||
import UnsupportedTransport from './_UnsupportedTransport.mjs'
|
||||
import xmlRpc from './xml-rpc.mjs'
|
||||
|
||||
const factories = [jsonRpc, xmlRpc]
|
||||
const { length } = factories
|
||||
|
||||
export default opts => {
|
||||
let i = 0
|
||||
|
||||
let call
|
||||
function create() {
|
||||
const current = factories[i++](opts)
|
||||
if (i < length) {
|
||||
const currentI = i
|
||||
call = (method, args) =>
|
||||
current(method, args).catch(error => {
|
||||
if (error instanceof UnsupportedTransport) {
|
||||
if (currentI === i) {
|
||||
// not changed yet
|
||||
create()
|
||||
}
|
||||
return call(method, args)
|
||||
}
|
||||
|
||||
throw error
|
||||
})
|
||||
} else {
|
||||
call = current
|
||||
}
|
||||
}
|
||||
create()
|
||||
|
||||
return (method, args) => call(method, args)
|
||||
}
|
||||
11
packages/xen-api/transports/index.mjs
Normal file
11
packages/xen-api/transports/index.mjs
Normal file
@@ -0,0 +1,11 @@
|
||||
import auto from './auto.mjs'
|
||||
import jsonRpc from './json-rpc.mjs'
|
||||
import xmlRpc from './xml-rpc.mjs'
|
||||
|
||||
export default {
|
||||
__proto__: null,
|
||||
|
||||
auto,
|
||||
'json-rpc': jsonRpc,
|
||||
'xml-rpc': xmlRpc,
|
||||
}
|
||||
37
packages/xen-api/transports/json-rpc.mjs
Normal file
37
packages/xen-api/transports/json-rpc.mjs
Normal file
@@ -0,0 +1,37 @@
|
||||
import httpRequestPlus from 'http-request-plus'
|
||||
import { format, parse } from 'json-rpc-protocol'
|
||||
|
||||
import XapiError from '../_XapiError.mjs'
|
||||
|
||||
import UnsupportedTransport from './_UnsupportedTransport.mjs'
|
||||
|
||||
// https://github.com/xenserver/xenadmin/blob/0df39a9d83cd82713f32d24704852a0fd57b8a64/XenModel/XenAPI/Session.cs#L403-L433
|
||||
export default ({ secureOptions, url, agent }) => {
|
||||
url = new URL('./jsonrpc', Object.assign(new URL('http://localhost'), url))
|
||||
|
||||
return async function (method, args) {
|
||||
const res = await httpRequestPlus(url, {
|
||||
...secureOptions,
|
||||
body: format.request(0, method, args),
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
method: 'POST',
|
||||
agent,
|
||||
})
|
||||
|
||||
// content-type is `text/xml` on old hosts where JSON-RPC is unsupported
|
||||
if (res.headers['content-type'] !== 'application/json') {
|
||||
throw new UnsupportedTransport()
|
||||
}
|
||||
|
||||
const response = parse(await res.text())
|
||||
|
||||
if (response.type === 'response') {
|
||||
return response.result
|
||||
}
|
||||
|
||||
throw XapiError.wrap(response.error)
|
||||
}
|
||||
}
|
||||
45
packages/xen-api/transports/xml-rpc.mjs
Normal file
45
packages/xen-api/transports/xml-rpc.mjs
Normal file
@@ -0,0 +1,45 @@
|
||||
import xmlrpc from 'xmlrpc'
|
||||
import { promisify } from 'promise-toolbox'
|
||||
|
||||
import XapiError from '../_XapiError.mjs'
|
||||
|
||||
import prepareXmlRpcParams from './_prepareXmlRpcParams.mjs'
|
||||
|
||||
const logError = error => {
|
||||
if (error.res) {
|
||||
console.error('XML-RPC Error: %s (response status %s)', error.message, error.res.statusCode)
|
||||
console.error('%s', error.body)
|
||||
}
|
||||
|
||||
throw error
|
||||
}
|
||||
|
||||
const parseResult = result => {
|
||||
const status = result.Status
|
||||
|
||||
// Return the plain result if it does not have a valid XAPI
|
||||
// format.
|
||||
if (status === undefined) {
|
||||
return result
|
||||
}
|
||||
|
||||
if (status !== 'Success') {
|
||||
throw XapiError.wrap(result.ErrorDescription)
|
||||
}
|
||||
|
||||
return result.Value
|
||||
}
|
||||
|
||||
export default ({ secureOptions, url: { hostnameRaw, pathname, port, protocol }, agent }) => {
|
||||
const secure = protocol === 'https:'
|
||||
const client = (secure ? xmlrpc.createSecureClient : xmlrpc.createClient)({
|
||||
...(secure ? secureOptions : undefined),
|
||||
agent,
|
||||
host: hostnameRaw,
|
||||
pathname,
|
||||
port,
|
||||
})
|
||||
const call = promisify(client.methodCall, client)
|
||||
|
||||
return (method, args) => call(method, prepareXmlRpcParams(args)).then(parseResult, logError)
|
||||
}
|
||||
@@ -50,6 +50,7 @@ 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.
|
||||
@@ -122,6 +123,18 @@ 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,6 +68,7 @@ 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.
|
||||
@@ -140,6 +141,18 @@ 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,6 +274,7 @@ 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.
|
||||
@@ -347,6 +348,18 @@ 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.21.0",
|
||||
"version": "0.22.0",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"description": "Basic CLI for Xen-Orchestra",
|
||||
"keywords": [
|
||||
|
||||
@@ -112,6 +112,18 @@ 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,6 +21,7 @@
|
||||
"node": ">=6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@xen-orchestra/log": "^0.6.0",
|
||||
"lodash": "^4.13.1",
|
||||
"url-parse": "^1.4.7"
|
||||
},
|
||||
|
||||
@@ -4,6 +4,9 @@ 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([^?]*))?(\?[^?]*)?$/
|
||||
@@ -15,6 +18,17 @@ 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.3.3",
|
||||
"version": "1.4.0",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"description": "Synchronizes pools managed by Xen Orchestra with Netbox",
|
||||
"keywords": [
|
||||
|
||||
@@ -381,6 +381,7 @@ class Netbox {
|
||||
await this.#request(`/virtualization/clusters/?type_id=${nbClusterType.id}`),
|
||||
'custom_fields.uuid'
|
||||
)
|
||||
delete allNbClusters.null
|
||||
const nbClusters = pick(allNbClusters, xoPools)
|
||||
|
||||
const clustersToCreate = []
|
||||
@@ -549,7 +550,9 @@ 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
|
||||
@@ -667,6 +670,7 @@ 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 = []
|
||||
@@ -910,7 +914,10 @@ class Netbox {
|
||||
}
|
||||
}
|
||||
|
||||
Object.assign(nbVms, keyBy(await this.#request('/virtualization/virtual-machines/', 'PATCH', vmsToUpdate2)))
|
||||
Object.assign(
|
||||
nbVms,
|
||||
keyBy(await this.#request('/virtualization/virtual-machines/', 'PATCH', vmsToUpdate2), 'custom_fields.uuid')
|
||||
)
|
||||
|
||||
log.info(`Done synchronizing ${xoPools.length} pools with Netbox`, { pools: xoPools })
|
||||
}
|
||||
|
||||
@@ -76,6 +76,8 @@ 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` and `name_description` at the moment.
|
||||
> This feature is restricted to `name_label`, `name_description` and `tags` at the moment.
|
||||
|
||||
```sh
|
||||
curl \
|
||||
@@ -131,6 +131,30 @@ 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
|
||||
@@ -156,6 +180,23 @@ 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
|
||||
@@ -178,7 +219,26 @@ curl \
|
||||
|
||||
## VDI Import
|
||||
|
||||
A VHD or a raw export can be imported on an SR to create a new VDI at `/rest/v0/srs/<sr uuid>/vdis`.
|
||||
### 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`.
|
||||
|
||||
```sh
|
||||
curl \
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "xo-server",
|
||||
"version": "5.126.0",
|
||||
"version": "5.129.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.4",
|
||||
"@vates/disposable": "^0.1.5",
|
||||
"@vates/event-listeners-manager": "^1.0.1",
|
||||
"@vates/multi-key-map": "^0.1.0",
|
||||
"@vates/multi-key-map": "^0.2.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.43.2",
|
||||
"@xen-orchestra/backups": "^0.44.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.2",
|
||||
"@xen-orchestra/fs": "^4.1.3",
|
||||
"@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.0",
|
||||
"@xen-orchestra/xapi": "^3.3.0",
|
||||
"@xen-orchestra/vmware-explorer": "^0.3.1",
|
||||
"@xen-orchestra/xapi": "^4.0.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.6.1",
|
||||
"vhd-lib": "^4.7.0",
|
||||
"ws": "^8.2.3",
|
||||
"xdg-basedir": "^5.1.0",
|
||||
"xen-api": "^1.3.6",
|
||||
"xen-api": "^2.0.0",
|
||||
"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.6"
|
||||
"xo-vmdk-to-vhd": "^2.5.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.0.0",
|
||||
|
||||
@@ -277,6 +277,10 @@ importVmBackup.params = {
|
||||
sr: {
|
||||
type: 'string',
|
||||
},
|
||||
useDifferentialRestore: {
|
||||
type: 'boolean',
|
||||
optional: true
|
||||
}
|
||||
}
|
||||
|
||||
export function checkBackup({ id, settings, sr }) {
|
||||
@@ -380,3 +384,47 @@ 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',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -181,7 +181,7 @@ getApplianceUpdaterState.params = {
|
||||
|
||||
export async function checkHealth({ id }) {
|
||||
try {
|
||||
await this.callProxyMethod(id, 'system.getServerVersion')
|
||||
await this.checkProxyHealth(id)
|
||||
return {
|
||||
success: true,
|
||||
}
|
||||
|
||||
@@ -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.importVm(input, { srId: sr })).$ref)
|
||||
await tgtXapi.VM_destroy(await tgtXapi.VM_import(input, sr._xapiRef))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user