Compare commits
1 Commits
xo-vmdk-to
...
xen-api-ev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3facbcda99 |
@@ -19,7 +19,7 @@ module.exports = {
|
||||
|
||||
overrides: [
|
||||
{
|
||||
files: ['cli.js', '*-cli.js', 'packages/*cli*/**/*.js'],
|
||||
files: ['packages/*cli*/**/*.js', '*-cli.js'],
|
||||
rules: {
|
||||
'no-console': 'off',
|
||||
},
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
"promise-toolbox": "^0.12.1",
|
||||
"readable-stream": "^3.0.6",
|
||||
"through2": "^3.0.0",
|
||||
"tmp": "^0.1.0",
|
||||
"tmp": "^0.0.33",
|
||||
"xo-remote-parser": "^0.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -5,17 +5,10 @@
|
||||
- [Settings/remotes] Expose mount options field for SMB [#4063](https://github.com/vatesfr/xen-orchestra/issues/4063) (PR [#4067](https://github.com/vatesfr/xen-orchestra/pull/4067))
|
||||
- [Backup/Schedule] Add warning regarding DST when you add a schedule [#4042](https://github.com/vatesfr/xen-orchestra/issues/4042) (PR [#4056](https://github.com/vatesfr/xen-orchestra/pull/4056))
|
||||
- [Import] Avoid blocking the UI when dropping a big OVA file on the UI (PR [#4018](https://github.com/vatesfr/xen-orchestra/pull/4018))
|
||||
- [Backup NG/Overview] Make backup list title clearer [#4111](https://github.com/vatesfr/xen-orchestra/issues/4111) (PR [#4129](https://github.com/vatesfr/xen-orchestra/pull/4129))
|
||||
- [Dashboard] Hide "Report" section for non-admins [#4123](https://github.com/vatesfr/xen-orchestra/issues/4123) (PR [#4126](https://github.com/vatesfr/xen-orchestra/pull/4126))
|
||||
- [VM migration] Auto select default SR and collapse optional actions [#3326](https://github.com/vatesfr/xen-orchestra/issues/3326) (PR [#4121](https://github.com/vatesfr/xen-orchestra/pull/4121))
|
||||
- [Metadata backup] Logs [#4005](https://github.com/vatesfr/xen-orchestra/issues/4005) (PR [#4014](https://github.com/vatesfr/xen-orchestra/pull/4014))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- [Continuous Replication] Fix VHD size guess for empty files [#4105](https://github.com/vatesfr/xen-orchestra/issues/4105) (PR [#4107](https://github.com/vatesfr/xen-orchestra/pull/4107))
|
||||
- [Backup NG] Only display full backup interval in case of a delta backup (PR [#4125](https://github.com/vatesfr/xen-orchestra/pull/4107))
|
||||
- [Dashboard/Health] fix 'an error has occurred' on the storage state table [#4128](https://github.com/vatesfr/xen-orchestra/issues/4128) (PR [#4132](https://github.com/vatesfr/xen-orchestra/pull/4132))
|
||||
- [Menu] XOA: Fixed empty slot when menu is collapsed [#4012](https://github.com/vatesfr/xen-orchestra/issues/4012) (PR [#4068](https://github.com/vatesfr/xen-orchestra/pull/4068)
|
||||
|
||||
### Released packages
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"eslint-plugin-react": "^7.6.1",
|
||||
"eslint-plugin-standard": "^4.0.0",
|
||||
"exec-promise": "^0.7.0",
|
||||
"flow-bin": "^0.96.0",
|
||||
"flow-bin": "^0.95.1",
|
||||
"globby": "^9.0.0",
|
||||
"husky": "^1.2.1",
|
||||
"jest": "^24.1.0",
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
"index-modules": "^0.3.0",
|
||||
"promise-toolbox": "^0.12.1",
|
||||
"rimraf": "^2.6.1",
|
||||
"tmp": "^0.1.0"
|
||||
"tmp": "^0.0.33"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
"index-modules": "^0.3.0",
|
||||
"readable-stream": "^3.0.6",
|
||||
"rimraf": "^2.6.2",
|
||||
"tmp": "^0.1.0"
|
||||
"tmp": "^0.0.33"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
|
||||
|
||||
@@ -33,7 +33,6 @@
|
||||
"node": ">=6"
|
||||
},
|
||||
"dependencies": {
|
||||
"bind-property-descriptor": "^1.0.0",
|
||||
"blocked": "^1.2.1",
|
||||
"debug": "^4.0.1",
|
||||
"event-to-promise": "^0.8.0",
|
||||
|
||||
8
packages/xen-api/src/_MultiCounter.js
Normal file
8
packages/xen-api/src/_MultiCounter.js
Normal file
@@ -0,0 +1,8 @@
|
||||
const handler = {
|
||||
get(target, property) {
|
||||
const value = target[property]
|
||||
return value !== undefined ? value : 0
|
||||
},
|
||||
}
|
||||
|
||||
export const create = () => new Proxy({ __proto__: null }, handler)
|
||||
@@ -9,7 +9,6 @@ import minimist from 'minimist'
|
||||
import pw from 'pw'
|
||||
import { asCallback, fromCallback } from 'promise-toolbox'
|
||||
import { filter, find, isArray } from 'lodash'
|
||||
import { getBoundPropertyDescriptor } from 'bind-property-descriptor'
|
||||
import { start as createRepl } from 'repl'
|
||||
|
||||
import { createClient } from './'
|
||||
@@ -26,20 +25,6 @@ function askPassword(prompt = 'Password: ') {
|
||||
})
|
||||
}
|
||||
|
||||
const { getPrototypeOf, ownKeys } = Reflect
|
||||
function getAllBoundDescriptors(object) {
|
||||
const descriptors = { __proto__: null }
|
||||
let current = object
|
||||
do {
|
||||
ownKeys(current).forEach(key => {
|
||||
if (!(key in descriptors)) {
|
||||
descriptors[key] = getBoundPropertyDescriptor(current, key, object)
|
||||
}
|
||||
})
|
||||
} while ((current = getPrototypeOf(current)) !== null)
|
||||
return descriptors
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const usage = 'Usage: xen-api <url> [<user> [<password>]]'
|
||||
@@ -93,17 +78,11 @@ const main = async args => {
|
||||
const repl = createRepl({
|
||||
prompt: `${xapi._humanId}> `,
|
||||
})
|
||||
repl.context.xapi = xapi
|
||||
|
||||
{
|
||||
const ctx = repl.context
|
||||
ctx.xapi = xapi
|
||||
|
||||
ctx.diff = (a, b) => console.log('%s', diff(a, b))
|
||||
ctx.find = predicate => find(xapi.objects.all, predicate)
|
||||
ctx.findAll = predicate => filter(xapi.objects.all, predicate)
|
||||
|
||||
Object.defineProperties(ctx, getAllBoundDescriptors(xapi))
|
||||
}
|
||||
repl.context.diff = (a, b) => console.log('%s', diff(a, b))
|
||||
repl.context.find = predicate => find(xapi.objects.all, predicate)
|
||||
repl.context.findAll = predicate => filter(xapi.objects.all, predicate)
|
||||
|
||||
// Make the REPL waits for promise completion.
|
||||
repl.eval = (evaluate => (cmd, context, filename, cb) => {
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
pTimeout,
|
||||
} from 'promise-toolbox'
|
||||
|
||||
import * as MultiCounter from './_MultiCounter'
|
||||
import autoTransport from './transports/auto'
|
||||
import coalesceCalls from './_coalesceCalls'
|
||||
import debug from './_debug'
|
||||
@@ -34,7 +35,7 @@ const EVENT_TIMEOUT = 60
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const { defineProperties, defineProperty, freeze, keys: getKeys } = Object
|
||||
const { defineProperties, freeze, keys: getKeys } = Object
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
@@ -99,6 +100,7 @@ export class Xapi extends EventEmitter {
|
||||
this._sessionId = undefined
|
||||
this._status = DISCONNECTED
|
||||
|
||||
this._counter = MultiCounter.create()
|
||||
this._debounce = opts.debounce ?? 200
|
||||
this._objects = new Collection()
|
||||
this._objectsByRef = { __proto__: null }
|
||||
@@ -773,6 +775,10 @@ export class Xapi extends EventEmitter {
|
||||
this._objects.set(object.$id, object)
|
||||
objectsByRef[ref] = object
|
||||
|
||||
if (prev === undefined) {
|
||||
++this._counter[type]
|
||||
}
|
||||
|
||||
if (type === 'pool') {
|
||||
this._pool = object
|
||||
|
||||
@@ -785,10 +791,6 @@ export class Xapi extends EventEmitter {
|
||||
}
|
||||
})
|
||||
} else if (type === 'task') {
|
||||
if (prev === undefined) {
|
||||
++this._nTasks
|
||||
}
|
||||
|
||||
const taskWatchers = this._taskWatchers
|
||||
const taskWatcher = taskWatchers[ref]
|
||||
if (taskWatcher !== undefined) {
|
||||
@@ -820,6 +822,7 @@ export class Xapi extends EventEmitter {
|
||||
}
|
||||
|
||||
async _refreshCachedRecords(types) {
|
||||
const counter = this._counter
|
||||
const toRemoveByType = { __proto__: null }
|
||||
types.forEach(type => {
|
||||
toRemoveByType[type] = new Set()
|
||||
@@ -851,8 +854,15 @@ export class Xapi extends EventEmitter {
|
||||
this._removeRecordFromCache(type, ref)
|
||||
})
|
||||
|
||||
if (type === 'task') {
|
||||
this._nTasks = refs.length
|
||||
const count = refs.length
|
||||
if (counter[type] !== count) {
|
||||
console.warn(
|
||||
'_refreshCachedRecords(%s): xapi=%d != local=%d',
|
||||
type,
|
||||
count,
|
||||
counter[type]
|
||||
)
|
||||
counter[type] = count
|
||||
}
|
||||
} catch (error) {
|
||||
// there is nothing ideal to do here, do not interrupt event
|
||||
@@ -873,9 +883,7 @@ export class Xapi extends EventEmitter {
|
||||
this._objects.unset(object.$id)
|
||||
delete byRefs[ref]
|
||||
|
||||
if (type === 'task') {
|
||||
--this._nTasks
|
||||
}
|
||||
--this._counter[type]
|
||||
}
|
||||
|
||||
const taskWatchers = this._taskWatchers
|
||||
@@ -927,6 +935,16 @@ export class Xapi extends EventEmitter {
|
||||
this._resolveObjectsFetched()
|
||||
this._resolveObjectsFetched = undefined
|
||||
|
||||
const IGNORED_TYPES = {
|
||||
__proto__: null,
|
||||
message: true,
|
||||
role: true,
|
||||
session: true,
|
||||
user: true,
|
||||
VBD_metrics: true,
|
||||
VIF_metrics: true,
|
||||
}
|
||||
|
||||
// event loop
|
||||
const debounce = this._debounce
|
||||
while (true) {
|
||||
@@ -959,10 +977,25 @@ export class Xapi extends EventEmitter {
|
||||
fromToken = result.token
|
||||
this._processEvents(result.events)
|
||||
|
||||
// detect and fix disappearing tasks (e.g. when toolstack restarts)
|
||||
if (result.valid_ref_counts.task !== this._nTasks) {
|
||||
await this._refreshCachedRecords(['task'])
|
||||
}
|
||||
// detect and fix desynchronized records
|
||||
const localCounts = this._counter
|
||||
const xapiCounts = result.valid_ref_counts
|
||||
await this._refreshCachedRecords(
|
||||
types.filter(type => {
|
||||
if (type in IGNORED_TYPES) {
|
||||
return false
|
||||
}
|
||||
|
||||
// XAPI uses lowercased types in events, but this may change, so we
|
||||
// handle both
|
||||
let xapiCount = xapiCounts[type]
|
||||
if (xapiCount === undefined) {
|
||||
xapiCount = xapiCounts[type.toLowerCase()]
|
||||
}
|
||||
|
||||
return localCounts[type] !== xapiCount
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1023,23 +1056,17 @@ export class Xapi extends EventEmitter {
|
||||
|
||||
const getObjectByRef = ref => this._objectsByRef[ref]
|
||||
|
||||
Record = defineProperty(
|
||||
function(ref, data) {
|
||||
defineProperties(this, {
|
||||
$id: { value: data.uuid ?? ref },
|
||||
$ref: { value: ref },
|
||||
$xapi: { value: xapi },
|
||||
})
|
||||
for (let i = 0; i < nFields; ++i) {
|
||||
const field = fields[i]
|
||||
this[field] = data[field]
|
||||
}
|
||||
},
|
||||
'name',
|
||||
{
|
||||
value: type,
|
||||
Record = function(ref, data) {
|
||||
defineProperties(this, {
|
||||
$id: { value: data.uuid ?? ref },
|
||||
$ref: { value: ref },
|
||||
$xapi: { value: xapi },
|
||||
})
|
||||
for (let i = 0; i < nFields; ++i) {
|
||||
const field = fields[i]
|
||||
this[field] = data[field]
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const getters = { $pool: getPool }
|
||||
const props = { $type: type }
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
"node": ">=6"
|
||||
},
|
||||
"dependencies": {
|
||||
"nodemailer": "^6.1.0",
|
||||
"nodemailer": "^5.0.0",
|
||||
"nodemailer-markdown": "^1.0.1",
|
||||
"promise-toolbox": "^0.12.1"
|
||||
},
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
"@xen-orchestra/cron": "^1.0.3",
|
||||
"@xen-orchestra/log": "^0.1.4",
|
||||
"handlebars": "^4.0.6",
|
||||
"html-minifier": "^4.0.0",
|
||||
"html-minifier": "^3.5.8",
|
||||
"human-format": "^0.10.0",
|
||||
"lodash": "^4.17.4",
|
||||
"promise-toolbox": "^0.12.1"
|
||||
|
||||
@@ -117,7 +117,7 @@
|
||||
"struct-fu": "^1.2.0",
|
||||
"tar-stream": "^2.0.1",
|
||||
"through2": "^3.0.0",
|
||||
"tmp": "^0.1.0",
|
||||
"tmp": "^0.0.33",
|
||||
"uuid": "^3.0.1",
|
||||
"value-matcher": "^0.2.0",
|
||||
"vhd-lib": "^0.6.0",
|
||||
@@ -128,7 +128,7 @@
|
||||
"xo-collection": "^0.4.1",
|
||||
"xo-common": "^0.2.0",
|
||||
"xo-remote-parser": "^0.5.0",
|
||||
"xo-vmdk-to-vhd": "^0.1.7",
|
||||
"xo-vmdk-to-vhd": "^0.1.6",
|
||||
"yazl": "^2.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -266,11 +266,6 @@ export default class Jobs {
|
||||
reportWhen: (settings && settings.reportWhen) || 'failure',
|
||||
}
|
||||
}
|
||||
if (type === 'metadataBackup') {
|
||||
data = {
|
||||
reportWhen: job.settings['']?.reportWhen ?? 'failure',
|
||||
}
|
||||
}
|
||||
|
||||
const logger = this._logger
|
||||
const runJobId = logger.notice(`Starting execution of ${id}.`, {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// @flow
|
||||
import asyncMap from '@xen-orchestra/async-map'
|
||||
import createLogger from '@xen-orchestra/log'
|
||||
import defer from 'golike-defer'
|
||||
import { fromEvent, ignoreErrors } from 'promise-toolbox'
|
||||
|
||||
import debounceWithKey from '../_pDebounceWithKey'
|
||||
@@ -24,14 +25,9 @@ const METADATA_BACKUP_JOB_TYPE = 'metadataBackup'
|
||||
|
||||
const compareTimestamp = (a, b) => a.timestamp - b.timestamp
|
||||
|
||||
const DEFAULT_RETENTION = 0
|
||||
|
||||
type ReportWhen = 'always' | 'failure' | 'never'
|
||||
|
||||
type Settings = {|
|
||||
reportWhen?: ReportWhen,
|
||||
retentionPoolMetadata?: number,
|
||||
retentionXoMetadata?: number,
|
||||
retentionPoolMetadata?: number,
|
||||
|}
|
||||
|
||||
type MetadataBackupJob = {
|
||||
@@ -51,22 +47,6 @@ const createSafeReaddir = (handler, methodName) => (path, options) =>
|
||||
return []
|
||||
})
|
||||
|
||||
const deleteOldBackups = (handler, dir, retention, handleError) =>
|
||||
handler.list(dir).then(list => {
|
||||
list.sort()
|
||||
list = list
|
||||
.filter(timestamp => /^\d{8}T\d{6}Z$/.test(timestamp))
|
||||
.slice(0, -retention)
|
||||
return Promise.all(
|
||||
list.map(timestamp => {
|
||||
const backupDir = `${dir}/${timestamp}`
|
||||
return handler
|
||||
.rmtree(backupDir)
|
||||
.catch(error => handleError(error, backupDir))
|
||||
})
|
||||
)
|
||||
}, handleError)
|
||||
|
||||
// metadata.json
|
||||
//
|
||||
// {
|
||||
@@ -96,14 +76,10 @@ const deleteOldBackups = (handler, dir, retention, handleError) =>
|
||||
//
|
||||
// Task logs emitted in a metadata backup execution:
|
||||
//
|
||||
// job.start(data: { reportWhen: ReportWhen })
|
||||
// job.start
|
||||
// ├─ task.start(data: { type: 'pool', id: string, pool: <Pool />, poolMaster: <Host /> })
|
||||
// │ ├─ task.start(data: { type: 'remote', id: string })
|
||||
// │ │ └─ task.end
|
||||
// │ └─ task.end
|
||||
// ├─ task.start(data: { type: 'xo' })
|
||||
// │ ├─ task.start(data: { type: 'remote', id: string })
|
||||
// │ │ └─ task.end
|
||||
// │ └─ task.end
|
||||
// └─ job.end
|
||||
export default class metadataBackup {
|
||||
@@ -156,286 +132,6 @@ export default class metadataBackup {
|
||||
})
|
||||
}
|
||||
|
||||
async _backupXo({ handlers, job, logger, retention, runJobId, schedule }) {
|
||||
const app = this._app
|
||||
|
||||
const timestamp = Date.now()
|
||||
const taskId = logger.notice(`Starting XO metadata backup. (${job.id})`, {
|
||||
data: {
|
||||
type: 'xo',
|
||||
},
|
||||
event: 'task.start',
|
||||
parentId: runJobId,
|
||||
})
|
||||
|
||||
try {
|
||||
const scheduleDir = `${DIR_XO_CONFIG_BACKUPS}/${schedule.id}`
|
||||
const dir = `${scheduleDir}/${safeDateFormat(timestamp)}`
|
||||
|
||||
const data = JSON.stringify(await app.exportConfig(), null, 2)
|
||||
const fileName = `${dir}/data.json`
|
||||
|
||||
const metadata = JSON.stringify(
|
||||
{
|
||||
jobId: job.id,
|
||||
jobName: job.name,
|
||||
scheduleId: schedule.id,
|
||||
scheduleName: schedule.name,
|
||||
timestamp,
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
const metaDataFileName = `${dir}/metadata.json`
|
||||
|
||||
await asyncMap(handlers, async (handler, remoteId) => {
|
||||
const subTaskId = logger.notice(
|
||||
`Starting XO metadata backup for the remote (${remoteId}). (${
|
||||
job.id
|
||||
})`,
|
||||
{
|
||||
data: {
|
||||
id: remoteId,
|
||||
type: 'remote',
|
||||
},
|
||||
event: 'task.start',
|
||||
parentId: taskId,
|
||||
}
|
||||
)
|
||||
|
||||
try {
|
||||
await Promise.all([
|
||||
handler.outputFile(fileName, data),
|
||||
handler.outputFile(metaDataFileName, metadata),
|
||||
])
|
||||
|
||||
await deleteOldBackups(
|
||||
handler,
|
||||
scheduleDir,
|
||||
retention,
|
||||
(error, backupDir) => {
|
||||
logger.warning(
|
||||
backupDir !== undefined
|
||||
? `unable to delete the folder ${backupDir}`
|
||||
: `unable to list backups for the remote (${remoteId})`,
|
||||
{
|
||||
event: 'task.warning',
|
||||
taskId: subTaskId,
|
||||
data: {
|
||||
error,
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
logger.notice(
|
||||
`Backuping XO metadata for the remote (${remoteId}) is a success. (${
|
||||
job.id
|
||||
})`,
|
||||
{
|
||||
event: 'task.end',
|
||||
status: 'success',
|
||||
taskId: subTaskId,
|
||||
}
|
||||
)
|
||||
} catch (error) {
|
||||
await handler.rmtree(dir).catch(error => {
|
||||
logger.warning(`unable to delete the folder ${dir}`, {
|
||||
event: 'task.warning',
|
||||
taskId: subTaskId,
|
||||
data: {
|
||||
error,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
logger.error(
|
||||
`Backuping XO metadata for the remote (${remoteId}) has failed. (${
|
||||
job.id
|
||||
})`,
|
||||
{
|
||||
event: 'task.end',
|
||||
result: serializeError(error),
|
||||
status: 'failure',
|
||||
taskId: subTaskId,
|
||||
}
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
logger.notice(`Backuping XO metadata is a success. (${job.id})`, {
|
||||
event: 'task.end',
|
||||
status: 'success',
|
||||
taskId,
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error(`Backuping XO metadata has failed. (${job.id})`, {
|
||||
event: 'task.end',
|
||||
result: serializeError(error),
|
||||
status: 'failure',
|
||||
taskId,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async _backupPool(
|
||||
poolId,
|
||||
{ cancelToken, handlers, job, logger, retention, runJobId, schedule, xapi }
|
||||
) {
|
||||
const poolMaster = await xapi
|
||||
.getRecord('host', xapi.pool.master)
|
||||
::ignoreErrors()
|
||||
const timestamp = Date.now()
|
||||
const taskId = logger.notice(
|
||||
`Starting metadata backup for the pool (${poolId}). (${job.id})`,
|
||||
{
|
||||
data: {
|
||||
id: poolId,
|
||||
pool: xapi.pool,
|
||||
poolMaster,
|
||||
type: 'pool',
|
||||
},
|
||||
event: 'task.start',
|
||||
parentId: runJobId,
|
||||
}
|
||||
)
|
||||
|
||||
try {
|
||||
const poolDir = `${DIR_XO_POOL_METADATA_BACKUPS}/${schedule.id}/${poolId}`
|
||||
const dir = `${poolDir}/${safeDateFormat(timestamp)}`
|
||||
|
||||
// TODO: export the metadata only once then split the stream between remotes
|
||||
const stream = await xapi.exportPoolMetadata(cancelToken)
|
||||
const fileName = `${dir}/data`
|
||||
|
||||
const metadata = JSON.stringify(
|
||||
{
|
||||
jobId: job.id,
|
||||
jobName: job.name,
|
||||
pool: xapi.pool,
|
||||
poolMaster,
|
||||
scheduleId: schedule.id,
|
||||
scheduleName: schedule.name,
|
||||
timestamp,
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
const metaDataFileName = `${dir}/metadata.json`
|
||||
|
||||
await asyncMap(handlers, async (handler, remoteId) => {
|
||||
const subTaskId = logger.notice(
|
||||
`Starting metadata backup for the pool (${poolId}) for the remote (${remoteId}). (${
|
||||
job.id
|
||||
})`,
|
||||
{
|
||||
data: {
|
||||
id: remoteId,
|
||||
type: 'remote',
|
||||
},
|
||||
event: 'task.start',
|
||||
parentId: taskId,
|
||||
}
|
||||
)
|
||||
|
||||
let outputStream
|
||||
try {
|
||||
await Promise.all([
|
||||
(async () => {
|
||||
outputStream = await handler.createOutputStream(fileName)
|
||||
|
||||
// 'readable-stream/pipeline' not call the callback when an error throws
|
||||
// from the readable stream
|
||||
stream.pipe(outputStream)
|
||||
return fromEvent(stream, 'end').catch(error => {
|
||||
if (error.message !== 'aborted') {
|
||||
throw error
|
||||
}
|
||||
})
|
||||
})(),
|
||||
handler.outputFile(metaDataFileName, metadata),
|
||||
])
|
||||
|
||||
await deleteOldBackups(
|
||||
handler,
|
||||
poolDir,
|
||||
retention,
|
||||
(error, backupDir) => {
|
||||
logger.warning(
|
||||
backupDir !== undefined
|
||||
? `unable to delete the folder ${backupDir}`
|
||||
: `unable to list backups for the remote (${remoteId})`,
|
||||
{
|
||||
event: 'task.warning',
|
||||
taskId: subTaskId,
|
||||
data: {
|
||||
error,
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
logger.notice(
|
||||
`Backuping pool metadata (${poolId}) for the remote (${remoteId}) is a success. (${
|
||||
job.id
|
||||
})`,
|
||||
{
|
||||
event: 'task.end',
|
||||
status: 'success',
|
||||
taskId: subTaskId,
|
||||
}
|
||||
)
|
||||
} catch (error) {
|
||||
if (outputStream !== undefined) {
|
||||
outputStream.destroy()
|
||||
}
|
||||
await handler.rmtree(dir).catch(error => {
|
||||
logger.warning(`unable to delete the folder ${dir}`, {
|
||||
event: 'task.warning',
|
||||
taskId: subTaskId,
|
||||
data: {
|
||||
error,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
logger.error(
|
||||
`Backuping pool metadata (${poolId}) for the remote (${remoteId}) has failed. (${
|
||||
job.id
|
||||
})`,
|
||||
{
|
||||
event: 'task.end',
|
||||
result: serializeError(error),
|
||||
status: 'failure',
|
||||
taskId: subTaskId,
|
||||
}
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
logger.notice(
|
||||
`Backuping pool metadata (${poolId}) is a success. (${job.id})`,
|
||||
{
|
||||
event: 'task.end',
|
||||
status: 'success',
|
||||
taskId,
|
||||
}
|
||||
)
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`Backuping pool metadata (${poolId}) has failed. (${job.id})`,
|
||||
{
|
||||
event: 'task.end',
|
||||
result: serializeError(error),
|
||||
status: 'failure',
|
||||
taskId,
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
async _executor({
|
||||
cancelToken,
|
||||
job: job_,
|
||||
@@ -459,103 +155,199 @@ export default class metadataBackup {
|
||||
throw new Error('no metadata mode found')
|
||||
}
|
||||
|
||||
let { retentionXoMetadata, retentionPoolMetadata } =
|
||||
job.settings[schedule.id] || {}
|
||||
const app = this._app
|
||||
const { retentionXoMetadata, retentionPoolMetadata } =
|
||||
job?.settings[schedule.id] || {}
|
||||
|
||||
// it also replaces null retentions introduced by the commit
|
||||
// https://github.com/vatesfr/xen-orchestra/commit/fea5117ed83b58d3a57715b32d63d46e3004a094#diff-c02703199db2a4c217943cf8e02b91deR40
|
||||
if (retentionXoMetadata == null) {
|
||||
retentionXoMetadata = DEFAULT_RETENTION
|
||||
}
|
||||
if (retentionPoolMetadata == null) {
|
||||
retentionPoolMetadata = DEFAULT_RETENTION
|
||||
const timestamp = Date.now()
|
||||
const formattedTimestamp = safeDateFormat(timestamp)
|
||||
const commonMetadata = {
|
||||
jobId: job.id,
|
||||
jobName: job.name,
|
||||
scheduleId: schedule.id,
|
||||
scheduleName: schedule.name,
|
||||
timestamp,
|
||||
}
|
||||
|
||||
if (
|
||||
(retentionPoolMetadata === 0 && retentionXoMetadata === 0) ||
|
||||
(!job.xoMetadata && retentionPoolMetadata === 0) ||
|
||||
(isEmptyPools && retentionXoMetadata === 0)
|
||||
) {
|
||||
const files = []
|
||||
if (job.xoMetadata && retentionXoMetadata > 0) {
|
||||
const taskId = logger.notice(`Starting XO metadata backup. (${job.id})`, {
|
||||
data: {
|
||||
type: 'xo',
|
||||
},
|
||||
event: 'task.start',
|
||||
parentId: runJobId,
|
||||
})
|
||||
|
||||
const xoMetadataDir = `${DIR_XO_CONFIG_BACKUPS}/${schedule.id}`
|
||||
const dir = `${xoMetadataDir}/${formattedTimestamp}`
|
||||
|
||||
const data = JSON.stringify(await app.exportConfig(), null, 2)
|
||||
const fileName = `${dir}/data.json`
|
||||
|
||||
const metadata = JSON.stringify(commonMetadata, null, 2)
|
||||
const metaDataFileName = `${dir}/metadata.json`
|
||||
|
||||
files.push({
|
||||
executeBackup: defer(($defer, handler) => {
|
||||
$defer.onFailure(() => handler.rmtree(dir))
|
||||
return Promise.all([
|
||||
handler.outputFile(fileName, data),
|
||||
handler.outputFile(metaDataFileName, metadata),
|
||||
]).then(
|
||||
result => {
|
||||
logger.notice(`Backuping XO metadata is a success. (${job.id})`, {
|
||||
event: 'task.end',
|
||||
status: 'success',
|
||||
taskId,
|
||||
})
|
||||
return result
|
||||
},
|
||||
error => {
|
||||
logger.notice(`Backuping XO metadata has failed. (${job.id})`, {
|
||||
event: 'task.end',
|
||||
result: serializeError(error),
|
||||
status: 'failure',
|
||||
taskId,
|
||||
})
|
||||
throw error
|
||||
}
|
||||
)
|
||||
}),
|
||||
dir: xoMetadataDir,
|
||||
retention: retentionXoMetadata,
|
||||
})
|
||||
}
|
||||
if (!isEmptyPools && retentionPoolMetadata > 0) {
|
||||
files.push(
|
||||
...(await Promise.all(
|
||||
poolIds.map(async id => {
|
||||
const xapi = this._app.getXapi(id)
|
||||
const poolMaster = await xapi.getRecord('host', xapi.pool.master)
|
||||
const taskId = logger.notice(
|
||||
`Starting metadata backup for the pool (${id}). (${job.id})`,
|
||||
{
|
||||
data: {
|
||||
id,
|
||||
pool: xapi.pool,
|
||||
poolMaster,
|
||||
type: 'pool',
|
||||
},
|
||||
event: 'task.start',
|
||||
parentId: runJobId,
|
||||
}
|
||||
)
|
||||
const poolMetadataDir = `${DIR_XO_POOL_METADATA_BACKUPS}/${
|
||||
schedule.id
|
||||
}/${id}`
|
||||
const dir = `${poolMetadataDir}/${formattedTimestamp}`
|
||||
|
||||
// TODO: export the metadata only once then split the stream between remotes
|
||||
const stream = await app.getXapi(id).exportPoolMetadata(cancelToken)
|
||||
const fileName = `${dir}/data`
|
||||
|
||||
const metadata = JSON.stringify(
|
||||
{
|
||||
...commonMetadata,
|
||||
pool: xapi.pool,
|
||||
poolMaster,
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
const metaDataFileName = `${dir}/metadata.json`
|
||||
|
||||
return {
|
||||
executeBackup: defer(($defer, handler) => {
|
||||
$defer.onFailure(() => handler.rmtree(dir))
|
||||
return Promise.all([
|
||||
(async () => {
|
||||
const outputStream = await handler.createOutputStream(
|
||||
fileName
|
||||
)
|
||||
$defer.onFailure(() => outputStream.destroy())
|
||||
|
||||
// 'readable-stream/pipeline' not call the callback when an error throws
|
||||
// from the readable stream
|
||||
stream.pipe(outputStream)
|
||||
return fromEvent(stream, 'end').catch(error => {
|
||||
if (error.message !== 'aborted') {
|
||||
throw error
|
||||
}
|
||||
})
|
||||
})(),
|
||||
handler.outputFile(metaDataFileName, metadata),
|
||||
]).then(
|
||||
result => {
|
||||
logger.notice(
|
||||
`Backuping pool metadata (${id}) is a success. (${
|
||||
job.id
|
||||
})`,
|
||||
{
|
||||
event: 'task.end',
|
||||
status: 'success',
|
||||
taskId,
|
||||
}
|
||||
)
|
||||
return result
|
||||
},
|
||||
error => {
|
||||
logger.notice(
|
||||
`Backuping pool metadata (${id}) has failed. (${job.id})`,
|
||||
{
|
||||
event: 'task.end',
|
||||
result: serializeError(error),
|
||||
status: 'failure',
|
||||
taskId,
|
||||
}
|
||||
)
|
||||
throw error
|
||||
}
|
||||
)
|
||||
}),
|
||||
dir: poolMetadataDir,
|
||||
retention: retentionPoolMetadata,
|
||||
}
|
||||
})
|
||||
))
|
||||
)
|
||||
}
|
||||
|
||||
if (files.length === 0) {
|
||||
throw new Error('no retentions corresponding to the metadata modes found')
|
||||
}
|
||||
|
||||
cancelToken.throwIfRequested()
|
||||
|
||||
const app = this._app
|
||||
const timestampReg = /^\d{8}T\d{6}Z$/
|
||||
return asyncMap(
|
||||
// TODO: emit a warning task if a remote is broken
|
||||
asyncMap(remoteIds, id => app.getRemoteHandler(id)::ignoreErrors()),
|
||||
async handler => {
|
||||
if (handler === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
const handlers = {}
|
||||
await Promise.all(
|
||||
remoteIds.map(id =>
|
||||
app.getRemoteHandler(id).then(
|
||||
handler => {
|
||||
handlers[id] = handler
|
||||
},
|
||||
error => {
|
||||
logger.warning(`unable to get the handler for the remote (${id})`, {
|
||||
event: 'task.warning',
|
||||
taskId: runJobId,
|
||||
data: {
|
||||
error,
|
||||
},
|
||||
await Promise.all(
|
||||
files.map(async ({ executeBackup, dir, retention }) => {
|
||||
await executeBackup(handler)
|
||||
|
||||
// deleting old backups
|
||||
await handler.list(dir).then(list => {
|
||||
list.sort()
|
||||
list = list
|
||||
.filter(timestampDir => timestampReg.test(timestampDir))
|
||||
.slice(0, -retention)
|
||||
return Promise.all(
|
||||
list.map(timestampDir =>
|
||||
handler.rmtree(`${dir}/${timestampDir}`)
|
||||
)
|
||||
)
|
||||
})
|
||||
}
|
||||
})
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
if (Object.keys(handlers).length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const promises = []
|
||||
if (job.xoMetadata && retentionXoMetadata !== 0) {
|
||||
promises.push(
|
||||
this._backupXo({
|
||||
handlers,
|
||||
job,
|
||||
logger,
|
||||
retention: retentionXoMetadata,
|
||||
runJobId,
|
||||
schedule,
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
if (!isEmptyPools && retentionPoolMetadata !== 0) {
|
||||
poolIds.forEach(id => {
|
||||
let xapi
|
||||
try {
|
||||
xapi = this._app.getXapi(id)
|
||||
} catch (error) {
|
||||
logger.warning(
|
||||
`unable to get the xapi associated to the pool (${id})`,
|
||||
{
|
||||
event: 'task.warning',
|
||||
taskId: runJobId,
|
||||
data: {
|
||||
error,
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
if (xapi !== undefined) {
|
||||
promises.push(
|
||||
this._backupPool(id, {
|
||||
cancelToken,
|
||||
handlers,
|
||||
job,
|
||||
logger,
|
||||
retention: retentionPoolMetadata,
|
||||
runJobId,
|
||||
schedule,
|
||||
xapi,
|
||||
})
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return Promise.all(promises)
|
||||
}
|
||||
|
||||
async createMetadataBackupJob(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "xo-vmdk-to-vhd",
|
||||
"version": "0.1.7",
|
||||
"version": "0.1.6",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "JS lib streaming a vmdk file to a vhd",
|
||||
"keywords": [
|
||||
@@ -28,7 +28,7 @@
|
||||
"core-js": "3.0.0",
|
||||
"pipette": "^0.9.3",
|
||||
"promise-toolbox": "^0.12.1",
|
||||
"tmp": "^0.1.0",
|
||||
"tmp": "^0.0.33",
|
||||
"vhd-lib": "^0.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -95,7 +95,7 @@
|
||||
"moment": "^2.20.1",
|
||||
"moment-timezone": "^0.5.14",
|
||||
"notifyjs": "^3.0.0",
|
||||
"otplib": "^11.0.0",
|
||||
"otplib": "^10.0.1",
|
||||
"promise-toolbox": "^0.12.1",
|
||||
"prop-types": "^15.6.0",
|
||||
"qrcode": "^1.3.2",
|
||||
@@ -128,7 +128,7 @@
|
||||
"redux-thunk": "^2.0.1",
|
||||
"reselect": "^2.5.4",
|
||||
"rimraf": "^2.6.2",
|
||||
"semver": "^6.0.0",
|
||||
"semver": "^5.4.1",
|
||||
"styled-components": "^3.1.5",
|
||||
"uglify-es": "^3.3.4",
|
||||
"uncontrollable-input": "^0.1.1",
|
||||
@@ -142,7 +142,7 @@
|
||||
"xo-common": "^0.2.0",
|
||||
"xo-lib": "^0.8.0",
|
||||
"xo-remote-parser": "^0.5.0",
|
||||
"xo-vmdk-to-vhd": "^0.1.7"
|
||||
"xo-vmdk-to-vhd": "^0.1.6"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "NODE_ENV=production gulp build",
|
||||
|
||||
@@ -3016,8 +3016,7 @@ export default {
|
||||
"Le SR par défaut n'est pas connecté à l'hôte",
|
||||
|
||||
// Original text: "For each VDI, select an SR:"
|
||||
chooseSrForEachVdisModalSelectSr:
|
||||
'Pour chaque VDI, sélectionner un SR (optionnel)',
|
||||
chooseSrForEachVdisModalSelectSr: 'Pour chaque VDI, sélectionner un SR :',
|
||||
|
||||
// Original text: "Select main SR…"
|
||||
chooseSrForEachVdisModalMainSr: 'Sélectionner le SR principal…',
|
||||
|
||||
@@ -46,7 +46,6 @@ const messages = {
|
||||
metadata: 'Metadata',
|
||||
chooseBackup: 'Choose a backup',
|
||||
clickToShowError: 'Click to show error',
|
||||
backupJobs: 'Backup jobs',
|
||||
|
||||
// ----- Modals -----
|
||||
alertOk: 'OK',
|
||||
@@ -1609,7 +1608,7 @@ const messages = {
|
||||
migrateVmNoTargetHostMessage: 'A target host is required to migrate a VM',
|
||||
migrateVmNoDefaultSrError: 'No default SR',
|
||||
migrateVmNotConnectedDefaultSrError: 'Default SR not connected to host',
|
||||
chooseSrForEachVdisModalSelectSr: 'For each VDI, select an SR (optional)',
|
||||
chooseSrForEachVdisModalSelectSr: 'For each VDI, select an SR:',
|
||||
chooseSrForEachVdisModalMainSr: 'Select main SR…',
|
||||
chooseSrForEachVdisModalVdiLabel: 'VDI',
|
||||
chooseSrForEachVdisModalSrLabel: 'SR*',
|
||||
|
||||
@@ -213,8 +213,6 @@ export const getStatus = state => state.status
|
||||
|
||||
export const getUser = state => state.user
|
||||
|
||||
export const getXoaState = state => state.xoaUpdaterState
|
||||
|
||||
export const getCheckPermissions = invoke(() => {
|
||||
const getPredicate = create(
|
||||
state => state.permissions,
|
||||
|
||||
@@ -70,8 +70,7 @@ export default class ChooseSrForEachVdisModal extends Component {
|
||||
{props.vdis != null && mainSr != null && (
|
||||
<Collapsible
|
||||
buttonText={_('chooseSrForEachVdisModalSelectSr')}
|
||||
collapsible
|
||||
size='small'
|
||||
collapsible={props.vdis.length >= 3}
|
||||
>
|
||||
<br />
|
||||
<Container>
|
||||
|
||||
@@ -152,7 +152,7 @@ export default class MigrateVmModalBody extends BaseComponent {
|
||||
return
|
||||
}
|
||||
|
||||
const { pools, vbds, vm } = this.props
|
||||
const { vbds, vm } = this.props
|
||||
const intraPool = vm.$pool === host.$pool
|
||||
|
||||
// Intra-pool
|
||||
@@ -178,7 +178,7 @@ export default class MigrateVmModalBody extends BaseComponent {
|
||||
intraPool,
|
||||
mapVifsNetworks: undefined,
|
||||
migrationNetwork: undefined,
|
||||
targetSrs: { mainSr: pools[host.$pool].default_SR },
|
||||
targetSrs: {},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ const MODES = {
|
||||
__proto__: null,
|
||||
|
||||
compression: 'full',
|
||||
fullInterval: 'delta',
|
||||
}
|
||||
|
||||
const getSettingsWithNonDefaultValue = (mode, settings) =>
|
||||
|
||||
@@ -340,7 +340,7 @@ const Overview = () => (
|
||||
<div>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Icon icon='backup' /> {_('backupJobs')}
|
||||
<Icon icon='schedule' /> {_('backupSchedules')}
|
||||
</CardHeader>
|
||||
<CardBlock>
|
||||
<JobsTable />
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import _ from 'intl'
|
||||
import decorate from 'apply-decorators'
|
||||
import defined from '@xen-orchestra/defined'
|
||||
import Icon from 'icon'
|
||||
import React from 'react'
|
||||
import Scheduler, { SchedulePreview } from 'scheduling'
|
||||
@@ -34,9 +35,9 @@ export default decorate([
|
||||
name: value.trim() === '' ? null : value,
|
||||
})
|
||||
},
|
||||
setRetention({ setSchedule }, value, { name }) {
|
||||
setRetention: ({ setSchedule }, value, { name }) => () => {
|
||||
setSchedule({
|
||||
[name]: value,
|
||||
[name]: defined(value, null),
|
||||
})
|
||||
},
|
||||
},
|
||||
@@ -65,6 +66,7 @@ export default decorate([
|
||||
value={schedule.name}
|
||||
/>
|
||||
</FormGroup>
|
||||
{/* retentions effects are defined on initialize() */}
|
||||
{retentions.map(({ name, valuePath }) => (
|
||||
<FormGroup key={valuePath}>
|
||||
<label>
|
||||
@@ -74,7 +76,6 @@ export default decorate([
|
||||
data-name={valuePath}
|
||||
min='0'
|
||||
onChange={effects.setRetention}
|
||||
required
|
||||
value={schedule[valuePath]}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
@@ -935,18 +935,16 @@ export default decorate([
|
||||
placeholder={formatMessage(messages.timeoutUnit)}
|
||||
/>
|
||||
</FormGroup>
|
||||
{state.isDelta && (
|
||||
<FormGroup>
|
||||
<label htmlFor={state.inputFullIntervalId}>
|
||||
<strong>{_('fullBackupInterval')}</strong>
|
||||
</label>{' '}
|
||||
<Number
|
||||
id={state.inputFullIntervalId}
|
||||
onChange={effects.setFullInterval}
|
||||
value={fullInterval}
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
<FormGroup>
|
||||
<label htmlFor={state.inputFullIntervalId}>
|
||||
<strong>{_('fullBackupInterval')}</strong>
|
||||
</label>{' '}
|
||||
<Number
|
||||
id={state.inputFullIntervalId}
|
||||
onChange={effects.setFullInterval}
|
||||
value={fullInterval}
|
||||
/>
|
||||
</FormGroup>
|
||||
{state.isFull && (
|
||||
<FormGroup>
|
||||
<label htmlFor={state.compressionId}>
|
||||
|
||||
@@ -38,6 +38,7 @@ import Schedules from '../_schedules'
|
||||
// A retention can be:
|
||||
// - number: set by user
|
||||
// - undefined: will be replaced by the default value in the display(table + modal) and on submitting the form
|
||||
// - null: when a user voluntarily deletes its value.
|
||||
const DEFAULT_RETENTION = 1
|
||||
|
||||
const RETENTION_POOL_METADATA = {
|
||||
@@ -206,20 +207,7 @@ export default decorate([
|
||||
schedules: ({ _schedules }, { schedules }) =>
|
||||
defined(_schedules, schedules),
|
||||
settings: ({ _settings }, { job }) =>
|
||||
// it replaces null retentions introduced by the commit
|
||||
// https://github.com/vatesfr/xen-orchestra/commit/fea5117ed83b58d3a57715b32d63d46e3004a094#diff-c02703199db2a4c217943cf8e02b91deR40
|
||||
defined(_settings, () =>
|
||||
mapValues(job.settings, setting => {
|
||||
const newSetting = { ...setting }
|
||||
if (newSetting.retentionPoolMetadata === null) {
|
||||
newSetting.retentionPoolMetadata = 0
|
||||
}
|
||||
if (newSetting.retentionXoMetadata === null) {
|
||||
newSetting.retentionXoMetadata = 0
|
||||
}
|
||||
return newSetting
|
||||
})
|
||||
),
|
||||
defined(_settings, () => job.settings),
|
||||
remotes: ({ _remotes }, { job }) =>
|
||||
defined(_remotes, () => destructPattern(job.remotes), []),
|
||||
remotesPredicate: ({ remotes }) => ({ id }) => !remotes.includes(id),
|
||||
@@ -239,13 +227,13 @@ export default decorate([
|
||||
state.modePoolMetadata &&
|
||||
every(
|
||||
state.settings,
|
||||
({ retentionPoolMetadata }) => retentionPoolMetadata === 0
|
||||
({ retentionPoolMetadata }) => retentionPoolMetadata === null
|
||||
),
|
||||
missingRetentionXoMetadata: state =>
|
||||
state.modeXoMetadata &&
|
||||
every(
|
||||
state.settings,
|
||||
({ retentionXoMetadata }) => retentionXoMetadata === 0
|
||||
({ retentionXoMetadata }) => retentionXoMetadata === null
|
||||
),
|
||||
missingSchedules: state => isEmpty(state.schedules),
|
||||
},
|
||||
|
||||
@@ -39,14 +39,9 @@ import {
|
||||
|
||||
const SrColContainer = connectStore(() => ({
|
||||
container: createGetObject(),
|
||||
}))(
|
||||
({ container }) =>
|
||||
container !== undefined && (
|
||||
<Link to={`${container.type}s/${container.id}`}>
|
||||
{container.name_label}
|
||||
</Link>
|
||||
)
|
||||
)
|
||||
}))(({ container }) => (
|
||||
<Link to={`${container.type}s/${container.id}`}>{container.name_label}</Link>
|
||||
))
|
||||
|
||||
const VmColContainer = connectStore(() => ({
|
||||
container: createGetObject(),
|
||||
|
||||
@@ -489,37 +489,35 @@ class DefaultCard extends Component {
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
{props.isAdmin && (
|
||||
<Row>
|
||||
<Col>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Icon icon='menu-dashboard-stats' /> {_('dashboardReport')}
|
||||
</CardHeader>
|
||||
<CardBlock className='text-xs-center'>
|
||||
<ActionButton
|
||||
btnStyle='primary'
|
||||
disabled={!canSendTheReport}
|
||||
handler={sendUsageReport}
|
||||
icon=''
|
||||
>
|
||||
{_('dashboardSendReport')}
|
||||
</ActionButton>
|
||||
<br />
|
||||
{!canSendTheReport && (
|
||||
<span>
|
||||
<Link to='/settings/plugins' className='text-info'>
|
||||
<Icon icon='info' /> {_('dashboardSendReportInfo')}
|
||||
</Link>
|
||||
<br />
|
||||
</span>
|
||||
)}
|
||||
{_('dashboardSendReportMessage')}
|
||||
</CardBlock>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
<Row>
|
||||
<Col>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Icon icon='menu-dashboard-stats' /> {_('dashboardReport')}
|
||||
</CardHeader>
|
||||
<CardBlock className='text-xs-center'>
|
||||
<ActionButton
|
||||
btnStyle='primary'
|
||||
disabled={!canSendTheReport}
|
||||
handler={sendUsageReport}
|
||||
icon=''
|
||||
>
|
||||
{_('dashboardSendReport')}
|
||||
</ActionButton>
|
||||
<br />
|
||||
{!canSendTheReport && (
|
||||
<span>
|
||||
<Link to='/settings/plugins' className='text-info'>
|
||||
<Icon icon='info' /> {_('dashboardSendReportInfo')}
|
||||
</Link>
|
||||
<br />
|
||||
</span>
|
||||
)}
|
||||
{_('dashboardSendReportMessage')}
|
||||
</CardBlock>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col>
|
||||
<PatchesCard hosts={this._getHosts()} />
|
||||
|
||||
@@ -38,6 +38,7 @@ export const LogStatus = ({ log, tooltip = _('logDisplayDetails') }) => {
|
||||
return (
|
||||
<ActionButton
|
||||
btnStyle={className}
|
||||
disabled={log.status !== 'failure' && isEmpty(log.tasks)}
|
||||
handler={showTasks}
|
||||
handlerParam={log.id}
|
||||
icon='preview'
|
||||
|
||||
@@ -1,20 +1,16 @@
|
||||
import _, { FormattedDuration } from 'intl'
|
||||
import ActionButton from 'action-button'
|
||||
import decorate from 'apply-decorators'
|
||||
import defined, { get } from '@xen-orchestra/defined'
|
||||
import Icon from 'icon'
|
||||
import React from 'react'
|
||||
import Select from 'form/select'
|
||||
import Tooltip from 'tooltip'
|
||||
import { addSubscriptions, formatSize, formatSpeed } from 'utils'
|
||||
import { countBy, cloneDeep, filter, keyBy, map } from 'lodash'
|
||||
import { countBy, filter, get, keyBy, map } from 'lodash'
|
||||
import { FormattedDate } from 'react-intl'
|
||||
import { injectState, provideState } from 'reaclette'
|
||||
import { runBackupNgJob, subscribeBackupNgLogs, subscribeRemotes } from 'xo'
|
||||
import { Vm, Sr, Remote, Pool } from 'render-xo-item'
|
||||
|
||||
const hasTaskFailed = ({ status }) =>
|
||||
status !== 'success' && status !== 'pending'
|
||||
import { Vm, Sr, Remote } from 'render-xo-item'
|
||||
|
||||
const TASK_STATUS = {
|
||||
failure: {
|
||||
@@ -48,75 +44,19 @@ const TaskStateInfos = ({ status }) => {
|
||||
)
|
||||
}
|
||||
|
||||
const TaskDate = ({ value }) => (
|
||||
<FormattedDate
|
||||
value={new Date(value)}
|
||||
month='short'
|
||||
day='numeric'
|
||||
year='numeric'
|
||||
hour='2-digit'
|
||||
minute='2-digit'
|
||||
second='2-digit'
|
||||
/>
|
||||
)
|
||||
|
||||
const TaskStart = ({ task }) => (
|
||||
<div>{_.keyValue(_('taskStart'), <TaskDate value={task.start} />)}</div>
|
||||
)
|
||||
const TaskEnd = ({ task }) =>
|
||||
task.end !== undefined ? (
|
||||
<div>{_.keyValue(_('taskEnd'), <TaskDate value={task.end} />)}</div>
|
||||
) : null
|
||||
const TaskDuration = ({ task }) =>
|
||||
task.end !== undefined ? (
|
||||
<div>
|
||||
{_.keyValue(
|
||||
_('taskDuration'),
|
||||
<FormattedDuration duration={task.end - task.start} />
|
||||
)}
|
||||
</div>
|
||||
) : null
|
||||
|
||||
const UNHEALTHY_VDI_CHAIN_ERROR = 'unhealthy VDI chain'
|
||||
const UNHEALTHY_VDI_CHAIN_LINK =
|
||||
'https://xen-orchestra.com/docs/backup_troubleshooting.html#vdi-chain-protection'
|
||||
|
||||
const TaskError = ({ task }) => {
|
||||
let message
|
||||
if (
|
||||
!hasTaskFailed(task) ||
|
||||
(message = defined(() => task.result.message, () => task.result.code)) ===
|
||||
undefined
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (message === UNHEALTHY_VDI_CHAIN_ERROR) {
|
||||
return (
|
||||
<div>
|
||||
<Tooltip content={_('clickForMoreInformation')}>
|
||||
<a
|
||||
className='text-info'
|
||||
href={UNHEALTHY_VDI_CHAIN_LINK}
|
||||
rel='noopener noreferrer'
|
||||
target='_blank'
|
||||
>
|
||||
<Icon icon='info' /> {_('unhealthyVdiChainError')}
|
||||
</a>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const [label, className] =
|
||||
task.status === 'skipped'
|
||||
? [_('taskReason'), 'text-info']
|
||||
: [_('taskError'), 'text-danger']
|
||||
|
||||
return (
|
||||
<div>{_.keyValue(label, <span className={className}>{message}</span>)}</div>
|
||||
const TaskDate = ({ label, value }) =>
|
||||
_.keyValue(
|
||||
_(label),
|
||||
<FormattedDate
|
||||
value={new Date(value)}
|
||||
month='short'
|
||||
day='numeric'
|
||||
year='numeric'
|
||||
hour='2-digit'
|
||||
minute='2-digit'
|
||||
second='2-digit'
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const Warnings = ({ warnings }) =>
|
||||
warnings !== undefined ? (
|
||||
@@ -132,168 +72,9 @@ const Warnings = ({ warnings }) =>
|
||||
</div>
|
||||
) : null
|
||||
|
||||
const VmTask = ({ children, restartVmJob, task }) => (
|
||||
<div>
|
||||
<Vm id={task.data.id} link newTab /> <TaskStateInfos status={task.status} />{' '}
|
||||
{restartVmJob !== undefined && hasTaskFailed(task) && (
|
||||
<ActionButton
|
||||
handler={restartVmJob}
|
||||
icon='run'
|
||||
size='small'
|
||||
tooltip={_('backupRestartVm')}
|
||||
data-vm={task.data.id}
|
||||
/>
|
||||
)}
|
||||
<Warnings warnings={task.warnings} />
|
||||
{children}
|
||||
<TaskStart task={task} />
|
||||
<TaskEnd task={task} />
|
||||
<TaskDuration task={task} />
|
||||
<TaskError task={task} />
|
||||
{task.transfer !== undefined && (
|
||||
<div>
|
||||
{_.keyValue(
|
||||
_('taskTransferredDataSize'),
|
||||
formatSize(task.transfer.size)
|
||||
)}
|
||||
<br />
|
||||
{_.keyValue(
|
||||
_('taskTransferredDataSpeed'),
|
||||
formatSpeed(task.transfer.size, task.transfer.duration)
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{task.merge !== undefined && (
|
||||
<div>
|
||||
{_.keyValue(_('taskMergedDataSize'), formatSize(task.merge.size))}
|
||||
<br />
|
||||
{_.keyValue(
|
||||
_('taskMergedDataSpeed'),
|
||||
formatSpeed(task.merge.size, task.merge.duration)
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{task.isFull !== undefined &&
|
||||
_.keyValue(_('exportType'), task.isFull ? 'full' : 'delta')}
|
||||
</div>
|
||||
)
|
||||
|
||||
const PoolTask = ({ children, task }) => (
|
||||
<div>
|
||||
<Pool id={task.data.id} link newTab />{' '}
|
||||
<TaskStateInfos status={task.status} />
|
||||
<Warnings warnings={task.warnings} />
|
||||
{children}
|
||||
<TaskStart task={task} />
|
||||
<TaskEnd task={task} />
|
||||
<TaskDuration task={task} />
|
||||
<TaskError task={task} />
|
||||
</div>
|
||||
)
|
||||
|
||||
const XoTask = ({ children, task }) => (
|
||||
<div>
|
||||
<Icon icon='menu-xoa' /> XO <TaskStateInfos status={task.status} />
|
||||
<Warnings warnings={task.warnings} />
|
||||
{children}
|
||||
<TaskStart task={task} />
|
||||
<TaskEnd task={task} />
|
||||
<TaskDuration task={task} />
|
||||
<TaskError task={task} />
|
||||
</div>
|
||||
)
|
||||
|
||||
const SnapshotTask = ({ task }) => (
|
||||
<div>
|
||||
<Icon icon='task' /> {_('snapshotVmLabel')}{' '}
|
||||
<TaskStateInfos status={task.status} />
|
||||
<Warnings warnings={task.warnings} />
|
||||
<TaskStart task={task} />
|
||||
<TaskEnd task={task} />
|
||||
<TaskError task={task} />
|
||||
</div>
|
||||
)
|
||||
|
||||
const RemoteTask = ({ children, task }) => (
|
||||
<div>
|
||||
<Remote id={task.data.id} link newTab />{' '}
|
||||
<TaskStateInfos status={task.status} />
|
||||
<Warnings warnings={task.warnings} />
|
||||
{children}
|
||||
<TaskStart task={task} />
|
||||
<TaskEnd task={task} />
|
||||
<TaskDuration task={task} />
|
||||
<TaskError task={task} />
|
||||
</div>
|
||||
)
|
||||
|
||||
const SrTask = ({ children, task }) => (
|
||||
<div>
|
||||
<Sr id={task.data.id} link newTab /> <TaskStateInfos status={task.status} />
|
||||
<Warnings warnings={task.warnings} />
|
||||
{children}
|
||||
<TaskStart task={task} />
|
||||
<TaskEnd task={task} />
|
||||
<TaskDuration task={task} />
|
||||
<TaskError task={task} />
|
||||
</div>
|
||||
)
|
||||
|
||||
const TransferMergeTask = ({ task }) => {
|
||||
const size = get(() => task.result.size)
|
||||
return (
|
||||
<div>
|
||||
<Icon icon='task' /> {task.message}{' '}
|
||||
<TaskStateInfos status={task.status} />
|
||||
<Warnings warnings={task.warnings} />
|
||||
<TaskStart task={task} />
|
||||
<TaskEnd task={task} />
|
||||
<TaskDuration task={task} />
|
||||
<TaskError task={task} />
|
||||
{size > 0 && (
|
||||
<div>
|
||||
{_.keyValue(_('operationSize'), formatSize(size))}
|
||||
<br />
|
||||
{_.keyValue(
|
||||
_('operationSpeed'),
|
||||
formatSpeed(size, task.end - task.start)
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const COMPONENT_BY_TYPE = {
|
||||
vm: VmTask,
|
||||
remote: RemoteTask,
|
||||
sr: SrTask,
|
||||
pool: PoolTask,
|
||||
xo: XoTask,
|
||||
}
|
||||
|
||||
const COMPONENT_BY_MESSAGE = {
|
||||
snapshot: SnapshotTask,
|
||||
merge: TransferMergeTask,
|
||||
transfer: TransferMergeTask,
|
||||
}
|
||||
|
||||
const TaskLi = ({ className, task, ...props }) => {
|
||||
let Component
|
||||
if (
|
||||
(Component = defined(
|
||||
() => COMPONENT_BY_TYPE[task.data.type.toLowerCase()],
|
||||
COMPONENT_BY_MESSAGE[task.message]
|
||||
)) === undefined
|
||||
) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<li className={className}>
|
||||
<Component task={task} {...props} />
|
||||
</li>
|
||||
)
|
||||
}
|
||||
const UNHEALTHY_VDI_CHAIN_ERROR = 'unhealthy VDI chain'
|
||||
const UNHEALTHY_VDI_CHAIN_LINK =
|
||||
'https://xen-orchestra.com/docs/backup_troubleshooting.html#vdi-chain-protection'
|
||||
|
||||
export default decorate([
|
||||
addSubscriptions(({ id }) => ({
|
||||
@@ -326,39 +107,10 @@ export default decorate([
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
log: (_, { log }) => {
|
||||
if (log === undefined) {
|
||||
return {}
|
||||
}
|
||||
|
||||
if (log.tasks === undefined) {
|
||||
return log
|
||||
}
|
||||
|
||||
let newLog
|
||||
log.tasks.forEach((task, key) => {
|
||||
if (task.tasks === undefined || get(() => task.data.type) !== 'VM') {
|
||||
return
|
||||
}
|
||||
|
||||
const subTaskWithIsFull = task.tasks.find(
|
||||
({ data = {} }) => data.isFull !== undefined
|
||||
)
|
||||
if (subTaskWithIsFull !== undefined) {
|
||||
if (newLog === undefined) {
|
||||
newLog = cloneDeep(log)
|
||||
}
|
||||
newLog.tasks[key].isFull = subTaskWithIsFull.data.isFull
|
||||
}
|
||||
})
|
||||
|
||||
return defined(newLog, log)
|
||||
},
|
||||
filteredTaskLogs: ({
|
||||
defaultFilter,
|
||||
filter: value = defaultFilter,
|
||||
log,
|
||||
}) =>
|
||||
filteredTaskLogs: (
|
||||
{ defaultFilter, filter: value = defaultFilter },
|
||||
{ log = {} }
|
||||
) =>
|
||||
value === 'all'
|
||||
? log.tasks
|
||||
: filter(log.tasks, ({ status }) => status === value),
|
||||
@@ -367,8 +119,8 @@ export default decorate([
|
||||
{_(label)} ({countByStatus[value] || 0})
|
||||
</span>
|
||||
),
|
||||
countByStatus: ({ log }) => ({
|
||||
all: get(() => log.tasks.length),
|
||||
countByStatus: (_, { log = {} }) => ({
|
||||
all: get(log.tasks, 'length'),
|
||||
...countBy(log.tasks, 'status'),
|
||||
}),
|
||||
options: ({ countByStatus }) => [
|
||||
@@ -417,13 +169,13 @@ export default decorate([
|
||||
},
|
||||
}),
|
||||
injectState,
|
||||
({ remotes, state, effects }) => {
|
||||
const { scheduleId, warnings, tasks = [] } = state.log
|
||||
return tasks.length === 0 ? (
|
||||
<div>
|
||||
<Warnings warnings={warnings} />
|
||||
<TaskError task={state.log} />
|
||||
</div>
|
||||
({ log = {}, remotes, state, effects }) => {
|
||||
const { status, result, scheduleId } = log
|
||||
return (status === 'failure' || status === 'skipped') &&
|
||||
result !== undefined ? (
|
||||
<span className={status === 'skipped' ? 'text-info' : 'text-danger'}>
|
||||
<Icon icon='alarm' /> {result.message}
|
||||
</span>
|
||||
) : (
|
||||
<div>
|
||||
<Select
|
||||
@@ -436,29 +188,238 @@ export default decorate([
|
||||
value={state.filter || state.defaultFilter}
|
||||
valueKey='value'
|
||||
/>
|
||||
<Warnings warnings={warnings} />
|
||||
<Warnings warnings={log.warnings} />
|
||||
<br />
|
||||
<ul className='list-group'>
|
||||
{map(state.filteredTaskLogs, taskLog => {
|
||||
let globalIsFull
|
||||
return (
|
||||
<TaskLi
|
||||
className='list-group-item'
|
||||
key={taskLog.id}
|
||||
restartVmJob={scheduleId && effects.restartVmJob}
|
||||
task={taskLog}
|
||||
>
|
||||
<li key={taskLog.data.id} className='list-group-item'>
|
||||
<Vm id={taskLog.data.id} link newTab /> (
|
||||
{taskLog.data.id.slice(4, 8)}){' '}
|
||||
<TaskStateInfos status={taskLog.status} />{' '}
|
||||
{scheduleId !== undefined &&
|
||||
taskLog.status !== 'success' &&
|
||||
taskLog.status !== 'pending' && (
|
||||
<ActionButton
|
||||
handler={effects.restartVmJob}
|
||||
icon='run'
|
||||
size='small'
|
||||
tooltip={_('backupRestartVm')}
|
||||
data-vm={taskLog.data.id}
|
||||
/>
|
||||
)}
|
||||
<Warnings warnings={taskLog.warnings} />
|
||||
<ul>
|
||||
{map(taskLog.tasks, subTaskLog => (
|
||||
<TaskLi key={subTaskLog.id} task={subTaskLog}>
|
||||
<ul>
|
||||
{map(subTaskLog.tasks, subSubTaskLog => (
|
||||
<TaskLi task={subSubTaskLog} key={subSubTaskLog.id} />
|
||||
))}
|
||||
</ul>
|
||||
</TaskLi>
|
||||
))}
|
||||
{map(taskLog.tasks, subTaskLog => {
|
||||
if (
|
||||
subTaskLog.message !== 'export' &&
|
||||
subTaskLog.message !== 'snapshot'
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
const isFull = get(subTaskLog.data, 'isFull')
|
||||
if (isFull !== undefined && globalIsFull === undefined) {
|
||||
globalIsFull = isFull
|
||||
}
|
||||
return (
|
||||
<li key={subTaskLog.id}>
|
||||
{subTaskLog.message === 'snapshot' ? (
|
||||
<span>
|
||||
<Icon icon='task' /> {_('snapshotVmLabel')}
|
||||
</span>
|
||||
) : subTaskLog.data.type === 'remote' ? (
|
||||
<span>
|
||||
<Remote id={subTaskLog.data.id} link newTab /> (
|
||||
{subTaskLog.data.id.slice(4, 8)})
|
||||
</span>
|
||||
) : (
|
||||
<span>
|
||||
<Sr id={subTaskLog.data.id} link newTab /> (
|
||||
{subTaskLog.data.id.slice(4, 8)})
|
||||
</span>
|
||||
)}{' '}
|
||||
<TaskStateInfos status={subTaskLog.status} />
|
||||
<Warnings warnings={subTaskLog.warnings} />
|
||||
<ul>
|
||||
{map(subTaskLog.tasks, operationLog => {
|
||||
if (
|
||||
operationLog.message !== 'merge' &&
|
||||
operationLog.message !== 'transfer'
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
return (
|
||||
<li key={operationLog.id}>
|
||||
<span>
|
||||
<Icon icon='task' /> {operationLog.message}
|
||||
</span>{' '}
|
||||
<TaskStateInfos status={operationLog.status} />
|
||||
<Warnings warnings={operationLog.warnings} />
|
||||
<br />
|
||||
<TaskDate
|
||||
label='taskStart'
|
||||
value={operationLog.start}
|
||||
/>
|
||||
{operationLog.end !== undefined && (
|
||||
<div>
|
||||
<TaskDate
|
||||
label='taskEnd'
|
||||
value={operationLog.end}
|
||||
/>
|
||||
<br />
|
||||
{_.keyValue(
|
||||
_('taskDuration'),
|
||||
<FormattedDuration
|
||||
duration={
|
||||
operationLog.end - operationLog.start
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<br />
|
||||
{operationLog.status === 'failure'
|
||||
? _.keyValue(
|
||||
_('taskError'),
|
||||
<span className='text-danger'>
|
||||
{operationLog.result.message}
|
||||
</span>
|
||||
)
|
||||
: operationLog.result.size > 0 && (
|
||||
<div>
|
||||
{_.keyValue(
|
||||
_('operationSize'),
|
||||
formatSize(
|
||||
operationLog.result.size
|
||||
)
|
||||
)}
|
||||
<br />
|
||||
{_.keyValue(
|
||||
_('operationSpeed'),
|
||||
formatSpeed(
|
||||
operationLog.result.size,
|
||||
operationLog.end -
|
||||
operationLog.start
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
<TaskDate label='taskStart' value={subTaskLog.start} />
|
||||
{subTaskLog.end !== undefined && (
|
||||
<div>
|
||||
<TaskDate label='taskEnd' value={subTaskLog.end} />
|
||||
<br />
|
||||
{subTaskLog.message !== 'snapshot' &&
|
||||
_.keyValue(
|
||||
_('taskDuration'),
|
||||
<FormattedDuration
|
||||
duration={subTaskLog.end - subTaskLog.start}
|
||||
/>
|
||||
)}
|
||||
<br />
|
||||
{subTaskLog.status === 'failure' &&
|
||||
subTaskLog.result !== undefined &&
|
||||
_.keyValue(
|
||||
_('taskError'),
|
||||
<span className='text-danger'>
|
||||
{subTaskLog.result.message}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</TaskLi>
|
||||
<TaskDate label='taskStart' value={taskLog.start} />
|
||||
<br />
|
||||
{taskLog.end !== undefined && (
|
||||
<div>
|
||||
<TaskDate label='taskEnd' value={taskLog.end} />
|
||||
<br />
|
||||
{_.keyValue(
|
||||
_('taskDuration'),
|
||||
<FormattedDuration
|
||||
duration={taskLog.end - taskLog.start}
|
||||
/>
|
||||
)}
|
||||
<br />
|
||||
{taskLog.result !== undefined ? (
|
||||
taskLog.result.message === UNHEALTHY_VDI_CHAIN_ERROR ? (
|
||||
<Tooltip content={_('clickForMoreInformation')}>
|
||||
<a
|
||||
className='text-info'
|
||||
href={UNHEALTHY_VDI_CHAIN_LINK}
|
||||
rel='noopener noreferrer'
|
||||
target='_blank'
|
||||
>
|
||||
<Icon icon='info' /> {_('unhealthyVdiChainError')}
|
||||
</a>
|
||||
</Tooltip>
|
||||
) : (
|
||||
_.keyValue(
|
||||
taskLog.status === 'skipped'
|
||||
? _('taskReason')
|
||||
: _('taskError'),
|
||||
<span
|
||||
className={
|
||||
taskLog.status === 'skipped'
|
||||
? 'text-info'
|
||||
: 'text-danger'
|
||||
}
|
||||
>
|
||||
{taskLog.result.message}
|
||||
</span>
|
||||
)
|
||||
)
|
||||
) : (
|
||||
<div>
|
||||
{taskLog.transfer !== undefined && (
|
||||
<div>
|
||||
{_.keyValue(
|
||||
_('taskTransferredDataSize'),
|
||||
formatSize(taskLog.transfer.size)
|
||||
)}
|
||||
<br />
|
||||
{_.keyValue(
|
||||
_('taskTransferredDataSpeed'),
|
||||
formatSpeed(
|
||||
taskLog.transfer.size,
|
||||
taskLog.transfer.duration
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{taskLog.merge !== undefined && (
|
||||
<div>
|
||||
{_.keyValue(
|
||||
_('taskMergedDataSize'),
|
||||
formatSize(taskLog.merge.size)
|
||||
)}
|
||||
<br />
|
||||
{_.keyValue(
|
||||
_('taskMergedDataSpeed'),
|
||||
formatSpeed(
|
||||
taskLog.merge.size,
|
||||
taskLog.merge.duration
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{globalIsFull !== undefined &&
|
||||
_.keyValue(_('exportType'), globalIsFull ? 'full' : 'delta')}
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
|
||||
@@ -2,7 +2,9 @@ import _ from 'intl'
|
||||
import classNames from 'classnames'
|
||||
import Component from 'base-component'
|
||||
import Icon from 'icon'
|
||||
import isEmpty from 'lodash/isEmpty'
|
||||
import Link from 'link'
|
||||
import map from 'lodash/map'
|
||||
import React from 'react'
|
||||
import Tooltip from 'tooltip'
|
||||
import { UpdateTag } from '../xoa/update'
|
||||
@@ -11,7 +13,6 @@ import { addSubscriptions, connectStore, getXoaPlan, noop } from 'utils'
|
||||
import {
|
||||
connect,
|
||||
signOut,
|
||||
subscribeNotifications,
|
||||
subscribePermissions,
|
||||
subscribeResourceSets,
|
||||
} from 'xo'
|
||||
@@ -22,10 +23,8 @@ import {
|
||||
getIsPoolAdmin,
|
||||
getStatus,
|
||||
getUser,
|
||||
getXoaState,
|
||||
isAdmin,
|
||||
} from 'selectors'
|
||||
import { every, identity, isEmpty, map } from 'lodash'
|
||||
|
||||
import styles from './index.css'
|
||||
|
||||
@@ -35,22 +34,20 @@ const returnTrue = () => true
|
||||
() => ({
|
||||
isAdmin,
|
||||
isPoolAdmin: getIsPoolAdmin,
|
||||
nHosts: createGetObjectsOfType('host').count(),
|
||||
nTasks: createGetObjectsOfType('task').count([
|
||||
task => task.status === 'pending',
|
||||
]),
|
||||
pools: createGetObjectsOfType('pool'),
|
||||
nHosts: createGetObjectsOfType('host').count(),
|
||||
srs: createGetObjectsOfType('SR'),
|
||||
status: getStatus,
|
||||
user: getUser,
|
||||
xoaState: getXoaState,
|
||||
}),
|
||||
{
|
||||
withRef: true,
|
||||
}
|
||||
)
|
||||
@addSubscriptions({
|
||||
notifications: subscribeNotifications,
|
||||
permissions: subscribePermissions,
|
||||
resourceSets: subscribeResourceSets,
|
||||
})
|
||||
@@ -91,11 +88,6 @@ export default class Menu extends Component {
|
||||
isEmpty
|
||||
)
|
||||
|
||||
_getNoNotifications = createSelector(
|
||||
() => this.props.notifications,
|
||||
notifications => every(notifications, { read: true })
|
||||
)
|
||||
|
||||
get height() {
|
||||
return this.refs.content.offsetHeight
|
||||
}
|
||||
@@ -126,11 +118,9 @@ export default class Menu extends Component {
|
||||
pools,
|
||||
nHosts,
|
||||
srs,
|
||||
xoaState,
|
||||
} = this.props
|
||||
const noOperatablePools = this._getNoOperatablePools()
|
||||
const noResourceSets = this._getNoResourceSets()
|
||||
const noNotifications = this._getNoNotifications()
|
||||
|
||||
/* eslint-disable object-property-newline */
|
||||
const items = [
|
||||
@@ -257,12 +247,16 @@ export default class Menu extends Component {
|
||||
to: isAdmin ? 'xoa/update' : 'xoa/notifications',
|
||||
icon: 'menu-xoa',
|
||||
label: 'xoa',
|
||||
extra: [
|
||||
!isAdmin || xoaState === 'upToDate' ? null : (
|
||||
<UpdateTag key='update' />
|
||||
),
|
||||
noNotifications ? null : <NotificationTag key='notification' />,
|
||||
],
|
||||
extra: (
|
||||
<span>
|
||||
{isAdmin && (
|
||||
<span>
|
||||
<UpdateTag />{' '}
|
||||
</span>
|
||||
)}
|
||||
<NotificationTag />
|
||||
</span>
|
||||
),
|
||||
subMenu: [
|
||||
isAdmin && {
|
||||
to: 'xoa/update',
|
||||
@@ -534,7 +528,6 @@ export default class Menu extends Component {
|
||||
const MenuLinkItem = props => {
|
||||
const { item } = props
|
||||
const { to, icon, label, subMenu, pill, extra } = item
|
||||
const _extra = extra !== undefined ? extra.find(e => e !== null) : undefined
|
||||
|
||||
return (
|
||||
<li className='nav-item xo-menu-item'>
|
||||
@@ -544,7 +537,7 @@ const MenuLinkItem = props => {
|
||||
to={to}
|
||||
>
|
||||
<Icon
|
||||
className={classNames((pill || _extra) && styles.hiddenCollapsed)}
|
||||
className={classNames((pill || extra) && styles.hiddenCollapsed)}
|
||||
icon={`${icon}`}
|
||||
size='lg'
|
||||
fixedWidth
|
||||
@@ -555,10 +548,7 @@ const MenuLinkItem = props => {
|
||||
|
||||
</span>
|
||||
{pill > 0 && <span className='tag tag-pill tag-primary'>{pill}</span>}
|
||||
<span className={styles.hiddenUncollapsed}>{_extra}</span>
|
||||
<span className={styles.hiddenCollapsed}>
|
||||
{extra !== undefined && extra.map(identity)}
|
||||
</span>
|
||||
{extra}
|
||||
</Link>
|
||||
{subMenu && <SubMenu items={subMenu} />}
|
||||
</li>
|
||||
|
||||
Reference in New Issue
Block a user