Compare commits
3 Commits
feat_path_
...
bugfix-rem
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c11ed381c7 | ||
|
|
265b545c0c | ||
|
|
86ccdd8f72 |
@@ -30,7 +30,6 @@ import { isValidXva } from './_isValidXva.mjs'
|
|||||||
import { listPartitions, LVM_PARTITION_TYPE } from './_listPartitions.mjs'
|
import { listPartitions, LVM_PARTITION_TYPE } from './_listPartitions.mjs'
|
||||||
import { lvs, pvs } from './_lvm.mjs'
|
import { lvs, pvs } from './_lvm.mjs'
|
||||||
import { watchStreamSize } from './_watchStreamSize.mjs'
|
import { watchStreamSize } from './_watchStreamSize.mjs'
|
||||||
import { pick } from 'lodash'
|
|
||||||
|
|
||||||
export const DIR_XO_CONFIG_BACKUPS = 'xo-config-backups'
|
export const DIR_XO_CONFIG_BACKUPS = 'xo-config-backups'
|
||||||
|
|
||||||
@@ -254,9 +253,7 @@ export class RemoteAdapter {
|
|||||||
const handler = this._handler
|
const handler = this._handler
|
||||||
|
|
||||||
// this will delete the json, unused VHDs will be detected by `cleanVm`
|
// this will delete the json, unused VHDs will be detected by `cleanVm`
|
||||||
await asyncMapSettled(backups, ({ _filename }) =>
|
await asyncMapSettled(backups, ({ _filename }) => handler.unlink(_filename))
|
||||||
Promise.all([handler.unlink(_filename), handler.unlink(_filename + '.plaintext').catch(() => {})])
|
|
||||||
)
|
|
||||||
|
|
||||||
await this.#removeVmBackupsFromCache(backups)
|
await this.#removeVmBackupsFromCache(backups)
|
||||||
}
|
}
|
||||||
@@ -649,14 +646,6 @@ export class RemoteAdapter {
|
|||||||
await this.handler.outputFile(path, JSON.stringify(metadata), {
|
await this.handler.outputFile(path, JSON.stringify(metadata), {
|
||||||
dirMode: this._dirMode,
|
dirMode: this._dirMode,
|
||||||
})
|
})
|
||||||
// using _outputFile means this will NOT be encrypted, to allow for immutability
|
|
||||||
await this.handler._outputFile(
|
|
||||||
path + '.plaintext',
|
|
||||||
JSON.stringify(pick(metadata, ['type', 'differentialVhds', 'vhds', 'xva'])),
|
|
||||||
{
|
|
||||||
dirMode: this._dirMode,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// will not throw
|
// will not throw
|
||||||
await this._updateCache(this.#getVmBackupsCache(vmUuid), backups => {
|
await this._updateCache(this.#getVmBackupsCache(vmUuid), backups => {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
export const isMetadataFile = filename => filename.endsWith('.json')
|
export const isMetadataFile = filename => filename.endsWith('.json')
|
||||||
export const isMetadataPlainTextFile = filename => filename.endsWith('.json.plaintext')
|
|
||||||
export const isVhdFile = filename => filename.endsWith('.vhd')
|
export const isVhdFile = filename => filename.endsWith('.vhd')
|
||||||
export const isXvaFile = filename => filename.endsWith('.xva')
|
export const isXvaFile = filename => filename.endsWith('.xva')
|
||||||
export const isXvaSumFile = filename => filename.endsWith('.xva.checksum')
|
export const isXvaSumFile = filename => filename.endsWith('.xva.checksum')
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { asyncMap } from '@xen-orchestra/async-map'
|
|||||||
import { Constants, openVhd, VhdAbstract, VhdFile } from 'vhd-lib'
|
import { Constants, openVhd, VhdAbstract, VhdFile } from 'vhd-lib'
|
||||||
import { isVhdAlias, resolveVhdAlias } from 'vhd-lib/aliases.js'
|
import { isVhdAlias, resolveVhdAlias } from 'vhd-lib/aliases.js'
|
||||||
import { dirname, resolve } from 'node:path'
|
import { dirname, resolve } from 'node:path'
|
||||||
import { isMetadataFile, isMetadataPlainTextFile, isVhdFile, isXvaFile, isXvaSumFile } from './_backupType.mjs'
|
import { isMetadataFile, isVhdFile, isXvaFile, isXvaSumFile } from './_backupType.mjs'
|
||||||
import { limitConcurrency } from 'limit-concurrency-decorator'
|
import { limitConcurrency } from 'limit-concurrency-decorator'
|
||||||
import { mergeVhdChain } from 'vhd-lib/merge.js'
|
import { mergeVhdChain } from 'vhd-lib/merge.js'
|
||||||
|
|
||||||
@@ -310,7 +310,6 @@ export async function cleanVm(
|
|||||||
const jsons = new Set()
|
const jsons = new Set()
|
||||||
const xvas = new Set()
|
const xvas = new Set()
|
||||||
const xvaSums = []
|
const xvaSums = []
|
||||||
const plainTexts = new Set()
|
|
||||||
const entries = await handler.list(vmDir, {
|
const entries = await handler.list(vmDir, {
|
||||||
prependDir: true,
|
prependDir: true,
|
||||||
})
|
})
|
||||||
@@ -321,18 +320,9 @@ export async function cleanVm(
|
|||||||
xvas.add(path)
|
xvas.add(path)
|
||||||
} else if (isXvaSumFile(path)) {
|
} else if (isXvaSumFile(path)) {
|
||||||
xvaSums.push(path)
|
xvaSums.push(path)
|
||||||
} else if (isMetadataPlainTextFile(path)) {
|
|
||||||
plainTexts.push(path)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
for (const plainTextPath of plainTexts) {
|
|
||||||
if (!jsons.has(plainTextPath.substring(0, -15))) {
|
|
||||||
logWarn('plaintext without metadata', { plainTextPath })
|
|
||||||
await handler.unlink(plainTextPath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const cachePath = vmDir + '/cache.json.gz'
|
const cachePath = vmDir + '/cache.json.gz'
|
||||||
|
|
||||||
let mustRegenerateCache
|
let mustRegenerateCache
|
||||||
|
|||||||
@@ -8,16 +8,12 @@
|
|||||||
> Users must be able to say: “Nice enhancement, I'm eager to test it”
|
> 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))
|
- [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))
|
||||||
- [REST API] `/backups` has been renamed to `/backup` (redirections are in place for compatibility)
|
|
||||||
- [REST API] _VM backup & Replication_ jobs have been moved from `/backup/jobs/:id` to `/backup/jobs/vm/:id` (redirections are in place for compatibility)
|
|
||||||
- [REST API] _XO config & Pool metadata Backup_ jobs are available at `/backup/jobs/metadata`
|
|
||||||
- [REST API] _Mirror Backup_ jobs are available at `/backup/jobs/metadata`
|
|
||||||
|
|
||||||
### Bug fixes
|
### Bug fixes
|
||||||
|
|
||||||
> Users must be able to say: “I had this issue, happy to know it's fixed”
|
> Users must be able to say: “I had this issue, happy to know it's fixed”
|
||||||
|
|
||||||
- [REST API] Returns a proper 404 _Not Found_ error when a job does not exist instead of _Internal Server Error_
|
- [Remotes] Prevents the "connection failed" alert from continuing to appear after successfull connection
|
||||||
|
|
||||||
### Packages to release
|
### Packages to release
|
||||||
|
|
||||||
@@ -35,6 +31,6 @@
|
|||||||
|
|
||||||
<!--packages-start-->
|
<!--packages-start-->
|
||||||
|
|
||||||
- xo-server minor
|
- xo-remote-parser patch
|
||||||
|
|
||||||
<!--packages-end-->
|
<!--packages-end-->
|
||||||
|
|||||||
@@ -66,20 +66,6 @@ describe('VhdAbstract', async () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Creates alias unencrypted in encrypted remote', async () => {
|
|
||||||
await Disposable.use(async function* () {
|
|
||||||
const handler = yield getSyncedHandler({
|
|
||||||
url: `file://${tempDir}/?encryptionKey="85704F498279D6FCD2B2AA7B793944DF"`,
|
|
||||||
})
|
|
||||||
const aliasPath = `alias.alias.vhd`
|
|
||||||
const aliasFsPath = `${tempDir}/${aliasPath}`
|
|
||||||
await VhdAbstract.createAlias(handler, aliasPath, 'target.vhd')
|
|
||||||
assert.equal(await fs.exists(aliasFsPath), true)
|
|
||||||
const content = await fs.readFile(aliasFsPath, 'utf-8')
|
|
||||||
assert.equal(content, 'target.vhd')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('has *.alias.vhd extension', async () => {
|
it('has *.alias.vhd extension', async () => {
|
||||||
await Disposable.use(async function* () {
|
await Disposable.use(async function* () {
|
||||||
const handler = yield getSyncedHandler({ url: 'file:///' })
|
const handler = yield getSyncedHandler({ url: 'file:///' })
|
||||||
|
|||||||
@@ -236,8 +236,7 @@ exports.VhdAbstract = class VhdAbstract {
|
|||||||
`Alias relative path ${relativePathToTarget} is too long : ${relativePathToTarget.length} chars, max is ${ALIAS_MAX_PATH_LENGTH}`
|
`Alias relative path ${relativePathToTarget} is too long : ${relativePathToTarget.length} chars, max is ${ALIAS_MAX_PATH_LENGTH}`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// using _outputFile means the checksum will NOT be encrypted
|
await handler.writeFile(aliasPath, relativePathToTarget)
|
||||||
await handler._outputFile(path.resolve('/', aliasPath), relativePathToTarget, { flags: 'wx' })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
streamSize() {
|
streamSize() {
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ const { Disposable, pFromCallback } = require('promise-toolbox')
|
|||||||
|
|
||||||
const { isVhdAlias, resolveVhdAlias } = require('./aliases')
|
const { isVhdAlias, resolveVhdAlias } = require('./aliases')
|
||||||
const { ALIAS_MAX_PATH_LENGTH } = require('./_constants')
|
const { ALIAS_MAX_PATH_LENGTH } = require('./_constants')
|
||||||
const fs = require('node:fs/promises')
|
|
||||||
|
|
||||||
let tempDir
|
let tempDir
|
||||||
|
|
||||||
@@ -75,17 +74,4 @@ describe('resolveVhdAlias()', async () => {
|
|||||||
await assert.rejects(async () => await resolveVhdAlias(handler, 'toobig.alias.vhd'), Error)
|
await assert.rejects(async () => await resolveVhdAlias(handler, 'toobig.alias.vhd'), Error)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('read an plaintext alias from an encrypted remote', async () => {
|
|
||||||
await Disposable.use(async function* () {
|
|
||||||
const handler = yield getSyncedHandler({
|
|
||||||
url: `file://${tempDir}/?encryptionKey="85704F498279D6FCD2B2AA7B793944DF"`,
|
|
||||||
})
|
|
||||||
await handler.writeFile(`crypted.alias.vhd`, `target.vhd`)
|
|
||||||
|
|
||||||
await fs.writeFile(`${tempDir}/plain.alias.vhd`, `target.vhd`)
|
|
||||||
assert.equal(await resolveVhdAlias(handler, `plain.alias.vhd`), `target.vhd`)
|
|
||||||
assert.equal(await resolveVhdAlias(handler, `crypted.alias.vhd`), `target.vhd`)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
const { ALIAS_MAX_PATH_LENGTH } = require('./_constants')
|
const { ALIAS_MAX_PATH_LENGTH } = require('./_constants')
|
||||||
const resolveRelativeFromFile = require('./_resolveRelativeFromFile')
|
const resolveRelativeFromFile = require('./_resolveRelativeFromFile')
|
||||||
const path = require('node:path')
|
|
||||||
|
|
||||||
function isVhdAlias(filename) {
|
function isVhdAlias(filename) {
|
||||||
return filename.endsWith('.alias.vhd')
|
return filename.endsWith('.alias.vhd')
|
||||||
@@ -21,22 +20,7 @@ exports.resolveVhdAlias = async function resolveVhdAlias(handler, filename) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let aliasContent
|
const aliasContent = (await handler.readFile(filename)).toString().trim()
|
||||||
try {
|
|
||||||
aliasContent = (await handler.readFile(filename)).toString().trim()
|
|
||||||
} catch (err) {
|
|
||||||
try {
|
|
||||||
// try to read as a plain text
|
|
||||||
aliasContent = (await handler._readFile(path.resolve('/', filename))).toString().trim()
|
|
||||||
if (!aliasContent.endsWith('.vhd')) {
|
|
||||||
throw new Error(`The alias file ${filename} is not a plaint text alias`)
|
|
||||||
}
|
|
||||||
} catch (plainTextErr) {
|
|
||||||
// throw original error
|
|
||||||
err.cause = plainTextErr
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// also handle circular references and unreasonnably long chains
|
// also handle circular references and unreasonnably long chains
|
||||||
if (isVhdAlias(aliasContent)) {
|
if (isVhdAlias(aliasContent)) {
|
||||||
throw new Error(`Chaining alias is forbidden ${filename} to ${aliasContent}`)
|
throw new Error(`Chaining alias is forbidden ${filename} to ${aliasContent}`)
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@xen-orchestra/log": "^0.6.0",
|
||||||
"lodash": "^4.13.1",
|
"lodash": "^4.13.1",
|
||||||
"url-parse": "^1.4.7"
|
"url-parse": "^1.4.7"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ import trim from 'lodash/trim'
|
|||||||
import trimStart from 'lodash/trimStart'
|
import trimStart from 'lodash/trimStart'
|
||||||
import queryString from 'querystring'
|
import queryString from 'querystring'
|
||||||
import urlParser from 'url-parse'
|
import urlParser from 'url-parse'
|
||||||
|
import { createLogger } from '@xen-orchestra/log'
|
||||||
|
|
||||||
|
const { warn } = createLogger('xo:xo-remote-parser')
|
||||||
|
|
||||||
const NFS_RE = /^([^:]+):(?:(\d+):)?([^:?]+)(\?[^?]*)?$/
|
const NFS_RE = /^([^:]+):(?:(\d+):)?([^:?]+)(\?[^?]*)?$/
|
||||||
const SMB_RE = /^([^:]+):(.+)@([^@]+)\\\\([^\0?]+)(?:\0([^?]*))?(\?[^?]*)?$/
|
const SMB_RE = /^([^:]+):(.+)@([^@]+)\\\\([^\0?]+)(?:\0([^?]*))?(\?[^?]*)?$/
|
||||||
@@ -15,6 +18,17 @@ const parseOptionList = (optionList = '') => {
|
|||||||
optionList = optionList.substring(1)
|
optionList = optionList.substring(1)
|
||||||
}
|
}
|
||||||
const parsed = queryString.parse(optionList)
|
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 => {
|
Object.keys(parsed).forEach(key => {
|
||||||
const val = parsed[key]
|
const val = parsed[key]
|
||||||
// some incorrect values have been saved in users database (introduced by #6270)
|
// some incorrect values have been saved in users database (introduced by #6270)
|
||||||
|
|||||||
@@ -175,7 +175,7 @@ export default class RestApi {
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
collections.backup = { id: 'backup' }
|
collections.backups = { id: 'backups' }
|
||||||
collections.restore = { id: 'restore' }
|
collections.restore = { id: 'restore' }
|
||||||
collections.tasks = { id: 'tasks' }
|
collections.tasks = { id: 'tasks' }
|
||||||
collections.users = { id: 'users' }
|
collections.users = { id: 'users' }
|
||||||
@@ -280,26 +280,23 @@ export default class RestApi {
|
|||||||
wrap((req, res) => sendObjects(collections, req, res))
|
wrap((req, res) => sendObjects(collections, req, res))
|
||||||
)
|
)
|
||||||
|
|
||||||
// For compatibility redirect from /backups* to /backup
|
|
||||||
api.get('/backups*', (req, res) => {
|
|
||||||
res.redirect(308, req.baseUrl + '/backup' + req.params[0])
|
|
||||||
})
|
|
||||||
|
|
||||||
const backupTypes = {
|
|
||||||
__proto__: null,
|
|
||||||
|
|
||||||
metadata: 'metadataBackup',
|
|
||||||
mirror: 'mirrorBackup',
|
|
||||||
vm: 'backup',
|
|
||||||
}
|
|
||||||
|
|
||||||
api
|
api
|
||||||
.get(
|
.get(
|
||||||
'/backup',
|
'/backups',
|
||||||
wrap((req, res) => sendObjects([{ id: 'jobs' }, { id: 'logs' }], req, res))
|
wrap((req, res) => sendObjects([{ id: 'jobs' }, { id: 'logs' }], req, res))
|
||||||
)
|
)
|
||||||
.get(
|
.get(
|
||||||
'/backup/logs',
|
'/backups/jobs',
|
||||||
|
wrap(async (req, res) => sendObjects(await app.getAllJobs('backup'), req, res))
|
||||||
|
)
|
||||||
|
.get(
|
||||||
|
'/backups/jobs/:id',
|
||||||
|
wrap(async (req, res) => {
|
||||||
|
res.json(await app.getJob(req.params.id, 'backup'))
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.get(
|
||||||
|
'/backups/logs',
|
||||||
wrap(async (req, res) => {
|
wrap(async (req, res) => {
|
||||||
const { filter, limit } = req.query
|
const { filter, limit } = req.query
|
||||||
const logs = await app.getBackupNgLogsSorted({
|
const logs = await app.getBackupNgLogsSorted({
|
||||||
@@ -309,37 +306,6 @@ export default class RestApi {
|
|||||||
await sendObjects(logs, req, res)
|
await sendObjects(logs, req, res)
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.get(
|
|
||||||
'/backup/jobs',
|
|
||||||
wrap((req, res) =>
|
|
||||||
sendObjects(
|
|
||||||
Object.keys(backupTypes).map(id => ({ id })),
|
|
||||||
req,
|
|
||||||
res
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
for (const [collection, type] of Object.entries(backupTypes)) {
|
|
||||||
api
|
|
||||||
.get(
|
|
||||||
'/backup/jobs/' + collection,
|
|
||||||
wrap(async (req, res) => sendObjects(await app.getAllJobs(type), req, res))
|
|
||||||
)
|
|
||||||
.get(
|
|
||||||
`/backup/jobs/${collection}/:id`,
|
|
||||||
wrap(async (req, res) => {
|
|
||||||
res.json(await app.getJob(req.params.id, type))
|
|
||||||
}, true)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// For compatibility, redirect /backup/jobs/:id to /backup/jobs/vm/:id
|
|
||||||
api.get('/backup/jobs/:id', (req, res) => {
|
|
||||||
res.redirect(308, req.baseUrl + '/backup/jobs/vm/' + req.params.id)
|
|
||||||
})
|
|
||||||
|
|
||||||
api
|
|
||||||
.get(
|
.get(
|
||||||
'/restore',
|
'/restore',
|
||||||
wrap((req, res) => sendObjects([{ id: 'logs' }], req, res))
|
wrap((req, res) => sendObjects([{ id: 'logs' }], req, res))
|
||||||
|
|||||||
Reference in New Issue
Block a user