Compare commits

..

135 Commits

Author SHA1 Message Date
Pierre Donias
273f208722 feat(xo-common): 0.2.0 2018-11-16 17:41:08 +01:00
Pierre Donias
c01e8e892e feat(xen-api): 0.21.0 2018-11-16 17:39:16 +01:00
Julien Fontanet
9dfd81c28f feat(xo-web/subscriptions): keep cached data for 10m (#3701)
Related to #3699

This should improve user experience when changing pages.
2018-11-16 17:37:33 +01:00
Pierre Donias
5dd26ebe33 fix(CHANGELOG): missing entries and links 2018-11-16 16:58:48 +01:00
Julien Fontanet
4c0fe3c14f fix(xo-server/importVdiContent): only throws when compiled with yarn dev
Related to #3678
2018-11-16 14:19:34 +01:00
badrAZ
2353581da8 chore(xo-web/backup-ng): improve setting power state implementation (#3684) 2018-11-16 14:05:58 +01:00
Julien Fontanet
2934b23d2f fix(xo-server/vm.delete): fix template handling (#3695)
Fixes #3498

It seems a recent version of XenServer forbids from destroying VM templates directly, we need to set `is_a_template` to `false` before calling `VM.destroy`.
2018-11-16 11:04:56 +01:00
Julien Fontanet
82e4197237 fix(xo-web/deleteVm): dont ask to force if error (#3696) 2018-11-16 10:39:09 +01:00
Julien Fontanet
a23189f132 fix(xo-server): update VM object when {,guest_}metrics have changed (#3694)
Fixes #3533
2018-11-16 10:32:13 +01:00
Julien Fontanet
47fa1ec81e fix(xo-web/settings/remotes): missing form id (#3697) 2018-11-16 09:35:14 +01:00
Julien Fontanet
4b468663f3 feat(xo-server): ability to set XAPI options in config (#3692) 2018-11-15 16:00:52 +01:00
Nicolas Raynaud
6628dc777d fix(xo-server/xosan.createSR): fix network detection (#3689)
Fixes #3688
2018-11-15 11:10:50 +01:00
Julien Fontanet
3ef3ae0166 fix(xo-server/remote.get{,All}): obfuscate sensitive values (#3687)
Fixes #3682
2018-11-14 14:39:20 +01:00
Julien Fontanet
bc6dbe2771 chore(xen-api/README): add 7.{4..6} compatibility 2018-11-14 14:18:14 +01:00
badrAZ
5651160d1c fix(xo-web/settings/remotes): fix incorrect input type (#3683)
Fixes #3681
2018-11-14 11:21:38 +01:00
Julien Fontanet
6da2669c6f fix(fs/smb#list): throws ENOTDIR instead of SMB error (#3685)
Fixes support#1014

This is more in line of other handlers and fix an issue in Backup NG where listing VM backups would fail when there are files in `xo-vm-backups/`.
2018-11-14 10:11:52 +01:00
Julien Fontanet
8094b5097f feat(xo-server/reaclette-utils/generateId): used as a computed for generated ID (#3680) 2018-11-13 22:58:37 +01:00
Pierre Donias
bdb0547b86 fix(xo-server/vm.create): merge double limit allocation (#3667)
This prevents unwanted error if the limits are exceeded in a transitory fashion.
2018-11-13 17:18:56 +01:00
Pierre Donias
ea08fbbfba fix(xo-server-cloud): missing protocol (#3686) 2018-11-13 17:01:03 +01:00
Julien Fontanet
b4cbd8b2b5 chore(fs): ignore enabled option
It makes no sense for this library to handled disabled remote.
2018-11-13 15:59:16 +01:00
Julien Fontanet
f8fbb6b7d3 feat(xen-api): add call params to errors (#3679)
It also moves `.method` to `.call.method`, but it appears it was not currently used in our code.
2018-11-13 12:03:05 +01:00
Julien Fontanet
c8da9fec0a chore: minor Flow fixes 2018-11-13 11:24:23 +01:00
Julien Fontanet
79fb3ec8bd fix(xo-server/plugin.get): hide sensitive config values (#3671) 2018-11-13 11:09:58 +01:00
Julien Fontanet
2243966ce1 feat(xo-server/checkPermissions): similar to hasPermissions but throws 2018-11-13 10:46:38 +01:00
Julien Fontanet
ca7d520997 feat(xo-acl-resolver/assert): throw error if unauthorized 2018-11-13 10:46:38 +01:00
Julien Fontanet
df44487363 chore(xo-web/backup-ng/log): disable bug reporting
Temporary measure because it is currently broken due to the size of the log in the HTTP request.
2018-11-13 10:13:20 +01:00
Julien Fontanet
b39eb0f60d chore(xo-server/api): add expected permission to unauthorized error 2018-11-12 17:34:15 +01:00
badrAZ
a3dcdc4fd5 chore(xo-server-perf-alert): unused property (#3672) 2018-11-12 15:38:58 +01:00
Julien Fontanet
2daac73c17 chore(xo-common/api-errors/unauthorized): add metadata
```ts
let data: {
  permission?: string,
  object: {
    id?: string,
    type?: string,
  },
}
```
2018-11-12 15:30:28 +01:00
Julien Fontanet
23eb3c3094 chore(xo-server/resourceSet.get{,All}): explicit permission check 2018-11-12 15:25:51 +01:00
Julien Fontanet
776d0f9e4a chore(xo-server/ipPool.getAll): explicit permission check 2018-11-12 15:25:51 +01:00
Enishowk
54bdcc6dd2 feat(xo-web/VM): switch virtualization mode (#3669)
Fixes #2372
2018-11-12 13:57:23 +01:00
Julien Fontanet
38084c8199 chore(xo-common/api-errors/unauthorized): remove "unauthorized" in message
It confuses users and the message is still relevant without in case of unauthorized users.
2018-11-12 13:50:33 +01:00
badrAZ
4525ee7491 fix(xo-server-usage-report): gracefully handle fetching stats error (#3656)
Fixes #3600
2018-11-09 16:10:54 +01:00
badrAZ
66a476bd21 feat(xo-web/backup-ng): warning & omitting VMs/pools on XS < 6.5 (#3668)
Fixes #3540
2018-11-09 15:02:29 +01:00
Pierre Donias
be6cc12632 fix(xo-server/vm.create): revert 37a906a2 (#3666)
Fixes #3658, fixes support#1064
2018-11-08 17:31:07 +01:00
Enishowk
673475dcb2 fix(xo-web/vm): restore display of pvhvm virtualization mode (#3662)
Fixes #3576
2018-11-08 17:05:46 +01:00
badrAZ
7dc1a80a83 fix(xo-server/xapi-stats/getVmStats): avoid sync exceptions (#3661) 2018-11-08 16:31:40 +01:00
Julien Fontanet
d49294849f chore(xo-server/vm.import): remove host parameter (#3663) 2018-11-08 16:30:50 +01:00
Rajaa.BARHTAOUI
6b394302c1 feat(xo-web/sortedTable): mutualize actions (#3594) 2018-11-08 14:40:54 +01:00
Julien Fontanet
00e1601f85 chore(xo-server): use VM.domain_type when available (#3664)
And fallback to our previous detection when unavailable.

Starting from this commit, `virtualizationMode` will no longer contain `'pvhvm'`, this must be computed UI side by using both `virtualizationMode` and `xenTools` properties.
2018-11-08 14:37:47 +01:00
Rajaa.BARHTAOUI
b75e746586 fix(xo-web/StrongConfirm): input loses focus (#3649) 2018-11-08 11:38:17 +01:00
Enishowk
32a9fa9bb0 feat(xo-web/migration): auto-select host/SR when there's only one (#3654)
Fixes #3502
2018-11-08 11:30:38 +01:00
badrAZ
79d68dece4 fix(xo-server/xapi-stats): re-fetch host stats if VM missing (#3660) 2018-11-08 11:28:17 +01:00
Julien Fontanet
1701e1d4ba feat(xo-server/_importVdiContent): require length in dev mode 2018-11-08 10:45:11 +01:00
Julien Fontanet
497b3eb296 feat(xo-server/disk.import): extract length from header 2018-11-08 10:44:35 +01:00
Julien Fontanet
ecfafa0fea chore: npm → yarn 2018-11-07 22:24:46 +01:00
Julien Fontanet
def66d8218 chore(xapi-explore-sr): remove Jest config/scripts 2018-11-07 22:23:17 +01:00
Julien Fontanet
eeb08abec2 chore(xapi-explore-sr): use Babel 7 2018-11-07 22:18:58 +01:00
Julien Fontanet
90923c657d chore: re-format code 2018-11-07 18:37:23 +01:00
Julien Fontanet
4ff6eeb424 chore: update dependencies 2018-11-07 18:15:57 +01:00
Julien Fontanet
2d98fb40f1 feat(xo-server/_assertConsistentHostServerTime): display delta 2018-11-07 17:06:17 +01:00
Julien Fontanet
256a58ded2 feat(xo-server/_assertConsistentHostServerTime): threshold 2s → 30s 2018-11-07 17:00:29 +01:00
badrAZ
bf3b31a9ef fix(xo-web/logs): properly display restore failures (#3648) 2018-11-07 15:20:25 +01:00
badrAZ
7fc8d59605 feat(xo-server/backup-ng): warnings for missing VMs (#3647) 2018-11-07 13:12:26 +01:00
Julien Fontanet
1a39b2113a fix(docs): XO requires Node 8 2018-11-07 10:53:02 +01:00
badrAZ
cb9f3fbb2c chore(xo-server/backup-ng): restore tasks documentation (#3652) 2018-11-07 10:44:33 +01:00
badrAZ
487f413cdd feat(xo-web/backup): move "restore/file-restore" to Backup NG view (#3610)
Fixes #3499
2018-11-07 10:22:31 +01:00
Pierre Donias
f847969206 fix(xo-server/patching): correctly ignore upgrade patches (#3651)
Possibly related to support#1009
2018-11-07 10:10:36 +01:00
Julien Fontanet
5d9aad44c2 Merge branch 'xapi-explore-sr/master' 2018-11-06 18:26:44 +01:00
Julien Fontanet
ba2027e6d7 feat(xapi-explore-sr): move all files to packages/xapi-explore-sr 2018-11-06 18:19:39 +01:00
Julien Fontanet
087da9376f Merge branch 'xo-import-servers-csv/master' 2018-11-06 18:11:58 +01:00
Julien Fontanet
218e3b46e0 feat(xo-import-servers-csv): move all files to packages/xo-import-servers-csv 2018-11-06 17:55:20 +01:00
Rajaa.BARHTAOUI
f9921e354e feat(xo-web/StrongConfirm): press Enter to validate (#2890)
Fixes #2735
2018-11-06 15:29:01 +01:00
badrAZ
341148a7d3 fix(xo-web/logs): fix restarting VMs with concurrency issue (#3634)
Fixes #3603
2018-11-06 13:45:30 +01:00
Julien Fontanet
7216165f1e chore: update dependencies 2018-11-06 13:27:33 +01:00
Julien Fontanet
a9557af04b fix(CHANGELOG): fix versions 2018-11-05 16:48:09 +01:00
Julien Fontanet
abb80270ad feat(xo-web): 5.29.3 2018-11-05 16:47:23 +01:00
Julien Fontanet
72e93384a5 feat(xo-server): 5.29.4 2018-11-05 16:46:54 +01:00
Julien Fontanet
663b1b76ec fix(CHANGELOG): move entry to correct release 2018-11-05 16:46:18 +01:00
Julien Fontanet
24b8c671fa fix(xo-server/mergeVhd): use remote options 2018-11-05 16:41:37 +01:00
Julien Fontanet
986fec1cd3 feat(xo-server): pass config to workers 2018-11-05 16:41:37 +01:00
badrAZ
f6c2cbc5cf chore(xo-web/backup): devs can create legacy backups (#3645)
Fixes #3624
2018-11-05 14:40:42 +01:00
Pierre Donias
289ed89a78 fix(xo-server/vm.create): dont extract cpus & memoryMax from params (#3646)
Fixes #3644
2018-11-05 14:34:58 +01:00
Enishowk
73de421d47 feat(xo-web/vm/advanced): add nested virt toggle (#3625)
Fixes #3619
2018-11-05 14:17:19 +01:00
badrAZ
dc1eb82295 chore(xo-server/backup-ng): tasks' documentation (#3640) 2018-11-05 14:16:36 +01:00
Enishowk
6629c12166 fix(xo-web/form/Toggle): dont emit onChange if disabled (#3643)
fix(xo-web/form/Toggle): dont emit onChange if disabled
2018-11-05 14:09:13 +01:00
badrAZ
ec5bc1db95 fix(xo-web/backup-ng-logs): incorrect started jobs filter (#3641)
Fixes #3636
2018-11-05 12:09:52 +01:00
Julien Fontanet
ac2c40c842 fix(xo-server/vm.*): ensure force params are optional 2018-11-05 10:42:28 +01:00
Julien Fontanet
61bf669252 feat(fs/nfs): ensure mount error not hidden 2018-11-05 10:11:57 +01:00
Julien Fontanet
4105c53155 chore(CHANGELOG): 5.28.1 2018-11-05 09:59:36 +01:00
Julien Fontanet
873db3bf26 0.2.1 2018-04-13 11:32:47 +02:00
Julien Fontanet
c795887a35 fix: display unmanaged snapshots as unmanaged 2018-04-13 11:32:33 +02:00
Julien Fontanet
23824bafe8 1.1.0 2018-04-09 16:07:31 +02:00
Julien Fontanet
5cca58f2b3 feat(README): add documentation 2018-04-09 16:03:25 +02:00
Julien Fontanet
d05c9b6133 chore: use console.error to display errors 2018-04-09 16:03:04 +02:00
Julien Fontanet
39a84a1ac0 feat: support allowUnauthorized, autoConnect and label 2018-04-09 16:02:46 +02:00
Julien Fontanet
b1c851c9d6 chore(package): prepublishOnly script 2018-04-09 16:02:17 +02:00
Julien Fontanet
6280a9365c chore(package): update dependencies 2018-04-09 16:02:04 +02:00
Julien Fontanet
2741dacd64 0.2.0 2018-04-09 14:01:47 +02:00
Julien Fontanet
4c2c2390bd chore(package): prepublish → prepublishOnly 2018-04-09 14:01:18 +02:00
Julien Fontanet
635b8ce5f0 chore(package): update dependencies 2018-04-09 14:00:17 +02:00
Julien Fontanet
efc13cc456 fix: display VDI with missing parent
Consider them parentless even though they are simply unknown.
2018-04-09 11:34:56 +02:00
Julien Fontanet
078f319fe1 0.1.1 2017-06-07 11:25:44 +02:00
Julien Fontanet
0f0e785871 fix: ensure vdi.physical_utilisation is a number 2017-06-07 11:25:39 +02:00
Julien Fontanet
4e4c85121c 0.1.0 2017-05-11 15:27:15 +02:00
Julien Fontanet
019d6f4cb6 feat: display VDI size 2017-05-11 15:27:06 +02:00
Julien Fontanet
725b0342d1 fix: Xen → XenServer 2017-05-11 15:22:49 +02:00
Julien Fontanet
c93ccb8111 feat: handle -h and --help flags 2017-05-11 15:22:12 +02:00
Julien Fontanet
670befdaf6 chore(package): update all dependencies 2017-05-11 15:19:08 +02:00
Julien Fontanet
55eefd865f 0.0.4 2017-03-30 16:51:27 +02:00
Julien Fontanet
43e5d610e3 fix(package): jest config testPathDirs → roots 2017-03-30 16:51:17 +02:00
Julien Fontanet
b1245bc5be fix(usage): Xen → XenServer 2017-03-30 16:50:44 +02:00
Julien Fontanet
c2feab245e fix: update yarn.lock 2017-03-30 16:48:18 +02:00
Julien Fontanet
cb3753213e fix(README): Xen → XenServer 2017-03-08 14:19:21 +01:00
greenkeeper[bot]
ec8c7a24af chore(package): update jest to version 19.0.1 (#2)
https://greenkeeper.io/
2017-02-22 12:10:24 +01:00
greenkeeper[bot]
2456be2da3 chore(package): update tslint-config-standard to version 3.0.0 (#6)
https://greenkeeper.io/
2017-01-19 10:10:08 +01:00
Julien Fontanet
8c5d4240f9 chore(package): update all dependencies 2017-01-17 10:34:25 +01:00
Julien Fontanet
b1e12d1542 chore: add yarn.lock 2017-01-17 10:28:34 +01:00
Julien Fontanet
a58d7d2ff4 chore(package): use husky instead of ghooks 2017-01-17 10:28:03 +01:00
Julien Fontanet
5308b8b9ed fix(README): should be installed globally 2017-01-17 10:26:32 +01:00
greenkeeper[bot]
c15dffce8f chore(package): update @types/node to version 7.0.0 (#5)
https://greenkeeper.io/
2017-01-11 09:22:18 +01:00
greenkeeper[bot]
874680462e chore(package): update dependencies (#4)
https://greenkeeper.io/
2016-11-28 15:13:31 +01:00
greenkeeper[bot]
bb42540775 chore(package): update tslint-config-standard to version 2.0.0 (#3)
https://greenkeeper.io/
2016-11-21 23:27:47 +01:00
Julien Fontanet
b18511c905 chore(package): update all dependencies 2016-11-08 15:44:21 +01:00
greenkeeper[bot]
5c660f4f64 chore(package): update dependencies (#1)
https://greenkeeper.io/
2016-11-02 09:33:33 +01:00
Julien Fontanet
f2bae73f77 0.0.3 2016-10-31 17:32:32 +01:00
Julien Fontanet
e54d34f269 feat(cli): prefix labels if colors not supported 2016-10-31 17:32:00 +01:00
Julien Fontanet
6470cbd2ee 0.0.2 2016-10-31 17:08:02 +01:00
Julien Fontanet
c06ebcb4a4 fix(askPassword): prompt on stderr 2016-10-31 17:07:34 +01:00
Julien Fontanet
3eaa72c98c 0.0.1 2016-10-31 16:37:40 +01:00
Julien Fontanet
694fff060d fix(package): fix bin 2016-10-31 16:37:36 +01:00
Julien Fontanet
2705062ac3 chore(README): replace placeholders 2016-10-31 16:31:59 +01:00
Julien Fontanet
3df055a296 chore(package): publish 2016-10-31 16:29:36 +01:00
Julien Fontanet
802bc15e0c initial commit 2016-10-31 16:27:15 +01:00
Julien Fontanet
ad2de40a9d chore(package): update @types/through2 to v2.0.29 2016-09-23 09:41:05 +02:00
Julien Fontanet
19298570f8 chore(package): remove unused dep 2016-09-19 14:55:50 +02:00
Julien Fontanet
1da4d1f1e9 chore: repo moved to vatesfr 2016-09-19 14:53:33 +02:00
Julien Fontanet
fe4e9c18fa feat(cli): print usage on missing argument 2016-09-19 14:48:52 +02:00
Julien Fontanet
2c9f84f17f feat(package): add description and keywords 2016-09-19 14:48:52 +02:00
Julien Fontanet
0b2e76600b feat(README): add usage 2016-09-19 14:48:52 +02:00
Julien Fontanet
873554fc01 It works! 2016-09-19 14:43:39 +02:00
Julien Fontanet
82e2d013ae chore(package): reorder entry in package.json 2016-09-19 10:23:33 +02:00
Julien Fontanet
1eb5e80f1f fix(types): fix type definitions 2016-09-19 10:23:18 +02:00
Julien Fontanet
9c0ab5b3cb Initial commit 2016-09-16 18:09:18 +02:00
133 changed files with 2697 additions and 1774 deletions

View File

@@ -4,6 +4,7 @@ module.exports = {
__DEV__: true,
$Dict: true,
$Diff: true,
$ElementType: true,
$Exact: true,
$Keys: true,
$PropertyType: true,

View File

@@ -42,8 +42,8 @@ const getConfig = (key, ...args) => {
return config === undefined
? {}
: typeof config === 'function'
? config(...args)
: config
? config(...args)
: config
}
module.exports = function (pkg, plugins, presets) {

View File

@@ -15,6 +15,6 @@
},
"dependencies": {
"golike-defer": "^0.4.1",
"xen-api": "^0.20.0"
"xen-api": "^0.21.0"
}
}

View File

@@ -50,8 +50,8 @@ class Schedule {
zone.toLowerCase() === 'utc'
? moment.utc
: zone === 'local'
? moment
: () => moment.tz(zone)
? moment
: () => moment.tz(zone)
}
createJob (fn) {

View File

@@ -26,7 +26,7 @@
"get-stream": "^4.0.0",
"lodash": "^4.17.4",
"promise-toolbox": "^0.11.0",
"through2": "^2.0.3",
"through2": "^3.0.0",
"tmp": "^0.0.33",
"xo-remote-parser": "^0.5.0"
},

View File

@@ -1,5 +1,6 @@
// @flow
// $FlowFixMe
import getStream from 'get-stream'
import { randomBytes } from 'crypto'
import { fromCallback, fromEvent, ignoreErrors, timeout } from 'promise-toolbox'

View File

@@ -1,6 +1,5 @@
// @flow
// $FlowFixMe
import through2 from 'through2'
import { createHash } from 'crypto'
import { defer, fromEvent } from 'promise-toolbox'

View File

@@ -27,11 +27,10 @@ export default class LocalHandler extends RemoteHandlerAbstract {
}
async _sync () {
if (this._remote.enabled) {
const path = this._getRealPath()
await fs.ensureDir(path)
await fs.access(path, fs.R_OK | fs.W_OK)
}
const path = this._getRealPath()
await fs.ensureDir(path)
await fs.access(path, fs.R_OK | fs.W_OK)
return this._remote
}

View File

@@ -44,18 +44,18 @@ export default class NfsHandler extends LocalHandler {
},
}
).catch(error => {
if (!error.stderr.includes('already mounted')) {
if (
error == null ||
typeof error.stderr !== 'string' ||
!error.stderr.includes('already mounted')
) {
throw error
}
})
}
async _sync () {
if (this._remote.enabled) {
await this._mount()
} else {
await this._umount()
}
await this._mount()
return this._remote
}
@@ -74,7 +74,11 @@ export default class NfsHandler extends LocalHandler {
LANG: 'C',
},
}).catch(error => {
if (!error.stderr.includes('not mounted')) {
if (
error == null ||
typeof error.stderr !== 'string' ||
!error.stderr.includes('not mounted')
) {
throw error
}
})

View File

@@ -6,19 +6,21 @@ import RemoteHandlerAbstract from './abstract'
const noop = () => {}
// Normalize the error code for file not found.
const normalizeError = error => {
class ErrorWrapper extends Error {
constructor (error, newCode) {
super(error.message)
this.cause = error
this.code = newCode
}
}
const normalizeError = (error, shouldBeDirectory) => {
const { code } = error
return code === 'STATUS_OBJECT_NAME_NOT_FOUND' ||
code === 'STATUS_OBJECT_PATH_NOT_FOUND'
? Object.create(error, {
code: {
configurable: true,
readable: true,
value: 'ENOENT',
writable: true,
},
})
? new ErrorWrapper(error, 'ENOENT')
: code === 'STATUS_NOT_SUPPORTED' || code === 'STATUS_INVALID_PARAMETER'
? new ErrorWrapper(error, shouldBeDirectory ? 'ENOTDIR' : 'EISDIR')
: error
}
@@ -70,10 +72,9 @@ export default class SmbHandler extends RemoteHandlerAbstract {
}
async _sync () {
if (this._remote.enabled) {
// Check access (smb2 does not expose connect in public so far...)
await this.list()
}
// Check access (smb2 does not expose connect in public so far...)
await this.list()
return this._remote
}
@@ -154,7 +155,7 @@ export default class SmbHandler extends RemoteHandlerAbstract {
client.disconnect()
})
} catch (error) {
throw normalizeError(error)
throw normalizeError(error, true)
}
return list

View File

@@ -13,10 +13,10 @@ const consoleTransport = ({ data, level, namespace, message, time }) => {
level < INFO
? debugConsole
: level < WARN
? infoConsole
: level < ERROR
? warnConsole
: errorConsole
? infoConsole
: level < ERROR
? warnConsole
: errorConsole
fn('%s - %s - [%s] %s', time.toISOString(), namespace, NAMES[level], message)
data != null && fn(data)

View File

@@ -53,14 +53,12 @@ export default ({
fromCallback(cb =>
transporter.sendMail(
{
subject: evalTemplate(
subject,
key =>
key === 'level'
? NAMES[log.level]
: key === 'time'
? log.time.toISOString()
: log[key]
subject: evalTemplate(subject, key =>
key === 'level'
? NAMES[log.level]
: key === 'time'
? log.time.toISOString()
: log[key]
),
text: prettyFormat(log.data),
},

View File

@@ -4,15 +4,55 @@
### Enhancements
- [VM] Ability to set nested virtualization in settings [#3619](https://github.com/vatesfr/xen-orchestra/issues/3619) (PR [#3625](https://github.com/vatesfr/xen-orchestra/pull/3625))
- [Legacy Backup] Restore and File restore functionalities moved to the Backup NG view [#3499](https://github.com/vatesfr/xen-orchestra/issues/3499) (PR [#3610](https://github.com/vatesfr/xen-orchestra/pull/3610))
- [Backup NG logs] Display warning in case of missing VMs instead of a ghosts VMs tasks (PR [#3647](https://github.com/vatesfr/xen-orchestra/pull/3647))
- [VM] On migration, automatically selects the host and SR when only one is available [#3502](https://github.com/vatesfr/xen-orchestra/issues/3502) (PR [#3654](https://github.com/vatesfr/xen-orchestra/pull/3654))
- [VM] Display VGA and video RAM for PVHVM guests [#3576](https://github.com/vatesfr/xen-orchestra/issues/3576) (PR [#3664](https://github.com/vatesfr/xen-orchestra/pull/3664))
- [Backup NG form] Display a warning to let the user know that the Delta Backup and the Continuous Replication are not supported on XenServer < 6.5 [#3540](https://github.com/vatesfr/xen-orchestra/issues/3540) (PR [#3668](https://github.com/vatesfr/xen-orchestra/pull/3668))
- [Backup NG form] Omit VMs(Simple Backup)/pools(Smart Backup/Resident on) with XenServer < 6.5 from the selection when the Delta Backup mode or the Continuous Replication mode are selected [#3540](https://github.com/vatesfr/xen-orchestra/issues/3540) (PR [#3668](https://github.com/vatesfr/xen-orchestra/pull/3668))
- [VM] Allow to switch the Virtualization mode [#2372](https://github.com/vatesfr/xen-orchestra/issues/2372) (PR [#3669](https://github.com/vatesfr/xen-orchestra/pull/3669))
### Bug fixes
- [Backup NG] Increase timeout in stale remotes detection to limit false positives (PR [#3632](https://github.com/vatesfr/xen-orchestra/pull/3632))
- [Backup ng logs] Fix restarting VMs with concurrency issue [#3603](https://github.com/vatesfr/xen-orchestra/issues/3603) (PR [#3634](https://github.com/vatesfr/xen-orchestra/pull/3634))
- Validate modal containing a confirm text input by pressing the Enter key [#2735](https://github.com/vatesfr/xen-orchestra/issues/2735) (PR [#2890](https://github.com/vatesfr/xen-orchestra/pull/2890))
- [Patches] Bulk install correctly ignores upgrade patches on licensed hosts (PR [#3651](https://github.com/vatesfr/xen-orchestra/pull/3651))
- [Backup NG logs] Handle failed restores (PR [#3648](https://github.com/vatesfr/xen-orchestra/pull/3648))
- [Self/New VM] Incorrect limit computation [#3658](https://github.com/vatesfr/xen-orchestra/issues/3658) (PR [#3666](https://github.com/vatesfr/xen-orchestra/pull/3666))
- [Plugins] Don't expose credentials in config to users (PR [#3671](https://github.com/vatesfr/xen-orchestra/pull/3671))
- [Self/New VM] `not enough … available in the set …` error in some cases (PR [#3667](https://github.com/vatesfr/xen-orchestra/pull/3667))
- [XOSAN] Creation stuck at "Configuring VMs" [#3688](https://github.com/vatesfr/xen-orchestra/issues/3688) (PR [#3689](https://github.com/vatesfr/xen-orchestra/pull/3689))
- [Backup NG] Errors listing backups on SMB remotes with extraneous files (PR [#3685](https://github.com/vatesfr/xen-orchestra/pull/3685))
- [Remotes] Don't expose credentials to users [#3682](https://github.com/vatesfr/xen-orchestra/issues/3682) (PR [#3687](https://github.com/vatesfr/xen-orchestra/pull/3687))
- [VM] Correctly display guest metrics updates (tools, network, etc.) [#3533](https://github.com/vatesfr/xen-orchestra/issues/3533) (PR [#3694](https://github.com/vatesfr/xen-orchestra/pull/3694))
- [VM Templates] Fix deletion [#3498](https://github.com/vatesfr/xen-orchestra/issues/3498) (PR [#3695](https://github.com/vatesfr/xen-orchestra/pull/3695))
### Released packages
- xen-api v0.21.0
- xo-common v0.2.0
- xo-acl-resolver v0.4.0
- xo-server v5.30.0
- xo-web v5.30.0
## **5.28.1** (2018-11-05)
### Enhancements
### Bug fixes
- [Backup NG] Increase timeout in stale remotes detection to limit false positives (PR [#3632](https://github.com/vatesfr/xen-orchestra/pull/3632))
- Fix re-registration issue ([4e35b19ac](https://github.com/vatesfr/xen-orchestra/commit/4e35b19ac56c60f61c0e771cde70a50402797b8a))
- [Backup NG logs] Fix started jobs filter [#3636](https://github.com/vatesfr/xen-orchestra/issues/3636) (PR [#3641](https://github.com/vatesfr/xen-orchestra/pull/3641))
- [New VM] CPU and memory user inputs were ignored since previous release [#3644](https://github.com/vatesfr/xen-orchestra/issues/3644) (PR [#3646](https://github.com/vatesfr/xen-orchestra/pull/3646))
### Released packages
- @xen-orchestra/fs v0.4.1
- xo-server v5.29.4
- xo-web v5.29.3
## **5.28.0** (2018-10-31)
### Enhancements

View File

@@ -14,13 +14,13 @@ As you may have seen,in other parts of the documentation, XO is composed of two
### NodeJS
XO needs Node.js. **Please always use the LTS version of Node**.
XO needs Node.js. **Please use Node 8**.
We'll consider at this point that you've got a working node on your box. E.g:
```
$ node -v
v8.9.1
v8.12.0
```
If not, see [this page](https://nodejs.org/en/download/package-manager/) for instructions on how to install Node.

View File

@@ -103,6 +103,6 @@ encoding by prefixing with `json:`:
##### VM import
```
> xo-cli vm.import host=60a6939e-8b0a-4352-9954-5bde44bcdf7d @=vm.xva
> xo-cli vm.import sr=60a6939e-8b0a-4352-9954-5bde44bcdf7d @=vm.xva
```
> Note: `xo-cli` only supports the import of XVA files. It will not import OVA files. To import OVA images, you must use the XOA web UI.

View File

@@ -10,12 +10,12 @@
"eslint-config-standard": "12.0.0",
"eslint-config-standard-jsx": "^6.0.2",
"eslint-plugin-import": "^2.8.0",
"eslint-plugin-node": "^7.0.1",
"eslint-plugin-node": "^8.0.0",
"eslint-plugin-promise": "^4.0.0",
"eslint-plugin-react": "^7.6.1",
"eslint-plugin-standard": "^4.0.0",
"exec-promise": "^0.7.0",
"flow-bin": "^0.82.0",
"flow-bin": "^0.85.0",
"globby": "^8.0.0",
"husky": "^1.0.0-rc.15",
"jest": "^23.0.1",

View File

@@ -410,11 +410,10 @@ class P {
static text (text) {
const { length } = text
return new P(
(input, pos) =>
input.startsWith(text, pos)
? new Success(pos + length, text)
: new Failure(pos, `'${text}'`)
return new P((input, pos) =>
input.startsWith(text, pos)
? new Success(pos + length, text)
: new Failure(pos, `'${text}'`)
)
}
@@ -478,17 +477,16 @@ class P {
}
}
P.eof = new P(
(input, pos, end) =>
pos < end ? new Failure(pos, 'end of input') : new Success(pos)
P.eof = new P((input, pos, end) =>
pos < end ? new Failure(pos, 'end of input') : new Success(pos)
)
// -------------------------------------------------------------------
const parser = P.grammar({
default: r =>
P.seq(r.ws, r.term.repeat(), P.eof).map(
([, terms]) => (terms.length === 0 ? new Null() : new And(terms))
P.seq(r.ws, r.term.repeat(), P.eof).map(([, terms]) =>
terms.length === 0 ? new Null() : new And(terms)
),
globPattern: new P((input, pos, end) => {
let value = ''

View File

@@ -228,16 +228,15 @@ export default class Vhd {
return this._read(
sectorsToBytes(blockAddr),
onlyBitmap ? this.bitmapSize : this.fullBlockSize
).then(
buf =>
onlyBitmap
? { id: blockId, bitmap: buf }
: {
id: blockId,
bitmap: buf.slice(0, this.bitmapSize),
data: buf.slice(this.bitmapSize),
buffer: buf,
}
).then(buf =>
onlyBitmap
? { id: blockId, bitmap: buf }
: {
id: blockId,
bitmap: buf.slice(0, this.bitmapSize),
data: buf.slice(this.bitmapSize),
buffer: buf,
}
)
}

View File

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

View File

@@ -0,0 +1,24 @@
/benchmark/
/benchmarks/
*.bench.js
*.bench.js.map
/examples/
example.js
example.js.map
*.example.js
*.example.js.map
/fixture/
/fixtures/
*.fixture.js
*.fixture.js.map
*.fixtures.js
*.fixtures.js.map
/test/
/tests/
*.spec.js
*.spec.js.map
__snapshots__/

View File

@@ -0,0 +1,52 @@
# xapi-explore-sr [![Build Status](https://travis-ci.org/vatesfr/xen-orchestra.png?branch=master)](https://travis-ci.org/vatesfr/xen-orchestra)
> Display the list of VDIs (unmanaged and snapshots included) of a SR
## Install
Installation of the [npm package](https://npmjs.org/package/xapi-explore-sr):
```
> npm install --global xapi-explore-sr
```
## Usage
```
> xapi-explore-sr
Usage: xapi-explore-sr [--full] <SR UUID> <XenServer URL> <XenServer user> [<XenServer password>]
```
## Development
```
# Install dependencies
> npm install
# Run the tests
> npm test
# Continuously compile
> npm run dev
# Continuously run the tests
> npm run dev-test
# Build for production (automatically called by npm install)
> npm run build
```
## Contributions
Contributions are *very* welcomed, either on the documentation or on
the code.
You may:
- report any [issue](https://github.com/vatesfr/xen-orchestra/issues)
you've encountered;
- fork and create a pull request.
## License
ISC © [Vates SAS](https://vates.fr)

View File

@@ -0,0 +1,60 @@
{
"name": "xapi-explore-sr",
"version": "0.2.1",
"license": "ISC",
"description": "Display the list of VDIs (unmanaged and snapshots included) of a SR",
"keywords": [
"api",
"sr",
"vdi",
"vdis",
"xen",
"xen-api",
"xenapi"
],
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/packages/xapi-explore-sr",
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
"repository": {
"type": "git",
"url": "https://github.com/vatesfr/xen-orchestra.git"
},
"author": {
"name": "Julien Fontanet",
"email": "julien.fontanet@isonoe.net"
},
"preferGlobal": true,
"main": "dist/",
"bin": {
"xapi-explore-sr": "dist/index.js"
},
"files": [
"dist/"
],
"engines": {
"node": ">=8"
},
"dependencies": {
"archy": "^1.0.0",
"chalk": "^2.3.2",
"exec-promise": "^0.7.0",
"human-format": "^0.10.0",
"lodash": "^4.17.4",
"pw": "^0.0.4",
"xen-api": "^0.21.0"
},
"devDependencies": {
"@babel/cli": "^7.1.5",
"@babel/core": "^7.1.5",
"@babel/preset-env": "^7.1.5",
"babel-plugin-lodash": "^3.2.11",
"cross-env": "^5.1.4",
"rimraf": "^2.6.1"
},
"scripts": {
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
"prebuild": "rimraf dist/",
"predev": "yarn run prebuild",
"prepublishOnly": "yarn run build"
}
}

View File

@@ -0,0 +1,161 @@
#!/usr/bin/env node
import archy from 'archy'
import chalk from 'chalk'
import execPromise from 'exec-promise'
import humanFormat from 'human-format'
import pw from 'pw'
import { createClient } from 'xen-api'
import { forEach, map, orderBy } from 'lodash'
// ===================================================================
const askPassword = prompt =>
new Promise(resolve => {
prompt && process.stderr.write(`${prompt}: `)
pw(resolve)
})
const formatSize = bytes =>
humanFormat(bytes, {
prefix: 'Gi',
scale: 'binary',
})
const required = name => {
const e = `missing required argument <${name}>`
throw e
}
// -------------------------------------------------------------------
const STYLES = [
[
vdi => !vdi.managed,
chalk.enabled ? chalk.red : label => `[unmanaged] ${label}`,
],
[
vdi => vdi.is_a_snapshot,
chalk.enabled ? chalk.yellow : label => `[snapshot] ${label}`,
],
]
const getStyle = vdi => {
for (let i = 0, n = STYLES.length; i < n; ++i) {
const entry = STYLES[i]
if (entry[0](vdi)) {
return entry[1]
}
}
}
const mapFilter = (collection, iteratee, results = []) => {
forEach(collection, function () {
const result = iteratee.apply(this, arguments)
if (result !== undefined) {
results.push(result)
}
})
return results
}
// -------------------------------------------------------------------
execPromise(async args => {
if (args.length === 0 || args[0] === '-h' || args[0] === '--help') {
return `Usage: xapi-explore-sr [--full] <SR UUID> <XenServer URL> <XenServer user> [<XenServer password>]`
}
const full = args[0] === '--full'
if (full) {
args.shift()
}
const [
srUuid = required('SR UUID'),
url = required('XenServer URL'),
user = required('XenServer user'),
password = await askPassword('XenServer password'),
] = args
const xapi = createClient({
allowUnauthorized: true,
auth: { user, password },
readOnly: true,
url,
watchEvents: false,
})
await xapi.connect()
const srRef = await xapi.call('SR.get_by_uuid', srUuid)
const sr = await xapi.call('SR.get_record', srRef)
const vdisByRef = {}
await Promise.all(
map(sr.VDIs, async ref => {
const vdi = await xapi.call('VDI.get_record', ref)
vdisByRef[ref] = vdi
})
)
const hasParents = {}
const vhdChildrenByUuid = {}
forEach(vdisByRef, vdi => {
const vhdParent = vdi.sm_config['vhd-parent']
if (vhdParent) {
;(
vhdChildrenByUuid[vhdParent] || (vhdChildrenByUuid[vhdParent] = [])
).push(vdi)
} else if (!(vdi.snapshot_of in vdisByRef)) {
return
}
hasParents[vdi.uuid] = true
})
const makeVdiNode = vdi => {
const { uuid } = vdi
let label = `${vdi.name_label} - ${uuid} - ${formatSize(
+vdi.physical_utilisation
)}`
const nodes = []
const vhdChildren = vhdChildrenByUuid[uuid]
if (vhdChildren) {
mapFilter(
orderBy(vhdChildren, 'is_a_snapshot', 'desc'),
makeVdiNode,
nodes
)
}
mapFilter(
vdi.snapshots,
ref => {
const vdi = vdisByRef[ref]
if (full || !vdi.sm_config['vhd-parent']) {
return makeVdiNode(vdi)
}
},
nodes
)
const style = getStyle(vdi)
if (style) {
label = style(label)
}
return { label, nodes }
}
const nodes = mapFilter(orderBy(vdisByRef, ['name_label', 'uuid']), vdi => {
if (!hasParents[vdi.uuid]) {
return makeVdiNode(vdi)
}
})
return archy({
label: `${sr.name_label} (${sr.VDIs.length} VDIs)`,
nodes,
})
})

View File

@@ -4,6 +4,9 @@
Tested with:
- XenServer 7.6
- XenServer 7.5
- XenServer 7.4
- XenServer 7.3
- XenServer 7.2
- XenServer 7.1

View File

@@ -1,6 +1,6 @@
{
"name": "xen-api",
"version": "0.20.0",
"version": "0.21.0",
"license": "ISC",
"description": "Connector to the Xen API",
"keywords": [
@@ -39,7 +39,7 @@
"http-request-plus": "^0.6.0",
"iterable-backoff": "^0.0.0",
"jest-diff": "^23.5.0",
"json-rpc-protocol": "^0.12.0",
"json-rpc-protocol": "^0.13.1",
"kindof": "^2.0.0",
"lodash": "^4.17.4",
"make-error": "^1.3.0",

View File

@@ -0,0 +1,17 @@
import mapValues from 'lodash/mapValues'
export default function replaceSensitiveValues (value, replacement) {
function helper (value, name) {
if (name === 'password' && typeof value === 'string') {
return replacement
}
if (typeof value !== 'object' || value === null) {
return value
}
return Array.isArray(value) ? value.map(helper) : mapValues(value, helper)
}
return helper(value)
}

View File

@@ -30,6 +30,7 @@ import {
} from 'promise-toolbox'
import autoTransport from './transports/auto'
import replaceSensitiveValues from './_replaceSensitiveValues'
const debug = createDebug('xen-api')
@@ -93,7 +94,7 @@ class XapiError extends BaseError {
this.params = params
// slots than can be assigned later
this.method = undefined
this.call = undefined
this.url = undefined
this.task = undefined
}
@@ -1071,7 +1072,10 @@ Xapi.prototype._transportCall = reduce(
error = wrapError(error)
}
error.method = method
error.call = {
method,
params: replaceSensitiveValues(args, '* obfuscated *'),
}
throw error
})
},

View File

@@ -1,5 +1,9 @@
'use strict'
const { unauthorized } = require('xo-common/api-errors')
// ===================================================================
// These global variables are not a problem because the algorithm is
// synchronous.
let permissionsByObject
@@ -105,23 +109,26 @@ function checkAuthorization (objectId, permission) {
// -------------------------------------------------------------------
module.exports = (
function assertPermissions (
permissionsByObject_,
getObject_,
permissions,
permission
) => {
) {
// Assign global variables.
permissionsByObject = permissionsByObject_
getObject = getObject_
try {
if (permission) {
return checkAuthorization(permissions, permission)
if (permission !== undefined) {
const objectId = permissions
if (!checkAuthorization(objectId, permission)) {
throw unauthorized(permission, objectId)
}
} else {
for (const [objectId, permission] of permissions) {
if (!checkAuthorization(objectId, permission)) {
return false
throw unauthorized(permission, objectId)
}
}
}
@@ -132,3 +139,16 @@ module.exports = (
permissionsByObject = getObject = null
}
}
exports.assert = assertPermissions
exports.check = function checkPermissions () {
try {
assertPermissions.apply(undefined, arguments)
return true
} catch (error) {
if (unauthorized.is(error)) {
return false
}
throw error
}
}

View File

@@ -21,5 +21,8 @@
],
"engines": {
"node": ">=6"
},
"dependencies": {
"xo-common": "^0.2.0"
}
}

View File

@@ -120,7 +120,7 @@ encoding by prefixing with `json:`:
##### VM import
```
> xo-cli vm.import host=60a6939e-8b0a-4352-9954-5bde44bcdf7d @=vm.xva
> xo-cli vm.import sr=60a6939e-8b0a-4352-9954-5bde44bcdf7d @=vm.xva
```
## Development

View File

@@ -1,6 +1,6 @@
{
"name": "xo-common",
"version": "0.1.2",
"version": "0.2.0",
"license": "AGPL-3.0",
"description": "Code shared between [XO](https://xen-orchestra.com) server and clients",
"keywords": [],

View File

@@ -37,8 +37,15 @@ export const noSuchObject = create(1, (id, type) => ({
message: `no such ${type || 'object'} ${id}`,
}))
export const unauthorized = create(2, () => ({
message: 'not authenticated or not enough permissions',
export const unauthorized = create(2, (permission, objectId, objectType) => ({
data: {
permission,
object: {
id: objectId,
type: objectType,
},
},
message: 'not enough permissions',
}))
export const invalidCredentials = create(3, () => ({

View File

@@ -0,0 +1,10 @@
/examples/
example.js
example.js.map
*.example.js
*.example.js.map
/test/
/tests/
*.spec.js
*.spec.js.map

View File

@@ -0,0 +1,64 @@
# xo-import-servers-csv [![Build Status](https://travis-ci.org/vatesfr/xen-orchestra.png?branch=master)](https://travis-ci.org/vatesfr/xen-orchestra)
> CLI to import servers in XO from a CSV file
## Install
Installation of the [npm package](https://npmjs.org/package/xo-import-servers-csv):
```
> npm install --global xo-import-servers-csv
```
## Usage
`servers.csv`:
```csv
host,username,password
xs1.company.net,user1,password1
xs2.company.net:8080,user2,password2
http://xs3.company.net,user3,password3
```
> The CSV file can also contains these optional fields: `label`, `autoConnect`, `allowUnauthorized`.
Shell command:
```
> xo-import-servers-csv 'https://xo.company.tld' admin@admin.net admin < servers.csv
```
## Development
```
# Install dependencies
> npm install
# Run the tests
> npm test
# Continuously compile
> npm run dev
# Continuously run the tests
> npm run dev-test
# Build for production (automatically called by npm install)
> npm run build
```
## Contributions
Contributions are *very* welcomed, either on the documentation or on
the code.
You may:
- report any [issue](https://github.com/vatesfr/xen-orchestra/issues)
you've encountered;
- fork and create a pull request.
## License
ISC © [Vates SAS](http://vates.fr)

View File

@@ -0,0 +1,59 @@
{
"name": "xo-import-servers-csv",
"version": "1.1.0",
"license": "ISC",
"description": "CLI to import servers in XO from a CSV file",
"keywords": [
"csv",
"host",
"import",
"orchestra",
"pool",
"server",
"xen",
"xen-orchestra"
],
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/packages/xo-import-servers-csv",
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
"repository": {
"type": "git",
"url": "https://github.com/vatesfr/xen-orchestra.git"
},
"author": {
"name": "Julien Fontanet",
"email": "julien.fontanet@vates.fr"
},
"main": "dist/",
"bin": {
"xo-import-servers-csv": "dist/index.js"
},
"files": [
"dist/"
],
"engines": {
"node": ">=4"
},
"dependencies": {
"csv-parser": "^2.1.0",
"end-of-stream": "^1.1.0",
"exec-promise": "^0.7.0",
"highland": "^2.10.1",
"through2": "^3.0.0",
"xo-lib": "^0.9.0"
},
"devDependencies": {
"@types/node": "^10.12.2",
"@types/through2": "^2.0.31",
"tslint": "^5.9.1",
"tslint-config-standard": "^8.0.1",
"typescript": "^3.1.6"
},
"scripts": {
"build": "tsc",
"dev": "tsc -w",
"lint": "tslint 'src/*.ts'",
"posttest": "yarn run lint",
"prepublishOnly": "yarn run build",
"start": "node dist/index.js"
}
}

View File

@@ -0,0 +1,23 @@
declare module 'csv-parser' {
function csvParser(opts?: Object): any
export = csvParser
}
declare module 'exec-promise' {
function execPromise(cb: (args: string[]) => any): void
export = execPromise
}
declare module 'xo-lib' {
export default class Xo {
user?: { email: string }
constructor(opts?: { credentials?: {}; url: string })
call(method: string, ...params: any[]): Promise<any>
open(): Promise<void>
signIn(credentials: {}): Promise<void>
}
}

View File

@@ -0,0 +1,87 @@
#!/usr/bin/env node
/// <reference path="./index.d.ts" />
import csvParser = require('csv-parser')
import execPromise = require('exec-promise')
import through2 = require('through2')
import Xo from 'xo-lib'
const parseBoolean = (
value: string,
defaultValue?: boolean
): boolean | undefined => {
if (value === undefined || value === '') {
return defaultValue
}
const lcValue = value.toLocaleLowerCase()
if (value === '0' || lcValue === 'false') {
return false
}
if (value === '1' || lcValue === 'true') {
return true
}
throw new Error(`invalid boolean value: ${value}`)
}
const requiredParam = (name: string) => {
throw `missing param: ${name}
Usage: xo-import-servers-csv $url $username $password < $csvFile`
}
execPromise(
async ([
url = requiredParam('url'),
username = requiredParam('username'),
password = requiredParam('password'),
]): Promise<void> => {
const xo = new Xo({ url })
await xo.open()
await xo.signIn({ username, password })
console.log('connected as', xo.user!.email)
const errors: any[] = []
const stream = process.stdin.pipe(csvParser()).pipe(
through2.obj(
(
{ allowUnauthorized, autoConnect, host, label, password, username },
_,
next
) => {
console.log('server', host)
xo.call('server.add', {
allowUnauthorized: parseBoolean(allowUnauthorized),
autoConnect: parseBoolean(autoConnect, false),
host,
label,
password,
username,
}).then(
() => next(),
(error: any) => {
errors.push({ host, error })
return next()
}
)
}
)
)
await new Promise((resolve, reject) => {
stream.on('error', reject)
stream.on('finish', resolve)
})
if (errors.length) {
console.error(errors)
}
}
)

View File

@@ -0,0 +1,14 @@
{
"compilerOptions": {
"module": "commonjs",
"moduleResolution": "node",
"newLine": "lf",
"noImplicitAny": true,
"outDir": "dist/",
"removeComments": true,
"sourceMap": true,
"strictNullChecks": true,
"target": "es2015"
},
"includes": "src/**/*"
}

View File

@@ -0,0 +1,3 @@
{
"extends": "tslint-config-standard"
}

View File

@@ -667,8 +667,8 @@ class BackupReportsXoPlugin {
const globalStatus = globalSuccess
? `Success`
: nFailures !== 0
? `Failure`
: `Skipped`
? `Failure`
: `Skipped`
let markdown = [
`## Global status: ${globalStatus}`,
@@ -727,8 +727,8 @@ class BackupReportsXoPlugin {
globalSuccess
? ICON_SUCCESS
: nFailures !== 0
? ICON_FAILURE
: ICON_SKIPPED
? ICON_FAILURE
: ICON_SKIPPED
}`,
nagiosStatus: globalSuccess ? 0 : 2,
nagiosMarkdown: globalSuccess

View File

@@ -1,9 +1,8 @@
import Client, { createBackoff } from 'jsonrpc-websocket-client'
import hrp from 'http-request-plus'
const UPDATER_URL = 'localhost'
const WS_PORT = 9001
const HTTP_PORT = 9002
const WS_URL = 'ws://localhost:9001'
const HTTP_URL = 'http://localhost:9002'
// ===================================================================
@@ -47,7 +46,7 @@ class XoServerCloud {
this
)
const updater = (this._updater = new Client(`${UPDATER_URL}:${WS_PORT}`))
const updater = (this._updater = new Client(WS_URL))
const connect = () =>
updater.open(createBackoff()).catch(error => {
console.error('xo-server-cloud: fail to connect to updater', error)
@@ -143,7 +142,7 @@ class XoServerCloud {
throw new Error('cannot get download token')
}
const response = await hrp(`${UPDATER_URL}:${HTTP_PORT}/`, {
const response = await hrp(HTTP_URL, {
headers: {
Authorization: `Bearer ${downloadToken}`,
},

View File

@@ -463,7 +463,6 @@ ${monitorBodies.join('\n')}`
try {
const result = {
uuid,
name: definition.name,
object: this._xo.getXapi(uuid).getObject(uuid),
}

View File

@@ -36,6 +36,7 @@
"dependencies": {
"@xen-orchestra/async-map": "^0.0.0",
"@xen-orchestra/cron": "^1.0.3",
"@xen-orchestra/log": "^0.1.4",
"handlebars": "^4.0.6",
"html-minifier": "^3.5.8",
"human-format": "^0.10.0",

View File

@@ -1,4 +1,5 @@
import asyncMap from '@xen-orchestra/async-map'
import createLogger from '@xen-orchestra/log'
import Handlebars from 'handlebars'
import humanFormat from 'human-format'
import { createSchedule } from '@xen-orchestra/cron'
@@ -23,6 +24,8 @@ import { readFile, writeFile } from 'fs'
// ===================================================================
const log = createLogger('xo:xo-server-usage-report')
const GRANULARITY = 'days'
const pReadFile = promisify(readFile)
@@ -251,12 +254,10 @@ function getTop (objects, options) {
function computePercentage (curr, prev, options) {
return zipObject(
options,
map(
options,
opt =>
prev[opt] === 0 || prev[opt] === null
? 'NONE'
: `${((curr[opt] - prev[opt]) * 100) / prev[opt]}`
map(options, opt =>
prev[opt] === 0 || prev[opt] === null
? 'NONE'
: `${((curr[opt] - prev[opt]) * 100) / prev[opt]}`
)
)
}
@@ -287,7 +288,18 @@ async function getVmsStats ({ runningVms, xo }) {
return orderBy(
await Promise.all(
map(runningVms, async vm => {
const { stats } = await xo.getXapiVmStats(vm, GRANULARITY)
const { stats } = await xo
.getXapiVmStats(vm, GRANULARITY)
.catch(error => {
log.warn('Error on fetching VM stats', {
error,
vmId: vm.id,
})
return {
stats: {},
}
})
const iopsRead = METRICS_MEAN.iops(get(stats.iops, 'r'))
const iopsWrite = METRICS_MEAN.iops(get(stats.iops, 'w'))
return {
@@ -314,7 +326,18 @@ async function getHostsStats ({ runningHosts, xo }) {
return orderBy(
await Promise.all(
map(runningHosts, async host => {
const { stats } = await xo.getXapiHostStats(host, GRANULARITY)
const { stats } = await xo
.getXapiHostStats(host, GRANULARITY)
.catch(error => {
log.warn('Error on fetching host stats', {
error,
hostId: host.id,
})
return {
stats: {},
}
})
return {
uuid: host.uuid,
name: host.name_label,
@@ -351,7 +374,18 @@ async function getSrsStats ({ xo, xoObjects }) {
name += ` (${container.name_label})`
}
const { stats } = await xo.getXapiSrStats(sr.id, GRANULARITY)
const { stats } = await xo
.getXapiSrStats(sr.id, GRANULARITY)
.catch(error => {
log.warn('Error on fetching SR stats', {
error,
srId: sr.id,
})
return {
stats: {},
}
})
const iopsRead = computeMean(get(stats.iops, 'r'))
const iopsWrite = computeMean(get(stats.iops, 'w'))

View File

@@ -1,6 +1,6 @@
{
"name": "xo-server",
"version": "5.29.3",
"version": "5.29.4",
"license": "AGPL-3.0",
"description": "Server part of Xen-Orchestra",
"keywords": [
@@ -109,17 +109,17 @@
"stoppable": "^1.0.5",
"struct-fu": "^1.2.0",
"tar-stream": "^1.5.5",
"through2": "^2.0.3",
"through2": "^3.0.0",
"tmp": "^0.0.33",
"uuid": "^3.0.1",
"value-matcher": "^0.2.0",
"vhd-lib": "^0.4.0",
"ws": "^6.0.0",
"xen-api": "^0.20.0",
"xen-api": "^0.21.0",
"xml2js": "^0.4.19",
"xo-acl-resolver": "^0.3.0",
"xo-collection": "^0.4.1",
"xo-common": "^0.1.2",
"xo-common": "^0.2.0",
"xo-remote-parser": "^0.5.0",
"xo-vmdk-to-vhd": "^0.1.5",
"yazl": "^2.4.3"
@@ -138,6 +138,7 @@
"@babel/preset-env": "^7.0.0",
"@babel/preset-flow": "^7.0.0",
"babel-plugin-lodash": "^3.3.2",
"babel-plugin-transform-dev": "^2.0.1",
"cross-env": "^5.1.3",
"index-modules": "^0.3.0",
"rimraf": "^2.6.2"

View File

@@ -1,7 +1,7 @@
import createLogger from '@xen-orchestra/log'
import pump from 'pump'
import { format } from 'json-rpc-peer'
import { noSuchObject, unauthorized } from 'xo-common/api-errors'
import { noSuchObject } from 'xo-common/api-errors'
import { parseSize } from '../utils'
@@ -31,9 +31,7 @@ export async function create ({ name, size, sr, vm, bootable, position, mode })
// the resource set does not exist, falls back to normal check
}
if (!(await this.hasPermissions(this.user.id, [[sr.id, 'administrate']]))) {
throw unauthorized()
}
await this.checkPermissions(this.user.id, [[sr.id, 'administrate']])
} while (false)
const xapi = this.getXapi(sr)
@@ -125,6 +123,7 @@ async function handleImportContent (req, res, { xapi, id }) {
req.setTimeout(43200000) // 12 hours
try {
req.length = +req.headers['content-length']
await xapi.importVdiContent(id, req)
res.end(format.response(0, true))
} catch (e) {

View File

@@ -1,5 +1,3 @@
import { unauthorized } from 'xo-common/api-errors'
export function create (props) {
return this.createIpPool(props)
}
@@ -22,15 +20,12 @@ delete_.description = 'Delete an ipPool'
export function getAll (params) {
const { user } = this
if (!user) {
throw unauthorized()
}
return this.getAllIpPools(
user.permission === 'admin' ? params && params.userId : user.id
)
}
getAll.permission = ''
getAll.description = 'List all ipPools'
// -------------------------------------------------------------------

View File

@@ -1,7 +1,3 @@
import { unauthorized } from 'xo-common/api-errors'
// ===================================================================
export function create ({ name, subjects, objects, limits }) {
return this.createResourceSet(name, subjects, objects, limits)
}
@@ -99,14 +95,10 @@ set.params = {
// -------------------------------------------------------------------
export function get ({ id }) {
const { user } = this
if (!user) {
throw unauthorized()
}
return this.getResourceSet(id)
}
get.permission = ''
get.params = {
id: {
type: 'string',
@@ -116,14 +108,10 @@ get.params = {
// -------------------------------------------------------------------
export async function getAll () {
const { user } = this
if (!user) {
throw unauthorized()
}
return this.getAllResourceSets(user.id)
return this.getAllResourceSets(this.user.id)
}
getAll.permission = ''
getAll.description = 'Get the list of all existing resource set'
// -------------------------------------------------------------------

View File

@@ -4,7 +4,7 @@ export async function add ({ autoConnect = true, ...props }) {
const server = await this.registerXenServer(props)
if (autoConnect) {
;this.connectXenServer(server.id)::ignoreErrors()
this.connectXenServer(server.id)::ignoreErrors()
}
return server.id
@@ -105,7 +105,7 @@ set.params = {
// -------------------------------------------------------------------
export async function connect ({ id }) {
;this.updateXenServer(id, { enabled: true })::ignoreErrors()
this.updateXenServer(id, { enabled: true })::ignoreErrors()
await this.connectXenServer(id)
}
@@ -122,7 +122,7 @@ connect.params = {
// -------------------------------------------------------------------
export async function disconnect ({ id }) {
;this.updateXenServer(id, { enabled: false })::ignoreErrors()
this.updateXenServer(id, { enabled: false })::ignoreErrors()
await this.disconnectXenServer(id)
}

View File

@@ -1,6 +1,6 @@
// FIXME: rename to disk.*
import { invalidParameters, unauthorized } from 'xo-common/api-errors'
import { invalidParameters } from 'xo-common/api-errors'
import { isArray, reduce } from 'lodash'
import { parseSize } from '../utils'
@@ -67,13 +67,8 @@ export async function set (params) {
{ disk: size - vdi.size },
resourceSetId
)
} else if (
!(
this.user.permission === 'admin' ||
(await this.hasPermissions(this.user.id, [[vdi.$SR, 'operate']]))
)
) {
throw unauthorized()
} else {
await this.checkPermissions(this.user.id, [[vdi.$SR, 'operate']])
}
await xapi.resizeVdi(ref, size)

View File

@@ -6,7 +6,7 @@ import { diffItems } from '../utils'
// TODO: move into vm and rename to removeInterface
async function delete_ ({ vif }) {
;this.allocIpAddresses(
this.allocIpAddresses(
vif.id,
null,
vif.allowedIpv4Addresses.concat(vif.allowedIpv6Addresses)

View File

@@ -1,7 +1,7 @@
import concat from 'lodash/concat'
import defer from 'golike-defer'
import { format } from 'json-rpc-peer'
import { ignoreErrors } from 'promise-toolbox'
import { assignWith, concat } from 'lodash'
import {
forbiddenOperation,
invalidParameters,
@@ -32,13 +32,7 @@ function checkPermissionOnSrs (vm, permission = 'operate') {
])
})
return this.hasPermissions(this.session.get('user_id'), permissions).then(
success => {
if (!success) {
throw unauthorized()
}
}
)
return this.checkPermissions(this.session.get('user_id'), permissions)
}
// ===================================================================
@@ -54,13 +48,10 @@ export async function create (params) {
const { user } = this
const resourceSet = extract(params, 'resourceSet')
const template = extract(params, 'template')
if (
resourceSet === undefined &&
!(await this.hasPermissions(this.user.id, [
if (resourceSet === undefined) {
await this.checkPermissions(this.user.id, [
[template.$pool, 'administrate'],
]))
) {
throw unauthorized()
])
}
params.template = template._xapiId
@@ -68,12 +59,10 @@ export async function create (params) {
const xapi = this.getXapi(template)
const objectIds = [template.id]
const cpus = extract(params, 'CPUs')
const memoryMax = extract(params, 'memoryMax')
const limits = {
cpus: cpus !== undefined ? cpus : template.CPUs.number,
cpus: template.CPUs.number,
disk: 0,
memory: memoryMax !== undefined ? memoryMax : template.memory.dynamic[1],
memory: template.memory.dynamic[1],
vms: 1,
}
const vdiSizesByDevice = {}
@@ -153,8 +142,10 @@ export async function create (params) {
if (resourceSet) {
await this.checkResourceSetConstraints(resourceSet, user.id, objectIds)
checkLimits = async limits2 => {
await this.allocateLimitsInResourceSet(limits, resourceSet)
await this.allocateLimitsInResourceSet(limits2, resourceSet)
await this.allocateLimitsInResourceSet(
assignWith({}, limits, limits2, (l1 = 0, l2) => l1 + l2),
resourceSet
)
}
}
@@ -375,7 +366,7 @@ async function delete_ ({
vm.type === 'VM' && // only regular VMs
xapi.xo.getData(vm._xapiId, 'resourceSet') != null
) {
;this.setVmResourceSet(vm._xapiId, null)::ignoreErrors()
this.setVmResourceSet(vm._xapiId, null)::ignoreErrors()
}
return xapi.deleteVm(
@@ -426,14 +417,14 @@ ejectCd.resolve = {
// -------------------------------------------------------------------
export async function insertCd ({ vm, vdi, force }) {
export async function insertCd ({ vm, vdi, force = true }) {
await this.getXapi(vm).insertCdIntoVm(vdi._xapiId, vm._xapiId, { force })
}
insertCd.params = {
id: { type: 'string' },
cd_id: { type: 'string' },
force: { type: 'boolean' },
force: { type: 'boolean', optional: true },
}
insertCd.resolve = {
@@ -477,9 +468,7 @@ export async function migrate ({
})
}
if (!(await this.hasPermissions(this.session.get('user_id'), permissions))) {
throw unauthorized()
}
await this.checkPermissions(this.user.id, permissions)
await this.getXapi(vm).migrateVm(
vm._xapiId,
@@ -614,6 +603,8 @@ set.params = {
// Emulate HVM C000 PCI device for Windows Update to fetch or update PV drivers
hasVendorDevice: { type: 'boolean', optional: true },
expNestedHvm: { type: 'boolean', optional: true },
// Move the vm In to/Out of Self Service
resourceSet: { type: ['string', 'null'], optional: true },
@@ -629,7 +620,7 @@ set.resolve = {
// -------------------------------------------------------------------
export async function restart ({ vm, force }) {
export async function restart ({ vm, force = false }) {
const xapi = this.getXapi(vm)
if (force) {
@@ -641,7 +632,7 @@ export async function restart ({ vm, force }) {
restart.params = {
id: { type: 'string' },
force: { type: 'boolean' },
force: { type: 'boolean', optional: true },
}
restart.resolve = {
@@ -735,13 +726,7 @@ copy.resolve = {
export async function convertToTemplate ({ vm }) {
// Convert to a template requires pool admin permission.
if (
!(await this.hasPermissions(this.session.get('user_id'), [
[vm.$pool, 'administrate'],
]))
) {
throw unauthorized()
}
await this.checkPermissions(this.user.id, [[vm.$pool, 'administrate']])
await this.getXapi(vm).call('VM.set_is_a_template', vm._xapiRef, true)
}
@@ -1187,34 +1172,17 @@ async function handleVmImport (req, res, { data, srId, type, xapi }) {
}
// TODO: "sr_id" can be passed in URL to target a specific SR
async function import_ ({ data, host, sr, type }) {
let xapi
async function import_ ({ data, sr, type }) {
if (data && type === 'xva') {
throw invalidParameters('unsupported field data for the file type xva')
}
if (!sr) {
if (!host) {
throw invalidParameters('you must provide either host or SR')
}
xapi = this.getXapi(host)
sr = xapi.pool.$default_SR
if (!sr) {
throw invalidParameters('there is not default SR in this pool')
}
// FIXME: must have administrate permission on default SR.
} else {
xapi = this.getXapi(sr)
}
return {
$sendTo: await this.registerHttpRequest(handleVmImport, {
data,
srId: sr._xapiId,
type,
xapi,
xapi: this.getXapi(sr),
}),
}
}
@@ -1249,13 +1217,11 @@ import_.params = {
},
},
},
host: { type: 'string', optional: true },
type: { type: 'string', optional: true },
sr: { type: 'string', optional: true },
sr: { type: 'string' },
}
import_.resolve = {
host: ['host', 'host', 'administrate'],
sr: ['sr', 'SR', 'administrate'],
}
@@ -1307,10 +1273,8 @@ export async function createInterface ({
await this.checkResourceSetConstraints(resourceSet, this.user.id, [
network.id,
])
} else if (
!(await this.hasPermissions(this.user.id, [[network.id, 'view']]))
) {
throw unauthorized()
} else {
await this.checkPermissions(this.user.id, [[network.id, 'view']])
}
let ipAddresses

View File

@@ -5,6 +5,7 @@ import execa from 'execa'
import fs from 'fs-extra'
import map from 'lodash/map'
import { tap, delay } from 'promise-toolbox'
import { NULL_REF } from 'xen-api'
import { invalidParameters } from 'xo-common/api-errors'
import { v4 as generateUuid } from 'uuid'
import { includes, remove, filter, find, range } from 'lodash'
@@ -1138,9 +1139,12 @@ async function _prepareGlusterVm (
await xapi.startVm(newVM)
log.debug(`waiting for boot of ${ip}`)
// wait until we find the assigned IP in the networks, we are just checking the boot is complete
const vmIsUp = vm =>
Boolean(vm.$guest_metrics && includes(vm.$guest_metrics.networks, ip))
const vm = await xapi._waitObjectState(newVM.$id, vmIsUp)
// fix #3688
const vm = await xapi._waitObjectState(
newVM.$id,
_ => _.guest_metrics !== NULL_REF
)
await xapi._waitObjectState(vm.guest_metrics, _ => includes(_.networks, ip))
log.debug(`booted ${ip}`)
const localEndpoint = { xapi: xapi, hosts: [host], addresses: [ip] }
const srFreeSpace = sr.physical_size - sr.physical_utilisation

View File

@@ -78,19 +78,18 @@ export default class Redis extends Collection {
.then(keys => keys.length !== 0 && redis.del(keys))
).then(() =>
asyncMap(redis.smembers(idsIndex), id =>
redis.hgetall(`${prefix}:${id}`).then(
values =>
values == null
? redis.srem(idsIndex, id) // entry no longer exists
: asyncMap(indexes, index => {
const value = values[index]
if (value !== undefined) {
return redis.sadd(
`${prefix}_${index}:${String(value).toLowerCase()}`,
id
)
}
})
redis.hgetall(`${prefix}:${id}`).then(values =>
values == null
? redis.srem(idsIndex, id) // entry no longer exists
: asyncMap(indexes, index => {
const value = values[index]
if (value !== undefined) {
return redis.sadd(
`${prefix}_${index}:${String(value).toLowerCase()}`,
id
)
}
})
)
)
)

View File

@@ -0,0 +1,43 @@
import mapValues from 'lodash/mapValues'
// this random value is used to obfuscate real data
const OBFUSCATED_VALUE = 'q3oi6d9X8uenGvdLnHk2'
export const merge = (newValue, oldValue) => {
if (newValue === OBFUSCATED_VALUE) {
return oldValue
}
let isArray
if (
newValue === null ||
oldValue === null ||
typeof newValue !== 'object' ||
typeof oldValue !== 'object' ||
(isArray = Array.isArray(newValue)) !== Array.isArray(oldValue)
) {
return newValue
}
const iteratee = (v, k) => merge(v, oldValue[k])
return isArray ? newValue.map(iteratee) : mapValues(newValue, iteratee)
}
export const obfuscate = value => replace(value, OBFUSCATED_VALUE)
export function replace (value, replacement) {
function helper (value, name) {
if (name === 'password' && typeof value === 'string') {
return replacement
}
if (typeof value !== 'object' || value === null) {
return value
}
return Array.isArray(value) ? value.map(helper) : mapValues(value, helper)
}
return helper(value)
}

View File

@@ -1,6 +1,5 @@
// @flow
// $FlowFixMe
import through2 from 'through2'
import { type Readable } from 'stream'

View File

@@ -10,7 +10,12 @@ import {
mapToArray,
parseXml,
} from './utils'
import { isHostRunning, isVmHvm, isVmRunning, parseDateTime } from './xapi'
import {
getVmDomainType,
isHostRunning,
isVmRunning,
parseDateTime,
} from './xapi'
import { useUpdateSystem } from './xapi/utils'
// ===================================================================
@@ -218,14 +223,18 @@ const TRANSFORMS = {
// -----------------------------------------------------------------
vm (obj) {
vm (obj, dependents) {
dependents[obj.guest_metrics] = obj.$id
dependents[obj.metrics] = obj.$id
const {
$guest_metrics: guestMetrics,
$metrics: metrics,
other_config: otherConfig,
} = obj
const isHvm = isVmHvm(obj)
const domainType = getVmDomainType(obj)
const isHvm = domainType === 'hvm'
const isRunning = isVmRunning(obj)
const xenTools = (() => {
if (!isRunning || !metrics) {
@@ -302,7 +311,7 @@ const TRANSFORMS = {
version: version && parseXml(version).docker_version,
}
})(),
expNestedHvm: obj.platform['exp-nested-hvm'] === 'true',
high_availability: obj.ha_restart_priority,
memory: (function () {
@@ -343,11 +352,7 @@ const TRANSFORMS = {
startTime: metrics && toTimestamp(metrics.start_time),
tags: obj.tags,
VIFs: link(obj, 'VIFs'),
virtualizationMode: isHvm
? guestMetrics !== undefined && guestMetrics.PV_drivers_detected
? 'pvhvm'
: 'hvm'
: 'pv',
virtualizationMode: domainType,
// <=> Are the Xen Server tools installed?
//
@@ -739,13 +744,13 @@ const TRANSFORMS = {
// ===================================================================
export default xapiObj => {
export default function xapiObjectToXo (xapiObj, dependents) {
const transform = TRANSFORMS[xapiObj.$type.toLowerCase()]
if (!transform) {
return
}
const xoObj = transform(xapiObj)
const xoObj = transform(xapiObj, dependents)
if (!xoObj) {
return
}

View File

@@ -99,8 +99,8 @@ const testMetric = (test, type) =>
typeof test === 'string'
? test === type
: typeof test === 'function'
? test(type)
: test.exec(type)
? test(type)
: test.exec(type)
const findMetric = (metrics, metricType) => {
let testResult
@@ -321,8 +321,12 @@ export default class XapiStats {
const hostUuid = host.uuid
if (
!(
vmUuid !== undefined &&
get(this._statsByObject, [vmUuid, step]) === undefined
) &&
get(this._statsByObject, [hostUuid, step, 'localTimestamp']) + step >
getCurrentTimestamp()
getCurrentTimestamp()
) {
return this._getStats(hostUuid, step, vmUuid)
}
@@ -414,7 +418,7 @@ export default class XapiStats {
})
}
getVmStats (xapi, vmId, granularity) {
async getVmStats (xapi, vmId, granularity) {
const vm = xapi.getObject(vmId)
const host = vm.$resident_on
if (!host) {

View File

@@ -5,6 +5,7 @@ import createLogger from '@xen-orchestra/log'
import deferrable from 'golike-defer'
import fatfs from 'fatfs'
import mixin from '@xen-orchestra/mixin'
import ms from 'ms'
import synchronized from 'decorator-synchronized'
import tarStream from 'tar-stream'
import vmdkToVhd from 'xo-vmdk-to-vhd'
@@ -207,8 +208,8 @@ export default class Xapi extends XapiBase {
}
const loop = () =>
this._waitObject(idOrUuidOrRef).then(
object => (predicate(object) ? object : loop())
this._waitObject(idOrUuidOrRef).then(object =>
predicate(object) ? object : loop()
)
return loop()
@@ -657,23 +658,32 @@ export default class Xapi extends XapiBase {
// ensure the vm record is up-to-date
vm = await this.barrier($ref)
if (!force && 'destroy' in vm.blocked_operations) {
throw forbiddenOperation('destroy', vm.blocked_operations.destroy.reason)
}
if (
!forceDeleteDefaultTemplate &&
vm.other_config.default_template === 'true'
) {
throw forbiddenOperation('destroy', 'VM is default template')
}
// It is necessary for suspended VMs to be shut down
// to be able to delete their VDIs.
if (vm.power_state !== 'Halted') {
await this.call('VM.hard_shutdown', $ref)
}
if (force) {
await this._updateObjectMapProperty(vm, 'blocked_operations', {
await Promise.all(
this.call('VM.set_is_a_template', vm.$ref, false),
this._updateObjectMapProperty(vm, 'blocked_operations', {
destroy: null,
})
}
if (forceDeleteDefaultTemplate) {
await this._updateObjectMapProperty(vm, 'other_config', {
}),
this._updateObjectMapProperty(vm, 'other_config', {
default_template: null,
})
}
)
// must be done before destroying the VM
const disks = getVmDisks(vm)
@@ -876,7 +886,7 @@ export default class Xapi extends XapiBase {
//
// The snapshot must not exist otherwise it could break the
// next export.
;this._deleteVdi(vdi)::ignoreErrors()
this._deleteVdi(vdi)::ignoreErrors()
return
}
@@ -1127,7 +1137,7 @@ export default class Xapi extends XapiBase {
])
if (deleteBase && baseVm) {
;this._deleteVm(baseVm)::ignoreErrors()
this._deleteVm(baseVm)::ignoreErrors()
}
await Promise.all([
@@ -1167,8 +1177,8 @@ export default class Xapi extends XapiBase {
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.
? hostXapi.getObject(sr).$ref
: defaultSr.$ref // Will error if there are no default SR.
}
}
@@ -1315,7 +1325,7 @@ export default class Xapi extends XapiBase {
}
if (onVmCreation != null) {
;this._waitObject(
this._waitObject(
obj =>
obj != null &&
obj.current_operations != null &&
@@ -1653,12 +1663,12 @@ export default class Xapi extends XapiBase {
await this._startVm(vm)
} finally {
;this._setObjectProperties(vm, {
this._setObjectProperties(vm, {
PV_bootloader: bootloader,
})::ignoreErrors()
forEach(bootables, ([vbd, bootable]) => {
;this._setObjectProperties(vbd, { bootable })::ignoreErrors()
this._setObjectProperties(vbd, { bootable })::ignoreErrors()
})
}
}
@@ -1995,6 +2005,11 @@ export default class Xapi extends XapiBase {
// -----------------------------------------------------------------
async _importVdiContent (vdi, body, format = VDI_FORMAT_VHD) {
if (__DEV__ && body.length == null) {
throw new Error(
'Trying to import a VDI without a length field. Please report this error to Xen Orchestra.'
)
}
await Promise.all([
body.task,
body.checksumVerified,
@@ -2357,15 +2372,14 @@ export default class Xapi extends XapiBase {
}
async _assertConsistentHostServerTime (hostRef) {
if (
Math.abs(
parseDateTime(
await this.call('host.get_servertime', hostRef)
).getTime() - Date.now()
) > 2e3
) {
const delta =
parseDateTime(await this.call('host.get_servertime', hostRef)).getTime() -
Date.now()
if (Math.abs(delta) > 30e3) {
throw new Error(
'host server time and XOA date are not consistent with each other'
`host server time and XOA date are not consistent with each other (${ms(
delta
)})`
)
}
}

View File

@@ -462,7 +462,9 @@ export default {
async _installAllPoolPatchesOnHost (host) {
const installableByUuid =
host.license_params.sku_type !== 'free'
? await this._listMissingPoolPatchesOnHost(host)
? pickBy(await this._listMissingPoolPatchesOnHost(host), {
upgrade: false,
})
: pickBy(await this._listMissingPoolPatchesOnHost(host), {
paid: false,
upgrade: false,
@@ -509,11 +511,10 @@ export default {
...(await Promise.all(
mapFilter(this.objects.all, host => {
if (host.$type === 'host') {
return this._listMissingPoolPatchesOnHost(host).then(
patches =>
host.license_params.sku_type !== 'free'
? patches
: pickBy(patches, { paid: false, upgrade: false })
return this._listMissingPoolPatchesOnHost(host).then(patches =>
host.license_params.sku_type !== 'free'
? pickBy(patches, { upgrade: false })
: pickBy(patches, { paid: false, upgrade: false })
)
}
})

View File

@@ -257,6 +257,18 @@ export default {
},
},
virtualizationMode: {
set (virtualizationMode, vm) {
if (virtualizationMode !== 'pv' && virtualizationMode !== 'hvm') {
throw new Error(`The virtualization mode must be 'pv' or 'hvm'`)
}
return this._set(
'HVM_boot_policy',
virtualizationMode === 'hvm' ? 'Boot order' : ''
)
},
},
coresPerSocket: {
set (coresPerSocket, vm) {
return this._updateObjectMapProperty(vm, 'platform', {
@@ -383,6 +395,14 @@ export default {
hasVendorDevice: true,
expNestedHvm: {
set (expNestedHvm, vm) {
return this._updateObjectMapProperty(vm, 'platform', {
'exp-nested-hvm': expNestedHvm ? 'true' : null,
})
},
},
nicType: {
set (nicType, vm) {
return this._updateObjectMapProperty(vm, 'platform', {
@@ -427,9 +447,9 @@ export default {
if (snapshot.snapshot_info['power-state-at-snapshot'] === 'Running') {
const vm = snapshot.$snapshot_of
if (vm.power_state === 'Halted') {
;this.startVm(vm.$id)::ignoreErrors()
this.startVm(vm.$id)::ignoreErrors()
} else if (vm.power_state === 'Suspended') {
;this.resumeVm(vm.$id)::ignoreErrors()
this.resumeVm(vm.$id)::ignoreErrors()
}
}
},

View File

@@ -143,7 +143,18 @@ export const isHostRunning = host => {
// -------------------------------------------------------------------
export const isVmHvm = vm => Boolean(vm.HVM_boot_policy)
export const getVmDomainType = vm => {
const dt = vm.domain_type
if (
dt !== undefined && // XS < 7.5
dt !== 'unspecified' // detection failed
) {
return dt
}
return vm.HVM_boot_policy === '' ? 'pv' : 'hvm'
}
export const isVmHvm = vm => getVmDomainType(vm) === 'hvm'
const VM_RUNNING_POWER_STATES = {
Running: true,

View File

@@ -1,4 +1,4 @@
import checkAuthorization from 'xo-acl-resolver'
import aclResolver from 'xo-acl-resolver'
import { forEach, includes, map } from 'lodash'
import { ModelAlreadyExists } from '../collection'
@@ -102,6 +102,21 @@ export default class {
return permissions
}
async checkPermissions (userId, permissions) {
const user = await this._xo.getUser(userId)
// Special case for super XO administrators.
if (user.permission === 'admin') {
return true
}
aclResolver.assert(
await this.getPermissionsForUser(userId),
id => this._xo.getObject(id),
permissions
)
}
async hasPermissions (userId, permissions) {
const user = await this._xo.getUser(userId)
@@ -110,7 +125,7 @@ export default class {
return true
}
return checkAuthorization(
return aclResolver.check(
await this.getPermissionsForUser(userId),
id => this._xo.getObject(id),
permissions

View File

@@ -2,10 +2,11 @@ import createLogger from '@xen-orchestra/log'
import kindOf from 'kindof'
import ms from 'ms'
import schemaInspector from 'schema-inspector'
import { forEach, isArray, isFunction, map, mapValues } from 'lodash'
import { forEach, isFunction } from 'lodash'
import { MethodNotFound } from 'json-rpc-peer'
import * as methods from '../api'
import { MethodNotFound } from 'json-rpc-peer'
import * as sensitiveValues from '../sensitive-values'
import { noop, serializeError } from '../utils'
import * as errors from 'xo-common/api-errors'
@@ -82,7 +83,7 @@ function checkPermission (method) {
const { user } = this
if (!user) {
throw errors.unauthorized()
throw errors.unauthorized(permission)
}
// The only requirement is login.
@@ -91,11 +92,11 @@ function checkPermission (method) {
}
if (!hasPermission(user, permission)) {
throw errors.unauthorized()
throw errors.unauthorized(permission)
}
}
function resolveParams (method, params) {
async function resolveParams (method, params) {
const resolve = method.resolve
if (!resolve) {
return params
@@ -134,33 +135,13 @@ function resolveParams (method, params) {
}
})
return this.hasPermissions(userId, permissions).then(success => {
if (success) {
return params
}
await this.checkPermissions(userId, permissions)
throw errors.unauthorized()
})
return params
}
// -------------------------------------------------------------------
const removeSensitiveParams = (value, name) => {
if (name === 'password' && typeof value === 'string') {
return '* obfuscated *'
}
if (typeof value !== 'object' || value === null) {
return value
}
return isArray(value)
? map(value, removeSensitiveParams)
: mapValues(value, removeSensitiveParams)
}
// ===================================================================
export default class Api {
constructor (xo) {
this._logger = null
@@ -295,7 +276,7 @@ export default class Api {
const data = {
userId,
method: name,
params: removeSensitiveParams(params),
params: sensitiveValues.replace(params, '* obfuscated *'),
duration: Date.now() - startTime,
error: serializeError(error),
}

View File

@@ -181,7 +181,7 @@ export default class {
token = token.properties
if (!(token.expiration > Date.now())) {
;this._tokens.remove(id)::ignoreErrors()
this._tokens.remove(id)::ignoreErrors()
throw noSuchAuthenticationToken(id)
}

View File

@@ -134,8 +134,8 @@ const getOldEntries = <T>(retention: number, entries?: T[]): T[] =>
entries === undefined
? []
: retention > 0
? entries.slice(0, -retention)
: entries
? entries.slice(0, -retention)
: entries
const defaultSettings: Settings = {
concurrency: 0,
@@ -147,12 +147,12 @@ const defaultSettings: Settings = {
timeout: 0,
vmTimeout: 0,
}
const getSetting = <T>(
const getSetting = <T, K: $Keys<Settings>>(
settings: $Dict<Settings>,
name: $Keys<Settings>,
name: K,
keys: string[],
defaultValue?: T
): T | any => {
): T | $ElementType<Settings, K> => {
for (let i = 0, n = keys.length; i < n; ++i) {
const objectSettings = settings[keys[i]]
if (objectSettings !== undefined) {
@@ -470,6 +470,28 @@ const extractIdsFromSimplePattern = (pattern: mixed) => {
// - copy in delta mode: `Continuous Replication`
// - copy in full mode: `Disaster Recovery`
// - imported from backup: `restored from backup`
//
// Task logs emitted in a backup execution:
//
// job.start(data: { mode: Mode, reportWhen: ReportWhen })
// ├─ task.info(message: 'vms', data: { vms: string[] })
// ├─ task.warning(message: 'missingVms', data: { vms: string[] })
// ├─ task.warning(message: string)
// ├─ task.start(data: { type: 'VM', id: string })
// │ ├─ task.warning(message: string)
// │ ├─ task.start(message: 'snapshot')
// │ │ └─ task.end
// │ ├─ task.start(message: 'export', data: { type: 'SR' | 'remote', id: string })
// │ │ ├─ task.warning(message: string)
// │ │ ├─ task.start(message: 'transfer')
// │ │ │ ├─ task.warning(message: string)
// │ │ │ └─ task.end(result: { size: number })
// │ │ ├─ task.start(message: 'merge')
// │ │ │ ├─ task.warning(message: string)
// │ │ │ └─ task.end(result: { size: number })
// │ │ └─ task.end
// │ └─ task.end
// └─ job.end
export default class BackupNg {
_app: {
createJob: ($Diff<BackupJob, {| id: string |}>) => Promise<BackupJob>,
@@ -522,29 +544,24 @@ export default class BackupNg {
(vmsId = extractIdsFromSimplePattern(vmsPattern)) !== undefined
) {
vms = {}
const missingVms = []
vmsId.forEach(id => {
try {
vms[id] = app.getObject(id, 'VM')
} catch (error) {
const taskId: string = logger.notice(
`Starting backup of ${id}. (${job.id})`,
{
event: 'task.start',
parentId: runJobId,
data: {
type: 'VM',
id,
},
}
)
logger.error(`Backuping ${id} has failed. (${job.id})`, {
event: 'task.end',
taskId,
status: 'failure',
result: serializeError(error),
})
missingVms.push(id)
}
})
if (missingVms.length !== 0) {
logger.warning('missingVms', {
event: 'task.warning',
taskId: runJobId,
data: {
vms: missingVms,
},
})
}
} else {
vms = app.getObjects({
filter: createPredicate({
@@ -717,6 +734,12 @@ export default class BackupNg {
}
}
// Task logs emitted in a restore execution:
//
// task.start(message: 'restore', data: { jobId: string, srId: string, time: number })
// ├─ task.start(message: 'transfer')
// │ └─ task.end(result: { id: string, size: number })
// └─ task.end
async importVmBackupNg (id: string, srId: string): Promise<string> {
const app = this._app
const { metadataFilename, remoteId } = parseVmBackupId(id)

View File

@@ -144,8 +144,8 @@ const listPartitions = (() => {
key === 'start' || key === 'size'
? +value
: key === 'type'
? TYPES[+value] || value
: value,
? TYPES[+value] || value
: value,
})
return device =>
@@ -445,17 +445,17 @@ export default class {
// Once done, (asynchronously) remove the (now obsolete) local
// base.
if (localBaseUuid) {
;promise.then(() => srcXapi.deleteVm(localBaseUuid))::ignoreErrors()
promise.then(() => srcXapi.deleteVm(localBaseUuid))::ignoreErrors()
}
if (toRemove !== undefined) {
;promise
promise
.then(() => asyncMap(toRemove, _ => targetXapi.deleteVm(_.$id)))
::ignoreErrors()
}
// (Asynchronously) Identify snapshot as future base.
;promise
promise
.then(() => {
return srcXapi._updateObjectMapProperty(srcVm, 'other_config', {
[TAG_LAST_BASE_DELTA]: delta.vm.uuid,
@@ -593,7 +593,7 @@ export default class {
base => base.snapshot_time
)
forEach(bases, base => {
;xapi.deleteVdi(base.$id)::ignoreErrors()
xapi.deleteVdi(base.$id)::ignoreErrors()
})
// Export full or delta backup.
@@ -652,7 +652,7 @@ export default class {
)
const baseVm = bases.pop()
forEach(bases, base => {
;xapi.deleteVm(base.$id)::ignoreErrors()
xapi.deleteVm(base.$id)::ignoreErrors()
})
// Check backup dirs.
@@ -780,7 +780,7 @@ export default class {
await this._removeOldDeltaVmBackups(xapi, { vm, handler, dir, retention })
if (baseVm) {
;xapi.deleteVm(baseVm.$id)::ignoreErrors()
xapi.deleteVm(baseVm.$id)::ignoreErrors()
}
return {

View File

@@ -56,8 +56,8 @@ const parsePartxLine = createPairsParser({
key === 'start' || key === 'size'
? +value
: key === 'type'
? PARTITION_TYPE_NAMES[+value] || value
: value,
? PARTITION_TYPE_NAMES[+value] || value
: value,
})
const listLvmLogicalVolumes = defer(

View File

@@ -328,7 +328,7 @@ export default class Jobs {
app.emit('job:terminated', undefined, job, schedule, runJobId)
throw error
} finally {
;this.updateJob({ id, runId: null })::ignoreErrors()
this.updateJob({ id, runId: null })::ignoreErrors()
delete runningJobs[id]
delete runs[runJobId]
if (session !== undefined) {

View File

@@ -44,7 +44,7 @@ export default class LevelDbLogger extends AbstractLogger {
return promise.then(() => key)
}
;promise::ignoreErrors()
promise::ignoreErrors()
return key
}

View File

@@ -1,8 +1,9 @@
import Ajv from 'ajv'
import createLogger from '@xen-orchestra/log'
import { PluginsMetadata } from '../models/plugin-metadata'
import { invalidParameters, noSuchObject } from 'xo-common/api-errors'
import * as sensitiveValues from '../sensitive-values'
import { PluginsMetadata } from '../models/plugin-metadata'
import { isFunction, mapToArray } from '../utils'
// ===================================================================
@@ -119,7 +120,7 @@ export default class {
loaded,
unloadable,
version,
configuration,
configuration: sensitiveValues.obfuscate(configuration),
configurationPresets,
configurationSchema,
testable,
@@ -165,6 +166,14 @@ export default class {
// save the new configuration.
async configurePlugin (id, configuration) {
const plugin = this._getRawPlugin(id)
const metadata = await this._getPluginMetadata()
if (metadata !== undefined) {
configuration = sensitiveValues.merge(
configuration,
metadata.configuration
)
}
await this._configurePlugin(plugin, configuration)

View File

@@ -1,14 +1,21 @@
import synchronized from 'decorator-synchronized'
import { format, parse } from 'xo-remote-parser'
import { getHandler } from '@xen-orchestra/fs'
import { noSuchObject } from 'xo-common/api-errors'
import { ignoreErrors } from 'promise-toolbox'
import { noSuchObject } from 'xo-common/api-errors'
import * as sensitiveValues from '../sensitive-values'
import patch from '../patch'
import { mapToArray } from '../utils'
import { Remotes } from '../models/remote'
// ===================================================================
const obfuscateRemote = ({ url, ...remote }) => {
remote.url = format(sensitiveValues.obfuscate(parse(url)))
return remote
}
export default class {
constructor (xo, { remoteOptions }) {
this._remoteOptions = remoteOptions
@@ -30,7 +37,7 @@ export default class {
)
)
const remotes = await this.getAllRemotes()
const remotes = await this._remotes.get()
remotes.forEach(remote => {
ignoreErrors.call(this.updateRemote(remote.id, {}))
})
@@ -47,7 +54,7 @@ export default class {
async getRemoteHandler (remote) {
if (typeof remote === 'string') {
remote = await this.getRemote(remote)
remote = await this._getRemote(remote)
}
if (!remote.enabled) {
@@ -78,10 +85,10 @@ export default class {
}
async getAllRemotes () {
return this._remotes.get()
return (await this._remotes.get()).map(_ => obfuscateRemote(_))
}
async getRemote (id) {
async _getRemote (id) {
const remote = await this._remotes.first(id)
if (remote === undefined) {
throw noSuchObject(id, 'remote')
@@ -89,6 +96,10 @@ export default class {
return remote.properties
}
getRemote (id) {
return this._getRemote(id).then(obfuscateRemote)
}
async createRemote ({ name, url, options }) {
const params = {
name,
@@ -120,9 +131,16 @@ export default class {
}
@synchronized()
async _updateRemote (id, props) {
const remote = await this.getRemote(id)
async _updateRemote (id, { url, ...props }) {
const remote = await this._getRemote(id)
// url is handled separately to take care of obfuscated values
if (typeof url === 'string') {
remote.url = format(sensitiveValues.merge(parse(url), parse(remote.url)))
}
patch(remote, props)
return (await this._remotes.update(remote)).properties
}

View File

@@ -57,15 +57,13 @@ const normalize = set => ({
id: set.id,
ipPools: set.ipPools || [],
limits: set.limits
? map(
set.limits,
limit =>
isObject(limit)
? limit
: {
available: limit,
total: limit,
}
? map(set.limits, limit =>
isObject(limit)
? limit
: {
available: limit,
total: limit,
}
)
: {},
name: set.name || '',

View File

@@ -10,10 +10,8 @@ import { forEach, isFunction, promisify } from '../utils'
const _levelHas = function has (key, cb) {
if (cb) {
return this.get(
key,
(error, value) =>
error ? (error.notFound ? cb(null, false) : cb(error)) : cb(null, true)
return this.get(key, (error, value) =>
error ? (error.notFound ? cb(null, false) : cb(error)) : cb(null, true)
)
}

View File

@@ -104,7 +104,7 @@ export default class {
.getAuthenticationTokensForUser(id)
.then(tokens => {
forEach(tokens, token => {
;this._xo.deleteAuthenticationToken(id)::ignoreErrors()
this._xo.deleteAuthenticationToken(id)::ignoreErrors()
})
})
::ignoreErrors()
@@ -112,13 +112,13 @@ export default class {
// Remove ACLs for this user.
this._xo.getAclsForSubject(id).then(acls => {
forEach(acls, acl => {
;this._xo.removeAcl(id, acl.object, acl.action)::ignoreErrors()
this._xo.removeAcl(id, acl.object, acl.action)::ignoreErrors()
})
})
// Remove the user from all its groups.
forEach(user.groups, groupId => {
;this.getGroup(groupId)
this.getGroup(groupId)
.then(group => this._removeUserFromGroup(id, group))
::ignoreErrors()
})
@@ -264,13 +264,13 @@ export default class {
// Remove ACLs for this group.
this._xo.getAclsForSubject(id).then(acls => {
forEach(acls, acl => {
;this._xo.removeAcl(id, acl.object, acl.action)::ignoreErrors()
this._xo.removeAcl(id, acl.object, acl.action)::ignoreErrors()
})
})
// Remove the group from all its users.
forEach(group.users, userId => {
;this.getUser(userId)
this.getUser(userId)
.then(user => this._removeGroupFromUser(id, user))
::ignoreErrors()
})

View File

@@ -5,8 +5,10 @@ export default class Workers {
return this._worker
}
constructor (app) {
constructor (app, config) {
app.on('start', () => {
process.env.XO_CONFIG = JSON.stringify(config)
this._worker = new Worker(require.resolve('./worker'))
})
app.on('stop', () => this._worker.end())

View File

@@ -5,8 +5,13 @@ import { mergeVhd as mergeVhd_ } from 'vhd-lib'
// Use Bluebird for all promises as it provides better performance and
// less memory usage.
//
// $FlowFixMe
global.Promise = require('bluebird')
// $FlowFixMe
const config: Object = JSON.parse(process.env.XO_CONFIG)
export function mergeVhd (
parentRemote: Remote,
parentPath: string,
@@ -14,9 +19,9 @@ export function mergeVhd (
childPath: string
) {
return mergeVhd_(
getHandler(parentRemote),
getHandler(parentRemote, config.remoteOptions),
parentPath,
getHandler(childRemote),
getHandler(childRemote, config.remoteOptions),
childPath
)
}

View File

@@ -20,7 +20,7 @@ import { Servers } from '../models/server'
const log = createLogger('xo:xo-mixins:xen-servers')
export default class {
constructor (xo) {
constructor (xo, { xapiOptions }) {
this._objectConflicts = { __proto__: null } // TODO: clean when a server is disconnected.
const serversDb = (this._servers = new Servers({
connection: xo._redis,
@@ -28,6 +28,7 @@ export default class {
indexes: ['host'],
}))
this._stats = new XapiStats()
this._xapiOptions = xapiOptions
this._xapis = { __proto__: null }
this._xapisByPool = { __proto__: null }
this._xo = xo
@@ -82,7 +83,7 @@ export default class {
}
async unregisterXenServer (id) {
;this.disconnectXenServer(id)::ignoreErrors()
this.disconnectXenServer(id)::ignoreErrors()
if (!(await this._servers.remove(id))) {
throw noSuchObject(id, 'xenServer')
@@ -158,13 +159,28 @@ export default class {
return server
}
_onXenAdd (xapiObjects, xapiIdsToXo, toRetry, conId) {
_onXenAdd (
newXapiObjects,
xapiIdsToXo,
toRetry,
conId,
dependents,
xapiObjects
) {
const conflicts = this._objectConflicts
const objects = this._xo._objects
forEach(xapiObjects, (xapiObject, xapiId) => {
forEach(newXapiObjects, function handleObject (xapiObject, xapiId) {
const { $ref } = xapiObject
const dependent = dependents[$ref]
if (dependent !== undefined) {
delete dependents[$ref]
return handleObject(xapiObjects[dependent], dependent)
}
try {
const xoObject = xapiObjectToXo(xapiObject)
const xoObject = xapiObjectToXo(xapiObject, dependents)
if (!xoObject) {
return
}
@@ -173,7 +189,7 @@ export default class {
xapiIdsToXo[xapiId] = xoId
const previous = objects.get(xoId, undefined)
if (previous && previous._xapiRef !== xapiObject.$ref) {
if (previous && previous._xapiRef !== $ref) {
const conflicts_ =
conflicts[xoId] || (conflicts[xoId] = { __proto__: null })
conflicts_[conId] = xoObject
@@ -225,11 +241,14 @@ export default class {
const xapi = (this._xapis[server.id] = new Xapi({
allowUnauthorized: Boolean(server.allowUnauthorized),
readOnly: Boolean(server.readOnly),
...this._xapiOptions,
auth: {
user: server.username,
password: server.password,
},
readOnly: Boolean(server.readOnly),
url: server.host,
}))
@@ -247,11 +266,20 @@ export default class {
let toRetry
let toRetryNext = { __proto__: null }
const dependents = { __proto__: null }
const onAddOrUpdate = objects => {
this._onXenAdd(objects, xapiIdsToXo, toRetryNext, conId)
this._onXenAdd(
objects,
xapiIdsToXo,
toRetryNext,
conId,
dependents,
xapi.objects.all
)
}
const onRemove = objects => {
this._onXenRemove(objects, xapiIdsToXo, toRetry, conId)
this._onXenRemove(objects, xapiIdsToXo, toRetry, conId, dependents)
}
const xapisByPool = this._xapisByPool
@@ -277,7 +305,7 @@ export default class {
const addObject = object => {
// TODO: optimize.
onAddOrUpdate({ [object.$id]: object })
return xapiObjectToXo(object)
return xapiObjectToXo(object, dependents)
}
return {

View File

@@ -1,7 +1,7 @@
{
"private": false,
"name": "xo-web",
"version": "5.29.2",
"version": "5.29.3",
"license": "AGPL-3.0",
"description": "Web interface client for Xen-Orchestra",
"keywords": [
@@ -133,10 +133,10 @@
"value-matcher": "^0.2.0",
"vinyl": "^2.1.0",
"watchify": "^3.7.0",
"whatwg-fetch": "^2.0.3",
"whatwg-fetch": "^3.0.0",
"xml2js": "^0.4.19",
"xo-acl-resolver": "^0.3.0",
"xo-common": "^0.1.2",
"xo-common": "^0.2.0",
"xo-lib": "^0.8.0",
"xo-remote-parser": "^0.5.0",
"xo-vmdk-to-vhd": "^0.1.5"

View File

@@ -15,16 +15,14 @@ export { Ellipsis as default }
export const EllipsisContainer = ({ children }) => (
<div style={ellipsisContainerStyle}>
{React.Children.map(
children,
child =>
child == null ||
child.type === Ellipsis ||
(child.type != null && child.type.originalRender === Ellipsis) ? (
child
) : (
<span>{child}</span>
)
{React.Children.map(children, child =>
child == null ||
child.type === Ellipsis ||
(child.type != null && child.type.originalRender === Ellipsis) ? (
child
) : (
<span>{child}</span>
)
)}
</div>
)

View File

@@ -26,7 +26,10 @@ export default class Toggle extends Component {
_toggle = () => {
const { props } = this
props.onChange(!props.value)
if (!props.disabled) {
props.onChange(!props.value)
}
}
render () {

View File

@@ -88,6 +88,7 @@ const messages = {
xosan: 'XOSAN',
backupDeprecatedMessage:
'Warning: Backup is deprecated, use Backup NG instead.',
moveRestoreLegacyMessage: 'Warning: Your legacy backups can be found here',
backupMigrationLink: 'How to migrate to Backup NG',
backupNgNewPage: 'Create a new backup with Backup NG',
backupOverviewPage: 'Overview',
@@ -418,6 +419,8 @@ const messages = {
'Tip: using a thin-provisioned storage will consume less space. Please click on the icon to get more information',
vmsOnThinProvisionedSrTip:
'Tip: creating VMs on a thin-provisioned storage will consume less space when backuping them. Please click on the icon to get more information',
deltaBackupOnOutdatedXenServerWarning:
'Delta Backup and Continuous Replication require at least XenServer 6.5.',
localRemoteWarningMessage:
'Warning: local remotes will use limited XOA disk space. Only for advanced users.',
backupVersionWarning:
@@ -1020,6 +1023,10 @@ const messages = {
// ----- VM advanced tab -----
vmRemoveButton: 'Remove',
vmConvertToTemplateButton: 'Convert to template',
vmSwitchVirtualizationMode: 'Convert to {mode}',
vmVirtualizationModeModalTitle: 'Change virtualization mode',
vmVirtualizationModeModalBody:
"You must know what you are doing, because it could break your setup (if you didn't install the bootloader in the MBR while switching from PV to HVM, or even worse, in HVM to PV, if you don't have the correct PV args)",
vmShareButton: 'Share',
xenSettingsLabel: 'Xen settings',
guestOsLabel: 'Guest OS',
@@ -1036,6 +1043,7 @@ const messages = {
osKernel: 'OS kernel',
autoPowerOn: 'Auto power on',
ha: 'HA',
nestedVirt: 'Nested virtualization',
vmAffinityHost: 'Affinity host',
vmVga: 'VGA',
vmVideoram: 'Video RAM',
@@ -1342,7 +1350,6 @@ const messages = {
remoteError: 'Error',
remoteErrorMessage:
'The URL ({url}) is invalid (colon in path). Click this button to change the URL to {newUrl}.',
noBackup: 'No backup available',
backupVmNameColumn: 'VM Name',
backupVmDescriptionColumn: 'VM Description',
backupTags: 'Tags',
@@ -1360,6 +1367,8 @@ const messages = {
importBackupMessage: 'Starting your backup import',
vmsToBackup: 'VMs to backup',
restoreResfreshList: 'Refresh backup list',
restoreLegacy: 'Legacy restore',
restoreFileLegacy: 'Legacy file restore',
restoreVmBackups: 'Restore',
restoreVmBackupsTitle: 'Restore {vm}',
restoreVmBackupsBulkTitle:
@@ -1395,6 +1404,7 @@ const messages = {
restoreFilesSelectFiles: 'Select a file…',
restoreFileContentNotFound: 'Content not found',
restoreFilesNoFilesSelected: 'No files selected',
restoreFilesSelectedFiles: 'Selected files ({files}):',
restoreFilesSelectedFilesAndFolders: 'Selected files/folders ({files}):',
restoreFilesDiskError: 'Error while scanning disk',
restoreFilesSelectAllFiles: "Select all this folder's files",
@@ -1806,6 +1816,9 @@ const messages = {
logsJobName: 'Job name',
logsJobTime: 'Job time',
logsVmNotFound: 'VM not found!',
logsMissingVms: 'Missing VMs skipped ({ vms })',
logsFailedRestoreError: 'Click to show error',
logsFailedRestoreTitle: 'Restore error',
logDeleteMultiple: 'Delete log{nLogs, plural, one {} other {s}}',
logDeleteMultipleMessage:
'Are you sure you want to delete {nLogs, number} log{nLogs, plural, one {} other {s}}?',

View File

@@ -107,17 +107,16 @@ export default class IsoDevice extends Component {
icon='vm-eject'
/>
</span>
{mountedIso &&
!cdDrive.device && (
<Tooltip content={_('cdDriveNotInstalled')}>
<a
className='text-warning btn btn-link'
onClick={this._showWarning}
>
<Icon icon='alarm' size='lg' />
</a>
</Tooltip>
)}
{mountedIso && !cdDrive.device && (
<Tooltip content={_('cdDriveNotInstalled')}>
<a
className='text-warning btn btn-link'
onClick={this._showWarning}
>
<Icon icon='alarm' size='lg' />
</a>
</Tooltip>
)}
</div>
)
}

View File

@@ -13,7 +13,7 @@ import decorate from './apply-decorators'
import getEventValue from './get-event-value'
import Icon from './icon'
import Tooltip from './tooltip'
import { generateRandomId } from './utils'
import { generateId } from './reaclette-utils'
import {
disable as disableShortcuts,
enable as enableShortcuts,
@@ -182,6 +182,32 @@ class StrongConfirm extends Component {
}
}
_confirm = () => {
this.props.resolve()
instance.close()
}
_handleKeyDown = event => {
if (event.keyCode === 13 && !this.state.buttons[0].disabled) {
this._confirm()
}
}
_focusAndAddEventListener = ref => {
if (ref !== null) {
// When the modal is triggered by a react-bootstrap Dropdown, the Dropdown takes the focus back
// https://github.com/vatesfr/react-bootstrap/blob/bootstrap-4/src/Dropdown.js#L63-L85
// FIXME: remove the setTimeout workaround when react-bootstrap-4 is removed
// See https://github.com/react-bootstrap/react-bootstrap/issues/2553#issuecomment-324356126
setTimeout(() => {
ref.focus()
})
ref.addEventListener('keydown', this._handleKeyDown)
this.componentWillUnmount = () =>
ref.removeEventListener('keydown', this._handleKeyDown)
}
}
render () {
const {
body,
@@ -209,9 +235,7 @@ class StrongConfirm extends Component {
<div>
<input
className='form-control'
ref={ref => {
ref && ref.focus()
}}
ref={this._focusAndAddEventListener}
onChange={this._onInputChange}
/>
</div>
@@ -338,7 +362,7 @@ export const FormModal = decorate([
},
},
computed: {
formId: generateRandomId,
formId: generateId,
},
}),
injectState,

View File

@@ -1,3 +1,10 @@
// this computed can be used to generate a random id for the lifetime of the
// component
export const generateId = () =>
`i${Math.random()
.toString(36)
.slice(2)}`
// TODO: remove these functions once the PR: https://github.com/JsCommunity/reaclette/pull/5 has been merged
// It only supports native inputs
export const linkState = (_, { target }) => () => ({

View File

@@ -384,9 +384,8 @@ const GenericXoItem = connectStore(() => {
return (state, props) => ({
xoItem: getObject(state, props),
})
})(
({ xoItem, ...props }) =>
xoItem ? renderXoItem(xoItem, props) : renderXoUnknownItem()
})(({ xoItem, ...props }) =>
xoItem ? renderXoItem(xoItem, props) : renderXoUnknownItem()
)
export const renderXoItemFromId = (id, props) => (

View File

@@ -5,7 +5,7 @@ import React from 'react'
import ActionButton from './action-button'
import ActionRowButton from './action-row-button'
export const CAN_REPORT_BUG = process.env.XOA_PLAN > 1
export const CAN_REPORT_BUG = __DEV__ && process.env.XOA_PLAN > 1
export const reportBug = ({ formatMessage, message, title }) => {
const encodedTitle = encodeURIComponent(title)

View File

@@ -281,8 +281,8 @@ const TimePicker = decorate([
step === 1
? optionsValues
: step !== undefined
? optionsValues.filter((_, i) => i % step === 0)
: value.split(',').map(Number),
? optionsValues.filter((_, i) => i % step === 0)
: value.split(',').map(Number),
// '*' => 1
// '*/2' => 2
@@ -291,8 +291,8 @@ const TimePicker = decorate([
value === '*'
? 1
: value.indexOf('/') === 1
? +value.split('/')[1]
: undefined,
? +value.split('/')[1]
: undefined,
},
}),
injectState,

View File

@@ -63,8 +63,8 @@ const getIds = value =>
value == null || isString(value) || isInteger(value)
? value
: isArray(value)
? map(value, getIds)
: value.id
? map(value, getIds)
: value.id
const getOption = (object, container) => ({
label: container
@@ -162,11 +162,11 @@ class GenericSelect extends React.Component {
return isEmpty(missingObjects)
? objects
: withContainers
? {
...objects,
missingObjects,
}
: [...objects, ...missingObjects]
? {
...objects,
missingObjects,
}
: [...objects, ...missingObjects]
}
)
@@ -620,13 +620,10 @@ export const SelectVdi = makeStoreSelect(
)
const getVdis = createGetObjectsOfType('VDI')
.filter(
createSelector(
getSrs,
getPredicate,
(srs, predicate) =>
predicate
? vdi => srs[vdi.$SR] && predicate(vdi)
: vdi => srs[vdi.$SR]
createSelector(getSrs, getPredicate, (srs, predicate) =>
predicate
? vdi => srs[vdi.$SR] && predicate(vdi)
: vdi => srs[vdi.$SR]
)
)
.sort()
@@ -691,9 +688,9 @@ export const SelectSubject = makeSubscriptionSelect(
const set = newSubjects => {
subjects = newSubjects
/* We must wait for groups AND users options to be loaded,
* or a previously setted value belonging to one type or another might be discarded
* by the internal <GenericSelect>
*/
* or a previously setted value belonging to one type or another might be discarded
* by the internal <GenericSelect>
*/
if (usersLoaded && groupsLoaded) {
subscriber({
xoObjects: subjects,

View File

@@ -1,5 +1,5 @@
import add from 'lodash/add'
import checkPermissions from 'xo-acl-resolver'
import { check as checkPermissions } from 'xo-acl-resolver'
import { createSelector as create } from 'reselect'
import {
filter,
@@ -146,15 +146,14 @@ export const createFilter = (collection, predicate) =>
_create2(
collection,
predicate,
_createCollectionWrapper(
(collection, predicate) =>
predicate === false
? isArrayLike(collection)
? EMPTY_ARRAY
: EMPTY_OBJECT
: predicate
? (isArrayLike(collection) ? filter : pickBy)(collection, predicate)
: collection
_createCollectionWrapper((collection, predicate) =>
predicate === false
? isArrayLike(collection)
? EMPTY_ARRAY
: EMPTY_OBJECT
: predicate
? (isArrayLike(collection) ? filter : pickBy)(collection, predicate)
: collection
)
)

View File

@@ -19,6 +19,7 @@ import {
findIndex,
forEach,
get as getProperty,
isArray,
isEmpty,
isFunction,
map,
@@ -211,28 +212,27 @@ const actionsShape = PropTypes.arrayOf(
})
)
const IndividualAction = decorate([
const Action = decorate([
provideState({
computed: {
disabled: ({ item }, { disabled, userData }) =>
isFunction(disabled) ? disabled(item, userData) : disabled,
handler: ({ item }, { handler, userData }) => () =>
handler(item, userData),
icon: ({ item }, { icon, userData }) =>
isFunction(icon) ? icon(item, userData) : icon,
item: (_, { item, grouped }) => (grouped ? [item] : item),
label: ({ item }, { label, userData }) =>
isFunction(label) ? label(item, userData) : label,
level: ({ item }, { level, userData }) =>
isFunction(level) ? level(item, userData) : level,
disabled: ({ items }, { disabled, userData }) =>
isFunction(disabled) ? disabled(items, userData) : disabled,
handler: ({ items }, { handler, userData }) => () =>
handler(items, userData),
icon: ({ items }, { icon, userData }) =>
isFunction(icon) ? icon(items, userData) : icon,
items: (_, { items, grouped }) =>
isArray(items) || !grouped ? items : [items],
label: ({ items }, { label, userData }) =>
isFunction(label) ? label(items, userData) : label,
level: ({ items }, { level, userData }) =>
isFunction(level) ? level(items, userData) : level,
},
}),
injectState,
({ state, redirectOnSuccess, userData }) => (
<ActionRowButton
btnStyle={state.level}
data-item={state.item}
data-userData={userData}
disabled={state.disabled}
handler={state.handler}
icon={state.icon}
@@ -242,42 +242,6 @@ const IndividualAction = decorate([
),
])
class GroupedAction extends Component {
_getIsDisabled = createSelector(
() => this.props.disabled,
() => this.props.selectedItems,
() => this.props.userData,
(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
return p.handler(p.selectedItems, p.userData)
}
render () {
const { icon, level } = this.props
return (
<ActionRowButton
btnStyle={level}
disabled={this._getIsDisabled()}
handler={this._executeAction}
icon={icon}
tooltip={this._getLabel()}
/>
)
}
}
const LEVELS = [undefined, 'primary', 'warning', 'danger']
// page number and sort info are optional for backward compatibility
const URL_STATE_RE = /^(?:(\d+)(?:_(\d+)(_desc)?)?-)?(.*)$/
@@ -502,8 +466,8 @@ export default class SortedTable extends Component {
) {
this.setState({
highlighted:
(itemIndex + visibleItems.length + 1) %
visibleItems.length || 0,
(itemIndex + visibleItems.length + 1) % visibleItems.length ||
0,
})
}
break
@@ -515,8 +479,8 @@ export default class SortedTable extends Component {
) {
this.setState({
highlighted:
(itemIndex + visibleItems.length - 1) %
visibleItems.length || 0,
(itemIndex + visibleItems.length - 1) % visibleItems.length ||
0,
})
}
break
@@ -792,12 +756,7 @@ export default class SortedTable extends Component {
<div className='pull-right'>
<ButtonGroup>
{map(this._getIndividualActions(), (props, key) => (
<IndividualAction
{...props}
item={item}
key={key}
userData={userData}
/>
<Action {...props} items={item} key={key} userData={userData} />
))}
</ButtonGroup>
</div>
@@ -931,10 +890,10 @@ export default class SortedTable extends Component {
<div className='pull-right'>
<ButtonGroup>
{map(groupedActions, (props, key) => (
<GroupedAction
<Action
{...props}
key={key}
selectedItems={this._getSelectedItems()}
items={this._getSelectedItems()}
userData={userData}
/>
))}

View File

@@ -47,6 +47,17 @@ export addSubscriptions from './add-subscriptions'
// ===================================================================
export const getVirtualizationModeLabel = vm => {
const virtualizationMode =
vm.virtualizationMode === 'hvm' && Boolean(vm.xenTools)
? 'pvhvm'
: vm.virtualizationMode
return VIRTUALIZATION_MODE_LABEL[virtualizationMode]
}
// ===================================================================
export const ensureArray = value => {
if (value === undefined) {
return []

View File

@@ -149,7 +149,7 @@ class XoWeekChart extends Component {
.selectAll('path')
.remove()
forEach(splittedData, data => {
;svg
svg
.select('.horizon-area')
.append('path')
.datum(data)

View File

@@ -63,6 +63,7 @@ export default class ChooseSrForEachVdisModal extends Component {
onChange={this._onChangeMainSr}
placeholder={_('chooseSrForEachVdisModalMainSr')}
predicate={mainSrPredicate}
required
value={mainSr}
/>
<br />

View File

@@ -193,7 +193,8 @@ export const resolveUrl = invoke(
// -------------------------------------------------------------------
const createSubscription = cb => {
const delay = 5e3
const delay = 5e3 // 5s
const clearCacheDelay = 6e5 // 10m
const subscribers = Object.create(null)
let cache
@@ -203,61 +204,69 @@ const createSubscription = cb => {
let running = false
const uninstall = () => {
clearTimeout(timeout)
const clearCache = () => {
cache = undefined
}
const uninstall = () => {
clearTimeout(timeout)
timeout = setTimeout(clearCache, clearCacheDelay)
}
const loop = () => {
clearTimeout(timeout)
if (running) {
return
}
running = true
_signIn.then(() => cb()).then(
result => {
running = false
_signIn
.then(() => cb())
.then(
result => {
running = false
if (n === 0) {
return uninstall()
if (n === 0) {
return uninstall()
}
timeout = setTimeout(loop, delay)
if (!isEqual(result, cache)) {
cache = result
forEach(subscribers, subscriber => {
// A subscriber might have disappeared during iteration.
//
// E.g.: if a subscriber triggers the subscription of another.
if (subscriber) {
subscriber(result)
}
})
}
},
error => {
running = false
if (n === 0) {
return uninstall()
}
console.error(error)
}
timeout = setTimeout(loop, delay)
if (!isEqual(result, cache)) {
cache = result
forEach(subscribers, subscriber => {
// A subscriber might have disappeared during iteration.
//
// E.g.: if a subscriber triggers the subscription of another.
if (subscriber) {
subscriber(result)
}
})
}
},
error => {
running = false
if (n === 0) {
return uninstall()
}
console.error(error)
}
)
)
}
const subscribe = cb => {
const id = nextId++
subscribers[id] = cb
if (n++ !== 0) {
if (cache !== undefined) {
asap(() => cb(cache))
}
} else {
if (cache !== undefined) {
asap(() => cb(cache))
}
if (n++ === 0) {
loop()
}
@@ -272,7 +281,6 @@ const createSubscription = cb => {
subscribe.forceRefresh = () => {
if (n) {
clearTimeout(timeout)
loop()
}
}
@@ -1035,6 +1043,16 @@ export const convertVmToTemplate = vm =>
),
}).then(() => _call('vm.convert', { id: resolveId(vm) }), noop)
export const changeVirtualizationMode = vm =>
confirm({
title: _('vmVirtualizationModeModalTitle'),
body: _('vmVirtualizationModeModalBody'),
}).then(() =>
editVm(vm, {
virtualizationMode: vm.virtualizationMode === 'hvm' ? 'pv' : 'hvm',
})
)
export const deleteTemplates = templates =>
confirm({
title: _('templateDeleteModalTitle', { templates: templates.length }),
@@ -1198,17 +1216,17 @@ export const deleteVm = (vm, retryWithForce = true) =>
})
.then(() => _call('vm.delete', { id: resolveId(vm) }), noop)
.catch(error => {
if (forbiddenOperation.is(error) || !retryWithForce) {
throw error
if (retryWithForce && forbiddenOperation.is(error)) {
return confirm({
title: _('deleteVmBlockedModalTitle'),
body: _('deleteVmBlockedModalMessage'),
}).then(
() => _call('vm.delete', { id: resolveId(vm), force: true }),
noop
)
}
return confirm({
title: _('deleteVmBlockedModalTitle'),
body: _('deleteVmBlockedModalMessage'),
}).then(
() => _call('vm.delete', { id: resolveId(vm), force: true }),
noop
)
throw error
})
export const deleteVms = vms =>

View File

@@ -73,12 +73,11 @@ export default class InstallPoolPatchesModalBody extends Component {
return (
<Container>
{!needDefaultSr &&
!someCdsInserted && (
<SingleLineRow>
<Col>{_('confirmPoolPatch')}</Col>
</SingleLineRow>
)}
{!needDefaultSr && !someCdsInserted && (
<SingleLineRow>
<Col>{_('confirmPoolPatch')}</Col>
</SingleLineRow>
)}
{needDefaultSr && [
<SingleLineRow className='mt-1' key='message'>
<Col>

View File

@@ -69,6 +69,8 @@ export default class MigrateVmModalBody extends BaseComponent {
this.state = {
mapVifsNetworks: {},
targetSrs: {},
host: props.host || undefined,
intraPool: props.host ? props.vm.$pool === props.host.$pool : undefined,
}
this._getHostPredicate = createSelector(
@@ -119,10 +121,6 @@ export default class MigrateVmModalBody extends BaseComponent {
)
}
componentDidMount () {
this._selectHost(this.props.host)
}
get value () {
return {
mapVdisSrs: resolveIds(this.state.targetSrs.mapVdisSrs),
@@ -231,6 +229,7 @@ export default class MigrateVmModalBody extends BaseComponent {
<SelectHost
onChange={this._selectHost}
predicate={this._getHostPredicate()}
required
value={host}
/>
</Col>
@@ -244,6 +243,7 @@ export default class MigrateVmModalBody extends BaseComponent {
<ChooseSrForEachVdisModal
mainSrPredicate={this._getSrPredicate()}
onChange={this.linkState('targetSrs')}
required
value={targetSrs}
vdis={vdis}
/>

View File

@@ -287,70 +287,66 @@ export default class MigrateVmsModalBody extends BaseComponent {
</SingleLineRow>
</div>
)}
{host &&
(!intraPool || !noVdisMigration) && (
<div key='sr' style={LINE_STYLE}>
<SingleLineRow>
<Col size={6}>
{!intraPool
? _('migrateVmsSelectSr')
: _('migrateVmsSelectSrIntraPool')}{' '}
{(defaultSrId === undefined || !defaultSrConnectedToHost) && (
<Tooltip
content={
defaultSrId !== undefined
? _('migrateVmNotConnectedDefaultSrError')
: _('migrateVmNoDefaultSrError')
{host && (!intraPool || !noVdisMigration) && (
<div key='sr' style={LINE_STYLE}>
<SingleLineRow>
<Col size={6}>
{!intraPool
? _('migrateVmsSelectSr')
: _('migrateVmsSelectSrIntraPool')}{' '}
{(defaultSrId === undefined || !defaultSrConnectedToHost) && (
<Tooltip
content={
defaultSrId !== undefined
? _('migrateVmNotConnectedDefaultSrError')
: _('migrateVmNoDefaultSrError')
}
>
<Icon
icon={defaultSrId !== undefined ? 'alarm' : 'info'}
className={
defaultSrId !== undefined ? 'text-warning' : 'text-info'
}
>
<Icon
icon={defaultSrId !== undefined ? 'alarm' : 'info'}
className={
defaultSrId !== undefined
? 'text-warning'
: 'text-info'
}
size='lg'
/>
</Tooltip>
)}
</Col>
<Col size={6}>
<SelectSr
onChange={this._selectSr}
predicate={this._getSrPredicate()}
value={srId}
/>
</Col>
</SingleLineRow>
</div>
)}
{host &&
!intraPool && (
<div key='network' style={LINE_STYLE}>
<SingleLineRow>
<Col size={6}>{_('migrateVmsSelectNetwork')}</Col>
<Col size={6}>
<SelectNetwork
disabled={smartVifMapping}
onChange={this._selectNetwork}
predicate={this._getTargetNetworkPredicate()}
value={networkId}
/>
</Col>
</SingleLineRow>
<SingleLineRow>
<Col size={6} offset={6}>
<input
type='checkbox'
onChange={this._toggleSmartVifMapping}
checked={smartVifMapping}
/>{' '}
{_('migrateVmsSmartMapping')}
</Col>
</SingleLineRow>
</div>
)}
size='lg'
/>
</Tooltip>
)}
</Col>
<Col size={6}>
<SelectSr
onChange={this._selectSr}
predicate={this._getSrPredicate()}
value={srId}
/>
</Col>
</SingleLineRow>
</div>
)}
{host && !intraPool && (
<div key='network' style={LINE_STYLE}>
<SingleLineRow>
<Col size={6}>{_('migrateVmsSelectNetwork')}</Col>
<Col size={6}>
<SelectNetwork
disabled={smartVifMapping}
onChange={this._selectNetwork}
predicate={this._getTargetNetworkPredicate()}
value={networkId}
/>
</Col>
</SingleLineRow>
<SingleLineRow>
<Col size={6} offset={6}>
<input
type='checkbox'
onChange={this._toggleSmartVifMapping}
checked={smartVifMapping}
/>{' '}
{_('migrateVmsSmartMapping')}
</Col>
</SingleLineRow>
</div>
)}
</div>
)
}

View File

@@ -10,37 +10,36 @@ import { isAdmin } from 'selectors'
const Upgrade = connectStore({
isAdmin,
})(
({ available, children, isAdmin, place, required = available }) =>
process.env.XOA_PLAN < required ? (
<Card>
<CardHeader>{_('upgradeNeeded')}</CardHeader>
{isAdmin ? (
<CardBlock className='text-xs-center'>
<p>{_('availableIn', { plan: getXoaPlan(required) })}</p>
<p>
<a
href={`https://xen-orchestra.com/#!/pricing?pk_campaign=xoa_${getXoaPlan()}_upgrade&pk_kwd=${place}`}
className='btn btn-primary btn-lg'
>
<Icon icon='plan-upgrade' /> {_('upgradeNow')}
</a>{' '}
{_('or')}
&nbsp;
<Link className='btn btn-success btn-lg' to='/xoa/update'>
<Icon icon='plan-trial' /> {_('tryIt')}
</Link>
</p>
</CardBlock>
) : (
<CardBlock className='text-xs-center'>
<p>{_('notAvailable')}</p>
</CardBlock>
)}
</Card>
) : (
children
)
})(({ available, children, isAdmin, place, required = available }) =>
process.env.XOA_PLAN < required ? (
<Card>
<CardHeader>{_('upgradeNeeded')}</CardHeader>
{isAdmin ? (
<CardBlock className='text-xs-center'>
<p>{_('availableIn', { plan: getXoaPlan(required) })}</p>
<p>
<a
href={`https://xen-orchestra.com/#!/pricing?pk_campaign=xoa_${getXoaPlan()}_upgrade&pk_kwd=${place}`}
className='btn btn-primary btn-lg'
>
<Icon icon='plan-upgrade' /> {_('upgradeNow')}
</a>{' '}
{_('or')}
&nbsp;
<Link className='btn btn-success btn-lg' to='/xoa/update'>
<Icon icon='plan-trial' /> {_('tryIt')}
</Link>
</p>
</CardBlock>
) : (
<CardBlock className='text-xs-center'>
<p>{_('notAvailable')}</p>
</CardBlock>
)}
</Card>
) : (
children
)
)
Upgrade.propTypes = {

View File

@@ -3,15 +3,14 @@ import Component from 'base-component'
import Icon from 'icon'
import React from 'react'
import SortedTable from 'sorted-table'
import Upgrade from 'xoa-upgrade'
import { confirm } from 'modal'
import { addSubscriptions, noop } from 'utils'
import { Container, Row, Col } from 'grid'
import { error } from 'notification'
import { FormattedDate } from 'react-intl'
import {
find,
filter,
find,
forEach,
groupBy,
isEmpty,
@@ -23,7 +22,6 @@ import {
import { fetchFiles, listRemoteBackups, subscribeRemotes } from 'xo'
import RestoreFileModalBody from './restore-file-modal'
import styles from './index.css'
const VM_COLUMNS = [
{
@@ -127,50 +125,20 @@ export default class FileRestore extends Component {
render () {
const { backupInfoByVm } = this.props
if (!backupInfoByVm) {
return <h2>{_('statusLoading')}</h2>
}
return !isEmpty(backupInfoByVm) ? (
<div>
<h3>{_('restoreFileLegacy')}</h3>
<em>
<Icon icon='info' /> {_('restoreBackupsInfo')}
</em>
return process.env.XOA_PLAN > 3 ? (
<Container>
<h2>{_('restoreFiles')}</h2>
{isEmpty(backupInfoByVm) ? (
<div>
<em>
<Icon icon='info' /> {_('restoreDeltaBackupsInfo')}
</em>
<div>
<a>{_('noBackup')}</a>
</div>
</div>
) : (
<div>
<ul className={styles.listRestoreBackupInfos}>
<li>
<em>
<Icon icon='info' /> {_('restoreBackupsInfo')}
</em>
</li>
<li>
<em>
<Icon icon='info' /> {_('restoreDeltaBackupsInfo')}
</em>
</li>
</ul>
<SortedTable
collection={backupInfoByVm}
columns={VM_COLUMNS}
rowAction={openImportModal}
defaultColumn={2}
/>
</div>
)}
</Container>
) : (
<Container>
<Upgrade place='restoreFiles' available={4} />
</Container>
)
<SortedTable
collection={backupInfoByVm}
columns={VM_COLUMNS}
rowAction={openImportModal}
defaultColumn={2}
/>
</div>
) : null
}
}

Some files were not shown because too many files have changed in this diff Show More