Compare commits
33 Commits
xo-server-
...
xen-api-v0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4535c81b44 | ||
|
|
f39312e789 | ||
|
|
6aad769995 | ||
|
|
b1acbaecc2 | ||
|
|
6d61e8efff | ||
|
|
482e6b3cb3 | ||
|
|
116af372dc | ||
|
|
970952783c | ||
|
|
e59cf13456 | ||
|
|
d0cfddce19 | ||
|
|
30b2a8dd8d | ||
|
|
b811ee7e7e | ||
|
|
ebe7f6784a | ||
|
|
e40792378f | ||
|
|
cc9c8fb891 | ||
|
|
ca06c4d403 | ||
|
|
c8aa058ede | ||
|
|
34169d685e | ||
|
|
d5a9d36815 | ||
|
|
c7aaeca530 | ||
|
|
863e4f0c19 | ||
|
|
0226e0553d | ||
|
|
02995d278f | ||
|
|
78a2104bcc | ||
|
|
0811e5c765 | ||
|
|
29024888fb | ||
|
|
dbcaab2bc1 | ||
|
|
28d445ae1c | ||
|
|
530360f859 | ||
|
|
738c55bad0 | ||
|
|
4b09bc85f5 | ||
|
|
5bc67d3570 | ||
|
|
f7ae6222b7 |
@@ -16,7 +16,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"golike-defer": "^0.4.1",
|
||||
"xen-api": "^0.25.2"
|
||||
"xen-api": "^0.27.0"
|
||||
},
|
||||
"scripts": {
|
||||
"postversion": "npm publish"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@xen-orchestra/fs",
|
||||
"version": "0.9.0",
|
||||
"version": "0.10.0",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "The File System for Xen Orchestra backups.",
|
||||
"keywords": [],
|
||||
@@ -28,8 +28,9 @@
|
||||
"execa": "^1.0.0",
|
||||
"fs-extra": "^8.0.1",
|
||||
"get-stream": "^4.0.0",
|
||||
"limit-concurrency-decorator": "^0.4.0",
|
||||
"lodash": "^4.17.4",
|
||||
"promise-toolbox": "^0.12.1",
|
||||
"promise-toolbox": "^0.13.0",
|
||||
"readable-stream": "^3.0.6",
|
||||
"through2": "^3.0.0",
|
||||
"tmp": "^0.1.0",
|
||||
@@ -40,6 +41,7 @@
|
||||
"@babel/core": "^7.0.0",
|
||||
"@babel/plugin-proposal-decorators": "^7.1.6",
|
||||
"@babel/plugin-proposal-function-bind": "^7.0.0",
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.4.4",
|
||||
"@babel/preset-env": "^7.0.0",
|
||||
"@babel/preset-flow": "^7.0.0",
|
||||
"async-iterator-to-stream": "^1.1.0",
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import getStream from 'get-stream'
|
||||
|
||||
import asyncMap from '@xen-orchestra/async-map'
|
||||
import limit from 'limit-concurrency-decorator'
|
||||
import path from 'path'
|
||||
import synchronized from 'decorator-synchronized'
|
||||
import { fromCallback, fromEvent, ignoreErrors, timeout } from 'promise-toolbox'
|
||||
@@ -31,6 +32,7 @@ const computeRate = (hrtime: number[], size: number) => {
|
||||
}
|
||||
|
||||
const DEFAULT_TIMEOUT = 6e5 // 10 min
|
||||
const DEFAULT_MAX_PARALLEL_OPERATIONS = 10
|
||||
|
||||
const ignoreEnoent = error => {
|
||||
if (error == null || error.code !== 'ENOENT') {
|
||||
@@ -83,6 +85,25 @@ export default class RemoteHandlerAbstract {
|
||||
}
|
||||
}
|
||||
;({ timeout: this._timeout = DEFAULT_TIMEOUT } = options)
|
||||
|
||||
const sharedLimit = limit(
|
||||
options.maxParallelOperations ?? DEFAULT_MAX_PARALLEL_OPERATIONS
|
||||
)
|
||||
this.closeFile = sharedLimit(this.closeFile)
|
||||
this.getInfo = sharedLimit(this.getInfo)
|
||||
this.getSize = sharedLimit(this.getSize)
|
||||
this.list = sharedLimit(this.list)
|
||||
this.mkdir = sharedLimit(this.mkdir)
|
||||
this.openFile = sharedLimit(this.openFile)
|
||||
this.outputFile = sharedLimit(this.outputFile)
|
||||
this.read = sharedLimit(this.read)
|
||||
this.readFile = sharedLimit(this.readFile)
|
||||
this.rename = sharedLimit(this.rename)
|
||||
this.rmdir = sharedLimit(this.rmdir)
|
||||
this.truncate = sharedLimit(this.truncate)
|
||||
this.unlink = sharedLimit(this.unlink)
|
||||
this.write = sharedLimit(this.write)
|
||||
this.writeFile = sharedLimit(this.writeFile)
|
||||
}
|
||||
|
||||
// Public members
|
||||
|
||||
@@ -24,6 +24,19 @@ log.info('this information is relevant to the user')
|
||||
log.warn('something went wrong but did not prevent current action')
|
||||
log.error('something went wrong')
|
||||
log.fatal('service/app is going down')
|
||||
|
||||
// you can add contextual info
|
||||
log.debug('new API request', {
|
||||
method: 'foo',
|
||||
params: [ 'bar', 'baz' ]
|
||||
user: 'qux'
|
||||
})
|
||||
|
||||
// by convention, errors go into the `error` field
|
||||
log.error('could not join server', {
|
||||
error,
|
||||
server: 'example.org',
|
||||
})
|
||||
```
|
||||
|
||||
Then, at application level, configure the logs are handled:
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"lodash": "^4.17.4",
|
||||
"promise-toolbox": "^0.12.1"
|
||||
"promise-toolbox": "^0.13.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.0.0",
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import LEVELS, { NAMES } from '../levels'
|
||||
|
||||
// Bind console methods (necessary for browsers)
|
||||
/* eslint-disable no-console */
|
||||
const debugConsole = console.log.bind(console)
|
||||
const infoConsole = console.info.bind(console)
|
||||
const warnConsole = console.warn.bind(console)
|
||||
const errorConsole = console.error.bind(console)
|
||||
/* eslint-enable no-console */
|
||||
|
||||
const { ERROR, INFO, WARN } = LEVELS
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import fromCallback from 'promise-toolbox/fromCallback'
|
||||
import splitHost from 'split-host' // eslint-disable-line node/no-extraneous-import node/no-missing-import
|
||||
import startsWith from 'lodash/startsWith'
|
||||
import { createClient, Facility, Severity, Transport } from 'syslog-client' // eslint-disable-line node/no-extraneous-import node/no-missing-import
|
||||
import splitHost from 'split-host'
|
||||
import { createClient, Facility, Severity, Transport } from 'syslog-client'
|
||||
|
||||
import LEVELS from '../levels'
|
||||
|
||||
@@ -19,10 +18,10 @@ const facility = Facility.User
|
||||
export default target => {
|
||||
const opts = {}
|
||||
if (target !== undefined) {
|
||||
if (startsWith(target, 'tcp://')) {
|
||||
if (target.startsWith('tcp://')) {
|
||||
target = target.slice(6)
|
||||
opts.transport = Transport.Tcp
|
||||
} else if (startsWith(target, 'udp://')) {
|
||||
} else if (target.startsWith('udp://')) {
|
||||
target = target.slice(6)
|
||||
opts.transport = Transport.Udp
|
||||
}
|
||||
|
||||
41
CHANGELOG.md
41
CHANGELOG.md
@@ -4,21 +4,52 @@
|
||||
|
||||
### Enhancements
|
||||
|
||||
### Bug fixes
|
||||
|
||||
### Released packages
|
||||
|
||||
## **5.36.0** (2019-06-27)
|
||||
|
||||

|
||||
|
||||
### Highlights
|
||||
|
||||
- [SR/new] Create ZFS storage [#4260](https://github.com/vatesfr/xen-orchestra/issues/4260) (PR [#4266](https://github.com/vatesfr/xen-orchestra/pull/4266))
|
||||
- [Host/advanced] Fix host CPU hyperthreading detection [#4262](https://github.com/vatesfr/xen-orchestra/issues/4262) (PR [#4285](https://github.com/vatesfr/xen-orchestra/pull/4285))
|
||||
- [VM/Advanced] Ability to use UEFI instead of BIOS [#4264](https://github.com/vatesfr/xen-orchestra/issues/4264) (PR [#4268](https://github.com/vatesfr/xen-orchestra/pull/4268))
|
||||
- [Backup-ng/restore] Display size for full VM backup [#4009](https://github.com/vatesfr/xen-orchestra/issues/4009) (PR [#4245](https://github.com/vatesfr/xen-orchestra/pull/4245))
|
||||
- [Sr/new] Ability to select NFS version when creating NFS storage [#3951](https://github.com/vatesfr/xen-orchestra/issues/3951) (PR [#4277](https://github.com/vatesfr/xen-orchestra/pull/4277))
|
||||
- [Host/storages, SR/hosts] Display PBD details [#4264](https://github.com/vatesfr/xen-orchestra/issues/4161) (PR [#4268](https://github.com/vatesfr/xen-orchestra/pull/4284))
|
||||
- [auth-saml] Improve compatibility with Microsoft Azure Active Directory (PR [#4294](https://github.com/vatesfr/xen-orchestra/pull/4294))
|
||||
|
||||
### Enhancements
|
||||
|
||||
- [Host] Display warning when "Citrix Hypervisor" license has restrictions [#4251](https://github.com/vatesfr/xen-orchestra/issues/4164) (PR [#4235](https://github.com/vatesfr/xen-orchestra/pull/4279))
|
||||
- [VM/Backup] Create backup bulk action [#2573](https://github.com/vatesfr/xen-orchestra/issues/2573) (PR [#4257](https://github.com/vatesfr/xen-orchestra/pull/4257))
|
||||
- [Host] Display warning when host's time differs too much from XOA's time [#4113](https://github.com/vatesfr/xen-orchestra/issues/4113) (PR [#4173](https://github.com/vatesfr/xen-orchestra/pull/4173))
|
||||
- [VM/network] Display and set bandwidth rate-limit of a VIF [#4215](https://github.com/vatesfr/xen-orchestra/issues/4215) (PR [#4293](https://github.com/vatesfr/xen-orchestra/pull/4293))
|
||||
- [SDN Controller] New plugin which enables creating pool-wide private networks [xcp-ng/xcp#175](https://github.com/xcp-ng/xcp/issues/175) (PR [#4269](https://github.com/vatesfr/xen-orchestra/pull/4269))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- [XOA] Don't require editing the _email_ field in case of re-registration (PR [#4259](https://github.com/vatesfr/xen-orchestra/pull/4259))
|
||||
- [Metadata backup] Missing XAPIs should trigger a failure job [#4281](https://github.com/vatesfr/xen-orchestra/issues/4281) (PR [#4283](https://github.com/vatesfr/xen-orchestra/pull/4283))
|
||||
- [iSCSI] Fix fibre channel paths display [#4291](https://github.com/vatesfr/xen-orchestra/issues/4291) (PR [#4303](https://github.com/vatesfr/xen-orchestra/pull/4303))
|
||||
- [New VM] Fix tooltips not displayed on disabled elements in some browsers (e.g. Google Chrome) [#4304](https://github.com/vatesfr/xen-orchestra/issues/4304) (PR [#4309](https://github.com/vatesfr/xen-orchestra/pull/4309))
|
||||
|
||||
### Released packages
|
||||
|
||||
- xen-api v0.25.2
|
||||
- xo-server v5.43.0
|
||||
- xo-web v5.43.0
|
||||
- xo-server-auth-ldap v0.6.5
|
||||
- xen-api v0.26.0
|
||||
- xo-server-sdn-controller v0.1
|
||||
- xo-server-auth-saml v0.6.0
|
||||
- xo-server-backup-reports v0.16.2
|
||||
- xo-server v5.44.0
|
||||
- xo-web v5.44.0
|
||||
|
||||
## **5.35.0** (2019-05-29)
|
||||
|
||||

|
||||

|
||||
|
||||
### Enhancements
|
||||
|
||||
@@ -53,8 +84,6 @@
|
||||
|
||||
## **5.34.0** (2019-04-30)
|
||||
|
||||

|
||||
|
||||
### Highlights
|
||||
|
||||
- [Self/New VM] Add network config box to custom cloud-init [#3872](https://github.com/vatesfr/xen-orchestra/issues/3872) (PR [#4150](https://github.com/vatesfr/xen-orchestra/pull/4150))
|
||||
|
||||
@@ -1,32 +1,37 @@
|
||||
> This file contains all changes that have not been released yet.
|
||||
>
|
||||
> Keep in mind the changelog is addressed to **users** and should be
|
||||
> understandable by them.
|
||||
|
||||
### Enhancements
|
||||
|
||||
- [Backup-ng/restore] Display size for full VM backup [#4009](https://github.com/vatesfr/xen-orchestra/issues/4009) (PR [#4245](https://github.com/vatesfr/xen-orchestra/pull/4245))
|
||||
- [Sr/new] Ability to select NFS version when creating NFS storage [#3951](https://github.com/vatesfr/xen-orchestra/issues/3951) (PR [#4277](https://github.com/vatesfr/xen-orchestra/pull/4277))
|
||||
- [auth-saml] Improve compatibility with Microsoft Azure Active Directory (PR [#4294](https://github.com/vatesfr/xen-orchestra/pull/4294))
|
||||
- [Host] Display warning when "Citrix Hypervisor" license has restrictions [#4251](https://github.com/vatesfr/xen-orchestra/issues/4164) (PR [#4235](https://github.com/vatesfr/xen-orchestra/pull/4279))
|
||||
- [VM/Backup] Create backup bulk action [#2573](https://github.com/vatesfr/xen-orchestra/issues/2573) (PR [#4257](https://github.com/vatesfr/xen-orchestra/pull/4257))
|
||||
- [Sr/new] Ability to select NFS version when creating NFS storage [#3951](https://github.com/vatesfr/xen-orchestra/issues/#3951) (PR [#4277](https://github.com/vatesfr/xen-orchestra/pull/4277))
|
||||
- [SR/new] Create ZFS storage [#4260](https://github.com/vatesfr/xen-orchestra/issues/4260) (PR [#4266](https://github.com/vatesfr/xen-orchestra/pull/4266))
|
||||
- [Host] Display warning when host's time differs too much from XOA's time [#4113](https://github.com/vatesfr/xen-orchestra/issues/4113) (PR [#4173](https://github.com/vatesfr/xen-orchestra/pull/4173))
|
||||
- [Host/storages, SR/hosts] Display PBD details [#4264](https://github.com/vatesfr/xen-orchestra/issues/4161) (PR [#4268](https://github.com/vatesfr/xen-orchestra/pull/4284))
|
||||
- [VM/network] Display and set bandwidth rate-limit of a VIF [#4215](https://github.com/vatesfr/xen-orchestra/issues/4215) (PR [#4293](https://github.com/vatesfr/xen-orchestra/pull/4293))
|
||||
- [SDN Controller] New plugin which enables creating pool-wide private networks [xcp-ng/xcp#175](https://github.com/xcp-ng/xcp/issues/175) (PR [#4269](https://github.com/vatesfr/xen-orchestra/pull/4269))
|
||||
> Users must be able to say: “Nice enhancement, I'm eager to test it”
|
||||
|
||||
- [Stats] Ability to display last day stats [#4160](https://github.com/vatesfr/xen-orchestra/issues/4160) (PR [#4168](https://github.com/vatesfr/xen-orchestra/pull/4168))
|
||||
- [Settings/servers] Display servers connection issues [#4300](https://github.com/vatesfr/xen-orchestra/issues/4300) (PR [#4310](https://github.com/vatesfr/xen-orchestra/pull/4310))
|
||||
- [VM] Permission to revert to any snapshot for VM operators [#3928](https://github.com/vatesfr/xen-orchestra/issues/3928) (PR [#4247](https://github.com/vatesfr/xen-orchestra/pull/4247))
|
||||
- [VM] Show current operations and progress [#3811](https://github.com/vatesfr/xen-orchestra/issues/3811) (PR [#3982](https://github.com/vatesfr/xen-orchestra/pull/3982))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- [Metadata backup] Missing XAPIs should trigger a failure job [#4281](https://github.com/vatesfr/xen-orchestra/issues/4281) (PR [#4283](https://github.com/vatesfr/xen-orchestra/pull/4283))
|
||||
- [Host/advanced] Fix host CPU hyperthreading detection [#4262](https://github.com/vatesfr/xen-orchestra/issues/4262) (PR [#4285](https://github.com/vatesfr/xen-orchestra/pull/4285))
|
||||
- [iSCSI] Fix fibre channel paths display [#4291](https://github.com/vatesfr/xen-orchestra/issues/4291) (PR [#4303](https://github.com/vatesfr/xen-orchestra/pull/4303))
|
||||
- [New VM] Fix tooltips not displayed on disabled elements in some browsers (e.g. Google Chrome) [#4304](https://github.com/vatesfr/xen-orchestra/issues/4304) (PR [#4309](https://github.com/vatesfr/xen-orchestra/pull/4309))
|
||||
> Users must be able to say: “I had this issue, happy to know it's fixed”
|
||||
|
||||
- [Settings/Servers] Fix read-only setting toggling
|
||||
- [SDN Controller] Do not choose physical PIF without IP configuration for tunnels. (PR [#4319](https://github.com/vatesfr/xen-orchestra/pull/4319))
|
||||
- [Xen servers] Fix `no connection found for object` error if pool master is reinstalled [#4299](https://github.com/vatesfr/xen-orchestra/issues/4299) (PR [#4302](https://github.com/vatesfr/xen-orchestra/pull/4302))
|
||||
- [Backup-ng/restore] Display correct size for full VM backup [#4316](https://github.com/vatesfr/xen-orchestra/issues/4316) (PR [#4332](https://github.com/vatesfr/xen-orchestra/pull/4332))
|
||||
- [VM/tab-advanced] Fix CPU limits edition (PR [#4337](https://github.com/vatesfr/xen-orchestra/pull/4337))
|
||||
- [Remotes] Fix `EIO` errors due to massive parallel fs operations [#4323](https://github.com/vatesfr/xen-orchestra/issues/4323) (PR [#4330](https://github.com/vatesfr/xen-orchestra/pull/4330))
|
||||
|
||||
### Released packages
|
||||
|
||||
- xo-server-auth-ldap v0.6.5
|
||||
> Packages will be released in the order they are here, therefore, they should
|
||||
> be listed by inverse order of dependency.
|
||||
>
|
||||
> Rule of thumb: add packages on top.
|
||||
|
||||
- @xen-orchestra/fs v0.10.0
|
||||
- xo-server-sdn-controller v0.1.1
|
||||
- xen-api v0.26.0
|
||||
- xo-server-sdn-controller v0.1
|
||||
- xo-server-auth-saml v0.6.0
|
||||
- xo-server-backup-reports v0.16.2
|
||||
- xo-server v5.44.0
|
||||
- xo-web v5.44.0
|
||||
- xo-server v5.45.0
|
||||
- xo-web v5.45.0
|
||||
|
||||
12
package.json
12
package.json
@@ -6,8 +6,8 @@
|
||||
"babel-eslint": "^10.0.1",
|
||||
"babel-jest": "^24.1.0",
|
||||
"benchmark": "^2.1.4",
|
||||
"eslint": "^5.1.0",
|
||||
"eslint-config-prettier": "^4.1.0",
|
||||
"eslint": "^6.0.1",
|
||||
"eslint-config-prettier": "^6.0.0",
|
||||
"eslint-config-standard": "12.0.0",
|
||||
"eslint-config-standard-jsx": "^6.0.2",
|
||||
"eslint-plugin-eslint-comments": "^3.1.1",
|
||||
@@ -17,13 +17,13 @@
|
||||
"eslint-plugin-react": "^7.6.1",
|
||||
"eslint-plugin-standard": "^4.0.0",
|
||||
"exec-promise": "^0.7.0",
|
||||
"flow-bin": "^0.100.0",
|
||||
"globby": "^9.0.0",
|
||||
"husky": "^2.2.0",
|
||||
"flow-bin": "^0.102.0",
|
||||
"globby": "^10.0.0",
|
||||
"husky": "^3.0.0",
|
||||
"jest": "^24.1.0",
|
||||
"lodash": "^4.17.4",
|
||||
"prettier": "^1.10.2",
|
||||
"promise-toolbox": "^0.12.1",
|
||||
"promise-toolbox": "^0.13.0",
|
||||
"sorted-object": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
"node": ">=6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@xen-orchestra/fs": "^0.9.0",
|
||||
"@xen-orchestra/fs": "^0.10.0",
|
||||
"cli-progress": "^2.0.0",
|
||||
"exec-promise": "^0.7.0",
|
||||
"getopts": "^2.2.3",
|
||||
@@ -40,9 +40,9 @@
|
||||
"@babel/preset-env": "^7.0.0",
|
||||
"babel-plugin-lodash": "^3.3.2",
|
||||
"cross-env": "^5.1.3",
|
||||
"execa": "^1.0.0",
|
||||
"execa": "^2.0.2",
|
||||
"index-modules": "^0.3.0",
|
||||
"promise-toolbox": "^0.12.1",
|
||||
"promise-toolbox": "^0.13.0",
|
||||
"rimraf": "^2.6.1",
|
||||
"tmp": "^0.1.0"
|
||||
},
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
"from2": "^2.3.0",
|
||||
"fs-extra": "^8.0.1",
|
||||
"limit-concurrency-decorator": "^0.4.0",
|
||||
"promise-toolbox": "^0.12.1",
|
||||
"promise-toolbox": "^0.13.0",
|
||||
"struct-fu": "^1.2.0",
|
||||
"uuid": "^3.0.1"
|
||||
},
|
||||
@@ -35,10 +35,10 @@
|
||||
"@babel/core": "^7.0.0",
|
||||
"@babel/preset-env": "^7.0.0",
|
||||
"@babel/preset-flow": "^7.0.0",
|
||||
"@xen-orchestra/fs": "^0.9.0",
|
||||
"@xen-orchestra/fs": "^0.10.0",
|
||||
"babel-plugin-lodash": "^3.3.2",
|
||||
"cross-env": "^5.1.3",
|
||||
"execa": "^1.0.0",
|
||||
"execa": "^2.0.2",
|
||||
"fs-promise": "^2.0.0",
|
||||
"get-stream": "^5.1.0",
|
||||
"index-modules": "^0.3.0",
|
||||
|
||||
@@ -364,9 +364,7 @@ export default class Vhd {
|
||||
const offset = blockAddr + this.sectorsOfBitmap + beginSectorId
|
||||
|
||||
debug(
|
||||
`writeBlockSectors at ${offset} block=${
|
||||
block.id
|
||||
}, sectors=${beginSectorId}...${endSectorId}`
|
||||
`writeBlockSectors at ${offset} block=${block.id}, sectors=${beginSectorId}...${endSectorId}`
|
||||
)
|
||||
|
||||
for (let i = beginSectorId; i < endSectorId; ++i) {
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
"human-format": "^0.10.0",
|
||||
"lodash": "^4.17.4",
|
||||
"pw": "^0.0.4",
|
||||
"xen-api": "^0.25.2"
|
||||
"xen-api": "^0.27.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.1.5",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "xen-api",
|
||||
"version": "0.25.2",
|
||||
"version": "0.27.0",
|
||||
"license": "ISC",
|
||||
"description": "Connector to the Xen API",
|
||||
"keywords": [
|
||||
@@ -46,7 +46,7 @@
|
||||
"make-error": "^1.3.0",
|
||||
"minimist": "^1.2.0",
|
||||
"ms": "^2.1.1",
|
||||
"promise-toolbox": "^0.12.1",
|
||||
"promise-toolbox": "^0.13.0",
|
||||
"pw": "0.0.4",
|
||||
"xmlrpc": "^1.3.2",
|
||||
"xo-collection": "^0.4.1"
|
||||
|
||||
@@ -99,6 +99,9 @@ export class Xapi extends EventEmitter {
|
||||
this._sessionId = undefined
|
||||
this._status = DISCONNECTED
|
||||
|
||||
this._watchEventsError = undefined
|
||||
this._lastEventFetchedTimestamp = undefined
|
||||
|
||||
this._debounce = opts.debounce ?? 200
|
||||
this._objects = new Collection()
|
||||
this._objectsByRef = { __proto__: null }
|
||||
@@ -479,6 +482,14 @@ export class Xapi extends EventEmitter {
|
||||
return this._objectsFetched
|
||||
}
|
||||
|
||||
get lastEventFetchedTimestamp() {
|
||||
return this._lastEventFetchedTimestamp
|
||||
}
|
||||
|
||||
get watchEventsError() {
|
||||
return this._watchEventsError
|
||||
}
|
||||
|
||||
// ensure we have received all events up to this call
|
||||
//
|
||||
// optionally returns the up to date object for the given ref
|
||||
@@ -954,6 +965,8 @@ export class Xapi extends EventEmitter {
|
||||
],
|
||||
EVENT_TIMEOUT * 1e3 * 1.1
|
||||
)
|
||||
this._lastEventFetchedTimestamp = Date.now()
|
||||
this._watchEventsError = undefined
|
||||
} catch (error) {
|
||||
const code = error?.code
|
||||
if (code === 'EVENTS_LOST' || code === 'SESSION_INVALID') {
|
||||
@@ -961,6 +974,7 @@ export class Xapi extends EventEmitter {
|
||||
continue mainLoop
|
||||
}
|
||||
|
||||
this._watchEventsError = error
|
||||
console.warn('_watchEvents', error)
|
||||
await pDelay(this._eventPollDelay)
|
||||
continue
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
"nice-pipe": "0.0.0",
|
||||
"pretty-ms": "^4.0.0",
|
||||
"progress-stream": "^2.0.0",
|
||||
"promise-toolbox": "^0.12.1",
|
||||
"promise-toolbox": "^0.13.0",
|
||||
"pump": "^3.0.0",
|
||||
"pw": "^0.0.4",
|
||||
"strip-indent": "^2.0.0",
|
||||
|
||||
@@ -24,7 +24,6 @@ const nicePipe = require('nice-pipe')
|
||||
const pairs = require('lodash/toPairs')
|
||||
const pick = require('lodash/pick')
|
||||
const pump = require('pump')
|
||||
const startsWith = require('lodash/startsWith')
|
||||
const prettyMs = require('pretty-ms')
|
||||
const progressStream = require('progress-stream')
|
||||
const pw = require('pw')
|
||||
@@ -81,7 +80,7 @@ function parseParameters(args) {
|
||||
const name = matches[1]
|
||||
let value = matches[2]
|
||||
|
||||
if (startsWith(value, 'json:')) {
|
||||
if (value.startsWith('json:')) {
|
||||
value = JSON.parse(value.slice(5))
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import JsonRpcWebSocketClient, { OPEN, CLOSED } from 'jsonrpc-websocket-client'
|
||||
import { BaseError } from 'make-error'
|
||||
import { startsWith } from 'lodash'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@@ -35,7 +34,7 @@ export default class Xo extends JsonRpcWebSocketClient {
|
||||
}
|
||||
|
||||
call(method, args, i) {
|
||||
if (startsWith(method, 'session.')) {
|
||||
if (method.startsWith('session.')) {
|
||||
return Promise.reject(
|
||||
new XoError('session.*() methods are disabled from this interface')
|
||||
)
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
"inquirer": "^6.0.0",
|
||||
"ldapjs": "^1.0.1",
|
||||
"lodash": "^4.17.4",
|
||||
"promise-toolbox": "^0.12.1"
|
||||
"promise-toolbox": "^0.13.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.0.0",
|
||||
|
||||
@@ -230,9 +230,7 @@ class AuthLdap {
|
||||
logger(`attempting to bind as ${entry.objectName}`)
|
||||
await bind(entry.objectName, password)
|
||||
logger(
|
||||
`successfully bound as ${
|
||||
entry.objectName
|
||||
} => ${username} authenticated`
|
||||
`successfully bound as ${entry.objectName} => ${username} authenticated`
|
||||
)
|
||||
logger(JSON.stringify(entry, null, 2))
|
||||
return { username }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "xo-server-auth-saml",
|
||||
"version": "0.5.3",
|
||||
"version": "0.6.0",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "SAML authentication plugin for XO-Server",
|
||||
"keywords": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "xo-server-backup-reports",
|
||||
"version": "0.16.1",
|
||||
"version": "0.16.2",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "Backup reports plugin for XO-Server",
|
||||
"keywords": [
|
||||
|
||||
@@ -357,9 +357,7 @@ class BackupReportsXoPlugin {
|
||||
nagiosStatus: log.status === 'success' ? 0 : 2,
|
||||
nagiosMarkdown:
|
||||
log.status === 'success'
|
||||
? `[Xen Orchestra] [Success] Metadata backup report for ${
|
||||
log.jobName
|
||||
}`
|
||||
? `[Xen Orchestra] [Success] Metadata backup report for ${log.jobName}`
|
||||
: `[Xen Orchestra] [${log.status}] Metadata backup report for ${
|
||||
log.jobName
|
||||
} - ${nagiosText.join(' ')}`,
|
||||
@@ -393,9 +391,7 @@ class BackupReportsXoPlugin {
|
||||
} − Backup report for ${jobName} ${STATUS_ICON[log.status]}`,
|
||||
markdown: toMarkdown(markdown),
|
||||
nagiosStatus: 2,
|
||||
nagiosMarkdown: `[Xen Orchestra] [${
|
||||
log.status
|
||||
}] Backup report for ${jobName} - Error : ${log.result.message}`,
|
||||
nagiosMarkdown: `[Xen Orchestra] [${log.status}] Backup report for ${jobName} - Error : ${log.result.message}`,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -713,9 +709,7 @@ class BackupReportsXoPlugin {
|
||||
subject: `[Xen Orchestra] ${globalStatus} ${icon}`,
|
||||
markdown,
|
||||
nagiosStatus: 2,
|
||||
nagiosMarkdown: `[Xen Orchestra] [${globalStatus}] Error : ${
|
||||
error.message
|
||||
}`,
|
||||
nagiosMarkdown: `[Xen Orchestra] [${globalStatus}] Error : ${error.message}`,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -189,9 +189,7 @@ export default class DensityPlan extends Plan {
|
||||
const { vm, destination } = move
|
||||
const xapiDest = this.xo.getXapi(destination)
|
||||
debug(
|
||||
`Migrate VM (${vm.id}) to Host (${destination.id}) from Host (${
|
||||
vm.$container
|
||||
}).`
|
||||
`Migrate VM (${vm.id}) to Host (${destination.id}) from Host (${vm.$container}).`
|
||||
)
|
||||
return xapiDest.migrateVm(
|
||||
vm._xapiId,
|
||||
|
||||
@@ -126,9 +126,7 @@ export default class PerformancePlan extends Plan {
|
||||
destinationAverages.memoryFree -= vmAverages.memory
|
||||
|
||||
debug(
|
||||
`Migrate VM (${vm.id}) to Host (${destination.id}) from Host (${
|
||||
exceededHost.id
|
||||
}).`
|
||||
`Migrate VM (${vm.id}) to Host (${destination.id}) from Host (${exceededHost.id}).`
|
||||
)
|
||||
optimizationsCount++
|
||||
|
||||
@@ -143,9 +141,7 @@ export default class PerformancePlan extends Plan {
|
||||
|
||||
await Promise.all(promises)
|
||||
debug(
|
||||
`Performance mode: ${optimizationsCount} optimizations for Host (${
|
||||
exceededHost.id
|
||||
}).`
|
||||
`Performance mode: ${optimizationsCount} optimizations for Host (${exceededHost.id}).`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,9 +183,7 @@ export const configurationSchema = {
|
||||
description: Object.keys(HOST_FUNCTIONS)
|
||||
.map(
|
||||
k =>
|
||||
` * ${k} (${HOST_FUNCTIONS[k].unit}): ${
|
||||
HOST_FUNCTIONS[k].description
|
||||
}`
|
||||
` * ${k} (${HOST_FUNCTIONS[k].unit}): ${HOST_FUNCTIONS[k].description}`
|
||||
)
|
||||
.join('\n'),
|
||||
type: 'string',
|
||||
@@ -233,9 +231,7 @@ export const configurationSchema = {
|
||||
description: Object.keys(VM_FUNCTIONS)
|
||||
.map(
|
||||
k =>
|
||||
` * ${k} (${VM_FUNCTIONS[k].unit}): ${
|
||||
VM_FUNCTIONS[k].description
|
||||
}`
|
||||
` * ${k} (${VM_FUNCTIONS[k].unit}): ${VM_FUNCTIONS[k].description}`
|
||||
)
|
||||
.join('\n'),
|
||||
type: 'string',
|
||||
@@ -284,9 +280,7 @@ export const configurationSchema = {
|
||||
description: Object.keys(SR_FUNCTIONS)
|
||||
.map(
|
||||
k =>
|
||||
` * ${k} (${SR_FUNCTIONS[k].unit}): ${
|
||||
SR_FUNCTIONS[k].description
|
||||
}`
|
||||
` * ${k} (${SR_FUNCTIONS[k].unit}): ${SR_FUNCTIONS[k].description}`
|
||||
)
|
||||
.join('\n'),
|
||||
type: 'string',
|
||||
@@ -414,9 +408,7 @@ ${monitorBodies.join('\n')}`
|
||||
}
|
||||
|
||||
_parseDefinition(definition) {
|
||||
const alarmId = `${definition.objectType}|${definition.variableName}|${
|
||||
definition.alarmTriggerLevel
|
||||
}`
|
||||
const alarmId = `${definition.objectType}|${definition.variableName}|${definition.alarmTriggerLevel}`
|
||||
const typeFunction =
|
||||
TYPE_FUNCTION_MAP[definition.objectType][definition.variableName]
|
||||
const parseData = (result, uuid) => {
|
||||
@@ -468,9 +460,7 @@ ${monitorBodies.join('\n')}`
|
||||
...definition,
|
||||
alarmId,
|
||||
vmFunction: typeFunction,
|
||||
title: `${typeFunction.name} ${definition.comparator} ${
|
||||
definition.alarmTriggerLevel
|
||||
}${typeFunction.unit}`,
|
||||
title: `${typeFunction.name} ${definition.comparator} ${definition.alarmTriggerLevel}${typeFunction.unit}`,
|
||||
snapshot: async () => {
|
||||
return Promise.all(
|
||||
map(definition.uuids, async uuid => {
|
||||
@@ -664,9 +654,7 @@ ${entry.listItem}
|
||||
subject: `[Xen Orchestra] − Performance Alert ${subjectSuffix}`,
|
||||
markdown:
|
||||
markdownBody +
|
||||
`\n\n\nSent from Xen Orchestra [perf-alert plugin](${
|
||||
this._configuration.baseUrl
|
||||
}#/settings/plugins)\n`,
|
||||
`\n\n\nSent from Xen Orchestra [perf-alert plugin](${this._configuration.baseUrl}#/settings/plugins)\n`,
|
||||
})
|
||||
} else {
|
||||
throw new Error('The email alert system has a configuration issue.')
|
||||
|
||||
@@ -15,20 +15,21 @@
|
||||
"predev": "yarn run prebuild",
|
||||
"prepublishOnly": "yarn run build"
|
||||
},
|
||||
"version": "0.0.0",
|
||||
"version": "0.1.1",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.4.4",
|
||||
"@babel/core": "^7.4.4",
|
||||
"@babel/plugin-proposal-optional-chaining": "^7.2.0",
|
||||
"@babel/preset-env": "^7.4.4",
|
||||
"cross-env": "^5.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@xen-orchestra/log": "^0.1.4",
|
||||
"lodash": "^4.17.11",
|
||||
"node-openssl-cert": "^0.0.81",
|
||||
"node-openssl-cert": "^0.0.84",
|
||||
"promise-toolbox": "^0.13.0"
|
||||
},
|
||||
"private": true
|
||||
|
||||
@@ -137,8 +137,7 @@ class SDNController extends EventEmitter {
|
||||
client.updateCertificates(this._clientKey, this._clientCert, this._caCert)
|
||||
})
|
||||
const updatedPools = []
|
||||
for (let i = 0; i < this._poolNetworks.length; ++i) {
|
||||
const poolNetwork = this._poolNetworks[i]
|
||||
for (const poolNetwork of this._poolNetworks) {
|
||||
if (updatedPools.includes(poolNetwork.pool)) {
|
||||
continue
|
||||
}
|
||||
@@ -186,15 +185,13 @@ class SDNController extends EventEmitter {
|
||||
forOwn(networks, async network => {
|
||||
if (network.other_config.private_pool_wide === 'true') {
|
||||
log.debug(
|
||||
`Adding network: '${network.name_label}' for pool: '${
|
||||
network.$pool.name_label
|
||||
}' to managed networks`
|
||||
`Adding network: '${network.name_label}' for pool: '${network.$pool.name_label}' to managed networks`
|
||||
)
|
||||
const center = await this._electNewCenter(network, true)
|
||||
this._poolNetworks.push({
|
||||
pool: network.$pool.$ref,
|
||||
network: network.$ref,
|
||||
starCenter: center ? center.$ref : null,
|
||||
starCenter: center?.$ref,
|
||||
})
|
||||
this._networks.set(network.$id, network.$ref)
|
||||
if (center != null) {
|
||||
@@ -246,9 +243,7 @@ class SDNController extends EventEmitter {
|
||||
const privateNetwork = await pool.$xapi._getOrWaitObject(privateNetworkRef)
|
||||
|
||||
log.info(
|
||||
`Private network '${
|
||||
privateNetwork.name_label
|
||||
}' has been created for pool '${pool.name_label}'`
|
||||
`Private network '${privateNetwork.name_label}' has been created for pool '${pool.name_label}'`
|
||||
)
|
||||
|
||||
// For each pool's host, create a tunnel to the private network
|
||||
@@ -264,7 +259,7 @@ class SDNController extends EventEmitter {
|
||||
this._poolNetworks.push({
|
||||
pool: pool.$ref,
|
||||
network: privateNetwork.$ref,
|
||||
starCenter: center ? center.$ref : null,
|
||||
starCenter: center?.$ref,
|
||||
encapsulation: encapsulation,
|
||||
})
|
||||
this._networks.set(privateNetwork.$id, privateNetwork.$ref)
|
||||
@@ -299,9 +294,7 @@ class SDNController extends EventEmitter {
|
||||
|
||||
if ($type === 'host') {
|
||||
log.debug(
|
||||
`New host: '${object.name_label}' in pool: '${
|
||||
object.$pool.name_label
|
||||
}'`
|
||||
`New host: '${object.name_label}' in pool: '${object.$pool.name_label}'`
|
||||
)
|
||||
|
||||
if (find(this._newHosts, { $ref: object.$ref }) == null) {
|
||||
@@ -342,11 +335,10 @@ class SDNController extends EventEmitter {
|
||||
const poolNetworks = filter(this._poolNetworks, {
|
||||
starCenter: starCenterRef,
|
||||
})
|
||||
for (let i = 0; i < poolNetworks.length; ++i) {
|
||||
const poolNetwork = poolNetworks[i]
|
||||
const network = await xapi._getOrWaitObject(poolNetwork.network)
|
||||
for (const poolNetwork of poolNetworks) {
|
||||
const network = xapi.getObjectByRef(poolNetwork.network)
|
||||
const newCenter = await this._electNewCenter(network, true)
|
||||
poolNetwork.starCenter = newCenter ? newCenter.$ref : null
|
||||
poolNetwork.starCenter = newCenter?.$ref
|
||||
if (newCenter != null) {
|
||||
this._starCenters.set(newCenter.$id, newCenter.$ref)
|
||||
}
|
||||
@@ -385,14 +377,10 @@ class SDNController extends EventEmitter {
|
||||
}
|
||||
|
||||
log.debug(
|
||||
`PIF: '${pif.device}' of network: '${
|
||||
pif.$network.name_label
|
||||
}' star-center host: '${
|
||||
pif.$host.name_label
|
||||
}' has been unplugged, electing a new host`
|
||||
`PIF: '${pif.device}' of network: '${pif.$network.name_label}' star-center host: '${pif.$host.name_label}' has been unplugged, electing a new host`
|
||||
)
|
||||
const newCenter = await this._electNewCenter(pif.$network, true)
|
||||
poolNetwork.starCenter = newCenter ? newCenter.$ref : null
|
||||
poolNetwork.starCenter = newCenter?.$ref
|
||||
this._starCenters.delete(pif.$host.$id)
|
||||
if (newCenter != null) {
|
||||
this._starCenters.set(newCenter.$id, newCenter.$ref)
|
||||
@@ -401,23 +389,17 @@ class SDNController extends EventEmitter {
|
||||
if (poolNetwork.starCenter == null) {
|
||||
const host = pif.$host
|
||||
log.debug(
|
||||
`First available host: '${
|
||||
host.name_label
|
||||
}' becomes star center of network: '${pif.$network.name_label}'`
|
||||
`First available host: '${host.name_label}' becomes star center of network: '${pif.$network.name_label}'`
|
||||
)
|
||||
poolNetwork.starCenter = pif.host
|
||||
this._starCenters.set(host.$id, host.$ref)
|
||||
}
|
||||
|
||||
log.debug(
|
||||
`PIF: '${pif.device}' of network: '${pif.$network.name_label}' host: '${
|
||||
pif.$host.name_label
|
||||
}' has been plugged`
|
||||
`PIF: '${pif.device}' of network: '${pif.$network.name_label}' host: '${pif.$host.name_label}' has been plugged`
|
||||
)
|
||||
|
||||
const starCenter = await pif.$xapi._getOrWaitObject(
|
||||
poolNetwork.starCenter
|
||||
)
|
||||
const starCenter = pif.$xapi.getObjectByRef(poolNetwork.starCenter)
|
||||
await this._addHostToNetwork(pif.$host, pif.$network, starCenter)
|
||||
}
|
||||
}
|
||||
@@ -438,15 +420,12 @@ class SDNController extends EventEmitter {
|
||||
await xapi.call('pool.certificate_sync')
|
||||
} catch (error) {
|
||||
log.error(
|
||||
`Couldn't sync SDN controller ca certificate in pool: '${
|
||||
host.$pool.name_label
|
||||
}' because: ${error}`
|
||||
`Couldn't sync SDN controller ca certificate in pool: '${host.$pool.name_label}' because: ${error}`
|
||||
)
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < tunnels.length; ++i) {
|
||||
const tunnel = tunnels[i]
|
||||
const accessPIF = await xapi._getOrWaitObject(tunnel.access_PIF)
|
||||
for (const tunnel of tunnels) {
|
||||
const accessPIF = xapi.getObjectByRef(tunnel.access_PIF)
|
||||
if (accessPIF.host !== host.$ref) {
|
||||
continue
|
||||
}
|
||||
@@ -463,40 +442,29 @@ class SDNController extends EventEmitter {
|
||||
}
|
||||
|
||||
log.debug(
|
||||
`Pluging PIF: '${accessPIF.device}' for host: '${
|
||||
host.name_label
|
||||
}' on network: '${accessPIF.$network.name_label}'`
|
||||
`Pluging PIF: '${accessPIF.device}' for host: '${host.name_label}' on network: '${accessPIF.$network.name_label}'`
|
||||
)
|
||||
try {
|
||||
await xapi.call('PIF.plug', accessPIF.$ref)
|
||||
} catch (error) {
|
||||
log.error(
|
||||
`XAPI error while pluging PIF: '${accessPIF.device}' on host: '${
|
||||
host.name_label
|
||||
}' for network: '${accessPIF.$network.name_label}'`
|
||||
`XAPI error while pluging PIF: '${accessPIF.device}' on host: '${host.name_label}' for network: '${accessPIF.$network.name_label}'`
|
||||
)
|
||||
}
|
||||
|
||||
const starCenter = await host.$xapi._getOrWaitObject(
|
||||
poolNetwork.starCenter
|
||||
)
|
||||
const starCenter = host.$xapi.getObjectByRef(poolNetwork.starCenter)
|
||||
await this._addHostToNetwork(host, accessPIF.$network, starCenter)
|
||||
}
|
||||
} else {
|
||||
const poolNetworks = filter(this._poolNetworks, { starCenter: host.$ref })
|
||||
for (let i = 0; i < poolNetworks.length; ++i) {
|
||||
const poolNetwork = poolNetworks[i]
|
||||
const network = await host.$xapi._getOrWaitObject(poolNetwork.network)
|
||||
for (const poolNetwork of poolNetworks) {
|
||||
const network = host.$xapi.getObjectByRef(poolNetwork.network)
|
||||
log.debug(
|
||||
`Star center host: '${host.name_label}' of network: '${
|
||||
network.name_label
|
||||
}' in pool: '${
|
||||
host.$pool.name_label
|
||||
}' is no longer reachable, electing a new host`
|
||||
`Star center host: '${host.name_label}' of network: '${network.name_label}' in pool: '${host.$pool.name_label}' is no longer reachable, electing a new host`
|
||||
)
|
||||
|
||||
const newCenter = await this._electNewCenter(network, true)
|
||||
poolNetwork.starCenter = newCenter ? newCenter.$ref : null
|
||||
poolNetwork.starCenter = newCenter?.$ref
|
||||
this._starCenters.delete(host.$id)
|
||||
if (newCenter != null) {
|
||||
this._starCenters.set(newCenter.$id, newCenter.$ref)
|
||||
@@ -545,9 +513,7 @@ class SDNController extends EventEmitter {
|
||||
} else if (this._overrideCerts) {
|
||||
await xapi.call('pool.certificate_uninstall', SDN_CONTROLLER_CERT)
|
||||
log.debug(
|
||||
`Old SDN Controller CA certificate uninstalled on pool: '${
|
||||
xapi.pool.name_label
|
||||
}'`
|
||||
`Old SDN Controller CA certificate uninstalled on pool: '${xapi.pool.name_label}'`
|
||||
)
|
||||
needInstall = true
|
||||
}
|
||||
@@ -568,15 +534,11 @@ class SDNController extends EventEmitter {
|
||||
)
|
||||
await xapi.call('pool.certificate_sync')
|
||||
log.debug(
|
||||
`SDN controller CA certificate install in pool: '${
|
||||
xapi.pool.name_label
|
||||
}'`
|
||||
`SDN controller CA certificate install in pool: '${xapi.pool.name_label}'`
|
||||
)
|
||||
} catch (error) {
|
||||
log.error(
|
||||
`Couldn't install SDN controller CA certificate in pool: '${
|
||||
xapi.pool.name_label
|
||||
}' because: ${error}`
|
||||
`Couldn't install SDN controller CA certificate in pool: '${xapi.pool.name_label}' because: ${error}`
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -598,9 +560,7 @@ class SDNController extends EventEmitter {
|
||||
await hostClient.resetForNetwork(network.uuid, network.name_label)
|
||||
} catch (error) {
|
||||
log.error(
|
||||
`Couldn't reset network: '${network.name_label}' for host: '${
|
||||
host.name_label
|
||||
}' in pool: '${network.$pool.name_label}' because: ${error}`
|
||||
`Couldn't reset network: '${network.name_label}' for host: '${host.name_label}' in pool: '${network.$pool.name_label}' because: ${error}`
|
||||
)
|
||||
return
|
||||
}
|
||||
@@ -620,11 +580,7 @@ class SDNController extends EventEmitter {
|
||||
|
||||
if (newCenter == null) {
|
||||
log.error(
|
||||
`Unable to elect a new star-center host to network: '${
|
||||
network.name_label
|
||||
}' for pool: '${
|
||||
network.$pool.name_label
|
||||
}' because there's no available host`
|
||||
`Unable to elect a new star-center host to network: '${network.name_label}' for pool: '${network.$pool.name_label}' because there's no available host`
|
||||
)
|
||||
return null
|
||||
}
|
||||
@@ -637,30 +593,26 @@ class SDNController extends EventEmitter {
|
||||
)
|
||||
|
||||
log.info(
|
||||
`New star center host elected: '${newCenter.name_label}' in network: '${
|
||||
network.name_label
|
||||
}'`
|
||||
`New star center host elected: '${newCenter.name_label}' in network: '${network.name_label}'`
|
||||
)
|
||||
|
||||
return newCenter
|
||||
}
|
||||
|
||||
async _createTunnel(host, network) {
|
||||
const pif = find(host.$PIFs, { physical: true })
|
||||
const pif = host.$PIFs.find(
|
||||
pif => pif.physical && pif.ip_configuration_mode !== 'None'
|
||||
)
|
||||
if (pif == null) {
|
||||
log.error(
|
||||
`No PIF found to create tunnel on host: '${
|
||||
host.name_label
|
||||
}' for network: '${network.name_label}'`
|
||||
`No PIF found to create tunnel on host: '${host.name_label}' for network: '${network.name_label}'`
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
await host.$xapi.call('tunnel.create', pif.$ref, network.$ref)
|
||||
log.debug(
|
||||
`Tunnel added on host '${host.name_label}' for network '${
|
||||
network.name_label
|
||||
}'`
|
||||
`Tunnel added on host '${host.name_label}' for network '${network.name_label}'`
|
||||
)
|
||||
}
|
||||
|
||||
@@ -708,9 +660,7 @@ class SDNController extends EventEmitter {
|
||||
)
|
||||
} catch (error) {
|
||||
log.error(
|
||||
`Couldn't add host: '${host.name_label}' to network: '${
|
||||
network.name_label
|
||||
}' in pool: '${host.$pool.name_label}' because: ${error}`
|
||||
`Couldn't add host: '${host.name_label}' to network: '${network.name_label}' in pool: '${host.$pool.name_label}' because: ${error}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,18 +135,14 @@ export class OvsdbClient {
|
||||
|
||||
if (error != null) {
|
||||
log.error(
|
||||
`[${
|
||||
this._host.name_label
|
||||
}] Error while adding port: '${portName}' and interface: '${interfaceName}' to bridge: '${bridgeName}' on network: '${networkName}' because: ${error}: ${details}`
|
||||
`[${this._host.name_label}] Error while adding port: '${portName}' and interface: '${interfaceName}' to bridge: '${bridgeName}' on network: '${networkName}' because: ${error}: ${details}`
|
||||
)
|
||||
socket.destroy()
|
||||
return
|
||||
}
|
||||
|
||||
log.debug(
|
||||
`[${
|
||||
this._host.name_label
|
||||
}] Port: '${portName}' and interface: '${interfaceName}' added to bridge: '${bridgeName}' on network: '${networkName}'`
|
||||
`[${this._host.name_label}] Port: '${portName}' and interface: '${interfaceName}' added to bridge: '${bridgeName}' on network: '${networkName}'`
|
||||
)
|
||||
socket.destroy()
|
||||
}
|
||||
@@ -170,8 +166,8 @@ export class OvsdbClient {
|
||||
return
|
||||
}
|
||||
const portsToDelete = []
|
||||
for (let i = 0; i < ports.length; ++i) {
|
||||
const portUuid = ports[i][1]
|
||||
for (const port of ports) {
|
||||
const portUuid = port[1]
|
||||
|
||||
const where = [['_uuid', '==', ['uuid', portUuid]]]
|
||||
const selectResult = await this._select(
|
||||
@@ -187,9 +183,7 @@ export class OvsdbClient {
|
||||
forOwn(selectResult.other_config[1], config => {
|
||||
if (config[0] === 'private_pool_wide' && config[1] === 'true') {
|
||||
log.debug(
|
||||
`[${this._host.name_label}] Adding port: '${
|
||||
selectResult.name
|
||||
}' to delete list from bridge: '${bridgeName}'`
|
||||
`[${this._host.name_label}] Adding port: '${selectResult.name}' to delete list from bridge: '${bridgeName}'`
|
||||
)
|
||||
portsToDelete.push(['uuid', portUuid])
|
||||
}
|
||||
@@ -217,20 +211,14 @@ export class OvsdbClient {
|
||||
}
|
||||
if (jsonObjects[0].error != null) {
|
||||
log.error(
|
||||
`[${
|
||||
this._host.name_label
|
||||
}] Couldn't delete ports from bridge: '${bridgeName}' because: ${
|
||||
jsonObjects.error
|
||||
}`
|
||||
`[${this._host.name_label}] Couldn't delete ports from bridge: '${bridgeName}' because: ${jsonObjects.error}`
|
||||
)
|
||||
socket.destroy()
|
||||
return
|
||||
}
|
||||
|
||||
log.debug(
|
||||
`[${this._host.name_label}] Deleted ${
|
||||
jsonObjects[0].result[0].count
|
||||
} ports from bridge: '${bridgeName}'`
|
||||
`[${this._host.name_label}] Deleted ${jsonObjects[0].result[0].count} ports from bridge: '${bridgeName}'`
|
||||
)
|
||||
socket.destroy()
|
||||
}
|
||||
@@ -288,9 +276,7 @@ export class OvsdbClient {
|
||||
const bridgeUuid = selectResult._uuid[1]
|
||||
const bridgeName = selectResult.name
|
||||
log.debug(
|
||||
`[${
|
||||
this._host.name_label
|
||||
}] Found bridge: '${bridgeName}' for network: '${networkName}'`
|
||||
`[${this._host.name_label}] Found bridge: '${bridgeName}' for network: '${networkName}'`
|
||||
)
|
||||
|
||||
return [bridgeUuid, bridgeName]
|
||||
@@ -307,16 +293,15 @@ export class OvsdbClient {
|
||||
return
|
||||
}
|
||||
|
||||
for (let i = 0; i < ports.length; ++i) {
|
||||
const portUuid = ports[i][1]
|
||||
for (const port of ports) {
|
||||
const portUuid = port[1]
|
||||
const interfaces = await this._getPortInterfaces(portUuid, socket)
|
||||
if (interfaces == null) {
|
||||
continue
|
||||
}
|
||||
|
||||
let j
|
||||
for (j = 0; j < interfaces.length; ++j) {
|
||||
const interfaceUuid = interfaces[j][1]
|
||||
for (const iface of interfaces) {
|
||||
const interfaceUuid = iface[1]
|
||||
const hasRemote = await this._interfaceHasRemote(
|
||||
interfaceUuid,
|
||||
remoteAddress,
|
||||
@@ -372,8 +357,7 @@ export class OvsdbClient {
|
||||
return false
|
||||
}
|
||||
|
||||
for (let i = 0; i < selectResult.options[1].length; ++i) {
|
||||
const option = selectResult.options[1][i]
|
||||
for (const option of selectResult.options[1]) {
|
||||
if (option[0] === 'remote_ip' && option[1] === remoteAddress) {
|
||||
return true
|
||||
}
|
||||
@@ -400,20 +384,14 @@ export class OvsdbClient {
|
||||
const jsonResult = jsonObjects[0].result[0]
|
||||
if (jsonResult.error != null) {
|
||||
log.error(
|
||||
`[${
|
||||
this._host.name_label
|
||||
}] Couldn't retrieve: '${columns}' in: '${table}' because: ${
|
||||
jsonResult.error
|
||||
}: ${jsonResult.details}`
|
||||
`[${this._host.name_label}] Couldn't retrieve: '${columns}' in: '${table}' because: ${jsonResult.error}: ${jsonResult.details}`
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
if (jsonResult.rows.length === 0) {
|
||||
log.error(
|
||||
`[${
|
||||
this._host.name_label
|
||||
}] No '${columns}' found in: '${table}' where: '${where}'`
|
||||
`[${this._host.name_label}] No '${columns}' found in: '${table}' where: '${where}'`
|
||||
)
|
||||
return null
|
||||
}
|
||||
@@ -421,9 +399,7 @@ export class OvsdbClient {
|
||||
// For now all select operations should return only 1 row
|
||||
assert(
|
||||
jsonResult.rows.length === 1,
|
||||
`[${
|
||||
this._host.name_label
|
||||
}] There should exactly 1 row when searching: '${columns}' in: '${table}' where: '${where}'`
|
||||
`[${this._host.name_label}] There should exactly 1 row when searching: '${columns}' in: '${table}' where: '${where}'`
|
||||
)
|
||||
|
||||
return jsonResult.rows[0]
|
||||
@@ -457,9 +433,7 @@ export class OvsdbClient {
|
||||
result = await fromEvent(stream, 'data', {})
|
||||
} catch (error) {
|
||||
log.error(
|
||||
`[${
|
||||
this._host.name_label
|
||||
}] Error while waiting for stream data: ${error}`
|
||||
`[${this._host.name_label}] Error while waiting for stream data: ${error}`
|
||||
)
|
||||
return null
|
||||
}
|
||||
@@ -489,9 +463,7 @@ export class OvsdbClient {
|
||||
await fromEvent(socket, 'secureConnect', {})
|
||||
} catch (error) {
|
||||
log.error(
|
||||
`[${this._host.name_label}] TLS connection failed because: ${error}: ${
|
||||
error.code
|
||||
}`
|
||||
`[${this._host.name_label}] TLS connection failed because: ${error}: ${error.code}`
|
||||
)
|
||||
throw error
|
||||
}
|
||||
@@ -500,9 +472,7 @@ export class OvsdbClient {
|
||||
|
||||
socket.on('error', error => {
|
||||
log.error(
|
||||
`[${
|
||||
this._host.name_label
|
||||
}] OVSDB client socket error: ${error} with code: ${error.code}`
|
||||
`[${this._host.name_label}] OVSDB client socket error: ${error} with code: ${error.code}`
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
"dependencies": {
|
||||
"nodemailer": "^6.1.0",
|
||||
"nodemailer-markdown": "^1.0.1",
|
||||
"promise-toolbox": "^0.12.1"
|
||||
"promise-toolbox": "^0.13.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.0.0",
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
"node": ">=6"
|
||||
},
|
||||
"dependencies": {
|
||||
"promise-toolbox": "^0.12.1",
|
||||
"promise-toolbox": "^0.13.0",
|
||||
"slack-node": "^0.1.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
"html-minifier": "^4.0.0",
|
||||
"human-format": "^0.10.0",
|
||||
"lodash": "^4.17.4",
|
||||
"promise-toolbox": "^0.12.1"
|
||||
"promise-toolbox": "^0.13.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.0.0",
|
||||
|
||||
@@ -29,6 +29,9 @@ guessVhdSizeOnImport = false
|
||||
# be turned for investigation by the administrator.
|
||||
verboseApiLogsOnErrors = false
|
||||
|
||||
# if no events could be fetched during this delay, the server will be marked as disconnected
|
||||
xapiMarkDisconnectedDelay = '5 minutes'
|
||||
|
||||
# https:#github.com/websockets/ws#websocket-compression
|
||||
[apiWebSocketOptions]
|
||||
perMessageDeflate = { threshold = 524288 } # 512kiB
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "xo-server",
|
||||
"version": "5.43.0",
|
||||
"version": "5.44.0",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "Server part of Xen-Orchestra",
|
||||
"keywords": [
|
||||
@@ -38,7 +38,7 @@
|
||||
"@xen-orchestra/cron": "^1.0.3",
|
||||
"@xen-orchestra/defined": "^0.0.0",
|
||||
"@xen-orchestra/emit-async": "^0.0.0",
|
||||
"@xen-orchestra/fs": "^0.9.0",
|
||||
"@xen-orchestra/fs": "^0.10.0",
|
||||
"@xen-orchestra/log": "^0.1.4",
|
||||
"@xen-orchestra/mixin": "^0.0.0",
|
||||
"ajv": "^6.1.1",
|
||||
@@ -102,7 +102,7 @@
|
||||
"passport": "^0.4.0",
|
||||
"passport-local": "^1.0.0",
|
||||
"pretty-format": "^24.0.0",
|
||||
"promise-toolbox": "^0.12.1",
|
||||
"promise-toolbox": "^0.13.0",
|
||||
"proxy-agent": "^3.0.0",
|
||||
"pug": "^2.0.0-rc.4",
|
||||
"pump": "^3.0.0",
|
||||
@@ -123,7 +123,7 @@
|
||||
"value-matcher": "^0.2.0",
|
||||
"vhd-lib": "^0.7.0",
|
||||
"ws": "^6.0.0",
|
||||
"xen-api": "^0.25.2",
|
||||
"xen-api": "^0.27.0",
|
||||
"xml2js": "^0.4.19",
|
||||
"xo-acl-resolver": "^0.4.1",
|
||||
"xo-collection": "^0.4.1",
|
||||
|
||||
@@ -123,10 +123,14 @@ getJob.params = {
|
||||
export async function runJob({
|
||||
id,
|
||||
schedule,
|
||||
settings,
|
||||
vm,
|
||||
vms = vm !== undefined ? [vm] : undefined,
|
||||
}) {
|
||||
return this.runJobSequence([id], await this.getSchedule(schedule), vms)
|
||||
return this.runJobSequence([id], await this.getSchedule(schedule), {
|
||||
settings,
|
||||
vms,
|
||||
})
|
||||
}
|
||||
|
||||
runJob.permission = 'admin'
|
||||
@@ -138,6 +142,13 @@ runJob.params = {
|
||||
schedule: {
|
||||
type: 'string',
|
||||
},
|
||||
settings: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
'*': { type: 'object' },
|
||||
},
|
||||
optional: true,
|
||||
},
|
||||
vm: {
|
||||
type: 'string',
|
||||
optional: true,
|
||||
|
||||
@@ -168,9 +168,7 @@ export async function mergeInto({ source, target, force }) {
|
||||
|
||||
if (sourceHost.productBrand !== targetHost.productBrand) {
|
||||
throw new Error(
|
||||
`a ${sourceHost.productBrand} pool cannot be merged into a ${
|
||||
targetHost.productBrand
|
||||
} pool`
|
||||
`a ${sourceHost.productBrand} pool cannot be merged into a ${targetHost.productBrand} pool`
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -100,20 +100,24 @@ set.params = {
|
||||
optional: true,
|
||||
type: 'boolean',
|
||||
},
|
||||
readOnly: {
|
||||
optional: true,
|
||||
type: 'boolean',
|
||||
},
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export async function connect({ id }) {
|
||||
export async function enable({ id }) {
|
||||
this.updateXenServer(id, { enabled: true })::ignoreErrors()
|
||||
await this.connectXenServer(id)
|
||||
}
|
||||
|
||||
connect.description = 'connect a Xen server'
|
||||
enable.description = 'enable a Xen server'
|
||||
|
||||
connect.permission = 'admin'
|
||||
enable.permission = 'admin'
|
||||
|
||||
connect.params = {
|
||||
enable.params = {
|
||||
id: {
|
||||
type: 'string',
|
||||
},
|
||||
@@ -121,16 +125,16 @@ connect.params = {
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export async function disconnect({ id }) {
|
||||
export async function disable({ id }) {
|
||||
this.updateXenServer(id, { enabled: false })::ignoreErrors()
|
||||
await this.disconnectXenServer(id)
|
||||
}
|
||||
|
||||
disconnect.description = 'disconnect a Xen server'
|
||||
disable.description = 'disable a Xen server'
|
||||
|
||||
disconnect.permission = 'admin'
|
||||
disable.permission = 'admin'
|
||||
|
||||
disconnect.params = {
|
||||
disable.params = {
|
||||
id: {
|
||||
type: 'string',
|
||||
},
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import assert from 'assert'
|
||||
import { fromEvent } from 'promise-toolbox'
|
||||
|
||||
export function getPermissionsForUser({ userId }) {
|
||||
return this.getPermissionsForUser(userId)
|
||||
}
|
||||
@@ -86,3 +89,35 @@ copyVm.resolve = {
|
||||
vm: ['vm', 'VM'],
|
||||
sr: ['sr', 'SR'],
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export async function changeConnectedXapiHostname({
|
||||
hostname,
|
||||
newObject,
|
||||
oldObject,
|
||||
}) {
|
||||
const xapi = this.getXapi(oldObject)
|
||||
const { pool: currentPool } = xapi
|
||||
|
||||
xapi._setUrl({ ...xapi._url, hostname })
|
||||
await fromEvent(xapi.objects, 'finish')
|
||||
if (xapi.pool.$id === currentPool.$id) {
|
||||
await fromEvent(xapi.objects, 'finish')
|
||||
}
|
||||
|
||||
assert(xapi.pool.$id !== currentPool.$id)
|
||||
assert.doesNotThrow(() => this.getXapi(newObject))
|
||||
assert.throws(() => this.getXapi(oldObject))
|
||||
}
|
||||
|
||||
changeConnectedXapiHostname.description =
|
||||
'change the connected XAPI hostname and check if the pool and the local cache are updated'
|
||||
|
||||
changeConnectedXapiHostname.permission = 'admin'
|
||||
|
||||
changeConnectedXapiHostname.params = {
|
||||
hostname: { type: 'string' },
|
||||
newObject: { type: 'string', description: "new connection's XO object" },
|
||||
oldObject: { type: 'string', description: "current connection's XO object" },
|
||||
}
|
||||
|
||||
@@ -1134,7 +1134,10 @@ resume.resolve = {
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export function revert({ snapshot, snapshotBefore }) {
|
||||
export async function revert({ snapshot, snapshotBefore }) {
|
||||
await this.checkPermissions(this.user.id, [
|
||||
[snapshot.$snapshot_of, 'operate'],
|
||||
])
|
||||
return this.getXapi(snapshot).revertVm(snapshot._xapiId, snapshotBefore)
|
||||
}
|
||||
|
||||
@@ -1144,7 +1147,7 @@ revert.params = {
|
||||
}
|
||||
|
||||
revert.resolve = {
|
||||
snapshot: ['snapshot', 'VM-snapshot', 'administrate'],
|
||||
snapshot: ['snapshot', 'VM-snapshot', 'view'],
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
@@ -446,9 +446,7 @@ const createNetworkAndInsertHosts = defer(async function(
|
||||
})
|
||||
if (result.exit !== 0) {
|
||||
throw invalidParameters(
|
||||
`Could not ping ${master.name_label}->${
|
||||
address.pif.$host.name_label
|
||||
} (${address.address}) \n${result.stdout}`
|
||||
`Could not ping ${master.name_label}->${address.pif.$host.name_label} (${address.address}) \n${result.stdout}`
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -1050,9 +1048,7 @@ export async function replaceBrick({
|
||||
CURRENT_POOL_OPERATIONS[poolId] = { ...OPERATION_OBJECT, state: 1 }
|
||||
await glusterCmd(
|
||||
glusterEndpoint,
|
||||
`volume replace-brick xosan ${previousBrick} ${
|
||||
addressAndHost.brickName
|
||||
} commit force`
|
||||
`volume replace-brick xosan ${previousBrick} ${addressAndHost.brickName} commit force`
|
||||
)
|
||||
await glusterCmd(glusterEndpoint, 'peer detach ' + previousIp)
|
||||
data.nodes.splice(nodeIndex, 1, {
|
||||
@@ -1126,9 +1122,7 @@ async function _prepareGlusterVm(
|
||||
}
|
||||
await newVM.add_tags('XOSAN')
|
||||
await xapi.editVm(newVM, {
|
||||
name_label: `XOSAN - ${lvmSr.name_label} - ${
|
||||
host.name_label
|
||||
} ${labelSuffix}`,
|
||||
name_label: `XOSAN - ${lvmSr.name_label} - ${host.name_label} ${labelSuffix}`,
|
||||
name_description: 'Xosan VM storage',
|
||||
memory: memorySize,
|
||||
})
|
||||
|
||||
@@ -13,7 +13,6 @@ import includes from 'lodash/includes'
|
||||
import proxyConsole from './proxy-console'
|
||||
import pw from 'pw'
|
||||
import serveStatic from 'serve-static'
|
||||
import startsWith from 'lodash/startsWith'
|
||||
import stoppable from 'stoppable'
|
||||
import WebServer from 'http-server-plus'
|
||||
import WebSocket from 'ws'
|
||||
@@ -332,7 +331,7 @@ async function registerPluginsInPath(path) {
|
||||
|
||||
await Promise.all(
|
||||
mapToArray(files, name => {
|
||||
if (startsWith(name, PLUGIN_PREFIX)) {
|
||||
if (name.startsWith(PLUGIN_PREFIX)) {
|
||||
return registerPluginWrapper.call(
|
||||
this,
|
||||
`${path}/${name}`,
|
||||
@@ -428,7 +427,7 @@ const setUpProxies = (express, opts, xo) => {
|
||||
const { url } = req
|
||||
|
||||
for (const prefix in opts) {
|
||||
if (startsWith(url, prefix)) {
|
||||
if (url.startsWith(prefix)) {
|
||||
const target = opts[prefix]
|
||||
|
||||
proxy.web(req, res, {
|
||||
@@ -452,7 +451,7 @@ const setUpProxies = (express, opts, xo) => {
|
||||
const { url } = req
|
||||
|
||||
for (const prefix in opts) {
|
||||
if (startsWith(url, prefix)) {
|
||||
if (url.startsWith(prefix)) {
|
||||
const target = opts[prefix]
|
||||
|
||||
proxy.ws(req, socket, head, {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import Collection from '../collection/redis'
|
||||
import Model from '../model'
|
||||
import { forEach } from '../utils'
|
||||
import { forEach, serializeError } from '../utils'
|
||||
|
||||
import { parseProp } from './utils'
|
||||
|
||||
@@ -30,13 +30,28 @@ export class Servers extends Collection {
|
||||
|
||||
// Deserializes
|
||||
forEach(servers, server => {
|
||||
server.allowUnauthorized = server.allowUnauthorized === 'true'
|
||||
server.enabled = server.enabled === 'true'
|
||||
if (server.error) {
|
||||
server.error = parseProp('server', server, 'error', '')
|
||||
} else {
|
||||
delete server.error
|
||||
}
|
||||
server.readOnly = server.readOnly === 'true'
|
||||
})
|
||||
|
||||
return servers
|
||||
}
|
||||
|
||||
_update(servers) {
|
||||
servers.map(server => {
|
||||
server.allowUnauthorized = server.allowUnauthorized ? 'true' : undefined
|
||||
server.enabled = server.enabled ? 'true' : undefined
|
||||
const { error } = server
|
||||
server.error =
|
||||
error != null ? JSON.stringify(serializeError(error)) : undefined
|
||||
server.readOnly = server.readOnly ? 'true' : undefined
|
||||
})
|
||||
return super._update(servers)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,9 +13,7 @@ export default function proxyConsole(ws, vmConsole, sessionId) {
|
||||
hostname = address
|
||||
|
||||
log.warn(
|
||||
`host is missing in console (${vmConsole.uuid}) URI (${
|
||||
vmConsole.location
|
||||
}) using host address (${address}) as fallback`
|
||||
`host is missing in console (${vmConsole.uuid}) URI (${vmConsole.location}) using host address (${address}) as fallback`
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { startsWith } from 'lodash'
|
||||
|
||||
import ensureArray from './_ensureArray'
|
||||
import {
|
||||
extractProperty,
|
||||
@@ -119,7 +117,7 @@ const TRANSFORMS = {
|
||||
size: update.installation_size,
|
||||
}
|
||||
|
||||
if (startsWith(update.name_label, 'XS')) {
|
||||
if (update.name_label.startsWith('XS')) {
|
||||
// It's a patch update but for homogeneity, we're still using pool_patches
|
||||
} else {
|
||||
supplementalPacks.push(formattedUpdate)
|
||||
@@ -265,6 +263,17 @@ const TRANSFORMS = {
|
||||
}
|
||||
}
|
||||
|
||||
// Build a { taskId → operation } map instead of forwarding the
|
||||
// { taskRef → operation } map directly
|
||||
const currentOperations = {}
|
||||
const { $xapi } = obj
|
||||
forEach(obj.current_operations, (operation, ref) => {
|
||||
const task = $xapi.getObjectByRef(ref, undefined)
|
||||
if (task !== undefined) {
|
||||
currentOperations[task.$id] = operation
|
||||
}
|
||||
})
|
||||
|
||||
const vm = {
|
||||
// type is redefined after for controllers/, templates &
|
||||
// snapshots.
|
||||
@@ -281,7 +290,7 @@ const TRANSFORMS = {
|
||||
? +metrics.VCPUs_number
|
||||
: +obj.VCPUs_at_startup,
|
||||
},
|
||||
current_operations: obj.current_operations,
|
||||
current_operations: currentOperations,
|
||||
docker: (function() {
|
||||
const monitor = otherConfig['xscontainer-monitor']
|
||||
if (!monitor) {
|
||||
|
||||
@@ -4,7 +4,6 @@ import synchronized from 'decorator-synchronized'
|
||||
import { BaseError } from 'make-error'
|
||||
import {
|
||||
defaults,
|
||||
endsWith,
|
||||
findKey,
|
||||
forEach,
|
||||
identity,
|
||||
@@ -184,7 +183,7 @@ const STATS = {
|
||||
transformValue: value => value * 1024,
|
||||
},
|
||||
memory: {
|
||||
test: metricType => endsWith(metricType, 'memory'),
|
||||
test: metricType => metricType.endsWith('memory'),
|
||||
},
|
||||
cpus: {
|
||||
test: /^cpu(\d+)$/,
|
||||
|
||||
@@ -31,7 +31,6 @@ import {
|
||||
isEmpty,
|
||||
noop,
|
||||
omit,
|
||||
startsWith,
|
||||
uniq,
|
||||
} from 'lodash'
|
||||
import { satisfies as versionSatisfies } from 'semver'
|
||||
@@ -830,7 +829,7 @@ export default class Xapi extends XapiBase {
|
||||
}
|
||||
|
||||
// If the VDI name start with `[NOBAK]`, do not export it.
|
||||
if (startsWith(vdi.name_label, '[NOBAK]')) {
|
||||
if (vdi.name_label.startsWith('[NOBAK]')) {
|
||||
// FIXME: find a way to not create the VDI snapshot in the
|
||||
// first time.
|
||||
//
|
||||
@@ -1724,9 +1723,7 @@ export default class Xapi extends XapiBase {
|
||||
}
|
||||
|
||||
log.debug(
|
||||
`Moving VDI ${vdi.name_label} from ${vdi.$SR.name_label} to ${
|
||||
sr.name_label
|
||||
}`
|
||||
`Moving VDI ${vdi.name_label} from ${vdi.$SR.name_label} to ${sr.name_label}`
|
||||
)
|
||||
try {
|
||||
await pRetry(
|
||||
|
||||
@@ -256,16 +256,12 @@ export default {
|
||||
) {
|
||||
if (getAll) {
|
||||
log(
|
||||
`patch ${
|
||||
patch.name
|
||||
} (${id}) conflicts with installed patch ${conflictId}`
|
||||
`patch ${patch.name} (${id}) conflicts with installed patch ${conflictId}`
|
||||
)
|
||||
return
|
||||
}
|
||||
throw new Error(
|
||||
`patch ${
|
||||
patch.name
|
||||
} (${id}) conflicts with installed patch ${conflictId}`
|
||||
`patch ${patch.name} (${id}) conflicts with installed patch ${conflictId}`
|
||||
)
|
||||
}
|
||||
|
||||
@@ -292,9 +288,7 @@ export default {
|
||||
if (!installed[id] && find(installable, { id }) === undefined) {
|
||||
if (requiredPatch.paid && freeHost) {
|
||||
throw new Error(
|
||||
`required patch ${
|
||||
requiredPatch.name
|
||||
} (${id}) requires a XenServer license`
|
||||
`required patch ${requiredPatch.name} (${id}) requires a XenServer license`
|
||||
)
|
||||
}
|
||||
installable.push(requiredPatch)
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
isEmpty,
|
||||
last,
|
||||
mapValues,
|
||||
merge,
|
||||
noop,
|
||||
some,
|
||||
sum,
|
||||
@@ -68,6 +69,7 @@ export type Mode = 'full' | 'delta'
|
||||
export type ReportWhen = 'always' | 'failure' | 'never'
|
||||
|
||||
type Settings = {|
|
||||
bypassVdiChainsCheck?: boolean,
|
||||
concurrency?: number,
|
||||
deleteFirst?: boolean,
|
||||
copyRetention?: number,
|
||||
@@ -139,6 +141,7 @@ const getOldEntries = <T>(retention: number, entries?: T[]): T[] =>
|
||||
: entries
|
||||
|
||||
const defaultSettings: Settings = {
|
||||
bypassVdiChainsCheck: false,
|
||||
concurrency: 0,
|
||||
deleteFirst: false,
|
||||
exportRetention: 0,
|
||||
@@ -557,7 +560,7 @@ export default class BackupNg {
|
||||
|
||||
const executor: Executor = async ({
|
||||
cancelToken,
|
||||
data: vmsId,
|
||||
data,
|
||||
job: job_,
|
||||
logger,
|
||||
runJobId,
|
||||
@@ -567,6 +570,8 @@ export default class BackupNg {
|
||||
throw new Error('backup job cannot run without a schedule')
|
||||
}
|
||||
|
||||
let vmsId = data?.vms
|
||||
|
||||
const job: BackupJob = (job_: any)
|
||||
const vmsPattern = job.vms
|
||||
|
||||
@@ -620,7 +625,9 @@ export default class BackupNg {
|
||||
}))
|
||||
)
|
||||
|
||||
const timeout = getSetting(job.settings, 'timeout', [''])
|
||||
const settings = merge(job.settings, data?.settings)
|
||||
|
||||
const timeout = getSetting(settings, 'timeout', [''])
|
||||
if (timeout !== 0) {
|
||||
const source = CancelToken.source([cancelToken])
|
||||
cancelToken = source.token
|
||||
@@ -653,6 +660,7 @@ export default class BackupNg {
|
||||
schedule,
|
||||
logger,
|
||||
taskId,
|
||||
settings,
|
||||
srs,
|
||||
remotes
|
||||
)
|
||||
@@ -660,7 +668,7 @@ export default class BackupNg {
|
||||
// 2018-07-20, JFT: vmTimeout is disabled for the time being until
|
||||
// we figure out exactly how it should behave.
|
||||
//
|
||||
// const vmTimeout: number = getSetting(job.settings, 'vmTimeout', [
|
||||
// const vmTimeout: number = getSetting(settings, 'vmTimeout', [
|
||||
// uuid,
|
||||
// scheduleId,
|
||||
// ])
|
||||
@@ -689,9 +697,7 @@ export default class BackupNg {
|
||||
}
|
||||
}
|
||||
|
||||
const concurrency: number = getSetting(job.settings, 'concurrency', [
|
||||
'',
|
||||
])
|
||||
const concurrency: number = getSetting(settings, 'concurrency', [''])
|
||||
if (concurrency !== 0) {
|
||||
handleVm = limitConcurrency(concurrency)(handleVm)
|
||||
logger.notice('vms', {
|
||||
@@ -930,6 +936,7 @@ export default class BackupNg {
|
||||
schedule: Schedule,
|
||||
logger: any,
|
||||
taskId: string,
|
||||
settings: Settings,
|
||||
srs: any[],
|
||||
remotes: any[]
|
||||
): Promise<void> {
|
||||
@@ -957,7 +964,7 @@ export default class BackupNg {
|
||||
)
|
||||
}
|
||||
|
||||
const { id: jobId, mode, settings } = job
|
||||
const { id: jobId, mode } = job
|
||||
const { id: scheduleId } = schedule
|
||||
|
||||
let exportRetention: number = getSetting(settings, 'exportRetention', [
|
||||
@@ -1018,7 +1025,14 @@ export default class BackupNg {
|
||||
.filter(_ => _.other_config['xo:backup:job'] === jobId)
|
||||
.sort(compareSnapshotTime)
|
||||
|
||||
xapi._assertHealthyVdiChains(vm)
|
||||
const bypassVdiChainsCheck: boolean = getSetting(
|
||||
settings,
|
||||
'bypassVdiChainsCheck',
|
||||
[vmUuid, '']
|
||||
)
|
||||
if (!bypassVdiChainsCheck) {
|
||||
xapi._assertHealthyVdiChains(vm)
|
||||
}
|
||||
|
||||
const offlineSnapshot: boolean = getSetting(settings, 'offlineSnapshot', [
|
||||
vmUuid,
|
||||
|
||||
@@ -10,17 +10,7 @@ import { createReadStream, readdir, stat } from 'fs'
|
||||
import { satisfies as versionSatisfies } from 'semver'
|
||||
import { utcFormat } from 'd3-time-format'
|
||||
import { basename, dirname } from 'path'
|
||||
import {
|
||||
endsWith,
|
||||
filter,
|
||||
find,
|
||||
includes,
|
||||
once,
|
||||
range,
|
||||
sortBy,
|
||||
startsWith,
|
||||
trim,
|
||||
} from 'lodash'
|
||||
import { filter, find, includes, once, range, sortBy, trim } from 'lodash'
|
||||
import {
|
||||
chainVhd,
|
||||
createSyntheticStream as createVhdReadStream,
|
||||
@@ -104,7 +94,7 @@ const getVdiTimestamp = name => {
|
||||
|
||||
const getDeltaBackupNameWithoutExt = name =>
|
||||
name.slice(0, -DELTA_BACKUP_EXT_LENGTH)
|
||||
const isDeltaBackup = name => endsWith(name, DELTA_BACKUP_EXT)
|
||||
const isDeltaBackup = name => name.endsWith(DELTA_BACKUP_EXT)
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
@@ -308,13 +298,13 @@ export default class {
|
||||
const handler = await this._xo.getRemoteHandler(remoteId)
|
||||
|
||||
// List backups. (No delta)
|
||||
const backupFilter = file => endsWith(file, '.xva')
|
||||
const backupFilter = file => file.endsWith('.xva')
|
||||
|
||||
const files = await handler.list('.')
|
||||
const backups = filter(files, backupFilter)
|
||||
|
||||
// List delta backups.
|
||||
const deltaDirs = filter(files, file => startsWith(file, 'vm_delta_'))
|
||||
const deltaDirs = filter(files, file => file.startsWith('vm_delta_'))
|
||||
|
||||
for (const deltaDir of deltaDirs) {
|
||||
const files = await handler.list(deltaDir)
|
||||
@@ -336,12 +326,12 @@ export default class {
|
||||
const backups = []
|
||||
|
||||
await asyncMap(handler.list('.'), entry => {
|
||||
if (endsWith(entry, '.xva')) {
|
||||
if (entry.endsWith('.xva')) {
|
||||
backups.push(parseVmBackupPath(entry))
|
||||
} else if (startsWith(entry, 'vm_delta_')) {
|
||||
} else if (entry.startsWith('vm_delta_')) {
|
||||
return handler.list(entry).then(children =>
|
||||
asyncMap(children, child => {
|
||||
if (endsWith(child, '.json')) {
|
||||
if (child.endsWith('.json')) {
|
||||
const path = `${entry}/${child}`
|
||||
|
||||
const record = parseVmBackupPath(path)
|
||||
@@ -411,9 +401,7 @@ export default class {
|
||||
localBaseUuid,
|
||||
{
|
||||
bypassVdiChainsCheck: force,
|
||||
snapshotNameLabel: `XO_DELTA_EXPORT: ${targetSr.name_label} (${
|
||||
targetSr.uuid
|
||||
})`,
|
||||
snapshotNameLabel: `XO_DELTA_EXPORT: ${targetSr.name_label} (${targetSr.uuid})`,
|
||||
}
|
||||
)
|
||||
$defer.onFailure(() => srcXapi.deleteVm(delta.vm.uuid))
|
||||
@@ -1009,7 +997,7 @@ export default class {
|
||||
// Currently, the filenames of the VHD changes over time
|
||||
// (delta → full), but the JSON is not updated, therefore the
|
||||
// VHD path may need to be fixed.
|
||||
return endsWith(vhdPath, '_delta.vhd')
|
||||
return vhdPath.endsWith('_delta.vhd')
|
||||
? pFromCallback(cb => stat(vhdPath, cb)).then(
|
||||
() => vhdPath,
|
||||
error => {
|
||||
|
||||
@@ -204,9 +204,7 @@ export default class metadataBackup {
|
||||
|
||||
await asyncMap(handlers, async (handler, remoteId) => {
|
||||
const subTaskId = logger.notice(
|
||||
`Starting XO metadata backup for the remote (${remoteId}). (${
|
||||
job.id
|
||||
})`,
|
||||
`Starting XO metadata backup for the remote (${remoteId}). (${job.id})`,
|
||||
{
|
||||
data: {
|
||||
id: remoteId,
|
||||
@@ -244,9 +242,7 @@ export default class metadataBackup {
|
||||
)
|
||||
|
||||
logger.notice(
|
||||
`Backuping XO metadata for the remote (${remoteId}) is a success. (${
|
||||
job.id
|
||||
})`,
|
||||
`Backuping XO metadata for the remote (${remoteId}) is a success. (${job.id})`,
|
||||
{
|
||||
event: 'task.end',
|
||||
status: 'success',
|
||||
@@ -265,9 +261,7 @@ export default class metadataBackup {
|
||||
})
|
||||
|
||||
logger.error(
|
||||
`Backuping XO metadata for the remote (${remoteId}) has failed. (${
|
||||
job.id
|
||||
})`,
|
||||
`Backuping XO metadata for the remote (${remoteId}) has failed. (${job.id})`,
|
||||
{
|
||||
event: 'task.end',
|
||||
result: serializeError(error),
|
||||
@@ -340,9 +334,7 @@ export default class metadataBackup {
|
||||
|
||||
await asyncMap(handlers, async (handler, remoteId) => {
|
||||
const subTaskId = logger.notice(
|
||||
`Starting metadata backup for the pool (${poolId}) for the remote (${remoteId}). (${
|
||||
job.id
|
||||
})`,
|
||||
`Starting metadata backup for the pool (${poolId}) for the remote (${remoteId}). (${job.id})`,
|
||||
{
|
||||
data: {
|
||||
id: remoteId,
|
||||
@@ -392,9 +384,7 @@ export default class metadataBackup {
|
||||
)
|
||||
|
||||
logger.notice(
|
||||
`Backuping pool metadata (${poolId}) for the remote (${remoteId}) is a success. (${
|
||||
job.id
|
||||
})`,
|
||||
`Backuping pool metadata (${poolId}) for the remote (${remoteId}) is a success. (${job.id})`,
|
||||
{
|
||||
event: 'task.end',
|
||||
status: 'success',
|
||||
@@ -416,9 +406,7 @@ export default class metadataBackup {
|
||||
})
|
||||
|
||||
logger.error(
|
||||
`Backuping pool metadata (${poolId}) for the remote (${remoteId}) has failed. (${
|
||||
job.id
|
||||
})`,
|
||||
`Backuping pool metadata (${poolId}) for the remote (${remoteId}) has failed. (${job.id})`,
|
||||
{
|
||||
event: 'task.end',
|
||||
result: serializeError(error),
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import endsWith from 'lodash/endsWith'
|
||||
import levelup from 'level-party'
|
||||
import startsWith from 'lodash/startsWith'
|
||||
import sublevel from 'level-sublevel'
|
||||
import { ensureDir } from 'fs-extra'
|
||||
|
||||
@@ -38,7 +36,7 @@ const levelPromise = db => {
|
||||
return
|
||||
}
|
||||
|
||||
if (endsWith(name, 'Stream') || startsWith(name, 'is')) {
|
||||
if (name.endsWith('Stream') || name.startsWith('is')) {
|
||||
dbP[name] = db::value
|
||||
} else {
|
||||
dbP[name] = promisify(value, db)
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import createLogger from '@xen-orchestra/log'
|
||||
import { BaseError } from 'make-error'
|
||||
import { fibonacci } from 'iterable-backoff'
|
||||
import { findKey } from 'lodash'
|
||||
import { noSuchObject } from 'xo-common/api-errors'
|
||||
import { pDelay, ignoreErrors } from 'promise-toolbox'
|
||||
|
||||
import * as XenStore from '../_XenStore'
|
||||
import parseDuration from '../_parseDuration'
|
||||
import Xapi from '../xapi'
|
||||
import xapiObjectToXo from '../xapi-object-to-xo'
|
||||
import XapiStats from '../xapi-stats'
|
||||
@@ -14,7 +16,6 @@ import {
|
||||
isEmpty,
|
||||
isString,
|
||||
popProperty,
|
||||
serializeError,
|
||||
} from '../utils'
|
||||
import { Servers } from '../models/server'
|
||||
|
||||
@@ -41,7 +42,10 @@ const log = createLogger('xo:xo-mixins:xen-servers')
|
||||
// - _xapis[server.id] id defined
|
||||
// - _serverIdsByPool[xapi.pool.$id] is server.id
|
||||
export default class {
|
||||
constructor(xo, { guessVhdSizeOnImport, xapiOptions }) {
|
||||
constructor(
|
||||
xo,
|
||||
{ guessVhdSizeOnImport, xapiMarkDisconnectedDelay, xapiOptions }
|
||||
) {
|
||||
this._objectConflicts = { __proto__: null } // TODO: clean when a server is disconnected.
|
||||
const serversDb = (this._servers = new Servers({
|
||||
connection: xo._redis,
|
||||
@@ -56,6 +60,7 @@ export default class {
|
||||
}
|
||||
this._xapis = { __proto__: null }
|
||||
this._xo = xo
|
||||
this._xapiMarkDisconnectedDelay = parseDuration(xapiMarkDisconnectedDelay)
|
||||
|
||||
xo.on('clean', () => serversDb.rebuildIndexes())
|
||||
xo.on('start', async () => {
|
||||
@@ -94,23 +99,23 @@ export default class {
|
||||
}
|
||||
|
||||
async registerXenServer({
|
||||
allowUnauthorized,
|
||||
allowUnauthorized = false,
|
||||
host,
|
||||
label,
|
||||
password,
|
||||
readOnly,
|
||||
readOnly = false,
|
||||
username,
|
||||
}) {
|
||||
// FIXME: We are storing passwords which is bad!
|
||||
// Could we use tokens instead?
|
||||
// TODO: use plain objects
|
||||
const server = await this._servers.create({
|
||||
allowUnauthorized: allowUnauthorized ? 'true' : undefined,
|
||||
enabled: 'true',
|
||||
allowUnauthorized,
|
||||
enabled: true,
|
||||
host,
|
||||
label: label || undefined,
|
||||
password,
|
||||
readOnly: readOnly ? 'true' : undefined,
|
||||
readOnly,
|
||||
username,
|
||||
})
|
||||
|
||||
@@ -162,22 +167,22 @@ export default class {
|
||||
if (password) server.set('password', password)
|
||||
|
||||
if (error !== undefined) {
|
||||
server.set('error', error ? JSON.stringify(error) : '')
|
||||
server.set('error', error)
|
||||
}
|
||||
|
||||
if (enabled !== undefined) {
|
||||
server.set('enabled', enabled ? 'true' : undefined)
|
||||
server.set('enabled', enabled)
|
||||
}
|
||||
|
||||
if (readOnly !== undefined) {
|
||||
server.set('readOnly', readOnly ? 'true' : undefined)
|
||||
server.set('readOnly', readOnly)
|
||||
if (xapi !== undefined) {
|
||||
xapi.readOnly = readOnly
|
||||
}
|
||||
}
|
||||
|
||||
if (allowUnauthorized !== undefined) {
|
||||
server.set('allowUnauthorized', allowUnauthorized ? 'true' : undefined)
|
||||
server.set('allowUnauthorized', allowUnauthorized)
|
||||
}
|
||||
|
||||
await this._servers.update(server)
|
||||
@@ -205,7 +210,21 @@ export default class {
|
||||
const conflicts = this._objectConflicts
|
||||
const objects = this._xo._objects
|
||||
|
||||
const serverIdsByPool = this._serverIdsByPool
|
||||
forEach(newXapiObjects, function handleObject(xapiObject, xapiId) {
|
||||
// handle pool UUID change
|
||||
if (
|
||||
xapiObject.$type === 'pool' &&
|
||||
serverIdsByPool[xapiObject.$id] === undefined
|
||||
) {
|
||||
const obsoletePoolId = findKey(
|
||||
serverIdsByPool,
|
||||
serverId => serverId === conId
|
||||
)
|
||||
delete serverIdsByPool[obsoletePoolId]
|
||||
serverIdsByPool[xapiObject.$id] = conId
|
||||
}
|
||||
|
||||
const { $ref } = xapiObject
|
||||
|
||||
const dependent = dependents[$ref]
|
||||
@@ -275,8 +294,8 @@ export default class {
|
||||
const server = (await this._getXenServer(id)).properties
|
||||
|
||||
const xapi = (this._xapis[server.id] = new Xapi({
|
||||
allowUnauthorized: Boolean(server.allowUnauthorized),
|
||||
readOnly: Boolean(server.readOnly),
|
||||
allowUnauthorized: server.allowUnauthorized,
|
||||
readOnly: server.readOnly,
|
||||
|
||||
...this._xapiOptions,
|
||||
|
||||
@@ -412,7 +431,7 @@ export default class {
|
||||
} catch (error) {
|
||||
delete this._xapis[server.id]
|
||||
xapi.disconnect()::ignoreErrors()
|
||||
this.updateXenServer(id, { error: serializeError(error) })::ignoreErrors()
|
||||
this.updateXenServer(id, { error })::ignoreErrors()
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -473,6 +492,14 @@ export default class {
|
||||
const servers = await this._servers.get()
|
||||
const xapis = this._xapis
|
||||
forEach(servers, server => {
|
||||
const lastEventFetchedTimestamp =
|
||||
xapis[server.id]?.lastEventFetchedTimestamp
|
||||
if (
|
||||
lastEventFetchedTimestamp !== undefined &&
|
||||
Date.now() > lastEventFetchedTimestamp + this._xapiMarkDisconnectedDelay
|
||||
) {
|
||||
server.error = xapis[server.id].watchEventsError
|
||||
}
|
||||
server.status = this._getXenServerStatus(server.id)
|
||||
if (server.status === 'connected') {
|
||||
server.poolId = xapis[server.id].pool.uuid
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
"child-process-promise": "^2.0.3",
|
||||
"core-js": "^3.0.0",
|
||||
"pipette": "^0.9.3",
|
||||
"promise-toolbox": "^0.12.1",
|
||||
"promise-toolbox": "^0.13.0",
|
||||
"tmp": "^0.1.0",
|
||||
"vhd-lib": "^0.7.0"
|
||||
},
|
||||
@@ -38,7 +38,7 @@
|
||||
"babel-plugin-lodash": "^3.3.2",
|
||||
"cross-env": "^5.1.3",
|
||||
"event-to-promise": "^0.8.0",
|
||||
"execa": "^1.0.0",
|
||||
"execa": "^2.0.2",
|
||||
"fs-extra": "^8.0.1",
|
||||
"get-stream": "^5.1.0",
|
||||
"index-modules": "^0.3.0",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "xo-web",
|
||||
"version": "5.43.0",
|
||||
"version": "5.44.0",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "Web interface client for Xen-Orchestra",
|
||||
"keywords": [
|
||||
@@ -84,7 +84,7 @@
|
||||
"human-format": "^0.10.0",
|
||||
"immutable": "^4.0.0-rc.9",
|
||||
"index-modules": "^0.3.0",
|
||||
"is-ip": "^2.0.0",
|
||||
"is-ip": "^3.1.0",
|
||||
"jsonrpc-websocket-client": "^0.5.0",
|
||||
"kindof": "^2.0.0",
|
||||
"lodash": "^4.6.1",
|
||||
@@ -96,7 +96,7 @@
|
||||
"moment-timezone": "^0.5.14",
|
||||
"notifyjs": "^3.0.0",
|
||||
"otplib": "^11.0.0",
|
||||
"promise-toolbox": "^0.12.1",
|
||||
"promise-toolbox": "^0.13.0",
|
||||
"prop-types": "^15.6.0",
|
||||
"qrcode": "^1.3.2",
|
||||
"random-password": "^0.1.2",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import { isFunction, startsWith } from 'lodash'
|
||||
import { isFunction } from 'lodash'
|
||||
|
||||
import Button from './button'
|
||||
import Component from './base-component'
|
||||
@@ -73,7 +73,7 @@ export default class ActionButton extends Component {
|
||||
let empty = true
|
||||
handlerParam = {}
|
||||
Object.keys(props).forEach(key => {
|
||||
if (startsWith(key, 'data-')) {
|
||||
if (key.startsWith('data-')) {
|
||||
empty = false
|
||||
handlerParam[key.slice(5)] = props[key]
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import classNames from 'classnames'
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { isEmpty, isFunction, isString, map, pick, startsWith } from 'lodash'
|
||||
import { isEmpty, isFunction, isString, map, pick } from 'lodash'
|
||||
|
||||
import _ from '../intl'
|
||||
import Component from '../base-component'
|
||||
@@ -119,7 +119,7 @@ class Editable extends Component {
|
||||
this.setState({ saving: true })
|
||||
|
||||
const params = Object.keys(props).reduce((res, val) => {
|
||||
if (startsWith(val, 'data-')) {
|
||||
if (val.startsWith('data-')) {
|
||||
res[val.slice(5)] = props[val]
|
||||
}
|
||||
return res
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import { injectState, provideState } from 'reaclette'
|
||||
import { startsWith } from 'lodash'
|
||||
|
||||
import decorate from '../apply-decorators'
|
||||
|
||||
@@ -23,7 +22,7 @@ const Number_ = decorate([
|
||||
const params = {}
|
||||
let empty = true
|
||||
Object.keys(props).forEach(key => {
|
||||
if (startsWith(key, 'data-')) {
|
||||
if (key.startsWith('data-')) {
|
||||
empty = false
|
||||
params[key.slice(5)] = props[key]
|
||||
}
|
||||
|
||||
@@ -3058,9 +3058,6 @@ export default {
|
||||
// Original text: "Enable it if your certificate is rejected, but it's not recommended because your connection will not be secured."
|
||||
serverUnauthorizedCertificatesInfo: undefined,
|
||||
|
||||
// Original text: 'Disconnect server'
|
||||
serverDisconnect: undefined,
|
||||
|
||||
// Original text: 'username'
|
||||
serverPlaceHolderUser: undefined,
|
||||
|
||||
@@ -3091,12 +3088,6 @@ export default {
|
||||
// Original text: 'Connecting…'
|
||||
serverConnecting: undefined,
|
||||
|
||||
// Original text: 'Connected'
|
||||
serverConnected: undefined,
|
||||
|
||||
// Original text: 'Disconnected'
|
||||
serverDisconnected: undefined,
|
||||
|
||||
// Original text: 'Authentication error'
|
||||
serverAuthFailed: undefined,
|
||||
|
||||
|
||||
@@ -3135,9 +3135,6 @@ export default {
|
||||
serverUnauthorizedCertificatesInfo:
|
||||
"Activez ceci si votre certificat est rejeté, mais ce n'est pas recommandé car votre connexion ne sera pas sécurisée.",
|
||||
|
||||
// Original text: "Disconnect server"
|
||||
serverDisconnect: 'Déconnecter le serveur',
|
||||
|
||||
// Original text: "username"
|
||||
serverPlaceHolderUser: "nom d'utilisateur",
|
||||
|
||||
@@ -3169,12 +3166,6 @@ export default {
|
||||
// Original text: "Connecting…"
|
||||
serverConnecting: 'Connexion…',
|
||||
|
||||
// Original text: "Connected"
|
||||
serverConnected: 'Connecté',
|
||||
|
||||
// Original text: "Disconnected"
|
||||
serverDisconnected: 'Déconnecté',
|
||||
|
||||
// Original text: "Authentication error"
|
||||
serverAuthFailed: "Erreur d'authentification",
|
||||
|
||||
|
||||
@@ -2612,9 +2612,6 @@ export default {
|
||||
// Original text: 'Read Only'
|
||||
serverReadOnly: undefined,
|
||||
|
||||
// Original text: 'Disconnect server'
|
||||
serverDisconnect: undefined,
|
||||
|
||||
// Original text: 'username'
|
||||
serverPlaceHolderUser: undefined,
|
||||
|
||||
|
||||
@@ -2909,9 +2909,6 @@ export default {
|
||||
// Original text: "Read Only"
|
||||
serverReadOnly: 'Csak Olvasható',
|
||||
|
||||
// Original text: "Disconnect server"
|
||||
serverDisconnect: 'Szerver Lecsatlakozás',
|
||||
|
||||
// Original text: "username"
|
||||
serverPlaceHolderUser: 'felhasználónév',
|
||||
|
||||
@@ -2939,12 +2936,6 @@ export default {
|
||||
// Original text: "Connecting…"
|
||||
serverConnecting: 'Csatlakozás…',
|
||||
|
||||
// Original text: "Connected"
|
||||
serverConnected: 'Kapcsolódva',
|
||||
|
||||
// Original text: "Disconnected"
|
||||
serverDisconnected: 'Lekapcsolódva',
|
||||
|
||||
// Original text: "Authentication error"
|
||||
serverAuthFailed: 'Bejelentkezési hiba',
|
||||
|
||||
|
||||
@@ -2648,9 +2648,6 @@ export default {
|
||||
// Original text: "Read Only"
|
||||
serverReadOnly: 'Tylko do odczytu',
|
||||
|
||||
// Original text: "Disconnect server"
|
||||
serverDisconnect: 'Rozłącz serwer',
|
||||
|
||||
// Original text: "username"
|
||||
serverPlaceHolderUser: 'Użytkownik',
|
||||
|
||||
|
||||
@@ -2636,9 +2636,6 @@ export default {
|
||||
// Original text: "Read Only"
|
||||
serverReadOnly: 'Modo Leitura',
|
||||
|
||||
// Original text: 'Disconnect server'
|
||||
serverDisconnect: undefined,
|
||||
|
||||
// Original text: 'username'
|
||||
serverPlaceHolderUser: undefined,
|
||||
|
||||
|
||||
@@ -3916,9 +3916,6 @@ export default {
|
||||
serverUnauthorizedCertificatesInfo:
|
||||
'Sertifikanız reddedildiğinde bunu yapın ancak bağlantınız güvenli olmayacağı için tavsiye edilmez.',
|
||||
|
||||
// Original text: "Disconnect server"
|
||||
serverDisconnect: 'Sunucu bağlantısını kes',
|
||||
|
||||
// Original text: "username"
|
||||
serverPlaceHolderUser: 'kullanıcı adı',
|
||||
|
||||
@@ -3949,12 +3946,6 @@ export default {
|
||||
// Original text: "Connecting…"
|
||||
serverConnecting: 'Bağlanıyor...',
|
||||
|
||||
// Original text: "Connected"
|
||||
serverConnected: 'Bağlandı',
|
||||
|
||||
// Original text: "Disconnected"
|
||||
serverDisconnected: 'Bağlantı kesildi',
|
||||
|
||||
// Original text: "Authentication error"
|
||||
serverAuthFailed: 'Kimlik doğrulama hatası',
|
||||
|
||||
|
||||
@@ -959,6 +959,7 @@ const messages = {
|
||||
statDisk: 'Disk throughput',
|
||||
statLastTenMinutes: 'Last 10 minutes',
|
||||
statLastTwoHours: 'Last 2 hours',
|
||||
statLastDay: 'Last day',
|
||||
statLastWeek: 'Last week',
|
||||
statLastYear: 'Last year',
|
||||
|
||||
@@ -1661,7 +1662,6 @@ const messages = {
|
||||
serverAllowUnauthorizedCertificates: 'Allow Unauthorized Certificates',
|
||||
serverUnauthorizedCertificatesInfo:
|
||||
"Enable it if your certificate is rejected, but it's not recommended because your connection will not be secured.",
|
||||
serverDisconnect: 'Disconnect server',
|
||||
serverPlaceHolderUser: 'username',
|
||||
serverPlaceHolderPassword: 'password',
|
||||
serverPlaceHolderAddress: 'address[:port]',
|
||||
@@ -1671,13 +1671,15 @@ const messages = {
|
||||
serverAddFailed: 'Adding server failed',
|
||||
serverStatus: 'Status',
|
||||
serverConnectionFailed: 'Connection failed. Click for more information.',
|
||||
serverConnected: 'Connected',
|
||||
serverDisconnected: 'Disconnected',
|
||||
serverAuthFailed: 'Authentication error',
|
||||
serverUnknownError: 'Unknown error',
|
||||
serverSelfSignedCertError: 'Invalid self-signed certificate',
|
||||
serverSelfSignedCertQuestion:
|
||||
'Do you want to accept self-signed certificate for this server even though it would decrease security?',
|
||||
serverEnable: 'Enable',
|
||||
serverEnabled: 'Enabled',
|
||||
serverDisabled: 'Disabled',
|
||||
serverDisable: 'Disable server',
|
||||
|
||||
// ----- Copy VM -----
|
||||
copyVm: 'Copy VM',
|
||||
|
||||
@@ -3,7 +3,7 @@ import CopyToClipboard from 'react-copy-to-clipboard'
|
||||
import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import { get } from '@xen-orchestra/defined'
|
||||
import { find, startsWith } from 'lodash'
|
||||
import { find } from 'lodash'
|
||||
|
||||
import decorate from './apply-decorators'
|
||||
import Icon from './icon'
|
||||
@@ -492,7 +492,7 @@ const xoItemToRender = {
|
||||
|
||||
gpuGroup: group => (
|
||||
<span>
|
||||
{startsWith(group.name_label, 'Group of ')
|
||||
{group.name_label.startsWith('Group of ')
|
||||
? group.name_label.slice(9)
|
||||
: group.name_label}
|
||||
</span>
|
||||
|
||||
@@ -24,7 +24,6 @@ import {
|
||||
isFunction,
|
||||
map,
|
||||
sortBy,
|
||||
startsWith,
|
||||
} from 'lodash'
|
||||
|
||||
import ActionRowButton from '../action-row-button'
|
||||
@@ -327,7 +326,7 @@ export default class SortedTable extends Component {
|
||||
const { props } = this
|
||||
const userData = {}
|
||||
Object.keys(props).forEach(key => {
|
||||
if (startsWith(key, 'data-')) {
|
||||
if (key.startsWith('data-')) {
|
||||
userData[key.slice(5)] = props[key]
|
||||
}
|
||||
})
|
||||
|
||||
68
packages/xo-web/src/common/stats.js
Normal file
68
packages/xo-web/src/common/stats.js
Normal file
@@ -0,0 +1,68 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import { forOwn } from 'lodash'
|
||||
|
||||
import _ from './intl'
|
||||
import { fetchHostStats, fetchSrStats, fetchVmStats } from './xo'
|
||||
import { Select } from './form'
|
||||
|
||||
export const DEFAULT_GRANULARITY = {
|
||||
granularity: 'seconds',
|
||||
label: _('statLastTenMinutes'),
|
||||
value: 'lastTenMinutes',
|
||||
}
|
||||
|
||||
const OPTIONS = [
|
||||
DEFAULT_GRANULARITY,
|
||||
{
|
||||
granularity: 'minutes',
|
||||
label: _('statLastTwoHours'),
|
||||
value: 'lastTwoHours',
|
||||
},
|
||||
{
|
||||
granularity: 'hours',
|
||||
keep: 24,
|
||||
label: _('statLastDay'),
|
||||
value: 'lastDay',
|
||||
},
|
||||
{
|
||||
granularity: 'hours',
|
||||
label: _('statLastWeek'),
|
||||
value: 'lastWeek',
|
||||
},
|
||||
{
|
||||
granularity: 'days',
|
||||
label: _('statLastYear'),
|
||||
value: 'lastYear',
|
||||
},
|
||||
]
|
||||
|
||||
export const SelectGranularity = ({ onChange, value, ...props }) => (
|
||||
<Select {...props} onChange={onChange} options={OPTIONS} value={value} />
|
||||
)
|
||||
|
||||
SelectGranularity.propTypes = {
|
||||
onChange: PropTypes.func.isRequired,
|
||||
value: PropTypes.object.isRequired,
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const FETCH_FN_BY_TYPE = {
|
||||
host: fetchHostStats,
|
||||
sr: fetchSrStats,
|
||||
vm: fetchVmStats,
|
||||
}
|
||||
|
||||
const keepNLastItems = (stats, n) =>
|
||||
Array.isArray(stats)
|
||||
? stats.splice(0, stats.length - n)
|
||||
: forOwn(stats, metrics => keepNLastItems(metrics, n))
|
||||
|
||||
export const fetchStats = async (objOrId, type, { granularity, keep }) => {
|
||||
const stats = await FETCH_FN_BY_TYPE[type](objOrId, granularity)
|
||||
if (keep !== undefined) {
|
||||
keepNLastItems(stats, keep)
|
||||
}
|
||||
return stats
|
||||
}
|
||||
@@ -22,7 +22,6 @@ import {
|
||||
replace,
|
||||
sample,
|
||||
some,
|
||||
startsWith,
|
||||
} from 'lodash'
|
||||
|
||||
import _ from './intl'
|
||||
@@ -477,7 +476,7 @@ export const compareVersions = makeNiceCompare((v1, v2) => {
|
||||
return 0
|
||||
})
|
||||
|
||||
export const isXosanPack = ({ name }) => startsWith(name, 'XOSAN')
|
||||
export const isXosanPack = ({ name }) => name.startsWith('XOSAN')
|
||||
|
||||
// ===================================================================
|
||||
|
||||
|
||||
@@ -536,13 +536,13 @@ export const editServer = (server, props) =>
|
||||
subscribeServers.forceRefresh
|
||||
)
|
||||
|
||||
export const connectServer = server =>
|
||||
_call('server.connect', { id: resolveId(server) })::pFinally(
|
||||
export const enableServer = server =>
|
||||
_call('server.enable', { id: resolveId(server) })::pFinally(
|
||||
subscribeServers.forceRefresh
|
||||
)
|
||||
|
||||
export const disconnectServer = server =>
|
||||
_call('server.disconnect', { id: resolveId(server) })::tap(
|
||||
export const disableServer = server =>
|
||||
_call('server.disable', { id: resolveId(server) })::tap(
|
||||
subscribeServers.forceRefresh
|
||||
)
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import _ from 'intl'
|
||||
import ActionButton from 'action-button'
|
||||
import Component from 'base-component'
|
||||
import endsWith from 'lodash/endsWith'
|
||||
import Icon from 'icon'
|
||||
import React from 'react'
|
||||
import replace from 'lodash/replace'
|
||||
@@ -192,7 +191,7 @@ export default class RestoreFileModalBody extends Component {
|
||||
select.blur()
|
||||
select.focus()
|
||||
|
||||
const isFile = file.id !== '..' && !endsWith(file.path, '/')
|
||||
const isFile = file.id !== '..' && !file.path.endsWith('/')
|
||||
if (isFile) {
|
||||
const { selectedFiles } = this.state
|
||||
if (!includes(selectedFiles, file)) {
|
||||
@@ -228,7 +227,7 @@ export default class RestoreFileModalBody extends Component {
|
||||
_selectAllFolderFiles = () => {
|
||||
this.setState({
|
||||
selectedFiles: (this.state.selectedFiles || []).concat(
|
||||
filter(this._getSelectableFiles(), ({ path }) => !endsWith(path, '/'))
|
||||
filter(this._getSelectableFiles(), ({ path }) => !path.endsWith('/'))
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -10,16 +10,7 @@ import { dirname } from 'path'
|
||||
import { Container, Col, Row } from 'grid'
|
||||
import { createSelector } from 'reselect'
|
||||
import { formatSize } from 'utils'
|
||||
import {
|
||||
endsWith,
|
||||
filter,
|
||||
find,
|
||||
forEach,
|
||||
includes,
|
||||
isEmpty,
|
||||
map,
|
||||
startsWith,
|
||||
} from 'lodash'
|
||||
import { filter, find, forEach, includes, isEmpty, map } from 'lodash'
|
||||
import { getRenderXoItemOfType } from 'render-xo-item'
|
||||
import { listPartitions, listFiles } from 'xo'
|
||||
|
||||
@@ -46,7 +37,7 @@ const fileOptionRenderer = ({ isFile, name }) => (
|
||||
</span>
|
||||
)
|
||||
|
||||
const ensureTrailingSlash = path => path + (endsWith(path, '/') ? '' : '/')
|
||||
const ensureTrailingSlash = path => path + (path.endsWith('/') ? '' : '/')
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
@@ -66,7 +57,7 @@ const formatFilesOptions = (rawFiles, path) => {
|
||||
return files.concat(
|
||||
map(rawFiles, (_, name) => ({
|
||||
id: `${path}${name}`,
|
||||
isFile: !endsWith(name, '/'),
|
||||
isFile: !name.endsWith('/'),
|
||||
name,
|
||||
path: `${path}${name}`,
|
||||
}))
|
||||
@@ -262,7 +253,7 @@ export default class RestoreFileModalBody extends Component {
|
||||
redundantFiles[file.path] =
|
||||
find(
|
||||
files,
|
||||
f => !f.isFile && f !== file && startsWith(file.path, f.path)
|
||||
f => !f.isFile && f !== file && file.path.startsWith(f.path)
|
||||
) !== undefined
|
||||
})
|
||||
return redundantFiles
|
||||
|
||||
@@ -155,11 +155,11 @@ export default class Restore extends Component {
|
||||
})
|
||||
// TODO: perf
|
||||
let first, last
|
||||
let size = 0
|
||||
forEach(backupDataByVm, (data, vmId) => {
|
||||
first = { timestamp: Infinity }
|
||||
last = { timestamp: 0 }
|
||||
const count = {}
|
||||
let size = 0
|
||||
forEach(data.backups, backup => {
|
||||
if (backup.timestamp > last.timestamp) {
|
||||
last = backup
|
||||
|
||||
@@ -22,7 +22,7 @@ import { createGetObjectsOfType, getUser } from 'selectors'
|
||||
import { createSelector } from 'reselect'
|
||||
import { generateUiSchema } from 'xo-json-schema-input'
|
||||
import { SelectSubject } from 'select-objects'
|
||||
import { forEach, isArray, map, mapValues, noop, startsWith } from 'lodash'
|
||||
import { forEach, isArray, map, mapValues, noop } from 'lodash'
|
||||
|
||||
import { createJob, createSchedule, getRemote, editJob, editSchedule } from 'xo'
|
||||
|
||||
@@ -479,7 +479,7 @@ export default class New extends Component {
|
||||
|
||||
if (remoteId) {
|
||||
const remote = await getRemote(remoteId)
|
||||
if (startsWith(remote.url, 'file:')) {
|
||||
if (remote.url.startsWith('file:')) {
|
||||
await confirm({
|
||||
title: _('localRemoteWarningTitle'),
|
||||
body: _('localRemoteWarningMessage'),
|
||||
|
||||
@@ -4,8 +4,8 @@ import Icon from 'icon'
|
||||
import React from 'react'
|
||||
import Tooltip from 'tooltip'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import { DEFAULT_GRANULARITY, fetchStats, SelectGranularity } from 'stats'
|
||||
import { Toggle } from 'form'
|
||||
import { fetchHostStats } from 'xo'
|
||||
import {
|
||||
CpuLineChart,
|
||||
MemoryLineChart,
|
||||
@@ -14,9 +14,9 @@ import {
|
||||
} from 'xo-line-chart'
|
||||
|
||||
export default class HostStats extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state.useCombinedValues = false
|
||||
state = {
|
||||
granularity: DEFAULT_GRANULARITY,
|
||||
useCombinedValues: false,
|
||||
}
|
||||
|
||||
loop(host = this.props.host) {
|
||||
@@ -33,7 +33,7 @@ export default class HostStats extends Component {
|
||||
cancelled = true
|
||||
}
|
||||
|
||||
fetchHostStats(host, this.state.granularity).then(stats => {
|
||||
fetchStats(host, 'host', this.state.granularity).then(stats => {
|
||||
if (cancelled) {
|
||||
return
|
||||
}
|
||||
@@ -80,8 +80,7 @@ export default class HostStats extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
handleSelectStats(event) {
|
||||
const granularity = event.target.value
|
||||
handleSelectStats(granularity) {
|
||||
clearTimeout(this.timeout)
|
||||
|
||||
this.setState(
|
||||
@@ -125,26 +124,11 @@ export default class HostStats extends Component {
|
||||
)}
|
||||
</Col>
|
||||
<Col mediumSize={6}>
|
||||
<div className='btn-tab'>
|
||||
<select
|
||||
className='form-control'
|
||||
onChange={this.handleSelectStats}
|
||||
defaultValue={granularity}
|
||||
>
|
||||
{_('statLastTenMinutes', message => (
|
||||
<option value='seconds'>{message}</option>
|
||||
))}
|
||||
{_('statLastTwoHours', message => (
|
||||
<option value='minutes'>{message}</option>
|
||||
))}
|
||||
{_('statLastWeek', message => (
|
||||
<option value='hours'>{message}</option>
|
||||
))}
|
||||
{_('statLastYear', message => (
|
||||
<option value='days'>{message}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<SelectGranularity
|
||||
onChange={this.handleSelectStats}
|
||||
required
|
||||
value={granularity}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import _ from 'intl'
|
||||
import Component from 'base-component'
|
||||
import getEventValue from 'get-event-value'
|
||||
import Icon from 'icon'
|
||||
import React from 'react'
|
||||
import Tooltip from 'tooltip'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import { Toggle } from 'form'
|
||||
import { fetchHostStats } from 'xo'
|
||||
import { createGetObjectsOfType, createSelector } from 'selectors'
|
||||
import { map } from 'lodash'
|
||||
import { connectStore } from 'utils'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import { createGetObjectsOfType, createSelector } from 'selectors'
|
||||
import { DEFAULT_GRANULARITY, fetchStats, SelectGranularity } from 'stats'
|
||||
import { map } from 'lodash'
|
||||
import { Toggle } from 'form'
|
||||
import {
|
||||
PoolCpuLineChart,
|
||||
PoolMemoryLineChart,
|
||||
@@ -27,6 +26,7 @@ import {
|
||||
})
|
||||
export default class PoolStats extends Component {
|
||||
state = {
|
||||
granularity: DEFAULT_GRANULARITY,
|
||||
useCombinedValues: false,
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ export default class PoolStats extends Component {
|
||||
|
||||
Promise.all(
|
||||
map(this.props.hosts, host =>
|
||||
fetchHostStats(host, this.state.granularity).then(stats => ({
|
||||
fetchStats(host, 'host', this.state.granularity).then(stats => ({
|
||||
host: host.name_label,
|
||||
...stats,
|
||||
}))
|
||||
@@ -74,8 +74,7 @@ export default class PoolStats extends Component {
|
||||
clearTimeout(this.timeout)
|
||||
}
|
||||
|
||||
_handleSelectStats = event => {
|
||||
const granularity = getEventValue(event)
|
||||
_handleSelectStats = granularity => {
|
||||
clearTimeout(this.timeout)
|
||||
|
||||
this.setState(
|
||||
@@ -116,26 +115,11 @@ export default class PoolStats extends Component {
|
||||
)}
|
||||
</Col>
|
||||
<Col mediumSize={6}>
|
||||
<div className='btn-tab'>
|
||||
<select
|
||||
className='form-control'
|
||||
onChange={this._handleSelectStats}
|
||||
defaultValue={granularity}
|
||||
>
|
||||
{_('statLastTenMinutes', message => (
|
||||
<option value='seconds'>{message}</option>
|
||||
))}
|
||||
{_('statLastTwoHours', message => (
|
||||
<option value='minutes'>{message}</option>
|
||||
))}
|
||||
{_('statLastWeek', message => (
|
||||
<option value='hours'>{message}</option>
|
||||
))}
|
||||
{_('statLastYear', message => (
|
||||
<option value='days'>{message}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<SelectGranularity
|
||||
onChange={this._handleSelectStats}
|
||||
required
|
||||
value={granularity}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
|
||||
@@ -25,12 +25,7 @@ import Upgrade from 'xoa-upgrade'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import { injectIntl } from 'react-intl'
|
||||
import { SizeInput } from 'form'
|
||||
import {
|
||||
addSubscriptions,
|
||||
adminOnly,
|
||||
connectStore,
|
||||
resolveIds
|
||||
} from 'utils'
|
||||
import { addSubscriptions, adminOnly, connectStore, resolveIds } from 'utils'
|
||||
import {
|
||||
createGetObjectsOfType,
|
||||
createSelector,
|
||||
|
||||
@@ -16,9 +16,9 @@ import { injectIntl } from 'react-intl'
|
||||
import { noop } from 'lodash'
|
||||
import {
|
||||
addServer,
|
||||
disableServer,
|
||||
editServer,
|
||||
connectServer,
|
||||
disconnectServer,
|
||||
enableServer,
|
||||
removeServer,
|
||||
subscribeServers,
|
||||
} from 'xo'
|
||||
@@ -38,7 +38,7 @@ const showServerError = server => {
|
||||
}).then(
|
||||
() =>
|
||||
editServer(server, { allowUnauthorized: true }).then(() =>
|
||||
connectServer(server)
|
||||
enableServer(server)
|
||||
),
|
||||
noop
|
||||
)
|
||||
@@ -100,17 +100,16 @@ const COLUMNS = [
|
||||
itemRenderer: server => (
|
||||
<div>
|
||||
<StateButton
|
||||
disabledLabel={_('serverDisconnected')}
|
||||
disabledHandler={connectServer}
|
||||
disabledTooltip={_('serverConnect')}
|
||||
enabledLabel={_('serverConnected')}
|
||||
enabledHandler={disconnectServer}
|
||||
enabledTooltip={_('serverDisconnect')}
|
||||
disabledLabel={_('serverDisabled')}
|
||||
disabledHandler={enableServer}
|
||||
disabledTooltip={_('serverEnable')}
|
||||
enabledLabel={_('serverEnabled')}
|
||||
enabledHandler={disableServer}
|
||||
enabledTooltip={_('serverDisable')}
|
||||
handlerParam={server}
|
||||
pending={server.status === 'connecting'}
|
||||
state={server.status === 'connected'}
|
||||
state={server.enabled}
|
||||
/>{' '}
|
||||
{server.error && (
|
||||
{server.error != null && (
|
||||
<Tooltip content={_('serverConnectionFailed')}>
|
||||
<a
|
||||
className='text-danger btn btn-link btn-sm'
|
||||
@@ -129,11 +128,11 @@ const COLUMNS = [
|
||||
itemRenderer: server => (
|
||||
<Toggle
|
||||
onChange={readOnly => editServer(server, { readOnly })}
|
||||
value={!!server.readOnly}
|
||||
value={server.readOnly}
|
||||
/>
|
||||
),
|
||||
name: _('serverReadOnly'),
|
||||
sortCriteria: _ => !!_.readOnly,
|
||||
sortCriteria: _ => _.readOnly,
|
||||
},
|
||||
{
|
||||
itemRenderer: server => (
|
||||
@@ -154,7 +153,7 @@ const COLUMNS = [
|
||||
</Tooltip>
|
||||
</span>
|
||||
),
|
||||
sortCriteria: _ => !!_.allowUnauthorized,
|
||||
sortCriteria: _ => _.allowUnauthorized,
|
||||
},
|
||||
{
|
||||
itemRenderer: ({ poolId }) =>
|
||||
|
||||
@@ -4,7 +4,7 @@ import Icon from 'icon'
|
||||
import React from 'react'
|
||||
import Tooltip from 'tooltip'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import { fetchSrStats } from 'xo'
|
||||
import { DEFAULT_GRANULARITY, fetchStats, SelectGranularity } from 'stats'
|
||||
import { get } from 'lodash'
|
||||
import { Toggle } from 'form'
|
||||
import {
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
|
||||
export default class SrStats extends Component {
|
||||
state = {
|
||||
granularity: 'seconds',
|
||||
granularity: DEFAULT_GRANULARITY,
|
||||
}
|
||||
|
||||
_loop(sr = get(this.props, 'sr')) {
|
||||
@@ -33,7 +33,7 @@ export default class SrStats extends Component {
|
||||
cancelled = true
|
||||
}
|
||||
|
||||
fetchSrStats(sr, this.state.granularity).then(data => {
|
||||
fetchStats(sr, 'sr', this.state.granularity).then(data => {
|
||||
if (cancelled) {
|
||||
return
|
||||
}
|
||||
@@ -62,7 +62,7 @@ export default class SrStats extends Component {
|
||||
clearTimeout(this.timeout)
|
||||
}
|
||||
|
||||
_onGranularityChange = ({ target: { value: granularity } }) => {
|
||||
_onGranularityChange = granularity => {
|
||||
clearTimeout(this.timeout)
|
||||
this.setState(
|
||||
{
|
||||
@@ -104,26 +104,11 @@ export default class SrStats extends Component {
|
||||
)}
|
||||
</Col>
|
||||
<Col mediumSize={6}>
|
||||
<div className='btn-tab'>
|
||||
<select
|
||||
className='form-control'
|
||||
onChange={this._onGranularityChange}
|
||||
defaultValue={granularity}
|
||||
>
|
||||
{_('statLastTenMinutes', message => (
|
||||
<option value='seconds'>{message}</option>
|
||||
))}
|
||||
{_('statLastTwoHours', message => (
|
||||
<option value='minutes'>{message}</option>
|
||||
))}
|
||||
{_('statLastWeek', message => (
|
||||
<option value='hours'>{message}</option>
|
||||
))}
|
||||
{_('statLastYear', message => (
|
||||
<option value='days'>{message}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<SelectGranularity
|
||||
onChange={this._onGranularityChange}
|
||||
required
|
||||
value={granularity}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
|
||||
@@ -861,7 +861,7 @@ export default class TabAdvanced extends Component {
|
||||
<td>
|
||||
<Number
|
||||
value={vm.CPUs.number}
|
||||
onChange={cpus => editVm(vm, { cpus })}
|
||||
onChange={CPUs => editVm(vm, { CPUs })}
|
||||
/>
|
||||
/
|
||||
{vm.power_state === 'Running' ? (
|
||||
@@ -869,9 +869,7 @@ export default class TabAdvanced extends Component {
|
||||
) : (
|
||||
<Number
|
||||
value={vm.CPUs.max}
|
||||
onChange={cpusStaticMax =>
|
||||
editVm(vm, { cpusStaticMax })
|
||||
}
|
||||
onChange={cpusMax => editVm(vm, { cpusMax })}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
|
||||
@@ -14,6 +14,7 @@ import { FormattedRelative, FormattedDate } from 'react-intl'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import { Number, Size } from 'editable'
|
||||
import {
|
||||
createCollectionWrapper,
|
||||
createFinder,
|
||||
createGetObjectsOfType,
|
||||
createGetVmLastShutdownTime,
|
||||
@@ -48,6 +49,15 @@ export default connectStore(() => {
|
||||
|
||||
return {
|
||||
lastShutdownTime: createGetVmLastShutdownTime(),
|
||||
tasks: createGetObjectsOfType('task')
|
||||
.pick(
|
||||
createSelector(
|
||||
(_, { vm }) => vm.current_operations,
|
||||
createCollectionWrapper(Object.keys)
|
||||
)
|
||||
)
|
||||
.filter({ status: 'pending' })
|
||||
.sort(),
|
||||
vgpu: getAttachedVgpu,
|
||||
vgpuTypes: getVgpuTypes,
|
||||
}
|
||||
@@ -55,6 +65,7 @@ export default connectStore(() => {
|
||||
({
|
||||
lastShutdownTime,
|
||||
statsOverview,
|
||||
tasks,
|
||||
vgpu,
|
||||
vgpuTypes,
|
||||
vm,
|
||||
@@ -63,7 +74,6 @@ export default connectStore(() => {
|
||||
const {
|
||||
addresses,
|
||||
CPUs: cpus,
|
||||
current_operations: currentOperations,
|
||||
id,
|
||||
installTime,
|
||||
memory,
|
||||
@@ -221,12 +231,18 @@ export default connectStore(() => {
|
||||
</h2>
|
||||
</Col>
|
||||
</Row>
|
||||
{isEmpty(currentOperations) ? null : (
|
||||
{isEmpty(tasks) ? null : (
|
||||
<Row className='text-xs-center'>
|
||||
<Col>
|
||||
<h4>
|
||||
{_('vmCurrentStatus')} {map(currentOperations)[0]}
|
||||
</h4>
|
||||
<h4>{_('vmCurrentStatus')}</h4>
|
||||
{map(tasks, task => (
|
||||
<p>
|
||||
<strong>{task.name_label}</strong>
|
||||
{task.progress > 0 && (
|
||||
<span>: {Math.round(task.progress * 100)}%</span>
|
||||
)}
|
||||
</p>
|
||||
))}
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import _, { messages } from 'intl'
|
||||
import _ from 'intl'
|
||||
import Component from 'base-component'
|
||||
import Icon from 'icon'
|
||||
import React from 'react'
|
||||
import Tooltip from 'tooltip'
|
||||
import { fetchVmStats } from 'xo'
|
||||
import { Toggle } from 'form'
|
||||
import { injectIntl } from 'react-intl'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import { DEFAULT_GRANULARITY, fetchStats, SelectGranularity } from 'stats'
|
||||
import { Toggle } from 'form'
|
||||
import {
|
||||
CpuLineChart,
|
||||
MemoryLineChart,
|
||||
@@ -14,169 +13,150 @@ import {
|
||||
XvdLineChart,
|
||||
} from 'xo-line-chart'
|
||||
|
||||
export default injectIntl(
|
||||
class VmStats extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state.useCombinedValues = false
|
||||
export default class VmStats extends Component {
|
||||
state = {
|
||||
granularity: DEFAULT_GRANULARITY,
|
||||
useCombinedValues: false,
|
||||
}
|
||||
|
||||
loop(vm = this.props.vm) {
|
||||
if (this.cancel) {
|
||||
this.cancel()
|
||||
}
|
||||
|
||||
loop(vm = this.props.vm) {
|
||||
if (this.cancel) {
|
||||
this.cancel()
|
||||
}
|
||||
if (vm.power_state !== 'Running') {
|
||||
return
|
||||
}
|
||||
|
||||
if (vm.power_state !== 'Running') {
|
||||
let cancelled = false
|
||||
this.cancel = () => {
|
||||
cancelled = true
|
||||
}
|
||||
|
||||
fetchStats(vm, 'vm', this.state.granularity).then(stats => {
|
||||
if (cancelled) {
|
||||
return
|
||||
}
|
||||
this.cancel = null
|
||||
|
||||
let cancelled = false
|
||||
this.cancel = () => {
|
||||
cancelled = true
|
||||
}
|
||||
|
||||
fetchVmStats(vm, this.state.granularity).then(stats => {
|
||||
if (cancelled) {
|
||||
return
|
||||
}
|
||||
this.cancel = null
|
||||
|
||||
clearTimeout(this.timeout)
|
||||
this.setState(
|
||||
{
|
||||
stats,
|
||||
selectStatsLoading: false,
|
||||
},
|
||||
() => {
|
||||
this.timeout = setTimeout(this.loop, stats.interval * 1000)
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
loop = ::this.loop
|
||||
|
||||
componentWillMount() {
|
||||
this.loop()
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
clearTimeout(this.timeout)
|
||||
}
|
||||
|
||||
componentWillReceiveProps(props) {
|
||||
const vmCur = this.props.vm
|
||||
const vmNext = props.vm
|
||||
|
||||
if (vmCur.power_state !== 'Running' && vmNext.power_state === 'Running') {
|
||||
this.loop(vmNext)
|
||||
} else if (
|
||||
vmCur.power_state === 'Running' &&
|
||||
vmNext.power_state !== 'Running'
|
||||
) {
|
||||
this.setState({
|
||||
stats: undefined,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
handleSelectStats(event) {
|
||||
const granularity = event.target.value
|
||||
clearTimeout(this.timeout)
|
||||
|
||||
this.setState(
|
||||
{
|
||||
granularity,
|
||||
selectStatsLoading: true,
|
||||
stats,
|
||||
selectStatsLoading: false,
|
||||
},
|
||||
this.loop
|
||||
() => {
|
||||
this.timeout = setTimeout(this.loop, stats.interval * 1000)
|
||||
}
|
||||
)
|
||||
}
|
||||
handleSelectStats = ::this.handleSelectStats
|
||||
})
|
||||
}
|
||||
loop = ::this.loop
|
||||
|
||||
render() {
|
||||
const { intl } = this.props
|
||||
const {
|
||||
granularity,
|
||||
selectStatsLoading,
|
||||
stats,
|
||||
useCombinedValues,
|
||||
} = this.state
|
||||
componentWillMount() {
|
||||
this.loop()
|
||||
}
|
||||
|
||||
return !stats ? (
|
||||
<p>No stats.</p>
|
||||
) : (
|
||||
<Container>
|
||||
<Row>
|
||||
<Col mediumSize={6}>
|
||||
<div className='form-group'>
|
||||
<Tooltip content={_('useStackedValuesOnStats')}>
|
||||
<Toggle
|
||||
value={useCombinedValues}
|
||||
onChange={this.linkState('useCombinedValues')}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
{selectStatsLoading && (
|
||||
<div className='text-xs-right'>
|
||||
<Icon icon='loading' size={2} />
|
||||
</div>
|
||||
)}
|
||||
</Col>
|
||||
<Col mediumSize={6}>
|
||||
<div className='btn-tab'>
|
||||
<select
|
||||
className='form-control'
|
||||
onChange={this.handleSelectStats}
|
||||
defaultValue={granularity}
|
||||
>
|
||||
<option value='seconds'>
|
||||
{intl.formatMessage(messages.statLastTenMinutes)}
|
||||
</option>
|
||||
<option value='minutes'>
|
||||
{intl.formatMessage(messages.statLastTwoHours)}
|
||||
</option>
|
||||
<option value='hours'>
|
||||
{intl.formatMessage(messages.statLastWeek)}
|
||||
</option>
|
||||
<option value='days'>
|
||||
{intl.formatMessage(messages.statLastYear)}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col mediumSize={6}>
|
||||
<h5 className='text-xs-center'>
|
||||
<Icon icon='cpu' size={1} /> {_('statsCpu')}
|
||||
</h5>
|
||||
<CpuLineChart addSumSeries={useCombinedValues} data={stats} />
|
||||
</Col>
|
||||
<Col mediumSize={6}>
|
||||
<h5 className='text-xs-center'>
|
||||
<Icon icon='memory' size={1} /> {_('statsMemory')}
|
||||
</h5>
|
||||
<MemoryLineChart data={stats} />
|
||||
</Col>
|
||||
</Row>
|
||||
<br />
|
||||
<hr />
|
||||
<Row>
|
||||
<Col mediumSize={6}>
|
||||
<h5 className='text-xs-center'>
|
||||
<Icon icon='network' size={1} /> {_('statsNetwork')}
|
||||
</h5>
|
||||
<VifLineChart addSumSeries={useCombinedValues} data={stats} />
|
||||
</Col>
|
||||
<Col mediumSize={6}>
|
||||
<h5 className='text-xs-center'>
|
||||
<Icon icon='disk' size={1} /> {_('statDisk')}
|
||||
</h5>
|
||||
<XvdLineChart addSumSeries={useCombinedValues} data={stats} />
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
)
|
||||
componentWillUnmount() {
|
||||
clearTimeout(this.timeout)
|
||||
}
|
||||
|
||||
componentWillReceiveProps(props) {
|
||||
const vmCur = this.props.vm
|
||||
const vmNext = props.vm
|
||||
|
||||
if (vmCur.power_state !== 'Running' && vmNext.power_state === 'Running') {
|
||||
this.loop(vmNext)
|
||||
} else if (
|
||||
vmCur.power_state === 'Running' &&
|
||||
vmNext.power_state !== 'Running'
|
||||
) {
|
||||
this.setState({
|
||||
stats: undefined,
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
handleSelectStats(granularity) {
|
||||
clearTimeout(this.timeout)
|
||||
|
||||
this.setState(
|
||||
{
|
||||
granularity,
|
||||
selectStatsLoading: true,
|
||||
},
|
||||
this.loop
|
||||
)
|
||||
}
|
||||
handleSelectStats = ::this.handleSelectStats
|
||||
|
||||
render() {
|
||||
const {
|
||||
granularity,
|
||||
selectStatsLoading,
|
||||
stats,
|
||||
useCombinedValues,
|
||||
} = this.state
|
||||
|
||||
return !stats ? (
|
||||
<p>No stats.</p>
|
||||
) : (
|
||||
<Container>
|
||||
<Row>
|
||||
<Col mediumSize={6}>
|
||||
<div className='form-group'>
|
||||
<Tooltip content={_('useStackedValuesOnStats')}>
|
||||
<Toggle
|
||||
value={useCombinedValues}
|
||||
onChange={this.linkState('useCombinedValues')}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
{selectStatsLoading && (
|
||||
<div className='text-xs-right'>
|
||||
<Icon icon='loading' size={2} />
|
||||
</div>
|
||||
)}
|
||||
</Col>
|
||||
<Col mediumSize={6}>
|
||||
<SelectGranularity
|
||||
onChange={this.handleSelectStats}
|
||||
required
|
||||
value={granularity}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col mediumSize={6}>
|
||||
<h5 className='text-xs-center'>
|
||||
<Icon icon='cpu' size={1} /> {_('statsCpu')}
|
||||
</h5>
|
||||
<CpuLineChart addSumSeries={useCombinedValues} data={stats} />
|
||||
</Col>
|
||||
<Col mediumSize={6}>
|
||||
<h5 className='text-xs-center'>
|
||||
<Icon icon='memory' size={1} /> {_('statsMemory')}
|
||||
</h5>
|
||||
<MemoryLineChart data={stats} />
|
||||
</Col>
|
||||
</Row>
|
||||
<br />
|
||||
<hr />
|
||||
<Row>
|
||||
<Col mediumSize={6}>
|
||||
<h5 className='text-xs-center'>
|
||||
<Icon icon='network' size={1} /> {_('statsNetwork')}
|
||||
</h5>
|
||||
<VifLineChart addSumSeries={useCombinedValues} data={stats} />
|
||||
</Col>
|
||||
<Col mediumSize={6}>
|
||||
<h5 className='text-xs-center'>
|
||||
<Icon icon='disk' size={1} /> {_('statDisk')}
|
||||
</h5>
|
||||
<XvdLineChart addSumSeries={useCombinedValues} data={stats} />
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user