Compare commits

..

1 Commits

Author SHA1 Message Date
Pierre Brunet
9829eb58f3 fix: add debug to better understand timeout error 2026-08-07 15:53:52 +02:00
1228 changed files with 9585 additions and 18372 deletions

View File

@@ -85,21 +85,6 @@ module.exports = {
XO_LITE_GIT_HEAD: true,
},
rules: {
'import/extensions': [
'error',
'ignorePackages',
{
pattern: { ts: 'always', vue: 'always' },
checkTypeImports: true,
// '@core/*' and '@/*' look like scoped packages to the plugin, which 'ignorePackages' would skip
pathGroupOverrides: [
{ pattern: '@core/packages/*', action: 'ignore' }, // barrel files
{ pattern: '@core/icons', action: 'ignore' },
{ pattern: '@core/**', action: 'enforce' },
{ pattern: '@/**', action: 'enforce' },
],
},
],
'import/order': [
'error',
{
@@ -269,7 +254,6 @@ module.exports = {
'zh-Hans',
'sk',
'pt',
'ar',
],
},
],

View File

@@ -5,7 +5,3 @@
# Generated files
/@xen-orchestra/web/src/route-map.d.ts
/@xen-orchestra/lite/src/route-map.d.ts
# Docusaurus MDX content: prettier's markdown parser corrupts JSX
# comments ({/* … */} becomes {/_ … _/}) and other MDX constructs
/docs/docs/**/*.md

View File

@@ -24,7 +24,7 @@
},
"dependencies": {
"@vates/multi-key-map": "^0.2.0",
"@xen-orchestra/async-map": "^0.1.4",
"@xen-orchestra/async-map": "^0.1.3",
"@xen-orchestra/log": "^0.7.2",
"ensure-array": "^1.0.0"
},

View File

@@ -7,7 +7,6 @@ const { warn } = createLogger('vates:nbd-client:multi')
export default class MultiNbdClient {
#clients = []
#nbdConcurrency
#nextClient = 0
#options
#readAhead
#settings
@@ -98,31 +97,8 @@ export default class MultiNbdClient {
* @returns {Promise<Buffer>}
*/
async readBlock(index, size = NBD_DEFAULT_BLOCK_SIZE) {
const clientId = this.#nextClient++ % this.#clients.length
const client = this.#clients[clientId]
try {
return await client.readBlock(index, size)
} catch (err) {
// client.readBlock() already exhausted its own retries/reconnects: this connection is dead.
// Evict it so future reads stop being routed to it, and retry this read on a surviving
// client, since the data is usually still reachable through the others.
this.#evict(client)
if (this.#clients.length === 0) {
throw err
}
warn(`evicted a dead nbd client, retrying block ${index} on a remaining client`, { err })
return this.readBlock(index, size)
}
}
// no-op if `client` was already evicted by a concurrent failed read on the same client
#evict(client) {
const i = this.#clients.indexOf(client)
if (i === -1) {
return
}
this.#clients.splice(i, 1)
client.disconnect().catch(() => {})
const clientId = index % this.#clients.length
return this.#clients[clientId].readBlock(index, size)
}
/**

View File

@@ -1,130 +0,0 @@
import { describe, it, mock } from 'node:test'
import assert from 'node:assert'
// MultiNbdClient instantiates NbdClient itself (`import NbdClient from './index.mjs'`), so the
// only way to control individual connections from a unit test is to replace that module with a
// fake before importing multi.mjs.
const created = []
class FakeNbdClient {
constructor(nbdInfo) {
this.nbdInfo = nbdInfo
this.disconnected = false
// overridden per-test after connect() to script this instance's behavior
this.readBlock = async () => {
throw new Error('readBlock not configured for this fake client')
}
created.push(this)
}
async connect() {}
async disconnect() {
this.disconnected = true
}
async getMap() {
return []
}
}
mock.module('./index.mjs', {
defaultExport: FakeNbdClient,
})
const { default: MultiNbdClient } = await import('./multi.mjs')
// a single settings entry is enough: connect() reuses candidates across concurrent slots, so
// nbdConcurrency still yields that many distinct FakeNbdClient instances
async function connectWithFakes(nbdConcurrency) {
created.length = 0
const client = new MultiNbdClient({ address: 'fake-host' }, { nbdConcurrency })
await client.connect()
return { client, fakes: created.slice() }
}
describe('MultiNbdClient.readBlock eviction', () => {
it('evicts a client whose readBlock rejects and retries on a surviving client', async () => {
const { client, fakes } = await connectWithFakes(2)
const [dead, alive] = fakes
dead.readBlock = async () => {
throw new Error('connection is dead')
}
alive.readBlock = async index => Buffer.from([index])
// index 0 initially routes to `dead` (0 % 2 === 0)
const data = await client.readBlock(0)
assert.deepStrictEqual(data, Buffer.from([0]))
assert.strictEqual(dead.disconnected, true, 'the dead client should have been disconnected')
// future reads must no longer be routed to the evicted client: with only `alive` left,
// every index should now succeed through it
alive.readBlock = async index => Buffer.from([index])
const data2 = await client.readBlock(1)
assert.deepStrictEqual(data2, Buffer.from([1]))
})
it('rejects once every client has been evicted, instead of retrying forever', async () => {
const { client, fakes } = await connectWithFakes(2)
const boom = new Error('connection is dead')
for (const fake of fakes) {
fake.readBlock = async () => {
throw boom
}
}
await assert.rejects(() => client.readBlock(0), boom)
assert.ok(
fakes.every(fake => fake.disconnected),
'every client should have been evicted (and disconnected) before giving up'
)
})
it('evicting the same dead client from concurrent failed reads is idempotent', async () => {
const { client, fakes } = await connectWithFakes(2)
const [dead, alive] = fakes
dead.readBlock = async () => {
throw new Error('connection is dead')
}
alive.readBlock = async index => Buffer.from([index])
let disconnectCalls = 0
dead.disconnect = async () => {
disconnectCalls++
dead.disconnected = true
}
// routing is round-robin by call order (0, 1, 0) over a 2-client pool: the 1st and 3rd
// calls both land on `dead` before either failure has been handled, so they race to evict
// the same client. The exact interleaving isn't guaranteed, only that eviction is safe
// either way and every read still completes.
const [data0, data1, data2] = await Promise.all([client.readBlock(10), client.readBlock(11), client.readBlock(12)])
assert.deepStrictEqual(data0, Buffer.from([10]))
assert.deepStrictEqual(data1, Buffer.from([11]))
assert.deepStrictEqual(data2, Buffer.from([12]))
assert.strictEqual(disconnectCalls, 1, 'the dead client must only be evicted/disconnected once')
})
})
describe('MultiNbdClient.readBlock routing', () => {
it('distributes reads round-robin by call order, not by index residue', async () => {
const { client, fakes } = await connectWithFakes(4)
const readsPerClient = fakes.map(() => 0)
fakes.forEach((fake, i) => {
fake.readBlock = async index => {
readsPerClient[i]++
return Buffer.from([index])
}
})
// a CBT-style changed-block set striding by exactly `nbdConcurrency`: under the old
// `index % clients.length` routing this would collapse entirely onto a single client
const stridedIndexes = Array.from({ length: 20 }, (_, i) => i * 4)
for (const index of stridedIndexes) {
await client.readBlock(index)
}
assert.deepStrictEqual(readsPerClient, [5, 5, 5, 5])
})
})

View File

@@ -13,7 +13,7 @@
"url": "https://vates.fr"
},
"license": "ISC",
"version": "3.5.1",
"version": "3.5.0",
"engines": {
"node": ">=14.0"
},
@@ -21,11 +21,11 @@
"dependencies": {
"@vates/async-each": "^1.0.3",
"@vates/read-chunk": "^1.2.1",
"@xen-orchestra/async-map": "^0.1.4",
"@xen-orchestra/async-map": "^0.1.3",
"@xen-orchestra/disk-transform": "^1.3.3",
"@xen-orchestra/log": "^0.7.2",
"promise-toolbox": "^0.21.0",
"xen-api": "^4.9.0"
"xen-api": "^4.8.0"
},
"devDependencies": {
"tap": "^18.7.0",
@@ -33,7 +33,6 @@
},
"scripts": {
"postversion": "npm publish --access public",
"test": "node --experimental-test-module-mocks --test",
"test-integration": "tap --allow-incomplete-coverage tests/*.integ.mjs"
"test-integration": "tap --allow-incomplete-coverage"
}
}

View File

@@ -153,13 +153,7 @@ CYu1Xn/FVPx1HoRgWc7E8wFhDcA/P3SJtfIQWHB9FzSaBflKGR4t8WCE2eE8+cTB
nbdServer = await spawnNbdKit(path)
}
}
// reading past the end of the export must reject: a single client so the failure isn't
// multiplied by nbdConcurrency evicting and retrying on every other (equally healthy, equally
// unable to satisfy this out-of-range request) client
const singleClient = new MultiNbdClient(connectionSettings, { nbdConcurrency: 1, readAhead: 2 })
await singleClient.connect()
await assert.rejects(() => singleClient.readBlock(100, CHUNK_SIZE))
await singleClient.disconnect()
assert.rejects(() => client.readBlock(100, CHUNK_SIZE))
await client.disconnect()
// double disconnection shouldn't pose any problem

View File

@@ -1,7 +1,7 @@
{
"name": "@vates/types",
"private": false,
"version": "2.0.1",
"version": "1.30.1",
"main": "./dist/index.mjs",
"exports": {
".": {

View File

@@ -755,9 +755,9 @@ export type TrafficRuleDirection = 'from' | 'to' | 'from/to'
export type RawTrafficRule = {
allow: boolean
protocol: TrafficRuleProtocol
protocol: string
ipRange: string
direction: TrafficRuleDirection
direction: string
port?: string
}

View File

@@ -4,34 +4,40 @@ import type { XoApp } from '../xo-app.mjs'
export type SecurityName = '*' | 'token' | 'basic' | 'none'
export type FieldDefinition = { optional?: boolean; nullable?: boolean } & (
export type FieldDefinition =
| {
type: 'string'
example?: string
optional?: boolean
}
| {
type: 'boolean'
example?: boolean
optional?: boolean
}
| {
type: 'number'
example?: number
optional?: boolean
}
| {
type: 'enum'
enum: string[]
example?: string
optional?: boolean
}
| {
type: 'object'
fields: Record<string, FieldDefinition>
optional?: boolean
}
| {
type: 'array'
items: FieldDefinition
example?: unknown[]
optional?: boolean
}
)
export type ParamFieldDefinition = Exclude<
FieldDefinition,
{ type: 'boolean' } | { type: 'object' } | { type: 'array' }

View File

@@ -204,8 +204,6 @@ export type XoApp = {
backupGuard(poolId: XoPool['id']): Promise<void>
/* Throw if no authorization */
checkFeatureAuthorization(featureCode: FeatureCode): Promise<void>
/* validate, apply and persist the configuration of a plugin */
configurePlugin(id: string, configuration: unknown, mergeWithExisting?: boolean): Promise<void>
/* connect a server (XCP-ng/XenServer) */
connectXenServer(id: XoServer['id']): Promise<void>
// TODO: replace all XoAclBasePrivilege with a more strict type. (discriminate union)
@@ -330,11 +328,10 @@ export type XoApp = {
xo: Record<XoBackupRepository['id'], XoConfigBackupArchive[]>
pool: Record<XoBackupRepository['id'], Record<XoPool['id'], XoPoolBackupArchive[]>>
}>
/** `null` when the listing of a backup repository failed */
listVmBackupsNg(
backupRepositoryIds: XoBackupRepository['id'][],
opts?: { _forceRefresh?: boolean; vmId?: XoVm['id'] }
): Promise<Record<XoBackupRepository['id'], Record<XoVm['id'], XoVmBackupArchive[]> | null>>
opts?: { _forceRefresh?: boolean; vmId: XoVm['id'] }
): Promise<Record<XoBackupRepository['id'], Record<XoVm['id'], XoVmBackupArchive[]>>>
pingRemote(id: XoBackupRepository['id']): Promise<{ success: true }>
/** Allow to add a new server in the DB (XCP-ng/XenServer) */
registerXenServer(

View File

@@ -19,7 +19,7 @@ import type {
VM_POWER_STATE,
} from './common.mjs'
import type * as CMType from './lib/complex-matcher.mjs'
import { XoAclBasePrivilege, XoAclRole } from './lib/xen-orchestra-acl.mjs'
import { XoAclPrivilege, XoAclRole, XoAclSupportedActionsByResource } from './lib/xen-orchestra-acl.mjs'
import type { XenApiHost, XenApiPool } from './xen-api.mjs'
type BaseXapiXo = {
@@ -889,7 +889,10 @@ export type XapiXoRecord =
| XoVtpm
| XoSm
export type NonXapiXoRecord =
export type NonXapiXoRecord<
TActionsByResource extends XoAclSupportedActionsByResource = never,
TResource extends string = never,
> =
| AnyXoBackupArchive
| AnyXoJob
| AnyXoLog
@@ -902,9 +905,12 @@ export type NonXapiXoRecord =
| XoTask
| XoUser
| XoAclRole
| XoAclBasePrivilege
| XoAclPrivilege<TActionsByResource, TResource>
export type XoRecord = XapiXoRecord | NonXapiXoRecord
export type XoRecord<
TActionsByResource extends XoAclSupportedActionsByResource = never,
TResource extends string = never,
> = XapiXoRecord | NonXapiXoRecord<TActionsByResource, TResource>
export type AnyXoVm = XoVm | XoVmSnapshot | XoVmTemplate | XoVmController

View File

@@ -6,7 +6,7 @@
"main": "dist/index.mjs",
"name": "@xen-orchestra/acl",
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/acl",
"version": "1.6.1",
"version": "1.5.0",
"license": "AGPL-3.0-or-later",
"private": false,
"type": "module",
@@ -30,7 +30,7 @@
},
"devDependencies": {
"@eslint/js": "^9.19.0",
"@vates/types": "^2.0.1",
"@vates/types": "^1.30.1",
"rimraf": "^6.0.1",
"typescript": "~5.6",
"typescript-eslint": "^8.61.0"

View File

@@ -13,11 +13,11 @@ const wrapCall = (fn, arg, thisArg) => {
*
* WARNING: Does not handle plain objects
*
* @template Item, Result, [This=Iterable<Item>]
* @template Item,This
* @param {Iterable<Item>} iterable
* @param {(this: This, item: Item) => (Result | PromiseLike<Result>)} mapFn
* @param {This} [thisArg] defaults to `iterable`
* @returns {Promise<Result[]>}
* @param {(this: This, item: Item) => (Item | PromiseLike<Item>)} mapFn
* @param {This} [thisArg]
* @returns {Promise<Item[]>}
*/
exports.asyncMap = function asyncMap(iterable, mapFn, thisArg = iterable) {
return Promise.all(Array.from(iterable, mapFn, thisArg))
@@ -26,11 +26,11 @@ exports.asyncMap = function asyncMap(iterable, mapFn, thisArg = iterable) {
/**
* Like `asyncMap` but wait for all promises to settle before rejecting
*
* @template Item, Result, [This=Iterable<Item>]
* @template Item,This
* @param {Iterable<Item>} iterable
* @param {(this: This, item: Item) => (Result | PromiseLike<Result>)} mapFn
* @param {This} [thisArg] defaults to `iterable`
* @returns {Promise<Result[]>}
* @param {(this: This, item: Item) => (Item | PromiseLike<Item>)} mapFn
* @param {This} [thisArg]
* @returns {Promise<Item[]>}
*/
exports.asyncMapSettled = function asyncMapSettled(iterable, mapFn, thisArg = iterable) {
return new Promise((resolve, reject) => {

View File

@@ -1,7 +1,7 @@
{
"private": false,
"name": "@xen-orchestra/async-map",
"version": "0.1.4",
"version": "0.1.3",
"license": "ISC",
"description": "Promise.all + map for all iterables",
"keywords": [

View File

@@ -1,6 +1,6 @@
{
"name": "@xen-orchestra/backup-archive",
"version": "1.1.2",
"version": "1.1.1",
"description": "Helper to handle generic backup archives",
"license": "ISC",
"author": "",
@@ -21,7 +21,7 @@
"tar-stream": "^3.1.8",
"@types/tar-stream": "^3.1.4",
"tmp": "^0.2.1",
"@vates/types": "^2.0.1"
"@vates/types": "^1.30.1"
},
"dependencies": {
"@xen-orchestra/log": "^0.7.1",

View File

@@ -7,8 +7,8 @@
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
"dependencies": {
"@vates/async-each": "^1.0.3",
"@xen-orchestra/async-map": "^0.1.4",
"@xen-orchestra/backups": "^0.74.2",
"@xen-orchestra/async-map": "^0.1.3",
"@xen-orchestra/backups": "^0.73.10",
"@xen-orchestra/fs": "^4.9.3",
"filenamify": "^6.0.0",
"getopts": "^2.2.5",
@@ -28,7 +28,7 @@
"scripts": {
"postversion": "npm publish --access public"
},
"version": "1.1.14",
"version": "1.1.13",
"license": "AGPL-3.0-or-later",
"author": {
"name": "Vates SAS",

View File

@@ -16,15 +16,42 @@ import { toQcow2Stream } from '@xen-orchestra/qcow2'
const ensureArray = value => (value === undefined ? [] : Array.isArray(value) ? value : [value])
// The dynamic memory range MUST respect this inequality at any moment: static_min <= dynamic_min <= dynamic_max <= static_max.
// VM.set_memory_limits sets all four fields atomically, so there is no intermediate state for XAPI to reject
export async function updateMemoryFields(xapi, targetVm, vmRecord) {
const staticMin = vmRecord.memory_static_min ?? targetVm.memory_static_min
const staticMax = vmRecord.memory_static_max ?? targetVm.memory_static_max
const dynamicMin = vmRecord.memory_dynamic_min ?? targetVm.memory_dynamic_min
const dynamicMax = vmRecord.memory_dynamic_max ?? targetVm.memory_dynamic_max
const orderedMemoryLimits = ['memory_static_min', 'memory_dynamic_min', 'memory_dynamic_max', 'memory_static_max']
await xapi.call('VM.set_memory_limits', targetVm.$ref, staticMin, staticMax, dynamicMin, dynamicMax)
// The dynamic memory range MUST respect this inequality at any moment: static_min <= dynamic_min <= dynamic_max <= static_max.
// We must update these properties in the right order to avoid XAPI error.
// The order depends on the values. It can be an increase, a decrease or a mix of both, so any order could be required.
export async function updateMemoryFields(xapi, targetVm, vmRecord) {
const memoryValues = {}
for (const key of orderedMemoryLimits) {
memoryValues[key] = {
currentValue: targetVm[key],
newValue: vmRecord[key] ?? targetVm[key],
}
}
while (await updateNextMemoryField(xapi, memoryValues, targetVm.$ref)) {
/* execute until all memory fields are updated */
}
}
// Update one more memory field if needed, then return a boolean describing if a field was updated or if all fields are up to date
async function updateNextMemoryField(xapi, memoryValues, vmRef) {
for (let i = 0; i < orderedMemoryLimits.length; i++) {
const currentField = memoryValues[orderedMemoryLimits[i]]
const nextField = i === orderedMemoryLimits.length - 1 ? undefined : memoryValues[orderedMemoryLimits[i + 1]]
if (
currentField.newValue !== currentField.currentValue &&
(nextField === undefined || currentField.newValue <= nextField.currentValue)
) {
// no need to check that previousField.currentValue <= currentField.newValue, as we can deduce it
await xapi.setField('VM', vmRef, orderedMemoryLimits[i], currentField.newValue)
await xapi.barrier()
currentField.currentValue = currentField.newValue
return true
}
}
return false
}
export async function exportIncrementalVm(

View File

@@ -0,0 +1,122 @@
import { describe, it } from 'node:test'
import assert from 'node:assert/strict'
import { updateMemoryFields } from './_incrementalVm.mjs'
const MOCK_REF = 'mockRef'
function mockXapiVm(initialMemoryValues) {
const res = {
...initialMemoryValues,
updates: [],
barrier: () => {},
setField: (type, ref, field, value) => {
assert(type === 'VM')
assert(ref === MOCK_REF)
res.updates.push({ field, value })
res[field] = value
assert(res.memory_static_min <= res.memory_dynamic_min)
assert(res.memory_dynamic_min <= res.memory_dynamic_max)
assert(res.memory_dynamic_max <= res.memory_static_max)
},
}
return res
}
const tests = [
{
label: 'handles a memory increase',
targetVm: {
memory_static_min: 2,
memory_dynamic_min: 4,
memory_dynamic_max: 4,
memory_static_max: 8,
},
vmRecord: {
memory_static_min: 16,
memory_dynamic_min: 16,
memory_dynamic_max: 16,
memory_static_max: 16,
},
expectedUpdates: [
{ field: 'memory_static_max', value: 16 },
{ field: 'memory_dynamic_max', value: 16 },
{ field: 'memory_dynamic_min', value: 16 },
{ field: 'memory_static_min', value: 16 },
],
},
{
label: 'handles a memory decrease',
targetVm: {
memory_static_min: 4,
memory_dynamic_min: 8,
memory_dynamic_max: 16,
memory_static_max: 16,
},
vmRecord: {
memory_static_min: 2,
memory_dynamic_min: 2,
memory_dynamic_max: 2,
memory_static_max: 2,
},
expectedUpdates: [
{ field: 'memory_static_min', value: 2 },
{ field: 'memory_dynamic_min', value: 2 },
{ field: 'memory_dynamic_max', value: 2 },
{ field: 'memory_static_max', value: 2 },
],
},
{
label: 'handles a more complex case',
targetVm: {
memory_static_min: 1,
memory_dynamic_min: 2,
memory_dynamic_max: 8,
memory_static_max: 16,
},
vmRecord: {
memory_static_min: 4,
memory_dynamic_min: 4,
memory_dynamic_max: 4,
memory_static_max: 4,
},
expectedUpdates: [
{ field: 'memory_dynamic_min', value: 4 },
{ field: 'memory_static_min', value: 4 },
{ field: 'memory_dynamic_max', value: 4 },
{ field: 'memory_static_max', value: 4 },
],
},
{
label: 'avoids updating identical value',
targetVm: {
memory_static_min: 1,
memory_dynamic_min: 2,
memory_dynamic_max: 4,
memory_static_max: 8,
},
vmRecord: {
memory_static_min: 1,
memory_dynamic_min: 2,
memory_dynamic_max: 4,
memory_static_max: 8,
},
expectedUpdates: [],
},
]
describe('updateMemoryFields() should update in the right order', () => {
for (const test of tests) {
it(test.label, async () => {
const mock = mockXapiVm(test.targetVm)
test.targetVm.$ref = MOCK_REF
await updateMemoryFields(mock, test.targetVm, test.vmRecord)
assert.equal(mock.updates.length, test.expectedUpdates.length)
for (let i = 0; i < mock.updates.length; i++) {
assert.equal(mock.updates[i].value, test.expectedUpdates[i].value)
assert.equal(mock.updates[i].field, test.expectedUpdates[i].field)
}
})
}
})

View File

@@ -9,9 +9,7 @@ import { getAdaptersByRemote } from './_getAdaptersByRemote.mjs'
import { IncrementalXapi } from './_vmRunners/IncrementalXapi.mjs'
import { FullXapi } from './_vmRunners/FullXapi.mjs'
import { Throttle } from '@vates/generator-toolbox'
import { asyncEach } from '@vates/async-each'
import createStreamThrottle from './_createStreamThrottle.mjs'
import { selectSynchronizedSnapshotVms } from './_selectSynchronizedSnapshotVms.mjs'
const noop = Function.prototype
@@ -33,9 +31,7 @@ const DEFAULT_XAPI_VM_SETTINGS = {
nRetriesVmBackupFailures: 0,
offlineBackup: false,
offlineSnapshot: false,
snapshotConcurrency: 2,
snapshotRetention: 0,
synchronizedSnapshot: false,
timeout: 0,
useNbd: false,
unconditionalSnapshot: false,
@@ -51,16 +47,6 @@ export const VmsXapi = class VmsXapiBackupRunner extends Abstract {
return baseSettings
}
_getVmBackup(jobMode, opts) {
if (jobMode === 'delta') {
return new IncrementalXapi(opts)
} else if (jobMode === 'full') {
return new FullXapi(opts)
}
throw new Error(`Job mode ${jobMode} not implemented`)
}
async run() {
const job = this._job
@@ -109,81 +95,7 @@ export const VmsXapi = class VmsXapiBackupRunner extends Abstract {
const allSettings = this._job.settings
const baseSettings = this._baseSettings
const preTakenTimestampByVmId = {}
const failedSnapshotByVmId = {}
if (settings.synchronizedSnapshot) {
await Task.run({ properties: { name: 'snapshot VMs' } }, async () => {
await Disposable.use(
Disposable.all(vmIds.map(vmId => this._getRecord('VM', vmId).catch(noop))),
async vms => {
// remove vms that failed (already handled)
vms = vms.filter(_ => _ !== undefined)
const batchIds = selectSynchronizedSnapshotVms(settings.synchronizedSnapshot, vms)
await asyncEach(
[...vms].filter(vm => batchIds.has(vm.uuid)),
async vm => {
const vmSettings = { ...settings, ...allSettings[vm.uuid] }
const opts = {
baseSettings,
config,
getSnapshotNameLabel,
healthCheckSr,
job,
remoteAdapters,
schedule,
settings: vmSettings,
srs,
throttleGenerator,
throttleStream,
vm,
}
let vmBackup
try {
vmBackup = this._getVmBackup(job.mode, opts)
if (
!vmBackup._settings.offlineBackup &&
!vmBackup._settings.offlineSnapshot &&
(await vmBackup._mustDoSnapshot())
) {
await vmBackup._prepareAndSnapshot()
preTakenTimestampByVmId[vm.uuid] = vmBackup.timestamp
}
} catch (error) {
failedSnapshotByVmId[vm.uuid] = error
if (vmBackup !== undefined) {
try {
await vmBackup._fetchJobSnapshots()
await vmBackup._removeUnusedSnapshots()
await vmBackup._cleanMetadata()
} catch (cleanupError) {
// Best effort cleanup
}
}
}
},
{
concurrency: settings.snapshotConcurrency,
}
)
}
)
})
}
const snapshotedVmIds = new Set(vmIds.filter(id => !(id in failedSnapshotByVmId)))
Object.entries(failedSnapshotByVmId).forEach(([vmId, error]) => {
Task.run(
{
properties: { id: vmId, name: 'backup VM', type: 'VM' },
},
() => Promise.reject(error)
).catch(noop)
})
const queue = new Set(snapshotedVmIds)
const queue = new Set(vmIds)
const taskByVmId = {}
const nTriesByVmId = {}
@@ -243,16 +155,21 @@ export const VmsXapi = class VmsXapiBackupRunner extends Abstract {
schedule,
settings: vmSettings,
srs,
// when set, the batch phase already snapshotted this VM; the
// runner re-finds that snapshot by its metadata (see _snapshot)
synchronizedSnapshotTimestamp: preTakenTimestampByVmId[vmUuid],
throttleGenerator,
throttleStream,
vm,
}
const vmBackup = this._getVmBackup(job.mode, opts)
let vmBackup
if (job.mode === 'delta') {
vmBackup = new IncrementalXapi(opts)
} else {
if (job.mode === 'full') {
vmBackup = new FullXapi(opts)
} else {
throw new Error(`Job mode ${job.mode} not implemented`)
}
}
return vmBackup.run().catch(error => {
taskError = error
})

View File

@@ -1,10 +1,13 @@
import { asyncMap } from '@xen-orchestra/async-map'
import { createLogger } from '@xen-orchestra/log'
import { Task } from '@vates/task'
import { DIR_XO_POOL_METADATA_BACKUPS } from '../RemoteAdapter.mjs'
import { forkStreamUnpipe } from './_forkStreamUnpipe.mjs'
import { formatFilenameDate } from '../_filenameDate.mjs'
const { debug } = createLogger('xo:backups:PoolMetadataBackup')
export const PATH_DB_DUMP = '/pool/xmldbdump'
export class PoolMetadataBackup {
@@ -31,7 +34,22 @@ export class PoolMetadataBackup {
const poolDir = `${DIR_XO_POOL_METADATA_BACKUPS}/${schedule.id}/${pool.$id}`
const dir = `${poolDir}/${formatFilenameDate(timestamp)}`
const tasks = Object.values(pool.$xapi.objects.indexes.type.task ?? {})
const pendingTasks = tasks.filter(task => task.status === 'pending')
debug('pool tasks before export', {
total: tasks.length,
pending: pendingTasks.length,
oldestPendingCreated: pendingTasks.map(task => task.created).sort()[0],
})
const stream = (await this._exportPoolMetadata()).body
const startedAt = Date.now()
let bytes = 0
stream.once('data', () => debug('first byte received', { afterMs: Date.now() - startedAt }))
stream.on('data', chunk => (bytes += chunk.length))
stream.once('end', () => debug('stream ended', { bytes, durationMs: Date.now() - startedAt }))
stream.once('error', error => debug('stream errored', { error, bytes, durationMs: Date.now() - startedAt }))
const fileName = `${dir}/data`
const metadata = JSON.stringify(

View File

@@ -1,19 +0,0 @@
import { Task } from '@vates/task'
export function selectSynchronizedSnapshotVms(synchronizedSnapshot, vms) {
if (synchronizedSnapshot === false) {
return new Set()
}
let matches
if (typeof synchronizedSnapshot === 'string') {
matches = vms.filter(vm => vm.tags.includes(synchronizedSnapshot))
} else if (synchronizedSnapshot === true) {
matches = vms
} else {
Task.warning('unsupported synchronizedSnapshot option, expected a boolean or a tag name', { synchronizedSnapshot })
return new Set()
}
return new Set(matches.map(vm => vm.uuid))
}

View File

@@ -1,73 +0,0 @@
import { describe, it } from 'node:test'
import assert from 'node:assert/strict'
import { selectSynchronizedSnapshotVms } from './_selectSynchronizedSnapshotVms.mjs'
// Helper mirroring the minimal VM record shape the selector needs.
const vm = (uuid, tags = []) => ({ uuid, tags })
// `selectSynchronizedSnapshotVms(synchronizedSnapshot, vms)` must return a Set
// of the UUIDs that should be snapshotted together in the batch phase.
// - false -> feature off, no VM is batched
// - true -> every VM is batched, regardless of tags
// - '<tagName>' -> only VMs carrying that exact tag are batched
const tests = [
{
label: 'false disables batching',
setting: false,
vms: [vm('a', ['prod']), vm('b')],
expected: [],
},
{
label: 'true batches every VM regardless of tags',
setting: true,
vms: [vm('a', ['prod']), vm('b')],
expected: ['a', 'b'],
},
{
label: 'tag batches only the VMs carrying it',
setting: 'prod',
vms: [vm('a', ['prod']), vm('b', ['dev']), vm('c', ['prod', 'other'])],
expected: ['a', 'c'],
},
{
label: 'tag carried by no VM yields an empty batch',
setting: 'prod',
vms: [vm('a', ['dev']), vm('b')],
expected: [],
},
{
label: 'tag carried by every VM batches all of them',
setting: 'prod',
vms: [vm('a', ['prod']), vm('b', ['prod'])],
expected: ['a', 'b'],
},
{
label: 'empty VM list yields an empty batch',
setting: true,
vms: [],
expected: [],
},
{
label: 'empty-string tag matches no VM when none carries an empty tag',
setting: '',
vms: [vm('a', ['prod'])],
expected: [],
},
{
label: 'non boolean or string yiels an empty batch',
setting: 1,
vms: [vm('a', ['prod'])],
expected: [],
},
]
describe('selectSynchronizedSnapshotVms()', () => {
for (const { label, setting, vms, expected } of tests) {
it(label, () => {
const result = selectSynchronizedSnapshotVms(setting, vms)
assert.ok(result instanceof Set, 'result must be a Set')
assert.deepEqual([...result].sort(), [...expected].sort())
})
}
})

View File

@@ -80,14 +80,14 @@ export const FullXapi = class FullXapiVmBackupRunner extends AbstractXapi {
const sizeContainer = watchStreamSize(stream)
const transferStart = Date.now()
const timestamp = Date.now()
await this._callWriters(
writer =>
writer.run({
maxStreamLength,
sizeContainer,
stream: forkStreamUnpipe(stream),
timestamp: this.timestamp,
timestamp,
vm,
vmSnapshot: exportedVm,
}),
@@ -95,7 +95,8 @@ export const FullXapi = class FullXapiVmBackupRunner extends AbstractXapi {
)
const { size } = sizeContainer
const duration = Date.now() - transferStart
const end = Date.now()
const duration = end - timestamp
debug('transfer complete', {
duration,
speed: duration !== 0 ? (size * 1e3) / 1024 / 1024 / duration : 0,

View File

@@ -13,6 +13,7 @@ import {
DELTA_CHAIN_LENGTH,
EXPORTED_SUCCESSFULLY,
setVmDeltaChainLength,
markExportSuccessfull,
} from '../../_otherConfig.mjs'
import { ThrottledDisk, SynchronizedDisk } from '@xen-orchestra/disk-transform'
import { AggregatedIncrementalRemoteWriter } from '../_writers/AggregatedIncrementalRemoteWriter.mjs'
@@ -67,6 +68,7 @@ export const IncrementalXapi = class IncrementalXapiVmBackupRunner extends Abstr
// @todo : reimplement throttle
const timestamp = Date.now()
await this._callWriters(
writer =>
writer.transfer({
@@ -76,7 +78,7 @@ export const IncrementalXapi = class IncrementalXapiVmBackupRunner extends Abstr
// clean; the tip's flag lets the next run skip re-probing
includeNonNbdQcow2Fix: true,
isVhdDifferencing,
timestamp: this.timestamp,
timestamp,
vm,
vmSnapshot: exportedVm,
}),
@@ -89,6 +91,11 @@ export const IncrementalXapi = class IncrementalXapiVmBackupRunner extends Abstr
await setVmDeltaChainLength(this._xapi, exportedVm.$ref, (this._deltaChainLength ?? 0) + 1)
}
// not the case if offlineBackup
if (exportedVm.is_a_snapshot) {
await markExportSuccessfull(this._xapi, exportedVm.$ref)
}
await this._callWriters(writer => writer.cleanup(), 'writer.cleanup()')
}

View File

@@ -7,7 +7,6 @@ import { asyncEach } from '@vates/async-each'
import { decorateMethodsWith } from '@vates/decorate-with'
import { defer } from 'golike-defer'
import { Task } from '@vates/task'
import { formatDateTime } from '@xen-orchestra/xapi'
import { getOldEntries } from '../../_getOldEntries.mjs'
import { Abstract } from './_Abstract.mjs'
@@ -17,11 +16,9 @@ import {
JOB_ID,
SCHEDULE_ID,
VM_UUID,
EXPORTED_SUCCESSFULLY,
resetVmOtherConfig,
setVmOtherConfig,
setVmSnapshotContentKeys,
markExportSuccessfull,
} from '../../_otherConfig.mjs'
const { warn, info } = createLogger('xo:backups:AbstractXapi')
@@ -39,7 +36,6 @@ export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract {
schedule,
settings,
srs,
synchronizedSnapshotTimestamp,
throttleGenerator,
throttleStream,
vm,
@@ -67,9 +63,9 @@ export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract {
// VM (snapshot) that is really exported
this._exportedVm = undefined
this._synchronizedSnapshotTimestamp = synchronizedSnapshotTimestamp
this._vm = vm
this._baseVdis = undefined
this._getSnapshotNameLabel = getSnapshotNameLabel
this._isIncremental = job.mode === 'delta'
this._healthCheckSr = healthCheckSr
@@ -184,27 +180,6 @@ export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract {
}
async _snapshot() {
if (this._synchronizedSnapshotTimestamp !== undefined) {
const datetime = formatDateTime(this._synchronizedSnapshotTimestamp)
const candidates = this._vm.$snapshots.filter(
snapshot =>
snapshot?.other_config[JOB_ID] === this._jobId &&
snapshot?.other_config[DATETIME] === datetime &&
!snapshot.other_config[EXPORTED_SUCCESSFULLY]
)
if (candidates.length === 1) {
this._exportedVm = candidates[0]
this.timestamp = this._synchronizedSnapshotTimestamp
return
}
warn('expected exactly one synchronized snapshot to reuse, taking a fresh one', {
vm: this._vm.uuid,
datetime,
count: candidates.length,
})
}
const vm = this._vm
const xapi = this._xapi
@@ -421,20 +396,7 @@ export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract {
await xapi.barrier()
// ensure cached object are up to date
this._jobSnapshotVdis = this._jobSnapshotVdis.map(vdi => xapi.getObject(vdi.$ref))
let disklessVmSnapshots = this._disklessJobSnapshotVms.map(vm => xapi.getObject(vm.$ref))
// The synchronized snapshot for this run is taken up-front by the batch
// phase but transferred later. Until it has been exported, hide it from
// retention so it is neither reclaimed nor allowed to steal the delta
// base's "most recent" protection. This makes the pre-transfer pass behave
// like a normal run, where the snapshot does not exist yet.
if (this._synchronizedSnapshotTimestamp !== undefined) {
const datetime = formatDateTime(this._synchronizedSnapshotTimestamp)
const isInFlightSyncSnapshot = ({ other_config }) =>
other_config[DATETIME] === datetime && !other_config[EXPORTED_SUCCESSFULLY]
this._jobSnapshotVdis = this._jobSnapshotVdis.filter(vdi => !isInFlightSyncSnapshot(vdi))
disklessVmSnapshots = disklessVmSnapshots.filter(vm => !isInFlightSyncSnapshot(vm))
}
const disklessVmSnapshots = this._disklessJobSnapshotVms.map(vm => xapi.getObject(vm.$ref))
// get the datetime of the most recent snapshot across both VDI and diskless VM snapshots
const lastSnapshotDateTime = [...this._jobSnapshotVdis, ...disklessVmSnapshots]
@@ -502,7 +464,6 @@ export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract {
vm = vdiVm
}
}
if (vm?.$ref !== undefined) {
return xapi.VM_destroy(vm.$ref)
} else {
@@ -532,7 +493,6 @@ export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract {
if (this.job.mode === 'delta' && datetime === lastSnapshotDateTime) {
return
}
await xapi.VM_destroy(snapshotPerDatetime[datetime])
})
})
@@ -591,11 +551,6 @@ export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract {
throw new Error('Not implemented')
}
async _prepareAndSnapshot() {
await this._cleanMetadata()
await this._snapshot()
}
async run($defer) {
const settings = this._settings
assert(
@@ -659,10 +614,6 @@ export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract {
if (this._writers.size !== 0) {
await this._copy()
}
// not the case if offlineBackup
if (this._exportedVm.is_a_snapshot) {
await markExportSuccessfull(this._xapi, this._exportedVm.$ref)
}
} finally {
if (startAfter) {
ignoreErrors.call(vm.$callAsync('start', false, false))

View File

@@ -1,327 +0,0 @@
import { describe, it } from 'node:test'
import assert from 'node:assert/strict'
import { formatDateTime } from '@xen-orchestra/xapi'
import { AbstractXapi } from './_AbstractXapi.mjs'
import { DATETIME, EXPORTED_SUCCESSFULLY, JOB_ID, SCHEDULE_ID } from '../../_otherConfig.mjs'
// Build an AbstractXapi instance without running its heavy constructor, then
// assign only the fields the method under test reads.
const makeRunner = props => Object.assign(Object.create(AbstractXapi.prototype), props)
describe('_snapshot() synchronized-snapshot reuse guard', () => {
it('reuses the batch snapshot, found by metadata, without snapshotting again', async () => {
let mustDoSnapshotCalls = 0
const timestamp = 1717200000000
// the snapshot taken by the batch phase, identified by its other_config
const batchSnapshot = {
$ref: 'batch-snapshot-ref',
other_config: {
[JOB_ID]: 'job-1',
[SCHEDULE_ID]: 'schedule-1',
[DATETIME]: formatDateTime(timestamp),
},
}
const runner = makeRunner({
_synchronizedSnapshotTimestamp: timestamp,
_jobId: 'job-1',
scheduleId: 'schedule-1',
_vm: { uuid: 'vm-uuid', $snapshots: [batchSnapshot] },
_xapi: { barrier: async () => {} },
// if the metadata lookup fails, _snapshot() falls through to here
_mustDoSnapshot: async () => {
mustDoSnapshotCalls++
return false
},
})
await runner._snapshot()
assert.equal(mustDoSnapshotCalls, 0, '_mustDoSnapshot() should not be called when the batch snapshot is found')
assert.equal(runner._exportedVm, batchSnapshot, '_exportedVm should be the batch snapshot found by metadata')
assert.equal(runner.timestamp, timestamp, 'timestamp should be the synchronized snapshot timestamp')
})
it('takes a fresh snapshot when the batch snapshot can no longer be found', async () => {
let mustDoSnapshotCalls = 0
const vm = { uuid: 'vm-uuid', $snapshots: [] } // the pre-taken snapshot is gone
const runner = makeRunner({
_synchronizedSnapshotTimestamp: 1717200000000,
_jobId: 'job-1',
scheduleId: 'schedule-1',
_vm: vm,
_xapi: { barrier: async () => {} },
_settings: {},
_mustDoSnapshot: async () => {
mustDoSnapshotCalls++
return false
},
})
await runner._snapshot()
assert.equal(
mustDoSnapshotCalls,
1,
'a missing batch snapshot should not be reused: _snapshot() should fall through and take a fresh snapshot'
)
assert.equal(
runner._exportedVm,
vm,
'_exportedVm should fall back to the freshly-snapshotted VM, not stay on a stale/missing batch snapshot'
)
})
it('without a pre-taken snapshot and no snapshot needed, exports the live VM', async () => {
let mustDoSnapshotCalls = 0
const vm = { uuid: 'vm-uuid' }
const runner = makeRunner({
_exportedVm: undefined,
_vm: vm,
_xapi: {},
_settings: {},
_mustDoSnapshot: async () => {
mustDoSnapshotCalls++
return false
},
})
await runner._snapshot()
assert.equal(mustDoSnapshotCalls, 1, '_mustDoSnapshot() should be called once when no snapshot was pre-taken')
assert.equal(runner._exportedVm, vm, '_exportedVm should be set to the live VM when no snapshot is needed')
assert.equal(typeof runner.timestamp, 'number', 'timestamp should be set when exporting the live VM')
})
})
describe('_removeUnusedSnapshots() protects the pre-taken synchronized snapshot', () => {
const OLD_DATETIME = '20240101T00:00:00Z'
const SYNC_TIMESTAMP = 1717200000000
const SYNC_DATETIME = formatDateTime(SYNC_TIMESTAMP)
// Full-mode job with snapshotRetention 0, so retention wants to remove every
// snapshot. `vdi-fresh` is the snapshot taken up-front by the synchronized
// batch phase (identified by SYNC_TIMESTAMP); until it is exported it must be
// hidden from retention.
const makeRemoveRunner = ({ exported }) => {
const destroyed = []
const snapshotVm = ($ref, name_label) => ({
$ref,
name_label,
is_control_domain: false,
$snapshot_of: 'live-vm-ref',
other_config: {},
})
const freshSnapshotVm = snapshotVm('vm-fresh', 'fresh')
const oldSnapshotVm = snapshotVm('vm-old', 'old')
const vdi = ($ref, datetime, snapshotVmRecord, isExported = false) => ({
$ref,
other_config: {
[DATETIME]: datetime,
[SCHEDULE_ID]: 'schedule-1',
...(isExported ? { [EXPORTED_SUCCESSFULLY]: 'true' } : {}),
},
$VBDs: [{ $VM: snapshotVmRecord }],
})
// the old snapshot was exported by a previous run
const oldVdi = vdi('vdi-old', OLD_DATETIME, oldSnapshotVm, true)
const freshVdi = vdi('vdi-fresh', SYNC_DATETIME, freshSnapshotVm, exported)
const registry = { 'vdi-old': oldVdi, 'vdi-fresh': freshVdi }
const runner = makeRunner({
_synchronizedSnapshotTimestamp: SYNC_TIMESTAMP,
_vm: { uuid: 'live-uuid', $snapshots: [] },
_baseSettings: { snapshotRetention: 0 },
_jobSnapshotVdis: [oldVdi, freshVdi],
_disklessJobSnapshotVms: [],
job: { mode: 'full', settings: {} },
_xapi: {
barrier: async () => {},
getObject: ref => registry[ref],
VM_destroy: async ref => {
destroyed.push(ref)
},
VDI_destroy: async ref => {
destroyed.push(ref)
},
},
})
return { runner, destroyed }
}
it('does not destroy the pre-taken snapshot before it has been transferred', async () => {
const { runner, destroyed } = makeRemoveRunner({ exported: false })
await runner._removeUnusedSnapshots()
assert.deepEqual(destroyed, ['vm-old'], 'only the older snapshot should be removed')
})
it('destroys the snapshot once it has been transferred (retention 0, no leak)', async () => {
const { runner, destroyed } = makeRemoveRunner({ exported: true })
await runner._removeUnusedSnapshots()
assert.deepEqual(destroyed.sort(), ['vm-fresh', 'vm-old'], 'both snapshots should be removed after transfer')
})
})
describe('_removeUnusedSnapshots() in the synchronized batch pre-snapshot state', () => {
it('delta mode: keeps the base snapshot when the fresh one has not been taken yet', async () => {
const destroyed = []
const BASE_DATETIME = '2024-06-01T00:00:00Z'
const baseSnapshotVm = {
$ref: 'vm-base',
name_label: 'base',
is_control_domain: false,
$snapshot_of: 'live-vm-ref',
// exported successfully by the previous run
other_config: { [EXPORTED_SUCCESSFULLY]: 'true' },
}
const baseVdi = {
$ref: 'vdi-base',
other_config: { [DATETIME]: BASE_DATETIME, [SCHEDULE_ID]: 'schedule-1' },
$VBDs: [{ $VM: baseSnapshotVm }],
}
const registry = { 'vdi-base': baseVdi }
const runner = makeRunner({
// batch pre-snapshot state: the synchronized snapshot has not been taken yet
_exportedVm: undefined,
_vm: { uuid: 'live-uuid', $snapshots: [] },
_baseSettings: { snapshotRetention: 0 },
_jobSnapshotVdis: [baseVdi],
_disklessJobSnapshotVms: [],
job: { mode: 'delta', settings: {} },
_xapi: {
barrier: async () => {},
getObject: ref => registry[ref],
VM_destroy: async ref => {
destroyed.push(ref)
},
VDI_destroy: async ref => {
destroyed.push(ref)
},
},
})
await runner._removeUnusedSnapshots()
assert.deepEqual(destroyed, [], 'the base snapshot must be kept as the delta base for the upcoming transfer')
})
})
describe('_removeUnusedSnapshots() reclaims orphan / CBT snapshot VDIs (no attached VM)', () => {
// Guards the `else` branch: snapshot VDIs that are not attached to any user VM
// (e.g. CBT metadata, orphans) must be reclaimed via VDI_destroy, not VM_destroy.
it('destroys snapshot VDIs directly when they are not attached to any VM', async () => {
const vmDestroyed = []
const vdiDestroyed = []
const orphanVdi = {
$ref: 'vdi-orphan',
other_config: { [DATETIME]: '2024-01-01T00:00:00Z', [SCHEDULE_ID]: 'schedule-1' },
$VBDs: [], // not attached to any VM
}
const registry = { 'vdi-orphan': orphanVdi }
const runner = makeRunner({
_exportedVm: undefined,
_vm: { uuid: 'live-uuid', $snapshots: [] },
_baseSettings: { snapshotRetention: 0 },
_jobSnapshotVdis: [orphanVdi],
_disklessJobSnapshotVms: [],
job: { mode: 'full', settings: {} },
_xapi: {
barrier: async () => {},
getObject: ref => registry[ref],
VM_destroy: async ref => {
vmDestroyed.push(ref)
},
VDI_destroy: async ref => {
vdiDestroyed.push(ref)
},
},
})
await runner._removeUnusedSnapshots()
assert.deepEqual(vdiDestroyed, ['vdi-orphan'], 'the orphan VDI should be reclaimed via VDI_destroy')
assert.deepEqual(vmDestroyed, [], 'no VM should be destroyed for an orphan VDI')
})
})
describe('_removeUnusedSnapshots() diskless VM snapshots', () => {
const OLD_DATETIME = '20240101T00:00:00Z'
const SYNC_TIMESTAMP = 1717200000000
const SYNC_DATETIME = formatDateTime(SYNC_TIMESTAMP)
// A diskless VM's backup snapshots are tracked as VM snapshots (no VDIs to
// anchor them). `dl-fresh` is the snapshot the synchronized batch pre-took,
// identified by SYNC_TIMESTAMP.
const makeDisklessRunner = ({ exported, mode = 'full' }) => {
const destroyed = []
const oldSnap = { $ref: 'dl-old', other_config: { [DATETIME]: OLD_DATETIME, [SCHEDULE_ID]: 'schedule-1' } }
const freshSnap = { $ref: 'dl-fresh', other_config: { [DATETIME]: SYNC_DATETIME, [SCHEDULE_ID]: 'schedule-1' } }
if (exported) freshSnap.other_config[EXPORTED_SUCCESSFULLY] = 'true'
const registry = { 'dl-old': oldSnap, 'dl-fresh': freshSnap }
const runner = makeRunner({
_synchronizedSnapshotTimestamp: SYNC_TIMESTAMP,
_vm: { uuid: 'live-uuid', $snapshots: [] },
_baseSettings: { snapshotRetention: 0 },
_jobSnapshotVdis: [],
_disklessJobSnapshotVms: [oldSnap, freshSnap],
job: { mode, settings: {} },
_xapi: {
barrier: async () => {},
getObject: ref => registry[ref],
VM_destroy: async ref => {
destroyed.push(ref)
},
VDI_destroy: async ref => {
destroyed.push(ref)
},
},
})
return { runner, destroyed }
}
it('does not destroy the pre-taken diskless snapshot before it has been transferred', async () => {
const { runner, destroyed } = makeDisklessRunner({ exported: false })
await runner._removeUnusedSnapshots()
assert.deepEqual(destroyed, ['dl-old'], 'only the older diskless snapshot should be removed')
})
it('destroys the diskless snapshot once it has been transferred (retention 0, no leak)', async () => {
const { runner, destroyed } = makeDisklessRunner({ exported: true })
await runner._removeUnusedSnapshots()
assert.deepEqual(
destroyed.sort(),
['dl-fresh', 'dl-old'],
'both diskless snapshots should be removed after transfer'
)
})
it('keeps the most recent diskless snapshot in delta mode (base for next delta)', async () => {
// Even after transfer (exported), delta mode must retain the latest
// snapshot so the next run can compute its delta against it.
const { runner, destroyed } = makeDisklessRunner({ exported: true, mode: 'delta' })
await runner._removeUnusedSnapshots()
assert.deepEqual(destroyed, ['dl-old'], 'the most recent diskless snapshot should be kept in delta mode')
})
})

View File

@@ -263,7 +263,6 @@ export class IncrementalXapiWriter extends MixinXapiWriter(AbstractIncrementalWr
vdiRef: activeVdi.$ref,
baseRef: snapshot.$ref,
onlyListChangedBlocks: true,
preferNbd: this._settings.preferNbd,
})
await diffDisk.init()
if (diffDisk.getBlockIndexes().length === 0) {

View File

@@ -8,7 +8,7 @@
"type": "git",
"url": "https://github.com/vatesfr/xen-orchestra.git"
},
"version": "0.74.2",
"version": "0.73.10",
"engines": {
"node": ">=14.18"
},
@@ -27,17 +27,17 @@
"@vates/disposable": "^0.1.6",
"@vates/fuse-vhd": "^2.1.3",
"@vates/generator-toolbox": "^1.1.3",
"@vates/nbd-client": "^3.5.1",
"@vates/nbd-client": "^3.5.0",
"@vates/parse-duration": "^0.1.1",
"@vates/task": "^0.7.0",
"@vates/types": "^2.0.1",
"@xen-orchestra/async-map": "^0.1.4",
"@vates/types": "^1.30.1",
"@xen-orchestra/async-map": "^0.1.3",
"@xen-orchestra/disk-transform": "^1.3.3",
"@xen-orchestra/fs": "^4.9.3",
"@xen-orchestra/log": "^0.7.2",
"@xen-orchestra/qcow2": "^1.3.1",
"@xen-orchestra/template": "^0.1.1",
"@xen-orchestra/backup-archive": "^1.1.2",
"@xen-orchestra/backup-archive": "^1.1.1",
"app-conf": "^3.0.0",
"compare-versions": "^6.0.0",
"d3-time-format": "^4.1.0",
@@ -52,11 +52,11 @@
"parse-pairs": "^2.0.0",
"promise-toolbox": "^0.21.0",
"proper-lockfile": "^4.1.2",
"tar": "^7.5.19",
"tar": "^7.5.3",
"uuid": "^9.0.0",
"value-matcher": "^0.2.0",
"vhd-lib": "^4.16.1",
"xen-api": "^4.9.0",
"xen-api": "^4.8.0",
"yazl": "^2.5.1"
},
"devDependencies": {
@@ -67,7 +67,7 @@
"typescript": "^5.9.3"
},
"peerDependencies": {
"@xen-orchestra/xapi": "^8.10.5"
"@xen-orchestra/xapi": "^8.10.2"
},
"license": "AGPL-3.0-or-later",
"author": {

View File

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

View File

@@ -1,7 +1,7 @@
{
"private": false,
"name": "@xen-orchestra/disk-cli",
"version": "2.2.1",
"version": "2.2.0",
"description": "CLI tool to inspect and manage disks in backup repositories",
"type": "module",
"bin": {
@@ -20,8 +20,8 @@
},
"dependencies": {
"@vates/async-each": "^1.0.3",
"@xen-orchestra/backups": "^0.74.2",
"@xen-orchestra/backup-archive": "^1.1.2",
"@xen-orchestra/backups": "^0.73.10",
"@xen-orchestra/backup-archive": "^1.1.1",
"@xen-orchestra/disk-transform": "^1.3.3",
"@xen-orchestra/fs": "^4.9.3",
"@xen-orchestra/qcow2": "^1.3.1",

View File

@@ -16,13 +16,13 @@
"xo-immutable-remote": "./dist/protectRemotes.mjs"
},
"license": "AGPL-3.0-or-later",
"version": "3.0.5",
"version": "3.0.4",
"engines": {
"node": ">=20.0.0"
},
"dependencies": {
"@vates/async-each": "^1.0.3",
"@xen-orchestra/backups": "^0.74.2",
"@xen-orchestra/backups": "^0.73.10",
"@xen-orchestra/log": "^0.7.2",
"app-conf": "^3.0.0",
"execa": "^5.0.0",

View File

@@ -1,15 +1,5 @@
# ChangeLog
## **0.25.0** (2026-08-31)
- [Treeview] Add VM tree actions (PR [#10304](https://github.com/vatesfr/xen-orchestra/pull/10304))
- [Pool/networks] Add the possibility to create new network or bonded network (PR [#10145](https://github.com/vatesfr/xen-orchestra/pull/10145))
- [VM] Add VDIs page with table and side panel (PR [#10269](https://github.com/vatesfr/xen-orchestra/pull/10269))
- [Host/Network] Add ability to rescan physical network interfaces (PIFs) (PR [#10147](https://github.com/vatesfr/xen-orchestra/pull/10147))
- Fix inconsistent spacing in side panel cards (PR [#10279](https://github.com/vatesfr/xen-orchestra/pull/10279))
- Fix missing collapse buttons on pools and hosts in the tree sidebar (added `hasChildren` prop) (PR [#10244](https://github.com/vatesfr/xen-orchestra/pull/10244))
- [Pool/networks] Add the possibility to create new internal network (PR [#10235](https://github.com/vatesfr/xen-orchestra/pull/10235))
## **0.24.0** (2026-07-30)
- [Host/dashboard] Switch CPU and RAM panels order to match Pool dashboard layout (PR [#10059](https://github.com/vatesfr/xen-orchestra/pull/10059))

View File

@@ -1,6 +1,6 @@
{
"name": "@xen-orchestra/lite",
"version": "0.25.0",
"version": "0.24.0",
"type": "module",
"scripts": {
"dev": "GIT_HEAD=$(git rev-parse HEAD) vite",
@@ -23,21 +23,18 @@
"@fortawesome/vue-fontawesome": "^3.0.8",
"@intlify/unplugin-vue-i18n": "^6.0.3",
"@novnc/novnc": "~1.5.0",
"@regle/core": "^1.26.0",
"@regle/rules": "^1.26.0",
"@regle/schemas": "^1.20.2",
"@tsconfig/node18": "^18.2.4",
"@types/d3-time-format": "^4.0.3",
"@types/file-saver": "^2.0.7",
"@types/lodash-es": "^4.17.12",
"@types/node": "^18.19.80",
"@vates/types": "^2.0.1",
"@vates/types": "^1.30.1",
"@vitejs/plugin-vue": "^6.0.0",
"@vue/tsconfig": "^0.9.0",
"@vueuse/core": "^14.0.0",
"@vueuse/math": "^14.0.0",
"@vueuse/shared": "^14.0.0",
"@xen-orchestra/web-core": "^0.60.0",
"@xen-orchestra/web-core": "^0.58.0",
"complex-matcher": "^1.1.1",
"d3-time-format": "^4.1.0",
"decorator-synchronized": "^0.6.0",

View File

@@ -19,13 +19,13 @@
import AppHeader from '@/components/AppHeader.vue'
import AppLogin from '@/components/AppLogin.vue'
import AppNavigation from '@/components/AppNavigation.vue'
import { useUnreachableHosts } from '@/composables/unreachable-hosts.composable.ts'
import { usePoolStore } from '@/stores/xen-api/pool.store.ts'
import { useXenApiStore } from '@/stores/xen-api.store.ts'
import { useUnreachableHosts } from '@/composables/unreachable-hosts.composable'
import { usePoolStore } from '@/stores/xen-api/pool.store'
import { useXenApiStore } from '@/stores/xen-api.store'
import VtsOverlayList from '@core/components/overlay/VtsOverlayList.vue'
import VtsTooltipList from '@core/components/tooltip-list/VtsTooltipList.vue'
import { useChartTheme } from '@core/composables/chart-theme.composable.ts'
import { useUiStore } from '@core/stores/ui.store.ts'
import { useChartTheme } from '@core/composables/chart-theme.composable'
import { useUiStore } from '@core/stores/ui.store'
import { useActiveElement, useMagicKeys, whenever } from '@vueuse/core'
import { logicAnd } from '@vueuse/math'
import { computed } from 'vue'

View File

@@ -26,11 +26,11 @@
import AccountMenu from '@/components/account-menu/AccountMenu.vue'
import PoolOverrideWarning from '@/components/PoolOverrideWarning.vue'
import XoaButton from '@/components/XoaButton.vue'
import { useNavigationStore } from '@/stores/navigation.store.ts'
import { useNavigationStore } from '@/stores/navigation.store'
import { usePoolStore } from '@/stores/xen-api/pool.store.ts'
import UiButtonIcon from '@core/components/ui/button-icon/UiButtonIcon.vue'
import UiLogoText from '@core/components/ui/logo-text/UiLogoText.vue'
import { useUiStore } from '@core/stores/ui.store.ts'
import { useUiStore } from '@core/stores/ui.store'
import { storeToRefs } from 'pinia'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'

View File

@@ -42,9 +42,9 @@
import FormInput from '@/components/form/FormInput.vue'
import LoginError from '@/components/LoginError.vue'
import PoolOverrideWarning from '@/components/PoolOverrideWarning.vue'
import type { XenApiError } from '@/libs/xen-api/xen-api.types.ts'
import { usePageTitleStore } from '@/stores/page-title.store.ts'
import { useXenApiStore } from '@/stores/xen-api.store.ts'
import type { XenApiError } from '@/libs/xen-api/xen-api.types'
import { usePageTitleStore } from '@/stores/page-title.store'
import { useXenApiStore } from '@/stores/xen-api.store'
import VtsIcon from '@core/components/icon/VtsIcon.vue'
import VtsInputWrapper from '@core/components/input-wrapper/VtsInputWrapper.vue'
import UiButton from '@core/components/ui/button/UiButton.vue'

View File

@@ -5,7 +5,7 @@
</template>
<script lang="ts" setup>
import markdown from '@/libs/markdown.ts'
import markdown from '@/libs/markdown'
import { useEventListener } from '@vueuse/core'
import { computed, type Ref, ref } from 'vue'

View File

@@ -10,7 +10,7 @@
<script lang="ts" setup>
import StoryMenu from '@/components/component-story/StoryMenu.vue'
import InfraPoolList from '@/components/infra/InfraPoolList.vue'
import { useNavigationStore } from '@/stores/navigation.store.ts'
import { useNavigationStore } from '@/stores/navigation.store'
import { onClickOutside, tryOnMounted, useTimeoutFn, watchImmediate } from '@vueuse/core'
import { storeToRefs } from 'pinia'
import { ref, watch } from 'vue'

View File

@@ -5,7 +5,7 @@
</template>
<script lang="ts" setup>
import { type AcceptedLanguage, highlight } from '@/libs/highlight.ts'
import { type AcceptedLanguage, highlight } from '@/libs/highlight'
import { computed } from 'vue'
const props = withDefaults(

View File

@@ -19,7 +19,7 @@
import UiActionButton from '@/components/ui/UiActionButton.vue'
import UiFilter from '@/components/ui/UiFilter.vue'
import UiFilterGroup from '@/components/ui/UiFilterGroup.vue'
import type { Filters } from '@/types/filter.ts'
import type { Filters } from '@/types/filter'
import { useOverlay } from '@core/packages/overlay/use-overlay.ts'
import { useI18n } from 'vue-i18n'

View File

@@ -19,8 +19,8 @@
<script lang="ts" setup>
import UiActionButton from '@/components/ui/UiActionButton.vue'
import { buildComplexMatcherNode } from '@/libs/complex-matcher.utils.ts'
import type { Filter, FilterComparisons, FilterComparisonType, Filters, FilterType, NewFilter } from '@/types/filter.ts'
import { buildComplexMatcherNode } from '@/libs/complex-matcher.utils'
import type { Filter, FilterComparisons, FilterComparisonType, Filters, FilterType, NewFilter } from '@/types/filter'
import VtsSelect from '@core/components/select/VtsSelect.vue'
import UiInput from '@core/components/ui/input/UiInput.vue'
import { useFormSelect } from '@core/packages/form-select'

View File

@@ -22,7 +22,7 @@
import UiActionButton from '@/components/ui/UiActionButton.vue'
import UiFilter from '@/components/ui/UiFilter.vue'
import UiFilterGroup from '@/components/ui/UiFilterGroup.vue'
import type { ActiveSorts, Sorts } from '@/types/sort.ts'
import type { ActiveSorts, Sorts } from '@/types/sort'
import VtsIcon from '@core/components/icon/VtsIcon.vue'
import { useOverlay } from '@core/packages/overlay/use-overlay.ts'
import { useI18n } from 'vue-i18n'

View File

@@ -14,12 +14,12 @@
</template>
<script lang="ts" setup>
import type { XenApiPatchWithHostRefs } from '@/composables/host-patches.composable.ts'
import type { XenApiPatchWithHostRefs } from '@/composables/host-patches.composable'
import VtsRow from '@core/components/table/VtsRow.vue'
import VtsTable from '@core/components/table/VtsTable.vue'
import { useTableState } from '@core/composables/table-state.composable.ts'
import { useUiStore } from '@core/stores/ui.store.ts'
import { usePatchColumns } from '@core/tables/column-sets/patch-columns.ts'
import { useTableState } from '@core/composables/table-state.composable'
import { useUiStore } from '@core/stores/ui.store'
import { usePatchColumns } from '@core/tables/column-sets/patch-columns'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'

View File

@@ -11,7 +11,7 @@
</template>
<script lang="ts" setup>
import type { XenApiError } from '@/libs/xen-api/xen-api.types.ts'
import type { XenApiError } from '@/libs/xen-api/xen-api.types'
import VtsIcon from '@core/components/icon/VtsIcon.vue'
import { useI18n } from 'vue-i18n'

View File

@@ -10,12 +10,12 @@
</template>
<script generic="T extends ObjectType" lang="ts" setup>
import type { ObjectType, ObjectTypeToRecord } from '@/libs/xen-api/xen-api.types.ts'
import type { XapiContext } from '@/stores/xen-api/create-xapi-store-config.ts'
import { useHostStore } from '@/stores/xen-api/host.store.ts'
import { usePoolStore } from '@/stores/xen-api/pool.store.ts'
import { useSrStore } from '@/stores/xen-api/sr.store.ts'
import { useVmStore } from '@/stores/xen-api/vm.store.ts'
import type { ObjectType, ObjectTypeToRecord } from '@/libs/xen-api/xen-api.types'
import type { XapiContext } from '@/stores/xen-api/create-xapi-store-config'
import { useHostStore } from '@/stores/xen-api/host.store'
import { usePoolStore } from '@/stores/xen-api/pool.store'
import { useSrStore } from '@/stores/xen-api/sr.store'
import { useVmStore } from '@/stores/xen-api/vm.store'
import UiLoader from '@core/components/ui/loader/UiLoader.vue'
import { computed, watch } from 'vue'
import { type RouteLocationTyped, type RouteRecordName } from 'vue-router'

View File

@@ -7,7 +7,7 @@
</template>
<script generic="T extends XenApiRecord<ObjectType>, I extends T['uuid']" lang="ts" setup>
import type { ObjectType, XenApiRecord } from '@/libs/xen-api/xen-api.types.ts'
import type { ObjectType, XenApiRecord } from '@/libs/xen-api/xen-api.types'
import ObjectNotFoundView from '@/pages/object-not-found.vue'
import UiLoader from '@core/components/ui/loader/UiLoader.vue'
import { computed } from 'vue'

View File

@@ -27,9 +27,9 @@
</template>
<script lang="ts" setup>
import { useXenApiStore } from '@/stores/xen-api.store.ts'
import { useXenApiStore } from '@/stores/xen-api.store'
import VtsIcon from '@core/components/icon/VtsIcon.vue'
import { vTooltip } from '@core/directives/tooltip.directive.ts'
import { vTooltip } from '@core/directives/tooltip.directive'
import { useSessionStorage } from '@vueuse/core'
import { useI18n } from 'vue-i18n'

View File

@@ -3,7 +3,7 @@
</template>
<script lang="ts" setup>
import { VM_POWER_STATE } from '@/libs/xen-api/xen-api.enums.ts'
import { VM_POWER_STATE } from '@/libs/xen-api/xen-api.enums'
import type { IconName } from '@core/icons'
import VtsIcon from '@core/components/icon/VtsIcon.vue'
import { computed } from 'vue'

View File

@@ -13,7 +13,7 @@
<script lang="ts" setup>
import TabItem from '@core/components/tab/TabItem.vue'
import { useDisabled } from '@core/composables/disabled.composable.ts'
import { useDisabled } from '@core/composables/disabled.composable'
import type { RouteLocationRaw } from 'vue-router'
const props = defineProps<{

View File

@@ -24,8 +24,8 @@
</template>
<script lang="ts" setup>
import type { PRIMARY_ADDRESS_TYPE } from '@/libs/xen-api/xen-api.enums.ts'
import { usePoolStore } from '@/stores/xen-api/pool.store.ts'
import type { PRIMARY_ADDRESS_TYPE } from '@/libs/xen-api/xen-api.enums'
import { usePoolStore } from '@/stores/xen-api/pool.store'
import UiButton from '@core/components/ui/button/UiButton.vue'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'

View File

@@ -31,11 +31,11 @@
</template>
<script lang="ts" setup>
import { useXenApiStore } from '@/stores/xen-api.store.ts'
import { useXenApiStore } from '@/stores/xen-api.store'
import MenuItem from '@core/components/menu/MenuItem.vue'
import MenuList from '@core/components/menu/MenuList.vue'
import UiAccountMenuButton from '@core/components/ui/account-menu-button/UiAccountMenuButton.vue'
import { vTooltip } from '@core/directives/tooltip.directive.ts'
import { vTooltip } from '@core/directives/tooltip.directive'
import { nextTick } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'

View File

@@ -87,7 +87,7 @@ import {
isSlotParam,
ModelParam,
type Param,
} from '@/libs/story/story-param.ts'
} from '@/libs/story/story-param'
import VtsIcon from '@core/components/icon/VtsIcon.vue'
import MenuItem from '@core/components/menu/MenuItem.vue'
import MenuList from '@core/components/menu/MenuList.vue'

View File

@@ -26,7 +26,7 @@
<script lang="ts" setup>
import CodeHighlight from '@/components/CodeHighlight.vue'
import StoryParamsTable from '@/components/component-story/StoryParamsTable.vue'
import type { EventParam } from '@/libs/story/story-param.ts'
import type { EventParam } from '@/libs/story/story-param'
defineProps<{
params: EventParam[]

View File

@@ -83,10 +83,10 @@
import CodeHighlight from '@/components/CodeHighlight.vue'
import StoryParamsTable from '@/components/component-story/StoryParamsTable.vue'
import StoryWidget from '@/components/component-story/StoryWidget.vue'
import useSortedCollection from '@/composables/sorted-collection.composable.ts'
import type { PropParam } from '@/libs/story/story-param.ts'
import useSortedCollection from '@/composables/sorted-collection.composable'
import type { PropParam } from '@/libs/story/story-param'
import VtsIcon from '@core/components/icon/VtsIcon.vue'
import { vTooltip } from '@core/directives/tooltip.directive.ts'
import { vTooltip } from '@core/directives/tooltip.directive'
import { useOverlay } from '@core/packages/overlay/use-overlay.ts'
import { useVModel } from '@vueuse/core'
import { toRef } from 'vue'

View File

@@ -37,7 +37,7 @@
<script lang="ts" setup>
import StoryParamsTable from '@/components/component-story/StoryParamsTable.vue'
import StoryWidget from '@/components/component-story/StoryWidget.vue'
import type { SettingParam } from '@/libs/story/story-param.ts'
import type { SettingParam } from '@/libs/story/story-param'
import { useVModel } from '@vueuse/core'
const props = defineProps<{

View File

@@ -27,7 +27,7 @@
<script lang="ts" setup>
import CodeHighlight from '@/components/CodeHighlight.vue'
import StoryParamsTable from '@/components/component-story/StoryParamsTable.vue'
import type { SlotParam } from '@/libs/story/story-param.ts'
import type { SlotParam } from '@/libs/story/story-param'
defineProps<{
params: SlotParam[]

View File

@@ -28,7 +28,7 @@ import {
isSelectWidget,
isTextWidget,
type Widget,
} from '@/libs/story/story-widget.ts'
} from '@/libs/story/story-widget'
import { useFormSelect } from '@core/packages/form-select'
import { computed, defineAsyncComponent } from 'vue'

View File

@@ -20,13 +20,13 @@
</template>
<script lang="ts" setup>
import { useContext } from '@/composables/context.composable.ts'
import { ColorContext } from '@/context.ts'
import type { Color } from '@/types/index.ts'
import { IK_INPUT_ID, IK_INPUT_TYPE } from '@/types/injection-keys.ts'
import { useContext } from '@/composables/context.composable'
import { ColorContext } from '@/context'
import type { Color } from '@/types'
import { IK_INPUT_ID, IK_INPUT_TYPE } from '@/types/injection-keys'
import type { IconName } from '@core/icons'
import VtsIcon from '@core/components/icon/VtsIcon.vue'
import { useDisabled } from '@core/composables/disabled.composable.ts'
import { useDisabled } from '@core/composables/disabled.composable'
import { useVModel } from '@vueuse/core'
import { computed, type HTMLAttributes, inject, ref, useAttrs } from 'vue'

View File

@@ -11,13 +11,13 @@
</template>
<script lang="ts" setup>
import type { XenApiHost } from '@/libs/xen-api/xen-api.types.ts'
import { useHostMetricsStore } from '@/stores/xen-api/host-metrics.store.ts'
import { usePoolStore } from '@/stores/xen-api/pool.store.ts'
import type { XenApiHost } from '@/libs/xen-api/xen-api.types'
import { useHostMetricsStore } from '@/stores/xen-api/host-metrics.store'
import { usePoolStore } from '@/stores/xen-api/pool.store'
import VtsIcon from '@core/components/icon/VtsIcon.vue'
import VtsObjectIcon from '@core/components/object-icon/VtsObjectIcon.vue'
import UiHeadBar from '@core/components/ui/head-bar/UiHeadBar.vue'
import { vTooltip } from '@core/directives/tooltip.directive.ts'
import { vTooltip } from '@core/directives/tooltip.directive'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'

View File

@@ -65,10 +65,10 @@ import VtsQuickInfoCard from '@core/components/quick-info-card/VtsQuickInfoCard.
import VtsTag from '@core/components/tag/VtsTag.vue'
import UiLink from '@core/components/ui/link/UiLink.vue'
import UiTagsList from '@core/components/ui/tag/UiTagsList.vue'
import useRelativeTime from '@core/composables/relative-time.composable.ts'
import { vTooltip } from '@core/directives/tooltip.directive.ts'
import { formatSizeRaw } from '@core/utils/size.util.ts'
import { parseDateTime } from '@core/utils/time.util.ts'
import useRelativeTime from '@core/composables/relative-time.composable'
import { vTooltip } from '@core/directives/tooltip.directive'
import { formatSizeRaw } from '@core/utils/size.util'
import { parseDateTime } from '@core/utils/time.util'
import { logicAnd } from '@vueuse/math'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'

View File

@@ -36,7 +36,7 @@ import VtsStateHero from '@core/components/state-hero/VtsStateHero.vue'
import UiCard from '@core/components/ui/card/UiCard.vue'
import UiCardNumbers from '@core/components/ui/card-numbers/UiCardNumbers.vue'
import UiCardTitle from '@core/components/ui/card-title/UiCardTitle.vue'
import { formatSizeRaw } from '@core/utils/size.util.ts'
import { formatSizeRaw } from '@core/utils/size.util'
import { logicAnd } from '@vueuse/math'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'

View File

@@ -20,7 +20,7 @@ import VtsStateHero from '@core/components/state-hero/VtsStateHero.vue'
import UiCard from '@core/components/ui/card/UiCard.vue'
import UiCardNumbers from '@core/components/ui/card-numbers/UiCardNumbers.vue'
import UiCardTitle from '@core/components/ui/card-title/UiCardTitle.vue'
import { useItemCounter } from '@core/composables/item-counter.composable.ts'
import { useItemCounter } from '@core/composables/item-counter.composable'
import { VM_POWER_STATE } from '@vates/types'
import { logicAnd, useSum } from '@vueuse/math'
import { computed } from 'vue'

View File

@@ -1,8 +1,8 @@
<template>
<VtsSidePanel :has-selection="!!pif" class="host-pif-side-panel" @close="emit('close')">
<VtsSidePanel :has-selection="!!pif" @close="emit('close')">
<template v-if="pif" #default>
<!-- PIF -->
<UiPanelCard>
<UiCard class="card">
<VtsCardObjectTitle :id="pif.uuid" :label="isBond ? t('bond') : t('pif')" />
<div class="content">
<!-- NETWORK -->
@@ -75,9 +75,9 @@
</template>
</VtsCardRowKeyValue>
</div>
</UiPanelCard>
</UiCard>
<!-- NETWORK INFORMATION -->
<UiPanelCard>
<UiCard class="card">
<UiCardTitle>{{ t('network-information') }}</UiCardTitle>
<div class="content">
<!-- IP ADDRESSES -->
@@ -180,9 +180,9 @@
</VtsCardRowKeyValue>
</div>
</div>
</UiPanelCard>
</UiCard>
<!-- PROPERTIES -->
<UiPanelCard>
<UiCard class="card">
<UiCardTitle>{{ t('properties') }}</UiCardTitle>
<div class="content">
<!-- MTU -->
@@ -219,16 +219,16 @@
</template>
</VtsCardRowKeyValue>
</div>
</UiPanelCard>
</UiCard>
</template>
</VtsSidePanel>
</template>
<script setup lang="ts">
import type { XenApiPif } from '@/libs/xen-api/xen-api.types.ts'
import { useNetworkStore } from '@/stores/xen-api/network.store.ts'
import { usePifMetricsStore } from '@/stores/xen-api/pif-metrics.store.ts'
import { usePifStore } from '@/stores/xen-api/pif.store.ts'
import type { XenApiPif } from '@/libs/xen-api/xen-api.types'
import { useNetworkStore } from '@/stores/xen-api/network.store'
import { usePifMetricsStore } from '@/stores/xen-api/pif-metrics.store'
import { usePifStore } from '@/stores/xen-api/pif.store'
import VtsCardRowKeyValue from '@core/components/card/VtsCardRowKeyValue.vue'
import VtsCardObjectTitle from '@core/components/card-object-title/VtsCardObjectTitle.vue'
import VtsCopyAllMenuItem from '@core/components/copy-all-menu-item/VtsCopyAllMenuItem.vue'
@@ -237,10 +237,10 @@ import VtsIcon from '@core/components/icon/VtsIcon.vue'
import VtsSidePanel from '@core/components/panel/VtsSidePanel.vue'
import VtsStatus from '@core/components/status/VtsStatus.vue'
import VtsTag from '@core/components/tag/VtsTag.vue'
import UiCard from '@core/components/ui/card/UiCard.vue'
import UiCardTitle from '@core/components/ui/card-title/UiCardTitle.vue'
import UiPanelCard from '@core/components/ui/panel-card/UiPanelCard.vue'
import UiTagsList from '@core/components/ui/tag/UiTagsList.vue'
import { vTooltip } from '@core/directives/tooltip.directive.ts'
import { vTooltip } from '@core/directives/tooltip.directive'
import humanFormat from 'human-format'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
@@ -317,7 +317,9 @@ const speed = computed(() => {
</script>
<style scoped lang="postcss">
.host-pif-side-panel {
.card {
gap: 1.6rem;
.content {
display: flex;
flex-direction: column;

View File

@@ -2,9 +2,6 @@
<div class="host-pif-table">
<UiTitle>
{{ t('pifs') }}
<template #action>
<slot name="title-actions" />
</template>
</UiTitle>
<div class="container">
<UiQuerySearchBar @search="(value: string) => (searchQuery = value)" />
@@ -25,18 +22,18 @@
</template>
<script lang="ts" setup>
import type { XenApiNetwork, XenApiPif } from '@/libs/xen-api/xen-api.types.ts'
import { useNetworkStore } from '@/stores/xen-api/network.store.ts'
import { usePifStore } from '@/stores/xen-api/pif.store.ts'
import type { XenApiNetwork, XenApiPif } from '@/libs/xen-api/xen-api.types'
import { useNetworkStore } from '@/stores/xen-api/network.store'
import { usePifStore } from '@/stores/xen-api/pif.store'
import VtsRow from '@core/components/table/VtsRow.vue'
import VtsTable from '@core/components/table/VtsTable.vue'
import UiQuerySearchBar from '@core/components/ui/query-search-bar/UiQuerySearchBar.vue'
import UiTitle from '@core/components/ui/title/UiTitle.vue'
import { usePagination } from '@core/composables/pagination.composable.ts'
import { useRouteQuery } from '@core/composables/route-query.composable.ts'
import { useTableState } from '@core/composables/table-state.composable.ts'
import { usePagination } from '@core/composables/pagination.composable'
import { useRouteQuery } from '@core/composables/route-query.composable'
import { useTableState } from '@core/composables/table-state.composable'
import { icon } from '@core/icons'
import { usePifColumns } from '@core/tables/column-sets/pif-columns.ts'
import { usePifColumns } from '@core/tables/column-sets/pif-columns'
import { logicNot } from '@vueuse/math'
import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
@@ -45,10 +42,6 @@ const { pifs } = defineProps<{
pifs: XenApiPif[]
}>()
defineSlots<{
'title-actions'(): any
}>()
const { isReady, hasError } = usePifStore().subscribe()
const { getByOpaqueRef } = useNetworkStore().subscribe()
const { getPifStatus } = usePifStore().subscribe()

View File

@@ -70,7 +70,7 @@ import UiCard from '@core/components/ui/card/UiCard.vue'
import UiLink from '@core/components/ui/link/UiLink.vue'
import UiTagsList from '@core/components/ui/tag/UiTagsList.vue'
import UiTitle from '@core/components/ui/title/UiTitle.vue'
import { vTooltip } from '@core/directives/tooltip.directive.ts'
import { vTooltip } from '@core/directives/tooltip.directive'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'

View File

@@ -1,11 +1,5 @@
<template>
<VtsTreeItem
v-if="host !== undefined"
:expanded="isExpanded"
has-children
class="infra-host-item"
:node-id="`host:${host.uuid}`"
>
<VtsTreeItem v-if="host !== undefined" :expanded="isExpanded" class="infra-host-item" :node-id="`host:${host.uuid}`">
<UiTreeItemLabel
:route="{ name: '/host/[uuid]', params: { uuid: host.uuid } }"
icon="object:host"
@@ -46,11 +40,11 @@
<script lang="ts" setup>
import InfraVmItems from '@/components/infra/InfraVmItems.vue'
import type { XenApiHost } from '@/libs/xen-api/xen-api.types.ts'
import type { XenApiHost } from '@/libs/xen-api/xen-api.types'
import HostTreeActions from '@/modules/host/components/actions/HostTreeActions.vue'
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'
import { useHostStore } from '@/stores/xen-api/host.store'
import { usePoolStore } from '@/stores/xen-api/pool.store'
import { useVmStore } from '@/stores/xen-api/vm.store'
import VtsIcon from '@core/components/icon/VtsIcon.vue'
import MenuList from '@core/components/menu/MenuList.vue'
import VtsTreeItem from '@core/components/tree/VtsTreeItem.vue'
@@ -58,7 +52,7 @@ 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 UiTreeItemLabel from '@core/components/ui/tree-item-label/UiTreeItemLabel.vue'
import { vTooltip } from '@core/directives/tooltip.directive.ts'
import { vTooltip } from '@core/directives/tooltip.directive'
import { useToggle } from '@vueuse/shared'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'

View File

@@ -8,7 +8,7 @@
<script lang="ts" setup>
import InfraHostItem from '@/components/infra/InfraHostItem.vue'
import { useHostStore } from '@/stores/xen-api/host.store.ts'
import { useHostStore } from '@/stores/xen-api/host.store'
import VtsTreeItemError from '@core/components/tree/VtsTreeItemError.vue'
import VtsTreeLoadingItem from '@core/components/tree/VtsTreeLoadingItem.vue'
import { useI18n } from 'vue-i18n'

View File

@@ -1,6 +1,6 @@
<template>
<VtsTreeList class="infra-pool-list">
<VtsTreeItem :expanded="isExpanded" has-children :node-id="`pool:${pool?.uuid}`">
<VtsTreeItem :expanded="isExpanded" :node-id="`pool:${pool?.uuid}`">
<VtsTreeItemError v-if="hasError">
{{ t('error-no-data') }}
</VtsTreeItemError>
@@ -41,7 +41,7 @@
import InfraHostItems from '@/components/infra/InfraHostItems.vue'
import InfraVmItems from '@/components/infra/InfraVmItems.vue'
import PoolTreeActions from '@/modules/pool/components/actions/PoolTreeActions.vue'
import { usePoolStore } from '@/stores/xen-api/pool.store.ts'
import { usePoolStore } from '@/stores/xen-api/pool.store'
import MenuList from '@core/components/menu/MenuList.vue'
import VtsTreeItem from '@core/components/tree/VtsTreeItem.vue'
import VtsTreeItemError from '@core/components/tree/VtsTreeItemError.vue'

View File

@@ -5,51 +5,24 @@
<template #icon>
<VtsObjectIcon size="medium" :state="vmPowerState!" type="vm" />
</template>
<template #addons>
<UiLoader v-if="isChangingState" v-tooltip="{ placement: 'top', content: currentOperation }" />
<MenuList placement="bottom-start">
<template #trigger="{ open }">
<UiButtonIcon
v-tooltip="{
placement: 'top',
content: t('quick-actions'),
}"
icon="action:more-actions"
accent="brand"
size="small"
@click="open($event)"
/>
</template>
<VmTreeActions :vm-opaque-ref />
</MenuList>
</template>
</UiTreeItemLabel>
</VtsTreeItem>
</template>
<script lang="ts" setup>
import type { VM_POWER_STATE } from '@/libs/xen-api/xen-api.enums.ts'
import type { XenApiVm } from '@/libs/xen-api/xen-api.types.ts'
import VmTreeActions from '@/modules/vm/components/actions/VmTreeActions.vue'
import { useVmOperation } from '@/modules/vm/composables/vm-operation.composable.ts'
import { useVmStore } from '@/stores/xen-api/vm.store.ts'
import MenuList from '@core/components/menu/MenuList.vue'
import type { VM_POWER_STATE } from '@/libs/xen-api/xen-api.enums'
import type { XenApiVm } from '@/libs/xen-api/xen-api.types'
import { useVmStore } from '@/stores/xen-api/vm.store'
import VtsObjectIcon from '@core/components/object-icon/VtsObjectIcon.vue'
import VtsTreeItem from '@core/components/tree/VtsTreeItem.vue'
import UiButtonIcon from '@core/components/ui/button-icon/UiButtonIcon.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 { useIntersectionObserver } from '@vueuse/core'
import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
const { vmOpaqueRef } = defineProps<{
vmOpaqueRef: XenApiVm['$ref']
}>()
const { t } = useI18n()
const { getByOpaqueRef } = useVmStore().subscribe()
const vm = computed(() => getByOpaqueRef(vmOpaqueRef))
const rootElement = ref()
@@ -63,6 +36,4 @@ const { stop } = useIntersectionObserver(rootElement, ([entry]) => {
})
const vmPowerState = computed(() => vm.value?.power_state.toLowerCase() as Lowercase<VM_POWER_STATE> | undefined)
const { isChangingState, currentOperation } = useVmOperation(vm)
</script>

View File

@@ -8,8 +8,8 @@
<script lang="ts" setup>
import InfraVmItem from '@/components/infra/InfraVmItem.vue'
import type { XenApiHost } from '@/libs/xen-api/xen-api.types.ts'
import { useVmStore } from '@/stores/xen-api/vm.store.ts'
import type { XenApiHost } from '@/libs/xen-api/xen-api.types'
import { useVmStore } from '@/stores/xen-api/vm.store'
import VtsTreeItemError from '@core/components/tree/VtsTreeItemError.vue'
import VtsTreeLoadingItem from '@core/components/tree/VtsTreeLoadingItem.vue'
import { computed } from 'vue'

View File

@@ -36,8 +36,8 @@
<script lang="ts" setup>
import CollectionFilterRow from '@/components/CollectionFilterRow.vue'
import UiBadge from '@/components/ui/UiBadge.vue'
import { getFilterIcon } from '@/libs/utils.ts'
import type { Filters, NewFilter } from '@/types/filter.ts'
import { getFilterIcon } from '@/libs/utils'
import type { Filters, NewFilter } from '@/types/filter'
import VtsOverlayButton from '@core/components/overlay/VtsOverlayButton.vue'
import VtsOverlayCancelButton from '@core/components/overlay/VtsOverlayCancelButton.vue'
import VtsOverlayConfirmButton from '@core/components/overlay/VtsOverlayConfirmButton.vue'

View File

@@ -15,7 +15,7 @@
</template>
<script lang="ts" setup>
import type { NewSort, Sorts } from '@/types/sort.ts'
import type { NewSort, Sorts } from '@/types/sort'
import VtsOverlayCancelButton from '@core/components/overlay/VtsOverlayCancelButton.vue'
import VtsOverlayConfirmButton from '@core/components/overlay/VtsOverlayConfirmButton.vue'
import VtsSelect from '@core/components/select/VtsSelect.vue'

View File

@@ -24,8 +24,8 @@
</template>
<script lang="ts" setup>
import type { XenApiVm } from '@/libs/xen-api/xen-api.types.ts'
import { useVmStore } from '@/stores/xen-api/vm.store.ts'
import type { XenApiVm } from '@/libs/xen-api/xen-api.types'
import { useVmStore } from '@/stores/xen-api/vm.store'
import VtsOverlayConfirmButton from '@core/components/overlay/VtsOverlayConfirmButton.vue'
import UiModal from '@core/components/ui/modal/UiModal.vue'
import { computed } from 'vue'

View File

@@ -21,7 +21,7 @@
</template>
<script lang="ts" setup>
import { VM_COMPRESSION_TYPE } from '@/libs/xen-api/xen-api.enums.ts'
import { VM_COMPRESSION_TYPE } from '@/libs/xen-api/xen-api.enums'
import VtsInputWrapper from '@core/components/input-wrapper/VtsInputWrapper.vue'
import VtsOverlayCancelButton from '@core/components/overlay/VtsOverlayCancelButton.vue'
import VtsOverlayConfirmButton from '@core/components/overlay/VtsOverlayConfirmButton.vue'

View File

@@ -22,7 +22,7 @@
</template>
<script lang="ts" setup>
import type { XenApiHost } from '@/libs/xen-api/xen-api.types.ts'
import type { XenApiHost } from '@/libs/xen-api/xen-api.types'
import VtsInputWrapper from '@core/components/input-wrapper/VtsInputWrapper.vue'
import VtsOverlayCancelButton from '@core/components/overlay/VtsOverlayCancelButton.vue'
import VtsOverlayConfirmButton from '@core/components/overlay/VtsOverlayConfirmButton.vue'

View File

@@ -21,14 +21,14 @@
</template>
<script setup lang="ts">
import type { XenApiNetwork } from '@/libs/xen-api/xen-api.types.ts'
import type { NetworkInterface, VmState } from '@/types/new-vm.ts'
import type { XenApiNetwork } from '@/libs/xen-api/xen-api.types'
import type { NetworkInterface, VmState } from '@/types/new-vm'
import VtsRow from '@core/components/table/VtsRow.vue'
import VtsTable from '@core/components/table/VtsTable.vue'
import UiButton from '@core/components/ui/button/UiButton.vue'
import UiTableCell from '@core/components/ui/table-cell/UiTableCell.vue'
import { useFormSelect } from '@core/packages/form-select'
import { useNewVmNetworkColumns } from '@core/tables/column-sets/new-vm-network-columns.ts'
import { useNewVmNetworkColumns } from '@core/tables/column-sets/new-vm-network-columns'
import { toRef } from 'vue'
import { useI18n } from 'vue-i18n'

View File

@@ -24,15 +24,15 @@
</template>
<script setup lang="ts">
import type { XenApiSr } from '@/libs/xen-api/xen-api.types.ts'
import type { Vdi, VmState } from '@/types/new-vm.ts'
import type { XenApiSr } from '@/libs/xen-api/xen-api.types'
import type { Vdi, VmState } from '@/types/new-vm'
import VtsRow from '@core/components/table/VtsRow.vue'
import VtsTable from '@core/components/table/VtsTable.vue'
import UiButton from '@core/components/ui/button/UiButton.vue'
import UiTableCell from '@core/components/ui/table-cell/UiTableCell.vue'
import { useFormSelect } from '@core/packages/form-select'
import { useNewVmSrColumns } from '@core/tables/column-sets/new-vm-sr-columns.ts'
import { renderBodyCell } from '@core/tables/helpers/render-body-cell.ts'
import { useNewVmSrColumns } from '@core/tables/column-sets/new-vm-sr-columns'
import { renderBodyCell } from '@core/tables/helpers/render-body-cell'
import { toRef } from 'vue'
import { useI18n } from 'vue-i18n'

View File

@@ -22,14 +22,14 @@
</template>
<script setup lang="ts">
import type { XenApiPif } from '@/libs/xen-api/xen-api.types.ts'
import { useHostMetricsStore } from '@/stores/xen-api/host-metrics.store.ts'
import { useHostStore } from '@/stores/xen-api/host.store.ts'
import { usePifStore } from '@/stores/xen-api/pif.store.ts'
import type { XenApiPif } from '@/libs/xen-api/xen-api.types'
import { useHostMetricsStore } from '@/stores/xen-api/host-metrics.store'
import { useHostStore } from '@/stores/xen-api/host.store'
import { usePifStore } from '@/stores/xen-api/pif.store'
import VtsObjectIcon from '@core/components/object-icon/VtsObjectIcon.vue'
import VtsStatus from '@core/components/status/VtsStatus.vue'
import UiButtonIcon from '@core/components/ui/button-icon/UiButtonIcon.vue'
import { vTooltip } from '@core/directives/tooltip.directive.ts'
import { vTooltip } from '@core/directives/tooltip.directive'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'

View File

@@ -10,7 +10,7 @@
</template>
<script lang="ts" setup>
import { usePoolStore } from '@/stores/xen-api/pool.store.ts'
import { usePoolStore } from '@/stores/xen-api/pool.store'
import UiHeadBar from '@core/components/ui/head-bar/UiHeadBar.vue'
import UiLink from '@core/components/ui/link/UiLink.vue'
import { computed } from 'vue'

View File

@@ -35,23 +35,23 @@
<script lang="ts" setup>
import ObjectLink from '@/components/ObjectLink.vue'
import type { RawObjectType } from '@/libs/xen-api/xen-api.types.ts'
import { rawTypeToType } from '@/libs/xen-api/xen-api.utils.ts'
import { useAlarmStore } from '@/stores/xen-api/alarm.store.ts'
import type { XenApiAlarm } from '@/types/xen-api.ts'
import type { RawObjectType } from '@/libs/xen-api/xen-api.types'
import { rawTypeToType } from '@/libs/xen-api/xen-api.utils'
import { useAlarmStore } from '@/stores/xen-api/alarm.store'
import type { XenApiAlarm } from '@/types/xen-api'
import VtsRow from '@core/components/table/VtsRow.vue'
import VtsTable from '@core/components/table/VtsTable.vue'
import UiButton from '@core/components/ui/button/UiButton.vue'
import UiCard from '@core/components/ui/card/UiCard.vue'
import UiCardTitle from '@core/components/ui/card-title/UiCardTitle.vue'
import UiCounter from '@core/components/ui/counter/UiCounter.vue'
import { useTableState } from '@core/composables/table-state.composable.ts'
import { useTableState } from '@core/composables/table-state.composable'
import { defineColumn, defineColumns } from '@core/packages/table'
import { useDateColumn } from '@core/tables/column-definitions/date-column.ts'
import { usePercentColumn } from '@core/tables/column-definitions/percent-column.ts'
import { useTextColumn } from '@core/tables/column-definitions/text-column.ts'
import { renderBodyCell } from '@core/tables/helpers/render-body-cell.ts'
import { renderHeadCell } from '@core/tables/helpers/render-head-cell.ts'
import { useDateColumn } from '@core/tables/column-definitions/date-column'
import { usePercentColumn } from '@core/tables/column-definitions/percent-column'
import { useTextColumn } from '@core/tables/column-definitions/text-column'
import { renderBodyCell } from '@core/tables/helpers/render-body-cell'
import { renderHeadCell } from '@core/tables/helpers/render-head-cell'
import { logicNot } from '@vueuse/math'
import { h } from 'vue'
import { useI18n } from 'vue-i18n'

View File

@@ -22,10 +22,10 @@
</template>
<script lang="ts" setup>
import { ACTIVE_STATES } from '@/libs/utils.ts'
import { useHostStore } from '@/stores/xen-api/host.store.ts'
import { useVmMetricsStore } from '@/stores/xen-api/vm-metrics.store.ts'
import { useVmStore } from '@/stores/xen-api/vm.store.ts'
import { ACTIVE_STATES } from '@/libs/utils'
import { useHostStore } from '@/stores/xen-api/host.store'
import { useVmMetricsStore } from '@/stores/xen-api/vm-metrics.store'
import { useVmStore } from '@/stores/xen-api/vm.store'
import VtsProgressBar from '@core/components/progress-bar/VtsProgressBar.vue'
import VtsStateHero from '@core/components/state-hero/VtsStateHero.vue'
import UiCard from '@core/components/ui/card/UiCard.vue'

View File

@@ -14,13 +14,13 @@
<script lang="ts" setup>
import HostsCpuUsage from '@/components/pool/dashboard/cpuUsage/HostsCpuUsage.vue'
import VmsCpuUsage from '@/components/pool/dashboard/cpuUsage/VmsCpuUsage.vue'
import type { Stat } from '@/composables/fetch-stats.composable.ts'
import { useHostStore } from '@/stores/xen-api/host.store.ts'
import { useVmStore } from '@/stores/xen-api/vm.store.ts'
import type { Stat } from '@/composables/fetch-stats.composable'
import { useHostStore } from '@/stores/xen-api/host.store'
import { useVmStore } from '@/stores/xen-api/vm.store'
import UiCard from '@core/components/ui/card/UiCard.vue'
import UiCardTitle from '@core/components/ui/card-title/UiCardTitle.vue'
import UiLoader from '@core/components/ui/loader/UiLoader.vue'
import { vTooltip } from '@core/directives/tooltip.directive.ts'
import { vTooltip } from '@core/directives/tooltip.directive'
import type { XapiHostStatsRaw, XapiVmStatsRaw } from '@vates/types/common'
import { computed, type ComputedRef, inject } from 'vue'
import { useI18n } from 'vue-i18n'

View File

@@ -16,8 +16,8 @@
<script lang="ts" setup>
import HostPatchesTable from '@/components/HostPatchesTable.vue'
import { useHostPatches } from '@/composables/host-patches.composable.ts'
import { useHostStore } from '@/stores/xen-api/host.store.ts'
import { useHostPatches } from '@/composables/host-patches.composable'
import { useHostStore } from '@/stores/xen-api/host.store'
import UiCard from '@core/components/ui/card/UiCard.vue'
import UiCardTitle from '@core/components/ui/card-title/UiCardTitle.vue'
import { useI18n } from 'vue-i18n'

View File

@@ -16,11 +16,11 @@
</template>
<script lang="ts" setup>
import { formatSize } from '@/libs/utils.ts'
import { RRD_STEP_FROM_STRING } from '@/libs/xapi-stats.ts'
import { useHostStore } from '@/stores/xen-api/host.store.ts'
import { IK_HOST_LAST_WEEK_STATS } from '@/types/injection-keys.ts'
import type { LinearChartData } from '@core/types/chart.ts'
import { formatSize } from '@/libs/utils'
import { RRD_STEP_FROM_STRING } from '@/libs/xapi-stats'
import { useHostStore } from '@/stores/xen-api/host.store'
import { IK_HOST_LAST_WEEK_STATS } from '@/types/injection-keys'
import type { LinearChartData } from '@core/types/chart'
import VtsStateHero from '@core/components/state-hero/VtsStateHero.vue'
import UiCard from '@core/components/ui/card/UiCard.vue'
import UiCardTitle from '@core/components/ui/card-title/UiCardTitle.vue'

View File

@@ -14,13 +14,13 @@
<script lang="ts" setup>
import HostsRamUsage from '@/components/pool/dashboard/ramUsage/HostsRamUsage.vue'
import VmsRamUsage from '@/components/pool/dashboard/ramUsage/VmsRamUsage.vue'
import type { Stat } from '@/composables/fetch-stats.composable.ts'
import { useHostStore } from '@/stores/xen-api/host.store.ts'
import { useVmStore } from '@/stores/xen-api/vm.store.ts'
import type { Stat } from '@/composables/fetch-stats.composable'
import { useHostStore } from '@/stores/xen-api/host.store'
import { useVmStore } from '@/stores/xen-api/vm.store'
import UiCard from '@core/components/ui/card/UiCard.vue'
import UiCardTitle from '@core/components/ui/card-title/UiCardTitle.vue'
import UiLoader from '@core/components/ui/loader/UiLoader.vue'
import { vTooltip } from '@core/directives/tooltip.directive.ts'
import { vTooltip } from '@core/directives/tooltip.directive'
import type { XapiHostStatsRaw, XapiVmStatsRaw } from '@vates/types/common'
import type { ComputedRef } from 'vue'
import { computed, inject } from 'vue'

View File

@@ -25,8 +25,8 @@
<script lang="ts" setup>
import PoolDashboardStatusItem from '@/components/pool/dashboard/PoolDashboardStatusItem.vue'
import { useHostMetricsStore } from '@/stores/xen-api/host-metrics.store.ts'
import { useVmStore } from '@/stores/xen-api/vm.store.ts'
import { useHostMetricsStore } from '@/stores/xen-api/host-metrics.store'
import { useVmStore } from '@/stores/xen-api/vm.store'
import VtsDivider from '@core/components/divider/VtsDivider.vue'
import VtsStateHero from '@core/components/state-hero/VtsStateHero.vue'
import UiCard from '@core/components/ui/card/UiCard.vue'

View File

@@ -24,7 +24,7 @@
<script lang="ts" setup>
import { N_ITEMS } from '@/pages/pool/[uuid]/dashboard.vue'
import { useSrStore } from '@/stores/xen-api/sr.store.ts'
import { useSrStore } from '@/stores/xen-api/sr.store'
import VtsProgressBarGroup, {
type ProgressBarGroupItem,
} from '@core/components/progress-bar-group/VtsProgressBarGroup.vue'

View File

@@ -16,7 +16,7 @@
<script lang="ts" setup>
import TasksTable from '@/components/tasks/TasksTable.vue'
import { useTaskStore } from '@/stores/xen-api/task.store.ts'
import { useTaskStore } from '@/stores/xen-api/task.store'
import UiCard from '@core/components/ui/card/UiCard.vue'
import UiCardTitle from '@core/components/ui/card-title/UiCardTitle.vue'
import UiCounter from '@core/components/ui/counter/UiCounter.vue'

View File

@@ -12,12 +12,12 @@
</template>
<script lang="ts" setup>
import { useStatStatus } from '@/composables/stat-status.composable.ts'
import { getAvgCpuUsage } from '@/libs/utils.ts'
import { useStatStatus } from '@/composables/stat-status.composable'
import { getAvgCpuUsage } from '@/libs/utils'
import { N_ITEMS } from '@/pages/pool/[uuid]/dashboard.vue'
import { useHostStore } from '@/stores/xen-api/host.store.ts'
import { IK_HOST_STATS } from '@/types/injection-keys.ts'
import type { StatData } from '@/types/stat.ts'
import { useHostStore } from '@/stores/xen-api/host.store'
import { IK_HOST_STATS } from '@/types/injection-keys'
import type { StatData } from '@/types/stat'
import VtsProgressBarGroup from '@core/components/progress-bar-group/VtsProgressBarGroup.vue'
import VtsStateHero from '@core/components/state-hero/VtsStateHero.vue'
import UiCardSubtitle from '@core/components/ui/card-subtitle/UiCardSubtitle.vue'

View File

@@ -18,10 +18,10 @@
</template>
<script lang="ts" setup>
import { RRD_STEP_FROM_STRING } from '@/libs/xapi-stats.ts'
import { useHostStore } from '@/stores/xen-api/host.store.ts'
import { IK_HOST_LAST_WEEK_STATS } from '@/types/injection-keys.ts'
import type { LinearChartData, ValueFormatter } from '@core/types/chart.ts'
import { RRD_STEP_FROM_STRING } from '@/libs/xapi-stats'
import { useHostStore } from '@/stores/xen-api/host.store'
import { IK_HOST_LAST_WEEK_STATS } from '@/types/injection-keys'
import type { LinearChartData, ValueFormatter } from '@core/types/chart'
import VtsStateHero from '@core/components/state-hero/VtsStateHero.vue'
import UiCard from '@core/components/ui/card/UiCard.vue'
import UiCardTitle from '@core/components/ui/card-title/UiCardTitle.vue'

View File

@@ -14,12 +14,12 @@
</template>
<script lang="ts" setup>
import { useStatStatus } from '@/composables/stat-status.composable.ts'
import { getAvgCpuUsage } from '@/libs/utils.ts'
import { useStatStatus } from '@/composables/stat-status.composable'
import { getAvgCpuUsage } from '@/libs/utils'
import { N_ITEMS } from '@/pages/pool/[uuid]/dashboard.vue'
import { useVmStore } from '@/stores/xen-api/vm.store.ts'
import { IK_VM_STATS } from '@/types/injection-keys.ts'
import type { StatData } from '@/types/stat.ts'
import { useVmStore } from '@/stores/xen-api/vm.store'
import { IK_VM_STATS } from '@/types/injection-keys'
import type { StatData } from '@/types/stat'
import VtsProgressBarGroup from '@core/components/progress-bar-group/VtsProgressBarGroup.vue'
import VtsStateHero from '@core/components/state-hero/VtsStateHero.vue'
import UiCardSubtitle from '@core/components/ui/card-subtitle/UiCardSubtitle.vue'

View File

@@ -12,12 +12,12 @@
</template>
<script lang="ts" setup>
import { useStatStatus } from '@/composables/stat-status.composable.ts'
import { parseRamUsage } from '@/libs/utils.ts'
import { useStatStatus } from '@/composables/stat-status.composable'
import { parseRamUsage } from '@/libs/utils'
import { N_ITEMS } from '@/pages/pool/[uuid]/dashboard.vue'
import { useHostStore } from '@/stores/xen-api/host.store.ts'
import { IK_HOST_STATS } from '@/types/injection-keys.ts'
import type { StatData } from '@/types/stat.ts'
import { useHostStore } from '@/stores/xen-api/host.store'
import { IK_HOST_STATS } from '@/types/injection-keys'
import type { StatData } from '@/types/stat'
import VtsProgressBarGroup from '@core/components/progress-bar-group/VtsProgressBarGroup.vue'
import VtsStateHero from '@core/components/state-hero/VtsStateHero.vue'
import UiCardSubtitle from '@core/components/ui/card-subtitle/UiCardSubtitle.vue'

View File

@@ -18,12 +18,12 @@
<script lang="ts" setup>
import SizeStatsSummary from '@/components/ui/SizeStatsSummary.vue'
import { formatSize } from '@/libs/utils.ts'
import { RRD_STEP_FROM_STRING } from '@/libs/xapi-stats.ts'
import { useHostMetricsStore } from '@/stores/xen-api/host-metrics.store.ts'
import { useHostStore } from '@/stores/xen-api/host.store.ts'
import { IK_HOST_LAST_WEEK_STATS } from '@/types/injection-keys.ts'
import type { LinearChartData } from '@core/types/chart.ts'
import { formatSize } from '@/libs/utils'
import { RRD_STEP_FROM_STRING } from '@/libs/xapi-stats'
import { useHostMetricsStore } from '@/stores/xen-api/host-metrics.store'
import { useHostStore } from '@/stores/xen-api/host.store'
import { IK_HOST_LAST_WEEK_STATS } from '@/types/injection-keys'
import type { LinearChartData } from '@core/types/chart'
import VtsStateHero from '@core/components/state-hero/VtsStateHero.vue'
import UiCard from '@core/components/ui/card/UiCard.vue'
import UiCardTitle from '@core/components/ui/card-title/UiCardTitle.vue'

View File

@@ -12,12 +12,12 @@
</template>
<script lang="ts" setup>
import { useStatStatus } from '@/composables/stat-status.composable.ts'
import { parseRamUsage } from '@/libs/utils.ts'
import { useStatStatus } from '@/composables/stat-status.composable'
import { parseRamUsage } from '@/libs/utils'
import { N_ITEMS } from '@/pages/pool/[uuid]/dashboard.vue'
import { useVmStore } from '@/stores/xen-api/vm.store.ts'
import { IK_VM_STATS } from '@/types/injection-keys.ts'
import type { StatData } from '@/types/stat.ts'
import { useVmStore } from '@/stores/xen-api/vm.store'
import { IK_VM_STATS } from '@/types/injection-keys'
import type { StatData } from '@/types/stat'
import VtsProgressBarGroup from '@core/components/progress-bar-group/VtsProgressBarGroup.vue'
import VtsStateHero from '@core/components/state-hero/VtsStateHero.vue'
import UiCardSubtitle from '@core/components/ui/card-subtitle/UiCardSubtitle.vue'

View File

@@ -2,9 +2,6 @@
<div class="pool-host-internal-networks-table">
<UiTitle>
{{ t('host-internal-networks') }}
<template #action>
<slot name="title-actions" />
</template>
</UiTitle>
<div class="container">
<UiQuerySearchBar @search="value => (searchQuery = value)" />
@@ -29,15 +26,15 @@
</template>
<script setup lang="ts">
import type { XenApiNetwork } from '@/libs/xen-api/xen-api.types.ts'
import { useNetworkStore } from '@/stores/xen-api/network.store.ts'
import type { XenApiNetwork } from '@/libs/xen-api/xen-api.types'
import { useNetworkStore } from '@/stores/xen-api/network.store'
import VtsRow from '@core/components/table/VtsRow.vue'
import VtsTable from '@core/components/table/VtsTable.vue'
import UiQuerySearchBar from '@core/components/ui/query-search-bar/UiQuerySearchBar.vue'
import UiTitle from '@core/components/ui/title/UiTitle.vue'
import { usePagination } from '@core/composables/pagination.composable.ts'
import { useRouteQuery } from '@core/composables/route-query.composable.ts'
import { useTableState } from '@core/composables/table-state.composable.ts'
import { usePagination } from '@core/composables/pagination.composable'
import { useRouteQuery } from '@core/composables/route-query.composable'
import { useTableState } from '@core/composables/table-state.composable'
import { useNetworkColumns } from '@core/tables/column-sets/network-columns.ts'
import { logicNot } from '@vueuse/math'
import { computed, ref } from 'vue'
@@ -47,10 +44,6 @@ const { networks } = defineProps<{
networks: XenApiNetwork[]
}>()
defineSlots<{
'title-actions'(): any
}>()
const { isReady, hasError } = useNetworkStore().subscribe()
const { t } = useI18n()

View File

@@ -1,5 +1,5 @@
<template>
<VtsSidePanel :has-selection="!!network" class="pool-network-side-panel" @close="emit('close')">
<VtsSidePanel :has-selection="!!network" @close="emit('close')">
<template v-if="network" #actions>
<MenuList placement="bottom-end">
<template #trigger="{ open, isOpen }">
@@ -18,7 +18,7 @@
</MenuList>
</template>
<template v-if="network" #default>
<UiPanelCard>
<UiCard class="card-container">
<VtsCardObjectTitle :id="network.uuid" :label="network.name_label" />
<div class="content">
<!-- DESCRIPTION -->
@@ -63,8 +63,8 @@
<template #value>{{ networkDefaultLockingMode }}</template>
</VtsCardRowKeyValue>
</div>
</UiPanelCard>
<UiPanelCard v-if="pifsCount && pifsCount > 0">
</UiCard>
<UiCard v-if="pifsCount && pifsCount > 0" class="card-container">
<div class="typo-body-bold">
{{ t('pifs') }}
<UiCounter :value="pifsCount" variant="primary" size="small" accent="neutral" />
@@ -88,7 +88,7 @@
<PifRow v-for="pif in pifs" :key="pif.uuid" :pif />
</tbody>
</table>
</UiPanelCard>
</UiCard>
</template>
</VtsSidePanel>
</template>
@@ -104,8 +104,8 @@ import MenuItem from '@core/components/menu/MenuItem.vue'
import MenuList from '@core/components/menu/MenuList.vue'
import VtsSidePanel from '@core/components/panel/VtsSidePanel.vue'
import UiButtonIcon from '@core/components/ui/button-icon/UiButtonIcon.vue'
import UiCard from '@core/components/ui/card/UiCard.vue'
import UiCounter from '@core/components/ui/counter/UiCounter.vue'
import UiPanelCard from '@core/components/ui/panel-card/UiPanelCard.vue'
import { vTooltip } from '@core/directives/tooltip.directive.ts'
import { useClipboard } from '@vueuse/core'
import { computed } from 'vue'
@@ -147,7 +147,11 @@ const { copy, copied, isSupported: isClipboardSupported } = useClipboard({ sourc
</script>
<style scoped lang="postcss">
.pool-network-side-panel {
.card-container {
display: flex;
flex-direction: column;
gap: 1.6rem;
.content {
display: flex;
flex-direction: column;

View File

@@ -2,15 +2,30 @@
<div class="pool-networks-table">
<UiTitle>
{{ t('networks') }}
<template #action>
<slot name="title-actions" />
</template>
</UiTitle>
<div class="container">
<UiQuerySearchBar @search="value => (searchQuery = value)" />
<div class="table-actions">
<UiQuerySearchBar @search="value => (searchQuery = value)" />
<UiTableActions :title="t('table-actions')">
<UiButton
v-tooltip="copied && t('copied')"
:disabled="!isClipboardSupported"
:left-icon="copied ? 'fa:check-circle' : 'action:copy'"
variant="tertiary"
accent="brand"
size="medium"
@click="selectedNetworkIds.length > 0 && copy()"
>
{{ t('action:copy-info-json') }}
</UiButton>
</UiTableActions>
</div>
<VtsTable :state :pagination-bindings sticky="right">
<thead>
<tr>
<VtsHeaderCell>
<UiCheckbox v-model="areAllNetworksSelected" accent="brand" />
</VtsHeaderCell>
<HeadCells />
</tr>
</thead>
@@ -20,6 +35,9 @@
:key="network.uuid"
:selected="selectedNetworkId === network.uuid"
>
<UiTableCell>
<UiCheckbox v-model="selectedNetworkIds" :value="network.uuid" accent="brand" />
</UiTableCell>
<BodyCells :item="network" />
</VtsRow>
</tbody>
@@ -28,31 +46,36 @@
</div>
</template>
<script lang="ts" setup>
<script setup lang="ts">
import type { XenApiNetwork } from '@/libs/xen-api/xen-api.types.ts'
import { useNetworkStore } from '@/stores/xen-api/network.store.ts'
import { usePifMetricsStore } from '@/stores/xen-api/pif-metrics.store.ts'
import { usePifStore } from '@/stores/xen-api/pif.store.ts'
import VtsHeaderCell from '@core/components/table/cells/VtsHeaderCell.vue'
import VtsRow from '@core/components/table/VtsRow.vue'
import VtsTable from '@core/components/table/VtsTable.vue'
import UiButton from '@core/components/ui/button/UiButton.vue'
import UiCheckbox from '@core/components/ui/checkbox/UiCheckbox.vue'
import UiQuerySearchBar from '@core/components/ui/query-search-bar/UiQuerySearchBar.vue'
import UiTableActions from '@core/components/ui/table-actions/UiTableActions.vue'
import UiTableCell from '@core/components/ui/table-cell/UiTableCell.vue'
import UiTitle from '@core/components/ui/title/UiTitle.vue'
import { usePagination } from '@core/composables/pagination.composable.ts'
import { useRouteQuery } from '@core/composables/route-query.composable.ts'
import useMultiSelect from '@core/composables/table/multi-select.composable.ts'
import { useTableState } from '@core/composables/table-state.composable.ts'
import { vTooltip } from '@core/directives/tooltip.directive.ts'
import { useNetworkColumns } from '@core/tables/column-sets/network-columns.ts'
import { useClipboard } from '@vueuse/core'
import { logicNot } from '@vueuse/math'
import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
const { networks, busy, error } = defineProps<{
const { networks } = defineProps<{
networks: XenApiNetwork[]
busy?: boolean
error?: boolean
}>()
defineSlots<{
'title-actions'(): any
}>()
const { isReady, hasError } = useNetworkStore().subscribe()
const { records: pifs } = usePifStore().subscribe()
const { getPifCarrier } = usePifMetricsStore().subscribe()
@@ -105,9 +128,29 @@ const filteredNetworks = computed(() => {
const { pageRecords: paginatedNetworks, paginationBindings } = usePagination('networks', filteredNetworks)
const { selected: selectedNetworkIds, areAllSelected: areAllNetworksSelected } = useMultiSelect(
computed(() => networks.map(network => network.uuid)),
computed(() => paginatedNetworks.value.map(network => network.uuid))
)
const selectedNetworksAsJson = computed(() => {
const selectedNetworks = networks.filter(network => selectedNetworkIds.value.includes(network.uuid))
return JSON.stringify(selectedNetworks)
})
const {
copy,
copied,
isSupported: isClipboardSupported,
} = useClipboard({
source: selectedNetworksAsJson,
legacy: true,
})
const state = useTableState({
busy: () => busy,
error: () => error,
busy: logicNot(isReady),
error: hasError,
empty: () =>
networks.length === 0
? t('no-network-detected')
@@ -136,7 +179,7 @@ const { HeadCells, BodyCells } = useNetworkColumns({
})
</script>
<style lang="postcss" scoped>
<style scoped lang="postcss">
.pool-networks-table,
.container {
display: flex;

View File

@@ -19,7 +19,7 @@
</template>
<script setup lang="ts">
import type { XenApiPool } from '@/libs/xen-api/xen-api.types.ts'
import type { XenApiPool } from '@/libs/xen-api/xen-api.types'
import VtsTabularKeyValueList from '@core/components/tabular-key-value-list/VtsTabularKeyValueList.vue'
import VtsTabularKeyValueRow from '@core/components/tabular-key-value-row/VtsTabularKeyValueRow.vue'
import VtsTag from '@core/components/tag/VtsTag.vue'

View File

@@ -42,8 +42,8 @@
</template>
<script setup lang="ts">
import type { XenApiPool } from '@/libs/xen-api/xen-api.types.ts'
import { usePoolStore } from '@/stores/xen-api/pool.store.ts'
import type { XenApiPool } from '@/libs/xen-api/xen-api.types'
import { usePoolStore } from '@/stores/xen-api/pool.store'
import VtsStateHero from '@core/components/state-hero/VtsStateHero.vue'
import VtsStatus from '@core/components/status/VtsStatus.vue'
import VtsTabularKeyValueList from '@core/components/tabular-key-value-list/VtsTabularKeyValueList.vue'

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