Compare commits

..

3 Commits

Author SHA1 Message Date
Julien Fontanet
d7e4a65ba7 WiP 2023-11-23 16:28:16 +01:00
Julien Fontanet
f26919cca7 chore(xapi/VM_import): typo snapshots → snapshot 2023-11-23 14:47:38 +01:00
Julien Fontanet
78afe122a6 chore(xo-server/proxy.checkHealth): call checkProxyHealth 2023-11-23 14:05:02 +01:00
90 changed files with 1811 additions and 3059 deletions

View File

@@ -68,11 +68,6 @@ module.exports = {
'no-console': ['error', { allow: ['warn', 'error'] }],
// this rule can prevent race condition bugs like parallel `a += await foo()`
//
// as it has a lots of false positive, it is only enabled as a warning for now
'require-atomic-updates': 'warn',
strict: 'error',
},
}

View File

@@ -14,7 +14,7 @@
"url": "https://vates.fr"
},
"license": "ISC",
"version": "0.1.5",
"version": "0.1.4",
"engines": {
"node": ">=8.10"
},
@@ -23,7 +23,7 @@
"test": "node--test"
},
"dependencies": {
"@vates/multi-key-map": "^0.2.0",
"@vates/multi-key-map": "^0.1.0",
"@xen-orchestra/async-map": "^0.1.2",
"@xen-orchestra/log": "^0.6.0",
"ensure-array": "^1.0.0"

View File

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

View File

@@ -17,14 +17,4 @@ map.get(['foo', 'bar']) // 2
map.get(['bar', 'foo']) // 3
map.get([OBJ]) // 4
map.get([{}]) // undefined
map.delete([])
for (const [key, value] of map.entries() {
console.log(key, value)
}
for (const value of map.values()) {
console.log(value)
}
```

View File

@@ -35,16 +35,6 @@ map.get(['foo', 'bar']) // 2
map.get(['bar', 'foo']) // 3
map.get([OBJ]) // 4
map.get([{}]) // undefined
map.delete([])
for (const [key, value] of map.entries() {
console.log(key, value)
}
for (const value of map.values()) {
console.log(value)
}
```
## Contributions

View File

@@ -36,23 +36,6 @@ function del(node, i, keys) {
return node
}
function* entries(node, key) {
if (node !== undefined) {
if (node instanceof Node) {
const { value } = node
if (value !== undefined) {
yield [key, node.value]
}
for (const [childKey, child] of node.children.entries()) {
yield* entries(child, key.concat(childKey))
}
} else {
yield [key, node]
}
}
}
function get(node, i, keys) {
return i === keys.length
? node instanceof Node
@@ -86,22 +69,6 @@ function set(node, i, keys, value) {
return node
}
function* values(node) {
if (node !== undefined) {
if (node instanceof Node) {
const { value } = node
if (value !== undefined) {
yield node.value
}
for (const child of node.children.values()) {
yield* values(child)
}
} else {
yield node
}
}
}
exports.MultiKeyMap = class MultiKeyMap {
constructor() {
// each node is either a value or a Node if it contains children
@@ -112,10 +79,6 @@ exports.MultiKeyMap = class MultiKeyMap {
this._root = del(this._root, 0, keys)
}
entries() {
return entries(this._root, [])
}
get(keys) {
return get(this._root, 0, keys)
}
@@ -123,8 +86,4 @@ exports.MultiKeyMap = class MultiKeyMap {
set(keys, value) {
this._root = set(this._root, 0, keys, value)
}
values() {
return values(this._root)
}
}

View File

@@ -19,7 +19,7 @@ describe('MultiKeyMap', () => {
// reverse composite key
['bar', 'foo'],
]
const values = keys.map(() => Math.random())
const values = keys.map(() => ({}))
// set all values first to make sure they are all stored and not only the
// last one
@@ -27,12 +27,6 @@ describe('MultiKeyMap', () => {
map.set(key, values[i])
})
assert.deepEqual(
Array.from(map.entries()),
keys.map((key, i) => [key, values[i]])
)
assert.deepEqual(Array.from(map.values()), values)
keys.forEach((key, i) => {
// copy the key to make sure the array itself is not the key
assert.strictEqual(map.get(key.slice()), values[i])

View File

@@ -18,7 +18,7 @@
"url": "https://vates.fr"
},
"license": "ISC",
"version": "0.2.0",
"version": "0.1.0",
"engines": {
"node": ">=8.10"
},

View File

@@ -13,7 +13,7 @@
"url": "https://vates.fr"
},
"license": "ISC",
"version": "2.0.1",
"version": "2.0.0",
"engines": {
"node": ">=14.0"
},
@@ -24,7 +24,7 @@
"@xen-orchestra/async-map": "^0.1.2",
"@xen-orchestra/log": "^0.6.0",
"promise-toolbox": "^0.21.0",
"xen-api": "^2.0.0"
"xen-api": "^1.3.6"
},
"devDependencies": {
"tap": "^16.3.0",

View File

@@ -7,8 +7,8 @@
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
"dependencies": {
"@xen-orchestra/async-map": "^0.1.2",
"@xen-orchestra/backups": "^0.44.2",
"@xen-orchestra/fs": "^4.1.3",
"@xen-orchestra/backups": "^0.43.2",
"@xen-orchestra/fs": "^4.1.2",
"filenamify": "^6.0.0",
"getopts": "^2.2.5",
"lodash": "^4.17.15",
@@ -27,7 +27,7 @@
"scripts": {
"postversion": "npm publish --access public"
},
"version": "1.0.14",
"version": "1.0.13",
"license": "AGPL-3.0-or-later",
"author": {
"name": "Vates SAS",

View File

@@ -4,14 +4,7 @@ import { formatFilenameDate } from './_filenameDate.mjs'
import { importIncrementalVm } from './_incrementalVm.mjs'
import { Task } from './Task.mjs'
import { watchStreamSize } from './_watchStreamSize.mjs'
import { VhdNegative, VhdSynthetic } from 'vhd-lib'
import { decorateClass } from '@vates/decorate-with'
import { createLogger } from '@xen-orchestra/log'
import { dirname, join } from 'node:path'
import pickBy from 'lodash/pickBy.js'
import { defer } from 'golike-defer'
const { debug, info, warn } = createLogger('xo:backups:importVmBackup')
async function resolveUuid(xapi, cache, uuid, type) {
if (uuid == null) {
return uuid
@@ -23,199 +16,26 @@ async function resolveUuid(xapi, cache, uuid, type) {
return cache.get(uuid)
}
export class ImportVmBackup {
constructor({
adapter,
metadata,
srUuid,
xapi,
settings: { additionnalVmTag, newMacAddresses, mapVdisSrs = {}, useDifferentialRestore = false } = {},
}) {
constructor({ adapter, metadata, srUuid, xapi, settings: { newMacAddresses, mapVdisSrs = {} } = {} }) {
this._adapter = adapter
this._importIncrementalVmSettings = { additionnalVmTag, newMacAddresses, mapVdisSrs, useDifferentialRestore }
this._importIncrementalVmSettings = { newMacAddresses, mapVdisSrs }
this._metadata = metadata
this._srUuid = srUuid
this._xapi = xapi
}
async #getPathOfVdiSnapshot(snapshotUuid) {
const metadata = this._metadata
if (this._pathToVdis === undefined) {
const backups = await this._adapter.listVmBackups(
this._metadata.vm.uuid,
({ mode, timestamp }) => mode === 'delta' && timestamp >= metadata.timestamp
)
const map = new Map()
for (const backup of backups) {
for (const [vdiRef, vdi] of Object.entries(backup.vdis)) {
map.set(vdi.uuid, backup.vhds[vdiRef])
}
}
this._pathToVdis = map
}
return this._pathToVdis.get(snapshotUuid)
}
async _reuseNearestSnapshot($defer, ignoredVdis) {
const metadata = this._metadata
async #decorateIncrementalVmMetadata(backup) {
const { mapVdisSrs } = this._importIncrementalVmSettings
const { vbds, vhds, vifs, vm, vmSnapshot } = metadata
const streams = {}
const metdataDir = dirname(metadata._filename)
const vdis = ignoredVdis === undefined ? metadata.vdis : pickBy(metadata.vdis, vdi => !ignoredVdis.has(vdi.uuid))
for (const [vdiRef, vdi] of Object.entries(vdis)) {
const vhdPath = join(metdataDir, vhds[vdiRef])
let xapiDisk
try {
xapiDisk = await this._xapi.getRecordByUuid('VDI', vdi.$snapshot_of$uuid)
} catch (err) {
// if this disk is not present anymore, fall back to default restore
warn(err)
}
let snapshotCandidate, backupCandidate
if (xapiDisk !== undefined) {
debug('found disks, wlll search its snapshots', { snapshots: xapiDisk.snapshots })
for (const snapshotRef of xapiDisk.snapshots) {
const snapshot = await this._xapi.getRecord('VDI', snapshotRef)
debug('handling snapshot', { snapshot })
// take only the first snapshot
if (snapshotCandidate && snapshotCandidate.snapshot_time < snapshot.snapshot_time) {
debug('already got a better candidate')
continue
}
// have a corresponding backup more recent than metadata ?
const pathToSnapshotData = await this.#getPathOfVdiSnapshot(snapshot.uuid)
if (pathToSnapshotData === undefined) {
debug('no backup linked to this snaphot')
continue
}
if (snapshot.$SR.uuid !== (mapVdisSrs[vdi.$snapshot_of$uuid] ?? this._srUuid)) {
debug('not restored on the same SR', { snapshotSr: snapshot.$SR.uuid, mapVdisSrs, srUuid: this._srUuid })
continue
}
debug('got a candidate', pathToSnapshotData)
snapshotCandidate = snapshot
backupCandidate = pathToSnapshotData
}
}
let stream
const backupWithSnapshotPath = join(metdataDir, backupCandidate ?? '')
if (vhdPath === backupWithSnapshotPath) {
// all the data are already on the host
debug('direct reuse of a snapshot')
stream = null
vdis[vdiRef].baseVdi = snapshotCandidate
// go next disk , we won't use this stream
continue
}
let disposableDescendants
const disposableSynthetic = await VhdSynthetic.fromVhdChain(this._adapter._handler, vhdPath)
// this will also clean if another disk of this VM backup fails
// if user really only need to restore non failing disks he can retry with ignoredVdis
let disposed = false
const disposeOnce = async () => {
if (!disposed) {
disposed = true
try {
await disposableDescendants?.dispose()
await disposableSynthetic?.dispose()
} catch (error) {
warn('openVhd: failed to dispose VHDs', { error })
}
}
}
$defer.onFailure(() => disposeOnce())
const parentVhd = disposableSynthetic.value
await parentVhd.readBlockAllocationTable()
debug('got vhd synthetic of parents', parentVhd.length)
if (snapshotCandidate !== undefined) {
try {
debug('will try to use differential restore', {
backupWithSnapshotPath,
vhdPath,
vdiRef,
})
disposableDescendants = await VhdSynthetic.fromVhdChain(this._adapter._handler, backupWithSnapshotPath, {
until: vhdPath,
})
const descendantsVhd = disposableDescendants.value
await descendantsVhd.readBlockAllocationTable()
debug('got vhd synthetic of descendants')
const negativeVhd = new VhdNegative(parentVhd, descendantsVhd)
debug('got vhd negative')
// update the stream with the negative vhd stream
stream = await negativeVhd.stream()
vdis[vdiRef].baseVdi = snapshotCandidate
} catch (err) {
// can be a broken VHD chain, a vhd chain with a key backup, ....
// not an irrecuperable error, don't dispose parentVhd, and fallback to full restore
warn(`can't use differential restore`, err)
disposableDescendants?.dispose()
}
}
// didn't make a negative stream : fallback to classic stream
if (stream === undefined) {
debug('use legacy restore')
stream = await parentVhd.stream()
}
stream.on('end', disposeOnce)
stream.on('close', disposeOnce)
stream.on('error', disposeOnce)
info('everything is ready, will transfer', stream.length)
streams[`${vdiRef}.vhd`] = stream
}
return {
streams,
vbds,
vdis,
version: '1.0.0',
vifs,
vm: { ...vm, suspend_VDI: vmSnapshot.suspend_VDI },
}
}
async #decorateIncrementalVmMetadata() {
const { additionnalVmTag, mapVdisSrs, useDifferentialRestore } = this._importIncrementalVmSettings
const ignoredVdis = new Set(
Object.entries(mapVdisSrs)
.filter(([_, srUuid]) => srUuid === null)
.map(([vdiUuid]) => vdiUuid)
)
let backup
if (useDifferentialRestore) {
backup = await this._reuseNearestSnapshot(ignoredVdis)
} else {
backup = await this._adapter.readIncrementalVmBackup(this._metadata, ignoredVdis)
}
const xapi = this._xapi
const cache = new Map()
const mapVdisSrRefs = {}
if (additionnalVmTag !== undefined) {
backup.vm.tags.push(additionnalVmTag)
}
for (const [vdiUuid, srUuid] of Object.entries(mapVdisSrs)) {
mapVdisSrRefs[vdiUuid] = await resolveUuid(xapi, cache, srUuid, 'SR')
}
const srRef = await resolveUuid(xapi, cache, this._srUuid, 'SR')
const sr = await resolveUuid(xapi, cache, this._srUuid, 'SR')
Object.values(backup.vdis).forEach(vdi => {
vdi.SR = mapVdisSrRefs[vdi.uuid] ?? srRef
vdi.SR = mapVdisSrRefs[vdi.uuid] ?? sr.$ref
})
return backup
}
@@ -226,7 +46,7 @@ export class ImportVmBackup {
const isFull = metadata.mode === 'full'
const sizeContainer = { size: 0 }
const { newMacAddresses } = this._importIncrementalVmSettings
const { mapVdisSrs, newMacAddresses } = this._importIncrementalVmSettings
let backup
if (isFull) {
backup = await adapter.readFullVmBackup(metadata)
@@ -234,7 +54,12 @@ export class ImportVmBackup {
} else {
assert.strictEqual(metadata.mode, 'delta')
backup = await this.#decorateIncrementalVmMetadata()
const ignoredVdis = new Set(
Object.entries(mapVdisSrs)
.filter(([_, srUuid]) => srUuid === null)
.map(([vdiUuid]) => vdiUuid)
)
backup = await this.#decorateIncrementalVmMetadata(await adapter.readIncrementalVmBackup(metadata, ignoredVdis))
Object.values(backup.streams).forEach(stream => watchStreamSize(stream, sizeContainer))
}
@@ -276,5 +101,3 @@ export class ImportVmBackup {
)
}
}
decorateClass(ImportVmBackup, { _reuseNearestSnapshot: defer })

View File

@@ -250,10 +250,6 @@ export const importIncrementalVm = defer(async function importIncrementalVm(
// Import VDI contents.
cancelableMap(cancelToken, Object.entries(newVdis), async (cancelToken, [id, vdi]) => {
for (let stream of ensureArray(streams[`${id}.vhd`])) {
if (stream === null) {
// we restore a backup and reuse completly a local snapshot
continue
}
if (typeof stream === 'function') {
stream = await stream()
}

View File

@@ -96,9 +96,6 @@ export const MixinRemoteWriter = (BaseClass = Object) =>
metadata,
srUuid,
xapi,
settings: {
additionnalVmTag: 'xo:no-bak=Health Check',
},
}).run()
const restoredVm = xapi.getObject(restoredId)
try {

View File

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

View File

@@ -8,7 +8,7 @@
"type": "git",
"url": "https://github.com/vatesfr/xen-orchestra.git"
},
"version": "0.44.2",
"version": "0.43.2",
"engines": {
"node": ">=14.18"
},
@@ -23,12 +23,12 @@
"@vates/cached-dns.lookup": "^1.0.0",
"@vates/compose": "^2.1.0",
"@vates/decorate-with": "^2.0.0",
"@vates/disposable": "^0.1.5",
"@vates/disposable": "^0.1.4",
"@vates/fuse-vhd": "^2.0.0",
"@vates/nbd-client": "^2.0.1",
"@vates/nbd-client": "^2.0.0",
"@vates/parse-duration": "^0.1.1",
"@xen-orchestra/async-map": "^0.1.2",
"@xen-orchestra/fs": "^4.1.3",
"@xen-orchestra/fs": "^4.1.2",
"@xen-orchestra/log": "^0.6.0",
"@xen-orchestra/template": "^0.1.0",
"app-conf": "^2.3.0",
@@ -44,8 +44,8 @@
"proper-lockfile": "^4.1.2",
"tar": "^6.1.15",
"uuid": "^9.0.0",
"vhd-lib": "^4.7.0",
"xen-api": "^2.0.0",
"vhd-lib": "^4.6.1",
"xen-api": "^1.3.6",
"yazl": "^2.5.1"
},
"devDependencies": {
@@ -56,7 +56,7 @@
"tmp": "^0.2.1"
},
"peerDependencies": {
"@xen-orchestra/xapi": "^4.0.0"
"@xen-orchestra/xapi": "^3.3.0"
},
"license": "AGPL-3.0-or-later",
"author": {

View File

@@ -1,7 +1,7 @@
{
"private": false,
"name": "@xen-orchestra/cr-seed-cli",
"version": "1.0.0",
"version": "0.2.0",
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/cr-seed-cli",
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
"repository": {
@@ -18,7 +18,7 @@
"preferGlobal": true,
"dependencies": {
"golike-defer": "^0.5.1",
"xen-api": "^2.0.0"
"xen-api": "^1.3.6"
},
"scripts": {
"postversion": "npm publish"

View File

@@ -1,7 +1,7 @@
{
"private": false,
"name": "@xen-orchestra/fs",
"version": "4.1.3",
"version": "4.1.2",
"license": "AGPL-3.0-or-later",
"description": "The File System for Xen Orchestra backups.",
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/fs",

View File

@@ -33,7 +33,7 @@ import { pRetry } from 'promise-toolbox'
const MAX_PART_SIZE = 1024 * 1024 * 1024 * 5 // 5GB
const MAX_PART_NUMBER = 10000
const MIN_PART_SIZE = 5 * 1024 * 1024
const { debug, info, warn } = createLogger('xo:fs:s3')
const { warn } = createLogger('xo:fs:s3')
export default class S3Handler extends RemoteHandlerAbstract {
#bucket
@@ -453,18 +453,10 @@ export default class S3Handler extends RemoteHandlerAbstract {
if (res.ObjectLockConfiguration?.ObjectLockEnabled === 'Enabled') {
// Workaround for https://github.com/aws/aws-sdk-js-v3/issues/2673
// will automatically add the contentMD5 header to any upload to S3
debug(`Object Lock is enable, enable content md5 header`)
this.#s3.middlewareStack.use(getApplyMd5BodyChecksumPlugin(this.#s3.config))
}
} catch (error) {
// maybe the account doesn't have enought privilege to query the object lock configuration
// be defensive and apply the md5 just in case
if (error.$metadata.httpStatusCode === 403) {
info(`s3 user doesnt have enough privilege to check for Object Lock, enable content MD5 header`)
this.#s3.middlewareStack.use(getApplyMd5BodyChecksumPlugin(this.#s3.config))
} else if (error.Code === 'ObjectLockConfigurationNotFoundError' || error.$metadata.httpStatusCode === 501) {
info(`Object lock is not available or not configured, don't add the content MD5 header`)
} else {
if (error.Code !== 'ObjectLockConfigurationNotFoundError' && error.$metadata.httpStatusCode !== 501) {
throw error
}
}

View File

@@ -2,17 +2,10 @@
## **next**
- [VM/Action] Ability to migrate a VM from its view (PR [#7164](https://github.com/vatesfr/xen-orchestra/pull/7164))
- Ability to override host address with `master` URL query param (PR [#7187](https://github.com/vatesfr/xen-orchestra/pull/7187))
- Added tooltip on CPU provisioning warning icon (PR [#7223](https://github.com/vatesfr/xen-orchestra/pull/7223))
## **0.1.6** (2023-11-30)
- Explicit error if users attempt to connect from a slave host (PR [#7110](https://github.com/vatesfr/xen-orchestra/pull/7110))
- More compact UI (PR [#7159](https://github.com/vatesfr/xen-orchestra/pull/7159))
- Fix dashboard host patches list (PR [#7169](https://github.com/vatesfr/xen-orchestra/pull/7169))
- Ability to export selected VMs (PR [#7174](https://github.com/vatesfr/xen-orchestra/pull/7174))
- [VM/Action] Ability to export a VM from its view (PR [#7190](https://github.com/vatesfr/xen-orchestra/pull/7190))
## **0.1.5** (2023-11-07)

View File

@@ -1,6 +1,6 @@
{
"name": "@xen-orchestra/lite",
"version": "0.1.6",
"version": "0.1.5",
"scripts": {
"dev": "GIT_HEAD=$(git rev-parse HEAD) vite",
"build": "run-p type-check build-only",
@@ -10,55 +10,57 @@
"test": "yarn run type-check",
"type-check": "vue-tsc --noEmit"
},
"devDependencies": {
"dependencies": {
"@fontsource/poppins": "^5.0.8",
"@fortawesome/fontawesome-svg-core": "^6.4.2",
"@fortawesome/free-regular-svg-icons": "^6.4.2",
"@fortawesome/free-solid-svg-icons": "^6.4.2",
"@fortawesome/vue-fontawesome": "^3.0.5",
"@intlify/unplugin-vue-i18n": "^1.5.0",
"@limegrass/eslint-plugin-import-alias": "^1.1.0",
"@novnc/novnc": "^1.4.0",
"@rushstack/eslint-patch": "^1.5.1",
"@tsconfig/node18": "^18.2.2",
"@types/d3-time-format": "^4.0.3",
"@types/file-saver": "^2.0.7",
"@types/lodash-es": "^4.17.11",
"@types/node": "^18.18.9",
"@vitejs/plugin-vue": "^4.4.0",
"@vue/eslint-config-prettier": "^8.0.0",
"@vue/eslint-config-typescript": "^12.0.0",
"@vue/tsconfig": "^0.4.0",
"@vueuse/core": "^10.5.0",
"@vueuse/math": "^10.5.0",
"@fortawesome/fontawesome-svg-core": "^6.1.1",
"@fortawesome/free-regular-svg-icons": "^6.2.0",
"@fortawesome/free-solid-svg-icons": "^6.2.0",
"@fortawesome/vue-fontawesome": "^3.0.1",
"@novnc/novnc": "^1.3.0",
"@types/d3-time-format": "^4.0.0",
"@types/file-saver": "^2.0.5",
"@types/lodash-es": "^4.17.6",
"@types/marked": "^4.0.8",
"@vueuse/core": "^10.1.2",
"@vueuse/math": "^10.1.2",
"complex-matcher": "^0.7.1",
"d3-time-format": "^4.1.0",
"decorator-synchronized": "^0.6.0",
"echarts": "^5.4.3",
"eslint-plugin-vue": "^9.18.1",
"echarts": "^5.3.3",
"file-saver": "^2.0.5",
"highlight.js": "^11.9.0",
"human-format": "^1.2.0",
"highlight.js": "^11.6.0",
"human-format": "^1.1.0",
"iterable-backoff": "^0.1.0",
"json-rpc-2.0": "^1.7.0",
"json5": "^2.2.3",
"json-rpc-2.0": "^1.3.0",
"json5": "^2.2.1",
"limit-concurrency-decorator": "^0.5.0",
"lodash-es": "^4.17.21",
"make-error": "^1.3.6",
"marked": "^9.1.5",
"npm-run-all": "^4.1.5",
"pinia": "^2.1.7",
"marked": "^4.2.12",
"pinia": "^2.1.2",
"placement.js": "^1.0.0-beta.5",
"postcss": "^8.4.31",
"postcss-custom-media": "^10.0.2",
"postcss-nested": "^6.0.1",
"typescript": "^5.2.2",
"vite": "^4.5.0",
"vue": "^3.3.8",
"vue-echarts": "^6.6.1",
"vue-i18n": "^9.6.5",
"vue-router": "^4.2.5",
"vue-tsc": "^1.8.22"
"vue": "^3.3.4",
"vue-echarts": "^6.2.3",
"vue-i18n": "^9.2.2",
"vue-router": "^4.2.1"
},
"devDependencies": {
"@intlify/unplugin-vue-i18n": "^0.10.0",
"@limegrass/eslint-plugin-import-alias": "^1.0.5",
"@rushstack/eslint-patch": "^1.1.0",
"@types/node": "^16.11.41",
"@vitejs/plugin-vue": "^4.2.3",
"@vue/eslint-config-prettier": "^8.0.0",
"@vue/eslint-config-typescript": "^11.0.0",
"@vue/tsconfig": "^0.1.3",
"eslint-plugin-vue": "^9.0.0",
"npm-run-all": "^4.1.5",
"postcss": "^8.4.19",
"postcss-custom-media": "^9.0.1",
"postcss-nested": "^6.0.0",
"typescript": "^4.9.3",
"vite": "^4.3.8",
"vue-tsc": "^1.6.5"
},
"private": true,
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/lite",
@@ -74,6 +76,6 @@
},
"license": "AGPL-3.0-or-later",
"engines": {
"node": ">=18"
"node": ">=8.10"
}
}

View File

@@ -12,7 +12,6 @@
</RouterLink>
<slot />
<div class="right">
<PoolOverrideWarning as-tooltip />
<AccountButton />
</div>
</header>
@@ -20,7 +19,6 @@
<script lang="ts" setup>
import AccountButton from "@/components/AccountButton.vue";
import PoolOverrideWarning from "@/components/PoolOverrideWarning.vue";
import TextLogo from "@/components/TextLogo.vue";
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import { useNavigationStore } from "@/stores/navigation.store";
@@ -53,10 +51,6 @@ const { trigger: navigationTrigger } = storeToRefs(navigationStore);
margin-left: 1rem;
vertical-align: middle;
}
.warning-not-current-pool {
font-size: 2.4rem;
}
}
.right {

View File

@@ -2,7 +2,6 @@
<div class="app-login form-container">
<form @submit.prevent="handleSubmit">
<img alt="XO Lite" src="../assets/logo-title.svg" />
<PoolOverrideWarning />
<p v-if="isHostIsSlaveErr(error)" class="error">
<UiIcon :icon="faExclamationCircle" />
{{ $t("login-only-on-master") }}
@@ -46,7 +45,6 @@ import FormCheckbox from "@/components/form/FormCheckbox.vue";
import FormInput from "@/components/form/FormInput.vue";
import FormInputWrapper from "@/components/form/FormInputWrapper.vue";
import LoginError from "@/components/LoginError.vue";
import PoolOverrideWarning from "@/components/PoolOverrideWarning.vue";
import UiButton from "@/components/ui/UiButton.vue";
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import type { XenApiError } from "@/libs/xen-api/xen-api.types";

View File

@@ -1,59 +0,0 @@
<template>
<div
v-if="xenApi.isPoolOverridden"
class="warning-not-current-pool"
@click="xenApi.resetPoolMasterIp"
v-tooltip="
asTooltip && {
placement: 'right',
content: `
${$t('you-are-currently-on', [masterSessionStorage])}.
${$t('click-to-return-default-pool')}
`,
}
"
>
<div class="wrapper">
<UiIcon :icon="faWarning" />
<p v-if="!asTooltip">
<i18n-t keypath="you-are-currently-on">
<strong>{{ masterSessionStorage }}</strong>
</i18n-t>
<br />
{{ $t("click-to-return-default-pool") }}
</p>
</div>
</div>
</template>
<script lang="ts" setup>
import { faWarning } from "@fortawesome/free-solid-svg-icons";
import { useSessionStorage } from "@vueuse/core";
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import { useXenApiStore } from "@/stores/xen-api.store";
import { vTooltip } from "@/directives/tooltip.directive";
defineProps<{
asTooltip?: boolean;
}>();
const xenApi = useXenApiStore();
const masterSessionStorage = useSessionStorage("master", null);
</script>
<style lang="postcss" scoped>
.warning-not-current-pool {
color: var(--color-orange-world-base);
cursor: pointer;
.wrapper {
display: flex;
justify-content: center;
svg {
margin: auto 1rem;
}
}
}
</style>

View File

@@ -3,14 +3,8 @@
<UiCardTitle>
{{ $t("cpu-provisioning") }}
<template v-if="!hasError" #right>
<UiStatusIcon
v-if="state !== 'success'"
v-tooltip="{
content: $t('cpu-provisioning-warning'),
placement: 'left',
}"
:state="state"
/>
<!-- TODO: add a tooltip for the warning icon -->
<UiStatusIcon v-if="state !== 'success'" :state="state" />
</template>
</UiCardTitle>
<NoDataError v-if="hasError" />
@@ -43,12 +37,11 @@ import UiCard from "@/components/ui/UiCard.vue";
import UiCardFooter from "@/components/ui/UiCardFooter.vue";
import UiCardSpinner from "@/components/ui/UiCardSpinner.vue";
import UiCardTitle from "@/components/ui/UiCardTitle.vue";
import { vTooltip } from "@/directives/tooltip.directive";
import { useHostCollection } from "@/stores/xen-api/host.store";
import { useVmCollection } from "@/stores/xen-api/vm.store";
import { useVmMetricsCollection } from "@/stores/xen-api/vm-metrics.store";
import { percent } from "@/libs/utils";
import { VM_POWER_STATE } from "@/libs/xen-api/xen-api.enums";
import { useHostCollection } from "@/stores/xen-api/host.store";
import { useVmMetricsCollection } from "@/stores/xen-api/vm-metrics.store";
import { useVmCollection } from "@/stores/xen-api/vm.store";
import { logicAnd } from "@vueuse/math";
import { computed } from "vue";

View File

@@ -11,7 +11,7 @@ import { useContext } from "@/composables/context.composable";
import { ColorContext, DisabledContext } from "@/context";
import type { Color } from "@/types";
import { IK_MODAL } from "@/types/injection-keys";
import { useMagicKeys, whenever } from "@vueuse/core";
import { useMagicKeys, whenever } from "@vueuse/core/index";
import { inject } from "vue";
const props = defineProps<{

View File

@@ -3,13 +3,13 @@
v-tooltip="
vmRefs.length > 0 &&
!isSomeExportable &&
$t(isSingleAction ? 'vm-is-running' : 'no-selected-vm-can-be-exported')
$t('no-selected-vm-can-be-exported')
"
:icon="faDisplay"
:disabled="isDisabled"
@click="openModal"
>
{{ $t(isSingleAction ? "export-vm" : "export-vms") }}
{{ $t("export-vms") }}
</MenuItem>
</template>
<script lang="ts" setup>
@@ -26,10 +26,7 @@ import { vTooltip } from "@/directives/tooltip.directive";
import type { XenApiVm } from "@/libs/xen-api/xen-api.types";
const props = defineProps<{
vmRefs: XenApiVm["$ref"][];
isSingleAction?: boolean;
}>();
const props = defineProps<{ vmRefs: XenApiVm["$ref"][] }>();
const { getByOpaqueRefs, areSomeOperationAllowed } = useVmCollection();

View File

@@ -3,11 +3,7 @@
v-tooltip="
selectedRefs.length > 0 &&
!isMigratable &&
$t(
isSingleAction
? 'this-vm-cant-be-migrated'
: 'no-selected-vm-can-be-migrated'
)
$t('no-selected-vm-can-be-migrated')
"
:busy="isMigrating"
:disabled="isParentDisabled || !isMigratable"
@@ -32,7 +28,6 @@ import { computed } from "vue";
const props = defineProps<{
selectedRefs: XenApiVm["$ref"][];
isSingleAction?: boolean;
}>();
const { getByOpaqueRefs, isOperationPending, areSomeOperationAllowed } =

View File

@@ -26,9 +26,7 @@
/>
</template>
<VmActionCopyItem :selected-refs="[vm.$ref]" is-single-action />
<VmActionExportItem :vm-refs="[vm.$ref]" is-single-action />
<VmActionSnapshotItem :vm-refs="[vm.$ref]" />
<VmActionMigrateItem :selected-refs="[vm.$ref]" is-single-action />
</AppMenu>
</template>
</TitleBar>
@@ -39,11 +37,9 @@ import AppMenu from "@/components/menu/AppMenu.vue";
import TitleBar from "@/components/TitleBar.vue";
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import UiButton from "@/components/ui/UiButton.vue";
import VmActionMigrateItem from "@/components/vm/VmActionItems/VmActionMigrateItem.vue";
import VmActionPowerStateItems from "@/components/vm/VmActionItems/VmActionPowerStateItems.vue";
import VmActionSnapshotItem from "@/components/vm/VmActionItems/VmActionSnapshotItem.vue";
import VmActionCopyItem from "@/components/vm/VmActionItems/VmActionCopyItem.vue";
import VmActionExportItem from "@/components/vm/VmActionItems/VmActionExportItem.vue";
import { useVmCollection } from "@/stores/xen-api/vm.store";
import { vTooltip } from "@/directives/tooltip.directive";
import type { XenApiVm } from "@/libs/xen-api/xen-api.types";

View File

@@ -1,4 +1,4 @@
import type { HighlightResult } from "highlight.js";
import type { HighlightResult, Language } from "highlight.js";
import HLJS from "highlight.js/lib/core";
import cssLang from "highlight.js/lib/languages/css";
import jsonLang from "highlight.js/lib/languages/json";
@@ -19,6 +19,10 @@ export const highlight: (
ignoreIllegals?: boolean
) => HighlightResult = HLJS.highlight;
export const getLanguage: (
languageName: AcceptedLanguage
) => Language | undefined = HLJS.getLanguage;
export type AcceptedLanguage =
| "xml"
| "css"

View File

@@ -1,5 +1,8 @@
import { type AcceptedLanguage, highlight } from "@/libs/highlight";
import HLJS from "highlight.js/lib/core";
import {
type AcceptedLanguage,
getLanguage,
highlight,
} from "@/libs/highlight";
import { marked } from "marked";
enum VUE_TAG {
@@ -8,26 +11,15 @@ enum VUE_TAG {
STYLE = "vue-style",
}
function extractLang(lang: string | undefined): AcceptedLanguage | VUE_TAG {
if (lang === undefined) {
return "plaintext";
}
if (Object.values(VUE_TAG).includes(lang as VUE_TAG)) {
return lang as VUE_TAG;
}
if (HLJS.getLanguage(lang) !== undefined) {
return lang as AcceptedLanguage;
}
return "plaintext";
}
marked.use({
renderer: {
code(str, lang) {
const code = customHighlight(str, extractLang(lang));
code(str: string, lang: AcceptedLanguage) {
const code = customHighlight(
str,
Object.values(VUE_TAG).includes(lang as VUE_TAG) || getLanguage(lang)
? lang
: "plaintext"
);
return `<pre class="hljs"><button class="copy-button" type="button">Copy</button><code class="hljs-code">${code}</code></pre>`;
},
},

View File

@@ -27,7 +27,6 @@
"cancel": "Cancel",
"change-state": "Change state",
"click-to-display-alarms": "Click to display alarms:",
"click-to-return-default-pool": "Click here to return to the default pool",
"close": "Close",
"coming-soon": "Coming soon!",
"community": "Community",
@@ -38,7 +37,6 @@
"console-unavailable": "Console unavailable",
"copy": "Copy",
"cpu-provisioning": "CPU provisioning",
"cpu-provisioning-warning": "The number of vCPUs allocated exceeds the number of physical CPUs available. System performance could be affected",
"cpu-usage": "CPU usage",
"dashboard": "Dashboard",
"delete": "Delete",
@@ -57,7 +55,6 @@
"export-n-vms": "Export 1 VM | Export {n} VMs",
"export-n-vms-manually": "Export 1 VM manually | Export {n} VMs manually",
"export-table-to": "Export table to {type}",
"export-vm": "Export VM",
"export-vms": "Export VMs",
"export-vms-manually-information": "Some VM exports were not able to start automatically, probably due to your browser settings. To export them, you should click on each one. (Alternatively, copy the link as well.)",
"fetching-fresh-data": "Fetching fresh data",
@@ -179,7 +176,6 @@
"theme-auto": "Auto",
"theme-dark": "Dark",
"theme-light": "Light",
"this-vm-cant-be-migrated": "This VM can't be migrated",
"top-#": "Top {n}",
"total-cpus": "Total CPUs",
"total-free": "Total free",
@@ -192,6 +188,5 @@
"vm-is-running": "The VM is running",
"vms": "VMs",
"xo-lite-under-construction": "XOLite is under construction",
"you-are-currently-on": "You are currently on: {0}",
"zstd": "zstd"
}

View File

@@ -27,7 +27,6 @@
"cancel": "Annuler",
"change-state": "Changer l'état",
"click-to-display-alarms": "Cliquer pour afficher les alarmes :",
"click-to-return-default-pool": "Cliquer ici pour revenir au pool par défaut",
"close": "Fermer",
"coming-soon": "Bientôt disponible !",
"community": "Communauté",
@@ -38,7 +37,6 @@
"console-unavailable": "Console indisponible",
"copy": "Copier",
"cpu-provisioning": "Provisionnement CPU",
"cpu-provisioning-warning": "Le nombre de vCPU alloués dépasse le nombre de CPU physique disponible. Les performances du système pourraient être affectées",
"cpu-usage": "Utilisation CPU",
"dashboard": "Tableau de bord",
"delete": "Supprimer",
@@ -57,7 +55,6 @@
"export-n-vms": "Exporter 1 VM | Exporter {n} VMs",
"export-n-vms-manually": "Exporter 1 VM manuellement | Exporter {n} VMs manuellement",
"export-table-to": "Exporter le tableau en {type}",
"export-vm": "Exporter la VM",
"export-vms": "Exporter les VMs",
"export-vms-manually-information": "Certaines exportations de VMs n'ont pas pu démarrer automatiquement, peut-être en raison des paramètres du navigateur. Pour les exporter, vous devrez cliquer sur chacune d'entre elles. (Ou copier le lien.)",
"fetching-fresh-data": "Récupération de données à jour",
@@ -179,7 +176,6 @@
"theme-auto": "Auto",
"theme-dark": "Sombre",
"theme-light": "Clair",
"this-vm-cant-be-migrated": "Cette VM ne peut pas être migrée",
"top-#": "Top {n}",
"total-cpus": "Total CPUs",
"total-free": "Total libre",
@@ -192,6 +188,5 @@
"vm-is-running": "La VM est en cours d'exécution",
"vms": "VMs",
"xo-lite-under-construction": "XOLite est en construction",
"you-are-currently-on": "Vous êtes actuellement sur : {0}",
"zstd": "zstd"
}

View File

@@ -1,10 +1,8 @@
import XapiStats from "@/libs/xapi-stats";
import XenApi from "@/libs/xen-api/xen-api";
import { useLocalStorage, useSessionStorage, whenever } from "@vueuse/core";
import { useLocalStorage } from "@vueuse/core";
import { defineStore } from "pinia";
import { computed, ref, watchEffect } from "vue";
import { useRouter } from "vue-router";
import { useRoute } from "vue-router";
const HOST_URL = import.meta.env.PROD
? window.origin
@@ -17,27 +15,7 @@ enum STATUS {
}
export const useXenApiStore = defineStore("xen-api", () => {
// undefined not correctly handled. See https://github.com/vueuse/vueuse/issues/3595
const masterSessionStorage = useSessionStorage<null | string>("master", null);
const router = useRouter();
const route = useRoute();
whenever(
() => route.query.master,
async (newMaster) => {
masterSessionStorage.value = newMaster as string;
await router.replace({ query: { ...route.query, master: undefined } });
window.location.reload();
}
);
const hostUrl = new URL(HOST_URL);
if (masterSessionStorage.value !== null) {
hostUrl.hostname = masterSessionStorage.value;
}
const isPoolOverridden = hostUrl.origin !== new URL(HOST_URL).origin;
const xenApi = new XenApi(hostUrl.origin);
const xenApi = new XenApi(HOST_URL);
const xapiStats = new XapiStats(xenApi);
const storedSessionId = useLocalStorage<string | undefined>(
"sessionId",
@@ -97,21 +75,14 @@ export const useXenApiStore = defineStore("xen-api", () => {
status.value = STATUS.DISCONNECTED;
}
function resetPoolMasterIp() {
masterSessionStorage.value = null;
window.location.reload();
}
return {
isConnected,
isConnecting,
isPoolOverridden,
connect,
reconnect,
disconnect,
getXapi,
getXapiStats,
currentSessionId,
resetPoolMasterIp,
};
});

View File

@@ -135,15 +135,23 @@
</UiCard>
<UiCard class="group">
<UiCardTitle>{{ $t("language") }}</UiCardTitle>
<FormSelect :before="faEarthAmericas" v-model="$i18n.locale">
<option
:value="locale"
v-for="locale in $i18n.availableLocales"
:key="locale"
>
{{ locales[locale].name ?? locale }}
</option>
</FormSelect>
<UiKeyValueList>
<UiKeyValueRow>
<template #value>
<FormWidget class="full-length" :before="faEarthAmericas">
<select v-model="$i18n.locale">
<option
:value="locale"
v-for="locale in $i18n.availableLocales"
:key="locale"
>
{{ locales[locale].name ?? locale }}
</option>
</select>
</FormWidget>
</template>
</UiKeyValueRow>
</UiKeyValueList>
</UiCard>
</div>
</template>
@@ -166,7 +174,7 @@ import {
faGear,
faCheck,
} from "@fortawesome/free-solid-svg-icons";
import FormSelect from "@/components/form/FormSelect.vue";
import FormWidget from "@/components/FormWidget.vue";
import TitleBar from "@/components/TitleBar.vue";
import UiCard from "@/components/ui/UiCard.vue";
import UiKeyValueList from "@/components/ui/UiKeyValueList.vue";
@@ -241,4 +249,8 @@ h5 {
}
}
}
.full-length {
width: 100%;
}
</style>

View File

@@ -1,5 +1,5 @@
{
"extends": ["@tsconfig/node18/tsconfig.json", "@vue/tsconfig/tsconfig.json"],
"extends": "@vue/tsconfig/tsconfig.node.json",
"include": ["vite.config.*", "vitest.config.*", "cypress.config.*"],
"compilerOptions": {
"composite": true,

View File

@@ -1,9 +1,10 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"extends": "@vue/tsconfig/tsconfig.web.json",
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
"exclude": ["src/stories/**/*"],
"compilerOptions": {
"experimentalDecorators": true,
"lib": ["ES2019", "ES2020.Intl", "dom"],
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]

View File

@@ -1,7 +1,7 @@
{
"private": true,
"name": "@xen-orchestra/proxy",
"version": "0.26.41",
"version": "0.26.38",
"license": "AGPL-3.0-or-later",
"description": "XO Proxy used to remotely execute backup jobs",
"keywords": [
@@ -30,15 +30,15 @@
"@vates/cached-dns.lookup": "^1.0.0",
"@vates/compose": "^2.1.0",
"@vates/decorate-with": "^2.0.0",
"@vates/disposable": "^0.1.5",
"@vates/disposable": "^0.1.4",
"@xen-orchestra/async-map": "^0.1.2",
"@xen-orchestra/backups": "^0.44.2",
"@xen-orchestra/fs": "^4.1.3",
"@xen-orchestra/backups": "^0.43.2",
"@xen-orchestra/fs": "^4.1.2",
"@xen-orchestra/log": "^0.6.0",
"@xen-orchestra/mixin": "^0.1.0",
"@xen-orchestra/mixins": "^0.14.0",
"@xen-orchestra/self-signed": "^0.1.3",
"@xen-orchestra/xapi": "^4.0.0",
"@xen-orchestra/xapi": "^3.3.0",
"ajv": "^8.0.3",
"app-conf": "^2.3.0",
"async-iterator-to-stream": "^1.1.0",
@@ -60,7 +60,7 @@
"source-map-support": "^0.5.16",
"stoppable": "^1.0.6",
"xdg-basedir": "^5.1.0",
"xen-api": "^2.0.0",
"xen-api": "^1.3.6",
"xo-common": "^0.8.0"
},
"devDependencies": {

View File

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

View File

@@ -1,7 +1,7 @@
{
"license": "ISC",
"private": false,
"version": "0.3.1",
"version": "0.3.0",
"name": "@xen-orchestra/vmware-explorer",
"dependencies": {
"@vates/node-vsphere-soap": "^2.0.0",
@@ -10,7 +10,7 @@
"@xen-orchestra/log": "^0.6.0",
"lodash": "^4.17.21",
"node-fetch": "^3.3.0",
"vhd-lib": "^4.7.0"
"vhd-lib": "^4.6.1"
},
"engines": {
"node": ">=14"

View File

@@ -1,6 +1,6 @@
{
"name": "@xen-orchestra/xapi",
"version": "4.0.0",
"version": "3.3.0",
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/xapi",
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
"repository": {
@@ -16,7 +16,7 @@
},
"main": "./index.mjs",
"peerDependencies": {
"xen-api": "^2.0.0"
"xen-api": "^1.3.6"
},
"scripts": {
"postversion": "npm publish --access public",
@@ -25,7 +25,7 @@
"dependencies": {
"@vates/async-each": "^1.0.0",
"@vates/decorate-with": "^2.0.0",
"@vates/nbd-client": "^2.0.1",
"@vates/nbd-client": "^2.0.0",
"@xen-orchestra/async-map": "^0.1.2",
"@xen-orchestra/log": "^0.6.0",
"d3-time-format": "^4.1.0",
@@ -34,7 +34,7 @@
"json-rpc-protocol": "^0.13.2",
"lodash": "^4.17.15",
"promise-toolbox": "^0.21.0",
"vhd-lib": "^4.7.0",
"vhd-lib": "^4.6.1",
"xo-common": "^0.8.0"
},
"private": false,

View File

@@ -200,6 +200,18 @@ class Vm {
}
}
_safeSetIsATemplate(ref) {
return pCatch.call(
this.setField('VM', ref, 'is_a_template', false),
// Ignore if this fails due to license restriction
//
// see https://bugs.xenserver.org/browse/XSO-766
{ code: 'LICENSE_RESTRICTION' },
noop
)
}
async assertHealthyVdiChains(vmRef, tolerance = this._maxUncoalescedVdis) {
const vdiRefs = {}
;(await this.getRecords('VBD', await this.getField('VM', vmRef, 'VBDs'))).forEach(({ VDI: ref }) => {
@@ -486,9 +498,10 @@ class Vm {
if (useSnapshot === undefined) {
useSnapshot = isVmRunning(vm)
}
let exportedVmRef, destroySnapshot
let exportedVmRef, destroySnapshot, isSnapshot
if (useSnapshot) {
exportedVmRef = await this.VM_snapshot(vmRef, { cancelToken, name_label: `[XO Export] ${vm.name_label}` })
isSnapshot = true
destroySnapshot = () =>
this.VM_destroy(exportedVmRef).catch(error => {
warn('VM_export: failed to destroy snapshot', {
@@ -500,8 +513,13 @@ class Vm {
$defer.onFailure(destroySnapshot)
} else {
exportedVmRef = vmRef
isSnapshot = vm.is_a_snapshot
}
try {
// VM snapshots are marked as templates, unfortunately it does not play well with XVA export/import
// which will import them as templates and not VM snapshots or plain VMs
await this._safeSetIsATemplate(exportedVmRef, false)
const stream = await this.getResource(cancelToken, '/export/', {
query: {
ref: exportedVmRef,
@@ -510,6 +528,16 @@ class Vm {
task: taskRef,
})
if (isSnapshot) {
// FIXME: VM_IS_SNAPSHOT(OpaqueRef:757d6cfd-a185-4114-bfc8-fb9fdd279bf2, make_into_template)
this._safeSetIsATemplate(exportedVmRef, true).catch(error => {
warn('VM_export: failed to reset is_a_template on snapshot', {
error,
snapshotRef: exportedVmRef,
vmRef,
})
})
}
if (useSnapshot) {
stream.once('end', destroySnapshot).once('error', destroySnapshot)
}
@@ -665,18 +693,6 @@ class Vm {
// detached async
this._httpHook(vm, '/post-sync').catch(noop)
// VM snapshots are marked as templates, unfortunately it does not play well with XVA export/import
// which will import them as templates and not VM snapshots or plain VMs
await pCatch.call(
this.setField('VM', ref, 'is_a_template', false),
// Ignore if this fails due to license restriction
//
// see https://bugs.xenserver.org/browse/XSO-766
{ code: 'LICENSE_RESTRICTION' },
noop
)
if (destroyNobakVdis) {
await asyncMap(await listNobakVbds(this, await this.getField('VM', ref, 'VBDs')), async vbd => {
try {

View File

@@ -1,61 +1,7 @@
# ChangeLog
## **5.89.0** (2023-11-30)
<img id="latest" src="https://badgen.net/badge/channel/latest/yellow" alt="Channel: latest" />
### Highlights
- [Restore] Show source remote and restoration time on a restored VM (PR [#7186](https://github.com/vatesfr/xen-orchestra/pull/7186))
- [Backup/Import] Show disk import status during Incremental Replication or restoration of Incremental Backup (PR [#7171](https://github.com/vatesfr/xen-orchestra/pull/7171))
- [VM/Console] Add a message to indicate that the console view has been [disabled](https://support.citrix.com/article/CTX217766/how-to-disable-the-console-for-the-vm-in-xencenter) for this VM [#6319](https://github.com/vatesfr/xen-orchestra/issues/6319) (PR [#7161](https://github.com/vatesfr/xen-orchestra/pull/7161))
- [REST API] `tags` property can be updated (PR [#7196](https://github.com/vatesfr/xen-orchestra/pull/7196))
- [REST API] A VDI export can now be imported in an existing VDI (PR [#7199](https://github.com/vatesfr/xen-orchestra/pull/7199))
- [REST API] Support VM import using the XVA format
- [File Restore] API method `backupNg.mountPartition` to manually mount a backup disk on the XOA
- [Backup] Implement differential restore (PR [#7202](https://github.com/vatesfr/xen-orchestra/pull/7202))
- [VM/Disks] Display task information when importing VDIs (PR [#7197](https://github.com/vatesfr/xen-orchestra/pull/7197))
- [VM Creation] Added ISO option in new VM form when creating from template with a disk [#3464](https://github.com/vatesfr/xen-orchestra/issues/3464) (PR [#7166](https://github.com/vatesfr/xen-orchestra/pull/7166))
- [Task] Show the related SR on the Garbage Collector Task ( vdi coalescing) (PR [#7189](https://github.com/vatesfr/xen-orchestra/pull/7189))
### Enhancements
- [Netbox] Ability to synchronize XO users as Netbox tenants (PR [#7158](https://github.com/vatesfr/xen-orchestra/pull/7158))
- [Backup] Don't backup VM with tag xo:no-bak (PR [#7173](https://github.com/vatesfr/xen-orchestra/pull/7173))
### Bug fixes
- [Backup/Restore] In case of snapshot with memory, create the suspend VDI on the correct SR instead of the default one
- [Import/ESXi] Handle `Cannot read properties of undefined (reading 'perDatastoreUsage')` error when importing VM without storage (PR [#7168](https://github.com/vatesfr/xen-orchestra/pull/7168))
- [Export/OVA] Handle export with resulting disk larger than 8.2GB (PR [#7183](https://github.com/vatesfr/xen-orchestra/pull/7183))
- [Self Service] Fix error displayed after adding a VM to a resource set (PR [#7144](https://github.com/vatesfr/xen-orchestra/pull/7144))
- [Backup/HealthCheck] Don't backup VM created by health check when using smart mode (PR [#7173](https://github.com/vatesfr/xen-orchestra/pull/7173))
### Released packages
- vhd-lib 4.7.0
- @vates/multi-key-map 0.2.0
- @vates/disposable 0.1.5
- @xen-orchestra/fs 4.1.3
- xen-api 2.0.0
- @vates/nbd-client 2.0.1
- @xen-orchestra/xapi 4.0.0
- @xen-orchestra/backups 0.44.2
- @xen-orchestra/backups-cli 1.0.14
- @xen-orchestra/cr-seed-cli 1.0.0
- @xen-orchestra/proxy 0.26.41
- xo-vmdk-to-vhd 2.5.7
- @xen-orchestra/vmware-explorer 0.3.1
- xapi-explore-sr 0.4.2
- xo-cli 0.22.0
- xo-server 5.129.0
- xo-server-netbox 1.4.0
- xo-web 5.130.0
## **5.88.2** (2023-11-13)
<img id="stable" src="https://badgen.net/badge/channel/stable/green" alt="Channel: stable" />
### Enhancement
- [REST API] Add `users` collection
@@ -67,6 +13,8 @@
## **5.88.1** (2023-11-07)
<img id="latest" src="https://badgen.net/badge/channel/latest/yellow" alt="Channel: latest" />
### Bug fixes
- [Netbox] Fix VMs' `site` property being unnecessarily updated on some versions of Netbox (PR [#7145](https://github.com/vatesfr/xen-orchestra/pull/7145))
@@ -135,6 +83,8 @@
## **5.87.0** (2023-09-29)
<img id="stable" src="https://badgen.net/badge/channel/stable/green" alt="Channel: stable" />
### Highlights
- [Patches] Support new XenServer Updates system. See [our documentation](https://xen-orchestra.com/docs/updater.html#xenserver-updates). (PR [#7044](https://github.com/vatesfr/xen-orchestra/pull/7044))

View File

@@ -7,13 +7,21 @@
> Users must be able to say: “Nice enhancement, I'm eager to test it”
- [Forget SR] Changed the modal message and added a confirmation text to be sure the action is understood by the user [#7148](https://github.com/vatesfr/xen-orchestra/issues/7148) (PR [#7155](https://github.com/vatesfr/xen-orchestra/pull/7155))
- [Netbox] Ability to synchronize XO users as Netbox tenants (PR [#7158](https://github.com/vatesfr/xen-orchestra/pull/7158))
- [VM/Console] Add a message to indicate that the console view has been [disabled](https://support.citrix.com/article/CTX217766/how-to-disable-the-console-for-the-vm-in-xencenter) for this VM [#6319](https://github.com/vatesfr/xen-orchestra/issues/6319) (PR [#7161](https://github.com/vatesfr/xen-orchestra/pull/7161))
- [Restore] Show source remote and restoration time on a restored VM (PR [#7186](https://github.com/vatesfr/xen-orchestra/pull/7186))
- [Backup/Import] Show disk import status during Incremental Replication or restoration of Incremental Backup (PR [#7171](https://github.com/vatesfr/xen-orchestra/pull/7171))
- [VM Creation] Added ISO option in new VM form when creating from template with a disk [#3464](https://github.com/vatesfr/xen-orchestra/issues/3464) (PR [#7166](https://github.com/vatesfr/xen-orchestra/pull/7166))
### Bug fixes
> Users must be able to say: “I had this issue, happy to know it's fixed”
- [Remotes] Prevents the "connection failed" alert from continuing to appear after successfull connection
- [Backup/Restore] In case of snapshot with memory, create the suspend VDI on the correct SR instead of the default one
- [Import/ESXi] Handle `Cannot read properties of undefined (reading 'perDatastoreUsage')` error when importing VM without storage (PR [#7168](https://github.com/vatesfr/xen-orchestra/pull/7168))
- [Export/OVA] Handle export with resulting disk larger than 8.2GB (PR [#7183](https://github.com/vatesfr/xen-orchestra/pull/7183))
- [Self Service] Fix error displayed after adding a VM to a resource set (PR [#7144](https://github.com/vatesfr/xen-orchestra/pull/7144))
- VMs snapshotted with XO will no longer appear as regular VMs in other clients like `xe`
### Packages to release
@@ -31,6 +39,15 @@
<!--packages-start-->
- xo-remote-parser patch
- @vates/nbd-client patch
- @xen-orchestra/backups minor
- @xen-orchestra/cr-seed-cli major
- @xen-orchestra/vmware-explorer patch
- @xen-orchestra/xapi patch
- xen-api major
- xo-server patch
- xo-server-netbox minor
- xo-vmdk-to-vhd patch
- xo-web minor
<!--packages-end-->

View File

@@ -106,13 +106,13 @@ XO needs the following packages to be installed. Redis is used as a database by
For example, on Debian/Ubuntu:
```sh
apt-get install build-essential redis-server libpng-dev git python3-minimal libvhdi-utils lvm2 cifs-utils nfs-common ntfs-3g
apt-get install build-essential redis-server libpng-dev git python3-minimal libvhdi-utils lvm2 cifs-utils nfs-common
```
On Fedora/CentOS like:
```sh
dnf install redis libpng-devel git lvm2 cifs-utils make automake gcc gcc-c++ nfs-utils ntfs-3g
dnf install redis libpng-devel git libvhdi-tools lvm2 cifs-utils make automake gcc gcc-c++
```
### Make sure Redis is running

View File

@@ -123,7 +123,7 @@ Content-Type: application/x-ndjson
## Properties update
> This feature is restricted to `name_label`, `name_description` and `tags` at the moment.
> This feature is restricted to `name_label` and `name_description` at the moment.
```sh
curl \
@@ -135,30 +135,6 @@ curl \
'https://xo.example.org/rest/v0/vms/770aa52a-fd42-8faf-f167-8c5c4a237cac'
```
### Collections
For collection properties, like `tags`, it can be more practical to touch a single item without impacting the others.
An item can be created with `PUT <collection>/<item id>` and can be destroyed with `DELETE <collection>/<item id>`.
Adding a tag:
```sh
curl \
-X PUT \
-b authenticationToken=KQxQdm2vMiv7jBIK0hgkmgxKzemd8wSJ7ugFGKFkTbs \
'https://xo.example.org/rest/v0/vms/770aa52a-fd42-8faf-f167-8c5c4a237cac/tags/My%20tag'
```
Removing a tag:
```sh
curl \
-X DELETE \
-b authenticationToken=KQxQdm2vMiv7jBIK0hgkmgxKzemd8wSJ7ugFGKFkTbs \
'https://xo.example.org/rest/v0/vms/770aa52a-fd42-8faf-f167-8c5c4a237cac/tags/My%20tag'
```
## VM and VDI destruction
For a VM:
@@ -199,45 +175,9 @@ curl \
> myDisk.vhd
```
## VM Import
A VM can be imported by posting to `/rest/v0/pools/:id/vms`.
```sh
curl \
-X POST \
-b authenticationToken=KQxQdm2vMiv7jBIK0hgkmgxKzemd8wSJ7ugFGKFkTbs \
-T myDisk.raw \
'https://xo.example.org/rest/v0/pools/355ee47d-ff4c-4924-3db2-fd86ae629676/vms?sr=357bd56c-71f9-4b2a-83b8-3451dec04b8f' \
| cat
```
The `sr` query parameter can be used to specify on which SR the VM should be imported, if not specified, the default SR will be used.
> Note: the final `| cat` ensures cURL's standard output is not a TTY, which is necessary for upload stats to be dislayed.
## VDI Import
### Existing VDI
A VHD or a raw export can be imported in an existing VDI respectively at `/rest/v0/vdis/<uuid>.vhd` and `/rest/v0/vdis/<uuid>.raw`.
> Note: the size of the VDI must match exactly the size of VDI that was previously exported.
```sh
curl \
-X PUT \
-b authenticationToken=KQxQdm2vMiv7jBIK0hgkmgxKzemd8wSJ7ugFGKFkTbs \
-T myDisk.vhd \
'https://xo.example.org/rest/v0/vdis/1a269782-ea93-4c4c-897a-475365f7b674.vhd' \
| cat
```
> Note: the final `| cat` ensures cURL's standard output is not a TTY, which is necessary for upload stats to be dislayed.
### New VDI
An export can also be imported on an SR to create a new VDI at `/rest/v0/srs/<sr uuid>/vdis`.
A VHD or a raw export can be imported on an SR to create a new VDI at `/rest/v0/srs/<sr uuid>/vdis`.
```sh
curl \

View File

@@ -23,7 +23,7 @@
"node": ">=10"
},
"dependencies": {
"@xen-orchestra/fs": "^4.1.3",
"@xen-orchestra/fs": "^4.1.2",
"cli-progress": "^3.1.0",
"exec-promise": "^0.7.0",
"getopts": "^2.2.3",
@@ -31,7 +31,7 @@
"lodash": "^4.17.21",
"promise-toolbox": "^0.21.0",
"uuid": "^9.0.0",
"vhd-lib": "^4.7.0"
"vhd-lib": "^4.6.1"
},
"scripts": {
"postversion": "npm publish",

View File

@@ -1,184 +0,0 @@
'use strict'
const { VhdAbstract, VhdNegative } = require('..')
const { describe, it } = require('test')
const assert = require('assert/strict')
const { unpackHeader, unpackFooter } = require('./_utils')
const { createHeader, createFooter } = require('../_createFooterHeader')
const _computeGeometryForSize = require('../_computeGeometryForSize')
const { FOOTER_SIZE, DISK_TYPES } = require('../_constants')
const VHD_BLOCK_LENGTH = 2 * 1024 * 1024
class VhdMock extends VhdAbstract {
#blockUsed
#header
#footer
get header() {
return this.#header
}
get footer() {
return this.#footer
}
constructor(header, footer, blockUsed = new Set()) {
super()
this.#header = header
this.#footer = footer
this.#blockUsed = blockUsed
}
containsBlock(blockId) {
return this.#blockUsed.has(blockId)
}
readBlock(blockId, onlyBitmap = false) {
const bitmap = Buffer.alloc(512, 255) // bitmap are full of bit 1
const data = Buffer.alloc(2 * 1024 * 1024, 0) // empty are full of bit 0
data.writeUint8(blockId)
return {
id: blockId,
bitmap,
data,
buffer: Buffer.concat([bitmap, data]),
}
}
readBlockAllocationTable() {}
readHeaderAndFooter() {}
_readParentLocatorData(id) {}
}
describe('vhd negative', async () => {
it(`throws when uid aren't chained `, () => {
const length = 10e8
let header = unpackHeader(createHeader(length / VHD_BLOCK_LENGTH))
const geometry = _computeGeometryForSize(length)
let footer = unpackFooter(
createFooter(length, Math.floor(Date.now() / 1000), geometry, FOOTER_SIZE, DISK_TYPES.DIFFERENCING)
)
const parent = new VhdMock(header, footer)
header = unpackHeader(createHeader(length / VHD_BLOCK_LENGTH))
footer = unpackFooter(
createFooter(length, Math.floor(Date.now() / 1000), geometry, FOOTER_SIZE, DISK_TYPES.DIFFERENCING)
)
const child = new VhdMock(header, footer)
assert.throws(() => new VhdNegative(parent, child), { message: 'NOT_CHAINED' })
})
it('throws when size changed', () => {
const childLength = 10e8
const parentLength = 10e8 + 1
let header = unpackHeader(createHeader(parentLength / VHD_BLOCK_LENGTH))
let geometry = _computeGeometryForSize(parentLength)
let footer = unpackFooter(
createFooter(parentLength, Math.floor(Date.now() / 1000), geometry, FOOTER_SIZE, DISK_TYPES.DIFFERENCING)
)
const parent = new VhdMock(header, footer)
header = unpackHeader(createHeader(childLength / VHD_BLOCK_LENGTH))
geometry = _computeGeometryForSize(childLength)
header.parentUuid = footer.uuid
footer = unpackFooter(
createFooter(childLength, Math.floor(Date.now() / 1000), geometry, FOOTER_SIZE, DISK_TYPES.DIFFERENCING)
)
const child = new VhdMock(header, footer)
assert.throws(() => new VhdNegative(parent, child), { message: 'GEOMETRY_CHANGED' })
})
it('throws when child is not differencing', () => {
const length = 10e8
let header = unpackHeader(createHeader(length / VHD_BLOCK_LENGTH))
const geometry = _computeGeometryForSize(length)
let footer = unpackFooter(
createFooter(length, Math.floor(Date.now() / 1000), geometry, FOOTER_SIZE, DISK_TYPES.DIFFERENCING)
)
const parent = new VhdMock(header, footer)
header = unpackHeader(createHeader(length / VHD_BLOCK_LENGTH))
header.parentUuid = footer.uuid
footer = unpackFooter(
createFooter(length, Math.floor(Date.now() / 1000), geometry, FOOTER_SIZE, DISK_TYPES.DYNAMIC)
)
const child = new VhdMock(header, footer)
assert.throws(() => new VhdNegative(parent, child), { message: 'CHILD_NOT_DIFFERENCING' })
})
it(`throws when writing into vhd negative `, async () => {
const length = 10e8
let header = unpackHeader(createHeader(length / VHD_BLOCK_LENGTH))
const geometry = _computeGeometryForSize(length)
let footer = unpackFooter(
createFooter(length, Math.floor(Date.now() / 1000), geometry, FOOTER_SIZE, DISK_TYPES.DIFFERENCING)
)
const parent = new VhdMock(header, footer)
const parentUuid = footer.uuid
header = unpackHeader(createHeader(length / VHD_BLOCK_LENGTH))
header.parentUuid = parentUuid
footer = unpackFooter(
createFooter(length, Math.floor(Date.now() / 1000), geometry, FOOTER_SIZE, DISK_TYPES.DIFFERENCING)
)
const child = new VhdMock(header, footer)
const vhd = new VhdNegative(parent, child)
// await assert.rejects( ()=> vhd.writeFooter())
assert.throws(() => vhd.writeHeader())
assert.throws(() => vhd.writeBlockAllocationTable())
assert.throws(() => vhd.writeEntireBlock())
assert.throws(() => vhd.mergeBlock(), { message: `can't coalesce block into a vhd negative` })
})
it('normal case', async () => {
const length = 10e8
let header = unpackHeader(createHeader(length / VHD_BLOCK_LENGTH))
let geometry = _computeGeometryForSize(length)
let footer = unpackFooter(
createFooter(length, Math.floor(Date.now() / 1000), geometry, FOOTER_SIZE, DISK_TYPES.DYNAMIC)
)
const parent = new VhdMock(header, footer, new Set([1, 3]))
const parentUuid = footer.uuid
header = unpackHeader(createHeader(length / VHD_BLOCK_LENGTH))
header.parentUuid = parentUuid
geometry = _computeGeometryForSize(length)
footer = unpackFooter(
createFooter(length, Math.floor(Date.now() / 1000), geometry, FOOTER_SIZE, DISK_TYPES.DIFFERENCING)
)
const childUuid = footer.uuid
const child = new VhdMock(header, footer, new Set([2, 3]))
const vhd = new VhdNegative(parent, child)
assert.equal(vhd.header.parentUuid.equals(childUuid), true)
assert.equal(vhd.footer.diskType, DISK_TYPES.DIFFERENCING)
await vhd.readBlockAllocationTable()
await vhd.readHeaderAndFooter()
await vhd.readParentLocator(0)
assert.equal(vhd.header.parentUuid, childUuid)
assert.equal(vhd.footer.diskType, DISK_TYPES.DIFFERENCING)
assert.equal(vhd.containsBlock(1), false)
assert.equal(vhd.containsBlock(2), true)
assert.equal(vhd.containsBlock(3), true)
assert.equal(vhd.containsBlock(4), false)
const expected = [0, 1, 0, 3, 0]
const expectedBitmap = Buffer.alloc(512, 255) // bitmap must always be full of bit 1
for (let index = 0; index < 5; index++) {
if (vhd.containsBlock(index)) {
const { id, data, bitmap } = await vhd.readBlock(index)
assert.equal(index, id)
assert.equal(expectedBitmap.equals(bitmap), true)
assert.equal(data.readUInt8(0), expected[index])
} else {
assert.equal([2, 3].includes(index), false)
}
}
})
})

View File

@@ -1,84 +0,0 @@
'use strict'
const UUID = require('uuid')
const { DISK_TYPES } = require('../_constants')
const { VhdAbstract } = require('./VhdAbstract')
const { computeBlockBitmapSize } = require('./_utils')
const assert = require('node:assert')
/**
* Build an incremental VHD which can be applied to a child to revert to the state of its parent.
* @param {*} parent
* @param {*} descendant
*/
class VhdNegative extends VhdAbstract {
#parent
#child
get header() {
// we want to have parent => child => negative
// where => means " is the parent of "
return {
...this.#parent.header,
parentUuid: this.#child.footer.uuid,
}
}
get footer() {
// by construct a negative vhd is differencing disk
return {
...this.#parent.footer,
diskType: DISK_TYPES.DIFFERENCING,
}
}
constructor(parent, child) {
super()
this.#parent = parent
this.#child = child
assert.strictEqual(UUID.stringify(child.header.parentUuid), UUID.stringify(parent.footer.uuid), 'NOT_CHAINED')
assert.strictEqual(child.footer.diskType, DISK_TYPES.DIFFERENCING, 'CHILD_NOT_DIFFERENCING')
// we don't want to handle alignment and missing block for now
// last block may contains partly empty data when changing size
assert.strictEqual(child.footer.currentSize, parent.footer.currentSize, 'GEOMETRY_CHANGED')
}
async readBlockAllocationTable() {
return Promise.all([this.#parent.readBlockAllocationTable(), this.#child.readBlockAllocationTable()])
}
containsBlock(blockId) {
return this.#child.containsBlock(blockId)
}
async readHeaderAndFooter() {
return Promise.all([this.#parent.readHeaderAndFooter(), this.#child.readHeaderAndFooter()])
}
async readBlock(blockId, onlyBitmap = false) {
// only read the content of the first vhd containing this block
if (this.#parent.containsBlock(blockId)) {
return this.#parent.readBlock(blockId, onlyBitmap)
}
const bitmap = Buffer.alloc(computeBlockBitmapSize(this.header.blockSize), 255) // bitmap are full of bit 1
const data = Buffer.alloc(this.header.blockSize, 0) // empty are full of bit 0
return {
id: blockId,
bitmap,
data,
buffer: Buffer.concat([bitmap, data]),
}
}
mergeBlock(child, blockId) {
throw new Error(`can't coalesce block into a vhd negative`)
}
_readParentLocatorData(id) {
return this.#parent._readParentLocatorData(id)
}
}
exports.VhdNegative = VhdNegative

View File

@@ -120,8 +120,7 @@ const VhdSynthetic = class VhdSynthetic extends VhdAbstract {
}
// add decorated static method
// until is not included in the result , the chain will stop at its child
VhdSynthetic.fromVhdChain = Disposable.factory(async function* fromVhdChain(handler, childPath, { until } = {}) {
VhdSynthetic.fromVhdChain = Disposable.factory(async function* fromVhdChain(handler, childPath) {
let vhdPath = childPath
let vhd
const vhds = []
@@ -129,11 +128,8 @@ VhdSynthetic.fromVhdChain = Disposable.factory(async function* fromVhdChain(hand
vhd = yield openVhd(handler, vhdPath)
vhds.unshift(vhd) // from oldest to most recent
vhdPath = resolveRelativeFromFile(vhdPath, vhd.header.parentUnicodeName)
} while (vhd.footer.diskType !== DISK_TYPES.DYNAMIC && vhdPath !== until)
} while (vhd.footer.diskType !== DISK_TYPES.DYNAMIC)
if (until !== undefined && vhdPath !== until) {
throw new Error(`Didn't find ${until} as a parent of ${childPath}`)
}
const synthetic = new VhdSynthetic(vhds)
await synthetic.readHeaderAndFooter()
yield synthetic

View File

@@ -2,10 +2,6 @@
const { dirname, resolve } = require('path')
const resolveRelativeFromFile = (file, path) => {
if (file.startsWith('/')) {
return resolve(dirname(file), path)
}
return resolve('/', dirname(file), path).slice(1)
}
const resolveRelativeFromFile = (file, path) => resolve('/', dirname(file), path).slice(1)
module.exports = resolveRelativeFromFile

View File

@@ -13,5 +13,4 @@ exports.VhdAbstract = require('./Vhd/VhdAbstract').VhdAbstract
exports.VhdDirectory = require('./Vhd/VhdDirectory').VhdDirectory
exports.VhdFile = require('./Vhd/VhdFile').VhdFile
exports.VhdSynthetic = require('./Vhd/VhdSynthetic').VhdSynthetic
exports.VhdNegative = require('./Vhd/VhdNegative').VhdNegative
exports.Constants = require('./_constants')

View File

@@ -1,7 +1,7 @@
{
"private": false,
"name": "vhd-lib",
"version": "4.7.0",
"version": "4.6.1",
"license": "AGPL-3.0-or-later",
"description": "Primitives for VHD file handling",
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/packages/vhd-lib",
@@ -20,7 +20,7 @@
"@vates/read-chunk": "^1.2.0",
"@vates/stream-reader": "^0.1.0",
"@xen-orchestra/async-map": "^0.1.2",
"@xen-orchestra/fs": "^4.1.3",
"@xen-orchestra/fs": "^4.1.2",
"@xen-orchestra/log": "^0.6.0",
"async-iterator-to-stream": "^1.0.2",
"decorator-synchronized": "^0.6.0",
@@ -33,7 +33,7 @@
"uuid": "^9.0.0"
},
"devDependencies": {
"@xen-orchestra/fs": "^4.1.3",
"@xen-orchestra/fs": "^4.1.2",
"execa": "^5.0.0",
"get-stream": "^6.0.0",
"rimraf": "^5.0.1",

View File

@@ -1,7 +1,7 @@
{
"private": false,
"name": "xapi-explore-sr",
"version": "0.4.2",
"version": "0.4.1",
"license": "ISC",
"description": "Display the list of VDIs (unmanaged and snapshots included) of a SR",
"keywords": [
@@ -40,7 +40,7 @@
"human-format": "^1.0.0",
"lodash": "^4.17.4",
"pw": "^0.0.4",
"xen-api": "^2.0.0"
"xen-api": "^1.3.6"
},
"scripts": {
"postversion": "npm publish"

View File

@@ -10,6 +10,6 @@
"readable-stream": "^4.4.2",
"source-map-support": "^0.5.21",
"throttle": "^1.0.3",
"vhd-lib": "^4.7.0"
"vhd-lib": "^4.6.1"
}
}

View File

@@ -1,7 +1,7 @@
{
"private": false,
"name": "xen-api",
"version": "2.0.0",
"version": "1.3.6",
"license": "ISC",
"description": "Connector to the Xen API",
"keywords": [

View File

@@ -50,7 +50,6 @@ Usage:
Examples:
xo-cli rest del tasks/<task id>
xo-cli rest del vms/<vm id>/tags/<tag>
xo-cli rest get <collection> [fields=<fields>] [filter=<filter>] [limit=<limit>]
List objects in a REST API collection.
@@ -123,18 +122,6 @@ Usage:
Examples:
xo-cli rest post tasks/<task id>/actions/abort
xo-cli rest post vms/<VM UUID>/actions/snapshot name_label='My snapshot'
xo-cli rest put <collection>/<item id> <name>=<value>...
Put a item in a collection
<collection>/<item id>
Full path of the item to add
<name>=<value>...
Properties of the item
Examples:
xo-cli rest put vms/<vm id>/tags/<tag>
```
#### Register your XO instance

View File

@@ -68,7 +68,6 @@ Usage:
Examples:
xo-cli rest del tasks/<task id>
xo-cli rest del vms/<vm id>/tags/<tag>
xo-cli rest get <collection> [fields=<fields>] [filter=<filter>] [limit=<limit>]
List objects in a REST API collection.
@@ -141,18 +140,6 @@ Usage:
Examples:
xo-cli rest post tasks/<task id>/actions/abort
xo-cli rest post vms/<VM UUID>/actions/snapshot name_label='My snapshot'
xo-cli rest put <collection>/<item id> <name>=<value>...
Put a item in a collection
<collection>/<item id>
Full path of the item to add
<name>=<value>...
Properties of the item
Examples:
xo-cli rest put vms/<vm id>/tags/<tag>
```
#### Register your XO instance

View File

@@ -274,7 +274,6 @@ const help = wrap(
Examples:
$name rest del tasks/<task id>
$name rest del vms/<vm id>/tags/<tag>
$name rest get <collection> [fields=<fields>] [filter=<filter>] [limit=<limit>]
List objects in a REST API collection.
@@ -348,18 +347,6 @@ const help = wrap(
$name rest post tasks/<task id>/actions/abort
$name rest post vms/<VM UUID>/actions/snapshot name_label='My snapshot'
$name rest put <collection>/<item id> <name>=<value>...
Put a item in a collection
<collection>/<item id>
Full path of the item to add
<name>=<value>...
Properties of the item
Examples:
$name rest put vms/<vm id>/tags/<tag>
$name v$version`.replace(/<([^>]+)>|\$(\w+)/g, function (_, arg, key) {
if (arg) {
return '<' + chalk.yellow(arg) + '>'

View File

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

View File

@@ -112,18 +112,6 @@ const COMMANDS = {
return stripPrefix(await response.text())
},
async put([path, ...params]) {
const response = await this.exec(path, {
body: JSON.stringify(parseParams(params)),
headers: {
'content-type': 'application/json',
},
method: 'PUT',
})
return stripPrefix(await response.text())
},
}
export async function rest(args) {

View File

@@ -21,7 +21,6 @@
"node": ">=6"
},
"dependencies": {
"@xen-orchestra/log": "^0.6.0",
"lodash": "^4.13.1",
"url-parse": "^1.4.7"
},

View File

@@ -4,9 +4,6 @@ import trim from 'lodash/trim'
import trimStart from 'lodash/trimStart'
import queryString from 'querystring'
import urlParser from 'url-parse'
import { createLogger } from '@xen-orchestra/log'
const { warn } = createLogger('xo:xo-remote-parser')
const NFS_RE = /^([^:]+):(?:(\d+):)?([^:?]+)(\?[^?]*)?$/
const SMB_RE = /^([^:]+):(.+)@([^@]+)\\\\([^\0?]+)(?:\0([^?]*))?(\?[^?]*)?$/
@@ -18,17 +15,6 @@ const parseOptionList = (optionList = '') => {
optionList = optionList.substring(1)
}
const parsed = queryString.parse(optionList)
// bugfix for persisting error notification
if ('error' in parsed) {
warn('Deleting "error" value in url query, resave your remote to clear this values', { error: parsed.error })
delete parsed.error
}
if ('name' in parsed) {
warn('Deleting "name" value in url query, resave your remote to clear this values', { name: parsed.name })
delete parsed.name
}
Object.keys(parsed).forEach(key => {
const val = parsed[key]
// some incorrect values have been saved in users database (introduced by #6270)

View File

@@ -1,6 +1,6 @@
{
"name": "xo-server-netbox",
"version": "1.4.0",
"version": "1.3.3",
"license": "AGPL-3.0-or-later",
"description": "Synchronizes pools managed by Xen Orchestra with Netbox",
"keywords": [

View File

@@ -381,7 +381,6 @@ class Netbox {
await this.#request(`/virtualization/clusters/?type_id=${nbClusterType.id}`),
'custom_fields.uuid'
)
delete allNbClusters.null
const nbClusters = pick(allNbClusters, xoPools)
const clustersToCreate = []
@@ -550,9 +549,7 @@ class Netbox {
// Then make them objects to map the Netbox VMs to their XO VMs
// { VM UUID → Netbox VM }
const allNbVms = keyBy(allNbVmsList, 'custom_fields.uuid')
delete allNbVms.null
const nbVms = keyBy(nbVmsList, 'custom_fields.uuid')
delete nbVms.null
// Used for name deduplication
// Start by storing the names of the VMs that have been created manually in
@@ -670,7 +667,6 @@ class Netbox {
const nbIfsList = await this.#request(`/virtualization/interfaces/?${clusterFilter}`)
// { ID → Interface }
const nbIfs = keyBy(nbIfsList, 'custom_fields.uuid')
delete nbIfs.null
const ifsToDelete = []
const ifsToUpdate = []
@@ -914,10 +910,7 @@ class Netbox {
}
}
Object.assign(
nbVms,
keyBy(await this.#request('/virtualization/virtual-machines/', 'PATCH', vmsToUpdate2), 'custom_fields.uuid')
)
Object.assign(nbVms, keyBy(await this.#request('/virtualization/virtual-machines/', 'PATCH', vmsToUpdate2)))
log.info(`Done synchronizing ${xoPools.length} pools with Netbox`, { pools: xoPools })
}

View File

@@ -76,8 +76,6 @@ defaultSignInPage = '/signin'
throttlingDelay = '2 seconds'
[backups]
autoUnmountPartitionDelay = '24h'
disableMergeWorker = false
# Mode to use for newly created backup directories

View File

@@ -119,7 +119,7 @@ Content-Type: application/x-ndjson
## Properties update
> This feature is restricted to `name_label`, `name_description` and `tags` at the moment.
> This feature is restricted to `name_label` and `name_description` at the moment.
```sh
curl \
@@ -131,30 +131,6 @@ curl \
'https://xo.company.lan/rest/v0/vms/770aa52a-fd42-8faf-f167-8c5c4a237cac'
```
### Collections
For collection properties, like `tags`, it can be more practical to touch a single item without impacting the others.
An item can be created with `PUT <collection>/<item id>` and can be destroyed with `DELETE <collection>/<item id>`.
Adding a tag:
```sh
curl \
-X PUT \
-b authenticationToken=KQxQdm2vMiv7jBIK0hgkmgxKzemd8wSJ7ugFGKFkTbs \
'https://xo.company.lan/rest/v0/vms/770aa52a-fd42-8faf-f167-8c5c4a237cac/tags/My%20tag'
```
Removing a tag:
```sh
curl \
-X DELETE \
-b authenticationToken=KQxQdm2vMiv7jBIK0hgkmgxKzemd8wSJ7ugFGKFkTbs \
'https://xo.company.lan/rest/v0/vms/770aa52a-fd42-8faf-f167-8c5c4a237cac/tags/My%20tag'
```
## VM destruction
```sh
@@ -180,23 +156,6 @@ curl \
> myVM.xva
```
## VM Import
A VM can be imported by posting to `/rest/v0/pools/:id/vms`.
```sh
curl \
-X POST \
-b authenticationToken=KQxQdm2vMiv7jBIK0hgkmgxKzemd8wSJ7ugFGKFkTbs \
-T myDisk.raw \
'https://xo.company.lan/rest/v0/pools/355ee47d-ff4c-4924-3db2-fd86ae629676/vms?sr=357bd56c-71f9-4b2a-83b8-3451dec04b8f' \
| cat
```
The `sr` query parameter can be used to specify on which SR the VM should be imported, if not specified, the default SR will be used.
> Note: the final `| cat` ensures cURL's standard output is not a TTY, which is necessary for upload stats to be dislayed.
## VDI destruction
```sh
@@ -219,26 +178,7 @@ curl \
## VDI Import
### Existing VDI
A VHD or a raw export can be imported in an existing VDI respectively at `/rest/v0/vdis/<uuid>.vhd` and `/rest/v0/vdis/<uuid>.raw`.
> Note: the size of the VDI must match exactly the size of VDI that was previously exported.
```sh
curl \
-X PUT \
-b authenticationToken=KQxQdm2vMiv7jBIK0hgkmgxKzemd8wSJ7ugFGKFkTbs \
-T myDisk.vhd \
'https://xo.company.lan/rest/v0/vdis/1a269782-ea93-4c4c-897a-475365f7b674.vhd' \
| cat
```
> Note: the final `| cat` ensures cURL's standard output is not a TTY, which is necessary for upload stats to be dislayed.
### New VDI
An export can also be imported on an SR to create a new VDI at `/rest/v0/srs/<sr uuid>/vdis`.
A VHD or a raw export can be imported on an SR to create a new VDI at `/rest/v0/srs/<sr uuid>/vdis`.
```sh
curl \

View File

@@ -1,7 +1,7 @@
{
"private": true,
"name": "xo-server",
"version": "5.129.0",
"version": "5.126.0",
"license": "AGPL-3.0-or-later",
"description": "Server part of Xen-Orchestra",
"keywords": [
@@ -33,26 +33,26 @@
"@vates/cached-dns.lookup": "^1.0.0",
"@vates/compose": "^2.1.0",
"@vates/decorate-with": "^2.0.0",
"@vates/disposable": "^0.1.5",
"@vates/disposable": "^0.1.4",
"@vates/event-listeners-manager": "^1.0.1",
"@vates/multi-key-map": "^0.2.0",
"@vates/multi-key-map": "^0.1.0",
"@vates/otp": "^1.0.0",
"@vates/parse-duration": "^0.1.1",
"@vates/predicates": "^1.1.0",
"@vates/read-chunk": "^1.2.0",
"@xen-orchestra/async-map": "^0.1.2",
"@xen-orchestra/backups": "^0.44.2",
"@xen-orchestra/backups": "^0.43.2",
"@xen-orchestra/cron": "^1.0.6",
"@xen-orchestra/defined": "^0.0.1",
"@xen-orchestra/emit-async": "^1.0.0",
"@xen-orchestra/fs": "^4.1.3",
"@xen-orchestra/fs": "^4.1.2",
"@xen-orchestra/log": "^0.6.0",
"@xen-orchestra/mixin": "^0.1.0",
"@xen-orchestra/mixins": "^0.14.0",
"@xen-orchestra/self-signed": "^0.1.3",
"@xen-orchestra/template": "^0.1.0",
"@xen-orchestra/vmware-explorer": "^0.3.1",
"@xen-orchestra/xapi": "^4.0.0",
"@xen-orchestra/vmware-explorer": "^0.3.0",
"@xen-orchestra/xapi": "^3.3.0",
"ajv": "^8.0.3",
"app-conf": "^2.3.0",
"async-iterator-to-stream": "^1.0.1",
@@ -127,15 +127,15 @@
"unzipper": "^0.10.5",
"uuid": "^9.0.0",
"value-matcher": "^0.2.0",
"vhd-lib": "^4.7.0",
"vhd-lib": "^4.6.1",
"ws": "^8.2.3",
"xdg-basedir": "^5.1.0",
"xen-api": "^2.0.0",
"xen-api": "^1.3.6",
"xo-acl-resolver": "^0.4.1",
"xo-collection": "^0.5.0",
"xo-common": "^0.8.0",
"xo-remote-parser": "^0.9.2",
"xo-vmdk-to-vhd": "^2.5.7"
"xo-vmdk-to-vhd": "^2.5.6"
},
"devDependencies": {
"@babel/cli": "^7.0.0",

View File

@@ -277,10 +277,6 @@ importVmBackup.params = {
sr: {
type: 'string',
},
useDifferentialRestore: {
type: 'boolean',
optional: true
}
}
export function checkBackup({ id, settings, sr }) {
@@ -384,47 +380,3 @@ fetchFiles.params = {
type: 'string',
},
}
export function listMountedPartitions() {
return this.listMountedPartitions()
}
listMountedPartitions.permission = 'admin'
export function mountPartition({ remote, disk, partition }) {
return this.mountPartition(remote, disk, partition)
}
mountPartition.permission = 'admin'
mountPartition.params = {
disk: {
type: 'string',
},
partition: {
optional: true,
type: 'string',
},
remote: {
type: 'string',
},
}
export function unmountPartition({ remote, disk, partition }) {
return this.unmountPartition(remote, disk, partition)
}
unmountPartition.permission = 'admin'
unmountPartition.params = {
disk: {
type: 'string',
},
partition: {
optional: true,
type: 'string',
},
remote: {
type: 'string',
},
}

View File

@@ -63,7 +63,7 @@ export async function copyVm({ vm, sr }) {
const input = await srcXapi.VM_export(vm._xapiRef)
// eslint-disable-next-line no-console
console.log('import full VM...')
await tgtXapi.VM_destroy(await tgtXapi.VM_import(input, sr._xapiRef))
await tgtXapi.VM_destroy((await tgtXapi.importVm(input, { srId: sr })).$ref)
}
}

View File

@@ -1297,8 +1297,7 @@ async function import_({ data, sr, type = 'xva', url }) {
throw invalidParameters('URL import is only compatible with XVA')
}
const ref = await xapi.VM_import(await hrp(url), sr._xapiRef)
return xapi.call('VM.get_by_uuid', ref)
return (await xapi.importVm(await hrp(url), { srId, type })).$id
}
return {

View File

@@ -965,7 +965,10 @@ async function _importGlusterVM(xapi, template, lvmsrId) {
namespace: 'xosan',
version: template.version,
})
const newVM = await xapi.VM_import(templateStream, this.getObject(lvmsrId, 'SR')._xapiRef)
const newVM = await xapi.importVm(templateStream, {
srId: lvmsrId,
type: 'xva',
})
await xapi.editVm(newVM, {
autoPoweron: true,
name_label: 'XOSAN imported VM',

View File

@@ -1,6 +1,6 @@
import { asyncEach } from '@vates/async-each'
import { createLogger } from '@xen-orchestra/log'
import { defer } from 'golike-defer'
import { Task } from '@xen-orchestra/mixins/Tasks.mjs'
const ENUM_PROVISIONING = {
Thin: 'thin',
@@ -9,48 +9,35 @@ const ENUM_PROVISIONING = {
const LV_NAME = 'thin_device'
const PROVISIONING = Object.values(ENUM_PROVISIONING)
const VG_NAME = 'linstor_group'
const XOSTOR_DEPENDENCIES = ['xcp-ng-release-linstor', 'xcp-ng-linstor']
const _XOSTOR_DEPENDENCIES = ['xcp-ng-release-linstor', 'xcp-ng-linstor']
const XOSTOR_DEPENDENCIES = _XOSTOR_DEPENDENCIES.join(',')
const log = createLogger('xo:api:pool')
function pluginCall(xapi, host, plugin, fnName, args) {
return Task.run(
{ properties: { name: `call plugin on: ${host.name_label}`, objectId: host.uuid, plugin, fnName, args } },
() => xapi.call('host.call_plugin', host._xapiRef, plugin, fnName, args)
)
return xapi.call('host.call_plugin', host._xapiRef, plugin, fnName, args)
}
async function destroyVolumeGroup(xapi, host, force) {
return Task.run(
{ properties: { name: `destroy volume group on ${host.name_label}`, objectId: host.uuid, vgName: VG_NAME } },
() =>
pluginCall(xapi, host, 'lvm.py', 'destroy_volume_group', {
vg_name: VG_NAME,
force: String(force),
})
)
log.info(`Trying to delete the ${VG_NAME} volume group.`, { hostId: host.id })
return pluginCall(xapi, host, 'lvm.py', 'destroy_volume_group', {
vg_name: VG_NAME,
force: String(force),
})
}
async function installOrUpdateDependencies(host, method = 'install') {
return Task.run(
{
properties: {
dependencies: XOSTOR_DEPENDENCIES,
name: `${method} XOSTOR dependencies on ${host.name_label}`,
objectId: host.uuid,
},
},
async () => {
if (method !== 'install' && method !== 'update') {
throw new Error('Invalid method')
}
if (method !== 'install' && method !== 'update') {
throw new Error('Invalid method')
}
const xapi = this.getXapi(host)
for (const _package of XOSTOR_DEPENDENCIES) {
await pluginCall(xapi, host, 'updater.py', method, {
packages: _package,
})
}
}
)
const xapi = this.getXapi(host)
log.info(`Trying to ${method} XOSTOR dependencies (${XOSTOR_DEPENDENCIES})`, { hostId: host.id })
for (const _package of _XOSTOR_DEPENDENCIES) {
await pluginCall(xapi, host, 'updater.py', method, {
packages: _package,
})
}
}
export function installDependencies({ host }) {
@@ -79,57 +66,53 @@ updateDependencies.resolve = {
export async function formatDisks({ disks, force, host, ignoreFileSystems, provisioning }) {
const rawDisks = disks.join(',')
return Task.run(
{ properties: { disks, name: `format disks on ${host.name_label}`, objectId: host.uuid } },
async () => {
const xapi = this.getXapi(host)
const xapi = this.getXapi(host)
const lvmPlugin = (fnName, args) => pluginCall(xapi, host, 'lvm.py', fnName, args)
const lvmPlugin = (fnName, args) => pluginCall(xapi, host, 'lvm.py', fnName, args)
log.info(`Format disks (${rawDisks}) with force: ${force}`, { hostId: host.id })
if (force) {
await destroyVolumeGroup(xapi, host, force)
}
if (force) {
await destroyVolumeGroup(xapi, host, force)
}
// ATM we are unable to correctly identify errors (error.code can be used for multiple errors.)
// so we are just adding some suggestion of "why there is this error"
// Error handling will be improved as errors are discovered and understood
try {
await lvmPlugin('create_physical_volume', {
devices: rawDisks,
ignore_existing_filesystems: String(ignoreFileSystems),
force: String(force),
})
} catch (error) {
if (error.code === 'LVM_ERROR(5)') {
error.params = error.params.concat([
"[XO] This error can be triggered if one of the disks is a 'tapdevs' disk.",
'[XO] This error can be triggered if one of the disks have children',
])
}
throw error
}
try {
await lvmPlugin('create_volume_group', {
devices: rawDisks,
vg_name: VG_NAME,
})
} catch (error) {
if (error.code === 'LVM_ERROR(5)') {
error.params = error.params.concat([
"[XO] This error can be triggered if a VG 'linstor_group' is already present on the host.",
])
}
throw error
}
if (provisioning === ENUM_PROVISIONING.Thin) {
await lvmPlugin('create_thin_pool', {
lv_name: LV_NAME,
vg_name: VG_NAME,
})
}
// ATM we are unable to correctly identify errors (error.code can be used for multiple errors.)
// so we are just adding some suggestion of "why there is this error"
// Error handling will be improved as errors are discovered and understood
try {
await lvmPlugin('create_physical_volume', {
devices: rawDisks,
ignore_existing_filesystems: String(ignoreFileSystems),
force: String(force),
})
} catch (error) {
if (error.code === 'LVM_ERROR(5)') {
error.params = error.params.concat([
"[XO] This error can be triggered if one of the disks is a 'tapdevs' disk.",
'[XO] This error can be triggered if one of the disks have children',
])
}
)
throw error
}
try {
await lvmPlugin('create_volume_group', {
devices: rawDisks,
vg_name: VG_NAME,
})
} catch (error) {
if (error.code === 'LVM_ERROR(5)') {
error.params = error.params.concat([
"[XO] This error can be triggered if a VG 'linstor_group' is already present on the host.",
])
}
throw error
}
if (provisioning === ENUM_PROVISIONING.Thin) {
await lvmPlugin('create_thin_pool', {
lv_name: LV_NAME,
vg_name: VG_NAME,
})
}
}
formatDisks.description = 'Format disks for a XOSTOR use'
formatDisks.permission = 'admin'
@@ -148,91 +131,83 @@ export const create = defer(async function (
$defer,
{ description, disksByHost, force, ignoreFileSystems, name, provisioning, replication }
) {
const task = await this.tasks.create({ name: `creation of XOSTOR: ${name}`, type: 'xo:xostor:create' })
return task.run(async () => {
const hostIds = Object.keys(disksByHost)
const hostIds = Object.keys(disksByHost)
const tmpBoundObjectId = `tmp_${hostIds.join(',')}_${Math.random().toString(32).slice(2)}`
const tmpBoundObjectId = `tmp_${hostIds.join(',')}_${Math.random().toString(32).slice(2)}`
const license = await Task.run({ properties: { name: 'license check' } }, async () => {
const xostorLicenses = await this.getLicenses({ productType: 'xostor' })
const xostorLicenses = await this.getLicenses({ productType: 'xostor' })
const now = Date.now()
const availableLicenses = xostorLicenses.filter(
({ boundObjectId, expires }) => boundObjectId === undefined && (expires === undefined || expires > now)
)
const now = Date.now()
const availableLicenses = xostorLicenses.filter(
({ boundObjectId, expires }) => boundObjectId === undefined && (expires === undefined || expires > now)
)
let _license = availableLicenses.find(({ productId }) => productId === 'xostor')
let license = availableLicenses.find(license => license.productId === 'xostor')
if (_license === undefined) {
_license = availableLicenses.find(({ productId }) => productId === 'xostor.trial')
}
if (license === undefined) {
license = availableLicenses.find(license => license.productId === 'xostor.trial')
}
if (_license === undefined) {
_license = await this.createBoundXostorTrialLicense({
boundObjectId: tmpBoundObjectId,
})
} else {
await this.bindLicense({
licenseId: _license.id,
boundObjectId: tmpBoundObjectId,
})
}
$defer.onFailure(() =>
this.unbindLicense({
licenseId: _license.id,
productId: _license.productId,
boundObjectId: tmpBoundObjectId,
})
)
return _license
if (license === undefined) {
license = await this.createBoundXostorTrialLicense({
boundObjectId: tmpBoundObjectId,
})
const hosts = hostIds.map(hostId => this.getObject(hostId, 'host'))
if (!hosts.every(host => host.$pool === hosts[0].$pool)) {
// we need to do this test to ensure it won't create a partial LV group with only the host of the pool of the first master
throw new Error('All hosts must be in the same pool')
}
const boundInstallDependencies = installDependencies.bind(this)
await asyncEach(hosts, host => boundInstallDependencies({ host }), { stopOnError: false })
const boundFormatDisks = formatDisks.bind(this)
await asyncEach(
hosts,
host => boundFormatDisks({ disks: disksByHost[host.id], host, force, ignoreFileSystems, provisioning }),
{
stopOnError: false,
}
)
const host = hosts[0]
const xapi = this.getXapi(host)
const srUuid = await Task.run({ properties: { name: 'creation of the storage' } }, async () => {
const srRef = await xapi.SR_create({
device_config: {
'group-name': 'linstor_group/' + LV_NAME,
redundancy: String(replication),
provisioning,
},
host: host.id,
name_description: description,
name_label: name,
shared: true,
type: 'linstor',
})
return xapi.getField('SR', srRef, 'uuid')
})
await this.rebindLicense({
} else {
await this.bindLicense({
licenseId: license.id,
oldBoundObjectId: tmpBoundObjectId,
newBoundObjectId: srUuid,
boundObjectId: tmpBoundObjectId,
})
}
$defer.onFailure(() =>
this.unbindLicense({
licenseId: license.id,
productId: license.productId,
boundObjectId: tmpBoundObjectId,
})
)
return srUuid
const hosts = hostIds.map(hostId => this.getObject(hostId, 'host'))
if (!hosts.every(host => host.$pool === hosts[0].$pool)) {
// we need to do this test to ensure it won't create a partial LV group with only the host of the pool of the first master
throw new Error('All hosts must be in the same pool')
}
const boundInstallDependencies = installDependencies.bind(this)
await asyncEach(hosts, host => boundInstallDependencies({ host }), { stopOnError: false })
const boundFormatDisks = formatDisks.bind(this)
await asyncEach(
hosts,
host => boundFormatDisks({ disks: disksByHost[host.id], host, force, ignoreFileSystems, provisioning }),
{
stopOnError: false,
}
)
const host = hosts[0]
const xapi = this.getXapi(host)
log.info(`Create XOSTOR (${name}) with provisioning: ${provisioning}`)
const srRef = await xapi.SR_create({
device_config: {
'group-name': 'linstor_group/' + LV_NAME,
redundancy: String(replication),
provisioning,
},
host: host.id,
name_description: description,
name_label: name,
shared: true,
type: 'linstor',
})
const srUuid = await xapi.getField('SR', srRef, 'uuid')
await this.rebindLicense({
licenseId: license.id,
oldBoundObjectId: tmpBoundObjectId,
newBoundObjectId: srUuid,
})
return srUuid
})
create.description = 'Create a XOSTOR storage'
@@ -249,34 +224,19 @@ create.params = {
// Also called by sr.destroy if sr.SR_type === 'linstor'
export async function destroy({ sr }) {
const task = this.tasks.create({
name: `deletion of XOSTOR: ${sr.name_label}`,
objectId: sr.uuid,
type: 'xo:xostor:destroy',
})
return task.run(async () => {
if (sr.SR_type !== 'linstor') {
throw new Error('Not a XOSTOR storage')
}
const xapi = this.getXapi(sr)
const hosts = Object.values(xapi.objects.indexes.type.host).map(host => this.getObject(host.uuid, 'host'))
if (sr.SR_type !== 'linstor') {
throw new Error('Not a XOSTOR storage')
}
const xapi = this.getXapi(sr)
const hosts = Object.values(xapi.objects.indexes.type.host).map(host => this.getObject(host.uuid, 'host'))
await Task.run({ properties: { name: 'deletion of the storage', objectId: sr.uuid } }, () =>
xapi.destroySr(sr._xapiId)
)
const license = (await this.getLicenses({ productType: 'xostor' })).find(
license => license.boundObjectId === sr.uuid
)
await Task.run({ properties: { name: 'unbind the attached license' } }, () =>
this.unbindLicense({
boundObjectId: license.boundObjectId,
productId: license.productId,
})
)
await Task.run({ properties: { name: `destroy volume group on ${hosts.length} hosts` } }, () =>
asyncEach(hosts, host => destroyVolumeGroup(xapi, host, true), { stopOnError: false })
)
await xapi.destroySr(sr._xapiId)
const license = (await this.getLicenses({ productType: 'xostor' })).find(license => license.boundObjectId === sr.uuid)
await this.unbindLicense({
boundObjectId: license.boundObjectId,
productId: license.productId,
})
return asyncEach(hosts, host => destroyVolumeGroup(xapi, host, true), { stopOnError: false })
}
destroy.description = 'Destroy a XOSTOR storage'
destroy.permission = 'admin'

View File

@@ -697,10 +697,6 @@ const TRANSFORMS = {
// -----------------------------------------------------------------
task(obj) {
let applies_to
if (obj.other_config.applies_to) {
applies_to = obj.$xapi.getObject(obj.other_config.applies_to, undefined).uuid
}
return {
allowedOperations: obj.allowed_operations,
created: toTimestamp(obj.created),
@@ -712,7 +708,7 @@ const TRANSFORMS = {
result: obj.result,
status: obj.status,
xapiRef: obj.$ref,
applies_to,
$host: link(obj, 'resident_on'),
}
},

View File

@@ -389,7 +389,7 @@ export default class Xapi extends XapiBase {
const onVmCreation = nameLabel !== undefined ? vm => vm.set_name_label(nameLabel) : null
const vm = await targetXapi._getOrWaitObject(await targetXapi.VM_import(stream, sr.$ref, onVmCreation))
const vm = await targetXapi._getOrWaitObject(await targetXapi._importVm(stream, sr, onVmCreation))
return {
vm,
@@ -674,6 +674,36 @@ export default class Xapi extends XapiBase {
)
}
@cancelable
async _importVm($cancelToken, stream, sr, onVmCreation = undefined) {
const taskRef = await this.task_create('VM import')
const query = {}
if (sr != null) {
query.sr_id = sr.$ref
}
if (onVmCreation != null) {
this.waitObject(
obj => obj != null && obj.current_operations != null && taskRef in obj.current_operations,
onVmCreation
)
}
const vmRef = await this.putResource($cancelToken, stream, '/import/', {
query,
task: taskRef,
}).then(extractOpaqueRef, error => {
// augment the error with as much relevant info as possible
error.pool_master = this.pool.$master
error.SR = sr
throw error
})
return vmRef
}
@decorateWith(deferrable)
async _importOvaVm($defer, stream, { descriptionLabel, disks, memory, nameLabel, networks, nCpus, tables }, sr) {
// 1. Create VM.
@@ -782,7 +812,7 @@ export default class Xapi extends XapiBase {
const sr = srId && this.getObject(srId)
if (type === 'xva') {
return /* await */ this._getOrWaitObject(await this.VM_import(stream, sr?.$ref))
return /* await */ this._getOrWaitObject(await this._importVm(stream, sr))
}
if (type === 'ova') {

View File

@@ -129,9 +129,7 @@ export default class BackupNg {
isMatchingVm(obj) &&
// don't match replicated VMs created by this very job otherwise
// they will be replicated again and again
!('start' in obj.blockedOperations && obj.other['xo:backup:job'] === job.id) &&
// handle xo:no-bak and xo:no-bak=reason tags. For example : VMs from Health Check
!obj.tags.some(t => t.split('=', 1)[0] === 'xo:no-bak')
!('start' in obj.blockedOperations && obj.other['xo:backup:job'] === job.id)
})(),
})
)
@@ -626,10 +624,7 @@ export default class BackupNg {
.run(async () => {
const app = this._app
const xapi = app.getXapi(srId)
const restoredId = await this.importVmBackupNg(backupId, srId, {
...settings,
additionnalVmTag: 'xo:no-bak=Health Check',
})
const restoredId = await this.importVmBackupNg(backupId, srId, settings)
const restoredVm = xapi.getObject(restoredId)
try {

View File

@@ -1,12 +1,5 @@
import Disposable from 'promise-toolbox/Disposable'
import isPromise from 'promise-toolbox/isPromise'
import { asyncEach } from '@vates/async-each'
import { createLogger } from '@xen-orchestra/log'
import { decorateWith } from '@vates/decorate-with'
import { execa } from 'execa'
import { MultiKeyMap } from '@vates/multi-key-map'
const { warn } = createLogger('xo:mixins:file-restore-ng')
// - [x] list partitions
// - [x] list files in a partition
@@ -29,8 +22,6 @@ const { warn } = createLogger('xo:mixins:file-restore-ng')
// - [ ] getMountedPartitions
// - [ ] unmountPartition
export default class BackupNgFileRestore {
#mounts = new MultiKeyMap()
constructor(app) {
this._app = app
@@ -40,16 +31,6 @@ export default class BackupNgFileRestore {
await Promise.all([execa('losetup', ['-D']), execa('vgchange', ['-an'])])
await execa('pvscan', ['--cache'])
})
app.hooks.on('stop', () =>
asyncEach(
this.#mounts.values(),
async pDisposable => {
await (await pDisposable).dispose()
},
{ stopOnError: false }
)
)
}
async fetchBackupNgPartitionFiles(remoteId, diskId, partitionId, paths, format) {
@@ -127,77 +108,4 @@ export default class BackupNgFileRestore {
adapter.listPartitionFiles(diskId, partitionId, path)
)
}
listMountedPartitions() {
const mounts = []
for (const [key, disposable] of this.#mounts.entries()) {
if (!isPromise(disposable)) {
const [remote, disk, partition] = key
mounts.push({ remote, disk, partition, path: disposable.value })
}
}
return mounts
}
@decorateWith(Disposable.factory)
*_mountPartition(remoteId, diskId, partitionId) {
const adapter = yield this._app.getBackupsRemoteAdapter(remoteId)
// yield(2) the disposable to use it
// yield(1) the value to make it available
yield yield adapter.getPartition(diskId, partitionId)
}
async mountPartition(remoteId, diskId, partitionId) {
const mounts = this.#mounts
const key = [remoteId, diskId, partitionId]
let pDisposable = mounts.get(key)
if (pDisposable !== undefined) {
return (await pDisposable).value
}
pDisposable = this._mountPartition(remoteId, diskId, partitionId)
mounts.set(key, pDisposable)
pDisposable.catch(() => mounts.delete(key))
const disposable = await pDisposable
// replace the promise by it's value so that it can be used directly in
// listMountedPartitions without breaking other uses
mounts.set(key, disposable)
const delay = await this._app.config.getDuration('backups.autoUnmountPartitionDelay')
if (delay !== 0) {
const dispose = disposable.dispose.bind(disposable)
const handle = setTimeout(
() =>
disposable.dispose().catch(error => {
warn('unmounting partition', { error })
}),
delay
)
disposable.dispose = () => {
clearTimeout(handle)
return dispose()
}
}
return disposable.value
}
async unmountPartition(remoteId, diskId, partitionId) {
const mounts = this.#mounts
const key = [remoteId, diskId, partitionId]
const pDisposable = mounts.get(key)
if (pDisposable === undefined) {
return
}
mounts.delete(key)
await (await pDisposable).dispose()
}
}

View File

@@ -271,13 +271,13 @@ export default class Proxy {
[namespace]: { xva },
} = await app.getResourceCatalog()
const xapi = app.getXapi(srId)
const vm = await xapi.VM_import(
const vm = await xapi.importVm(
await app.requestResource({
id: xva.id,
namespace,
version: xva.version,
}),
srId && this.getObject(srId, 'SR')._xapiRef
{ srId }
)
$defer.onFailure(() => xapi.VM_destroy(vm.$ref))

View File

@@ -453,15 +453,6 @@ export default class RestApi {
await pipeline(stream, res)
})
)
api.put(
'/:collection(vdis|vdi-snapshots)/:object.:format(vhd|raw)',
wrap(async (req, res) => {
req.length = +req.headers['content-length']
await req.xapiObject.$importContent(req, { format: req.params.format })
res.sendStatus(204)
})
)
api.get(
'/:collection(vms|vm-snapshots|vm-templates)/:object.xva',
wrap(async (req, res) => {
@@ -488,43 +479,24 @@ export default class RestApi {
res.json(result)
})
api
.patch(
'/:collection/:object',
json(),
wrap(async (req, res) => {
const obj = req.xapiObject
api.patch(
'/:collection/:object',
json(),
wrap(async (req, res) => {
const obj = req.xapiObject
const promises = []
const { body } = req
for (const key of ['name_description', 'name_label', 'tags']) {
const value = body[key]
if (value !== undefined) {
promises.push(obj['set_' + key](value))
}
const promises = []
const { body } = req
for (const key of ['name_description', 'name_label']) {
const value = body[key]
if (value !== undefined) {
promises.push(obj['set_' + key](value))
}
await promises
res.sendStatus(204)
})
)
.delete(
'/:collection/:object/tags/:tag',
wrap(async (req, res) => {
await req.xapiObject.$call('remove_tags', req.params.tag)
res.sendStatus(204)
})
)
.put(
'/:collection/:object/tags/:tag',
wrap(async (req, res) => {
await req.xapiObject.$call('add_tags', req.params.tag)
res.sendStatus(204)
})
)
}
await promises
res.sendStatus(204)
})
)
api.get(
'/:collection/:object/tasks',
@@ -576,22 +548,6 @@ export default class RestApi {
})
)
api.post(
'/:collection(pools)/:object/vms',
wrap(async (req, res) => {
let srRef
const { sr } = req.params
if (sr !== undefined) {
srRef = app.getXapiObject(sr, 'SR').$ref
}
const { $xapi } = req.xapiObject
const ref = await $xapi.VM_import(req, srRef)
res.end(await $xapi.getField('VM', ref, 'uuid'))
})
)
api.post(
'/:collection(srs)/:object/vdis',
wrap(async (req, res) => {

View File

@@ -7,7 +7,6 @@ import isEmpty from 'lodash/isEmpty.js'
import iteratee from 'lodash/iteratee.js'
import mixin from '@xen-orchestra/mixin'
import mixinLegacy from '@xen-orchestra/mixin/legacy.js'
import once from 'lodash/once.js'
import stubTrue from 'lodash/stubTrue.js'
import SslCertificate from '@xen-orchestra/mixins/SslCertificate.mjs'
import Tasks from '@xen-orchestra/mixins/Tasks.mjs'
@@ -127,16 +126,16 @@ export default class Xo extends EventEmitter {
// -----------------------------------------------------------------
_handleHttpRequest(req, res, next) {
const { path } = req
const { url } = req
const { _httpRequestWatchers: watchers } = this
const watcher = watchers[path]
const watcher = watchers[url]
if (!watcher) {
next()
return
}
if (!watcher.persistent) {
delete watchers[path]
delete watchers[url]
}
const { fn, data } = watcher
@@ -172,35 +171,35 @@ export default class Xo extends EventEmitter {
async registerHttpRequest(fn, data, { suffix = '' } = {}) {
const { _httpRequestWatchers: watchers } = this
let path
let url
do {
path = `/api/${await generateToken()}${suffix}`
} while (path in watchers)
url = `/api/${await generateToken()}${suffix}`
} while (url in watchers)
watchers[path] = {
watchers[url] = {
data,
fn,
}
return path
return url
}
async registerHttpRequestHandler(path, fn, { data = undefined, persistent = true } = {}) {
async registerHttpRequestHandler(url, fn, { data = undefined, persistent = true } = {}) {
const { _httpRequestWatchers: watchers } = this
if (path in watchers) {
throw new Error(`a handler is already registered for ${path}`)
if (url in watchers) {
throw new Error(`a handler is already registered for ${url}`)
}
watchers[path] = {
watchers[url] = {
data,
fn,
persistent,
}
}
return once(() => {
delete this._httpRequestWatchers[path]
})
async unregisterHttpRequestHandler(url) {
delete this._httpRequestWatchers[url]
}
// -----------------------------------------------------------------

View File

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

View File

@@ -1,7 +1,7 @@
{
"private": true,
"name": "xo-web",
"version": "5.130.0",
"version": "5.127.2",
"license": "AGPL-3.0-or-later",
"description": "Web interface client for Xen-Orchestra",
"keywords": [
@@ -137,7 +137,7 @@
"xo-common": "^0.8.0",
"xo-lib": "^0.11.1",
"xo-remote-parser": "^0.9.2",
"xo-vmdk-to-vhd": "^2.5.7"
"xo-vmdk-to-vhd": "^2.5.6"
},
"scripts": {
"build": "GIT_HEAD=$(git rev-parse HEAD) NODE_ENV=production gulp build",

View File

@@ -572,8 +572,6 @@ const messages = {
editBackupSmartNotResidentOn: 'Not resident on',
editBackupSmartPools: 'Pools',
editBackupSmartTags: 'Tags',
editBackupSmartTagsInfo:
"VMs with tags in the form of <b>xo:no-bak</b> or <b>xo:no-bak=Reason</b>won't be included in any backup.For example, ephemeral VMs created by health check have this tag",
sampleOfMatchingVms: 'Sample of matching VMs',
backupReplicatedVmsInfo:
'Replicated VMs (VMs with Continuous Replication or Disaster Recovery tag) must be excluded!',
@@ -864,7 +862,6 @@ const messages = {
srDisconnectAll: 'Disconnect from all hosts',
srForget: 'Forget this SR',
srsForget: 'Forget SRs',
nSrsForget: 'Forget {nSrs, number} SR{nSrs, plural, one {} other{s}}',
srRemoveButton: 'Remove this SR',
srNoVdis: 'No VDIs in this storage',
srReclaimSpace: 'Reclaim freed space',
@@ -1241,7 +1238,6 @@ const messages = {
vdiNameDescription: 'Description',
vdiPool: 'Pool',
vdiTags: 'Tags',
vdiTasks: 'VDI tasks',
vdiSize: 'Size',
vdiSr: 'SR',
vdiVms: 'VMs',
@@ -1803,7 +1799,6 @@ const messages = {
latest: 'latest',
restoreVmBackupsStart: 'Start VM{nVms, plural, one {} other {s}} after restore',
restoreVmBackupsBulkErrorTitle: 'Multi-restore error',
restoreVmUseDifferentialRestore: 'Use differential restore',
restoreMetadataBackupTitle: 'Restore {item}',
bulkRestoreMetadataBackupTitle:
'Restore {nMetadataBackups, number} metadata backup{nMetadataBackups, plural, one {} other {s}}',
@@ -2376,9 +2371,11 @@ const messages = {
srDisconnectAllModalMessage: 'This will disconnect this SR from all its hosts.',
srsDisconnectAllModalMessage:
'This will disconnect each selected SR from its host (local SR) or from every hosts of its pool (shared SR).',
forgetNSrsModalMessage: 'Are you sure you want to forget {nSrs, number} SR{nSrs, plural, one {} other{s}}?',
srForgetModalWarning:
'You will lose all the metadata, meaning all the links between the VDIs (disks) and their respective VMs. This operation cannot be undone.',
srForgetModalTitle: 'Forget SR',
srsForgetModalTitle: 'Forget selected SRs',
srForgetModalMessage: "Are you sure you want to forget this SR? VDIs on this storage won't be removed.",
srsForgetModalMessage:
"Are you sure you want to forget all the selected SRs? VDIs on these storages won't be removed.",
srAllDisconnected: 'Disconnected',
srSomeConnected: 'Partially connected',
srAllConnected: 'Connected',

View File

@@ -2268,20 +2268,15 @@ export const deleteSr = sr =>
export const fetchSrStats = (sr, granularity) => _call('sr.stats', { id: resolveId(sr), granularity })
export const forgetSr = sr => forgetSrs([sr])
export const forgetSr = sr =>
confirm({
title: _('srForgetModalTitle'),
body: _('srForgetModalMessage'),
}).then(() => _call('sr.forget', { id: resolveId(sr) }), noop)
export const forgetSrs = srs =>
confirm({
title: _('nSrsForget', { nSrs: srs.length }),
body: (
<p className='text-warning font-weight-bold'>
{_('forgetNSrsModalMessage', { nSrs: srs.length })} {_('srForgetModalWarning')}
</p>
),
strongConfirm: {
messageId: 'nSrsForget',
values: { nSrs: srs.length },
},
title: _('srsForgetModalTitle'),
body: _('srsForgetModalMessage'),
}).then(() => Promise.all(map(resolveIds(srs), id => _call('sr.forget', { id }))), noop)
export const reconnectAllHostsSr = sr =>
@@ -2589,11 +2584,11 @@ export const listVmBackups = remotes => _call('backupNg.listVmBackups', { remote
export const restoreBackup = (
backup,
sr,
{ generateNewMacAddresses = false, mapVdisSrs = {}, startOnRestore = false, useDifferentialRestore= false } = {}
{ generateNewMacAddresses = false, mapVdisSrs = {}, startOnRestore = false } = {}
) => {
const promise = _call('backupNg.importVmBackup', {
id: resolveId(backup),
settings: { mapVdisSrs: resolveIds(mapVdisSrs), newMacAddresses: generateNewMacAddresses, useDifferentialRestore },
settings: { mapVdisSrs: resolveIds(mapVdisSrs), newMacAddresses: generateNewMacAddresses },
sr: resolveId(sr),
})

View File

@@ -97,12 +97,7 @@ const SmartBackup = decorate([
</label>
<SelectPool multi onChange={effects.setPoolNotValues} value={state.pools.notValues} />
</FormGroup>
<h3>
{_('editBackupSmartTags')}
<Tooltip content={_('editBackupSmartTagsInfo')}>
<Icon icon='info' />
</Tooltip>{' '}
</h3>
<h3>{_('editBackupSmartTags')}</h3>
<hr />
<FormGroup>
<label>

View File

@@ -184,16 +184,16 @@ export default class Restore extends Component {
body: <RestoreBackupsModalBody data={data} />,
icon: 'restore',
})
.then(({ backup, generateNewMacAddresses, targetSrs: { mainSr, mapVdisSrs }, start, useDifferentialRestore }) => {
.then(({ backup, generateNewMacAddresses, targetSrs: { mainSr, mapVdisSrs }, start }) => {
if (backup == null || mainSr == null) {
error(_('backupRestoreErrorTitle'), _('backupRestoreErrorMessage'))
return
}
return restoreBackup(backup, mainSr, {
generateNewMacAddresses,
mapVdisSrs,
startOnRestore: start,
useDifferentialRestore,
})
}, noop)
.then(() => this._refreshBackupList())

View File

@@ -12,11 +12,7 @@ import { SelectSr } from 'select-objects'
const BACKUP_RENDERER = getRenderXoItemOfType('backup')
export default class RestoreBackupsModalBody extends Component {
state = {
generateNewMacAddresses: false,
targetSrs: { mainSr: undefined, mapVdisSrs: undefined },
useDifferentialRestore: false,
}
state = { generateNewMacAddresses: false, targetSrs: { mainSr: undefined, mapVdisSrs: undefined } }
get value() {
return this.state
@@ -83,17 +79,6 @@ export default class RestoreBackupsModalBody extends Component {
{_('generateNewMacAddress')}
</div>
)}
{this.state.backup.mode === 'delta' && (
<div>
<Toggle
iconSize={1}
value={this.state.useDifferentialRestore}
onChange={this.toggleState('useDifferentialRestore')}
/>{' '}
{_('restoreVmUseDifferentialRestore')}
</div>
)}
</div>
)}
</div>

View File

@@ -66,22 +66,15 @@ const FILTERS = {
@connectStore(() => ({
host: createGetObject((_, props) => props.item.$host),
appliesTo: createGetObject((_, props) => props.item.applies_to),
}))
export class TaskItem extends Component {
render() {
const { appliesTo, host, item: task } = this.props
// garbage collection task has an uuid in the desc
const showDesc = task.name_description && task.name_label !== 'Garbage Collection'
const { host, item: task } = this.props
return (
<div>
{task.name_label} ({showDesc && `${task.name_description} `}
{task.name_label} ({task.name_description && `${task.name_description} `}
on {host ? <Link to={`/hosts/${host.id}`}>{host.name_label}</Link> : `unknown host ${task.$host}`})
{appliesTo !== undefined && (
<span>
, applies to <Link to={`/srs/${appliesTo.id}`}>{appliesTo.name_label}</Link>
</span>
)}
{task.disappeared === undefined && ` ${Math.round(task.progress * 100)}%`}
</div>
)

View File

@@ -11,19 +11,19 @@ import React from 'react'
import StateButton from 'state-button'
import SortedTable from 'sorted-table'
import TabButton from 'tab-button'
import { compact, every, filter, find, forEach, get, map, reduce, some, sortedUniq, uniq } from 'lodash'
import { Sr, Vdi } from 'render-xo-item'
import { compact, every, filter, find, forEach, get, map, some, sortedUniq, uniq } from 'lodash'
import { Sr } from 'render-xo-item'
import { Container, Row, Col } from 'grid'
import {
createCollectionWrapper,
createGetObjectsOfType,
createSelector,
createFilter,
createFinder,
getCheckPermissions,
getResolvedResourceSet,
isAdmin,
} from 'selectors'
import { injectIntl } from 'react-intl'
import {
addSubscriptions,
connectStore,
@@ -57,8 +57,6 @@ import {
setBootableVbd,
subscribeResourceSets,
} from 'xo'
import { Card, CardHeader, CardBlock } from 'card'
import { FormattedRelative, injectIntl } from 'react-intl'
import { SelectResourceSetsSr, SelectSr as SelectAnySr, SelectVdi } from 'select-objects'
const compareSrs = createCompare([isSrShared])
@@ -170,55 +168,6 @@ const COLUMNS_VM_PV = [
const COLUMNS = filter(COLUMNS_VM_PV, col => col.id !== 'vbdBootableStatus')
const PROGRESS_STYLES = { margin: 0 }
const COLUMNS_VDI_TASKS = [
{
itemRenderer: task => task.name_label,
name: _('name'),
sortCriteria: 'name_label',
},
{
itemRenderer: task => <Vdi id={task.details.vdiId} />,
name: _('object'),
sortCriteria: 'details.vdiName',
},
{
itemRenderer: task => task.details.action,
name: _('action'),
sortCriteria: 'details.action',
},
{
itemRenderer: task => formatSize(task.details.length),
name: _('size'),
sortCriteria: 'details.length',
},
{
itemRenderer: task => (
<progress style={PROGRESS_STYLES} className='progress' value={task.progress * 100} max='100' />
),
name: _('progress'),
sortCriteria: 'progress',
},
{
itemRenderer: task => <FormattedRelative value={task.created * 1000} />,
name: _('taskStarted'),
sortCriteria: 'created',
},
{
itemRenderer: task => {
const started = task.created * 1000
const { progress } = task
if (progress === 0 || progress === 1) {
return // not yet started or already finished
}
return <FormattedRelative value={started + (Date.now() - started) / progress} />
},
name: _('taskEstimatedEnd'),
},
]
const INDIVIDUAL_ACTIONS = [
...(process.env.XOA_PLAN > 1
? [
@@ -505,39 +454,10 @@ class AttachDisk extends Component {
}))
@connectStore(() => {
const getAllVbds = createGetObjectsOfType('VBD')
const getTasks = createGetObjectsOfType('task')
const getDetailedImportVdiTasks = createSelector(
getTasks,
createFilter((state, props) => props.vdis, [vdi => vdi.other_config['xo:import:task'] !== undefined]),
createCollectionWrapper((tasks, vdis) =>
reduce(
vdis,
(acc, vdi) => {
const task = tasks[vdi.other_config['xo:import:task']]
const length = vdi.other_config['xo:import:length']
acc.push({
...task,
details: {
action: 'import',
length: Number(length),
vdiId: vdi.uuid,
vdiName: vdi.name_label,
},
})
return acc
},
[]
)
)
)
return (state, props) => ({
allVbds: getAllVbds(state, props),
checkPermissions: getCheckPermissions(state, props),
detailedImportVdiTasks: getDetailedImportVdiTasks(state, props),
isAdmin: isAdmin(state, props),
resolvedResourceSet: getResolvedResourceSet(state, props, !props.isAdmin && props.resourceSet !== undefined),
})
@@ -792,20 +712,6 @@ export default class TabDisks extends Component {
<IsoDevice vm={vm} />
</Col>
</Row>
<Row className='mt-1'>
<Col>
<Card>
<CardHeader>{_('vdiTasks')}</CardHeader>
<CardBlock>
<SortedTable
collection={this.props.detailedImportVdiTasks}
columns={COLUMNS_VDI_TASKS}
stateUrlParam='t'
/>
</CardBlock>
</Card>
</Col>
</Row>
</Container>
)
}

View File

@@ -123,10 +123,6 @@ async function readPackagesFromChangelog(toRelease) {
}
const { name, releaseType } = match.groups
if (name in toRelease) {
throw new Error('duplicate package to release in CHANGELOG.unreleased.md: ' + name)
}
toRelease[name] = releaseType
})
}

2715
yarn.lock

File diff suppressed because it is too large Load Diff