fix(@xen-orchestra/rest-api): identify errors case for some computed object

This commit is contained in:
mathieuRA
2025-07-18 17:14:43 +02:00
parent e5702e407c
commit ecd18b84cf
5 changed files with 124 additions and 55 deletions

View File

@@ -16,3 +16,5 @@ export interface XoError extends Error {
export type NdjsonStream = Readable
export type SendObjects<T> = string[] | WithHref<T>[] | NdjsonStream
export type PromiseWriteInStreamError = { error: true }

View File

@@ -1,10 +1,13 @@
import { AnyXoVm, XoSr } from '@vates/types'
import { createLogger } from '@xen-orchestra/log'
import { isPromise } from 'node:util/types'
import { MaybePromise } from './helper.type.mjs'
import { MaybePromise, PromiseWriteInStreamError } from './helper.type.mjs'
import { Writable } from 'node:stream'
export const NDJSON_CONTENT_TYPE = 'application/x-ndjson'
const log = createLogger('xo:rest-api:utils-helper')
export const isSrWritable = (sr: XoSr) => isSrWritableOrIso(sr) && sr.content_type !== 'iso'
export const isSrWritableOrIso = (sr: XoSr) => sr.size > 0
export const isReplicaVm = (vm: AnyXoVm) => 'start' in vm.blockedOperations && vm.other['xo:backup:job'] !== undefined
@@ -44,18 +47,42 @@ export const getTopPerProperty = <T,>(
return arr
}
export const promiseWriteInStream = async <T,>({
export async function promiseWriteInStream<T>(args: {
maybePromise: MaybePromise<T>
path: string
stream?: Writable
handleError: true
}): Promise<T | PromiseWriteInStreamError>
export async function promiseWriteInStream<T>(args: {
maybePromise: MaybePromise<T>
path: string
stream?: Writable
handleError?: false
}): Promise<T>
export async function promiseWriteInStream<T>({
maybePromise,
path,
stream,
handleError = false,
}: {
maybePromise: MaybePromise<T>
path: string
stream?: Writable
}): Promise<T> => {
let data: T
handleError?: boolean
}): Promise<T | PromiseWriteInStreamError> {
let data: T | { error: true }
if (isPromise(maybePromise)) {
data = await maybePromise
try {
data = await maybePromise
} catch (err) {
if (!handleError) {
throw err
}
log.error(`promiseWriteInStream for ${path} failed`, err)
data = { error: true }
}
} else {
data = maybePromise
}

View File

@@ -97,9 +97,9 @@ export type XoApp = {
XoBackupRepository['id'],
{
size?: number
used: number
used?: number
available?: number
encryption: {
encryption?: {
algorithm: string
isLegacy: boolean
recommanded: string

View File

@@ -30,7 +30,7 @@ import { HostService } from '../hosts/host.service.mjs'
const log = createLogger('xo:rest-api:xoa-service')
type DashboardAsyncCache = {
backupRepositories: MaybePromise<DashboardBackupRepositoriesSizeInfo>
backupRepositories: MaybePromise<DashboardBackupRepositoriesSizeInfo | undefined>
backups: MaybePromise<DashboardBackupsInfo>
}
@@ -64,16 +64,18 @@ export class XoaService {
async () => {
const xoApp = this.#restApi.xoApp
const s3Brsize: DashboardBackupRepositoriesSizeInfo['s3']['size'] = { backups: 0 }
const otherBrSize: DashboardBackupRepositoriesSizeInfo['other']['size'] = {
available: 0,
backups: 0,
other: 0,
total: 0,
used: 0,
}
let s3Brsize: { size: { backups: number } } | undefined
let otherBrSize:
| {
size: { available?: number; backups: number; other?: number; total?: number; used?: number }
}
| undefined
const backupRepositories = await xoApp.getAllRemotes()
if (backupRepositories.length === 0) {
return undefined
}
const backupRepositoriesInfo = await xoApp.getAllRemotesInfo()
for (const backupRepository of backupRepositories) {
const { type } = parse(backupRepository.url)
@@ -88,21 +90,53 @@ export class XoaService {
const { available, size, used } = backupRepositoryInfo
const isS3 = type === 's3'
const target = isS3 ? s3Brsize : otherBrSize
target.backups += totalBackupSize.onDisk
if (!isS3) {
const _target = target as DashboardBackupRepositoriesSizeInfo['other']['size']
_target.available += available ?? 0
_target.other += used - totalBackupSize.onDisk
_target.total += size ?? 0
_target.used += used
if (isS3) {
if (s3Brsize === undefined) {
s3Brsize = { size: { backups: 0 } }
}
s3Brsize.size.backups += totalBackupSize.onDisk
} else {
if (otherBrSize === undefined) {
otherBrSize = {
size: {
backups: 0,
available: undefined,
other: undefined,
total: undefined,
used: undefined,
},
}
}
if (available === undefined || size === undefined || used === undefined) {
log.info('#getBackupRepositoriesSizeInfo missing info for BR:', backupRepository.id)
}
otherBrSize.size.backups += totalBackupSize.onDisk
if (available !== undefined) {
otherBrSize.size.available = (otherBrSize.size.available ?? 0) + available
}
if (used !== undefined) {
otherBrSize.size.used = (otherBrSize.size.used ?? 0) + used
otherBrSize.size.other = (otherBrSize.size.other ?? 0) + (used - totalBackupSize.onDisk)
}
if (size !== undefined) {
otherBrSize.size.total = (otherBrSize.size.total ?? 0) + size
}
}
}
return { s3: { size: s3Brsize }, other: { size: otherBrSize } }
const result: DashboardBackupRepositoriesSizeInfo = {}
if (s3Brsize !== undefined) {
result.s3 = s3Brsize
}
if (otherBrSize !== undefined) {
result.other = otherBrSize
}
return result
},
this.#dashboardCacheOpts
{ ...this.#dashboardCacheOpts, expiresIn: 1 }
)
if (brResult?.value !== undefined) {
@@ -514,39 +548,41 @@ export class XoaService {
promiseWriteInStream({ maybePromise: this.#getNumberOfPools(), path: 'nPools', stream }),
promiseWriteInStream({ maybePromise: this.#getNumberOfHosts(), path: 'nHosts', stream }),
promiseWriteInStream({ maybePromise: this.#getHostsStatus(), path: 'hostsStatus', stream }),
promiseWriteInStream({ maybePromise: this.#getResourcesOverview(), path: 'resourcesOverview', stream }),
promiseWriteInStream({
maybePromise: this.#getResourcesOverview(),
path: 'resourcesOverview',
stream,
}),
promiseWriteInStream({ maybePromise: this.#getVmsStatus(), path: 'vmsStatus', stream }),
promiseWriteInStream({
maybePromise: this.#getStorageRepositoriesSizeInfo(),
path: 'storageRepositories',
stream,
handleError: true,
}),
promiseWriteInStream({ maybePromise: this.#getPoolsStatus(), path: 'poolsStatus', stream }),
promiseWriteInStream({ maybePromise: this.#getMissingPatchesInfo(), path: 'missingPatches', stream }),
promiseWriteInStream({
maybePromise: this.#getBackupRepositoriesSizeInfo().catch(err => {
log.error('#getBackupRepositoriesSizeInfo failed', err)
// explicitly return undefined because typescript understand it as void instead of undefined
return undefined
}),
maybePromise: this.#getMissingPatchesInfo(),
path: 'missingPatches',
stream,
}),
promiseWriteInStream({
maybePromise: this.#getBackupRepositoriesSizeInfo(),
path: 'backupRepositories',
stream,
handleError: true,
}),
promiseWriteInStream({
maybePromise: this.#getNumberOfEolHosts().catch(err => {
log.error('#getNumberOfEolHosts failed', err)
return undefined
}),
maybePromise: this.#getNumberOfEolHosts(),
path: 'nHostsEol',
stream,
handleError: true,
}),
promiseWriteInStream({
maybePromise: this.#getbackupsInfo().catch(err => {
log.error('#getbackupsInfo failed', err)
return undefined
}),
maybePromise: this.#getbackupsInfo(),
path: 'backups',
stream,
handleError: true,
}),
])

View File

@@ -1,12 +1,13 @@
import { BACKUP_TYPE } from '@vates/types'
import type { PromiseWriteInStreamError } from '../helpers/helper.type.mjs'
export type DashboardBackupRepositoriesSizeInfo = {
s3: {
s3?: {
size: {
backups: number
}
}
other: { size: { available: number; backups: number; other: number; total: number; used: number } }
other?: { size: { available?: number; backups: number; other?: number; total?: number; used?: number } }
}
export type DashboardBackupsInfo = {
@@ -33,7 +34,7 @@ export type DashboardBackupsInfo = {
export type XoaDashboard = {
nPools: number
nHosts: number
nHostsEol?: number
nHostsEol?: number | PromiseWriteInStreamError
hostsStatus: {
running: number
halted: number
@@ -54,17 +55,20 @@ export type XoaDashboard = {
nPoolsWithMissingPatches: number
nHostsFailed: number
}
backupRepositories?: DashboardBackupRepositoriesSizeInfo
storageRepositories: {
size: {
available: number
other: number
replicated: number
total: number
used: number
}
}
backups?: DashboardBackupsInfo
backupRepositories?: DashboardBackupRepositoriesSizeInfo | PromiseWriteInStreamError
storageRepositories:
| {
size: {
available: number
other: number
replicated: number
total: number
used: number
}
}
| PromiseWriteInStreamError
backups?: DashboardBackupsInfo | PromiseWriteInStreamError
resourcesOverview: {
nCpus: number
memorySize: number