Compare commits
2 Commits
refacto_ss
...
check_vhd_
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
13d9edf4ce | ||
|
|
64a6b624b7 |
@@ -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`
|
||||
|
||||
|
||||
@@ -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`
|
||||
|
||||
|
||||
@@ -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 = []
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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])
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
25
CHANGELOG.md
25
CHANGELOG.md
@@ -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))
|
||||
|
||||
@@ -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-->
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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('.')
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user