Compare commits

..

4 Commits

Author SHA1 Message Date
b-Nollet
a75034bc6d execute prepositionning only for significant enough differences 2024-02-01 09:49:13 +01:00
b-Nollet
921e847711 post-review corrections 2024-01-31 18:03:18 +01:00
b-Nollet
7670ddf2e8 changelog entry 2024-01-23 17:14:04 +01:00
b-Nollet
d0476b563f feat(xo-server-load-balancer): balance vCPU option 2024-01-23 17:02:03 +01:00
169 changed files with 3368 additions and 6769 deletions

View File

@@ -65,11 +65,10 @@ module.exports = {
typescript: true,
'eslint-import-resolver-custom-alias': {
alias: {
'@core': '../web-core/lib',
'@': './src',
},
extensions: ['.ts'],
packages: ['@xen-orchestra/lite', '@xen-orchestra/web'],
packages: ['@xen-orchestra/lite'],
},
},
},
@@ -80,25 +79,6 @@ module.exports = {
'vue/require-default-prop': 'off', // https://github.com/vuejs/eslint-plugin-vue/issues/2051
},
},
{
files: ['@xen-orchestra/{web-core,lite,web}/src/pages/**/*.vue'],
parserOptions: {
sourceType: 'module',
},
rules: {
'vue/multi-word-component-names': 'off',
},
},
{
files: ['@xen-orchestra/{web-core,lite,web}/typed-router.d.ts'],
parserOptions: {
sourceType: 'module',
},
rules: {
'eslint-comments/disable-enable-pair': 'off',
'eslint-comments/no-unlimited-disable': 'off',
},
},
],
parserOptions: {

View File

@@ -64,7 +64,7 @@ body:
id: error-message
attributes:
label: Error message
render: Text
render: Markdown
validations:
required: false
- type: textarea

View File

@@ -24,12 +24,8 @@ jobs:
cache: 'yarn'
- name: Install project dependencies
run: yarn
- name: Ensure yarn.lock is up-to-date
run: git diff --exit-code yarn.lock
- name: Build the project
run: yarn build
- name: Unit tests
run: yarn test-unit
- name: Lint tests
run: yarn test-lint
- name: Integration tests

View File

@@ -34,6 +34,7 @@
},
"devDependencies": {
"sinon": "^17.0.1",
"tap": "^16.3.0",
"test": "^3.2.1"
}
}

View File

@@ -20,7 +20,7 @@
"url": "https://vates.fr"
},
"license": "ISC",
"version": "2.1.0",
"version": "2.0.0",
"engines": {
"node": ">=8.10"
},

View File

@@ -20,9 +20,6 @@ function assertListeners(t, event, listeners) {
}
t.beforeEach(function (t) {
// work around https://github.com/tapjs/tapjs/issues/998
t.context = {}
t.context.ee = new EventEmitter()
t.context.em = new EventListenersManager(t.context.ee)
})

View File

@@ -38,9 +38,9 @@
"version": "1.0.1",
"scripts": {
"postversion": "npm publish --access public",
"test": "tap --allow-incomplete-coverage"
"test": "tap --branches=72"
},
"devDependencies": {
"tap": "^18.7.0"
"tap": "^16.2.0"
}
}

View File

@@ -1,28 +0,0 @@
Mount a vhd generated by xen-orchestra to filesystem
### Library
```js
import { mount } from 'fuse-vhd'
// return a disposable, see promise-toolbox/Disposable
// unmount automatically when disposable is disposed
// in case of differencing VHD, it mounts the full chain
await mount(handler, diskId, mountPoint)
```
### cli
From the install folder :
```
cli.mjs <remoteUrl> <vhdPathInRemote> <mountPoint>
```
After installing the package
```
xo-fuse-vhd <remoteUrl> <vhdPathInRemote> <mountPoint>
```
remoteUrl can be found by using cli in `@xen-orchestra/fs` , for example a local remote will have a url like `file:///path/to/remote/root`

View File

@@ -1,59 +0,0 @@
<!-- DO NOT EDIT MANUALLY, THIS FILE HAS BEEN GENERATED -->
# @vates/fuse-vhd
[![Package Version](https://badgen.net/npm/v/@vates/fuse-vhd)](https://npmjs.org/package/@vates/fuse-vhd) ![License](https://badgen.net/npm/license/@vates/fuse-vhd) [![PackagePhobia](https://badgen.net/bundlephobia/minzip/@vates/fuse-vhd)](https://bundlephobia.com/result?p=@vates/fuse-vhd) [![Node compatibility](https://badgen.net/npm/node/@vates/fuse-vhd)](https://npmjs.org/package/@vates/fuse-vhd)
## Install
Installation of the [npm package](https://npmjs.org/package/@vates/fuse-vhd):
```sh
npm install --save @vates/fuse-vhd
```
## Usage
Mount a vhd generated by xen-orchestra to filesystem
### Library
```js
import { mount } from 'fuse-vhd'
// return a disposable, see promise-toolbox/Disposable
// unmount automatically when disposable is disposed
// in case of differencing VHD, it mounts the full chain
await mount(handler, diskId, mountPoint)
```
### cli
From the install folder :
```
cli.mjs <remoteUrl> <vhdPathInRemote> <mountPoint>
```
After installing the package
```
xo-fuse-vhd <remoteUrl> <vhdPathInRemote> <mountPoint>
```
remoteUrl can be found by using cli in `@xen-orchestra/fs` , for example a local remote will have a url like `file:///path/to/remote/root`
## Contributions
Contributions are _very_ welcomed, either on the documentation or on
the code.
You may:
- report any [issue](https://github.com/vatesfr/xen-orchestra/issues)
you've encountered;
- fork and create a pull request.
## License
[ISC](https://spdx.org/licenses/ISC) © [Vates SAS](https://vates.fr)

View File

@@ -1,26 +0,0 @@
#!/usr/bin/env node
import Disposable from 'promise-toolbox/Disposable'
import { getSyncedHandler } from '@xen-orchestra/fs'
import { mount } from './index.mjs'
async function* main([remoteUrl, vhdPathInRemote, mountPoint]) {
if (mountPoint === undefined) {
throw new TypeError('missing arg: cli <remoteUrl> <vhdPathInRemote> <mountPoint>')
}
const handler = yield getSyncedHandler({ url: remoteUrl })
const mounted = await mount(handler, vhdPathInRemote, mountPoint)
let disposePromise
process.on('SIGINT', async () => {
// ensure single dispose
if (!disposePromise) {
disposePromise = mounted.dispose()
}
await disposePromise
process.exit()
})
}
Disposable.wrap(main)(process.argv.slice(2))

View File

@@ -1,6 +1,6 @@
{
"name": "@vates/fuse-vhd",
"version": "2.1.0",
"version": "2.0.0",
"license": "ISC",
"private": false,
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@vates/fuse-vhd",
@@ -19,15 +19,11 @@
},
"main": "./index.mjs",
"dependencies": {
"@xen-orchestra/fs": "^4.1.4",
"fuse-native": "^2.2.6",
"lru-cache": "^7.14.0",
"promise-toolbox": "^0.21.0",
"vhd-lib": "^4.9.0"
},
"bin": {
"xo-fuse-vhd": "./cli.mjs"
},
"scripts": {
"postversion": "npm publish --access public"
}

View File

@@ -24,14 +24,14 @@
"@xen-orchestra/async-map": "^0.1.2",
"@xen-orchestra/log": "^0.6.0",
"promise-toolbox": "^0.21.0",
"xen-api": "^2.0.1"
"xen-api": "^2.0.0"
},
"devDependencies": {
"tap": "^18.7.0",
"tap": "^16.3.0",
"tmp": "^0.2.1"
},
"scripts": {
"postversion": "npm publish --access public",
"test-integration": "tap --allow-incomplete-coverage"
"test-integration": "tap --lines 97 --functions 95 --branches 74 --statements 97 tests/*.integ.mjs"
}
}

View File

@@ -1,5 +1,5 @@
import { strict as assert } from 'node:assert'
import test from 'test'
import { describe, it } from 'tap/mocha'
import {
generateHotp,
@@ -11,8 +11,6 @@ import {
verifyTotp,
} from './index.mjs'
const { describe, it } = test
describe('generateSecret', function () {
it('generates a string of 32 chars', async function () {
const secret = generateSecret()

View File

@@ -31,9 +31,9 @@
},
"scripts": {
"postversion": "npm publish --access public",
"test": "node--test"
"test": "tap"
},
"devDependencies": {
"test": "^3.3.0"
"tap": "^16.3.0"
}
}

View File

@@ -1,7 +1,7 @@
'use strict'
const assert = require('assert/strict')
const { describe, it } = require('test')
const { describe, it } = require('tap').mocha
const { every, not, some } = require('./')

View File

@@ -32,9 +32,9 @@
},
"scripts": {
"postversion": "npm publish --access public",
"test": "node--test"
"test": "tap"
},
"devDependencies": {
"test": "^3.3.0"
"tap": "^16.0.1"
}
}

View File

@@ -1,7 +1,7 @@
'use strict'
const assert = require('assert/strict')
const { afterEach, describe, it } = require('test')
const { afterEach, describe, it } = require('tap').mocha
const { AlteredRecordError, AuditCore, MissingRecordError, NULL_ID, Storage } = require('.')

View File

@@ -13,10 +13,10 @@
},
"scripts": {
"postversion": "npm publish --access public",
"test": "node--test"
"test": "tap --lines 67 --functions 92 --branches 52 --statements 67"
},
"dependencies": {
"@vates/decorate-with": "^2.1.0",
"@vates/decorate-with": "^2.0.0",
"@xen-orchestra/log": "^0.6.0",
"golike-defer": "^0.5.1",
"object-hash": "^2.0.1"
@@ -28,6 +28,6 @@
"url": "https://vates.fr"
},
"devDependencies": {
"test": "^3.3.0"
"tap": "^16.0.1"
}
}

View File

@@ -7,8 +7,8 @@
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
"dependencies": {
"@xen-orchestra/async-map": "^0.1.2",
"@xen-orchestra/backups": "^0.44.6",
"@xen-orchestra/fs": "^4.1.4",
"@xen-orchestra/backups": "^0.44.3",
"@xen-orchestra/fs": "^4.1.3",
"filenamify": "^6.0.0",
"getopts": "^2.2.5",
"lodash": "^4.17.15",

View File

@@ -160,10 +160,10 @@ export class ImportVmBackup {
// update the stream with the negative vhd stream
stream = await negativeVhd.stream()
vdis[vdiRef].baseVdi = snapshotCandidate
} catch (error) {
} catch (err) {
// can be a broken VHD chain, a vhd chain with a key backup, ....
// not an irrecuperable error, don't dispose parentVhd, and fallback to full restore
warn(`can't use differential restore`, { error })
warn(`can't use differential restore`, err)
disposableDescendants?.dispose()
}
}

View File

@@ -35,8 +35,6 @@ export const DIR_XO_CONFIG_BACKUPS = 'xo-config-backups'
export const DIR_XO_POOL_METADATA_BACKUPS = 'xo-pool-metadata-backups'
const IMMUTABILTY_METADATA_FILENAME = '/immutability.json'
const { debug, warn } = createLogger('xo:backups:RemoteAdapter')
const compareTimestamp = (a, b) => a.timestamp - b.timestamp
@@ -751,37 +749,10 @@ export class RemoteAdapter {
}
async readVmBackupMetadata(path) {
let json
let isImmutable = false
let remoteIsImmutable = false
// if the remote is immutable, check if this metadatas are also immutables
try {
// this file is not encrypted
await this._handler._readFile(IMMUTABILTY_METADATA_FILENAME)
remoteIsImmutable = true
} catch (error) {
if (error.code !== 'ENOENT') {
throw error
}
}
try {
// this will trigger an EPERM error if the file is immutable
json = await this.handler.readFile(path, { flag: 'r+' })
// s3 handler don't respect flags
} catch (err) {
// retry without triggerring immutbaility check ,only on immutable remote
if (err.code === 'EPERM' && remoteIsImmutable) {
isImmutable = true
json = await this._handler.readFile(path, { flag: 'r' })
} else {
throw err
}
}
// _filename is a private field used to compute the backup id
//
// it's enumerable to make it cacheable
const metadata = { ...JSON.parse(json), _filename: path, isImmutable }
const metadata = { ...JSON.parse(await this._handler.readFile(path)), _filename: path }
// backups created on XenServer < 7.1 via JSON in XML-RPC transports have boolean values encoded as integers, which make them unusable with more recent XAPIs
if (typeof metadata.vm.is_a_template === 'number') {

View File

@@ -143,10 +143,8 @@ export class IncrementalRemoteWriter extends MixinRemoteWriter(AbstractIncrement
let metadataContent = await this._isAlreadyTransferred(timestamp)
if (metadataContent !== undefined) {
// skip backup while being vigilant to not stuck the forked stream
// @todo : should skip backup while being vigilant to not stuck the forked stream
Task.info('This backup has already been transfered')
Object.values(deltaExport.streams).forEach(stream => stream.destroy())
return { size: 0 }
}
const basename = formatFilenameDate(timestamp)

View File

@@ -113,13 +113,13 @@ export const MixinRemoteWriter = (BaseClass = Object) =>
)
}
async _isAlreadyTransferred(timestamp) {
_isAlreadyTransferred(timestamp) {
const vmUuid = this._vmUuid
const adapter = this._adapter
const backupDir = getVmBackupDir(vmUuid)
try {
const actualMetadata = JSON.parse(
await adapter._handler.readFile(`${backupDir}/${formatFilenameDate(timestamp)}.json`)
adapter._handler.readFile(`${backupDir}/${formatFilenameDate(timestamp)}.json`)
)
return actualMetadata
} catch (error) {}

View File

@@ -6,8 +6,7 @@ function formatVmBackup(backup) {
let differencingVhds
let dynamicVhds
// some backups don't use snapshots, therefore cannot be with memory
const withMemory = vmSnapshot !== undefined && vmSnapshot.suspend_VDI !== 'OpaqueRef:NULL'
const withMemory = vmSnapshot.suspend_VDI !== 'OpaqueRef:NULL'
// isVhdDifferencing is either undefined or an object
if (isVhdDifferencing !== undefined) {
differencingVhds = Object.values(isVhdDifferencing).filter(t => t).length
@@ -31,7 +30,6 @@ function formatVmBackup(backup) {
}),
id: backup.id,
isImmutable: backup.isImmutable,
jobId: backup.jobId,
mode: backup.mode,
scheduleId: backup.scheduleId,

View File

@@ -8,7 +8,7 @@
"type": "git",
"url": "https://github.com/vatesfr/xen-orchestra.git"
},
"version": "0.44.6",
"version": "0.44.3",
"engines": {
"node": ">=14.18"
},
@@ -22,13 +22,13 @@
"@vates/async-each": "^1.0.0",
"@vates/cached-dns.lookup": "^1.0.0",
"@vates/compose": "^2.1.0",
"@vates/decorate-with": "^2.1.0",
"@vates/decorate-with": "^2.0.0",
"@vates/disposable": "^0.1.5",
"@vates/fuse-vhd": "^2.1.0",
"@vates/fuse-vhd": "^2.0.0",
"@vates/nbd-client": "^3.0.0",
"@vates/parse-duration": "^0.1.1",
"@xen-orchestra/async-map": "^0.1.2",
"@xen-orchestra/fs": "^4.1.4",
"@xen-orchestra/fs": "^4.1.3",
"@xen-orchestra/log": "^0.6.0",
"@xen-orchestra/template": "^0.1.0",
"app-conf": "^2.3.0",
@@ -45,7 +45,7 @@
"tar": "^6.1.15",
"uuid": "^9.0.0",
"vhd-lib": "^4.9.0",
"xen-api": "^2.0.1",
"xen-api": "^2.0.0",
"yazl": "^2.5.1"
},
"devDependencies": {
@@ -56,7 +56,7 @@
"tmp": "^0.2.1"
},
"peerDependencies": {
"@xen-orchestra/xapi": "^4.2.0"
"@xen-orchestra/xapi": "^4.1.0"
},
"license": "AGPL-3.0-or-later",
"author": {

View File

@@ -18,7 +18,7 @@
"preferGlobal": true,
"dependencies": {
"golike-defer": "^0.5.1",
"xen-api": "^2.0.1"
"xen-api": "^2.0.0"
},
"scripts": {
"postversion": "npm publish"

View File

@@ -1,7 +1,7 @@
{
"private": false,
"name": "@xen-orchestra/fs",
"version": "4.1.4",
"version": "4.1.3",
"license": "AGPL-3.0-or-later",
"description": "The File System for Xen Orchestra backups.",
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/fs",
@@ -28,7 +28,7 @@
"@sindresorhus/df": "^3.1.1",
"@vates/async-each": "^1.0.0",
"@vates/coalesce-calls": "^0.1.0",
"@vates/decorate-with": "^2.1.0",
"@vates/decorate-with": "^2.0.0",
"@vates/read-chunk": "^1.2.0",
"@xen-orchestra/log": "^0.6.0",
"bind-property-descriptor": "^2.0.0",

View File

@@ -364,7 +364,7 @@ export default class RemoteHandlerAbstract {
let data
try {
// this file is not encrypted
data = await this._readFile(normalizePath(ENCRYPTION_DESC_FILENAME))
data = await this._readFile(normalizePath(ENCRYPTION_DESC_FILENAME), 'utf-8')
const json = JSON.parse(data)
encryptionAlgorithm = json.algorithm
} catch (error) {
@@ -377,7 +377,7 @@ export default class RemoteHandlerAbstract {
try {
this.#rawEncryptor = _getEncryptor(encryptionAlgorithm, this._remote.encryptionKey)
// this file is encrypted
const data = await this.__readFile(ENCRYPTION_METADATA_FILENAME)
const data = await this.__readFile(ENCRYPTION_METADATA_FILENAME, 'utf-8')
JSON.parse(data)
} catch (error) {
// can be enoent, bad algorithm, or broeken json ( bad key or algorithm)

View File

@@ -171,12 +171,7 @@ export default class LocalHandler extends RemoteHandlerAbstract {
}
}
async _readFile(file, { flags, ...options } = {}) {
// contrary to createReadStream, readFile expect singular `flag`
if (flags !== undefined) {
options.flag = flags
}
async _readFile(file, options) {
const filePath = this.getFilePath(file)
return await this.#addSyncStackTrace(retry, () => fs.readFile(filePath, options), this.#retriesOnEagain)
}

View File

@@ -20,7 +20,5 @@ export function split(path) {
return parts
}
// paths are made absolute otherwise fs.relative() would resolve them against working directory
export const relativeFromFile = (file, path) => relative(dirname(normalize(file)), normalize(path))
export const relativeFromFile = (file, path) => relative(dirname(file), path)
export const resolveFromFile = (file, path) => resolve('/', dirname(file), path).slice(1)

View File

@@ -1,17 +0,0 @@
import { describe, it } from 'test'
import { strict as assert } from 'assert'
import { relativeFromFile } from './path.js'
describe('relativeFromFile()', function () {
for (const [title, args] of Object.entries({
'file absolute and path absolute': ['/foo/bar/file.vhd', '/foo/baz/path.vhd'],
'file relative and path absolute': ['foo/bar/file.vhd', '/foo/baz/path.vhd'],
'file absolute and path relative': ['/foo/bar/file.vhd', 'foo/baz/path.vhd'],
'file relative and path relative': ['foo/bar/file.vhd', 'foo/baz/path.vhd'],
})) {
it('works with ' + title, function () {
assert.equal(relativeFromFile(...args), '../baz/path.vhd')
})
}
})

View File

@@ -1,10 +0,0 @@
### make a remote immutable
launch the `xo-immutable-remote` command. The configuration is stored in the config file.
This script must be kept running to make file immutable reliably.
### make file mutable
launch the `xo-lift-remote-immutability` cli. The configuration is stored in the config file .
If the config file have a `liftEvery`, this script will contiue to run and check regularly if there are files to update.

View File

@@ -1 +0,0 @@
../../scripts/npmignore

View File

@@ -1,41 +0,0 @@
<!-- DO NOT EDIT MANUALLY, THIS FILE HAS BEEN GENERATED -->
# @xen-orchestra/immutable-backups
[![Package Version](https://badgen.net/npm/v/@xen-orchestra/immutable-backups)](https://npmjs.org/package/@xen-orchestra/immutable-backups) ![License](https://badgen.net/npm/license/@xen-orchestra/immutable-backups) [![PackagePhobia](https://badgen.net/bundlephobia/minzip/@xen-orchestra/immutable-backups)](https://bundlephobia.com/result?p=@xen-orchestra/immutable-backups) [![Node compatibility](https://badgen.net/npm/node/@xen-orchestra/immutable-backups)](https://npmjs.org/package/@xen-orchestra/immutable-backups)
## Install
Installation of the [npm package](https://npmjs.org/package/@xen-orchestra/immutable-backups):
```sh
npm install --save @xen-orchestra/immutable-backups
```
## Usage
### make a remote immutable
launch the `xo-immutable-remote` command. The configuration is stored in the config file.
This script must be kept running to make file immutable reliably.
### make file mutable
launch the `xo-lift-remote-immutability` cli. The configuration is stored in the config file .
If the config file have a `liftEvery`, this script will contiue to run and check regularly if there are files to update.
## Contributions
Contributions are _very_ welcomed, either on the documentation or on
the code.
You may:
- report any [issue](https://github.com/vatesfr/xen-orchestra/issues)
you've encountered;
- fork and create a pull request.
## License
[AGPL-3.0-or-later](https://spdx.org/licenses/AGPL-3.0-or-later) © [Vates SAS](https://vates.fr)

View File

@@ -1,10 +0,0 @@
import fs from 'node:fs/promises'
import { dirname, join } from 'node:path'
import isBackupMetadata from './isBackupMetadata.mjs'
export default async path => {
if (isBackupMetadata(path)) {
// snipe vm metadata cache to force XO to update it
await fs.unlink(join(dirname(path), 'cache.json.gz'))
}
}

View File

@@ -1,4 +0,0 @@
import { dirname } from 'node:path'
// check if we are handling file directly under a vhd directory ( bat, headr, footer,..)
export default path => dirname(path).endsWith('.vhd')

View File

@@ -1,46 +0,0 @@
import { load } from 'app-conf'
import { homedir } from 'os'
import { join } from 'node:path'
import ms from 'ms'
const APP_NAME = 'xo-immutable-backups'
const APP_DIR = new URL('.', import.meta.url).pathname
export default async function loadConfig() {
const config = await load(APP_NAME, {
appDir: APP_DIR,
ignoreUnknownFormats: true,
})
if (config.remotes === undefined || config.remotes?.length < 1) {
throw new Error(
'No remotes are configured in the config file, please add at least one [remotes.<remoteid>] with a root property pointing to the absolute path of the remote to watch'
)
}
if (config.liftEvery) {
config.liftEvery = ms(config.liftEvery)
}
for (const [remoteId, { indexPath, immutabilityDuration, root }] of Object.entries(config.remotes)) {
if (!root) {
throw new Error(
`Remote ${remoteId} don't have a root property,containing the absolute path to the root of a backup repository `
)
}
if (!immutabilityDuration) {
throw new Error(
`Remote ${remoteId} don't have a immutabilityDuration property to indicate the minimal duration the backups should be protected by immutability `
)
}
if (ms(immutabilityDuration) < ms('1d')) {
throw new Error(
`Remote ${remoteId} immutability duration is smaller than the minimum allowed (1d), current : ${immutabilityDuration}`
)
}
if (!indexPath) {
const basePath = indexPath ?? process.env.XDG_DATA_HOME ?? join(homedir(), '.local', 'share')
const immutabilityIndexPath = join(basePath, APP_NAME, remoteId)
config.remotes[remoteId].indexPath = immutabilityIndexPath
}
config.remotes[remoteId].immutabilityDuration = ms(immutabilityDuration)
}
return config
}

View File

@@ -1,14 +0,0 @@
# how often does the lift immutability script will run to check if
# some files need to be made mutable
liftEvery = 1h
# you can add as many remote as you want, if you change the id ( here : remote1)
#[remotes.remote1]
#root = "/mnt/ssd/vhdblock/" # the absolute path of the root of the backup repository
#immutabilityDuration = 7d # mandatory
# optional, default value is false will scan and update the index on start, can be expensive
#rebuildIndexOnStart = true
# the index path is optional, default in XDG_DATA_HOME, or if this is not set, in ~/.local/share
#indexPath = "/var/lib/" # will add automatically the application name immutable-backup

View File

@@ -1,33 +0,0 @@
import { describe, it } from 'node:test'
import assert from 'node:assert/strict'
import fs from 'node:fs/promises'
import path from 'node:path'
import { tmpdir } from 'node:os'
import * as Directory from './directory.mjs'
import { rimraf } from 'rimraf'
describe('immutable-backups/file', async () => {
it('really lock a directory', async () => {
const dir = await fs.mkdtemp(path.join(tmpdir(), 'immutable-backups-tests'))
const dataDir = path.join(dir, 'data')
await fs.mkdir(dataDir)
const immutDir = path.join(dir, '.immutable')
const filePath = path.join(dataDir, 'test')
await fs.writeFile(filePath, 'data')
await Directory.makeImmutable(dataDir, immutDir)
assert.strictEqual(await Directory.isImmutable(dataDir), true)
await assert.rejects(() => fs.writeFile(filePath, 'data'))
await assert.rejects(() => fs.appendFile(filePath, 'data'))
await assert.rejects(() => fs.unlink(filePath))
await assert.rejects(() => fs.rename(filePath, filePath + 'copy'))
await assert.rejects(() => fs.writeFile(path.join(dataDir, 'test2'), 'data'))
await assert.rejects(() => fs.rename(dataDir, dataDir + 'copy'))
await Directory.liftImmutability(dataDir, immutDir)
assert.strictEqual(await Directory.isImmutable(dataDir), false)
await fs.writeFile(filePath, 'data')
await fs.appendFile(filePath, 'data')
await fs.unlink(filePath)
await fs.rename(dataDir, dataDir + 'copy')
await rimraf(dir)
})
})

View File

@@ -1,21 +0,0 @@
import execa from 'execa'
import { unindexFile, indexFile } from './fileIndex.mjs'
export async function makeImmutable(dirPath, immutabilityCachePath) {
if (immutabilityCachePath) {
await indexFile(dirPath, immutabilityCachePath)
}
await execa('chattr', ['+i', '-R', dirPath])
}
export async function liftImmutability(dirPath, immutabilityCachePath) {
if (immutabilityCachePath) {
await unindexFile(dirPath, immutabilityCachePath)
}
await execa('chattr', ['-i', '-R', dirPath])
}
export async function isImmutable(path) {
const { stdout } = await execa('lsattr', ['-d', path])
return stdout[4] === 'i'
}

View File

@@ -1,114 +0,0 @@
# Imutability
the goal is to make a remote that XO can write, but not modify during the immutability duration set on the remote. That way, it's not possible for XO to delete or encrypt any backup during this period. It protects your backup agains ransomware, at least as long as the attacker does not have a root access to the remote server.
We target `governance` type of immutability, **the local root account of the remote server will be able to lift immutability**.
We use the file system capabilities, they are tested on the protection process start.
It is compatible with encryption at rest made by XO.
## Prerequisites
The commands must be run as root on the remote, or by a user with the `CAP_LINUX_IMMUTABLE` capability . On start, the protect process writes into the remote `imutability.json` file its status and the immutability duration.
the `chattr` and `lsattr` should be installed on the system
## Configuring
this package uses app-conf to store its config. The application name is `xo-immutable-backup`. A sample config file is provided in this package.
## Making a file immutable
when marking a file or a folder immutable, it create an alias file in the `<indexPath>/<DayOfFileCreation>/<sha256(fullpath)>`.
`indexPath` can be defined in the config file, otherwise `XDG_HOME` is used. If not available it goes to `~/.local/share`
This index is used when lifting the immutability of the remote, it will only look at the old enough `<indexPath>/<DayOfFileCreation>/` folders.
## Real time protecting
On start, the watcher will create the index if it does not exists.
It will also do a checkup to ensure immutability could work on this remote and handle the easiest issues.
The watching process depends on the backup type, since we don't want to make temporary files and cache immutable.
It won't protect files during upload, only when the files have been completly written on disk. Real time, in this case, means "protecting critical files as soon as possible after they are uploaded"
This can be alleviated by :
- Coupling immutability with encryption to ensure the file is not modified
- Making health check to ensure the data are exactly as the snapshot data
List of protected files :
```js
const PATHS = [
// xo configuration backupq
'xo-config-backups/*/*/data',
'xo-config-backups/*/*/data.json',
'xo-config-backups/*/*/metadata.json',
// pool backupq
'xo-pool-metadata-backups/*/metadata.json',
'xo-pool-metadata-backups/*/data',
// vm backups , xo-vm-backups/<vmuuid>/
'xo-vm-backups/*/*.json',
'xo-vm-backups/*/*.xva',
'xo-vm-backups/*/*.xva.checksum',
// xo-vm-backups/<vmuuid>/vdis/<jobid>/<vdiUuid>
'xo-vm-backups/*/vdis/*/*/*.vhd', // can be an alias or a vhd file
// for vhd directory :
'xo-vm-backups/*/vdis/*/*/data/*.vhd/bat',
'xo-vm-backups/*/vdis/*/*/data/*.vhd/header',
'xo-vm-backups/*/vdis/*/*/data/*.vhd/footer',
]
```
## Releasing protection on old enough files on a remote
the watcher will periodically check if some file must by unlocked
## Troubleshooting
### some files are still locked
add the `rebuildIndexOnStart` option to the config file
### make remote fully mutable again
- Update the immutability setting with a 0 duration
- launch the `liftProtection` cli.
- remove the `protectRemotes` service
### increasing the immutability duration
this will prolong immutable file, but won't protect files that are already out of immutability
### reducing the immutability duration
change the setting, and launch the `liftProtection` cli , or wait for next planed execution
### why are my incremental backups not marked as protected in XO ?
are not marked as protected in XO ?
For incremental backups to be marked as protected in XO, the entire chain must be under protection. To ensure at least 7 days of backups are protected, you need to set the immutability duration and retention at 14 days, the full backup interval at 7 days
That means that if the last backup chain is complete ( 7 backup ) it is completely under protection, and if not, the precedent chain is also under protection. K are key backups, and are delta
```
Kd Kdddddd Kdddddd K # 8 backups protected, 2 chains
K Kdddddd Kdddddd Kd # 9 backups protected, 2 chains
Kdddddd Kdddddd Kdd # 10 backups protected, 2 chains
Kddddd Kdddddd Kddd # 11 backups protected, 2 chains
Kdddd Kdddddd Kdddd # 12 backups protected, 2 chains
Kddd Kdddddd Kddddd # 13 backups protected, 2 chains
Kdd Kdddddd Kdddddd # 7 backups protected, 1 chain since precedent full is now mutable
Kd Kdddddd Kdddddd K # 8 backups protected, 2 chains
```
### Why doesn't the protect process start ?
- it should be run as root or by a user with the `CAP_LINUX_IMMUTABLE` capability
- the underlying file system should support immutability, especially the `chattr` and `lsattr` command
- logs are in journalctl

View File

@@ -1,29 +0,0 @@
import { describe, it } from 'node:test'
import assert from 'node:assert/strict'
import fs from 'node:fs/promises'
import path from 'node:path'
import * as File from './file.mjs'
import { tmpdir } from 'node:os'
import { rimraf } from 'rimraf'
describe('immutable-backups/file', async () => {
it('really lock a file', async () => {
const dir = await fs.mkdtemp(path.join(tmpdir(), 'immutable-backups-tests'))
const immutDir = path.join(dir, '.immutable')
const filePath = path.join(dir, 'test.ext')
await fs.writeFile(filePath, 'data')
assert.strictEqual(await File.isImmutable(filePath), false)
await File.makeImmutable(filePath, immutDir)
assert.strictEqual(await File.isImmutable(filePath), true)
await assert.rejects(() => fs.writeFile(filePath, 'data'))
await assert.rejects(() => fs.appendFile(filePath, 'data'))
await assert.rejects(() => fs.unlink(filePath))
await assert.rejects(() => fs.rename(filePath, filePath + 'copy'))
await File.liftImmutability(filePath, immutDir)
assert.strictEqual(await File.isImmutable(filePath), false)
await fs.writeFile(filePath, 'data')
await fs.appendFile(filePath, 'data')
await fs.unlink(filePath)
await rimraf(dir)
})
})

View File

@@ -1,24 +0,0 @@
import execa from 'execa'
import { unindexFile, indexFile } from './fileIndex.mjs'
// this work only on linux like systems
// this could work on windows : https://4sysops.com/archives/set-and-remove-the-read-only-file-attribute-with-powershell/
export async function makeImmutable(path, immutabilityCachePath) {
if (immutabilityCachePath) {
await indexFile(path, immutabilityCachePath)
}
await execa('chattr', ['+i', path])
}
export async function liftImmutability(filePath, immutabilityCachePath) {
if (immutabilityCachePath) {
await unindexFile(filePath, immutabilityCachePath)
}
await execa('chattr', ['-i', filePath])
}
export async function isImmutable(path) {
const { stdout } = await execa('lsattr', ['-d', path])
return stdout[4] === 'i'
}

View File

@@ -1,81 +0,0 @@
import { describe, it } from 'node:test'
import assert from 'node:assert/strict'
import fs from 'node:fs/promises'
import path from 'node:path'
import * as FileIndex from './fileIndex.mjs'
import * as Directory from './directory.mjs'
import { tmpdir } from 'node:os'
import { rimraf } from 'rimraf'
describe('immutable-backups/fileIndex', async () => {
it('index File changes', async () => {
const dir = await fs.mkdtemp(path.join(tmpdir(), 'immutable-backups-tests'))
const immutDir = path.join(dir, '.immutable')
const filePath = path.join(dir, 'test.ext')
await fs.writeFile(filePath, 'data')
await FileIndex.indexFile(filePath, immutDir)
await fs.mkdir(path.join(immutDir, 'NOTADATE'))
await fs.writeFile(path.join(immutDir, 'NOTADATE.file'), 'content')
let nb = 0
let index, target
for await ({ index, target } of FileIndex.listOlderTargets(immutDir, 0)) {
assert.strictEqual(true, false, 'Nothing should be eligible for deletion')
}
nb = 0
for await ({ index, target } of FileIndex.listOlderTargets(immutDir, -24 * 60 * 60 * 1000)) {
assert.strictEqual(target, filePath)
await fs.unlink(index)
nb++
}
assert.strictEqual(nb, 1)
await fs.rmdir(path.join(immutDir, 'NOTADATE'))
await fs.rm(path.join(immutDir, 'NOTADATE.file'))
for await ({ index, target } of FileIndex.listOlderTargets(immutDir, -24 * 60 * 60 * 1000)) {
// should remove the empty dir
assert.strictEqual(true, false, 'Nothing should have stayed here')
}
assert.strictEqual((await fs.readdir(immutDir)).length, 0)
await rimraf(dir)
})
it('fails correctly', async () => {
const dir = await fs.mkdtemp(path.join(tmpdir(), 'immutable-backups-tests'))
const immutDir = path.join(dir, '.immutable')
await fs.mkdir(immutDir)
const placeholderFile = path.join(dir, 'test.ext')
await fs.writeFile(placeholderFile, 'data')
await FileIndex.indexFile(placeholderFile, immutDir)
const filePath = path.join(dir, 'test2.ext')
await fs.writeFile(filePath, 'data')
await FileIndex.indexFile(filePath, immutDir)
await assert.rejects(() => FileIndex.indexFile(filePath, immutDir), { code: 'EEXIST' })
await Directory.makeImmutable(immutDir)
await assert.rejects(() => FileIndex.unindexFile(filePath, immutDir), { code: 'EPERM' })
await Directory.liftImmutability(immutDir)
await rimraf(dir)
})
it('handles bomb index files', async () => {
const dir = await fs.mkdtemp(path.join(tmpdir(), 'immutable-backups-tests'))
const immutDir = path.join(dir, '.immutable')
await fs.mkdir(immutDir)
const placeholderFile = path.join(dir, 'test.ext')
await fs.writeFile(placeholderFile, 'data')
await FileIndex.indexFile(placeholderFile, immutDir)
const indexDayDir = path.join(immutDir, '1980,11-28')
await fs.mkdir(indexDayDir)
await fs.writeFile(path.join(indexDayDir, 'big'), Buffer.alloc(2 * 1024 * 1024))
assert.rejects(async () => {
let index, target
for await ({ index, target } of FileIndex.listOlderTargets(immutDir, 0)) {
// should remove the empty dir
assert.strictEqual(true, false, `Nothing should have stayed here, got ${index} ${target}`)
}
})
await rimraf(dir)
})
})

View File

@@ -1,88 +0,0 @@
import { join } from 'node:path'
import { createHash } from 'node:crypto'
import fs from 'node:fs/promises'
import { dirname } from 'path'
const MAX_INDEX_FILE_SIZE = 1024 * 1024
function sha256(content) {
return createHash('sha256').update(content).digest('hex')
}
function formatDate(date) {
return date.toISOString().split('T')[0]
}
async function computeIndexFilePath(path, immutabilityIndexPath) {
const stat = await fs.stat(path)
const date = new Date(stat.birthtimeMs)
const day = formatDate(date)
const hash = sha256(path)
return join(immutabilityIndexPath, day, hash)
}
export async function indexFile(path, immutabilityIndexPath) {
const indexFilePath = await computeIndexFilePath(path, immutabilityIndexPath)
try {
await fs.writeFile(indexFilePath, path, { flag: 'wx' })
} catch (err) {
// missing dir: make it
if (err.code === 'ENOENT') {
await fs.mkdir(dirname(indexFilePath), { recursive: true })
await fs.writeFile(indexFilePath, path)
} else {
throw err
}
}
return indexFilePath
}
export async function unindexFile(path, immutabilityIndexPath) {
try {
const cacheFileName = await computeIndexFilePath(path, immutabilityIndexPath)
await fs.unlink(cacheFileName)
} catch (err) {
if (err.code !== 'ENOENT') {
throw err
}
}
}
export async function* listOlderTargets(immutabilityCachePath, immutabilityDuration) {
// walk all dir by day until the limit day
const limitDate = new Date(Date.now() - immutabilityDuration)
const limitDay = formatDate(limitDate)
const dir = await fs.opendir(immutabilityCachePath)
for await (const dirent of dir) {
if (dirent.isFile()) {
continue
}
// ensure we have a valid date
if (isNaN(new Date(dirent.name))) {
continue
}
// recent enough to be kept
if (dirent.name >= limitDay) {
continue
}
const subDirPath = join(immutabilityCachePath, dirent.name)
const subdir = await fs.opendir(subDirPath)
let nb = 0
for await (const hashFileEntry of subdir) {
const entryFullPath = join(subDirPath, hashFileEntry.name)
const { size } = await fs.stat(entryFullPath)
if (size > MAX_INDEX_FILE_SIZE) {
throw new Error(`Index file at ${entryFullPath} is too big, ${size} bytes `)
}
const targetPath = await fs.readFile(entryFullPath, { encoding: 'utf8' })
yield {
index: entryFullPath,
target: targetPath,
}
nb++
}
// cleanup older folder
if (nb === 0) {
await fs.rmdir(subDirPath)
}
}
}

View File

@@ -1 +0,0 @@
export default path => path.match(/xo-vm-backups\/[^/]+\/[^/]+\.json$/)

View File

@@ -1,37 +0,0 @@
#!/usr/bin/env node
import fs from 'node:fs/promises'
import * as Directory from './directory.mjs'
import { createLogger } from '@xen-orchestra/log'
import { listOlderTargets } from './fileIndex.mjs'
import cleanXoCache from './_cleanXoCache.mjs'
import loadConfig from './_loadConfig.mjs'
const { info, warn } = createLogger('xen-orchestra:immutable-backups:liftProtection')
async function liftRemoteImmutability(immutabilityCachePath, immutabilityDuration) {
for await (const { index, target } of listOlderTargets(immutabilityCachePath, immutabilityDuration)) {
await Directory.liftImmutability(target, immutabilityCachePath)
await fs.unlink(index)
await cleanXoCache(target)
}
}
async function liftImmutability(remotes) {
for (const [remoteId, { indexPath, immutabilityDuration }] of Object.entries(remotes)) {
liftRemoteImmutability(indexPath, immutabilityDuration).catch(err =>
warn('error during watchRemote', { err, remoteId, indexPath, immutabilityDuration })
)
}
}
const { liftEvery, remotes } = await loadConfig()
if (liftEvery > 0) {
info('setup watcher for immutability lifting')
setInterval(async () => {
liftImmutability(remotes)
}, liftEvery)
} else {
liftImmutability(remotes)
}

View File

@@ -1,42 +0,0 @@
{
"private": false,
"name": "@xen-orchestra/immutable-backups",
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/immutable-backups",
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
"repository": {
"directory": "@xen-orchestra/immutable-backups",
"type": "git",
"url": "https://github.com/vatesfr/xen-orchestra.git"
},
"author": {
"name": "Vates SAS",
"url": "https://vates.fr"
},
"bin": {
"xo-immutable-remote": "./protectRemotes.mjs",
"xo-lift-remote-immutability": "./liftProtection.mjs"
},
"license": "AGPL-3.0-or-later",
"version": "1.0.1",
"engines": {
"node": ">=14.0.0"
},
"dependencies": {
"@vates/async-each": "^1.0.0",
"@xen-orchestra/backups": "^0.44.6",
"@xen-orchestra/log": "^0.6.0",
"app-conf": "^2.3.0",
"chokidar": "^3.5.3",
"execa": "^5.0.0",
"ms": "^2.1.3",
"vhd-lib": "^4.7.0"
},
"devDependencies": {
"rimraf": "^5.0.5",
"tap": "^18.6.1"
},
"scripts": {
"postversion": "npm publish --access public",
"test-integration": "tap *.integ.mjs"
}
}

View File

@@ -1,191 +0,0 @@
#!/usr/bin/env node
import fs from 'node:fs/promises'
import * as File from './file.mjs'
import * as Directory from './directory.mjs'
import assert from 'node:assert'
import { dirname, join, sep } from 'node:path'
import { createLogger } from '@xen-orchestra/log'
import chokidar from 'chokidar'
import { indexFile } from './fileIndex.mjs'
import cleanXoCache from './_cleanXoCache.mjs'
import loadConfig from './_loadConfig.mjs'
import isInVhdDirectory from './_isInVhdDirectory.mjs'
const { debug, info, warn } = createLogger('xen-orchestra:immutable-backups:remote')
async function test(remotePath, indexPath) {
await fs.readdir(remotePath)
const testPath = join(remotePath, '.test-immut')
// cleanup
try {
await File.liftImmutability(testPath, indexPath)
await fs.unlink(testPath)
} catch (err) {}
// can create , modify and delete a file
await fs.writeFile(testPath, `test immut ${new Date()}`)
await fs.writeFile(testPath, `test immut change 1 ${new Date()}`)
await fs.unlink(testPath)
// cannot modify or delete an immutable file
await fs.writeFile(testPath, `test immut ${new Date()}`)
await File.makeImmutable(testPath, indexPath)
await assert.rejects(fs.writeFile(testPath, `test immut change 2 ${new Date()}`), { code: 'EPERM' })
await assert.rejects(fs.unlink(testPath), { code: 'EPERM' })
// can modify and delete a file after lifting immutability
await File.liftImmutability(testPath, indexPath)
await fs.writeFile(testPath, `test immut change 3 ${new Date()}`)
await fs.unlink(testPath)
}
async function handleExistingFile(root, indexPath, path) {
try {
// a vhd block directory is completly immutable
if (isInVhdDirectory(path)) {
// this will trigger 3 times per vhd blocks
const dir = join(root, dirname(path))
if (Directory.isImmutable(dir)) {
await indexFile(dir, indexPath)
}
} else {
// other files are immutable a file basis
const fullPath = join(root, path)
if (File.isImmutable(fullPath)) {
await indexFile(fullPath, indexPath)
}
}
} catch (error) {
if (error.code !== 'EEXIST') {
// there can be a symbolic link in the tree
warn('handleExistingFile', { error })
}
}
}
async function handleNewFile(root, indexPath, pendingVhds, path) {
// with awaitWriteFinish we have complete files here
// we can make them immutable
if (isInVhdDirectory(path)) {
// watching a vhd block
// wait for header/footer and BAT before making this immutable recursively
const splitted = path.split(sep)
const vmUuid = splitted[1]
const vdiUuid = splitted[4]
const uniqPath = `${vmUuid}/${vdiUuid}`
const { existing } = pendingVhds.get(uniqPath) ?? {}
if (existing === undefined) {
pendingVhds.set(uniqPath, { existing: 1, lastModified: Date.now() })
} else {
// already two of the key files,and we got the last one
if (existing === 2) {
await Directory.makeImmutable(join(root, dirname(path)), indexPath)
pendingVhds.delete(uniqPath)
} else {
// wait for the other
pendingVhds.set(uniqPath, { existing: existing + 1, lastModified: Date.now() })
}
}
} else {
const fullFilePath = join(root, path)
await File.makeImmutable(fullFilePath, indexPath)
await cleanXoCache(fullFilePath)
}
}
export async function watchRemote(remoteId, { root, immutabilityDuration, rebuildIndexOnStart = false, indexPath }) {
// create index directory
await fs.mkdir(indexPath, { recursive: true })
// test if fs and index directories are well configured
await test(root, indexPath)
// add duration and watch status in the metadata.json of the remote
const settingPath = join(root, 'immutability.json')
try {
// this file won't be made mutable by liftimmutability
await File.liftImmutability(settingPath)
} catch (error) {
// file may not exists, and it's not really a problem
info('lifting immutability on current settings', { error })
}
await fs.writeFile(
settingPath,
JSON.stringify({
since: Date.now(),
immutable: true,
duration: immutabilityDuration,
})
)
// no index path in makeImmutable(): the immutability won't be lifted
File.makeImmutable(settingPath)
// we wait for footer/header AND BAT to be written before locking a vhd directory
// this map allow us to track the vhd with partial metadata
const pendingVhds = new Map()
// cleanup pending vhd map periodically
setInterval(
() => {
pendingVhds.forEach(({ lastModified, existing }, path) => {
if (Date.now() - lastModified > 60 * 60 * 1000) {
pendingVhds.delete(path)
warn(`vhd at ${path} is incomplete since ${lastModified}`, { existing, lastModified, path })
}
})
},
60 * 60 * 1000
)
// watch the remote for any new VM metadata json file
const PATHS = [
'xo-config-backups/*/*/data',
'xo-config-backups/*/*/data.json',
'xo-config-backups/*/*/metadata.json',
'xo-pool-metadata-backups/*/metadata.json',
'xo-pool-metadata-backups/*/data',
// xo-vm-backups/<vmuuid>/
'xo-vm-backups/*/*.json',
'xo-vm-backups/*/*.xva',
'xo-vm-backups/*/*.xva.checksum',
// xo-vm-backups/<vmuuid>/vdis/<jobid>/<vdiUuid>
'xo-vm-backups/*/vdis/*/*/*.vhd', // can be an alias or a vhd file
// for vhd directory :
'xo-vm-backups/*/vdis/*/*/data/*.vhd/bat',
'xo-vm-backups/*/vdis/*/*/data/*.vhd/header',
'xo-vm-backups/*/vdis/*/*/data/*.vhd/footer',
]
let ready = false
const watcher = chokidar.watch(PATHS, {
ignored: [
/(^|[/\\])\../, // ignore dotfiles
/\.lock$/,
],
cwd: root,
recursive: false, // vhd directory can generate a lot of folder, don't let chokidar choke on this
ignoreInitial: !rebuildIndexOnStart,
depth: 7,
awaitWriteFinish: true,
})
// Add event listeners.
watcher
.on('add', async path => {
debug(`File ${path} has been added ${path.split('/').length}`)
if (ready) {
await handleNewFile(root, indexPath, pendingVhds, path)
} else {
await handleExistingFile(root, indexPath, path)
}
})
.on('error', error => warn(`Watcher error: ${error}`))
.on('ready', () => {
ready = true
info('Ready for changes')
})
}
const { remotes } = await loadConfig()
for (const [remoteId, remote] of Object.entries(remotes)) {
watchRemote(remoteId, remote).catch(err => warn('error during watchRemote', { err, remoteId, remote }))
}

View File

@@ -1,15 +1,13 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"include": ["env.d.ts", "src/**/*", "src/**/*.vue", "../web-core/lib/**/*", "../web-core/lib/**/*.vue"],
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
"exclude": ["src/**/__tests__/*"],
"compilerOptions": {
"composite": true,
"noEmit": true,
"baseUrl": ".",
"rootDir": "..",
"paths": {
"@/*": ["./src/*"],
"@core/*": ["../web-core/lib/*"]
"@/*": ["./src/*"]
}
}
}

View File

@@ -2,10 +2,10 @@
"files": [],
"references": [
{
"path": "./tsconfig.node.json",
"path": "./tsconfig.node.json"
},
{
"path": "./tsconfig.app.json",
},
],
"path": "./tsconfig.app.json"
}
]
}

View File

@@ -23,7 +23,6 @@ export default defineConfig({
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
'@core': fileURLToPath(new URL('../web-core/lib', import.meta.url)),
},
},

View File

@@ -27,16 +27,6 @@ log.error('could not join server', {
})
```
A logging method has the following signature:
```ts
interface LoggingMethod {
(error): void
(message: string, data?: { error?: Error; [property: string]: any }): void
}
```
### Consumer
Then, at application level, configure the logs are handled:

View File

@@ -45,16 +45,6 @@ log.error('could not join server', {
})
```
A logging method has the following signature:
```ts
interface LoggingMethod {
(error): void
(message: string, data?: { error?: Error; [property: string]: any }): void
}
```
### Consumer
Then, at application level, configure the logs are handled:

View File

@@ -1,7 +1,7 @@
{
"private": true,
"name": "@xen-orchestra/proxy",
"version": "0.26.45",
"version": "0.26.42",
"license": "AGPL-3.0-or-later",
"description": "XO Proxy used to remotely execute backup jobs",
"keywords": [
@@ -29,16 +29,16 @@
"@koa/router": "^12.0.0",
"@vates/cached-dns.lookup": "^1.0.0",
"@vates/compose": "^2.1.0",
"@vates/decorate-with": "^2.1.0",
"@vates/decorate-with": "^2.0.0",
"@vates/disposable": "^0.1.5",
"@xen-orchestra/async-map": "^0.1.2",
"@xen-orchestra/backups": "^0.44.6",
"@xen-orchestra/fs": "^4.1.4",
"@xen-orchestra/backups": "^0.44.3",
"@xen-orchestra/fs": "^4.1.3",
"@xen-orchestra/log": "^0.6.0",
"@xen-orchestra/mixin": "^0.1.0",
"@xen-orchestra/mixins": "^0.14.0",
"@xen-orchestra/self-signed": "^0.2.0",
"@xen-orchestra/xapi": "^4.2.0",
"@xen-orchestra/self-signed": "^0.1.3",
"@xen-orchestra/xapi": "^4.1.0",
"ajv": "^8.0.3",
"app-conf": "^2.3.0",
"async-iterator-to-stream": "^1.1.0",
@@ -60,7 +60,7 @@
"source-map-support": "^0.5.16",
"stoppable": "^1.0.6",
"xdg-basedir": "^5.1.0",
"xen-api": "^2.0.1",
"xen-api": "^2.0.0",
"xo-common": "^0.8.0"
},
"devDependencies": {

View File

@@ -9,7 +9,7 @@
"type": "git",
"url": "https://github.com/vatesfr/xen-orchestra.git"
},
"version": "0.2.0",
"version": "0.1.3",
"engines": {
"node": ">=15.6"
},

View File

@@ -1,4 +1,3 @@
import { readChunkStrict, skipStrict } from '@vates/read-chunk'
import _computeGeometryForSize from 'vhd-lib/_computeGeometryForSize.js'
import { createFooter, createHeader } from 'vhd-lib/_createFooterHeader.js'
import { DISK_TYPES, FOOTER_SIZE } from 'vhd-lib/_constants.js'
@@ -69,10 +68,6 @@ export default class VhdEsxiSeSparse extends VhdAbstract {
#grainTableOffsetBytes
#grainOffsetBytes
#reading = false
#stream
#streamOffset = 0
static async open(esxi, datastore, path, parentVhd, opts) {
const vhd = new VhdEsxiSeSparse(esxi, datastore, path, parentVhd, opts)
await vhd.readHeaderAndFooter()
@@ -107,58 +102,12 @@ export default class VhdEsxiSeSparse extends VhdAbstract {
)
}
// since most of the data are writtent sequentially we always open a stream from start to the end of the file
// If we have to rewind it, we destroy the stream and recreate with the right "start"
// We also recreate the stream if there is too much distance between current position and the wanted position
async #read(start, length) {
if (!this.#footer) {
// we need to be able before the footer is loaded, to read the header and footer
return (await this.#esxi.download(this.#datastore, this.#path, `${start}-${start + length - 1}`)).buffer()
}
if (this.#reading) {
throw new Error('reading must be done sequentially')
}
try {
const MAX_SKIPPABLE_LENGTH = 2 * 1024 * 1024
this.#reading = true
if (this.#stream !== undefined) {
// stream is already ahead or to far behind
if (this.#streamOffset > start || this.#streamOffset + MAX_SKIPPABLE_LENGTH < start) {
this.#stream.destroy()
this.#stream = undefined
this.#streamOffset = 0
}
}
// no stream
if (this.#stream === undefined) {
const end = this.footer.currentSize - 1
const res = await this.#esxi.download(this.#datastore, this.#path, `${start}-${end}`)
this.#stream = res.body
this.#streamOffset = start
}
// stream a little behind
if (this.#streamOffset < start) {
await skipStrict(this.#stream, start - this.#streamOffset)
this.#streamOffset = start
}
// really read data
this.#streamOffset += length
const data = await readChunkStrict(this.#stream, length)
return data
} catch (error) {
error.start = start
error.length = length
error.streamLength = this.footer.currentSize
this.#stream?.destroy()
this.#stream = undefined
this.#streamOffset = 0
throw error
} finally {
this.#reading = false
}
const buffer = await (
await this.#esxi.download(this.#datastore, this.#path, `${start}-${start + length - 1}`)
).buffer()
strictEqual(buffer.length, length)
return buffer
}
async readHeaderAndFooter() {
@@ -250,28 +199,15 @@ export default class VhdEsxiSeSparse extends VhdAbstract {
async readBlock(blockId) {
let changed = false
const parentBlock = await this.#parentVhd.readBlock(blockId)
const parentBuffer = parentBlock.buffer
const grainOffsets = this.#grainIndex.get(blockId) // may be undefined if the child contains block and lookMissingBlockInParent=true
// negative value indicate that it's not an offset
// SE_SPARSE_GRAIN_NON_ALLOCATED means we have to look into the parent data
const isLocallyFull = !grainOffsets.some(value => value === -SE_SPARSE_GRAIN_NON_ALLOCATED)
let parentBuffer, parentBlock
// don't read from parent is current block is already completly described
if (isLocallyFull) {
parentBuffer = Buffer.alloc(512 /* bitmap */ + 2 * 1024 * 1024 /* data */, 0)
parentBuffer.fill(255, 0, 512) // bitmap is full of bit 1
} else {
parentBlock = await this.#parentVhd.readBlock(blockId)
parentBuffer = parentBlock.buffer
}
const EMPTY_GRAIN = Buffer.alloc(GRAIN_SIZE_BYTES, 0)
for (const index in grainOffsets) {
const value = grainOffsets[index]
let data
if (value > 0) {
// it's the offset in byte of a grain type SE_SPARSE_GRAIN_ALLOCATED
// @todo this part can be quite slow when grain are not sorted
data = await this.#read(value, GRAIN_SIZE_BYTES)
} else {
// back to the real grain type
@@ -294,7 +230,7 @@ export default class VhdEsxiSeSparse extends VhdAbstract {
}
}
// no need to copy if data all come from parent
return changed || !parentBlock
return changed
? {
id: blockId,
bitmap: parentBuffer.slice(0, 512),

View File

@@ -1,7 +1,7 @@
{
"license": "ISC",
"private": false,
"version": "0.4.0",
"version": "0.3.1",
"name": "@xen-orchestra/vmware-explorer",
"dependencies": {
"@vates/node-vsphere-soap": "^2.0.0",

View File

@@ -10,8 +10,7 @@
}
},
"devDependencies": {
"vue": "^3.4.13",
"@vue/tsconfig": "^0.5.1"
"vue": "^3.4.13"
},
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/web-core",
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
@@ -26,6 +25,6 @@
},
"license": "AGPL-3.0-or-later",
"engines": {
"node": ">=18"
"node": ">=8.10"
}
}

View File

@@ -1,12 +0,0 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"include": ["env.d.ts", "lib/**/*", "lib/**/*.vue"],
"exclude": ["lib/**/__tests__/*"],
"compilerOptions": {
"noEmit": true,
"baseUrl": ".",
"paths": {
"@core/*": ["./lib/*"]
}
}
}

View File

@@ -2,12 +2,12 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="./favicon.svg" type="image/svg+xml" />
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Xen Orchestra</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="./src/main.ts"></script>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

View File

@@ -18,7 +18,6 @@
"npm-run-all2": "^6.1.1",
"pinia": "^2.1.7",
"typescript": "~5.3.3",
"unplugin-vue-router": "^0.7.0",
"vite": "^5.0.11",
"vue": "^3.4.13",
"vue-tsc": "^1.8.27"

View File

@@ -1,3 +1,3 @@
<template>
<RouterView />
<div>Xen Orchestra 6</div>
</template>

View File

@@ -1,17 +1,10 @@
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
// eslint-disable-next-line import/no-unresolved -- https://github.com/posva/unplugin-vue-router/issues/232
import { createRouter, createWebHashHistory } from 'vue-router/auto'
import '@xen-orchestra/web-core/assets/css/base.pcss'
const app = createApp(App)
const router = createRouter({
history: createWebHashHistory(),
})
app.use(createPinia())
app.use(router)
app.mount('#app')

View File

@@ -1 +0,0 @@
<template>Welcome to Xen Orchestra 6</template>

View File

@@ -1,22 +1,13 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"include": [
"env.d.ts",
"typed-router.d.ts",
"src/**/*",
"src/**/*.vue",
"../web-core/lib/**/*",
"../web-core/lib/**/*.vue"
],
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
"exclude": ["src/**/__tests__/*"],
"compilerOptions": {
"composite": true,
"noEmit": true,
"baseUrl": ".",
"rootDir": "..",
"paths": {
"@/*": ["./src/*"],
"@core/*": ["../web-core/lib/*"]
"@/*": ["./src/*"]
}
}
}

View File

@@ -2,10 +2,10 @@
"files": [],
"references": [
{
"path": "./tsconfig.node.json",
"path": "./tsconfig.node.json"
},
{
"path": "./tsconfig.app.json",
},
],
"path": "./tsconfig.app.json"
}
]
}

View File

@@ -1,144 +0,0 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// Generated by unplugin-vue-router. ‼️ DO NOT MODIFY THIS FILE ‼️
// It's recommended to commit this file.
// Make sure to add this file to your tsconfig.json file as an "includes" or "files" entry.
/// <reference types="unplugin-vue-router/client" />
import type {
// type safe route locations
RouteLocationTypedList,
RouteLocationResolvedTypedList,
RouteLocationNormalizedTypedList,
RouteLocationNormalizedLoadedTypedList,
RouteLocationAsString,
RouteLocationAsRelativeTypedList,
RouteLocationAsPathTypedList,
// helper types
// route definitions
RouteRecordInfo,
ParamValue,
ParamValueOneOrMore,
ParamValueZeroOrMore,
ParamValueZeroOrOne,
// vue-router extensions
_RouterTyped,
RouterLinkTyped,
RouterLinkPropsTyped,
NavigationGuard,
UseLinkFnTyped,
// data fetching
_DataLoader,
_DefineLoaderOptions,
} from 'unplugin-vue-router/types'
declare module 'vue-router/auto/routes' {
export interface RouteNamedMap {
'/': RouteRecordInfo<'/', '/', Record<never, never>, Record<never, never>>
}
}
declare module 'vue-router/auto' {
import type { RouteNamedMap } from 'vue-router/auto/routes'
export type RouterTyped = _RouterTyped<RouteNamedMap>
/**
* Type safe version of `RouteLocationNormalized` (the type of `to` and `from` in navigation guards).
* Allows passing the name of the route to be passed as a generic.
*/
export type RouteLocationNormalized<Name extends keyof RouteNamedMap = keyof RouteNamedMap> =
RouteLocationNormalizedTypedList<RouteNamedMap>[Name]
/**
* Type safe version of `RouteLocationNormalizedLoaded` (the return type of `useRoute()`).
* Allows passing the name of the route to be passed as a generic.
*/
export type RouteLocationNormalizedLoaded<Name extends keyof RouteNamedMap = keyof RouteNamedMap> =
RouteLocationNormalizedLoadedTypedList<RouteNamedMap>[Name]
/**
* Type safe version of `RouteLocationResolved` (the returned route of `router.resolve()`).
* Allows passing the name of the route to be passed as a generic.
*/
export type RouteLocationResolved<Name extends keyof RouteNamedMap = keyof RouteNamedMap> =
RouteLocationResolvedTypedList<RouteNamedMap>[Name]
/**
* Type safe version of `RouteLocation` . Allows passing the name of the route to be passed as a generic.
*/
export type RouteLocation<Name extends keyof RouteNamedMap = keyof RouteNamedMap> =
RouteLocationTypedList<RouteNamedMap>[Name]
/**
* Type safe version of `RouteLocationRaw` . Allows passing the name of the route to be passed as a generic.
*/
export type RouteLocationRaw<Name extends keyof RouteNamedMap = keyof RouteNamedMap> =
| RouteLocationAsString<RouteNamedMap>
| RouteLocationAsRelativeTypedList<RouteNamedMap>[Name]
| RouteLocationAsPathTypedList<RouteNamedMap>[Name]
/**
* Generate a type safe params for a route location. Requires the name of the route to be passed as a generic.
*/
export type RouteParams<Name extends keyof RouteNamedMap> = RouteNamedMap[Name]['params']
/**
* Generate a type safe raw params for a route location. Requires the name of the route to be passed as a generic.
*/
export type RouteParamsRaw<Name extends keyof RouteNamedMap> = RouteNamedMap[Name]['paramsRaw']
export function useRouter(): RouterTyped
export function useRoute<Name extends keyof RouteNamedMap = keyof RouteNamedMap>(
name?: Name
): RouteLocationNormalizedLoadedTypedList<RouteNamedMap>[Name]
export const useLink: UseLinkFnTyped<RouteNamedMap>
export function onBeforeRouteLeave(guard: NavigationGuard<RouteNamedMap>): void
export function onBeforeRouteUpdate(guard: NavigationGuard<RouteNamedMap>): void
export const RouterLink: RouterLinkTyped<RouteNamedMap>
export const RouterLinkProps: RouterLinkPropsTyped<RouteNamedMap>
// Experimental Data Fetching
export function defineLoader<
P extends Promise<any>,
Name extends keyof RouteNamedMap = keyof RouteNamedMap,
isLazy extends boolean = false,
>(
name: Name,
loader: (route: RouteLocationNormalizedLoaded<Name>) => P,
options?: _DefineLoaderOptions<isLazy>
): _DataLoader<Awaited<P>, isLazy>
export function defineLoader<P extends Promise<any>, isLazy extends boolean = false>(
loader: (route: RouteLocationNormalizedLoaded) => P,
options?: _DefineLoaderOptions<isLazy>
): _DataLoader<Awaited<P>, isLazy>
export {
_definePage as definePage,
_HasDataLoaderMeta as HasDataLoaderMeta,
_setupDataFetchingGuard as setupDataFetchingGuard,
_stopDataFetchingScope as stopDataFetchingScope,
} from 'unplugin-vue-router/runtime'
}
declare module 'vue-router' {
import type { RouteNamedMap } from 'vue-router/auto/routes'
export interface TypesConfig {
beforeRouteUpdate: NavigationGuard<RouteNamedMap>
beforeRouteLeave: NavigationGuard<RouteNamedMap>
$route: RouteLocationNormalizedLoadedTypedList<RouteNamedMap>[keyof RouteNamedMap]
$router: _RouterTyped<RouteNamedMap>
RouterLink: RouterLinkTyped<RouteNamedMap>
}
}

View File

@@ -2,16 +2,13 @@ import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueRouter from 'unplugin-vue-router/vite'
// https://vitejs.dev/config/
export default defineConfig({
base: './',
plugins: [vueRouter(), vue()],
plugins: [vue()],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
'@core': fileURLToPath(new URL('../web-core/lib', import.meta.url)),
},
},
})

View File

@@ -1,6 +1,6 @@
{
"name": "@xen-orchestra/xapi",
"version": "4.2.0",
"version": "4.1.0",
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/xapi",
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
"repository": {
@@ -16,7 +16,7 @@
},
"main": "./index.mjs",
"peerDependencies": {
"xen-api": "^2.0.1"
"xen-api": "^2.0.0"
},
"scripts": {
"postversion": "npm publish --access public",
@@ -24,7 +24,7 @@
},
"dependencies": {
"@vates/async-each": "^1.0.0",
"@vates/decorate-with": "^2.1.0",
"@vates/decorate-with": "^2.0.0",
"@vates/nbd-client": "^3.0.0",
"@vates/task": "^0.2.0",
"@xen-orchestra/async-map": "^0.1.2",

View File

@@ -21,23 +21,12 @@ export default class Vif {
MAC = '',
} = {}
) {
if (device === undefined) {
const allowedDevices = await this.call('VM.get_allowed_VIF_devices', VM)
if (allowedDevices.length === 0) {
const error = new Error('could not find an allowed VIF device')
error.poolUuid = this.pool.uuid
error.vmRef = VM
throw error
}
device = allowedDevices[0]
}
const [powerState, ...rest] = await Promise.all([
this.getField('VM', VM, 'power_state'),
MTU ?? this.getField('network', network, 'MTU'),
device ?? (await this.call('VM.get_allowed_VIF_devices', VM))[0],
MTU ?? (await this.getField('network', network, 'MTU')),
])
;[MTU] = rest
;[device, MTU] = rest
const vifRef = await this.call('VIF.create', {
currently_attached: powerState === 'Suspended' ? currently_attached : undefined,

View File

@@ -1 +0,0 @@
../../scripts/npmignore

View File

@@ -1,3 +0,0 @@
const formatCounter = counter => String(counter).padStart(8, '0')
export const formatBlockPath = (basePath, counter) => `${basePath}/${formatCounter(counter)}`

View File

@@ -1,5 +0,0 @@
export function isNotEmptyRef(val) {
const EMPTY = 'OpaqueRef:NULL'
const PREFIX = 'OpaqueRef:'
return val !== EMPTY && typeof val === 'string' && val.startsWith(PREFIX)
}

View File

@@ -1,42 +0,0 @@
// from package xml-escape
function escape(string) {
if (string === null || string === undefined) return
if (typeof string === 'number') {
return string
}
const map = {
'>': '&gt;',
'<': '&lt;',
"'": '&apos;',
'"': '&quot;',
'&': '&amp;',
}
const pattern = '([&"<>\'])'
return string.replace(new RegExp(pattern, 'g'), function (str, item) {
return map[item]
})
}
function formatDate(d) {
return d.toISOString().replaceAll('-', '').replace('.000Z', 'Z')
}
export default function toOvaXml(obj) {
if (Array.isArray(obj)) {
return `<value><array><data>${obj.map(val => toOvaXml(val)).join('')}</data></array></value>`
}
if (typeof obj === 'object') {
if (obj instanceof Date) {
return `<value><dateTime.iso8601>${escape(formatDate(obj))}</dateTime.iso8601></value>`
}
return `<value><struct>${Object.entries(obj)
.map(([key, value]) => `<member><name>${escape(key)}</name>${toOvaXml(value)}</member>`)
.join('')}</struct></value>`
}
if (typeof obj === 'boolean') {
return `<value><boolean>${obj ? 1 : 0}</boolean></value>`
}
return `<value>${escape(obj)}</value>`
}

View File

@@ -1,57 +0,0 @@
import { formatBlockPath } from './_formatBlockPath.mjs'
import { fromCallback } from 'promise-toolbox'
import { readChunkStrict } from '@vates/read-chunk'
import { xxhash64 } from 'hash-wasm'
export const XVA_DISK_CHUNK_LENGTH = 1024 * 1024
async function addEntry(pack, name, buffer) {
await fromCallback.call(pack, pack.entry, { name }, buffer)
}
async function writeBlock(pack, data, name) {
if (data.length < XVA_DISK_CHUNK_LENGTH) {
data = Buffer.concat([data, Buffer.alloc(XVA_DISK_CHUNK_LENGTH - data.length, 0)])
}
await addEntry(pack, name, data)
// weirdly, ocaml and xxhash return the bytes in reverse order to each other
const hash = (await xxhash64(data)).toString('hex').toUpperCase()
await addEntry(pack, `${name}.xxhash`, Buffer.from(hash, 'utf8'))
}
export default async function addDisk(pack, vhd, basePath) {
let counter = 0
let written
let lastBlockWrittenAt = Date.now()
const MAX_INTERVAL_BETWEEN_BLOCKS = 60 * 1000
const empty = Buffer.alloc(XVA_DISK_CHUNK_LENGTH, 0)
const stream = await vhd.rawContent()
let lastBlockLength
const diskSize = vhd.footer.currentSize
let remaining = diskSize
while (remaining > 0) {
lastBlockLength = Math.min(XVA_DISK_CHUNK_LENGTH, remaining)
const data = await readChunkStrict(stream, lastBlockLength)
remaining -= lastBlockLength
if (
// write first block
counter === 0 ||
// write all non empty blocks
!data.equals(empty) ||
// write one block from time to time to ensure there is no timeout
// occurring while passing empty blocks
Date.now() - lastBlockWrittenAt > MAX_INTERVAL_BETWEEN_BLOCKS
) {
written = true
await writeBlock(pack, data, formatBlockPath(basePath, counter))
lastBlockWrittenAt = Date.now()
} else {
written = false
}
counter++
}
if (!written) {
// last block must be present
await writeBlock(pack, empty, formatBlockPath(basePath, counter - 1))
}
}

View File

@@ -1,156 +0,0 @@
import assert from 'node:assert'
import { fromCallback } from 'promise-toolbox'
import { v4 as uuid } from 'uuid'
import defaultsDeep from 'lodash.defaultsdeep'
import { DEFAULT_VBD } from './templates/vbd.mjs'
import { DEFAULT_VDI } from './templates/vdi.mjs'
import { DEFAULT_VIF } from './templates/vif.mjs'
import { DEFAULT_VM } from './templates/vm.mjs'
import toOvaXml from './_toOvaXml.mjs'
import { XVA_DISK_CHUNK_LENGTH } from './_writeDisk.mjs'
export default async function writeOvaXml(
pack,
{ memory, networks, nCpus, firmware, vdis, vhds, ...vmSnapshot },
{ sr, network }
) {
let refId = 0
function nextRef() {
return 'Ref:' + String(refId++).padStart(3, '0')
}
const data = {
version: {
hostname: 'localhost',
date: '2022-01-01',
product_version: '8.2.1',
product_brand: 'XCP-ng',
build_number: 'release/yangtze/master/58',
xapi_major: 1,
xapi_minor: 20,
export_vsn: 2,
},
objects: [],
}
const vm = defaultsDeep(
{
id: nextRef(),
// you can pass a full snapshot and nothing more to do
snapshot: vmSnapshot,
},
{
// some data need a little more work to be usable
// if they are not already in vm
snapshot: {
HVM_boot_params: {
firmware,
},
memory_static_max: memory,
memory_static_min: memory,
memory_dynamic_max: memory,
memory_dynamic_min: memory,
other_config: {
mac_seed: uuid(),
},
uuid: uuid(),
VCPUs_at_startup: nCpus,
VCPUs_max: nCpus,
},
},
DEFAULT_VM
)
data.objects.push(vm)
const srObj = defaultsDeep(
{
class: 'SR',
id: nextRef(),
snapshot: sr,
},
{
snapshot: {
VDIs: [],
},
}
)
data.objects.push(srObj)
assert.strictEqual(vhds.length, vdis.length)
for (let index = 0; index < vhds.length; index++) {
const userdevice = index + 1
const vhd = vhds[index]
const alignedSize = Math.ceil(vdis[index].virtual_size / XVA_DISK_CHUNK_LENGTH) * XVA_DISK_CHUNK_LENGTH
const vdi = defaultsDeep(
{
id: nextRef(),
// overwrite SR from an opaque ref to a ref:
snapshot: { ...vdis[index], SR: srObj.id, virtual_size: alignedSize },
},
{
snapshot: {
uuid: uuid(),
},
},
DEFAULT_VDI
)
data.objects.push(vdi)
srObj.snapshot.VDIs.push(vdi.id)
vhd.ref = vdi.id
const vbd = defaultsDeep(
{
id: nextRef(),
snapshot: {
device: `xvd${String.fromCharCode('a'.charCodeAt(0) + index)}`,
uuid: uuid(),
userdevice,
VM: vm.id,
VDI: vdi.id,
},
},
DEFAULT_VBD
)
data.objects.push(vbd)
vdi.snapshot.vbds.push(vbd.id)
vm.snapshot.VBDs.push(vbd.id)
}
if (network && networks?.length) {
const networkObj = defaultsDeep(
{
class: 'network',
id: nextRef(),
snapshot: network,
},
{
snapshot: {
vifs: [],
},
}
)
data.objects.push(networkObj)
let vifIndex = 0
for (const sourceNetwork of networks) {
const vif = defaultsDeep(
{
id: nextRef(),
snapshot: {
device: ++vifIndex,
MAC: sourceNetwork.macAddress,
MAC_autogenerated: sourceNetwork.isGenerated,
uuid: uuid(),
VM: vm.id,
network: networkObj.id,
},
},
DEFAULT_VIF
)
data.objects.push(vif)
networkObj.snapshot.vifs.push(vif.id)
}
}
const xml = toOvaXml(data)
await fromCallback.call(pack, pack.entry, { name: `ova.xml` }, xml)
}

View File

@@ -1,32 +0,0 @@
import { isNotEmptyRef } from './_isNotEmptyRef.mjs'
import { importVm } from './importVm.mjs'
export async function importVdi(vdi, vhd, xapi, sr) {
// create a fake VM
const vmRef = await importVm(
{
name_label: `[xva-disp-import]${vdi.name_label}`,
memory: 1024 * 1024 * 32,
nCpus: 1,
firmware: 'bios',
vdis: [vdi],
vhds: [vhd],
},
xapi,
sr
)
// wait for the VM to be loaded if necessary
xapi.getObject(vmRef, undefined) ?? (await xapi.waitObject(vmRef))
const vbdRefs = await xapi.getField('VM', vmRef, 'VBDs')
// get the disk
const disks = { __proto__: null }
;(await xapi.getRecords('VBD', vbdRefs)).forEach(vbd => {
if (vbd.type === 'Disk' && isNotEmptyRef(vbd.VDI)) {
disks[vbd.VDI] = true
}
})
// destroy the VM and VBD
await xapi.call('VM.destroy', vmRef)
return await xapi.getRecord('VDI', Object.keys(disks)[0])
}

View File

@@ -1,32 +0,0 @@
import tar from 'tar-stream'
import writeOvaXml from './_writeOvaXml.mjs'
import writeDisk from './_writeDisk.mjs'
export async function importVm(vm, xapi, sr, network) {
const pack = tar.pack()
const taskRef = await xapi.task_create('VM import')
const query = {
sr_id: sr.$ref,
}
const promise = xapi
.putResource(pack, '/import/', {
query,
task: taskRef,
})
.catch(err => console.error(err))
await writeOvaXml(pack, vm, { sr, network })
for (const vhd of vm.vhds) {
await writeDisk(pack, vhd, vhd.ref)
}
pack.finalize()
const str = await promise
const matches = /OpaqueRef:[0-9a-z-]+/.exec(str)
if (!matches) {
const error = new Error(`no opaque ref found in ${str}`)
throw error
}
return matches[0]
}

View File

@@ -1,29 +0,0 @@
{
"name": "@xen-orchestra/xva",
"version": "1.0.2",
"main": "index.js",
"author": "",
"license": "ISC",
"private": false,
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/xva",
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
"repository": {
"directory": "@xen-orchestra/xva",
"type": "git",
"url": "https://github.com/vatesfr/xen-orchestra.git"
},
"engines": {
"node": ">=14.0"
},
"dependencies": {
"@vates/read-chunk": "^1.2.0",
"hash-wasm": "^4.11.0",
"lodash.defaultsdeep": "^4.6.1",
"promise-toolbox": "^0.21.0",
"tar-stream": "^3.1.6",
"uuid": "^9.0.0"
},
"scripts": {
"postversion": "npm publish --access public"
}
}

View File

@@ -1,22 +0,0 @@
export const DEFAULT_VBD = {
class: 'VBD',
snapshot: {
allowed_operations: [],
bootable: true, // @todo : fix it
current_operations: {},
currently_attached: false,
empty: false,
metrics: 'OpaqueRef:NULL',
mode: 'RW',
other_config: {},
qos_algorithm_params: {},
qos_algorithm_type: '',
qos_supported_algorithms: [],
runtime_properties: {},
status_code: 0,
status_detail: '',
storage_lock: false,
type: 'Disk',
unpluggable: false,
},
}

View File

@@ -1,29 +0,0 @@
export const DEFAULT_VDI = {
class: 'VDI',
snapshot: {
allow_caching: false,
cbt_enabled: false,
descriptionLabel: 'description',
is_a_snapshot: false,
managed: true,
metrics: 'OpaqueRef:NULL',
missing: false,
name_label: 'name_label',
on_boot: 'persist',
other_config: {},
parent: 'OpaqueRef:NULL',
physical_utilisation: 1024 * 1024,
read_only: false,
sharable: false,
snapshot_of: 'OpaqueRef:NULL',
snapshots: [],
SR: 'OpaqueRef:NULL',
storage_lock: false,
tags: [],
type: 'user',
uuid: '',
vbds: [],
virtual_size: 0,
xenstore_data: {},
},
}

View File

@@ -1,26 +0,0 @@
export const DEFAULT_VIF = {
class: 'VIF',
snapshot: {
allowed_operations: [],
currently_attached: false,
current_operations: {},
ipv4_addresses: [],
ipv4_allowed: [],
ipv4_configuration_mode: 'None',
ipv4_gateway: '',
ipv6_addresses: [],
ipv6_allowed: [],
ipv6_configuration_mode: 'None',
ipv6_gateway: '',
locking_mode: 'network_default',
MTU: 1500,
metrics: 'OpaqueRef:NULL',
other_config: {},
qos_algorithm_params: {},
qos_algorithm_type: '',
qos_supported_algorithms: [],
runtime_properties: {},
status_code: 0,
status_detail: '',
},
}

View File

@@ -1,106 +0,0 @@
export const DEFAULT_VM = {
class: 'VM',
id: null,
snapshot: {
actions_after_crash: 'restart',
actions_after_reboot: 'restart',
actions_after_shutdown: 'destroy',
affinity: 'Ref:53',
allowed_operations: [],
// appliance:'OpaqueRef:NULL',
attached_PCIs: [],
blobs: {},
blocked_operations: {},
children: [],
consoles: [],
crash_dumps: [],
current_operations: {},
domain_type: 'hvm',
domarch: '',
domid: -1,
generation_id: '',
guest_metrics: 'Ref:53',
ha_always_run: false,
ha_restart_priority: '',
hardware_platform_version: 0,
has_vendor_device: false,
HVM_boot_params: {
firmware: 'bios',
order: 'dcn',
},
HVM_boot_policy: 'BIOS order',
HVM_shadow_multiplier: 1,
is_a_template: false,
is_control_domain: false,
is_default_template: false,
is_snapshot_from_vmpp: false,
is_vmss_snapshot: false,
last_booted_record: '',
memory_dynamic_max: 1,
memory_dynamic_min: 1,
memory_overhead: 11534336,
memory_static_max: 1,
memory_static_min: 1,
memory_target: 0,
metrics: 'OpaqueRef:NULL',
name_label: 'from xva',
NVRAM: {},
name_description: ' from xva',
order: 0,
other_config: {
base_template_name: 'Other install media',
// mac_seed,
'install-methods': 'cdrom',
},
parent: 'OpaqueRef:NULL',
PCI_bus: '',
platform: {
timeoffset: 1,
'device-model': 'qemu-upstream-compat',
secureboot: 'false',
hpet: 'true',
nx: 'true',
pae: 'true',
apic: 'true',
viridian: 'true',
acpi: 1,
},
power_state: 'halted',
// protection_policy:'OpaqueRef:NULL',
PV_args: '',
PV_bootloader_args: '',
PV_bootloader: '',
PV_kernel: '',
PV_legacy_args: '',
PV_ramdisk: '',
recommendations: '',
reference_label: 'other-install-media',
requires_reboot: false,
resident_on: 'Ref:53',
// scheduled_to_be_resident_on:'OpaqueRef:NULL',
shutdown_delay: 0,
// snapshot_schedule: 'OpaqueRef:NULL',
snapshot_info: {},
snapshot_metadata: '',
snapshot_of: 'OpaqueRef:NULL',
snapshot_time: new Date(0),
snapshots: [],
start_delay: 0,
// suspend_VDI:'OpaqueRef:NULL',
// suspend_SR:'OpaqueRef:NULL',
tags: [],
transportable_snapshot_id: '',
// uuid,
user_version: 1,
VBDs: [],
VCPUs_at_startup: 1,
VCPUs_max: 1,
VCPUs_params: {},
version: 0,
VGPUs: [],
VIFs: [],
VTPMs: [],
VUSBs: [],
xenstore_data: {},
},
}

View File

@@ -1,117 +1,17 @@
# ChangeLog
## **5.91.2** (2024-02-09)
<img id="latest" src="https://badgen.net/badge/channel/latest/yellow" alt="Channel: latest" />
### Enhancements
- [REST API] Add `/groups` collection [Forum#70500](https://xcp-ng.org/forum/post/70500)
- [REST API] Add `/groups/:id/users` and `/users/:id/groups` collection [Forum#70500](https://xcp-ng.org/forum/post/70500)
- [REST API] Expose messages associated to XAPI objects at `/:collection/:object/messages`
### Bug fixes
- [Import/VMWare] Fix `(Failure \"Expected string, got 'I(0)'\")` (PR [#7361](https://github.com/vatesfr/xen-orchestra/issues/7361))
- [Plugin/load-balancer] Fixing `TypeError: Cannot read properties of undefined (reading 'high')` happening when trying to optimize a host with performance plan [#7359](https://github.com/vatesfr/xen-orchestra/issues/7359) (PR [#7362](https://github.com/vatesfr/xen-orchestra/pull/7362))
- Changing the number of displayed items per page should send back to the first page [#7350](https://github.com/vatesfr/xen-orchestra/issues/7350)
- [Plugin/load-balancer] Correctly create a _simple_ instead of a _density_ plan when it is selected (PR [#7358](https://github.com/vatesfr/xen-orchestra/pull/7358))
### Released packages
- xo-server 5.136.0
- xo-server-load-balancer 0.8.1
- xo-web 5.136.1
## **5.91.1** (2024-02-06)
### Bug fixes
- [Import/VMWare] Fix `Error: task has been destroyed before completion` with XVA import [Forum#70513](https://xcp-ng.org/forum/post/70513)
- [Import/VM] Fix `UUID_INVALID(VM, OpaqueRef:...)` error when importing from URL
- [Proxies] Fix `xapi.getOrWaitObject is not a function` is not a function during deployment
- [REST API] Fix empty object's tasks list
- [REST API] Fix incorrect `href` in `/:collection/:object/tasks`
### Released packages
- @xen-orchestra/immutable-backups 1.0.1
- @xen-orchestra/xva 1.0.2
- xo-server 5.135.1
## **5.91.0** (2024-01-31)
### Highlights
- [Import/VMWare] Speed up import and make all imports thin [#7323](https://github.com/vatesfr/xen-orchestra/issues/7323)
- [Backup] Implement Backup Repository immutability (PR [#6928](https://github.com/vatesfr/xen-orchestra/pull/6928))
- [REST API] New pool action: `emergency_shutdown`, it suspends all the VMs and then shuts down all the host [#7277](https://github.com/vatesfr/xen-orchestra/issues/7277) (PR [#7279](https://github.com/vatesfr/xen-orchestra/pull/7279))
- [REST API] New pool action: `create_vm` [#6749](https://github.com/vatesfr/xen-orchestra/issues/6749)
- [Pool/Advanced] Ability to do a `Rolling Pool Reboot` (Enterprise plans) [#6885](https://github.com/vatesfr/xen-orchestra/issues/6885)
- [Tags] Admins can create colored tags (PR [#7262](https://github.com/vatesfr/xen-orchestra/pull/7262))
- [VM] Custom notes [#5792](https://github.com/vatesfr/xen-orchestra/issues/5792) (PR [#7322](https://github.com/vatesfr/xen-orchestra/pull/7322))(PRs [#7243](https://github.com/vatesfr/xen-orchestra/pull/7243), [#7242](https://github.com/vatesfr/xen-orchestra/pull/7242))
- [Plugin/load-balancer] Limit concurrent VM migrations to 2 (configurable) to avoid long paused VMs [#7084](https://github.com/vatesfr/xen-orchestra/issues/7084) (PR [#7297](https://github.com/vatesfr/xen-orchestra/pull/7297))
- [SR] Possibility to create SMB shared SR [#991](https://github.com/vatesfr/xen-orchestra/issues/991) (PR [#7330](https://github.com/vatesfr/xen-orchestra/pull/7330))
- [Tags] Add tooltips on `xo:no-bak` and `xo:notify-on-snapshot` tags (PR [#7335](https://github.com/vatesfr/xen-orchestra/pull/7335))
- [Host/Reboot] Confirmation modal to reboot an updated slave host if the master is not [#7059](https://github.com/vatesfr/xen-orchestra/issues/7059) (PR [#7293](https://github.com/vatesfr/xen-orchestra/pull/7293))
- [Pool/Host] Add a warning if hosts do not have the same version within a pool [#7059](https://github.com/vatesfr/xen-orchestra/issues/7059) (PR [#7280](https://github.com/vatesfr/xen-orchestra/pull/7280))
- [VM/Advanced] Admins can change VM creator [Forum#7313](https://xcp-ng.org/forum/topic/7313/change-created-by-and-date-information) (PR [#7276](https://github.com/vatesfr/xen-orchestra/pull/7276))
- [XOSTOR] Allow user to ignore file systems at storage creation (PR[#7338](https://github.com/vatesfr/xen-orchestra/pull/7338))
- [Settings/Logs] Transform objects UUIDs and OpaqueRefs into clickable links, leading to the corresponding object page (PR [#7300](https://github.com/vatesfr/xen-orchestra/pull/7300))
- [Pool/patches] Disable Rolling Pool Update button if host is alone in its pool [#6415](https://github.com/vatesfr/xen-orchestra/issues/6415) (PR [#7286](https://github.com/vatesfr/xen-orchestra/pull/7286))
## **next**
### Enhancements
- [xo-cli] Supports NDJSON response for the `rest get` command (it also respects the `--json` flag) [Forum#69326](https://xcp-ng.org/forum/post/69326)
- [Settings/Logs] Use GitHub issue form with pre-filled fields when reporting a bug [#7142](https://github.com/vatesfr/xen-orchestra/issues/7142) (PR [#7274](https://github.com/vatesfr/xen-orchestra/pull/7274))
- [Tasks] Hide `/rrd_updates` tasks by default
- [Sign in] Support _Remember me_ feature with external providers (PR [#7298](https://github.com/vatesfr/xen-orchestra/pull/7298))
- [Plugins] Loading, or unloading, will respectively enable, or disable, _Auto-load at server start_, this should lead to least surprising behaviors (PR [#7317](https://github.com/vatesfr/xen-orchestra/pull/7317))
- [Backup/Restore] Show whether the memory was backed up (PR [#7315](https://github.com/vatesfr/xen-orchestra/pull/7315))
### Bug fixes
- [Proxies] Fix `this.getObject` is not a function during deployment
- [Settings/Logs] Fix `sr.getAllUnhealthyVdiChainsLength: not enough permissions` error with non-admin users (PR [#7265](https://github.com/vatesfr/xen-orchestra/pull/7265))
- [Settings/Logs] Fix `proxy.getAll: not enough permissions` error with non-admin users (PR [#7249](https://github.com/vatesfr/xen-orchestra/pull/7249))
- [Replication/Health Check] Fix `healthCheckVm.add_tag is not a function` error [Forum#69156](https://xcp-ng.org/forum/post/69156)
- [Plugin/load-balancer] Prevent unwanted migrations to hosts with low free memory (PR [#7288](https://github.com/vatesfr/xen-orchestra/pull/7288))
- Avoid unnecessary `pool.add_to_other_config: Duplicate key` error in XAPI log [Forum#68761](https://xcp-ng.org/forum/post/68761)
- [Jobs] Reset parameters when editing method to avoid invalid parameters on execution [Forum#69299](https://xcp-ng.org/forum/post/69299)
- [Metadata Backup] Fix `ENOENT` error when restoring an _XO Config_ backup [Forum#68999](https://xcp-ng.org/forum/post/68999)
- [REST API] Fix `/backup/log/<id>` which was broken by the `/backups` to `/backup` renaming [Forum#69426](https://xcp-ng.org/forum/post/69426)
- [Backup/Restore] Fix unnecessary pool selector in XO config backup restore modal [Forum#8130](https://xcp-ng.org/forum/topic/8130/xo-configbackup-restore) (PR [#7287](https://github.com/vatesfr/xen-orchestra/pull/7287))
- [File restore] Fix potential race condition in partition mount/unmount (PR [#7312](https://github.com/vatesfr/xen-orchestra/pull/7312))
- [Modal] Fix opened modal not closing when navigating to another route/URL (PR [#7301](https://github.com/vatesfr/xen-orchestra/pull/7301))
- [Backup/Restore] Don't count memory as a key (i.e. complete) disk [Forum#8212](https://xcp-ng.org/forum/post/69591) (PR [#7315](https://github.com/vatesfr/xen-orchestra/pull/7315))
- [PIF] Fix IPv4 reconfiguration only worked when the IPv4 mode was updated (PR [#7324](https://github.com/vatesfr/xen-orchestra/pull/7324))
- [Backup/Smart mode] Make preview correctly ignoring `xo:no-bak` tags [Forum#69797](https://xcp-ng.org/forum/post/69797) (PR [#7331](https://github.com/vatesfr/xen-orchestra/pull/7331))
- [Pool/Advanced] Show pool backup/migration network even if they no longer exist (PR [#7303](https://github.com/vatesfr/xen-orchestra/pull/7303))
- [Import/disk] Couldn't update 'name' field when importing from a URL [#7326](https://github.com/vatesfr/xen-orchestra/issues/7326) (PR [#7332](https://github.com/vatesfr/xen-orchestra/pull/7332))
- [Pool/patches] Disable Rolling Pool Update button if some powered up VMs are using a non-shared storage [#6415](https://github.com/vatesfr/xen-orchestra/issues/6415) (PR [#7294](https://github.com/vatesfr/xen-orchestra/pull/7294))
## Released packages
- @vates/decorate-with 2.1.0
- @vates/fuse-vhd 2.1.0
- xen-api 2.0.1
- @xen-orchestra/xapi 4.2.0
- @xen-orchestra/self-signed 0.2.0
- xo-server-load-balancer 0.8.0
- @xen-orchestra/vmware-explorer 0.4.0
- @xen-orchestra/xva 1.0.0
- @xen-orchestra/fs 4.1.4
- @xen-orchestra/backups 0.44.6
- @xen-orchestra/immutable-backups 1.0.0
- @xen-orchestra/proxy 0.26.45
- xo-cli 0.26.0
- xo-server 5.135.0
- xo-web 5.136.0
- xo-cli 0.24.0
## **5.90.0** (2023-12-29)
<img id="stable" src="https://badgen.net/badge/channel/stable/green" alt="Channel: stable" />
### Highlights
- [VDI] Create XAPI task during NBD export (PR [#7228](https://github.com/vatesfr/xen-orchestra/pull/7228))
@@ -162,6 +62,8 @@
## **5.89.0** (2023-11-30)
<img id="latest" src="https://badgen.net/badge/channel/latest/yellow" alt="Channel: latest" />
### Highlights
- [Restore] Show source remote and restoration time on a restored VM (PR [#7186](https://github.com/vatesfr/xen-orchestra/pull/7186))
@@ -212,6 +114,8 @@
## **5.88.2** (2023-11-13)
<img id="stable" src="https://badgen.net/badge/channel/stable/green" alt="Channel: stable" />
### Enhancement
- [REST API] Add `users` collection

View File

@@ -7,16 +7,38 @@
> Users must be able to say: “Nice enhancement, I'm eager to test it”
- Disable search engine indexing via a `robots.txt`
- [Stats] Support format used by XAPI 23.31
- [Settings/Logs] Use GitHub issue form with pre-filled fields when reporting a bug [#7142](https://github.com/vatesfr/xen-orchestra/issues/7142) (PR [#7274](https://github.com/vatesfr/xen-orchestra/pull/7274))
- [REST API] New pool action: `emergency_shutdown`, it suspends all the VMs and then shuts down all the host [#7277](https://github.com/vatesfr/xen-orchestra/issues/7277) (PR [#7279](https://github.com/vatesfr/xen-orchestra/pull/7279))
- [Tasks] Hide `/rrd_updates` tasks by default
- [Sign in] Support _Remember me_ feature with external providers (PR [#7298](https://github.com/vatesfr/xen-orchestra/pull/7298))
- [Pool/Host] Add a warning if hosts do not have the same version within a pool [#7059](https://github.com/vatesfr/xen-orchestra/issues/7059) (PR [#7280](https://github.com/vatesfr/xen-orchestra/pull/7280))
- [Plugins] Loading, or unloading, will respectively enable, or disable, _Auto-load at server start_, this should lead to least surprising behaviors (PR [#7317](https://github.com/vatesfr/xen-orchestra/pull/7317))
- [VM/Advanced] Admin can change VM creator [Forum#7313](https://xcp-ng.org/forum/topic/7313/change-created-by-and-date-information) (PR [#7276](https://github.com/vatesfr/xen-orchestra/pull/7276))
- [Host/Reboot] Confirmation modal to reboot an updated slave host if the master is not [#7059](https://github.com/vatesfr/xen-orchestra/issues/7059) (PR [#7293](https://github.com/vatesfr/xen-orchestra/pull/7293))
- [Backup/Restore] Show whether the memory was backed up (PR [#7315](https://github.com/vatesfr/xen-orchestra/pull/7315))
- [Plugin/load-balancer] Limit concurrent VM migrations to 2 (configurable) to avoid long paused VMs [#7084](https://github.com/vatesfr/xen-orchestra/issues/7084) (PR [#7297](https://github.com/vatesfr/xen-orchestra/pull/7297))
- [Plugin/load-balancer] A parameter was added in performance mode to balance VMs on hosts depending on their number of vCPU, when it does not cause performance issues. [#5389](https://github.com/vatesfr/xen-orchestra/issues/5389) (PR [#7333](https://github.com/vatesfr/xen-orchestra/pull/7333))
- [Tags] Admin can create colored tags (PR [#7262](https://github.com/vatesfr/xen-orchestra/pull/7262))
### Bug fixes
> Users must be able to say: “I had this issue, happy to know it's fixed”
- [Settings/XO Config] Sort backups from newest to oldest
- [Plugins/audit] Don't log `tag.getAllConfigured` calls
- [Remotes] Correctly clear error when the remote is tested with success
- [Proxies] Fix `this.getObject` is not a function during deployment
- [Settings/Logs] Fix `sr.getAllUnhealthyVdiChainsLength: not enough permissions` error with non-admin users (PR [#7265](https://github.com/vatesfr/xen-orchestra/pull/7265))
- [Settings/Logs] Fix `proxy.getAll: not enough permissions` error with non-admin users (PR [#7249](https://github.com/vatesfr/xen-orchestra/pull/7249))
- [Replication/Health Check] Fix `healthCheckVm.add_tag is not a function` error [Forum#69156](https://xcp-ng.org/forum/post/69156)
- [Plugin/load-balancer] Prevent unwanted migrations to hosts with low free memory (PR [#7288](https://github.com/vatesfr/xen-orchestra/pull/7288))
- Avoid unnecessary `pool.add_to_other_config: Duplicate key` error in XAPI log [Forum#68761](https://xcp-ng.org/forum/post/68761)
- [Jobs] Reset parameters when editing method to avoid invalid parameters on execution [Forum#69299](https://xcp-ng.org/forum/post/69299)
- [Metadata Backup] Fix `ENOENT` error when restoring an _XO Config_ backup [Forum#68999](https://xcp-ng.org/forum/post/68999)
- [REST API] Fix `/backup/log/<id>` which was broken by the `/backups` to `/backup` renaming [Forum#69426](https://xcp-ng.org/forum/post/69426)
- [Backup/Restore] Fix unnecessary pool selector in XO config backup restore modal [Forum#8130](https://xcp-ng.org/forum/topic/8130/xo-configbackup-restore) (PR [#7287](https://github.com/vatesfr/xen-orchestra/pull/7287))
- [File restore] Fix potential race condition in partition mount/unmount (PR [#7312](https://github.com/vatesfr/xen-orchestra/pull/7312))
- [Modal] Fix opened modal not closing when navigating to another route/URL (PR [#7301](https://github.com/vatesfr/xen-orchestra/pull/7301))
- [Backup/Restore] Don't count memory as a key (i.e. complete) disk [Forum#8212](https://xcp-ng.org/forum/post/69591) (PR [#7315](https://github.com/vatesfr/xen-orchestra/pull/7315))
- [Pool/patches] Disable Rolling Pool Update button if host is alone in its pool [#6415](https://github.com/vatesfr/xen-orchestra/issues/6415) (PR [#7286](https://github.com/vatesfr/xen-orchestra/pull/7286))
- [PIF] Fix IPv4 reconfiguration only worked when the IPv4 mode was updated (PR [#7324](https://github.com/vatesfr/xen-orchestra/pull/7324))
### Packages to release
@@ -34,12 +56,14 @@
<!--packages-start-->
- @vates/decorate-with minor
- @vates/fuse-vhd patch
- @xen-orchestra/backups patch
- @xen-orchestra/fs patch
- @xen-orchestra/xapi patch
- vhd-lib patch
- @xen-orchestra/self-signed minor
- @xen-orchestra/xapi minor
- xen-api patch
- xo-server minor
- xo-server-audit patch
- xo-web patch
- xo-server-load-balancer minor
- xo-web minor
<!--packages-end-->

View File

@@ -1,127 +0,0 @@
# Contributing to Xen Orchestra
First off, thanks for taking the time to contribute! ❤️
All types of contributions are encouraged and valued. See the [Table of Contents](#table-of-contents) for different ways to help and details about how this project handles them. Please make sure to read the relevant section before making your contribution. It will make it a lot easier for us maintainers and smooth out the experience for all involved. The community looks forward to your contributions. 🎉
> And if you like the project, but just don't have time to contribute, that's fine. There are other easy ways to support the project and show your appreciation, which we would also be very happy about:
>
> - Star the project
> - Tweet about it
> - Refer this project in your project's readme
> - Mention the project at local meetups and tell your friends/colleagues
## Table of Contents
- [Code of Conduct](#code-of-conduct)
- [I Have a Question](#i-have-a-question)
- [I Want To Contribute](#i-want-to-contribute)
- [Reporting Bugs](#reporting-bugs)
- [Suggesting Enhancements](#suggesting-enhancements)
- [Your First Code Contribution](#your-first-code-contribution)
- [Improving The Documentation](#improving-the-documentation)
- [Styleguides](#styleguides)
- [Commit Messages](#commit-messages)
- [Join The Project Team](#join-the-project-team)
## Code of Conduct
This project and everyone participating in it is governed by the
[Xen Orchestra Code of Conduct](https://github.com/vatesfr/xen-orchestra/blob/master/CODE_OF_CONDUCT.md).
By participating, you are expected to uphold this code. Please report unacceptable behavior
to julien.fontanet@vates.fr.
## I Have a Question
> If you want to ask a question, we assume that you have read the available [Documentation](https://xen-orchestra.com/docs/).
Before you ask a question, it is best to search for existing [topics on our forum](https://xcp-ng.org/forum/category/12/xen-orchestra) that might help you. In case you have found a suitable topic and still need clarification, you can write your question in this thread. It is also advisable to search the internet for answers first.
If you then still feel the need to ask a question and need clarification, we recommend the following:
- Open a new Topic.
- Provide as much context as you can about what you're running into.
- Provide project and platform versions (nodejs, npm, etc), depending on what seems relevant.
The community will then answer you as soon as possible.
## I Want To Contribute
> ### Legal Notice
>
> When contributing to this project, you must agree that you have authored 100% of the content, that you have the necessary rights to the content and that the content you contribute may be provided under the project license.
### Reporting Bugs
#### Before Submitting a Bug Report
A good bug report shouldn't leave others needing to chase you up for more information. Therefore, we ask you to investigate carefully, collect information and describe the issue in detail in your report. Please complete the following steps in advance to help us fix any potential bug as fast as possible.
- Make sure that you are using the latest version.
- Determine if your bug is really a bug and not an error on your side e.g. using incompatible environment components/versions (Make sure that you have read the [documentation](https://xen-orchestra.com/docs/). If you are looking for support, you might want to check [this section](#i-have-a-question)).
- To see if other users have experienced (and potentially already solved) the same issue you are having, check if there is not already a bug report existing for your bug or error in the [bug tracker](https://github.com/vatesfr/xen-orchestra/issues?q=label%3Abug).
- Also make sure to search the internet (including Stack Overflow) to see if users outside of the GitHub community have discussed the issue.
- Collect information about the bug:
- Stack trace (Traceback)
- OS, Platform and Version (Windows, Linux, macOS, x86, ARM)
- Version of the interpreter, compiler, SDK, runtime environment, package manager, depending on what seems relevant.
- Possibly your input and the output
- Can you reliably reproduce the issue? And can you also reproduce it with older versions?
#### How Do I Submit a Good Bug Report?
> You must never report security related issues, vulnerabilities or bugs including sensitive information to the issue tracker, or elsewhere in public. Instead sensitive bugs must be reported on [this dedicated page](https://github.com/vatesfr/xen-orchestra/security/advisories/new).
We use GitHub issues to track bugs and errors. If you run into an issue with the project:
- Open an [Issue](https://github.com/vatesfr/xen-orchestra/issues/new/choose). (Since we can't be sure at this point whether it is a bug or not, we ask you not to talk about a bug yet and not to label the issue.)
- Explain the behavior you would expect and the actual behavior.
- Please provide as much context as possible and describe the _reproduction steps_ that someone else can follow to recreate the issue on their own. This usually includes your code. For good bug reports you should isolate the problem and create a reduced test case.
- Provide the information you collected in the previous section.
### Suggesting Enhancements
This section guides you through submitting an enhancement suggestion for Xen Orchestra, **including completely new features and minor improvements to existing functionality**. Following these guidelines will help maintainers and the community to understand your suggestion and find related suggestions.
#### Before Submitting an Enhancement
- Make sure that you are using the latest version.
- Read the [documentation](https://xen-orchestra.com/docs/) carefully and find out if the functionality is already covered, maybe by an individual configuration.
- Perform a [search](https://github.com/vatesfr/xen-orchestra/issues) to see if the enhancement has already been suggested. If it has, add a comment to the existing issue instead of opening a new one.
- Find out whether your idea fits with the scope and aims of the project. It's up to you to make a strong case to convince the project's developers of the merits of this feature. Keep in mind that we want features that will be useful to the majority of our users and not just a small subset. If you're just targeting a minority of users, consider writing an add-on/plugin library.
#### How Do I Submit a Good Enhancement Suggestion?
Enhancement suggestions are tracked as [GitHub issues](https://github.com/vatesfr/xen-orchestra/issues).
- Use a **clear and descriptive title** for the issue to identify the suggestion.
- Provide a **step-by-step description of the suggested enhancement** in as many details as possible.
- **Describe the current behavior** and **explain which behavior you expected to see instead** and why. At this point you can also tell which alternatives do not work for you.
- You may want to **include screenshots and animated GIFs** which help you demonstrate the steps or point out the part which the suggestion is related to. You can use [this tool](https://www.cockos.com/licecap/) to record GIFs on macOS and Windows, and [this tool](https://github.com/colinkeenan/silentcast) or [this tool](https://github.com/GNOME/byzanz) on Linux.
- **Explain why this enhancement would be useful** to most Xen Orchestra users. You may also want to point out the other projects that solved it better and which could serve as inspiration.
### Your First Code Contribution
<!-- TODO
include Setup of env, IDE and typical getting started instructions?
-->
### Improving The Documentation
<!-- TODO
Updating, improving and correcting the documentation
-->
## Styleguides
### Commit Messages
<!-- TODO
-->
## Attribution
This guide is based on the **contributing-gen**. [Make your own](https://github.com/bttger/contributing-gen)!

View File

@@ -34,8 +34,9 @@ But it's not the only way to see this: there is multiple possibilities to "optim
- maybe you want to spread the VM load on the maximum number of server, to get the most of your hardware? (previous example)
- maybe you want to reduce power consumption and migrate your VMs to the minimum number of hosts possible? (and shutdown useless hosts)
- or maybe both, depending of your own schedule?
Those ways can be also called modes: "performance" for 1 and "density" for number 2.
Those ways can be also called modes: "performance" for 1, "density" for number 2 and "mixed" for the last.
## Configure a plan
@@ -46,6 +47,7 @@ A plan has:
- a name
- pool(s) where to apply the policy
- a mode (see paragraph below)
- a behavior (aggressive, normal, low)
### Plan modes
@@ -53,7 +55,7 @@ There are 3 modes possible:
- performance
- density
- simple
- mixed
#### Performance
@@ -63,9 +65,14 @@ VMs are placed to use all possible resources. This means balance the load to giv
This time, the objective is to use the least hosts possible, and to concentrate your VMs. In this mode, you can choose to shutdown unused (and compatible) hosts.
#### Simple
#### Mixed
This mode allows you to use VM anti-affinity without using any load balancing mechanism. (see paragraph below)
This mode allows you to use both performance and density, but alternatively, depending of a schedule. E.g:
- **performance** from 6:00 AM to 7:00 PM
- **density** from 7:01 PM to 5:59 AM
In this case, you'll have the best of both when needed (energy saving during the night and performance during the day).
### Threshold
@@ -80,10 +87,6 @@ If the CPU threshold is set to 90%, the load balancer will be only triggered if
For free memory, it will be triggered if there is **less** free RAM than the threshold.
### Exclusion
If you want to prevent load balancing from triggering migrations on a particular host or VM, it is possible to exclude it from load balancing. It can be configured via the "Excluded hosts" parameter in each plan, and in the "Ignored VM tags" parameter which is common to every plan.
### Timing
The global situation (resource usage) is examined **every minute**.

View File

@@ -279,45 +279,12 @@ The following query parameters are supported to customize the created VDI:
To see the actions available on a given object, get the collection at `/rest/v0/<type>/<uuid>/actions`.
The field `params` contains the [JSON schema](https://json-schema.org/) for the parameters. Use `fields=params` to see it when fetching the collection.
For example, to list all actions on a given VM:
```sh
curl \
-b authenticationToken=KQxQdm2vMiv7jBIK0hgkmgxKzemd8wSJ7ugFGKFkTbs \
'https://xo.example.org/rest/v0/vms/770aa52a-fd42-8faf-f167-8c5c4a237cac/actions?fields=params'
```
Example response:
```json
[
{
"href": "/rest/v0/vms/770aa52a-fd42-8faf-f167-8c5c4a237cac/actions/clean_reboot"
},
{
"href": "/rest/v0/vms/770aa52a-fd42-8faf-f167-8c5c4a237cac/actions/clean_shutdown"
},
{
"href": "/rest/v0/vms/770aa52a-fd42-8faf-f167-8c5c4a237cac/actions/hard_reboot"
},
{
"href": "/rest/v0/vms/770aa52a-fd42-8faf-f167-8c5c4a237cac/actions/hard_shutdown"
},
{
"params": {
"name_label": {
"type": "string",
"optional": true
}
},
"href": "/rest/v0/vms/770aa52a-fd42-8faf-f167-8c5c4a237cac/actions/snapshot"
},
{
"href": "/rest/v0/vms/770aa52a-fd42-8faf-f167-8c5c4a237cac/actions/start"
}
]
'https://xo.example.org/rest/v0/vms/770aa52a-fd42-8faf-f167-8c5c4a237cac/actions'
```
### Start an action

View File

@@ -1,6 +1,4 @@
{
"name": "xen-orchestra",
"version": "0.0.0",
"devDependencies": {
"@babel/core": "^7.0.0",
"@babel/eslint-parser": "^7.13.8",
@@ -96,8 +94,9 @@
},
"private": true,
"scripts": {
"build": "TURBO_TELEMETRY_DISABLED=1 turbo run build --scope xo-server --scope xo-server-'*' --scope xo-web",
"build": "turbo run build --scope xo-server --scope xo-server-'*' --scope xo-web",
"build:xo-lite": "turbo run build --scope @xen-orchestra/lite",
"ci": "yarn && yarn build && yarn test-lint && yarn test-integration",
"clean": "scripts/run-script.js --parallel clean",
"dev": "scripts/run-script.js --parallel --concurrency 0 --verbose dev",
"dev-test": "jest --bail --watch \"^(?!.*\\.integ\\.spec\\.js$)\"",

View File

@@ -23,7 +23,7 @@
"node": ">=10"
},
"dependencies": {
"@xen-orchestra/fs": "^4.1.4",
"@xen-orchestra/fs": "^4.1.3",
"cli-progress": "^3.1.0",
"exec-promise": "^0.7.0",
"getopts": "^2.2.3",

View File

@@ -1,6 +1,6 @@
'use strict'
const { relativeFromFile } = require('@xen-orchestra/fs/path')
const { dirname, relative } = require('path')
const { openVhd } = require('./openVhd')
const { DISK_TYPES } = require('./_constants')
@@ -21,7 +21,7 @@ module.exports = async function chain(parentHandler, parentPath, childHandler, c
}
await childVhd.readBlockAllocationTable()
const parentName = relativeFromFile(childPath, parentPath)
const parentName = relative(dirname(childPath), parentPath)
header.parentUuid = parentVhd.footer.uuid
header.parentUnicodeName = parentName
await childVhd.setUniqueParentLocator(parentName)

View File

@@ -20,7 +20,7 @@
"@vates/read-chunk": "^1.2.0",
"@vates/stream-reader": "^0.1.0",
"@xen-orchestra/async-map": "^0.1.2",
"@xen-orchestra/fs": "^4.1.4",
"@xen-orchestra/fs": "^4.1.3",
"@xen-orchestra/log": "^0.6.0",
"async-iterator-to-stream": "^1.0.2",
"decorator-synchronized": "^0.6.0",
@@ -33,7 +33,7 @@
"uuid": "^9.0.0"
},
"devDependencies": {
"@xen-orchestra/fs": "^4.1.4",
"@xen-orchestra/fs": "^4.1.3",
"execa": "^5.0.0",
"get-stream": "^6.0.0",
"rimraf": "^5.0.1",

View File

@@ -40,7 +40,7 @@
"human-format": "^1.0.0",
"lodash": "^4.17.4",
"pw": "^0.0.4",
"xen-api": "^2.0.1"
"xen-api": "^2.0.0"
},
"scripts": {
"postversion": "npm publish"

View File

@@ -1,7 +1,7 @@
{
"private": false,
"name": "xen-api",
"version": "2.0.1",
"version": "2.0.0",
"license": "ISC",
"description": "Connector to the Xen API",
"keywords": [
@@ -32,7 +32,7 @@
},
"dependencies": {
"@vates/coalesce-calls": "^0.1.0",
"@vates/decorate-with": "^2.1.0",
"@vates/decorate-with": "^2.0.0",
"bind-property-descriptor": "^2.0.0",
"blocked": "^1.2.1",
"debug": "^4.0.1",
@@ -52,7 +52,7 @@
"xo-collection": "^0.5.0"
},
"devDependencies": {
"tap": "^18.7.0"
"tap": "^16.1.0"
},
"scripts": {
"plot": "gnuplot -p memory-test.gnu",

View File

@@ -1,7 +1,7 @@
{
"private": false,
"name": "xo-cli",
"version": "0.26.0",
"version": "0.24.0",
"license": "AGPL-3.0-or-later",
"description": "Basic CLI for Xen-Orchestra",
"keywords": [

View File

@@ -8,7 +8,6 @@ import { readChunk } from '@vates/read-chunk'
import getopts from 'getopts'
import hrp from 'http-request-plus'
import merge from 'lodash/merge.js'
import set from 'lodash/set.js'
import split2 from 'split2'
import * as config from './config.mjs'
@@ -30,10 +29,10 @@ function parseParams(args) {
for (const arg of args) {
const i = arg.indexOf('=')
if (i === -1) {
set(params, arg, '')
params[arg] = ''
} else {
const value = arg.slice(i + 1)
set(params, arg.slice(0, i), value.startsWith('json:') ? JSON.parse(value.slice(5)) : value)
params[arg.slice(0, i)] = value.startsWith('json:') ? JSON.parse(value.slice(5)) : value
}
}
return params
@@ -167,7 +166,7 @@ export async function rest(args) {
return COMMANDS[command].call(
{
async exec(path, { query = {}, ...opts } = {}) {
exec(path, { query = {}, ...opts } = {}) {
const url = new URL(baseUrl)
const i = path.indexOf('?')
@@ -186,17 +185,7 @@ export async function rest(args) {
}
}
try {
return await hrp(url, merge({}, baseOpts, opts))
} catch (error) {
const { response } = error
if (response === undefined) {
throw error
}
console.error(response.statusCode, response.statusMessage)
throw await response.text()
}
return hrp(url, merge({}, baseOpts, opts))
},
json,
},

View File

@@ -8,7 +8,7 @@
"removeComments": true,
"sourceMap": true,
"strictNullChecks": true,
"target": "es2015",
"target": "es2015"
},
"includes": "src/**/*",
"includes": "src/**/*"
}

View File

@@ -72,7 +72,6 @@ const DEFAULT_BLOCKED_LIST = {
'system.getServerTimezone': true,
'system.getServerVersion': true,
'system.getVersion': true,
'tag.getAllConfigured': true,
'test.getPermissionsForUser': true,
'user.getAll': true,
'user.getAuthenticationTokens': true,

View File

@@ -1,6 +1,6 @@
{
"name": "xo-server-load-balancer",
"version": "0.8.1",
"version": "0.7.3",
"license": "AGPL-3.0-or-later",
"description": "Load balancer for XO-Server",
"keywords": [
@@ -24,7 +24,7 @@
"preferGlobal": false,
"main": "dist/",
"engines": {
"node": ">=7"
"node": ">=12.9"
},
"dependencies": {
"@xen-orchestra/cron": "^1.0.6",

Some files were not shown because too many files have changed in this diff Show More