feat(vhd-lib): implement VhdNegative

it's a virtual Vhd that contains all the changes to be applied
to reset a child to its parent value
This commit is contained in:
Florent Beauchamp
2023-11-24 15:51:58 +00:00
committed by Julien Fontanet
parent 2748aea4e9
commit 408fc5af84
4 changed files with 274 additions and 0 deletions

View File

@@ -27,4 +27,9 @@
<!--packages-start-->
- @xen-orchestra/backups patch
- vhd-lib minor
- xo-server minor
- xo-web minor
<!--packages-end-->

View File

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

View File

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

View File

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