Compare commits
3 Commits
vm-backup-
...
linkSuspen
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b3624ae804 | ||
|
|
73c607a0a7 | ||
|
|
dcaa2ed75b |
@@ -6,12 +6,11 @@ import { extractIdsFromSimplePattern } from '../extractIdsFromSimplePattern.mjs'
|
||||
import { Task } from '../Task.mjs'
|
||||
import createStreamThrottle from './_createStreamThrottle.mjs'
|
||||
import { DEFAULT_SETTINGS, Abstract } from './_Abstract.mjs'
|
||||
import { runTask } from './_runTask.mjs'
|
||||
import { getAdaptersByRemote } from './_getAdaptersByRemote.mjs'
|
||||
import { FullRemote } from './_vmRunners/FullRemote.mjs'
|
||||
import { IncrementalRemote } from './_vmRunners/IncrementalRemote.mjs'
|
||||
|
||||
const noop = Function.prototype
|
||||
|
||||
const DEFAULT_REMOTE_VM_SETTINGS = {
|
||||
concurrency: 2,
|
||||
copyRetention: 0,
|
||||
@@ -21,7 +20,6 @@ const DEFAULT_REMOTE_VM_SETTINGS = {
|
||||
healthCheckVmsWithTags: [],
|
||||
maxExportRate: 0,
|
||||
maxMergedDeltasPerRun: Infinity,
|
||||
nRetriesVmBackupFailures: 0,
|
||||
timeout: 0,
|
||||
validateVhdStreams: false,
|
||||
vmTimeout: 0,
|
||||
@@ -43,7 +41,6 @@ export const VmsRemote = class RemoteVmsBackupRunner extends Abstract {
|
||||
const throttleStream = createStreamThrottle(settings.maxExportRate)
|
||||
|
||||
const config = this._config
|
||||
|
||||
await Disposable.use(
|
||||
() => this._getAdapter(job.sourceRemote),
|
||||
() => (settings.healthCheckSr !== undefined ? this._getRecord('SR', settings.healthCheckSr) : undefined),
|
||||
@@ -65,19 +62,8 @@ export const VmsRemote = class RemoteVmsBackupRunner extends Abstract {
|
||||
const allSettings = this._job.settings
|
||||
const baseSettings = this._baseSettings
|
||||
|
||||
const queue = new Set(vmsUuids)
|
||||
const taskByVmId = {}
|
||||
const nTriesByVmId = {}
|
||||
|
||||
const handleVm = vmUuid => {
|
||||
if (nTriesByVmId[vmUuid] === undefined) {
|
||||
nTriesByVmId[vmUuid] = 0
|
||||
}
|
||||
nTriesByVmId[vmUuid]++
|
||||
|
||||
const taskStart = { name: 'backup VM', data: { type: 'VM', id: vmUuid } }
|
||||
const vmSettings = { ...settings, ...allSettings[vmUuid] }
|
||||
const isLastRun = nTriesByVmId[vmUuid] === vmSettings.nRetriesVmBackupFailures + 1
|
||||
|
||||
const opts = {
|
||||
baseSettings,
|
||||
@@ -86,7 +72,7 @@ export const VmsRemote = class RemoteVmsBackupRunner extends Abstract {
|
||||
healthCheckSr,
|
||||
remoteAdapters,
|
||||
schedule,
|
||||
settings: vmSettings,
|
||||
settings: { ...settings, ...allSettings[vmUuid] },
|
||||
sourceRemoteAdapter,
|
||||
throttleStream,
|
||||
vmUuid,
|
||||
@@ -100,39 +86,10 @@ export const VmsRemote = class RemoteVmsBackupRunner extends Abstract {
|
||||
throw new Error(`Job mode ${job.mode} not implemented for mirror backup`)
|
||||
}
|
||||
|
||||
if (taskByVmId[vmUuid] === undefined) {
|
||||
taskByVmId[vmUuid] = new Task(taskStart)
|
||||
}
|
||||
const task = taskByVmId[vmUuid]
|
||||
return task
|
||||
.run(async () => {
|
||||
try {
|
||||
const result = await vmBackup.run()
|
||||
task.success(result)
|
||||
return result
|
||||
} catch (error) {
|
||||
if (isLastRun) {
|
||||
throw error
|
||||
} else {
|
||||
Task.warning(`Retry the VM mirror backup due to an error`, {
|
||||
attempt: nTriesByVmId[vmUuid],
|
||||
error: error.message,
|
||||
})
|
||||
queue.add(vmUuid)
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(noop)
|
||||
return runTask(taskStart, () => vmBackup.run())
|
||||
}
|
||||
const { concurrency } = settings
|
||||
const _handleVm = !concurrency ? handleVm : limitConcurrency(concurrency)(handleVm)
|
||||
|
||||
while (queue.size > 0) {
|
||||
const vmIds = Array.from(queue)
|
||||
queue.clear()
|
||||
|
||||
await asyncMapSettled(vmIds, _handleVm)
|
||||
}
|
||||
await asyncMapSettled(vmsUuids, !concurrency ? handleVm : limitConcurrency(concurrency)(handleVm))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -11,8 +11,6 @@ import { getAdaptersByRemote } from './_getAdaptersByRemote.mjs'
|
||||
import { IncrementalXapi } from './_vmRunners/IncrementalXapi.mjs'
|
||||
import { FullXapi } from './_vmRunners/FullXapi.mjs'
|
||||
|
||||
const noop = Function.prototype
|
||||
|
||||
const DEFAULT_XAPI_VM_SETTINGS = {
|
||||
bypassVdiChainsCheck: false,
|
||||
checkpointSnapshot: false,
|
||||
@@ -26,7 +24,6 @@ const DEFAULT_XAPI_VM_SETTINGS = {
|
||||
healthCheckVmsWithTags: [],
|
||||
maxExportRate: 0,
|
||||
maxMergedDeltasPerRun: Infinity,
|
||||
nRetriesVmBackupFailures: 0,
|
||||
offlineBackup: false,
|
||||
offlineSnapshot: false,
|
||||
snapshotRetention: 0,
|
||||
@@ -56,7 +53,6 @@ export const VmsXapi = class VmsXapiBackupRunner extends Abstract {
|
||||
const throttleStream = createStreamThrottle(settings.maxExportRate)
|
||||
|
||||
const config = this._config
|
||||
|
||||
await Disposable.use(
|
||||
Disposable.all(
|
||||
extractIdsFromSimplePattern(job.srs).map(id =>
|
||||
@@ -93,98 +89,48 @@ export const VmsXapi = class VmsXapiBackupRunner extends Abstract {
|
||||
const allSettings = this._job.settings
|
||||
const baseSettings = this._baseSettings
|
||||
|
||||
const queue = new Set(vmIds)
|
||||
const taskByVmId = {}
|
||||
const nTriesByVmId = {}
|
||||
|
||||
const handleVm = vmUuid => {
|
||||
const getVmTask = () => {
|
||||
if (taskByVmId[vmUuid] === undefined) {
|
||||
taskByVmId[vmUuid] = new Task(taskStart)
|
||||
}
|
||||
return taskByVmId[vmUuid]
|
||||
}
|
||||
const vmBackupFailed = error => {
|
||||
if (isLastRun) {
|
||||
throw error
|
||||
} else {
|
||||
Task.warning(`Retry the VM backup due to an error`, {
|
||||
attempt: nTriesByVmId[vmUuid],
|
||||
error: error.message,
|
||||
})
|
||||
queue.add(vmUuid)
|
||||
}
|
||||
}
|
||||
|
||||
if (nTriesByVmId[vmUuid] === undefined) {
|
||||
nTriesByVmId[vmUuid] = 0
|
||||
}
|
||||
nTriesByVmId[vmUuid]++
|
||||
|
||||
const vmSettings = { ...settings, ...allSettings[vmUuid] }
|
||||
const taskStart = { name: 'backup VM', data: { type: 'VM', id: vmUuid } }
|
||||
const isLastRun = nTriesByVmId[vmUuid] === vmSettings.nRetriesVmBackupFailures + 1
|
||||
|
||||
return this._getRecord('VM', vmUuid).then(
|
||||
disposableVm =>
|
||||
Disposable.use(disposableVm, async vm => {
|
||||
if (taskStart.data.name_label === undefined) {
|
||||
taskStart.data.name_label = vm.name_label
|
||||
}
|
||||
|
||||
const task = getVmTask()
|
||||
return task
|
||||
.run(async () => {
|
||||
const opts = {
|
||||
baseSettings,
|
||||
config,
|
||||
getSnapshotNameLabel,
|
||||
healthCheckSr,
|
||||
job,
|
||||
remoteAdapters,
|
||||
schedule,
|
||||
settings: vmSettings,
|
||||
srs,
|
||||
throttleStream,
|
||||
vm,
|
||||
}
|
||||
|
||||
let vmBackup
|
||||
if (job.mode === 'delta') {
|
||||
vmBackup = new IncrementalXapi(opts)
|
||||
Disposable.use(disposableVm, vm => {
|
||||
taskStart.data.name_label = vm.name_label
|
||||
return runTask(taskStart, () => {
|
||||
const opts = {
|
||||
baseSettings,
|
||||
config,
|
||||
getSnapshotNameLabel,
|
||||
healthCheckSr,
|
||||
job,
|
||||
remoteAdapters,
|
||||
schedule,
|
||||
settings: { ...settings, ...allSettings[vm.uuid] },
|
||||
srs,
|
||||
throttleStream,
|
||||
vm,
|
||||
}
|
||||
let vmBackup
|
||||
if (job.mode === 'delta') {
|
||||
vmBackup = new IncrementalXapi(opts)
|
||||
} else {
|
||||
if (job.mode === 'full') {
|
||||
vmBackup = new FullXapi(opts)
|
||||
} else {
|
||||
if (job.mode === 'full') {
|
||||
vmBackup = new FullXapi(opts)
|
||||
} else {
|
||||
throw new Error(`Job mode ${job.mode} not implemented`)
|
||||
}
|
||||
throw new Error(`Job mode ${job.mode} not implemented`)
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await vmBackup.run()
|
||||
task.success(result)
|
||||
return result
|
||||
} catch (error) {
|
||||
vmBackupFailed(error)
|
||||
}
|
||||
})
|
||||
.catch(noop) // errors are handled by logs
|
||||
}
|
||||
return vmBackup.run()
|
||||
})
|
||||
}),
|
||||
error =>
|
||||
getVmTask().run(() => {
|
||||
vmBackupFailed(error)
|
||||
runTask(taskStart, () => {
|
||||
throw error
|
||||
})
|
||||
)
|
||||
}
|
||||
const { concurrency } = settings
|
||||
const _handleVm = concurrency === 0 ? handleVm : limitConcurrency(concurrency)(handleVm)
|
||||
|
||||
while (queue.size > 0) {
|
||||
const vmIds = Array.from(queue)
|
||||
queue.clear()
|
||||
|
||||
await asyncMapSettled(vmIds, _handleVm)
|
||||
}
|
||||
await asyncMapSettled(vmIds, concurrency === 0 ? handleVm : limitConcurrency(concurrency)(handleVm))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -50,17 +50,7 @@ const RRD_POINTS_PER_STEP: { [key in RRD_STEP]: number } = {
|
||||
// Utils
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
function parseNumber(value: number | string) {
|
||||
// Starting from XAPI 23.31, numbers in the JSON payload are encoded as
|
||||
// strings to support NaN, Infinity and -Infinity
|
||||
if (typeof value === 'string') {
|
||||
const asNumber = +value
|
||||
if (isNaN(asNumber) && value !== 'NaN') {
|
||||
throw new Error('cannot parse number: ' + value)
|
||||
}
|
||||
value = asNumber
|
||||
}
|
||||
|
||||
function convertNanToNull(value: number) {
|
||||
return isNaN(value) ? null : value
|
||||
}
|
||||
|
||||
@@ -69,7 +59,7 @@ function parseNumber(value: number | string) {
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
const computeValues = (dataRow: any, legendIndex: number, transformValue = identity) =>
|
||||
map(dataRow, ({ values }) => transformValue(parseNumber(values[legendIndex])))
|
||||
map(dataRow, ({ values }) => transformValue(convertNanToNull(values[legendIndex])))
|
||||
|
||||
const createGetProperty = (obj: object, property: string, defaultValue: unknown) =>
|
||||
defaults(obj, { [property]: defaultValue })[property] as any
|
||||
@@ -329,14 +319,8 @@ export default class XapiStats {
|
||||
},
|
||||
abortSignal,
|
||||
})
|
||||
const text = await resp.text()
|
||||
try {
|
||||
// starting from XAPI 23.31, the response is valid JSON
|
||||
return JSON.parse(text)
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line import/no-named-as-default-member -- https://github.com/json5/json5/issues/287
|
||||
return JSON5.parse(text)
|
||||
}
|
||||
// eslint-disable-next-line import/no-named-as-default-member -- https://github.com/json5/json5/issues/287
|
||||
return JSON5.parse(await resp.text())
|
||||
}
|
||||
|
||||
// To avoid multiple requests, we keep a cache for the stats and
|
||||
@@ -399,10 +383,7 @@ export default class XapiStats {
|
||||
abortSignal,
|
||||
})
|
||||
|
||||
const actualStep = parseNumber(json.meta.step)
|
||||
if (actualStep !== step) {
|
||||
throw new FaultyGranularity(`Unable to get the true granularity: ${actualStep}`)
|
||||
}
|
||||
const actualStep = json.meta.step as number
|
||||
|
||||
if (json.data.length > 0) {
|
||||
// fetched data is organized from the newest to the oldest
|
||||
@@ -426,15 +407,14 @@ export default class XapiStats {
|
||||
|
||||
let stepStats = xoObjectStats[actualStep]
|
||||
let cacheStepStats = cacheXoObjectStats[actualStep]
|
||||
const endTimestamp = parseNumber(json.meta.end)
|
||||
if (stepStats === undefined || stepStats.endTimestamp !== endTimestamp) {
|
||||
if (stepStats === undefined || stepStats.endTimestamp !== json.meta.end) {
|
||||
stepStats = xoObjectStats[actualStep] = {
|
||||
endTimestamp,
|
||||
endTimestamp: json.meta.end,
|
||||
interval: actualStep,
|
||||
canBeExpired: false,
|
||||
}
|
||||
cacheStepStats = cacheXoObjectStats[actualStep] = {
|
||||
endTimestamp,
|
||||
endTimestamp: json.meta.end,
|
||||
interval: actualStep,
|
||||
canBeExpired: true,
|
||||
}
|
||||
@@ -458,6 +438,10 @@ export default class XapiStats {
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
if (actualStep !== step) {
|
||||
throw new FaultyGranularity(`Unable to get the true granularity: ${actualStep}`)
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.name === 'AbortError') {
|
||||
return
|
||||
|
||||
@@ -9,10 +9,7 @@
|
||||
|
||||
- Disable search engine indexing via a `robots.txt`
|
||||
- [Stats] Support format used by XAPI 23.31
|
||||
- [REST API] Export host [SMT](https://en.wikipedia.org/wiki/Simultaneous_multithreading) status at `/hosts/:id/smt` [Forum#71374](https://xcp-ng.org/forum/post/71374)
|
||||
- [Home & REST API] `$container` field of an halted VM now points to a host if a VDI is on a local storage [Forum#71769](https://xcp-ng.org/forum/post/71769)
|
||||
- [Size Input] Ability to select two new units in the dropdown (`TiB`, `PiB`) (PR [#7382](https://github.com/vatesfr/xen-orchestra/pull/7382))
|
||||
- [Backup] Ability to set a number of retries for VM backup failures [#2139](https://github.com/vatesfr/xen-orchestra/issues/2139) (PR [#7308](https://github.com/vatesfr/xen-orchestra/pull/7308))
|
||||
- [Storage/Disks] Handle link to VM for suspended VDIs (PR [#7391](https://github.com/vatesfr/xen-orchestra/pull/7391))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
@@ -21,7 +18,6 @@
|
||||
- [Settings/XO Config] Sort backups from newest to oldest
|
||||
- [Plugins/audit] Don't log `tag.getAllConfigured` calls
|
||||
- [Remotes] Correctly clear error when the remote is tested with success
|
||||
- [Import/VMWare] Fix importing last snapshot (PR [#7370](https://github.com/vatesfr/xen-orchestra/pull/7370))
|
||||
|
||||
### Packages to release
|
||||
|
||||
|
||||
@@ -27,11 +27,6 @@ const SCHEMA_SETTINGS = {
|
||||
minimum: 1,
|
||||
optional: true,
|
||||
},
|
||||
nRetriesVmBackupFailures: {
|
||||
minimum: 0,
|
||||
optional: true,
|
||||
type: 'number',
|
||||
},
|
||||
preferNbd: {
|
||||
type: 'boolean',
|
||||
optional: true,
|
||||
|
||||
@@ -328,34 +328,6 @@ const TRANSFORMS = {
|
||||
|
||||
const { creation } = xoData.extract(obj) ?? {}
|
||||
|
||||
let $container
|
||||
if (obj.resident_on !== 'OpaqueRef:NULL') {
|
||||
// resident_on is set when the VM is running (or paused or suspended on a host)
|
||||
$container = link(obj, 'resident_on')
|
||||
} else {
|
||||
// if the VM is halted, the $container is the pool
|
||||
$container = link(obj, 'pool')
|
||||
|
||||
// unless one of its VDI is on a non shared SR
|
||||
//
|
||||
// linked objects may not be there when this code run, and it will only be
|
||||
// refreshed when the VM XAPI record change, this value is not guaranteed
|
||||
// to be up-to-date, but it practice it appears to work fine thanks to
|
||||
// `VBDs` and `current_operations` changing when a VDI is
|
||||
// added/removed/migrated
|
||||
for (const vbd of obj.$VBDs) {
|
||||
const sr = vbd?.$VDI?.$SR
|
||||
if (sr !== undefined && !sr.shared) {
|
||||
const pbd = sr.$PBDs[0]
|
||||
const hostId = pbd && link(pbd, 'host')
|
||||
if (hostId !== undefined) {
|
||||
$container = hostId
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const vm = {
|
||||
// type is redefined after for controllers/, templates &
|
||||
// snapshots.
|
||||
@@ -441,6 +413,7 @@ const TRANSFORMS = {
|
||||
startTime: metrics && toTimestamp(metrics.start_time),
|
||||
secureBoot: obj.platform.secureboot === 'true',
|
||||
suspendSr: link(obj, 'suspend_SR'),
|
||||
suspendVdi: link(obj, 'suspend_VDI'),
|
||||
tags: obj.tags,
|
||||
VIFs: link(obj, 'VIFs'),
|
||||
VTPMs: link(obj, 'VTPMs'),
|
||||
@@ -450,7 +423,8 @@ const TRANSFORMS = {
|
||||
xenTools,
|
||||
...getVmGuestToolsProps(obj),
|
||||
|
||||
$container,
|
||||
// TODO: handle local VMs (`VM.get_possible_hosts()`).
|
||||
$container: isRunning ? link(obj, 'resident_on') : link(obj, 'pool'),
|
||||
$VBDs: link(obj, 'VBDs'),
|
||||
|
||||
// TODO: dedupe
|
||||
@@ -476,7 +450,6 @@ const TRANSFORMS = {
|
||||
|
||||
vm.snapshot_time = toTimestamp(obj.snapshot_time)
|
||||
vm.$snapshot_of = link(obj, 'snapshot_of')
|
||||
vm.suspendVdi = link(obj, 'suspend_VDI')
|
||||
} else if (obj.is_a_template) {
|
||||
const defaultTemplate = isDefaultTemplate(obj)
|
||||
vm.type += '-template'
|
||||
|
||||
@@ -280,7 +280,7 @@ export default class MigrateVm {
|
||||
const stream = vhd.stream()
|
||||
await vdi.$importContent(stream, { format: VDI_FORMAT_VHD })
|
||||
}
|
||||
return { vdi, vhd }
|
||||
return vhd
|
||||
})
|
||||
)
|
||||
)
|
||||
|
||||
@@ -253,10 +253,6 @@ export default class RestApi {
|
||||
const host = req.xapiObject
|
||||
res.json(await host.$xapi.listMissingPatches(host))
|
||||
},
|
||||
|
||||
async smt({ xapiObject }, res) {
|
||||
res.json({ enabled: await xapiObject.$xapi.isHyperThreadingEnabled(xapiObject.$id) })
|
||||
},
|
||||
}
|
||||
|
||||
collections.pools.routes = {
|
||||
|
||||
@@ -138,7 +138,7 @@ export class Range extends Component {
|
||||
|
||||
export Toggle from './toggle'
|
||||
|
||||
const UNITS = ['kiB', 'MiB', 'GiB', 'TiB', 'PiB']
|
||||
const UNITS = ['kiB', 'MiB', 'GiB']
|
||||
const DEFAULT_UNIT = 'GiB'
|
||||
|
||||
export class SizeInput extends BaseComponent {
|
||||
|
||||
@@ -141,7 +141,6 @@ const messages = {
|
||||
removeColor: 'Remove color',
|
||||
xcpNg: 'XCP-ng',
|
||||
noFileSelected: 'No file selected',
|
||||
nRetriesVmBackupFailures: 'Number of retries if VM backup fails',
|
||||
|
||||
// ----- Modals -----
|
||||
alertOk: 'OK',
|
||||
|
||||
@@ -189,7 +189,6 @@ const getInitialState = ({ preSelectedVmIds, setHomeVmIdsSelection, suggestedExc
|
||||
drMode: false,
|
||||
name: '',
|
||||
nbdConcurrency: 1,
|
||||
nRetriesVmBackupFailures: 0,
|
||||
preferNbd: false,
|
||||
remotes: [],
|
||||
schedules: {},
|
||||
@@ -636,11 +635,6 @@ const New = decorate([
|
||||
nbdConcurrency,
|
||||
})
|
||||
},
|
||||
setNRetriesVmBackupFailures({ setGlobalSettings }, nRetries) {
|
||||
setGlobalSettings({
|
||||
nRetriesVmBackupFailures: nRetries,
|
||||
})
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
compressionId: generateId,
|
||||
@@ -650,7 +644,6 @@ const New = decorate([
|
||||
inputMaxExportRate: generateId,
|
||||
inputPreferNbd: generateId,
|
||||
inputNbdConcurrency: generateId,
|
||||
inputNRetriesVmBackupFailures: generateId,
|
||||
inputTimeoutId: generateId,
|
||||
|
||||
// In order to keep the user preference, the offline backup is kept in the DB
|
||||
@@ -763,7 +756,6 @@ const New = decorate([
|
||||
fullInterval,
|
||||
maxExportRate,
|
||||
nbdConcurrency = 1,
|
||||
nRetriesVmBackupFailures = 0,
|
||||
offlineBackup,
|
||||
offlineSnapshot,
|
||||
preferNbd,
|
||||
@@ -998,17 +990,6 @@ const New = decorate([
|
||||
value={concurrency}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<label htmlFor={state.inputNRetriesVmBackupFailures}>
|
||||
<strong>{_('nRetriesVmBackupFailures')}</strong>
|
||||
</label>
|
||||
<Number
|
||||
id={state.inputNRetriesVmBackupFailures}
|
||||
min={0}
|
||||
onChange={effects.setNRetriesVmBackupFailures}
|
||||
value={nRetriesVmBackupFailures}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<label htmlFor={state.inputTimeoutId}>
|
||||
<strong>{_('timeout')}</strong>
|
||||
|
||||
@@ -124,8 +124,6 @@ const NewMirrorBackup = decorate([
|
||||
setAdvancedSettings({ timeout: timeout !== undefined ? timeout * 3600e3 : undefined }),
|
||||
setMaxExportRate: ({ setAdvancedSettings }, rate) =>
|
||||
setAdvancedSettings({ maxExportRate: rate !== undefined ? rate * (1024 * 1024) : undefined }),
|
||||
setNRetriesVmBackupFailures: ({ setAdvancedSettings }, nRetriesVmBackupFailures) =>
|
||||
setAdvancedSettings({ nRetriesVmBackupFailures }),
|
||||
setSourceRemote: (_, obj) => () => ({
|
||||
sourceRemote: obj === null ? {} : obj.value,
|
||||
}),
|
||||
@@ -206,7 +204,6 @@ const NewMirrorBackup = decorate([
|
||||
inputConcurrencyId: generateId,
|
||||
inputTimeoutId: generateId,
|
||||
inputMaxExportRateId: generateId,
|
||||
inputNRetriesVmBackupFailures: generateId,
|
||||
isBackupInvalid: state =>
|
||||
state.isMissingName || state.isMissingBackupMode || state.isMissingSchedules || state.isMissingRetention,
|
||||
isFull: state => state.mode === 'full',
|
||||
@@ -234,7 +231,7 @@ const NewMirrorBackup = decorate([
|
||||
}),
|
||||
injectState,
|
||||
({ state, effects, intl: { formatMessage } }) => {
|
||||
const { concurrency, timeout, maxExportRate, nRetriesVmBackupFailures = 0 } = state.advancedSettings
|
||||
const { concurrency, timeout, maxExportRate } = state.advancedSettings
|
||||
return (
|
||||
<form id={state.formId}>
|
||||
<Container>
|
||||
@@ -317,17 +314,6 @@ const NewMirrorBackup = decorate([
|
||||
value={concurrency}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<label htmlFor={state.inputNRetriesVmBackupFailures}>
|
||||
<strong>{_('nRetriesVmBackupFailures')}</strong>
|
||||
</label>
|
||||
<Number
|
||||
id={state.inputNRetriesVmBackupFailures}
|
||||
min={0}
|
||||
onChange={effects.setNRetriesVmBackupFailures}
|
||||
value={nRetriesVmBackupFailures}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<label htmlFor={state.inputTimeoutId}>
|
||||
<strong>{_('timeout')}</strong>
|
||||
|
||||
@@ -319,7 +319,6 @@ class JobsTable extends React.Component {
|
||||
compression,
|
||||
concurrency,
|
||||
fullInterval,
|
||||
nRetriesVmBackupFailures,
|
||||
offlineBackup,
|
||||
offlineSnapshot,
|
||||
proxyId,
|
||||
@@ -350,9 +349,6 @@ class JobsTable extends React.Component {
|
||||
{compression !== undefined && (
|
||||
<Li>{_.keyValue(_('compression'), compression === 'native' ? 'GZIP' : compression)}</Li>
|
||||
)}
|
||||
{nRetriesVmBackupFailures > 0 && (
|
||||
<Li>{_.keyValue(_('nRetriesVmBackupFailures'), nRetriesVmBackupFailures)}</Li>
|
||||
)}
|
||||
</Ul>
|
||||
)
|
||||
},
|
||||
|
||||
@@ -12,7 +12,7 @@ import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import SortedTable from 'sorted-table'
|
||||
import TabButton from 'tab-button'
|
||||
import renderXoItem, { Vdi } from 'render-xo-item'
|
||||
import renderXoItem, { Vdi, Vm } from 'render-xo-item'
|
||||
import { confirm } from 'modal'
|
||||
import { injectIntl } from 'react-intl'
|
||||
import { Text } from 'editable'
|
||||
@@ -123,67 +123,76 @@ const COLUMNS = [
|
||||
vms: getAllVms(state, props),
|
||||
vbds: getVbds(state, props),
|
||||
})
|
||||
})(({ vbds, vms }) => {
|
||||
})(({ item: vdi, vbds, vms, userData: { vmsSnapshotsBySuspendVdi } }) => {
|
||||
const vmSnapshot = vmsSnapshotsBySuspendVdi[vdi.uuid]?.[0]
|
||||
if (isEmpty(vms)) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Container>
|
||||
{map(vbds, (vbd, index) => {
|
||||
const vm = vms[vbd.VM]
|
||||
{vbds.length > 0 ? (
|
||||
map(vbds, (vbd, index) => {
|
||||
const vm = vms[vbd.VM]
|
||||
|
||||
if (vm === undefined) {
|
||||
return null
|
||||
}
|
||||
if (vm === undefined) {
|
||||
return null
|
||||
}
|
||||
|
||||
const type = vm.type
|
||||
let link
|
||||
if (type === 'VM') {
|
||||
link = `/vms/${vm.id}`
|
||||
} else if (type === 'VM-template') {
|
||||
link = `/home?s=${vm.id}&t=VM-template`
|
||||
} else {
|
||||
link = vm.$snapshot_of === undefined ? '/dashboard/health' : `/vms/${vm.$snapshot_of}/snapshots`
|
||||
}
|
||||
const type = vm.type
|
||||
let link
|
||||
if (type === 'VM') {
|
||||
link = `/vms/${vm.id}`
|
||||
} else if (type === 'VM-template') {
|
||||
link = `/home?s=${vm.id}&t=VM-template`
|
||||
} else {
|
||||
link = vm.$snapshot_of === undefined ? '/dashboard/health' : `/vms/${vm.$snapshot_of}/snapshots`
|
||||
}
|
||||
|
||||
return (
|
||||
<Row className={index > 0 && 'mt-1'}>
|
||||
<Col mediumSize={8}>
|
||||
<Link to={link}>{renderXoItem(vm)}</Link>
|
||||
</Col>
|
||||
<Col mediumSize={4}>
|
||||
<ButtonGroup>
|
||||
{vbd.attached ? (
|
||||
return (
|
||||
<Row className={index > 0 && 'mt-1'}>
|
||||
<Col mediumSize={8}>
|
||||
<Link to={link}>{renderXoItem(vm)}</Link>
|
||||
</Col>
|
||||
<Col mediumSize={4}>
|
||||
<ButtonGroup>
|
||||
{vbd.attached ? (
|
||||
<ActionRowButton
|
||||
btnStyle='danger'
|
||||
handler={disconnectVbd}
|
||||
handlerParam={vbd}
|
||||
icon='disconnect'
|
||||
tooltip={_('vbdDisconnect')}
|
||||
/>
|
||||
) : (
|
||||
<ActionRowButton
|
||||
btnStyle='primary'
|
||||
disabled={some(vbds, 'attached') || !isVmRunning(vm)}
|
||||
handler={connectVbd}
|
||||
handlerParam={vbd}
|
||||
icon='connect'
|
||||
tooltip={_('vbdConnect')}
|
||||
/>
|
||||
)}
|
||||
<ActionRowButton
|
||||
btnStyle='danger'
|
||||
handler={disconnectVbd}
|
||||
handler={deleteVbd}
|
||||
handlerParam={vbd}
|
||||
icon='disconnect'
|
||||
tooltip={_('vbdDisconnect')}
|
||||
icon='vdi-forget'
|
||||
tooltip={_('vdiForget')}
|
||||
/>
|
||||
) : (
|
||||
<ActionRowButton
|
||||
btnStyle='primary'
|
||||
disabled={some(vbds, 'attached') || !isVmRunning(vm)}
|
||||
handler={connectVbd}
|
||||
handlerParam={vbd}
|
||||
icon='connect'
|
||||
tooltip={_('vbdConnect')}
|
||||
/>
|
||||
)}
|
||||
<ActionRowButton
|
||||
btnStyle='danger'
|
||||
handler={deleteVbd}
|
||||
handlerParam={vbd}
|
||||
icon='vdi-forget'
|
||||
tooltip={_('vdiForget')}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
</Col>
|
||||
</Row>
|
||||
)
|
||||
})}
|
||||
</ButtonGroup>
|
||||
</Col>
|
||||
</Row>
|
||||
)
|
||||
})
|
||||
) : (
|
||||
<Col mediumSize={8}>
|
||||
<Link to={`/vms/${vmSnapshot.$snapshot_of}/snapshots`}>
|
||||
<Vm id={vmSnapshot.$snapshot_of} />
|
||||
</Link>
|
||||
</Col>
|
||||
)}
|
||||
</Container>
|
||||
)
|
||||
}),
|
||||
@@ -304,6 +313,7 @@ class NewDisk extends Component {
|
||||
@connectStore(() => ({
|
||||
checkPermissions: getCheckPermissions,
|
||||
vbds: createGetObjectsOfType('VBD'),
|
||||
vmsSnapshotsBySuspendVdi: createGetObjectsOfType('VM-snapshot').groupBy('suspendVdi'),
|
||||
}))
|
||||
export default class SrDisks extends Component {
|
||||
_closeNewDiskForm = () => this.setState({ newDisk: false })
|
||||
@@ -434,6 +444,7 @@ export default class SrDisks extends Component {
|
||||
columns={COLUMNS}
|
||||
data-isVdiAttached={this._getIsVdiAttached()}
|
||||
data-vdisByBaseCopy={this._getVdisByBaseCopy()}
|
||||
data-vmsSnapshotsBySuspendVdi={this.props.vmsSnapshotsBySuspendVdi}
|
||||
defaultFilter='filterOnlyManaged'
|
||||
filters={FILTERS}
|
||||
groupedActions={GROUPED_ACTIONS}
|
||||
|
||||
Reference in New Issue
Block a user