parent
fe722c8b31
commit
6d1048e5c5
@ -17,6 +17,7 @@
|
||||
- [SR/Disks] Add tooltip for disabled migration (PR [#4884](https://github.com/vatesfr/xen-orchestra/pull/4884))
|
||||
- [SR/Advanced, SR selector] Show thin/thick provisioning [#2208](https://github.com/vatesfr/xen-orchestra/issues/2208) (PR [#5081](https://github.com/vatesfr/xen-orchestra/pull/5081))
|
||||
- [Licenses] Ability to move a license from another XOA to the current XOA (PR [#5110](https://github.com/vatesfr/xen-orchestra/pull/5110))
|
||||
- [Backup/health] Show VM backups with missing jobs, schedules and VMs [#4716](https://github.com/vatesfr/xen-orchestra/issues/4716) (PR [#5062](https://github.com/vatesfr/xen-orchestra/pull/5062))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
|
@ -4030,7 +4030,7 @@ export default {
|
||||
vmsToBackup: 'VMs per il backup',
|
||||
|
||||
// Original text: 'Refresh backup list'
|
||||
restoreResfreshList: "Aggiorna l'elenco di backup",
|
||||
refreshBackupList: "Aggiorna l'elenco di backup",
|
||||
|
||||
// Original text: 'Legacy restore'
|
||||
restoreLegacy: 'Ripristino legacy',
|
||||
|
@ -3430,7 +3430,7 @@ export default {
|
||||
vmsToBackup: "Yedeklenecek VM'ler",
|
||||
|
||||
// Original text: "Refresh backup list"
|
||||
restoreResfreshList: 'Yedek listesini yenile',
|
||||
refreshBackupList: 'Yedek listesini yenile',
|
||||
|
||||
// Original text: "Restore"
|
||||
restoreVmBackups: 'Geri yükle',
|
||||
|
@ -1324,6 +1324,16 @@ const messages = {
|
||||
metricsLoading: 'Loading…',
|
||||
|
||||
// ----- Health -----
|
||||
deleteBackups: 'Delete backup{nBackups, plural, one {} other {s}}',
|
||||
deleteBackupsMessage:
|
||||
'Are you sure you want to delete {nBackups, number} backup{nBackups, plural, one {} other {s}}?',
|
||||
detachedBackups: 'Detached backups',
|
||||
missingJob: 'Missing job',
|
||||
missingVm: 'Missing VM',
|
||||
missingVmInJob: 'This VM does not belong to this job',
|
||||
missingSchedule: 'Missing schedule',
|
||||
noDetachedBackups: 'No backups',
|
||||
reason: 'Reason',
|
||||
orphanedVdis: 'Orphaned snapshot VDIs',
|
||||
orphanedVms: 'Orphaned VMs snapshot',
|
||||
noOrphanedObject: 'No orphans',
|
||||
@ -1542,7 +1552,7 @@ const messages = {
|
||||
importBackupTitle: 'Import VM',
|
||||
importBackupMessage: 'Starting your backup import',
|
||||
vmsToBackup: 'VMs to backup',
|
||||
restoreResfreshList: 'Refresh backup list',
|
||||
refreshBackupList: 'Refresh backup list',
|
||||
restoreLegacy: 'Legacy restore',
|
||||
restoreFileLegacy: 'Legacy file restore',
|
||||
restoreVmBackups: 'Restore',
|
||||
|
@ -14,6 +14,7 @@ import { createGetObject, createSelector } from './selectors'
|
||||
import { FormattedDate } from 'react-intl'
|
||||
import {
|
||||
isSrWritable,
|
||||
subscribeBackupNgJobs,
|
||||
subscribeProxies,
|
||||
subscribeRemotes,
|
||||
subscribeUsers,
|
||||
@ -21,11 +22,13 @@ import {
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const unknowItem = (uuid, type) => (
|
||||
const unknowItem = (uuid, type, placeholder) => (
|
||||
<Tooltip content={_('copyUuid', { uuid })}>
|
||||
<CopyToClipboard text={uuid}>
|
||||
<span className='text-muted' style={{ cursor: 'pointer' }}>
|
||||
{_('errorUnknownItem', { type })}
|
||||
{placeholder === undefined
|
||||
? _('errorUnknownItem', { type })
|
||||
: placeholder}
|
||||
</span>
|
||||
</CopyToClipboard>
|
||||
</Tooltip>
|
||||
@ -142,9 +145,9 @@ export const Vm = decorate([
|
||||
),
|
||||
}
|
||||
}),
|
||||
({ id, vm, container, link, newTab }) => {
|
||||
({ id, vm, container, link, newTab, name }) => {
|
||||
if (vm === undefined) {
|
||||
return unknowItem(id, 'VM')
|
||||
return unknowItem(id, 'VM', name)
|
||||
}
|
||||
|
||||
return (
|
||||
@ -159,6 +162,7 @@ export const Vm = decorate([
|
||||
Vm.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
link: PropTypes.bool,
|
||||
name: PropTypes.string,
|
||||
newTab: PropTypes.bool,
|
||||
}
|
||||
|
||||
@ -425,6 +429,41 @@ Proxy.defaultProps = {
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export const BackupJob = decorate([
|
||||
addSubscriptions(({ id }) => ({
|
||||
job: cb =>
|
||||
subscribeBackupNgJobs(jobs => cb(jobs.find(job => job.id === id))),
|
||||
})),
|
||||
({ id, job, link, newTab }) => {
|
||||
if (job === undefined) {
|
||||
return unknowItem(id, 'job')
|
||||
}
|
||||
|
||||
return (
|
||||
<LinkWrapper
|
||||
link={link}
|
||||
newTab={newTab}
|
||||
to={`/backup/overview?s=id:${id}`}
|
||||
>
|
||||
{job.name}
|
||||
</LinkWrapper>
|
||||
)
|
||||
},
|
||||
])
|
||||
|
||||
BackupJob.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
link: PropTypes.bool,
|
||||
newTab: PropTypes.bool,
|
||||
}
|
||||
|
||||
BackupJob.defaultProps = {
|
||||
link: false,
|
||||
newTab: false,
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export const Vgpu = connectStore(() => ({
|
||||
vgpuType: createGetObject((_, props) => props.vgpu.vgpuType),
|
||||
}))(({ vgpu, vgpuType }) => (
|
||||
|
@ -2307,6 +2307,8 @@ export const getResourceSet = id =>
|
||||
|
||||
// Remote ------------------------------------------------------------
|
||||
|
||||
export const getRemotes = () => _call('remote.getAll')
|
||||
|
||||
export const getRemote = remote =>
|
||||
_call('remote.get', resolveIds({ id: remote }))::tap(null, err =>
|
||||
error(_('getRemote'), err.message || String(err))
|
||||
|
@ -230,7 +230,7 @@ export default class Restore extends Component {
|
||||
handler={this._refreshBackupList}
|
||||
icon='refresh'
|
||||
>
|
||||
{_('restoreResfreshList')}
|
||||
{_('refreshBackupList')}
|
||||
</ActionButton>
|
||||
</div>
|
||||
<em>
|
||||
|
@ -1,17 +1,87 @@
|
||||
import _ from 'intl'
|
||||
import Component from 'base-component'
|
||||
import ActionButton from 'action-button'
|
||||
import decorate from 'apply-decorators'
|
||||
import defined, { get } from '@xen-orchestra/defined'
|
||||
import Icon from 'icon'
|
||||
import Link from 'link'
|
||||
import NoObjects from 'no-objects'
|
||||
import React from 'react'
|
||||
import renderXoItem from 'render-xo-item'
|
||||
import renderXoItem, { BackupJob, Vm } from 'render-xo-item'
|
||||
import SortedTable from 'sorted-table'
|
||||
import { addSubscriptions, connectStore } from 'utils'
|
||||
import { Card, CardHeader, CardBlock } from 'card'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import { confirm } from 'modal'
|
||||
import { createPredicate } from 'value-matcher'
|
||||
import { createGetLoneSnapshots, createGetObjectsOfType } from 'selectors'
|
||||
import { deleteSnapshot, deleteSnapshots, subscribeSchedules } from 'xo'
|
||||
import { FormattedRelative, FormattedTime } from 'react-intl'
|
||||
import { forEach, keyBy, omit, toArray } from 'lodash'
|
||||
import { FormattedDate, FormattedRelative, FormattedTime } from 'react-intl'
|
||||
import { injectState, provideState } from 'reaclette'
|
||||
import {
|
||||
deleteBackups,
|
||||
deleteSnapshot,
|
||||
deleteSnapshots,
|
||||
getRemotes,
|
||||
listVmBackups,
|
||||
subscribeBackupNgJobs,
|
||||
subscribeSchedules,
|
||||
} from 'xo'
|
||||
|
||||
const DETACHED_BACKUP_COLUMNS = [
|
||||
{
|
||||
name: _('date'),
|
||||
itemRenderer: backup => (
|
||||
<FormattedDate
|
||||
value={new Date(backup.timestamp)}
|
||||
month='long'
|
||||
day='numeric'
|
||||
year='numeric'
|
||||
hour='2-digit'
|
||||
minute='2-digit'
|
||||
second='2-digit'
|
||||
/>
|
||||
),
|
||||
sortCriteria: 'timestamp',
|
||||
sortOrder: 'desc',
|
||||
},
|
||||
{
|
||||
name: _('vm'),
|
||||
itemRenderer: ({ vm, vmId }) => <Vm id={vmId} link name={vm.name_label} />,
|
||||
sortCriteria: ({ vm, vmId }, { vms }) => defined(vms[vmId], vm).name_label,
|
||||
},
|
||||
{
|
||||
name: _('job'),
|
||||
itemRenderer: ({ jobId }) => <BackupJob id={jobId} link />,
|
||||
sortCriteria: ({ jobId }, { jobs }) => get(() => jobs[jobId].name),
|
||||
},
|
||||
{
|
||||
name: _('jobModes'),
|
||||
valuePath: 'mode',
|
||||
},
|
||||
{
|
||||
name: _('reason'),
|
||||
itemRenderer: ({ reason }) => _(reason),
|
||||
sortCriteria: 'reason',
|
||||
},
|
||||
]
|
||||
|
||||
const DETACHED_BACKUP_ACTIONS = [
|
||||
{
|
||||
handler: (backups, { fetchBackupList }) => {
|
||||
const nBackups = backups.length
|
||||
return confirm({
|
||||
title: _('deleteBackups', { nBackups }),
|
||||
body: _('deleteBackupsMessage', { nBackups }),
|
||||
icon: 'delete',
|
||||
})
|
||||
.then(() => deleteBackups(backups), noop)
|
||||
.then(fetchBackupList)
|
||||
},
|
||||
icon: 'delete',
|
||||
label: backups => _('deleteBackups', { nBackups: backups.length }),
|
||||
level: 'danger',
|
||||
},
|
||||
]
|
||||
|
||||
const SNAPSHOT_COLUMNS = [
|
||||
{
|
||||
@ -66,69 +136,167 @@ const ACTIONS = [
|
||||
},
|
||||
]
|
||||
|
||||
@addSubscriptions({
|
||||
// used by createGetLoneSnapshots
|
||||
schedules: subscribeSchedules,
|
||||
})
|
||||
@connectStore({
|
||||
loneSnapshots: createGetLoneSnapshots,
|
||||
legacySnapshots: createGetObjectsOfType('VM-snapshot').filter([
|
||||
(() => {
|
||||
const RE = /^(?:XO_DELTA_EXPORT:|XO_DELTA_BASE_VM_SNAPSHOT_|rollingSnapshot_)/
|
||||
return (
|
||||
{ name_label } // eslint-disable-line camelcase
|
||||
) => RE.test(name_label)
|
||||
})(),
|
||||
]),
|
||||
vms: createGetObjectsOfType('VM'),
|
||||
})
|
||||
export default class Health extends Component {
|
||||
render() {
|
||||
return (
|
||||
<Container>
|
||||
<Row className='lone-snapshots'>
|
||||
<Col>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Icon icon='vm' /> {_('vmSnapshotsRelatedToNonExistentBackups')}
|
||||
</CardHeader>
|
||||
<CardBlock>
|
||||
<NoObjects
|
||||
actions={ACTIONS}
|
||||
collection={this.props.loneSnapshots}
|
||||
columns={SNAPSHOT_COLUMNS}
|
||||
component={SortedTable}
|
||||
data-vms={this.props.vms}
|
||||
emptyMessage={_('noSnapshots')}
|
||||
shortcutsTarget='.lone-snapshots'
|
||||
stateUrlParam='s_vm_snapshots'
|
||||
/>
|
||||
</CardBlock>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row className='legacy-snapshots'>
|
||||
<Col>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Icon icon='vm' /> {_('legacySnapshots')}
|
||||
</CardHeader>
|
||||
<CardBlock>
|
||||
<NoObjects
|
||||
actions={ACTIONS}
|
||||
collection={this.props.legacySnapshots}
|
||||
columns={SNAPSHOT_COLUMNS}
|
||||
component={SortedTable}
|
||||
data-vms={this.props.vms}
|
||||
emptyMessage={_('noSnapshots')}
|
||||
shortcutsTarget='.legacy-snapshots'
|
||||
stateUrlParam='s_legacy_vm_snapshots'
|
||||
/>
|
||||
</CardBlock>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
}
|
||||
const Health = decorate([
|
||||
addSubscriptions({
|
||||
// used by createGetLoneSnapshots
|
||||
schedules: cb =>
|
||||
subscribeSchedules(schedules => {
|
||||
cb(keyBy(schedules, 'id'))
|
||||
}),
|
||||
jobs: cb =>
|
||||
subscribeBackupNgJobs(jobs => {
|
||||
cb(keyBy(jobs, 'id'))
|
||||
}),
|
||||
}),
|
||||
connectStore({
|
||||
loneSnapshots: createGetLoneSnapshots,
|
||||
legacySnapshots: createGetObjectsOfType('VM-snapshot').filter([
|
||||
(() => {
|
||||
const RE = /^(?:XO_DELTA_EXPORT:|XO_DELTA_BASE_VM_SNAPSHOT_|rollingSnapshot_)/
|
||||
return (
|
||||
{ name_label } // eslint-disable-line camelcase
|
||||
) => RE.test(name_label)
|
||||
})(),
|
||||
]),
|
||||
vms: createGetObjectsOfType('VM'),
|
||||
}),
|
||||
provideState({
|
||||
initialState: () => ({
|
||||
backupsByRemote: undefined,
|
||||
}),
|
||||
effects: {
|
||||
initialize({ fetchBackupList }) {
|
||||
return fetchBackupList()
|
||||
},
|
||||
async fetchBackupList() {
|
||||
this.state.backupsByRemote = await listVmBackups(
|
||||
toArray(await getRemotes())
|
||||
)
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
detachedBackups: ({ backupsByRemote }, { jobs, vms, schedules }) => {
|
||||
if (jobs === undefined || schedules === undefined) {
|
||||
return []
|
||||
}
|
||||
|
||||
const detachedBackups = []
|
||||
let job
|
||||
forEach(backupsByRemote, backupsByVm => {
|
||||
forEach(backupsByVm, (vmBackups, vmId) => {
|
||||
const vm = vms[vmId]
|
||||
vmBackups.forEach(backup => {
|
||||
const reason =
|
||||
vm === undefined
|
||||
? 'missingVm'
|
||||
: (job = jobs[backup.jobId]) === undefined
|
||||
? 'missingJob'
|
||||
: schedules[backup.scheduleId] === undefined
|
||||
? 'missingSchedule'
|
||||
: !createPredicate(omit(job.vms, 'power_state'))(vm)
|
||||
? 'missingVmInJob'
|
||||
: undefined
|
||||
|
||||
if (reason !== undefined) {
|
||||
detachedBackups.push({
|
||||
...backup,
|
||||
vmId,
|
||||
reason,
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
return detachedBackups
|
||||
},
|
||||
},
|
||||
}),
|
||||
injectState,
|
||||
({
|
||||
effects: { fetchBackupList },
|
||||
jobs,
|
||||
legacySnapshots,
|
||||
loneSnapshots,
|
||||
state: { detachedBackups },
|
||||
vms,
|
||||
}) => (
|
||||
<Container>
|
||||
<Row className='detached-backups'>
|
||||
<Col>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Icon icon='backup' /> {_('detachedBackups')}
|
||||
</CardHeader>
|
||||
<CardBlock>
|
||||
<div className='mb-1'>
|
||||
<ActionButton
|
||||
btnStyle='primary'
|
||||
handler={fetchBackupList}
|
||||
icon='refresh'
|
||||
>
|
||||
{_('refreshBackupList')}
|
||||
</ActionButton>
|
||||
</div>
|
||||
<NoObjects
|
||||
actions={DETACHED_BACKUP_ACTIONS}
|
||||
collection={detachedBackups}
|
||||
columns={DETACHED_BACKUP_COLUMNS}
|
||||
component={SortedTable}
|
||||
data-fetchBackupList={fetchBackupList}
|
||||
data-jobs={jobs}
|
||||
data-vms={vms}
|
||||
emptyMessage={_('noDetachedBackups')}
|
||||
shortcutsTarget='.detached-backups'
|
||||
stateUrlParam='s_detached_backups'
|
||||
/>
|
||||
</CardBlock>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row className='lone-snapshots'>
|
||||
<Col>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Icon icon='vm' /> {_('vmSnapshotsRelatedToNonExistentBackups')}
|
||||
</CardHeader>
|
||||
<CardBlock>
|
||||
<NoObjects
|
||||
actions={ACTIONS}
|
||||
collection={loneSnapshots}
|
||||
columns={SNAPSHOT_COLUMNS}
|
||||
component={SortedTable}
|
||||
data-vms={vms}
|
||||
emptyMessage={_('noSnapshots')}
|
||||
shortcutsTarget='.lone-snapshots'
|
||||
stateUrlParam='s_vm_snapshots'
|
||||
/>
|
||||
</CardBlock>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row className='legacy-snapshots'>
|
||||
<Col>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Icon icon='vm' /> {_('legacySnapshots')}
|
||||
</CardHeader>
|
||||
<CardBlock>
|
||||
<NoObjects
|
||||
actions={ACTIONS}
|
||||
collection={legacySnapshots}
|
||||
columns={SNAPSHOT_COLUMNS}
|
||||
component={SortedTable}
|
||||
data-vms={vms}
|
||||
emptyMessage={_('noSnapshots')}
|
||||
shortcutsTarget='.legacy-snapshots'
|
||||
stateUrlParam='s_legacy_vm_snapshots'
|
||||
/>
|
||||
</CardBlock>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
),
|
||||
])
|
||||
|
||||
export default Health
|
||||
|
@ -280,7 +280,7 @@ export default class Restore extends Component {
|
||||
handler={this._refreshBackupList}
|
||||
icon='refresh'
|
||||
>
|
||||
{_('restoreResfreshList')}
|
||||
{_('refreshBackupList')}
|
||||
</ActionButton>{' '}
|
||||
<ButtonLink to='backup/restore/metadata'>
|
||||
<Icon icon='database' /> {_('metadata')}
|
||||
|
Loading…
Reference in New Issue
Block a user