Compare commits

...

72 Commits

Author SHA1 Message Date
Julien Fontanet
5bd468791f feat(xo-server-backup-reports): 0.12.0 2018-05-31 18:28:21 +02:00
Julien Fontanet
37f71bb36c feat(xo-acl-resolver): 0.2.4 2018-05-31 18:26:53 +02:00
Julien Fontanet
2ed4b7ad3f feat(vhd-lib): 0.1.1 2018-05-31 17:59:02 +02:00
Julien Fontanet
7eb970f22a feat(fs): 0.0.1 2018-05-31 17:57:13 +02:00
badrAZ
13db4a8411 feat(Backup NG): improve logs (#3013) 2018-05-31 17:54:35 +02:00
badrAZ
49a7a89bbf feat(xo-web/new-vm): ability to use template vars in the CloudConfig (#3006)
Fixes #2140
2018-05-31 17:44:52 +02:00
Rajaa.BARHTAOUI
0af8a60c1c fix(xo-web/SR/disks): show VM templates attached to VDIs (#3012)
Fixes #2974
2018-05-31 17:32:34 +02:00
Rajaa.BARHTAOUI
e1650b376c fix(xo-web/Self new VM): do not auto-select resource set's 1st SR (#3007)
New behaviour: either auto-select the template's pool's default SR if
it's in the resource set or do not auto-select any SR at all

Fixes #3001
2018-05-31 16:57:52 +02:00
Rajaa.BARHTAOUI
873b40cc70 feat(xo-server,xo-web): allow setting remote syslog host (#2958)
Fixes #2900
2018-05-31 16:22:39 +02:00
Rajaa.BARHTAOUI
d45265b180 feat(xo-web): create single-server private network (#3004)
Fixes #2944
2018-05-31 11:25:08 +02:00
badrAZ
ff50b2848e feat(xo-server,xo-web): allow pool admins to create VMs (#2995)
Fixes #2350
2018-05-31 10:42:03 +02:00
Julien Fontanet
d67fae22ab chore: initial PR template 2018-05-30 18:10:11 +02:00
badrAZ
d809002558 feat(Backup NG): ability to retry a single failed VM backup (#3009)
Fixes 2912
2018-05-30 17:04:50 +02:00
Pierre Donias
5c30559d15 feat(xo-server,xo-web): handle XCP-ng patches (#3005) 2018-05-30 16:03:43 +02:00
Rajaa.BARHTAOUI
cbb5b011e1 fix(xo-web/SR/disks): show control domain VMs attached to VDIs (#3002)
Fixes #2999
2018-05-30 15:55:44 +02:00
Julien Fontanet
f5bff408a8 feat(xo-server/exportDeltaVm): dont check chain if no snapshot
Should fix a race condition with XenServer DB during Delta Backup NG.
2018-05-30 12:22:10 +02:00
Nicolas Raynaud
d7cfe4d3dc fix(vhd/merge): fix performance enhancement (#2980) 2018-05-30 10:26:15 +02:00
badrAZ
7be8f38c6b fix(xo-web/jobs/edit): support all types of inputs (#2997) 2018-05-30 10:20:22 +02:00
badrAZ
08a7e605ce feat(xo-web/backup logs): show # of calls for each state (#2860) 2018-05-30 09:58:48 +02:00
Julien Fontanet
4b57db5893 feat(xo-server/delta NG): validate parent VHD 2018-05-29 16:36:33 +02:00
Julien Fontanet
8b1ae3f3c9 fix(xo-server/delta NG): dont select tmp file as parent 2018-05-29 16:36:10 +02:00
Julien Fontanet
77d35a5928 chore(fs/Handler#list): prepend dir after filtering 2018-05-29 16:34:51 +02:00
Julien Fontanet
323d409e6c chore(package): require yarn >1.7
Previous releases prevented xo-web from being built to an incorrect resolution of dependencies.
2018-05-29 16:29:14 +02:00
Pierre Donias
9f2f2b7b69 fix(xo-web/render-xo-item): show SR container name (#3003)
Fixes #3000
2018-05-29 12:01:50 +02:00
Julien Fontanet
b44fa7beca chore(xo-server): bump xo:perf threshold to 500ms 2018-05-29 10:53:14 +02:00
Julien Fontanet
6d4e310b8e chore(vhd-cli/build): migrate to Babel 7 2018-05-28 18:18:44 +02:00
Julien Fontanet
6726530229 fix(tests): run all test with Babel 7
Temporarily disable xo-web tests which uses Babel 6.
2018-05-28 18:18:44 +02:00
Julien Fontanet
8351352541 chore(xo-remote-parser/build): migrate to Babel 7 2018-05-28 18:18:44 +02:00
Julien Fontanet
3f9e8d79ea chore(xo-collection/build): migrate to Babel 7 2018-05-28 18:18:44 +02:00
Julien Fontanet
685f2328bd chore(package): update dependencies 2018-05-28 14:54:55 +02:00
Pierre Donias
746567a8a7 fix(xo-acl-resolver,xo-web): SR & PIF ACLs inheritance (#2994) 2018-05-25 15:26:45 +02:00
Julien Fontanet
c116c41c42 chore(package): update dependencies 2018-05-25 11:42:55 +02:00
Pierre Donias
3768a7de37 fix(xo-web/select): do not auto-select disabled option (#2992) 2018-05-25 11:11:21 +02:00
Julien Fontanet
11ef0ee54f feat(rolling snapshots legacy): check VDI chains (#2986) 2018-05-24 16:39:06 +02:00
Julien Fontanet
33ae531e3a fix(package): update hashy to 0.7.1
Correctly handle hash with identifier `2b`.
2018-05-24 14:52:38 +02:00
Julien Fontanet
8cc9924751 feat(xo-server/importDeltaVm): add UUID of missing base VM 2018-05-24 00:04:52 +02:00
Julien Fontanet
c329ab863b feat(Backup NG): configurable concurrency (#2918) 2018-05-23 16:29:31 +02:00
Julien Fontanet
41820ea316 fix(xo-server/backup legacy): fix reports (#2979) 2018-05-23 15:56:40 +02:00
badrAZ
bf00f80716 fix(xo-web): update @julien-f/freactal to 0.1.1 (#2972)
This fix prevent the cursor from jumping to the end of the input when editing the backup's name.
2018-05-23 14:47:28 +02:00
Julien Fontanet
9baf0c74e4 chore(vhd-lib): move some deps to devDeps 2018-05-23 11:49:59 +02:00
Julien Fontanet
b59ccdf26f chore(package): update dependencies 2018-05-23 11:23:25 +02:00
Julien Fontanet
9cae978923 feat(xo-server): 5.19.9 2018-05-22 19:43:19 +02:00
Julien Fontanet
311d914b96 feat(xo-web/import): remove restriction for Free 2018-05-22 16:27:17 +02:00
Julien Fontanet
592cb4ef9e feat(xo-web): 5.19.8 2018-05-22 15:53:45 +02:00
Julien Fontanet
ec2db7f2d0 feat(xo-vmdk-to-vhd): 0.1.2 2018-05-22 15:53:03 +02:00
Julien Fontanet
71eab7ba9b feat(xo-web): 5.19.7 2018-05-22 15:36:54 +02:00
badrAZ
5e07171d60 fix(xo-server/backup-ng): fix incorrect condition (#2971) 2018-05-22 11:45:02 +02:00
Rajaa.BARHTAOUI
3f73e3d964 fix(xo-server,xo-web): message when no Xen tools (#2916)
Fixes #2911
2018-05-22 09:57:46 +02:00
badrAZ
0ebe78b4a2 feat(xo-server-usage-report): improve the report (#2970)
Fixes #2968
2018-05-21 16:25:40 +02:00
Julien Fontanet
61c3379298 feat(xo-server): 5.19.8 2018-05-21 11:00:19 +02:00
Julien Fontanet
44866f3316 feat(xo-web): 5.19.6 2018-05-21 10:42:37 +02:00
Julien Fontanet
4bb8ce8779 feat(vhd-lib): 0.1.0 2018-05-21 10:03:37 +02:00
Julien Fontanet
58eb6a8b5f feat(xo-server): 5.19.7 2018-05-18 18:44:34 +02:00
Julien Fontanet
52f6a79e01 fix(xo-server/backupNg/logs): include merge/transfer size (#2965) 2018-05-18 18:44:07 +02:00
Julien Fontanet
129f79d44b feat(xo-web): 5.19.5 2018-05-18 18:42:31 +02:00
Julien Fontanet
385c3eb563 feat(xo-vmdk-to-vhd): 0.1.1 2018-05-18 18:40:26 +02:00
Julien Fontanet
e56be51b45 chore(xo-server/backups-ng): remove incorrect TODO 2018-05-18 17:14:50 +02:00
Olivier Lambert
24ae65b254 fix(xo-server/sr.createNfs): nfsVersion → nfsOptions (#2904) 2018-05-18 16:28:02 +02:00
badrAZ
d5dffbacbd fix(xo-web/FormattedDuration): handle duration < 0 seconds (#2964) 2018-05-18 15:06:23 +02:00
Julien Fontanet
c6ae969a82 fix(xo-server/https): ask for passphrase (#2963)
Fixes #2962
2018-05-18 15:05:49 +02:00
Nicolas Raynaud
005a9fdc01 fix(xo-vmdk-to-vhd): various bugs (#2961) 2018-05-18 14:02:19 +02:00
Jerome Charaoui
f505d4d911 Fix SR creation when using options or NFSv4 (#2960) 2018-05-17 22:12:09 +02:00
badrAZ
8ada6b121e fix(backup-ng/logs): handle the case when transfer duration equals 0 (#2954) 2018-05-17 16:58:29 +02:00
Julien Fontanet
b9a87efb0d fix(xo-server/backupNg): dont fail on corrupted VHDs (#2957)
Corrupted VHD files (usually uncleaned temporary) could fail the job.
2018-05-17 11:27:02 +02:00
Pierre Donias
89485a82d2 feat(xo-web): make many objects' UUID copiable (#2955)
Fixes #2925

- host/tab-network
- pool/tab-network
- vm/tab-disks
- vm/tab-network
- vm/tab-snapshots
2018-05-16 17:39:47 +02:00
Pierre Donias
451f87c6b4 feat(xo-web/servers): allow unauthorized cert. when adding server (#2953)
Fixes #2926
2018-05-16 13:44:27 +02:00
Rajaa.BARHTAOUI
c3cb5a3221 feat(xo-server,xo-web): VM HA options (#2946)
Fixes #2917
2018-05-16 13:27:40 +02:00
Julien Fontanet
458609ed2e feat(xo-server): 5.19.6 2018-05-16 10:32:59 +02:00
Julien Fontanet
fcec8113f3 fix(xo-server/backupNg): await writeStream (#2951) 2018-05-16 10:32:38 +02:00
Julien Fontanet
ebbd882ee4 feat(xo-web): 5.19.4 2018-05-15 17:44:25 +02:00
Julien Fontanet
0506e19a66 chore(xo-server/backups-ng): update todo list 2018-05-15 17:44:09 +02:00
Pierre Donias
ecc62e4f54 fix(xo-web/xosan): install packs button condition (#2950) 2018-05-15 17:40:40 +02:00
88 changed files with 3859 additions and 3167 deletions

View File

@@ -1,40 +1,56 @@
'use strict'
const PLUGINS_RE = /^(?:@babel\/plugin-.+|babel-plugin-lodash)$/
const PLUGINS_RE = /^(?:@babel\/|babel-)plugin-.+$/
const PRESETS_RE = /^@babel\/preset-.+$/
const NODE_ENV = process.env.NODE_ENV || 'development'
const __PROD__ = NODE_ENV === 'production'
const __TEST__ = NODE_ENV === 'test'
const configs = {
'@babel/plugin-proposal-decorators': {
legacy: true,
},
'@babel/preset-env' (pkg) {
return {
debug: !__TEST__,
loose: true,
shippedProposals: true,
targets: __PROD__
? (() => {
let node = (pkg.engines || {}).node
if (node !== undefined) {
const trimChars = '^=>~'
while (trimChars.includes(node[0])) {
node = node.slice(1)
}
return { node: node }
}
})()
: { browsers: '', node: 'current' },
useBuiltIns: '@babel/polyfill' in (pkg.dependencies || {}) && 'usage',
}
},
}
const getConfig = (key, ...args) => {
const config = configs[key]
return config === undefined
? {}
: typeof config === 'function'
? config(...args)
: config
}
module.exports = function (pkg, plugins, presets) {
plugins === undefined && (plugins = {})
presets === undefined && (presets = {})
presets['@babel/preset-env'] = {
debug: !__TEST__,
loose: true,
shippedProposals: true,
targets: __PROD__
? (() => {
let node = (pkg.engines || {}).node
if (node !== undefined) {
const trimChars = '^=>~'
while (trimChars.includes(node[0])) {
node = node.slice(1)
}
return { node: node }
}
})()
: { browsers: '', node: 'current' },
useBuiltIns: '@babel/polyfill' in (pkg.dependencies || {}) && 'usage',
}
Object.keys(pkg.devDependencies || {}).forEach(name => {
if (!(name in presets) && PLUGINS_RE.test(name)) {
plugins[name] = {}
plugins[name] = getConfig(name, pkg)
} else if (!(name in presets) && PRESETS_RE.test(name)) {
presets[name] = {}
presets[name] = getConfig(name, pkg)
}
})

View File

@@ -41,10 +41,10 @@
"moment-timezone": "^0.5.14"
},
"devDependencies": {
"@babel/cli": "7.0.0-beta.44",
"@babel/core": "7.0.0-beta.44",
"@babel/preset-env": "7.0.0-beta.44",
"@babel/preset-flow": "7.0.0-beta.44",
"@babel/cli": "7.0.0-beta.49",
"@babel/core": "7.0.0-beta.49",
"@babel/preset-env": "7.0.0-beta.49",
"@babel/preset-flow": "7.0.0-beta.49",
"cross-env": "^5.1.3",
"rimraf": "^2.6.2"
},

View File

@@ -1,6 +1,6 @@
{
"name": "@xen-orchestra/fs",
"version": "0.0.0",
"version": "0.0.1",
"license": "AGPL-3.0",
"description": "The File System for Xen Orchestra backups.",
"keywords": [],
@@ -20,10 +20,10 @@
"node": ">=6"
},
"dependencies": {
"@babel/runtime": "^7.0.0-beta.44",
"@babel/runtime": "^7.0.0-beta.49",
"@marsaud/smb2-promise": "^0.2.1",
"execa": "^0.10.0",
"fs-extra": "^5.0.0",
"fs-extra": "^6.0.1",
"get-stream": "^3.0.0",
"lodash": "^4.17.4",
"promise-toolbox": "^0.9.5",
@@ -32,12 +32,12 @@
"xo-remote-parser": "^0.3"
},
"devDependencies": {
"@babel/cli": "7.0.0-beta.44",
"@babel/core": "7.0.0-beta.44",
"@babel/plugin-proposal-function-bind": "7.0.0-beta.44",
"@babel/plugin-transform-runtime": "^7.0.0-beta.44",
"@babel/preset-env": "7.0.0-beta.44",
"@babel/preset-flow": "7.0.0-beta.44",
"@babel/cli": "7.0.0-beta.49",
"@babel/core": "7.0.0-beta.49",
"@babel/plugin-proposal-function-bind": "7.0.0-beta.49",
"@babel/plugin-transform-runtime": "^7.0.0-beta.49",
"@babel/preset-env": "7.0.0-beta.49",
"@babel/preset-flow": "7.0.0-beta.49",
"babel-plugin-lodash": "^3.3.2",
"cross-env": "^5.1.3",
"index-modules": "^0.3.0",

View File

@@ -126,7 +126,10 @@ export default class RemoteHandlerAbstract {
prependDir = false,
}: { filter?: (name: string) => boolean, prependDir?: boolean } = {}
): Promise<string[]> {
const entries = await this._list(dir)
let entries = await this._list(dir)
if (filter !== undefined) {
entries = entries.filter(filter)
}
if (prependDir) {
entries.forEach((entry, i) => {
@@ -134,7 +137,7 @@ export default class RemoteHandlerAbstract {
})
}
return filter === undefined ? entries : entries.filter(filter)
return entries
}
async _list (dir: string): Promise<string[]> {

16
PULL_REQUEST_TEMPLATE.md Normal file
View File

@@ -0,0 +1,16 @@
### Check list
- [ ] if UI changes, a screenshot has been added to the PR
- [ ] CHANGELOG updated
- [ ] documentation updated
### Process
1. create a PR as soon as possible
1. mark it as `WiP:` (Work in Progress) if not ready to be merged
1. when you want a review, add a reviewer
1. if necessary, update your PR, and readd a reviewer
### List of packages to release
> No need to mention xo-server and xo-web.

5
babel.config.js Normal file
View File

@@ -0,0 +1,5 @@
module.exports = {
// Necessary for jest to be able to find the `.babelrc.js` closest to the file
// instead of only the one in this directory.
babelrcRoots: true,
}

View File

@@ -0,0 +1,6 @@
declare module 'limit-concurrency-decorator' {
declare function limitConcurrencyDecorator(
concurrency: number
): <T: Function>(T) => T
declare export default typeof limitConcurrencyDecorator
}

View File

@@ -20,5 +20,10 @@ declare module 'lodash' {
iteratee: (V1, K) => V2
): { [K]: V2 }
declare export function noop(...args: mixed[]): void
declare export function some<T>(
collection: T[],
iteratee: (T, number) => boolean
): boolean
declare export function sum(values: number[]): number
declare export function values<K, V>(object: { [K]: V }): V[]
}

View File

@@ -1,8 +1,10 @@
{
"devDependencies": {
"@babel/register": "^7.0.0-beta.44",
"babel-7-jest": "^21.3.2",
"@babel/core": "^7.0.0-beta.49",
"@babel/register": "^7.0.0-beta.49",
"babel-core": "^7.0.0-0",
"babel-eslint": "^8.1.2",
"babel-jest": "^23.0.1",
"benchmark": "^2.1.4",
"eslint": "^4.14.0",
"eslint-config-standard": "^11.0.0-beta.0",
@@ -13,23 +15,22 @@
"eslint-plugin-react": "^7.6.1",
"eslint-plugin-standard": "^3.0.1",
"exec-promise": "^0.7.0",
"flow-bin": "^0.69.0",
"flow-bin": "^0.73.0",
"globby": "^8.0.0",
"husky": "^0.14.3",
"jest": "^22.0.4",
"jest": "^23.0.1",
"lodash": "^4.17.4",
"prettier": "^1.10.2",
"promise-toolbox": "^0.9.5",
"sorted-object": "^2.0.1"
},
"engines": {
"yarn": "^1.2.1"
"yarn": "^1.7.0"
},
"jest": {
"collectCoverage": true,
"projects": [
"<rootDir>",
"<rootDir>/packages/xo-web"
"<rootDir>"
],
"testEnvironment": "node",
"testPathIgnorePatterns": [
@@ -38,14 +39,6 @@
],
"testRegex": "\\.spec\\.js$",
"transform": {
"/@xen-orchestra/cron/.+\\.jsx?$": "babel-7-jest",
"/@xen-orchestra/fs/.+\\.jsx?$": "babel-7-jest",
"/packages/complex-matcher/.+\\.jsx?$": "babel-7-jest",
"/packages/value-matcher/.+\\.jsx?$": "babel-7-jest",
"/packages/vhd-lib/.+\\.jsx?$": "babel-7-jest",
"/packages/xo-cli/.+\\.jsx?$": "babel-7-jest",
"/packages/xo-server/.+\\.jsx?$": "babel-7-jest",
"/packages/xo-vmdk-to-vhd/.+\\.jsx?$": "babel-7-jest",
"\\.jsx?$": "babel-jest"
}
},

View File

@@ -30,9 +30,9 @@
"lodash": "^4.17.4"
},
"devDependencies": {
"@babel/cli": "7.0.0-beta.44",
"@babel/core": "7.0.0-beta.44",
"@babel/preset-env": "7.0.0-beta.44",
"@babel/cli": "7.0.0-beta.49",
"@babel/core": "7.0.0-beta.49",
"@babel/preset-env": "7.0.0-beta.49",
"babel-plugin-lodash": "^3.3.2",
"cross-env": "^5.1.1",
"rimraf": "^2.6.2"

View File

@@ -28,10 +28,10 @@
},
"dependencies": {},
"devDependencies": {
"@babel/cli": "7.0.0-beta.44",
"@babel/core": "7.0.0-beta.44",
"@babel/preset-env": "7.0.0-beta.44",
"@babel/preset-flow": "7.0.0-beta.44",
"@babel/cli": "7.0.0-beta.49",
"@babel/core": "7.0.0-beta.49",
"@babel/preset-env": "7.0.0-beta.49",
"@babel/preset-flow": "7.0.0-beta.49",
"cross-env": "^5.1.3",
"rimraf": "^2.6.2"
},

View File

@@ -0,0 +1,3 @@
module.exports = require('../../@xen-orchestra/babel-config')(
require('./package.json')
)

View File

@@ -23,21 +23,20 @@
"dist/"
],
"engines": {
"node": ">=4"
"node": ">=6"
},
"dependencies": {
"@xen-orchestra/fs": "^0.0.0",
"babel-runtime": "^6.22.0",
"@xen-orchestra/fs": "^0.0.1",
"exec-promise": "^0.7.0",
"struct-fu": "^1.2.0",
"vhd-lib": "^0.0.0"
"vhd-lib": "^0.1.1"
},
"devDependencies": {
"babel-cli": "^6.24.1",
"@babel/cli": "^7.0.0-beta.49",
"@babel/core": "^7.0.0-beta.49",
"@babel/plugin-transform-runtime": "^7.0.0-beta.49",
"@babel/preset-env": "^7.0.0-beta.49",
"babel-plugin-lodash": "^3.3.2",
"babel-plugin-transform-runtime": "^6.23.0",
"babel-preset-env": "^1.5.2",
"babel-preset-stage-3": "^6.24.1",
"cross-env": "^5.1.3",
"execa": "^0.10.0",
"index-modules": "^0.3.0",
@@ -51,22 +50,5 @@
"prebuild": "rimraf dist/ && index-modules --cjs-lazy src/commands",
"predev": "yarn run prebuild",
"prepare": "yarn run build"
},
"babel": {
"plugins": [
"lodash",
"transform-runtime"
],
"presets": [
[
"env",
{
"targets": {
"node": 4
}
}
],
"stage-3"
]
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "vhd-lib",
"version": "0.0.0",
"version": "0.1.1",
"license": "AGPL-3.0",
"description": "Primitives for VHD file handling",
"keywords": [],
@@ -20,30 +20,30 @@
"node": ">=6"
},
"dependencies": {
"@babel/runtime": "^7.0.0-beta.44",
"@xen-orchestra/fs": "^0.0.0",
"@babel/runtime": "^7.0.0-beta.49",
"async-iterator-to-stream": "^1.0.2",
"execa": "^0.10.0",
"from2": "^2.3.0",
"fs-extra": "^5.0.0",
"fs-extra": "^6.0.1",
"get-stream": "^3.0.0",
"limit-concurrency-decorator": "^0.4.0",
"promise-toolbox": "^0.9.5",
"struct-fu": "^1.2.0",
"uuid": "^3.0.1",
"tmp": "^0.0.33"
"uuid": "^3.0.1"
},
"devDependencies": {
"@babel/cli": "7.0.0-beta.44",
"@babel/core": "7.0.0-beta.44",
"@babel/plugin-transform-runtime": "^7.0.0-beta.44",
"@babel/preset-env": "7.0.0-beta.44",
"@babel/preset-flow": "7.0.0-beta.44",
"@babel/cli": "7.0.0-beta.49",
"@babel/core": "7.0.0-beta.49",
"@babel/plugin-transform-runtime": "^7.0.0-beta.49",
"@babel/preset-env": "7.0.0-beta.49",
"@babel/preset-flow": "7.0.0-beta.49",
"@xen-orchestra/fs": "^0.0.1",
"babel-plugin-lodash": "^3.3.2",
"cross-env": "^5.1.3",
"execa": "^0.10.0",
"fs-promise": "^2.0.0",
"index-modules": "^0.3.0",
"rimraf": "^2.6.2"
"rimraf": "^2.6.2",
"tmp": "^0.0.33"
},
"scripts": {
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",

View File

@@ -1,3 +1,4 @@
import assert from 'assert'
import asyncIteratorToStream from 'async-iterator-to-stream'
import computeGeometryForSize from './_computeGeometryForSize'
@@ -25,62 +26,16 @@ function createBAT (
bat,
bitmapSize
) {
const vhdOccupationTable = []
let currentVhdPositionSector = firstBlockPosition / SECTOR_SIZE
blockAddressList.forEach(blockPosition => {
const scaled = blockPosition / VHD_BLOCK_SIZE_BYTES
const vhdTableIndex = Math.floor(scaled)
assert.strictEqual(blockPosition % 512, 0)
const vhdTableIndex = Math.floor(blockPosition / VHD_BLOCK_SIZE_BYTES)
if (bat.readUInt32BE(vhdTableIndex * 4) === BLOCK_UNUSED) {
bat.writeUInt32BE(currentVhdPositionSector, vhdTableIndex * 4)
currentVhdPositionSector +=
(bitmapSize + VHD_BLOCK_SIZE_BYTES) / SECTOR_SIZE
}
// not using bit operators to avoid the int32 coercion, that way we can go to 53 bits
vhdOccupationTable[vhdTableIndex] =
(vhdOccupationTable[vhdTableIndex] || 0) +
Math.pow(2, (scaled % 1) * ratio)
})
return vhdOccupationTable
}
function createBitmap (bitmapSize, ratio, vhdOccupationBucket) {
const bitmap = Buffer.alloc(bitmapSize)
for (let i = 0; i < VHD_BLOCK_SIZE_SECTORS / ratio; i++) {
// do not shift to avoid int32 coercion
if ((vhdOccupationBucket * Math.pow(2, -i)) & 1) {
for (let j = 0; j < ratio; j++) {
setBitmap(bitmap, i * ratio + j)
}
}
}
return bitmap
}
function * yieldIfNotEmpty (buffer) {
if (buffer.length > 0) {
yield buffer
}
}
async function * generateFileContent (
blockIterator,
bitmapSize,
ratio,
vhdOccupationTable
) {
let currentVhdBlockIndex = -1
let currentBlockBuffer = Buffer.alloc(0)
for await (const next of blockIterator) {
const batEntry = Math.floor(next.offsetBytes / VHD_BLOCK_SIZE_BYTES)
if (batEntry !== currentVhdBlockIndex) {
yield * yieldIfNotEmpty(currentBlockBuffer)
currentBlockBuffer = Buffer.alloc(VHD_BLOCK_SIZE_BYTES)
currentVhdBlockIndex = batEntry
yield createBitmap(bitmapSize, ratio, vhdOccupationTable[batEntry])
}
next.data.copy(currentBlockBuffer, next.offsetBytes % VHD_BLOCK_SIZE_BYTES)
}
yield * yieldIfNotEmpty(currentBlockBuffer)
}
export default asyncIteratorToStream(async function * (
@@ -123,21 +78,49 @@ export default asyncIteratorToStream(async function * (
const bitmapSize =
Math.ceil(VHD_BLOCK_SIZE_SECTORS / 8 / SECTOR_SIZE) * SECTOR_SIZE
const bat = Buffer.alloc(tablePhysicalSizeBytes, 0xff)
const vhdOccupationTable = createBAT(
firstBlockPosition,
blockAddressList,
ratio,
bat,
bitmapSize
)
yield footer
yield header
yield bat
yield * generateFileContent(
blockIterator,
bitmapSize,
ratio,
vhdOccupationTable
)
yield footer
createBAT(firstBlockPosition, blockAddressList, ratio, bat, bitmapSize)
let position = 0
function * yieldAndTrack (buffer, expectedPosition) {
if (expectedPosition !== undefined) {
assert.strictEqual(position, expectedPosition)
}
if (buffer.length > 0) {
yield buffer
position += buffer.length
}
}
async function * generateFileContent (blockIterator, bitmapSize, ratio) {
let currentBlock = -1
let currentVhdBlockIndex = -1
let currentBlockWithBitmap = Buffer.alloc(0)
for await (const next of blockIterator) {
currentBlock++
assert.strictEqual(blockAddressList[currentBlock], next.offsetBytes)
const batIndex = Math.floor(next.offsetBytes / VHD_BLOCK_SIZE_BYTES)
if (batIndex !== currentVhdBlockIndex) {
if (currentVhdBlockIndex >= 0) {
yield * yieldAndTrack(
currentBlockWithBitmap,
bat.readUInt32BE(currentVhdBlockIndex * 4) * 512
)
}
currentBlockWithBitmap = Buffer.alloc(bitmapSize + VHD_BLOCK_SIZE_BYTES)
currentVhdBlockIndex = batIndex
}
const blockOffset = (next.offsetBytes / 512) % VHD_BLOCK_SIZE_SECTORS
for (let bitPos = 0; bitPos < VHD_BLOCK_SIZE_SECTORS / ratio; bitPos++) {
setBitmap(currentBlockWithBitmap, blockOffset + bitPos)
}
next.data.copy(
currentBlockWithBitmap,
bitmapSize + next.offsetBytes % VHD_BLOCK_SIZE_BYTES
)
}
yield * yieldAndTrack(currentBlockWithBitmap)
}
yield * yieldAndTrack(footer, 0)
yield * yieldAndTrack(header, FOOTER_SIZE)
yield * yieldAndTrack(bat, FOOTER_SIZE + HEADER_SIZE)
yield * generateFileContent(blockIterator, bitmapSize, ratio)
yield * yieldAndTrack(footer)
})

View File

@@ -102,15 +102,15 @@ test('ReadableSparseVHDStream can handle a sparse file', async () => {
data: Buffer.alloc(blockSize, 'azerzaerazeraze', 'ascii'),
},
{
offsetBytes: blockSize * 5,
offsetBytes: blockSize * 100,
data: Buffer.alloc(blockSize, 'gdfslkdfguer', 'ascii'),
},
]
const fileSize = blockSize * 10
const fileSize = blockSize * 110
const stream = createReadableSparseVHDStream(
fileSize,
blockSize,
[100, 700],
blocks.map(b => b.offsetBytes),
blocks
)
const pipe = stream.pipe(createWriteStream('output.vhd'))

View File

@@ -476,12 +476,12 @@ export default class Vhd {
// For each sector of block data...
const { sectorsPerBlock } = child
let parentBitmap = null
for (let i = 0; i < sectorsPerBlock; i++) {
// If no changes on one sector, skip.
if (!mapTestBit(bitmap, i)) {
continue
}
let parentBitmap = null
let endSector = i + 1
// Count changed sectors.

View File

@@ -1,6 +1,6 @@
{
"name": "xo-acl-resolver",
"version": "0.2.3",
"version": "0.2.4",
"license": "ISC",
"description": "Xen-Orchestra internal: do ACLs resolution",
"keywords": [],

View File

@@ -50,7 +50,9 @@ const checkAuthorizationByTypes = {
network: or(checkSelf, checkMember('$pool')),
SR: or(checkSelf, checkMember('$pool')),
PIF: checkMember('$host'),
SR: or(checkSelf, checkMember('$container')),
task: checkMember('$host'),

View File

@@ -28,7 +28,7 @@
"node": ">=6"
},
"dependencies": {
"@babel/polyfill": "7.0.0-beta.44",
"@babel/polyfill": "7.0.0-beta.49",
"bluebird": "^3.5.1",
"chalk": "^2.2.0",
"event-to-promise": "^0.8.0",
@@ -49,10 +49,10 @@
"xo-lib": "^0.9.0"
},
"devDependencies": {
"@babel/cli": "7.0.0-beta.44",
"@babel/core": "7.0.0-beta.44",
"@babel/preset-env": "7.0.0-beta.44",
"@babel/preset-flow": "7.0.0-beta.44",
"@babel/cli": "7.0.0-beta.49",
"@babel/core": "7.0.0-beta.49",
"@babel/preset-env": "7.0.0-beta.49",
"@babel/preset-flow": "7.0.0-beta.49",
"babel-plugin-lodash": "^3.3.2",
"cross-env": "^5.1.3",
"rimraf": "^2.6.2"

View File

@@ -0,0 +1,3 @@
module.exports = require('../../@xen-orchestra/babel-config')(
require('./package.json')
)

View File

@@ -25,17 +25,16 @@
"node": ">=4"
},
"dependencies": {
"babel-runtime": "^6.18.0",
"@babel/runtime": "^7.0.0-beta.49",
"kindof": "^2.0.0",
"lodash": "^4.17.2",
"make-error": "^1.0.2"
},
"devDependencies": {
"babel-cli": "^6.24.1",
"babel-plugin-lodash": "^3.3.2",
"babel-plugin-transform-runtime": "^6.23.0",
"babel-preset-env": "^1.5.2",
"babel-preset-stage-3": "^6.24.1",
"@babel/cli": "^7.0.0-beta.49",
"@babel/core": "^7.0.0-beta.49",
"@babel/plugin-transform-runtime": "^7.0.0-beta.49",
"@babel/preset-env": "^7.0.0-beta.49",
"cross-env": "^5.1.3",
"event-to-promise": "^0.8.0",
"rimraf": "^2.6.1"
@@ -46,22 +45,5 @@
"prebuild": "rimraf dist/",
"predev": "yarn run prebuild",
"prepublishOnly": "yarn run build"
},
"babel": {
"plugins": [
"lodash",
"transform-runtime"
],
"presets": [
[
"env",
{
"targets": {
"node": 4
}
}
],
"stage-3"
]
}
}

View File

@@ -0,0 +1,3 @@
module.exports = require('../../@xen-orchestra/babel-config')(
require('./package.json')
)

View File

@@ -27,10 +27,10 @@
"lodash": "^4.13.1"
},
"devDependencies": {
"babel-cli": "^6.24.1",
"@babel/cli": "^7.0.0-beta.49",
"@babel/core": "^7.0.0-beta.49",
"@babel/preset-env": "^7.0.0-beta.49",
"babel-plugin-lodash": "^3.3.2",
"babel-preset-env": "^1.5.2",
"babel-preset-stage-3": "^6.24.1",
"cross-env": "^5.1.3",
"deep-freeze": "^0.0.1",
"rimraf": "^2.6.1"
@@ -41,22 +41,5 @@
"prebuild": "rimraf dist/",
"predev": "yarn run prebuild",
"prepare": "yarn run build"
},
"babel": {
"plugins": [
"lodash"
],
"presets": [
[
"env",
{
"targets": {
"browsers": "> 5%",
"node": 4
}
}
],
"stage-3"
]
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "xo-server-backup-reports",
"version": "0.11.0",
"version": "0.12.0",
"license": "AGPL-3.0",
"description": "Backup reports plugin for XO-Server",
"keywords": [

View File

@@ -1,7 +1,6 @@
import humanFormat from 'human-format'
import moment from 'moment-timezone'
import { find, forEach, get, startCase } from 'lodash'
import { forEach, get, startCase } from 'lodash'
import pkg from '../package'
export const configurationSchema = {
@@ -37,6 +36,12 @@ const ICON_FAILURE = '🚨'
const ICON_SKIPPED = '⏩'
const ICON_SUCCESS = '✔'
const STATUS_ICON = {
skipped: ICON_SKIPPED,
success: ICON_SUCCESS,
failure: ICON_FAILURE,
}
const DATE_FORMAT = 'dddd, MMMM Do YYYY, h:mm:ss a'
const createDateFormater = timezone =>
timezone !== undefined
@@ -95,43 +100,41 @@ class BackupReportsXoPlugin {
this._xo.removeListener('job:terminated', this._report)
}
_wrapper (status, job, schedule) {
_wrapper (status, job, schedule, runJobId) {
return new Promise(resolve =>
resolve(
job.type === 'backup'
? this._backupNgListener(status, job, schedule)
: this._listener(status, job, schedule)
? this._backupNgListener(status, job, schedule, runJobId)
: this._listener(status, job, schedule, runJobId)
)
).catch(logError)
}
async _backupNgListener (runJobId, _, { timezone }) {
async _backupNgListener (_1, _2, { timezone }, runJobId) {
const xo = this._xo
const logs = await xo.getBackupNgLogs(runJobId)
const jobLog = logs['roots'][0]
const vmsTaskLog = logs[jobLog.id]
const log = await xo.getBackupNgLogs(runJobId)
const { reportWhen, mode } = jobLog.data || {}
const { reportWhen, mode } = log.data || {}
if (reportWhen === 'never') {
return
}
const formatDate = createDateFormater(timezone)
const jobName = (await xo.getJob(jobLog.jobId, 'backup')).name
if (jobLog.error !== undefined) {
const [globalStatus, icon] =
jobLog.error.message === NO_VMS_MATCH_THIS_PATTERN
? ['Skipped', ICON_SKIPPED]
: ['Failure', ICON_FAILURE]
if (log.status === 'success' && reportWhen === 'failure') {
return
}
const jobName = (await xo.getJob(log.jobId, 'backup')).name
if (log.result !== undefined) {
let markdown = [
`## Global status: ${globalStatus}`,
`## Global status: ${log.status}`,
'',
`- **mode**: ${mode}`,
`- **Start time**: ${formatDate(jobLog.start)}`,
`- **End time**: ${formatDate(jobLog.end)}`,
`- **Duration**: ${formatDuration(jobLog.duration)}`,
`- **Error**: ${jobLog.error.message}`,
`- **Start time**: ${formatDate(log.start)}`,
`- **End time**: ${formatDate(log.end)}`,
`- **Duration**: ${formatDuration(log.end - log.start)}`,
`- **Error**: ${log.result.message}`,
'---',
'',
`*${pkg.name} v${pkg.version}*`,
@@ -139,12 +142,14 @@ class BackupReportsXoPlugin {
markdown = markdown.join('\n')
return this._sendReport({
subject: `[Xen Orchestra] ${globalStatus} Backup report for ${jobName} ${icon}`,
subject: `[Xen Orchestra] ${
log.status
} Backup report for ${jobName} ${STATUS_ICON[log.status]}`,
markdown,
nagiosStatus: 2,
nagiosMarkdown: `[Xen Orchestra] [${globalStatus}] Backup report for ${jobName} - Error : ${
jobLog.error.message
}`,
nagiosMarkdown: `[Xen Orchestra] [${
log.status
}] Backup report for ${jobName} - Error : ${log.result.message}`,
})
}
@@ -157,14 +162,12 @@ class BackupReportsXoPlugin {
let globalTransferSize = 0
let nFailures = 0
let nSkipped = 0
for (const vmTaskLog of vmsTaskLog || []) {
const vmTaskStatus = vmTaskLog.status
if (vmTaskStatus === 'success' && reportWhen === 'failure') {
for (const taskLog of log.tasks) {
if (taskLog.status === 'success' && reportWhen === 'failure') {
return
}
const vmId = vmTaskLog.data.id
const vmId = taskLog.data.id
let vm
try {
vm = xo.getObject(vmId)
@@ -173,132 +176,143 @@ class BackupReportsXoPlugin {
`### ${vm !== undefined ? vm.name_label : 'VM not found'}`,
'',
`- **UUID**: ${vm !== undefined ? vm.uuid : vmId}`,
`- **Start time**: ${formatDate(vmTaskLog.start)}`,
`- **End time**: ${formatDate(vmTaskLog.end)}`,
`- **Duration**: ${formatDuration(vmTaskLog.duration)}`,
`- **Start time**: ${formatDate(taskLog.start)}`,
`- **End time**: ${formatDate(taskLog.end)}`,
`- **Duration**: ${formatDuration(taskLog.end - taskLog.start)}`,
]
const failedSubTasks = []
const operationsText = []
const snapshotText = []
const srsText = []
const remotesText = []
for (const subTaskLog of logs[vmTaskLog.taskId] || []) {
const { data, status, result, message } = subTaskLog
const icon =
subTaskLog.status === 'success' ? ICON_SUCCESS : ICON_FAILURE
const errorMessage = ` **Error**: ${get(result, 'message')}`
if (message === 'snapshot') {
operationsText.push(`- **Snapshot** ${icon}`)
if (status === 'failure') {
failedSubTasks.push('Snapshot')
operationsText.push('', errorMessage)
}
} else if (data.type === 'remote') {
const remoteId = data.id
const remote = await xo.getRemote(remoteId).catch(() => {})
remotesText.push(
`- **${
remote !== undefined ? remote.name : `Remote Not found`
}** (${remoteId}) ${icon}`
for (const subTaskLog of taskLog.tasks || []) {
const icon = STATUS_ICON[subTaskLog.status]
const errorMessage = ` - **Error**: ${get(
subTaskLog.result,
'message'
)}`
if (subTaskLog.message === 'snapshot') {
snapshotText.push(
`- **Snapshot** ${icon}`,
` - **Start time**: ${formatDate(subTaskLog.start)}`,
` - **End time**: ${formatDate(subTaskLog.end)}`
)
if (status === 'failure') {
failedSubTasks.push(remote !== undefined ? remote.name : remoteId)
} else if (subTaskLog.data.type === 'remote') {
const id = subTaskLog.data.id
const remote = await xo.getRemote(id).catch(() => {})
remotesText.push(
` - **${
remote !== undefined ? remote.name : `Remote Not found`
}** (${id}) ${icon}`,
` - **Start time**: ${formatDate(subTaskLog.start)}`,
` - **End time**: ${formatDate(subTaskLog.end)}`,
` - **Duration**: ${formatDuration(
subTaskLog.end - subTaskLog.start
)}`
)
if (subTaskLog.status === 'failure') {
failedSubTasks.push(remote !== undefined ? remote.name : id)
remotesText.push('', errorMessage)
}
} else {
const srId = data.id
const id = subTaskLog.data.id
let sr
try {
sr = xo.getObject(srId)
sr = xo.getObject(id)
} catch (e) {}
const [srName, srUuid] =
sr !== undefined ? [sr.name_label, sr.uuid] : [`SR Not found`, srId]
srsText.push(`- **${srName}** (${srUuid}) ${icon}`)
if (status === 'failure') {
failedSubTasks.push(sr !== undefined ? sr.name_label : srId)
sr !== undefined ? [sr.name_label, sr.uuid] : [`SR Not found`, id]
srsText.push(
` - **${srName}** (${srUuid}) ${icon}`,
` - **Start time**: ${formatDate(subTaskLog.start)}`,
` - **End time**: ${formatDate(subTaskLog.end)}`,
` - **Duration**: ${formatDuration(
subTaskLog.end - subTaskLog.start
)}`
)
if (subTaskLog.status === 'failure') {
failedSubTasks.push(sr !== undefined ? sr.name_label : id)
srsText.push('', errorMessage)
}
}
forEach(subTaskLog.tasks, operationLog => {
const size = operationLog.result.size
if (operationLog.message === 'merge') {
globalMergeSize += size
} else {
globalTransferSize += size
}
const operationText = [
` - **${operationLog.message}** ${
STATUS_ICON[operationLog.status]
}`,
` - **Start time**: ${formatDate(operationLog.start)}`,
` - **End time**: ${formatDate(operationLog.end)}`,
` - **Duration**: ${formatDuration(
operationLog.end - operationLog.start
)}`,
operationLog.status === 'failure'
? `- **Error**: ${get(operationLog.result, 'message')}`
: ` - **Size**: ${formatSize(size)}`,
` - **Speed**: ${formatSpeed(
size,
operationLog.end - operationLog.start
)}`,
].join('\n')
if (get(subTaskLog, 'data.type') === 'remote') {
remotesText.push(operationText)
remotesText.join('\n')
}
if (get(subTaskLog, 'data.type') === 'SR') {
srsText.push(operationText)
srsText.join('\n')
}
})
}
if (operationsText.length !== 0) {
operationsText.unshift(`#### Operations`, '')
}
if (srsText.length !== 0) {
srsText.unshift(`#### SRs`, '')
srsText.unshift(`- **SRs**`)
}
if (remotesText.length !== 0) {
remotesText.unshift(`#### remotes`, '')
remotesText.unshift(`- **Remotes**`)
}
const subText = [...operationsText, '', ...srsText, '', ...remotesText]
const result = vmTaskLog.result
if (vmTaskStatus === 'failure' && result !== undefined) {
const { message } = result
if (isSkippedError(result)) {
const subText = [...snapshotText, '', ...srsText, '', ...remotesText]
if (taskLog.result !== undefined) {
if (taskLog.status === 'skipped') {
++nSkipped
skippedVmsText.push(
...text,
`- **Reason**: ${
message === UNHEALTHY_VDI_CHAIN_ERROR
taskLog.result.message === UNHEALTHY_VDI_CHAIN_ERROR
? UNHEALTHY_VDI_CHAIN_MESSAGE
: message
: taskLog.result.message
}`,
''
)
nagiosText.push(
`[(Skipped) ${
vm !== undefined ? vm.name_label : 'undefined'
} : ${message} ]`
`[(Skipped) ${vm !== undefined ? vm.name_label : 'undefined'} : ${
taskLog.result.message
} ]`
)
} else {
++nFailures
failedVmsText.push(...text, `- **Error**: ${message}`, '')
failedVmsText.push(
...text,
`- **Error**: ${taskLog.result.message}`,
''
)
nagiosText.push(
`[(Failed) ${
vm !== undefined ? vm.name_label : 'undefined'
} : ${message} ]`
`[(Failed) ${vm !== undefined ? vm.name_label : 'undefined'} : ${
taskLog.result.message
} ]`
)
}
} else {
let transferSize, transferDuration, mergeSize, mergeDuration
forEach(logs[vmTaskLog.taskId], ({ taskId }) => {
if (transferSize !== undefined) {
return false
}
const transferTask = find(logs[taskId], { message: 'transfer' })
if (transferTask !== undefined) {
transferSize = transferTask.result.size
transferDuration = transferTask.end - transferTask.start
}
const mergeTask = find(logs[taskId], { message: 'merge' })
if (mergeTask !== undefined) {
mergeSize = mergeTask.result.size
mergeDuration = mergeTask.end - mergeTask.start
}
})
if (transferSize !== undefined) {
globalTransferSize += transferSize
text.push(
`- **Transfer size**: ${formatSize(transferSize)}`,
`- **Transfer speed**: ${formatSpeed(
transferSize,
transferDuration
)}`
)
}
if (mergeSize !== undefined) {
globalMergeSize += mergeSize
text.push(
`- **Merge size**: ${formatSize(mergeSize)}`,
`- **Merge speed**: ${formatSpeed(mergeSize, mergeDuration)}`
)
}
if (vmTaskStatus === 'failure') {
if (taskLog.status === 'failure') {
++nFailures
failedVmsText.push(...text, '', '', ...subText, '')
nagiosText.push(
@@ -311,23 +325,16 @@ class BackupReportsXoPlugin {
}
}
}
const globalSuccess = nFailures === 0 && nSkipped === 0
if (reportWhen === 'failure' && globalSuccess) {
return
}
const nVms = vmsTaskLog.length
const nVms = log.tasks.length
const nSuccesses = nVms - nFailures - nSkipped
const globalStatus = globalSuccess
? `Success`
: nFailures !== 0 ? `Failure` : `Skipped`
let markdown = [
`## Global status: ${globalStatus}`,
`## Global status: ${log.status}`,
'',
`- **mode**: ${mode}`,
`- **Start time**: ${formatDate(jobLog.start)}`,
`- **End time**: ${formatDate(jobLog.end)}`,
`- **Duration**: ${formatDuration(jobLog.duration)}`,
`- **Start time**: ${formatDate(log.start)}`,
`- **End time**: ${formatDate(log.end)}`,
`- **Duration**: ${formatDuration(log.start - log.end)}`,
`- **Successes**: ${nSuccesses} / ${nVms}`,
]
@@ -367,17 +374,16 @@ class BackupReportsXoPlugin {
markdown = markdown.join('\n')
return this._sendReport({
markdown,
subject: `[Xen Orchestra] ${globalStatus} Backup report for ${jobName} ${
globalSuccess
? ICON_SUCCESS
: nFailures !== 0 ? ICON_FAILURE : ICON_SKIPPED
subject: `[Xen Orchestra] ${log.status} Backup report for ${jobName} ${
STATUS_ICON[log.status]
}`,
nagiosStatus: globalSuccess ? 0 : 2,
nagiosMarkdown: globalSuccess
? `[Xen Orchestra] [Success] Backup report for ${jobName}`
: `[Xen Orchestra] [${
nFailures !== 0 ? 'Failure' : 'Skipped'
}] Backup report for ${jobName} - VMs : ${nagiosText.join(' ')}`,
nagiosStatus: log.status === 'success' ? 0 : 2,
nagiosMarkdown:
log.status === 'success'
? `[Xen Orchestra] [Success] Backup report for ${jobName}`
: `[Xen Orchestra] [${
nFailures !== 0 ? 'Failure' : 'Skipped'
}] Backup report for ${jobName} - VMs : ${nagiosText.join(' ')}`,
})
}
@@ -567,7 +573,9 @@ class BackupReportsXoPlugin {
const nSuccesses = nCalls - nFailures - nSkipped
const globalStatus = globalSuccess
? `Success`
: nFailures !== 0 ? `Failure` : `Skipped`
: nFailures !== 0
? `Failure`
: `Skipped`
let markdown = [
`## Global status: ${globalStatus}`,
@@ -625,7 +633,9 @@ class BackupReportsXoPlugin {
subject: `[Xen Orchestra] ${globalStatus} Backup report for ${tag} ${
globalSuccess
? ICON_SUCCESS
: nFailures !== 0 ? ICON_FAILURE : ICON_SKIPPED
: nFailures !== 0
? ICON_FAILURE
: ICON_SKIPPED
}`,
nagiosStatus: globalSuccess ? 0 : 2,
nagiosMarkdown: globalSuccess

View File

@@ -26,10 +26,10 @@
"lodash": "^4.17.4"
},
"devDependencies": {
"@babel/cli": "7.0.0-beta.44",
"@babel/core": "7.0.0-beta.44",
"@babel/preset-env": "7.0.0-beta.44",
"@babel/preset-flow": "^7.0.0-beta.44",
"@babel/cli": "7.0.0-beta.49",
"@babel/core": "7.0.0-beta.49",
"@babel/preset-env": "7.0.0-beta.49",
"@babel/preset-flow": "^7.0.0-beta.49",
"babel-plugin-lodash": "^3.3.2",
"cross-env": "^5.1.3",
"rimraf": "^2.6.2"

View File

@@ -139,8 +139,8 @@ Handlebars.registerHelper(
new Handlebars.SafeString(
isFinite(+value) && +value !== 0
? (value = round(value, 2)) > 0
? `(<b style="color: green;">▲ ${value}</b>)`
: `(<b style="color: red;">▼ ${String(value).slice(1)}</b>)`
? `(<b style="color: green;">▲ ${value}%</b>)`
: `(<b style="color: red;">▼ ${String(value).slice(1)}%</b>)`
: ''
)
)
@@ -270,12 +270,16 @@ async function getHostsStats ({ runningHosts, xo }) {
function getSrsStats (xoObjects) {
return orderBy(
map(filter(xoObjects, { type: 'SR' }), sr => {
map(filter(xoObjects, obj => obj.type === 'SR' && obj.size > 0), sr => {
const total = sr.size / gibPower
const used = sr.physical_usage / gibPower
let name = sr.name_label
if (!sr.shared) {
name += ` (${find(xoObjects, { id: sr.$container }).name_label})`
}
return {
uuid: sr.uuid,
name: sr.name_label,
name,
total,
used,
free: total - used,

View File

@@ -1,6 +1,6 @@
{
"name": "xo-server",
"version": "5.19.5",
"version": "5.19.9",
"license": "AGPL-3.0",
"description": "Server part of Xen-Orchestra",
"keywords": [
@@ -31,15 +31,15 @@
"node": ">=6"
},
"dependencies": {
"@babel/polyfill": "7.0.0-beta.44",
"@babel/polyfill": "7.0.0-beta.49",
"@marsaud/smb2-promise": "^0.2.1",
"@xen-orchestra/cron": "^1.0.3",
"@xen-orchestra/fs": "^0.0.0",
"@xen-orchestra/fs": "^0.0.1",
"ajv": "^6.1.1",
"app-conf": "^0.5.0",
"archiver": "^2.1.0",
"async-iterator-to-stream": "^1.0.1",
"base64url": "^2.0.0",
"base64url": "^3.0.0",
"bind-property-descriptor": "^1.0.0",
"blocked": "^1.2.1",
"bluebird": "^3.5.1",
@@ -59,10 +59,10 @@
"express-session": "^1.15.6",
"fatfs": "^0.10.4",
"from2": "^2.3.0",
"fs-extra": "^5.0.0",
"fs-extra": "^6.0.1",
"get-stream": "^3.0.0",
"golike-defer": "^0.4.1",
"hashy": "^0.6.2",
"hashy": "^0.7.1",
"helmet": "^3.9.0",
"highland": "^2.11.1",
"http-proxy": "^1.16.2",
@@ -70,14 +70,14 @@
"http-server-plus": "^0.10.0",
"human-format": "^0.10.0",
"is-redirect": "^1.0.0",
"jest-worker": "^22.4.3",
"jest-worker": "^23.0.0",
"js-yaml": "^3.10.0",
"json-rpc-peer": "^0.15.3",
"json5": "^1.0.0",
"julien-f-source-map-support": "0.1.0",
"julien-f-unzip": "^0.2.1",
"kindof": "^2.0.0",
"level": "^3.0.0",
"level": "^4.0.0",
"level-party": "^3.0.4",
"level-sublevel": "^6.6.1",
"limit-concurrency-decorator": "^0.4.0",
@@ -93,16 +93,16 @@
"partial-stream": "0.0.0",
"passport": "^0.4.0",
"passport-local": "^1.0.0",
"pretty-format": "^22.0.3",
"pretty-format": "^23.0.0",
"promise-toolbox": "^0.9.5",
"proxy-agent": "^2.1.0",
"proxy-agent": "^3.0.0",
"pug": "^2.0.0-rc.4",
"pw": "^0.0.4",
"redis": "^2.8.0",
"schema-inspector": "^1.6.8",
"semver": "^5.4.1",
"serve-static": "^1.13.1",
"split-lines": "^1.1.0",
"split-lines": "^2.0.0",
"stack-chain": "^2.0.0",
"stoppable": "^1.0.5",
"struct-fu": "^1.2.0",
@@ -111,29 +111,29 @@
"tmp": "^0.0.33",
"uuid": "^3.0.1",
"value-matcher": "^0.2.0",
"vhd-lib": "^0.0.0",
"vhd-lib": "^0.1.1",
"ws": "^5.0.0",
"xen-api": "^0.16.9",
"xml2js": "^0.4.19",
"xo-acl-resolver": "^0.2.3",
"xo-acl-resolver": "^0.2.4",
"xo-collection": "^0.4.1",
"xo-common": "^0.1.1",
"xo-remote-parser": "^0.3",
"xo-vmdk-to-vhd": "0.1.0",
"xo-vmdk-to-vhd": "^0.1.2",
"yazl": "^2.4.3"
},
"devDependencies": {
"@babel/cli": "7.0.0-beta.44",
"@babel/core": "7.0.0-beta.44",
"@babel/plugin-proposal-decorators": "7.0.0-beta.44",
"@babel/plugin-proposal-export-default-from": "7.0.0-beta.44",
"@babel/plugin-proposal-export-namespace-from": "7.0.0-beta.44",
"@babel/plugin-proposal-function-bind": "7.0.0-beta.44",
"@babel/plugin-proposal-optional-chaining": "^7.0.0-beta.44",
"@babel/plugin-proposal-pipeline-operator": "^7.0.0-beta.44",
"@babel/plugin-proposal-throw-expressions": "^7.0.0-beta.44",
"@babel/preset-env": "7.0.0-beta.44",
"@babel/preset-flow": "7.0.0-beta.44",
"@babel/cli": "7.0.0-beta.49",
"@babel/core": "7.0.0-beta.49",
"@babel/plugin-proposal-decorators": "7.0.0-beta.49",
"@babel/plugin-proposal-export-default-from": "7.0.0-beta.49",
"@babel/plugin-proposal-export-namespace-from": "7.0.0-beta.49",
"@babel/plugin-proposal-function-bind": "7.0.0-beta.49",
"@babel/plugin-proposal-optional-chaining": "^7.0.0-beta.49",
"@babel/plugin-proposal-pipeline-operator": "^7.0.0-beta.49",
"@babel/plugin-proposal-throw-expressions": "^7.0.0-beta.49",
"@babel/preset-env": "7.0.0-beta.49",
"@babel/preset-flow": "7.0.0-beta.49",
"babel-plugin-lodash": "^3.3.2",
"cross-env": "^5.1.3",
"index-modules": "^0.3.0",

View File

@@ -1,4 +1,5 @@
import { basename } from 'path'
import { isEmpty, pickBy } from 'lodash'
import { safeDateFormat } from '../utils'
@@ -117,8 +118,8 @@ getJob.params = {
},
}
export async function runJob ({ id, schedule }) {
return this.runJobSequence([id], await this.getSchedule(schedule))
export async function runJob ({ id, schedule, vm }) {
return this.runJobSequence([id], await this.getSchedule(schedule), vm)
}
runJob.permission = 'admin'
@@ -130,12 +131,17 @@ runJob.params = {
schedule: {
type: 'string',
},
vm: {
type: 'string',
optional: true,
},
}
// -----------------------------------------------------------------------------
export function getAllLogs () {
return this.getBackupNgLogs()
export async function getAllLogs (filter) {
const logs = await this.getBackupNgLogs()
return isEmpty(filter) ? logs : pickBy(logs, filter)
}
getAllLogs.permission = 'admin'

View File

@@ -76,6 +76,21 @@ export { restartAgent as restart_agent } // eslint-disable-line camelcase
// -------------------------------------------------------------------
export function setRemoteSyslogHost ({ host, syslogDestination }) {
return this.getXapi(host).setRemoteSyslogHost(host._xapiId, syslogDestination)
}
setRemoteSyslogHost.params = {
id: { type: 'string' },
syslogDestination: { type: 'string' },
}
setRemoteSyslogHost.resolve = {
host: ['id', 'host', 'administrate'],
}
// -------------------------------------------------------------------
export function start ({ host }) {
return this.getXapi(host).powerOnHost(host._xapiId)
}

View File

@@ -204,8 +204,8 @@ export async function createNfs ({
}
// if NFS options given
if (nfsVersion) {
deviceConfig.options = nfsVersion
if (nfsOptions) {
deviceConfig.options = nfsOptions
}
const srRef = await xapi.call(

View File

@@ -12,6 +12,10 @@ import { forEach, map, mapFilter, parseSize } from '../utils'
// ===================================================================
export function getHaValues () {
return ['best-effort', 'restart', '']
}
function checkPermissionOnSrs (vm, permission = 'operate') {
const permissions = []
forEach(vm.$VBDs, vbdId => {
@@ -46,11 +50,16 @@ const extract = (obj, prop) => {
export async function create (params) {
const { user } = this
const resourceSet = extract(params, 'resourceSet')
if (resourceSet === undefined && user.permission !== 'admin') {
const template = extract(params, 'template')
if (
resourceSet === undefined &&
!(await this.hasPermissions(this.user.id, [
[template.$pool, 'administrate'],
]))
) {
throw unauthorized()
}
const template = extract(params, 'template')
params.template = template._xapiId
const xapi = this.getXapi(template)
@@ -463,7 +472,7 @@ export async function migrate ({
})
}
if (!await this.hasPermissions(this.session.get('user_id'), permissions)) {
if (!(await this.hasPermissions(this.session.get('user_id'), permissions))) {
throw unauthorized()
}
@@ -556,11 +565,11 @@ set.params = {
name_description: { type: 'string', optional: true },
// TODO: provides better filtering of values for HA possible values: "best-
// effort" meaning "try to restart this VM if possible but don't consider the
// Pool to be overcommitted if this is not possible"; "restart" meaning "this
// VM should be restarted"; "" meaning "do not try to restart this VM"
high_availability: { type: 'boolean', optional: true },
high_availability: {
optional: true,
pattern: new RegExp(`^(${getHaValues().join('|')})$`),
type: 'string',
},
// Number of virtual CPUs to allocate.
CPUs: { type: 'integer', optional: true },
@@ -703,9 +712,9 @@ copy.resolve = {
export async function convertToTemplate ({ vm }) {
// Convert to a template requires pool admin permission.
if (
!await this.hasPermissions(this.session.get('user_id'), [
!(await this.hasPermissions(this.session.get('user_id'), [
[vm.$pool, 'administrate'],
])
]))
) {
throw unauthorized()
}
@@ -1265,7 +1274,9 @@ export async function createInterface ({
await this.checkResourceSetConstraints(resourceSet, this.user.id, [
network.id,
])
} else if (!await this.hasPermissions(this.user.id, [[network.id, 'view']])) {
} else if (
!(await this.hasPermissions(this.user.id, [[network.id, 'view']]))
) {
throw unauthorized()
}

View File

@@ -7,6 +7,7 @@ import has from 'lodash/has'
import helmet from 'helmet'
import includes from 'lodash/includes'
import proxyConsole from './proxy-console'
import pw from 'pw'
import serveStatic from 'serve-static'
import startsWith from 'lodash/startsWith'
import stoppable from 'stoppable'
@@ -227,12 +228,12 @@ async function registerPlugin (pluginPath, pluginName) {
// instance.
const instance = isFunction(factory)
? factory({
xo: this,
getDataDir: () => {
const dir = `${this._config.datadir}/${pluginName}`
return ensureDir(dir).then(() => dir)
},
})
xo: this,
getDataDir: () => {
const dir = `${this._config.datadir}/${pluginName}`
return ensureDir(dir).then(() => dir)
},
})
: factory
await this.registerPlugin(
@@ -311,6 +312,13 @@ async function makeWebServerListen (
) {
if (cert && key) {
;[opts.cert, opts.key] = await Promise.all([readFile(cert), readFile(key)])
if (opts.key.includes('ENCRYPTED')) {
opts.passphrase = await new Promise(resolve => {
console.log('Encrypted key %s', key)
process.stdout.write(`Enter pass phrase: `)
pw(resolve)
})
}
}
try {
const niceAddress = await webServer.listen(opts)
@@ -499,7 +507,7 @@ const setUpConsoleProxy = (webServer, xo) => {
const { token } = parseCookies(req.headers.cookie)
const user = await xo.authenticateUser({ token })
if (!await xo.hasPermissions(user.id, [[id, 'operate']])) {
if (!(await xo.hasPermissions(user.id, [[id, 'operate']]))) {
throw invalidCredentials()
}
@@ -543,7 +551,7 @@ export default async function main (args) {
debug('blocked for %sms', ms | 0)
},
{
threshold: 50,
threshold: 500,
}
)
}

View File

@@ -13,6 +13,10 @@ export default {
type: 'string',
description: 'identifier of this job',
},
scheduleId: {
type: 'string',
description: 'identifier of the schedule which ran the job',
},
key: {
type: 'string',
},

View File

@@ -146,6 +146,7 @@ const TRANSFORMS = {
license_params: obj.license_params,
license_server: obj.license_server,
license_expiry: toTimestamp(obj.license_params.expiry),
logging: obj.logging,
name_description: obj.name_description,
name_label: obj.name_label,
memory: (function () {
@@ -186,9 +187,14 @@ const TRANSFORMS = {
}
}),
agentStartTime: toTimestamp(otherConfig.agent_start_time),
rebootRequired: !isEmpty(obj.updates_requiring_reboot),
rebootRequired:
softwareVersion.product_brand === 'XCP-ng'
? toTimestamp(otherConfig.boot_time) <
+otherConfig.rpm_patch_installation_time
: !isEmpty(obj.updates_requiring_reboot),
tags: obj.tags,
version: softwareVersion.product_version,
productBrand: softwareVersion.product_brand,
// TODO: dedupe.
PIFs: link(obj, 'PIFs'),
@@ -227,12 +233,16 @@ const TRANSFORMS = {
return
}
if (!guestMetrics) {
if (guestMetrics === undefined) {
return false
}
const { major, minor } = guestMetrics.PV_drivers_version
if (major === undefined || minor === undefined) {
return false
}
return {
major,
minor,
@@ -292,8 +302,7 @@ const TRANSFORMS = {
}
})(),
// TODO: there is two possible value: "best-effort" and "restart"
high_availability: Boolean(obj.ha_restart_priority),
high_availability: obj.ha_restart_priority,
memory: (function () {
const dynamicMin = +obj.memory_dynamic_min

View File

@@ -426,6 +426,14 @@ export default class Xapi extends XapiBase {
await this.call('host.restart_agent', this.getObject(hostId).$ref)
}
async setRemoteSyslogHost (hostId, syslogDestination) {
const host = this.getObject(hostId)
await this.call('host.set_logging', host.$ref, {
syslog_destination: syslogDestination,
})
await this.call('host.syslog_reconfigure', host.$ref)
}
async shutdownHost (hostId, force = false) {
const host = this.getObject(hostId)
@@ -816,12 +824,14 @@ export default class Xapi extends XapiBase {
} = {}
): Promise<DeltaVmExport> {
let vm = this.getObject(vmId)
if (!bypassVdiChainsCheck) {
this._assertHealthyVdiChains(vm)
}
// do not use the snapshot name in the delta export
const exportedNameLabel = vm.name_label
if (!vm.is_a_snapshot) {
if (!bypassVdiChainsCheck) {
this._assertHealthyVdiChains(vm)
}
vm = await this._snapshotVm($cancelToken, vm, snapshotNameLabel)
$defer.onFailure(() => this._deleteVm(vm))
}
@@ -958,7 +968,9 @@ export default class Xapi extends XapiBase {
)
if (!baseVm) {
throw new Error('could not find the base VM')
throw new Error(
`could not find the base VM (copy of ${remoteBaseVmUuid})`
)
}
}
}
@@ -1142,7 +1154,9 @@ export default class Xapi extends XapiBase {
vdis[vdi.$ref] =
mapVdisSrs && mapVdisSrs[vdi.$id]
? hostXapi.getObject(mapVdisSrs[vdi.$id]).$ref
: sr !== undefined ? hostXapi.getObject(sr).$ref : defaultSr.$ref // Will error if there are no default SR.
: sr !== undefined
? hostXapi.getObject(sr).$ref
: defaultSr.$ref // Will error if there are no default SR.
}
}

View File

@@ -35,8 +35,15 @@ declare class XapiObject {
}
type Id = string | XapiObject
declare export class Vbd extends XapiObject {
type: string;
VDI: string;
}
declare export class Vm extends XapiObject {
$snapshots: Vm[];
$VBDs: Vbd[];
is_a_snapshot: boolean;
is_a_template: boolean;
name_label: string;

View File

@@ -1,5 +1,6 @@
import deferrable from 'golike-defer'
import every from 'lodash/every'
import filter from 'lodash/filter'
import find from 'lodash/find'
import includes from 'lodash/includes'
import isObject from 'lodash/isObject'
@@ -11,6 +12,7 @@ import unzip from 'julien-f-unzip'
import { debounce } from '../../decorators'
import {
asyncMap,
ensureArray,
forEach,
mapFilter,
@@ -149,9 +151,12 @@ export default {
},
async listMissingPoolPatchesOnHost (hostId) {
const host = this.getObject(hostId)
// Returns an array to not break compatibility.
return mapToArray(
await this._listMissingPoolPatchesOnHost(this.getObject(hostId))
await (host.software_version.product_brand === 'XCP-ng'
? this._xcpListHostUpdates(host)
: this._listMissingPoolPatchesOnHost(host))
)
},
@@ -440,8 +445,14 @@ export default {
},
async installAllPoolPatchesOnHost (hostId) {
let host = this.getObject(hostId)
const host = this.getObject(hostId)
if (host.software_version.product_brand === 'XCP-ng') {
return this._xcpInstallHostUpdates(host)
}
return this._installAllPoolPatchesOnHost(host)
},
async _installAllPoolPatchesOnHost (host) {
const installableByUuid =
host.license_params.sku_type !== 'free'
? await this._listMissingPoolPatchesOnHost(host)
@@ -479,6 +490,13 @@ export default {
},
async installAllPoolPatchesOnAllHosts () {
if (this.pool.$master.software_version.product_brand === 'XCP-ng') {
return this._xcpInstallAllPoolUpdatesOnHost()
}
return this._installAllPoolPatchesOnAllHosts()
},
async _installAllPoolPatchesOnAllHosts () {
const installableByUuid = assign(
{},
...(await Promise.all(
@@ -518,4 +536,47 @@ export default {
})
}
},
// ----------------------------------
// XCP-ng dedicated zone for patching
// ----------------------------------
// list all yum updates available for a XCP-ng host
async _xcpListHostUpdates (host) {
return JSON.parse(
await this.call(
'host.call_plugin',
host.$ref,
'updater.py',
'check_update',
{}
)
)
},
// install all yum updates for a XCP-ng host
async _xcpInstallHostUpdates (host) {
const update = await this.call(
'host.call_plugin',
host.$ref,
'updater.py',
'update',
{}
)
if (JSON.parse(update).exit !== 0) {
throw new Error('Update install failed')
} else {
await this._updateObjectMapProperty(host, 'other_config', {
rpm_patch_installation_time: String(Date.now() / 1000),
})
}
},
// install all yum updates for all XCP-ng hosts in a give pool
async _xcpInstallAllPoolUpdatesOnHost () {
await asyncMap(filter(this.objects.all, { $type: 'host' }), host =>
this._xcpInstallHostUpdates(host)
)
},
}

View File

@@ -310,11 +310,7 @@ export default {
highAvailability: {
set (ha, vm) {
return this.call(
'VM.set_ha_restart_priority',
vm.$ref,
ha ? 'restart' : ''
)
return this.call('VM.set_ha_restart_priority', vm.$ref, ha)
},
},

View File

@@ -0,0 +1,119 @@
import { forEach } from 'lodash'
const isSkippedError = error =>
error.message === 'no disks found' ||
error.message === 'no such object' ||
error.message === 'no VMs match this pattern' ||
error.message === 'unhealthy VDI chain'
const getStatus = (
error,
status = error === undefined ? 'success' : 'failure'
) => (status === 'failure' && isSkippedError(error) ? 'skipped' : status)
const computeStatus = (status, tasks) => {
if (status === 'failure' || tasks === undefined) {
return status
}
for (let i = 0, n = tasks.length; i < n; ++i) {
const taskStatus = tasks[i].status
if (taskStatus === 'failure') {
return taskStatus
}
if (taskStatus === 'skipped') {
status = taskStatus
}
}
return status
}
export default {
async getBackupNgLogs (runId?: string) {
const { runningJobs } = this
const consolidated = {}
const started = {}
forEach(await this.getLogs('jobs'), ({ data, time, message }, id) => {
const { event } = data
if (event === 'job.start') {
if (
(data.type === 'backup' || data.key === undefined) &&
(runId === undefined || runId === id)
) {
const { jobId } = data
consolidated[id] = started[id] = {
data: data.data,
id,
jobId,
start: time,
status: runningJobs[jobId] === id ? 'pending' : 'interrupted',
}
}
} else if (event === 'job.end') {
const { runJobId } = data
const log = started[runJobId]
if (log !== undefined) {
delete started[runJobId]
log.end = time
log.status = computeStatus(
getStatus((log.result = data.error)),
log.tasks
)
}
} else if (event === 'task.start') {
const parent = started[data.parentId]
if (parent !== undefined) {
;(parent.tasks || (parent.tasks = [])).push(
(started[id] = {
data: data.data,
id,
message,
start: time,
status: parent.status,
})
)
}
} else if (event === 'task.end') {
const { taskId } = data
const log = started[taskId]
if (log !== undefined) {
// TODO: merge/transfer work-around
delete started[taskId]
log.end = time
log.status = computeStatus(
getStatus((log.result = data.result), data.status),
log.tasks
)
}
} else if (event === 'jobCall.start') {
const parent = started[data.runJobId]
if (parent !== undefined) {
;(parent.tasks || (parent.tasks = [])).push(
(started[id] = {
data: {
type: 'VM',
id: data.params.id,
},
id,
start: time,
status: parent.status,
})
)
}
} else if (event === 'jobCall.end') {
const { runCallId } = data
const log = started[runCallId]
if (log !== undefined) {
delete started[runCallId]
log.end = time
log.status = computeStatus(
getStatus((log.result = data.error)),
log.tasks
)
}
}
})
return runId === undefined ? consolidated : consolidated[runId]
},
}

View File

@@ -3,19 +3,12 @@
// $FlowFixMe
import type RemoteHandler from '@xen-orchestra/fs'
import defer from 'golike-defer'
import limitConcurrency from 'limit-concurrency-decorator'
import { type Pattern, createPredicate } from 'value-matcher'
import { type Readable, PassThrough } from 'stream'
import { basename, dirname } from 'path'
import {
forEach,
groupBy,
isEmpty,
last,
mapValues,
noop,
values,
} from 'lodash'
import { timeout as pTimeout } from 'promise-toolbox'
import { isEmpty, last, mapValues, noop, some, sum, values } from 'lodash'
import { fromEvent as pFromEvent, timeout as pTimeout } from 'promise-toolbox'
import Vhd, {
chainVhd,
createSyntheticStream as createVhdReadStream,
@@ -40,10 +33,11 @@ import {
import { translateLegacyJob } from './migration'
type Mode = 'full' | 'delta'
type ReportWhen = 'always' | 'failure' | 'never'
export type Mode = 'full' | 'delta'
export type ReportWhen = 'always' | 'failure' | 'never'
type Settings = {|
concurrency?: number,
deleteFirst?: boolean,
exportRetention?: number,
reportWhen?: ReportWhen,
@@ -90,33 +84,6 @@ type MetadataFull = {|
|}
type Metadata = MetadataDelta | MetadataFull
type ConsolidatedJob = {|
duration?: number,
end?: number,
error?: Object,
id: string,
jobId: string,
mode: Mode,
start: number,
type: 'backup' | 'call',
userId: string,
|}
type ConsolidatedTask = {|
data?: Object,
duration?: number,
end?: number,
parentId: string,
message: string,
result?: Object,
start: number,
status: 'canceled' | 'failure' | 'success',
taskId: string,
|}
type ConsolidatedBackupNgLog = {
roots: Array<ConsolidatedJob>,
[parentId: string]: Array<ConsolidatedTask>,
}
const compareSnapshotTime = (a: Vm, b: Vm): number =>
a.snapshot_time < b.snapshot_time ? -1 : 1
@@ -130,7 +97,9 @@ const compareTimestamp = (a: Metadata, b: Metadata): number =>
const getOldEntries = <T>(retention: number, entries?: T[]): T[] =>
entries === undefined
? []
: --retention > 0 ? entries.slice(0, -retention) : entries
: --retention > 0
? entries.slice(0, -retention)
: entries
const defaultSettings: Settings = {
deleteFirst: false,
@@ -159,6 +128,7 @@ const getSetting = (
const BACKUP_DIR = 'xo-vm-backups'
const getVmBackupDir = (uuid: string) => `${BACKUP_DIR}/${uuid}`
const isHiddenFile = (filename: string) => filename[0] === '.'
const isMetadataFile = (filename: string) => filename.endsWith('.json')
const isVhd = (filename: string) => filename.endsWith('.vhd')
@@ -304,6 +274,7 @@ const writeStream = async (
const output = await handler.createOutputStream(tmpPath, { checksum })
try {
input.pipe(output)
await pFromEvent(output, 'finish')
await output.checksumWritten
// $FlowFixMe
await input.task
@@ -330,7 +301,9 @@ const wrapTask = async <T>(opts: any, task: Promise<T>): Promise<T> => {
result:
result === undefined
? value
: typeof result === 'function' ? result(value) : result,
: typeof result === 'function'
? result(value)
: result,
status: 'success',
taskId,
})
@@ -369,7 +342,9 @@ const wrapTaskFn = <T>(
result:
result === undefined
? value
: typeof result === 'function' ? result(value) : result,
: typeof result === 'function'
? result(value)
: result,
status: 'success',
taskId,
})
@@ -431,6 +406,7 @@ export default class BackupNg {
app.on('start', () => {
const executor: Executor = async ({
cancelToken,
data: vmId,
job: job_,
logger,
runJobId,
@@ -441,18 +417,21 @@ export default class BackupNg {
}
const job: BackupJob = (job_: any)
const vms: $Dict<Vm> = app.getObjects({
filter: createPredicate({
type: 'VM',
...job.vms,
}),
})
if (isEmpty(vms)) {
throw new Error('no VMs match this pattern')
let vms: $Dict<Vm>
if (vmId === undefined) {
vms = app.getObjects({
filter: createPredicate({
type: 'VM',
...job.vms,
}),
})
if (isEmpty(vms)) {
throw new Error('no VMs match this pattern')
}
}
const jobId = job.id
const scheduleId = schedule.id
await asyncMap(vms, async vm => {
let handleVm = async vm => {
const { name_label: name, uuid } = vm
const taskId: string = logger.notice(
`Starting backup of ${name}. (${jobId})`,
@@ -504,7 +483,21 @@ export default class BackupNg {
: serializeError(error),
})
}
})
}
if (vmId !== undefined) {
return handleVm(await app.getObject(vmId))
}
const concurrency: number | void = getSetting(
job.settings,
'concurrency',
''
)
if (concurrency !== undefined) {
handleVm = limitConcurrency(concurrency)(handleVm)
}
await asyncMap(vms, handleVm)
}
app.registerJobExecutor('backup', executor)
})
@@ -659,7 +652,7 @@ export default class BackupNg {
// 2. next run should be a full
// - [ ] add a lock on the job/VDI during merge which should prevent other merges and restoration
// - [ ] check merge/transfert duration/size are what we want for delta
// - [ ] fix backup reports
// - [ ] in case of failure, correctly clean VHDs for all VDIs
//
// Low:
// - [ ] jobs should be cancelable
@@ -692,6 +685,7 @@ export default class BackupNg {
// - [x] replicated VMs should be discriminated by VM (vatesfr/xen-orchestra#2807)
// - [x] clones of replicated VMs should not be garbage collected
// - [x] import for delta
// - [x] fix backup reports
@defer
async _backupVm (
$defer: any,
@@ -736,6 +730,15 @@ export default class BackupNg {
}
}
if (
!some(
vm.$VBDs,
vbd => vbd.type === 'Disk' && vbd.VDI !== 'OpaqueRef:NULL'
)
) {
throw new Error('no disks found')
}
const snapshots = vm.$snapshots
.filter(_ => _.other_config['xo:backup:job'] === jobId)
.sort(compareSnapshotTime)
@@ -869,9 +872,7 @@ export default class BackupNg {
logger,
message: 'transfer',
parentId: taskId,
result: {
size: 0,
},
result: () => ({ size: xva.size }),
},
writeStream(fork, handler, dataFilename)
)
@@ -914,9 +915,7 @@ export default class BackupNg {
logger,
message: 'transfer',
parentId: taskId,
result: {
size: 0,
},
result: () => ({ size: xva.size }),
},
xapi._importVm($cancelToken, fork, sr, vm =>
xapi._setObjectProperties(vm, {
@@ -1048,9 +1047,7 @@ export default class BackupNg {
logger,
message: 'merge',
parentId: taskId,
result: {
size: 0,
},
result: size => ({ size }),
},
this._deleteDeltaVmBackups(handler, oldBackups)
)
@@ -1067,9 +1064,7 @@ export default class BackupNg {
logger,
message: 'transfer',
parentId: taskId,
result: {
size: 0,
},
result: size => ({ size }),
},
asyncMap(
fork.vdis,
@@ -1081,11 +1076,16 @@ export default class BackupNg {
let parentPath
if (isDelta) {
const vdiDir = dirname(path)
const parent = (await handler.list(vdiDir))
.filter(isVhd)
parentPath = (await handler.list(vdiDir, {
filter: filename =>
!isHiddenFile(filename) && isVhd(filename),
prependDir: true,
}))
.sort()
.pop()
parentPath = `${vdiDir}/${parent}`
// ensure parent exists and is a valid VHD
await new Vhd(handler, parentPath).readHeaderAndFooter()
}
await writeStream(
@@ -1103,10 +1103,11 @@ export default class BackupNg {
if (isDelta) {
await chainVhd(handler, parentPath, handler, path)
}
})
)
)
return handler.getSize(path)
})
).then(sum)
)
await handler.outputFile(metadataFilename, jsonMetadata)
if (!deleteFirst) {
@@ -1144,9 +1145,7 @@ export default class BackupNg {
logger,
message: 'transfer',
parentId: taskId,
result: {
size: 0,
},
result: ({ transferSize }) => ({ size: transferSize }),
},
xapi.importDeltaVm(fork, {
disableStartAfterImport: false, // we'll take care of that
@@ -1185,19 +1184,17 @@ export default class BackupNg {
async _deleteDeltaVmBackups (
handler: RemoteHandler,
backups: MetadataDelta[]
): Promise<void> {
// TODO: remove VHD as well
await asyncMap(backups, async backup => {
): Promise<number> {
return asyncMap(backups, async backup => {
const filename = ((backup._filename: any): string)
return Promise.all([
handler.unlink(filename),
asyncMap(backup.vhds, _ =>
// $FlowFixMe injected $defer param
this._deleteVhd(handler, resolveRelativeFromFile(filename, _))
),
])
})
await handler.unlink(filename)
return asyncMap(backup.vhds, _ =>
// $FlowFixMe injected $defer param
this._deleteVhd(handler, resolveRelativeFromFile(filename, _))
).then(sum)
}).then(sum)
}
async _deleteFullVmBackups (
@@ -1215,35 +1212,50 @@ export default class BackupNg {
// FIXME: synchronize by job/VDI, otherwise it can cause issues with the merge
@defer
async _deleteVhd ($defer: any, handler: RemoteHandler, path: string) {
async _deleteVhd (
$defer: any,
handler: RemoteHandler,
path: string
): Promise<number> {
const vhds = await asyncMap(
await handler.list(dirname(path), { filter: isVhd, prependDir: true }),
async path => {
const vhd = new Vhd(handler, path)
await vhd.readHeaderAndFooter()
return {
footer: vhd.footer,
header: vhd.header,
path,
try {
const vhd = new Vhd(handler, path)
await vhd.readHeaderAndFooter()
return {
footer: vhd.footer,
header: vhd.header,
path,
}
} catch (error) {
// Do not fail on corrupted VHDs (usually uncleaned temporary files),
// they are probably inconsequent to the backup process and should not
// fail it.
console.warn('BackupNg#_deleteVhd', path, error)
}
}
)
const base = basename(path)
const child = vhds.find(_ => _.header.parentUnicodeName === base)
const child = vhds.find(
_ => _ !== undefined && _.header.parentUnicodeName === base
)
if (child === undefined) {
return handler.unlink(path)
await handler.unlink(path)
return 0
}
$defer.onFailure.call(handler, 'unlink', path)
const childPath = child.path
await this._app.worker.mergeVhd(
const mergedDataSize: number = await this._app.worker.mergeVhd(
handler._remote,
path,
handler._remote,
childPath
)
await handler.rename(path, childPath)
return mergedDataSize
}
async _deleteVms (xapi: Xapi, vms: Vm[]): Promise<void> {
@@ -1288,59 +1300,4 @@ export default class BackupNg {
return backups.sort(compareTimestamp)
}
async getBackupNgLogs (runId?: string): Promise<ConsolidatedBackupNgLog> {
const rawLogs = await this._app.getLogs('jobs')
const logs: $Dict<ConsolidatedJob & ConsolidatedTask> = {}
forEach(rawLogs, (log, id) => {
const { data, time, message } = log
const { event } = data
delete data.event
switch (event) {
case 'job.start':
if (data.type === 'backup' && (runId === undefined || runId === id)) {
logs[id] = {
...data,
id,
start: time,
}
}
break
case 'job.end':
const job = logs[data.runJobId]
if (job !== undefined) {
job.end = time
job.duration = time - job.start
job.error = data.error
}
break
case 'task.start':
if (logs[data.parentId] !== undefined) {
logs[id] = {
...data,
start: time,
message,
}
}
break
case 'task.end':
const task = logs[data.taskId]
if (task !== undefined) {
// work-around
if (time === task.start && message === 'merge') {
delete logs[data.taskId]
} else {
task.status = data.status
task.taskId = data.taskId
task.result = data.result
task.end = time
task.duration = time - task.start
}
}
}
})
return groupBy(logs, log => log.parentId || 'roots')
}
}

View File

@@ -903,6 +903,8 @@ export default class {
const xapi = this._xo.getXapi(vm)
vm = xapi.getObject(vm._xapiId)
await xapi._assertHealthyVdiChains(vm)
const reg = new RegExp(
'^rollingSnapshot_[^_]+_' + escapeStringRegexp(tag) + '_'
)

View File

@@ -120,7 +120,11 @@ export default class Jobs {
_executors: { __proto__: null, [string]: Executor }
_jobs: JobsDb
_logger: Logger
_runningJobs: { __proto__: null, [string]: boolean }
_runningJobs: { __proto__: null, [string]: string }
get runningJobs () {
return this._runningJobs
}
constructor (xo: any) {
this._app = xo
@@ -201,7 +205,7 @@ export default class Jobs {
return /* await */ this._jobs.remove(id)
}
async _runJob (cancelToken: any, job: Job, schedule?: Schedule) {
async _runJob (cancelToken: any, job: Job, schedule?: Schedule, data_?: any) {
const { id } = job
const runningJobs = this._runningJobs
@@ -232,6 +236,7 @@ export default class Jobs {
event: 'job.start',
userId: job.userId,
jobId: id,
scheduleId: schedule?.id,
// $FlowFixMe only defined for CallJob
key: job.key,
type,
@@ -245,9 +250,10 @@ export default class Jobs {
session = app.createUserConnection()
session.set('user_id', job.userId)
await executor({
const status = await executor({
app,
cancelToken,
data: data_,
job,
logger,
runJobId,
@@ -259,7 +265,7 @@ export default class Jobs {
runJobId,
})
app.emit('job:terminated', runJobId, job, schedule)
app.emit('job:terminated', status, job, schedule, runJobId)
} catch (error) {
logger.error(`The execution of ${id} has failed.`, {
event: 'job.end',
@@ -279,7 +285,8 @@ export default class Jobs {
async runJobSequence (
$cancelToken: any,
idSequence: Array<string>,
schedule?: Schedule
schedule?: Schedule,
data?: any
) {
const jobs = await Promise.all(
mapToArray(idSequence, id => this.getJob(id))
@@ -289,7 +296,7 @@ export default class Jobs {
if ($cancelToken.requested) {
break
}
await this._runJob($cancelToken, job, schedule)
await this._runJob($cancelToken, job, schedule, data)
}
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "xo-vmdk-to-vhd",
"version": "0.1.0",
"version": "0.1.2",
"license": "AGPL-3.0",
"description": "JS lib streaming a vmdk file to a vhd",
"keywords": [
@@ -23,24 +23,23 @@
"node": ">=4"
},
"dependencies": {
"@babel/runtime": "^7.0.0-beta.44",
"@babel/runtime": "^7.0.0-beta.49",
"child-process-promise": "^2.0.3",
"fs-promise": "^2.0.0",
"pipette": "^0.9.3",
"promise-toolbox": "^0.9.5",
"tmp": "^0.0.33",
"vhd-lib": "^0.0.0"
"vhd-lib": "^0.1.1"
},
"devDependencies": {
"@babel/cli": "7.0.0-beta.44",
"@babel/core": "7.0.0-beta.44",
"@babel/plugin-transform-runtime": "^7.0.0-beta.44",
"@babel/preset-env": "7.0.0-beta.44",
"@babel/cli": "7.0.0-beta.49",
"@babel/core": "7.0.0-beta.49",
"@babel/plugin-transform-runtime": "^7.0.0-beta.49",
"@babel/preset-env": "7.0.0-beta.49",
"babel-plugin-lodash": "^3.3.2",
"cross-env": "^5.1.3",
"event-to-promise": "^0.8.0",
"execa": "^0.10.0",
"fs-extra": "^5.0.0",
"fs-extra": "^6.0.1",
"get-stream": "^3.0.0",
"index-modules": "^0.3.0",
"rimraf": "^2.6.2"

View File

@@ -1,6 +1,6 @@
/* eslint-env jest */
import { createReadStream, readFile } from 'fs-promise'
import { createReadStream, readFile } from 'fs-extra'
import { exec } from 'child-process-promise'
import { fromCallback as pFromCallback } from 'promise-toolbox'
import rimraf from 'rimraf'

View File

@@ -1,6 +1,6 @@
/* eslint-env jest */
import { createReadStream } from 'fs-promise'
import { createReadStream } from 'fs-extra'
import { exec } from 'child-process-promise'
import { fromCallback as pFromCallback } from 'promise-toolbox'
import rimraf from 'rimraf'

View File

@@ -308,17 +308,15 @@ export class VMDKDirectParser {
export async function readVmdkGrainTable (fileAccessor) {
let headerBuffer = await fileAccessor(0, 512)
let grainDirAddr = headerBuffer.slice(56, 56 + 8)
let grainAddrBuffer = headerBuffer.slice(56, 56 + 8)
if (
new Int8Array(grainDirAddr).reduce((acc, val) => acc && val === -1, true)
new Int8Array(grainAddrBuffer).reduce((acc, val) => acc && val === -1, true)
) {
headerBuffer = await fileAccessor(-1024, -1024 + 512)
grainDirAddr = new DataView(headerBuffer.slice(56, 56 + 8)).getUint32(
0,
true
)
grainAddrBuffer = headerBuffer.slice(56, 56 + 8)
}
const grainDirPosBytes = grainDirAddr * 512
const grainDirPosBytes =
new DataView(grainAddrBuffer).getUint32(0, true) * 512
const capacity =
new DataView(headerBuffer.slice(12, 12 + 8)).getUint32(0, true) * 512
const grainSize =

View File

@@ -6,7 +6,7 @@ import getStream from 'get-stream'
import rimraf from 'rimraf'
import tmp from 'tmp'
import { createReadStream, createWriteStream, stat } from 'fs-promise'
import { createReadStream, createWriteStream, stat } from 'fs-extra'
import { fromCallback as pFromCallback } from 'promise-toolbox'
import convertFromVMDK, { readVmdkGrainTable } from '.'
@@ -49,7 +49,7 @@ test('VMDK to VHD can convert a random data file with VMDKDirectParser', async (
const vhdFileName = 'from-vmdk-VMDKDirectParser.vhd'
const reconvertedFromVhd = 'from-vhd.raw'
const reconvertedFromVmdk = 'from-vhd-by-vbox.raw'
const dataSize = 8355840 // this number is an integer head/cylinder/count equation solution
const dataSize = 100 * 1024 * 1024 // this number is an integer head/cylinder/count equation solution
try {
await execa.shell(
'base64 /dev/urandom | head -c ' + dataSize + ' > ' + inputRawFileName
@@ -82,6 +82,7 @@ test('VMDK to VHD can convert a random data file with VMDKDirectParser', async (
reconvertedFromVhd,
])
await execa('qemu-img', ['compare', inputRawFileName, vhdFileName])
await execa('qemu-img', ['compare', vmdkFileName, vhdFileName])
} catch (error) {
console.error(error.stdout)
console.error(error.stderr)

View File

@@ -1,7 +1,7 @@
{
"private": false,
"name": "xo-web",
"version": "5.19.3",
"version": "5.19.8",
"license": "AGPL-3.0",
"description": "Web interface client for Xen-Orchestra",
"keywords": [
@@ -30,10 +30,9 @@
"node": ">=6"
},
"devDependencies": {
"@julien-f/freactal": "0.1.0",
"@julien-f/freactal": "0.1.1",
"@nraynaud/novnc": "0.6.1",
"@xen-orchestra/cron": "^1.0.3",
"xo-vmdk-to-vhd": "0.1.0",
"ansi_up": "^3.0.0",
"asap": "^2.0.6",
"babel-core": "^6.26.0",
@@ -60,6 +59,7 @@
"classnames": "^2.2.3",
"complex-matcher": "^0.3.0",
"cookies-js": "^1.2.2",
"copy-to-clipboard": "^3.0.8",
"d3": "^5.0.0",
"debounce-input-decorator": "^0.1.0",
"enzyme": "^3.3.0",
@@ -89,8 +89,8 @@
"lodash": "^4.6.1",
"loose-envify": "^1.1.0",
"make-error": "^1.3.2",
"marked": "^0.3.9",
"modular-cssify": "^8.0.0",
"marked": "^0.4.0",
"modular-cssify": "^10.0.0",
"moment": "^2.20.1",
"moment-timezone": "^0.5.14",
"notifyjs": "^3.0.0",
@@ -120,7 +120,7 @@
"react-test-renderer": "^15.6.2",
"react-virtualized": "^9.15.0",
"readable-stream": "^2.3.3",
"redux": "^3.7.2",
"redux": "^4.0.0",
"redux-thunk": "^2.0.1",
"reselect": "^2.5.4",
"rimraf": "^2.6.2",
@@ -134,10 +134,11 @@
"watchify": "^3.7.0",
"whatwg-fetch": "^2.0.3",
"xml2js": "^0.4.19",
"xo-acl-resolver": "^0.2.3",
"xo-acl-resolver": "^0.2.4",
"xo-common": "^0.1.1",
"xo-lib": "^0.8.0",
"xo-remote-parser": "^0.3"
"xo-remote-parser": "^0.3",
"xo-vmdk-to-vhd": "^0.1.2"
},
"scripts": {
"build": "NODE_ENV=production gulp build",

View File

@@ -142,15 +142,15 @@ export default class Select extends React.PureComponent {
simpleValue,
value,
} = props
let option
if (
autoSelectSingleOption &&
options != null &&
options.length === 1 &&
(value == null ||
(simpleValue && value === '') ||
(multi && value.length === 0))
(multi && value.length === 0)) &&
([option] = options.filter(_ => !_.disabled)).length === 1
) {
const option = options[0]
props.onChange(
simpleValue ? option[props.valueKey] : multi ? [option] : option
)

View File

@@ -1,10 +1,9 @@
import isFunction from 'lodash/isFunction'
import isString from 'lodash/isString'
import moment from 'moment'
import PropTypes from 'prop-types'
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { FormattedMessage, IntlProvider as IntlProvider_ } from 'react-intl'
import { every, isFunction, isString } from 'lodash'
import locales from './locales'
import messages from './messages'
@@ -102,8 +101,16 @@ export class FormattedDuration extends Component {
)
render () {
const parsedDuration = this._parseDuration()
return (
<Tooltip content={getMessage('durationFormat', this._parseDuration())}>
<Tooltip
content={getMessage(
every(parsedDuration, n => n === 0)
? 'secondsFormat'
: 'durationFormat',
parsedDuration
)}
>
<span>{this._humanizeDuration()}</span>
</Tooltip>
)

View File

@@ -41,6 +41,7 @@ const messages = {
// ----- Copiable component -----
copyToClipboard: 'Copy to clipboard',
copyUuid: 'Copy {uuid}',
// ----- Pills -----
pillMaster: 'Master',
@@ -258,6 +259,8 @@ const messages = {
jobCallInProgess: 'In progress',
jobTransferredDataSize: 'Transfer size:',
jobTransferredDataSpeed: 'Transfer speed:',
operationSize: 'Size',
operationSpeed: 'Speed',
jobMergedDataSize: 'Merge size:',
jobMergedDataSpeed: 'Merge speed:',
allJobCalls: 'All',
@@ -305,6 +308,7 @@ const messages = {
taskMergedDataSize: 'Merge size',
taskMergedDataSpeed: 'Merge speed',
taskError: 'Error',
taskReason: 'Reason',
saveBackupJob: 'Save',
deleteBackupSchedule: 'Remove backup job',
deleteBackupScheduleQuestion:
@@ -348,6 +352,7 @@ const messages = {
reportWhenFailure: 'Failure',
reportWhenNever: 'Never',
reportWhen: 'Report when',
concurrency: 'Concurrency',
newBackupSelection: 'Select your backup type:',
smartBackupModeSelection: 'Select backup mode:',
normalBackup: 'Normal backup',
@@ -596,11 +601,15 @@ const messages = {
vmsTabName: 'Vms',
srsTabName: 'Srs',
// ----- Pool advanced tab -----
poolEditAll: 'Edit all',
poolEditRemoteSyslog: 'Edit remote syslog for all hosts',
poolHaStatus: 'High Availability',
poolHaEnabled: 'Enabled',
poolHaDisabled: 'Disabled',
setpoolMaster: 'Master',
poolGpuGroups: 'GPU groups',
poolRemoteSyslogPlaceHolder: 'Logging host',
setpoolMaster: 'Master',
syslogRemoteHost: 'Remote syslog host',
// ----- Pool host tab -----
hostNameLabel: 'Name',
hostDescription: 'Description',
@@ -680,6 +689,7 @@ const messages = {
hostLicenseType: 'Type',
hostLicenseSocket: 'Socket',
hostLicenseExpiry: 'Expiry',
hostRemoteSyslog: 'Remote syslog',
supplementalPacks: 'Installed supplemental packs',
supplementalPackNew: 'Install new supplemental pack',
supplementalPackPoolNew: 'Install supplemental pack on every host',
@@ -734,6 +744,7 @@ const messages = {
patchNameLabel: 'Name',
patchUpdateButton: 'Install all patches',
patchDescription: 'Description',
patchVersion: 'Version',
patchApplied: 'Applied date',
patchSize: 'Size',
patchStatus: 'Status',
@@ -751,6 +762,15 @@ const messages = {
'This will install a patch only on this host. This is NOT the recommended way: please go into the Pool patch view and follow instructions there. If you are sure about this, you can continue anyway',
installPatchWarningReject: 'Go to pool',
installPatchWarningResolve: 'Install',
patchRelease: 'Release',
updatePluginNotInstalled:
'Update plugin is not installed on this host. Please run `yum install xcp-ng-updater` first.',
showChangelog: 'Show changelog',
changelog: 'Changelog',
changelogPatch: 'Patch',
changelogAuthor: 'Author',
changelogDate: 'Date',
changelogDescription: 'Description',
// ----- Pool patch tabs -----
refreshPatches: 'Refresh patches',
installPoolPatches: 'Install pool patches',
@@ -932,6 +952,7 @@ const messages = {
defaultCpuCap: 'Default ({value, number})',
pvArgsLabel: 'PV args',
xenToolsStatus: 'Xen tools version',
xenToolsNotInstalled: 'Not installed',
osName: 'OS name',
osKernel: 'OS kernel',
autoPowerOn: 'Auto power on',
@@ -956,6 +977,7 @@ const messages = {
vmCoresPerSocketIncorrectValue: 'Incorrect cores per socket value',
vmCoresPerSocketIncorrectValueSolution:
'Please change the selected value to fix it.',
vmHaDisabled: 'disabled',
vmMemoryLimitsLabel: 'Memory limits (min/max)',
vmMaxVcpus: 'vCPUs max:',
vmMaxRam: 'Memory max:',
@@ -1104,6 +1126,11 @@ const messages = {
newVmSshKey: 'SSH key',
newVmConfigDrive: 'Config drive',
newVmCustomConfig: 'Custom config',
availableTemplateVarsInfo:
'Click here to see the available template variables',
availableTemplateVarsTitle: 'Available template variables',
templateNameInfo: 'the VM\'s name. It must not contain "_"',
templateIndexInfo: "the VM's index, it will take 0 in case of single VM",
newVmBootAfterCreate: 'Boot VM after creation',
newVmMacPlaceholder: 'Auto-generated if empty',
newVmCpuWeightLabel: 'CPU weight',
@@ -1672,6 +1699,7 @@ const messages = {
logIndicationToDisable: 'Click to disable',
reportBug: 'Report a bug',
unhealthyVdiChainError: 'Job canceled to protect the VDI chain',
backupRestartVm: "Restart VM's backup",
clickForMoreInformation: 'Click for more information',
// ----- IPs ------
@@ -1903,6 +1931,7 @@ const messages = {
xosanLoadXoaPlugin: 'Load XOA plugin first',
// ----- Utils -----
secondsFormat: '{seconds, plural, one {# second} other {# seconds}}',
durationFormat:
'{days, plural, =0 {} one {# day } other {# days }}{hours, plural, =0 {} one {# hour } other {# hours }}{minutes, plural, =0 {} one {# minute } other {# minutes }}{seconds, plural, =0 {} one {# second} other {# seconds}}',
}

View File

@@ -50,19 +50,17 @@ const SrItem = propTypes({
return (state, props) => ({
container: getContainer(state, props),
})
})(({ sr, container }) => {
let label = `${sr.name_label || sr.id}`
if (isSrWritable(sr)) {
label += ` (${formatSize(sr.size - sr.physical_usage)} free)`
}
return (
<span>
<Icon icon='sr' /> {label}
</span>
)
})
})(({ sr, container }) => (
<span>
<Icon icon='sr' /> {sr.name_label || sr.id}
{container !== undefined && (
<span className='text-muted'> - {container.name_label}</span>
)}
{isSrWritable(sr) && (
<span>{` (${formatSize(sr.size - sr.physical_usage)} free)`}</span>
)}
</span>
))
)
// VM.

View File

@@ -6,7 +6,6 @@ import {
filter,
flatten,
forEach,
get,
groupBy,
includes,
isArray,
@@ -362,40 +361,10 @@ export const SelectSr = makeStoreSelect(
const getPools = createGetObjectsOfType('pool')
const getHosts = createGetObjectsOfType('host')
const getSrsByContainer = createSelector(
createGetObjectsOfType('SR')
.filter((_, { predicate }) => predicate || isSrWritable)
.sort(),
createSelector(getHosts, getPools, (hosts, pools) => id =>
hosts[id] || pools[id]
),
(srs, containerFinder) => {
const { length } = srs
if (length >= 2) {
let sr1, sr2
const srsToModify = {}
for (let i = 1; i < length; ++i) {
sr1 = srs[i]
for (let j = 0; j < i; ++j) {
sr2 = srs[j]
if (sr1.name_label === sr2.name_label) {
srsToModify[sr1.id] = sr1
srsToModify[sr2.id] = sr2
}
}
}
forEach(srsToModify, sr => {
sr.name_label = `(${get(
containerFinder(sr.$container),
'name_label'
)}) ${sr.name_label}`
})
}
return groupBy(srs, '$container')
}
)
const getSrsByContainer = createGetObjectsOfType('SR')
.filter((_, { predicate }) => predicate || isSrWritable)
.sort()
.groupBy('$container')
const getContainerIds = createSelector(getSrsByContainer, srsByContainer =>
keys(srsByContainer)

View File

@@ -15,6 +15,7 @@ import {
pickBy,
size,
slice,
some,
} from 'lodash'
import invoke from './invoke'
@@ -147,7 +148,9 @@ export const createFilter = (collection, predicate) =>
_createCollectionWrapper(
(collection, predicate) =>
predicate === false
? isArrayLike(collection) ? EMPTY_ARRAY : EMPTY_OBJECT
? isArrayLike(collection)
? EMPTY_ARRAY
: EMPTY_OBJECT
: predicate
? (isArrayLike(collection) ? filter : pickBy)(collection, predicate)
: collection
@@ -541,3 +544,9 @@ export const createGetVmDisks = vmSelector =>
)
)
)
export const getIsPoolAdmin = create(
create(createGetObjectsOfType('pool'), _createCollectionWrapper(Object.keys)),
getCheckPermissions,
(poolsIds, check) => some(poolsIds, poolId => check(poolId, 'administrate'))
)

View File

@@ -209,13 +209,21 @@ class IndividualAction extends Component {
(disabled, item, userData) =>
isFunction(disabled) ? disabled(item, userData) : disabled
)
_getLabel = createSelector(
() => this.props.label,
() => this.props.item,
() => this.props.userData,
(label, item, userData) =>
isFunction(label) ? label(item, userData) : label
)
_executeAction = () => {
const p = this.props
return p.handler(p.item, p.userData)
}
render () {
const { icon, item, label, level, redirectOnSuccess, userData } = this.props
const { icon, item, level, redirectOnSuccess, userData } = this.props
return (
<ActionRowButton
@@ -226,7 +234,7 @@ class IndividualAction extends Component {
handler={this._executeAction}
icon={icon}
redirectOnSuccess={redirectOnSuccess}
tooltip={label}
tooltip={this._getLabel()}
/>
)
}
@@ -240,6 +248,13 @@ class GroupedAction extends Component {
(disabled, selectedItems, userData) =>
isFunction(disabled) ? disabled(selectedItems, userData) : disabled
)
_getLabel = createSelector(
() => this.props.label,
() => this.props.selectedItems,
() => this.props.userData,
(label, selectedItems, userData) =>
isFunction(label) ? label(selectedItems, userData) : label
)
_executeAction = () => {
const p = this.props
@@ -247,7 +262,7 @@ class GroupedAction extends Component {
}
render () {
const { icon, label, level } = this.props
const { icon, level } = this.props
return (
<ActionRowButton
@@ -255,7 +270,7 @@ class GroupedAction extends Component {
disabled={this._getIsDisabled()}
handler={this._executeAction}
icon={icon}
tooltip={label}
tooltip={this._getLabel()}
/>
)
}

View File

@@ -23,7 +23,7 @@ class CreateNetworkModalBody extends Component {
pool: container.$pool,
name: refs.name.value,
description: refs.description.value,
pif: refs.pif.value.id,
pif: refs.pif.value && refs.pif.value.id,
mtu: refs.mtu.value,
vlan: refs.vlan.value,
}

View File

@@ -9,6 +9,7 @@ import {
assign,
filter,
forEach,
get,
includes,
isEmpty,
isEqual,
@@ -461,10 +462,15 @@ export const exportConfig = () =>
// Server ------------------------------------------------------------
export const addServer = (host, username, password, label) =>
_call('server.add', { host, label, password, username })::tap(
subscribeServers.forceRefresh,
() => error(_('serverError'), _('serverAddFailed'))
export const addServer = (host, username, password, label, allowUnauthorized) =>
_call('server.add', {
allowUnauthorized,
host,
label,
password,
username,
})::tap(subscribeServers.forceRefresh, () =>
error(_('serverError'), _('serverAddFailed'))
)
export const editServer = (server, props) =>
@@ -571,6 +577,15 @@ export const editHost = (host, props) =>
export const fetchHostStats = (host, granularity) =>
_call('host.stats', { host: resolveId(host), granularity })
export const setRemoteSyslogHost = (host, syslogDestination) =>
_call('host.setRemoteSyslogHost', {
id: resolveId(host),
syslogDestination,
})
export const setRemoteSyslogHosts = (hosts, syslogDestination) =>
Promise.all(map(hosts, host => setRemoteSyslogHost(host, syslogDestination)))
export const restartHost = (host, force = false) =>
confirm({
title: _('restartHostModalTitle'),
@@ -650,14 +665,26 @@ export const enableHost = host => _call('host.enable', { id: resolveId(host) })
export const disableHost = host =>
_call('host.disable', { id: resolveId(host) })
export const getHostMissingPatches = host =>
_call('host.listMissingPatches', { host: resolveId(host) }).then(
patches =>
// Hide paid patches to XS-free users
host.license_params.sku_type !== 'free'
? patches
: filter(patches, ['paid', false])
)
const missingUpdatePluginByHost = { __proto__: null }
export const getHostMissingPatches = async host => {
const hostId = resolveId(host)
if (host.productBrand !== 'XCP-ng') {
const patches = await _call('host.listMissingPatches', { host: hostId })
// Hide paid patches to XS-free users
return host.license_params.sku_type !== 'free'
? patches
: filter(patches, { paid: false })
}
if (missingUpdatePluginByHost[hostId]) {
return null
}
try {
return await _call('host.listMissingPatches', { host: hostId })
} catch (_) {
missingUpdatePluginByHost[hostId] = true
return null
}
}
export const emergencyShutdownHost = host =>
confirm({
@@ -1102,7 +1129,7 @@ export const migrateVms = vms =>
export const createVm = args => _call('vm.create', args)
export const createVms = (args, nameLabels) =>
export const createVms = (args, nameLabels, cloudConfigs) =>
confirm({
title: _('newVmCreateVms'),
body: _('newVmCreateVmsConfirm', { nbVms: nameLabels.length }),
@@ -1110,8 +1137,15 @@ export const createVms = (args, nameLabels) =>
() =>
Promise.all(
map(nameLabels, (
name_label // eslint-disable-line camelcase
) => _call('vm.create', { ...args, name_label }))
name_label, // eslint-disable-line camelcase
i
) =>
_call('vm.create', {
...args,
name_label,
cloudConfig: get(cloudConfigs, i),
})
)
),
noop
)
@@ -1191,6 +1225,8 @@ export const editVm = (vm, props) =>
export const fetchVmStats = (vm, granularity) =>
_call('vm.stats', { id: resolveId(vm), granularity })
export const getVmsHaValues = () => _call('vm.getHaValues')
export const importVm = (file, type = 'xva', data = undefined, sr) => {
const { name } = file

View File

@@ -4,12 +4,7 @@ import Component from 'base-component'
import { createGetObjectsOfType, createSelector } from 'selectors'
import { map } from 'lodash'
import { subscribeResourceCatalog } from 'xo'
import {
addSubscriptions,
isLatestXosanPackInstalled,
connectStore,
findLatestPack,
} from 'utils'
import { isLatestXosanPackInstalled, connectStore, findLatestPack } from 'utils'
@connectStore(
{
@@ -19,20 +14,19 @@ import {
},
{ withRef: true }
)
@addSubscriptions(() => ({
catalog: subscribeResourceCatalog,
}))
export default class UpdateXosanPacksModal extends Component {
state = {
status: 'checking',
componentDidMount () {
this.componentWillUnmount = subscribeResourceCatalog(catalog =>
this.setState({ catalog })
)
}
get value () {
return this.state.pack
return this._getStatus().pack
}
_getStatus = createSelector(
() => this.props.catalog,
() => this.state.catalog,
() => this.props.hosts,
(catalog, hosts) => {
if (catalog === undefined) {

View File

@@ -35,7 +35,7 @@ import {
import Schedules from './schedules'
import SmartBackup from './smart-backup'
import { FormGroup, getRandomId, Input, Ul, Li } from './utils'
import { FormGroup, getRandomId, Input, Number, Ul, Li } from './utils'
// ===================================================================
@@ -112,6 +112,7 @@ const getInitialState = () => ({
$pool: {},
backupMode: false,
compression: true,
concurrency: 0,
crMode: false,
deltaMode: false,
drMode: false,
@@ -158,6 +159,7 @@ export default [
...getNewSettings(state.newSchedules),
'': {
reportWhen: state.reportWhen,
concurrency: state.concurrency || undefined,
},
},
remotes:
@@ -227,6 +229,7 @@ export default [
if (id === '') {
oldSetting.reportWhen = state.reportWhen
oldSetting.concurrency = state.concurrency || undefined
} else if (!(id in settings)) {
delete oldSettings[id]
} else if (
@@ -330,6 +333,7 @@ export default [
remotes,
srs,
reportWhen: get(globalSettings, 'reportWhen') || 'failure',
concurrency: get(globalSettings, 'concurrency') || 0,
settings,
schedules,
...destructVmsPattern(job.vms),
@@ -491,6 +495,10 @@ export default [
...state,
reportWhen: value,
}),
setConcurrency: (_, concurrency) => state => ({
...state,
concurrency,
}),
},
computed: {
needUpdateParams: (state, { job, schedules }) =>
@@ -751,6 +759,15 @@ export default [
valueKey='value'
/>
</FormGroup>
<FormGroup>
<label>
<strong>{_('concurrency')}</strong>
</label>
<Number
onChange={effects.setConcurrency}
value={state.concurrency}
/>
</FormGroup>
</CardBlock>
</Card>
</Col>

View File

@@ -1,41 +1,13 @@
import _ from 'intl'
import ActionButton from 'action-button'
import moment from 'moment-timezone'
import PropTypes from 'prop-types'
import React from 'react'
import Scheduler, { SchedulePreview } from 'scheduling'
import { Card, CardBlock } from 'card'
import { injectState, provideState } from '@julien-f/freactal'
import { isEqual } from 'lodash'
import { FormGroup, getRandomId, Input } from './utils'
const Number = [
provideState({
effects: {
onChange: (_, { target: { value } }) => (state, props) => {
if (value === '') {
return
}
props.onChange(+value)
},
},
}),
injectState,
({ effects, state, value }) => (
<Input
type='number'
onChange={effects.onChange}
value={String(value)}
min='0'
/>
),
].reduceRight((value, decorator) => decorator(value))
Number.propTypes = {
onChange: PropTypes.func.isRequired,
value: PropTypes.number.isRequired,
}
import { FormGroup, getRandomId, Number } from './utils'
export default [
injectState,

View File

@@ -1,4 +1,6 @@
import PropTypes from 'prop-types'
import React from 'react'
import { injectState, provideState } from '@julien-f/freactal'
export const FormGroup = props => <div {...props} className='form-group' />
export const Input = props => <input {...props} className='form-control' />
@@ -9,3 +11,30 @@ export const getRandomId = () =>
Math.random()
.toString(36)
.slice(2)
export const Number = [
provideState({
effects: {
onChange: (_, { target: { value } }) => (state, props) => {
if (value === '') {
return
}
props.onChange(+value)
},
},
}),
injectState,
({ effects, state, value }) => (
<Input
type='number'
onChange={effects.onChange}
value={String(value)}
min='0'
/>
),
].reduceRight((value, decorator) => decorator(value))
Number.propTypes = {
onChange: PropTypes.func.isRequired,
value: PropTypes.number.isRequired,
}

View File

@@ -74,6 +74,7 @@ import {
createPager,
createSelector,
createSort,
getIsPoolAdmin,
getUser,
isAdmin,
} from 'selectors'
@@ -302,6 +303,7 @@ const DEFAULT_TYPE = 'VM'
})
@propTypes({
isAdmin: propTypes.bool.isRequired,
isPoolAdmin: propTypes.bool.isRequired,
noResourceSets: propTypes.bool.isRequired,
})
class NoObjects_ extends Component {
@@ -309,6 +311,7 @@ class NoObjects_ extends Component {
const {
areObjectsFetched,
isAdmin,
isPoolAdmin,
noRegisteredServers,
noResourceSets,
noServersConnected,
@@ -378,7 +381,9 @@ class NoObjects_ extends Component {
<CenterPanel>
<Card shadow>
<CardHeader>{_('homeNoVms')}</CardHeader>
{(isAdmin || !noResourceSets) && (
{(isAdmin ||
(isPoolAdmin && process.env.XOA_PLAN > 3) ||
!noResourceSets) && (
<CardBlock>
<Row>
<Col>
@@ -428,6 +433,7 @@ class NoObjects_ extends Component {
return {
isAdmin,
isPoolAdmin: getIsPoolAdmin,
items: createSelector(
createSelector(
createGetObjectsOfType('host'),
@@ -818,7 +824,7 @@ export default class Home extends Component {
const customFilters = this._getCustomFilters()
const filteredItems = this._getFilteredItems()
const nItems = this._getNumberOfItems()
const { isAdmin, items, noResourceSets, type } = this.props
const { isAdmin, isPoolAdmin, items, noResourceSets, type } = this.props
const {
selectedHosts,
@@ -906,7 +912,9 @@ export default class Home extends Component {
</span>
</div>
</Col>
{(isAdmin || !noResourceSets) && (
{(isAdmin ||
(isPoolAdmin && process.env.XOA_PLAN > 3) ||
!noResourceSets) && (
<Col mediumSize={3} className='text-xs-right'>
<Link className='btn btn-success' to='/vms/new'>
<Icon icon='vm-new' /> {_('homeNewVm')}
@@ -1105,12 +1113,18 @@ export default class Home extends Component {
// ---------------------------------------------------------------------------
render () {
const { isAdmin, noResourceSets } = this.props
const { isAdmin, isPoolAdmin, noResourceSets } = this.props
const nItems = this._getNumberOfItems()
if (nItems < 1) {
return <NoObjects_ isAdmin={isAdmin} noResourceSets={noResourceSets} />
return (
<NoObjects_
isAdmin={isAdmin}
isPoolAdmin={isPoolAdmin}
noResourceSets={noResourceSets}
/>
)
}
const filteredItems = this._getFilteredItems()

View File

@@ -8,13 +8,7 @@ import React, { cloneElement, Component } from 'react'
import Tooltip from 'tooltip'
import { Text } from 'editable'
import { Container, Row, Col } from 'grid'
import {
editHost,
fetchHostStats,
installAllHostPatches,
installHostPatch,
subscribeHostMissingPatches,
} from 'xo'
import { editHost, fetchHostStats, subscribeHostMissingPatches } from 'xo'
import { connectStore, routes } from 'utils'
import {
createDoesHostNeedRestart,
@@ -110,7 +104,8 @@ const isRunning = host => host && host.power_state === 'Running'
return {
host,
hostPatches: getHostPatches(state, props),
hostPatches:
host.productBrand !== 'XCP-ng' && getHostPatches(state, props),
logs: getLogs(state, props),
memoryUsed: getMemoryUsed(state, props),
needsRestart: doesNeedRestart(state, props),
@@ -207,21 +202,12 @@ export default class Host extends Component {
host,
missingPatches =>
this.setState({
missingPatches: sortBy(missingPatches, patch => -patch.time),
missingPatches:
missingPatches && sortBy(missingPatches, patch => -patch.time),
})
)
}
_installAllPatches = () => {
const { host } = this.props
return installAllHostPatches(host)
}
_installPatch = patch => {
const { host } = this.props
return installHostPatch(host, patch)
}
_setNameDescription = nameDescription =>
editHost(this.props.host, { name_description: nameDescription })
_setNameLabel = nameLabel =>
@@ -331,11 +317,7 @@ export default class Host extends Component {
'vmController',
'vms',
]),
pick(this.state, ['missingPatches', 'statsOverview']),
{
installAllPatches: this._installAllPatches,
installPatch: this._installPatch,
}
pick(this.state, ['missingPatches', 'statsOverview'])
)
return (
<Page

View File

@@ -5,20 +5,22 @@ import React from 'react'
import TabButton from 'tab-button'
import SelectFiles from 'select-files'
import Upgrade from 'xoa-upgrade'
import { compareVersions, connectStore } from 'utils'
import { Text } from 'editable'
import { Toggle } from 'form'
import { compareVersions, connectStore } from 'utils'
import { FormattedRelative, FormattedTime } from 'react-intl'
import { Container, Row, Col } from 'grid'
import { forEach, map, noop } from 'lodash'
import { createGetObjectsOfType, createSelector } from 'selectors'
import {
enableHost,
detachHost,
disableHost,
forgetHost,
setRemoteSyslogHost,
restartHost,
installSupplementalPack,
} from 'xo'
import { FormattedRelative, FormattedTime } from 'react-intl'
import { Container, Row, Col } from 'grid'
import { createGetObjectsOfType, createSelector } from 'selectors'
import { forEach, map, noop } from 'lodash'
const ALLOW_INSTALL_SUPP_PACK = process.env.XOA_PLAN > 1
@@ -66,6 +68,7 @@ export default class extends Component {
return uniqPacks
}
)
_setRemoteSyslogHost = value => setRemoteSyslogHost(this.props.host, value)
render () {
const { host, pcis, pgpus } = this.props
@@ -182,6 +185,15 @@ export default class extends Component {
<th>{_('hostIscsiName')}</th>
<Copiable tagName='td'>{host.iSCSI_name}</Copiable>
</tr>
<tr>
<th>{_('hostRemoteSyslog')}</th>
<td>
<Text
value={host.logging.syslog_destination || ''}
onChange={this._setRemoteSyslogHost}
/>
</td>
</tr>
</tbody>
</table>
<br />

View File

@@ -85,9 +85,9 @@ export default ({
</Col>
<Col mediumSize={3}>
<p>
{host.license_params.sku_marketing_name} {host.version} ({
host.license_params.sku_type
})
{host.productBrand} {host.version} ({host.productBrand !== 'XCP-ng'
? host.license_params.sku_type
: 'GPLv2'})
</p>
</Col>
<Col mediumSize={3}>

View File

@@ -1,5 +1,6 @@
import _ from 'intl'
import Component from 'base-component'
import copy from 'copy-to-clipboard'
import React from 'react'
import Icon from 'icon'
import pick from 'lodash/pick'
@@ -14,6 +15,7 @@ import { connectStore, noop } from 'utils'
import { Container, Row, Col } from 'grid'
import { createGetObjectsOfType } from 'selectors'
import { error } from 'notification'
import { get } from 'xo-defined'
import { Select, Number } from 'editable'
import { Toggle } from 'form'
import {
@@ -191,12 +193,18 @@ class PifItemLock extends Component {
render () {
const { networks, pif, vifsByNetwork } = this.props
const network = networks[pif.$network]
if (network === undefined) {
return null
}
const pifInUse = some(vifsByNetwork[pif.$network], vif => vif.attached)
return _toggleDefaultLockingMode(
<Toggle
disabled={pifInUse}
onChange={this._editNetwork}
value={networks[pif.$network].defaultIsLocked}
value={network.defaultIsLocked}
/>,
pifInUse && _('pifInUse')
)
@@ -211,9 +219,11 @@ const COLUMNS = [
sortCriteria: 'device',
},
{
itemRenderer: (pif, userData) => userData.networks[pif.$network].name_label,
itemRenderer: (pif, userData) =>
get(() => userData.networks[pif.$network].name_label),
name: _('pifNetworkLabel'),
sortCriteria: (pif, userData) => userData.networks[pif.$network].name_label,
sortCriteria: (pif, userData) =>
get(() => userData.networks[pif.$network].name_label),
},
{
component: PifItemVlan,
@@ -284,6 +294,11 @@ const COLUMNS = [
]
const INDIVIDUAL_ACTIONS = [
{
handler: pif => copy(pif.uuid),
icon: 'clipboard',
label: pif => _('copyUuid', { uuid: pif.uuid }),
},
{
handler: deletePif,
icon: 'delete',

View File

@@ -1,15 +1,14 @@
import _ from 'intl'
import ActionRowButton from 'action-row-button'
import React, { Component } from 'react'
import SortedTable from 'sorted-table'
import TabButton from 'tab-button'
import Upgrade from 'xoa-upgrade'
import { chooseAction } from 'modal'
import { alert, chooseAction } from 'modal'
import { connectStore, formatSize } from 'utils'
import { Container, Row, Col } from 'grid'
import { createDoesHostNeedRestart, createSelector } from 'selectors'
import { FormattedRelative, FormattedTime } from 'react-intl'
import { restartHost } from 'xo'
import { restartHost, installAllHostPatches, installHostPatch } from 'xo'
import { isEmpty, isString } from 'lodash'
const MISSING_PATCH_COLUMNS = [
@@ -48,15 +47,76 @@ const MISSING_PATCH_COLUMNS = [
itemRenderer: patch => patch.guidance,
sortCriteria: patch => patch.guidance,
},
]
const MISSING_PATCH_COLUMNS_XCP = [
{
name: _('patchAction'),
itemRenderer: (patch, { installPatch, _installPatchWarning }) => (
<ActionRowButton
btnStyle='primary'
handler={() => _installPatchWarning(patch, installPatch)}
icon='host-patch-update'
/>
),
name: _('patchNameLabel'),
itemRenderer: patch => patch.name,
sortCriteria: 'name',
},
{
name: _('patchDescription'),
itemRenderer: patch => patch.description,
sortCriteria: 'description',
},
{
name: _('patchVersion'),
itemRenderer: patch => patch.version,
},
{
name: _('patchRelease'),
itemRenderer: patch => patch.release,
},
{
name: _('patchSize'),
itemRenderer: patch => formatSize(patch.size),
sortCriteria: 'size',
},
]
const INDIVIDUAL_ACTIONS_XCP = [
{
disabled: patch => patch.changelog === null,
handler: patch =>
alert(
_('changelog'),
<Container>
<Row className='mb-1'>
<Col size={3}>
<strong>{_('changelogPatch')}</strong>
</Col>
<Col size={9}>{patch.name}</Col>
</Row>
<Row className='mb-1'>
<Col size={3}>
<strong>{_('changelogDate')}</strong>
</Col>
<Col size={9}>
<FormattedTime
value={patch.changelog.date * 1000}
day='numeric'
month='long'
year='numeric'
/>
</Col>
</Row>
<Row className='mb-1'>
<Col size={3}>
<strong>{_('changelogAuthor')}</strong>
</Col>
<Col size={9}>{patch.changelog.author}</Col>
</Row>
<Row>
<Col size={3}>
<strong>{_('changelogDescription')}</strong>
</Col>
<Col size={9}>{patch.changelog.description}</Col>
</Row>
</Container>
),
icon: 'preview',
label: _('showChangelog'),
},
]
@@ -118,10 +178,139 @@ const INSTALLED_PATCH_COLUMNS_2 = [
},
]
class XcpPatches extends Component {
render () {
const { missingPatches, host, installAllPatches } = this.props
const hasMissingPatches = !isEmpty(missingPatches)
return (
<Container>
<Row>
<Col className='text-xs-right'>
{this.props.needsRestart && (
<TabButton
btnStyle='warning'
handler={restartHost}
handlerParam={host}
icon='host-reboot'
labelId='rebootUpdateHostLabel'
/>
)}
<TabButton
disabled={!hasMissingPatches}
btnStyle={hasMissingPatches ? 'primary' : undefined}
handler={installAllPatches}
icon={hasMissingPatches ? 'host-patch-update' : 'success'}
labelId={hasMissingPatches ? 'patchUpdateButton' : 'hostUpToDate'}
/>
</Col>
</Row>
{hasMissingPatches && (
<Row>
<Col>
<SortedTable
columns={MISSING_PATCH_COLUMNS_XCP}
collection={missingPatches}
individualActions={INDIVIDUAL_ACTIONS_XCP}
/>
</Col>
</Row>
)}
</Container>
)
}
}
@connectStore(() => ({
needsRestart: createDoesHostNeedRestart((_, props) => props.host),
}))
export default class HostPatches extends Component {
class XenServerPatches extends Component {
_getPatches = createSelector(
() => this.props.host,
() => this.props.hostPatches,
(host, hostPatches) => {
if (isEmpty(host.patches) && isEmpty(hostPatches)) {
return { patches: null }
}
if (isString(host.patches[0])) {
return {
patches: hostPatches,
columns: INSTALLED_PATCH_COLUMNS,
}
}
return {
patches: host.patches,
columns: INSTALLED_PATCH_COLUMNS_2,
}
}
)
_individualActions = [
{
name: _('patchAction'),
level: 'primary',
handler: this.props.installPatch,
icon: 'host-patch-update',
},
]
render () {
const { host, missingPatches, installAllPatches } = this.props
const { patches, columns } = this._getPatches()
const hasMissingPatches = !isEmpty(missingPatches)
return (
<Container>
<Row>
<Col className='text-xs-right'>
{this.props.needsRestart && (
<TabButton
btnStyle='warning'
handler={restartHost}
handlerParam={host}
icon='host-reboot'
labelId='rebootUpdateHostLabel'
/>
)}
<TabButton
disabled={!hasMissingPatches}
btnStyle={hasMissingPatches ? 'primary' : undefined}
handler={installAllPatches}
icon={hasMissingPatches ? 'host-patch-update' : 'success'}
labelId={hasMissingPatches ? 'patchUpdateButton' : 'hostUpToDate'}
/>
</Col>
</Row>
{hasMissingPatches && (
<Row>
<Col>
<h3>{_('hostMissingPatches')}</h3>
<SortedTable
individualActions={this._individualActions}
collection={missingPatches}
columns={MISSING_PATCH_COLUMNS}
/>
</Col>
</Row>
)}
<Row>
<Col>
{patches ? (
<span>
<h3>{_('hostAppliedPatches')}</h3>
<SortedTable collection={patches} columns={columns} />
</span>
) : (
<h4 className='text-xs-center'>{_('patchNothing')}</h4>
)}
</Col>
</Row>
</Container>
)
}
}
export default class TabPatches extends Component {
static contextTypes = {
router: React.PropTypes.object,
}
@@ -145,93 +334,31 @@ export default class HostPatches extends Component {
: this.context.router.push(`/pools/${this.props.host.$pool}/patches`)
}
_installPatchWarning = (patch, installPatch) =>
this._chooseActionPatch(() => installPatch(patch))
_installAllPatches = () =>
this._chooseActionPatch(() => installAllHostPatches(this.props.host))
_installAllPatchesWarning = installAllPatches =>
this._chooseActionPatch(installAllPatches)
_getPatches = createSelector(
() => this.props.host,
() => this.props.hostPatches,
(host, hostPatches) => {
if (isEmpty(host.patches) && isEmpty(hostPatches)) {
return { patches: null }
}
if (isString(host.patches[0])) {
return {
patches: hostPatches,
columns: INSTALLED_PATCH_COLUMNS,
}
}
return {
patches: host.patches,
columns: INSTALLED_PATCH_COLUMNS_2,
}
}
)
_installPatch = patch =>
this._chooseActionPatch(() => installHostPatch(this.props.host, patch))
render () {
const { host, missingPatches, installAllPatches, installPatch } = this.props
const { patches, columns } = this._getPatches()
const hasMissingPatches = !isEmpty(missingPatches)
return process.env.XOA_PLAN > 1 ? (
<Container>
<Row>
<Col className='text-xs-right'>
{this.props.needsRestart && (
<TabButton
btnStyle='warning'
handler={restartHost}
handlerParam={host}
icon='host-reboot'
labelId='rebootUpdateHostLabel'
/>
)}
<TabButton
disabled={!hasMissingPatches}
btnStyle={hasMissingPatches ? 'primary' : undefined}
handler={this._installAllPatchesWarning}
handlerParam={installAllPatches}
icon={hasMissingPatches ? 'host-patch-update' : 'success'}
labelId={hasMissingPatches ? 'patchUpdateButton' : 'hostUpToDate'}
/>
</Col>
</Row>
{hasMissingPatches && (
<Row>
<Col>
<h3>{_('hostMissingPatches')}</h3>
<SortedTable
collection={missingPatches}
userData={{
installPatch,
_installPatchWarning: this._installPatchWarning,
}}
columns={MISSING_PATCH_COLUMNS}
/>
</Col>
</Row>
)}
<Row>
<Col>
{patches ? (
<span>
<h3>{_('hostAppliedPatches')}</h3>
<SortedTable collection={patches} columns={columns} />
</span>
) : (
<h4 className='text-xs-center'>{_('patchNothing')}</h4>
)}
</Col>
</Row>
</Container>
if (process.env.XOA_PLAN < 2) {
return (
<Container>
<Upgrade place='hostPatches' available={2} />
</Container>
)
}
if (this.props.missingPatches === null) {
return <em>{_('updatePluginNotInstalled')}</em>
}
return this.props.host.productBrand === 'XCP-ng' ? (
<XcpPatches {...this.props} installAllPatches={this._installAllPatches} />
) : (
<Container>
<Upgrade place='hostPatches' available={2} />
</Container>
<XenServerPatches
{...this.props}
installAllPatches={this._installAllPatches}
installPatch={this._installPatch}
/>
)
}
}

View File

@@ -374,9 +374,12 @@ export default class Jobs extends Component {
forEach(item.values, valueItem => {
forEach(valueItem, (value, key) => {
if (data[key] === undefined) {
data[key] = []
data[key] = value
} else if (Array.isArray(data[key])) {
data[key].push(value)
} else {
data[key] = [data[key], value]
}
data[key].push(value)
})
})
})

View File

@@ -6,7 +6,7 @@ import React from 'react'
import SortedTable from 'sorted-table'
import { alert } from 'modal'
import { Card, CardHeader, CardBlock } from 'card'
import { forEach, keyBy } from 'lodash'
import { keyBy } from 'lodash'
import { FormattedDate } from 'react-intl'
import { get } from 'xo-defined'
import {
@@ -16,7 +16,6 @@ import {
} from 'xo'
import LogAlertBody from './log-alert-body'
import { isSkippedError, NO_VMS_MATCH_THIS_PATTERN } from './utils'
const STATUS_LABELS = {
failure: {
@@ -31,7 +30,7 @@ const STATUS_LABELS = {
className: 'success',
label: 'jobSuccess',
},
started: {
pending: {
className: 'warning',
label: 'jobStarted',
},
@@ -94,10 +93,10 @@ const LOG_COLUMNS = [
{
name: _('jobDuration'),
itemRenderer: log =>
log.duration !== undefined && (
<FormattedDuration duration={log.duration} />
log.end !== undefined && (
<FormattedDuration duration={log.end - log.start} />
),
sortCriteria: log => log.duration,
sortCriteria: log => log.end - log.start,
},
{
name: _('jobStatus'),
@@ -108,15 +107,20 @@ const LOG_COLUMNS = [
},
]
const showCalls = (log, { logs, jobs }) =>
const showTasks = log =>
alert(
_('jobModalTitle', { job: log.jobId.slice(4, 8) }),
<LogAlertBody log={log} job={get(() => jobs[log.jobId])} logs={logs} />
<span>
{_('jobModalTitle', { job: log.jobId.slice(4, 8) })}{' '}
<span style={{ fontSize: '0.5em' }} className='text-muted'>
{log.id}
</span>
</span>,
<LogAlertBody log={log} />
)
const LOG_INDIVIDUAL_ACTIONS = [
{
handler: showCalls,
handler: showTasks,
icon: 'preview',
label: _('logDisplayDetails'),
},
@@ -138,38 +142,6 @@ const LOG_FILTERS = {
jobSuccess: 'status: success',
}
const rowTransform = (log, { logs, jobs }) => {
let status
if (log.end !== undefined) {
if (log.error !== undefined) {
status =
log.error.message === NO_VMS_MATCH_THIS_PATTERN ? 'skipped' : 'failure'
} else {
let hasError = false
let hasTaskSkipped = false
forEach(logs[log.id], ({ status, result }) => {
if (status !== 'failure') {
return
}
if (result === undefined || !isSkippedError(result)) {
hasError = true
return false
}
hasTaskSkipped = true
})
status = hasError ? 'failure' : hasTaskSkipped ? 'skipped' : 'success'
}
} else {
status =
log.id === get(() => jobs[log.jobId].runId) ? 'started' : 'interrupted'
}
return {
...log,
status,
}
}
export default [
addSubscriptions({
logs: subscribeBackupNgLogs,
@@ -183,15 +155,13 @@ export default [
<CardBlock>
<NoObjects
actions={LOG_ACTIONS}
collection={get(() => logs['roots'])}
collection={logs}
columns={LOG_COLUMNS}
component={SortedTable}
data-jobs={jobs}
data-logs={logs}
emptyMessage={_('noLogs')}
filters={LOG_FILTERS}
individualActions={LOG_INDIVIDUAL_ACTIONS}
rowTransform={rowTransform}
/>
</CardBlock>
</Card>

View File

@@ -13,8 +13,8 @@ import Tooltip from 'tooltip'
import { alert } from 'modal'
import { Card, CardHeader, CardBlock } from 'card'
import { connectStore, formatSize, formatSpeed } from 'utils'
import { createFilter, createGetObject, createSelector } from 'selectors'
import { forEach, includes, keyBy, map, orderBy } from 'lodash'
import { createGetObject, createSelector } from 'selectors'
import { filter, forEach, includes, keyBy, map, orderBy } from 'lodash'
import { FormattedDate } from 'react-intl'
import { get } from 'xo-defined'
import {
@@ -141,8 +141,6 @@ const isSkippedError = error =>
error.message === UNHEALTHY_VDI_CHAIN_ERROR ||
error.message === NO_SUCH_OBJECT_ERROR
const filterOptionRenderer = ({ label }) => _(label)
class Log extends BaseComponent {
state = {
filter: DEFAULT_CALL_FILTER,
@@ -154,21 +152,29 @@ class Log extends BaseComponent {
(logId, runId) => logId !== runId
)
_getFilteredCalls = createFilter(
_getCallsByState = createSelector(
() => this.props.log.calls,
createSelector(
() => this.state.filter.value,
this._getIsJobInterrupted,
(value, isInterrupted) => PREDICATES[value](isInterrupted)
)
this._getIsJobInterrupted,
(calls, isInterrupted) => {
const callsByState = {}
forEach(CALL_FILTER_OPTIONS, ({ value }) => {
callsByState[value] = filter(calls, PREDICATES[value](isInterrupted))
})
return callsByState
}
)
_filterValueRenderer = createSelector(
() => this._getFilteredCalls().length,
({ label }) => label,
(size, label) => (
_getFilteredCalls = createSelector(
() => this.state.filter.value,
this._getCallsByState,
(value, calls) => calls[value]
)
_getFilterOptionRenderer = createSelector(
this._getCallsByState,
calls => ({ label, value }) => (
<span>
{_(label)} ({size})
{_(label)} ({calls[value].length})
</span>
)
)
@@ -190,12 +196,11 @@ class Log extends BaseComponent {
<Select
labelKey='label'
onChange={this.linkState('filter')}
optionRenderer={filterOptionRenderer}
optionRenderer={this._getFilterOptionRenderer()}
options={CALL_FILTER_OPTIONS}
required
value={this.state.filter}
valueKey='value'
valueRenderer={this._filterValueRenderer}
/>
<br />
<ul className='list-group'>
@@ -394,7 +399,9 @@ const LOG_COLUMNS = [
'tag',
log.hasErrors
? 'tag-danger'
: log.callSkipped ? 'tag-info' : 'tag-success'
: log.callSkipped
? 'tag-info'
: 'tag-success'
)}
>
{_('jobFinished')}

View File

@@ -1,4 +1,5 @@
import _, { FormattedDuration } from 'intl'
import ActionButton from 'action-button'
import Copiable from 'copiable'
import Icon from 'icon'
import React from 'react'
@@ -6,37 +7,10 @@ import renderXoItem, { renderXoItemFromId } from 'render-xo-item'
import Select from 'form/select'
import Tooltip from 'tooltip'
import { addSubscriptions, formatSize, formatSpeed } from 'utils'
import { createSelector } from 'selectors'
import { find, filter, isEmpty, get, keyBy, map, forEach } from 'lodash'
import { filter, isEmpty, get, keyBy, map } from 'lodash'
import { FormattedDate } from 'react-intl'
import { injectState, provideState } from '@julien-f/freactal'
import { subscribeRemotes } from 'xo'
import {
isSkippedError,
NO_VMS_MATCH_THIS_PATTERN,
UNHEALTHY_VDI_CHAIN_ERROR,
} from './utils'
const getTaskStatus = createSelector(
taskLog => taskLog,
isJobRunning => isJobRunning,
({ end, status, result }, isJobRunning) =>
end !== undefined
? status === 'success'
? 'success'
: result !== undefined && isSkippedError(result) ? 'skipped' : 'failure'
: isJobRunning ? 'started' : 'interrupted'
)
const getSubTaskStatus = createSelector(
taskLog => taskLog,
isJobRunning => isJobRunning,
({ end, status, result }, isJobRunning) =>
end !== undefined
? status === 'success' ? 'success' : 'failure'
: isJobRunning ? 'started' : 'interrupted'
)
import { runBackupNgJob, subscribeRemotes } from 'xo'
const TASK_STATUS = {
failure: {
@@ -51,7 +25,7 @@ const TASK_STATUS = {
icon: 'running',
label: 'taskSuccess',
},
started: {
pending: {
icon: 'busy',
label: 'taskStarted',
},
@@ -70,90 +44,60 @@ const TaskStateInfos = ({ status }) => {
)
}
const VmTaskDataInfos = ({ logs, vmTaskId }) => {
let transferSize, transferDuration, mergeSize, mergeDuration
forEach(logs[vmTaskId], ({ taskId }) => {
if (transferSize !== undefined) {
return false
}
const transferTask = find(logs[taskId], { message: 'transfer' })
if (transferTask !== undefined) {
transferSize = transferTask.result.size
transferDuration = transferTask.end - transferTask.start
}
const mergeTask = find(logs[taskId], { message: 'merge' })
if (mergeTask !== undefined) {
mergeSize = mergeTask.result.size
mergeDuration = mergeTask.end - mergeTask.start
}
})
if (transferSize === undefined) {
return null
}
return (
<div>
{_.keyValue(_('taskTransferredDataSize'), formatSize(transferSize))}
<br />
{_.keyValue(
_('taskTransferredDataSpeed'),
formatSpeed(transferSize, transferDuration)
)}
{mergeSize !== undefined && (
<div>
{_.keyValue(_('taskMergedDataSize'), formatSize(mergeSize))}
<br />
{_.keyValue(
_('taskMergedDataSpeed'),
formatSpeed(mergeSize, mergeDuration)
)}
</div>
)}
</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 UNHEALTHY_VDI_CHAIN_ERROR = 'unhealthy VDI chain'
const UNHEALTHY_VDI_CHAIN_LINK =
'https://xen-orchestra.com/docs/backup_troubleshooting.html#vdi-chain-protection'
const ALL_FILTER_OPTION = { label: 'allTasks', value: 'all' }
const FAILURE_FILTER_OPTION = { label: 'taskFailed', value: 'failure' }
const STARTED_FILTER_OPTION = { label: 'taskStarted', value: 'started' }
const PENDING_FILTER_OPTION = { label: 'taskStarted', value: 'pending' }
const INTERRUPTED_FILTER_OPTION = {
label: 'taskInterrupted',
value: 'interrupted',
}
const TASK_FILTER_OPTIONS = [
ALL_FILTER_OPTION,
FAILURE_FILTER_OPTION,
STARTED_FILTER_OPTION,
{ label: 'taskInterrupted', value: 'interrupted' },
PENDING_FILTER_OPTION,
INTERRUPTED_FILTER_OPTION,
{ label: 'taskSkipped', value: 'skipped' },
{ label: 'taskSuccess', value: 'success' },
]
const getFilteredTaskLogs = (logs, isJobRunning, filterValue) =>
const getFilteredTaskLogs = (logs, filterValue) =>
filterValue === 'all'
? logs
: filter(logs, log => getTaskStatus(log, isJobRunning) === filterValue)
: filter(logs, ({ status }) => status === filterValue)
const getInitialFilter = (job, logs, log) => {
const getInitialFilter = tasks => {
const isEmptyFilter = filterValue =>
isEmpty(
getFilteredTaskLogs(
logs[log.id],
get(job, 'runId') === log.id,
filterValue
)
)
isEmpty(getFilteredTaskLogs(tasks, filterValue))
if (!isEmptyFilter('started')) {
return STARTED_FILTER_OPTION
if (!isEmptyFilter('pending')) {
return PENDING_FILTER_OPTION
}
if (!isEmptyFilter('failure')) {
return FAILURE_FILTER_OPTION
}
if (!isEmptyFilter('interrupted')) {
return INTERRUPTED_FILTER_OPTION
}
return ALL_FILTER_OPTION
}
@@ -165,43 +109,41 @@ export default [
}),
}),
provideState({
initialState: ({ job, logs, log }) => ({
filter: getInitialFilter(job, logs, log),
initialState: ({ log }) => ({
filter: getInitialFilter(log.tasks),
}),
effects: {
setFilter: (_, filter) => state => ({
...state,
filter,
}),
restartVmJob: (_, { vm }) => async (
_,
{ log: { scheduleId, jobId } }
) => {
await runBackupNgJob({
id: jobId,
vm,
schedule: scheduleId,
})
},
},
computed: {
isJobRunning: (_, { job, log }) => get(job, 'runId') === log.id,
filteredTaskLogs: ({ filter: { value }, isJobRunning }, { log, logs }) =>
getFilteredTaskLogs(logs[log.id], isJobRunning, value),
optionRenderer: ({ isJobRunning }, { log, logs }) => ({
label,
value,
}) => (
filteredTaskLogs: ({ filter: { value } }, { log }) =>
getFilteredTaskLogs(log.tasks, value),
optionRenderer: (state, { log }) => ({ label, value }) => (
<span>
{_(label)} ({
getFilteredTaskLogs(logs[log.id], isJobRunning, value).length
})
{_(label)} ({getFilteredTaskLogs(log.tasks, value).length})
</span>
),
},
}),
injectState,
({ job, log, logs, remotes, state, effects }) =>
log.error !== undefined ? (
<span
className={
log.error.message === NO_VMS_MATCH_THIS_PATTERN
? 'text-info'
: 'text-danger'
}
>
<Copiable tagName='p' data={JSON.stringify(log.error, null, 2)}>
<Icon icon='alarm' /> {log.error.message}
({ log, remotes, state, effects }) =>
log.result !== undefined ? (
<span className={log.status === 'skipped' ? 'text-info' : 'text-danger'}>
<Copiable tagName='p' data={JSON.stringify(log.result, null, 2)}>
<Icon icon='alarm' /> {log.result.message}
</Copiable>
</span>
) : (
@@ -217,18 +159,25 @@ export default [
/>
<br />
<ul className='list-group'>
{map(state.filteredTaskLogs, vmTaskLog => (
<li key={vmTaskLog.data.id} className='list-group-item'>
{renderXoItemFromId(vmTaskLog.data.id)} ({vmTaskLog.data.id.slice(
{map(state.filteredTaskLogs, taskLog => (
<li key={taskLog.data.id} className='list-group-item'>
{renderXoItemFromId(taskLog.data.id)} ({taskLog.data.id.slice(
4,
8
)}){' '}
<TaskStateInfos
status={getTaskStatus(vmTaskLog, state.isJobRunning)}
/>
)}) <TaskStateInfos status={taskLog.status} />{' '}
{log.scheduleId !== undefined &&
taskLog.status === 'failure' && (
<ActionButton
handler={effects.restartVmJob}
icon='run'
size='small'
tooltip={_('backupRestartVm')}
data-vm={taskLog.data.id}
/>
)}
<ul>
{map(logs[vmTaskLog.taskId], subTaskLog => (
<li key={subTaskLog.taskId}>
{map(taskLog.tasks, subTaskLog => (
<li key={subTaskLog.id}>
{subTaskLog.message === 'snapshot' ? (
<span>
<Icon icon='task' /> {_('snapshotVmLabel')}
@@ -251,61 +200,116 @@ export default [
)})
</span>
)}{' '}
<TaskStateInfos
status={getSubTaskStatus(subTaskLog, state.isJobRunning)}
/>
<br />
{subTaskLog.status === 'failure' && (
<Copiable
tagName='p'
data={JSON.stringify(subTaskLog.result, null, 2)}
>
{_.keyValue(
_('taskError'),
<span className={'text-danger'}>
{subTaskLog.result.message}
</span>
)}
</Copiable>
<TaskStateInfos status={subTaskLog.status} />
<ul>
{map(subTaskLog.tasks, operationLog => (
<li key={operationLog.id}>
<span>
<Icon icon='task' /> {operationLog.message}
</span>{' '}
<TaskStateInfos status={operationLog.status} />
<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' ? (
<Copiable
tagName='p'
data={JSON.stringify(
operationLog.result,
null,
2
)}
>
{_.keyValue(
_('taskError'),
<span className='text-danger'>
{operationLog.result.message}
</span>
)}
</Copiable>
) : (
<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 && (
<Copiable
tagName='p'
data={JSON.stringify(subTaskLog.result, null, 2)}
>
{_.keyValue(
_('taskError'),
<span className='text-danger'>
{subTaskLog.result.message}
</span>
)}
</Copiable>
)}
</div>
)}
</li>
))}
</ul>
{_.keyValue(
_('taskStart'),
<FormattedDate
value={new Date(vmTaskLog.start)}
month='short'
day='numeric'
year='numeric'
hour='2-digit'
minute='2-digit'
second='2-digit'
/>
)}
{vmTaskLog.end !== undefined && (
<TaskDate label='taskStart' value={taskLog.start} />
{taskLog.end !== undefined && (
<div>
{_.keyValue(
_('taskEnd'),
<FormattedDate
value={new Date(vmTaskLog.end)}
month='short'
day='numeric'
year='numeric'
hour='2-digit'
minute='2-digit'
second='2-digit'
/>
)}
<TaskDate label='taskEnd' value={taskLog.end} />
<br />
{_.keyValue(
_('taskDuration'),
<FormattedDuration duration={vmTaskLog.duration} />
<FormattedDuration duration={taskLog.end - taskLog.start} />
)}
<br />
{vmTaskLog.status === 'failure' &&
vmTaskLog.result !== undefined ? (
vmTaskLog.result.message === UNHEALTHY_VDI_CHAIN_ERROR ? (
{taskLog.result !== undefined ? (
taskLog.result.message === UNHEALTHY_VDI_CHAIN_ERROR ? (
<Tooltip content={_('clickForMoreInformation')}>
<a
className='text-info'
@@ -319,24 +323,59 @@ export default [
) : (
<Copiable
tagName='p'
data={JSON.stringify(vmTaskLog.result, null, 2)}
data={JSON.stringify(taskLog.result, null, 2)}
>
{_.keyValue(
_('taskError'),
taskLog.status === 'skipped'
? _('taskReason')
: _('taskError'),
<span
className={
isSkippedError(vmTaskLog.result)
taskLog.status === 'skipped'
? 'text-info'
: 'text-danger'
}
>
{vmTaskLog.result.message}
{taskLog.result.message}
</span>
)}
</Copiable>
)
) : (
<VmTaskDataInfos logs={logs} vmTaskId={vmTaskLog.taskId} />
<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>
)}

View File

@@ -1,7 +0,0 @@
export const NO_VMS_MATCH_THIS_PATTERN = 'no VMs match this pattern'
export const UNHEALTHY_VDI_CHAIN_ERROR = 'unhealthy VDI chain'
const NO_SUCH_OBJECT_ERROR = 'no such object'
export const isSkippedError = error =>
error.message === UNHEALTHY_VDI_CHAIN_ERROR ||
error.message === NO_SUCH_OBJECT_ERROR

View File

@@ -19,6 +19,7 @@ import {
createFilter,
createGetObjectsOfType,
createSelector,
getIsPoolAdmin,
getStatus,
getUser,
isAdmin,
@@ -31,6 +32,7 @@ const returnTrue = () => true
@connectStore(
() => ({
isAdmin,
isPoolAdmin: getIsPoolAdmin,
nTasks: createGetObjectsOfType('task').count([
task => task.status === 'pending',
]),
@@ -80,11 +82,6 @@ export default class Menu extends Component {
isEmpty
)
_getNoOperatableSrs = createSelector(
createFilter(() => this.props.srs, this._checkPermissions),
isEmpty
)
_getNoResourceSets = createSelector(() => this.props.resourceSets, isEmpty)
get height () {
@@ -108,9 +105,17 @@ export default class Menu extends Component {
}
render () {
const { isAdmin, nTasks, status, user, pools, nHosts } = this.props
const {
isAdmin,
isPoolAdmin,
nTasks,
status,
user,
pools,
nHosts,
srs,
} = this.props
const noOperatablePools = this._getNoOperatablePools()
const noOperatableSrs = this._getNoOperatableSrs()
const noResourceSets = this._getNoResourceSets()
/* eslint-disable object-property-newline */
@@ -136,7 +141,7 @@ export default class Menu extends Component {
icon: 'template',
label: 'homeTemplatePage',
},
!noOperatableSrs && {
!isEmpty(srs) && {
to: '/home?t=SR',
icon: 'sr',
label: 'homeSrPage',
@@ -313,7 +318,9 @@ export default class Menu extends Component {
icon: 'menu-new',
label: 'newMenu',
subMenu: [
(isAdmin || !noResourceSets) && {
(isAdmin ||
(isPoolAdmin && process.env.XOA_PLAN > 3) ||
!noResourceSets) && {
to: '/vms/new',
icon: 'menu-new-vm',
label: 'newVmPage',

View File

@@ -63,7 +63,7 @@
margin: auto;
}
.refreshNames {
.refreshNames, .availableTemplateVars {
cursor: pointer;
}

View File

@@ -13,6 +13,7 @@ import store from 'store'
import Tags from 'tags'
import Tooltip from 'tooltip'
import Wizard, { Section } from 'wizard'
import { alert } from 'modal'
import { Container, Row, Col } from 'grid'
import { injectIntl } from 'react-intl'
import {
@@ -30,8 +31,8 @@ import {
isEmpty,
join,
map,
slice,
size,
slice,
sum,
sumBy,
} from 'lodash'
@@ -73,12 +74,14 @@ import {
getCoresPerSocketPossibilities,
generateReadableRandomString,
noop,
resolveIds,
resolveResourceSet,
} from 'utils'
import {
createSelector,
createGetObject,
createGetObjectsOfType,
getIsPoolAdmin,
getUser,
} from 'selectors'
@@ -87,6 +90,32 @@ import styles from './index.css'
const NB_VMS_MIN = 2
const NB_VMS_MAX = 100
const AVAILABLE_TEMPLATE_VARS = {
'{name}': 'templateNameInfo',
'%': 'templateIndexInfo',
}
const showAvailableTemplateVars = () =>
alert(
_('availableTemplateVarsTitle'),
<ul>
{map(AVAILABLE_TEMPLATE_VARS, (value, key) => (
<li key={key}>{_.keyValue(key, _(value))}</li>
))}
</ul>
)
const AvailableTemplateVarsInfo = () => (
<Tooltip content={_('availableTemplateVarsInfo')}>
<a
className={classNames('text-info', styles.availableTemplateVars)}
onClick={showAvailableTemplateVars}
>
<Icon icon='info' />
</a>
</Tooltip>
)
/* eslint-disable camelcase */
const getObject = createGetObject((_, id) => id)
@@ -210,6 +239,7 @@ class Vif extends BaseComponent {
})
@connectStore(() => ({
isAdmin: createSelector(getUser, user => user && user.permission === 'admin'),
isPoolAdmin: getIsPoolAdmin,
networks: createGetObjectsOfType('network').sort(),
pool: createGetObject((_, props) => props.location.query.pool),
pools: createGetObjectsOfType('pool'),
@@ -241,7 +271,9 @@ export default class NewVm extends BaseComponent {
_getResourceSet = () => {
const {
location: { query: { resourceSet: resourceSetId } },
location: {
query: { resourceSet: resourceSetId },
},
resourceSets,
} = this.props
return resourceSets && find(resourceSets, ({ id }) => id === resourceSetId)
@@ -292,7 +324,7 @@ export default class NewVm extends BaseComponent {
name_label: '',
name_description: '',
nameLabels: map(Array(NB_VMS_MIN), (_, index) => `VM_${index + 1}`),
namePattern: '{name}_%',
namePattern: '{name}%',
nbVms: NB_VMS_MIN,
VDIs: [],
VIFs: [],
@@ -330,6 +362,7 @@ export default class NewVm extends BaseComponent {
}
let cloudConfig
let cloudConfigs
if (state.configDrive) {
const hostname = state.name_label
.replace(/^\s+|\s+$/g, '')
@@ -344,7 +377,14 @@ export default class NewVm extends BaseComponent {
''
)}`
} else {
cloudConfig = state.customConfig
const replacer = this._buildTemplate(state.customConfig)
cloudConfig = replacer(this.state.state, 0)
if (state.multipleVms) {
const seqStart = state.seqStart
cloudConfigs = map(state.nameLabels, (_, i) =>
replacer(state, i + +seqStart)
)
}
}
} else if (state.template.name_label === 'CoreOS') {
cloudConfig = state.cloudConfig
@@ -403,7 +443,7 @@ export default class NewVm extends BaseComponent {
}
return state.multipleVms
? createVms(data, state.nameLabels)
? createVms(data, state.nameLabels, cloudConfigs)
: createVm(data)
}
@@ -435,7 +475,7 @@ export default class NewVm extends BaseComponent {
$SR:
pool || isInResourceSet(vdi.$SR)
? vdi.$SR
: resourceSet.objectsByType['SR'][0].id,
: this._getDefaultSr(template),
}
}
})
@@ -464,7 +504,7 @@ export default class NewVm extends BaseComponent {
state.name_description === '' || !state.name_descriptionHasChanged
? template.name_description || ''
: state.name_description
const replacer = this._buildTemplate()
const replacer = this._buildVmsNameTemplate()
this._setState({
// infos
name_label,
@@ -484,7 +524,7 @@ export default class NewVm extends BaseComponent {
'SSH',
sshKeys: this.props.userSshKeys && this.props.userSshKeys.length && [0],
customConfig:
'#cloud-config\n#hostname: myhostname\n#ssh_authorized_keys:\n# - ssh-rsa <myKey>\n#packages:\n# - htop\n',
'#cloud-config\n#hostname: {name}%\n#ssh_authorized_keys:\n# - ssh-rsa <myKey>\n#packages:\n# - htop\n',
// interfaces
VIFs,
// disks
@@ -495,7 +535,7 @@ export default class NewVm extends BaseComponent {
name_description: disk.name_description || 'Created by XO',
name_label:
(name_label || 'disk') + '_' + generateReadableRandomString(5),
SR: pool ? pool.default_SR : resourceSet.objectsByType['SR'][0].id,
SR: this._getDefaultSr(template),
}
}),
})
@@ -592,15 +632,18 @@ export default class NewVm extends BaseComponent {
})
return network && network.id
}
_buildTemplate = createSelector(
_buildVmsNameTemplate = createSelector(
() => this.state.state.namePattern,
namePattern =>
buildTemplate(namePattern, {
'{name}': state => state.name_label || '',
'%': (_, i) => i,
})
namePattern => this._buildTemplate(namePattern)
)
_buildTemplate = pattern =>
buildTemplate(pattern, {
'{name}': state => state.name_label || '',
'%': (_, i) => i,
})
_getVgpuTypePredicate = createSelector(
() => this.props.pool,
pool => vgpuType => pool !== undefined && pool.id === vgpuType.$pool
@@ -630,7 +673,7 @@ export default class NewVm extends BaseComponent {
if (nbVmsClamped < nameLabels.length) {
this._setState({ nameLabels: slice(newNameLabels, 0, nbVmsClamped) })
} else {
const replacer = this._buildTemplate()
const replacer = this._buildVmsNameTemplate()
for (
let i = +seqStart + nameLabels.length;
i <= +seqStart + nbVmsClamped - 1;
@@ -645,7 +688,7 @@ export default class NewVm extends BaseComponent {
const { nameLabels, seqStart } = this.state.state
const nbVms = nameLabels.length
const newNameLabels = []
const replacer = this._buildTemplate()
const replacer = this._buildVmsNameTemplate()
for (let i = +seqStart; i <= +seqStart + nbVms - 1; i++) {
newNameLabels.push(replacer(this.state.state, i))
@@ -670,9 +713,34 @@ export default class NewVm extends BaseComponent {
})
this._reset()
}
_getDefaultSr = template => {
const { pool } = this.props
if (pool !== undefined) {
return pool.default_SR
}
if (template === undefined) {
return
}
const defaultSr = getObject(store.getState(), template.$pool, true)
.default_SR
return includes(
resolveIds(
filter(
this._getResolvedResourceSet().objectsByType.SR,
this._getSrPredicate()
)
),
defaultSr
)
? defaultSr
: undefined
}
_addVdi = () => {
const { state } = this.state
const { pool } = this.props
this._setState({
VDIs: [
@@ -683,7 +751,7 @@ export default class NewVm extends BaseComponent {
(state.name_label || 'disk') +
'_' +
generateReadableRandomString(5),
SR: pool && pool.default_SR,
SR: this._getDefaultSr(state.template),
type: 'system',
},
],
@@ -744,7 +812,7 @@ export default class NewVm extends BaseComponent {
// MAIN ------------------------------------------------------------------------
_renderHeader = () => {
const { isAdmin, pool, resourceSets } = this.props
const { isAdmin, isPoolAdmin, pool, resourceSets } = this.props
const selectPool = (
<span className={styles.inlineSelect}>
<SelectPool onChange={this._selectPool} value={pool} />
@@ -763,9 +831,12 @@ export default class NewVm extends BaseComponent {
<Row>
<Col mediumSize={12}>
<h2>
{isAdmin || !isEmpty(resourceSets)
{isAdmin ||
(isPoolAdmin && process.env.XOA_PLAN > 3) ||
!isEmpty(resourceSets)
? _('newVmCreateNewVmOn', {
select: isAdmin ? selectPool : selectResourceSet,
select:
isAdmin || isPoolAdmin ? selectPool : selectResourceSet,
})
: _('newVmCreateNewVmNoPermission')}
</h2>
@@ -1027,7 +1098,11 @@ export default class NewVm extends BaseComponent {
value='customConfig'
/>
&nbsp;
<span>{_('newVmCustomConfig')}</span>
<span>
{_('newVmCustomConfig')}
&nbsp;
<AvailableTemplateVarsInfo />
</span>
&nbsp;
<DebounceTextarea
className={classNames('form-control', styles.customConfig)}
@@ -1158,7 +1233,9 @@ export default class NewVm extends BaseComponent {
// INTERFACES ------------------------------------------------------------------
_renderInterfaces = () => {
const { state: { VIFs } } = this.state
const {
state: { VIFs },
} = this.state
return (
<Section
@@ -1199,7 +1276,9 @@ export default class NewVm extends BaseComponent {
// DISKS -----------------------------------------------------------------------
_renderDisks = () => {
const { state: { configDrive, existingDisks, VDIs } } = this.state
const {
state: { configDrive, existingDisks, VDIs },
} = this.state
const { pool } = this.props
let i = 0
const resourceSet = this._getResolvedResourceSet()
@@ -1481,6 +1560,8 @@ export default class NewVm extends BaseComponent {
)}
value={namePattern}
/>
&nbsp;
<AvailableTemplateVarsInfo />
</Item>
<Item label={_('newVmFirstIndex')}>
<DebounceInput
@@ -1491,6 +1572,16 @@ export default class NewVm extends BaseComponent {
value={seqStart}
/>
</Item>
<Item>
<Tooltip content={_('newVmNameRefresh')}>
<a
className={styles.refreshNames}
onClick={this._updateNameLabels}
>
<Icon icon='refresh' />
</a>
</Tooltip>
</Item>
<Item className='input-group'>
<DebounceInput
className='form-control'
@@ -1509,16 +1600,6 @@ export default class NewVm extends BaseComponent {
</Tooltip>
</span>
</Item>
<Item>
<Tooltip content={_('newVmNameRefresh')}>
<a
className={styles.refreshNames}
onClick={this._updateNameLabels}
>
<Icon icon='refresh' />
</a>
</Tooltip>
</Item>
{multipleVms && (
<LineItem>
{map(nameLabels, (nameLabel, index) => (

View File

@@ -1,17 +1,25 @@
import React from 'react'
import _ from 'intl'
import _, { messages } from 'intl'
import ActionButton from 'action-button'
import ActionRowButton from 'action-row-button'
import Component from 'base-component'
import Copiable from 'copiable'
import renderXoItem from 'render-xo-item'
import SelectFiles from 'select-files'
import Upgrade from 'xoa-upgrade'
import { connectStore } from 'utils'
import { createGetObjectsOfType } from 'selectors'
import { XoSelect } from 'editable'
import { installSupplementalPackOnAllHosts, setPoolMaster } from 'xo'
import { map } from 'lodash'
import { connectStore } from 'utils'
import { injectIntl } from 'react-intl'
import { createGetObjectsOfType } from 'selectors'
import { Text, XoSelect } from 'editable'
import { Container, Row, Col } from 'grid'
import {
installSupplementalPackOnAllHosts,
setPoolMaster,
setRemoteSyslogHost,
setRemoteSyslogHosts,
} from 'xo'
@connectStore(() => ({
master: createGetObjectsOfType('host').find((_, { pool }) => ({
@@ -39,56 +47,128 @@ class PoolMaster extends Component {
}
}
export default connectStore({
@injectIntl
@connectStore({
hosts: createGetObjectsOfType('host')
.filter((_, { pool }) => ({ $pool: pool.id }))
.sort(),
gpuGroups: createGetObjectsOfType('gpuGroup'),
})(({ gpuGroups, pool }) => (
<div>
<h3 className='mb-1'>{_('xenSettingsLabel')}</h3>
<Container>
<Row>
<Col size={3}>
<strong>{_('uuid')}</strong>
</Col>
<Col size={9}>
<Copiable tagName='div'>{pool.uuid}</Copiable>
</Col>
</Row>
<Row>
<Col size={3}>
<strong>{_('poolHaStatus')}</strong>
</Col>
<Col size={9}>
{pool.HA_enabled ? _('poolHaEnabled') : _('poolHaDisabled')}
</Col>
</Row>
<Row>
<Col size={3}>
<strong>{_('setpoolMaster')}</strong>
</Col>
<Col size={9}>
<PoolMaster pool={pool} />
</Col>
</Row>
</Container>
<h3 className='mt-1 mb-1'>{_('poolGpuGroups')}</h3>
<Container>
<Row>
<Col size={9}>
<ul className='list-group'>
{map(gpuGroups, gpuGroup => (
<li key={gpuGroup.id} className='list-group-item'>
{renderXoItem(gpuGroup)}
</li>
))}
</ul>
</Col>
</Row>
</Container>
<h3 className='mt-1 mb-1'>{_('supplementalPackPoolNew')}</h3>
<Upgrade place='poolSupplementalPacks' required={2}>
<SelectFiles
onChange={file => installSupplementalPackOnAllHosts(pool, file)}
/>
</Upgrade>
</div>
))
})
export default class TabAdvanced extends Component {
_setRemoteSyslogHosts = () =>
setRemoteSyslogHosts(this.props.hosts, this.state.syslogDestination).then(
() => this.setState({ editRemoteSyslog: false, syslogDestination: '' })
)
render () {
const { hosts, gpuGroups, pool } = this.props
const { state } = this
const { editRemoteSyslog } = state
return (
<div>
<Container>
<Row>
<Col>
<h3>{_('xenSettingsLabel')}</h3>
<table className='table'>
<tbody>
<tr>
<th>{_('uuid')}</th>
<Copiable tagName='td'>{pool.uuid}</Copiable>
</tr>
<tr>
<th>{_('poolHaStatus')}</th>
<td>
{pool.HA_enabled
? _('poolHaEnabled')
: _('poolHaDisabled')}
</td>
</tr>
<tr>
<th>{_('setpoolMaster')}</th>
<td>
<PoolMaster pool={pool} />
</td>
</tr>
<tr>
<th>{_('syslogRemoteHost')}</th>
<td>
<ul className='pl-0'>
{map(hosts, host => (
<li key={host.id}>
<span>{`${host.name_label}: `}</span>
<Text
value={host.logging.syslog_destination || ''}
onChange={value =>
setRemoteSyslogHost(host, value)
}
/>
</li>
))}
</ul>
<ActionRowButton
btnStyle={editRemoteSyslog ? 'info' : 'primary'}
handler={this.toggleState('editRemoteSyslog')}
icon='edit'
>
{_('poolEditAll')}
</ActionRowButton>
{editRemoteSyslog && (
<form
id='formRemoteSyslog'
className='form-inline mt-1'
>
<div className='form-group'>
<input
className='form-control'
onChange={this.linkState('syslogDestination')}
placeholder={this.props.intl.formatMessage(
messages.poolRemoteSyslogPlaceHolder
)}
type='text'
value={state.syslogDestination}
/>
</div>
<div className='form-group ml-1'>
<ActionButton
btnStyle='primary'
form='formRemoteSyslog'
handler={this._setRemoteSyslogHosts}
icon='save'
>
{_('confirmOk')}
</ActionButton>
</div>
</form>
)}
</td>
</tr>
</tbody>
</table>
</Col>
</Row>
</Container>
<h3 className='mt-1 mb-1'>{_('poolGpuGroups')}</h3>
<Container>
<Row>
<Col size={9}>
<ul className='list-group'>
{map(gpuGroups, gpuGroup => (
<li key={gpuGroup.id} className='list-group-item'>
{renderXoItem(gpuGroup)}
</li>
))}
</ul>
</Col>
</Row>
</Container>
<h3 className='mt-1 mb-1'>{_('supplementalPackPoolNew')}</h3>
<Upgrade place='poolSupplementalPacks' required={2}>
<SelectFiles
onChange={file => installSupplementalPackOnAllHosts(pool, file)}
/>
</Upgrade>
</div>
)
}
}

View File

@@ -3,6 +3,7 @@ import ActionRowButton from 'action-row-button'
import BaseComponent from 'base-component'
import Button from 'button'
import ButtonGroup from 'button-group'
import copy from 'copy-to-clipboard'
import Icon from 'icon'
import isEmpty from 'lodash/isEmpty'
import map from 'lodash/map'
@@ -280,6 +281,11 @@ class NetworkActions extends Component {
return (
<ButtonGroup>
<ActionRowButton
handler={() => copy(network.uuid)}
icon='clipboard'
tooltip={_('copyUuid', { uuid: network.uuid })}
/>
<ActionRowButton
disabled={disableNetworkDelete}
handler={deleteNetwork}

View File

@@ -170,12 +170,22 @@ const INDIVIDUAL_ACTIONS = [
})
@injectIntl
export default class Servers extends Component {
state = {
allowUnauthorized: false,
}
_addServer = async () => {
const { label, host, password, username } = this.state
const { label, host, password, username, allowUnauthorized } = this.state
await addServer(host, username, password, label)
await addServer(host, username, password, label, allowUnauthorized)
this.setState({ label: '', host: '', password: '', username: '' })
this.setState({
allowUnauthorized: false,
host: '',
label: '',
password: '',
username: '',
})
}
render () {
@@ -228,6 +238,14 @@ export default class Servers extends Component {
value={state.password}
/>
</div>{' '}
<div className='form-group'>
<Tooltip content={_('serverAllowUnauthorizedCertificates')}>
<Toggle
onChange={this.linkState('allowUnauthorized')}
value={state.allowUnauthorized}
/>
</Tooltip>
</div>{' '}
<ActionButton
btnStyle='primary'
form='form-add-server'

View File

@@ -78,13 +78,26 @@ const COLUMNS = [
.sort()
const getVmIds = createSelector(getVbds, vbds => map(vbds, 'VM'))
const getVms = createGetObjectsOfType('VM').pick(getVmIds)
const getVmControllers = createGetObjectsOfType('VM-controller').pick(
getVmIds
)
const getVmSnapshots = createGetObjectsOfType('VM-snapshot').pick(
getVmIds
)
const getVmTemplates = createGetObjectsOfType('VM-template').pick(
getVmIds
)
const getAllVms = createSelector(
getVms,
getVmControllers,
getVmSnapshots,
(vms, vmSnapshots) => ({ ...vms, ...vmSnapshots })
getVmTemplates,
(vms, vmControllers, vmSnapshots, vmTemplates) => ({
...vms,
...vmControllers,
...vmSnapshots,
...vmTemplates,
})
)
return (state, props) => ({
@@ -105,12 +118,18 @@ const COLUMNS = [
return null
}
const link =
vm.type === 'VM'
? `/vms/${vm.id}`
: vm.$snapshot_of === undefined
const type = vm.type
let link
if (type === 'VM') {
link = `/vms/${vm.id}`
} else if (type === 'VM-template') {
link = `/home?s=${vm.id}&t=VM-template`
} else {
link =
vm.$snapshot_of === undefined
? '/dashboard/health'
: `/vms/${vm.$snapshot_of}/snapshots`
}
return (
<Row className={index > 0 && 'mt-1'}>

View File

@@ -10,7 +10,6 @@ import map from 'lodash/map'
import orderBy from 'lodash/orderBy'
import propTypes from 'prop-types-decorator'
import React from 'react'
import Upgrade from 'xoa-upgrade'
import { Container, Col, Row } from 'grid'
import { importVms, isSrWritable } from 'xo'
import { SizeInput } from 'form'
@@ -235,9 +234,10 @@ const parseFile = async (file, type, func) => {
}
}
const getRedirectionUrl = vms => vms.length === 1
? `/vms/${vms[0]}`
: `/home?s=${encodeURIComponent(`id:|(${vms.join(' ')})`)}&t=VM`
const getRedirectionUrl = vms =>
vms.length === 1
? `/vms/${vms[0]}`
: `/home?s=${encodeURIComponent(`id:|(${vms.join(' ')})`)}&t=VM`
export default class Import extends Component {
constructor (props) {
@@ -318,107 +318,101 @@ export default class Import extends Component {
return (
<Page header={HEADER} title='newImport' formatTitle>
{process.env.XOA_PLAN > 1 ? (
<Container>
<form id='import-form'>
<FormGrid.Row>
<FormGrid.LabelCol>{_('vmImportToPool')}</FormGrid.LabelCol>
<FormGrid.InputCol>
<SelectPool
value={pool}
onChange={this._handleSelectedPool}
required
/>
</FormGrid.InputCol>
</FormGrid.Row>
<FormGrid.Row>
<FormGrid.LabelCol>{_('vmImportToSr')}</FormGrid.LabelCol>
<FormGrid.InputCol>
<SelectSr
disabled={!pool}
onChange={this._handleSelectedSr}
predicate={srPredicate}
required
value={sr}
/>
</FormGrid.InputCol>
</FormGrid.Row>
{sr && (
<div>
<Dropzone
onDrop={this._handleDrop}
message={_('importVmsList')}
/>
<hr />
<h5>{_('vmsToImport')}</h5>
{vms.length > 0 ? (
<div>
{map(vms, ({ data, error, file, type }, vmIndex) => (
<div key={file.preview} className={styles.vmContainer}>
<strong>{file.name}</strong>
<span className='pull-right'>
<strong>{`(${formatSize(file.size)})`}</strong>
</span>
{!error ? (
data && (
<div>
<hr />
<div className='alert alert-info' role='alert'>
<strong>
{_('vmImportFileType', { type })}
</strong>{' '}
{_('vmImportConfigAlert')}
</div>
<VmData
{...data}
ref={`vm-data-${vmIndex}`}
pool={pool}
/>
</div>
)
) : (
<Container>
<form id='import-form'>
<FormGrid.Row>
<FormGrid.LabelCol>{_('vmImportToPool')}</FormGrid.LabelCol>
<FormGrid.InputCol>
<SelectPool
value={pool}
onChange={this._handleSelectedPool}
required
/>
</FormGrid.InputCol>
</FormGrid.Row>
<FormGrid.Row>
<FormGrid.LabelCol>{_('vmImportToSr')}</FormGrid.LabelCol>
<FormGrid.InputCol>
<SelectSr
disabled={!pool}
onChange={this._handleSelectedSr}
predicate={srPredicate}
required
value={sr}
/>
</FormGrid.InputCol>
</FormGrid.Row>
{sr && (
<div>
<Dropzone
onDrop={this._handleDrop}
message={_('importVmsList')}
/>
<hr />
<h5>{_('vmsToImport')}</h5>
{vms.length > 0 ? (
<div>
{map(vms, ({ data, error, file, type }, vmIndex) => (
<div key={file.preview} className={styles.vmContainer}>
<strong>{file.name}</strong>
<span className='pull-right'>
<strong>{`(${formatSize(file.size)})`}</strong>
</span>
{!error ? (
data && (
<div>
<hr />
<div className='alert alert-danger' role='alert'>
<strong>{_('vmImportError')}</strong>{' '}
{(error && error.message) ||
_('noVmImportErrorDescription')}
<div className='alert alert-info' role='alert'>
<strong>
{_('vmImportFileType', { type })}
</strong>{' '}
{_('vmImportConfigAlert')}
</div>
<VmData
{...data}
ref={`vm-data-${vmIndex}`}
pool={pool}
/>
</div>
)}
</div>
))}
</div>
) : (
<p>{_('noSelectedVms')}</p>
)}
<hr />
<div className='form-group pull-right'>
<ActionButton
btnStyle='primary'
disabled={!vms.length}
className='mr-1'
form='import-form'
handler={this._import}
icon='import'
redirectOnSuccess={getRedirectionUrl}
type='submit'
>
{_('newImport')}
</ActionButton>
<Button onClick={this._handleCleanSelectedVms}>
{_('importVmsCleanList')}
</Button>
)
) : (
<div>
<hr />
<div className='alert alert-danger' role='alert'>
<strong>{_('vmImportError')}</strong>{' '}
{(error && error.message) ||
_('noVmImportErrorDescription')}
</div>
</div>
)}
</div>
))}
</div>
) : (
<p>{_('noSelectedVms')}</p>
)}
<hr />
<div className='form-group pull-right'>
<ActionButton
btnStyle='primary'
disabled={!vms.length}
className='mr-1'
form='import-form'
handler={this._import}
icon='import'
redirectOnSuccess={getRedirectionUrl}
type='submit'
>
{_('newImport')}
</ActionButton>
<Button onClick={this._handleCleanSelectedVms}>
{_('importVmsCleanList')}
</Button>
</div>
)}
</form>
</Container>
) : (
<Container>
<Upgrade place='vmImport' available={2} />
</Container>
)}
</div>
)}
</form>
</Container>
</Page>
)
}

View File

@@ -29,6 +29,7 @@ import {
deleteVgpu,
deleteVm,
editVm,
getVmsHaValues,
isVmRunning,
recoveryStartVm,
restartVm,
@@ -278,13 +279,8 @@ class CoresPerSocket extends Component {
}
}
export default connectStore(() => {
@connectStore(() => {
const getVgpus = createGetObjectsOfType('vgpu').pick((_, { vm }) => vm.$VGPUs)
const getVgpuTypes = createGetObjectsOfType('vgpuType').pick(
createSelector(getVgpus, vgpus => map(vgpus, 'vgpuType'))
)
const getGpuGroup = createGetObjectsOfType('gpuGroup').pick(
createSelector(getVgpus, vgpus => map(vgpus, 'gpuGroup'))
)
@@ -293,367 +289,394 @@ export default connectStore(() => {
gpuGroup: getGpuGroup,
isAdmin,
vgpus: getVgpus,
vgpuTypes: getVgpuTypes,
}
})(({ container, gpuGroup, isAdmin, vgpus, vgpuTypes, vm }) => (
<Container>
<Row>
<Col className='text-xs-right'>
{vm.power_state === 'Running' && (
<span>
<TabButton
btnStyle='primary'
handler={suspendVm}
handlerParam={vm}
icon='vm-suspend'
labelId='suspendVmLabel'
/>
<TabButton
btnStyle='warning'
handler={forceReboot}
handlerParam={vm}
icon='vm-force-reboot'
labelId='forceRebootVmLabel'
/>
<TabButton
btnStyle='warning'
handler={forceShutdown}
handlerParam={vm}
icon='vm-force-shutdown'
labelId='forceShutdownVmLabel'
/>
</span>
)}
{vm.power_state === 'Halted' && (
<span>
<TabButton
btnStyle='primary'
handler={recoveryStartVm}
handlerParam={vm}
icon='vm-recovery-mode'
labelId='recoveryModeLabel'
/>
<TabButton
btnStyle='primary'
handler={fullCopy}
handlerParam={vm}
icon='vm-clone'
labelId='cloneVmLabel'
/>
})
export default class TabAdvanced extends Component {
componentDidMount () {
getVmsHaValues().then(vmsHaValues => this.setState({ vmsHaValues }))
}
render () {
const { container, isAdmin, vgpus, vm } = this.props
return (
<Container>
<Row>
<Col className='text-xs-right'>
{vm.power_state === 'Running' && (
<span>
<TabButton
btnStyle='primary'
handler={suspendVm}
handlerParam={vm}
icon='vm-suspend'
labelId='suspendVmLabel'
/>
<TabButton
btnStyle='warning'
handler={forceReboot}
handlerParam={vm}
icon='vm-force-reboot'
labelId='forceRebootVmLabel'
/>
<TabButton
btnStyle='warning'
handler={forceShutdown}
handlerParam={vm}
icon='vm-force-shutdown'
labelId='forceShutdownVmLabel'
/>
</span>
)}
{vm.power_state === 'Halted' && (
<span>
<TabButton
btnStyle='primary'
handler={recoveryStartVm}
handlerParam={vm}
icon='vm-recovery-mode'
labelId='recoveryModeLabel'
/>
<TabButton
btnStyle='primary'
handler={fullCopy}
handlerParam={vm}
icon='vm-clone'
labelId='cloneVmLabel'
/>
<TabButton
btnStyle='danger'
handler={convertVmToTemplate}
handlerParam={vm}
icon='vm-create-template'
labelId='vmConvertButton'
redirectOnSuccess='/'
/>
</span>
)}
{vm.power_state === 'Suspended' && (
<span>
<TabButton
btnStyle='primary'
handler={resumeVm}
handlerParam={vm}
icon='vm-start'
labelId='resumeVmLabel'
/>
<TabButton
btnStyle='warning'
handler={forceShutdown}
handlerParam={vm}
icon='vm-force-shutdown'
labelId='forceShutdownVmLabel'
/>
</span>
)}
<TabButton
btnStyle='danger'
handler={convertVmToTemplate}
handler={deleteVm}
handlerParam={vm}
icon='vm-create-template'
labelId='vmConvertButton'
redirectOnSuccess='/'
icon='vm-delete'
labelId='vmRemoveButton'
/>
</span>
)}
{vm.power_state === 'Suspended' && (
<span>
<TabButton
btnStyle='primary'
handler={resumeVm}
handlerParam={vm}
icon='vm-start'
labelId='resumeVmLabel'
/>
<TabButton
btnStyle='warning'
handler={forceShutdown}
handlerParam={vm}
icon='vm-force-shutdown'
labelId='forceShutdownVmLabel'
/>
</span>
)}
<TabButton
btnStyle='danger'
handler={deleteVm}
handlerParam={vm}
icon='vm-delete'
labelId='vmRemoveButton'
/>
</Col>
</Row>
<Row>
<Col>
<h3>{_('xenSettingsLabel')}</h3>
<table className='table'>
<tbody>
<tr>
<th>{_('uuid')}</th>
<Copiable tagName='td'>{vm.uuid}</Copiable>
</tr>
<tr>
<th>{_('virtualizationMode')}</th>
<td>
{vm.virtualizationMode === 'pv'
? _('paraVirtualizedMode')
: _('hardwareVirtualizedMode')}
</td>
</tr>
{vm.virtualizationMode === 'pv' && (
<tr>
<th>{_('pvArgsLabel')}</th>
<td>
<Text
value={vm.PV_args}
onChange={value => editVm(vm, { PV_args: value })}
/>
</td>
</tr>
)}
<tr>
<th>{_('cpuWeightLabel')}</th>
<td>
<Number
value={vm.cpuWeight == null ? null : vm.cpuWeight}
onChange={value => editVm(vm, { cpuWeight: value })}
nullable
>
{vm.cpuWeight == null
? _('defaultCpuWeight', { value: XEN_DEFAULT_CPU_WEIGHT })
: vm.cpuWeight}
</Number>
</td>
</tr>
<tr>
<th>{_('cpuCapLabel')}</th>
<td>
<Number
value={vm.cpuCap == null ? null : vm.cpuCap}
onChange={value => editVm(vm, { cpuCap: value })}
nullable
>
{vm.cpuCap == null
? _('defaultCpuCap', { value: XEN_DEFAULT_CPU_CAP })
: vm.cpuCap}
</Number>
</td>
</tr>
<tr>
<th>{_('autoPowerOn')}</th>
<td>
<Toggle
value={Boolean(vm.auto_poweron)}
onChange={value => editVm(vm, { auto_poweron: value })}
/>
</td>
</tr>
<tr>
<th>{_('windowsUpdateTools')}</th>
<td>
<Toggle
value={vm.hasVendorDevice}
onChange={value => editVm(vm, { hasVendorDevice: value })}
/>
</td>
</tr>
<tr>
<th>{_('ha')}</th>
<td>
<Toggle
value={vm.high_availability}
onChange={value => editVm(vm, { high_availability: value })}
/>
</td>
</tr>
<tr>
<th>{_('vmAffinityHost')}</th>
<td>
<AffinityHost vm={vm} />
</td>
</tr>
{vm.virtualizationMode === 'hvm' && (
<tr>
<th>{_('vmVgpus')}</th>
<td>
<Vgpus vgpus={vgpus} vm={vm} />
</td>
</tr>
)}
{vm.virtualizationMode === 'hvm' && (
<tr>
<th>{_('vmVga')}</th>
<td>
<Toggle
value={vm.vga === 'std'}
onChange={value =>
editVm(vm, { vga: value ? 'std' : 'cirrus' })
}
/>
</td>
</tr>
)}
{vm.vga === 'std' && (
<tr>
<th>{_('vmVideoram')}</th>
<td>
<select
className='form-control'
onChange={event =>
editVm(vm, { videoram: +getEventValue(event) })
}
value={vm.videoram}
>
{map(XEN_VIDEORAM_VALUES, val => (
<option key={val} value={val}>
{formatSize(val * 1048576)}
</option>
))}
</select>
</td>
</tr>
)}
</tbody>
</table>
<br />
<h3>{_('vmLimitsLabel')}</h3>
<table className='table table-hover'>
<tbody>
<tr>
<th>{_('vmCpuLimitsLabel')}</th>
<td>
<Number
value={vm.CPUs.number}
onChange={cpus => editVm(vm, { cpus })}
/>
/
{vm.power_state === 'Running' ? (
vm.CPUs.max
) : (
<Number
value={vm.CPUs.max}
onChange={cpusStaticMax => editVm(vm, { cpusStaticMax })}
/>
</Col>
</Row>
<Row>
<Col>
<h3>{_('xenSettingsLabel')}</h3>
<table className='table'>
<tbody>
<tr>
<th>{_('uuid')}</th>
<Copiable tagName='td'>{vm.uuid}</Copiable>
</tr>
<tr>
<th>{_('virtualizationMode')}</th>
<td>
{vm.virtualizationMode === 'pv'
? _('paraVirtualizedMode')
: _('hardwareVirtualizedMode')}
</td>
</tr>
{vm.virtualizationMode === 'pv' && (
<tr>
<th>{_('pvArgsLabel')}</th>
<td>
<Text
value={vm.PV_args}
onChange={value => editVm(vm, { PV_args: value })}
/>
</td>
</tr>
)}
</td>
</tr>
<tr>
<th>{_('vmCpuTopology')}</th>
<td>
<CoresPerSocket container={container} vm={vm} />
</td>
</tr>
<tr>
<th>{_('vmMemoryLimitsLabel')}</th>
<td>
<p>
Static: {formatSize(vm.memory.static[0])}/<Size
value={defined(vm.memory.static[1], null)}
onChange={memoryStaticMax =>
editVm(vm, { memoryStaticMax })
}
/>
</p>
<p>
Dynamic:{' '}
<Size
value={defined(vm.memory.dynamic[0], null)}
onChange={memoryMin => editVm(vm, { memoryMin })}
/>/<Size
value={defined(vm.memory.dynamic[1], null)}
onChange={memoryMax => editVm(vm, { memoryMax })}
/>
</p>
</td>
</tr>
</tbody>
</table>
<br />
<h3>{_('guestOsLabel')}</h3>
<table className='table table-hover'>
<tbody>
<tr>
<th>{_('xenToolsStatus')}</th>
<td>
{vm.xenTools && `${vm.xenTools.major}.${vm.xenTools.minor}`}
</td>
</tr>
<tr>
<th>{_('osName')}</th>
<td>
{isEmpty(vm.os_version) ? (
_('unknownOsName')
) : (
<span>
<Icon
className='text-info'
icon={osFamily(vm.os_version.distro)}
/>&nbsp;{vm.os_version.name}
</span>
)}
</td>
</tr>
<tr>
<th>{_('osKernel')}</th>
<td>
{(vm.os_version && vm.os_version.uname) || _('unknownOsKernel')}
</td>
</tr>
</tbody>
</table>
<br />
<h3>{_('miscLabel')}</h3>
<table className='table table-hover'>
<tbody>
<tr>
<th>{_('originalTemplate')}</th>
<td>
{vm.other.base_template_name
? vm.other.base_template_name
: _('unknownOriginalTemplate')}
</td>
</tr>
<tr>
<th>{_('resourceSet')}</th>
<td>
{isAdmin ? (
<div className='input-group'>
<SelectResourceSet
onChange={resourceSet =>
editVm(vm, {
resourceSet:
resourceSet != null ? resourceSet.id : resourceSet,
})
}
value={vm.resourceSet}
<tr>
<th>{_('cpuWeightLabel')}</th>
<td>
<Number
value={vm.cpuWeight == null ? null : vm.cpuWeight}
onChange={value => editVm(vm, { cpuWeight: value })}
nullable
>
{vm.cpuWeight == null
? _('defaultCpuWeight', {
value: XEN_DEFAULT_CPU_WEIGHT,
})
: vm.cpuWeight}
</Number>
</td>
</tr>
<tr>
<th>{_('cpuCapLabel')}</th>
<td>
<Number
value={vm.cpuCap == null ? null : vm.cpuCap}
onChange={value => editVm(vm, { cpuCap: value })}
nullable
>
{vm.cpuCap == null
? _('defaultCpuCap', { value: XEN_DEFAULT_CPU_CAP })
: vm.cpuCap}
</Number>
</td>
</tr>
<tr>
<th>{_('autoPowerOn')}</th>
<td>
<Toggle
value={Boolean(vm.auto_poweron)}
onChange={value => editVm(vm, { auto_poweron: value })}
/>
{vm.resourceSet !== undefined && (
<span className='input-group-btn'>
</td>
</tr>
<tr>
<th>{_('windowsUpdateTools')}</th>
<td>
<Toggle
value={vm.hasVendorDevice}
onChange={value => editVm(vm, { hasVendorDevice: value })}
/>
</td>
</tr>
<tr>
<th>{_('ha')}</th>
<td>
<select
className='form-control'
onChange={event =>
editVm(vm, { high_availability: getEventValue(event) })
}
value={vm.high_availability}
>
{map(this.state.vmsHaValues, vmsHaValue => (
<option key={vmsHaValue} value={vmsHaValue}>
{vmsHaValue === '' ? _('vmHaDisabled') : vmsHaValue}
</option>
))}
</select>
</td>
</tr>
<tr>
<th>{_('vmAffinityHost')}</th>
<td>
<AffinityHost vm={vm} />
</td>
</tr>
{vm.virtualizationMode === 'hvm' && (
<tr>
<th>{_('vmVgpus')}</th>
<td>
<Vgpus vgpus={vgpus} vm={vm} />
</td>
</tr>
)}
{vm.virtualizationMode === 'hvm' && (
<tr>
<th>{_('vmVga')}</th>
<td>
<Toggle
value={vm.vga === 'std'}
onChange={value =>
editVm(vm, { vga: value ? 'std' : 'cirrus' })
}
/>
</td>
</tr>
)}
{vm.vga === 'std' && (
<tr>
<th>{_('vmVideoram')}</th>
<td>
<select
className='form-control'
onChange={event =>
editVm(vm, { videoram: +getEventValue(event) })
}
value={vm.videoram}
>
{map(XEN_VIDEORAM_VALUES, val => (
<option key={val} value={val}>
{formatSize(val * 1048576)}
</option>
))}
</select>
</td>
</tr>
)}
</tbody>
</table>
<br />
<h3>{_('vmLimitsLabel')}</h3>
<table className='table table-hover'>
<tbody>
<tr>
<th>{_('vmCpuLimitsLabel')}</th>
<td>
<Number
value={vm.CPUs.number}
onChange={cpus => editVm(vm, { cpus })}
/>
/
{vm.power_state === 'Running' ? (
vm.CPUs.max
) : (
<Number
value={vm.CPUs.max}
onChange={cpusStaticMax =>
editVm(vm, { cpusStaticMax })
}
/>
)}
</td>
</tr>
<tr>
<th>{_('vmCpuTopology')}</th>
<td>
<CoresPerSocket container={container} vm={vm} />
</td>
</tr>
<tr>
<th>{_('vmMemoryLimitsLabel')}</th>
<td>
<p>
Static: {formatSize(vm.memory.static[0])}/<Size
value={defined(vm.memory.static[1], null)}
onChange={memoryStaticMax =>
editVm(vm, { memoryStaticMax })
}
/>
</p>
<p>
Dynamic:{' '}
<Size
value={defined(vm.memory.dynamic[0], null)}
onChange={memoryMin => editVm(vm, { memoryMin })}
/>/<Size
value={defined(vm.memory.dynamic[1], null)}
onChange={memoryMax => editVm(vm, { memoryMax })}
/>
</p>
</td>
</tr>
</tbody>
</table>
<br />
<h3>{_('guestOsLabel')}</h3>
<table className='table table-hover'>
<tbody>
<tr>
<th>{_('xenToolsStatus')}</th>
<td>
{vm.xenTools
? `${vm.xenTools.major}.${vm.xenTools.minor}`
: _('xenToolsNotInstalled')}
</td>
</tr>
<tr>
<th>{_('osName')}</th>
<td>
{isEmpty(vm.os_version) ? (
_('unknownOsName')
) : (
<span>
<Icon
className='text-info'
icon={osFamily(vm.os_version.distro)}
/>&nbsp;{vm.os_version.name}
</span>
)}
</td>
</tr>
<tr>
<th>{_('osKernel')}</th>
<td>
{(vm.os_version && vm.os_version.uname) ||
_('unknownOsKernel')}
</td>
</tr>
</tbody>
</table>
<br />
<h3>{_('miscLabel')}</h3>
<table className='table table-hover'>
<tbody>
<tr>
<th>{_('originalTemplate')}</th>
<td>
{vm.other.base_template_name
? vm.other.base_template_name
: _('unknownOriginalTemplate')}
</td>
</tr>
<tr>
<th>{_('resourceSet')}</th>
<td>
{isAdmin ? (
<div className='input-group'>
<SelectResourceSet
onChange={resourceSet =>
editVm(vm, {
resourceSet:
resourceSet != null
? resourceSet.id
: resourceSet,
})
}
value={vm.resourceSet}
/>
{vm.resourceSet !== undefined && (
<span className='input-group-btn'>
<ActionButton
btnStyle='primary'
handler={shareVmProxy}
handlerParam={vm}
icon='vm-share'
style={SHARE_BUTTON_STYLE}
tooltip={_('vmShareButton')}
/>
</span>
)}
</div>
) : vm.resourceSet !== undefined ? (
<span>
<ResourceSetItem id={vm.resourceSet} />{' '}
<ActionButton
btnStyle='primary'
handler={shareVmProxy}
handlerParam={vm}
icon='vm-share'
style={SHARE_BUTTON_STYLE}
size='small'
tooltip={_('vmShareButton')}
/>
</span>
) : (
_('resourceSetNone')
)}
</div>
) : vm.resourceSet !== undefined ? (
<span>
<ResourceSetItem id={vm.resourceSet} />{' '}
<ActionButton
btnStyle='primary'
handler={shareVmProxy}
handlerParam={vm}
icon='vm-share'
size='small'
tooltip={_('vmShareButton')}
/>
</span>
) : (
_('resourceSetNone')
)}
</td>
</tr>
</tbody>
</table>
</Col>
</Row>
</Container>
))
</td>
</tr>
</tbody>
</table>
</Col>
</Row>
</Container>
)
}
}

View File

@@ -1,6 +1,7 @@
import _, { messages } from 'intl'
import ActionButton from 'action-button'
import Component from 'base-component'
import copy from 'copy-to-clipboard'
import HTML5Backend from 'react-dnd-html5-backend'
import Icon from 'icon'
import IsoDevice from 'iso-device'
@@ -656,6 +657,11 @@ export default class TabDisks extends Component {
icon: 'vdi-migrate',
label: _('vdiMigrate'),
},
{
handler: vdi => copy(vdi.uuid),
icon: 'clipboard',
label: vdi => _('copyUuid', { uuid: vdi.uuid }),
},
]
render () {

View File

@@ -2,6 +2,7 @@ import _, { messages } from 'intl'
import ActionButton from 'action-button'
import ActionRowButton from 'action-row-button'
import BaseComponent from 'base-component'
import copy from 'copy-to-clipboard'
import Icon from 'icon'
import propTypes from 'prop-types-decorator'
import React from 'react'
@@ -217,7 +218,6 @@ class VifAllowedIps extends BaseComponent {
containerPredicate={this._getIsNetworkAllowed()}
onChange={this._addIp}
predicate={this._getIpPredicate()}
required
resourceSetId={resourceSet}
/>
) : (
@@ -226,7 +226,6 @@ class VifAllowedIps extends BaseComponent {
containerPredicate={this._getIsNetworkAllowed()}
onChange={this._addIp}
predicate={this._getIpPredicate()}
required
/>
)}
</span>
@@ -351,6 +350,11 @@ const GROUPED_ACTIONS = [
},
]
const INDIVIDUAL_ACTIONS = [
{
handler: vif => copy(vif.uuid),
icon: 'clipboard',
label: vif => _('copyUuid', { uuid: vif.uuid }),
},
{
disabled: vif => vif.attached,
handler: deleteVif,

View File

@@ -1,4 +1,5 @@
import _ from 'intl'
import copy from 'copy-to-clipboard'
import Icon from 'icon'
import React, { Component } from 'react'
import SortedTable from 'sorted-table'
@@ -92,6 +93,11 @@ const INDIVIDUAL_ACTIONS = [
label: _('revertSnapshot'),
level: 'warning',
},
{
handler: snapshot => copy(snapshot.uuid),
icon: 'clipboard',
label: snapshot => _('copyUuid', { uuid: snapshot.uuid }),
},
{
handler: deleteSnapshot,
icon: 'delete',

View File

@@ -92,7 +92,7 @@ export default class NewXosan extends Component {
const hosts = filter(this.props.hosts, { $pool: pool.id })
const pack = findLatestPack(catalog.xosan, map(hosts, 'version'))
if (isLatestXosanPackInstalled(pack, hosts)) {
if (!isLatestXosanPackInstalled(pack, hosts)) {
this.setState({
needsUpdate: true,
})

2973
yarn.lock

File diff suppressed because it is too large Load Diff