Compare commits

..

2 Commits

Author SHA1 Message Date
Florent Beauchamp
13d9edf4ce fix 2022-07-04 14:29:35 +02:00
Florent Beauchamp
64a6b624b7 feat(cleanVm): add recovery method for duplicated vhd uuid containing the same data 2022-07-04 09:42:15 +02:00
38 changed files with 2242 additions and 2249 deletions

View File

@@ -14,7 +14,7 @@ Returns a promise wich rejects as soon as a call to `iteratee` throws or a promi
`opts` is an object that can contains the following options:
- `concurrency`: a number which indicates the maximum number of parallel call to `iteratee`, defaults to `10`. The value `0` means no concurrency limit.
- `concurrency`: a number which indicates the maximum number of parallel call to `iteratee`, defaults to `1`
- `signal`: an abort signal to stop the iteration
- `stopOnError`: wether to stop iteration of first error, or wait for all calls to finish and throw an `AggregateError`, defaults to `true`

View File

@@ -32,7 +32,7 @@ Returns a promise wich rejects as soon as a call to `iteratee` throws or a promi
`opts` is an object that can contains the following options:
- `concurrency`: a number which indicates the maximum number of parallel call to `iteratee`, defaults to `10`. The value `0` means no concurrency limit.
- `concurrency`: a number which indicates the maximum number of parallel call to `iteratee`, defaults to `1`
- `signal`: an abort signal to stop the iteration
- `stopOnError`: wether to stop iteration of first error, or wait for all calls to finish and throw an `AggregateError`, defaults to `true`

View File

@@ -9,16 +9,7 @@ class AggregateError extends Error {
}
}
/**
* @template Item
* @param {Iterable<Item>} iterable
* @param {(item: Item, index: number, iterable: Iterable<Item>) => Promise<void>} iteratee
* @returns {Promise<void>}
*/
exports.asyncEach = function asyncEach(iterable, iteratee, { concurrency = 10, signal, stopOnError = true } = {}) {
if (concurrency === 0) {
concurrency = Infinity
}
exports.asyncEach = function asyncEach(iterable, iteratee, { concurrency = 1, signal, stopOnError = true } = {}) {
return new Promise((resolve, reject) => {
const it = (iterable[Symbol.iterator] || iterable[Symbol.asyncIterator]).call(iterable)
const errors = []

View File

@@ -36,7 +36,7 @@ describe('asyncEach', () => {
it('works', async () => {
const iteratee = jest.fn(async () => {})
await asyncEach.call(thisArg, iterable, iteratee, { concurrency: 1 })
await asyncEach.call(thisArg, iterable, iteratee)
expect(iteratee.mock.instances).toEqual(Array.from(values, () => thisArg))
expect(iteratee.mock.calls).toEqual(Array.from(values, (value, index) => [value, index, iterable]))
@@ -66,7 +66,7 @@ describe('asyncEach', () => {
}
})
expect(await rejectionOf(asyncEach(iterable, iteratee, { concurrency: 1, stopOnError: true }))).toBe(error)
expect(await rejectionOf(asyncEach(iterable, iteratee, { stopOnError: true }))).toBe(error)
expect(iteratee).toHaveBeenCalledTimes(2)
})
@@ -91,9 +91,7 @@ describe('asyncEach', () => {
}
})
await expect(asyncEach(iterable, iteratee, { concurrency: 1, signal: ac.signal })).rejects.toThrow(
'asyncEach aborted'
)
await expect(asyncEach(iterable, iteratee, { signal: ac.signal })).rejects.toThrow('asyncEach aborted')
expect(iteratee).toHaveBeenCalledTimes(2)
})
})

View File

@@ -7,7 +7,7 @@
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
"dependencies": {
"@xen-orchestra/async-map": "^0.1.2",
"@xen-orchestra/backups": "^0.27.0",
"@xen-orchestra/backups": "^0.26.0",
"@xen-orchestra/fs": "^1.1.0",
"filenamify": "^4.1.0",
"getopts": "^2.2.5",
@@ -27,7 +27,7 @@
"scripts": {
"postversion": "npm publish --access public"
},
"version": "0.7.5",
"version": "0.7.4",
"license": "AGPL-3.0-or-later",
"author": {
"name": "Vates SAS",

View File

@@ -2,7 +2,6 @@
const assert = require('assert')
const sum = require('lodash/sum')
const UUID = require('uuid')
const { asyncMap } = require('@xen-orchestra/async-map')
const { Constants, mergeVhd, openVhd, VhdAbstract, VhdFile } = require('vhd-lib')
const { isVhdAlias, resolveVhdAlias } = require('vhd-lib/aliases')
@@ -51,7 +50,7 @@ const computeVhdsSize = (handler, vhdPaths) =>
async function mergeVhdChain(chain, { handler, logInfo, remove, merge }) {
assert(chain.length >= 2)
const chainCopy = [...chain]
const parent = chainCopy.shift()
const parent = chainCopy.pop()
const children = chainCopy
if (merge) {
@@ -188,9 +187,9 @@ exports.cleanVm = async function cleanVm(
const handler = this._handler
const vhdsToJSons = new Set()
const vhdById = new Map()
const vhdParents = { __proto__: null }
const vhdChildren = { __proto__: null }
const vhdById = new Map()
const { vhds, interruptedVhds, aliases } = await listVhds(handler, vmDir)
@@ -210,27 +209,18 @@ exports.cleanVm = async function cleanVm(
}
vhdChildren[parent] = path
}
// Detect VHDs with the same UUIDs
//
// Due to a bug introduced in a1bcd35e2
const duplicate = vhdById.get(UUID.stringify(vhd.footer.uuid))
let vhdKept = vhd
const duplicate = vhdById.get(vhd.footer.uuid)
if (duplicate !== undefined) {
logWarn('uuid is duplicated', { uuid: UUID.stringify(vhd.footer.uuid) })
logWarn('uuid is duplicated', { uuid: vhd.footer.uuid })
if (duplicate.containsAllDataOf(vhd)) {
logWarn(`should delete ${path}`)
vhdKept = duplicate
vhds.delete(path)
} else if (vhd.containsAllDataOf(duplicate)) {
logWarn(`should delete ${duplicate._path}`)
vhds.delete(duplicate._path)
} else {
logWarn(`same ids but different content`)
}
} else {
logInfo('not duplicate', UUID.stringify(vhd.footer.uuid), path)
}
vhdById.set(UUID.stringify(vhdKept.footer.uuid), vhdKept)
vhdById.set(vhd.footer.uuid, vhd)
})
} catch (error) {
vhds.delete(path)
@@ -241,6 +231,9 @@ exports.cleanVm = async function cleanVm(
}
}
})
// the vhd are closed at the end of the disposable
// it's unsafe to use them later
vhdById.clear()
// remove interrupted merge states for missing VHDs
for (const interruptedVhd of interruptedVhds.keys()) {
@@ -385,7 +378,7 @@ exports.cleanVm = async function cleanVm(
const unusedVhdsDeletion = []
const toMerge = []
{
// VHD chains (as list from oldest to most recent) to merge indexed by most recent
// VHD chains (as list from child to ancestor) to merge indexed by last
// ancestor
const vhdChainsToMerge = { __proto__: null }
@@ -409,7 +402,7 @@ exports.cleanVm = async function cleanVm(
if (child !== undefined) {
const chain = getUsedChildChainOrDelete(child)
if (chain !== undefined) {
chain.unshift(vhd)
chain.push(vhd)
return chain
}
}

View File

@@ -8,7 +8,7 @@
"type": "git",
"url": "https://github.com/vatesfr/xen-orchestra.git"
},
"version": "0.27.0",
"version": "0.26.0",
"engines": {
"node": ">=14.6"
},
@@ -38,7 +38,7 @@
"promise-toolbox": "^0.21.0",
"proper-lockfile": "^4.1.2",
"uuid": "^8.3.2",
"vhd-lib": "^3.3.2",
"vhd-lib": "^3.3.1",
"yazl": "^2.5.1"
},
"devDependencies": {

View File

@@ -1,219 +0,0 @@
import { createLogger } from '@xen-orchestra/log'
import { genSelfSignedCert } from '@xen-orchestra/self-signed'
import pRetry from 'promise-toolbox/retry'
import { X509Certificate } from 'crypto'
import fs from 'node:fs/promises'
import { dirname } from 'path'
import pw from 'pw'
import tls from 'node:tls'
const { debug, info, warn } = createLogger('xo:mixins:sslCertificate')
async function outputFile(path, content) {
await fs.mkdir(dirname(path), { recursive: true })
await fs.writeFile(path, content, { flag: 'w', mode: 0o400 })
}
class SslCertificate {
#app
#configKey
#updateSslCertificatePromise
#secureContext
#validTo
constructor(app, configKey) {
this.#app = app
this.#configKey = configKey
}
#createSecureContext(cert, key, passphrase) {
return tls.createSecureContext({
cert,
key,
passphrase,
})
}
// load on register
async #loadSslCertificate(config) {
const certPath = config.cert
const keyPath = config.key
let key, cert, passphrase
try {
;[cert, key] = await Promise.all([fs.readFile(certPath), fs.readFile(keyPath)])
if (keyPath.includes('ENCRYPTED')) {
if (config.autoCert) {
throw new Error(`encrytped certificates aren't compatible with autoCert option`)
}
passphrase = await new Promise(resolve => {
// eslint-disable-next-line no-console
process.stdout.write(`Enter pass phrase: `)
pw(resolve)
})
}
} catch (error) {
if (!(config.autoCert && error.code === 'ENOENT')) {
throw error
}
// self signed certificate or let's encrypt will be generated on demand
}
// create secure context also make a validation of the certificate
const secureContext = this.#createSecureContext(cert, key, passphrase)
this.#secureContext = secureContext
// will be tested and eventually renewed on first query
const { validTo } = new X509Certificate(cert)
this.#validTo = new Date(validTo)
}
#getConfig() {
const config = this.#app.config.get(this.#configKey)
if (config === undefined) {
throw new Error(`config for key ${this.#configKey} is unavailable`)
}
return config
}
async #getSelfSignedContext(config) {
return pRetry(
async () => {
const { cert, key } = await genSelfSignedCert()
info('new certificates generated', { cert, key })
try {
await Promise.all([outputFile(config.cert, cert), outputFile(config.key, key)])
} catch (error) {
warn(`can't save self signed certificates `, { error, config })
}
// create secure context also make a validation of the certificate
const { validTo } = new X509Certificate(cert)
return { secureContext: this.#createSecureContext(cert, key), validTo: new Date(validTo) }
},
{
tries: 2,
when: e => e.code === 'ERR_SSL_EE_KEY_TOO_SMALL',
onRetry: () => {
warn('got ERR_SSL_EE_KEY_TOO_SMALL while generating self signed certificate ')
},
}
)
}
// get the current certificate for this hostname
async getSecureContext(hostName) {
const config = this.#getConfig()
if (config === undefined) {
throw new Error(`config for key ${this.#configKey} is unavailable`)
}
if (this.#updateSslCertificatePromise) {
debug('certificate is already refreshing')
return this.#updateSslCertificatePromise
}
let certificateIsValid = this.#validTo !== undefined
let shouldRenew = !certificateIsValid
if (certificateIsValid) {
certificateIsValid = this.#validTo >= new Date()
shouldRenew = !certificateIsValid || this.#validTo - new Date() < 30 * 24 * 60 * 60 * 1000
}
let promise = Promise.resolve()
if (shouldRenew) {
try {
// @todo : should also handle let's encrypt
if (config.autoCert === true) {
promise = promise.then(() => this.#getSelfSignedContext(config))
}
this.#updateSslCertificatePromise = promise
// cleanup and store
promise = promise.then(
({ secureContext, validTo }) => {
this.#validTo = validTo
this.#secureContext = secureContext
this.#updateSslCertificatePromise = undefined
return secureContext
},
async error => {
console.warn('error while updating ssl certificate', { error })
this.#updateSslCertificatePromise = undefined
if (!certificateIsValid) {
// we couldn't generate a valid certificate
// only throw if the current certificate is invalid
warn('deleting invalid certificate')
this.#secureContext = undefined
this.#validTo = undefined
await Promise.all([fs.unlink(config.cert), fs.unlink(config.key)])
throw error
}
}
)
} catch (error) {
warn('error while refreshing ssl certificate', { error })
throw error
}
}
if (certificateIsValid) {
// still valid : does not need to wait for the refresh
return this.#secureContext
}
if (this.#updateSslCertificatePromise === undefined) {
throw new Error(`Invalid certificate and no strategy defined to renew it. Try activating autoCert in the config`)
}
// invalid cert : wait for refresh
return this.#updateSslCertificatePromise
}
async register() {
await this.#loadSslCertificate(this.#getConfig())
}
}
export default class SslCertificates {
#app
#handlers = {}
constructor(app, { httpServer }) {
// don't setup the proxy if httpServer is not present
//
// that can happen when the app is instanciated in another context like xo-server-recover-account
if (httpServer === undefined) {
return
}
this.#app = app
httpServer.getSecureContext = this.getSecureContext.bind(this)
}
async getSecureContext(hostname, configKey) {
const config = this.#app.config.get(`http.listen.${configKey}`)
if (!config || !config.cert || !config.key) {
throw new Error(`HTTPS configuration does no exists for key http.listen.${configKey}`)
}
if (this.#handlers[configKey] === undefined) {
throw new Error(`the SslCertificate handler for key http.listen.${configKey} does not exists.`)
}
return this.#handlers[configKey].getSecureContext(hostname, config)
}
async register() {
// http.listen can be an array or an object
const configs = this.#app.config.get('http.listen') || []
const configKeys = Object.keys(configs) || []
await Promise.all(
configKeys
.filter(configKey => configs[configKey].cert !== undefined && configs[configKey].key !== undefined)
.map(async configKey => {
this.#handlers[configKey] = new SslCertificate(this.#app, `http.listen.${configKey}`)
return this.#handlers[configKey].register(configs[configKey])
})
)
}
}

View File

@@ -16,18 +16,16 @@
"license": "AGPL-3.0-or-later",
"version": "0.5.0",
"engines": {
"node": ">=14"
"node": ">=12"
},
"dependencies": {
"@vates/event-listeners-manager": "^1.0.1",
"@vates/parse-duration": "^0.1.1",
"@xen-orchestra/emit-async": "^1.0.0",
"@xen-orchestra/log": "^0.3.0",
"@xen-orchestra/self-signed": "^0.1.3",
"app-conf": "^2.1.0",
"lodash": "^4.17.21",
"promise-toolbox": "^0.21.0",
"pw": "^0.0.4"
"promise-toolbox": "^0.21.0"
},
"scripts": {
"postversion": "npm publish --access public"

View File

@@ -3,11 +3,13 @@
import forOwn from 'lodash/forOwn.js'
import fse from 'fs-extra'
import getopts from 'getopts'
import pRetry from 'promise-toolbox/retry'
import { catchGlobalErrors } from '@xen-orchestra/log/configure.js'
import { create as createServer } from 'http-server-plus'
import { createCachedLookup } from '@vates/cached-dns.lookup'
import { createLogger } from '@xen-orchestra/log'
import { createSecureServer } from 'http2'
import { genSelfSignedCert } from '@xen-orchestra/self-signed'
import { load as loadConfig } from 'app-conf'
// -------------------------------------------------------------------
@@ -54,21 +56,41 @@ ${APP_NAME} v${APP_VERSION}
createSecureServer: opts => createSecureServer({ ...opts, allowHTTP1: true }),
})
forOwn(config.http.listen, async ({ autoCert, cert, key, ...opts }, listenKey) => {
forOwn(config.http.listen, async ({ autoCert, cert, key, ...opts }) => {
try {
if (cert !== undefined && key !== undefined) {
opts.SNICallback = async (serverName, callback) => {
// injected by @xen-orchestr/mixins/sslCertificate.mjs
try {
const secureContext = await httpServer.getSecureContext(serverName, listenKey)
callback(null, secureContext)
} catch (error) {
warn('An error occured during certificate context creation', { error, listenKey, serverName })
callback(error)
const niceAddress = await pRetry(
async () => {
if (cert !== undefined && key !== undefined) {
try {
opts.cert = fse.readFileSync(cert)
opts.key = fse.readFileSync(key)
} catch (error) {
if (!(autoCert && error.code === 'ENOENT')) {
throw error
}
const pems = await genSelfSignedCert()
fse.outputFileSync(cert, pems.cert, { flag: 'wx', mode: 0o400 })
fse.outputFileSync(key, pems.key, { flag: 'wx', mode: 0o400 })
info('new certificate generated', { cert, key })
opts.cert = pems.cert
opts.key = pems.key
}
}
return httpServer.listen(opts)
},
{
tries: 2,
when: e => autoCert && e.code === 'ERR_SSL_EE_KEY_TOO_SMALL',
onRetry: () => {
warn('deleting invalid certificate')
fse.unlinkSync(cert)
fse.unlinkSync(key)
},
}
}
const niceAddress = httpServer.listen(opts)
)
info(`Web server listening on ${niceAddress}`)
} catch (error) {
if (error.niceAddress !== undefined) {
@@ -116,8 +138,6 @@ ${APP_NAME} v${APP_VERSION}
const { default: fromCallback } = await import('promise-toolbox/fromCallback')
app.hooks.on('stop', () => fromCallback(cb => httpServer.stop(cb)))
await app.sslCertificate.register()
await app.hooks.start()
// Gracefully shutdown on signals.
@@ -126,7 +146,6 @@ ${APP_NAME} v${APP_VERSION}
process.on(signal, () => {
if (alreadyCalled) {
warn('forced exit')
// eslint-disable-next-line n/no-process-exit
process.exit(1)
}
alreadyCalled = true
@@ -144,7 +163,7 @@ main(process.argv.slice(2)).then(
},
error => {
fatal(error)
// eslint-disable-next-line n/no-process-exit
process.exit(1)
}
)

View File

@@ -1,7 +1,7 @@
{
"private": true,
"name": "@xen-orchestra/proxy",
"version": "0.23.5",
"version": "0.23.4",
"license": "AGPL-3.0-or-later",
"description": "XO Proxy used to remotely execute backup jobs",
"keywords": [
@@ -26,17 +26,18 @@
},
"dependencies": {
"@iarna/toml": "^2.2.0",
"@koa/router": "^11.0.1",
"@koa/router": "^10.0.0",
"@vates/cached-dns.lookup": "^1.0.0",
"@vates/compose": "^2.1.0",
"@vates/decorate-with": "^2.0.0",
"@vates/disposable": "^0.1.1",
"@xen-orchestra/async-map": "^0.1.2",
"@xen-orchestra/backups": "^0.27.0",
"@xen-orchestra/backups": "^0.26.0",
"@xen-orchestra/fs": "^1.1.0",
"@xen-orchestra/log": "^0.3.0",
"@xen-orchestra/mixin": "^0.1.0",
"@xen-orchestra/mixins": "^0.5.0",
"@xen-orchestra/self-signed": "^0.1.3",
"@xen-orchestra/xapi": "^1.4.0",
"ajv": "^8.0.3",
"app-conf": "^2.1.0",

View File

@@ -29,7 +29,7 @@
"json-rpc-protocol": "^0.13.2",
"lodash": "^4.17.15",
"promise-toolbox": "^0.21.0",
"vhd-lib": "^3.3.2",
"vhd-lib": "^3.3.1",
"xo-common": "^0.8.0"
},
"private": false,

View File

@@ -1,32 +1,9 @@
# ChangeLog
## **5.72.1** (2022-07-11)
## **5.72.0** (2022-06-30)
<img id="latest" src="https://badgen.net/badge/channel/latest/yellow" alt="Channel: latest" />
### Enhancements
- [SR] When SR is in maintenance, add "Maintenance mode" badge next to its name (PR [#6313](https://github.com/vatesfr/xen-orchestra/pull/6313))
### Bug fixes
- [Tasks] Fix tasks not displayed when running CR backup job [Forum#6038](https://xcp-ng.org/forum/topic/6038/not-seeing-tasks-any-more-as-admin) (PR [#6315](https://github.com/vatesfr/xen-orchestra/pull/6315))
- [Backup] Fix failing merge multiple VHDs at once (PR [#6317](https://github.com/vatesfr/xen-orchestra/pull/6317))
- [VM/Console] Fix _Connect with SSH/RDP_ when address is IPv6
- [Audit] Ignore side-effects free API methods `xoa.check`, `xoa.clearCheckCache` and `xoa.getHVSupportedVersions`
### Released packages
- @xen-orchestra/backups 0.27.0
- @xen-orchestra/backups-cli 0.7.5
- @xen-orchestra/proxy 0.23.5
- vhd-lib 3.3.2
- xo-server 5.98.1
- xo-server-audit 0.10.0
- xo-web 5.100.0
## **5.72.0** (2022-06-30)
### Highlights
- [Backup] Merge delta backups without copying data when using VHD directories on NFS/SMB/local remote(https://github.com/vatesfr/xen-orchestra/pull/6271))

View File

@@ -7,6 +7,8 @@
> Users must be able to say: “Nice enhancement, I'm eager to test it”
- [SR] When SR is in maintenance, add "Maintenance mode" badge next to its name (PR [#6313](https://github.com/vatesfr/xen-orchestra/pull/6313))
### Bug fixes
> Users must be able to say: “I had this issue, happy to know it's fixed”
@@ -27,9 +29,8 @@
<!--packages-start-->
- @vates/async-each major
- @xen-orchestra/mixins minor
- @xen-orchestra/proxy patch
- @xen-orchestra/xo-server patch
- @xen-orchestra/backups minor
- xo-web minor
- vhd-lib minor
<!--packages-end-->

View File

@@ -24,15 +24,16 @@ Please, do explain:
The best way to propose a change to the documentation or code is
to create a [GitHub pull request](https://help.github.com/articles/using-pull-requests/).
1. Fork the [Xen Orchestra repository](https://github.com/vatesfr/xen-orchestra) using the Fork button
2. Follow [the documentation](installation.md#from-the-sources) to install and run Xen Orchestra from the sources
3. Create a branch for your work
4. Edit the source files
5. Add a summary of your changes to `CHANGELOG.unreleased.md`, if your changes do not relate to an existing changelog item and update the list of packages that must be released to take your changes into account
6. [Create a pull request](https://github.com/vatesfr/xen-orchestra/compare) for this branch against the `master` branch
7. Push into the branch until the pull request is ready to merge
8. Avoid unnecessary merges: keep you branch up to date by regularly rebasing `git rebase origin/master`
9. When ready to merge, clean up the history (reorder commits, squash some of them together, rephrase messages): `git rebase -i origin/master`
:::tip
Your pull request should always be against the `master` branch and not against `stable` which is the stable branch!
:::
1. Create a branch for your work
2. Add a summary of your changes to `CHANGELOG.md` under the `next` section, if your changes do not relate to an existing changelog item
3. Create a pull request for this branch against the `master` branch
4. Push into the branch until the pull request is ready to merge
5. Avoid unnecessary merges: keep you branch up to date by regularly rebasing `git rebase origin/master`
6. When ready to merge, clean up the history (reorder commits, squash some of them together, rephrase messages): `git rebase -i origin/master`
### Issue triage

View File

@@ -3,7 +3,7 @@
"@babel/core": "^7.0.0",
"@babel/eslint-parser": "^7.13.8",
"@babel/register": "^7.0.0",
"babel-jest": "^28.1.2",
"babel-jest": "^27.3.1",
"benchmark": "^2.1.4",
"deptree": "^1.0.0",
"eslint": "^8.7.0",
@@ -19,7 +19,7 @@
"globby": "^13.1.1",
"handlebars": "^4.7.6",
"husky": "^4.2.5",
"jest": "^28.1.2",
"jest": "^27.3.1",
"lint-staged": "^12.0.3",
"lodash": "^4.17.4",
"prettier": "^2.0.5",

View File

@@ -31,7 +31,7 @@
"lodash": "^4.17.21",
"promise-toolbox": "^0.21.0",
"uuid": "^8.3.2",
"vhd-lib": "^3.3.2"
"vhd-lib": "^3.3.1"
},
"scripts": {
"postversion": "npm publish"

View File

@@ -275,3 +275,26 @@ it('can stream content', async () => {
}
})
})
it('can check vhd contained in on another', async () => {
const rawFile = `${tempDir}/contained`
await createRandomFile(rawFile, 4)
const containedVhdFileName = `${tempDir}/contained.vhd`
await convertFromRawToVhd(rawFile, containedVhdFileName)
const after = `${tempDir}/after`
await createRandomFile(after, 4)
fs.appendFile(rawFile, await fs.readFile(after))
const cnotainerVhdFileName = `${tempDir}/container.vhd`
await convertFromRawToVhd(rawFile, cnotainerVhdFileName)
await Disposable.use(async function* () {
const handler = yield getSyncedHandler({ url: 'file://' + tempDir })
const contained = yield openVhd(handler, 'contained.vhd')
const container = yield openVhd(handler, 'container.vhd')
expect(await contained.contains(container)).toEqual(false)
expect(await container.contains(contained)).toEqual(true)
})
})

View File

@@ -86,19 +86,10 @@ exports.VhdAbstract = class VhdAbstract {
}
/**
* @typedef {Object} BitmapBlock
* @property {number} id
* @property {Buffer} bitmap
*
* @typedef {Object} FullBlock
* @property {number} id
* @property {Buffer} bitmap
* @property {Buffer} data
* @property {Buffer} buffer - bitmap + data
*
* @param {number} blockId
* @param {boolean} onlyBitmap
* @returns {Promise<BitmapBlock | FullBlock>}
* @returns {Buffer}
*/
readBlock(blockId, onlyBitmap = false) {
throw new Error(`reading ${onlyBitmap ? 'bitmap of block' : 'block'} ${blockId} is not implemented`)
@@ -344,6 +335,10 @@ exports.VhdAbstract = class VhdAbstract {
return stream
}
/*
* check if all the data of a child are already contained in this vhd
*/
async containsAllDataOf(child) {
await this.readBlockAllocationTable()
await child.readBlockAllocationTable()

View File

@@ -55,7 +55,7 @@ test('It can read block and parent locator from a synthetic vhd', async () => {
await bigVhd.readHeaderAndFooter()
const syntheticVhd = yield VhdSynthetic.open(handler, [bigVhdFileName, smallVhdFileName])
const syntheticVhd = yield VhdSynthetic.open(handler, [smallVhdFileName, bigVhdFileName])
await syntheticVhd.readBlockAllocationTable()
expect(syntheticVhd.header.diskType).toEqual(bigVhd.header.diskType)

View File

@@ -15,13 +15,14 @@ const VhdSynthetic = class VhdSynthetic extends VhdAbstract {
#vhds = []
get header() {
// this the most recent vhd
const vhd = this.#vhds[this.#vhds.length - 1]
// this the VHD we want to synthetize
const vhd = this.#vhds[0]
// this is the root VHD
const rootVhd = this.#vhds[0]
const rootVhd = this.#vhds[this.#vhds.length - 1]
// data of our synthetic VHD
// TODO: set parentLocatorEntry-s in header
return {
...vhd.header,
parentLocatorEntry: cloneDeep(rootVhd.header.parentLocatorEntry),
@@ -33,13 +34,10 @@ const VhdSynthetic = class VhdSynthetic extends VhdAbstract {
}
get footer() {
// this the most recent vhd
const vhd = this.#vhds[this.#vhds.length - 1]
// this is the oldest VHD
const rootVhd = this.#vhds[0]
// this is the root VHD
const rootVhd = this.#vhds[this.#vhds.length - 1]
return {
...vhd.footer,
...this.#vhds[0].footer,
dataOffset: FOOTER_SIZE,
diskType: rootVhd.footer.diskType,
}
@@ -79,21 +77,17 @@ const VhdSynthetic = class VhdSynthetic extends VhdAbstract {
await asyncMap(vhds, vhd => vhd.readHeaderAndFooter())
for (let i = 0, n = vhds.length - 1; i < n; ++i) {
const parent = vhds[i]
const child = vhds[i + 1]
const child = vhds[i]
const parent = vhds[i + 1]
assert.strictEqual(child.footer.diskType, DISK_TYPES.DIFFERENCING)
assert.strictEqual(UUID.stringify(child.header.parentUuid), UUID.stringify(parent.footer.uuid))
}
}
#getVhdWithBlock(blockId) {
for (let i = this.#vhds.length - 1; i >= 0; i--) {
const vhd = this.#vhds[i]
if (vhd.containsBlock(blockId)) {
return vhd
}
}
assert(false, `no such block ${blockId}`)
const index = this.#vhds.findIndex(vhd => vhd.containsBlock(blockId))
assert(index !== -1, `no such block ${blockId}`)
return this.#vhds[index]
}
async readBlock(blockId, onlyBitmap = false) {
@@ -126,7 +120,7 @@ VhdSynthetic.fromVhdChain = Disposable.factory(async function* fromVhdChain(hand
const vhds = []
do {
vhd = yield openVhd(handler, vhdPath)
vhds.unshift(vhd) // from oldest to most recent
vhds.push(vhd)
vhdPath = resolveRelativeFromFile(vhdPath, vhd.header.parentUnicodeName)
} while (vhd.footer.diskType !== DISK_TYPES.DYNAMIC)

View File

@@ -217,7 +217,8 @@ test('it cleans vhd mergedfiles', async () => {
await handler.writeFile('child2', 'child2Data')
await handler.writeFile('child3', 'child3Data')
await cleanupVhds(handler, 'parent', ['child1', 'child2', 'child3'], { remove: true })
// childPath is from the grand children to the children
await cleanupVhds(handler, 'parent', ['child3', 'child2', 'child1'], { remove: true })
// only child3 should stay, with the data of parent
const [child3, ...other] = await handler.list('.')

View File

@@ -59,7 +59,7 @@ function cleanupVhds(handler, parent, children, { logInfo = noop, remove = false
if (!Array.isArray(children)) {
children = [children]
}
const mergeTargetChild = children.pop()
const mergeTargetChild = children.shift()
return Promise.all([
VhdAbstract.rename(handler, parent, mergeTargetChild),

View File

@@ -1,7 +1,7 @@
{
"private": false,
"name": "vhd-lib",
"version": "3.3.2",
"version": "3.3.1",
"license": "AGPL-3.0-or-later",
"description": "Primitives for VHD file handling",
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/packages/vhd-lib",

View File

@@ -8,6 +8,6 @@
"promise-toolbox": "^0.19.2",
"readable-stream": "^3.1.1",
"throttle": "^1.0.3",
"vhd-lib": "^3.3.2"
"vhd-lib": "^3.3.1"
}
}

View File

@@ -40,7 +40,7 @@
"xo-lib": "^0.11.1"
},
"devDependencies": {
"@types/node": "^18.0.1",
"@types/node": "^17.0.25",
"@types/through2": "^2.0.31",
"typescript": "^4.6.3"
},

View File

@@ -1,6 +1,6 @@
{
"name": "xo-server-audit",
"version": "0.10.0",
"version": "0.9.3",
"license": "AGPL-3.0-or-later",
"description": "Audit plugin for XO-Server",
"keywords": [

View File

@@ -56,10 +56,7 @@ const DEFAULT_BLOCKED_LIST = {
'vm.getHaValues': true,
'vm.stats': true,
'xo.getAllObjects': true,
'xoa.check': true,
'xoa.clearCheckCache': true,
'xoa.getApplianceInfo': true,
'xoa.getHVSupportedVersions': true,
'xoa.licenses.get': true,
'xoa.licenses.getAll': true,
'xoa.licenses.getSelf': true,

View File

@@ -35,7 +35,7 @@
"ensure-array": "^1.0.0",
"exec-promise": "^0.7.0",
"inquirer": "^8.0.0",
"ldapts": "^4.1.0",
"ldapts": "^3.1.1",
"promise-toolbox": "^0.21.0"
},
"devDependencies": {

View File

@@ -31,7 +31,7 @@
"app-conf": "^2.1.0",
"babel-plugin-lodash": "^3.2.11",
"golike-defer": "^0.5.1",
"jest": "^28.1.2",
"jest": "^27.3.1",
"lodash": "^4.17.11",
"promise-toolbox": "^0.21.0",
"xo-collection": "^0.5.0",

View File

@@ -1,7 +1,7 @@
{
"private": true,
"name": "xo-server",
"version": "5.98.1",
"version": "5.98.0",
"license": "AGPL-3.0-or-later",
"description": "Server part of Xen-Orchestra",
"keywords": [
@@ -40,7 +40,7 @@
"@vates/predicates": "^1.0.0",
"@vates/read-chunk": "^1.0.0",
"@xen-orchestra/async-map": "^0.1.2",
"@xen-orchestra/backups": "^0.27.0",
"@xen-orchestra/backups": "^0.26.0",
"@xen-orchestra/cron": "^1.0.6",
"@xen-orchestra/defined": "^0.0.1",
"@xen-orchestra/emit-async": "^1.0.0",
@@ -48,6 +48,7 @@
"@xen-orchestra/log": "^0.3.0",
"@xen-orchestra/mixin": "^0.1.0",
"@xen-orchestra/mixins": "^0.5.0",
"@xen-orchestra/self-signed": "^0.1.3",
"@xen-orchestra/template": "^0.1.0",
"@xen-orchestra/xapi": "^1.4.0",
"ajv": "^8.0.3",
@@ -75,7 +76,7 @@
"fs-extra": "^10.0.0",
"get-stream": "^6.0.0",
"golike-defer": "^0.5.1",
"hashy": "^0.11.1",
"hashy": "^0.11.0",
"helmet": "^3.9.0",
"highland": "^2.11.1",
"http-proxy": "^1.16.2",
@@ -103,12 +104,13 @@
"openpgp": "^5.0.0",
"otplib": "^11.0.0",
"partial-stream": "0.0.0",
"passport": "^0.6.0",
"passport": "^0.5.2",
"passport-local": "^1.0.0",
"promise-toolbox": "^0.21.0",
"proxy-agent": "^5.0.0",
"pug": "^3.0.0",
"pumpify": "^2.0.0",
"pw": "^0.0.4",
"readable-stream": "^3.2.0",
"redis": "^3.0.2",
"schema-inspector": "^2.0.1",
@@ -124,7 +126,7 @@
"unzipper": "^0.10.5",
"uuid": "^8.3.1",
"value-matcher": "^0.2.0",
"vhd-lib": "^3.3.2",
"vhd-lib": "^3.3.1",
"ws": "^8.2.3",
"xdg-basedir": "^5.1.0",
"xen-api": "^1.2.1",

View File

@@ -17,6 +17,7 @@ import merge from 'lodash/merge.js'
import ms from 'ms'
import once from 'lodash/once.js'
import proxyConsole from './proxy-console.mjs'
import pw from 'pw'
import serveStatic from 'serve-static'
import stoppable from 'stoppable'
import WebServer from 'http-server-plus'
@@ -25,6 +26,7 @@ import { asyncMap } from '@xen-orchestra/async-map'
import { xdgConfig } from 'xdg-basedir'
import { createLogger } from '@xen-orchestra/log'
import { createRequire } from 'module'
import { genSelfSignedCert } from '@xen-orchestra/self-signed'
import { parseDuration } from '@vates/parse-duration'
import { URL } from 'url'
@@ -75,6 +77,7 @@ configure([
])
const log = createLogger('xo:main')
// ===================================================================
const DEPRECATED_ENTRIES = ['users', 'servers']
@@ -397,24 +400,34 @@ async function makeWebServerListen(
cert = certificate,
key,
listenKey,
...opts
}
) {
try {
if (cert && key) {
opts.SNICallback = async (serverName, callback) => {
// injected by @xen-orchestr/mixins/sslCertificate.mjs
try {
const secureContext = await webServer.getSecureContext(serverName, listenKey)
callback(null, secureContext)
} catch (error) {
log.warn('An error occured during certificate context creation', { error, listenKey, serverName })
callback(error)
try {
;[opts.cert, opts.key] = await Promise.all([fse.readFile(cert), fse.readFile(key)])
if (opts.key.includes('ENCRYPTED')) {
opts.passphrase = await new Promise(resolve => {
// eslint-disable-next-line no-console
console.log('Encrypted key %s', key)
process.stdout.write(`Enter pass phrase: `)
pw(resolve)
})
}
} catch (error) {
if (!(autoCert && error.code === 'ENOENT')) {
throw error
}
const pems = await genSelfSignedCert()
await Promise.all([
fse.outputFile(cert, pems.cert, { flag: 'wx', mode: 0o400 }),
fse.outputFile(key, pems.key, { flag: 'wx', mode: 0o400 }),
])
log.info('new certificate generated', { cert, key })
opts.cert = pems.cert
opts.key = pems.key
}
}
@@ -440,9 +453,7 @@ async function makeWebServerListen(
async function createWebServer({ listen, listenOptions }) {
const webServer = stoppable(new WebServer())
await Promise.all(
map(listen, (opts, listenKey) => makeWebServerListen(webServer, { ...listenOptions, ...opts, listenKey }))
)
await Promise.all(map(listen, opts => makeWebServerListen(webServer, { ...listenOptions, ...opts })))
return webServer
}
@@ -786,8 +797,6 @@ export default async function main(args) {
// Must be set up before the API.
express.use(xo._handleHttpRequest.bind(xo))
await xo.sslCertificate.register()
// Everything above is not protected by the sign in, allowing xo-cli
// to work properly.
await setUpPassport(express, xo, config)
@@ -814,7 +823,6 @@ export default async function main(args) {
process.on(signal, () => {
if (alreadyCalled) {
log.warn('forced exit')
// eslint-disable-next-line n/no-process-exit
process.exit(1)
}
alreadyCalled = true

View File

@@ -8,7 +8,6 @@ import iteratee from 'lodash/iteratee.js'
import mixin from '@xen-orchestra/mixin'
import mixinLegacy from '@xen-orchestra/mixin/legacy.js'
import stubTrue from 'lodash/stubTrue.js'
import SslCertificate from '@xen-orchestra/mixins/SslCertificate.mjs'
import { Collection as XoCollection } from 'xo-collection'
import { createClient as createRedisClient } from 'redis'
import { createDebounceResource } from '@vates/disposable/debounceResource.js'
@@ -30,7 +29,7 @@ export default class Xo extends EventEmitter {
constructor(opts) {
super()
mixin(this, { Config, Hooks, HttpProxy, SslCertificate }, [opts])
mixin(this, { Config, Hooks, HttpProxy }, [opts])
// a lot of mixins adds listener for start/stop/… events
this.hooks.setMaxListeners(0)

View File

@@ -26,7 +26,7 @@
"pako": "^2.0.4",
"promise-toolbox": "^0.21.0",
"tar-stream": "^2.2.0",
"vhd-lib": "^3.3.2",
"vhd-lib": "^3.3.1",
"xml2js": "^0.4.23"
},
"devDependencies": {

View File

@@ -1,7 +1,7 @@
{
"private": true,
"name": "xo-web",
"version": "5.100.0",
"version": "5.99.0",
"license": "AGPL-3.0-or-later",
"description": "Web interface client for Xen-Orchestra",
"keywords": [

View File

@@ -638,10 +638,11 @@ export const getResolvedPendingTasks = create(
// { taskId → operation } map instead of { taskRef → operation } map
...defined(linkedObjectsByTaskRefOrId[task.id], []),
]
resolvedTasks.push({
...task,
objects,
})
objects.length > 0 &&
resolvedTasks.push({
...task,
objects,
})
})
return resolvedTasks
}

View File

@@ -17,9 +17,6 @@ import { Col, Container, Row } from 'grid'
import { confirm, form } from 'modal'
import { CpuSparkLines, MemorySparkLines, NetworkSparkLines, XvdSparkLines } from 'xo-sparklines'
// add `[]` around the hostname if it's an IPv6 address
const formatHostname = h => (h.indexOf(':') !== -1 ? `[${h}]` : h)
class SendToClipboard extends Component {
state = { value: this.props.clipboard }
@@ -93,7 +90,7 @@ export default class TabConsole extends Component {
}
_openSsh = (username = 'root') => {
window.location = `ssh://${encodeURIComponent(username)}@${formatHostname(this.props.vm.mainIpAddress)}`
window.location = `ssh://${encodeURIComponent(username)}@${this.props.vm.mainIpAddress}`
}
_openSshMore = async () => {
@@ -114,7 +111,7 @@ export default class TabConsole extends Component {
}
_openRdp = () => {
window.location = `rdp://${formatHostname(this.props.vm.mainIpAddress)}`
window.location = `rdp://${this.props.vm.mainIpAddress}`
}
render() {

3918
yarn.lock

File diff suppressed because it is too large Load Diff