Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 24 additions & 1 deletion @xen-orchestra/backups/RemoteAdapter.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -702,7 +702,8 @@ export class RemoteAdapter {
) {
const container = watchStreamSize(input)
await this._handler.outputStream(path, input, {
checksum,
// a known checksum is the one of unencrypted data: an encrypted remote stores no checksum
checksum: typeof checksum === 'string' && this._handler.isEncrypted ? false : checksum,
dirMode: this._dirMode,
maxStreamLength,
streamLength,
Expand Down Expand Up @@ -753,6 +754,28 @@ export class RemoteAdapter {
return this._handler.createReadStream(resolve('/', dirname(metadata._filename), metadata.xva))
}

/**
* @returns {Promise<string | undefined>} the checksum of the XVA as stored, undefined if it is unknown (no checksum
* file, or encrypted remote: the stored data are not the data read)
*/
async readFullVmBackupChecksum(metadata) {
if (this._handler.isEncrypted) {
return
}
try {
const checksum = String(
await this._handler.readFile(resolve('/', dirname(metadata._filename), metadata.xva) + '.checksum')
).trim()
// $<algorithm id>$<salt>$<hash>
return checksum.startsWith('$') ? checksum : undefined
} catch (error) {
if (error.code === 'ENOENT') {
return
}
throw error
}
}

async readVmBackupMetadata(path) {
let json
let isImmutable = false
Expand Down
17 changes: 16 additions & 1 deletion @xen-orchestra/backups/_runners/_vmRunners/FullRemote.mjs
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import { createLogger } from '@xen-orchestra/log'

import { AbstractRemote } from './_AbstractRemote.mjs'
import { FullRemoteWriter } from '../_writers/FullRemoteWriter.mjs'
import { forkStreamUnpipe } from '../_forkStreamUnpipe.mjs'
import { watchStreamSize } from '../../_watchStreamSize.mjs'
import { AggregatedFullRemoteWriter } from '../_writers/AggregatedFullRemoteWriter.mjs'
import { Task } from '@vates/task'

const { warn } = createLogger('xo:backups:FullRemoteVmBackup')

export const FullRemote = class FullRemoteVmBackupRunner extends AbstractRemote {
_getRemoteWriters() {
return [FullRemoteWriter, AggregatedFullRemoteWriter]
Expand All @@ -21,12 +25,23 @@ export const FullRemote = class FullRemoteVmBackupRunner extends AbstractRemote
for (const metadata of transferList) {
const stream = this._throttleStream(await this._sourceRemoteAdapter.readFullVmBackup(metadata))
const sizeContainer = watchStreamSize(stream)
let checksum
try {
// the unencrypted destinations store the same data as the source: no need to compute their checksum again
checksum = await this._sourceRemoteAdapter.readFullVmBackupChecksum(metadata)
} catch (error) {
warn('error while copying reading source checksum, recomputing it', error)
}

// a single writer can consume the stream itself: no need to fork it
const isForked = this._writers.size > 1

// @todo shouldn't transfer backup if it will be deleted by retention policy (higher retention on source than destination)
await this._callWriters(
writer =>
writer.run({
stream: forkStreamUnpipe(stream),
checksum,
stream: isForked ? forkStreamUnpipe(stream) : stream,
// stream will be forked and transformed, it's not safe to attach additional properties to it
streamLength: stream.length,
maxStreamLength: stream.maxStreamLength, // for encrypted destination/source without length
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,11 @@ export class FullRemoteWriter extends MixinRemoteWriter(AbstractFullWriter) {
)
}

async _run({ maxStreamLength, timestamp, sizeContainer, stream, streamLength, vm, vmSnapshot }) {
/**
* @param {object} params
* @param {string} [params.checksum] - checksum of the stream data if known (full mirror of an unencrypted backup)
*/
async _run({ checksum, maxStreamLength, timestamp, sizeContainer, stream, streamLength, vm, vmSnapshot }) {
const settings = this._settings
const job = this._job
const scheduleId = this._schedule.id
Expand Down Expand Up @@ -67,6 +71,7 @@ export class FullRemoteWriter extends MixinRemoteWriter(AbstractFullWriter) {

await Task.run({ properties: { name: 'transfer' } }, async () => {
await adapter.outputStream(dataFilename, stream, {
checksum,
maxStreamLength,
streamLength,
validator: tmpPath => adapter.isValidXva(tmpPath),
Expand Down
13 changes: 11 additions & 2 deletions @xen-orchestra/backups/_runners/_writers/_AbstractFullWriter.mjs
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
import { AbstractWriter } from './_AbstractWriter.mjs'

export class AbstractFullWriter extends AbstractWriter {
async run({ maxStreamLength, timestamp, sizeContainer, stream, streamLength, vm, vmSnapshot }) {
async run({ checksum, maxStreamLength, timestamp, sizeContainer, stream, streamLength, vm, vmSnapshot }) {
try {
return await this._run({ maxStreamLength, timestamp, sizeContainer, stream, streamLength, vm, vmSnapshot })
return await this._run({
checksum,
maxStreamLength,
timestamp,
sizeContainer,
stream,
streamLength,
vm,
vmSnapshot,
})
} finally {
// ensure stream is properly closed
stream.destroy()
Expand Down
22 changes: 18 additions & 4 deletions @xen-orchestra/fs/src/abstract.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ const computeRate = (hrtime, size) => {
const DEFAULT_TIMEOUT = 12e5 // 20 min
const DEFAULT_MAX_PARALLEL_OPERATIONS = 10

// size of the chunks of the file streams, same as the backup streams: with the 64 KiB of Node, each chunk costs a
// round trip to the thread pool, which limits a sequential read to about 1.5 GB/s
const DEFAULT_HIGH_WATER_MARK = 10 * 1024 * 1024

const ENCRYPTION_DESC_FILENAME = 'encryption.json'
const ENCRYPTION_METADATA_FILENAME = 'metadata.json'

Expand Down Expand Up @@ -158,7 +162,7 @@ export default class RemoteHandlerAbstract {
}
}
;({
highWaterMark: this._highWaterMark,
highWaterMark: this._highWaterMark = DEFAULT_HIGH_WATER_MARK,
timeout: this._timeout = DEFAULT_TIMEOUT,
withLimit: this._withLimit = WITH_LIMIT,
withTimeout: this._withTimeout = WITH_TIMEOUT,
Expand Down Expand Up @@ -289,12 +293,19 @@ export default class RemoteHandlerAbstract {
* @param {string} path
* @param {ReadableStream} input
* @param {object} [options]
* @param {boolean} [options.checksum]
* @param {boolean|string} [options.checksum=true] - whether to compute and store the checksum of the stored data,
* or this checksum itself when it is already known (an unencrypted file copied from another remote): it is then
* stored as is, without reading the data
* @param {number} [options.dirMode]
* @param {(this: RemoteHandlerAbstract, path: string) => Promise<undefined>} [options.validator] Function that will be called before the data is commited to the remote, if it fails, file should not exist
*/
async outputStream(path, input, { checksum = true, dirMode, maxStreamLength, streamLength, validator } = {}) {
path = normalizePath(path)
const knownChecksum = typeof checksum === 'string' ? checksum : undefined
if (knownChecksum !== undefined) {
// the checksum is the one of the stored data, which are different once encrypted
assert.strictEqual(this.isEncrypted, false, `a known checksum can't be used on an encrypted remote for ${path}`)
}
let checksumStream
info('will output stream ', { path, streamLength, maxStreamLength, isEncrypted: this.isEncrypted })
if (this.isEncrypted) {
Expand All @@ -305,7 +316,7 @@ export default class RemoteHandlerAbstract {
info('stream is updated ', { path, streamLength, maxStreamLength, overhead })
}
input = this.#encryptor.encryptStream(input)
if (checksum) {
if (checksum === true) {
checksumStream = createChecksumStream()
pipeline(input, checksumStream, noop)
input = checksumStream
Expand All @@ -319,7 +330,10 @@ export default class RemoteHandlerAbstract {
if (checksum) {
// using _outputFile means the checksum will NOT be encrypted
// it is by design to allow checking of encrypted files without the key
await this._outputFile(checksumFile(path), await checksumStream.checksum, { dirMode, flags: 'wx' })
await this._outputFile(checksumFile(path), knownChecksum ?? (await checksumStream.checksum), {
dirMode,
flags: 'wx',
})
}
}

Expand Down
5 changes: 4 additions & 1 deletion CHANGELOG.unreleased.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
- [Treeview] Press Ctrl+K (Cmd+K on macOS) to focus the treeview search (PR [#10492](https://github.com/vatesfr/xen-orchestra/pull/10492))
- [New VM] Allow resizing existing (template) disks when a config drive is used (PR [#10359](https://github.com/vatesfr/xen-orchestra/pull/10359))
- [Host] Add possibility to emergency shutdown a host (PR [#10412](https://github.com/vatesfr/xen-orchestra/pull/10412))
- [Backup/Restore] Faster reading of the files of a remote, like the full backups (10 MiB chunks instead of 64 KiB) (PR [#10537](https://github.com/vatesfr/xen-orchestra/pull/10537))
- [Mirror] The full mirrors between unencrypted remotes reuse the checksum of the source instead of computing it again (PR [#10537](https://github.com/vatesfr/xen-orchestra/pull/10537))

### Bug fixes

Expand Down Expand Up @@ -63,7 +65,8 @@
- @vates/nbd-client patch
- @vates/types minor
- @xen-orchestra/backup-archive patch
- @xen-orchestra/backups patch
- @xen-orchestra/backups minor
- @xen-orchestra/fs minor
- @xen-orchestra/rest-api minor
- @xen-orchestra/web minor
- @xen-orchestra/web-core minor
Expand Down
Loading