Compare commits
135 Commits
xo-web-v5.
...
xo-common-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
273f208722 | ||
|
|
c01e8e892e | ||
|
|
9dfd81c28f | ||
|
|
5dd26ebe33 | ||
|
|
4c0fe3c14f | ||
|
|
2353581da8 | ||
|
|
2934b23d2f | ||
|
|
82e4197237 | ||
|
|
a23189f132 | ||
|
|
47fa1ec81e | ||
|
|
4b468663f3 | ||
|
|
6628dc777d | ||
|
|
3ef3ae0166 | ||
|
|
bc6dbe2771 | ||
|
|
5651160d1c | ||
|
|
6da2669c6f | ||
|
|
8094b5097f | ||
|
|
bdb0547b86 | ||
|
|
ea08fbbfba | ||
|
|
b4cbd8b2b5 | ||
|
|
f8fbb6b7d3 | ||
|
|
c8da9fec0a | ||
|
|
79fb3ec8bd | ||
|
|
2243966ce1 | ||
|
|
ca7d520997 | ||
|
|
df44487363 | ||
|
|
b39eb0f60d | ||
|
|
a3dcdc4fd5 | ||
|
|
2daac73c17 | ||
|
|
23eb3c3094 | ||
|
|
776d0f9e4a | ||
|
|
54bdcc6dd2 | ||
|
|
38084c8199 | ||
|
|
4525ee7491 | ||
|
|
66a476bd21 | ||
|
|
be6cc12632 | ||
|
|
673475dcb2 | ||
|
|
7dc1a80a83 | ||
|
|
d49294849f | ||
|
|
6b394302c1 | ||
|
|
00e1601f85 | ||
|
|
b75e746586 | ||
|
|
32a9fa9bb0 | ||
|
|
79d68dece4 | ||
|
|
1701e1d4ba | ||
|
|
497b3eb296 | ||
|
|
ecfafa0fea | ||
|
|
def66d8218 | ||
|
|
eeb08abec2 | ||
|
|
90923c657d | ||
|
|
4ff6eeb424 | ||
|
|
2d98fb40f1 | ||
|
|
256a58ded2 | ||
|
|
bf3b31a9ef | ||
|
|
7fc8d59605 | ||
|
|
1a39b2113a | ||
|
|
cb9f3fbb2c | ||
|
|
487f413cdd | ||
|
|
f847969206 | ||
|
|
5d9aad44c2 | ||
|
|
ba2027e6d7 | ||
|
|
087da9376f | ||
|
|
218e3b46e0 | ||
|
|
f9921e354e | ||
|
|
341148a7d3 | ||
|
|
7216165f1e | ||
|
|
a9557af04b | ||
|
|
abb80270ad | ||
|
|
72e93384a5 | ||
|
|
663b1b76ec | ||
|
|
24b8c671fa | ||
|
|
986fec1cd3 | ||
|
|
f6c2cbc5cf | ||
|
|
289ed89a78 | ||
|
|
73de421d47 | ||
|
|
dc1eb82295 | ||
|
|
6629c12166 | ||
|
|
ec5bc1db95 | ||
|
|
ac2c40c842 | ||
|
|
61bf669252 | ||
|
|
4105c53155 | ||
|
|
873db3bf26 | ||
|
|
c795887a35 | ||
|
|
23824bafe8 | ||
|
|
5cca58f2b3 | ||
|
|
d05c9b6133 | ||
|
|
39a84a1ac0 | ||
|
|
b1c851c9d6 | ||
|
|
6280a9365c | ||
|
|
2741dacd64 | ||
|
|
4c2c2390bd | ||
|
|
635b8ce5f0 | ||
|
|
efc13cc456 | ||
|
|
078f319fe1 | ||
|
|
0f0e785871 | ||
|
|
4e4c85121c | ||
|
|
019d6f4cb6 | ||
|
|
725b0342d1 | ||
|
|
c93ccb8111 | ||
|
|
670befdaf6 | ||
|
|
55eefd865f | ||
|
|
43e5d610e3 | ||
|
|
b1245bc5be | ||
|
|
c2feab245e | ||
|
|
cb3753213e | ||
|
|
ec8c7a24af | ||
|
|
2456be2da3 | ||
|
|
8c5d4240f9 | ||
|
|
b1e12d1542 | ||
|
|
a58d7d2ff4 | ||
|
|
5308b8b9ed | ||
|
|
c15dffce8f | ||
|
|
874680462e | ||
|
|
bb42540775 | ||
|
|
b18511c905 | ||
|
|
5c660f4f64 | ||
|
|
f2bae73f77 | ||
|
|
e54d34f269 | ||
|
|
6470cbd2ee | ||
|
|
c06ebcb4a4 | ||
|
|
3eaa72c98c | ||
|
|
694fff060d | ||
|
|
2705062ac3 | ||
|
|
3df055a296 | ||
|
|
802bc15e0c | ||
|
|
ad2de40a9d | ||
|
|
19298570f8 | ||
|
|
1da4d1f1e9 | ||
|
|
fe4e9c18fa | ||
|
|
2c9f84f17f | ||
|
|
0b2e76600b | ||
|
|
873554fc01 | ||
|
|
82e2d013ae | ||
|
|
1eb5e80f1f | ||
|
|
9c0ab5b3cb |
@@ -4,6 +4,7 @@ module.exports = {
|
||||
__DEV__: true,
|
||||
$Dict: true,
|
||||
$Diff: true,
|
||||
$ElementType: true,
|
||||
$Exact: true,
|
||||
$Keys: true,
|
||||
$PropertyType: true,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -15,6 +15,6 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"golike-defer": "^0.4.1",
|
||||
"xen-api": "^0.20.0"
|
||||
"xen-api": "^0.21.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,8 +50,8 @@ class Schedule {
|
||||
zone.toLowerCase() === 'utc'
|
||||
? moment.utc
|
||||
: zone === 'local'
|
||||
? moment
|
||||
: () => moment.tz(zone)
|
||||
? moment
|
||||
: () => moment.tz(zone)
|
||||
}
|
||||
|
||||
createJob (fn) {
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// @flow
|
||||
|
||||
// $FlowFixMe
|
||||
import getStream from 'get-stream'
|
||||
import { randomBytes } from 'crypto'
|
||||
import { fromCallback, fromEvent, ignoreErrors, timeout } from 'promise-toolbox'
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// @flow
|
||||
|
||||
// $FlowFixMe
|
||||
import through2 from 'through2'
|
||||
import { createHash } from 'crypto'
|
||||
import { defer, fromEvent } from 'promise-toolbox'
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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),
|
||||
},
|
||||
|
||||
42
CHANGELOG.md
42
CHANGELOG.md
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 = ''
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
3
packages/xapi-explore-sr/.babelrc.js
Normal file
3
packages/xapi-explore-sr/.babelrc.js
Normal file
@@ -0,0 +1,3 @@
|
||||
module.exports = require('../../@xen-orchestra/babel-config')(
|
||||
require('./package.json')
|
||||
)
|
||||
24
packages/xapi-explore-sr/.npmignore
Normal file
24
packages/xapi-explore-sr/.npmignore
Normal 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__/
|
||||
52
packages/xapi-explore-sr/README.md
Normal file
52
packages/xapi-explore-sr/README.md
Normal file
@@ -0,0 +1,52 @@
|
||||
# xapi-explore-sr [](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)
|
||||
60
packages/xapi-explore-sr/package.json
Normal file
60
packages/xapi-explore-sr/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
161
packages/xapi-explore-sr/src/index.js
Executable file
161
packages/xapi-explore-sr/src/index.js
Executable 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,
|
||||
})
|
||||
})
|
||||
@@ -4,6 +4,9 @@
|
||||
|
||||
Tested with:
|
||||
|
||||
- XenServer 7.6
|
||||
- XenServer 7.5
|
||||
- XenServer 7.4
|
||||
- XenServer 7.3
|
||||
- XenServer 7.2
|
||||
- XenServer 7.1
|
||||
|
||||
@@ -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",
|
||||
|
||||
17
packages/xen-api/src/_replaceSensitiveValues.js
Normal file
17
packages/xen-api/src/_replaceSensitiveValues.js
Normal 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)
|
||||
}
|
||||
@@ -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
|
||||
})
|
||||
},
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,5 +21,8 @@
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
},
|
||||
"dependencies": {
|
||||
"xo-common": "^0.2.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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": [],
|
||||
|
||||
@@ -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, () => ({
|
||||
|
||||
10
packages/xo-import-servers-csv/.npmignore
Normal file
10
packages/xo-import-servers-csv/.npmignore
Normal file
@@ -0,0 +1,10 @@
|
||||
/examples/
|
||||
example.js
|
||||
example.js.map
|
||||
*.example.js
|
||||
*.example.js.map
|
||||
|
||||
/test/
|
||||
/tests/
|
||||
*.spec.js
|
||||
*.spec.js.map
|
||||
64
packages/xo-import-servers-csv/README.md
Normal file
64
packages/xo-import-servers-csv/README.md
Normal file
@@ -0,0 +1,64 @@
|
||||
# xo-import-servers-csv [](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)
|
||||
59
packages/xo-import-servers-csv/package.json
Normal file
59
packages/xo-import-servers-csv/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
23
packages/xo-import-servers-csv/src/index.d.ts
vendored
Normal file
23
packages/xo-import-servers-csv/src/index.d.ts
vendored
Normal 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>
|
||||
}
|
||||
}
|
||||
87
packages/xo-import-servers-csv/src/index.ts
Executable file
87
packages/xo-import-servers-csv/src/index.ts
Executable 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)
|
||||
}
|
||||
}
|
||||
)
|
||||
14
packages/xo-import-servers-csv/tsconfig.json
Normal file
14
packages/xo-import-servers-csv/tsconfig.json
Normal 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/**/*"
|
||||
}
|
||||
3
packages/xo-import-servers-csv/tslint.json
Normal file
3
packages/xo-import-servers-csv/tslint.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "tslint-config-standard"
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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}`,
|
||||
},
|
||||
|
||||
@@ -463,7 +463,6 @@ ${monitorBodies.join('\n')}`
|
||||
try {
|
||||
const result = {
|
||||
uuid,
|
||||
name: definition.name,
|
||||
object: this._xo.getXapi(uuid).getObject(uuid),
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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'))
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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'
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
@@ -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'
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
})
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
43
packages/xo-server/src/sensitive-values.js
Normal file
43
packages/xo-server/src/sensitive-values.js
Normal 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)
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
// @flow
|
||||
|
||||
// $FlowFixMe
|
||||
import through2 from 'through2'
|
||||
import { type Readable } from 'stream'
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
)})`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 })
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -44,7 +44,7 @@ export default class LevelDbLogger extends AbstractLogger {
|
||||
return promise.then(() => key)
|
||||
}
|
||||
|
||||
;promise::ignoreErrors()
|
||||
promise::ignoreErrors()
|
||||
return key
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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 || '',
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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 () {
|
||||
|
||||
@@ -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}}?',
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 }) => () => ({
|
||||
|
||||
@@ -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) => (
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -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 []
|
||||
|
||||
@@ -149,7 +149,7 @@ class XoWeekChart extends Component {
|
||||
.selectAll('path')
|
||||
.remove()
|
||||
forEach(splittedData, data => {
|
||||
;svg
|
||||
svg
|
||||
.select('.horizon-area')
|
||||
.append('path')
|
||||
.datum(data)
|
||||
|
||||
@@ -63,6 +63,7 @@ export default class ChooseSrForEachVdisModal extends Component {
|
||||
onChange={this._onChangeMainSr}
|
||||
placeholder={_('chooseSrForEachVdisModalMainSr')}
|
||||
predicate={mainSrPredicate}
|
||||
required
|
||||
value={mainSr}
|
||||
/>
|
||||
<br />
|
||||
|
||||
@@ -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 =>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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')}
|
||||
|
||||
<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')}
|
||||
|
||||
<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 = {
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user