Compare commits
44 Commits
xo5/fix-bu
...
contributi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7420a5d6a3 | ||
|
|
e9a23755b6 | ||
|
|
5712f29a58 | ||
|
|
509ebf900e | ||
|
|
757a8915d9 | ||
|
|
35c660dbf6 | ||
|
|
f23fd69e7e | ||
|
|
39c10a7197 | ||
|
|
7a1bc16468 | ||
|
|
93dd1a63da | ||
|
|
b4e1064914 | ||
|
|
810cdc1a77 | ||
|
|
1023131828 | ||
|
|
e2d83324ac | ||
|
|
7cea445c21 | ||
|
|
b5d9d9a9e1 | ||
|
|
3a4e9b8f8e | ||
|
|
92efd28b33 | ||
|
|
a2c36c0832 | ||
|
|
2eb49cfdf1 | ||
|
|
ba9d4d4bb5 | ||
|
|
18dea2f2fe | ||
|
|
70c51227bf | ||
|
|
e162fd835b | ||
|
|
bcdcfbf20b | ||
|
|
a6e93c895c | ||
|
|
5c4f907358 | ||
|
|
e19dbc06fe | ||
|
|
287378f9c6 | ||
|
|
83a94eefd6 | ||
|
|
92fc19e2e3 | ||
|
|
521d31ac84 | ||
|
|
2b3ccb4b0e | ||
|
|
2498a4f47c | ||
|
|
dd61feeaf3 | ||
|
|
7851f8c196 | ||
|
|
404a764821 | ||
|
|
59cc418973 | ||
|
|
bc00551cb3 | ||
|
|
4d24248ea2 | ||
|
|
5c731fd56e | ||
|
|
79abb97b1f | ||
|
|
3314ba6e08 | ||
|
|
0fe8f8cac3 |
@@ -65,10 +65,11 @@ module.exports = {
|
||||
typescript: true,
|
||||
'eslint-import-resolver-custom-alias': {
|
||||
alias: {
|
||||
'@core': '../web-core/lib',
|
||||
'@': './src',
|
||||
},
|
||||
extensions: ['.ts'],
|
||||
packages: ['@xen-orchestra/lite'],
|
||||
packages: ['@xen-orchestra/lite', '@xen-orchestra/web'],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -160,10 +160,10 @@ export class ImportVmBackup {
|
||||
// update the stream with the negative vhd stream
|
||||
stream = await negativeVhd.stream()
|
||||
vdis[vdiRef].baseVdi = snapshotCandidate
|
||||
} catch (err) {
|
||||
} catch (error) {
|
||||
// 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`, err)
|
||||
warn(`can't use differential restore`, { error })
|
||||
disposableDescendants?.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,8 +143,10 @@ export class IncrementalRemoteWriter extends MixinRemoteWriter(AbstractIncrement
|
||||
|
||||
let metadataContent = await this._isAlreadyTransferred(timestamp)
|
||||
if (metadataContent !== undefined) {
|
||||
// @todo : should skip backup while being vigilant to not stuck the forked stream
|
||||
// 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)
|
||||
|
||||
@@ -113,13 +113,13 @@ export const MixinRemoteWriter = (BaseClass = Object) =>
|
||||
)
|
||||
}
|
||||
|
||||
_isAlreadyTransferred(timestamp) {
|
||||
async _isAlreadyTransferred(timestamp) {
|
||||
const vmUuid = this._vmUuid
|
||||
const adapter = this._adapter
|
||||
const backupDir = getVmBackupDir(vmUuid)
|
||||
try {
|
||||
const actualMetadata = JSON.parse(
|
||||
adapter._handler.readFile(`${backupDir}/${formatFilenameDate(timestamp)}.json`)
|
||||
await adapter._handler.readFile(`${backupDir}/${formatFilenameDate(timestamp)}.json`)
|
||||
)
|
||||
return actualMetadata
|
||||
} catch (error) {}
|
||||
|
||||
@@ -20,5 +20,7 @@ export function split(path) {
|
||||
return parts
|
||||
}
|
||||
|
||||
export const relativeFromFile = (file, path) => relative(dirname(file), path)
|
||||
// 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 resolveFromFile = (file, path) => resolve('/', dirname(file), path).slice(1)
|
||||
|
||||
17
@xen-orchestra/fs/src/path.test.js
Normal file
17
@xen-orchestra/fs/src/path.test.js
Normal file
@@ -0,0 +1,17 @@
|
||||
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')
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -17,7 +17,7 @@
|
||||
"xo-lift-remote-immutability": "./liftProtection.mjs"
|
||||
},
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.1",
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
|
||||
@@ -54,10 +54,10 @@ async function handleExistingFile(root, indexPath, path) {
|
||||
await indexFile(fullPath, indexPath)
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
if (err.code !== 'EEXIST') {
|
||||
} catch (error) {
|
||||
if (error.code !== 'EEXIST') {
|
||||
// there can be a symbolic link in the tree
|
||||
warn('handleExistingFile', err)
|
||||
warn('handleExistingFile', { error })
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -106,7 +106,7 @@ export async function watchRemote(remoteId, { root, immutabilityDuration, rebuil
|
||||
await File.liftImmutability(settingPath)
|
||||
} catch (error) {
|
||||
// file may not exists, and it's not really a problem
|
||||
info('lifting immutability on current settings', error)
|
||||
info('lifting immutability on current settings', { error })
|
||||
}
|
||||
await fs.writeFile(
|
||||
settingPath,
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
|
||||
"include": ["env.d.ts", "src/**/*", "src/**/*.vue", "../web-core/lib/**/*", "../web-core/lib/**/*.vue"],
|
||||
"exclude": ["src/**/__tests__/*"],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"noEmit": true,
|
||||
"baseUrl": ".",
|
||||
"rootDir": "..",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
"@/*": ["./src/*"],
|
||||
"@core/*": ["../web-core/lib/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ export default defineConfig({
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||
'@core': fileURLToPath(new URL('../web-core/lib', import.meta.url)),
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
@@ -27,6 +27,16 @@ 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:
|
||||
|
||||
@@ -45,6 +45,16 @@ 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:
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"vue": "^3.4.13"
|
||||
"vue": "^3.4.13",
|
||||
"@vue/tsconfig": "^0.5.1"
|
||||
},
|
||||
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/web-core",
|
||||
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
|
||||
@@ -25,6 +26,6 @@
|
||||
},
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"engines": {
|
||||
"node": ">=8.10"
|
||||
"node": ">=18"
|
||||
}
|
||||
}
|
||||
|
||||
12
@xen-orchestra/web-core/tsconfig.json
Normal file
12
@xen-orchestra/web-core/tsconfig.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||
"include": ["env.d.ts", "lib/**/*", "lib/**/*.vue"],
|
||||
"exclude": ["lib/**/__tests__/*"],
|
||||
"compilerOptions": {
|
||||
"noEmit": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@core/*": ["./lib/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,22 @@
|
||||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||
"include": ["env.d.ts", "typed-router.d.ts", "src/**/*", "src/**/*.vue"],
|
||||
"include": [
|
||||
"env.d.ts",
|
||||
"typed-router.d.ts",
|
||||
"src/**/*",
|
||||
"src/**/*.vue",
|
||||
"../web-core/lib/**/*",
|
||||
"../web-core/lib/**/*.vue"
|
||||
],
|
||||
"exclude": ["src/**/__tests__/*"],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"noEmit": true,
|
||||
"baseUrl": ".",
|
||||
"rootDir": "..",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
"@/*": ["./src/*"],
|
||||
"@core/*": ["../web-core/lib/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ export default defineConfig({
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||
'@core': fileURLToPath(new URL('../web-core/lib', import.meta.url)),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -21,12 +21,23 @@ 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'),
|
||||
device ?? (await this.call('VM.get_allowed_VIF_devices', VM))[0],
|
||||
MTU ?? (await this.getField('network', network, 'MTU')),
|
||||
MTU ?? this.getField('network', network, 'MTU'),
|
||||
])
|
||||
;[device, MTU] = rest
|
||||
;[MTU] = rest
|
||||
|
||||
const vifRef = await this.call('VIF.create', {
|
||||
currently_attached: powerState === 'Suspended' ? currently_attached : undefined,
|
||||
|
||||
3
@xen-orchestra/xva/_formatBlockPath.mjs
Normal file
3
@xen-orchestra/xva/_formatBlockPath.mjs
Normal file
@@ -0,0 +1,3 @@
|
||||
const formatCounter = counter => String(counter).padStart(8, '0')
|
||||
|
||||
export const formatBlockPath = (basePath, counter) => `${basePath}/${formatCounter(counter)}`
|
||||
@@ -1,30 +1,50 @@
|
||||
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) {
|
||||
await fromCallback.call(pack, pack.entry, { name }, data)
|
||||
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 fromCallback.call(pack, pack.entry, { name: `${name}.xxhash` }, Buffer.from(hash, 'utf8'))
|
||||
await addEntry(pack, `${name}.xxhash`, Buffer.from(hash, 'utf8'))
|
||||
}
|
||||
|
||||
export default async function addDisk(pack, vhd, basePath) {
|
||||
let counter = 0
|
||||
let written
|
||||
const chunk_length = 1024 * 1024
|
||||
const empty = Buffer.alloc(chunk_length, 0)
|
||||
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) {
|
||||
const data = await readChunkStrict(stream, Math.min(chunk_length, remaining))
|
||||
lastBlockLength = data.length
|
||||
lastBlockLength = Math.min(XVA_DISK_CHUNK_LENGTH, remaining)
|
||||
const data = await readChunkStrict(stream, lastBlockLength)
|
||||
remaining -= lastBlockLength
|
||||
|
||||
if (counter === 0 || !data.equals(empty)) {
|
||||
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, `${basePath}/${('' + counter).padStart(8, '0')}`)
|
||||
await writeBlock(pack, data, formatBlockPath(basePath, counter))
|
||||
lastBlockWrittenAt = Date.now()
|
||||
} else {
|
||||
written = false
|
||||
}
|
||||
@@ -32,6 +52,6 @@ export default async function addDisk(pack, vhd, basePath) {
|
||||
}
|
||||
if (!written) {
|
||||
// last block must be present
|
||||
writeBlock(pack, empty.slice(0, lastBlockLength), `${basePath}/${counter}`)
|
||||
await writeBlock(pack, empty, formatBlockPath(basePath, counter - 1))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ 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,
|
||||
@@ -79,11 +80,12 @@ export default async function writeOvaXml(
|
||||
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 opaqref to a ref:
|
||||
snapshot: { ...vdis[index], SR: srObj.id },
|
||||
// overwrite SR from an opaque ref to a ref:
|
||||
snapshot: { ...vdis[index], SR: srObj.id, virtual_size: alignedSize },
|
||||
},
|
||||
{
|
||||
snapshot: {
|
||||
|
||||
@@ -25,8 +25,7 @@ export async function importVm(vm, xapi, sr, network) {
|
||||
const str = await promise
|
||||
const matches = /OpaqueRef:[0-9a-z-]+/.exec(str)
|
||||
if (!matches) {
|
||||
const error = new Error('no opaque ref found')
|
||||
error.haystack = str
|
||||
const error = new Error(`no opaque ref found in ${str}`)
|
||||
throw error
|
||||
}
|
||||
return matches[0]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@xen-orchestra/xva",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.2",
|
||||
"main": "index.js",
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
|
||||
39
CHANGELOG.md
39
CHANGELOG.md
@@ -1,9 +1,46 @@
|
||||
# ChangeLog
|
||||
|
||||
## **5.91.0** (2024-01-31)
|
||||
## **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)
|
||||
|
||||
@@ -7,14 +7,16 @@
|
||||
|
||||
> 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
|
||||
|
||||
### Bug fixes
|
||||
|
||||
> Users must be able to say: “I had this issue, happy to know it's fixed”
|
||||
|
||||
- [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`
|
||||
- [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
|
||||
|
||||
### Packages to release
|
||||
|
||||
@@ -32,8 +34,12 @@
|
||||
|
||||
<!--packages-start-->
|
||||
|
||||
- @xen-orchestra/immutable-backups patch
|
||||
- @xen-orchestra/xva patch
|
||||
- xo-server patch
|
||||
- @xen-orchestra/backups patch
|
||||
- @xen-orchestra/fs patch
|
||||
- @xen-orchestra/xapi patch
|
||||
- vhd-lib patch
|
||||
- xo-server minor
|
||||
- xo-server-audit patch
|
||||
- xo-web patch
|
||||
|
||||
<!--packages-end-->
|
||||
|
||||
127
CONTRIBUTING.md
Normal file
127
CONTRIBUTING.md
Normal file
@@ -0,0 +1,127 @@
|
||||
# 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)!
|
||||
@@ -34,9 +34,8 @@ 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, "density" for number 2 and "mixed" for the last.
|
||||
Those ways can be also called modes: "performance" for 1 and "density" for number 2.
|
||||
|
||||
## Configure a plan
|
||||
|
||||
@@ -47,7 +46,6 @@ A plan has:
|
||||
- a name
|
||||
- pool(s) where to apply the policy
|
||||
- a mode (see paragraph below)
|
||||
- a behavior (aggressive, normal, low)
|
||||
|
||||
### Plan modes
|
||||
|
||||
@@ -55,7 +53,7 @@ There are 3 modes possible:
|
||||
|
||||
- performance
|
||||
- density
|
||||
- mixed
|
||||
- simple
|
||||
|
||||
#### Performance
|
||||
|
||||
@@ -65,14 +63,9 @@ 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.
|
||||
|
||||
#### Mixed
|
||||
#### Simple
|
||||
|
||||
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).
|
||||
This mode allows you to use VM anti-affinity without using any load balancing mechanism. (see paragraph below)
|
||||
|
||||
### Threshold
|
||||
|
||||
@@ -87,6 +80,10 @@ 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**.
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
{
|
||||
"name": "xen-orchestra",
|
||||
"version": "0.0.0",
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.0.0",
|
||||
"@babel/eslint-parser": "^7.13.8",
|
||||
@@ -94,7 +96,7 @@
|
||||
},
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "turbo run build --scope xo-server --scope xo-server-'*' --scope xo-web",
|
||||
"build": "TURBO_TELEMETRY_DISABLED=1 turbo run build --scope xo-server --scope xo-server-'*' --scope xo-web",
|
||||
"build:xo-lite": "turbo run build --scope @xen-orchestra/lite",
|
||||
"clean": "scripts/run-script.js --parallel clean",
|
||||
"dev": "scripts/run-script.js --parallel --concurrency 0 --verbose dev",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use strict'
|
||||
|
||||
const { dirname, relative } = require('path')
|
||||
const { relativeFromFile } = require('@xen-orchestra/fs/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 = relative(dirname(childPath), parentPath)
|
||||
const parentName = relativeFromFile(childPath, parentPath)
|
||||
header.parentUuid = parentVhd.footer.uuid
|
||||
header.parentUnicodeName = parentName
|
||||
await childVhd.setUniqueParentLocator(parentName)
|
||||
|
||||
@@ -72,6 +72,7 @@ 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,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "xo-server-load-balancer",
|
||||
"version": "0.8.0",
|
||||
"version": "0.8.1",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"description": "Load balancer for XO-Server",
|
||||
"keywords": [
|
||||
|
||||
@@ -12,6 +12,8 @@ import { EXECUTION_DELAY, debug } from './utils'
|
||||
|
||||
const PERFORMANCE_MODE = 0
|
||||
const DENSITY_MODE = 1
|
||||
const SIMPLE_MODE = 2
|
||||
const MODES = { 'Performance mode': PERFORMANCE_MODE, 'Density mode': DENSITY_MODE, 'Simple mode': SIMPLE_MODE }
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@@ -35,7 +37,7 @@ export const configurationSchema = {
|
||||
},
|
||||
|
||||
mode: {
|
||||
enum: ['Performance mode', 'Density mode', 'Simple mode'],
|
||||
enum: Object.keys(MODES),
|
||||
title: 'Mode',
|
||||
},
|
||||
|
||||
@@ -147,7 +149,7 @@ class LoadBalancerPlugin {
|
||||
|
||||
if (plans) {
|
||||
for (const plan of plans) {
|
||||
this._addPlan(plan.mode === 'Performance mode' ? PERFORMANCE_MODE : DENSITY_MODE, plan)
|
||||
this._addPlan(MODES[plan.mode], plan)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,7 +178,7 @@ export default class PerformancePlan extends Plan {
|
||||
const state = this._getThresholdState(exceededAverages)
|
||||
if (
|
||||
destinationAverages.cpu + vmAverages.cpu >= this._thresholds.cpu.low ||
|
||||
destinationAverages.memoryFree - vmAverages.memory <= this._thresholds.memory.high ||
|
||||
destinationAverages.memoryFree - vmAverages.memory <= this._thresholds.memoryFree.high ||
|
||||
(!state.cpu &&
|
||||
!state.memory &&
|
||||
(exceededAverages.cpu - vmAverages.cpu < destinationAverages.cpu + vmAverages.cpu ||
|
||||
|
||||
@@ -143,6 +143,7 @@ port = 80
|
||||
requestTimeout = 0
|
||||
|
||||
[http.mounts]
|
||||
'/robots.txt' = './robots.txt'
|
||||
'/' = '../xo-web/dist/'
|
||||
'/v6' = '../../@xen-orchestra/web/dist/'
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "xo-server",
|
||||
"version": "5.135.0",
|
||||
"version": "5.136.0",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"description": "Server part of Xen-Orchestra",
|
||||
"keywords": [
|
||||
@@ -53,7 +53,7 @@
|
||||
"@xen-orchestra/template": "^0.1.0",
|
||||
"@xen-orchestra/vmware-explorer": "^0.4.0",
|
||||
"@xen-orchestra/xapi": "^4.2.0",
|
||||
"@xen-orchestra/xva": "^1.0.0",
|
||||
"@xen-orchestra/xva": "^1.0.2",
|
||||
"ajv": "^8.0.3",
|
||||
"app-conf": "^2.3.0",
|
||||
"async-iterator-to-stream": "^1.0.1",
|
||||
|
||||
2
packages/xo-server/robots.txt
Normal file
2
packages/xo-server/robots.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
User-agent: *
|
||||
Disallow: /
|
||||
@@ -27,7 +27,7 @@ async function sendToNagios(app, jobName, vmBackupInfo) {
|
||||
jobName
|
||||
)
|
||||
} catch (error) {
|
||||
warn('sendToNagios:', error)
|
||||
warn('sendToNagios:', { error })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -48,15 +48,18 @@ const getLogs = (db, args) => {
|
||||
|
||||
const deleteLogs = (db, args) =>
|
||||
new Promise(resolve => {
|
||||
let count = 1
|
||||
let nDeleted = 0
|
||||
let nRunning = 1
|
||||
const cb = () => {
|
||||
if (--count === 0) {
|
||||
if (--nRunning === 0) {
|
||||
console.log(nDeleted.toLocaleString(), 'deleted entries')
|
||||
resolve()
|
||||
}
|
||||
}
|
||||
|
||||
const deleteEntry = key => {
|
||||
++count
|
||||
++nDeleted
|
||||
++nRunning
|
||||
db.del(key, cb)
|
||||
}
|
||||
|
||||
|
||||
@@ -45,7 +45,17 @@ const RRD_POINTS_PER_STEP = {
|
||||
// Utils
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
function convertNanToNull(value) {
|
||||
function parseNumber(value) {
|
||||
// Starting from XAPI 23.31, numbers in the JSON payload are encoded as
|
||||
// strings to support NaN, Infinity and -Infinity
|
||||
if (typeof value === 'string') {
|
||||
const asNumber = +value
|
||||
if (isNaN(asNumber) && value !== 'NaN') {
|
||||
throw new Error('cannot parse number: ' + value)
|
||||
}
|
||||
value = asNumber
|
||||
}
|
||||
|
||||
return isNaN(value) ? null : value
|
||||
}
|
||||
|
||||
@@ -58,7 +68,7 @@ async function getServerTimestamp(xapi, hostRef) {
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
const computeValues = (dataRow, legendIndex, transformValue = identity) =>
|
||||
map(dataRow, ({ values }) => transformValue(convertNanToNull(values[legendIndex])))
|
||||
map(dataRow, ({ values }) => transformValue(parseNumber(values[legendIndex])))
|
||||
|
||||
const combineStats = (stats, path, combineValues) => zipWith(...map(stats, path), (...values) => combineValues(values))
|
||||
|
||||
@@ -245,7 +255,15 @@ export default class XapiStats {
|
||||
start: timestamp,
|
||||
},
|
||||
})
|
||||
.then(response => response.text().then(JSON5.parse))
|
||||
.then(response => response.text())
|
||||
.then(data => {
|
||||
try {
|
||||
// starting from XAPI 23.31, the response is valid JSON
|
||||
return JSON.parse(data)
|
||||
} catch (_) {
|
||||
return JSON5.parse(data)
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
delete this.#hostCache[hostUuid][step]
|
||||
throw err
|
||||
@@ -299,7 +317,7 @@ export default class XapiStats {
|
||||
// To avoid crossing over the boundary, we ask for one less step
|
||||
const optimumTimestamp = currentTimeStamp - maxDuration + step
|
||||
const json = await this._getJson(xapi, host, optimumTimestamp, step)
|
||||
const actualStep = json.meta.step
|
||||
const actualStep = parseNumber(json.meta.step)
|
||||
|
||||
if (actualStep !== step) {
|
||||
throw new FaultyGranularity(`Unable to get the true granularity: ${actualStep}`)
|
||||
@@ -326,9 +344,10 @@ export default class XapiStats {
|
||||
return
|
||||
}
|
||||
|
||||
if (stepStats === undefined || stepStats.endTimestamp !== json.meta.end) {
|
||||
const endTimestamp = parseNumber(json.meta.end)
|
||||
if (stepStats === undefined || stepStats.endTimestamp !== endTimestamp) {
|
||||
stepStats = {
|
||||
endTimestamp: json.meta.end,
|
||||
endTimestamp,
|
||||
interval: actualStep,
|
||||
stats: {},
|
||||
}
|
||||
|
||||
@@ -410,7 +410,9 @@ export default class Api {
|
||||
// 2021-02-11: Work-around: ECONNREFUSED error can be triggered by
|
||||
// 'host.stats' method because there is no connection to the host during a
|
||||
// toolstack restart and xo-web may call it often
|
||||
if (name !== 'pool.listMissingPatches' && name !== 'host.stats') {
|
||||
// 2024-02-05: Work-around: in case of XO Proxy errors, `proxy.getApplianceUpdaterState` will
|
||||
// flood the logs.
|
||||
if (name !== 'pool.listMissingPatches' && name !== 'host.stats' && name !== 'proxy.getApplianceUpdaterState') {
|
||||
this._logger.error(message, {
|
||||
...data,
|
||||
duration: Date.now() - startTime,
|
||||
|
||||
@@ -249,7 +249,7 @@ export default class MigrateVm {
|
||||
const disk = chainByNode[diskIndex]
|
||||
const { capacity, descriptionLabel, fileName, nameLabel, path, datastore, isFull } = disk
|
||||
if (isFull) {
|
||||
vhd = await VhdEsxiRaw.open(esxi, datastore, path + '/' + fileName)
|
||||
vhd = await VhdEsxiRaw.open(esxi, datastore, path + '/' + fileName, { thin: false })
|
||||
// we don't need to read the BAT with the importVdiThroughXva process
|
||||
const vdiMetadata = {
|
||||
name_description: 'fromESXI' + descriptionLabel,
|
||||
@@ -258,11 +258,14 @@ export default class MigrateVm {
|
||||
virtual_size: capacity,
|
||||
}
|
||||
vdi = await importVdiThroughXva(vdiMetadata, vhd, xapi, sr)
|
||||
|
||||
// it can fail before the vdi is connected to the vm
|
||||
$defer.onFailure.call(xapi, 'VDI_destroy', vdi.$ref)
|
||||
await xapi.VBD_create({
|
||||
VDI: vdi.$ref,
|
||||
VM: vm.$ref,
|
||||
device: `xvd${String.fromCharCode('a'.charCodeAt(0) + userdevice)}`,
|
||||
userdevice: String(userdevice < 3 ? userdevice : userdevice + 1),
|
||||
})
|
||||
} else {
|
||||
vhd = await openDeltaVmdkasVhd(esxi, datastore, path + '/' + fileName, parentVhd, {
|
||||
@@ -312,6 +315,8 @@ export default class MigrateVm {
|
||||
await xapi.VBD_create({
|
||||
VDI: vdi.$ref,
|
||||
VM: vm.$ref,
|
||||
device: `xvd${String.fromCharCode('a'.charCodeAt(0) + userdevice)}`,
|
||||
userdevice: String(userdevice < 3 ? userdevice : userdevice + 1),
|
||||
})
|
||||
} else {
|
||||
if (parentVhd === undefined) {
|
||||
|
||||
@@ -100,6 +100,17 @@ async function sendObjects(iterable, req, res, path = req.path) {
|
||||
return pipeline(makeObjectsStream(iterable, makeResult, json, res), res)
|
||||
}
|
||||
|
||||
function handleArray(array, filter, limit) {
|
||||
if (filter !== undefined) {
|
||||
array = array.filter(filter)
|
||||
}
|
||||
if (limit < array.length) {
|
||||
array.length = limit
|
||||
}
|
||||
|
||||
return array
|
||||
}
|
||||
|
||||
const handleOptionalUserFilter = filter => filter && CM.parse(filter).createPredicate()
|
||||
|
||||
const subRouter = (app, path) => {
|
||||
@@ -160,77 +171,7 @@ export default class RestApi {
|
||||
)
|
||||
})
|
||||
|
||||
const types = [
|
||||
'host',
|
||||
'network',
|
||||
'pool',
|
||||
'SR',
|
||||
'VBD',
|
||||
'VDI-snapshot',
|
||||
'VDI',
|
||||
'VIF',
|
||||
'VM-snapshot',
|
||||
'VM-template',
|
||||
'VM',
|
||||
]
|
||||
const collections = Object.fromEntries(
|
||||
types.map(type => {
|
||||
const id = type.toLocaleLowerCase() + 's'
|
||||
return [id, { id, isCorrectType: _ => _.type === type, type }]
|
||||
})
|
||||
)
|
||||
|
||||
collections.backup = { id: 'backup' }
|
||||
collections.restore = { id: 'restore' }
|
||||
collections.tasks = { id: 'tasks' }
|
||||
collections.users = { id: 'users' }
|
||||
|
||||
collections.hosts.routes = {
|
||||
__proto__: null,
|
||||
|
||||
async 'audit.txt'(req, res) {
|
||||
const host = req.xapiObject
|
||||
|
||||
res.setHeader('content-type', 'text/plain')
|
||||
await pipeline(await host.$xapi.getResource('/audit_log', { host }), compressMaybe(req, res))
|
||||
},
|
||||
|
||||
async 'logs.tar'(req, res) {
|
||||
const host = req.xapiObject
|
||||
|
||||
res.setHeader('content-type', 'application/x-tar')
|
||||
await pipeline(await host.$xapi.getResource('/host_logs_download', { host }), compressMaybe(req, res))
|
||||
},
|
||||
|
||||
async missing_patches(req, res) {
|
||||
await app.checkFeatureAuthorization('LIST_MISSING_PATCHES')
|
||||
|
||||
const host = req.xapiObject
|
||||
res.json(await host.$xapi.listMissingPatches(host))
|
||||
},
|
||||
}
|
||||
|
||||
collections.pools.routes = {
|
||||
__proto__: null,
|
||||
|
||||
async missing_patches(req, res) {
|
||||
await app.checkFeatureAuthorization('LIST_MISSING_PATCHES')
|
||||
|
||||
const xapi = req.xapiObject.$xapi
|
||||
const missingPatches = new Map()
|
||||
await asyncEach(Object.values(xapi.objects.indexes.type.host ?? {}), async host => {
|
||||
try {
|
||||
for (const patch of await xapi.listMissingPatches(host)) {
|
||||
const { uuid: key = `${patch.name}-${patch.version}-${patch.release}` } = patch
|
||||
missingPatches.set(key, patch)
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(host.uuid, error)
|
||||
}
|
||||
})
|
||||
res.json(Array.from(missingPatches.values()))
|
||||
},
|
||||
}
|
||||
const collections = { __proto__: null }
|
||||
|
||||
const withParams = (fn, paramsSchema) => {
|
||||
fn.params = paramsSchema
|
||||
@@ -238,68 +179,231 @@ export default class RestApi {
|
||||
return fn
|
||||
}
|
||||
|
||||
collections.pools.actions = {
|
||||
__proto__: null,
|
||||
{
|
||||
const types = [
|
||||
'host',
|
||||
'message',
|
||||
'network',
|
||||
'pool',
|
||||
'SR',
|
||||
'VBD',
|
||||
'VDI-snapshot',
|
||||
'VDI',
|
||||
'VIF',
|
||||
'VM-snapshot',
|
||||
'VM-template',
|
||||
'VM',
|
||||
]
|
||||
function getObject(id, req) {
|
||||
const { type } = this
|
||||
const object = app.getObject(id, type)
|
||||
|
||||
create_vm: withParams(
|
||||
defer(async ($defer, { xapiObject: { $xapi } }, { affinity, boot, install, template, ...params }, req) => {
|
||||
params.affinityHost = affinity
|
||||
params.installRepository = install?.repository
|
||||
// add also the XAPI version of the object
|
||||
req.xapiObject = app.getXapiObject(object)
|
||||
|
||||
const vm = await $xapi.createVm(template, params, undefined, req.user.id)
|
||||
$defer.onFailure.call($xapi, 'VM_destroy', vm.$ref)
|
||||
return object
|
||||
}
|
||||
function getObjects(filter, limit) {
|
||||
return app.getObjects({
|
||||
filter: every(this.isCorrectType, filter),
|
||||
limit,
|
||||
})
|
||||
}
|
||||
async function messages(req, res) {
|
||||
const {
|
||||
object: { id },
|
||||
query,
|
||||
} = req
|
||||
await sendObjects(
|
||||
app.getObjects({
|
||||
filter: every(_ => _.type === 'message' && _.$object === id, handleOptionalUserFilter(query.filter)),
|
||||
limit: ifDef(query.limit, Number),
|
||||
}),
|
||||
req,
|
||||
res,
|
||||
'/messages'
|
||||
)
|
||||
}
|
||||
for (const type of types) {
|
||||
const id = type.toLocaleLowerCase() + 's'
|
||||
|
||||
if (boot) {
|
||||
await $xapi.callAsync('VM.start', vm.$ref, false, false)
|
||||
}
|
||||
collections[id] = { getObject, getObjects, routes: { messages }, isCorrectType: _ => _.type === type, type }
|
||||
}
|
||||
|
||||
return vm.uuid
|
||||
}),
|
||||
{
|
||||
affinity: { type: 'string', optional: true },
|
||||
auto_poweron: { type: 'boolean', optional: true },
|
||||
boot: { type: 'boolean', default: false },
|
||||
clone: { type: 'boolean', default: true },
|
||||
install: {
|
||||
type: 'object',
|
||||
optional: true,
|
||||
properties: {
|
||||
method: { enum: ['cdrom', 'network'] },
|
||||
repository: { type: 'string' },
|
||||
collections.hosts.routes = {
|
||||
...collections.hosts.routes,
|
||||
|
||||
async 'audit.txt'(req, res) {
|
||||
const host = req.xapiObject
|
||||
|
||||
res.setHeader('content-type', 'text/plain')
|
||||
await pipeline(await host.$xapi.getResource('/audit_log', { host }), compressMaybe(req, res))
|
||||
},
|
||||
|
||||
async 'logs.tar'(req, res) {
|
||||
const host = req.xapiObject
|
||||
|
||||
res.setHeader('content-type', 'application/x-tar')
|
||||
await pipeline(await host.$xapi.getResource('/host_logs_download', { host }), compressMaybe(req, res))
|
||||
},
|
||||
|
||||
async missing_patches(req, res) {
|
||||
await app.checkFeatureAuthorization('LIST_MISSING_PATCHES')
|
||||
|
||||
const host = req.xapiObject
|
||||
res.json(await host.$xapi.listMissingPatches(host))
|
||||
},
|
||||
}
|
||||
|
||||
collections.pools.routes = {
|
||||
...collections.pools.routes,
|
||||
|
||||
async missing_patches(req, res) {
|
||||
await app.checkFeatureAuthorization('LIST_MISSING_PATCHES')
|
||||
|
||||
const xapi = req.xapiObject.$xapi
|
||||
const missingPatches = new Map()
|
||||
await asyncEach(Object.values(xapi.objects.indexes.type.host ?? {}), async host => {
|
||||
try {
|
||||
for (const patch of await xapi.listMissingPatches(host)) {
|
||||
const { uuid: key = `${patch.name}-${patch.version}-${patch.release}` } = patch
|
||||
missingPatches.set(key, patch)
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(host.uuid, error)
|
||||
}
|
||||
})
|
||||
res.json(Array.from(missingPatches.values()))
|
||||
},
|
||||
}
|
||||
|
||||
collections.pools.actions = {
|
||||
create_vm: withParams(
|
||||
defer(async ($defer, { xapiObject: { $xapi } }, { affinity, boot, install, template, ...params }, req) => {
|
||||
params.affinityHost = affinity
|
||||
params.installRepository = install?.repository
|
||||
|
||||
const vm = await $xapi.createVm(template, params, undefined, req.user.id)
|
||||
$defer.onFailure.call($xapi, 'VM_destroy', vm.$ref)
|
||||
|
||||
if (boot) {
|
||||
await $xapi.callAsync('VM.start', vm.$ref, false, false)
|
||||
}
|
||||
|
||||
return vm.uuid
|
||||
}),
|
||||
{
|
||||
affinity: { type: 'string', optional: true },
|
||||
auto_poweron: { type: 'boolean', optional: true },
|
||||
boot: { type: 'boolean', default: false },
|
||||
clone: { type: 'boolean', default: true },
|
||||
install: {
|
||||
type: 'object',
|
||||
optional: true,
|
||||
properties: {
|
||||
method: { enum: ['cdrom', 'network'] },
|
||||
repository: { type: 'string' },
|
||||
},
|
||||
},
|
||||
memory: { type: 'integer', optional: true },
|
||||
name_description: { type: 'string', minLength: 0, optional: true },
|
||||
name_label: { type: 'string' },
|
||||
template: { type: 'string' },
|
||||
}
|
||||
),
|
||||
emergency_shutdown: async ({ xapiObject }) => {
|
||||
await app.checkFeatureAuthorization('POOL_EMERGENCY_SHUTDOWN')
|
||||
|
||||
await xapiObject.$xapi.pool_emergencyShutdown()
|
||||
},
|
||||
rolling_update: async ({ object }) => {
|
||||
await app.checkFeatureAuthorization('ROLLING_POOL_UPDATE')
|
||||
|
||||
await app.rollingPoolUpdate(object)
|
||||
},
|
||||
}
|
||||
collections.vms.actions = {
|
||||
clean_reboot: ({ xapiObject: vm }) => vm.$callAsync('clean_reboot').then(noop),
|
||||
clean_shutdown: ({ xapiObject: vm }) => vm.$callAsync('clean_shutdown').then(noop),
|
||||
hard_reboot: ({ xapiObject: vm }) => vm.$callAsync('hard_reboot').then(noop),
|
||||
hard_shutdown: ({ xapiObject: vm }) => vm.$callAsync('hard_shutdown').then(noop),
|
||||
snapshot: withParams(
|
||||
async ({ xapiObject: vm }, { name_label }) => {
|
||||
const ref = await vm.$snapshot({ name_label })
|
||||
return vm.$xapi.getField('VM', ref, 'uuid')
|
||||
},
|
||||
memory: { type: 'integer', optional: true },
|
||||
name_description: { type: 'string', minLength: 0, optional: true },
|
||||
name_label: { type: 'string' },
|
||||
template: { type: 'string' },
|
||||
}
|
||||
),
|
||||
emergency_shutdown: async ({ xapiObject }) => {
|
||||
await app.checkFeatureAuthorization('POOL_EMERGENCY_SHUTDOWN')
|
||||
{ name_label: { type: 'string', optional: true } }
|
||||
),
|
||||
start: ({ xapiObject: vm }) => vm.$callAsync('start', false, false).then(noop),
|
||||
}
|
||||
}
|
||||
|
||||
await xapiObject.$xapi.pool_emergencyShutdown()
|
||||
collections.backup = {}
|
||||
collections.groups = {
|
||||
getObject(id) {
|
||||
return app.getGroup(id)
|
||||
},
|
||||
rolling_update: async ({ xoObject }) => {
|
||||
await app.checkFeatureAuthorization('ROLLING_POOL_UPDATE')
|
||||
|
||||
await app.rollingPoolUpdate(xoObject)
|
||||
async getObjects(filter, limit) {
|
||||
return handleArray(await app.getAllGroups(), filter, limit)
|
||||
},
|
||||
routes: {
|
||||
async users(req, res) {
|
||||
const { filter, limit } = req.query
|
||||
await sendObjects(
|
||||
handleArray(
|
||||
await Promise.all(req.object.users.map(id => app.getUser(id).then(getUserPublicProperties))),
|
||||
handleOptionalUserFilter(filter),
|
||||
ifDef(limit, Number)
|
||||
),
|
||||
req,
|
||||
res,
|
||||
'/users'
|
||||
)
|
||||
},
|
||||
},
|
||||
}
|
||||
collections.vms.actions = {
|
||||
__proto__: null,
|
||||
|
||||
clean_reboot: ({ xapiObject: vm }) => vm.$callAsync('clean_reboot').then(noop),
|
||||
clean_shutdown: ({ xapiObject: vm }) => vm.$callAsync('clean_shutdown').then(noop),
|
||||
hard_reboot: ({ xapiObject: vm }) => vm.$callAsync('hard_reboot').then(noop),
|
||||
hard_shutdown: ({ xapiObject: vm }) => vm.$callAsync('hard_shutdown').then(noop),
|
||||
snapshot: withParams(
|
||||
async ({ xapiObject: vm }, { name_label }) => {
|
||||
const ref = await vm.$snapshot({ name_label })
|
||||
return vm.$xapi.getField('VM', ref, 'uuid')
|
||||
collections.restore = {}
|
||||
collections.tasks = {}
|
||||
collections.users = {
|
||||
getObject(id) {
|
||||
return app.getUser(id).then(getUserPublicProperties)
|
||||
},
|
||||
async getObjects(filter, limit) {
|
||||
return handleArray(await app.getAllUsers(), filter, limit)
|
||||
},
|
||||
routes: {
|
||||
async groups(req, res) {
|
||||
const { filter, limit } = req.query
|
||||
await sendObjects(
|
||||
handleArray(
|
||||
await Promise.all(req.object.groups.map(id => app.getGroup(id))),
|
||||
handleOptionalUserFilter(filter),
|
||||
ifDef(limit, Number)
|
||||
),
|
||||
req,
|
||||
res,
|
||||
'/groups'
|
||||
)
|
||||
},
|
||||
{ name_label: { type: 'string', optional: true } }
|
||||
),
|
||||
start: ({ xapiObject: vm }) => vm.$callAsync('start', false, false).then(noop),
|
||||
},
|
||||
}
|
||||
|
||||
// normalize collections
|
||||
for (const id of Object.keys(collections)) {
|
||||
const collection = collections[id]
|
||||
|
||||
// inject id into the collection
|
||||
collection.id = id
|
||||
|
||||
// set null as prototypes to speed-up look-ups
|
||||
Object.setPrototypeOf(collection, null)
|
||||
const { actions, routes } = collection
|
||||
if (actions !== undefined) {
|
||||
Object.setPrototypeOf(actions, null)
|
||||
}
|
||||
if (routes !== undefined) {
|
||||
Object.setPrototypeOf(routes, null)
|
||||
}
|
||||
}
|
||||
|
||||
api.param('collection', (req, res, next) => {
|
||||
@@ -312,14 +416,14 @@ export default class RestApi {
|
||||
next()
|
||||
}
|
||||
})
|
||||
api.param('object', (req, res, next) => {
|
||||
api.param('object', async (req, res, next) => {
|
||||
const id = req.params.object
|
||||
const { type } = req.collection
|
||||
try {
|
||||
req.xapiObject = app.getXapiObject((req.xoObject = app.getObject(id, type)))
|
||||
next()
|
||||
// eslint-disable-next-line require-atomic-updates
|
||||
req.object = await req.collection.getObject(id, req)
|
||||
return next()
|
||||
} catch (error) {
|
||||
if (noSuchObject.is(error, { id, type })) {
|
||||
if (noSuchObject.is(error, { id })) {
|
||||
next('route')
|
||||
} else {
|
||||
next(error)
|
||||
@@ -478,39 +582,12 @@ export default class RestApi {
|
||||
}, true)
|
||||
)
|
||||
|
||||
api
|
||||
.get(
|
||||
'/users',
|
||||
wrap(async (req, res) => {
|
||||
let users = await app.getAllUsers()
|
||||
|
||||
const { filter, limit } = req.query
|
||||
if (filter !== undefined) {
|
||||
users = users.filter(CM.parse(filter).createPredicate())
|
||||
}
|
||||
if (limit < users.length) {
|
||||
users.length = limit
|
||||
}
|
||||
|
||||
sendObjects(users.map(getUserPublicProperties), req, res)
|
||||
})
|
||||
)
|
||||
.get(
|
||||
'/users/:id',
|
||||
wrap(async (req, res) => {
|
||||
res.json(getUserPublicProperties(await app.getUser(req.params.id)))
|
||||
})
|
||||
)
|
||||
|
||||
api.get(
|
||||
'/:collection',
|
||||
wrap(async (req, res) => {
|
||||
const { query } = req
|
||||
await sendObjects(
|
||||
await app.getObjects({
|
||||
filter: every(req.collection.isCorrectType, handleOptionalUserFilter(query.filter)),
|
||||
limit: ifDef(query.limit, Number),
|
||||
}),
|
||||
await req.collection.getObjects(handleOptionalUserFilter(query.filter), ifDef(query.limit, Number)),
|
||||
req,
|
||||
res
|
||||
)
|
||||
@@ -563,7 +640,7 @@ export default class RestApi {
|
||||
)
|
||||
|
||||
api.get('/:collection/:object', (req, res) => {
|
||||
let result = req.xoObject
|
||||
let result = req.object
|
||||
|
||||
// add locations of sub-routes for discoverability
|
||||
const { routes } = req.collection
|
||||
@@ -618,7 +695,7 @@ export default class RestApi {
|
||||
'/:collection/:object/tasks',
|
||||
wrap(async (req, res) => {
|
||||
const { query } = req
|
||||
const objectId = req.xoObject.id
|
||||
const objectId = req.object.id
|
||||
const tasks = app.tasks.list({
|
||||
filter: every(
|
||||
_ => _.status === 'pending' && _.properties.objectId === objectId,
|
||||
@@ -658,9 +735,9 @@ export default class RestApi {
|
||||
}
|
||||
}
|
||||
|
||||
const { xapiObject, xoObject } = req
|
||||
const task = app.tasks.create({ name: `REST: ${action} ${req.collection.type}`, objectId: xoObject.id })
|
||||
const pResult = task.run(() => fn({ xapiObject, xoObject }, params, req))
|
||||
const { object, xapiObject } = req
|
||||
const task = app.tasks.create({ name: `REST: ${action} ${req.collection.type}`, objectId: object.id })
|
||||
const pResult = task.run(() => fn({ object, xapiObject }, params, req))
|
||||
if (Object.hasOwn(req.query, 'sync')) {
|
||||
pResult.then(result => res.json(result), next)
|
||||
} else {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "xo-web",
|
||||
"version": "5.136.0",
|
||||
"version": "5.136.1",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"description": "Web interface client for Xen-Orchestra",
|
||||
"keywords": [
|
||||
|
||||
@@ -75,7 +75,7 @@ export const reportOnSupportPanel = async ({ files = [], formatMessage = identit
|
||||
ADDITIONAL_FILES.map(({ fetch, name }) =>
|
||||
timeout.call(fetch(), ADDITIONAL_FILES_FETCH_TIMEOUT).then(
|
||||
file => formData.append('attachments', createBlobFromString(file), name),
|
||||
error => logger.warn(`cannot get ${name}`, error)
|
||||
error => logger.warn(`cannot get ${name}`, { error })
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1099,7 +1099,9 @@ export const SelectXoCloudConfig = makeSubscriptionSelect(
|
||||
subscriber =>
|
||||
subscribeCloudXoConfigBackups(configs => {
|
||||
const xoObjects = groupBy(
|
||||
map(configs, config => ({ ...config, type: 'xoConfig' })),
|
||||
map(configs, config => ({ ...config, type: 'xoConfig' }))
|
||||
// from newest to oldest
|
||||
.sort((a, b) => b.createdAt - a.createdAt),
|
||||
'xoaId'
|
||||
)
|
||||
subscriber({
|
||||
|
||||
@@ -752,6 +752,11 @@ class SortedTable extends Component {
|
||||
const { location, stateUrlParam } = this.props
|
||||
this.setState({ itemsPerPage })
|
||||
cookies.set(`${location.pathname}-${stateUrlParam}`, itemsPerPage)
|
||||
|
||||
// changing the number of items per page should send back to the first page
|
||||
//
|
||||
// see https://github.com/vatesfr/xen-orchestra/issues/7350
|
||||
this._setPage(1)
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
@@ -555,6 +555,11 @@ export default class Home extends Component {
|
||||
_setNItemsPerPage(nItems) {
|
||||
this.setState({ homeItemsPerPage: nItems })
|
||||
cookies.set('homeItemsPerPage', nItems)
|
||||
|
||||
// changing the number of items per page should send back to the first page
|
||||
//
|
||||
// see https://github.com/vatesfr/xen-orchestra/issues/7350
|
||||
this._onPageSelection(1)
|
||||
}
|
||||
|
||||
_getPage() {
|
||||
|
||||
@@ -5,10 +5,9 @@ import decorate from 'apply-decorators'
|
||||
import Icon from 'icon'
|
||||
import React from 'react'
|
||||
import { confirm } from 'modal'
|
||||
import { getApiApplianceInfo, subscribeCloudXoConfig, subscribeCloudXoConfigBackups } from 'xo'
|
||||
import { groupBy, sortBy } from 'lodash'
|
||||
import { injectState, provideState } from 'reaclette'
|
||||
import { SelectXoCloudConfig } from 'select-objects'
|
||||
import { subscribeCloudXoConfig, subscribeCloudXoConfigBackups } from 'xo'
|
||||
|
||||
import BackupXoConfigModal from './backup-xo-config-modal'
|
||||
import RestoreXoConfigModal from './restore-xo-config-modal'
|
||||
@@ -88,15 +87,7 @@ const CloudConfig = decorate([
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
applianceId: async () => {
|
||||
const { id } = await getApiApplianceInfo()
|
||||
return id
|
||||
},
|
||||
groupedConfigs: ({ applianceId, sortedConfigs }) =>
|
||||
sortBy(groupBy(sortedConfigs, 'xoaId'), config => (config[0].xoaId === applianceId ? -1 : 1)),
|
||||
isConfigDefined: ({ config }) => config != null,
|
||||
sortedConfigs: (_, { cloudXoConfigBackups }) =>
|
||||
cloudXoConfigBackups?.sort((config, nextConfig) => config.createdAt - nextConfig.createdAt),
|
||||
},
|
||||
}),
|
||||
injectState,
|
||||
|
||||
@@ -33,7 +33,7 @@ const formatError = error => (typeof error === 'string' ? error : JSON.stringify
|
||||
|
||||
const _changeUrlElement = (value, { remote, element }) =>
|
||||
editRemote(remote, {
|
||||
url: format({ ...remote, [element]: value === null ? undefined : value }),
|
||||
url: format({ ...parse(remote.url), [element]: value === null ? undefined : value }),
|
||||
})
|
||||
const _showError = remote => alert(_('remoteConnectionFailed'), <pre>{formatError(remote.error)}</pre>)
|
||||
const _editRemoteName = (name, { remote }) => editRemote(remote, { name })
|
||||
|
||||
Reference in New Issue
Block a user