Compare commits
1 Commits
xo-web-v5.
...
xen-api-ev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3facbcda99 |
@@ -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',
|
||||
},
|
||||
|
||||
@@ -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": {
|
||||
|
||||
41
CHANGELOG.md
41
CHANGELOG.md
@@ -1,46 +1,5 @@
|
||||
# ChangeLog
|
||||
|
||||
## **next** (2019-04-26)
|
||||
|
||||
### Highlights
|
||||
|
||||
- [Self/New VM] Add network config box to custom cloud-init [#3872](https://github.com/vatesfr/xen-orchestra/issues/3872) (PR [#4150](https://github.com/vatesfr/xen-orchestra/pull/4150))
|
||||
- [Metadata backup] Detailed logs [#4005](https://github.com/vatesfr/xen-orchestra/issues/4005) (PR [#4014](https://github.com/vatesfr/xen-orchestra/pull/4014))
|
||||
- [Backup reports] Support metadata backups (PR [#4084](https://github.com/vatesfr/xen-orchestra/pull/4084))
|
||||
- [VM migration] Auto select default SR and collapse optional actions [#3326](https://github.com/vatesfr/xen-orchestra/issues/3326) (PR [#4121](https://github.com/vatesfr/xen-orchestra/pull/4121))
|
||||
- Unlock basic stats on all editions [#4166](https://github.com/vatesfr/xen-orchestra/issues/4166) (PR [#4172](https://github.com/vatesfr/xen-orchestra/pull/4172))
|
||||
|
||||
### Enhancements
|
||||
|
||||
- [Settings/remotes] Expose mount options field for SMB [#4063](https://github.com/vatesfr/xen-orchestra/issues/4063) (PR [#4067](https://github.com/vatesfr/xen-orchestra/pull/4067))
|
||||
- [Backup/Schedule] Add warning regarding DST when you add a schedule [#4042](https://github.com/vatesfr/xen-orchestra/issues/4042) (PR [#4056](https://github.com/vatesfr/xen-orchestra/pull/4056))
|
||||
- [Import] Avoid blocking the UI when dropping a big OVA file on the UI (PR [#4018](https://github.com/vatesfr/xen-orchestra/pull/4018))
|
||||
- [Backup NG/Overview] Make backup list title clearer [#4111](https://github.com/vatesfr/xen-orchestra/issues/4111) (PR [#4129](https://github.com/vatesfr/xen-orchestra/pull/4129))
|
||||
- [Dashboard] Hide "Report" section for non-admins [#4123](https://github.com/vatesfr/xen-orchestra/issues/4123) (PR [#4126](https://github.com/vatesfr/xen-orchestra/pull/4126))
|
||||
- [Self/New VM] Display confirmation modal when user will use a large amount of resources [#4044](https://github.com/vatesfr/xen-orchestra/issues/4044) (PR [#4127](https://github.com/vatesfr/xen-orchestra/pull/4127))
|
||||
- [VDI migration, New disk] Warning when SR host is different from the other disks [#3911](https://github.com/vatesfr/xen-orchestra/issues/3911) (PR [#4035](https://github.com/vatesfr/xen-orchestra/pull/4035))
|
||||
- [Attach disk] Display warning message when VDI SR is on different host from the other disks [#3911](https://github.com/vatesfr/xen-orchestra/issues/3911) (PR [#4117](https://github.com/vatesfr/xen-orchestra/pull/4117))
|
||||
- [Editable] Notify user when editable undo fails [#3799](https://github.com/vatesfr/xen-orchestra/issues/3799) (PR [#4150](https://github.com/vatesfr/xen-orchestra/pull/4157))
|
||||
- [XO] Add banner for sources users to clarify support conditions [#4165](https://github.com/vatesfr/xen-orchestra/issues/4165) (PR [#4167](https://github.com/vatesfr/xen-orchestra/pull/4167))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- [Continuous Replication] Fix VHD size guess for empty files [#4105](https://github.com/vatesfr/xen-orchestra/issues/4105) (PR [#4107](https://github.com/vatesfr/xen-orchestra/pull/4107))
|
||||
- [Backup NG] Only display full backup interval in case of a delta backup (PR [#4125](https://github.com/vatesfr/xen-orchestra/pull/4107))
|
||||
- [Dashboard/Health] fix 'an error has occurred' on the storage state table [#4128](https://github.com/vatesfr/xen-orchestra/issues/4128) (PR [#4132](https://github.com/vatesfr/xen-orchestra/pull/4132))
|
||||
- [Menu] XOA: Fixed empty slot when menu is collapsed [#4012](https://github.com/vatesfr/xen-orchestra/issues/4012) (PR [#4068](https://github.com/vatesfr/xen-orchestra/pull/4068)
|
||||
- [Self/New VM] Fix missing templates when refreshing page [#3265](https://github.com/vatesfr/xen-orchestra/issues/3265) (PR [#3565](https://github.com/vatesfr/xen-orchestra/pull/3565))
|
||||
- [Home] No more false positives when select Tag on Home page [#4087](https://github.com/vatesfr/xen-orchestra/issues/4087) (PR [#4112](https://github.com/vatesfr/xen-orchestra/pull/4112))
|
||||
|
||||
### Released packages
|
||||
|
||||
- xo-server-backup-reports v0.16.0
|
||||
- complex-matcher v0.6.0
|
||||
- xo-vmdk-to-vhd v0.1.7
|
||||
- vhd-lib v0.6.1
|
||||
- xo-server v5.40.0
|
||||
- xo-web v5.40.0
|
||||
|
||||
## **5.33.1** (2019-04-04)
|
||||
|
||||
### Bug fix
|
||||
|
||||
@@ -2,9 +2,17 @@
|
||||
|
||||
### Enhancements
|
||||
|
||||
- [Settings/remotes] Expose mount options field for SMB [#4063](https://github.com/vatesfr/xen-orchestra/issues/4063) (PR [#4067](https://github.com/vatesfr/xen-orchestra/pull/4067))
|
||||
- [Backup/Schedule] Add warning regarding DST when you add a schedule [#4042](https://github.com/vatesfr/xen-orchestra/issues/4042) (PR [#4056](https://github.com/vatesfr/xen-orchestra/pull/4056))
|
||||
- [Import] Avoid blocking the UI when dropping a big OVA file on the UI (PR [#4018](https://github.com/vatesfr/xen-orchestra/pull/4018))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- [Continuous Replication] Fix VHD size guess for empty files [#4105](https://github.com/vatesfr/xen-orchestra/issues/4105) (PR [#4107](https://github.com/vatesfr/xen-orchestra/pull/4107))
|
||||
|
||||
### Released packages
|
||||
|
||||
- xo-server v5.41.0
|
||||
- xo-web v5.41.0
|
||||
- xo-vmdk-to-vhd v0.1.7
|
||||
- vhd-lib v0.6.1
|
||||
- xo-server v5.39.0
|
||||
- xo-web v5.39.0
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "complex-matcher",
|
||||
"version": "0.6.0",
|
||||
"version": "0.5.0",
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
"keywords": [],
|
||||
|
||||
@@ -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 []
|
||||
}
|
||||
|
||||
@@ -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'],
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -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/",
|
||||
|
||||
@@ -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/",
|
||||
|
||||
@@ -33,7 +33,6 @@
|
||||
"node": ">=6"
|
||||
},
|
||||
"dependencies": {
|
||||
"bind-property-descriptor": "^1.0.0",
|
||||
"blocked": "^1.2.1",
|
||||
"debug": "^4.0.1",
|
||||
"event-to-promise": "^0.8.0",
|
||||
|
||||
8
packages/xen-api/src/_MultiCounter.js
Normal file
8
packages/xen-api/src/_MultiCounter.js
Normal file
@@ -0,0 +1,8 @@
|
||||
const handler = {
|
||||
get(target, property) {
|
||||
const value = target[property]
|
||||
return value !== undefined ? value : 0
|
||||
},
|
||||
}
|
||||
|
||||
export const create = () => new Proxy({ __proto__: null }, handler)
|
||||
@@ -9,7 +9,6 @@ import minimist from 'minimist'
|
||||
import pw from 'pw'
|
||||
import { asCallback, fromCallback } from 'promise-toolbox'
|
||||
import { filter, find, isArray } from 'lodash'
|
||||
import { getBoundPropertyDescriptor } from 'bind-property-descriptor'
|
||||
import { start as createRepl } from 'repl'
|
||||
|
||||
import { createClient } from './'
|
||||
@@ -26,20 +25,6 @@ function askPassword(prompt = 'Password: ') {
|
||||
})
|
||||
}
|
||||
|
||||
const { getPrototypeOf, ownKeys } = Reflect
|
||||
function getAllBoundDescriptors(object) {
|
||||
const descriptors = { __proto__: null }
|
||||
let current = object
|
||||
do {
|
||||
ownKeys(current).forEach(key => {
|
||||
if (!(key in descriptors)) {
|
||||
descriptors[key] = getBoundPropertyDescriptor(current, key, object)
|
||||
}
|
||||
})
|
||||
} while ((current = getPrototypeOf(current)) !== null)
|
||||
return descriptors
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const usage = 'Usage: xen-api <url> [<user> [<password>]]'
|
||||
@@ -93,17 +78,11 @@ const main = async args => {
|
||||
const repl = createRepl({
|
||||
prompt: `${xapi._humanId}> `,
|
||||
})
|
||||
repl.context.xapi = xapi
|
||||
|
||||
{
|
||||
const ctx = repl.context
|
||||
ctx.xapi = xapi
|
||||
|
||||
ctx.diff = (a, b) => console.log('%s', diff(a, b))
|
||||
ctx.find = predicate => find(xapi.objects.all, predicate)
|
||||
ctx.findAll = predicate => filter(xapi.objects.all, predicate)
|
||||
|
||||
Object.defineProperties(ctx, getAllBoundDescriptors(xapi))
|
||||
}
|
||||
repl.context.diff = (a, b) => console.log('%s', diff(a, b))
|
||||
repl.context.find = predicate => find(xapi.objects.all, predicate)
|
||||
repl.context.findAll = predicate => filter(xapi.objects.all, predicate)
|
||||
|
||||
// Make the REPL waits for promise completion.
|
||||
repl.eval = (evaluate => (cmd, context, filename, cb) => {
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
pTimeout,
|
||||
} from 'promise-toolbox'
|
||||
|
||||
import * as MultiCounter from './_MultiCounter'
|
||||
import autoTransport from './transports/auto'
|
||||
import coalesceCalls from './_coalesceCalls'
|
||||
import debug from './_debug'
|
||||
@@ -34,7 +35,7 @@ const EVENT_TIMEOUT = 60
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const { defineProperties, defineProperty, freeze, keys: getKeys } = Object
|
||||
const { defineProperties, freeze, keys: getKeys } = Object
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
@@ -99,6 +100,7 @@ export class Xapi extends EventEmitter {
|
||||
this._sessionId = undefined
|
||||
this._status = DISCONNECTED
|
||||
|
||||
this._counter = MultiCounter.create()
|
||||
this._debounce = opts.debounce ?? 200
|
||||
this._objects = new Collection()
|
||||
this._objectsByRef = { __proto__: null }
|
||||
@@ -773,6 +775,10 @@ export class Xapi extends EventEmitter {
|
||||
this._objects.set(object.$id, object)
|
||||
objectsByRef[ref] = object
|
||||
|
||||
if (prev === undefined) {
|
||||
++this._counter[type]
|
||||
}
|
||||
|
||||
if (type === 'pool') {
|
||||
this._pool = object
|
||||
|
||||
@@ -785,10 +791,6 @@ export class Xapi extends EventEmitter {
|
||||
}
|
||||
})
|
||||
} else if (type === 'task') {
|
||||
if (prev === undefined) {
|
||||
++this._nTasks
|
||||
}
|
||||
|
||||
const taskWatchers = this._taskWatchers
|
||||
const taskWatcher = taskWatchers[ref]
|
||||
if (taskWatcher !== undefined) {
|
||||
@@ -820,6 +822,7 @@ export class Xapi extends EventEmitter {
|
||||
}
|
||||
|
||||
async _refreshCachedRecords(types) {
|
||||
const counter = this._counter
|
||||
const toRemoveByType = { __proto__: null }
|
||||
types.forEach(type => {
|
||||
toRemoveByType[type] = new Set()
|
||||
@@ -851,8 +854,15 @@ export class Xapi extends EventEmitter {
|
||||
this._removeRecordFromCache(type, ref)
|
||||
})
|
||||
|
||||
if (type === 'task') {
|
||||
this._nTasks = refs.length
|
||||
const count = refs.length
|
||||
if (counter[type] !== count) {
|
||||
console.warn(
|
||||
'_refreshCachedRecords(%s): xapi=%d != local=%d',
|
||||
type,
|
||||
count,
|
||||
counter[type]
|
||||
)
|
||||
counter[type] = count
|
||||
}
|
||||
} catch (error) {
|
||||
// there is nothing ideal to do here, do not interrupt event
|
||||
@@ -873,9 +883,7 @@ export class Xapi extends EventEmitter {
|
||||
this._objects.unset(object.$id)
|
||||
delete byRefs[ref]
|
||||
|
||||
if (type === 'task') {
|
||||
--this._nTasks
|
||||
}
|
||||
--this._counter[type]
|
||||
}
|
||||
|
||||
const taskWatchers = this._taskWatchers
|
||||
@@ -927,6 +935,16 @@ export class Xapi extends EventEmitter {
|
||||
this._resolveObjectsFetched()
|
||||
this._resolveObjectsFetched = undefined
|
||||
|
||||
const IGNORED_TYPES = {
|
||||
__proto__: null,
|
||||
message: true,
|
||||
role: true,
|
||||
session: true,
|
||||
user: true,
|
||||
VBD_metrics: true,
|
||||
VIF_metrics: true,
|
||||
}
|
||||
|
||||
// event loop
|
||||
const debounce = this._debounce
|
||||
while (true) {
|
||||
@@ -959,10 +977,25 @@ export class Xapi extends EventEmitter {
|
||||
fromToken = result.token
|
||||
this._processEvents(result.events)
|
||||
|
||||
// detect and fix disappearing tasks (e.g. when toolstack restarts)
|
||||
if (result.valid_ref_counts.task !== this._nTasks) {
|
||||
await this._refreshCachedRecords(['task'])
|
||||
}
|
||||
// detect and fix desynchronized records
|
||||
const localCounts = this._counter
|
||||
const xapiCounts = result.valid_ref_counts
|
||||
await this._refreshCachedRecords(
|
||||
types.filter(type => {
|
||||
if (type in IGNORED_TYPES) {
|
||||
return false
|
||||
}
|
||||
|
||||
// XAPI uses lowercased types in events, but this may change, so we
|
||||
// handle both
|
||||
let xapiCount = xapiCounts[type]
|
||||
if (xapiCount === undefined) {
|
||||
xapiCount = xapiCounts[type.toLowerCase()]
|
||||
}
|
||||
|
||||
return localCounts[type] !== xapiCount
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1023,23 +1056,17 @@ export class Xapi extends EventEmitter {
|
||||
|
||||
const getObjectByRef = ref => this._objectsByRef[ref]
|
||||
|
||||
Record = defineProperty(
|
||||
function(ref, data) {
|
||||
defineProperties(this, {
|
||||
$id: { value: data.uuid ?? ref },
|
||||
$ref: { value: ref },
|
||||
$xapi: { value: xapi },
|
||||
})
|
||||
for (let i = 0; i < nFields; ++i) {
|
||||
const field = fields[i]
|
||||
this[field] = data[field]
|
||||
}
|
||||
},
|
||||
'name',
|
||||
{
|
||||
value: type,
|
||||
Record = function(ref, data) {
|
||||
defineProperties(this, {
|
||||
$id: { value: data.uuid ?? ref },
|
||||
$ref: { value: ref },
|
||||
$xapi: { value: xapi },
|
||||
})
|
||||
for (let i = 0; i < nFields; ++i) {
|
||||
const field = fields[i]
|
||||
this[field] = data[field]
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const getters = { $pool: getPool }
|
||||
const props = { $type: type }
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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] =
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
"node": ">=6"
|
||||
},
|
||||
"dependencies": {
|
||||
"nodemailer": "^6.1.0",
|
||||
"nodemailer": "^5.0.0",
|
||||
"nodemailer-markdown": "^1.0.1",
|
||||
"promise-toolbox": "^0.12.1"
|
||||
},
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
"@xen-orchestra/cron": "^1.0.3",
|
||||
"@xen-orchestra/log": "^0.1.4",
|
||||
"handlebars": "^4.0.6",
|
||||
"html-minifier": "^4.0.0",
|
||||
"html-minifier": "^3.5.8",
|
||||
"human-format": "^0.10.0",
|
||||
"lodash": "^4.17.4",
|
||||
"promise-toolbox": "^0.12.1"
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -193,11 +193,6 @@ create.params = {
|
||||
optional: true,
|
||||
},
|
||||
|
||||
networkConfig: {
|
||||
type: 'string',
|
||||
optional: true,
|
||||
},
|
||||
|
||||
coreOs: {
|
||||
type: 'boolean',
|
||||
optional: true,
|
||||
|
||||
@@ -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())
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// @flow
|
||||
import asyncMap from '@xen-orchestra/async-map'
|
||||
import createLogger from '@xen-orchestra/log'
|
||||
import defer from 'golike-defer'
|
||||
import { fromEvent, ignoreErrors } from 'promise-toolbox'
|
||||
|
||||
import debounceWithKey from '../_pDebounceWithKey'
|
||||
@@ -24,14 +25,9 @@ const METADATA_BACKUP_JOB_TYPE = 'metadataBackup'
|
||||
|
||||
const compareTimestamp = (a, b) => a.timestamp - b.timestamp
|
||||
|
||||
const DEFAULT_RETENTION = 0
|
||||
|
||||
type ReportWhen = 'always' | 'failure' | 'never'
|
||||
|
||||
type Settings = {|
|
||||
reportWhen?: ReportWhen,
|
||||
retentionPoolMetadata?: number,
|
||||
retentionXoMetadata?: number,
|
||||
retentionPoolMetadata?: number,
|
||||
|}
|
||||
|
||||
type MetadataBackupJob = {
|
||||
@@ -51,22 +47,6 @@ const createSafeReaddir = (handler, methodName) => (path, options) =>
|
||||
return []
|
||||
})
|
||||
|
||||
const deleteOldBackups = (handler, dir, retention, handleError) =>
|
||||
handler.list(dir).then(list => {
|
||||
list.sort()
|
||||
list = list
|
||||
.filter(timestamp => /^\d{8}T\d{6}Z$/.test(timestamp))
|
||||
.slice(0, -retention)
|
||||
return Promise.all(
|
||||
list.map(timestamp => {
|
||||
const backupDir = `${dir}/${timestamp}`
|
||||
return handler
|
||||
.rmtree(backupDir)
|
||||
.catch(error => handleError(error, backupDir))
|
||||
})
|
||||
)
|
||||
}, handleError)
|
||||
|
||||
// metadata.json
|
||||
//
|
||||
// {
|
||||
@@ -96,14 +76,10 @@ const deleteOldBackups = (handler, dir, retention, handleError) =>
|
||||
//
|
||||
// Task logs emitted in a metadata backup execution:
|
||||
//
|
||||
// job.start(data: { reportWhen: ReportWhen })
|
||||
// job.start
|
||||
// ├─ task.start(data: { type: 'pool', id: string, pool: <Pool />, poolMaster: <Host /> })
|
||||
// │ ├─ task.start(data: { type: 'remote', id: string })
|
||||
// │ │ └─ task.end
|
||||
// │ └─ task.end
|
||||
// ├─ task.start(data: { type: 'xo' })
|
||||
// │ ├─ task.start(data: { type: 'remote', id: string })
|
||||
// │ │ └─ task.end
|
||||
// │ └─ task.end
|
||||
// └─ job.end
|
||||
export default class metadataBackup {
|
||||
@@ -156,286 +132,6 @@ export default class metadataBackup {
|
||||
})
|
||||
}
|
||||
|
||||
async _backupXo({ handlers, job, logger, retention, runJobId, schedule }) {
|
||||
const app = this._app
|
||||
|
||||
const timestamp = Date.now()
|
||||
const taskId = logger.notice(`Starting XO metadata backup. (${job.id})`, {
|
||||
data: {
|
||||
type: 'xo',
|
||||
},
|
||||
event: 'task.start',
|
||||
parentId: runJobId,
|
||||
})
|
||||
|
||||
try {
|
||||
const scheduleDir = `${DIR_XO_CONFIG_BACKUPS}/${schedule.id}`
|
||||
const dir = `${scheduleDir}/${safeDateFormat(timestamp)}`
|
||||
|
||||
const data = JSON.stringify(await app.exportConfig(), null, 2)
|
||||
const fileName = `${dir}/data.json`
|
||||
|
||||
const metadata = JSON.stringify(
|
||||
{
|
||||
jobId: job.id,
|
||||
jobName: job.name,
|
||||
scheduleId: schedule.id,
|
||||
scheduleName: schedule.name,
|
||||
timestamp,
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
const metaDataFileName = `${dir}/metadata.json`
|
||||
|
||||
await asyncMap(handlers, async (handler, remoteId) => {
|
||||
const subTaskId = logger.notice(
|
||||
`Starting XO metadata backup for the remote (${remoteId}). (${
|
||||
job.id
|
||||
})`,
|
||||
{
|
||||
data: {
|
||||
id: remoteId,
|
||||
type: 'remote',
|
||||
},
|
||||
event: 'task.start',
|
||||
parentId: taskId,
|
||||
}
|
||||
)
|
||||
|
||||
try {
|
||||
await Promise.all([
|
||||
handler.outputFile(fileName, data),
|
||||
handler.outputFile(metaDataFileName, metadata),
|
||||
])
|
||||
|
||||
await deleteOldBackups(
|
||||
handler,
|
||||
scheduleDir,
|
||||
retention,
|
||||
(error, backupDir) => {
|
||||
logger.warning(
|
||||
backupDir !== undefined
|
||||
? `unable to delete the folder ${backupDir}`
|
||||
: `unable to list backups for the remote (${remoteId})`,
|
||||
{
|
||||
event: 'task.warning',
|
||||
taskId: subTaskId,
|
||||
data: {
|
||||
error,
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
logger.notice(
|
||||
`Backuping XO metadata for the remote (${remoteId}) is a success. (${
|
||||
job.id
|
||||
})`,
|
||||
{
|
||||
event: 'task.end',
|
||||
status: 'success',
|
||||
taskId: subTaskId,
|
||||
}
|
||||
)
|
||||
} catch (error) {
|
||||
await handler.rmtree(dir).catch(error => {
|
||||
logger.warning(`unable to delete the folder ${dir}`, {
|
||||
event: 'task.warning',
|
||||
taskId: subTaskId,
|
||||
data: {
|
||||
error,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
logger.error(
|
||||
`Backuping XO metadata for the remote (${remoteId}) has failed. (${
|
||||
job.id
|
||||
})`,
|
||||
{
|
||||
event: 'task.end',
|
||||
result: serializeError(error),
|
||||
status: 'failure',
|
||||
taskId: subTaskId,
|
||||
}
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
logger.notice(`Backuping XO metadata is a success. (${job.id})`, {
|
||||
event: 'task.end',
|
||||
status: 'success',
|
||||
taskId,
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error(`Backuping XO metadata has failed. (${job.id})`, {
|
||||
event: 'task.end',
|
||||
result: serializeError(error),
|
||||
status: 'failure',
|
||||
taskId,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async _backupPool(
|
||||
poolId,
|
||||
{ cancelToken, handlers, job, logger, retention, runJobId, schedule, xapi }
|
||||
) {
|
||||
const poolMaster = await xapi
|
||||
.getRecord('host', xapi.pool.master)
|
||||
::ignoreErrors()
|
||||
const timestamp = Date.now()
|
||||
const taskId = logger.notice(
|
||||
`Starting metadata backup for the pool (${poolId}). (${job.id})`,
|
||||
{
|
||||
data: {
|
||||
id: poolId,
|
||||
pool: xapi.pool,
|
||||
poolMaster,
|
||||
type: 'pool',
|
||||
},
|
||||
event: 'task.start',
|
||||
parentId: runJobId,
|
||||
}
|
||||
)
|
||||
|
||||
try {
|
||||
const poolDir = `${DIR_XO_POOL_METADATA_BACKUPS}/${schedule.id}/${poolId}`
|
||||
const dir = `${poolDir}/${safeDateFormat(timestamp)}`
|
||||
|
||||
// TODO: export the metadata only once then split the stream between remotes
|
||||
const stream = await xapi.exportPoolMetadata(cancelToken)
|
||||
const fileName = `${dir}/data`
|
||||
|
||||
const metadata = JSON.stringify(
|
||||
{
|
||||
jobId: job.id,
|
||||
jobName: job.name,
|
||||
pool: xapi.pool,
|
||||
poolMaster,
|
||||
scheduleId: schedule.id,
|
||||
scheduleName: schedule.name,
|
||||
timestamp,
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
const metaDataFileName = `${dir}/metadata.json`
|
||||
|
||||
await asyncMap(handlers, async (handler, remoteId) => {
|
||||
const subTaskId = logger.notice(
|
||||
`Starting metadata backup for the pool (${poolId}) for the remote (${remoteId}). (${
|
||||
job.id
|
||||
})`,
|
||||
{
|
||||
data: {
|
||||
id: remoteId,
|
||||
type: 'remote',
|
||||
},
|
||||
event: 'task.start',
|
||||
parentId: taskId,
|
||||
}
|
||||
)
|
||||
|
||||
let outputStream
|
||||
try {
|
||||
await Promise.all([
|
||||
(async () => {
|
||||
outputStream = await handler.createOutputStream(fileName)
|
||||
|
||||
// 'readable-stream/pipeline' not call the callback when an error throws
|
||||
// from the readable stream
|
||||
stream.pipe(outputStream)
|
||||
return fromEvent(stream, 'end').catch(error => {
|
||||
if (error.message !== 'aborted') {
|
||||
throw error
|
||||
}
|
||||
})
|
||||
})(),
|
||||
handler.outputFile(metaDataFileName, metadata),
|
||||
])
|
||||
|
||||
await deleteOldBackups(
|
||||
handler,
|
||||
poolDir,
|
||||
retention,
|
||||
(error, backupDir) => {
|
||||
logger.warning(
|
||||
backupDir !== undefined
|
||||
? `unable to delete the folder ${backupDir}`
|
||||
: `unable to list backups for the remote (${remoteId})`,
|
||||
{
|
||||
event: 'task.warning',
|
||||
taskId: subTaskId,
|
||||
data: {
|
||||
error,
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
logger.notice(
|
||||
`Backuping pool metadata (${poolId}) for the remote (${remoteId}) is a success. (${
|
||||
job.id
|
||||
})`,
|
||||
{
|
||||
event: 'task.end',
|
||||
status: 'success',
|
||||
taskId: subTaskId,
|
||||
}
|
||||
)
|
||||
} catch (error) {
|
||||
if (outputStream !== undefined) {
|
||||
outputStream.destroy()
|
||||
}
|
||||
await handler.rmtree(dir).catch(error => {
|
||||
logger.warning(`unable to delete the folder ${dir}`, {
|
||||
event: 'task.warning',
|
||||
taskId: subTaskId,
|
||||
data: {
|
||||
error,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
logger.error(
|
||||
`Backuping pool metadata (${poolId}) for the remote (${remoteId}) has failed. (${
|
||||
job.id
|
||||
})`,
|
||||
{
|
||||
event: 'task.end',
|
||||
result: serializeError(error),
|
||||
status: 'failure',
|
||||
taskId: subTaskId,
|
||||
}
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
logger.notice(
|
||||
`Backuping pool metadata (${poolId}) is a success. (${job.id})`,
|
||||
{
|
||||
event: 'task.end',
|
||||
status: 'success',
|
||||
taskId,
|
||||
}
|
||||
)
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`Backuping pool metadata (${poolId}) has failed. (${job.id})`,
|
||||
{
|
||||
event: 'task.end',
|
||||
result: serializeError(error),
|
||||
status: 'failure',
|
||||
taskId,
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
async _executor({
|
||||
cancelToken,
|
||||
job: job_,
|
||||
@@ -459,103 +155,199 @@ export default class metadataBackup {
|
||||
throw new Error('no metadata mode found')
|
||||
}
|
||||
|
||||
let { retentionXoMetadata, retentionPoolMetadata } =
|
||||
job.settings[schedule.id] || {}
|
||||
const app = this._app
|
||||
const { retentionXoMetadata, retentionPoolMetadata } =
|
||||
job?.settings[schedule.id] || {}
|
||||
|
||||
// it also replaces null retentions introduced by the commit
|
||||
// https://github.com/vatesfr/xen-orchestra/commit/fea5117ed83b58d3a57715b32d63d46e3004a094#diff-c02703199db2a4c217943cf8e02b91deR40
|
||||
if (retentionXoMetadata == null) {
|
||||
retentionXoMetadata = DEFAULT_RETENTION
|
||||
}
|
||||
if (retentionPoolMetadata == null) {
|
||||
retentionPoolMetadata = DEFAULT_RETENTION
|
||||
const timestamp = Date.now()
|
||||
const formattedTimestamp = safeDateFormat(timestamp)
|
||||
const commonMetadata = {
|
||||
jobId: job.id,
|
||||
jobName: job.name,
|
||||
scheduleId: schedule.id,
|
||||
scheduleName: schedule.name,
|
||||
timestamp,
|
||||
}
|
||||
|
||||
if (
|
||||
(retentionPoolMetadata === 0 && retentionXoMetadata === 0) ||
|
||||
(!job.xoMetadata && retentionPoolMetadata === 0) ||
|
||||
(isEmptyPools && retentionXoMetadata === 0)
|
||||
) {
|
||||
const files = []
|
||||
if (job.xoMetadata && retentionXoMetadata > 0) {
|
||||
const taskId = logger.notice(`Starting XO metadata backup. (${job.id})`, {
|
||||
data: {
|
||||
type: 'xo',
|
||||
},
|
||||
event: 'task.start',
|
||||
parentId: runJobId,
|
||||
})
|
||||
|
||||
const xoMetadataDir = `${DIR_XO_CONFIG_BACKUPS}/${schedule.id}`
|
||||
const dir = `${xoMetadataDir}/${formattedTimestamp}`
|
||||
|
||||
const data = JSON.stringify(await app.exportConfig(), null, 2)
|
||||
const fileName = `${dir}/data.json`
|
||||
|
||||
const metadata = JSON.stringify(commonMetadata, null, 2)
|
||||
const metaDataFileName = `${dir}/metadata.json`
|
||||
|
||||
files.push({
|
||||
executeBackup: defer(($defer, handler) => {
|
||||
$defer.onFailure(() => handler.rmtree(dir))
|
||||
return Promise.all([
|
||||
handler.outputFile(fileName, data),
|
||||
handler.outputFile(metaDataFileName, metadata),
|
||||
]).then(
|
||||
result => {
|
||||
logger.notice(`Backuping XO metadata is a success. (${job.id})`, {
|
||||
event: 'task.end',
|
||||
status: 'success',
|
||||
taskId,
|
||||
})
|
||||
return result
|
||||
},
|
||||
error => {
|
||||
logger.notice(`Backuping XO metadata has failed. (${job.id})`, {
|
||||
event: 'task.end',
|
||||
result: serializeError(error),
|
||||
status: 'failure',
|
||||
taskId,
|
||||
})
|
||||
throw error
|
||||
}
|
||||
)
|
||||
}),
|
||||
dir: xoMetadataDir,
|
||||
retention: retentionXoMetadata,
|
||||
})
|
||||
}
|
||||
if (!isEmptyPools && retentionPoolMetadata > 0) {
|
||||
files.push(
|
||||
...(await Promise.all(
|
||||
poolIds.map(async id => {
|
||||
const xapi = this._app.getXapi(id)
|
||||
const poolMaster = await xapi.getRecord('host', xapi.pool.master)
|
||||
const taskId = logger.notice(
|
||||
`Starting metadata backup for the pool (${id}). (${job.id})`,
|
||||
{
|
||||
data: {
|
||||
id,
|
||||
pool: xapi.pool,
|
||||
poolMaster,
|
||||
type: 'pool',
|
||||
},
|
||||
event: 'task.start',
|
||||
parentId: runJobId,
|
||||
}
|
||||
)
|
||||
const poolMetadataDir = `${DIR_XO_POOL_METADATA_BACKUPS}/${
|
||||
schedule.id
|
||||
}/${id}`
|
||||
const dir = `${poolMetadataDir}/${formattedTimestamp}`
|
||||
|
||||
// TODO: export the metadata only once then split the stream between remotes
|
||||
const stream = await app.getXapi(id).exportPoolMetadata(cancelToken)
|
||||
const fileName = `${dir}/data`
|
||||
|
||||
const metadata = JSON.stringify(
|
||||
{
|
||||
...commonMetadata,
|
||||
pool: xapi.pool,
|
||||
poolMaster,
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
const metaDataFileName = `${dir}/metadata.json`
|
||||
|
||||
return {
|
||||
executeBackup: defer(($defer, handler) => {
|
||||
$defer.onFailure(() => handler.rmtree(dir))
|
||||
return Promise.all([
|
||||
(async () => {
|
||||
const outputStream = await handler.createOutputStream(
|
||||
fileName
|
||||
)
|
||||
$defer.onFailure(() => outputStream.destroy())
|
||||
|
||||
// 'readable-stream/pipeline' not call the callback when an error throws
|
||||
// from the readable stream
|
||||
stream.pipe(outputStream)
|
||||
return fromEvent(stream, 'end').catch(error => {
|
||||
if (error.message !== 'aborted') {
|
||||
throw error
|
||||
}
|
||||
})
|
||||
})(),
|
||||
handler.outputFile(metaDataFileName, metadata),
|
||||
]).then(
|
||||
result => {
|
||||
logger.notice(
|
||||
`Backuping pool metadata (${id}) is a success. (${
|
||||
job.id
|
||||
})`,
|
||||
{
|
||||
event: 'task.end',
|
||||
status: 'success',
|
||||
taskId,
|
||||
}
|
||||
)
|
||||
return result
|
||||
},
|
||||
error => {
|
||||
logger.notice(
|
||||
`Backuping pool metadata (${id}) has failed. (${job.id})`,
|
||||
{
|
||||
event: 'task.end',
|
||||
result: serializeError(error),
|
||||
status: 'failure',
|
||||
taskId,
|
||||
}
|
||||
)
|
||||
throw error
|
||||
}
|
||||
)
|
||||
}),
|
||||
dir: poolMetadataDir,
|
||||
retention: retentionPoolMetadata,
|
||||
}
|
||||
})
|
||||
))
|
||||
)
|
||||
}
|
||||
|
||||
if (files.length === 0) {
|
||||
throw new Error('no retentions corresponding to the metadata modes found')
|
||||
}
|
||||
|
||||
cancelToken.throwIfRequested()
|
||||
|
||||
const app = this._app
|
||||
const timestampReg = /^\d{8}T\d{6}Z$/
|
||||
return asyncMap(
|
||||
// TODO: emit a warning task if a remote is broken
|
||||
asyncMap(remoteIds, id => app.getRemoteHandler(id)::ignoreErrors()),
|
||||
async handler => {
|
||||
if (handler === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
const handlers = {}
|
||||
await Promise.all(
|
||||
remoteIds.map(id =>
|
||||
app.getRemoteHandler(id).then(
|
||||
handler => {
|
||||
handlers[id] = handler
|
||||
},
|
||||
error => {
|
||||
logger.warning(`unable to get the handler for the remote (${id})`, {
|
||||
event: 'task.warning',
|
||||
taskId: runJobId,
|
||||
data: {
|
||||
error,
|
||||
},
|
||||
await Promise.all(
|
||||
files.map(async ({ executeBackup, dir, retention }) => {
|
||||
await executeBackup(handler)
|
||||
|
||||
// deleting old backups
|
||||
await handler.list(dir).then(list => {
|
||||
list.sort()
|
||||
list = list
|
||||
.filter(timestampDir => timestampReg.test(timestampDir))
|
||||
.slice(0, -retention)
|
||||
return Promise.all(
|
||||
list.map(timestampDir =>
|
||||
handler.rmtree(`${dir}/${timestampDir}`)
|
||||
)
|
||||
)
|
||||
})
|
||||
}
|
||||
})
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
if (Object.keys(handlers).length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const promises = []
|
||||
if (job.xoMetadata && retentionXoMetadata !== 0) {
|
||||
promises.push(
|
||||
this._backupXo({
|
||||
handlers,
|
||||
job,
|
||||
logger,
|
||||
retention: retentionXoMetadata,
|
||||
runJobId,
|
||||
schedule,
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
if (!isEmptyPools && retentionPoolMetadata !== 0) {
|
||||
poolIds.forEach(id => {
|
||||
let xapi
|
||||
try {
|
||||
xapi = this._app.getXapi(id)
|
||||
} catch (error) {
|
||||
logger.warning(
|
||||
`unable to get the xapi associated to the pool (${id})`,
|
||||
{
|
||||
event: 'task.warning',
|
||||
taskId: runJobId,
|
||||
data: {
|
||||
error,
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
if (xapi !== undefined) {
|
||||
promises.push(
|
||||
this._backupPool(id, {
|
||||
cancelToken,
|
||||
handlers,
|
||||
job,
|
||||
logger,
|
||||
retention: retentionPoolMetadata,
|
||||
runJobId,
|
||||
schedule,
|
||||
xapi,
|
||||
})
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return Promise.all(promises)
|
||||
}
|
||||
|
||||
async createMetadataBackupJob(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "xo-vmdk-to-vhd",
|
||||
"version": "0.1.7",
|
||||
"version": "0.1.6",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "JS lib streaming a vmdk file to a vhd",
|
||||
"keywords": [
|
||||
@@ -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",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": false,
|
||||
"name": "xo-web",
|
||||
"version": "5.40.1",
|
||||
"version": "5.38.0",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "Web interface client for Xen-Orchestra",
|
||||
"keywords": [
|
||||
@@ -58,7 +58,7 @@
|
||||
"chartist-plugin-legend": "^0.6.1",
|
||||
"chartist-plugin-tooltip": "0.0.11",
|
||||
"classnames": "^2.2.3",
|
||||
"complex-matcher": "^0.6.0",
|
||||
"complex-matcher": "^0.5.0",
|
||||
"cookies-js": "^1.2.2",
|
||||
"copy-to-clipboard": "^3.0.8",
|
||||
"d3": "^5.0.0",
|
||||
@@ -95,7 +95,7 @@
|
||||
"moment": "^2.20.1",
|
||||
"moment-timezone": "^0.5.14",
|
||||
"notifyjs": "^3.0.0",
|
||||
"otplib": "^11.0.0",
|
||||
"otplib": "^10.0.1",
|
||||
"promise-toolbox": "^0.12.1",
|
||||
"prop-types": "^15.6.0",
|
||||
"qrcode": "^1.3.2",
|
||||
@@ -128,7 +128,7 @@
|
||||
"redux-thunk": "^2.0.1",
|
||||
"reselect": "^2.5.4",
|
||||
"rimraf": "^2.6.2",
|
||||
"semver": "^6.0.0",
|
||||
"semver": "^5.4.1",
|
||||
"styled-components": "^3.1.5",
|
||||
"uglify-es": "^3.3.4",
|
||||
"uncontrollable-input": "^0.1.1",
|
||||
@@ -142,7 +142,7 @@
|
||||
"xo-common": "^0.2.0",
|
||||
"xo-lib": "^0.8.0",
|
||||
"xo-remote-parser": "^0.5.0",
|
||||
"xo-vmdk-to-vhd": "^0.1.7"
|
||||
"xo-vmdk-to-vhd": "^0.1.6"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "NODE_ENV=production gulp build",
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -3411,7 +3411,7 @@ export default {
|
||||
|
||||
// Original text: "This version is not bundled with any support nor updates. Use it with caution for critical tasks."
|
||||
disclaimerText3:
|
||||
'Esta versión no está creada para recibir soporte ni actualizaciones. Úsala con precaución.',
|
||||
'Esta versión no está creada para recibir soporte ni actualizaciones. Úsala con precaución para tareas críticas.',
|
||||
|
||||
// Original text: "Connect PIF"
|
||||
connectPif: 'Conectar PIF',
|
||||
|
||||
@@ -3016,8 +3016,7 @@ export default {
|
||||
"Le SR par défaut n'est pas connecté à l'hôte",
|
||||
|
||||
// Original text: "For each VDI, select an SR:"
|
||||
chooseSrForEachVdisModalSelectSr:
|
||||
'Pour chaque VDI, sélectionner un SR (optionnel)',
|
||||
chooseSrForEachVdisModalSelectSr: 'Pour chaque VDI, sélectionner un SR :',
|
||||
|
||||
// Original text: "Select main SR…"
|
||||
chooseSrForEachVdisModalMainSr: 'Sélectionner le SR principal…',
|
||||
@@ -3497,7 +3496,7 @@ export default {
|
||||
|
||||
// Original text: "This version is not bundled with any support nor updates. Use it with caution for critical tasks."
|
||||
disclaimerText3:
|
||||
"Cette version n'est fournie avec aucun support ni aucune mise à jour. Utilisez-la avec précaution.",
|
||||
"Cette version n'est fournie avec aucun support ni aucune mise à jour. Soyez prudent en cas d'utilisation pour des tâches importantes.",
|
||||
|
||||
// Original text: "Connect PIF"
|
||||
connectPif: 'Connecter la PIF',
|
||||
|
||||
@@ -3248,7 +3248,7 @@ export default {
|
||||
|
||||
// Original text: "This version is not bundled with any support nor updates. Use it with caution for critical tasks."
|
||||
disclaimerText3:
|
||||
'This Verzió is not bundled with any support nor upDates. Use it with caution.',
|
||||
'This Verzió is not bundled with any support nor upDates. Use it with caution for critical tasks.',
|
||||
|
||||
// Original text: "Connect PIF"
|
||||
connectPif: 'Csatlakozás PIF',
|
||||
|
||||
@@ -2958,7 +2958,7 @@ export default {
|
||||
|
||||
// Original text: "This version is not bundled with any support nor updates. Use it with caution for critical tasks."
|
||||
disclaimerText3:
|
||||
'This version is not bundled with any support nor updates. Use it with caution.',
|
||||
'This version is not bundled with any support nor updates. Use it with caution for critical tasks.',
|
||||
|
||||
// Original text: "Connect PIF"
|
||||
connectPif: 'Connect PIF',
|
||||
|
||||
@@ -2944,7 +2944,7 @@ export default {
|
||||
|
||||
// Original text: "This version is not bundled with any support nor updates. Use it with caution for critical tasks."
|
||||
disclaimerText3:
|
||||
'Esta versão não está vinculada a qualquer tipo de suporte nem atualizações. Use-a com cuidado.',
|
||||
'Esta versão não está vinculada a qualquer tipo de suporte nem atualizações. Use-a com cuidado em se tratando de tarefas críticas.',
|
||||
|
||||
// Original text: "Connect PIF"
|
||||
connectPif: 'Conectar PIF',
|
||||
|
||||
@@ -4294,7 +4294,7 @@ export default {
|
||||
|
||||
// Original text: "This version is not bundled with any support nor updates. Use it with caution for critical tasks."
|
||||
disclaimerText3:
|
||||
'Bu sürüm herhangi bir destek veya güncellemeyle birlikte verilmez. Dikkatli kullanın.',
|
||||
'Bu sürüm herhangi bir destek veya güncellemeyle birlikte verilmez. Kritik görevler için dikkatli kullanın.',
|
||||
|
||||
// Original text: "Connect PIF"
|
||||
connectPif: "PIF'e bağlan",
|
||||
|
||||
@@ -2236,7 +2236,7 @@ export default {
|
||||
disclaimerText2: '如果你是一个公司,建议使用我们的设备结合专业的支持',
|
||||
|
||||
// Original text: "This version is not bundled with any support nor updates. Use it with caution for critical tasks."
|
||||
disclaimerText3: '这个版本没有绑定任何支持或更新,请谨慎使用',
|
||||
disclaimerText3: '这个版本没有绑定任何支持或更新,在紧急任务下,请谨慎使用',
|
||||
|
||||
// Original text: "Connect PIF"
|
||||
connectPif: '连接物理网卡',
|
||||
|
||||
@@ -46,7 +46,6 @@ const messages = {
|
||||
metadata: 'Metadata',
|
||||
chooseBackup: 'Choose a backup',
|
||||
clickToShowError: 'Click to show error',
|
||||
backupJobs: 'Backup jobs',
|
||||
|
||||
// ----- Modals -----
|
||||
alertOk: 'OK',
|
||||
@@ -1051,12 +1050,12 @@ const messages = {
|
||||
importVdi: 'Import VDI content',
|
||||
importVdiNoFile: 'No file selected',
|
||||
selectVdiMessage: 'Drop VHD file here',
|
||||
srsNotOnSameHost:
|
||||
'The SRs must either be shared or on the same host for the VM to be able to start.',
|
||||
useQuotaWarning:
|
||||
'Creating this disk will use the disk space quota from the resource set {resourceSet} ({spaceLeft} left)',
|
||||
notEnoughSpaceInResourceSet:
|
||||
'Not enough space in resource set {resourceSet} ({spaceLeft} left)',
|
||||
warningVdiSr:
|
||||
"The VDIs' SRs must either be shared or on the same host for the VM to be able to start.",
|
||||
|
||||
// ----- VM network tab -----
|
||||
vifCreateDeviceButton: 'New device',
|
||||
@@ -1285,9 +1284,6 @@ const messages = {
|
||||
spaceLeftTooltip: '{used}% used ({free} left)',
|
||||
|
||||
// ----- New VM -----
|
||||
createVmModalTitle: 'Create VM',
|
||||
createVmModalWarningMessage:
|
||||
"You're about to use a large amount of resources available on the resource set. Are you sure you want to continue?",
|
||||
newVmCreateNewVmOn: 'Create a new VM on {select}',
|
||||
newVmCreateNewVmNoPermission: 'You have no permission to create a VM',
|
||||
newVmInfoPanel: 'Infos',
|
||||
@@ -1350,15 +1346,6 @@ const messages = {
|
||||
newVmHideAdvanced: 'Hide advanced settings',
|
||||
newVmShare: 'Share this VM',
|
||||
newVmSrsNotOnSameHost: 'The SRs must either be on the same host or shared',
|
||||
newVmNetworkConfigLabel: 'Network config',
|
||||
newVmNetworkConfigInfo:
|
||||
'Network configuration is only compatible with the {noCloudDatasourceLink}.',
|
||||
newVmNetworkConfigDocLink: 'See {networkConfigDocLink}.',
|
||||
newVmNetworkConfigTooltip:
|
||||
'Click here to get more information about network config',
|
||||
newVmUserConfigLabel: 'User config',
|
||||
newVmNoCloudDatasource: 'NoCloud datasource',
|
||||
newVmNetworkConfigDoc: 'Network config documentation',
|
||||
|
||||
// ----- Self -----
|
||||
resourceSets: 'Resource sets',
|
||||
@@ -1621,7 +1608,7 @@ const messages = {
|
||||
migrateVmNoTargetHostMessage: 'A target host is required to migrate a VM',
|
||||
migrateVmNoDefaultSrError: 'No default SR',
|
||||
migrateVmNotConnectedDefaultSrError: 'Default SR not connected to host',
|
||||
chooseSrForEachVdisModalSelectSr: 'For each VDI, select an SR (optional)',
|
||||
chooseSrForEachVdisModalSelectSr: 'For each VDI, select an SR:',
|
||||
chooseSrForEachVdisModalMainSr: 'Select main SR…',
|
||||
chooseSrForEachVdisModalVdiLabel: 'VDI',
|
||||
chooseSrForEachVdisModalSrLabel: 'SR*',
|
||||
@@ -1868,7 +1855,7 @@ const messages = {
|
||||
disclaimerText2:
|
||||
"If you are a company, it's better to use it with our appliance + pro support included:",
|
||||
disclaimerText3:
|
||||
'This version is not bundled with any support nor updates. Use it with caution.',
|
||||
'This version is not bundled with any support nor updates. Use it with caution for critical tasks.',
|
||||
notRegisteredDisclaimerInfo:
|
||||
'You are not registered. Your XOA may not be up to date.',
|
||||
notRegisteredDisclaimerCreateAccount: 'Click here to create an account.',
|
||||
|
||||
@@ -409,7 +409,8 @@ const xoItemToRender = {
|
||||
<span>
|
||||
<strong>
|
||||
<Icon icon='resource-set' /> {resourceSet.name}
|
||||
</strong>
|
||||
</strong>{' '}
|
||||
({resourceSet.id})
|
||||
</span>
|
||||
),
|
||||
sshKey: key => (
|
||||
|
||||
@@ -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.
|
||||
}
|
||||
|
||||
|
||||
@@ -70,8 +70,7 @@ export default class ChooseSrForEachVdisModal extends Component {
|
||||
{props.vdis != null && mainSr != null && (
|
||||
<Collapsible
|
||||
buttonText={_('chooseSrForEachVdisModalSelectSr')}
|
||||
collapsible
|
||||
size='small'
|
||||
collapsible={props.vdis.length >= 3}
|
||||
>
|
||||
<br />
|
||||
<Container>
|
||||
|
||||
@@ -152,7 +152,7 @@ export default class MigrateVmModalBody extends BaseComponent {
|
||||
return
|
||||
}
|
||||
|
||||
const { pools, vbds, vm } = this.props
|
||||
const { vbds, vm } = this.props
|
||||
const intraPool = vm.$pool === host.$pool
|
||||
|
||||
// Intra-pool
|
||||
@@ -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: {},
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ const MODES = {
|
||||
__proto__: null,
|
||||
|
||||
compression: 'full',
|
||||
fullInterval: 'delta',
|
||||
}
|
||||
|
||||
const getSettingsWithNonDefaultValue = (mode, settings) =>
|
||||
|
||||
@@ -340,7 +340,7 @@ const Overview = () => (
|
||||
<div>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Icon icon='backup' /> {_('backupJobs')}
|
||||
<Icon icon='schedule' /> {_('backupSchedules')}
|
||||
</CardHeader>
|
||||
<CardBlock>
|
||||
<JobsTable />
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import _ from 'intl'
|
||||
import decorate from 'apply-decorators'
|
||||
import defined from '@xen-orchestra/defined'
|
||||
import Icon from 'icon'
|
||||
import React from 'react'
|
||||
import Scheduler, { SchedulePreview } from 'scheduling'
|
||||
@@ -34,9 +35,9 @@ export default decorate([
|
||||
name: value.trim() === '' ? null : value,
|
||||
})
|
||||
},
|
||||
setRetention({ setSchedule }, value, { name }) {
|
||||
setRetention: ({ setSchedule }, value, { name }) => () => {
|
||||
setSchedule({
|
||||
[name]: value,
|
||||
[name]: defined(value, null),
|
||||
})
|
||||
},
|
||||
},
|
||||
@@ -65,6 +66,7 @@ export default decorate([
|
||||
value={schedule.name}
|
||||
/>
|
||||
</FormGroup>
|
||||
{/* retentions effects are defined on initialize() */}
|
||||
{retentions.map(({ name, valuePath }) => (
|
||||
<FormGroup key={valuePath}>
|
||||
<label>
|
||||
@@ -74,7 +76,6 @@ export default decorate([
|
||||
data-name={valuePath}
|
||||
min='0'
|
||||
onChange={effects.setRetention}
|
||||
required
|
||||
value={schedule[valuePath]}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
@@ -935,18 +935,16 @@ export default decorate([
|
||||
placeholder={formatMessage(messages.timeoutUnit)}
|
||||
/>
|
||||
</FormGroup>
|
||||
{state.isDelta && (
|
||||
<FormGroup>
|
||||
<label htmlFor={state.inputFullIntervalId}>
|
||||
<strong>{_('fullBackupInterval')}</strong>
|
||||
</label>{' '}
|
||||
<Number
|
||||
id={state.inputFullIntervalId}
|
||||
onChange={effects.setFullInterval}
|
||||
value={fullInterval}
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
<FormGroup>
|
||||
<label htmlFor={state.inputFullIntervalId}>
|
||||
<strong>{_('fullBackupInterval')}</strong>
|
||||
</label>{' '}
|
||||
<Number
|
||||
id={state.inputFullIntervalId}
|
||||
onChange={effects.setFullInterval}
|
||||
value={fullInterval}
|
||||
/>
|
||||
</FormGroup>
|
||||
{state.isFull && (
|
||||
<FormGroup>
|
||||
<label htmlFor={state.compressionId}>
|
||||
|
||||
@@ -38,6 +38,7 @@ import Schedules from '../_schedules'
|
||||
// A retention can be:
|
||||
// - number: set by user
|
||||
// - undefined: will be replaced by the default value in the display(table + modal) and on submitting the form
|
||||
// - null: when a user voluntarily deletes its value.
|
||||
const DEFAULT_RETENTION = 1
|
||||
|
||||
const RETENTION_POOL_METADATA = {
|
||||
@@ -206,20 +207,7 @@ export default decorate([
|
||||
schedules: ({ _schedules }, { schedules }) =>
|
||||
defined(_schedules, schedules),
|
||||
settings: ({ _settings }, { job }) =>
|
||||
// it replaces null retentions introduced by the commit
|
||||
// https://github.com/vatesfr/xen-orchestra/commit/fea5117ed83b58d3a57715b32d63d46e3004a094#diff-c02703199db2a4c217943cf8e02b91deR40
|
||||
defined(_settings, () =>
|
||||
mapValues(job.settings, setting => {
|
||||
const newSetting = { ...setting }
|
||||
if (newSetting.retentionPoolMetadata === null) {
|
||||
newSetting.retentionPoolMetadata = 0
|
||||
}
|
||||
if (newSetting.retentionXoMetadata === null) {
|
||||
newSetting.retentionXoMetadata = 0
|
||||
}
|
||||
return newSetting
|
||||
})
|
||||
),
|
||||
defined(_settings, () => job.settings),
|
||||
remotes: ({ _remotes }, { job }) =>
|
||||
defined(_remotes, () => destructPattern(job.remotes), []),
|
||||
remotesPredicate: ({ remotes }) => ({ id }) => !remotes.includes(id),
|
||||
@@ -239,13 +227,13 @@ export default decorate([
|
||||
state.modePoolMetadata &&
|
||||
every(
|
||||
state.settings,
|
||||
({ retentionPoolMetadata }) => retentionPoolMetadata === 0
|
||||
({ retentionPoolMetadata }) => retentionPoolMetadata === null
|
||||
),
|
||||
missingRetentionXoMetadata: state =>
|
||||
state.modeXoMetadata &&
|
||||
every(
|
||||
state.settings,
|
||||
({ retentionXoMetadata }) => retentionXoMetadata === 0
|
||||
({ retentionXoMetadata }) => retentionXoMetadata === null
|
||||
),
|
||||
missingSchedules: state => isEmpty(state.schedules),
|
||||
},
|
||||
|
||||
@@ -39,14 +39,9 @@ import {
|
||||
|
||||
const SrColContainer = connectStore(() => ({
|
||||
container: createGetObject(),
|
||||
}))(
|
||||
({ container }) =>
|
||||
container !== undefined && (
|
||||
<Link to={`${container.type}s/${container.id}`}>
|
||||
{container.name_label}
|
||||
</Link>
|
||||
)
|
||||
)
|
||||
}))(({ container }) => (
|
||||
<Link to={`${container.type}s/${container.id}`}>{container.name_label}</Link>
|
||||
))
|
||||
|
||||
const VmColContainer = connectStore(() => ({
|
||||
container: createGetObject(),
|
||||
|
||||
@@ -489,37 +489,35 @@ class DefaultCard extends Component {
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
{props.isAdmin && (
|
||||
<Row>
|
||||
<Col>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Icon icon='menu-dashboard-stats' /> {_('dashboardReport')}
|
||||
</CardHeader>
|
||||
<CardBlock className='text-xs-center'>
|
||||
<ActionButton
|
||||
btnStyle='primary'
|
||||
disabled={!canSendTheReport}
|
||||
handler={sendUsageReport}
|
||||
icon=''
|
||||
>
|
||||
{_('dashboardSendReport')}
|
||||
</ActionButton>
|
||||
<br />
|
||||
{!canSendTheReport && (
|
||||
<span>
|
||||
<Link to='/settings/plugins' className='text-info'>
|
||||
<Icon icon='info' /> {_('dashboardSendReportInfo')}
|
||||
</Link>
|
||||
<br />
|
||||
</span>
|
||||
)}
|
||||
{_('dashboardSendReportMessage')}
|
||||
</CardBlock>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
<Row>
|
||||
<Col>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Icon icon='menu-dashboard-stats' /> {_('dashboardReport')}
|
||||
</CardHeader>
|
||||
<CardBlock className='text-xs-center'>
|
||||
<ActionButton
|
||||
btnStyle='primary'
|
||||
disabled={!canSendTheReport}
|
||||
handler={sendUsageReport}
|
||||
icon=''
|
||||
>
|
||||
{_('dashboardSendReport')}
|
||||
</ActionButton>
|
||||
<br />
|
||||
{!canSendTheReport && (
|
||||
<span>
|
||||
<Link to='/settings/plugins' className='text-info'>
|
||||
<Icon icon='info' /> {_('dashboardSendReportInfo')}
|
||||
</Link>
|
||||
<br />
|
||||
</span>
|
||||
)}
|
||||
{_('dashboardSendReportMessage')}
|
||||
</CardBlock>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col>
|
||||
<PatchesCard hosts={this._getHosts()} />
|
||||
|
||||
@@ -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':
|
||||
|
||||
@@ -3,6 +3,7 @@ import Component from 'base-component'
|
||||
import Icon from 'icon'
|
||||
import React from 'react'
|
||||
import Tooltip from 'tooltip'
|
||||
import Upgrade from 'xoa-upgrade'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import { Toggle } from 'form'
|
||||
import { fetchHostStats } from 'xo'
|
||||
@@ -104,7 +105,7 @@ export default class HostStats extends Component {
|
||||
|
||||
return !stats ? (
|
||||
<p>No stats.</p>
|
||||
) : (
|
||||
) : process.env.XOA_PLAN > 2 ? (
|
||||
<Container>
|
||||
<Row>
|
||||
<Col mediumSize={5}>
|
||||
@@ -178,6 +179,10 @@ export default class HostStats extends Component {
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
) : (
|
||||
<Container>
|
||||
<Upgrade place='hostStats' available={3} />
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,17 +222,6 @@ export default class XoApp extends Component {
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
{+process.env.XOA_PLAN === 5 && (
|
||||
<div className='alert alert-danger mb-0'>
|
||||
<a
|
||||
href='https://xen-orchestra.com/#!/xoa?pk_campaign=xo_source_banner'
|
||||
rel='noopener noreferrer'
|
||||
target='_blank'
|
||||
>
|
||||
{_('disclaimerText3')}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
<div style={CONTAINER_STYLE}>
|
||||
<Shortcuts
|
||||
name='XoApp'
|
||||
|
||||
@@ -38,6 +38,7 @@ export const LogStatus = ({ log, tooltip = _('logDisplayDetails') }) => {
|
||||
return (
|
||||
<ActionButton
|
||||
btnStyle={className}
|
||||
disabled={log.status !== 'failure' && isEmpty(log.tasks)}
|
||||
handler={showTasks}
|
||||
handlerParam={log.id}
|
||||
icon='preview'
|
||||
|
||||
@@ -1,20 +1,16 @@
|
||||
import _, { FormattedDuration } from 'intl'
|
||||
import ActionButton from 'action-button'
|
||||
import decorate from 'apply-decorators'
|
||||
import defined, { get } from '@xen-orchestra/defined'
|
||||
import Icon from 'icon'
|
||||
import React from 'react'
|
||||
import Select from 'form/select'
|
||||
import Tooltip from 'tooltip'
|
||||
import { addSubscriptions, formatSize, formatSpeed } from 'utils'
|
||||
import { countBy, cloneDeep, filter, keyBy, map } from 'lodash'
|
||||
import { countBy, filter, get, keyBy, map } from 'lodash'
|
||||
import { FormattedDate } from 'react-intl'
|
||||
import { injectState, provideState } from 'reaclette'
|
||||
import { runBackupNgJob, subscribeBackupNgLogs, subscribeRemotes } from 'xo'
|
||||
import { Vm, Sr, Remote, Pool } from 'render-xo-item'
|
||||
|
||||
const hasTaskFailed = ({ status }) =>
|
||||
status !== 'success' && status !== 'pending'
|
||||
import { Vm, Sr, Remote } from 'render-xo-item'
|
||||
|
||||
const TASK_STATUS = {
|
||||
failure: {
|
||||
@@ -48,75 +44,19 @@ const TaskStateInfos = ({ status }) => {
|
||||
)
|
||||
}
|
||||
|
||||
const TaskDate = ({ value }) => (
|
||||
<FormattedDate
|
||||
value={new Date(value)}
|
||||
month='short'
|
||||
day='numeric'
|
||||
year='numeric'
|
||||
hour='2-digit'
|
||||
minute='2-digit'
|
||||
second='2-digit'
|
||||
/>
|
||||
)
|
||||
|
||||
const TaskStart = ({ task }) => (
|
||||
<div>{_.keyValue(_('taskStart'), <TaskDate value={task.start} />)}</div>
|
||||
)
|
||||
const TaskEnd = ({ task }) =>
|
||||
task.end !== undefined ? (
|
||||
<div>{_.keyValue(_('taskEnd'), <TaskDate value={task.end} />)}</div>
|
||||
) : null
|
||||
const TaskDuration = ({ task }) =>
|
||||
task.end !== undefined ? (
|
||||
<div>
|
||||
{_.keyValue(
|
||||
_('taskDuration'),
|
||||
<FormattedDuration duration={task.end - task.start} />
|
||||
)}
|
||||
</div>
|
||||
) : null
|
||||
|
||||
const UNHEALTHY_VDI_CHAIN_ERROR = 'unhealthy VDI chain'
|
||||
const UNHEALTHY_VDI_CHAIN_LINK =
|
||||
'https://xen-orchestra.com/docs/backup_troubleshooting.html#vdi-chain-protection'
|
||||
|
||||
const TaskError = ({ task }) => {
|
||||
let message
|
||||
if (
|
||||
!hasTaskFailed(task) ||
|
||||
(message = defined(() => task.result.message, () => task.result.code)) ===
|
||||
undefined
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (message === UNHEALTHY_VDI_CHAIN_ERROR) {
|
||||
return (
|
||||
<div>
|
||||
<Tooltip content={_('clickForMoreInformation')}>
|
||||
<a
|
||||
className='text-info'
|
||||
href={UNHEALTHY_VDI_CHAIN_LINK}
|
||||
rel='noopener noreferrer'
|
||||
target='_blank'
|
||||
>
|
||||
<Icon icon='info' /> {_('unhealthyVdiChainError')}
|
||||
</a>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const [label, className] =
|
||||
task.status === 'skipped'
|
||||
? [_('taskReason'), 'text-info']
|
||||
: [_('taskError'), 'text-danger']
|
||||
|
||||
return (
|
||||
<div>{_.keyValue(label, <span className={className}>{message}</span>)}</div>
|
||||
const TaskDate = ({ label, value }) =>
|
||||
_.keyValue(
|
||||
_(label),
|
||||
<FormattedDate
|
||||
value={new Date(value)}
|
||||
month='short'
|
||||
day='numeric'
|
||||
year='numeric'
|
||||
hour='2-digit'
|
||||
minute='2-digit'
|
||||
second='2-digit'
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const Warnings = ({ warnings }) =>
|
||||
warnings !== undefined ? (
|
||||
@@ -132,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>
|
||||
|
||||
@@ -2,7 +2,9 @@ import _ from 'intl'
|
||||
import classNames from 'classnames'
|
||||
import Component from 'base-component'
|
||||
import Icon from 'icon'
|
||||
import isEmpty from 'lodash/isEmpty'
|
||||
import Link from 'link'
|
||||
import map from 'lodash/map'
|
||||
import React from 'react'
|
||||
import Tooltip from 'tooltip'
|
||||
import { UpdateTag } from '../xoa/update'
|
||||
@@ -11,7 +13,6 @@ import { addSubscriptions, connectStore, getXoaPlan, noop } from 'utils'
|
||||
import {
|
||||
connect,
|
||||
signOut,
|
||||
subscribeNotifications,
|
||||
subscribePermissions,
|
||||
subscribeResourceSets,
|
||||
} from 'xo'
|
||||
@@ -22,10 +23,8 @@ import {
|
||||
getIsPoolAdmin,
|
||||
getStatus,
|
||||
getUser,
|
||||
getXoaState,
|
||||
isAdmin,
|
||||
} from 'selectors'
|
||||
import { every, identity, isEmpty, map } from 'lodash'
|
||||
|
||||
import styles from './index.css'
|
||||
|
||||
@@ -35,22 +34,20 @@ const returnTrue = () => true
|
||||
() => ({
|
||||
isAdmin,
|
||||
isPoolAdmin: getIsPoolAdmin,
|
||||
nHosts: createGetObjectsOfType('host').count(),
|
||||
nTasks: createGetObjectsOfType('task').count([
|
||||
task => task.status === 'pending',
|
||||
]),
|
||||
pools: createGetObjectsOfType('pool'),
|
||||
nHosts: createGetObjectsOfType('host').count(),
|
||||
srs: createGetObjectsOfType('SR'),
|
||||
status: getStatus,
|
||||
user: getUser,
|
||||
xoaState: getXoaState,
|
||||
}),
|
||||
{
|
||||
withRef: true,
|
||||
}
|
||||
)
|
||||
@addSubscriptions({
|
||||
notifications: subscribeNotifications,
|
||||
permissions: subscribePermissions,
|
||||
resourceSets: subscribeResourceSets,
|
||||
})
|
||||
@@ -91,11 +88,6 @@ export default class Menu extends Component {
|
||||
isEmpty
|
||||
)
|
||||
|
||||
_getNoNotifications = createSelector(
|
||||
() => this.props.notifications,
|
||||
notifications => every(notifications, { read: true })
|
||||
)
|
||||
|
||||
get height() {
|
||||
return this.refs.content.offsetHeight
|
||||
}
|
||||
@@ -126,11 +118,9 @@ export default class Menu extends Component {
|
||||
pools,
|
||||
nHosts,
|
||||
srs,
|
||||
xoaState,
|
||||
} = this.props
|
||||
const noOperatablePools = this._getNoOperatablePools()
|
||||
const noResourceSets = this._getNoResourceSets()
|
||||
const noNotifications = this._getNoNotifications()
|
||||
|
||||
/* eslint-disable object-property-newline */
|
||||
const items = [
|
||||
@@ -257,12 +247,16 @@ export default class Menu extends Component {
|
||||
to: isAdmin ? 'xoa/update' : 'xoa/notifications',
|
||||
icon: 'menu-xoa',
|
||||
label: 'xoa',
|
||||
extra: [
|
||||
!isAdmin || xoaState === 'upToDate' ? null : (
|
||||
<UpdateTag key='update' />
|
||||
),
|
||||
noNotifications ? null : <NotificationTag key='notification' />,
|
||||
],
|
||||
extra: (
|
||||
<span>
|
||||
{isAdmin && (
|
||||
<span>
|
||||
<UpdateTag />{' '}
|
||||
</span>
|
||||
)}
|
||||
<NotificationTag />
|
||||
</span>
|
||||
),
|
||||
subMenu: [
|
||||
isAdmin && {
|
||||
to: 'xoa/update',
|
||||
@@ -534,7 +528,6 @@ export default class Menu extends Component {
|
||||
const MenuLinkItem = props => {
|
||||
const { item } = props
|
||||
const { to, icon, label, subMenu, pill, extra } = item
|
||||
const _extra = extra !== undefined ? extra.find(e => e !== null) : undefined
|
||||
|
||||
return (
|
||||
<li className='nav-item xo-menu-item'>
|
||||
@@ -544,7 +537,7 @@ const MenuLinkItem = props => {
|
||||
to={to}
|
||||
>
|
||||
<Icon
|
||||
className={classNames((pill || _extra) && styles.hiddenCollapsed)}
|
||||
className={classNames((pill || extra) && styles.hiddenCollapsed)}
|
||||
icon={`${icon}`}
|
||||
size='lg'
|
||||
fixedWidth
|
||||
@@ -555,10 +548,7 @@ const MenuLinkItem = props => {
|
||||
|
||||
</span>
|
||||
{pill > 0 && <span className='tag tag-pill tag-primary'>{pill}</span>}
|
||||
<span className={styles.hiddenUncollapsed}>{_extra}</span>
|
||||
<span className={styles.hiddenCollapsed}>
|
||||
{extra !== undefined && extra.map(identity)}
|
||||
</span>
|
||||
{extra}
|
||||
</Link>
|
||||
{subMenu && <SubMenu items={subMenu} />}
|
||||
</li>
|
||||
|
||||
@@ -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 ? (
|
||||
|
||||
@@ -4,6 +4,7 @@ import getEventValue from 'get-event-value'
|
||||
import Icon from 'icon'
|
||||
import React from 'react'
|
||||
import Tooltip from 'tooltip'
|
||||
import Upgrade from 'xoa-upgrade'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import { Toggle } from 'form'
|
||||
import { fetchHostStats } from 'xo'
|
||||
@@ -95,91 +96,100 @@ export default class PoolStats extends Component {
|
||||
useCombinedValues,
|
||||
} = this.state
|
||||
|
||||
return stats ? (
|
||||
<Container>
|
||||
<Row>
|
||||
<Col mediumSize={5}>
|
||||
<div className='form-group'>
|
||||
<Tooltip content={_('useStackedValuesOnStats')}>
|
||||
<Toggle
|
||||
value={useCombinedValues}
|
||||
onChange={this.linkState('useCombinedValues')}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Col>
|
||||
<Col mediumSize={1}>
|
||||
{selectStatsLoading && (
|
||||
<div className='text-xs-right'>
|
||||
<Icon icon='loading' size={2} />
|
||||
return process.env.XOA_PLAN > 2 ? (
|
||||
stats ? (
|
||||
<Container>
|
||||
<Row>
|
||||
<Col mediumSize={5}>
|
||||
<div className='form-group'>
|
||||
<Tooltip content={_('useStackedValuesOnStats')}>
|
||||
<Toggle
|
||||
value={useCombinedValues}
|
||||
onChange={this.linkState('useCombinedValues')}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</Col>
|
||||
<Col mediumSize={6}>
|
||||
<div className='btn-tab'>
|
||||
<select
|
||||
className='form-control'
|
||||
onChange={this._handleSelectStats}
|
||||
defaultValue={granularity}
|
||||
>
|
||||
{_('statLastTenMinutes', message => (
|
||||
<option value='seconds'>{message}</option>
|
||||
))}
|
||||
{_('statLastTwoHours', message => (
|
||||
<option value='minutes'>{message}</option>
|
||||
))}
|
||||
{_('statLastWeek', message => (
|
||||
<option value='hours'>{message}</option>
|
||||
))}
|
||||
{_('statLastYear', message => (
|
||||
<option value='days'>{message}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col mediumSize={6}>
|
||||
<h5 className='text-xs-center'>
|
||||
<Icon icon='cpu' /> {_('statsCpu')}
|
||||
</h5>
|
||||
<PoolCpuLineChart addSumSeries={useCombinedValues} data={stats} />
|
||||
</Col>
|
||||
<Col mediumSize={6}>
|
||||
<h5 className='text-xs-center'>
|
||||
<Icon icon='memory' /> {_('statsMemory')}
|
||||
</h5>
|
||||
<PoolMemoryLineChart
|
||||
addSumSeries={useCombinedValues}
|
||||
data={stats}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<br />
|
||||
<hr />
|
||||
<Row>
|
||||
<Col mediumSize={6}>
|
||||
<h5 className='text-xs-center'>
|
||||
<Icon icon='network' /> {_('statsNetwork')}
|
||||
</h5>
|
||||
{/* key: workaround that unmounts and re-mounts the chart to make sure the legend updates when toggling "stacked values"
|
||||
</Col>
|
||||
<Col mediumSize={1}>
|
||||
{selectStatsLoading && (
|
||||
<div className='text-xs-right'>
|
||||
<Icon icon='loading' size={2} />
|
||||
</div>
|
||||
)}
|
||||
</Col>
|
||||
<Col mediumSize={6}>
|
||||
<div className='btn-tab'>
|
||||
<select
|
||||
className='form-control'
|
||||
onChange={this._handleSelectStats}
|
||||
defaultValue={granularity}
|
||||
>
|
||||
{_('statLastTenMinutes', message => (
|
||||
<option value='seconds'>{message}</option>
|
||||
))}
|
||||
{_('statLastTwoHours', message => (
|
||||
<option value='minutes'>{message}</option>
|
||||
))}
|
||||
{_('statLastWeek', message => (
|
||||
<option value='hours'>{message}</option>
|
||||
))}
|
||||
{_('statLastYear', message => (
|
||||
<option value='days'>{message}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col mediumSize={6}>
|
||||
<h5 className='text-xs-center'>
|
||||
<Icon icon='cpu' /> {_('statsCpu')}
|
||||
</h5>
|
||||
<PoolCpuLineChart addSumSeries={useCombinedValues} data={stats} />
|
||||
</Col>
|
||||
<Col mediumSize={6}>
|
||||
<h5 className='text-xs-center'>
|
||||
<Icon icon='memory' /> {_('statsMemory')}
|
||||
</h5>
|
||||
<PoolMemoryLineChart
|
||||
addSumSeries={useCombinedValues}
|
||||
data={stats}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<br />
|
||||
<hr />
|
||||
<Row>
|
||||
<Col mediumSize={6}>
|
||||
<h5 className='text-xs-center'>
|
||||
<Icon icon='network' /> {_('statsNetwork')}
|
||||
</h5>
|
||||
{/* key: workaround that unmounts and re-mounts the chart to make sure the legend updates when toggling "stacked values"
|
||||
FIXME: remove key prop once this issue is fixed: https://github.com/CodeYellowBV/chartist-plugin-legend/issues/5 */}
|
||||
<PoolPifLineChart
|
||||
key={useCombinedValues ? 'stacked' : 'unstacked'}
|
||||
addSumSeries={useCombinedValues}
|
||||
data={stats}
|
||||
/>
|
||||
</Col>
|
||||
<Col mediumSize={6}>
|
||||
<h5 className='text-xs-center'>
|
||||
<Icon icon='disk' /> {_('statLoad')}
|
||||
</h5>
|
||||
<PoolLoadLineChart addSumSeries={useCombinedValues} data={stats} />
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
<PoolPifLineChart
|
||||
key={useCombinedValues ? 'stacked' : 'unstacked'}
|
||||
addSumSeries={useCombinedValues}
|
||||
data={stats}
|
||||
/>
|
||||
</Col>
|
||||
<Col mediumSize={6}>
|
||||
<h5 className='text-xs-center'>
|
||||
<Icon icon='disk' /> {_('statLoad')}
|
||||
</h5>
|
||||
<PoolLoadLineChart
|
||||
addSumSeries={useCombinedValues}
|
||||
data={stats}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
) : (
|
||||
<p>{_('poolNoStats')}</p>
|
||||
)
|
||||
) : (
|
||||
<p>{_('poolNoStats')}</p>
|
||||
<Container>
|
||||
<Upgrade place='hostStats' available={3} />
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -3,6 +3,7 @@ import Component from 'base-component'
|
||||
import Icon from 'icon'
|
||||
import React from 'react'
|
||||
import Tooltip from 'tooltip'
|
||||
import Upgrade from 'xoa-upgrade'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import { fetchSrStats } from 'xo'
|
||||
import { get } from 'lodash'
|
||||
@@ -84,79 +85,81 @@ export default class SrStats extends Component {
|
||||
return data === undefined ? (
|
||||
<span>{_('srNoStats')}</span>
|
||||
) : (
|
||||
<Container>
|
||||
<Row>
|
||||
<Col mediumSize={5}>
|
||||
<div className='form-group'>
|
||||
<Tooltip content={_('useStackedValuesOnStats')}>
|
||||
<Toggle
|
||||
value={useCombinedValues}
|
||||
onChange={this.linkState('useCombinedValues')}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Col>
|
||||
<Col mediumSize={1}>
|
||||
{selectStatsLoading && (
|
||||
<div className='text-xs-right'>
|
||||
<Icon icon='loading' size={2} />
|
||||
<Upgrade place='srStats' available={3}>
|
||||
<Container>
|
||||
<Row>
|
||||
<Col mediumSize={5}>
|
||||
<div className='form-group'>
|
||||
<Tooltip content={_('useStackedValuesOnStats')}>
|
||||
<Toggle
|
||||
value={useCombinedValues}
|
||||
onChange={this.linkState('useCombinedValues')}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</Col>
|
||||
<Col mediumSize={6}>
|
||||
<div className='btn-tab'>
|
||||
<select
|
||||
className='form-control'
|
||||
onChange={this._onGranularityChange}
|
||||
defaultValue={granularity}
|
||||
>
|
||||
{_('statLastTenMinutes', message => (
|
||||
<option value='seconds'>{message}</option>
|
||||
))}
|
||||
{_('statLastTwoHours', message => (
|
||||
<option value='minutes'>{message}</option>
|
||||
))}
|
||||
{_('statLastWeek', message => (
|
||||
<option value='hours'>{message}</option>
|
||||
))}
|
||||
{_('statLastYear', message => (
|
||||
<option value='days'>{message}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col mediumSize={6}>
|
||||
<h5 className='text-xs-center'>
|
||||
<Icon icon='iops' size={1} /> {_('statsIops')}
|
||||
</h5>
|
||||
<IopsLineChart addSumSeries={useCombinedValues} data={data} />
|
||||
</Col>
|
||||
<Col mediumSize={6}>
|
||||
<h5 className='text-xs-center'>
|
||||
<Icon icon='disk' size={1} /> {_('statsIoThroughput')}
|
||||
</h5>
|
||||
<IoThroughputChart addSumSeries={useCombinedValues} data={data} />
|
||||
</Col>
|
||||
</Row>
|
||||
<br />
|
||||
<hr />
|
||||
<Row>
|
||||
<Col mediumSize={6}>
|
||||
<h5 className='text-xs-center'>
|
||||
<Icon icon='latency' size={1} /> {_('statsLatency')}
|
||||
</h5>
|
||||
<LatencyChart addSumSeries={useCombinedValues} data={data} />
|
||||
</Col>
|
||||
<Col mediumSize={6}>
|
||||
<h5 className='text-xs-center'>
|
||||
<Icon icon='iowait' size={1} /> {_('statsIowait')}
|
||||
</h5>
|
||||
<IowaitChart addSumSeries={useCombinedValues} data={data} />
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
</Col>
|
||||
<Col mediumSize={1}>
|
||||
{selectStatsLoading && (
|
||||
<div className='text-xs-right'>
|
||||
<Icon icon='loading' size={2} />
|
||||
</div>
|
||||
)}
|
||||
</Col>
|
||||
<Col mediumSize={6}>
|
||||
<div className='btn-tab'>
|
||||
<select
|
||||
className='form-control'
|
||||
onChange={this._onGranularityChange}
|
||||
defaultValue={granularity}
|
||||
>
|
||||
{_('statLastTenMinutes', message => (
|
||||
<option value='seconds'>{message}</option>
|
||||
))}
|
||||
{_('statLastTwoHours', message => (
|
||||
<option value='minutes'>{message}</option>
|
||||
))}
|
||||
{_('statLastWeek', message => (
|
||||
<option value='hours'>{message}</option>
|
||||
))}
|
||||
{_('statLastYear', message => (
|
||||
<option value='days'>{message}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col mediumSize={6}>
|
||||
<h5 className='text-xs-center'>
|
||||
<Icon icon='iops' size={1} /> {_('statsIops')}
|
||||
</h5>
|
||||
<IopsLineChart addSumSeries={useCombinedValues} data={data} />
|
||||
</Col>
|
||||
<Col mediumSize={6}>
|
||||
<h5 className='text-xs-center'>
|
||||
<Icon icon='disk' size={1} /> {_('statsIoThroughput')}
|
||||
</h5>
|
||||
<IoThroughputChart addSumSeries={useCombinedValues} data={data} />
|
||||
</Col>
|
||||
</Row>
|
||||
<br />
|
||||
<hr />
|
||||
<Row>
|
||||
<Col mediumSize={6}>
|
||||
<h5 className='text-xs-center'>
|
||||
<Icon icon='latency' size={1} /> {_('statsLatency')}
|
||||
</h5>
|
||||
<LatencyChart addSumSeries={useCombinedValues} data={data} />
|
||||
</Col>
|
||||
<Col mediumSize={6}>
|
||||
<h5 className='text-xs-center'>
|
||||
<Icon icon='iowait' size={1} /> {_('statsIowait')}
|
||||
</h5>
|
||||
<IowaitChart addSumSeries={useCombinedValues} data={data} />
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
</Upgrade>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -3,6 +3,7 @@ import Component from 'base-component'
|
||||
import Icon from 'icon'
|
||||
import React from 'react'
|
||||
import Tooltip from 'tooltip'
|
||||
import Upgrade from 'xoa-upgrade'
|
||||
import { fetchVmStats } from 'xo'
|
||||
import { Toggle } from 'form'
|
||||
import { injectIntl } from 'react-intl'
|
||||
@@ -104,7 +105,7 @@ export default injectIntl(
|
||||
|
||||
return !stats ? (
|
||||
<p>No stats.</p>
|
||||
) : (
|
||||
) : process.env.XOA_PLAN > 2 ? (
|
||||
<Container>
|
||||
<Row>
|
||||
<Col mediumSize={6}>
|
||||
@@ -176,6 +177,10 @@ export default injectIntl(
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
) : (
|
||||
<Container>
|
||||
<Upgrade place='vmStats' available={3} />
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user