Compare commits

..

1 Commits

Author SHA1 Message Date
Julien Fontanet
3facbcda99 feat(xen-api/_watchEvents): detect and fix desynchornizations 2019-04-08 15:46:26 +02:00
63 changed files with 1779 additions and 2697 deletions

View File

@@ -1,7 +1,5 @@
module.exports = {
extends: [
'plugin:eslint-comments/recommended',
'standard',
'standard-jsx',
'prettier',
@@ -21,7 +19,7 @@ module.exports = {
overrides: [
{
files: ['cli.js', '*-cli.js', 'packages/*cli*/**/*.js'],
files: ['packages/*cli*/**/*.js', '*-cli.js'],
rules: {
'no-console': 'off',
},

View File

@@ -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": {

View File

@@ -1,46 +1,5 @@
# ChangeLog
## **next** (2019-04-26)
### Highlights
- [Self/New VM] Add network config box to custom cloud-init [#3872](https://github.com/vatesfr/xen-orchestra/issues/3872) (PR [#4150](https://github.com/vatesfr/xen-orchestra/pull/4150))
- [Metadata backup] Detailed logs [#4005](https://github.com/vatesfr/xen-orchestra/issues/4005) (PR [#4014](https://github.com/vatesfr/xen-orchestra/pull/4014))
- [Backup reports] Support metadata backups (PR [#4084](https://github.com/vatesfr/xen-orchestra/pull/4084))
- [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))
- Unlock basic stats on all editions [#4166](https://github.com/vatesfr/xen-orchestra/issues/4166) (PR [#4172](https://github.com/vatesfr/xen-orchestra/pull/4172))
### Enhancements
- [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))
- [Self/New VM] Display confirmation modal when user will use a large amount of resources [#4044](https://github.com/vatesfr/xen-orchestra/issues/4044) (PR [#4127](https://github.com/vatesfr/xen-orchestra/pull/4127))
- [VDI migration, New disk] Warning when SR host is different from the other disks [#3911](https://github.com/vatesfr/xen-orchestra/issues/3911) (PR [#4035](https://github.com/vatesfr/xen-orchestra/pull/4035))
- [Attach disk] Display warning message when VDI SR is on different host from the other disks [#3911](https://github.com/vatesfr/xen-orchestra/issues/3911) (PR [#4117](https://github.com/vatesfr/xen-orchestra/pull/4117))
- [Editable] Notify user when editable undo fails [#3799](https://github.com/vatesfr/xen-orchestra/issues/3799) (PR [#4150](https://github.com/vatesfr/xen-orchestra/pull/4157))
- [XO] Add banner for sources users to clarify support conditions [#4165](https://github.com/vatesfr/xen-orchestra/issues/4165) (PR [#4167](https://github.com/vatesfr/xen-orchestra/pull/4167))
### 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)
- [Self/New VM] Fix missing templates when refreshing page [#3265](https://github.com/vatesfr/xen-orchestra/issues/3265) (PR [#3565](https://github.com/vatesfr/xen-orchestra/pull/3565))
- [Home] No more false positives when select Tag on Home page [#4087](https://github.com/vatesfr/xen-orchestra/issues/4087) (PR [#4112](https://github.com/vatesfr/xen-orchestra/pull/4112))
### Released packages
- xo-server-backup-reports v0.16.0
- complex-matcher v0.6.0
- xo-vmdk-to-vhd v0.1.7
- vhd-lib v0.6.1
- xo-server v5.40.0
- xo-web v5.40.0
## **5.33.1** (2019-04-04)
### Bug fix

View File

@@ -2,9 +2,17 @@
### Enhancements
- [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))
### 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))
### Released packages
- xo-server v5.41.0
- xo-web v5.41.0
- xo-vmdk-to-vhd v0.1.7
- vhd-lib v0.6.1
- xo-server v5.39.0
- xo-web v5.39.0

View File

@@ -10,14 +10,13 @@
"eslint-config-prettier": "^4.1.0",
"eslint-config-standard": "12.0.0",
"eslint-config-standard-jsx": "^6.0.2",
"eslint-plugin-eslint-comments": "^3.1.1",
"eslint-plugin-import": "^2.8.0",
"eslint-plugin-node": "^8.0.0",
"eslint-plugin-promise": "^4.0.0",
"eslint-plugin-react": "^7.6.1",
"eslint-plugin-standard": "^4.0.0",
"exec-promise": "^0.7.0",
"flow-bin": "^0.97.0",
"flow-bin": "^0.95.1",
"globby": "^9.0.0",
"husky": "^1.2.1",
"jest": "^24.1.0",

View File

@@ -1,6 +1,6 @@
{
"name": "complex-matcher",
"version": "0.6.0",
"version": "0.5.0",
"license": "ISC",
"description": "",
"keywords": [],

View File

@@ -599,13 +599,6 @@ export const parse = parser.parse.bind(parser)
// -------------------------------------------------------------------
const _extractStringFromRegexp = child => {
const unescapedRegexp = child.re.source.replace(/^(\^)|\\|\$$/g, '')
if (child.re.source === `^${escapeRegExp(unescapedRegexp)}$`) {
return unescapedRegexp
}
}
const _getPropertyClauseStrings = ({ child }) => {
if (child instanceof Or) {
const strings = []
@@ -613,12 +606,6 @@ const _getPropertyClauseStrings = ({ child }) => {
if (child instanceof StringNode) {
strings.push(child.value)
}
if (child instanceof RegExpNode) {
const unescapedRegexp = _extractStringFromRegexp(child)
if (unescapedRegexp !== undefined) {
strings.push(unescapedRegexp)
}
}
})
return strings
}
@@ -626,12 +613,6 @@ const _getPropertyClauseStrings = ({ child }) => {
if (child instanceof StringNode) {
return [child.value]
}
if (child instanceof RegExpNode) {
const unescapedRegexp = _extractStringFromRegexp(child)
if (unescapedRegexp !== undefined) {
return [unescapedRegexp]
}
}
return []
}

View File

@@ -12,13 +12,10 @@ import {
} from './'
it('getPropertyClausesStrings', () => {
const tmp = getPropertyClausesStrings(
parse('foo bar:baz baz:|(foo bar /^boo$/ /^far$/) foo:/^bar$/')
)
const tmp = getPropertyClausesStrings(parse('foo bar:baz baz:|(foo bar)'))
expect(tmp).toEqual({
bar: ['baz'],
baz: ['foo', 'bar', 'boo', 'far'],
foo: ['bar'],
baz: ['foo', 'bar'],
})
})

View File

@@ -32,7 +32,7 @@
"exec-promise": "^0.7.0",
"getopts": "^2.2.3",
"struct-fu": "^1.2.0",
"vhd-lib": "^0.6.1"
"vhd-lib": "^0.6.0"
},
"devDependencies": {
"@babel/cli": "^7.0.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/",

View File

@@ -1,6 +1,6 @@
{
"name": "vhd-lib",
"version": "0.6.1",
"version": "0.6.0",
"license": "AGPL-3.0",
"description": "Primitives for VHD file handling",
"keywords": [],
@@ -22,7 +22,7 @@
},
"dependencies": {
"async-iterator-to-stream": "^1.0.2",
"core-js": "^3.0.0",
"core-js": "3.0.0",
"from2": "^2.3.0",
"fs-extra": "^7.0.0",
"limit-concurrency-decorator": "^0.4.0",
@@ -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/",

View File

@@ -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",

View 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)

View File

@@ -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) => {

View File

@@ -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 }

View File

@@ -1,6 +1,6 @@
{
"name": "xo-server-backup-reports",
"version": "0.16.0",
"version": "0.15.0",
"license": "AGPL-3.0",
"description": "Backup reports plugin for XO-Server",
"keywords": [
@@ -36,7 +36,6 @@
"node": ">=6"
},
"dependencies": {
"@xen-orchestra/log": "^0.1.4",
"human-format": "^0.10.0",
"lodash": "^4.13.1",
"moment-timezone": "^0.5.13"
@@ -44,8 +43,6 @@
"devDependencies": {
"@babel/cli": "^7.0.0",
"@babel/core": "^7.0.0",
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.4.3",
"@babel/plugin-proposal-optional-chaining": "^7.2.0",
"@babel/preset-env": "^7.0.0",
"babel-plugin-lodash": "^3.3.2",
"cross-env": "^5.1.3",

View File

@@ -1,11 +1,8 @@
import createLogger from '@xen-orchestra/log'
import humanFormat from 'human-format'
import moment from 'moment-timezone'
import { forEach, groupBy, startCase } from 'lodash'
import { forEach, get, startCase } from 'lodash'
import pkg from '../package'
const logger = createLogger('xo:xo-server-backup-reports')
export const configurationSchema = {
type: 'object',
@@ -49,9 +46,6 @@ export const testSchema = {
// ===================================================================
const INDENT = ' '
const UNKNOWN_ITEM = 'Unknown'
const ICON_FAILURE = '🚨'
const ICON_INTERRUPTED = '⚠️'
const ICON_SKIPPED = '⏩'
@@ -66,7 +60,7 @@ const STATUS_ICON = {
}
const DATE_FORMAT = 'dddd, MMMM Do YYYY, h:mm:ss a'
const createDateFormatter = timezone =>
const createDateFormater = timezone =>
timezone !== undefined
? timestamp =>
moment(timestamp)
@@ -92,6 +86,10 @@ const formatSpeed = (bytes, milliseconds) =>
})
: 'N/A'
const logError = e => {
console.error('backup report error:', e)
}
const NO_VMS_MATCH_THIS_PATTERN = 'no VMs match this pattern'
const NO_SUCH_OBJECT_ERROR = 'no such object'
const UNHEALTHY_VDI_CHAIN_ERROR = 'unhealthy VDI chain'
@@ -102,114 +100,40 @@ const isSkippedError = error =>
error.message === UNHEALTHY_VDI_CHAIN_ERROR ||
error.message === NO_SUCH_OBJECT_ERROR
// ===================================================================
const INDENT = ' '
const createGetTemporalDataMarkdown = formatDate => (
start,
end,
nbIndent = 0
) => {
const indent = INDENT.repeat(nbIndent)
const STATUS = ['failure', 'interrupted', 'skipped', 'success']
const TITLE_BY_STATUS = {
failure: n => `## ${n} Failure${n === 1 ? '' : 's'}`,
interrupted: n => `## ${n} Interrupted`,
skipped: n => `## ${n} Skipped`,
success: n => `## ${n} Success${n === 1 ? '' : 'es'}`,
}
const getTemporalDataMarkdown = (end, start, formatDate) => {
const markdown = [`- **Start time**: ${formatDate(start)}`]
const markdown = [`${indent}- **Start time**: ${formatDate(start)}`]
if (end !== undefined) {
markdown.push(`- **End time**: ${formatDate(end)}`)
markdown.push(`${indent}- **End time**: ${formatDate(end)}`)
const duration = end - start
if (duration >= 1) {
markdown.push(`- **Duration**: ${formatDuration(duration)}`)
markdown.push(`${indent}- **Duration**: ${formatDuration(duration)}`)
}
}
return markdown
}
const getWarningsMarkdown = (warnings = []) =>
warnings.map(({ message }) => `- **${ICON_WARNING} ${message}**`)
const getErrorMarkdown = task => {
let message
if (
task.status === 'success' ||
(message = task.result?.message ?? task.result?.code) === undefined
) {
const addWarnings = (text, warnings, nbIndent = 0) => {
if (warnings === undefined) {
return
}
const label = task.status === 'skipped' ? 'Reason' : 'Error'
return `- **${label}**: ${message}`
const indent = INDENT.repeat(nbIndent)
warnings.forEach(({ message }) => {
text.push(`${indent}- **${ICON_WARNING} ${message}**`)
})
}
const MARKDOWN_BY_TYPE = {
pool(task, { formatDate }) {
const { pool, poolMaster = {} } = task.data
const name = pool.name_label || poolMaster.name_label || UNKNOWN_ITEM
return {
body: [
`- **UUID**: ${pool.uuid}`,
...getTemporalDataMarkdown(task.end, task.start, formatDate),
getErrorMarkdown(task),
],
title: `[pool] ${name}`,
}
},
xo(task, { formatDate, jobName }) {
return {
body: [
...getTemporalDataMarkdown(task.end, task.start, formatDate),
getErrorMarkdown(task),
],
title: `[XO] ${jobName}`,
}
},
async remote(task, { formatDate, xo }) {
const id = task.data.id
const name = await xo.getRemote(id).then(
({ name }) => name,
error => {
logger.warn(error)
return UNKNOWN_ITEM
}
)
return {
body: [
`- **ID**: ${id}`,
...getTemporalDataMarkdown(task.end, task.start, formatDate),
getErrorMarkdown(task),
],
title: `[remote] ${name}`,
}
},
}
const getMarkdown = (task, props) =>
MARKDOWN_BY_TYPE[(task.data?.type)]?.(task, props)
const toMarkdown = parts => {
const lines = []
let indentLevel = 0
const helper = part => {
if (typeof part === 'string') {
lines.push(`${INDENT.repeat(indentLevel)}${part}`)
} else if (Array.isArray(part)) {
++indentLevel
part.forEach(helper)
--indentLevel
}
}
helper(parts)
return lines.join('\n')
}
// ===================================================================
class BackupReportsXoPlugin {
constructor(xo) {
this._xo = xo
this._report = this._report.bind(this)
this._report = this._wrapper.bind(this)
}
configure({ toMails, toXmpp }) {
@@ -222,171 +146,72 @@ class BackupReportsXoPlugin {
}
test({ runId }) {
return this._report(runId, undefined, true)
return this._backupNgListener(undefined, undefined, undefined, runId)
}
unload() {
this._xo.removeListener('job:terminated', this._report)
}
async _report(runJobId, { type, status } = {}, force) {
const xo = this._xo
try {
if (type === 'call') {
return this._legacyVmHandler(status)
}
const log = await xo.getBackupNgLogs(runJobId)
if (log === undefined) {
throw new Error(`no log found with runId=${JSON.stringify(runJobId)}`)
}
const reportWhen = log.data.reportWhen
if (
!force &&
(reportWhen === 'never' ||
(reportWhen === 'failure' && log.status === 'success'))
) {
return
}
const [job, schedule] = await Promise.all([
await xo.getJob(log.jobId),
await xo.getSchedule(log.scheduleId).catch(error => {
logger.warn(error)
}),
])
if (job.type === 'backup') {
return this._ngVmHandler(log, job, schedule, force)
} else if (job.type === 'metadataBackup') {
return this._metadataHandler(log, job, schedule, force)
}
throw new Error(`Unknown backup job type: ${job.type}`)
} catch (error) {
logger.warn(error)
_wrapper(status, job, schedule, runJobId) {
if (job.type === 'metadataBackup') {
return
}
return new Promise(resolve =>
resolve(
job.type === 'backup'
? this._backupNgListener(status, job, schedule, runJobId)
: this._listener(status, job, schedule, runJobId)
)
).catch(logError)
}
async _metadataHandler(log, { name: jobName }, schedule, force) {
async _backupNgListener(_1, _2, schedule, runJobId) {
const xo = this._xo
const formatDate = createDateFormatter(schedule?.timezone)
const tasksByStatus = groupBy(log.tasks, 'status')
const n = log.tasks?.length ?? 0
const nSuccesses = tasksByStatus.success?.length ?? 0
if (!force && log.data.reportWhen === 'failure') {
delete tasksByStatus.success
const log = await xo.getBackupNgLogs(runJobId)
if (log === undefined) {
throw new Error(`no log found with runId=${JSON.stringify(runJobId)}`)
}
// header
const markdown = [
`## Global status: ${log.status}`,
'',
`- **Job ID**: ${log.jobId}`,
`- **Job name**: ${jobName}`,
`- **Run ID**: ${log.id}`,
...getTemporalDataMarkdown(log.end, log.start, formatDate),
n !== 0 && `- **Successes**: ${nSuccesses} / ${n}`,
...getWarningsMarkdown(log.warnings),
getErrorMarkdown(log),
]
const nagiosText = []
// body
for (const status of STATUS) {
const tasks = tasksByStatus[status]
if (tasks === undefined) {
continue
}
// tasks header
markdown.push('---', '', TITLE_BY_STATUS[status](tasks.length))
// tasks body
for (const task of tasks) {
const taskMarkdown = await getMarkdown(task, {
formatDate,
jobName: log.jobName,
})
if (taskMarkdown === undefined) {
continue
}
const { title, body } = taskMarkdown
const subMarkdown = [...body, ...getWarningsMarkdown(task.warnings)]
if (task.status !== 'success') {
nagiosText.push(`[${task.status}] ${title}`)
}
for (const subTask of task.tasks ?? []) {
const taskMarkdown = await getMarkdown(subTask, { formatDate, xo })
if (taskMarkdown === undefined) {
continue
}
const icon = STATUS_ICON[subTask.status]
const { title, body } = taskMarkdown
subMarkdown.push([
`- **${title}** ${icon}`,
[...body, ...getWarningsMarkdown(subTask.warnings)],
])
}
markdown.push('', '', `### ${title}`, ...subMarkdown)
}
}
// footer
markdown.push('---', '', `*${pkg.name} v${pkg.version}*`)
return this._sendReport({
subject: `[Xen Orchestra] ${log.status} Metadata backup report for ${
log.jobName
} ${STATUS_ICON[log.status]}`,
markdown: toMarkdown(markdown),
nagiosStatus: log.status === 'success' ? 0 : 2,
nagiosMarkdown:
log.status === 'success'
? `[Xen Orchestra] [Success] Metadata backup report for ${
log.jobName
}`
: `[Xen Orchestra] [${log.status}] Metadata backup report for ${
log.jobName
} - ${nagiosText.join(' ')}`,
})
}
async _ngVmHandler(log, { name: jobName }, schedule, force) {
const xo = this._xo
const { reportWhen, mode } = log.data || {}
if (
reportWhen === 'never' ||
(log.status === 'success' && reportWhen === 'failure')
) {
return
}
const formatDate = createDateFormatter(schedule?.timezone)
if (schedule === undefined) {
schedule = await xo.getSchedule(log.scheduleId)
}
if (log.tasks === undefined) {
const markdown = [
const jobName = (await xo.getJob(log.jobId, 'backup')).name
const formatDate = createDateFormater(schedule.timezone)
const getTemporalDataMarkdown = createGetTemporalDataMarkdown(formatDate)
if (
(log.status === 'failure' || log.status === 'skipped') &&
log.result !== undefined
) {
let markdown = [
`## Global status: ${log.status}`,
'',
`- **Job ID**: ${log.jobId}`,
`- **Run ID**: ${log.id}`,
`- **Run ID**: ${runJobId}`,
`- **mode**: ${mode}`,
...getTemporalDataMarkdown(log.end, log.start, formatDate),
getErrorMarkdown(log),
...getWarningsMarkdown(log.warnings),
'---',
'',
`*${pkg.name} v${pkg.version}*`,
...getTemporalDataMarkdown(log.start, log.end),
`- **Error**: ${log.result.message}`,
]
addWarnings(markdown, log.warnings)
markdown.push('---', '', `*${pkg.name} v${pkg.version}*`)
markdown = markdown.join('\n')
return this._sendReport({
subject: `[Xen Orchestra] ${
log.status
} Backup report for ${jobName} ${STATUS_ICON[log.status]}`,
markdown: toMarkdown(markdown),
markdown,
nagiosStatus: 2,
nagiosMarkdown: `[Xen Orchestra] [${
log.status
@@ -406,7 +231,7 @@ class BackupReportsXoPlugin {
let nSkipped = 0
let nInterrupted = 0
for (const taskLog of log.tasks) {
if (!force && taskLog.status === 'success' && reportWhen === 'failure') {
if (taskLog.status === 'success' && reportWhen === 'failure') {
continue
}
@@ -419,16 +244,16 @@ class BackupReportsXoPlugin {
`### ${vm !== undefined ? vm.name_label : 'VM not found'}`,
'',
`- **UUID**: ${vm !== undefined ? vm.uuid : vmId}`,
...getTemporalDataMarkdown(taskLog.end, taskLog.start, formatDate),
...getWarningsMarkdown(taskLog.warnings),
...getTemporalDataMarkdown(taskLog.start, taskLog.end),
]
addWarnings(text, taskLog.warnings)
const failedSubTasks = []
const snapshotText = []
const srsText = []
const remotesText = []
for (const subTaskLog of taskLog.tasks ?? []) {
for (const subTaskLog of taskLog.tasks || []) {
if (
subTaskLog.message !== 'export' &&
subTaskLog.message !== 'snapshot'
@@ -437,36 +262,29 @@ class BackupReportsXoPlugin {
}
const icon = STATUS_ICON[subTaskLog.status]
const type = subTaskLog.data?.type
const errorMarkdown = getErrorMarkdown(subTaskLog)
const errorMessage = ` - **Error**: ${get(
subTaskLog.result,
'message'
)}`
if (subTaskLog.message === 'snapshot') {
snapshotText.push(`- **Snapshot** ${icon}`, [
...getTemporalDataMarkdown(
subTaskLog.end,
subTaskLog.start,
formatDate
),
])
} else if (type === 'remote') {
snapshotText.push(
`- **Snapshot** ${icon}`,
...getTemporalDataMarkdown(subTaskLog.start, subTaskLog.end, 1)
)
} else if (subTaskLog.data.type === 'remote') {
const id = subTaskLog.data.id
const remote = await xo.getRemote(id).catch(error => {
logger.warn(error)
})
const title = remote !== undefined ? remote.name : `Remote Not found`
remotesText.push(`- **${title}** (${id}) ${icon}`, [
...getTemporalDataMarkdown(
subTaskLog.end,
subTaskLog.start,
formatDate
),
...getWarningsMarkdown(subTaskLog.warnings),
errorMarkdown,
])
const remote = await xo.getRemote(id).catch(() => {})
remotesText.push(
` - **${
remote !== undefined ? remote.name : `Remote Not found`
}** (${id}) ${icon}`,
...getTemporalDataMarkdown(subTaskLog.start, subTaskLog.end, 2)
)
addWarnings(remotesText, subTaskLog.warnings, 2)
if (subTaskLog.status === 'failure') {
failedSubTasks.push(remote !== undefined ? remote.name : id)
remotesText.push('', errorMessage)
}
} else {
const id = subTaskLog.data.id
@@ -476,17 +294,14 @@ class BackupReportsXoPlugin {
} catch (e) {}
const [srName, srUuid] =
sr !== undefined ? [sr.name_label, sr.uuid] : [`SR Not found`, id]
srsText.push(`- **${srName}** (${srUuid}) ${icon}`, [
...getTemporalDataMarkdown(
subTaskLog.end,
subTaskLog.start,
formatDate
),
...getWarningsMarkdown(subTaskLog.warnings),
errorMarkdown,
])
srsText.push(
` - **${srName}** (${srUuid}) ${icon}`,
...getTemporalDataMarkdown(subTaskLog.start, subTaskLog.end, 2)
)
addWarnings(srsText, subTaskLog.warnings, 2)
if (subTaskLog.status === 'failure') {
failedSubTasks.push(sr !== undefined ? sr.name_label : id)
srsText.push('', errorMessage)
}
}
@@ -498,48 +313,53 @@ class BackupReportsXoPlugin {
return
}
const size = operationLog.result?.size
if (size > 0) {
const operationInfoText = []
addWarnings(operationInfoText, operationLog.warnings, 3)
if (operationLog.status === 'success') {
const size = operationLog.result.size
if (operationLog.message === 'merge') {
globalMergeSize += size
} else {
globalTransferSize += size
}
}
operationInfoText.push(
` - **Size**: ${formatSize(size)}`,
` - **Speed**: ${formatSpeed(
size,
operationLog.end - operationLog.start
)}`
)
} else if (get(operationLog.result, 'message') !== undefined) {
operationInfoText.push(
` - **Error**: ${get(operationLog.result, 'message')}`
)
}
const operationText = [
`- **${operationLog.message}** ${STATUS_ICON[operationLog.status]}`,
[
...getTemporalDataMarkdown(
operationLog.end,
operationLog.start,
formatDate
),
size > 0 && `- **Size**: ${formatSize(size)}`,
size > 0 &&
`- **Speed**: ${formatSpeed(
size,
operationLog.end - operationLog.start
)}`,
...getWarningsMarkdown(operationLog.warnings),
getErrorMarkdown(operationLog),
],
]
if (type === 'remote') {
` - **${operationLog.message}** ${
STATUS_ICON[operationLog.status]
}`,
...getTemporalDataMarkdown(operationLog.start, operationLog.end, 3),
...operationInfoText,
].join('\n')
if (get(subTaskLog, 'data.type') === 'remote') {
remotesText.push(operationText)
} else if (type === 'SR') {
remotesText.join('\n')
}
if (get(subTaskLog, 'data.type') === 'SR') {
srsText.push(operationText)
srsText.join('\n')
}
})
}
const subText = [
...snapshotText,
srsText.length !== 0 && `- **SRs**`,
srsText,
remotesText.length !== 0 && `- **Remotes**`,
remotesText,
]
if (srsText.length !== 0) {
srsText.unshift(`- **SRs**`)
}
if (remotesText.length !== 0) {
remotesText.unshift(`- **Remotes**`)
}
const subText = [...snapshotText, '', ...srsText, '', ...remotesText]
if (taskLog.result !== undefined) {
if (taskLog.status === 'skipped') {
++nSkipped
@@ -549,7 +369,8 @@ class BackupReportsXoPlugin {
taskLog.result.message === UNHEALTHY_VDI_CHAIN_ERROR
? UNHEALTHY_VDI_CHAIN_MESSAGE
: taskLog.result.message
}`
}`,
''
)
nagiosText.push(
`[(Skipped) ${vm !== undefined ? vm.name_label : 'undefined'} : ${
@@ -558,7 +379,11 @@ class BackupReportsXoPlugin {
)
} else {
++nFailures
failedVmsText.push(...text, `- **Error**: ${taskLog.result.message}`)
failedVmsText.push(
...text,
`- **Error**: ${taskLog.result.message}`,
''
)
nagiosText.push(
`[(Failed) ${vm !== undefined ? vm.name_label : 'undefined'} : ${
@@ -569,7 +394,7 @@ class BackupReportsXoPlugin {
} else {
if (taskLog.status === 'failure') {
++nFailures
failedVmsText.push(...text, ...subText)
failedVmsText.push(...text, '', '', ...subText, '')
nagiosText.push(
`[${
vm !== undefined ? vm.name_label : 'undefined'
@@ -577,34 +402,37 @@ class BackupReportsXoPlugin {
)
} else if (taskLog.status === 'interrupted') {
++nInterrupted
interruptedVmsText.push(...text, ...subText)
interruptedVmsText.push(...text, '', '', ...subText, '')
nagiosText.push(
`[(Interrupted) ${vm !== undefined ? vm.name_label : 'undefined'}]`
)
} else {
successfulVmsText.push(...text, ...subText)
successfulVmsText.push(...text, '', '', ...subText, '')
}
}
}
const nVms = log.tasks.length
const nSuccesses = nVms - nFailures - nSkipped - nInterrupted
const markdown = [
let markdown = [
`## Global status: ${log.status}`,
'',
`- **Job ID**: ${log.jobId}`,
`- **Run ID**: ${log.id}`,
`- **Run ID**: ${runJobId}`,
`- **mode**: ${mode}`,
...getTemporalDataMarkdown(log.end, log.start, formatDate),
...getTemporalDataMarkdown(log.start, log.end),
`- **Successes**: ${nSuccesses} / ${nVms}`,
globalTransferSize !== 0 &&
`- **Transfer size**: ${formatSize(globalTransferSize)}`,
globalMergeSize !== 0 &&
`- **Merge size**: ${formatSize(globalMergeSize)}`,
...getWarningsMarkdown(log.warnings),
'',
]
if (globalTransferSize !== 0) {
markdown.push(`- **Transfer size**: ${formatSize(globalTransferSize)}`)
}
if (globalMergeSize !== 0) {
markdown.push(`- **Merge size**: ${formatSize(globalMergeSize)}`)
}
addWarnings(markdown, log.warnings)
markdown.push('')
if (nFailures !== 0) {
markdown.push(
'---',
@@ -629,7 +457,7 @@ class BackupReportsXoPlugin {
)
}
if (nSuccesses !== 0 && (force || reportWhen !== 'failure')) {
if (nSuccesses !== 0 && reportWhen !== 'failure') {
markdown.push(
'---',
'',
@@ -640,8 +468,9 @@ class BackupReportsXoPlugin {
}
markdown.push('---', '', `*${pkg.name} v${pkg.version}*`)
markdown = markdown.join('\n')
return this._sendReport({
markdown: toMarkdown(markdown),
markdown,
subject: `[Xen Orchestra] ${log.status} Backup report for ${jobName} ${
STATUS_ICON[log.status]
}`,
@@ -681,9 +510,9 @@ class BackupReportsXoPlugin {
])
}
_legacyVmHandler(status) {
_listener(status) {
const { calls, timezone, error } = status
const formatDate = createDateFormatter(timezone)
const formatDate = createDateFormater(timezone)
if (status.error !== undefined) {
const [globalStatus, icon] =

View File

@@ -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"
},

View File

@@ -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"

View File

@@ -49,12 +49,6 @@ maxTokenValidity = '0.5 year'
# Delay for which backups listing on a remote is cached
listingDebounce = '1 min'
# Helmet handles HTTP security via headers
#
# https://helmetjs.github.io/docs/
#[http.helmet.hsts]
#includeSubDomains = false
[[http.listen]]
port = 80

View File

@@ -1,6 +1,6 @@
{
"name": "xo-server",
"version": "5.40.0",
"version": "5.38.2",
"license": "AGPL-3.0",
"description": "Server part of Xen-Orchestra",
"keywords": [
@@ -117,10 +117,10 @@
"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.1",
"vhd-lib": "^0.6.0",
"ws": "^6.0.0",
"xen-api": "^0.25.1",
"xml2js": "^0.4.19",
@@ -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": {

View File

@@ -193,11 +193,6 @@ create.params = {
optional: true,
},
networkConfig: {
type: 'string',
optional: true,
},
coreOs: {
type: 'boolean',
optional: true,

View File

@@ -93,7 +93,7 @@ async function loadConfiguration() {
function createExpressApp(config) {
const app = createExpress()
app.use(helmet(config.http.helmet))
app.use(helmet())
app.use(compression())

View File

@@ -1,4 +1,3 @@
/* eslint eslint-comments/disable-enable-pair: [error, {allowWholeFile: true}] */
/* eslint-disable camelcase */
import asyncMap from '@xen-orchestra/async-map'
import concurrency from 'limit-concurrency-decorator'

View File

@@ -52,7 +52,6 @@ export default {
coreOs = false,
cloudConfig = undefined,
networkConfig = undefined,
vgpuType = undefined,
gpuGroup = undefined,
@@ -242,16 +241,10 @@ export default {
}
})
if (coreOs) {
await this.createCoreOsCloudInitConfigDrive(vm.$id, srRef, cloudConfig)
} else {
await this.createCloudInitConfigDrive(
vm.$id,
srRef,
cloudConfig,
networkConfig
)
}
const method = coreOs
? 'createCoreOsCloudInitConfigDrive'
: 'createCloudInitConfigDrive'
await this[method](vm.$id, srRef, cloudConfig)
}
// wait for the record with all the VBDs and VIFs

View File

@@ -164,11 +164,11 @@ export default class Jobs {
xo.emit(
'job:terminated',
undefined,
job,
undefined,
// This cast can be removed after merging the PR: https://github.com/vatesfr/xen-orchestra/pull/3209
String(job.runId),
{
type: job.type,
}
String(job.runId)
)
return this.updateJob({ id: job.id, runId: null })
})
@@ -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}.`, {
@@ -319,10 +314,7 @@ export default class Jobs {
true
)
app.emit('job:terminated', runJobId, {
type: job.type,
status,
})
app.emit('job:terminated', status, job, schedule, runJobId)
} catch (error) {
await logger.error(
`The execution of ${id} has failed.`,
@@ -333,9 +325,7 @@ export default class Jobs {
},
true
)
app.emit('job:terminated', runJobId, {
type: job.type,
})
app.emit('job:terminated', undefined, job, schedule, runJobId)
throw error
} finally {
this.updateJob({ id, runId: null })::ignoreErrors()

View File

@@ -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(

View File

@@ -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": [
@@ -25,11 +25,11 @@
},
"dependencies": {
"child-process-promise": "^2.0.3",
"core-js": "^3.0.0",
"core-js": "3.0.0",
"pipette": "^0.9.3",
"promise-toolbox": "^0.12.1",
"tmp": "^0.1.0",
"vhd-lib": "^0.6.1"
"tmp": "^0.0.33",
"vhd-lib": "^0.6.0"
},
"devDependencies": {
"@babel/cli": "^7.0.0",

View File

@@ -1,7 +1,7 @@
{
"private": false,
"name": "xo-web",
"version": "5.40.1",
"version": "5.38.0",
"license": "AGPL-3.0",
"description": "Web interface client for Xen-Orchestra",
"keywords": [
@@ -58,7 +58,7 @@
"chartist-plugin-legend": "^0.6.1",
"chartist-plugin-tooltip": "0.0.11",
"classnames": "^2.2.3",
"complex-matcher": "^0.6.0",
"complex-matcher": "^0.5.0",
"cookies-js": "^1.2.2",
"copy-to-clipboard": "^3.0.8",
"d3": "^5.0.0",
@@ -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",

View File

@@ -21,37 +21,6 @@ const showAvailableTemplateVars = () =>
</ul>
)
const showNetworkConfigInfo = () =>
alert(
_('newVmNetworkConfigLabel'),
<div>
<p>
{_('newVmNetworkConfigInfo', {
noCloudDatasourceLink: (
<a
href='https://cloudinit.readthedocs.io/en/latest/topics/datasources/nocloud.html#datasource-nocloud'
target='_blank'
>
{_('newVmNoCloudDatasource')}
</a>
),
})}
</p>
<p>
{_('newVmNetworkConfigDocLink', {
networkConfigDocLink: (
<a
href='https://cloudinit.readthedocs.io/en/latest/topics/network-config-format-v1.html'
target='_blank'
>
{_('newVmNetworkConfigDoc')}
</a>
),
})}
</p>
</div>
)
export const AvailableTemplateVars = () => (
<Tooltip content={_('availableTemplateVarsInfo')}>
<a
@@ -64,26 +33,5 @@ export const AvailableTemplateVars = () => (
</Tooltip>
)
export const NetworkConfigInfo = () => (
<Tooltip content={_('newVmNetworkConfigTooltip')}>
<a
className='text-info'
style={{ cursor: 'pointer' }}
onClick={showNetworkConfigInfo}
>
<Icon icon='info' />
</a>
</Tooltip>
)
export const DEFAULT_CLOUD_CONFIG_TEMPLATE =
'#cloud-config\n#hostname: {name}%\n#ssh_authorized_keys:\n# - ssh-rsa <myKey>\n#packages:\n# - htop\n'
// SOURCE: https://cloudinit.readthedocs.io/en/latest/topics/network-config-format-v1.html
export const DEFAULT_NETWORK_CONFIG_TEMPLATE = `#network:
# version: 1
# config:
# - type: physical
# name: eth0
# subnets:
# - type: dhcp`

View File

@@ -150,17 +150,6 @@ class Editable extends Component {
render() {
const { state, props } = this
const { error, saving } = state
const ErrorTooltip = props =>
props.error != null && (
<span>
{' '}
<Tooltip content={error}>
<Icon icon='error' />
</Tooltip>
</span>
)
if (!state.editing) {
const { onUndo, previous } = state
@@ -195,11 +184,12 @@ class Editable extends Component {
) : (
success
))}
<ErrorTooltip error={error} />
</span>
)
}
const { error, saving } = state
return (
<span>
{this._renderEdition()}
@@ -209,7 +199,14 @@ class Editable extends Component {
<Icon icon='loading' />
</span>
)}
<ErrorTooltip error={error} />
{error != null && (
<span>
{' '}
<Tooltip content={error}>
<Icon icon='error' />
</Tooltip>
</span>
)}
</span>
)
}
@@ -343,9 +340,7 @@ class SimpleSelect_ extends Editable {
return this.state.value === undefined ? this.props.value : this.state.value
}
_onChange = value => {
this.setState({ value }, this._save)
}
_onChange = value => this.setState({ value }, this._save)
_renderDisplay() {
const { children, optionRenderer, value } = this.props
@@ -455,9 +450,7 @@ export class XoSelect extends Editable {
)
}
_onChange = object => {
this.setState({ value: object }, object && this._save)
}
_onChange = object => this.setState({ value: object }, object && this._save)
_renderEdition() {
const { saving, xoType, ...props } = this.props

View File

@@ -3411,7 +3411,7 @@ export default {
// Original text: "This version is not bundled with any support nor updates. Use it with caution for critical tasks."
disclaimerText3:
'Esta versión no está creada para recibir soporte ni actualizaciones. Úsala con precaución.',
'Esta versión no está creada para recibir soporte ni actualizaciones. Úsala con precaución para tareas críticas.',
// Original text: "Connect PIF"
connectPif: 'Conectar PIF',

View File

@@ -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…',
@@ -3497,7 +3496,7 @@ export default {
// Original text: "This version is not bundled with any support nor updates. Use it with caution for critical tasks."
disclaimerText3:
"Cette version n'est fournie avec aucun support ni aucune mise à jour. Utilisez-la avec précaution.",
"Cette version n'est fournie avec aucun support ni aucune mise à jour. Soyez prudent en cas d'utilisation pour des tâches importantes.",
// Original text: "Connect PIF"
connectPif: 'Connecter la PIF',

View File

@@ -3248,7 +3248,7 @@ export default {
// Original text: "This version is not bundled with any support nor updates. Use it with caution for critical tasks."
disclaimerText3:
'This Verzió is not bundled with any support nor upDates. Use it with caution.',
'This Verzió is not bundled with any support nor upDates. Use it with caution for critical tasks.',
// Original text: "Connect PIF"
connectPif: 'Csatlakozás PIF',

View File

@@ -2958,7 +2958,7 @@ export default {
// Original text: "This version is not bundled with any support nor updates. Use it with caution for critical tasks."
disclaimerText3:
'This version is not bundled with any support nor updates. Use it with caution.',
'This version is not bundled with any support nor updates. Use it with caution for critical tasks.',
// Original text: "Connect PIF"
connectPif: 'Connect PIF',

View File

@@ -2944,7 +2944,7 @@ export default {
// Original text: "This version is not bundled with any support nor updates. Use it with caution for critical tasks."
disclaimerText3:
'Esta versão não está vinculada a qualquer tipo de suporte nem atualizações. Use-a com cuidado.',
'Esta versão não está vinculada a qualquer tipo de suporte nem atualizações. Use-a com cuidado em se tratando de tarefas críticas.',
// Original text: "Connect PIF"
connectPif: 'Conectar PIF',

View File

@@ -4294,7 +4294,7 @@ export default {
// Original text: "This version is not bundled with any support nor updates. Use it with caution for critical tasks."
disclaimerText3:
'Bu sürüm herhangi bir destek veya güncellemeyle birlikte verilmez. Dikkatli kullanın.',
'Bu sürüm herhangi bir destek veya güncellemeyle birlikte verilmez. Kritik görevler için dikkatli kullanın.',
// Original text: "Connect PIF"
connectPif: "PIF'e bağlan",

View File

@@ -2236,7 +2236,7 @@ export default {
disclaimerText2: '如果你是一个公司,建议使用我们的设备结合专业的支持',
// Original text: "This version is not bundled with any support nor updates. Use it with caution for critical tasks."
disclaimerText3: '这个版本没有绑定任何支持或更新,请谨慎使用',
disclaimerText3: '这个版本没有绑定任何支持或更新,在紧急任务下,请谨慎使用',
// Original text: "Connect PIF"
connectPif: '连接物理网卡',

View File

@@ -46,7 +46,6 @@ const messages = {
metadata: 'Metadata',
chooseBackup: 'Choose a backup',
clickToShowError: 'Click to show error',
backupJobs: 'Backup jobs',
// ----- Modals -----
alertOk: 'OK',
@@ -1051,12 +1050,12 @@ const messages = {
importVdi: 'Import VDI content',
importVdiNoFile: 'No file selected',
selectVdiMessage: 'Drop VHD file here',
srsNotOnSameHost:
'The SRs must either be shared or on the same host for the VM to be able to start.',
useQuotaWarning:
'Creating this disk will use the disk space quota from the resource set {resourceSet} ({spaceLeft} left)',
notEnoughSpaceInResourceSet:
'Not enough space in resource set {resourceSet} ({spaceLeft} left)',
warningVdiSr:
"The VDIs' SRs must either be shared or on the same host for the VM to be able to start.",
// ----- VM network tab -----
vifCreateDeviceButton: 'New device',
@@ -1285,9 +1284,6 @@ const messages = {
spaceLeftTooltip: '{used}% used ({free} left)',
// ----- New VM -----
createVmModalTitle: 'Create VM',
createVmModalWarningMessage:
"You're about to use a large amount of resources available on the resource set. Are you sure you want to continue?",
newVmCreateNewVmOn: 'Create a new VM on {select}',
newVmCreateNewVmNoPermission: 'You have no permission to create a VM',
newVmInfoPanel: 'Infos',
@@ -1350,15 +1346,6 @@ const messages = {
newVmHideAdvanced: 'Hide advanced settings',
newVmShare: 'Share this VM',
newVmSrsNotOnSameHost: 'The SRs must either be on the same host or shared',
newVmNetworkConfigLabel: 'Network config',
newVmNetworkConfigInfo:
'Network configuration is only compatible with the {noCloudDatasourceLink}.',
newVmNetworkConfigDocLink: 'See {networkConfigDocLink}.',
newVmNetworkConfigTooltip:
'Click here to get more information about network config',
newVmUserConfigLabel: 'User config',
newVmNoCloudDatasource: 'NoCloud datasource',
newVmNetworkConfigDoc: 'Network config documentation',
// ----- Self -----
resourceSets: 'Resource sets',
@@ -1621,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*',
@@ -1868,7 +1855,7 @@ const messages = {
disclaimerText2:
"If you are a company, it's better to use it with our appliance + pro support included:",
disclaimerText3:
'This version is not bundled with any support nor updates. Use it with caution.',
'This version is not bundled with any support nor updates. Use it with caution for critical tasks.',
notRegisteredDisclaimerInfo:
'You are not registered. Your XOA may not be up to date.',
notRegisteredDisclaimerCreateAccount: 'Click here to create an account.',

View File

@@ -409,7 +409,8 @@ const xoItemToRender = {
<span>
<strong>
<Icon icon='resource-set' /> {resourceSet.name}
</strong>
</strong>{' '}
({resourceSet.id})
</span>
),
sshKey: key => (

View File

@@ -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,
@@ -275,13 +273,13 @@ const _getPermissionsPredicate = invoke(() => {
}
)
return (state, props, useResourceSet) => {
return state => {
const user = getUser(state)
if (!user) {
return false
}
if (user.permission === 'admin' || useResourceSet) {
if (user.permission === 'admin') {
return // No predicate means no filtering.
}

View File

@@ -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>

View File

@@ -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
@@ -177,7 +177,7 @@ export default class MigrateVmModalBody extends BaseComponent {
host,
intraPool,
mapVifsNetworks: undefined,
migrationNetworkId: undefined,
migrationNetwork: undefined,
targetSrs: {},
})
return
@@ -210,7 +210,7 @@ export default class MigrateVmModalBody extends BaseComponent {
intraPool,
mapVifsNetworks: defaultNetworksForVif,
migrationNetworkId: defaultMigrationNetworkId,
targetSrs: { mainSr: pools[host.$pool].default_SR },
targetSrs: {},
})
}

View File

@@ -14,7 +14,6 @@ const MODES = {
__proto__: null,
compression: 'full',
fullInterval: 'delta',
}
const getSettingsWithNonDefaultValue = (mode, settings) =>

View File

@@ -340,7 +340,7 @@ const Overview = () => (
<div>
<Card>
<CardHeader>
<Icon icon='backup' /> {_('backupJobs')}
<Icon icon='schedule' /> {_('backupSchedules')}
</CardHeader>
<CardBlock>
<JobsTable />

View File

@@ -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>

View File

@@ -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}>

View File

@@ -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),
},

View File

@@ -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(),

View File

@@ -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()} />

View File

@@ -20,7 +20,6 @@ import { Card, CardHeader, CardBlock } from 'card'
import {
ceil,
debounce,
escapeRegExp,
filter,
find,
forEach,
@@ -748,10 +747,7 @@ export default class Home extends Component {
filter,
'tags',
new ComplexMatcher.Or(
map(
tags,
tag => new ComplexMatcher.RegExp(`^${escapeRegExp(tag.id)}$`)
)
map(tags, tag => new ComplexMatcher.String(tag.id))
)
)
: ComplexMatcher.setPropertyClause(filter, 'tags', undefined)
@@ -830,10 +826,7 @@ export default class Home extends Component {
break
case 'NAV_UP':
this.setState({
highlighted:
this.state.highlighted > 0
? this.state.highlighted - 1
: items.length - 1,
highlighted: (this.state.highlighted - 1) % items.length || 0,
})
break
case 'SELECT':

View File

@@ -3,6 +3,7 @@ import Component from 'base-component'
import Icon from 'icon'
import React from 'react'
import Tooltip from 'tooltip'
import Upgrade from 'xoa-upgrade'
import { Container, Row, Col } from 'grid'
import { Toggle } from 'form'
import { fetchHostStats } from 'xo'
@@ -104,7 +105,7 @@ export default class HostStats extends Component {
return !stats ? (
<p>No stats.</p>
) : (
) : process.env.XOA_PLAN > 2 ? (
<Container>
<Row>
<Col mediumSize={5}>
@@ -178,6 +179,10 @@ export default class HostStats extends Component {
</Col>
</Row>
</Container>
) : (
<Container>
<Upgrade place='hostStats' available={3} />
</Container>
)
}
}

View File

@@ -222,17 +222,6 @@ export default class XoApp extends Component {
</Link>
</div>
)}
{+process.env.XOA_PLAN === 5 && (
<div className='alert alert-danger mb-0'>
<a
href='https://xen-orchestra.com/#!/xoa?pk_campaign=xo_source_banner'
rel='noopener noreferrer'
target='_blank'
>
{_('disclaimerText3')}
</a>
</div>
)}
<div style={CONTAINER_STYLE}>
<Shortcuts
name='XoApp'

View File

@@ -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'

View File

@@ -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,170 +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()],
// work-around to not let defined handle the component as a safety function
() => 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 }) => ({
@@ -328,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),
@@ -369,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 }) => [
@@ -419,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
@@ -438,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>

View File

@@ -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 => {
&nbsp;
</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>

View File

@@ -17,10 +17,7 @@ import Wizard, { Section } from 'wizard'
import {
AvailableTemplateVars,
DEFAULT_CLOUD_CONFIG_TEMPLATE,
DEFAULT_NETWORK_CONFIG_TEMPLATE,
NetworkConfigInfo,
} from 'cloud-config'
import { confirm } from 'modal'
import { Container, Row, Col } from 'grid'
import { injectIntl } from 'react-intl'
import {
@@ -82,21 +79,19 @@ import {
getCoresPerSocketPossibilities,
generateReadableRandomString,
resolveIds,
resolveResourceSet,
} from 'utils'
import {
createFilter,
createFinder,
createSelector,
createGetObject,
createGetObjectsOfType,
createSelector,
getIsPoolAdmin,
getResolvedResourceSets,
getUser,
} from 'selectors'
import styles from './index.css'
const MULTIPLICAND = 2
const NB_VMS_MIN = 2
const NB_VMS_MAX = 100
@@ -226,39 +221,25 @@ class Vif extends BaseComponent {
resourceSets: subscribeResourceSets,
user: subscribeCurrentUser,
})
@connectStore(() => {
const getIsAdmin = createSelector(
@connectStore(() => ({
isAdmin: createSelector(
getUser,
user => user && user.permission === 'admin'
)
const getNetworks = createGetObjectsOfType('network').sort()
const getPool = createGetObject((_, props) => props.location.query.pool)
const getPools = createGetObjectsOfType('pool')
const getSrs = createGetObjectsOfType('SR')
const getTemplates = createGetObjectsOfType('VM-template').sort()
const getUserSshKeys = createSelector(
),
isPoolAdmin: getIsPoolAdmin,
networks: createGetObjectsOfType('network').sort(),
pool: createGetObject((_, props) => props.location.query.pool),
pools: createGetObjectsOfType('pool'),
templates: createGetObjectsOfType('VM-template').sort(),
userSshKeys: createSelector(
(_, props) => {
const user = props.user
return user && user.preferences && user.preferences.sshKeys
},
keys => keys
)
return (state, props) => ({
isAdmin: getIsAdmin(state, props),
isPoolAdmin: getIsPoolAdmin(state, props),
networks: getNetworks(state, props),
pool: getPool(state, props),
pools: getPools(state, props),
resolvedResourceSets: getResolvedResourceSets(
state,
props,
props.pool === undefined // to get objects as a self user
),
srs: getSrs(state, props),
templates: getTemplates(state, props),
userSshKeys: getUserSshKeys(state, props),
})
})
),
srs: createGetObjectsOfType('SR'),
}))
@injectIntl
export default class NewVm extends BaseComponent {
static contextTypes = {
@@ -278,24 +259,19 @@ export default class NewVm extends BaseComponent {
this._reset()
}
_getResourceSet = createFinder(
() => this.props.resourceSets,
createSelector(
() => this.props.location.query.resourceSet,
resourceSetId => resourceSet =>
resourceSet !== undefined ? resourceSetId === resourceSet.id : undefined
)
)
_getResourceSet = () => {
const {
location: {
query: { resourceSet: resourceSetId },
},
resourceSets,
} = this.props
return resourceSets && find(resourceSets, ({ id }) => id === resourceSetId)
}
_getResolvedResourceSet = createFinder(
() => this.props.resolvedResourceSets,
createSelector(
this._getResourceSet,
resourceSet =>
resourceSet !== undefined
? resolvedResourceSet => resolvedResourceSet.id === resourceSet.id
: false
)
_getResolvedResourceSet = createSelector(
this._getResourceSet,
resolveResourceSet
)
// Utils -----------------------------------------------------------------------
@@ -348,29 +324,6 @@ export default class NewVm extends BaseComponent {
})
}
_selfCreate = () => {
const {
CPUs,
VDIs,
existingDisks,
memoryDynamicMax,
template,
} = this.state.state
const disksSize = sumBy(VDIs, 'size') + sumBy(existingDisks, 'size')
const templateDisksSize = sumBy(template.template_info.disks, 'size')
const templateMemoryDynamicMax = template.memory.dynamic[1]
const templateVcpusMax = template.CPUs.max
return CPUs > MULTIPLICAND * templateVcpusMax ||
memoryDynamicMax > MULTIPLICAND * templateMemoryDynamicMax ||
disksSize > MULTIPLICAND * templateDisksSize
? confirm({
title: _('createVmModalTitle'),
body: _('createVmModalWarningMessage'),
}).then(this._create)
: this._create()
}
_create = () => {
const { state } = this.state
let installation
@@ -400,7 +353,6 @@ export default class NewVm extends BaseComponent {
let cloudConfig
let cloudConfigs
let networkConfig
if (state.installMethod !== 'noConfigDrive') {
if (state.installMethod === 'SSH') {
const format = hostname =>
@@ -437,12 +389,8 @@ export default class NewVm extends BaseComponent {
replacer(state, i + +seqStart)
)
}
networkConfig = defined(
state.networkConfig,
DEFAULT_NETWORK_CONFIG_TEMPLATE
)
}
} else if (this._isCoreOs()) {
} else if (state.template.name_label === 'CoreOS') {
cloudConfig = state.cloudConfig
if (state.multipleVms) {
cloudConfigs = new Array(state.nbVms).fill(state.cloudConfig)
@@ -495,8 +443,7 @@ export default class NewVm extends BaseComponent {
bootAfterCreate: state.bootAfterCreate,
share: state.share,
cloudConfig,
networkConfig: this._isCoreOs() ? undefined : networkConfig,
coreOs: this._isCoreOs(),
coreOs: state.template.name_label === 'CoreOS',
tags: state.tags,
vgpuType: get(() => state.vgpuType.id),
gpuGroup: get(() => state.vgpuType.gpuGroup),
@@ -596,7 +543,7 @@ export default class NewVm extends BaseComponent {
}),
})
if (this._isCoreOs()) {
if (template.name_label === 'CoreOS') {
getCloudInitConfig(template.id).then(
cloudConfig =>
this._setState({ cloudConfig, coreOsDefaultTemplateError: false }),
@@ -746,11 +693,6 @@ export default class NewVm extends BaseComponent {
getCoresPerSocketPossibilities
)
_isCoreOs = createSelector(
() => this.state.template,
template => template && template.name_label === 'CoreOS'
)
// On change -------------------------------------------------------------------
_onChangeSshKeys = keys =>
@@ -973,7 +915,7 @@ export default class NewVm extends BaseComponent {
) || !this._availableResources()
}
form='vmCreation'
handler={pool === undefined ? this._selfCreate : this._create}
handler={this._create}
icon='new-vm-create'
redirectOnSuccess={this._getRedirectionUrl}
>
@@ -1130,7 +1072,6 @@ export default class NewVm extends BaseComponent {
const {
cloudConfig,
customConfig,
networkConfig,
installIso,
installMethod,
installNetwork,
@@ -1222,39 +1163,13 @@ export default class NewVm extends BaseComponent {
</span>
</LineItem>
<br />
<LineItem>
<Item>
<label className='text-muted'>
{_('newVmUserConfigLabel')}
<br />
<DebounceTextarea
className='form-control'
disabled={installMethod !== 'customConfig'}
onChange={this._linkState('customConfig')}
rows={7}
value={defined(customConfig, DEFAULT_CLOUD_CONFIG_TEMPLATE)}
/>
</label>
</Item>
{!this._isCoreOs() && (
<Item>
<label className='text-muted'>
{_('newVmNetworkConfigLabel')} <NetworkConfigInfo />
<br />
<DebounceTextarea
className='form-control'
disabled={installMethod !== 'customConfig'}
onChange={this._linkState('networkConfig')}
rows={7}
value={defined(
networkConfig,
DEFAULT_NETWORK_CONFIG_TEMPLATE
)}
/>
</label>
</Item>
)}
</LineItem>
<DebounceTextarea
className='form-control'
disabled={installMethod !== 'customConfig'}
onChange={this._linkState('customConfig')}
rows={7}
value={defined(customConfig, DEFAULT_CLOUD_CONFIG_TEMPLATE)}
/>
</SectionContent>
) : (
<SectionContent>
@@ -1334,7 +1249,7 @@ export default class NewVm extends BaseComponent {
)}
</SectionContent>
)}
{this._isCoreOs() && (
{template.name_label === 'CoreOS' && (
<div>
<label>{_('newVmCloudConfig')}</label>{' '}
{!coreOsDefaultTemplateError ? (

View File

@@ -4,6 +4,7 @@ import getEventValue from 'get-event-value'
import Icon from 'icon'
import React from 'react'
import Tooltip from 'tooltip'
import Upgrade from 'xoa-upgrade'
import { Container, Row, Col } from 'grid'
import { Toggle } from 'form'
import { fetchHostStats } from 'xo'
@@ -95,91 +96,100 @@ export default class PoolStats extends Component {
useCombinedValues,
} = this.state
return stats ? (
<Container>
<Row>
<Col mediumSize={5}>
<div className='form-group'>
<Tooltip content={_('useStackedValuesOnStats')}>
<Toggle
value={useCombinedValues}
onChange={this.linkState('useCombinedValues')}
/>
</Tooltip>
</div>
</Col>
<Col mediumSize={1}>
{selectStatsLoading && (
<div className='text-xs-right'>
<Icon icon='loading' size={2} />
return process.env.XOA_PLAN > 2 ? (
stats ? (
<Container>
<Row>
<Col mediumSize={5}>
<div className='form-group'>
<Tooltip content={_('useStackedValuesOnStats')}>
<Toggle
value={useCombinedValues}
onChange={this.linkState('useCombinedValues')}
/>
</Tooltip>
</div>
)}
</Col>
<Col mediumSize={6}>
<div className='btn-tab'>
<select
className='form-control'
onChange={this._handleSelectStats}
defaultValue={granularity}
>
{_('statLastTenMinutes', message => (
<option value='seconds'>{message}</option>
))}
{_('statLastTwoHours', message => (
<option value='minutes'>{message}</option>
))}
{_('statLastWeek', message => (
<option value='hours'>{message}</option>
))}
{_('statLastYear', message => (
<option value='days'>{message}</option>
))}
</select>
</div>
</Col>
</Row>
<Row>
<Col mediumSize={6}>
<h5 className='text-xs-center'>
<Icon icon='cpu' /> {_('statsCpu')}
</h5>
<PoolCpuLineChart addSumSeries={useCombinedValues} data={stats} />
</Col>
<Col mediumSize={6}>
<h5 className='text-xs-center'>
<Icon icon='memory' /> {_('statsMemory')}
</h5>
<PoolMemoryLineChart
addSumSeries={useCombinedValues}
data={stats}
/>
</Col>
</Row>
<br />
<hr />
<Row>
<Col mediumSize={6}>
<h5 className='text-xs-center'>
<Icon icon='network' /> {_('statsNetwork')}
</h5>
{/* key: workaround that unmounts and re-mounts the chart to make sure the legend updates when toggling "stacked values"
</Col>
<Col mediumSize={1}>
{selectStatsLoading && (
<div className='text-xs-right'>
<Icon icon='loading' size={2} />
</div>
)}
</Col>
<Col mediumSize={6}>
<div className='btn-tab'>
<select
className='form-control'
onChange={this._handleSelectStats}
defaultValue={granularity}
>
{_('statLastTenMinutes', message => (
<option value='seconds'>{message}</option>
))}
{_('statLastTwoHours', message => (
<option value='minutes'>{message}</option>
))}
{_('statLastWeek', message => (
<option value='hours'>{message}</option>
))}
{_('statLastYear', message => (
<option value='days'>{message}</option>
))}
</select>
</div>
</Col>
</Row>
<Row>
<Col mediumSize={6}>
<h5 className='text-xs-center'>
<Icon icon='cpu' /> {_('statsCpu')}
</h5>
<PoolCpuLineChart addSumSeries={useCombinedValues} data={stats} />
</Col>
<Col mediumSize={6}>
<h5 className='text-xs-center'>
<Icon icon='memory' /> {_('statsMemory')}
</h5>
<PoolMemoryLineChart
addSumSeries={useCombinedValues}
data={stats}
/>
</Col>
</Row>
<br />
<hr />
<Row>
<Col mediumSize={6}>
<h5 className='text-xs-center'>
<Icon icon='network' /> {_('statsNetwork')}
</h5>
{/* key: workaround that unmounts and re-mounts the chart to make sure the legend updates when toggling "stacked values"
FIXME: remove key prop once this issue is fixed: https://github.com/CodeYellowBV/chartist-plugin-legend/issues/5 */}
<PoolPifLineChart
key={useCombinedValues ? 'stacked' : 'unstacked'}
addSumSeries={useCombinedValues}
data={stats}
/>
</Col>
<Col mediumSize={6}>
<h5 className='text-xs-center'>
<Icon icon='disk' /> {_('statLoad')}
</h5>
<PoolLoadLineChart addSumSeries={useCombinedValues} data={stats} />
</Col>
</Row>
</Container>
<PoolPifLineChart
key={useCombinedValues ? 'stacked' : 'unstacked'}
addSumSeries={useCombinedValues}
data={stats}
/>
</Col>
<Col mediumSize={6}>
<h5 className='text-xs-center'>
<Icon icon='disk' /> {_('statLoad')}
</h5>
<PoolLoadLineChart
addSumSeries={useCombinedValues}
data={stats}
/>
</Col>
</Row>
</Container>
) : (
<p>{_('poolNoStats')}</p>
)
) : (
<p>{_('poolNoStats')}</p>
<Container>
<Upgrade place='hostStats' available={3} />
</Container>
)
}
}

View File

@@ -670,10 +670,7 @@ class ResourceSet extends Component {
return (
<div className='mb-1' ref={this._autoExpand}>
<Collapse
buttonText={`${resourceSet.name} (${resourceSet.id})`}
defaultOpen={autoExpand}
>
<Collapse buttonText={resourceSet.name} defaultOpen={autoExpand}>
<ul className='list-group'>
{this.state.editionMode ? (
<Edit

View File

@@ -3,6 +3,7 @@ import Component from 'base-component'
import Icon from 'icon'
import React from 'react'
import Tooltip from 'tooltip'
import Upgrade from 'xoa-upgrade'
import { Container, Row, Col } from 'grid'
import { fetchSrStats } from 'xo'
import { get } from 'lodash'
@@ -84,79 +85,81 @@ export default class SrStats extends Component {
return data === undefined ? (
<span>{_('srNoStats')}</span>
) : (
<Container>
<Row>
<Col mediumSize={5}>
<div className='form-group'>
<Tooltip content={_('useStackedValuesOnStats')}>
<Toggle
value={useCombinedValues}
onChange={this.linkState('useCombinedValues')}
/>
</Tooltip>
</div>
</Col>
<Col mediumSize={1}>
{selectStatsLoading && (
<div className='text-xs-right'>
<Icon icon='loading' size={2} />
<Upgrade place='srStats' available={3}>
<Container>
<Row>
<Col mediumSize={5}>
<div className='form-group'>
<Tooltip content={_('useStackedValuesOnStats')}>
<Toggle
value={useCombinedValues}
onChange={this.linkState('useCombinedValues')}
/>
</Tooltip>
</div>
)}
</Col>
<Col mediumSize={6}>
<div className='btn-tab'>
<select
className='form-control'
onChange={this._onGranularityChange}
defaultValue={granularity}
>
{_('statLastTenMinutes', message => (
<option value='seconds'>{message}</option>
))}
{_('statLastTwoHours', message => (
<option value='minutes'>{message}</option>
))}
{_('statLastWeek', message => (
<option value='hours'>{message}</option>
))}
{_('statLastYear', message => (
<option value='days'>{message}</option>
))}
</select>
</div>
</Col>
</Row>
<Row>
<Col mediumSize={6}>
<h5 className='text-xs-center'>
<Icon icon='iops' size={1} /> {_('statsIops')}
</h5>
<IopsLineChart addSumSeries={useCombinedValues} data={data} />
</Col>
<Col mediumSize={6}>
<h5 className='text-xs-center'>
<Icon icon='disk' size={1} /> {_('statsIoThroughput')}
</h5>
<IoThroughputChart addSumSeries={useCombinedValues} data={data} />
</Col>
</Row>
<br />
<hr />
<Row>
<Col mediumSize={6}>
<h5 className='text-xs-center'>
<Icon icon='latency' size={1} /> {_('statsLatency')}
</h5>
<LatencyChart addSumSeries={useCombinedValues} data={data} />
</Col>
<Col mediumSize={6}>
<h5 className='text-xs-center'>
<Icon icon='iowait' size={1} /> {_('statsIowait')}
</h5>
<IowaitChart addSumSeries={useCombinedValues} data={data} />
</Col>
</Row>
</Container>
</Col>
<Col mediumSize={1}>
{selectStatsLoading && (
<div className='text-xs-right'>
<Icon icon='loading' size={2} />
</div>
)}
</Col>
<Col mediumSize={6}>
<div className='btn-tab'>
<select
className='form-control'
onChange={this._onGranularityChange}
defaultValue={granularity}
>
{_('statLastTenMinutes', message => (
<option value='seconds'>{message}</option>
))}
{_('statLastTwoHours', message => (
<option value='minutes'>{message}</option>
))}
{_('statLastWeek', message => (
<option value='hours'>{message}</option>
))}
{_('statLastYear', message => (
<option value='days'>{message}</option>
))}
</select>
</div>
</Col>
</Row>
<Row>
<Col mediumSize={6}>
<h5 className='text-xs-center'>
<Icon icon='iops' size={1} /> {_('statsIops')}
</h5>
<IopsLineChart addSumSeries={useCombinedValues} data={data} />
</Col>
<Col mediumSize={6}>
<h5 className='text-xs-center'>
<Icon icon='disk' size={1} /> {_('statsIoThroughput')}
</h5>
<IoThroughputChart addSumSeries={useCombinedValues} data={data} />
</Col>
</Row>
<br />
<hr />
<Row>
<Col mediumSize={6}>
<h5 className='text-xs-center'>
<Icon icon='latency' size={1} /> {_('statsLatency')}
</h5>
<LatencyChart addSumSeries={useCombinedValues} data={data} />
</Col>
<Col mediumSize={6}>
<h5 className='text-xs-center'>
<Icon icon='iowait' size={1} /> {_('statsIowait')}
</h5>
<IowaitChart addSumSeries={useCombinedValues} data={data} />
</Col>
</Row>
</Container>
</Upgrade>
)
}
}

View File

@@ -257,7 +257,6 @@ const parseBootOrder = bootOrder => {
})
class NewDisk extends Component {
static propTypes = {
checkSr: PropTypes.func.isRequired,
onClose: PropTypes.func,
vm: PropTypes.object.isRequired,
}
@@ -300,12 +299,6 @@ class NewDisk extends Component {
resourceSet => get(resourceSet, 'limits.disk.available')
)
_checkSr = createSelector(
() => this.props.checkSr,
() => this.state.sr,
(check, sr) => check(sr)
)
render() {
const { vm, isAdmin } = this.props
const { formatMessage } = this.props.intl
@@ -377,13 +370,6 @@ class NewDisk extends Component {
</ActionButton>
</span>
</fieldset>
{!this._checkSr() && (
<div>
<span className='text-danger'>
<Icon icon='alarm' /> {_('warningVdiSr')}
</span>
</div>
)}
{resourceSet != null &&
diskLimit != null &&
(diskLimit < size ? (
@@ -406,12 +392,8 @@ class NewDisk extends Component {
}
}
@connectStore({
srs: createGetObjectsOfType('SR'),
})
class AttachDisk extends Component {
static propTypes = {
checkSr: PropTypes.func.isRequired,
onClose: PropTypes.func,
vbds: PropTypes.array.isRequired,
vm: PropTypes.object.isRequired,
@@ -436,13 +418,6 @@ class AttachDisk extends Component {
_selectVdi = vdi => this.setState({ vdi })
_checkSr = createSelector(
() => this.props.checkSr,
() => this.props.srs,
() => this.state.vdi,
(check, srs, vdi) => check(srs[vdi.$SR])
)
_addVdi = () => {
const { vm, vbds, onClose = noop } = this.props
const { bootable, readOnly, vdi } = this.state
@@ -494,13 +469,6 @@ class AttachDisk extends Component {
{_('vbdAttach')}
</ActionButton>
</span>
{!this._checkSr() && (
<div>
<span className='text-danger'>
<Icon icon='alarm' /> {_('warningVdiSr')}
</span>
</div>
)}
</fieldset>
)}
</form>
@@ -641,7 +609,6 @@ class BootOrder extends Component {
class MigrateVdiModalBody extends Component {
static propTypes = {
checkSr: PropTypes.func.isRequired,
pool: PropTypes.string.isRequired,
}
@@ -654,12 +621,6 @@ class MigrateVdiModalBody extends Component {
poolId => createCompareContainers(poolId)
)
_checkSr = createSelector(
() => this.props.checkSr,
() => this.state.sr,
(check, sr) => check(sr)
)
render() {
return (
<Container>
@@ -682,15 +643,6 @@ class MigrateVdiModalBody extends Component {
</label>
</Col>
</SingleLineRow>
{!this._checkSr() && (
<SingleLineRow>
<Col>
<span className='text-danger'>
<Icon icon='alarm' /> {_('warningVdiSr')}
</span>
</Col>
</SingleLineRow>
)}
</Container>
)
}
@@ -711,13 +663,11 @@ export default class TabDisks extends Component {
}
}
_getVdiSrs = createSelector(
() => this.props.vdis,
createCollectionWrapper(vdis => sortedUniq(map(vdis, '$SR').sort()))
)
_areSrsOnSameHost = createSelector(
this._getVdiSrs,
createSelector(
() => this.props.vdis,
createCollectionWrapper(vdis => sortedUniq(map(vdis, '$SR').sort()))
),
() => this.props.srs,
(vdiSrs, srs) => {
if (some(vdiSrs, srId => srs[srId] === undefined)) {
@@ -761,12 +711,7 @@ export default class TabDisks extends Component {
_migrateVdi = vdi => {
return confirm({
title: _('vdiMigrate'),
body: (
<MigrateVdiModalBody
checkSr={this._getCheckSr()}
pool={this.props.vm.$pool}
/>
),
body: <MigrateVdiModalBody pool={this.props.vm.$pool} />,
}).then(({ sr, migrateAll }) => {
if (!sr) {
return error(_('vdiMigrateNoSr'), _('vdiMigrateNoSrMessage'))
@@ -791,37 +736,6 @@ export default class TabDisks extends Component {
isAdmin || (resourceSet == null && isVmAdmin)
)
_getRequiredHost = createSelector(
this._areSrsOnSameHost,
this._getVdiSrs,
() => this.props.srs,
(areSrsOnSameHost, vdiSrs, srs) => {
if (!areSrsOnSameHost) {
return
}
let container
let sr
forEach(vdiSrs, srId => {
sr = srs[srId]
if (sr !== undefined && !isSrShared(sr)) {
container = sr.$container
return false
}
})
return container
}
)
_getCheckSr = createSelector(
this._getRequiredHost,
requiredHost => sr =>
sr === undefined ||
isSrShared(sr) ||
requiredHost === undefined ||
sr.$container === requiredHost
)
_getVbdsByVdi = createSelector(
() => this.props.vdis,
() => this.props.vbds,
@@ -903,18 +817,13 @@ export default class TabDisks extends Component {
<Col>
{newDisk && (
<div>
<NewDisk
checkSr={this._getCheckSr()}
vm={vm}
onClose={this._toggleNewDisk}
/>
<NewDisk vm={vm} onClose={this._toggleNewDisk} />
<hr />
</div>
)}
{attachDisk && (
<div>
<AttachDisk
checkSr={this._getCheckSr()}
vm={vm}
vbds={vbds}
onClose={this._toggleAttachDisk}
@@ -934,7 +843,7 @@ export default class TabDisks extends Component {
{!this._areSrsOnSameHost() && (
<div>
<span className='text-danger'>
<Icon icon='alarm' /> {_('warningVdiSr')}
<Icon icon='alarm' /> {_('srsNotOnSameHost')}
</span>
</div>
)}

View File

@@ -3,6 +3,7 @@ import Component from 'base-component'
import Icon from 'icon'
import React from 'react'
import Tooltip from 'tooltip'
import Upgrade from 'xoa-upgrade'
import { fetchVmStats } from 'xo'
import { Toggle } from 'form'
import { injectIntl } from 'react-intl'
@@ -104,7 +105,7 @@ export default injectIntl(
return !stats ? (
<p>No stats.</p>
) : (
) : process.env.XOA_PLAN > 2 ? (
<Container>
<Row>
<Col mediumSize={6}>
@@ -176,6 +177,10 @@ export default injectIntl(
</Col>
</Row>
</Container>
) : (
<Container>
<Upgrade place='vmStats' available={3} />
</Container>
)
}
}

View File

@@ -1,53 +0,0 @@
#!/bin/sh
if [ $# -eq 0 ] || [ "$1" = "-h" ]
then
echo "Usage: $0 <package> <version>"
exit
fi
# move to the root repo dir
cd "$(dirname "$(which "$0")")/.."
pkg=$1
case "$pkg" in
@*/*)
cd "$pkg"
;;
*)
cd "packages/$pkg"
esac
npm version "$2"
git add --patch
newVersion=$(node --eval 'console.log(require("./package.json").version)')
cd -
git grep -Flz "\"$pkg\":" '**/package.json' | xargs -0 node -e '
const {readFileSync, writeFileSync} = require("fs")
const [, pkg, version, ...files] = process.argv
const updateDep = deps => {
if (deps !== undefined && pkg in deps) {
deps[pkg] = "^" + version
}
}
files.forEach(file => {
const data = JSON.parse(readFileSync(file, "utf8"))
updateDep(data.dependencies)
updateDep(data.devDependencies)
updateDep(data.peerDependencies)
writeFileSync(file, JSON.stringify(data, null, 2) + "\n")
})
' "$pkg" "$newVersion"
git add --patch
git commit -m "feat($pkg): $newVersion"
git tag $pkg-v$newVersion

1573
yarn.lock

File diff suppressed because it is too large Load Diff