feat(xo-server/api): backupNg.{,un}mountPartition
(#7176)
Manual method to mount a backup partition on the XOA.
This commit is contained in:
parent
e108cb0990
commit
865461bfb9
@ -17,4 +17,14 @@ map.get(['foo', 'bar']) // 2
|
||||
map.get(['bar', 'foo']) // 3
|
||||
map.get([OBJ]) // 4
|
||||
map.get([{}]) // undefined
|
||||
|
||||
map.delete([])
|
||||
|
||||
for (const [key, value] of map.entries() {
|
||||
console.log(key, value)
|
||||
}
|
||||
|
||||
for (const value of map.values()) {
|
||||
console.log(value)
|
||||
}
|
||||
```
|
||||
|
@ -35,6 +35,16 @@ map.get(['foo', 'bar']) // 2
|
||||
map.get(['bar', 'foo']) // 3
|
||||
map.get([OBJ]) // 4
|
||||
map.get([{}]) // undefined
|
||||
|
||||
map.delete([])
|
||||
|
||||
for (const [key, value] of map.entries() {
|
||||
console.log(key, value)
|
||||
}
|
||||
|
||||
for (const value of map.values()) {
|
||||
console.log(value)
|
||||
}
|
||||
```
|
||||
|
||||
## Contributions
|
||||
|
@ -36,6 +36,23 @@ function del(node, i, keys) {
|
||||
return node
|
||||
}
|
||||
|
||||
function* entries(node, key) {
|
||||
if (node !== undefined) {
|
||||
if (node instanceof Node) {
|
||||
const { value } = node
|
||||
if (value !== undefined) {
|
||||
yield [key, node.value]
|
||||
}
|
||||
|
||||
for (const [childKey, child] of node.children.entries()) {
|
||||
yield* entries(child, key.concat(childKey))
|
||||
}
|
||||
} else {
|
||||
yield [key, node]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function get(node, i, keys) {
|
||||
return i === keys.length
|
||||
? node instanceof Node
|
||||
@ -69,6 +86,22 @@ function set(node, i, keys, value) {
|
||||
return node
|
||||
}
|
||||
|
||||
function* values(node) {
|
||||
if (node !== undefined) {
|
||||
if (node instanceof Node) {
|
||||
const { value } = node
|
||||
if (value !== undefined) {
|
||||
yield node.value
|
||||
}
|
||||
for (const child of node.children.values()) {
|
||||
yield* values(child)
|
||||
}
|
||||
} else {
|
||||
yield node
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
exports.MultiKeyMap = class MultiKeyMap {
|
||||
constructor() {
|
||||
// each node is either a value or a Node if it contains children
|
||||
@ -79,6 +112,10 @@ exports.MultiKeyMap = class MultiKeyMap {
|
||||
this._root = del(this._root, 0, keys)
|
||||
}
|
||||
|
||||
entries() {
|
||||
return entries(this._root, [])
|
||||
}
|
||||
|
||||
get(keys) {
|
||||
return get(this._root, 0, keys)
|
||||
}
|
||||
@ -86,4 +123,8 @@ exports.MultiKeyMap = class MultiKeyMap {
|
||||
set(keys, value) {
|
||||
this._root = set(this._root, 0, keys, value)
|
||||
}
|
||||
|
||||
values() {
|
||||
return values(this._root)
|
||||
}
|
||||
}
|
||||
|
@ -19,7 +19,7 @@ describe('MultiKeyMap', () => {
|
||||
// reverse composite key
|
||||
['bar', 'foo'],
|
||||
]
|
||||
const values = keys.map(() => ({}))
|
||||
const values = keys.map(() => Math.random())
|
||||
|
||||
// set all values first to make sure they are all stored and not only the
|
||||
// last one
|
||||
@ -27,6 +27,12 @@ describe('MultiKeyMap', () => {
|
||||
map.set(key, values[i])
|
||||
})
|
||||
|
||||
assert.deepEqual(
|
||||
Array.from(map.entries()),
|
||||
keys.map((key, i) => [key, values[i]])
|
||||
)
|
||||
assert.deepEqual(Array.from(map.values()), values)
|
||||
|
||||
keys.forEach((key, i) => {
|
||||
// copy the key to make sure the array itself is not the key
|
||||
assert.strictEqual(map.get(key.slice()), values[i])
|
||||
|
@ -14,6 +14,7 @@
|
||||
- [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))
|
||||
- [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))
|
||||
- [File Restore] API method `backupNg.mountPartition` to manually mount a backup disk on the XOA
|
||||
|
||||
### Bug fixes
|
||||
|
||||
@ -40,6 +41,7 @@
|
||||
|
||||
<!--packages-start-->
|
||||
|
||||
- @vates/multi-key-map minor
|
||||
- @vates/nbd-client patch
|
||||
- @xen-orchestra/backups minor
|
||||
- @xen-orchestra/cr-seed-cli major
|
||||
@ -51,5 +53,6 @@
|
||||
- xo-server-netbox minor
|
||||
- xo-vmdk-to-vhd patch
|
||||
- xo-web minor
|
||||
- xo-server minor
|
||||
|
||||
<!--packages-end-->
|
||||
|
@ -76,6 +76,8 @@ defaultSignInPage = '/signin'
|
||||
throttlingDelay = '2 seconds'
|
||||
|
||||
[backups]
|
||||
autoUnmountPartitionDelay = '24h'
|
||||
|
||||
disableMergeWorker = false
|
||||
|
||||
# Mode to use for newly created backup directories
|
||||
|
@ -380,3 +380,47 @@ fetchFiles.params = {
|
||||
type: 'string',
|
||||
},
|
||||
}
|
||||
|
||||
export function listMountedPartitions() {
|
||||
return this.listMountedPartitions()
|
||||
}
|
||||
|
||||
listMountedPartitions.permission = 'admin'
|
||||
|
||||
export function mountPartition({ remote, disk, partition }) {
|
||||
return this.mountPartition(remote, disk, partition)
|
||||
}
|
||||
|
||||
mountPartition.permission = 'admin'
|
||||
|
||||
mountPartition.params = {
|
||||
disk: {
|
||||
type: 'string',
|
||||
},
|
||||
partition: {
|
||||
optional: true,
|
||||
type: 'string',
|
||||
},
|
||||
remote: {
|
||||
type: 'string',
|
||||
},
|
||||
}
|
||||
|
||||
export function unmountPartition({ remote, disk, partition }) {
|
||||
return this.unmountPartition(remote, disk, partition)
|
||||
}
|
||||
|
||||
unmountPartition.permission = 'admin'
|
||||
|
||||
unmountPartition.params = {
|
||||
disk: {
|
||||
type: 'string',
|
||||
},
|
||||
partition: {
|
||||
optional: true,
|
||||
type: 'string',
|
||||
},
|
||||
remote: {
|
||||
type: 'string',
|
||||
},
|
||||
}
|
||||
|
@ -1,5 +1,12 @@
|
||||
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
|
||||
@ -22,6 +29,8 @@ import { execa } from 'execa'
|
||||
// - [ ] getMountedPartitions
|
||||
// - [ ] unmountPartition
|
||||
export default class BackupNgFileRestore {
|
||||
#mounts = new MultiKeyMap()
|
||||
|
||||
constructor(app) {
|
||||
this._app = app
|
||||
|
||||
@ -31,6 +40,16 @@ 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) {
|
||||
@ -108,4 +127,77 @@ 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()
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user