Compare commits
3 Commits
xo5/rebind
...
vhd_hashes
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5859d86ba0 | ||
|
|
c475c99157 | ||
|
|
722b96701a |
@@ -65,11 +65,10 @@ module.exports = {
|
||||
typescript: true,
|
||||
'eslint-import-resolver-custom-alias': {
|
||||
alias: {
|
||||
'@core': '../web-core/lib',
|
||||
'@': './src',
|
||||
},
|
||||
extensions: ['.ts'],
|
||||
packages: ['@xen-orchestra/lite', '@xen-orchestra/web'],
|
||||
packages: ['@xen-orchestra/lite'],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -160,10 +160,10 @@ export class ImportVmBackup {
|
||||
// update the stream with the negative vhd stream
|
||||
stream = await negativeVhd.stream()
|
||||
vdis[vdiRef].baseVdi = snapshotCandidate
|
||||
} catch (error) {
|
||||
} 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`, { error })
|
||||
warn(`can't use differential restore`, err)
|
||||
disposableDescendants?.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,16 @@ export const IncrementalXapi = class IncrementalXapiVmBackupRunner extends Abstr
|
||||
}
|
||||
|
||||
_mustDoSnapshot() {
|
||||
return true
|
||||
const vm = this._vm
|
||||
|
||||
const settings = this._settings
|
||||
return (
|
||||
settings.unconditionalSnapshot ||
|
||||
(!settings.offlineBackup && vm.power_state === 'Running') ||
|
||||
settings.snapshotRetention !== 0 ||
|
||||
settings.fullInterval !== 1 ||
|
||||
settings.deltaComputationMode === 'AGAINST_PARENT_VHD'
|
||||
)
|
||||
}
|
||||
|
||||
async _copy() {
|
||||
|
||||
@@ -3,7 +3,7 @@ import mapValues from 'lodash/mapValues.js'
|
||||
import ignoreErrors from 'promise-toolbox/ignoreErrors'
|
||||
import { asyncEach } from '@vates/async-each'
|
||||
import { asyncMap } from '@xen-orchestra/async-map'
|
||||
import { chainVhd, checkVhdChain, openVhd, VhdAbstract } from 'vhd-lib'
|
||||
import { chainVhd, checkVhdChain, openVhd, VhdAbstract, VhdSynthetic } from 'vhd-lib'
|
||||
import { createLogger } from '@xen-orchestra/log'
|
||||
import { decorateClass } from '@vates/decorate-with'
|
||||
import { defer } from 'golike-defer'
|
||||
@@ -143,10 +143,8 @@ export class IncrementalRemoteWriter extends MixinRemoteWriter(AbstractIncrement
|
||||
|
||||
let metadataContent = await this._isAlreadyTransferred(timestamp)
|
||||
if (metadataContent !== undefined) {
|
||||
// skip backup while being vigilant to not stuck the forked stream
|
||||
// @todo : should skip backup while being vigilant to not stuck the forked stream
|
||||
Task.info('This backup has already been transfered')
|
||||
Object.values(deltaExport.streams).forEach(stream => stream.destroy())
|
||||
return { size: 0 }
|
||||
}
|
||||
|
||||
const basename = formatFilenameDate(timestamp)
|
||||
@@ -185,6 +183,7 @@ export class IncrementalRemoteWriter extends MixinRemoteWriter(AbstractIncrement
|
||||
|
||||
const isDifferencing = isVhdDifferencing[`${id}.vhd`]
|
||||
let parentPath
|
||||
let parentVhd
|
||||
if (isDifferencing) {
|
||||
const vdiDir = dirname(path)
|
||||
parentPath = (
|
||||
@@ -206,6 +205,11 @@ export class IncrementalRemoteWriter extends MixinRemoteWriter(AbstractIncrement
|
||||
|
||||
// TODO remove when this has been done before the export
|
||||
await checkVhd(handler, parentPath)
|
||||
if(settings.deltaComputationMode === 'AGAINST_PARENT_VHD'){
|
||||
const {dispose, value } = await VhdSynthetic.fromVhdChain(handler, parentPath)
|
||||
parentVhd = value
|
||||
$defer(()=>dispose())
|
||||
}
|
||||
}
|
||||
|
||||
// don't write it as transferSize += await async function
|
||||
@@ -215,6 +219,7 @@ export class IncrementalRemoteWriter extends MixinRemoteWriter(AbstractIncrement
|
||||
// no checksum for VHDs, because they will be invalidated by
|
||||
// merges and chainings
|
||||
checksum: false,
|
||||
parentVhd,
|
||||
validator: tmpPath => checkVhd(handler, tmpPath),
|
||||
writeBlockConcurrency: this._config.writeBlockConcurrency,
|
||||
})
|
||||
|
||||
@@ -113,13 +113,13 @@ export const MixinRemoteWriter = (BaseClass = Object) =>
|
||||
)
|
||||
}
|
||||
|
||||
async _isAlreadyTransferred(timestamp) {
|
||||
_isAlreadyTransferred(timestamp) {
|
||||
const vmUuid = this._vmUuid
|
||||
const adapter = this._adapter
|
||||
const backupDir = getVmBackupDir(vmUuid)
|
||||
try {
|
||||
const actualMetadata = JSON.parse(
|
||||
await adapter._handler.readFile(`${backupDir}/${formatFilenameDate(timestamp)}.json`)
|
||||
adapter._handler.readFile(`${backupDir}/${formatFilenameDate(timestamp)}.json`)
|
||||
)
|
||||
return actualMetadata
|
||||
} catch (error) {}
|
||||
|
||||
@@ -20,7 +20,5 @@ export function split(path) {
|
||||
return parts
|
||||
}
|
||||
|
||||
// paths are made absolute otherwise fs.relative() would resolve them against working directory
|
||||
export const relativeFromFile = (file, path) => relative(dirname(normalize(file)), normalize(path))
|
||||
|
||||
export const relativeFromFile = (file, path) => relative(dirname(file), path)
|
||||
export const resolveFromFile = (file, path) => resolve('/', dirname(file), path).slice(1)
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import { describe, it } from 'test'
|
||||
import { strict as assert } from 'assert'
|
||||
|
||||
import { relativeFromFile } from './path.js'
|
||||
|
||||
describe('relativeFromFile()', function () {
|
||||
for (const [title, args] of Object.entries({
|
||||
'file absolute and path absolute': ['/foo/bar/file.vhd', '/foo/baz/path.vhd'],
|
||||
'file relative and path absolute': ['foo/bar/file.vhd', '/foo/baz/path.vhd'],
|
||||
'file absolute and path relative': ['/foo/bar/file.vhd', 'foo/baz/path.vhd'],
|
||||
'file relative and path relative': ['foo/bar/file.vhd', 'foo/baz/path.vhd'],
|
||||
})) {
|
||||
it('works with ' + title, function () {
|
||||
assert.equal(relativeFromFile(...args), '../baz/path.vhd')
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -54,10 +54,10 @@ async function handleExistingFile(root, indexPath, path) {
|
||||
await indexFile(fullPath, indexPath)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.code !== 'EEXIST') {
|
||||
} catch (err) {
|
||||
if (err.code !== 'EEXIST') {
|
||||
// there can be a symbolic link in the tree
|
||||
warn('handleExistingFile', { error })
|
||||
warn('handleExistingFile', err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -106,7 +106,7 @@ export async function watchRemote(remoteId, { root, immutabilityDuration, rebuil
|
||||
await File.liftImmutability(settingPath)
|
||||
} catch (error) {
|
||||
// file may not exists, and it's not really a problem
|
||||
info('lifting immutability on current settings', { error })
|
||||
info('lifting immutability on current settings', error)
|
||||
}
|
||||
await fs.writeFile(
|
||||
settingPath,
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||
"include": ["env.d.ts", "src/**/*", "src/**/*.vue", "../web-core/lib/**/*", "../web-core/lib/**/*.vue"],
|
||||
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
|
||||
"exclude": ["src/**/__tests__/*"],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"noEmit": true,
|
||||
"baseUrl": ".",
|
||||
"rootDir": "..",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"],
|
||||
"@core/*": ["../web-core/lib/*"]
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ export default defineConfig({
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||
'@core': fileURLToPath(new URL('../web-core/lib', import.meta.url)),
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
@@ -27,16 +27,6 @@ log.error('could not join server', {
|
||||
})
|
||||
```
|
||||
|
||||
A logging method has the following signature:
|
||||
|
||||
```ts
|
||||
interface LoggingMethod {
|
||||
(error): void
|
||||
|
||||
(message: string, data?: { error?: Error; [property: string]: any }): void
|
||||
}
|
||||
```
|
||||
|
||||
### Consumer
|
||||
|
||||
Then, at application level, configure the logs are handled:
|
||||
|
||||
@@ -45,16 +45,6 @@ log.error('could not join server', {
|
||||
})
|
||||
```
|
||||
|
||||
A logging method has the following signature:
|
||||
|
||||
```ts
|
||||
interface LoggingMethod {
|
||||
(error): void
|
||||
|
||||
(message: string, data?: { error?: Error; [property: string]: any }): void
|
||||
}
|
||||
```
|
||||
|
||||
### Consumer
|
||||
|
||||
Then, at application level, configure the logs are handled:
|
||||
|
||||
@@ -10,8 +10,7 @@
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"vue": "^3.4.13",
|
||||
"@vue/tsconfig": "^0.5.1"
|
||||
"vue": "^3.4.13"
|
||||
},
|
||||
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/web-core",
|
||||
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
|
||||
@@ -26,6 +25,6 @@
|
||||
},
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
"node": ">=8.10"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||
"include": ["env.d.ts", "lib/**/*", "lib/**/*.vue"],
|
||||
"exclude": ["lib/**/__tests__/*"],
|
||||
"compilerOptions": {
|
||||
"noEmit": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@core/*": ["./lib/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,13 @@
|
||||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||
"include": [
|
||||
"env.d.ts",
|
||||
"typed-router.d.ts",
|
||||
"src/**/*",
|
||||
"src/**/*.vue",
|
||||
"../web-core/lib/**/*",
|
||||
"../web-core/lib/**/*.vue"
|
||||
],
|
||||
"include": ["env.d.ts", "typed-router.d.ts", "src/**/*", "src/**/*.vue"],
|
||||
"exclude": ["src/**/__tests__/*"],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"noEmit": true,
|
||||
"baseUrl": ".",
|
||||
"rootDir": "..",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"],
|
||||
"@core/*": ["../web-core/lib/*"]
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ export default defineConfig({
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||
'@core': fileURLToPath(new URL('../web-core/lib', import.meta.url)),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -21,23 +21,12 @@ export default class Vif {
|
||||
MAC = '',
|
||||
} = {}
|
||||
) {
|
||||
if (device === undefined) {
|
||||
const allowedDevices = await this.call('VM.get_allowed_VIF_devices', VM)
|
||||
if (allowedDevices.length === 0) {
|
||||
const error = new Error('could not find an allowed VIF device')
|
||||
error.poolUuid = this.pool.uuid
|
||||
error.vmRef = VM
|
||||
throw error
|
||||
}
|
||||
|
||||
device = allowedDevices[0]
|
||||
}
|
||||
|
||||
const [powerState, ...rest] = await Promise.all([
|
||||
this.getField('VM', VM, 'power_state'),
|
||||
MTU ?? this.getField('network', network, 'MTU'),
|
||||
device ?? (await this.call('VM.get_allowed_VIF_devices', VM))[0],
|
||||
MTU ?? (await this.getField('network', network, 'MTU')),
|
||||
])
|
||||
;[MTU] = rest
|
||||
;[device, MTU] = rest
|
||||
|
||||
const vifRef = await this.call('VIF.create', {
|
||||
currently_attached: powerState === 'Suspended' ? currently_attached : undefined,
|
||||
|
||||
@@ -8,16 +8,11 @@
|
||||
> Users must be able to say: “Nice enhancement, I'm eager to test it”
|
||||
|
||||
- Disable search engine indexing via a `robots.txt`
|
||||
- [Stats] Support format used by XAPI 23.31
|
||||
|
||||
### Bug fixes
|
||||
|
||||
> Users must be able to say: “I had this issue, happy to know it's fixed”
|
||||
|
||||
- [Settings/XO Config] Sort backups from newest to oldest
|
||||
- [Plugins/audit] Don't log `tag.getAllConfigured` calls
|
||||
- [Remotes] Correctly clear error when the remote is tested with success
|
||||
|
||||
### Packages to release
|
||||
|
||||
> When modifying a package, add it here with its release type.
|
||||
@@ -34,12 +29,6 @@
|
||||
|
||||
<!--packages-start-->
|
||||
|
||||
- @xen-orchestra/backups patch
|
||||
- @xen-orchestra/fs patch
|
||||
- @xen-orchestra/xapi patch
|
||||
- vhd-lib patch
|
||||
- xo-server minor
|
||||
- xo-server-audit patch
|
||||
- xo-web patch
|
||||
- xo-server patch
|
||||
|
||||
<!--packages-end-->
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
{
|
||||
"name": "xen-orchestra",
|
||||
"version": "0.0.0",
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.0.0",
|
||||
"@babel/eslint-parser": "^7.13.8",
|
||||
@@ -96,7 +94,7 @@
|
||||
},
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "TURBO_TELEMETRY_DISABLED=1 turbo run build --scope xo-server --scope xo-server-'*' --scope xo-web",
|
||||
"build": "turbo run build --scope xo-server --scope xo-server-'*' --scope xo-web",
|
||||
"build:xo-lite": "turbo run build --scope @xen-orchestra/lite",
|
||||
"clean": "scripts/run-script.js --parallel clean",
|
||||
"dev": "scripts/run-script.js --parallel --concurrency 0 --verbose dev",
|
||||
|
||||
@@ -84,6 +84,9 @@ exports.VhdAbstract = class VhdAbstract {
|
||||
readBlockAllocationTable() {
|
||||
throw new Error(`reading block allocation table is not implemented`)
|
||||
}
|
||||
readBlockHashes() {
|
||||
throw new Error(`reading block hashes table is not implemented`)
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {Object} BitmapBlock
|
||||
@@ -104,6 +107,10 @@ exports.VhdAbstract = class VhdAbstract {
|
||||
throw new Error(`reading ${onlyBitmap ? 'bitmap of block' : 'block'} ${blockId} is not implemented`)
|
||||
}
|
||||
|
||||
getBlockHash(blockId){
|
||||
throw new Error(`reading block hash ${blockId} is not implemented`)
|
||||
}
|
||||
|
||||
/**
|
||||
* coalesce the block with id blockId from the child vhd into
|
||||
* this vhd
|
||||
|
||||
@@ -4,6 +4,7 @@ const { unpackHeader, unpackFooter, sectorsToBytes } = require('./_utils')
|
||||
const { createLogger } = require('@xen-orchestra/log')
|
||||
const { fuFooter, fuHeader, checksumStruct } = require('../_structs')
|
||||
const { test, set: setBitmap } = require('../_bitmap')
|
||||
const { hashBlock } = require('../hashBlock')
|
||||
const { VhdAbstract } = require('./VhdAbstract')
|
||||
const assert = require('assert')
|
||||
const { synchronized } = require('decorator-synchronized')
|
||||
@@ -75,6 +76,7 @@ function getCompressor(compressorType) {
|
||||
|
||||
exports.VhdDirectory = class VhdDirectory extends VhdAbstract {
|
||||
#uncheckedBlockTable
|
||||
#blockHashes
|
||||
#header
|
||||
footer
|
||||
#compressor
|
||||
@@ -140,6 +142,17 @@ exports.VhdDirectory = class VhdDirectory extends VhdAbstract {
|
||||
this.#blockTable = buffer
|
||||
}
|
||||
|
||||
async readBlockHashes() {
|
||||
try {
|
||||
const { buffer } = await this._readChunk('hashes')
|
||||
this.#blockHashes = JSON.parse(buffer)
|
||||
} catch (err) {
|
||||
if (err.code !== 'ENOENT') {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
containsBlock(blockId) {
|
||||
return test(this.#blockTable, blockId)
|
||||
}
|
||||
@@ -177,6 +190,11 @@ exports.VhdDirectory = class VhdDirectory extends VhdAbstract {
|
||||
const blockSuffix = blockId - blockPrefix * 1e3
|
||||
return `blocks/${blockPrefix}/${blockSuffix}`
|
||||
}
|
||||
getBlockHash(blockId) {
|
||||
if (this.#blockHashes !== undefined) {
|
||||
return this.#blockHashes[blockId]
|
||||
}
|
||||
}
|
||||
|
||||
_getFullBlockPath(blockId) {
|
||||
return this.#getChunkPath(this.#getBlockPath(blockId))
|
||||
@@ -209,6 +227,10 @@ exports.VhdDirectory = class VhdDirectory extends VhdAbstract {
|
||||
throw new Error(`reading 'bitmap of block' ${blockId} in a VhdDirectory is not implemented`)
|
||||
}
|
||||
const { buffer } = await this._readChunk(this.#getBlockPath(blockId))
|
||||
const hash = this.getBlockHash(blockId)
|
||||
if (hash) {
|
||||
assert.strictEqual(hash, hash(buffer))
|
||||
}
|
||||
return {
|
||||
id: blockId,
|
||||
bitmap: buffer.slice(0, this.bitmapSize),
|
||||
@@ -244,7 +266,7 @@ exports.VhdDirectory = class VhdDirectory extends VhdAbstract {
|
||||
assert.notStrictEqual(this.#blockTable, undefined, 'Block allocation table has not been read')
|
||||
assert.notStrictEqual(this.#blockTable.length, 0, 'Block allocation table is empty')
|
||||
|
||||
return this._writeChunk('bat', this.#blockTable)
|
||||
return Promise.all([this._writeChunk('bat', this.#blockTable), this._writeChunk('hashes', this.#blockHashes)])
|
||||
}
|
||||
|
||||
// only works if data are in the same handler
|
||||
@@ -265,8 +287,11 @@ exports.VhdDirectory = class VhdDirectory extends VhdAbstract {
|
||||
await this._handler.rename(childBlockPath, this._getFullBlockPath(blockId))
|
||||
if (!blockExists) {
|
||||
setBitmap(this.#blockTable, blockId)
|
||||
this.#blockHashes[blockId] = child.getBlockHash(blockId)
|
||||
await this.writeBlockAllocationTable()
|
||||
}
|
||||
// @todo block hashes changs may be lost if the vhd merging fail
|
||||
// should migrate to writing bat from time to time, sync with the metadata
|
||||
} catch (error) {
|
||||
if (error.code === 'ENOENT' && isResumingMerge === true) {
|
||||
// when resuming, the blocks moved since the last merge state write are
|
||||
@@ -287,6 +312,7 @@ exports.VhdDirectory = class VhdDirectory extends VhdAbstract {
|
||||
async writeEntireBlock(block) {
|
||||
await this._writeChunk(this.#getBlockPath(block.id), block.buffer)
|
||||
setBitmap(this.#blockTable, block.id)
|
||||
this.#blockHashes[block.id] = hashBlock(block.buffer)
|
||||
}
|
||||
|
||||
async _readParentLocatorData(id) {
|
||||
|
||||
@@ -96,6 +96,10 @@ const VhdSynthetic = class VhdSynthetic extends VhdAbstract {
|
||||
assert(false, `no such block ${blockId}`)
|
||||
}
|
||||
|
||||
async getBlockHash(blockId){
|
||||
return this.#getVhdWithBlock(blockId).getBlockHash(blockId)
|
||||
}
|
||||
|
||||
async readBlock(blockId, onlyBitmap = false) {
|
||||
// only read the content of the first vhd containing this block
|
||||
return await this.#getVhdWithBlock(blockId).readBlock(blockId, onlyBitmap)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use strict'
|
||||
|
||||
const { relativeFromFile } = require('@xen-orchestra/fs/path')
|
||||
const { dirname, relative } = require('path')
|
||||
|
||||
const { openVhd } = require('./openVhd')
|
||||
const { DISK_TYPES } = require('./_constants')
|
||||
@@ -21,7 +21,7 @@ module.exports = async function chain(parentHandler, parentPath, childHandler, c
|
||||
}
|
||||
await childVhd.readBlockAllocationTable()
|
||||
|
||||
const parentName = relativeFromFile(childPath, parentPath)
|
||||
const parentName = relative(dirname(childPath), parentPath)
|
||||
header.parentUuid = parentVhd.footer.uuid
|
||||
header.parentUnicodeName = parentName
|
||||
await childVhd.setUniqueParentLocator(parentName)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use strict'
|
||||
|
||||
const { createLogger } = require('@xen-orchestra/log')
|
||||
const { hashBlock } = require('./hashBlock.js')
|
||||
const { parseVhdStream } = require('./parseVhdStream.js')
|
||||
const { VhdDirectory } = require('./Vhd/VhdDirectory.js')
|
||||
const { Disposable } = require('promise-toolbox')
|
||||
@@ -8,7 +9,7 @@ const { asyncEach } = require('@vates/async-each')
|
||||
|
||||
const { warn } = createLogger('vhd-lib:createVhdDirectoryFromStream')
|
||||
|
||||
const buildVhd = Disposable.wrap(async function* (handler, path, inputStream, { concurrency, compression }) {
|
||||
const buildVhd = Disposable.wrap(async function* (handler, path, inputStream, { concurrency, compression, parentVhd }) {
|
||||
const vhd = yield VhdDirectory.create(handler, path, { compression })
|
||||
await asyncEach(
|
||||
parseVhdStream(inputStream),
|
||||
@@ -24,6 +25,10 @@ const buildVhd = Disposable.wrap(async function* (handler, path, inputStream, {
|
||||
await vhd.writeParentLocator({ ...item, data: item.buffer })
|
||||
break
|
||||
case 'block':
|
||||
if (parentVhd !== undefined && hashBlock(item.buffer) === parentVhd.getBlockHash(item.id)) {
|
||||
// already in parent
|
||||
return
|
||||
}
|
||||
await vhd.writeEntireBlock(item)
|
||||
break
|
||||
case 'bat':
|
||||
@@ -45,10 +50,10 @@ exports.createVhdDirectoryFromStream = async function createVhdDirectoryFromStre
|
||||
handler,
|
||||
path,
|
||||
inputStream,
|
||||
{ validator, concurrency = 16, compression } = {}
|
||||
{ validator, concurrency = 16, compression, parentVhd } = {}
|
||||
) {
|
||||
try {
|
||||
const size = await buildVhd(handler, path, inputStream, { concurrency, compression })
|
||||
const size = await buildVhd(handler, path, inputStream, { concurrency, compression, parentVhd })
|
||||
if (validator !== undefined) {
|
||||
await validator.call(this, path)
|
||||
}
|
||||
|
||||
12
packages/vhd-lib/hashBlock.js
Normal file
12
packages/vhd-lib/hashBlock.js
Normal file
@@ -0,0 +1,12 @@
|
||||
'use strict'
|
||||
|
||||
const { createHash } = require('node:crypto')
|
||||
|
||||
// using xxhash as for xva would make smaller hash and the collision risk would be low for the dedup,
|
||||
// since we have a tuple(index, hash), but it would be notable if
|
||||
// we implement dedup on top of this later
|
||||
// at most, a 2TB full vhd will use 32MB for its hashes
|
||||
// and this file is compressed with vhd block
|
||||
exports.hashBlock = function (buffer) {
|
||||
return createHash('sha256').update(buffer).digest('hex')
|
||||
}
|
||||
@@ -72,7 +72,6 @@ const DEFAULT_BLOCKED_LIST = {
|
||||
'system.getServerTimezone': true,
|
||||
'system.getServerVersion': true,
|
||||
'system.getVersion': true,
|
||||
'tag.getAllConfigured': true,
|
||||
'test.getPermissionsForUser': true,
|
||||
'user.getAll': true,
|
||||
'user.getAuthenticationTokens': true,
|
||||
|
||||
@@ -27,7 +27,7 @@ async function sendToNagios(app, jobName, vmBackupInfo) {
|
||||
jobName
|
||||
)
|
||||
} catch (error) {
|
||||
warn('sendToNagios:', { error })
|
||||
warn('sendToNagios:', error)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -45,17 +45,7 @@ const RRD_POINTS_PER_STEP = {
|
||||
// Utils
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
function parseNumber(value) {
|
||||
// Starting from XAPI 23.31, numbers in the JSON payload are encoded as
|
||||
// strings to support NaN, Infinity and -Infinity
|
||||
if (typeof value === 'string') {
|
||||
const asNumber = +value
|
||||
if (isNaN(asNumber) && value !== 'NaN') {
|
||||
throw new Error('cannot parse number: ' + value)
|
||||
}
|
||||
value = asNumber
|
||||
}
|
||||
|
||||
function convertNanToNull(value) {
|
||||
return isNaN(value) ? null : value
|
||||
}
|
||||
|
||||
@@ -68,7 +58,7 @@ async function getServerTimestamp(xapi, hostRef) {
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
const computeValues = (dataRow, legendIndex, transformValue = identity) =>
|
||||
map(dataRow, ({ values }) => transformValue(parseNumber(values[legendIndex])))
|
||||
map(dataRow, ({ values }) => transformValue(convertNanToNull(values[legendIndex])))
|
||||
|
||||
const combineStats = (stats, path, combineValues) => zipWith(...map(stats, path), (...values) => combineValues(values))
|
||||
|
||||
@@ -255,15 +245,7 @@ export default class XapiStats {
|
||||
start: timestamp,
|
||||
},
|
||||
})
|
||||
.then(response => response.text())
|
||||
.then(data => {
|
||||
try {
|
||||
// starting from XAPI 23.31, the response is valid JSON
|
||||
return JSON.parse(data)
|
||||
} catch (_) {
|
||||
return JSON5.parse(data)
|
||||
}
|
||||
})
|
||||
.then(response => response.text().then(JSON5.parse))
|
||||
.catch(err => {
|
||||
delete this.#hostCache[hostUuid][step]
|
||||
throw err
|
||||
@@ -317,7 +299,7 @@ export default class XapiStats {
|
||||
// To avoid crossing over the boundary, we ask for one less step
|
||||
const optimumTimestamp = currentTimeStamp - maxDuration + step
|
||||
const json = await this._getJson(xapi, host, optimumTimestamp, step)
|
||||
const actualStep = parseNumber(json.meta.step)
|
||||
const actualStep = json.meta.step
|
||||
|
||||
if (actualStep !== step) {
|
||||
throw new FaultyGranularity(`Unable to get the true granularity: ${actualStep}`)
|
||||
@@ -344,10 +326,9 @@ export default class XapiStats {
|
||||
return
|
||||
}
|
||||
|
||||
const endTimestamp = parseNumber(json.meta.end)
|
||||
if (stepStats === undefined || stepStats.endTimestamp !== endTimestamp) {
|
||||
if (stepStats === undefined || stepStats.endTimestamp !== json.meta.end) {
|
||||
stepStats = {
|
||||
endTimestamp,
|
||||
endTimestamp: json.meta.end,
|
||||
interval: actualStep,
|
||||
stats: {},
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ export const reportOnSupportPanel = async ({ files = [], formatMessage = identit
|
||||
ADDITIONAL_FILES.map(({ fetch, name }) =>
|
||||
timeout.call(fetch(), ADDITIONAL_FILES_FETCH_TIMEOUT).then(
|
||||
file => formData.append('attachments', createBlobFromString(file), name),
|
||||
error => logger.warn(`cannot get ${name}`, { error })
|
||||
error => logger.warn(`cannot get ${name}`, error)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1099,9 +1099,7 @@ export const SelectXoCloudConfig = makeSubscriptionSelect(
|
||||
subscriber =>
|
||||
subscribeCloudXoConfigBackups(configs => {
|
||||
const xoObjects = groupBy(
|
||||
map(configs, config => ({ ...config, type: 'xoConfig' }))
|
||||
// from newest to oldest
|
||||
.sort((a, b) => b.createdAt - a.createdAt),
|
||||
map(configs, config => ({ ...config, type: 'xoConfig' })),
|
||||
'xoaId'
|
||||
)
|
||||
subscriber({
|
||||
|
||||
@@ -3621,9 +3621,6 @@ export const unlockXosan = (licenseId, srId) => _call('xosan.unlock', { licenseI
|
||||
|
||||
export const bindLicense = (licenseId, boundObjectId) => _call('xoa.licenses.bind', { licenseId, boundObjectId })
|
||||
|
||||
export const rebindObjectLicense = (boundObjectId, licenseId, productId) =>
|
||||
_call('xoa.licenses.rebindObject', { boundObjectId, licenseId, productId })
|
||||
|
||||
export const bindXcpngLicense = (licenseId, boundObjectId) =>
|
||||
bindLicense(licenseId, boundObjectId)::tap(subscribeXcpngLicenses.forceRefresh)
|
||||
|
||||
|
||||
@@ -5,9 +5,10 @@ import decorate from 'apply-decorators'
|
||||
import Icon from 'icon'
|
||||
import React from 'react'
|
||||
import { confirm } from 'modal'
|
||||
import { getApiApplianceInfo, subscribeCloudXoConfig, subscribeCloudXoConfigBackups } from 'xo'
|
||||
import { groupBy, sortBy } from 'lodash'
|
||||
import { injectState, provideState } from 'reaclette'
|
||||
import { SelectXoCloudConfig } from 'select-objects'
|
||||
import { subscribeCloudXoConfig, subscribeCloudXoConfigBackups } from 'xo'
|
||||
|
||||
import BackupXoConfigModal from './backup-xo-config-modal'
|
||||
import RestoreXoConfigModal from './restore-xo-config-modal'
|
||||
@@ -87,7 +88,15 @@ const CloudConfig = decorate([
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
applianceId: async () => {
|
||||
const { id } = await getApiApplianceInfo()
|
||||
return id
|
||||
},
|
||||
groupedConfigs: ({ applianceId, sortedConfigs }) =>
|
||||
sortBy(groupBy(sortedConfigs, 'xoaId'), config => (config[0].xoaId === applianceId ? -1 : 1)),
|
||||
isConfigDefined: ({ config }) => config != null,
|
||||
sortedConfigs: (_, { cloudXoConfigBackups }) =>
|
||||
cloudXoConfigBackups?.sort((config, nextConfig) => config.createdAt - nextConfig.createdAt),
|
||||
},
|
||||
}),
|
||||
injectState,
|
||||
|
||||
@@ -33,7 +33,7 @@ const formatError = error => (typeof error === 'string' ? error : JSON.stringify
|
||||
|
||||
const _changeUrlElement = (value, { remote, element }) =>
|
||||
editRemote(remote, {
|
||||
url: format({ ...parse(remote.url), [element]: value === null ? undefined : value }),
|
||||
url: format({ ...remote, [element]: value === null ? undefined : value }),
|
||||
})
|
||||
const _showError = remote => alert(_('remoteConnectionFailed'), <pre>{formatError(remote.error)}</pre>)
|
||||
const _editRemoteName = (name, { remote }) => editRemote(remote, { name })
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
import _ from 'intl'
|
||||
import ActionButton from 'action-button'
|
||||
import Component from 'base-component'
|
||||
import React from 'react'
|
||||
import SelectLicense from 'select-license'
|
||||
import { bindLicense, rebindObjectLicense } from 'xo'
|
||||
|
||||
import BulkIcons from '../../../common/bulk-icons'
|
||||
|
||||
export default class LicenseForm extends Component {
|
||||
state = {
|
||||
licenseId: 'none',
|
||||
}
|
||||
|
||||
bind = async () => {
|
||||
const { userData, item, itemUuidPath = 'uuid', license } = this.props
|
||||
if (license !== undefined) {
|
||||
await rebindObjectLicense(item[itemUuidPath], this.state.licenseId, license.productId)
|
||||
} else {
|
||||
await bindLicense(this.state.licenseId, item[itemUuidPath])
|
||||
}
|
||||
userData.updateLicenses()
|
||||
this.setState({ licenseId: 'none' })
|
||||
}
|
||||
|
||||
render() {
|
||||
const { license } = this.props
|
||||
return (
|
||||
<div className='d-flex'>
|
||||
<div>
|
||||
{license !== undefined && license.id.slice(-4)}
|
||||
<BulkIcons alerts={this.props.alerts} />
|
||||
</div>
|
||||
<form className='form-inline ml-1'>
|
||||
<SelectLicense
|
||||
onChange={this.linkState('licenseId')}
|
||||
productType={this.props.productType}
|
||||
value={this.state.licenseId}
|
||||
/>
|
||||
<ActionButton
|
||||
btnStyle='primary'
|
||||
className='ml-1'
|
||||
disabled={this.state.licenseId === 'none'}
|
||||
handler={this.bind}
|
||||
icon='connect'
|
||||
>
|
||||
{_(license === undefined ? 'bindLicense' : 'update')}
|
||||
</ActionButton>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,68 +1,74 @@
|
||||
import _ from 'intl'
|
||||
import ActionButton from 'action-button'
|
||||
import Component from 'base-component'
|
||||
import decorate from 'apply-decorators'
|
||||
import Icon from 'icon'
|
||||
import React from 'react'
|
||||
import SelectLicense from 'select-license'
|
||||
import SortedTable from 'sorted-table'
|
||||
import Tooltip from 'tooltip'
|
||||
import { addSubscriptions } from 'utils'
|
||||
import groupBy from 'lodash/groupBy.js'
|
||||
import { createSelector } from 'selectors'
|
||||
import { injectState, provideState } from 'reaclette'
|
||||
import { Proxy, Vm } from 'render-xo-item'
|
||||
import { subscribeProxies } from 'xo'
|
||||
|
||||
import LicenseForm from './license-form'
|
||||
import { subscribeProxies, bindLicense } from 'xo'
|
||||
|
||||
class ProxyLicensesForm extends Component {
|
||||
getAlerts = createSelector(
|
||||
() => this.props.item,
|
||||
() => this.props.userData,
|
||||
(proxy, userData) => {
|
||||
const alerts = []
|
||||
const licenses = userData.licensesByVmUuid[proxy.vmUuid]
|
||||
state = {
|
||||
licenseId: 'none',
|
||||
}
|
||||
|
||||
if (proxy.vmUuid === undefined) {
|
||||
alerts.push({
|
||||
level: 'danger',
|
||||
render: (
|
||||
<p>
|
||||
{_('proxyUnknownVm')} <a href='https://xen-orchestra.com/'>{_('contactUs')}</a>
|
||||
</p>
|
||||
),
|
||||
})
|
||||
}
|
||||
onChangeLicense = event => {
|
||||
this.setState({ licenseId: event.target.value })
|
||||
}
|
||||
|
||||
// Proxy bound to multiple licenses
|
||||
if (licenses?.length > 1) {
|
||||
alerts.push({
|
||||
level: 'danger',
|
||||
render: (
|
||||
<p>
|
||||
{_('proxyMultipleLicenses')}
|
||||
<br />
|
||||
{licenses.map(license => license.id.slice(-4)).join(',')}
|
||||
</p>
|
||||
),
|
||||
})
|
||||
}
|
||||
return alerts
|
||||
}
|
||||
)
|
||||
bind = () => {
|
||||
const { item, userData } = this.props
|
||||
return bindLicense(this.state.licenseId, item.vmUuid).then(userData.updateLicenses)
|
||||
}
|
||||
|
||||
render() {
|
||||
const alerts = this.getAlerts()
|
||||
const { item, userData } = this.props
|
||||
const { licenseId } = this.state
|
||||
const licenses = userData.licensesByVmUuid[item.vmUuid]
|
||||
|
||||
if (item.vmUuid === undefined) {
|
||||
return (
|
||||
<span className='text-danger'>
|
||||
{_('proxyUnknownVm')} <a href='https://xen-orchestra.com/'>{_('contactUs')}</a>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
// Proxy bound to multiple licenses
|
||||
if (licenses?.length > 1) {
|
||||
return (
|
||||
<div>
|
||||
<span>{licenses.map(license => license.id.slice(-4)).join(',')}</span>{' '}
|
||||
<Tooltip content={_('proxyMultipleLicenses')}>
|
||||
<Icon color='text-danger' icon='alarm' />
|
||||
</Tooltip>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const license = licenses?.[0]
|
||||
return (
|
||||
<LicenseForm
|
||||
alerts={alerts}
|
||||
item={item}
|
||||
itemUuidPath='vmUuid'
|
||||
license={license}
|
||||
productType='xoproxy'
|
||||
userData={userData}
|
||||
/>
|
||||
return license !== undefined ? (
|
||||
<span>{license.id.slice(-4)}</span>
|
||||
) : (
|
||||
<form className='form-inline'>
|
||||
<SelectLicense onChange={this.onChangeLicense} productType='xoproxy' />
|
||||
<ActionButton
|
||||
btnStyle='primary'
|
||||
className='ml-1'
|
||||
disabled={licenseId === 'none'}
|
||||
handler={this.bind}
|
||||
handlerParam={licenseId}
|
||||
icon='connect'
|
||||
>
|
||||
{_('bindLicense')}
|
||||
</ActionButton>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,30 @@
|
||||
import _ from 'intl'
|
||||
import ActionButton from 'action-button'
|
||||
import Component from 'base-component'
|
||||
import decorate from 'apply-decorators'
|
||||
import Icon from 'icon'
|
||||
import React from 'react'
|
||||
import SelectLicense from 'select-license'
|
||||
import SortedTable from 'sorted-table'
|
||||
import { bindLicense } from 'xo'
|
||||
import { connectStore } from 'utils'
|
||||
import { createGetObjectsOfType, createSelector } from 'selectors'
|
||||
import { groupBy } from 'lodash'
|
||||
import { injectState, provideState } from 'reaclette'
|
||||
import { Pool, Sr } from 'render-xo-item'
|
||||
|
||||
import LicenseForm from './license-form'
|
||||
import BulkIcons from '../../../common/bulk-icons'
|
||||
|
||||
class XostorLicensesForm extends Component {
|
||||
state = {
|
||||
licenseId: 'none',
|
||||
}
|
||||
|
||||
bind = () => {
|
||||
const { item, userData } = this.props
|
||||
return bindLicense(this.state.licenseId, item.uuid).then(userData.updateLicenses)
|
||||
}
|
||||
|
||||
getAlerts = createSelector(
|
||||
() => this.props.item,
|
||||
() => this.props.userData,
|
||||
@@ -46,12 +59,39 @@ class XostorLicensesForm extends Component {
|
||||
|
||||
render() {
|
||||
const alerts = this.getAlerts()
|
||||
if (alerts.length > 0) {
|
||||
return <BulkIcons alerts={alerts} />
|
||||
}
|
||||
|
||||
const { item, userData } = this.props
|
||||
const { licenseId } = this.state
|
||||
const licenses = userData.licensesByXostorUuid[item.id]
|
||||
const license = licenses?.[0]
|
||||
|
||||
return <LicenseForm alerts={alerts} item={item} license={license} productType='xostor' userData={userData} />
|
||||
return license !== undefined ? (
|
||||
<span>{license?.id.slice(-4)}</span>
|
||||
) : (
|
||||
<div>
|
||||
{license !== undefined && (
|
||||
<div className='text-danger mb-1'>
|
||||
<Icon icon='alarm' /> {_('licenseHasExpired')}
|
||||
</div>
|
||||
)}
|
||||
<form className='form-inline'>
|
||||
<SelectLicense onChange={this.linkState('licenseId')} productType='xostor' />
|
||||
<ActionButton
|
||||
btnStyle='primary'
|
||||
className='ml-1'
|
||||
disabled={licenseId === 'none'}
|
||||
handler={this.bind}
|
||||
handlerParam={licenseId}
|
||||
icon='connect'
|
||||
>
|
||||
{_('bindLicense')}
|
||||
</ActionButton>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user