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
52 changed files with 1590 additions and 2517 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,31 +1,5 @@
# ChangeLog
## **next** (2019-04-11)
### 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))
- [VM migration] Auto select default SR and collapse optional actions [#3326](https://github.com/vatesfr/xen-orchestra/issues/3326) (PR [#4121](https://github.com/vatesfr/xen-orchestra/pull/4121))
- [Metadata backup] Logs [#4005](https://github.com/vatesfr/xen-orchestra/issues/4005) (PR [#4014](https://github.com/vatesfr/xen-orchestra/pull/4014))
### Bug fixes
- [Continuous Replication] Fix VHD size guess for empty files [#4105](https://github.com/vatesfr/xen-orchestra/issues/4105) (PR [#4107](https://github.com/vatesfr/xen-orchestra/pull/4107))
- [Backup NG] Only display full backup interval in case of a delta backup (PR [#4125](https://github.com/vatesfr/xen-orchestra/pull/4107))
- [Dashboard/Health] fix 'an error has occurred' on the storage state table [#4128](https://github.com/vatesfr/xen-orchestra/issues/4128) (PR [#4132](https://github.com/vatesfr/xen-orchestra/pull/4132))
- [Menu] XOA: Fixed empty slot when menu is collapsed [#4012](https://github.com/vatesfr/xen-orchestra/issues/4012) (PR [#4068](https://github.com/vatesfr/xen-orchestra/pull/4068)
### Released packages
- xo-vmdk-to-vhd v0.1.7
- vhd-lib v0.6.1
- xo-server v5.39.0
- xo-web v5.39.1
## **5.33.1** (2019-04-04)
### Bug fix

View File

@@ -2,21 +2,17 @@
### Enhancements
- [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))
- [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))
- [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))
- [Backup reports] Support metadata backups (PR [#4084](https://github.com/vatesfr/xen-orchestra/pull/4084))
- [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))
- [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))
- [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))
- [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
- [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))
- [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-backup-reports v0.16.0
- complex-matcher v0.6.0
- xo-server v5.40.0
- xo-web v5.40.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.39.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

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

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*',

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

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

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

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

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