Compare commits

..

4 Commits

Author SHA1 Message Date
Pierre Brunet
8d56fae768 feat: add integ test 2026-09-04 11:25:58 +02:00
Pierre Brunet
2ba2ee4f50 chore: update log 2026-09-04 11:25:58 +02:00
Pierre Brunet
855c9fdf34 chore: update log 2026-09-04 11:25:58 +02:00
Pierre Brunet
846a387fcc fix(@xen-orchestra/proxy): give getState and upgrade their own updater connection 2026-09-04 11:25:57 +02:00
166 changed files with 1713 additions and 3075 deletions

View File

@@ -1,49 +0,0 @@
import { RemoteHandlerAbstract } from '@xen-orchestra/fs'
import { promisify } from 'node:util'
import zlib from 'node:zlib'
const gzip = promisify(zlib.gzip)
const gunzip = promisify(zlib.gunzip)
type LogWarn = (message: any, opts?: object) => void
/**
* Reads a gzip-compressed JSON cache file, tolerant of a missing or corrupt file.
* Shared with the legacy @xen-orchestra/backups RemoteAdapter.
*/
export async function readBackupCache(
handler: RemoteHandlerAbstract,
path: string,
logWarn: LogWarn
): Promise<Record<string, unknown> | undefined> {
try {
return JSON.parse((await gunzip(await handler.readFile(path))).toString())
} catch (error) {
if (error?.code !== 'ENOENT') {
logWarn('failed to read cache', { error, path })
}
}
}
/**
* Writes data as a gzip-compressed JSON cache file.
* Shared with the legacy @xen-orchestra/backups RemoteAdapter.
*
* No-op on immutable remotes: the cache file may not be modifiable there,
* and callers should be regenerating/reading the data directly instead.
*/
export async function writeBackupCache(
handler: RemoteHandlerAbstract,
path: string,
data: Record<string, unknown>,
logWarn: LogWarn
): Promise<void> {
if (handler.isImmutable()) {
return
}
try {
await handler.writeFile(path, await gzip(JSON.stringify(data)), { flags: 'w' })
} catch (error) {
logWarn('failed to write cache', { error, path })
}
}

View File

@@ -1,16 +0,0 @@
import { RemoteHandlerAbstract } from '@xen-orchestra/fs'
const METADATA_BACKUP_ID_REGEXP =
/^xo-(config|pool-metadata)-backups\/\w{8}(-\w{4}){3}-\w{12}(\/\w{8}(-\w{4}){3}-\w{12})?\/\d{8}T\d{6}Z/
/**
* Deletes an XO config or pool metadata backup directory (not a VM backup).
* Shared with the legacy @xen-orchestra/backups RemoteAdapter.
*/
export async function deleteMetadataBackup(handler: RemoteHandlerAbstract, backupId: string): Promise<void> {
if (!METADATA_BACKUP_ID_REGEXP.test(backupId)) {
throw new Error(`The id (${backupId}) not correspond to a metadata folder`)
}
await handler.rmtree(backupId)
}

View File

@@ -17,8 +17,11 @@ import {
import { cleanOrphanDiskDirs } from '@xen-orchestra/backup-archive/disks'
import { asyncEach } from '@vates/async-each'
import { createLogger } from '@xen-orchestra/log'
import { readBackupCache, writeBackupCache } from './BackupCache.mjs'
import { unlinkTolerant } from './_unlinkTolerant.mjs'
import { promisify } from 'node:util'
import zlib from 'node:zlib'
const gzip = promisify(zlib.gzip)
const gunzip = promisify(zlib.gunzip)
const { info: logInfo, warn: logWarn } = createLogger('xo:backup-archive')
@@ -27,7 +30,7 @@ const FILES_TO_KEEP = ['cache.json.gz', 'vdis']
export class VmBackupDirectory implements VmBackupInterface {
handler: RemoteHandlerAbstract
rootPath: string
files: Array<string> = []
files: Array<string> = new Array()
orphans: Set<string> = new Set()
backupArchives: Map<string, VmBackupInterface> = new Map()
opts: ResolvedBackupCleanOptions
@@ -40,9 +43,6 @@ export class VmBackupDirectory implements VmBackupInterface {
// on disk, so clean() must regenerate it even when nothing was merged/removed.
#cacheOutOfSync = false
// Set by #checkCacheCount(): whether cache.json.gz existed at the start of the run.
#cacheExisted = false
constructor(
handler: RemoteHandlerAbstract,
vmBackupPath: string,
@@ -67,11 +67,21 @@ export class VmBackupDirectory implements VmBackupInterface {
}
async #readCache(path: string): Promise<Record<string, unknown> | undefined> {
return readBackupCache(this.handler, path, logWarn)
try {
return JSON.parse((await gunzip(await this.handler.readFile(path))).toString())
} catch (error) {
if (error?.code !== 'ENOENT') {
logWarn('failed to read cache', { error, path })
}
}
}
async #writeCache(path: string, data: Record<string, unknown>): Promise<void> {
return writeBackupCache(this.handler, path, data, logWarn)
try {
await this.handler.writeFile(path, await gzip(JSON.stringify(data)), { flags: 'w' })
} catch (error) {
logWarn('failed to write cache', { error, path })
}
}
async init() {
@@ -80,7 +90,7 @@ export class VmBackupDirectory implements VmBackupInterface {
this.#uniqueLineages = new Map()
for (const fullPath of this.files.filter(path => path.endsWith('.json'))) {
let metadata: PartialBackupMetadata | undefined
let metadata: PartialBackupMetadata | undefined = undefined
try {
metadata = JSON.parse(await this.handler.readFile(fullPath)) satisfies PartialBackupMetadata
} catch (error) {
@@ -166,28 +176,18 @@ export class VmBackupDirectory implements VmBackupInterface {
// Let each archive clean its own files (e.g. remove metadata for incomplete backups)
// and update metadata with merged sizes if applicable
const goneArchives: string[] = []
await asyncEach(
Array.from(this.backupArchives.entries()),
async ([metadataPath, archive]: [string, VmBackupInterface]) => {
Array.from(this.backupArchives.values()),
async (archive: VmBackupInterface) => {
const { removedFiles } = await archive.clean({ remove, mergedSizes: allMergedSizes })
if (removedFiles.length > 0) {
cacheNeedsRegen = true
}
if (removedFiles.includes(metadataPath)) {
goneArchives.push(metadataPath)
}
},
{ concurrency: 2 }
)
// an archive whose metadata has just been deleted is not a backup anymore: it must not be
// advertised by the regenerated cache
for (const metadataPath of goneArchives) {
this.backupArchives.delete(metadataPath)
}
if (this.#cacheExisted && (allMergedSizes.size > 0 || cacheNeedsRegen || this.#cacheOutOfSync)) {
if (allMergedSizes.size > 0 || cacheNeedsRegen || this.#cacheOutOfSync) {
await this.#regenerateCache()
}
@@ -215,34 +215,8 @@ export class VmBackupDirectory implements VmBackupInterface {
async #checkCacheCount(): Promise<void> {
const cachePath = `${this.rootPath}/cache.json.gz`
this.#cacheExisted = false
this.#cacheOutOfSync = false
if (!this.files.includes(normalize(cachePath))) {
return
}
if (this.handler.isImmutable()) {
//Best effort: some remotes (e.g. S3) may not normalize a permission error to EPERM, so tolerate any error.
try {
await this.handler.unlink(cachePath)
} catch (error) {
this.opts.logWarn('error while deleting leftover backup cache', { path: cachePath, error })
}
return
}
const existingCache = await this.#readCache(cachePath)
if (existingCache === undefined) {
// present but unreadable: drop it, RemoteAdapter will recreate a valid one
this.opts.logWarn('removing unreadable backup cache', { path: cachePath })
await unlinkTolerant(this.handler, cachePath)
return
}
this.#cacheExisted = true
const actual = Object.keys(existingCache).length
const actual = existingCache === undefined ? 0 : Object.keys(existingCache).length
const expected = this.backupArchives.size
this.#cacheOutOfSync = actual !== expected
if (this.#cacheOutOfSync) {

View File

@@ -10,13 +10,8 @@ import { rimraf } from 'rimraf'
// eslint-disable-next-line n/no-missing-import
import { VmBackupDirectory } from '../dist/VmBackupDirectory.mjs'
import tar from 'tar-stream'
import { promisify } from 'node:util'
import zlib from 'node:zlib'
const { beforeEach, afterEach, describe } = test
const gzip = promisify(zlib.gzip)
const gunzip = promisify(zlib.gunzip)
let tempDir, handler, vmBackupDir
const vmUuid = 'test-vm-uuid'
const rootPath = `xo-vm-backups/${vmUuid}`
@@ -129,10 +124,7 @@ describe('VmBackupDirectory with full backups', { concurrency: 1 }, () => {
test('clean() preserves cache.json.gz', async () => {
await createFullBackupMetadata('backup1.json', 'backup1.xva')
await handler.writeFile(
`${rootPath}/cache.json.gz`,
await gzip(JSON.stringify({ [`/${rootPath}/backup1.json`]: {} }))
)
await handler.writeFile(`${rootPath}/cache.json.gz`, 'cache')
await VmBackupDirectory.cleanVm(handler, rootPath)
@@ -141,104 +133,6 @@ describe('VmBackupDirectory with full backups', { concurrency: 1 }, () => {
assert.ok(remainingFiles.includes('cache.json.gz'))
})
test('clean() removes an unreadable cache.json.gz', async () => {
await createFullBackupMetadata('backup1.json', 'backup1.xva')
await handler.writeFile(`${rootPath}/cache.json.gz`, 'not gzipped json')
await VmBackupDirectory.cleanVm(handler, rootPath, { logWarn: () => {} })
const remainingFiles = await handler.list(rootPath)
assert.ok(!remainingFiles.includes('cache.json.gz'), 'an unreadable cache.json.gz should be removed, not kept')
})
test('clean() on an immutable remote never creates cache.json.gz', async () => {
await createFullBackupMetadata('backup1.json', 'backup1.xva')
await handler.writeFile(`${rootPath}/orphan.xva`, 'orphan-content')
handler.isImmutable = () => true
await VmBackupDirectory.cleanVm(handler, rootPath, { remove: true })
const remainingFiles = await handler.list(rootPath)
assert.ok(!remainingFiles.includes('cache.json.gz'), 'cache.json.gz should never be created on an immutable remote')
})
test('clean() removes a leftover cache.json.gz found on an immutable remote', async () => {
await createFullBackupMetadata('backup1.json', 'backup1.xva')
await handler.writeFile(`${rootPath}/cache.json.gz`, await gzip(JSON.stringify({})))
handler.isImmutable = () => true
await VmBackupDirectory.cleanVm(handler, rootPath)
const remainingFiles = await handler.list(rootPath)
assert.ok(!remainingFiles.includes('cache.json.gz'), 'erroneous cache.json.gz should be removed, not rewritten')
})
test('clean() removes an unreadable leftover cache.json.gz found on an immutable remote', async () => {
await createFullBackupMetadata('backup1.json', 'backup1.xva')
await handler.writeFile(`${rootPath}/cache.json.gz`, 'not gzipped json')
handler.isImmutable = () => true
await VmBackupDirectory.cleanVm(handler, rootPath)
const remainingFiles = await handler.list(rootPath)
assert.ok(
!remainingFiles.includes('cache.json.gz'),
'an unreadable cache.json.gz should be removed from an immutable remote'
)
})
test('clean() tolerates an EPERM while removing the cache of an immutable remote', async () => {
await createFullBackupMetadata('backup1.json', 'backup1.xva')
await handler.writeFile(`${rootPath}/cache.json.gz`, await gzip(JSON.stringify({})))
handler.isImmutable = () => true
handler.unlink = () => {
const error = new Error('EPERM: operation not permitted')
error.code = 'EPERM'
throw error
}
await VmBackupDirectory.cleanVm(handler, rootPath)
const remainingFiles = await handler.list(rootPath)
assert.ok(remainingFiles.includes('cache.json.gz'), 'the immutable cache.json.gz could not be removed')
})
test('clean() keeps maintaining a pre-existing cache.json.gz across a remove', async () => {
await createFullBackupMetadata('backup1.json', 'backup1.xva')
await createFullBackupMetadata('backup2.json', 'backup2.xva')
// a cache with the expected number of entries: only the removal can trigger the regeneration
const cachePath = `${rootPath}/cache.json.gz`
await handler.writeFile(
cachePath,
await gzip(
JSON.stringify({
[`/${rootPath}/backup1.json`]: { stale: true },
[`/${rootPath}/backup2.json`]: { stale: true },
})
)
)
// backup2 loses its XVA: its metadata will be removed by clean()
await handler.unlink(`${rootPath}/backup2.xva`)
await VmBackupDirectory.cleanVm(handler, rootPath, { remove: true, logInfo: () => {}, logWarn: () => {} })
const remainingFiles = await handler.list(rootPath)
assert.ok(remainingFiles.includes('backup1.json'))
assert.ok(remainingFiles.includes('backup1.xva'))
assert.ok(!remainingFiles.includes('backup2.json'), 'metadata of the backup with a missing XVA should be removed')
assert.ok(remainingFiles.includes('cache.json.gz'), 'an existing cache.json.gz must not be dropped by a remove')
const cache = JSON.parse((await gunzip(await handler.readFile(cachePath))).toString())
assert.deepEqual(
Object.keys(cache),
[`/${rootPath}/backup1.json`],
'cache.json.gz should have been regenerated with the surviving backup only'
)
assert.equal(cache[`/${rootPath}/backup1.json`].stale, undefined, 'the entry must come from the archive on disk')
})
test('clean() removes an orphan XVA checksum file (no matching metadata or XVA)', async () => {
await handler.writeFile(`${rootPath}/stray.xva.checksum`, 'sha256:deadbeef')

View File

@@ -1,6 +1,6 @@
import { basename, normalize, resolveFromFile } from '@xen-orchestra/fs/path'
import { basename, normalize } from '@xen-orchestra/fs/path'
import assert from 'node:assert'
import { FileDescriptor, RemoteHandlerAbstract } from '@xen-orchestra/fs'
import { FileDescriptor } from '@xen-orchestra/fs'
import {
ArchiveCleanOptions,
CheckResult,
@@ -8,10 +8,8 @@ import {
VmBackupInterface,
PartialBackupMetadata,
ResolvedBackupCleanOptions,
DEFAULT_REMOVE_CONCURRENCY,
} from './VmBackup.types.mjs'
import { asyncEach } from '@vates/async-each'
import { unlinkTolerant } from './_unlinkTolerant.mjs'
import { RemoteHandlerAbstract } from '@xen-orchestra/fs'
const COMPRESSED_MAGIC_NUMBERS: Buffer[] = [
// https://tools.ietf.org/html/rfc1952.html#page-5
@@ -82,29 +80,6 @@ export async function isValidXva(
const noop = (): void => {}
/**
* Deletes a full VM backup's metadata json and, when provided, its xva and checksum.
* `xva` is the path as stored in the metadata, i.e. relative to the metadata file.
* Shared with the legacy @xen-orchestra/backups RemoteAdapter.
*/
export async function deleteFullVmBackups(
handler: RemoteHandlerAbstract,
backups: Array<{ metadataPath: string; xva?: string }>
): Promise<void> {
await asyncEach(
backups,
async ({ metadataPath, xva }) => {
await unlinkTolerant(handler, metadataPath)
if (xva !== undefined) {
const xvaPath = resolveFromFile(metadataPath, xva)
await unlinkTolerant(handler, xvaPath)
await unlinkTolerant(handler, `${xvaPath}.checksum`)
}
},
{ concurrency: DEFAULT_REMOVE_CONCURRENCY, stopOnError: false }
)
}
export class VmFullBackupArchive implements VmBackupInterface {
handler: RemoteHandlerAbstract
rootPath: string
@@ -183,10 +158,12 @@ export class VmFullBackupArchive implements VmBackupInterface {
files: this.getAssociatedFiles({ prefix: false }),
})
} else {
try {
await deleteFullVmBackups(this.handler, [{ metadataPath: this.metadataPath, xva: this.metadata.xva }])
} catch (error) {
this.opts.logWarn(`Issue removing backup files`, { error, metadataPath: this.metadataPath })
for (const file of removedFiles) {
try {
await this.handler.unlink(file)
} catch (error) {
this.opts.logWarn(`Issue removing ${file}`, { error })
}
}
}
}
@@ -196,7 +173,7 @@ export class VmFullBackupArchive implements VmBackupInterface {
}
getAssociatedFiles({ prefix = false }): Array<string> {
const validFiles = [this.metadataPath, this.xvaPath, `${this.xvaPath}.checksum`]
let validFiles = [this.metadataPath, this.xvaPath, `${this.xvaPath}.checksum`]
return prefix ? validFiles : validFiles.map(file => basename(file))
}
}

View File

@@ -6,13 +6,10 @@ import fs from 'fs-extra'
import * as uuid from 'uuid'
import { getHandler } from '@xen-orchestra/fs'
import { pFromCallback } from 'promise-toolbox'
// `dist/` is built by the `test-integration` script, which starts by removing it: these imports
// cannot be resolved when linting a working tree that has not been built yet
/* eslint-disable n/no-missing-import */
// eslint-disable-next-line n/no-missing-import
import { VmBackupDirectory } from '../dist/VmBackupDirectory.mjs'
import { RemoteVhdDisk } from '../dist/disks/RemoteVhdDisk.mjs'
import { MergeRemoteDisk } from '../dist/disks/MergeRemoteDisk.mjs'
/* eslint-enable n/no-missing-import */
import { VHDFOOTER, VHDHEADER } from './tests.fixtures.mjs'
import { VhdFile, Constants, VhdDirectory, VhdAbstract } from 'vhd-lib'
import { dirname, basename } from 'node:path'
@@ -23,7 +20,6 @@ import { rimraf } from 'rimraf'
const { beforeEach, afterEach, describe } = test
const gunzip = promisify(zlib.gunzip)
const gzip = promisify(zlib.gzip)
let tempDir, handler, jobId, vdiId, basePath, relativePath, vdiId2, basePath2, relativePath2
const rootPath = 'xo-vm-backups/VMUUID/'
@@ -209,8 +205,8 @@ test('it remove backup metadata referencing a missing vhd in delta backup', asyn
assert.equal(matched.length, 2) // all vhds (orphan and child ) should have been deleted
assert.ok(
!(await handler.list(rootPath)).includes('cache.json.gz'),
'cache.json.gz should not be created when none existed before the clean'
(await handler.list(rootPath)).includes('cache.json.gz'),
'cache.json.gz should be regenerated after metadata deletion'
)
})
@@ -368,6 +364,7 @@ test('it merges delta of non destroyed chain', async () => {
logged.push(message)
}
await VmBackupDirectory.cleanVm(handler, rootPath, { remove: true, logInfo, logWarn: logInfo })
assert.equal(logged[0], `unexpected number of entries in backup cache`)
logged = []
await VmBackupDirectory.cleanVm(handler, rootPath, { remove: true, merge: true, logInfo, logWarn: () => {} })
@@ -729,10 +726,10 @@ describe('tests multiple combination ', { concurrency: 1 }, () => {
const metadata = JSON.parse(await handler.readFile(`${rootPath}/metadata.json`))
// size should be the size of children + grand children + clean after the merge
assert.deepEqual(metadata.size, 6501888)
// no cache.json.gz existed before this run, so a merge must not create one
// cache.json.gz must be regenerated when metadata changes after a merge
assert.ok(
!(await handler.list(rootPath)).includes('cache.json.gz'),
'cache.json.gz should not be created when none existed before the merge'
(await handler.list(rootPath)).includes('cache.json.gz'),
'cache.json.gz should be regenerated after a merge'
)
// broken vhd, non referenced, abandoned should be deleted ( alias and data)
@@ -836,7 +833,7 @@ test('it preserves VDI directory when handler.list() throws during lineage init'
assert.ok(remaining.includes('snapshot.vhd'), 'VHD must survive when handler.list() throws during lineage init')
})
test('it does not create nor warn about the cache when none existed, even with a count mismatch', async () => {
test('it regenerates the cache when its entry count is out of sync, even with nothing to merge/remove', async () => {
// a referenced disk + its metadata, but no cache.json.gz on disk
await generateVhd(`${basePath}/diskA1.vhd`)
await handler.writeFile(
@@ -845,78 +842,6 @@ test('it does not create nor warn about the cache when none existed, even with a
)
await assert.rejects(handler.readFile(`${rootPath}/cache.json.gz`), { code: 'ENOENT' })
const logged = []
// remove/merge both false: cacheNeedsRegen stays false and no merge happens,
// so only a pre-existing cache's count mismatch could trigger regeneration
await VmBackupDirectory.cleanVm(handler, rootPath, {
remove: false,
merge: false,
logInfo: () => {},
logWarn: message => logged.push(message),
})
assert.ok(!logged.includes('unexpected number of entries in backup cache'), 'no cache to be mismatched, no warning')
await assert.rejects(handler.readFile(`${rootPath}/cache.json.gz`), { code: 'ENOENT' })
})
test('it regenerates a pre-existing cache after a merge', async () => {
await handler.writeFile(
`${rootPath}/metadata.json`,
JSON.stringify({
mode: 'delta',
size: 12000, // a size too small, the merge will fix it
vhds: [`${relativePath}/grandchild.vhd`, `${relativePath}/child.vhd`],
})
)
// one orphan, which is a full vhd, no parent
const orphan = await generateVhd(`${basePath}/orphan.vhd`)
// a child to the orphan, orphan will be merged into it
const child = await generateVhd(`${basePath}/child.vhd`, {
header: {
parentUnicodeName: 'orphan.vhd',
parentUuid: orphan.footer.uuid,
},
blocks: [0, 1],
})
await generateVhd(`${basePath}/grandchild.vhd`, {
header: {
parentUnicodeName: 'child.vhd',
parentUuid: child.footer.uuid,
},
})
// a cache with the expected number of entries: it is not out of sync, only the merge can trigger the regeneration
await handler.writeFile(
`${rootPath}/cache.json.gz`,
await gzip(JSON.stringify({ [`/${rootPath}metadata.json`]: { size: 12000 } }))
)
await VmBackupDirectory.cleanVm(handler, rootPath, {
remove: true,
merge: true,
logInfo: () => {},
logWarn: () => {},
})
const metadata = JSON.parse(await handler.readFile(`${rootPath}/metadata.json`))
// size of child + grandchild after orphan has been merged into child
assert.equal(metadata.size, 4299776, 'metadata.size should be updated after merge')
const cache = JSON.parse((await gunzip(await handler.readFile(`${rootPath}/cache.json.gz`))).toString())
assert.equal(Object.keys(cache).length, 1)
assert.equal(Object.values(cache)[0].size, metadata.size, 'cache entry should carry the post-merge size')
})
test('it regenerates a pre-existing cache when its entry count is out of sync, even with nothing to merge/remove', async () => {
// a referenced disk + its metadata, and a pre-existing cache with the wrong entry count
await generateVhd(`${basePath}/diskA1.vhd`)
await handler.writeFile(
`${rootPath}/metadata.json`,
JSON.stringify({ mode: 'delta', vhds: [`${relativePath}/diskA1.vhd`] })
)
await handler.writeFile(`${rootPath}/cache.json.gz`, await gzip(JSON.stringify({})))
const logged = []
// remove/merge both false: cacheNeedsRegen stays false and no merge happens,
// so only the cache-count mismatch can trigger the regeneration
@@ -932,53 +857,3 @@ test('it regenerates a pre-existing cache when its entry count is out of sync, e
const cache = JSON.parse((await gunzip(await handler.readFile(`${rootPath}/cache.json.gz`))).toString())
assert.equal(Object.keys(cache).length, 1, 'cache should be regenerated to match the archives on disk')
})
test('it regenerates a pre-existing cache after a metadata removal', async () => {
// two backups, each on its own VDI directory, so removing one does not touch the other's disks
await generateVhd(`${basePath}/diskA1.vhd`)
await handler.writeFile(
`${rootPath}/metadata1.json`,
JSON.stringify({ mode: 'delta', vhds: [`${relativePath}/diskA1.vhd`] })
)
// metadata2 references a vhd missing from disk: clean() removes it
await handler.writeFile(
`${rootPath}/metadata2.json`,
JSON.stringify({ mode: 'delta', vhds: [`${relativePath2}/deleted.vhd`] })
)
// a cache with the expected number of entries: only the removal can trigger the regeneration
await handler.writeFile(
`${rootPath}/cache.json.gz`,
await gzip(
JSON.stringify({
[`/${rootPath}metadata1.json`]: { stale: true },
[`/${rootPath}metadata2.json`]: { stale: true },
})
)
)
const logged = []
await VmBackupDirectory.cleanVm(handler, rootPath, {
remove: true,
merge: false,
logInfo: () => {},
logWarn: message => logged.push(message),
})
const rootFiles = await handler.list(rootPath)
assert.ok(rootFiles.includes('metadata1.json'), 'the complete backup must survive')
assert.ok(!rootFiles.includes('metadata2.json'), 'the backup with a missing vhd must be removed')
assert.ok(rootFiles.includes('cache.json.gz'), 'an existing cache.json.gz must not be dropped by a remove')
assert.ok(
!logged.includes('unexpected number of entries in backup cache'),
'the cache matched the archives before the clean: the removal alone must trigger the regeneration'
)
const cache = JSON.parse((await gunzip(await handler.readFile(`${rootPath}/cache.json.gz`))).toString())
assert.deepEqual(
Object.keys(cache),
[`/${rootPath}metadata1.json`],
'the removed backup must be gone from the regenerated cache'
)
assert.equal(cache[`/${rootPath}metadata1.json`].stale, undefined, 'the entry must come from the archive on disk')
})

View File

@@ -5,27 +5,10 @@ import {
ResolvedBackupCleanOptions,
VmBackupInterface,
PartialBackupMetadata,
DEFAULT_REMOVE_CONCURRENCY,
} from './VmBackup.types.mjs'
import { RemoteHandlerAbstract } from '@xen-orchestra/fs'
import { basename, dirname, normalize } from '@xen-orchestra/fs/path'
import { RemoteDiskLineage } from './RemoteDiskLineage.mjs'
import { asyncEach } from '@vates/async-each'
/**
* Deletes a delta VM backup's metadata json. Unused VHDs are detected and removed
* separately by VmBackupDirectory's clean(). Shared with the legacy @xen-orchestra/backups
* RemoteAdapter.
*/
export async function deleteDeltaVmBackups(
handler: RemoteHandlerAbstract,
backups: Array<{ metadataPath: string }>
): Promise<void> {
await asyncEach(backups, ({ metadataPath }) => handler.unlink(metadataPath), {
concurrency: DEFAULT_REMOVE_CONCURRENCY,
stopOnError: false,
})
}
export class VmIncrementalBackupArchive implements VmBackupInterface {
handler: RemoteHandlerAbstract

View File

@@ -1,20 +0,0 @@
import { RemoteHandlerAbstract } from '@xen-orchestra/fs'
/**
* Deletes a file, ignoring the given error codes.
* By default only a missing file is tolerated.
*/
export async function unlinkTolerant(
handler: RemoteHandlerAbstract,
path: string,
ignoredCodes: string[] = ['ENOENT']
): Promise<void> {
try {
await handler.unlink(path)
} catch (error) {
if (!ignoredCodes.includes(error?.code)) {
error.path ??= path
throw error
}
}
}

View File

@@ -1,5 +1 @@
export { VmBackupDirectory } from './VmBackupDirectory.mjs'
export { deleteFullVmBackups } from './VmFullBackupArchive.mjs'
export { deleteDeltaVmBackups } from './VmIncrementalBackupArchive.mjs'
export { deleteMetadataBackup } from './MetadataBackup.mjs'
export { readBackupCache, writeBackupCache } from './BackupCache.mjs'

View File

@@ -14,6 +14,5 @@
"types": ["node"] // Only use types from Node.js and packages that are explicitly imported
},
"include": ["src/**/*"],
// *.integ.mjs deliberately imports from the built dist/ output to test it
"exclude": ["node_modules", "dist", "**/*.integ.mjs"]
"exclude": ["node_modules", "dist"]
}

View File

@@ -6,19 +6,14 @@ import { decorateMethodsWith } from '@vates/decorate-with'
import { dirname, join, resolve } from 'node:path'
import { synchronized } from 'decorator-synchronized'
import Disposable from 'promise-toolbox/Disposable'
import fromCallback from 'promise-toolbox/fromCallback'
import groupBy from 'lodash/groupBy.js'
import pickBy from 'lodash/pickBy.js'
import reduce from 'lodash/reduce.js'
import zlib from 'zlib'
import { BACKUP_DIR } from './_getVmBackupDir.mjs'
import {
VmBackupDirectory,
deleteFullVmBackups as deleteFullVmBackupFiles,
deleteDeltaVmBackups as deleteDeltaVmBackupFiles,
deleteMetadataBackup as deleteMetadataBackupFiles,
readBackupCache,
writeBackupCache,
} from '@xen-orchestra/backup-archive'
import { VmBackupDirectory } from '@xen-orchestra/backup-archive'
import { fileRestoreDecorators, fileRestoreMethods } from './_fileRestore.mjs'
import { formatFilenameDate } from './_filenameDate.mjs'
import { isMetadataFile } from './_backupType.mjs'
@@ -41,6 +36,8 @@ export const compareTimestamp = (a, b) => a.timestamp - b.timestamp
const noop = Function.prototype
const resolveRelativeFromFile = (file, path) => resolve('/', dirname(file), path).slice(1)
const createSafeReaddir = (handler, methodName) => (path, options) =>
handler.list(path, options).catch(error => {
if (error?.code !== 'ENOENT') {
@@ -103,17 +100,24 @@ export class RemoteAdapter {
}
async deleteDeltaVmBackups(backups) {
const handler = this._handler
// this will delete the json, unused VHDs will be detected by `cleanVm`
await deleteDeltaVmBackupFiles(
this._handler,
backups.map(({ _filename }) => ({ metadataPath: _filename }))
)
await asyncMapSettled(backups, ({ _filename }) => handler.unlink(_filename))
await this.#removeVmBackupsFromCache(backups)
}
async deleteMetadataBackup(backupId) {
await deleteMetadataBackupFiles(this._handler, backupId)
const uuidReg = '\\w{8}(-\\w{4}){3}-\\w{12}'
const metadataDirReg = 'xo-(config|pool-metadata)-backups'
const timestampReg = '\\d{8}T\\d{6}Z'
const regexp = new RegExp(`^${metadataDirReg}/${uuidReg}(/${uuidReg})?/${timestampReg}`)
if (!regexp.test(backupId)) {
throw new Error(`The id (${backupId}) not correspond to a metadata folder`)
}
await this._handler.rmtree(backupId)
}
async deleteOldMetadataBackups(dir, retention) {
@@ -125,10 +129,24 @@ export class RemoteAdapter {
}
async deleteFullVmBackups(backups) {
await deleteFullVmBackupFiles(
this._handler,
backups.map(({ _filename, xva }) => ({ metadataPath: _filename, xva }))
const handler = this._handler
await asyncMapSettled(backups, ({ _filename, xva }) =>
Promise.all([
handler.unlink(_filename).catch(error => {
warn('error while removing full vm backup metadata', { error, filename: _filename })
if (error.code !== 'ENOENT') throw error
}),
handler.unlink(resolveRelativeFromFile(_filename, xva)).catch(error => {
warn('error while removing full vm backup file', { error, filename: _filename })
if (error.code !== 'ENOENT') throw error
}),
handler.unlink(resolveRelativeFromFile(_filename, `${xva}.checksum`)).catch(error => {
// checksum can be missing , it's not an issue
if (error.code !== 'ENOENT') throw error
}),
])
)
await this.#removeVmBackupsFromCache(backups)
}
@@ -280,7 +298,13 @@ export class RemoteAdapter {
}
async _readCache(path) {
return readBackupCache(this.handler, path, warn)
try {
return JSON.parse(await fromCallback(zlib.gunzip, await this.handler.readFile(path)))
} catch (error) {
if (error.code !== 'ENOENT') {
warn('#readCache', { error, path })
}
}
}
_updateCache = synchronized.withKey()(this._updateCache)
@@ -300,7 +324,11 @@ export class RemoteAdapter {
}
async _writeCache(path, data) {
return writeBackupCache(this.handler, path, data, warn)
try {
await this.handler.writeFile(path, await fromCallback(zlib.gzip, JSON.stringify(data)), { flags: 'w' })
} catch (error) {
warn('#writeCache', { error, path })
}
}
async #getCacheableDataListVmBackups(dir) {

View File

@@ -230,6 +230,7 @@ test('it merges delta of non destroyed chain', async () => {
logged.push(message)
}
await adapter.cleanVm(rootPath, { remove: true, logInfo, logWarn: logInfo, lock: false })
assert.equal(logged[0], `unexpected number of entries in backup cache`)
logged = []
const result = await adapter.cleanVm(rootPath, { remove: true, merge: true, logInfo, logWarn: () => {}, lock: false })

View File

@@ -6,14 +6,13 @@ export class AggregatedIncrementalXapiWriter extends AbstractAggregatedXapiWrite
/**
*
* @param {Map<string,string>} baseUuidToSrcVdi
* @param {Map<string,string>} contentKeys
*/
async checkBaseVdis(baseUuidToSrcVdi, contentKeys) {
async checkBaseVdis(baseUuidToSrcVdi) {
debug('checkBaseVdis', { baseUuidToSrcVdi })
let selectedCopy = new Map()
for (const writer of this.writers) {
const copy = new Map(baseUuidToSrcVdi)
await writer.checkBaseVdis(copy, contentKeys)
await writer.checkBaseVdis(copy)
if (copy.size > 0) {
debug('checkBaseVdis found a mainwriter candidate ', writer._sr.name_label)
// there can be multiple candidates

View File

@@ -1,9 +1,5 @@
# ChangeLog
## **next**
- [Host] Add possibility to reboot a host (PR [#10250](https://github.com/vatesfr/xen-orchestra/pull/10250))
## **0.25.0** (2026-08-31)
- [Treeview] Add VM tree actions (PR [#10304](https://github.com/vatesfr/xen-orchestra/pull/10304))

View File

@@ -1,39 +1,21 @@
<template>
<UiHeadBar>
<template #icon>
<VtsObjectIcon
size="medium"
type="host"
:state="powerState"
:busy="isChangingState"
:busy-tooltip="currentOperation"
/>
<VtsObjectIcon size="medium" type="host" :state="powerState" />
</template>
{{ host.name_label }}
<template v-if="isMaster" #status>
<VtsIcon v-tooltip="t('master')" name="status:primary-circle" size="medium" />
</template>
<template #actions>
<MenuList placement="bottom-end">
<template #trigger="{ open }">
<UiDropdownButton @click="open($event)">{{ t('action:change-state') }}</UiDropdownButton>
</template>
<HostPowerStateActions :host />
</MenuList>
</template>
</UiHeadBar>
</template>
<script lang="ts" setup>
import type { XenApiHost } from '@/libs/xen-api/xen-api.types.ts'
import HostPowerStateActions from '@/modules/host/components/actions/HostPowerStateActions.vue'
import { useHostUtils } from '@/modules/host/composables/host-utils.composable.ts'
import { useHostMetricsStore } from '@/stores/xen-api/host-metrics.store.ts'
import { usePoolStore } from '@/stores/xen-api/pool.store.ts'
import VtsIcon from '@core/components/icon/VtsIcon.vue'
import MenuList from '@core/components/menu/MenuList.vue'
import VtsObjectIcon from '@core/components/object-icon/VtsObjectIcon.vue'
import UiDropdownButton from '@core/components/ui/dropdown-button/UiDropdownButton.vue'
import UiHeadBar from '@core/components/ui/head-bar/UiHeadBar.vue'
import { vTooltip } from '@core/directives/tooltip.directive.ts'
import { computed } from 'vue'
@@ -51,6 +33,4 @@ const powerState = computed(() => (isHostRunning(host) ? 'running' : 'halted'))
const { isMasterHost } = usePoolStore().subscribe()
const isMaster = computed(() => isMasterHost(host.$ref))
const { isChangingState, currentOperation } = useHostUtils(() => host)
</script>

View File

@@ -111,7 +111,6 @@ function getManagementIcon(pif: XenApiPif) {
}
const { HeadCells, BodyCells } = usePifColumns({
exclude: ['actions'],
body: (pif: XenApiPif) => {
const name = computed(() => getNetworkName(pif.network))
const status = computed(() => getPifStatus(pif))

View File

@@ -13,7 +13,6 @@
>
{{ host.name_label || '(Host)' }}
<template #addons>
<UiLoader v-if="isChangingState" v-tooltip="currentOperation" />
<VtsIcon v-if="isPoolMaster" v-tooltip="t('master')" name="status:primary-circle" size="medium" />
<UiCounter
v-if="isReady"
@@ -33,7 +32,7 @@
@click="open($event)"
/>
</template>
<HostTreeActions :host />
<HostTreeActions :host-opaque-ref="hostOpaqueRef" />
</MenuList>
</template>
</UiTreeItemLabel>
@@ -49,7 +48,6 @@
import InfraVmItems from '@/components/infra/InfraVmItems.vue'
import type { XenApiHost } from '@/libs/xen-api/xen-api.types.ts'
import HostTreeActions from '@/modules/host/components/actions/HostTreeActions.vue'
import { useHostUtils } from '@/modules/host/composables/host-utils.composable.ts'
import { useHostStore } from '@/stores/xen-api/host.store.ts'
import { usePoolStore } from '@/stores/xen-api/pool.store.ts'
import { useVmStore } from '@/stores/xen-api/vm.store.ts'
@@ -59,7 +57,6 @@ import VtsTreeItem from '@core/components/tree/VtsTreeItem.vue'
import VtsTreeList from '@core/components/tree/VtsTreeList.vue'
import UiButtonIcon from '@core/components/ui/button-icon/UiButtonIcon.vue'
import UiCounter from '@core/components/ui/counter/UiCounter.vue'
import UiLoader from '@core/components/ui/loader/UiLoader.vue'
import UiTreeItemLabel from '@core/components/ui/tree-item-label/UiTreeItemLabel.vue'
import { vTooltip } from '@core/directives/tooltip.directive.ts'
import { useToggle } from '@vueuse/shared'
@@ -78,8 +75,6 @@ const host = computed(() => getByOpaqueRef(hostOpaqueRef))
const { pool } = usePoolStore().subscribe()
const isPoolMaster = computed(() => pool.value?.master === hostOpaqueRef)
const { isChangingState, currentOperation } = useHostUtils(host)
const { runningVms, isReady } = useVmStore().subscribe()
const vmCount = computed(() => runningVms.value.filter(vm => vm.resident_on === hostOpaqueRef)?.length ?? 0)

View File

@@ -1,6 +1,8 @@
import type { HOST_OPERATION } from '@/libs/xen-api/xen-api.enums.ts'
import type { XenApiHost } from '@/libs/xen-api/xen-api.types.ts'
import { useXenApiStore } from '@/stores/xen-api.store.ts'
import type { XenApiPatch } from '@/types/xen-api.ts'
import { castArray } from 'lodash-es'
export async function fetchMissingHostPatches(hostRef: XenApiHost['$ref']): Promise<XenApiPatch[]> {
const xenApiStore = useXenApiStore()
@@ -16,3 +18,9 @@ export async function fetchMissingHostPatches(hostRef: XenApiHost['$ref']): Prom
$id: `${rawPatch.name}-${rawPatch.version}`,
}))
}
export const isHostOperationPending = (host: XenApiHost, operations: HOST_OPERATION[] | HOST_OPERATION) => {
const currentOperations = Object.values(host.current_operations)
return castArray(operations).some(operation => currentOperations.includes(operation))
}

View File

@@ -1,30 +0,0 @@
import type XenApi from '@/libs/xen-api/xen-api.ts'
import type { XenApiHost } from '@/libs/xen-api/xen-api.types.ts'
export function createHostOperations(xenApi: XenApi) {
type HostRef = XenApiHost['$ref']
const disable = (hostRef: HostRef) => xenApi.call('host.disable', [hostRef])
const enable = (hostRef: HostRef) => xenApi.call('host.enable', [hostRef])
const evacuate = (hostRef: HostRef) => xenApi.call('host.evacuate', [hostRef])
const reboot = (hostRef: HostRef) => xenApi.call('host.reboot', [hostRef])
return {
cleanReboot: async (hostRef: HostRef) => {
await disable(hostRef)
try {
await evacuate(hostRef)
} catch (error) {
await enable(hostRef)
throw error
}
await reboot(hostRef)
},
}
}

View File

@@ -91,8 +91,6 @@ export enum ON_NORMAL_EXIT {
}
export enum HOST_OPERATION {
EVACUATE = 'evacuate',
REBOOT = 'reboot',
SHUTDOWN = 'shutdown',
}

View File

@@ -1,5 +1,4 @@
import { ipToHostname } from '@/libs/utils.ts'
import { createHostOperations } from '@/libs/xen-api/operations/host-operations.ts'
import { createNetworkOperations } from '@/libs/xen-api/operations/network-operations.ts'
import { createPbdOperations } from '@/libs/xen-api/operations/pbd-operations.ts'
import { createPifOperations } from '@/libs/xen-api/operations/pif-operations.ts'
@@ -297,8 +296,4 @@ export default class XenApi {
get network() {
return createNetworkOperations(this)
}
get host() {
return createHostOperations(this)
}
}

View File

@@ -1,10 +0,0 @@
<template>
<HostRebootButton :host />
</template>
<script lang="ts" setup>
import type { XenApiHost } from '@/libs/xen-api/xen-api.types.ts'
import HostRebootButton from '@/modules/host/components/actions/reboot/HostRebootButton.vue'
defineProps<{ host: XenApiHost }>()
</script>

View File

@@ -1,31 +1,12 @@
<template>
<MenuItem icon="action:change-state" class="change-state">
{{ t('action:change-state') }}
<template #submenu>
<HostPowerStateActions :host />
</template>
</MenuItem>
<VtsDivider type="stretch" />
<HostDownloadButton :host-opaque-ref="host.$ref" />
<HostDownloadButton :host-opaque-ref />
</template>
<script setup lang="ts">
import type { XenApiHost } from '@/libs/xen-api/xen-api.types.ts'
import HostDownloadButton from '@/modules/host/components/actions/download/HostDownloadButton.vue'
import HostPowerStateActions from '@/modules/host/components/actions/HostPowerStateActions.vue'
import VtsDivider from '@core/components/divider/VtsDivider.vue'
import MenuItem from '@core/components/menu/MenuItem.vue'
import { useI18n } from 'vue-i18n'
defineProps<{
host: XenApiHost
hostOpaqueRef: XenApiHost['$ref']
}>()
const { t } = useI18n()
</script>
<style lang="postcss" scoped>
.change-state {
color: var(--color-brand-txt-base);
}
</style>

View File

@@ -1,52 +0,0 @@
<template>
<MenuItem
v-tooltip="!canRebootHost && rebootHostErrorMessage"
:busy="isRebootingHost"
:disabled="!canRebootHost"
icon="action:reboot"
@click="rebootHost()"
>
{{ t('action:reboot') }}
</MenuItem>
</template>
<script lang="ts" setup>
import type { XenApiHost } from '@/libs/xen-api/xen-api.types.ts'
import { useHostRebootJob } from '@/modules/host/jobs/host-reboot.job.ts'
import MenuItem from '@core/components/menu/MenuItem.vue'
import { useActionModal } from '@core/composables/modals/use-action-modal.ts'
import { vTooltip } from '@core/directives/tooltip.directive.ts'
import { useI18n } from 'vue-i18n'
const { host } = defineProps<{
host: XenApiHost
}>()
const { t } = useI18n()
const { open: openActionModal } = useActionModal()
const {
run,
canRun: canRebootHost,
isRunning: isRebootingHost,
errorMessage: rebootHostErrorMessage,
} = useHostRebootJob(() => host)
function rebootHost() {
return openActionModal({
props: {
accent: 'info',
action: 'reboot',
object: 'host',
hostName: host.name_label,
icon: 'status:info-picto',
},
events: {
onConfirm: async () => {
void run()
},
},
})
}
</script>

View File

@@ -1,39 +0,0 @@
import { HOST_OPERATION } from '@/libs/xen-api/xen-api.enums.ts'
import type { XenApiHost } from '@/libs/xen-api/xen-api.types.ts'
import { getHostPendingStateOperation } from '@/modules/host/utils/host.util.ts'
import { useMapper } from '@core/packages/mapper'
import { toComputed } from '@core/utils/to-computed.util.ts'
import { computed, type MaybeRefOrGetter } from 'vue'
import { useI18n } from 'vue-i18n'
export function useHostUtils(rawHost: MaybeRefOrGetter<XenApiHost | undefined>) {
const { t } = useI18n()
const host = toComputed(rawHost)
const pendingStateOperation = computed(() => {
if (host.value === undefined) {
return undefined
}
return getHostPendingStateOperation(host.value)
})
const isChangingState = computed(() => pendingStateOperation.value !== undefined)
const currentOperation = useMapper<string, string>(
() => pendingStateOperation.value,
{
[HOST_OPERATION.EVACUATE]: t('operation:evacuate'),
[HOST_OPERATION.REBOOT]: t('operation:clean-reboot'),
[HOST_OPERATION.SHUTDOWN]: t('operation:shutdown'),
unknown: '',
},
'unknown'
)
return {
isChangingState,
currentOperation,
}
}

View File

@@ -1,35 +0,0 @@
import { HOST_OPERATION } from '@/libs/xen-api/xen-api.enums.ts'
import { hostArg } from '@/modules/host/jobs/host-args.ts'
import { isHostOperationPending } from '@/modules/host/utils/host.util.ts'
import { useHostMetricsStore } from '@/stores/xen-api/host-metrics.store.ts'
import { useXenApiStore } from '@/stores/xen-api.store.ts'
import { defineJob, JobError, JobRunningError } from '@core/packages/job'
import { useI18n } from 'vue-i18n'
export const useHostRebootJob = defineJob('host.reboot', [hostArg], () => {
const xapi = useXenApiStore().getXapi()
const { t } = useI18n()
const { isHostRunning } = useHostMetricsStore().subscribe()
return {
run: host => xapi.host.cleanReboot(host.$ref),
validate: (isRunning, host) => {
if (host === undefined) {
throw new JobError(t('job:host-reboot:missing-host'))
}
if (isRunning || isHostOperationPending(host, HOST_OPERATION.REBOOT)) {
throw new JobRunningError(t('job:host-reboot:in-progress'))
}
if (isHostOperationPending(host, HOST_OPERATION.EVACUATE)) {
throw new JobRunningError(t('job:host-reboot:evacuate-in-progress'))
}
if (!isHostRunning(host)) {
throw new JobError(t('job:host-reboot:bad-power-state'))
}
},
}
})

View File

@@ -1,20 +0,0 @@
import { HOST_OPERATION } from '@/libs/xen-api/xen-api.enums.ts'
import type { XenApiHost } from '@/libs/xen-api/xen-api.types.ts'
import { castArray } from 'lodash-es'
const CHANGING_STATE_OPERATIONS = [HOST_OPERATION.EVACUATE, HOST_OPERATION.REBOOT, HOST_OPERATION.SHUTDOWN]
export const isHostOperationPending = (host: XenApiHost, operations: HOST_OPERATION[] | HOST_OPERATION) => {
const currentOperations = Object.values(host.current_operations)
return castArray(operations).some(operation => currentOperations.includes(operation))
}
export const getHostPendingOperation = (host: XenApiHost, operations: HOST_OPERATION[] | HOST_OPERATION) => {
const currentOperations = Object.values(host.current_operations)
return castArray(operations).find(operation => currentOperations.includes(operation))
}
export const getHostPendingStateOperation = (host: XenApiHost) =>
getHostPendingOperation(host, CHANGING_STATE_OPERATIONS)

View File

@@ -24,9 +24,9 @@
</template>
<script lang="ts" setup>
import { isHostOperationPending } from '@/libs/host.ts'
import { HOST_OPERATION } from '@/libs/xen-api/xen-api.enums.ts'
import type { XenApiHost } from '@/libs/xen-api/xen-api.types.ts'
import { isHostOperationPending } from '@/modules/host/utils/host.util.ts'
import { usePageTitleStore } from '@/stores/page-title.store.ts'
import { useConsoleStore } from '@/stores/xen-api/console.store.ts'
import { useControlDomainStore } from '@/stores/xen-api/control-domain.store.ts'

View File

@@ -50,7 +50,7 @@ const state = useTableState({
})
const { HeadCells, BodyCells } = useHostColumns({
exclude: ['selectItem', 'srStatus'],
exclude: ['selectItem'],
body: (host: XenApiHost) => {
const state = isHostRunning(host) ? 'running' : 'halted'

View File

@@ -12,15 +12,20 @@ const TUNNEL_SERVICE = 'xoa-support-tunnel.service'
const { debug, warn } = createLogger('xo:proxy:appliance')
const getUpdater = deduped(async function () {
const createUpdater = async function () {
const updater = new JsonRpcWebSocketClient('ws://localhost:9001')
await updater.open()
return new Disposable(() => updater.close(), updater)
})
}
const getUpdater = deduped(createUpdater)
// `update` reports its progress as notifications on the connection, therefore it
// gets its own instead of the shared one: concurrent calls would otherwise each
// receive the others' notifications, and their listeners would pile up on it
const callUpdate = params =>
Disposable.use(
getUpdater(),
createUpdater(),
updater =>
new Promise((resolve, reject) => {
updater

View File

@@ -0,0 +1,142 @@
import assert from 'node:assert/strict'
import { after, before, describe, it } from 'node:test'
import { WebSocketServer } from 'ws'
import { format, parse } from 'json-rpc-protocol'
import Appliance from '../app/mixins/appliance.mjs'
// `appliance.mjs` connects to this hardcoded address
const UPDATER_PORT = 9001
// `update` is sent as a notification and answered with notifications, so nothing
// but the connection ties a reply to its request. This fake updater answers on
// the socket which asked, exactly like `xoa-updater`'s server does, and is
// therefore able to tell whether the callers share a connection.
function createFakeUpdater({ updateDelays = {}, licensesDelay = 0 } = {}) {
const sockets = []
const wss = new WebSocketServer({ port: UPDATER_PORT })
wss.on('connection', socket => {
const entry = { updates: [], calls: [] }
sockets.push(entry)
socket.on('message', data => {
const message = parse(String(data))
const { id, method, params = {} } = message
if (method === 'update') {
entry.updates.push(params)
const key = params.upgrade === true ? 'upgrade' : 'getState'
setTimeout(() => {
socket.send(format.notification('print', { content: `working on ${key}` }))
socket.send(format.notification('end', { state: key }))
}, updateDelays[key] ?? 0)
return
}
entry.calls.push(method)
if (method === 'getSelfLicenses') {
setTimeout(() => socket.send(format.response(id, [{ id: 'licence-1' }])), licensesDelay)
} else {
setTimeout(() => socket.send(format.response(id, 'ok')), 0)
}
})
})
return {
sockets,
close: () => new Promise(resolve => wss.close(resolve)),
ready: new Promise((resolve, reject) => {
wss.on('listening', resolve)
wss.on('error', reject)
}),
}
}
// `Appliance` only exposes itself through `app.api.addMethods()`
function instantiate() {
let methods
const appliance = new Appliance({
api: {
addMethods: tree => {
methods = tree.appliance
},
},
})
return { appliance, updater: methods.updater }
}
describe('appliance updater', function () {
let fake
let appliance
let updater
before(async function () {
fake = createFakeUpdater({
// the upgrade answers first, so a shared connection would hand its `end`
// to the getState call, which started earlier
updateDelays: { getState: 400, upgrade: 100 },
licensesDelay: 200,
})
await fake.ready.catch(error => {
assert.fail(
`could not listen on ${UPDATER_PORT}: ${error.code}. ` +
'A real xoa-updater is probably running on this machine.'
)
})
;({ appliance, updater } = instantiate())
})
after(async function () {
await fake.close()
})
it('does not mix up the results of concurrent getState and upgrade', async function () {
const [state, upgraded] = await Promise.all([updater.getState(), updater.upgrade()])
// each call must get the answer to its own request, not the first one to
// arrive on a shared connection
assert.equal(state.state, 'getState')
assert.equal(upgraded.state, 'upgrade')
})
it('gives each update its own connection, carrying a single request', async function () {
const before = fake.sockets.length
await Promise.all([updater.getState(), updater.getState(), updater.getState()])
const opened = fake.sockets.slice(before)
assert.equal(opened.length, 3, 'each update opens its own connection')
for (const { updates } of opened) {
assert.equal(updates.length, 1, 'a connection carries a single update')
}
})
it('does not leak listeners when many updates overlap', async function () {
const warnings = []
const onWarning = warning => {
if (warning.name === 'MaxListenersExceededWarning') {
warnings.push(warning.message)
}
}
process.on('warning', onWarning)
try {
await Promise.all(Array.from({ length: 15 }, () => updater.getState()))
// let the warnings, which are emitted asynchronously, be delivered
await new Promise(resolve => setImmediate(resolve))
} finally {
process.off('warning', onWarning)
}
assert.deepEqual(warnings, [])
})
it('shares a single connection between the id-correlated calls', async function () {
const before = fake.sockets.length
const [licence] = await Promise.all([appliance.getSelfLicense(), appliance.getSelfLicense()])
const opened = fake.sockets.slice(before)
assert.equal(licence.id, 'licence-1')
assert.equal(opened.length, 1, 'concurrent calls reuse the deduped connection')
assert.deepEqual(opened[0].calls, ['getSelfLicenses', 'getSelfLicenses'])
})
})

View File

@@ -1,7 +1,177 @@
REST API to manage your XOA.
# @xen-orchestra/rest-api
The user-facing documentation is available at [docs.xen-orchestra.com](https://docs.xen-orchestra.com/automation/restapi).
## Rules
## Development guidelines
The REST API is based on the `TSOA` framework and therefore we use decorators a lot to define the behavior of a route or a group of routes. To keep things easily visible, it is best to always use the decorators in the same order.
Any change to this package **must** follow the [REST API guidelines](https://github.com/vatesfr/xen-orchestra/blob/master/%40xen-orchestra/rest-api/docs/guidelines.md), in addition to the [backend project guidelines](https://github.com/vatesfr/xen-orchestra/blob/master/packages/xo-server/docs/guidelines.md).
At the request of the DevOps team, any REST API PR that updates the OpenAPI specification must include a reviewer from the DevOps team. (Non-blocking for merge.)
### Resource consistency
For a given resource (VMs, VIFs, users, etc.), property names must be consistent across all endpoints.
> Example: a PATCH request on a VM must use the same property names as the VM representation returned by the REST API.
```
GET /vms/:id
{
name_label: 'Foo',
name_description: 'Foo Bar',
...
}
PATCH /vms/:id
{
name_label: 'Bar' // OK ✅
nameDescription: 'Bar Foo' // Not OK ❌
}
```
### Class decorator
```ts
@Routes('foo')
@Security('*')
@Response(401)
@Tags('foo')
@provide(Foo)
class Foo extends Controller {}
```
### Methods decorator
```ts
@Routes('foo')
...
class Foo extends Controller {
/**
* any jsdoc annotations
* @example id 1234
*/
@Example(['foo', 'bar'])
@Extension('x-mcp-exposure', 'allow')
@Get('{id}')
@Security('*')
@Middlewares(json())
@Tags('foo')
@SuccessResponse(202)
@Response(404)
getFoo(@Path() id: string) {
return this.getFoo(id)
}
}
```
### Examples
In order not to pollute important decorators, all example structures should be in a separate file. `src/open-api/oa-examples/<resource>.oa-example.mts`
### ACLs
To define an ACL for an endpoint, simply add the `acl` middleware and pass the required ACL(s).
If an endpoint does not have a middleware ACL, it will be accessible **ONLY** to administrators.
It is sometimes necessary to check ACLs based on the body of the request sent by the user (for example, for a PATCH endpoint). For this, you can use `actions` (which allows you to pass multiple actions) and `actionsFromBody` (a function exported from `acl.middleware.mts`).
`actionsFromBody(['update:nameLabel', 'update:nameDescription'])` checks if `nameLabel` is present in the request body, and then applies the ACL check. The same applies to `nameDescription`.
`actionIfNotSelfUser('read')` returns the given action only if the current user is **not** the target user. If the current user is the target (self), no action is returned and the ACL check is skipped entirely.
#### Guidelines
- **JSDoc Documentation**: Always document the required privileges in the JSDoc annotation so users know which permissions are needed. Use the format: `Required privilege: - ...`
- **Error Handling**: If you define an ACL for an endpoint, you **must** add a `@Response(403)` decorator.
- **Non XAPI objects**: When dealing with non XAPI XO Record, you must define the `getObject` function.
##### Example: ACL on an existing resource
```ts
/**
* Start a VM
*
* Required privilege:
* - resource: vm, action: start
*/
@Post('{id}/actions/start')
@Middlewares(acl({resource: 'vm', action: 'start', objectId: 'params.id'}))
@Response(403)
getVm(@Path() id: string) {
const action = async () => {
const vm = await this.getObject(id)
// ...
}
}
```
##### Example: Resource creation
```ts
/**
* Create a new VDI
*
* Required privilege:
* - resource: vdi, action: create
*/
@Post('/')
@Middlewares(acl({resource: 'vdi', action: 'create', object: ({req}) => req.body }))
@Response(403)
createVdi(@Body() body: VdiConfig) {
const {srId, ...rest}
const bodyParam = {$SR: srId, ...rest}
await VDI_create(bodyParam)
// ...
}
```
##### Example: Resource update
```ts
/**
* Update a VM
*
* Required privileges:
* - resource: vm, action: update (grants all fields)
* - resource: vm, action: update:nameLabel (if nameLabel is passed)
* - resource: vm, action: update:nameDescription (if nameDescription is passed)
*/
@Patch('{id}')
@Middlewares(acl({resource: 'vm', actions: actionsFromBody(['update:nameLabel', 'update:nameDescription']), objectId: 'params.id'}))
@Response(403)
createVdi(@Path() id: string, @Body() body: patchBody) {
updateVm(id, body)
// ...
}
```
##### Example: Self-bypass ACL
```ts
/**
* Get a user
*
* Required privilege:
* - resource: user, action: read (if not self)
*/
@Get('{id}')
@Middlewares(acl({
resource: 'user',
actions: actionsIfNotSelfUser(['read']),
objectId: 'params.id',
getObject: ({ restApi }) => restApi.xoApp.getUser,
}))
@Response(403)
getUser(@Path() id: string) { ... }
```
If you need to use a privilege that doesn't exist yet (e.g., `resource: 'vm', action: 'foo'`), you must register it in ACL Definition: here `@xen-orchestra/acl/src/actions/vm.mts`, add: `foo: true`.
### MCP exposure
All REST API endpoints must define an MCP exposure policy using the `@Extension` decorator.
- `@Extension('x-mcp-exposure', 'allow')` for all `GET` endpoints.
- `@Extension('x-mcp-exposure', 'confirm')` for all non-`GET` endpoints (`POST`, `PATCH`, `PUT`, `DELETE`, etc.).
- `@Extension('x-mcp-exposure', 'deny')` only for exceptional cases.

View File

@@ -16,13 +16,183 @@ npm install --save @xen-orchestra/rest-api
## Usage
REST API to manage your XOA.
# @xen-orchestra/rest-api
The user-facing documentation is available at [docs.xen-orchestra.com](https://docs.xen-orchestra.com/rest_api).
## Rules
## Development guidelines
The REST API is based on the `TSOA` framework and therefore we use decorators a lot to define the behavior of a route or a group of routes. To keep things easily visible, it is best to always use the decorators in the same order.
Any change to this package **must** follow the [REST API guidelines](https://github.com/vatesfr/xen-orchestra/blob/master/%40xen-orchestra/rest-api/docs/guidelines.md), in addition to the [backend project guidelines](https://github.com/vatesfr/xen-orchestra/blob/master/packages/xo-server/docs/guidelines.md).
At the request of the DevOps team, any REST API PR that updates the OpenAPI specification must include a reviewer from the DevOps team. (Non-blocking for merge.)
### Resource consistency
For a given resource (VMs, VIFs, users, etc.), property names must be consistent across all endpoints.
> Example: a PATCH request on a VM must use the same property names as the VM representation returned by the REST API.
```
GET /vms/:id
{
name_label: 'Foo',
name_description: 'Foo Bar',
...
}
PATCH /vms/:id
{
name_label: 'Bar' // OK ✅
nameDescription: 'Bar Foo' // Not OK ❌
}
```
### Class decorator
```ts
@Routes('foo')
@Security('*')
@Response(401)
@Tags('foo')
@provide(Foo)
class Foo extends Controller {}
```
### Methods decorator
```ts
@Routes('foo')
...
class Foo extends Controller {
/**
* any jsdoc annotations
* @example id 1234
*/
@Example(['foo', 'bar'])
@Extension('x-mcp-exposure', 'allow')
@Get('{id}')
@Security('*')
@Middlewares(json())
@Tags('foo')
@SuccessResponse(202)
@Response(404)
getFoo(@Path() id: string) {
return this.getFoo(id)
}
}
```
### Examples
In order not to pollute important decorators, all example structures should be in a separate file. `src/open-api/oa-examples/<resource>.oa-example.mts`
### ACLs
To define an ACL for an endpoint, simply add the `acl` middleware and pass the required ACL(s).
If an endpoint does not have a middleware ACL, it will be accessible **ONLY** to administrators.
It is sometimes necessary to check ACLs based on the body of the request sent by the user (for example, for a PATCH endpoint). For this, you can use `actions` (which allows you to pass multiple actions) and `actionsFromBody` (a function exported from `acl.middleware.mts`).
`actionsFromBody(['update:nameLabel', 'update:nameDescription'])` checks if `nameLabel` is present in the request body, and then applies the ACL check. The same applies to `nameDescription`.
`actionIfNotSelfUser('read')` returns the given action only if the current user is **not** the target user. If the current user is the target (self), no action is returned and the ACL check is skipped entirely.
#### Guidelines
- **JSDoc Documentation**: Always document the required privileges in the JSDoc annotation so users know which permissions are needed. Use the format: `Required privilege: - ...`
- **Error Handling**: If you define an ACL for an endpoint, you **must** add a `@Response(403)` decorator.
- **Non XAPI objects**: When dealing with non XAPI XO Record, you must define the `getObject` function.
##### Example: ACL on an existing resource
```ts
/**
* Start a VM
*
* Required privilege:
* - resource: vm, action: start
*/
@Post('{id}/actions/start')
@Middlewares(acl({resource: 'vm', action: 'start', objectId: 'params.id'}))
@Response(403)
getVm(@Path() id: string) {
const action = async () => {
const vm = await this.getObject(id)
// ...
}
}
```
##### Example: Resource creation
```ts
/**
* Create a new VDI
*
* Required privilege:
* - resource: vdi, action: create
*/
@Post('/')
@Middlewares(acl({resource: 'vdi', action: 'create', object: ({req}) => req.body }))
@Response(403)
createVdi(@Body() body: VdiConfig) {
const {srId, ...rest}
const bodyParam = {$SR: srId, ...rest}
await VDI_create(bodyParam)
// ...
}
```
##### Example: Resource update
```ts
/**
* Update a VM
*
* Required privileges:
* - resource: vm, action: update (grants all fields)
* - resource: vm, action: update:nameLabel (if nameLabel is passed)
* - resource: vm, action: update:nameDescription (if nameDescription is passed)
*/
@Patch('{id}')
@Middlewares(acl({resource: 'vm', actions: actionsFromBody(['update:nameLabel', 'update:nameDescription']), objectId: 'params.id'}))
@Response(403)
createVdi(@Path() id: string, @Body() body: patchBody) {
updateVm(id, body)
// ...
}
```
##### Example: Self-bypass ACL
```ts
/**
* Get a user
*
* Required privilege:
* - resource: user, action: read (if not self)
*/
@Get('{id}')
@Middlewares(acl({
resource: 'user',
actions: actionsIfNotSelfUser(['read']),
objectId: 'params.id',
getObject: ({ restApi }) => restApi.xoApp.getUser,
}))
@Response(403)
getUser(@Path() id: string) { ... }
```
If you need to use a privilege that doesn't exist yet (e.g., `resource: 'vm', action: 'foo'`), you must register it in ACL Definition: here `@xen-orchestra/acl/src/actions/vm.mts`, add: `foo: true`.
### MCP exposure
All REST API endpoints must define an MCP exposure policy using the `@Extension` decorator.
- `@Extension('x-mcp-exposure', 'allow')` for all `GET` endpoints.
- `@Extension('x-mcp-exposure', 'confirm')` for all non-`GET` endpoints (`POST`, `PATCH`, `PUT`, `DELETE`, etc.).
- `@Extension('x-mcp-exposure', 'deny')` only for exceptional cases.
## Contributions

View File

@@ -1,248 +0,0 @@
# REST API guidelines
These rules are **mandatory** for any change under `@xen-orchestra/rest-api`. They complement, and never replace, the [backend project guidelines](../../../packages/xo-server/docs/guidelines.md).
They are not absolute rules, but expect to have to explain to the reviewer if the PR does not explicitly state why a guideline is not used.
The REST API is based on the `TSOA` framework and therefore we use decorators a lot to define the behavior of a route or a group of routes. To keep things easily visible, it is best to always use the decorators in the same order.
At the request of the DevOps team, any REST API PR that updates the OpenAPI specification must include a reviewer from the DevOps team. (Non-blocking for merge.)
## API design
Naming is hard, building a coherent API is hard: ask/propose naming **before** starting to push code.
- `GET` for getting information of an object or a collection
- `POST` for creating an object and for actions
- `PUT`/`PATCH` for updating an object without side effect
Use the `@Deprecated()` decorator if needed, and never remove a route without changing the API version.
### Resource consistency
For a given resource (VMs, VIFs, users, etc.), property names must be consistent across all endpoints. The types and names of properties must also be coherent with the types in `@vates/types/xo.mts`.
> Example: a PATCH request on a VM must use the same property names as the VM representation returned by the REST API.
```
GET /vms/:id
{
name_label: 'Foo',
name_description: 'Foo Bar',
...
}
PATCH /vms/:id
{
name_label: 'Bar' // OK ✅
nameDescription: 'Bar Foo' // Not OK ❌
}
```
## Code organisation
- the service must handle the domain/functional logic of one or multiple REST API routes
- the controller is focused on serving the result
## Decorators
### Class decorator
```ts
@Routes('foo')
@Security('*')
@Response(401)
@Tags('foo')
@provide(Foo)
class Foo extends Controller {}
```
### Methods decorator
```ts
@Routes('foo')
...
class Foo extends Controller {
/**
* any jsdoc annotations
* @example id 1234
*/
@Example(['foo', 'bar'])
@Extension('x-mcp-exposure', 'allow')
@Get('{id}')
@Security('*')
@Middlewares(json())
@Tags('foo')
@SuccessResponse(202)
@Response(404)
getFoo(@Path() id: string) {
return this.getFoo(id)
}
}
```
### Route definition
- `@Get(route with named parameter)`: must be defined before writing code
- `@Middlewares(acl(...))`: ACLs are defined in `@xen-orchestra/acl/src/actions/`, and should be discussed before coding
- `@Extension('x-mcp-exposure', value)`: see [MCP exposure](#mcp-exposure)
- `@SuccessResponse(status, description)`: response in case of success. Statuses and descriptions are defined in `src/open-api/common/response.common.mts`. Take care of explaining any non obvious result, like an UUID change during VDI migration. This documentation must be clear for users out of the XO team, or even out of Vates.
- `@Response(status, description)`: possible responses including errors. Statuses and descriptions are defined in `src/open-api/common/response.common.mts`. Order by status code.
- `@Example` for **every** parameter and the body. Path parameters are the easy ones to forget — they have no body schema to fall back on, so list them explicitly (`@example id "…"`, `@example diskId "…"`)
### Route declaration order (tsoa)
tsoa's route-collision check is declaration-order sensitive: a `…/files.{format}` route declared **after** the plain `…/files` route is wrongly flagged as overlapping (it does a `startsWith`, not an equality, on the last segment). Declare each `.{format}` variant **before** its plain sibling.
## Examples
In order not to pollute important decorators, all example structures should be in a separate file: `src/open-api/oa-examples/<resource>.oa-example.mts`.
## Actions
REST API actions are reserved to user actions that won't fit the REST API. The REST API actions must use the `this.createAction` method to handle correctly synchronous and asynchronous mode.
If your action endpoint includes a request body, don't forget to add `params` to the `taskProperties` property (obfuscate sensitive params such as passwords)
```js
createAction(action, {
//...
taskProperties: {
// ...
params: body,
},
})
```
On the caller side, action calls are asynchronous by default; the caller can add `sync=true` on any action call to force the synchronous mode.
## ACLs
To define an ACL for an endpoint, simply add the `acl` middleware and pass the required ACL(s).
If an endpoint does not have a middleware ACL, it will be accessible **ONLY** to administrators.
It is sometimes necessary to check ACLs based on the body of the request sent by the user (for example, for a PATCH endpoint). For this, you can use `actions` (which allows you to pass multiple actions) and `actionsFromBody` (a function exported from `acl.middleware.mts`).
`actionsFromBody(['update:nameLabel', 'update:nameDescription'])` checks if `nameLabel` is present in the request body, and then applies the ACL check. The same applies to `nameDescription`.
`actionIfNotSelfUser('read')` returns the given action only if the current user is **not** the target user. If the current user is the target (self), no action is returned and the ACL check is skipped entirely.
### Guidelines
- **JSDoc Documentation**: Always document the required privileges in the JSDoc annotation so users know which permissions are needed. Use the format: `Required privilege: - ...`
- **Error Handling**: If you define an ACL for an endpoint, you **must** add a `@Response(403)` decorator.
- **Non XAPI objects**: When dealing with non XAPI XO Record, you must define the `getObject` function.
#### Example: ACL on an existing resource
```ts
/**
* Start a VM
*
* Required privilege:
* - resource: vm, action: start
*/
@Post('{id}/actions/start')
@Middlewares(acl({resource: 'vm', action: 'start', objectId: 'params.id'}))
@Response(403)
getVm(@Path() id: string) {
const action = async () => {
const vm = await this.getObject(id)
// ...
}
}
```
#### Example: Resource creation
```ts
/**
* Create a new VDI
*
* Required privilege:
* - resource: vdi, action: create
*/
@Post('/')
@Middlewares(acl({resource: 'vdi', action: 'create', object: ({req}) => req.body }))
@Response(403)
createVdi(@Body() body: VdiConfig) {
const {srId, ...rest}
const bodyParam = {$SR: srId, ...rest}
await VDI_create(bodyParam)
// ...
}
```
#### Example: Resource update
```ts
/**
* Update a VM
*
* Required privileges:
* - resource: vm, action: update (grants all fields)
* - resource: vm, action: update:nameLabel (if nameLabel is passed)
* - resource: vm, action: update:nameDescription (if nameDescription is passed)
*/
@Patch('{id}')
@Middlewares(acl({resource: 'vm', actions: actionsFromBody(['update:nameLabel', 'update:nameDescription']), objectId: 'params.id'}))
@Response(403)
createVdi(@Path() id: string, @Body() body: patchBody) {
updateVm(id, body)
// ...
}
```
#### Example: Self-bypass ACL
```ts
/**
* Get a user
*
* Required privilege:
* - resource: user, action: read (if not self)
*/
@Get('{id}')
@Middlewares(acl({
resource: 'user',
actions: actionsIfNotSelfUser(['read']),
objectId: 'params.id',
getObject: ({ restApi }) => restApi.xoApp.getUser,
}))
@Response(403)
getUser(@Path() id: string) { ... }
```
If you need to use a privilege that doesn't exist yet (e.g., `resource: 'vm', action: 'foo'`), you must register it in the ACL definition: here `@xen-orchestra/acl/src/actions/vm.mts`, add: `foo: true`.
## MCP exposure
All REST API endpoints must define an MCP exposure policy using the `@Extension` decorator. This rule is enforced by the `require-mcp-expose` ESLint rule (`eslint-rules/require-mcp-expose.cjs`).
- `@Extension('x-mcp-exposure', 'allow')` for all `GET` endpoints. MCP can use this with the default permission of the user. Only for idempotent reads, with a constrained output size.
- `@Extension('x-mcp-exposure', 'confirm')` for all non-`GET` endpoints (`POST`, `PATCH`, `PUT`, `DELETE`, etc.). MCP will ask for the user permission before using it. For modifications, and bigger exports.
- `@Extension('x-mcp-exposure', 'deny')` only for exceptional cases. MCP won't use this endpoint. For example: binary export of VMs/disks/files.
## Status codes & errors
**Never hand-roll a status code** (`res.status(404)`). `throw` the semantic error from `xo-common/api-errors` and let `generic-error-handler.middleware` map it centrally. Reuse the shared descriptors in `src/open-api/common/response.common.mts` (`notFoundResp`, `invalidParameters`, …) instead of magic numbers. Throw an `ApiError`, set up with the right parameters, if a new error / error code is needed.
| Throw (`xo-common/api-errors`) | HTTP status |
| ------------------------------------------------------------- | ----------- |
| `noSuchObject(id, type)` (`NO_SUCH_OBJECT`) | 404 |
| `unauthorized` / `forbiddenOperation` / `featureUnauthorized` | 403 |
| `invalidCredentials` | 401 |
| `objectAlreadyExists` / `incorrectState` | 409 |
| `invalidParameters` | 422 |
| `notImplemented` | 501 |
Throw `noSuchObject(id, type)` to get an automatic 404 when the routing doesn't already handle it. Resolve the object **first** so the 404 fires before any work starts.
## Streaming & file-download routes
- Obtain and validate the resource **before** setting any response header, so a `noSuchObject` returns a clean JSON 404 instead of a body labelled as a download
- Set `content-disposition: attachment; filename="…"` with a meaningful name built from domain labels (VM / disk / partition). Fold accents to ASCII and collapse anything outside `[\w.-]` so the header and the on-disk filename stay safe (also blocks header injection)
- Tie the stream to the request lifetime (`req.on('close', () => stream.destroy())`) so a client disconnect doesn't leak the export

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 1.0 MiB

After

Width:  |  Height:  |  Size: 1.0 MiB

View File

@@ -1,79 +1,118 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="offline-illustration" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 397.13 396.85">
<svg id="Power_Off" data-name="Power Off" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 397.13 396.85">
<defs>
<linearGradient id="Dégradé_sans_nom_70" x1="3691.32" y1="4318.86" x2="3681.99" y2="4311.56" gradientTransform="translate(5619.39 1941.51) rotate(148.86)" gradientUnits="userSpaceOnUse">
<style>
.cls-1 {
fill: #eaea92;
}
.cls-2 {
fill: #fff;
}
.cls-3 {
fill: url(#Dégradé_sans_nom_70);
}
.cls-4 {
fill: #484960;
}
.cls-5 {
fill: #f0f;
}
.cls-6, .cls-7 {
fill: #1a1b38;
}
.cls-8 {
fill: #be1621;
}
.cls-9 {
fill: #8f84ff;
}
.cls-7 {
opacity: .3;
}
</style>
<linearGradient id="Dégradé_sans_nom_70" data-name="Dégradé sans nom 70" x1="3691.32" y1="4318.86" x2="3681.99" y2="4311.56" gradientTransform="translate(5619.39 1941.51) rotate(148.86)" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#871818"/>
<stop offset="1" stop-color="#bf1818" stop-opacity=".5"/>
</linearGradient>
</defs>
<path fill="#1a1b38" d="M344.8,118.73c6.42,10.62,9.31,22.95,8.42,35.17-.07,1.01-.17,2.03-.3,3.04-.07.52-.13,1.02-.21,1.52-.15,1.02-.32,2.03-.54,3.03-.02.11-.05.22-.07.32-.18.9-.38,1.79-.62,2.68-.13.5-.27,1-.41,1.5-.1.39-.22.78-.33,1.17-.11.35-.22.71-.34,1.07-.25.75-.51,1.51-.79,2.26-.22.59-.45,1.17-.69,1.75-.47,1.16-.98,2.29-1.53,3.41-1.09,2.23-2.32,4.41-3.66,6.49-.34.51-.68,1.03-1.04,1.54-1.42,2.03-2.97,3.98-4.63,5.82-.83.93-1.69,1.81-2.57,2.68-.89.88-1.8,1.71-2.74,2.52-.17.14-.33.29-.51.44-.54.44-1.13.93-1.77,1.42-.45.35-.9.7-1.37,1.04-3.73,2.73-8.02,5.17-12.49,7.05-.93.39-1.87.75-2.8,1.09-.58.21-1.14.39-1.69.57-.55.18-1.1.35-1.63.51-1.52.44-3.06.83-4.6,1.15-3.59.75-7.22,1.15-10.86,1.23h-.01c-1.98.03-3.98-.03-5.96-.19h-.01c-1.14-.08-2.28-.21-3.43-.37-.39-.06-.76-.12-1.14-.18h-.01c-.4-.05-.81-.12-1.22-.2-.78-.15-1.56-.3-2.31-.46-7.55-1.66-14.53-4.67-20.72-8.86-7-4.75-12.95-11.02-17.49-18.53-1.5-2.49-2.8-5.03-3.91-7.66-1.26-2.97-2.28-6.03-3.03-9.18-.6-2.44-1.03-4.91-1.3-7.44-.03-.26-.06-.51-.08-.77-.08-.71-.14-1.45-.18-2.2-.01-.16-.02-.34-.03-.52h0c-.05-1.02-.08-2.03-.07-3.04,0-7.09,1.27-14.08,3.68-20.65.51-1.41,1.08-2.79,1.71-4.16.21-.45.42-.9.63-1.35.88-1.81,1.83-3.56,2.89-5.26.53-.85,1.08-1.69,1.65-2.52.57-.82,1.15-1.64,1.77-2.43.61-.81,1.24-1.58,1.9-2.36.65-.76,1.32-1.52,2.02-2.25l.34-.36c.94-.99,1.96-1.97,3.04-2.95.23-.22.47-.43.71-.64,1.12-.99,2.29-1.93,3.51-2.84,1.58-1.18,3.23-2.29,4.94-3.32,2.73-1.65,5.58-3.07,8.51-4.25.97-.39,1.95-.76,2.94-1.1.02,0,.01,0,.01,0,.99-.34,1.99-.65,3-.94,9.55-2.73,19.69-3.06,29.5-.84,1.55.35,3.09.76,4.62,1.24.85.27,1.7.55,2.5.84.71.25,1.37.51,2,.75.42.18.83.35,1.25.54.43.18.84.35,1.26.55,4.8,2.17,9.23,4.96,13.23,8.27,1.71,1.41,3.35,2.93,4.9,4.52,1.55,1.61,3,3.29,4.37,5.07.46.6.9,1.2,1.33,1.81.87,1.22,1.69,2.47,2.47,3.77Z"/>
<path fill="#1a1b38" d="M154.43,257.1c15.11,25.01,10.04,56.96-12.06,75.98-.47.38-1.03.86-1.65,1.36-.21.17-.42.34-.64.5-.48.37-.98.76-1.48,1.12-3.71,2.69-7.96,5.11-12.37,6.97-.93.39-1.87.75-2.8,1.09-.57.2-1.13.39-1.68.57-.55.18-1.1.35-1.65.52-1.81.53-3.65.98-5.5,1.33-1.23.22-2.47.42-3.72.58-1.87.24-3.76.39-5.66.44-1.37.04-2.75.03-4.22-.02-5.65-.23-11.14-1.25-16.37-2.96-.59-.19-1.17-.39-1.74-.61-1.71-.62-3.39-1.33-5.04-2.1-.56-.25-1.11-.53-1.64-.81-1.08-.55-2.13-1.13-3.18-1.75-8.36-4.93-15.5-11.92-20.69-20.5-8.5-14.08-10.49-30.3-6.82-45.13,3.64-14.82,12.94-28.27,27.03-36.78,8.67-5.24,18.43-8.14,28.38-8.53,1.81-.08,3.63-.07,5.45.03,2.72.14,5.45.48,8.17.99,1.07.2,2.17.43,3.29.72.56.14,1.11.28,1.66.44.55.16,1.08.32,1.59.47.84.28,1.7.55,2.56.86.4.15.79.29,1.18.44.28.11.55.21.84.32.82.33,1.65.68,2.46,1.06.68.32,1.37.64,2.04.98,4.03,2.01,7.78,4.46,11.2,7.29,1.71,1.41,3.35,2.93,4.9,4.53,1.54,1.6,3,3.28,4.37,5.06.45.59.89,1.19,1.33,1.81.86,1.21,1.69,2.47,2.47,3.76Z"/>
<path fill="#484960" d="M245.8,102.94l.32-.32s.02-.03.03-.05c-.11.12-.22.23-.34.36Z"/>
<path fill="#8f84ff" d="M257.96,101.84c-1.21.9-2.39,1.85-3.51,2.84-.24.21-.48.42-.71.64-1.08.98-2.11,1.96-3.04,2.95l-.34.36c-.7.73-1.37,1.49-2.02,2.25-.66.78-1.29,1.55-1.9,2.36-.62.8-1.2,1.61-1.77,2.43-.57.82-1.12,1.66-1.65,2.52-1.05,1.71-2,3.46-2.89,5.26-.21.45-.42.9-.63,1.35-.63,1.36-1.2,2.75-1.71,4.16-2.41,6.57-3.69,13.56-3.68,20.65-.01,1.01.02,2.01.07,3.03h0c0,.19.02.37.03.53.03.75.1,1.49.18,2.2.03.25.05.5.08.77.27,2.52.7,5,1.3,7.44.74,3.14,1.75,6.21,3.03,9.18,1.11,2.62,2.41,5.17,3.91,7.66,4.54,7.51,10.49,13.78,17.49,18.53,6.18,4.19,13.17,7.22,20.72,8.88.76.16,1.53.32,2.31.45.41.08.82.15,1.22.2.39.06.77.13,1.15.18,1.15.16,2.3.29,3.43.37-1.26,1.15-2.58,2.23-3.93,3.21-1.53,1.11-3.11,2.09-4.72,2.92-1.8.93-3.61,1.72-5.43,2.36-1.98-.55-3.94-1.18-5.84-1.89-14.86-5.49-27.32-15.77-35.68-29.61-5.49-9.09-8.8-19.02-9.82-29.52-.11-1.1-.2-2.22-.25-3.33-.8-16.05,3.94-31.91,13.21-44.8,1.14-1.59,2.33-3.13,3.6-4.61,1.96-.48,3.95-.88,5.98-1.18-.11.12-.22.23-.34.36l.32-.32s.02-.03.03-.05c.49-.07,1-.14,1.51-.21,1.47-.18,2.93-.32,4.43-.41,1.94-.13,3.9-.16,5.88-.12Z"/>
<path fill="#8f84ff" d="M354.08,113.12c10.1,16.73,12.81,37.07,7.44,55.79-.48,1.74-1.07,3.54-1.76,5.38-4.13,11.08-11.04,20.98-19.97,28.66-.71.61-1.65,1.41-2.69,2.19-2.14,1.67-4.47,3.28-6.91,4.75-4.03,2.44-8.32,4.48-12.79,6.08-1.33.48-2.6.88-3.89,1.27-9.61,2.81-19.5,3.52-29.42,2.14-1.87-.26-3.72-.59-5.5-.98-1.22-.28-2.43-.57-3.63-.89,1.82-.65,3.63-1.43,5.43-2.36,1.61-.83,3.19-1.82,4.72-2.92,1.35-.97,2.67-2.05,3.93-3.21,1.99.18,3.98.23,5.97.19h.01c3.65-.07,7.28-.47,10.86-1.23,1.54-.31,3.08-.71,4.6-1.15.54-.16,1.09-.32,1.63-.51.56-.17,1.12-.36,1.69-.57.93-.34,1.87-.7,2.8-1.09,4.46-1.86,8.76-4.31,12.49-7.05.47-.34.92-.68,1.37-1.04.65-.49,1.24-.97,1.77-1.42.18-.15.34-.29.51-.44.95-.81,1.85-1.64,2.74-2.52.89-.86,1.74-1.76,2.57-2.68,1.66-1.84,3.22-3.79,4.63-5.82.35-.51.7-1.03,1.04-1.54,1.35-2.07,2.58-4.25,3.66-6.49.55-1.12,1.06-2.26,1.53-3.41.25-.57.47-1.16.69-1.74.27-.76.54-1.52.78-2.27.12-.36.23-.72.34-1.07.11-.39.23-.79.33-1.17.15-.5.28-1,.41-1.5.24-.89.44-1.79.62-2.68.02-.11.05-.21.07-.32.21-1,.39-2.01.54-3.03.08-.5.14-1,.21-1.52.14-1.01.24-2.03.3-3.04.88-12.22-2.01-24.55-8.42-35.17-.78-1.3-1.61-2.55-2.47-3.77-.44-.61-.88-1.22-1.33-1.81-1.37-1.78-2.82-3.46-4.37-5.07-1.55-1.58-3.18-3.1-4.9-4.52-3.99-3.3-8.43-6.1-13.23-8.27-.42-.2-.83-.37-1.26-.55-.42-.18-.83-.35-1.25-.54-.63-.24-1.29-.5-2-.75-.81-.29-1.65-.57-2.5-.84-1.52-.47-3.07-.89-4.62-1.24-9.81-2.22-19.95-1.9-29.5.84-1.01.29-2.01.59-3,.94,0,0,0,.01-.01,0-.99.34-1.96.71-2.94,1.1-2.93,1.18-5.78,2.6-8.51,4.25-1.71,1.03-3.36,2.14-4.94,3.32-1.98-.04-3.95,0-5.88.12-1.5.09-2.96.23-4.43.41-.51.07-1.02.14-1.51.21-2.03.3-4.01.69-5.98,1.18.87-1,1.76-1.99,2.69-2.96l.67-.7h.01c1.18-1.21,2.45-2.4,3.76-3.56,3.07-2.68,6.43-5.14,9.99-7.29,17.24-10.42,38.15-12.96,57.33-6.98.96.29,1.99.63,3.02.99l.03.02h.03c.78.3,1.53.58,2.29.89l.07.02.07.03c.92.39,1.88.79,2.84,1.23,12.96,5.89,23.72,15.47,31.1,27.69Z"/>
<path fill="#8f84ff" d="M120.42,219.55c-.5-.13-1.03-.25-1.55-.37-.09.17-.2.35-.3.52-1.1,1.86-2.24,3.9-3.39,6.1-.47.89-.94,1.79-1.41,2.74-.11.21-.21.42-.31.62.36.06.72.13,1.09.19.81.15,1.62.31,2.45.52.56-1.14,1.13-2.25,1.7-3.31,1.03-1.93,2.06-3.75,3.04-5.41.22-.37.44-.74.65-1.09-.64-.18-1.29-.35-1.95-.51Z"/>
<path fill="#8f84ff" d="M163.71,251.49c8.67,14.34,11.94,31.34,9.23,47.84-2.68,16.33-11.02,31.24-23.52,42-.76.66-1.68,1.42-2.68,2.18-2.14,1.67-4.47,3.28-6.91,4.75-4.03,2.44-8.32,4.48-12.79,6.08-1.33.48-2.6.88-3.89,1.27-5.71,1.66-11.63,2.61-17.55,2.78-1.6.05-3.24.04-5.04-.02-23.74-.97-45.24-13.67-57.5-33.98-20.1-33.26-9.38-76.69,23.88-96.78,14.9-9,32.5-12.16,49.56-8.9.8.14,1.6.3,2.38.47-.09.17-.2.35-.3.52-1.1,1.86-2.24,3.9-3.39,6.1-.47.89-.94,1.79-1.41,2.74-.11.21-.21.42-.31.62-2.36-.41-4.73-.68-7.08-.8-1.82-.1-3.64-.11-5.45-.03-9.95.4-19.71,3.3-28.38,8.53-14.09,8.51-23.39,21.96-27.03,36.78-3.67,14.83-1.68,31.05,6.82,45.13,5.19,8.59,12.34,15.57,20.69,20.5,1.03.61,2.1,1.19,3.18,1.75.54.28,1.08.56,1.64.81,1.64.78,3.32,1.49,5.04,2.1.57.22,1.14.42,1.74.61,4.45,1.48,9.11,2.43,13.89,2.82.82.07,1.65.12,2.47.15.53.02,1.06.03,1.57.04.91.01,1.79,0,2.66-.03,1.9-.05,3.79-.2,5.66-.44,1.25-.15,2.49-.35,3.72-.58,1.86-.35,3.7-.8,5.5-1.33.54-.16,1.09-.32,1.65-.52.55-.16,1.11-.36,1.68-.57.93-.34,1.87-.7,2.8-1.09,4.41-1.85,8.67-4.27,12.37-6.97.5-.36,1-.74,1.48-1.12.22-.16.43-.33.64-.5.63-.49,1.19-.97,1.65-1.36,22.09-19.02,27.16-50.98,12.06-75.98-.78-1.29-1.6-2.54-2.47-3.76-.44-.62-.88-1.23-1.33-1.81-1.36-1.77-2.82-3.46-4.37-5.06-1.55-1.59-3.19-3.11-4.9-4.53-3.43-2.83-7.17-5.28-11.2-7.29-.67-.34-1.36-.66-2.04-.98-.81-.38-1.64-.73-2.46-1.06-.7-.27-1.34-.52-2.02-.76-.86-.31-1.72-.58-2.56-.86-.5-.16-1.05-.31-1.59-.47-.54-.15-1.09-.3-1.66-.44-.28-.07-.56-.14-.84-.2.56-1.14,1.13-2.25,1.7-3.31,1.03-1.93,2.06-3.75,3.04-5.41.22-.37.44-.74.65-1.09.66.19,1.33.38,1.95.58.89.28,1.94.62,2.97,1,.77.28,1.55.57,2.38.91,1.03.4,2.02.83,2.94,1.25,12.97,5.91,23.73,15.49,31.1,27.69Z"/>
<path fill="#be1621" d="M129.36,209.48c-.07.1-2.27,2.99-5.28,7.81-.54.86-1.12,1.79-1.7,2.77-.21.35-.42.72-.65,1.09-.98,1.66-2.01,3.48-3.04,5.41-.57,1.06-1.14,2.17-1.7,3.31-1.46,2.91-2.91,6.05-4.23,9.33-4.25,10.49-7.35,22.55-6.28,34.08,1.47,15.86,8.29,20.58,14.32,24.75,5.39,3.74,10.48,7.26,9.25,17.52-1.08,9.07-8.44,10.32-14.93,11.43-7.58,1.29-14.16,2.4-14.71,13.33-.12,2.5.67,4.93,2.15,7.26-.51,0-1.04-.02-1.57-.04-.82-.04-1.66-.08-2.47-.15-.49-1.03-.87-2.08-1.14-3.14-.34-1.33-.46-2.7-.39-4.1.69-13.69,10.03-15.27,17.56-16.55,7.01-1.18,11.34-2.24,12.09-8.44.98-8.21-2.53-10.63-7.81-14.31-6.29-4.36-14.13-9.8-15.77-27.24-.71-7.61,0-19.54,6.4-35.51,1.12-2.85,2.45-5.82,3.99-8.93.1-.2.2-.42.31-.62.47-.95.94-1.85,1.41-2.74,1.15-2.2,2.29-4.24,3.39-6.1.1-.18.21-.35.3-.52.68-1.17,1.33-2.27,1.98-3.28,2.6-4.16,4.65-7,5.45-8.06.19-.25.3-.41.34-.46l.33.27,2.39,1.84Z"/>
<path fill="#be1621" d="M134.79,203.8l-2.79,5.14c-1.05,1.93-3.45,2.65-5.38,1.6-1.93-1.05-2.64-3.46-1.59-5.39l2.54-4.66c.48.35,1.01.64,1.56.9l3.64,1.7c.66.3,1.34.54,2.03.7Z"/>
<path fill="var(--color-neutral-txt-primary)" d="M151.55,172.89l5.91-12.21c.46-.96,1.62-1.36,2.57-.89h0c.96.46,1.36,1.62.89,2.57l-5.91,12.21-3.47-1.68Z"/>
<path fill="var(--color-neutral-txt-primary)" d="M136.95,166.36l5.91-12.21c.46-.96,1.62-1.36,2.57-.89h0c.96.46,1.36,1.62.89,2.57l-5.91,12.21-3.47-1.68Z"/>
<path fill="#be1621" d="M150.85,192.52c1.35,1.04,1.83,2.92,1.05,4.52-.88,1.84-3.1,2.6-4.93,1.71l-.38-.19c-2.33,4.25-7.2,6.35-11.8,5.24-.69-.16-1.37-.4-2.03-.7l-3.64-1.7c-.55-.26-1.08-.56-1.56-.9-4.11-2.82-5.75-8.24-3.73-12.95-.63-.32-1.13-.8-1.47-1.36-.63-1.04-.72-2.36-.16-3.53.82-1.69,2.78-2.48,4.53-1.88l8.36-18.17,24.51,12.02-8.74,17.89Z"/>
<path fill="var(--color-neutral-txt-primary)" d="M146.58,198.56s-.34-.11-.93-.36c-.59-.25-1.44-.64-2.48-1.12-2.09-.96-4.95-2.35-8.1-3.9-3.12-1.54-5.94-2.94-8.05-3.99-.99-.49-1.81-.9-2.44-1.22-.57-.29-.89-.46-.88-.48,0-.02.34.11.93.36.59.25,1.44.64,2.48,1.12,2.09.96,4.95,2.35,8.1,3.9,3.12,1.54,5.93,2.94,8.05,3.99.99.49,1.81.9,2.44,1.22.57.29.89.46.88.48Z"/>
<path fill="var(--color-neutral-txt-primary)" d="M150.85,192.52s-.36-.12-.98-.4c-.62-.28-1.52-.7-2.62-1.22-2.2-1.05-5.23-2.54-8.55-4.22-3.3-1.67-6.28-3.18-8.51-4.31-1.05-.54-1.91-.98-2.57-1.32-.61-.32-.93-.5-.92-.52s.36.12.98.4c.62.28,1.52.7,2.62,1.22,2.2,1.05,5.23,2.54,8.56,4.22,3.3,1.67,6.28,3.18,8.51,4.31,1.05.54,1.91.98,2.57,1.32.61.32.93.5.92.52Z"/>
<path fill="#be1621" d="M280.44,159.94c-.87-.3-1.72-.59-2.55-.88-5.58-1.95-23.61-3.95-35.91-5.16-3.03-.29-5.72-.54-7.78-.73-1.68-.15-2.95-.27-3.64-.34-.36-.04-.57-.05-.62-.06l-.79,2.81s.6.06,1.62.17c.91.08,2.16.22,3.68.38,1.83.19,4.06.43,6.52.71,12.24,1.35,30.35,3.57,35.77,5.47.82.29,1.69.58,2.58.89,7.88,2.67,17.67,5.99,19.21,13.74,1.89,9.39-3.04,22.99-14.1,31.52h-.01c-1.34,1.04-2.77,2.01-4.31,2.87-.43.26-.89.51-1.35.75-3.2,1.65-6.45,2.8-9.68,3.54-2.31.54-4.63.87-6.9,1.03-8.41.62-16.36-1.04-22.52-3-.9-.3-1.79-.59-2.68-.92-15.05-5.45-26.88-15.05-33.5-27.26-2.37-4.37-4.06-9.12-5.05-14.04-.61-2.99-.95-6.05-1.04-9.13l-3.46-1.21c.02,3.67.4,7.35,1.11,10.93,1.06,5.29,2.86,10.39,5.4,15.09,7.44,13.71,21,24.29,38.16,29.81.77.24,1.54.49,2.35.72,7.68,2.19,17.5,3.66,27.59,1.63,2.13-.41,4.26-.99,6.37-1.75,1.82-.65,3.63-1.43,5.43-2.36,1.61-.83,3.19-1.82,4.72-2.92,1.35-.97,2.67-2.05,3.93-3.21h.01c3.24-2.99,6.1-6.49,8.32-10.3,4.36-7.44,5.98-15.42,4.58-22.45-1.95-9.7-12.78-13.38-21.49-16.33ZM326.31,134.24c-.61-.35-1.22-.67-1.84-.97-4.1-2.06-8.04-3.31-11.62-4-1.79-4.2-4.9-8.32-9.46-12.57-2.18-2.05-4.59-3.85-7.18-5.39-2.41-1.42-4.96-2.59-7.66-3.54-.39-.14-.77-.28-1.17-.4-10.3-3.52-20.15-5.37-29.42-5.53-1.98-.04-3.95,0-5.88.12-1.5.09-2.96.23-4.43.41-.51.07-1.02.14-1.51.21-2.03.3-4.01.69-5.98,1.18-2.06.5-4.08,1.12-6.07,1.84-4.9,1.74-9.54,4.09-13.88,7.02-4.41,2.99-8.02,6.25-10.89,9.32-2.87,3.06-5.01,5.92-6.48,8.14l3.32,1.26c1.43-2.09,3.43-4.69,6.04-7.43,2.66-2.8,5.96-5.74,9.95-8.44,2.73-1.85,5.58-3.44,8.53-4.79,1.91-.87,3.87-1.63,5.88-2.3,3.69-1.21,7.53-2.07,11.51-2.57h.01c1.86-.24,3.74-.39,5.66-.47,7.84-.32,16.18.69,24.9,3.01,2.9.74,5.81,1.65,8.78,2.69,5.15,1.8,9.74,4.56,13.64,8.2,3.43,3.18,5.98,6.31,7.68,9.42-2.51-.26-4.76-.29-6.65-.18-1.11.03-2.09.13-2.93.23-4.37.55-8.25,3.03-10.41,6.65-2.68,4.49-2.63,10.12.13,15.06,2.49,4.48,6.69,7.3,10.96,7.36,2.99.04,7.38-1.22,11.14-7.52,3.41-5.75,4.46-11.52,3.14-17.2,2.67.67,5.55,1.67,8.51,3.14.67.32,1.34.68,2.01,1.05,9.9,5.49,19,16.17,26.09,29.91.11-.39.23-.79.33-1.17.15-.5.28-1,.41-1.5.24-.89.44-1.79.62-2.68-6.82-13.34-15.09-21.62-25.79-27.56ZM308.02,148.5c-2.31,3.86-5.12,5.88-8.13,5.84-1.3-.01-2.59-.42-3.81-1.13-1.65-.98-3.11-2.5-4.21-4.47-2.15-3.87-2.22-8.2-.18-11.62,1.63-2.71,4.57-4.59,7.89-5,.81-.1,1.76-.19,2.81-.24,2.2-.12,4.9-.04,7.95.42,1.76,5.34.97,10.66-2.33,16.2Z"/>
<path fill="var(--color-neutral-txt-primary)" d="M171.67,165.78c-.32,0-.32.25-.33.89l.14,7.91.38,12.18.36,11.11-8.66-1.46,2.23,12.7.64,3.38c.27,1.37.3,1.48.62,1.43.06-.01.12-.02.15-.05.16-.1.12-.39-.03-1.51l-2.27-14.6,8.51,1.49-.94-24.69-.3-6.21-.11-1.69c-.05-.64-.07-.88-.38-.86Z"/>
<path fill="var(--color-neutral-txt-primary)" d="M218.8,38.43l-.1.03c-.13.04-.19.15-.29.37l-5.52,12.6-13.29,31.09-10.91-2.79-11.05,31.62-3.18,9.27-.61,1.84-.43,1.33c-.06.22-.15.51.14.6.1.03.18.02.24-.02.11-.07.18-.23.24-.37l4.51-12.33,10.87-30.57,10.85,2.77,13.51-32.07,3.93-9.47,1.3-3.25c.09-.24.12-.34.06-.47l-.06-.11-.22-.07Z"/>
<path fill="var(--color-neutral-txt-primary)" d="M149.66,128.4l-1.41-1.22h0c-1.4-1.2-3.11-2.68-5.17-4.51-3.81-3.38-9.65-8.79-15.98-15.46l3.3-3.15-16.03-16.25-4.71-4.7-1.3-1.25c-.5-.47-.66-.62-.89-.41-.23.22-.08.38.39.9l5.82,6.14,15.09,15.53-3.29,3.14.39.42c7.04,7.44,13.4,13.11,16.57,15.82,1.92,1.65,3.8,3.21,5.3,4.4l1.47,1.14c.49.37.68.51.85.41.03-.02.07-.05.1-.09.2-.24.05-.38-.51-.87Z"/>
<path fill="var(--color-neutral-txt-primary)" d="M136.38,131.07c-8.29-6.98-12.66-10.5-12.99-10.47h-.13s-.08.11-.08.11c-.23.28-.37.44,12.46,11.24,3.37,2.84,6.55,5.48,8.96,7.44,3.42,2.78,3.91,3.15,4.14,3.02.04-.02.07-.06.1-.1.23-.28.37-.44-12.46-11.24Z"/>
<path fill="var(--color-neutral-txt-primary)" d="M193.59,102.44l-.12.02s-.02,0-.03.01c-.56.34-4.74,6.49-8.19,11.65-7.98,11.94-7.79,12.06-7.5,12.26h0c.06.04.12.08.24,0,.47-.28,1.9-2.17,8.22-11.62,3.53-5.28,7.71-11.65,7.63-12.06l-.03-.13-.21-.14Z"/>
<path fill="var(--color-neutral-txt-primary)" d="M167.06,109.15l-.26-.04-.1.07c-.33.24-1.04,4.44-1.57,7.91-1.23,8.06-.98,8.1-.66,8.15.05,0,.11.02.16-.02.27-.16.62-1.29,1.64-7.96.53-3.48,1.11-7.69.87-8.02l-.08-.11Z"/>
<path fill="var(--color-neutral-txt-primary)" d="M135.11,83.75l-.12-.04-.23.11-.04.12c-.11.32,3.32,7.82,10.19,22.3,2.78,5.86,5.43,11.36,7.45,15.49q3.26,6.66,3.63,6.49s.05-.02.07-.04c.28-.17.09-.6-2.8-6.88-1.92-4.17-4.52-9.7-7.3-15.56-6.88-14.48-10.52-21.88-10.84-21.99Z"/>
<path fill="var(--color-neutral-txt-primary)" d="M180.77,168.84c-2.86-1.8-4.46-2.68-4.76-2.61l-.12.03-.14.21.02.13c.02.13.09.46,4.39,3.22,2.86,1.8,4.46,2.68,4.76,2.61l.14-.04.13-.21-.02-.13c-.06-.32-1.54-1.4-4.39-3.22Z"/>
<path fill="url(#Dégradé_sans_nom_70)" d="M229.16,155.58l.8-2.82c.21.05,5.24.46,12.03,1.13l-1,2.94c-6.8-.76-11.77-1.24-11.83-1.25Z"/>
<path fill="#be1621" d="M218.48,155.46l2.23-7.43,7.4,2.24c1.35.41,2.31,1.47,2.63,2.75.18.67.18,1.39-.03,2.08-.06.2-.14.39-.22.57-.81,1.68-2.76,2.59-4.61,2.03l-7.4-2.24Z"/>
<polygon fill="#8f84ff" points="134.68 264.31 134.14 262.21 135.6 260.61 133.5 261.15 131.89 259.69 132.44 261.79 130.98 263.4 133.08 262.85 134.68 264.31"/>
<polygon fill="#eaea92" points="91.52 324.83 90.42 320.62 93.35 317.4 89.14 318.5 85.92 315.56 87.02 319.78 84.08 323 88.3 321.9 91.52 324.83"/>
<path fill="#fff" d="M111.56,249.99c-.46.28-.89-.43-.43-.71s.89.43.43.71Z"/>
<path fill="#fff" d="M106.99,311.38c-.12.25-.28.41-.56.47-.33.06-.54-.28-.33-.55.17-.22.4-.29.67-.28.19,0,.3.19.22.36Z"/>
<path fill="#fff" d="M146.16,319.4c-.43.26-.82-.4-.4-.66s.82.4.4.66Z"/>
<path fill="#fff" d="M159.31,296.49c-.46.28-.89-.43-.43-.71s.89.43.43.71Z"/>
<path fill="#fff" d="M128.85,281.56c-.47.29-.92-.45-.44-.74s.92.45.44.74Z"/>
<path fill="#fff" d="M123.22,276.64c-.43.26-.84-.41-.41-.67s.84.41.41.67Z"/>
<path fill="#fff" d="M132.58,243.24c-.62.38-1.2-.59-.58-.96s1.2.59.58.96Z"/>
<path fill="#fff" d="M100.05,266.35c-.24-.04-.4-.18-.59-.32-.21-.16.04-.43.25-.35.2.08.38.16.52.33.13.15.02.38-.19.34Z"/>
<path fill="#fff" d="M107.16,271.81c-.11-.04-.22-.09-.34-.13-.13-.05-.09-.23.05-.22.12.01.24.03.36.04.2.02.11.37-.07.3Z"/>
<path fill="#fff" d="M89.51,280.65c-.07,0-.15,0-.22.01-.27.02-.47-.31-.33-.55.04-.06.07-.13.11-.19.19-.31.65-.35.85-.02s-.05.72-.41.74Z"/>
<path fill="#fff" d="M72.13,295.29c-.83.5-1.6-.78-.77-1.28.83-.5,1.6.78.77,1.28Z"/>
<path fill="#fff" d="M102.08,291.43c.02.06.04.12.05.18.08.29-.28.5-.5.3-.05-.04-.09-.08-.13-.13-.16-.16-.14-.43.06-.55s.45-.01.52.2Z"/>
<path fill="#fff" d="M109.11,302.25c-.37.22-.71-.35-.35-.57s.71.35.35.57Z"/>
<path fill="#fff" d="M74.96,267.09c-.56.34-1.08-.53-.52-.86s1.08.53.52.86Z"/>
<path fill="#fff" d="M78.59,264.62c.04.13.02.27-.04.39-.11.21-.35.31-.58.26-.34-.08-.45-.46-.34-.73-.07-.12-.06-.29.09-.39.28-.18.65,0,.77.29.05.05.08.12.1.19Z"/>
<path fill="#fff" d="M90.68,237.87c-.1.42-.77.58-.98.15,0,0,0,0,0,0-.02-.01-.05-.02-.07-.04-.33-.17-.2-.7.17-.7.03,0,.05,0,.08,0h0c.39-.29.9.16.79.58Z"/>
<path fill="#fff" d="M75.61,311.91c-.07,0-.15,0-.22.01-.27.02-.47-.31-.33-.55.04-.06.07-.13.11-.19.19-.31.65-.35.85-.02s-.05.72-.41.74Z"/>
<path fill="#fff" d="M54.14,277.89c-.07,0-.15,0-.22.01-.27.02-.47-.31-.33-.55.04-.06.07-.13.11-.19.19-.31.65-.35.85-.02s-.05.72-.41.74Z"/>
<polygon fill="#fff" points="280.37 190.01 281.57 192.38 280.23 194.69 282.61 193.49 284.91 194.82 283.71 192.45 285.05 190.15 282.67 191.35 280.37 190.01"/>
<polygon fill="#eaea92" points="269.83 121.73 271.98 126 269.58 130.13 273.85 127.98 277.98 130.38 275.83 126.11 278.23 121.98 273.96 124.13 269.83 121.73"/>
<path fill="#fff" d="M304.34,202.61c.48-.45,1.18.29.7.74s-1.18-.29-.7-.74Z"/>
<path fill="#fff" d="M282.45,138.43c.08-.33.23-.57.55-.7.38-.16.72.2.54.57-.15.31-.4.44-.73.51-.23.04-.41-.15-.36-.38Z"/>
<path fill="#fff" d="M249.66,141.01c.44-.42,1.09.27.65.69s-1.09-.27-.65-.69Z"/>
<path fill="#fff" d="M247.71,169.5c.48-.45,1.18.29.7.74-.48.45-1.18-.29-.7-.74Z"/>
<path fill="#fff" d="M263.97,179.79c.49-.47,1.22.3.72.77-.49.47-1.22-.3-.72-.77Z"/>
<path fill="#fff" d="M271.99,184.23c.45-.43,1.11.27.66.7s-1.11-.27-.66-.7Z"/>
<path fill="#fff" d="M286.04,96.63c.53-.5,1.3.32.77.82s-1.3-.32-.77-.82Z"/>
<path fill="#fff" d="M302.39,190.57c.29-.01.53.11.79.24.29.14.06.53-.21.48-.26-.05-.5-.09-.71-.26-.19-.15-.13-.45.13-.45Z"/>
<path fill="#fff" d="M292.46,185.86c.15.02.29.05.44.07.17.03.17.26,0,.27-.15.01-.29.03-.44.04-.25.02-.23-.42.01-.38Z"/>
<path fill="#fff" d="M332.61,123.71c.09-.02.17-.05.26-.07.32-.09.65.25.54.57-.03.08-.06.17-.09.25-.14.43-.69.58-1.01.24s-.13-.88.3-1Z"/>
<path fill="#fff" d="M328.36,148.7c.86-.81,2.12.52,1.26,1.34-.86.81-2.12-.52-1.26-1.34Z"/>
<path fill="#fff" d="M293.48,161.06c-.04-.07-.07-.13-.11-.2-.17-.33.2-.67.52-.49.06.04.13.08.19.12.23.15.28.48.07.68s-.53.13-.67-.11Z"/>
<path fill="#fff" d="M282.27,149.92c.38-.36.94.23.56.6s-.94-.23-.56-.6Z"/>
<path fill="#fff" d="M332.25,183.21c.58-.55,1.43.35.85.9s-1.43-.35-.85-.9Z"/>
<path fill="#fff" d="M328.54,187.1c-.08-.15-.09-.32-.05-.48.08-.27.34-.46.63-.46.43.01.65.44.6.79.11.13.15.33,0,.49-.29.29-.77.17-1-.14-.07-.05-.13-.12-.17-.2Z"/>
<path fill="#fff" d="M257.12,125.94c.02-.53.77-.9,1.13-.43,0,0,0,0,0,0,.03,0,.06.02.09.03.44.12.42.78-.03.88-.03,0-.06.01-.09.02h0c-.39.45-1.12.03-1.1-.5Z"/>
<path fill="#fff" d="M318.93,121.66c.09-.02.17-.05.26-.07.32-.09.65.25.54.57-.03.08-.06.17-.09.25-.14.43-.69.58-1.01.24-.32-.34-.13-.88.3-1Z"/>
<path fill="#fff" d="M340.29,169.77c.09-.02.17-.05.26-.07.32-.09.65.25.54.57-.03.08-.06.17-.09.25-.14.43-.69.58-1.01.24s-.13-.88.3-1Z"/>
<path fill="var(--color-neutral-txt-primary)" d="M183.72,155.7s.08-.37.28-1.02c.2-.65.51-1.59.9-2.75.78-2.31,1.9-5.49,3.17-8.99,1.26-3.47,2.41-6.61,3.26-8.96.41-1.1.75-2.01,1-2.71.24-.64.39-.99.41-.98s-.08.37-.28,1.02c-.2.65-.51,1.59-.9,2.75-.78,2.31-1.9,5.49-3.17,9-1.26,3.47-2.41,6.61-3.26,8.96-.41,1.1-.75,2.01-1,2.71-.24.64-.39.99-.41.98Z"/>
<path fill="#be1621" d="M221.36,145.91l-.65,2.12-2.24,7.43-.83,2.75c-.78,2.62-3.55,4.1-6.16,3.32l-7.56-2.29c-1.59,1.28-3.6,1.98-5.7,1.88-.27,0-.55-.03-.83-.07-.43-.05-.85-.15-1.27-.28l-2.17-.65-2.03-.62-8.31-2.51-.49,1.38c-.44,1.23-1.79,1.87-3.01,1.44-1.23-.44-1.87-1.8-1.45-3.02l10.77-30.28c.44-1.22,1.8-1.87,3.02-1.43,1.23.44,1.87,1.8,1.45,3.02l-.25.64,8.4,2.53,2.72.82c.24.07.47.15.69.24,2.36.94,4.09,2.86,4.88,5.13l.03-.07,7.68,2.33c2.62.78,4.09,3.54,3.3,6.17Z"/>
<path fill="#1a1b38" opacity=".3" d="M134.79,203.8l-2.79,5.14c-1.05,1.93-3.45,2.65-5.38,1.6-1.93-1.05-2.64-3.46-1.59-5.39l2.54-4.66c.48.35,1.01.64,1.56.9l3.64,1.7c.66.3,1.34.54,2.03.7Z"/>
<path fill="#1a1b38" opacity=".3" d="M218.48,155.46l2.23-7.43,7.4,2.24c1.35.41,2.31,1.47,2.63,2.75.18.67.18,1.39-.03,2.08-.06.2-.14.39-.22.57-.81,1.68-2.76,2.59-4.61,2.03l-7.4-2.24Z"/>
<path fill="var(--color-neutral-txt-primary)" d="M210.32,137.81s-.05.3-.18.83c-.13.53-.34,1.29-.61,2.23-.53,1.88-1.31,4.46-2.2,7.3-.88,2.82-1.68,5.36-2.28,7.27-.28.89-.52,1.63-.7,2.2-.17.52-.27.8-.29.8-.02,0,.05-.3.18-.83.13-.53.34-1.29.61-2.23.53-1.88,1.31-4.46,2.2-7.3.88-2.82,1.68-5.36,2.28-7.27.28-.89.52-1.63.7-2.2.17-.52.27-.8.29-.8Z"/>
</svg>
<path class="cls-6" d="M344.8,118.73c6.42,10.62,9.31,22.95,8.42,35.17-.07,1.01-.17,2.03-.3,3.04-.07.52-.13,1.02-.21,1.52-.15,1.02-.32,2.03-.54,3.03-.02.11-.05.22-.07.32-.18.9-.38,1.79-.62,2.68-.13.5-.27,1-.41,1.5-.1.39-.22.78-.33,1.17-.11.35-.22.71-.34,1.07-.25.75-.51,1.51-.79,2.26-.22.59-.45,1.17-.69,1.75-.47,1.16-.98,2.29-1.53,3.41-1.09,2.23-2.32,4.41-3.66,6.49-.34.51-.68,1.03-1.04,1.54-1.42,2.03-2.97,3.98-4.63,5.82-.83.93-1.69,1.81-2.57,2.68-.89.88-1.8,1.71-2.74,2.52-.17.14-.33.29-.51.44-.54.44-1.13.93-1.77,1.42-.45.35-.9.7-1.37,1.04-3.73,2.73-8.02,5.17-12.49,7.05-.93.39-1.87.75-2.8,1.09-.58.21-1.14.39-1.69.57-.55.18-1.1.35-1.63.51-1.52.44-3.06.83-4.6,1.15-3.59.75-7.22,1.15-10.86,1.23h-.01c-1.98.03-3.98-.03-5.96-.19h-.01c-1.14-.08-2.28-.21-3.43-.37-.39-.06-.76-.12-1.14-.18h-.01c-.4-.05-.81-.12-1.22-.2-.78-.15-1.56-.3-2.31-.46-7.55-1.66-14.53-4.67-20.72-8.86-7-4.75-12.95-11.02-17.49-18.53-1.5-2.49-2.8-5.03-3.91-7.66-1.26-2.97-2.28-6.03-3.03-9.18-.6-2.44-1.03-4.91-1.3-7.44-.03-.26-.06-.51-.08-.77-.08-.71-.14-1.45-.18-2.2-.01-.16-.02-.34-.03-.52h0c-.05-1.02-.08-2.03-.07-3.04,0-7.09,1.27-14.08,3.68-20.65.51-1.41,1.08-2.79,1.71-4.16.21-.45.42-.9.63-1.35.88-1.81,1.83-3.56,2.89-5.26.53-.85,1.08-1.69,1.65-2.52.57-.82,1.15-1.64,1.77-2.43.61-.81,1.24-1.58,1.9-2.36.65-.76,1.32-1.52,2.02-2.25l.34-.36c.94-.99,1.96-1.97,3.04-2.95.23-.22.47-.43.71-.64,1.12-.99,2.29-1.93,3.51-2.84,1.58-1.18,3.23-2.29,4.94-3.32,2.73-1.65,5.58-3.07,8.51-4.25.97-.39,1.95-.76,2.94-1.1.02,0,.01,0,.01,0,.99-.34,1.99-.65,3-.94,9.55-2.73,19.69-3.06,29.5-.84,1.55.35,3.09.76,4.62,1.24.85.27,1.7.55,2.5.84.71.25,1.37.51,2,.75.42.18.83.35,1.25.54.43.18.84.35,1.26.55,4.8,2.17,9.23,4.96,13.23,8.27,1.71,1.41,3.35,2.93,4.9,4.52,1.55,1.61,3,3.29,4.37,5.07.46.6.9,1.2,1.33,1.81.87,1.22,1.69,2.47,2.47,3.77Z"/>
<path class="cls-6" d="M154.43,257.1c15.11,25.01,10.04,56.96-12.06,75.98-.47.38-1.03.86-1.65,1.36-.21.17-.42.34-.64.5-.48.37-.98.76-1.48,1.12-3.71,2.69-7.96,5.11-12.37,6.97-.93.39-1.87.75-2.8,1.09-.57.2-1.13.39-1.68.57-.55.18-1.1.35-1.65.52-1.81.53-3.65.98-5.5,1.33-1.23.22-2.47.42-3.72.58-1.87.24-3.76.39-5.66.44-1.37.04-2.75.03-4.22-.02-5.65-.23-11.14-1.25-16.37-2.96-.59-.19-1.17-.39-1.74-.61-1.71-.62-3.39-1.33-5.04-2.1-.56-.25-1.11-.53-1.64-.81-1.08-.55-2.13-1.13-3.18-1.75-8.36-4.93-15.5-11.92-20.69-20.5-8.5-14.08-10.49-30.3-6.82-45.13,3.64-14.82,12.94-28.27,27.03-36.78,8.67-5.24,18.43-8.14,28.38-8.53,1.81-.08,3.63-.07,5.45.03,2.72.14,5.45.48,8.17.99,1.07.2,2.17.43,3.29.72.56.14,1.11.28,1.66.44.55.16,1.08.32,1.59.47.84.28,1.7.55,2.56.86.4.15.79.29,1.18.44.28.11.55.21.84.32.82.33,1.65.68,2.46,1.06.68.32,1.37.64,2.04.98,4.03,2.01,7.78,4.46,11.2,7.29,1.71,1.41,3.35,2.93,4.9,4.53,1.54,1.6,3,3.28,4.37,5.06.45.59.89,1.19,1.33,1.81.86,1.21,1.69,2.47,2.47,3.76Z"/>
<path class="cls-4" d="M245.8,102.94l.32-.32s.02-.03.03-.05c-.11.12-.22.23-.34.36Z"/>
<path class="cls-9" d="M257.96,101.84c-1.21.9-2.39,1.85-3.51,2.84-.24.21-.48.42-.71.64-1.08.98-2.11,1.96-3.04,2.95l-.34.36c-.7.73-1.37,1.49-2.02,2.25-.66.78-1.29,1.55-1.9,2.36-.62.8-1.2,1.61-1.77,2.43-.57.82-1.12,1.66-1.65,2.52-1.05,1.71-2,3.46-2.89,5.26-.21.45-.42.9-.63,1.35-.63,1.36-1.2,2.75-1.71,4.16-2.41,6.57-3.69,13.56-3.68,20.65-.01,1.01.02,2.01.07,3.03h0c0,.19.02.37.03.53.03.75.1,1.49.18,2.2.03.25.05.5.08.77.27,2.52.7,5,1.3,7.44.74,3.14,1.75,6.21,3.03,9.18,1.11,2.62,2.41,5.17,3.91,7.66,4.54,7.51,10.49,13.78,17.49,18.53,6.18,4.19,13.17,7.22,20.72,8.88.76.16,1.53.32,2.31.45.41.08.82.15,1.22.2.39.06.77.13,1.15.18,1.15.16,2.3.29,3.43.37-1.26,1.15-2.58,2.23-3.93,3.21-1.53,1.11-3.11,2.09-4.72,2.92-1.8.93-3.61,1.72-5.43,2.36-1.98-.55-3.94-1.18-5.84-1.89-14.86-5.49-27.32-15.77-35.68-29.61-5.49-9.09-8.8-19.02-9.82-29.52-.11-1.1-.2-2.22-.25-3.33-.8-16.05,3.94-31.91,13.21-44.8,1.14-1.59,2.33-3.13,3.6-4.61,1.96-.48,3.95-.88,5.98-1.18-.11.12-.22.23-.34.36l.32-.32s.02-.03.03-.05c.49-.07,1-.14,1.51-.21,1.47-.18,2.93-.32,4.43-.41,1.94-.13,3.9-.16,5.88-.12Z"/>
<path class="cls-9" d="M354.08,113.12c10.1,16.73,12.81,37.07,7.44,55.79-.48,1.74-1.07,3.54-1.76,5.38-4.13,11.08-11.04,20.98-19.97,28.66-.71.61-1.65,1.41-2.69,2.19-2.14,1.67-4.47,3.28-6.91,4.75-4.03,2.44-8.32,4.48-12.79,6.08-1.33.48-2.6.88-3.89,1.27-9.61,2.81-19.5,3.52-29.42,2.14-1.87-.26-3.72-.59-5.5-.98-1.22-.28-2.43-.57-3.63-.89,1.82-.65,3.63-1.43,5.43-2.36,1.61-.83,3.19-1.82,4.72-2.92,1.35-.97,2.67-2.05,3.93-3.21,1.99.18,3.98.23,5.97.19h.01c3.65-.07,7.28-.47,10.86-1.23,1.54-.31,3.08-.71,4.6-1.15.54-.16,1.09-.32,1.63-.51.56-.17,1.12-.36,1.69-.57.93-.34,1.87-.7,2.8-1.09,4.46-1.86,8.76-4.31,12.49-7.05.47-.34.92-.68,1.37-1.04.65-.49,1.24-.97,1.77-1.42.18-.15.34-.29.51-.44.95-.81,1.85-1.64,2.74-2.52.89-.86,1.74-1.76,2.57-2.68,1.66-1.84,3.22-3.79,4.63-5.82.35-.51.7-1.03,1.04-1.54,1.35-2.07,2.58-4.25,3.66-6.49.55-1.12,1.06-2.26,1.53-3.41.25-.57.47-1.16.69-1.74.27-.76.54-1.52.78-2.27.12-.36.23-.72.34-1.07.11-.39.23-.79.33-1.17.15-.5.28-1,.41-1.5.24-.89.44-1.79.62-2.68.02-.11.05-.21.07-.32.21-1,.39-2.01.54-3.03.08-.5.14-1,.21-1.52.14-1.01.24-2.03.3-3.04.88-12.22-2.01-24.55-8.42-35.17-.78-1.3-1.61-2.55-2.47-3.77-.44-.61-.88-1.22-1.33-1.81-1.37-1.78-2.82-3.46-4.37-5.07-1.55-1.58-3.18-3.1-4.9-4.52-3.99-3.3-8.43-6.1-13.23-8.27-.42-.2-.83-.37-1.26-.55-.42-.18-.83-.35-1.25-.54-.63-.24-1.29-.5-2-.75-.81-.29-1.65-.57-2.5-.84-1.52-.47-3.07-.89-4.62-1.24-9.81-2.22-19.95-1.9-29.5.84-1.01.29-2.01.59-3,.94,0,0,0,.01-.01,0-.99.34-1.96.71-2.94,1.1-2.93,1.18-5.78,2.6-8.51,4.25-1.71,1.03-3.36,2.14-4.94,3.32-1.98-.04-3.95,0-5.88.12-1.5.09-2.96.23-4.43.41-.51.07-1.02.14-1.51.21-2.03.3-4.01.69-5.98,1.18.87-1,1.76-1.99,2.69-2.96l.67-.7h.01c1.18-1.21,2.45-2.4,3.76-3.56,3.07-2.68,6.43-5.14,9.99-7.29,17.24-10.42,38.15-12.96,57.33-6.98.96.29,1.99.63,3.02.99l.03.02h.03c.78.3,1.53.58,2.29.89l.07.02.07.03c.92.39,1.88.79,2.84,1.23,12.96,5.89,23.72,15.47,31.1,27.69Z"/>
<path class="cls-9" d="M120.42,219.55c-.5-.13-1.03-.25-1.55-.37-.09.17-.2.35-.3.52-1.1,1.86-2.24,3.9-3.39,6.1-.47.89-.94,1.79-1.41,2.74-.11.21-.21.42-.31.62.36.06.72.13,1.09.19.81.15,1.62.31,2.45.52.56-1.14,1.13-2.25,1.7-3.31,1.03-1.93,2.06-3.75,3.04-5.41.22-.37.44-.74.65-1.09-.64-.18-1.29-.35-1.95-.51Z"/>
<path class="cls-9" d="M163.71,251.49c8.67,14.34,11.94,31.34,9.23,47.84-2.68,16.33-11.02,31.24-23.52,42-.76.66-1.68,1.42-2.68,2.18-2.14,1.67-4.47,3.28-6.91,4.75-4.03,2.44-8.32,4.48-12.79,6.08-1.33.48-2.6.88-3.89,1.27-5.71,1.66-11.63,2.61-17.55,2.78-1.6.05-3.24.04-5.04-.02-23.74-.97-45.24-13.67-57.5-33.98-20.1-33.26-9.38-76.69,23.88-96.78,14.9-9,32.5-12.16,49.56-8.9.8.14,1.6.3,2.38.47-.09.17-.2.35-.3.52-1.1,1.86-2.24,3.9-3.39,6.1-.47.89-.94,1.79-1.41,2.74-.11.21-.21.42-.31.62-2.36-.41-4.73-.68-7.08-.8-1.82-.1-3.64-.11-5.45-.03-9.95.4-19.71,3.3-28.38,8.53-14.09,8.51-23.39,21.96-27.03,36.78-3.67,14.83-1.68,31.05,6.82,45.13,5.19,8.59,12.34,15.57,20.69,20.5,1.03.61,2.1,1.19,3.18,1.75.54.28,1.08.56,1.64.81,1.64.78,3.32,1.49,5.04,2.1.57.22,1.14.42,1.74.61,4.45,1.48,9.11,2.43,13.89,2.82.82.07,1.65.12,2.47.15.53.02,1.06.03,1.57.04.91.01,1.79,0,2.66-.03,1.9-.05,3.79-.2,5.66-.44,1.25-.15,2.49-.35,3.72-.58,1.86-.35,3.7-.8,5.5-1.33.54-.16,1.09-.32,1.65-.52.55-.16,1.11-.36,1.68-.57.93-.34,1.87-.7,2.8-1.09,4.41-1.85,8.67-4.27,12.37-6.97.5-.36,1-.74,1.48-1.12.22-.16.43-.33.64-.5.63-.49,1.19-.97,1.65-1.36,22.09-19.02,27.16-50.98,12.06-75.98-.78-1.29-1.6-2.54-2.47-3.76-.44-.62-.88-1.23-1.33-1.81-1.36-1.77-2.82-3.46-4.37-5.06-1.55-1.59-3.19-3.11-4.9-4.53-3.43-2.83-7.17-5.28-11.2-7.29-.67-.34-1.36-.66-2.04-.98-.81-.38-1.64-.73-2.46-1.06-.7-.27-1.34-.52-2.02-.76-.86-.31-1.72-.58-2.56-.86-.5-.16-1.05-.31-1.59-.47-.54-.15-1.09-.3-1.66-.44-.28-.07-.56-.14-.84-.2.56-1.14,1.13-2.25,1.7-3.31,1.03-1.93,2.06-3.75,3.04-5.41.22-.37.44-.74.65-1.09.66.19,1.33.38,1.95.58.89.28,1.94.62,2.97,1,.77.28,1.55.57,2.38.91,1.03.4,2.02.83,2.94,1.25,12.97,5.91,23.73,15.49,31.1,27.69Z"/>
<path class="cls-8" d="M129.36,209.48c-.07.1-2.27,2.99-5.28,7.81-.54.86-1.12,1.79-1.7,2.77-.21.35-.42.72-.65,1.09-.98,1.66-2.01,3.48-3.04,5.41-.57,1.06-1.14,2.17-1.7,3.31-1.46,2.91-2.91,6.05-4.23,9.33-4.25,10.49-7.35,22.55-6.28,34.08,1.47,15.86,8.29,20.58,14.32,24.75,5.39,3.74,10.48,7.26,9.25,17.52-1.08,9.07-8.44,10.32-14.93,11.43-7.58,1.29-14.16,2.4-14.71,13.33-.12,2.5.67,4.93,2.15,7.26-.51,0-1.04-.02-1.57-.04-.82-.04-1.66-.08-2.47-.15-.49-1.03-.87-2.08-1.14-3.14-.34-1.33-.46-2.7-.39-4.1.69-13.69,10.03-15.27,17.56-16.55,7.01-1.18,11.34-2.24,12.09-8.44.98-8.21-2.53-10.63-7.81-14.31-6.29-4.36-14.13-9.8-15.77-27.24-.71-7.61,0-19.54,6.4-35.51,1.12-2.85,2.45-5.82,3.99-8.93.1-.2.2-.42.31-.62.47-.95.94-1.85,1.41-2.74,1.15-2.2,2.29-4.24,3.39-6.1.1-.18.21-.35.3-.52.68-1.17,1.33-2.27,1.98-3.28,2.6-4.16,4.65-7,5.45-8.06.19-.25.3-.41.34-.46l.33.27,2.39,1.84Z"/>
<path class="cls-8" d="M134.79,203.8l-2.79,5.14c-1.05,1.93-3.45,2.65-5.38,1.6-1.93-1.05-2.64-3.46-1.59-5.39l2.54-4.66c.48.35,1.01.64,1.56.9l3.64,1.7c.66.3,1.34.54,2.03.7Z"/>
<path class="cls-6" d="M151.55,172.89l5.91-12.21c.46-.96,1.62-1.36,2.57-.89h0c.96.46,1.36,1.62.89,2.57l-5.91,12.21-3.47-1.68Z"/>
<path class="cls-6" d="M136.95,166.36l5.91-12.21c.46-.96,1.62-1.36,2.57-.89h0c.96.46,1.36,1.62.89,2.57l-5.91,12.21-3.47-1.68Z"/>
<path class="cls-5" d="M151.55,172.89l5.91-12.21c.46-.96,1.62-1.36,2.57-.89h0c.96.46,1.36,1.62.89,2.57l-5.91,12.21-3.47-1.68Z"/>
<path class="cls-5" d="M136.95,166.36l5.91-12.21c.46-.96,1.62-1.36,2.57-.89h0c.96.46,1.36,1.62.89,2.57l-5.91,12.21-3.47-1.68Z"/>
<path class="cls-8" d="M150.85,192.52c1.35,1.04,1.83,2.92,1.05,4.52-.88,1.84-3.1,2.6-4.93,1.71l-.38-.19c-2.33,4.25-7.2,6.35-11.8,5.24-.69-.16-1.37-.4-2.03-.7l-3.64-1.7c-.55-.26-1.08-.56-1.56-.9-4.11-2.82-5.75-8.24-3.73-12.95-.63-.32-1.13-.8-1.47-1.36-.63-1.04-.72-2.36-.16-3.53.82-1.69,2.78-2.48,4.53-1.88l8.36-18.17,24.51,12.02-8.74,17.89Z"/>
<path class="cls-6" d="M146.58,198.56s-.34-.11-.93-.36c-.59-.25-1.44-.64-2.48-1.12-2.09-.96-4.95-2.35-8.1-3.9-3.12-1.54-5.94-2.94-8.05-3.99-.99-.49-1.81-.9-2.44-1.22-.57-.29-.89-.46-.88-.48,0-.02.34.11.93.36.59.25,1.44.64,2.48,1.12,2.09.96,4.95,2.35,8.1,3.9,3.12,1.54,5.93,2.94,8.05,3.99.99.49,1.81.9,2.44,1.22.57.29.89.46.88.48Z"/>
<path class="cls-6" d="M150.85,192.52s-.36-.12-.98-.4c-.62-.28-1.52-.7-2.62-1.22-2.2-1.05-5.23-2.54-8.55-4.22-3.3-1.67-6.28-3.18-8.51-4.31-1.05-.54-1.91-.98-2.57-1.32-.61-.32-.93-.5-.92-.52s.36.12.98.4c.62.28,1.52.7,2.62,1.22,2.2,1.05,5.23,2.54,8.56,4.22,3.3,1.67,6.28,3.18,8.51,4.31,1.05.54,1.91.98,2.57,1.32.61.32.93.5.92.52Z"/>
<path class="cls-8" d="M280.44,159.94c-.87-.3-1.72-.59-2.55-.88-5.58-1.95-23.61-3.95-35.91-5.16-3.03-.29-5.72-.54-7.78-.73-1.68-.15-2.95-.27-3.64-.34-.36-.04-.57-.05-.62-.06l-.79,2.81s.6.06,1.62.17c.91.08,2.16.22,3.68.38,1.83.19,4.06.43,6.52.71,12.24,1.35,30.35,3.57,35.77,5.47.82.29,1.69.58,2.58.89,7.88,2.67,17.67,5.99,19.21,13.74,1.89,9.39-3.04,22.99-14.1,31.52h-.01c-1.34,1.04-2.77,2.01-4.31,2.87-.43.26-.89.51-1.35.75-3.2,1.65-6.45,2.8-9.68,3.54-2.31.54-4.63.87-6.9,1.03-8.41.62-16.36-1.04-22.52-3-.9-.3-1.79-.59-2.68-.92-15.05-5.45-26.88-15.05-33.5-27.26-2.37-4.37-4.06-9.12-5.05-14.04-.61-2.99-.95-6.05-1.04-9.13l-3.46-1.21c.02,3.67.4,7.35,1.11,10.93,1.06,5.29,2.86,10.39,5.4,15.09,7.44,13.71,21,24.29,38.16,29.81.77.24,1.54.49,2.35.72,7.68,2.19,17.5,3.66,27.59,1.63,2.13-.41,4.26-.99,6.37-1.75,1.82-.65,3.63-1.43,5.43-2.36,1.61-.83,3.19-1.82,4.72-2.92,1.35-.97,2.67-2.05,3.93-3.21h.01c3.24-2.99,6.1-6.49,8.32-10.3,4.36-7.44,5.98-15.42,4.58-22.45-1.95-9.7-12.78-13.38-21.49-16.33ZM326.31,134.24c-.61-.35-1.22-.67-1.84-.97-4.1-2.06-8.04-3.31-11.62-4-1.79-4.2-4.9-8.32-9.46-12.57-2.18-2.05-4.59-3.85-7.18-5.39-2.41-1.42-4.96-2.59-7.66-3.54-.39-.14-.77-.28-1.17-.4-10.3-3.52-20.15-5.37-29.42-5.53-1.98-.04-3.95,0-5.88.12-1.5.09-2.96.23-4.43.41-.51.07-1.02.14-1.51.21-2.03.3-4.01.69-5.98,1.18-2.06.5-4.08,1.12-6.07,1.84-4.9,1.74-9.54,4.09-13.88,7.02-4.41,2.99-8.02,6.25-10.89,9.32-2.87,3.06-5.01,5.92-6.48,8.14l3.32,1.26c1.43-2.09,3.43-4.69,6.04-7.43,2.66-2.8,5.96-5.74,9.95-8.44,2.73-1.85,5.58-3.44,8.53-4.79,1.91-.87,3.87-1.63,5.88-2.3,3.69-1.21,7.53-2.07,11.51-2.57h.01c1.86-.24,3.74-.39,5.66-.47,7.84-.32,16.18.69,24.9,3.01,2.9.74,5.81,1.65,8.78,2.69,5.15,1.8,9.74,4.56,13.64,8.2,3.43,3.18,5.98,6.31,7.68,9.42-2.51-.26-4.76-.29-6.65-.18-1.11.03-2.09.13-2.93.23-4.37.55-8.25,3.03-10.41,6.65-2.68,4.49-2.63,10.12.13,15.06,2.49,4.48,6.69,7.3,10.96,7.36,2.99.04,7.38-1.22,11.14-7.52,3.41-5.75,4.46-11.52,3.14-17.2,2.67.67,5.55,1.67,8.51,3.14.67.32,1.34.68,2.01,1.05,9.9,5.49,19,16.17,26.09,29.91.11-.39.23-.79.33-1.17.15-.5.28-1,.41-1.5.24-.89.44-1.79.62-2.68-6.82-13.34-15.09-21.62-25.79-27.56ZM308.02,148.5c-2.31,3.86-5.12,5.88-8.13,5.84-1.3-.01-2.59-.42-3.81-1.13-1.65-.98-3.11-2.5-4.21-4.47-2.15-3.87-2.22-8.2-.18-11.62,1.63-2.71,4.57-4.59,7.89-5,.81-.1,1.76-.19,2.81-.24,2.2-.12,4.9-.04,7.95.42,1.76,5.34.97,10.66-2.33,16.2Z"/>
<path class="cls-5" d="M171.67,165.78c-.32,0-.32.25-.33.89l.14,7.91.38,12.18.36,11.11-8.66-1.46,2.23,12.7.64,3.38c.27,1.37.3,1.48.62,1.43.06-.01.12-.02.15-.05.16-.1.12-.39-.03-1.51l-2.27-14.6,8.51,1.49-.94-24.69-.3-6.21-.11-1.69c-.05-.64-.07-.88-.38-.86Z"/>
<path class="cls-5" d="M218.8,38.43l-.1.03c-.13.04-.19.15-.29.37l-5.52,12.6-13.29,31.09-10.91-2.79-11.05,31.62-3.18,9.27-.61,1.84-.43,1.33c-.06.22-.15.51.14.6.1.03.18.02.24-.02.11-.07.18-.23.24-.37l4.51-12.33,10.87-30.57,10.85,2.77,13.51-32.07,3.93-9.47,1.3-3.25c.09-.24.12-.34.06-.47l-.06-.11-.22-.07Z"/>
<path class="cls-5" d="M149.66,128.4l-1.41-1.22h0c-1.4-1.2-3.11-2.68-5.17-4.51-3.81-3.38-9.65-8.79-15.98-15.46l3.3-3.15-16.03-16.25-4.71-4.7-1.3-1.25c-.5-.47-.66-.62-.89-.41-.23.22-.08.38.39.9l5.82,6.14,15.09,15.53-3.29,3.14.39.42c7.04,7.44,13.4,13.11,16.57,15.82,1.92,1.65,3.8,3.21,5.3,4.4l1.47,1.14c.49.37.68.51.85.41.03-.02.07-.05.1-.09.2-.24.05-.38-.51-.87Z"/>
<path class="cls-5" d="M136.38,131.07c-8.29-6.98-12.66-10.5-12.99-10.47h-.13s-.08.11-.08.11c-.23.28-.37.44,12.46,11.24,3.37,2.84,6.55,5.48,8.96,7.44,3.42,2.78,3.91,3.15,4.14,3.02.04-.02.07-.06.1-.1.23-.28.37-.44-12.46-11.24Z"/>
<path class="cls-5" d="M193.59,102.44l-.12.02s-.02,0-.03.01c-.56.34-4.74,6.49-8.19,11.65-7.98,11.94-7.79,12.06-7.5,12.26h0c.06.04.12.08.24,0,.47-.28,1.9-2.17,8.22-11.62,3.53-5.28,7.71-11.65,7.63-12.06l-.03-.13-.21-.14Z"/>
<path class="cls-5" d="M167.06,109.15l-.26-.04-.1.07c-.33.24-1.04,4.44-1.57,7.91-1.23,8.06-.98,8.1-.66,8.15.05,0,.11.02.16-.02.27-.16.62-1.29,1.64-7.96.53-3.48,1.11-7.69.87-8.02l-.08-.11Z"/>
<path class="cls-5" d="M135.11,83.75l-.12-.04-.23.11-.04.12c-.11.32,3.32,7.82,10.19,22.3,2.78,5.86,5.43,11.36,7.45,15.49q3.26,6.66,3.63,6.49s.05-.02.07-.04c.28-.17.09-.6-2.8-6.88-1.92-4.17-4.52-9.7-7.3-15.56-6.88-14.48-10.52-21.88-10.84-21.99Z"/>
<path class="cls-5" d="M180.77,168.84c-2.86-1.8-4.46-2.68-4.76-2.61l-.12.03-.14.21.02.13c.02.13.09.46,4.39,3.22,2.86,1.8,4.46,2.68,4.76,2.61l.14-.04.13-.21-.02-.13c-.06-.32-1.54-1.4-4.39-3.22Z"/>
<path class="cls-3" d="M229.16,155.58l.8-2.82c.21.05,5.24.46,12.03,1.13l-1,2.94c-6.8-.76-11.77-1.24-11.83-1.25Z"/>
<path class="cls-8" d="M218.48,155.46l2.23-7.43,7.4,2.24c1.35.41,2.31,1.47,2.63,2.75.18.67.18,1.39-.03,2.08-.06.2-.14.39-.22.57-.81,1.68-2.76,2.59-4.61,2.03l-7.4-2.24Z"/>
<polygon class="cls-9" points="134.68 264.31 134.14 262.21 135.6 260.61 133.5 261.15 131.89 259.69 132.44 261.79 130.98 263.4 133.08 262.85 134.68 264.31"/>
<polygon class="cls-1" points="91.52 324.83 90.42 320.62 93.35 317.4 89.14 318.5 85.92 315.56 87.02 319.78 84.08 323 88.3 321.9 91.52 324.83"/>
<path class="cls-2" d="M111.56,249.99c-.46.28-.89-.43-.43-.71s.89.43.43.71Z"/>
<path class="cls-2" d="M106.99,311.38c-.12.25-.28.41-.56.47-.33.06-.54-.28-.33-.55.17-.22.4-.29.67-.28.19,0,.3.19.22.36Z"/>
<path class="cls-2" d="M146.16,319.4c-.43.26-.82-.4-.4-.66s.82.4.4.66Z"/>
<path class="cls-2" d="M159.31,296.49c-.46.28-.89-.43-.43-.71s.89.43.43.71Z"/>
<path class="cls-2" d="M128.85,281.56c-.47.29-.92-.45-.44-.74s.92.45.44.74Z"/>
<path class="cls-2" d="M123.22,276.64c-.43.26-.84-.41-.41-.67s.84.41.41.67Z"/>
<path class="cls-2" d="M132.58,243.24c-.62.38-1.2-.59-.58-.96s1.2.59.58.96Z"/>
<path class="cls-2" d="M100.05,266.35c-.24-.04-.4-.18-.59-.32-.21-.16.04-.43.25-.35.2.08.38.16.52.33.13.15.02.38-.19.34Z"/>
<path class="cls-2" d="M107.16,271.81c-.11-.04-.22-.09-.34-.13-.13-.05-.09-.23.05-.22.12.01.24.03.36.04.2.02.11.37-.07.3Z"/>
<path class="cls-2" d="M89.51,280.65c-.07,0-.15,0-.22.01-.27.02-.47-.31-.33-.55.04-.06.07-.13.11-.19.19-.31.65-.35.85-.02s-.05.72-.41.74Z"/>
<path class="cls-2" d="M72.13,295.29c-.83.5-1.6-.78-.77-1.28.83-.5,1.6.78.77,1.28Z"/>
<path class="cls-2" d="M102.08,291.43c.02.06.04.12.05.18.08.29-.28.5-.5.3-.05-.04-.09-.08-.13-.13-.16-.16-.14-.43.06-.55s.45-.01.52.2Z"/>
<path class="cls-2" d="M109.11,302.25c-.37.22-.71-.35-.35-.57s.71.35.35.57Z"/>
<path class="cls-2" d="M74.96,267.09c-.56.34-1.08-.53-.52-.86s1.08.53.52.86Z"/>
<path class="cls-2" d="M78.59,264.62c.04.13.02.27-.04.39-.11.21-.35.31-.58.26-.34-.08-.45-.46-.34-.73-.07-.12-.06-.29.09-.39.28-.18.65,0,.77.29.05.05.08.12.1.19Z"/>
<path class="cls-2" d="M90.68,237.87c-.1.42-.77.58-.98.15,0,0,0,0,0,0-.02-.01-.05-.02-.07-.04-.33-.17-.2-.7.17-.7.03,0,.05,0,.08,0h0c.39-.29.9.16.79.58Z"/>
<path class="cls-2" d="M75.61,311.91c-.07,0-.15,0-.22.01-.27.02-.47-.31-.33-.55.04-.06.07-.13.11-.19.19-.31.65-.35.85-.02s-.05.72-.41.74Z"/>
<path class="cls-2" d="M54.14,277.89c-.07,0-.15,0-.22.01-.27.02-.47-.31-.33-.55.04-.06.07-.13.11-.19.19-.31.65-.35.85-.02s-.05.72-.41.74Z"/>
<polygon class="cls-2" points="280.37 190.01 281.57 192.38 280.23 194.69 282.61 193.49 284.91 194.82 283.71 192.45 285.05 190.15 282.67 191.35 280.37 190.01"/>
<polygon class="cls-1" points="269.83 121.73 271.98 126 269.58 130.13 273.85 127.98 277.98 130.38 275.83 126.11 278.23 121.98 273.96 124.13 269.83 121.73"/>
<path class="cls-2" d="M304.34,202.61c.48-.45,1.18.29.7.74s-1.18-.29-.7-.74Z"/>
<path class="cls-2" d="M282.45,138.43c.08-.33.23-.57.55-.7.38-.16.72.2.54.57-.15.31-.4.44-.73.51-.23.04-.41-.15-.36-.38Z"/>
<path class="cls-2" d="M249.66,141.01c.44-.42,1.09.27.65.69s-1.09-.27-.65-.69Z"/>
<path class="cls-2" d="M247.71,169.5c.48-.45,1.18.29.7.74-.48.45-1.18-.29-.7-.74Z"/>
<path class="cls-2" d="M263.97,179.79c.49-.47,1.22.3.72.77-.49.47-1.22-.3-.72-.77Z"/>
<path class="cls-2" d="M271.99,184.23c.45-.43,1.11.27.66.7s-1.11-.27-.66-.7Z"/>
<path class="cls-2" d="M286.04,96.63c.53-.5,1.3.32.77.82s-1.3-.32-.77-.82Z"/>
<path class="cls-2" d="M302.39,190.57c.29-.01.53.11.79.24.29.14.06.53-.21.48-.26-.05-.5-.09-.71-.26-.19-.15-.13-.45.13-.45Z"/>
<path class="cls-2" d="M292.46,185.86c.15.02.29.05.44.07.17.03.17.26,0,.27-.15.01-.29.03-.44.04-.25.02-.23-.42.01-.38Z"/>
<path class="cls-2" d="M332.61,123.71c.09-.02.17-.05.26-.07.32-.09.65.25.54.57-.03.08-.06.17-.09.25-.14.43-.69.58-1.01.24s-.13-.88.3-1Z"/>
<path class="cls-2" d="M328.36,148.7c.86-.81,2.12.52,1.26,1.34-.86.81-2.12-.52-1.26-1.34Z"/>
<path class="cls-2" d="M293.48,161.06c-.04-.07-.07-.13-.11-.2-.17-.33.2-.67.52-.49.06.04.13.08.19.12.23.15.28.48.07.68s-.53.13-.67-.11Z"/>
<path class="cls-2" d="M282.27,149.92c.38-.36.94.23.56.6s-.94-.23-.56-.6Z"/>
<path class="cls-2" d="M332.25,183.21c.58-.55,1.43.35.85.9s-1.43-.35-.85-.9Z"/>
<path class="cls-2" d="M328.54,187.1c-.08-.15-.09-.32-.05-.48.08-.27.34-.46.63-.46.43.01.65.44.6.79.11.13.15.33,0,.49-.29.29-.77.17-1-.14-.07-.05-.13-.12-.17-.2Z"/>
<path class="cls-2" d="M257.12,125.94c.02-.53.77-.9,1.13-.43,0,0,0,0,0,0,.03,0,.06.02.09.03.44.12.42.78-.03.88-.03,0-.06.01-.09.02h0c-.39.45-1.12.03-1.1-.5Z"/>
<path class="cls-2" d="M318.93,121.66c.09-.02.17-.05.26-.07.32-.09.65.25.54.57-.03.08-.06.17-.09.25-.14.43-.69.58-1.01.24-.32-.34-.13-.88.3-1Z"/>
<path class="cls-2" d="M340.29,169.77c.09-.02.17-.05.26-.07.32-.09.65.25.54.57-.03.08-.06.17-.09.25-.14.43-.69.58-1.01.24s-.13-.88.3-1Z"/>
<path class="cls-6" d="M183.72,155.7s.08-.37.28-1.02c.2-.65.51-1.59.9-2.75.78-2.31,1.9-5.49,3.17-8.99,1.26-3.47,2.41-6.61,3.26-8.96.41-1.1.75-2.01,1-2.71.24-.64.39-.99.41-.98s-.08.37-.28,1.02c-.2.65-.51,1.59-.9,2.75-.78,2.31-1.9,5.49-3.17,9-1.26,3.47-2.41,6.61-3.26,8.96-.41,1.1-.75,2.01-1,2.71-.24.64-.39.99-.41.98Z"/>
<path class="cls-8" d="M221.36,145.91l-.65,2.12-2.24,7.43-.83,2.75c-.78,2.62-3.55,4.1-6.16,3.32l-7.56-2.29c-1.59,1.28-3.6,1.98-5.7,1.88-.27,0-.55-.03-.83-.07-.43-.05-.85-.15-1.27-.28l-2.17-.65-2.03-.62-8.31-2.51-.49,1.38c-.44,1.23-1.79,1.87-3.01,1.44-1.23-.44-1.87-1.8-1.45-3.02l10.77-30.28c.44-1.22,1.8-1.87,3.02-1.43,1.23.44,1.87,1.8,1.45,3.02l-.25.64,8.4,2.53,2.72.82c.24.07.47.15.69.24,2.36.94,4.09,2.86,4.88,5.13l.03-.07,7.68,2.33c2.62.78,4.09,3.54,3.3,6.17Z"/>
<path class="cls-7" d="M134.79,203.8l-2.79,5.14c-1.05,1.93-3.45,2.65-5.38,1.6-1.93-1.05-2.64-3.46-1.59-5.39l2.54-4.66c.48.35,1.01.64,1.56.9l3.64,1.7c.66.3,1.34.54,2.03.7Z"/>
<path class="cls-7" d="M218.48,155.46l2.23-7.43,7.4,2.24c1.35.41,2.31,1.47,2.63,2.75.18.67.18,1.39-.03,2.08-.06.2-.14.39-.22.57-.81,1.68-2.76,2.59-4.61,2.03l-7.4-2.24Z"/>
<path class="cls-6" d="M210.32,137.81s-.05.3-.18.83c-.13.53-.34,1.29-.61,2.23-.53,1.88-1.31,4.46-2.2,7.3-.88,2.82-1.68,5.36-2.28,7.27-.28.89-.52,1.63-.7,2.2-.17.52-.27.8-.29.8-.02,0,.05-.3.18-.83.13-.53.34-1.29.61-2.23.53-1.88,1.31-4.46,2.2-7.3.88-2.82,1.68-5.36,2.28-7.27.28-.89.52-1.63.7-2.2.17-.52.27-.8.29-.8Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

@@ -1,5 +1,5 @@
<template>
<MenuList placement="bottom-end">
<MenuList placement="bottom-start">
<template #trigger="{ open }">
<UiButtonIcon
v-tooltip="{

View File

@@ -38,18 +38,16 @@ type TextMappingByObject = {
type VtsActionModalVmProps = {
object: 'vm'
action: VmActions
vmName: string
hostName?: never
}
type VtsActionModalHostProps = {
object: 'host'
action: HostActions
vmName?: never
hostName: string
}
const { action, object, vmName, hostName } = defineProps<
const { action, object, hostName } = defineProps<
(VtsActionModalVmProps | VtsActionModalHostProps) & {
accent: ModalAccent
icon: IconName
@@ -71,22 +69,22 @@ const { t } = useI18n()
const textMappingsByObject: TextMappingByObject = {
vm: {
'force-reboot': {
title: t('modal:confirm-vm-force-reboot?', { vm: vmName }),
title: t('modal:confirm-vm-force-reboot'),
message: t('modal:vm-force-reboot-message'),
action: t('modal:action:vm-force-reboot'),
},
'force-shutdown': {
title: t('modal:confirm-vm-force-shutdown?', { vm: vmName }),
title: t('modal:confirm-vm-force-shutdown'),
message: t('modal:vm-force-shutdown-message'),
action: t('modal:action:vm-force-shutdown'),
},
reboot: {
title: t('modal:confirm-vm-reboot?', { vm: vmName }),
title: t('modal:confirm-vm-reboot'),
message: t('modal:vm-reboot-message'),
action: t('modal:action:vm-reboot'),
},
shutdown: {
title: t('modal:confirm-vm-shutdown?', { vm: vmName }),
title: t('modal:confirm-vm-shutdown'),
message: t('modal:vm-shutdown-message'),
action: t('modal:action:vm-shutdown'),
},

View File

@@ -1,7 +1,11 @@
<template>
<UiModal accent="warning" icon="status:warning-picto" @confirm="emit('confirm')" @dismiss="emit('cancel')">
<template #title>
{{ t('confirm-delete', { name: subject }) }}
<I18nT keypath="confirm-delete" scope="global" tag="div">
<span class="n-delete">
{{ subject }}
</span>
</I18nT>
</template>
<template #content>
@@ -36,3 +40,9 @@ const emit = defineEmits<{
const { t } = useI18n()
</script>
<style lang="postcss" scoped>
.n-delete {
color: var(--color-warning-item-base);
}
</style>

View File

@@ -1,11 +0,0 @@
<template>
<svg aria-hidden="true" viewBox="0 0 397.13 396.85">
<use :href />
</svg>
</template>
<script lang="ts" setup>
import notFoundUrl from '@core/assets/not-found.svg'
const href = `${notFoundUrl}#not-found-illustration`
</script>

View File

@@ -1,11 +0,0 @@
<template>
<svg aria-hidden="true" viewBox="0 0 397.13 396.85">
<use :href />
</svg>
</template>
<script lang="ts" setup>
import offlineUrl from '@core/assets/offline.svg'
const href = `${offlineUrl}#offline-illustration`
</script>

View File

@@ -4,8 +4,6 @@
class="vts-state-hero"
>
<UiLoader v-if="type === 'busy'" class="loader" />
<OfflineIllustration v-else-if="type === 'offline'" class="image" />
<NotFoundIllustration v-else-if="type === 'not-found'" class="image" />
<img v-else-if="imageSrc" :src="imageSrc" :alt="type" class="image" />
<div v-if="slots.default || success" :class="[typoClass, { mobile: isMobile }]" class="content">
<div v-if="success">{{ t('all-good!') }}</div>
@@ -22,14 +20,12 @@
</template>
<script lang="ts" setup>
import NotFoundIllustration from '@core/components/state-hero/NotFoundIllustration.vue'
import UiLoader from '@core/components/ui/loader/UiLoader.vue'
import { useUiStore } from '@core/stores/ui.store.ts'
import type { StateHeroFormat, StateHeroSize, StateHeroType } from '@core/types/state-hero.type.ts'
import { toVariants } from '@core/utils/to-variants.util.ts'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import OfflineIllustration from './OfflineIllustration.vue'
import VtsIcon from '../icon/VtsIcon.vue'
const { format, type, size, horizontal } = defineProps<{
@@ -61,7 +57,7 @@ const error = computed(() => type === 'error')
const success = computed(() => type === 'all-good' || type === 'all-done')
const imageSrc = computed(() => {
if (type === 'busy' || type === 'not-found') {
if (type === 'busy') {
return undefined
}

View File

@@ -203,17 +203,6 @@ export const statusIcons = defineIconPack({
size: 10,
},
],
'primary-circle-disabled': [
{
icon: faCircle,
color: 'var(--color-neutral-txt-secondary)',
},
{
icon: faStar,
color: 'var(--color-neutral-background-primary)',
size: 10,
},
],
'force-circle': [
{
icon: faCircle,

View File

@@ -653,10 +653,10 @@
"modal:confirm-host-enable?": "Opravdu chcete {host} povolit?",
"modal:confirm-host-shutdown?": "Opravdu chcete {host} vypnout?",
"modal:confirm-host-start?": "Opravdu chcete {host} spustit?",
"modal:confirm-vm-force-reboot?": "Potvrďte vynucený restart virt. stroje",
"modal:confirm-vm-force-shutdown?": "Potvrďte vynucení vypnutí virt. stroje",
"modal:confirm-vm-reboot?": "Potvrďte restart virt. stroje",
"modal:confirm-vm-shutdown?": "Potvrďte vypnutí virt. stroje",
"modal:confirm-vm-force-reboot": "Potvrďte vynucený restart virt. stroje",
"modal:confirm-vm-force-shutdown": "Potvrďte vynucení vypnutí virt. stroje",
"modal:confirm-vm-reboot": "Potvrďte restart virt. stroje",
"modal:confirm-vm-shutdown": "Potvrďte vypnutí virt. stroje",
"modal:host-disable-message": "Hostitel bude označen jako nedostupný pro spouštění nebo přijímání nových virt. strojů. Už běžící virt. stroje tím nebudou ovlivněny.",
"modal:host-enable-message": "Hostitel se stane znovu dostupným pro spouštění nebo přijímání virt. strojů.",
"modal:host-shutdown-message": "Toto hostitele vypne. Jakékoli virt. stroje které na něm jsou pořád ještě spuštěné budou okamžitě vypnuty (pokud jste je předtím neodstěhovali).",

View File

@@ -522,10 +522,10 @@
"modal:action:vm-force-shutdown": "@:action:force-shutdown",
"modal:action:vm-reboot": "@:action:reboot VM",
"modal:action:vm-shutdown": "@:action:shutdown VM",
"modal:confirm-vm-force-reboot?": "Erzwungenen Neustart der VM bestätigen",
"modal:confirm-vm-force-shutdown?": "Erzwungenes Herunterfahren der VM bestätigen",
"modal:confirm-vm-reboot?": "VM Neustart bestätigen",
"modal:confirm-vm-shutdown?": "VM Abschaltung bestätigen",
"modal:confirm-vm-force-reboot": "Erzwungenen Neustart der VM bestätigen",
"modal:confirm-vm-force-shutdown": "Erzwungenes Herunterfahren der VM bestätigen",
"modal:confirm-vm-reboot": "VM Neustart bestätigen",
"modal:confirm-vm-shutdown": "VM Abschaltung bestätigen",
"modal:vm-force-reboot-message": "Dadurch wird die virtuelle Maschine sofort ohne sauberes Herunterfahren neu gestartet. Es kann zu Datenverlust kommen und laufende Prozesse werden unterbrochen.",
"modal:vm-force-shutdown-message": "Dadurch wird die virtuelle Maschine sofort ohne sauberes Herunterfahren ausgeschaltet. Es kann zu Datenverlust kommen und laufende Prozesse werden unterbrochen.",
"modal:vm-reboot-message": "Startet die virtuelle Maschine ordnungsgemäß neu, indem sie heruntergefahren und erneut gestartet wird. Laufende Prozesse werden ordnungsgemäß geschlossen.",

View File

@@ -20,7 +20,6 @@
"action:boot-vm": "Boot VM",
"action:change-state": "Change state",
"action:close": "Close",
"action:confirm": "Confirm",
"action:connect": "Connect",
"action:connect-another-pool": "Connect another pool",
"action:connect-n": "Connect {n}",
@@ -131,7 +130,6 @@
"action:select-storage": "Select a storage",
"action:send": "Send",
"action:send-ctrl-alt-del": "Send Ctrl+Alt+Del",
"action:set-pif-management": "Set management PIF",
"action:show-less": "Show less",
"action:show-more": "Show more",
"action:shutdown": "Shutdown",
@@ -276,12 +274,11 @@
"configure-for-protected": "To ensure your VMs protection, consider configuring a {backup-job}",
"configure-in-xo-5": "Configure in XO 5",
"confirm-cancel?": "Are you sure you want to cancel?",
"confirm-delete": "You're about to delete {name}",
"confirm-delete": "You're about to delete {0}",
"confirm-disconnect": "You're about to disconnect {0}",
"confirm-vm-revert": "Confirm VM revert",
"connect": "Connect",
"connected": "Connected",
"connected-srs": "Connected SRs",
"connected-to-ip": "Connected to {ip}",
"connecting": "Connecting",
"connection": "Connection",
@@ -554,10 +551,6 @@
"job:pbd-plug:pbd-connected": "One or several PBDs are already connected",
"job:pbd-unplug:missing-pbd": "No PBDs to disconnect",
"job:pbd-unplug:pbd-disconnected": "One or several PBDs are already disconnected",
"job:pif-management-reconfigure:bad-power-state": "Host is not running",
"job:pif-management-reconfigure:current-pif": "Current PIF",
"job:pif-management-reconfigure:in-progress": "Management interface reconfiguration in progress…",
"job:pif-management-reconfigure:missing-ip": "IP address missing",
"job:server-connect:missing-server": "No servers to connect",
"job:server-disconnect:missing-server": "No servers to disconnect",
"job:server-disconnect:server-disconnected": "One or several servers are already disconnected",
@@ -709,10 +702,10 @@
"modal:confirm-host-shutdown?": "Are you sure you want to shut down {host}?",
"modal:confirm-host-smart-reboot?": "Are you sure you want to smart reboot {host}?",
"modal:confirm-host-start?": "Are you sure you want to start {host}?",
"modal:confirm-vm-force-reboot?": "Are you sure you want to force reboot {vm}?",
"modal:confirm-vm-force-shutdown?": "Are you sure you want to force shut down {vm}?",
"modal:confirm-vm-reboot?": "Are you sure you want to reboot {vm}?",
"modal:confirm-vm-shutdown?": "Are you sure you want to shut down {vm}?",
"modal:confirm-vm-force-reboot": "Confirm VM force reboot",
"modal:confirm-vm-force-shutdown": "Confirm VM force shutdown",
"modal:confirm-vm-reboot": "Confirm VM reboot",
"modal:confirm-vm-shutdown": "Confirm VM shutdown",
"modal:host-detach-message": "This host will be detached from the pool and its local storage reset. Make sure no VMs are still running on it and that anything you need from its local storage has been backed up.",
"modal:host-disable-and-evacuate-vms-message": "The host will be disabled and its running VMs will be automatically migrated to other hosts in the pool, with no interruption as long as resources allow.",
"modal:host-disable-message": "The host will be marked unavailable for starting or receiving new VMs. Already-running VMs are not affected.",
@@ -723,8 +716,6 @@
"modal:host-restart-toolstack-message": "The host's management service (toolstack) is about to restart. Running VMs won't be interrupted, but they'll be temporarily unmanageable (a few minutes).",
"modal:host-shutdown-message": "This will power off the host. Any VMs still running on it will be migrated to another running host.",
"modal:host-smart-reboot-message": "All running VMs on this host will be suspended, the host will reboot, then the VMs will automatically resume. This causes temporary downtime. If Xen Orchestra itself runs on this host, VMs may not resume automatically.",
"modal:pif-management-reconfigure-info": "Setting this PIF as the management PIF will make it the default PIF used for management traffic. Ensure this PIF is properly configured and reachable before proceeding.",
"modal:pif-management-reconfigure-title": "You are about to set this as the management PIF",
"modal:pool-disconnect-message": "The pool will no longer be manageable from XO. You can reconnect it later.",
"modal:vm-force-reboot-message": "This will immediately reboot the virtual machine without a clean shutdown. Data loss may occur and ongoing processes will be interrupted.",
"modal:vm-force-shutdown-message": "This will immediately power off the virtual machine without a clean shutdown. Data loss may occur and ongoing processes will be interrupted.",
@@ -842,7 +833,6 @@
"no-selected-vm-can-be-migrated": "No selected VM can be migrated",
"no-server-detected": "No servers detected",
"no-snapshot-detected": "No snapshots detected",
"no-sr-connected": "No SRs connected",
"no-storage-repository-detected": "No storage repositories detected",
"no-task": "No tasks",
"no-task-detected": "No tasks detected",

View File

@@ -514,10 +514,10 @@
"modal:action:vm-force-shutdown": "@:action:force-shutdown VM",
"modal:action:vm-reboot": "@:action:reboot VM",
"modal:action:vm-shutdown": "@:action:shutdown VM",
"modal:confirm-vm-force-reboot?": "Confirmar el reinicio forzado de la VM",
"modal:confirm-vm-force-shutdown?": "Confirmar el apagado forzado de la VM",
"modal:confirm-vm-reboot?": "Confirmar reinicio VM",
"modal:confirm-vm-shutdown?": "Confirmar apagado VM",
"modal:confirm-vm-force-reboot": "Confirmar el reinicio forzado de la VM",
"modal:confirm-vm-force-shutdown": "Confirmar el apagado forzado de la VM",
"modal:confirm-vm-reboot": "Confirmar reinicio VM",
"modal:confirm-vm-shutdown": "Confirmar apagado VM",
"modal:vm-force-reboot-message": "Esto reiniciará inmediatamente la máquina virtual sin un apagado limpio. Podrían producirse pérdidas de datos y se interrumpirán los procesos en curso.",
"modal:vm-force-shutdown-message": "Esto apagará inmediatamente la máquina virtual sin un apagado limpio. Podrían producirse pérdidas de datos y se interrumpirán los procesos en curso.",
"modal:vm-reboot-message": "Reinicia la máquina virtual correctamente, apagando y encendiendo de nuevo. Los procesos en ejecución se cerrarán correctamente.",

View File

@@ -20,7 +20,6 @@
"action:boot-vm": "Démarrer la VM",
"action:change-state": "Changer l'état",
"action:close": "Fermer",
"action:confirm": "Confirmer",
"action:connect": "Connecter",
"action:connect-another-pool": "Connecter un autre pool",
"action:connect-n": "Connecter {n}",
@@ -131,7 +130,6 @@
"action:select-storage": "Sélectionner un SR",
"action:send": "Envoyer",
"action:send-ctrl-alt-del": "Envoyer Ctrl+Alt+Suppr",
"action:set-pif-management": "Définir comme PIF de gestion",
"action:show-less": "Voir moins",
"action:show-more": "Voir plus",
"action:shutdown": "Arrêter",
@@ -276,12 +274,11 @@
"configure-for-protected": "Pour garantir la protection de vos VMs, vous pouvez configurer un {backup-job}",
"configure-in-xo-5": "Configurer dans XO 5",
"confirm-cancel?": "Êtes-vous sûr de vouloir annuler ?",
"confirm-delete": "Vous êtes sur le point de supprimer {name}",
"confirm-delete": "Vous êtes sur le point de supprimer {0}",
"confirm-disconnect": "Vous êtes sur le point de déconnecter {0}",
"confirm-vm-revert": "Confirmer la restauration de la VM à l'instantané",
"connect": "Se connecter",
"connected": "Connecté",
"connected-srs": "SRs connectés",
"connected-to-ip": "Connecté à {ip}",
"connecting": "Connexion en cours",
"connection": "Connexion",
@@ -436,7 +433,6 @@
"host-disable-title": "Êtes-vous sûr de vouloir désactiver {host}?",
"host-enable-title": "Êtes-vous sûr de vouloir activer {host}?",
"host-internal-networks": "Réseaux internes de l'hôte",
"host-missing": "L'hôte est manquant",
"host-not-running": "Hôte non démarré",
"host-off": "L’hôte est éteint",
"host:status:active": "@:status:active",
@@ -554,10 +550,6 @@
"job:pbd-plug:pbd-connected": "Un ou plusieurs PBDs sont déjà connectés",
"job:pbd-unplug:missing-pbd": "Aucun PBD à déconnecter",
"job:pbd-unplug:pbd-disconnected": "Un ou plusieurs PBDs sont déjà déconnectés",
"job:pif-management-reconfigure:bad-power-state": "L'hôte n'est pas lancé",
"job:pif-management-reconfigure:current-pif": "PIF actuel",
"job:pif-management-reconfigure:in-progress": "Reconfiguration de l'interface de gestion en cours…",
"job:pif-management-reconfigure:missing-ip": "Le PIF cible n'a pas d'adresse IP configurée",
"job:server-connect:missing-server": "Aucun serveur à connecter",
"job:server-disconnect:missing-server": "Aucun serveur à déconnecter",
"job:server-disconnect:server-disconnected": "Un ou plusieurs serveurs sont déjà déconnectés",
@@ -709,10 +701,11 @@
"modal:confirm-host-shutdown?": "Êtes-vous sûr de vouloir arrêter {host} ?",
"modal:confirm-host-smart-reboot?": "Êtes-vous sûr de vouloir effectuer un redémarrage optimisé de {host} ?",
"modal:confirm-host-start?": "Êtes-vous sûr de vouloir démarrer {host} ?",
"modal:confirm-vm-force-reboot?": "Êtes-vous sûr de vouloir forcer le redémarrage de {vm} ?",
"modal:confirm-vm-force-shutdown?": "Êtes-vous sûr de vouloir forcer l'arrêt de {vm} ?",
"modal:confirm-vm-reboot?": "Êtes-vous sûr de vouloir redémarrer {vm} ?",
"modal:confirm-vm-shutdown?": "Êtes-vous sûr de vouloir arrêter {vm} ?",
"modal:confirm-vm-force-reboot": "Confirmer le redémarrage forcé de la VM",
"modal:confirm-vm-force-shutdown": "Confirmer l'arrêt forcé de la VM",
"modal:confirm-vm-reboot": "Confirmer le redémarrage de la VM",
"host-missing": "L'hôte est manquant",
"modal:confirm-vm-shutdown": "Confirmer l'arrêt de la VM",
"modal:host-detach-message": "Cet hôte sera détaché du pool et son stockage local réinitialisé. Assurez-vous qu'aucune VM n'est encore en cours d'exécution et que tout ce dont vous avez besoin sur son stockage local a été sauvegardé.",
"modal:host-disable-and-evacuate-vms-message": "L'hôte sera désactivé et ses VMs en cours d'exécution seront automatiquement migrées vers d'autres hôtes du pool, sans interruption si les ressources le permettent.",
"modal:host-disable-message": "L'hôte sera marqué comme indisponible pour le démarrage ou la réception de nouvelles VMs. Les VMs déjà en cours d’exécution ne sont pas affectées.",
@@ -723,8 +716,6 @@
"modal:host-restart-toolstack-message": "Le service de gestion de l'hôte (toolstack) est sur le point de redémarrer. Les VMs en cours d'exécution ne seront pas interrompues, mais elles seront temporairement ingérables (quelques minutes).",
"modal:host-shutdown-message": "Cela va éteindre l'hôte. Les VMs encore en cours d'exécution seront migrées vers un autre hôte actif.",
"modal:host-smart-reboot-message": "Toutes les VMs en cours d'exécution sur cet hôte seront suspendues, l'hôte redémarrera, puis les VMs reprendront automatiquement. Cela entraîne une interruption temporaire. Si Xen Orchestra lui-même s'exécute sur cet hôte, les VMs pourraient ne pas reprendre automatiquement.",
"modal:pif-management-reconfigure-info": "Définir ce PIF comme PIF de gestion en fera le PIF par défaut utilisé pour le trafic de gestion. Assurez-vous que ce PIF est correctement configuré et accessible avant de continuer.",
"modal:pif-management-reconfigure-title": "Vous êtes sur le point de le définir comme PIF de gestion",
"modal:pool-disconnect-message": "Le pool ne pourra plus être géré depuis XO. Vous pourrez le reconnecter ultérieurement.",
"modal:vm-force-reboot-message": "Cela redémarrera immédiatement la VM sans arrêt propre. Des pertes de données peuvent survenir et les processus en cours seront interrompus.",
"modal:vm-force-shutdown-message": "Cela éteindra immédiatement la VM sans arrêt propre. Des pertes de données peuvent survenir et les processus en cours seront interrompus.",
@@ -842,7 +833,6 @@
"no-selected-vm-can-be-migrated": "Aucune VM sélectionnée ne peut être migrée",
"no-server-detected": "Aucun serveur détecté",
"no-snapshot-detected": "Aucun instantané détecté",
"no-sr-connected": "Aucun SR connecté",
"no-storage-repository-detected": "Aucun dépôt de stockage détecté",
"no-task": "Aucune tâche",
"no-task-detected": "Aucune tâche détectée",

View File

@@ -653,10 +653,10 @@
"modal:confirm-host-enable?": "Weet je zeker dat je {host} wilt inschakelen?",
"modal:confirm-host-shutdown?": "Weet je zeker dat je {host} wilt afsluiten?",
"modal:confirm-host-start?": "Weet je zeker dat je {host} wilt starten?",
"modal:confirm-vm-force-reboot?": "Bevestig gedwongen herstart VM",
"modal:confirm-vm-force-shutdown?": "Bevestig gedwongen afsluiten van VM",
"modal:confirm-vm-reboot?": "Bevestig opnieuw opstarten van VM",
"modal:confirm-vm-shutdown?": "Bevestig afsluiten van VM",
"modal:confirm-vm-force-reboot": "Bevestig gedwongen herstart VM",
"modal:confirm-vm-force-shutdown": "Bevestig gedwongen afsluiten van VM",
"modal:confirm-vm-reboot": "Bevestig opnieuw opstarten van VM",
"modal:confirm-vm-shutdown": "Bevestig afsluiten van VM",
"modal:host-disable-message": "De host wordt gemarkeerd als niet beschikbaar voor het starten of ontvangen van nieuwe VM’s. Dit heeft geen invloed op VM’s die al actief zijn.",
"modal:host-enable-message": "De host wordt weer beschikbaar voor het starten of ontvangen van VM’s.",
"modal:host-shutdown-message": "Hierdoor wordt de host uitgeschakeld. Alle virtuele machines die nog op de host draaien, worden onmiddellijk afgesloten, tenzij je ze eerst hebt gemigreerd.",

View File

@@ -466,10 +466,10 @@
"modal:action:vm-force-shutdown": "@:action:force-shutdown VM",
"modal:action:vm-reboot": "@:action:reboot VM",
"modal:action:vm-shutdown": "@:action:shutdown VM",
"modal:confirm-vm-force-reboot?": "Confirmar reinicialização forçada da VM",
"modal:confirm-vm-force-shutdown?": "Confirmar o encerramento forçado da VM",
"modal:confirm-vm-reboot?": "Confirmar o reinício da VM",
"modal:confirm-vm-shutdown?": "Confirmar o encerramento da VM",
"modal:confirm-vm-force-reboot": "Confirmar reinicialização forçada da VM",
"modal:confirm-vm-force-shutdown": "Confirmar o encerramento forçado da VM",
"modal:confirm-vm-reboot": "Confirmar o reinício da VM",
"modal:confirm-vm-shutdown": "Confirmar o encerramento da VM",
"modal:vm-force-reboot-message": "Isto irá reiniciar imediatamente a máquina virtual sem um encerramento normal. Poderá ocorrer perda de dados e os processos em execução serão interrompidos.",
"modal:vm-force-shutdown-message": "Isto irá desligar imediatamente a máquina virtual sem um encerramento normal. Poderá ocorrer perda de dados e os processos em execução serão interrompidos.",
"modal:vm-reboot-message": "Reinicia a máquina virtual de forma controlada, desligando-a e ligando-a novamente. Os processos em execução serão encerrados corretamente.",

View File

@@ -466,10 +466,10 @@
"modal:action:vm-force-shutdown": "@:action:force-shutdown VM",
"modal:action:vm-reboot": "@:action:reboot VM",
"modal:action:vm-shutdown": "@:action:shutdown VM",
"modal:confirm-vm-force-reboot?": "Confirmar reinicialização forçada da VM",
"modal:confirm-vm-force-shutdown?": "Confirmar o encerramento forçado da VM",
"modal:confirm-vm-reboot?": "Confirmar o reinício da VM",
"modal:confirm-vm-shutdown?": "Confirmar o encerramento da VM",
"modal:confirm-vm-force-reboot": "Confirmar reinicialização forçada da VM",
"modal:confirm-vm-force-shutdown": "Confirmar o encerramento forçado da VM",
"modal:confirm-vm-reboot": "Confirmar o reinício da VM",
"modal:confirm-vm-shutdown": "Confirmar o encerramento da VM",
"modal:vm-force-reboot-message": "Isto irá reiniciar imediatamente a máquina virtual sem um encerramento normal. Poderá ocorrer perda de dados e os processos em execução serão interrompidos.",
"modal:vm-force-shutdown-message": "Isto irá desligar imediatamente a máquina virtual sem um encerramento normal. Poderá ocorrer perda de dados e os processos em execução serão interrompidos.",
"modal:vm-reboot-message": "Reinicia a máquina virtual de forma controlada, desligando-a e ligando-a novamente. Os processos em execução serão encerrados corretamente.",

View File

@@ -653,10 +653,10 @@
"modal:confirm-host-enable?": "Naozaj chcete {host} povoliť?",
"modal:confirm-host-shutdown?": "Naozaj chcete {host} vypnúť?",
"modal:confirm-host-start?": "Naozaj chcete {host} spustiť?",
"modal:confirm-vm-force-reboot?": "Potvrďte vynútený reštart VM",
"modal:confirm-vm-force-shutdown?": "Potvrďte vynútené vypnutie VM",
"modal:confirm-vm-reboot?": "Potvrďte reštart VM",
"modal:confirm-vm-shutdown?": "Potvrďte vypnutie VM",
"modal:confirm-vm-force-reboot": "Potvrďte vynútený reštart VM",
"modal:confirm-vm-force-shutdown": "Potvrďte vynútené vypnutie VM",
"modal:confirm-vm-reboot": "Potvrďte reštart VM",
"modal:confirm-vm-shutdown": "Potvrďte vypnutie VM",
"modal:host-disable-message": "Hostiteľ bude označený ako nedostupný na spúšťanie alebo prijímanie nových virt. strojov. Už bežiaci virt. stroje tým nebudú ovplyvnené.",
"modal:host-enable-message": "Hostiteľ sa stane znova dostupným na spúšťanie alebo prijímanie virt. strojov.",
"modal:host-shutdown-message": "Toto hostiteľa vypne. Akékoľvek virt. stroje ktoré na ňom sú stále ešte spustené budú okamžite vypnuté (ak ste ich predtým neodsťahovali).",

View File

@@ -557,10 +557,10 @@
"modal:action:vm-force-shutdown": "@:action:force-shutdown VM",
"modal:action:vm-reboot": "@:action:reboot VM",
"modal:action:vm-shutdown": "@:action:shutdown VM",
"modal:confirm-vm-force-reboot?": "Bekräfta tvingad omstart av VM",
"modal:confirm-vm-force-shutdown?": "Bekräftaa tvingad nedstängning av VM",
"modal:confirm-vm-reboot?": "Bekräfta omstart av VM",
"modal:confirm-vm-shutdown?": "Bekräfta nedstängning av VM",
"modal:confirm-vm-force-reboot": "Bekräfta tvingad omstart av VM",
"modal:confirm-vm-force-shutdown": "Bekräftaa tvingad nedstängning av VM",
"modal:confirm-vm-reboot": "Bekräfta omstart av VM",
"modal:confirm-vm-shutdown": "Bekräfta nedstängning av VM",
"modal:vm-force-reboot-message": "Detta kommer omedelbart startar om den virtuella maskinen utan en korrekt nedstängning. Dataförlust kan ske och pågående processer kommer att avbrytas.",
"modal:vm-force-shutdown-message": "Detta kommer omedelbart att stänga av den virtuella maskinen utan en korrekt nedstängning. Dataförlust kan ske och pågående processer kommer att avbrytas.",
"modal:vm-reboot-message": "Startar om den virtuella maskinen korrekt genom att stänga ned den och starta upp den igen. Processer stängs ner korrekt.",

View File

@@ -565,10 +565,10 @@
"modal:action:vm-force-shutdown": "@:action:force-shutdown",
"modal:action:vm-reboot": "@:action:reboot",
"modal:action:vm-shutdown": "@:action:shutdown",
"modal:confirm-vm-force-reboot?": "确认强制重启虚拟机",
"modal:confirm-vm-force-shutdown?": "确认强制关闭虚拟机",
"modal:confirm-vm-reboot?": "确认重启虚拟机",
"modal:confirm-vm-shutdown?": "确认关闭虚拟机",
"modal:confirm-vm-force-reboot": "确认强制重启虚拟机",
"modal:confirm-vm-force-shutdown": "确认强制关闭虚拟机",
"modal:confirm-vm-reboot": "确认重启虚拟机",
"modal:confirm-vm-shutdown": "确认关闭虚拟机",
"modal:vm-force-reboot-message": "此操作直接重启虚拟机,未保存数据可能丢失。",
"modal:vm-force-shutdown-message": "此操作直接断电关机,极易造成数据损坏。",
"modal:vm-reboot-message": "优雅重启虚拟机,正常保存并关闭业务进程。",

View File

@@ -2,7 +2,6 @@ import { defineColumns } from '@core/packages/table/define-columns.ts'
import { useAddressColumn } from '@core/tables/column-definitions/address-column.ts'
import { useLinkColumn } from '@core/tables/column-definitions/link-column.ts'
import { useSelectItemColumn } from '@core/tables/column-definitions/select-item-column.ts'
import { useStatusColumn } from '@core/tables/column-definitions/status-column.ts'
import { useTagColumn } from '@core/tables/column-definitions/tag-column.ts'
import { useTruncatedTextColumn } from '@core/tables/column-definitions/truncated-text-column.ts'
import { useI18n } from 'vue-i18n'
@@ -15,7 +14,6 @@ export const useHostColumns = defineColumns(() => {
description: useTruncatedTextColumn({ headerLabel: () => t('description') }),
ipAddresses: useAddressColumn({ headerLabel: () => t('management-ip') }),
tags: useTagColumn({ headerLabel: () => t('tags') }),
srStatus: useStatusColumn({ headerLabel: () => t('status') }),
selectItem: useSelectItemColumn(),
}
})

View File

@@ -1,5 +1,4 @@
import { defineColumns } from '@core/packages/table/define-columns.ts'
import { useActionColumn } from '@core/tables/column-definitions/action-column.ts'
import { useAddressColumn } from '@core/tables/column-definitions/address-column.ts'
import { useLinkColumn } from '@core/tables/column-definitions/link-column.ts'
import { useNumberColumn } from '@core/tables/column-definitions/number-column.ts'
@@ -20,6 +19,5 @@ export const usePifColumns = defineColumns(() => {
mac: useAddressColumn({ headerLabel: () => t('mac-address') }),
mode: useTextColumn({ headerLabel: () => t('mode') }),
selectItem: useSelectItemColumn(),
actions: useActionColumn({}),
}
})

View File

@@ -19,11 +19,8 @@
<script setup lang="ts">
import { useXoHostCollection, type FrontXoHost } from '@/modules/host/remote-resources/use-xo-host-collection.ts'
import { getPbdsConnectionStatus } from '@/modules/pbd/utils/xo-pbd.util.ts'
import { useXoPifCollection } from '@/modules/pif/remote-resources/use-xo-pif-collection.ts'
import { getHostIpAddresses } from '@/modules/pif/utils/xo-pif.util.ts'
import { useGetPbdsInScope } from '@/modules/storage-repository/composables/xo-sr-utils.composable.ts'
import type { FrontXoSr } from '@/modules/storage-repository/remote-resources/use-xo-sr-collection.ts'
import VtsQueryBuilder from '@core/components/query-builder/VtsQueryBuilder.vue'
import VtsRow from '@core/components/table/VtsRow.vue'
import VtsTable from '@core/components/table/VtsTable.vue'
@@ -35,8 +32,6 @@ import { icon, objectIcon } from '@core/icons'
import { useQueryBuilderSchema } from '@core/packages/query-builder/schema/use-query-builder-schema.ts'
import { useQueryBuilderFilter } from '@core/packages/query-builder/use-query-builder-filter.ts'
import { useHostColumns } from '@core/tables/column-sets/host-columns.ts'
import { CONNECTION_STATUS } from '@core/types/connection.ts'
import { SR_SCOPE_TYPE, type SrScope } from '@core/types/storage-repository.type.ts'
import { useStringSchema } from '@core/utils/query-builder/use-string-schema.ts'
import { HOST_POWER_STATE } from '@vates/types'
import { logicAnd, logicNot, logicOr } from '@vueuse/math'
@@ -48,12 +43,10 @@ const {
busy,
hosts: rawHosts,
error,
sr,
} = defineProps<{
hosts: FrontXoHost[]
busy?: boolean
error?: boolean
sr?: FrontXoSr
}>()
const { t } = useI18n()
@@ -92,14 +85,6 @@ const state = useTableState({
const { pageRecords: paginatedHosts, paginationBindings } = usePagination('hosts', filteredHosts)
const { getPbdsInScope } = useGetPbdsInScope()
function getSrConnectionStatus(sr: FrontXoSr, host: FrontXoHost) {
const scope: SrScope = { type: SR_SCOPE_TYPE.HOST, hostId: host.id }
return getPbdsConnectionStatus(getPbdsInScope(sr, scope))
}
function getMasterIcon(host: FrontXoHost) {
if (!isMasterHost(host.id)) {
return undefined
@@ -112,14 +97,10 @@ function getMasterIcon(host: FrontXoHost) {
}
const { HeadCells, BodyCells } = useHostColumns({
exclude: sr === undefined ? ['srStatus'] : [],
body: (host: FrontXoHost) => {
const ipAddresses = computed(() => getHostIpAddresses(host.address, pifsByHost.value.get(host.id)))
const hostIcon = computed(() => objectIcon('host', toLower(host.power_state)))
const rightIcon = computed(() => getMasterIcon(host))
const srConnectionStatus = computed(() =>
sr === undefined ? CONNECTION_STATUS.DISCONNECTED : getSrConnectionStatus(sr, host)
)
return {
host: r =>
@@ -132,7 +113,6 @@ const { HeadCells, BodyCells } = useHostColumns({
description: r => r(host.name_description),
ipAddresses: r => r(ipAddresses.value),
tags: r => r(host.tags),
srStatus: r => r(srConnectionStatus.value),
selectItem: r => r(() => (selectedHostId.value = host.id)),
}
},

View File

@@ -2,7 +2,6 @@
<VtsSidePanel :has-selection="!!host" @close="emit('close')">
<template v-if="host">
<HostInfoCard :host />
<HostSrsCard :host />
<HostNetworkCard :host />
<!-- host licensing -->
<HostSoftwareCard :host />
@@ -17,7 +16,6 @@ import HostHardwareSpecificationsCard from '@/modules/host/components/list/panel
import HostInfoCard from '@/modules/host/components/list/panel/card/HostInfoCard.vue'
import HostNetworkCard from '@/modules/host/components/list/panel/card/HostNetworkCard.vue'
import HostSoftwareCard from '@/modules/host/components/list/panel/card/HostSoftwareCard.vue'
import HostSrsCard from '@/modules/host/components/list/panel/card/HostSrsCard.vue'
import type { FrontXoHost } from '@/modules/host/remote-resources/use-xo-host-collection.ts'
import VtsSidePanel from '@core/components/panel/VtsSidePanel.vue'

View File

@@ -1,74 +0,0 @@
<template>
<UiPanelCard>
<UiCardTitle>
{{ t('connected-srs') }}
<UiCounter :value="srs.length" accent="neutral" size="small" variant="primary" />
</UiCardTitle>
<VtsStateHero v-if="!isReady" format="card" type="busy" size="extra-small" />
<VtsStateHero v-else-if="hasFetchError" format="card" type="error" horizontal size="extra-small">
{{ t('error-no-data') }}
</VtsStateHero>
<UiCollapsibleList v-else-if="srs.length > 0" tag="ul" :total-items="srs.length">
<li v-for="sr in srs" :key="sr.id" v-tooltip class="text-ellipsis">
<UiLink
size="small"
:icon="getSrStatusIcon(sr)"
:to="{
name: '/sr/[id]/general',
params: { id: sr.id },
query: toSrScopeQuery(scope),
}"
>
{{ sr.name_label }}
</UiLink>
</li>
</UiCollapsibleList>
<VtsStateHero v-else type="no-data" format="card" horizontal size="extra-small">
{{ t('no-sr-connected') }}
</VtsStateHero>
</UiPanelCard>
</template>
<script lang="ts" setup>
import type { FrontXoHost } from '@/modules/host/remote-resources/use-xo-host-collection.ts'
import { useXoPbdCollection } from '@/modules/pbd/remote-resources/use-xo-pbd-collection.ts'
import { useGetPbdsInScope, useXoSrUtils } from '@/modules/storage-repository/composables/xo-sr-utils.composable.ts'
import { useXoSrCollection } from '@/modules/storage-repository/remote-resources/use-xo-sr-collection.ts'
import { toSrScopeQuery } from '@/modules/storage-repository/utils/sr-scope.util.ts'
import VtsStateHero from '@core/components/state-hero/VtsStateHero.vue'
import UiCardTitle from '@core/components/ui/card-title/UiCardTitle.vue'
import UiCollapsibleList from '@core/components/ui/collapsible-list/UiCollapsibleList.vue'
import UiCounter from '@core/components/ui/counter/UiCounter.vue'
import UiLink from '@core/components/ui/link/UiLink.vue'
import UiPanelCard from '@core/components/ui/panel-card/UiPanelCard.vue'
import { vTooltip } from '@core/directives/tooltip.directive.ts'
import { SR_SCOPE_TYPE, type SrScope } from '@core/types/storage-repository.type.ts'
import { logicAnd, logicOr } from '@vueuse/math'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
const { host } = defineProps<{
host: FrontXoHost
}>()
const { t } = useI18n()
const { srsByHost, areSrsReady, hasSrFetchError } = useXoSrCollection()
const { arePbdsReady, hasPbdFetchError } = useXoPbdCollection()
const isReady = logicAnd(areSrsReady, arePbdsReady)
const hasFetchError = logicOr(hasSrFetchError, hasPbdFetchError)
const scope = computed<SrScope>(() => ({ type: SR_SCOPE_TYPE.HOST, hostId: host.id }))
const { isConnectedInScope } = useGetPbdsInScope()
const { getSrStatusIcon } = useXoSrUtils(undefined, scope)
const srs = computed(() => {
const hostSrs = srsByHost.value.get(host.id) ?? []
return hostSrs.filter(sr => isConnectedInScope(sr, scope.value))
})
</script>

View File

@@ -1,40 +0,0 @@
<template>
<UiButton
v-tooltip="!canReconfigureManagement && reconfigureManagementErrorMessage"
size="medium"
variant="tertiary"
accent="brand"
:disabled="!canReconfigureManagement"
:busy="isReconfiguringManagement"
@click="openManagementReconfigureModal()"
>
<VtsIcon
v-if="!isReconfiguringManagement"
:name="canReconfigureManagement ? 'status:primary-circle' : 'status:primary-circle-disabled'"
size="medium"
/>
{{ t('action:set-pif-management') }}
</UiButton>
</template>
<script lang="ts" setup>
import { usePifManagementReconfigureModal } from '@/modules/pif/composables/use-pif-management-reconfigure-modal.composable.ts'
import type { FrontXoPif } from '@/modules/pif/remote-resources/use-xo-pif-collection.ts'
import VtsIcon from '@core/components/icon/VtsIcon.vue'
import UiButton from '@core/components/ui/button/UiButton.vue'
import { vTooltip } from '@core/directives/tooltip.directive.ts'
import { useI18n } from 'vue-i18n'
const { pif } = defineProps<{
pif: FrontXoPif
}>()
const { t } = useI18n()
const {
openModal: openManagementReconfigureModal,
canRun: canReconfigureManagement,
isRunning: isReconfiguringManagement,
errorMessage: reconfigureManagementErrorMessage,
} = usePifManagementReconfigureModal(() => pif)
</script>

View File

@@ -27,7 +27,6 @@
<script setup lang="ts">
import { useXoNetworkCollection } from '@/modules/network/remote-resources/use-xo-network-collection.ts'
import { getPoolNetworkRoute } from '@/modules/network/utils/xo-network.util.ts'
import { usePifManagementReconfigureModal } from '@/modules/pif/composables/use-pif-management-reconfigure-modal.composable.ts'
import { type FrontXoPif, useXoPifCollection } from '@/modules/pif/remote-resources/use-xo-pif-collection.ts'
import { getPifStatus } from '@/modules/pif/utils/xo-pif.util.ts'
import VtsRow from '@core/components/table/VtsRow.vue'
@@ -107,7 +106,6 @@ function getManagementIcon(pif: FrontXoPif) {
}
const { HeadCells, BodyCells } = usePifColumns({
exclude: ['selectItem'],
body: (pif: FrontXoPif) => {
const status = computed(() => getPifStatus(pif))
const vlan = computed(() => getVlanData(pif.vlan))
@@ -121,13 +119,6 @@ const { HeadCells, BodyCells } = usePifColumns({
network.value ? getPoolNetworkRoute(network.value.$pool, network.value.id) : undefined
)
const {
openModal: openManagementReconfigureModal,
canRun: canReconfigureManagement,
isRunning: isReconfiguringManagement,
errorMessage: reconfigureManagementErrorMessage,
} = usePifManagementReconfigureModal(() => pif)
return {
network: r =>
network.value
@@ -143,20 +134,7 @@ const { HeadCells, BodyCells } = usePifColumns({
ip: r => r(ip.value),
mac: r => r(pif.mac),
mode: r => r(mode.value),
actions: r =>
r({
onClick: () => (selectedPifId.value = pif.id),
actions: [
{
label: t('action:set-pif-management'),
hint: reconfigureManagementErrorMessage.value,
icon: canReconfigureManagement.value ? 'status:primary-circle' : 'status:primary-circle-disabled',
onClick: () => openManagementReconfigureModal(),
disabled: !canReconfigureManagement.value,
busy: isReconfiguringManagement.value,
},
],
}),
selectItem: r => r(() => (selectedPifId.value = pif.id)),
}
},
})

View File

@@ -1,8 +1,5 @@
<template>
<VtsSidePanel :has-selection="!!pif" class="pif-side-panel" @close="emit('close')">
<template v-if="pif" #actions>
<PifSetManagementButton :pif />
</template>
<template v-if="pif">
<!-- PIF -->
<UiPanelCard>
@@ -228,7 +225,6 @@
<script setup lang="ts">
import { useXoNetworkCollection } from '@/modules/network/remote-resources/use-xo-network-collection.ts'
import { getPoolNetworkRoute } from '@/modules/network/utils/xo-network.util.ts'
import PifSetManagementButton from '@/modules/pif/components/actions/set-management/PifSetManagementButton.vue'
import { type FrontXoPif, useXoPifCollection } from '@/modules/pif/remote-resources/use-xo-pif-collection.ts'
import VtsCardRowKeyValue from '@core/components/card/VtsCardRowKeyValue.vue'
import VtsCardObjectTitle from '@core/components/card-object-title/VtsCardObjectTitle.vue'

View File

@@ -1,32 +0,0 @@
<template>
<UiModal accent="info" icon="status:info-picto" @confirm="emit('confirm')" @dismiss="emit('cancel')">
<template #title>
{{ t('modal:pif-management-reconfigure-title') }}
</template>
<template #content>
{{ t('modal:pif-management-reconfigure-info') }}
</template>
<template #buttons>
<VtsOverlayCancelButton @click="emit('cancel')">{{ t('action:go-back') }}</VtsOverlayCancelButton>
<VtsOverlayConfirmButton>
{{ t('action:confirm') }}
</VtsOverlayConfirmButton>
</template>
</UiModal>
</template>
<script lang="ts" setup>
import VtsOverlayCancelButton from '@core/components/overlay/VtsOverlayCancelButton.vue'
import VtsOverlayConfirmButton from '@core/components/overlay/VtsOverlayConfirmButton.vue'
import UiModal from '@core/components/ui/modal/UiModal.vue'
import { useI18n } from 'vue-i18n'
const emit = defineEmits<{
confirm: []
cancel: []
}>()
const { t } = useI18n()
</script>

View File

@@ -1,34 +0,0 @@
import { useXoPifManagementReconfigureJob } from '@/modules/pif/jobs/xo-pif-management-reconfigure.job.ts'
import type { FrontXoPif } from '@/modules/pif/remote-resources/use-xo-pif-collection.ts'
import { useOverlay } from '@core/packages/overlay/use-overlay.ts'
import { toComputed } from '@core/utils/to-computed.util.ts'
import type { MaybeRefOrGetter } from 'vue'
export function usePifManagementReconfigureModal(rawPif: MaybeRefOrGetter<FrontXoPif | undefined>) {
const pif = toComputed(rawPif)
const { run, canRun, isRunning, errorMessage } = useXoPifManagementReconfigureJob(pif)
const { open } = useOverlay({
component: () => import('@/modules/pif/components/modal/PifManagementReconfigureModal.vue'),
events: {
onConfirm: true,
onCancel: true,
},
})
function openModal() {
return open({
events: {
onConfirm: async () => {
try {
void run()
} catch (error) {
console.error('Error when reconfiguring PIF management interface:', error)
}
},
},
})
}
return { openModal, canRun, isRunning, errorMessage }
}

View File

@@ -1,7 +0,0 @@
import type { FrontXoPif } from '@/modules/pif/remote-resources/use-xo-pif-collection.ts'
import { defineJobArg } from '@core/packages/job'
export const xoPifArg = defineJobArg({
identify: (pif: FrontXoPif) => pif.id,
toArray: false,
})

View File

@@ -1,61 +0,0 @@
import { useXoHostCollection } from '@/modules/host/remote-resources/use-xo-host-collection.ts'
import { xoPifArg } from '@/modules/pif/jobs/xo-pif-args.ts'
import type { FrontXoPif } from '@/modules/pif/remote-resources/use-xo-pif-collection.ts'
import { useXoTaskUtils } from '@/shared/composables/xo-task-utils.composable.ts'
import { fetchPost } from '@/shared/utils/fetch.util.ts'
import { defineJob, JobError, JobRunningError } from '@core/packages/job'
import {
HOST_POWER_STATE,
IP_CONFIGURATION_MODE,
IPV6_CONFIGURATION_MODE,
PRIMARY_ADDRESS_TYPE,
type XoTask,
} from '@vates/types'
import { useI18n } from 'vue-i18n'
function hasIpConfiguration(pif: FrontXoPif) {
return pif.primaryAddressType === PRIMARY_ADDRESS_TYPE.IPV6
? pif.ipv6Mode !== IPV6_CONFIGURATION_MODE.NONE
: pif.mode !== IP_CONFIGURATION_MODE.NONE
}
export const useXoPifManagementReconfigureJob = defineJob('pif.management-reconfigure', [xoPifArg], () => {
const { t } = useI18n()
const { monitorTask } = useXoTaskUtils()
const { getHostById } = useXoHostCollection()
return {
async run(pif: FrontXoPif) {
const { taskId } = await fetchPost<{ taskId: XoTask['id'] }>(
`hosts/${pif.$host}/actions/management_reconfigure`,
{ pif: pif.id }
)
return monitorTask(taskId)
},
validate: (isRunning, pif: FrontXoPif | undefined) => {
if (!pif) {
throw new JobError(t('job:arg:missing-payload'))
}
if (isRunning) {
throw new JobRunningError(t('job:pif-management-reconfigure:in-progress'))
}
const host = getHostById(pif.$host)
if (host?.power_state !== HOST_POWER_STATE.RUNNING) {
throw new JobError(t('job:pif-management-reconfigure:bad-power-state'))
}
if (pif.management) {
throw new JobError(t('job:pif-management-reconfigure:current-pif'))
}
if (!hasIpConfiguration(pif)) {
throw new JobError(t('job:pif-management-reconfigure:missing-ip'))
}
},
}
})

View File

@@ -34,8 +34,6 @@ const pifFields = [
'bondSlaves',
'physical',
'type',
'ipv6Mode',
'primaryAddressType',
] as const satisfies readonly (keyof XoPif)[]
export const useXoPifCollection = defineRemoteResource({

View File

@@ -1,5 +1,5 @@
<template>
<SrHeaderBreadcrumb :sr :scope />
<SrHeaderBreadcrumbLink :sr :host :from-context />
<UiHeadBar>
<template #icon>
<VtsObjectIcon type="sr" :state="srIconState" size="medium" />
@@ -12,55 +12,46 @@
<TabList>
<RouterLink
v-slot="{ isActive, href }"
:to="{ name: '/sr/[id]/general', params: { id: sr.id }, query: scopeQuery }"
:to="{ name: '/sr/[id]/general', params: { id: sr.id }, query: route.query }"
custom
>
<TabItem :active="isActive" :href tag="a">
{{ t('general') }}
</TabItem>
</RouterLink>
<RouterLink
v-slot="{ isActive, href }"
:to="{ name: '/sr/[id]/hosts', params: { id: sr.id }, query: scopeQuery }"
custom
>
<TabItem :active="isActive" :href tag="a">
{{ t('hosts') }}
</TabItem>
</RouterLink>
</TabList>
</template>
<script setup lang="ts">
import type { FrontXoHost } from '@/modules/host/remote-resources/use-xo-host-collection.ts'
import { useXoPbdCollection } from '@/modules/pbd/remote-resources/use-xo-pbd-collection.ts'
import SrHeaderBreadcrumb from '@/modules/storage-repository/components/header/SrHeaderBreadcrumb.vue'
import SrHeaderBreadcrumbLink from '@/modules/storage-repository/components/header/SrHeaderBreadcrumbLink.vue'
import { useXoSrUtils } from '@/modules/storage-repository/composables/xo-sr-utils.composable.ts'
import { useXoSrCollection } from '@/modules/storage-repository/remote-resources/use-xo-sr-collection.ts'
import type { FrontXoSr } from '@/modules/storage-repository/remote-resources/use-xo-sr-collection.ts'
import { toSrScopeQuery } from '@/modules/storage-repository/utils/sr-scope.util.ts'
import type { SrScope } from '@core/types/storage-repository.type.ts'
import VtsIcon from '@core/components/icon/VtsIcon.vue'
import VtsObjectIcon from '@core/components/object-icon/VtsObjectIcon.vue'
import TabItem from '@core/components/tab/TabItem.vue'
import TabList from '@core/components/tab/TabList.vue'
import UiHeadBar from '@core/components/ui/head-bar/UiHeadBar.vue'
import { vTooltip } from '@core/directives/tooltip.directive.ts'
import { SR_SCOPE_TYPE, type SrScope } from '@core/types/storage-repository.type.ts'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRoute } from 'vue-router'
const { sr, scope } = defineProps<{ sr: FrontXoSr; scope: SrScope }>()
const { sr, fromContext } = defineProps<{ sr: FrontXoSr; host?: FrontXoHost; fromContext?: SrScope }>()
const { t } = useI18n()
const route = useRoute()
const { arePbdsReady } = useXoPbdCollection()
const { isDefaultSr } = useXoSrCollection()
const scopeQuery = computed(() => toSrScopeQuery(scope))
const { srConnectionStatus } = useXoSrUtils(
() => sr,
() => scope
() => fromContext ?? { type: SR_SCOPE_TYPE.POOL }
)
const srIconState = computed(() => (arePbdsReady.value ? srConnectionStatus.value : undefined))

View File

@@ -1,115 +0,0 @@
<template>
<div v-if="parent" class="sr-header-breadcrumb">
<UiBreadcrumb :size>
<UiLink :size :to="parent.dashboardTo" :icon="parent.icon">
{{ parent.label }}
</UiLink>
<UiLink :size :to="parent.storageTo">
{{ t('storage') }}
</UiLink>
<span class="sr-name">
<VtsObjectIcon type="sr" :state="srConnectionState" size="current" />
{{ sr.name_label }}
</span>
</UiBreadcrumb>
</div>
</template>
<script setup lang="ts">
import { useXoHostCollection, type FrontXoHost } from '@/modules/host/remote-resources/use-xo-host-collection.ts'
import { useXoPbdCollection } from '@/modules/pbd/remote-resources/use-xo-pbd-collection.ts'
import { useXoPoolCollection } from '@/modules/pool/remote-resources/use-xo-pool-collection.ts'
import { useXoSrUtils } from '@/modules/storage-repository/composables/xo-sr-utils.composable.ts'
import type { FrontXoSr } from '@/modules/storage-repository/remote-resources/use-xo-sr-collection.ts'
import VtsObjectIcon from '@core/components/object-icon/VtsObjectIcon.vue'
import UiBreadcrumb from '@core/components/ui/breadcrumb/UiBreadcrumb.vue'
import UiLink from '@core/components/ui/link/UiLink.vue'
import { objectIcon, type IconName } from '@core/icons'
import { useUiStore } from '@core/stores/ui.store.ts'
import { SR_SCOPE_TYPE, type SrScope } from '@core/types/storage-repository.type.ts'
import { toLower } from 'lodash-es'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import type { RouteLocationRaw } from 'vue-router'
type SrBreadcrumbParent = {
icon: IconName
label: string
dashboardTo: RouteLocationRaw
storageTo: RouteLocationRaw
}
const { sr, scope } = defineProps<{ sr: FrontXoSr; scope: SrScope }>()
const { t } = useI18n()
const uiStore = useUiStore()
const { useGetHostById } = useXoHostCollection()
const { useGetPoolById } = useXoPoolCollection()
const size = computed(() => (uiStore.isSmall ? 'small' : 'medium'))
const host = useGetHostById(() => (scope.type === SR_SCOPE_TYPE.HOST ? (scope.hostId as FrontXoHost['id']) : undefined))
const pool = useGetPoolById(() => sr.$pool)
const { arePbdsReady } = useXoPbdCollection()
const parent = computed<SrBreadcrumbParent | undefined>(() => {
if (scope.type === SR_SCOPE_TYPE.HOST) {
const scopedHost = host.value
if (scopedHost === undefined) {
return undefined
}
return {
icon: objectIcon('host', toLower(scopedHost.power_state)),
label: scopedHost.name_label,
dashboardTo: { name: '/host/[id]/dashboard', params: { id: scopedHost.id } },
storageTo: { name: '/host/[id]/storage', params: { id: scopedHost.id } },
}
}
const srPool = pool.value
if (srPool === undefined) {
return undefined
}
return {
icon: 'object:pool',
label: srPool.name_label,
dashboardTo: { name: '/pool/[id]/dashboard', params: { id: srPool.id } },
storageTo: { name: '/pool/[id]/storage', params: { id: srPool.id } },
}
})
const { srConnectionStatus } = useXoSrUtils(
() => sr,
() => scope
)
const srConnectionState = computed(() => (arePbdsReady.value ? srConnectionStatus.value : undefined))
</script>
<style lang="postcss" scoped>
.sr-header-breadcrumb {
min-height: 5.6rem;
padding: 1.2rem 1.6rem;
display: flex;
gap: 1.6rem;
align-items: center;
border-bottom: 0.1rem solid var(--color-neutral-border);
background-color: var(--color-neutral-background-primary);
justify-content: space-between;
overflow-y: auto;
.sr-name {
display: flex;
align-items: center;
gap: 1rem;
}
}
</style>

View File

@@ -0,0 +1,91 @@
<template>
<div class="breadcrumb-container">
<UiBreadcrumb v-if="fromContext?.type === SR_SCOPE_TYPE.HOST && host" :size>
<UiLink :size :to="{ name: '/host/[id]/dashboard', params: { id: host.id } }">
<VtsObjectIcon type="host" :state="toLower(host.power_state)" size="current" />
{{ host.name_label }}
</UiLink>
<UiLink :size :to="{ name: '/host/[id]/storage', params: { id: host.id } }">
{{ t('storage') }}
</UiLink>
<span class="sr-name">
<VtsObjectIcon type="sr" :state="srIconState" size="current" />
{{ sr.name_label }}
</span>
</UiBreadcrumb>
<UiBreadcrumb v-else-if="fromContext?.type === SR_SCOPE_TYPE.POOL && pool" :size>
<UiLink :size :to="{ name: '/pool/[id]/dashboard', params: { id: pool.id } }">
<VtsIcon name="object:pool" size="current" />
{{ pool.name_label }}
</UiLink>
<UiLink :size :to="{ name: '/pool/[id]/storage', params: { id: pool.id } }">
{{ t('storage') }}
</UiLink>
<span class="sr-name">
<VtsObjectIcon type="sr" :state="srIconState" size="current" />
{{ sr.name_label }}
</span>
</UiBreadcrumb>
<UiBreadcrumb v-else :size>
<span class="sr-name">
<VtsObjectIcon type="sr" :state="srIconState" size="current" />
{{ sr.name_label }}
</span>
</UiBreadcrumb>
</div>
</template>
<script setup lang="ts">
import type { FrontXoHost } from '@/modules/host/remote-resources/use-xo-host-collection.ts'
import { useXoPbdCollection } from '@/modules/pbd/remote-resources/use-xo-pbd-collection.ts'
import { useXoPoolCollection } from '@/modules/pool/remote-resources/use-xo-pool-collection.ts'
import { useXoSrUtils } from '@/modules/storage-repository/composables/xo-sr-utils.composable.ts'
import type { FrontXoSr } from '@/modules/storage-repository/remote-resources/use-xo-sr-collection.ts'
import VtsIcon from '@core/components/icon/VtsIcon.vue'
import VtsObjectIcon from '@core/components/object-icon/VtsObjectIcon.vue'
import UiBreadcrumb from '@core/components/ui/breadcrumb/UiBreadcrumb.vue'
import UiLink from '@core/components/ui/link/UiLink.vue'
import { useUiStore } from '@core/stores/ui.store.ts'
import { SR_SCOPE_TYPE, type SrScope } from '@core/types/storage-repository.type.ts'
import { toLower } from 'lodash-es'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
const { sr, fromContext } = defineProps<{ sr: FrontXoSr; host?: FrontXoHost; fromContext?: SrScope }>()
const { t } = useI18n()
const uiStore = useUiStore()
const size = computed(() => (uiStore.isSmall ? 'small' : 'medium'))
const { useGetPoolById } = useXoPoolCollection()
const pool = useGetPoolById(() => sr.$pool)
const { arePbdsReady } = useXoPbdCollection()
const { srConnectionStatus } = useXoSrUtils(
() => sr,
() => fromContext ?? { type: SR_SCOPE_TYPE.POOL }
)
const srIconState = computed(() => (arePbdsReady.value ? srConnectionStatus.value : undefined))
</script>
<style lang="postcss" scoped>
.breadcrumb-container {
min-height: 5.6rem;
padding: 1.2rem 1.6rem;
display: flex;
gap: 1.6rem;
align-items: center;
border-bottom: 0.1rem solid var(--color-neutral-border);
background-color: var(--color-neutral-background-primary);
justify-content: space-between;
overflow-y: auto;
.sr-name {
display: flex;
align-items: center;
gap: 1rem;
}
}
</style>

View File

@@ -1,7 +1,6 @@
import { useXoHostCollection } from '@/modules/host/remote-resources/use-xo-host-collection.ts'
import { useXoPbdUtils } from '@/modules/pbd/composables/xo-pbd-utils.composable.ts'
import { useXoPbdCollection, type FrontXoPbd } from '@/modules/pbd/remote-resources/use-xo-pbd-collection.ts'
import { getPbdsConnectionStatus } from '@/modules/pbd/utils/xo-pbd.util.ts'
import type { FrontXoSr } from '@/modules/storage-repository/remote-resources/use-xo-sr-collection.ts'
import { type IconName, objectIcon } from '@core/icons'
import { SR_SCOPE_TYPE, type SrScope } from '@core/types/storage-repository.type.ts'
@@ -34,7 +33,7 @@ export function useGetPbdsInScope() {
function getSrPbdsSignature(sr: FrontXoSr | undefined, scope: SrScope) {
if (sr === undefined) {
return scope.type === SR_SCOPE_TYPE.HOST ? `host:${scope.hostId}` : 'pool'
return scope.type === 'host' ? `host:${scope.hostId}` : 'pool'
}
const scopedPbds = getPbdsInScope(sr, scope)
@@ -42,10 +41,6 @@ export function useGetPbdsInScope() {
return scopedPbds.map(pbd => `${pbd.id}:${pbd.attached}`).join('|') || sr.id
}
function isConnectedInScope(sr: FrontXoSr, scope: SrScope) {
return getPbdsInScope(sr, scope).some(pbd => pbd.attached)
}
function isPartiallyConnectedInScope(sr: FrontXoSr, scope: SrScope) {
const pbds = getPbdsInScope(sr, scope)
@@ -57,7 +52,6 @@ export function useGetPbdsInScope() {
getAttachedPbdsInScope,
getDetachedPbdsInScope,
getSrPbdsSignature,
isConnectedInScope,
isPartiallyConnectedInScope,
}
}
@@ -95,10 +89,6 @@ export function useXoSrUtils(
const srStatusIcon = computed<IconName>(() => objectIcon('sr', allPbdsConnectionStatus.value))
function getSrStatusIcon(sr: FrontXoSr): IconName {
return objectIcon('sr', getPbdsConnectionStatus(getPbdsInScope(sr, scope.value)))
}
function getSrLocation(sr: FrontXoSr): string {
if (sr.shared) {
return t('shared')
@@ -125,7 +115,6 @@ export function useXoSrUtils(
srConnectionStatus: allPbdsConnectionStatus,
isPartiallyConnectedInScope,
srStatusIcon,
getSrStatusIcon,
getSrLocation,
getSrAccessModeLabel,
getSrProvisioningLabel,

View File

@@ -1,5 +1,3 @@
import type { FrontXoHost } from '@/modules/host/remote-resources/use-xo-host-collection.ts'
import { useXoPbdCollection } from '@/modules/pbd/remote-resources/use-xo-pbd-collection.ts'
import { useXoPoolCollection, type FrontXoPool } from '@/modules/pool/remote-resources/use-xo-pool-collection.ts'
import { useXoVdiCollection, type FrontXoVdi } from '@/modules/vdi/remote-resources/use-xo-vdi-collection.ts'
import { useWatchCollection } from '@/shared/composables/watch-collection.composable.ts'
@@ -10,7 +8,7 @@ import { defineRemoteResource } from '@core/packages/remote-resource/define-remo
import { sortByNameLabel } from '@core/utils/sort-by-name-label.util.ts'
import type { XoSr } from '@vates/types'
import { reactify, useSorted } from '@vueuse/core'
import { computed, ref, watch } from 'vue'
import { ref, watch } from 'vue'
export type FrontXoSr = Pick<XoSr, (typeof srFields)[number]>
@@ -45,7 +43,6 @@ export const useXoSrCollection = defineRemoteResource({
const { getVdiById } = useXoVdiCollection(context)
const { getPoolById } = useXoPoolCollection()
const { pbdsBySr } = useXoPbdCollection()
const state = useXoCollectionState(sortedSrs, {
context,
@@ -80,18 +77,6 @@ export const useXoSrCollection = defineRemoteResource({
srsByPool.value = tmpSrsByPool
})
const srsByHost = computed(() => {
const tmpSrsByHost = new Map<FrontXoHost['id'], FrontXoSr[]>()
sortedSrs.value.forEach(sr => {
pbdsBySr.value.get(sr.id)?.forEach(pbd => {
safePushInMap(tmpSrsByHost, pbd.host, sr)
})
})
return tmpSrsByHost
})
const isDefaultSr = (sr: FrontXoSr) => getPoolById(sr.$pool)?.default_SR === sr.id
const isHighAvailabilitySr = reactify((sr: FrontXoSr) => {
@@ -104,7 +89,6 @@ export const useXoSrCollection = defineRemoteResource({
...state,
vdiIsosBySrName,
srsByPool,
srsByHost,
isDefaultSr,
isHighAvailabilitySr,
}

View File

@@ -1,27 +0,0 @@
import type { FrontXoHost } from '@/modules/host/remote-resources/use-xo-host-collection.ts'
import { SR_SCOPE_TYPE, type SrScope } from '@core/types/storage-repository.type.ts'
import type { LocationQuery } from 'vue-router'
export type SrScopeQuery =
| { from: typeof SR_SCOPE_TYPE.POOL }
| { from: typeof SR_SCOPE_TYPE.HOST; host: FrontXoHost['id'] }
export function toSrScopeQuery(scope: SrScope): SrScopeQuery {
if (scope.type === SR_SCOPE_TYPE.HOST) {
return { from: SR_SCOPE_TYPE.HOST, host: scope.hostId as FrontXoHost['id'] }
}
return { from: SR_SCOPE_TYPE.POOL }
}
export function parseSrScopeQuery(query: LocationQuery): SrScope {
const { from, host } = query
// `?host` alone yields null and a repeated `?host=a&host=b` yields an array,
// so the id is only trusted once narrowed to a string
if (from === SR_SCOPE_TYPE.HOST && typeof host === 'string') {
return { type: SR_SCOPE_TYPE.HOST, hostId: host as FrontXoHost['id'] }
}
return { type: SR_SCOPE_TYPE.POOL }
}

View File

@@ -70,7 +70,7 @@ function openModal() {
},
},
props: {
subject: vm.name_label,
subject: t('n-vms', { n: 1 }),
confirmLabel: t('action:delete-n-vms', { n: 1 }),
},
})

View File

@@ -44,7 +44,7 @@ const openModal = () => {
openRebootModal({
events: { onConfirm: () => forceReboot() },
props: { accent: 'info', action: 'force-reboot', object: 'vm', vmName: vm.name_label, icon: 'status:info-picto' },
props: { accent: 'info', action: 'force-reboot', object: 'vm', icon: 'status:info-picto' },
})
}
</script>

View File

@@ -47,7 +47,7 @@ const openModal = () => {
openRebootModal({
events: { onConfirm: () => reboot() },
props: { accent: 'info', action: 'reboot', object: 'vm', vmName: vm.name_label, icon: 'status:info-picto' },
props: { accent: 'info', action: 'reboot', object: 'vm', icon: 'status:info-picto' },
})
}
</script>

View File

@@ -43,7 +43,7 @@ const openModal = () => {
openForceShutdownModal({
events: { onConfirm: () => forceShutdown() },
props: { accent: 'info', action: 'force-shutdown', object: 'vm', vmName: vm.name_label, icon: 'status:info-picto' },
props: { accent: 'info', action: 'force-shutdown', object: 'vm', icon: 'status:info-picto' },
})
}
</script>

View File

@@ -43,7 +43,7 @@ const openModal = () => {
openShutdownModal({
events: { onConfirm: () => shutdown() },
props: { accent: 'info', action: 'shutdown', object: 'vm', vmName: vm.name_label, icon: 'status:info-picto' },
props: { accent: 'info', action: 'shutdown', object: 'vm', icon: 'status:info-picto' },
})
}
</script>

View File

@@ -1,7 +1,7 @@
<template>
<VtsContentSidePanel class="storage">
<UiCard class="container">
<StorageRepositoriesTable :srs :busy="!isReady" :error="hasFetchError" :scope />
<StorageRepositoriesTable :srs :busy="!isReady" :error="hasSrFetchError" :scope />
</UiCard>
<StorageRepositorySidePanel :sr="selectedSr" :scope @close="selectedSr = undefined" />
</VtsContentSidePanel>
@@ -20,21 +20,34 @@ import VtsContentSidePanel from '@core/components/layout/VtsContentSidePanel.vue
import UiCard from '@core/components/ui/card/UiCard.vue'
import { useRouteQuery } from '@core/composables/route-query.composable.ts'
import { SR_SCOPE_TYPE, type SrScope } from '@core/types/storage-repository.type.ts'
import { logicAnd, logicOr } from '@vueuse/math'
import { sortByNameLabel } from '@core/utils/sort-by-name-label.util.ts'
import { logicAnd } from '@vueuse/math'
import { computed } from 'vue'
const { host } = defineProps<{
host: FrontXoHost
}>()
const { srsByHost, getSrById, areSrsReady, hasSrFetchError } = useXoSrCollection()
const { arePbdsReady, hasPbdFetchError } = useXoPbdCollection()
const { hasSrFetchError, getSrById, areSrsReady } = useXoSrCollection()
const { pbdsByHost, arePbdsReady } = useXoPbdCollection()
const isReady = logicAnd(areSrsReady, arePbdsReady)
const hasFetchError = logicOr(hasSrFetchError, hasPbdFetchError)
const srs = computed(() => {
const hostPbds = pbdsByHost.value.get(host.id) ?? []
const srs = computed(() => srsByHost.value.get(host.id) ?? [])
return hostPbds
.reduce<FrontXoSr[]>((acc, pbd) => {
const sr = getSrById(pbd.SR)
if (sr !== undefined) {
acc.push(sr)
}
return acc
}, [])
.sort((sr1, sr2) => sortByNameLabel(sr1, sr2))
})
const selectedSr = useRouteQuery<FrontXoSr | undefined>('id', {
toData: id => getSrById(id as FrontXoSr['id']),

View File

@@ -4,21 +4,22 @@
{{ t('object-not-found', { id: route.params.id }) }}
</VtsStateHero>
<RouterView v-else v-slot="{ Component }">
<SrHeader v-if="uiStore.hasUi" :sr :scope />
<SrHeader v-if="uiStore.hasUi" :sr :host :from-context />
<component :is="Component" :sr />
</RouterView>
</template>
<script lang="ts" setup>
import SrHeader from '@/modules/storage-repository/components/header/SrHeader.vue'
import { useXoHostCollection, type FrontXoHost } from '@/modules/host/remote-resources/use-xo-host-collection.ts'
import SrHeader from '@/modules/storage-repository/components/SrHeader.vue'
import {
type FrontXoSr,
useXoSrCollection,
} from '@/modules/storage-repository/remote-resources/use-xo-sr-collection.ts'
import { parseSrScopeQuery } from '@/modules/storage-repository/utils/sr-scope.util.ts'
import VtsStateHero from '@core/components/state-hero/VtsStateHero.vue'
import { useDefaultTab } from '@core/composables/default-tab.composable.ts'
import { useUiStore } from '@core/stores/ui.store.ts'
import { SR_SCOPE_TYPE, type SrScope } from '@core/types/storage-repository.type.ts'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRoute } from 'vue-router'
@@ -35,5 +36,15 @@ const { areSrsReady, useGetSrById } = useXoSrCollection()
const sr = useGetSrById(() => route.params.id as FrontXoSr['id'])
const scope = computed(() => parseSrScopeQuery(route.query))
const fromContext = computed<SrScope>(() =>
route.query.from === SR_SCOPE_TYPE.HOST && route.query.host
? { type: SR_SCOPE_TYPE.HOST, hostId: route.query.host as FrontXoHost['id'] }
: { type: SR_SCOPE_TYPE.POOL }
)
const { useGetHostById } = useXoHostCollection()
const host = useGetHostById(() =>
fromContext.value.type === SR_SCOPE_TYPE.HOST ? (fromContext.value.hostId as FrontXoHost['id']) : undefined
)
</script>

View File

@@ -1,52 +0,0 @@
<template>
<VtsContentSidePanel class="hosts">
<UiCard class="container">
<HostsTable :hosts :sr :busy="!isReady" :error="hasError" />
</UiCard>
<HostSidePanel :host="selectedHost" @close="selectedHost = undefined" />
</VtsContentSidePanel>
</template>
<script lang="ts" setup>
import HostsTable from '@/modules/host/components/list/HostsTable.vue'
import HostSidePanel from '@/modules/host/components/list/panel/HostSidePanel.vue'
import { useXoHostCollection, type FrontXoHost } from '@/modules/host/remote-resources/use-xo-host-collection.ts'
import { useXoPbdCollection } from '@/modules/pbd/remote-resources/use-xo-pbd-collection.ts'
import type { FrontXoSr } from '@/modules/storage-repository/remote-resources/use-xo-sr-collection.ts'
import VtsContentSidePanel from '@core/components/layout/VtsContentSidePanel.vue'
import UiCard from '@core/components/ui/card/UiCard.vue'
import { useRouteQuery } from '@core/composables/route-query.composable.ts'
import { logicAnd, logicOr } from '@vueuse/math'
import { computed } from 'vue'
const { sr } = defineProps<{
sr: FrontXoSr
}>()
const { getHostById, areHostsReady, hasHostFetchError } = useXoHostCollection()
const { pbdsBySr, arePbdsReady, hasPbdFetchError } = useXoPbdCollection()
const isReady = logicAnd(areHostsReady, arePbdsReady)
const hasError = logicOr(hasHostFetchError, hasPbdFetchError)
const hosts = computed(() =>
(pbdsBySr.value.get(sr.id) ?? [])
.map(pbd => getHostById(pbd.host))
.filter((host): host is FrontXoHost => host !== undefined)
)
const selectedHost = useRouteQuery<FrontXoHost | undefined>('id', {
toData: id => hosts.value.find(host => host.id === id),
toQuery: host => host?.id ?? '',
})
</script>
<style scoped lang="postcss">
.hosts {
.container {
height: fit-content;
gap: 4rem;
margin: 0.8rem;
}
}
</style>

View File

@@ -316,7 +316,6 @@ declare module 'vue-router/auto-routes' {
{ id: ParamValue<true> },
{ id: ParamValue<false> },
| '/sr/[id]/general'
| '/sr/[id]/hosts'
>,
'/sr/[id]/general': RouteRecordInfo<
'/sr/[id]/general',
@@ -325,13 +324,6 @@ declare module 'vue-router/auto-routes' {
{ id: ParamValue<false> },
| never
>,
'/sr/[id]/hosts': RouteRecordInfo<
'/sr/[id]/hosts',
'/sr/:id/hosts',
{ id: ParamValue<true> },
{ id: ParamValue<false> },
| never
>,
'/traffic-rule/new': RouteRecordInfo<
'/traffic-rule/new',
'/traffic-rule/new',
@@ -745,7 +737,6 @@ declare module 'vue-router/auto-routes' {
routes:
| '/sr/[id]'
| '/sr/[id]/general'
| '/sr/[id]/hosts'
views:
| 'default'
}
@@ -755,12 +746,6 @@ declare module 'vue-router/auto-routes' {
views:
| never
}
'src/pages/sr/[id]/hosts.vue': {
routes:
| '/sr/[id]/hosts'
views:
| never
}
'src/pages/traffic-rule/new.vue': {
routes:
| '/traffic-rule/new'

View File

@@ -1,8 +1,6 @@
# ChangeLog
## **6.8.2** (2026-09-04)
<img id="latest" src="https://badgen.net/badge/channel/latest/yellow" alt="Channel: latest" />
## **next**
### Bug fixes
@@ -15,6 +13,8 @@
## **6.8.1** (2026-09-02)
<img id="latest" src="https://badgen.net/badge/channel/latest/yellow" alt="Channel: latest" />
### Enhancements
- [XO6/Traffic rules] Add possibility of editing a traffic rule (PR [#10056](https://github.com/vatesfr/xen-orchestra/pull/10056))

View File

@@ -11,24 +11,11 @@
> Users must be able to say: "Nice enhancement, I'm eager to test it"
- [XO6/Vm] Add the VM name to VM related actions that open a modal (PR [#10310](https://github.com/vatesfr/xen-orchestra/pull/10310))
- [XO6] Allow changing which PIF a host uses for its management interface, without deleting and recreating the network config (PR [#10110](https://github.com/vatesfr/xen-orchestra/pull/10110))
- [XO6/SR] Add dedicated Storage Repository page hosts sidepanel (PR [#10140](https://github.com/vatesfr/xen-orchestra/pull/10140))
- [Rolling pool update/reboot] A pool can now skip the phase which brings the VMs back to the host they were running on, which halves the migrations of the run (PR [#10295](https://github.com/vatesfr/xen-orchestra/pull/10295))
- [XO5/Backups] Open the backup job edition form in the same tab when editing a backup job from the VM page (PR [#10342](https://github.com/vatesfr/xen-orchestra/pull/10342))
### Bug fixes
> Users must be able to say: "I had this issue, happy to know it's fixed"
- [Web-core] Fix "console offline" illustration sparks color (PR [#10309](https://github.com/vatesfr/xen-orchestra/pull/10309))
- [Web-core] Fix 404 illustration color (PR [#10325](https://github.com/vatesfr/xen-orchestra/pull/10325))
- [Backup-archive] No longer create a `cache.json.gz` file on immutable/S3 remote during cleanup, which could not be deleted afterwards and stayed billed forever (PR [#10243](https://github.com/vatesfr/xen-orchestra/pull/10243))
- [Servers] Fix endless connection attempts to a pool which is already connected through another server entry (PR [#10355](https://github.com/vatesfr/xen-orchestra/pull/10355))
- [Servers] fix a mishandling in the grace period before marking a pool disconnected, this will keep the ui in sync AND not redownload all the xapi object for a transient issue (PR [#10355](https://github.com/vatesfr/xen-orchestra/pull/10355))
- [Rolling pool update/reboot] VMs are brought back to the host they were running on more reliably, and a VM that cannot be moved back no longer fails the whole operation (PR [#10295](https://github.com/vatesfr/xen-orchestra/pull/10295))
- [XO server] Fix current_operations format on host and pool objects (PR [#10283](https://github.com/vatesfr/xen-orchestra/pull/10283))
- [Backup/Replication] Fix incremental replication silently stopping after the first run when "Distribute replications across the storage repositories" is enabled, with the warning `the writer AggregatedIncrementalXapiWriter has failed the step writer.checkBaseVdis()` (PR [#10375](https://github.com/vatesfr/xen-orchestra/pull/10375))
- [Proxy] Fix `MaxListenersExceededWarning` when several updater state checks overlap (PR [#10351](https://github.com/vatesfr/xen-orchestra/pull/10351))
### Packages to release
@@ -46,11 +33,6 @@
<!--packages-start-->
- @xen-orchestra/backup-archive patch
- @xen-orchestra/backups patch
- @xen-orchestra/web minor
- @xen-orchestra/web-core minor
- xo-server minor
- xo-web minor
- @xen-orchestra/proxy patch
<!--packages-end-->

View File

@@ -1,7 +1,3 @@
---
unlisted: true
---
# Airgap support and deployment
:::tip
@@ -19,11 +15,11 @@ In this scenario, you will need a QA/pre-production XCP-ng pool with Internet ac
### Deployment
[Follow the standard procedure](./getting-started/installation.md) to deploy the XOA on your pool with Internet access.
[Follow the standard procedure](./installation.md) to deploy the XOA on your pool with Internet access.
Make sure that your appliance is [properly registered](./getting-started/installation.md#registration) and [up-to-date](./getting-started/updater.md).
Make sure that your appliance is [properly registered](./installation.md#registration) and [up-to-date](./updater.md).
It's also good to take a quick look at [the XOA check](./getting-started/troubleshooting.md#first-reflex-xoa-check) to detect issues early.
It's also good to take a quick look at [the XOA check](./troubleshooting.md#first-reflex-xoa-check) to detect issues early.
When everything is good, you can shutdown your XOA and export it:
@@ -54,7 +50,7 @@ To upgrade your XOA, you need to:
1. shutdown the XOA on your airgapped pool
2. export it to an XVA file
3. move it to your connected pool and import it
4. start it, run the [upgrade process](./getting-started/updater.md)
4. start it, run the [upgrade process](./updater.md)
5. shutdown this XOA
6. export it to an XVA file
7. move it to your airgapped pool and import it

View File

@@ -1,10 +1,6 @@
---
sidebar_position: 9
---
# Architecture
Xen Orchestra (XO) is built as **one server and several clients**: the two web interfaces ([XO 6](../discover-xen-orchestra/gettingstarted.md) and XO 5), the command line client `xo-cli`, and anything speaking the [REST API](automation/restapi.md). The server, `xo-server`, is the only piece that talks to your infrastructure.
Xen Orchestra (XO) is built as **one server and several clients**: the two web interfaces ([XO 6](xo6/gettingstarted.md) and XO 5), the command line client `xo-cli`, and anything speaking the [REST API](automation/restapi.md). The server, `xo-server`, is the only piece that talks to your infrastructure.
:::tip
XO is totally agent-less: nothing to install on your hosts. `xo-server` speaks XAPI, the native toolstack of XCP-ng, directly over the network.
@@ -99,7 +95,7 @@ The _Xen Orchestra Appliance_ (XOA) is a VM with the whole stack pre-installed,
- **Always on**: as a daemon, it listens to and records every event of your infrastructure, around the clock, whether a client is watching or not. That is what makes always-up-to-date views, scheduled backup jobs and task history possible.
- **Event driven**: `xo-server` subscribes to XAPI events instead of polling. Changes on a host or VM appear in your browser the moment they happen, and the server-side cache answers clients instantly, at any infrastructure size.
- **A single connection point**: clients connect to `xo-server`, never to your hosts. Legacy thick clients opened connections from every admin workstation to every server; here one daemon holds one connection per pool, and any number of clients share it.
- **A proxy for your hosts**: because everything flows through it, `xo-server` can expose VM consoles to clients that have no network access to the hosts, or stream a VM export from one pool straight into another (that is [warm migration](../manage-your-infrastructure/manage_infrastructure.md territory) without a file stop in between.
- **A proxy for your hosts**: because everything flows through it, `xo-server` can expose VM consoles to clients that have no network access to the hosts, or stream a VM export from one pool straight into another (that is [warm migration](xo5/manage_infrastructure.md) territory) without a file stop in between.
- **Pluggable**: features like LDAP authentication, load balancing, netbox synchronization or backup reports are [plugins](#plugins), not core patches.
### Events, not polling
@@ -172,12 +168,12 @@ Legacy clients ask the servers "anything new?" every few seconds: slow, and it c
Two web UIs currently ship with Xen Orchestra, served by `xo-server` and talking to it over WebSockets:
- **XO 6** (`@xen-orchestra/web`), the new default interface, built with Vue.js on a dedicated design system. See the [XO 6 documentation](../discover-xen-orchestra/gettingstarted.md).
- **XO 5** (`xo-web`), the complete historical interface, built with React. See [the infrastructure management section](../manage-your-infrastructure/manage_infrastructure.md) for what you can do with it.
- **XO 6** (`@xen-orchestra/web`), the new default interface, built with Vue.js on a dedicated design system. See the [XO 6 documentation](xo6/gettingstarted.md).
- **XO 5** (`xo-web`), the complete historical interface, built with React. See [the infrastructure management section](xo5/manage_infrastructure.md) for what you can do with it.
<UiShot light="/img/xo6/dashboard-light.png" dark="/img/xo6/dashboard-dark.png" alt="XO 6, the new default web interface" url="https://your-xo/v6/#/dashboard" />
Both run against the same server and the same data at the same time: see [XO 6 and XO 5](../discover-xen-orchestra/xo6vsxo5.md).
Both run against the same server and the same data at the same time: see [XO 6 and XO 5](xo6/xo6vsxo5.md).
## xo-cli

View File

@@ -1,8 +0,0 @@
{
"label": "Automation",
"position": 8,
"link": {
"type": "generated-index",
"description": "Various tools to automate your workflow in XO"
}
}

View File

@@ -2,7 +2,7 @@
## Introduction
Xen Orchestra includes a Kubernetes cluster [recipe](../manage-your-infrastructure/advanced.md#recipes) that provides a simple way to deploy an official Kubernetes distribution called **MicroK8s** (maintained by Canonical).
Xen Orchestra includes a Kubernetes cluster [recipe](../xo5/advanced.md#recipes) that provides a simple way to deploy an official Kubernetes distribution called **MicroK8s** (maintained by Canonical).
:::tip
One of the key benefits of MicroK8s is its automatic security updates. For example, patch releases (like 1.30.x to 1.30.x+1) are applied automatically. This saves Kubernetes admins a lot of time and effort.

View File

@@ -222,7 +222,7 @@ Because the tool surface is generated from the live OpenAPI spec, it always refl
The server exposes:
- **Three utility tools**, always present: `check_connection` (validate your setup), `get_infrastructure_summary` (aggregate pools, hosts and VMs into a single overview), and `search_documentation` (look up the XO documentation by topic).
- **One `{domain}_query` tool per REST resource domain**, generated from the live spec: pools, hosts, VMs, storage (SRs, VDIs), network, backup jobs and logs, users, tasks, alarms, and more. All query tools share the same arguments: `operation` (the OpenAPI `operationId` to invoke), `id`, an optional [`filter` expression](../manage-your-infrastructure/manage_infrastructure.md#live-filter-search), `fields`, and `limit`.
- **One `{domain}_query` tool per REST resource domain**, generated from the live spec: pools, hosts, VMs, storage (SRs, VDIs), network, backup jobs and logs, users, tasks, alarms, and more. All query tools share the same arguments: `operation` (the OpenAPI `operationId` to invoke), `id`, an optional [`filter` expression](../xo5/manage_infrastructure.md#live-filter-search), `fields`, and `limit`.
- **A built-in prompt**, `infrastructure-overview`, which turns the infrastructure summary into a readable report.
Stats endpoints and binary downloads (`.xva`, `.vhd`, ...) are deliberately not exposed: their payloads are unsuitable for LLM context. Use the REST API directly when you need them.

View File

@@ -1,10 +1,6 @@
---
sidebar_position: 1
---
# REST API
The Xen Orchestra REST API is the modern, public way to automate your infrastructure. We built it from scratch, next to [our historical JSON-RPC API](../getting-started/architecture.md#apis), to be [REST-like](https://en.wikipedia.org/wiki/Representational_state_transfer) and usable with a plain `curl` command. It is now almost complete: nearly all of Xen Orchestra's capabilities are exposed through it, and it is ready to be used in production. It is also the API we are building the future of Xen Orchestra on, so it is the right choice for any new automation.
The Xen Orchestra REST API is the modern, public way to automate your infrastructure. We built it from scratch, next to [our historical JSON-RPC API](../architecture.md#apis), to be [REST-like](https://en.wikipedia.org/wiki/Representational_state_transfer) and usable with a plain `curl` command. It is now almost complete: nearly all of Xen Orchestra's capabilities are exposed through it, and it is ready to be used in production. It is also the API we are building the future of Xen Orchestra on, so it is the right choice for any new automation.
This page teaches the principles: how to authenticate, then how each HTTP verb behaves, with one example per pattern. For the complete, always up-to-date list of endpoints, use the [built-in Swagger UI](#openapiswagger) shipped with your Xen Orchestra: it documents every route and lets you try them from your browser.
@@ -22,7 +18,7 @@ Endpoints require authentication. Two forms are accepted, and you must pick exac
An invalid token or invalid credentials get a `401 Unauthorized` response.
:::tip
Admin users have access to all REST API endpoints. Non-admin users can use the REST API according to the [RBAC](../users-and-access/rbac.md) permissions defined on their account.
Admin users have access to all REST API endpoints. Non-admin users can use the REST API according to the [RBAC](../rbac.md) permissions defined on their account.
:::
For scripts and integrations, prefer a token: it does not expose your password and it can be revoked at any time.
@@ -68,7 +64,7 @@ If you don't know your user ID, use the `me` alias, as above. Tokens expire (see
Every object type lives in a collection at `/rest/v0/<name>` (e.g. `/rest/v0/vms`, `/rest/v0/hosts`, `/rest/v0/srs`). A plain `GET` returns the objects' URLs, and the following query parameters shape the result:
- `fields`: return objects containing the requested fields, instead of plain URLs
- `filter`: select only matching objects, using [the live filter search syntax](../manage-your-infrastructure/manage_infrastructure.md#live-filter-search)
- `filter`: select only matching objects, using [the live filter search syntax](../xo5/manage_infrastructure.md#live-filter-search)
- `limit`: maximum number of objects returned
- `ndjson`: if `true`, the result is streamed in [NDJSON format](https://github.com/ndjson/ndjson-spec), one object per line

View File

@@ -1,7 +1,3 @@
---
sidebar_position: 2
---
# Backup strategy guide
This guide explains how to design and implement a backup strategy in Xen Orchestra.
@@ -13,22 +9,22 @@ Instead of a simple list of questions and answers, it walks you through **key de
This part explains the terminology of backup types and features.
- [**Backup sequence**](./backup-features-and-settings.md#sequences): A feature that allows you to chain multiple backup jobs to run one after the other, automatically.
- [**Backup repository (BR)**](./backup-features-and-settings.md#remotes): Formerly called _Remote_. A storage location for backups. For instance:
- [**Backup sequence**](xo5/backups.md#sequences): A feature that allows you to chain multiple backup jobs to run one after the other, automatically.
- [**Backup repository (BR)**](xo5/backups.md#remotes): Formerly called _Remote_. A storage location for backups. For instance:
- Local storage (not recommended)
- NFS
- SMB
- Amazon S3 and compatible
- Microsoft Azure
- Azurite
- [**File restore**](./backup-features-and-settings.md#restore-a-file): A feature that allows you to restore individual files from a VM backup without restoring the full VM.
- [**Full backup**](./backup-types/full_backups.md): Copies the entire VM to backup repositories each time, regardless of previous backups.
- [**Full replication**](./backup-types/full_replication.md): Creates a replica of a VM on other storage repositories (on the same pool or on another) by copying it completely on each run.
- [**Incremental backup**](./backup-types/incremental_backups.md): Transfers and stores only the changes since the last backup to backup repositories, reducing storage and network needs. The first run transfers the VM completely.
- [**Incremental replication**](./backup-types/incremental_replication.md): Transfers and stores only the changes since the last backup to storage repositories (on the same pool or on another), reducing network needs. The first run transfers the VM completely.
- [**Long-term retention**](./backup-features-and-settings.md#long-term-backup-retention-with-gfs-strategy): Keeps backups over extended periods (weeks, months, or years) for compliance or archival purposes.
- [**Mirror backup**](./backup-types/mirror_backup.md): Mirror a backup repository to another. Retention and encryption of source and destination can be different.
- [**Distributed backup and replication**](./scale-and-security/distributed_backups.md): Distribute the backups and replications across multiple targets.
- [**File restore**](xo5/backups.md#restore-a-file): A feature that allows you to restore individual files from a VM backup without restoring the full VM.
- [**Full backup**](full_backups.md): Copies the entire VM to backup repositories each time, regardless of previous backups.
- [**Full replication**](full_replication.md): Creates a replica of a VM on other storage repositories (on the same pool or on another) by copying it completely on each run.
- [**Incremental backup**](xo5/incremental_backups.md): Transfers and stores only the changes since the last backup to backup repositories, reducing storage and network needs. The first run transfers the VM completely.
- [**Incremental replication**](xo5/incremental_replication.md): Transfers and stores only the changes since the last backup to storage repositories (on the same pool or on another), reducing network needs. The first run transfers the VM completely.
- [**Long-term retention**](xo5/backups.md#long-term-backup-retention-with-gfs-strategy): Keeps backups over extended periods (weeks, months, or years) for compliance or archival purposes.
- [**Mirror backup**](mirror_backup.md): Mirror a backup repository to another. Retention and encryption of source and destination can be different.
- [**Distributed backup and replication**](distributed_backups.md): Distribute the backups and replications across multiple targets.
## What should I do before setting up my backup?
@@ -70,13 +66,13 @@ Here is the whole toolbox at a glance. Most infrastructures combine several of t
| Type | What you get | Storage & network cost | Restore | Typical use |
| ---------------------------------------------------------- | ------------------------------------------- | --------------------------- | -------------------------------- | -------------------------------------- |
| [Rolling snapshots](./backup-types/rolling_snapshots.md) | Instant restore points, on the same storage | SR space only | Instant revert | Oops protection, before risky changes |
| [Full backup](./backup-types/full_backups.md) | Complete standalone archive on a BR | High, every run | Simple, anywhere | Small fleets, simplicity first |
| [Incremental backup](./backup-types/incremental_backups.md) | Compact archives after the first full | Low per run | Whole VM or single files | The default choice for most VMs |
| [Full replication (DR)](./backup-types/full_replication.md) | Boot-ready copy on another host/SR | High, every run | Start the copy | DR with a modest RPO |
| [Incremental replication (CR)](./backup-types/incremental_replication.md) | Boot-ready copy, updated by deltas | Low per run | Clone and start | Low-RPO DR for critical VMs |
| [Mirror backup](./backup-types/mirror_backup.md) | A second copy of a backup repository | Follows the source | Same as the source backups | 3-2-1 strategies, offsite archives |
| [Metadata backup](./backup-types/metadata_backup.md) | XO config and pool metadata | Tiny | Rebuild your orchestration | Always: it protects the tool itself |
| [Rolling snapshots](rolling_snapshots.md) | Instant restore points, on the same storage | SR space only | Instant revert | Oops protection, before risky changes |
| [Full backup](full_backups.md) | Complete standalone archive on a BR | High, every run | Simple, anywhere | Small fleets, simplicity first |
| [Incremental backup](xo5/incremental_backups.md) | Compact archives after the first full | Low per run | Whole VM or single files | The default choice for most VMs |
| [Full replication (DR)](full_replication.md) | Boot-ready copy on another host/SR | High, every run | Start the copy | DR with a modest RPO |
| [Incremental replication (CR)](xo5/incremental_replication.md) | Boot-ready copy, updated by deltas | Low per run | Clone and start | Low-RPO DR for critical VMs |
| [Mirror backup](mirror_backup.md) | A second copy of a backup repository | Follows the source | Same as the source backups | 3-2-1 strategies, offsite archives |
| [Metadata backup](xo5/metadata_backup.md) | XO config and pool metadata | Tiny | Rebuild your orchestration | Always: it protects the tool itself |
:::note The two replication modes have been renamed
**Full replication** used to be called **Disaster Recovery (DR)**, and **incremental replication** used to be called **Continuous Replication (CR)**. Only the names changed. The old ones are kept in parentheses here because you will still meet them: the XO 5 interface labels the two buttons **Disaster Recovery** and **Continuous Replication**, and XO tags replicas accordingly. XO 6 uses the current names.
@@ -102,8 +98,8 @@ A complete export of each VM to a backup repository, on every run.
After an initial full, only the changed blocks are exported.
- **Pros**: fast runs, small transfers, restore to any date, [file-level restore](./backup-features-and-settings.md#file-level-restore), works well at high frequency.
- **Watch out**: restores rely on a chain (full + deltas): set a [full backup interval](./backup-types/incremental_backups.md#key-backup-interval) to keep chains short, or enable health checks.
- **Pros**: fast runs, small transfers, restore to any date, [file-level restore](xo5/backups.md#file-level-restore), works well at high frequency.
- **Watch out**: restores rely on a chain (full + deltas): set a [full backup interval](xo5/incremental_backups.md#key-backup-interval) to keep chains short, or enable health checks.
- **First steps**: this is the right default for most VMs; start here if in doubt.
### Full replication (formerly Disaster Recovery, DR) {#full-replication-dr}
@@ -132,7 +128,7 @@ Replicates an existing backup repository to another one, possibly with different
### Sequence
Not a backup type, but the way to chain the jobs above: a [sequence](./backup-features-and-settings.md#sequences) runs several schedules one after the other, in a fixed order.
Not a backup type, but the way to chain the jobs above: a [sequence](xo5/backups.md#sequences) runs several schedules one after the other, in a fixed order.
- **Pros**: deterministic ordering (backup first, then replication), no overlapping jobs competing for resources.
- **First steps**: identify the right order, and test the sequence on non-critical VMs first.
@@ -183,7 +179,7 @@ For **advanced scenarios**, you can use the `fuse-vhd` helper script to manually
## Long-term retention strategy
For compliance or archival needs, you rarely want *every* daily backup kept for years: you want something like 7 dailies, 4 weeklies, 12 monthlies, a few yearlies. This is the **GFS (Grandfather-Father-Son)** scheme, configured directly in the retention settings of a backup job: see [Long-term backup retention with GFS](./backup-features-and-settings.md#long-term-backup-retention-with-gfs-strategy).
For compliance or archival needs, you rarely want *every* daily backup kept for years: you want something like 7 dailies, 4 weeklies, 12 monthlies, a few yearlies. This is the **GFS (Grandfather-Father-Son)** scheme, configured directly in the retention settings of a backup job: see [Long-term backup retention with GFS](xo5/backups.md#long-term-backup-retention-with-gfs-strategy).
- Define retention periods based on compliance and operational needs
- Size the required storage with the [retention calculator](calculator.md)

View File

@@ -1,8 +0,0 @@
{
"label": "Backups and DR",
"position": 5,
"link": {
"type": "generated-index",
"description": "Detailed information on backups and disaster recovery"
}
}

View File

@@ -1,8 +0,0 @@
{
"label": "Backup types",
"position": 4,
"link": {
"type": "generated-index",
"description": "All the different backup types"
}
}

View File

@@ -1,48 +0,0 @@
---
slug: backup
sidebar_position: 1
---
# Backup overview
Xen Orchestra ships a complete, **agentless** backup engine for your whole infrastructure: nothing to install inside your VMs, everything managed from one place, from quick rolling snapshots to cross-site disaster recovery.
New to backups in XO? Start with these two:
<CardGrid>
<LinkCard title="Backup strategy guide" href="/backups-and-dr/backup_howto">Which backup type for which need: design your protection plan before clicking anything.</LinkCard>
<LinkCard title="Backups in XO 6" href="/backups-and-dr/backups-in-xo6">Follow backup health from the new dashboards: job status, per-VM protection, restore points.</LinkCard>
</CardGrid>
## Pick your backup type
- **[Rolling snapshots](./backup-types/rolling_snapshots.md)**: scheduled snapshots kept on a rotation. No repository needed, instant restore points.
- **[Full backups](./backup-types/full_backups.md)**: a complete export of the VM to a backup repository (BR), every time. Simple and self-contained.
- **[Incremental backups](./backup-types/incremental_backups.md)**: after an initial full, only the changed blocks are sent. Fast, compact, deduplicable.
- **[Full replication](./backup-types/full_replication.md)** (formerly Disaster Recovery, DR): a ready-to-boot copy of your VMs, kept up to date on another host or SR.
- **[Incremental replication](./backup-types/incremental_replication.md)** (formerly Continuous Replication, CR): the same standby copy, sending only the deltas.
- **[Mirror backups](./backup-types/mirror_backup.md)**: replicate a whole backup repository to another one, the key to [3-2-1 strategies](backup_howto.md#long-term-retention-strategy).
- **[Metadata backup](./backup-types/metadata_backup.md)**: the XO configuration and pool metadata themselves, so the orchestrator is never your single point of failure.
:::tip
You don't have to pick VMs one by one: **[smart backup](./backup-features-and-settings.md#smart-backup)** selects them dynamically by pool, tag or power state, so new VMs are protected automatically.
:::
## Going further
<CardGrid>
<LinkCard title="Features and settings" href="/backups-and-dr/backup-features-and-settings">Encryption, schedules, smart backup, backup repositories, restore (including file-level), retention, health checks: the full reference.</LinkCard>
<LinkCard title="Backup proxies" href="/backups-and-dr/scale-and-security/proxy">Offload backup traffic to proxies, closer to your pools and repositories.</LinkCard>
<LinkCard title="Distributed backups" href="/backups-and-dr/scale-and-security/distributed_backups">Combine proxies and mirrors for multi-site, resilient backup architectures.</LinkCard>
<LinkCard title="Immutability" href="/backups-and-dr/scale-and-security/immutability">Make your backup repositories tamper-proof against ransomware.</LinkCard>
<LinkCard title="Backup reports" href="/backups-and-dr/backup_reports">Get notified after each run: email, XMPP and more.</LinkCard>
<LinkCard title="Troubleshooting" href="/backups-and-dr/backup_troubleshooting">Interrupted runs, full SRs, slow transfers: the usual suspects and their fixes.</LinkCard>
</CardGrid>
:::tip
Sizing question? The [retention calculator](calculator.md) estimates how much storage a given schedule and retention will consume.
:::
## See it in action
<iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/FfUqIwT8KzI?si=kTvxIFhPjv-8Iwri" title="Administer and backup your VM infrastructure the easiest way" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>

View File

@@ -1,8 +0,0 @@
{
"label": "Scale and security",
"position": 6,
"link": {
"type": "generated-index",
"description": "Scalability, performance, and security for large-scale deployments"
}
}

View File

@@ -1,6 +1,5 @@
---
sidebar_label: Retention calculator
sidebar_position: 9
---
# Backup retention calculator

View File

@@ -3,7 +3,7 @@
We do our best to assist users running Xen Orchestra from the sources on [our community forum](https://xcp-ng.org/forum/category/12/xen-orchestra) (you can also chat with the community on [Discord](https://discord.gg/Hr98F6wRvx)). However there's no guarantee you will receive a solution (from us or the community), nor any guarantee on a timeframe. Usually we are responsive, but keep in mind that if you need production level support, you should consider subscribing to our [XOA offers](https://vates.tech/pricing-and-support/).
:::warning
If you installed your XO from a 3rd party script (any method outside [our official documentation](../getting-started/install-from-sources.md)), please report your issue **first** on the 3rd party script repository. We can't waste time debugging 3rd party installers that very often lag behind our own deployment methods and fall out of date.
If you installed your XO from a 3rd party script (any method outside [our official documentation](install-from-sources.md)), please report your issue **first** on the 3rd party script repository. We can't waste time debugging 3rd party installers that very often lag behind our own deployment methods and fall out of date.
:::
## Current version
@@ -24,14 +24,14 @@ Before reporting a bug, please check that your **current version is up-to-date w
If you have issues with XO installed from the sources (via GitHub), please **always** first:
1. update your XO to the latest `master` commit (see how-to in the [dedicated section](../getting-started/install-from-sources.md#updating)) and retest again
2. if the issue persists, double check you have all the requirements checked from the [installation section](../getting-started/install-from-sources.md) and test again
3. then, if you still experience problems, [remove and rebuild from scratch](../getting-started/install-from-sources.md#troubleshooting)
1. update your XO to the latest `master` commit (see how-to in the [dedicated section](install-from-sources.md#updating)) and retest again
2. if the issue persists, double check you have all the requirements checked from the [installation section](install-from-sources.md) and test again
3. then, if you still experience problems, [remove and rebuild from scratch](install-from-sources.md#troubleshooting)
Only after that, you can go to the [dedicated forum](https://xcp-ng.org/forum/category/12/xen-orchestra) to ask about your problem.
:::tip
If you want a consistent and tested solution for using Xen Orchestra, we strongly advise using our preconfigured and validated [Xen Orchestra Virtual Appliance](../getting-started/installation.md#xoa).
If you want a consistent and tested solution for using Xen Orchestra, we strongly advise using our preconfigured and validated [Xen Orchestra Virtual Appliance](installation.md#xoa).
:::
## Feature requests

View File

@@ -1,7 +1,3 @@
---
sidebar_position: 5
---
# Configuration
Once Xen Orchestra is installed, you can configure some parameters in the configuration file. Let's see how to do that.
@@ -157,7 +153,7 @@ useDirectChannel = false
Changing the mode is only supported forward : from direct OpenFlow Protocol channel to XAPI Plugin communication.
A migration script is provided to automatically convert the traffic rules.
See [SDN Controller: OpenFlow rules](../manage-your-infrastructure/sdn_controller.md#migration-path) for details.
See [SDN Controller: OpenFlow rules](xo5/sdn_controller.md#migration-path) for details.
## Custom certificate authority
@@ -299,7 +295,7 @@ For advanced usage, you can customize the way XO connect to Redis:
#encryptCredentialDatabase = true
```
For more information about the encryption feature, see [credential database encryption](../users-and-access/credential-encryption.md)
For more information about the encryption feature, see [credential database encryption](credential-encryption.md)
## Proxy for updates and patches

View File

@@ -45,7 +45,7 @@ The best way to propose a change to the documentation or code is
to create a [GitHub pull request](https://help.github.com/articles/using-pull-requests/).
1. Fork the [Xen Orchestra repository](https://github.com/vatesfr/xen-orchestra) using the Fork button
2. Follow [the documentation](../getting-started/install-from-sources.md) to install and run Xen Orchestra from the sources
2. Follow [the documentation](./install-from-sources.md) to install and run Xen Orchestra from the sources
3. Create a branch for your work
4. Edit the source files
5. Add a summary of your changes to `CHANGELOG.unreleased.md`, if your changes do not relate to an existing changelog item and update the list of packages that must be released to take your changes into account

View File

@@ -9,7 +9,7 @@ Xen Orchestra stores its records (server connections, backup jobs and remotes, s
## Enabling Encryption
Add the following to your [configuration file](../getting-started/configuration.md):
Add the following to your [configuration file](configuration.md):
```toml
[redis]
@@ -55,7 +55,7 @@ The export contains the decrypted credential data (hence the mandatory passphras
xo-cli xo.importConfig passphrase=<your-passphrase> @=/path/to/xo-config.bin
`}</Terminal>
See also [Back up XO metadata](../backups-and-dr/backup_howto.md#specific-steps-for-the-xoa-vm).
See also [Back up XO metadata](./backup_howto.md#specific-steps-for-the-xoa-vm).
:::tip
Always export a fresh config backup after enabling encryption and store it securely.

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