mirror of
https://github.com/vatesfr/xen-orchestra.git
synced 2026-11-13 18:51:03 -06:00
Compare commits
4 Commits
fix_checkB
...
fix_proxy_
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8d56fae768 | ||
|
|
2ba2ee4f50 | ||
|
|
855c9fdf34 | ||
|
|
846a387fcc |
@@ -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 })
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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')
|
||||
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -91,8 +91,6 @@ export enum ON_NORMAL_EXIT {
|
||||
}
|
||||
|
||||
export enum HOST_OPERATION {
|
||||
EVACUATE = 'evacuate',
|
||||
REBOOT = 'reboot',
|
||||
SHUTDOWN = 'shutdown',
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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'))
|
||||
}
|
||||
},
|
||||
}
|
||||
})
|
||||
@@ -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)
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
142
@xen-orchestra/proxy/tests/appliance.integ.mjs
Normal file
142
@xen-orchestra/proxy/tests/appliance.integ.mjs
Normal 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'])
|
||||
})
|
||||
})
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 |
@@ -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 |
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<MenuList placement="bottom-end">
|
||||
<MenuList placement="bottom-start">
|
||||
<template #trigger="{ open }">
|
||||
<UiButtonIcon
|
||||
v-tooltip="{
|
||||
|
||||
@@ -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'),
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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).",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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).",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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": "优雅重启虚拟机,正常保存并关闭业务进程。",
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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({}),
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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)),
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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)),
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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>
|
||||
@@ -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 }
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
@@ -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'))
|
||||
}
|
||||
},
|
||||
}
|
||||
})
|
||||
@@ -34,8 +34,6 @@ const pifFields = [
|
||||
'bondSlaves',
|
||||
'physical',
|
||||
'type',
|
||||
'ipv6Mode',
|
||||
'primaryAddressType',
|
||||
] as const satisfies readonly (keyof XoPif)[]
|
||||
|
||||
export const useXoPifCollection = defineRemoteResource({
|
||||
|
||||
@@ -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))
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
@@ -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 }),
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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']),
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
15
@xen-orchestra/web/src/route-map.d.ts
vendored
15
@xen-orchestra/web/src/route-map.d.ts
vendored
@@ -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'
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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-->
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"label": "Automation",
|
||||
"position": 8,
|
||||
"link": {
|
||||
"type": "generated-index",
|
||||
"description": "Various tools to automate your workflow in XO"
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"label": "Backups and DR",
|
||||
"position": 5,
|
||||
"link": {
|
||||
"type": "generated-index",
|
||||
"description": "Detailed information on backups and disaster recovery"
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"label": "Backup types",
|
||||
"position": 4,
|
||||
"link": {
|
||||
"type": "generated-index",
|
||||
"description": "All the different backup types"
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"label": "Scale and security",
|
||||
"position": 6,
|
||||
"link": {
|
||||
"type": "generated-index",
|
||||
"description": "Scalability, performance, and security for large-scale deployments"
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
---
|
||||
sidebar_label: Retention calculator
|
||||
sidebar_position: 9
|
||||
---
|
||||
|
||||
# Backup retention calculator
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
Reference in New Issue
Block a user