Compare commits

..

7 Commits

Author SHA1 Message Date
Mohamedox
6bf2399495 adapt to PR comments 2019-06-27 17:03:08 +02:00
Mohamedox
76fb2a159d fix 2019-06-27 16:09:52 +02:00
Mohamedox
f8a7249e32 fix 2019-06-27 15:16:30 +02:00
Mohamedox
4c2dd689e4 fix 2019-06-27 15:03:55 +02:00
Mohamedox
456e55d54a add enable/disable button 2019-06-27 14:16:31 +02:00
Mohamedox
862e382c12 chore(xen-server/xen-servers): disconnect server if events not been fetched since 5 minutes 2019-06-27 12:09:47 +02:00
Mohamedox
203a5c681e feat(xen-api): Expose last (successful/failed) events time 2019-06-26 16:38:36 +02:00
92 changed files with 1264 additions and 3033 deletions

View File

@@ -16,7 +16,7 @@
}, },
"dependencies": { "dependencies": {
"golike-defer": "^0.4.1", "golike-defer": "^0.4.1",
"xen-api": "^0.26.0" "xen-api": "^0.25.2"
}, },
"scripts": { "scripts": {
"postversion": "npm publish" "postversion": "npm publish"

View File

@@ -1,6 +1,6 @@
{ {
"name": "@xen-orchestra/fs", "name": "@xen-orchestra/fs",
"version": "0.10.0", "version": "0.9.0",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"description": "The File System for Xen Orchestra backups.", "description": "The File System for Xen Orchestra backups.",
"keywords": [], "keywords": [],
@@ -28,9 +28,8 @@
"execa": "^1.0.0", "execa": "^1.0.0",
"fs-extra": "^8.0.1", "fs-extra": "^8.0.1",
"get-stream": "^4.0.0", "get-stream": "^4.0.0",
"limit-concurrency-decorator": "^0.4.0",
"lodash": "^4.17.4", "lodash": "^4.17.4",
"promise-toolbox": "^0.13.0", "promise-toolbox": "^0.12.1",
"readable-stream": "^3.0.6", "readable-stream": "^3.0.6",
"through2": "^3.0.0", "through2": "^3.0.0",
"tmp": "^0.1.0", "tmp": "^0.1.0",
@@ -41,7 +40,6 @@
"@babel/core": "^7.0.0", "@babel/core": "^7.0.0",
"@babel/plugin-proposal-decorators": "^7.1.6", "@babel/plugin-proposal-decorators": "^7.1.6",
"@babel/plugin-proposal-function-bind": "^7.0.0", "@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-env": "^7.0.0",
"@babel/preset-flow": "^7.0.0", "@babel/preset-flow": "^7.0.0",
"async-iterator-to-stream": "^1.1.0", "async-iterator-to-stream": "^1.1.0",

View File

@@ -4,7 +4,6 @@
import getStream from 'get-stream' import getStream from 'get-stream'
import asyncMap from '@xen-orchestra/async-map' import asyncMap from '@xen-orchestra/async-map'
import limit from 'limit-concurrency-decorator'
import path from 'path' import path from 'path'
import synchronized from 'decorator-synchronized' import synchronized from 'decorator-synchronized'
import { fromCallback, fromEvent, ignoreErrors, timeout } from 'promise-toolbox' import { fromCallback, fromEvent, ignoreErrors, timeout } from 'promise-toolbox'
@@ -32,7 +31,6 @@ const computeRate = (hrtime: number[], size: number) => {
} }
const DEFAULT_TIMEOUT = 6e5 // 10 min const DEFAULT_TIMEOUT = 6e5 // 10 min
const DEFAULT_MAX_PARALLEL_OPERATIONS = 10
const ignoreEnoent = error => { const ignoreEnoent = error => {
if (error == null || error.code !== 'ENOENT') { if (error == null || error.code !== 'ENOENT') {
@@ -85,25 +83,6 @@ export default class RemoteHandlerAbstract {
} }
} }
;({ timeout: this._timeout = DEFAULT_TIMEOUT } = options) ;({ 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 // Public members

View File

@@ -24,19 +24,6 @@ log.info('this information is relevant to the user')
log.warn('something went wrong but did not prevent current action') log.warn('something went wrong but did not prevent current action')
log.error('something went wrong') log.error('something went wrong')
log.fatal('service/app is going down') 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: Then, at application level, configure the logs are handled:

View File

@@ -31,7 +31,7 @@
}, },
"dependencies": { "dependencies": {
"lodash": "^4.17.4", "lodash": "^4.17.4",
"promise-toolbox": "^0.13.0" "promise-toolbox": "^0.12.1"
}, },
"devDependencies": { "devDependencies": {
"@babel/cli": "^7.0.0", "@babel/cli": "^7.0.0",

View File

@@ -1,12 +1,10 @@
import LEVELS, { NAMES } from '../levels' import LEVELS, { NAMES } from '../levels'
// Bind console methods (necessary for browsers) // Bind console methods (necessary for browsers)
/* eslint-disable no-console */
const debugConsole = console.log.bind(console) const debugConsole = console.log.bind(console)
const infoConsole = console.info.bind(console) const infoConsole = console.info.bind(console)
const warnConsole = console.warn.bind(console) const warnConsole = console.warn.bind(console)
const errorConsole = console.error.bind(console) const errorConsole = console.error.bind(console)
/* eslint-enable no-console */
const { ERROR, INFO, WARN } = LEVELS const { ERROR, INFO, WARN } = LEVELS

View File

@@ -1,6 +1,7 @@
import fromCallback from 'promise-toolbox/fromCallback' import fromCallback from 'promise-toolbox/fromCallback'
import splitHost from 'split-host' import splitHost from 'split-host' // eslint-disable-line node/no-extraneous-import node/no-missing-import
import { createClient, Facility, Severity, Transport } from 'syslog-client' 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 LEVELS from '../levels' import LEVELS from '../levels'
@@ -18,10 +19,10 @@ const facility = Facility.User
export default target => { export default target => {
const opts = {} const opts = {}
if (target !== undefined) { if (target !== undefined) {
if (target.startsWith('tcp://')) { if (startsWith(target, 'tcp://')) {
target = target.slice(6) target = target.slice(6)
opts.transport = Transport.Tcp opts.transport = Transport.Tcp
} else if (target.startsWith('udp://')) { } else if (startsWith(target, 'udp://')) {
target = target.slice(6) target = target.slice(6)
opts.transport = Transport.Udp opts.transport = Transport.Udp
} }

View File

@@ -4,52 +4,21 @@
### Enhancements ### Enhancements
### Bug fixes
### Released packages
## **5.36.0** (2019-06-27)
![Channel: latest](https://badgen.net/badge/channel/latest/yellow)
### 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)) - [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 ### 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)) - [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 ### Released packages
- xo-server-auth-ldap v0.6.5 - xen-api v0.25.2
- xen-api v0.26.0 - xo-server v5.43.0
- xo-server-sdn-controller v0.1 - xo-web v5.43.0
- 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) ## **5.35.0** (2019-05-29)
![Channel: stable](https://badgen.net/badge/channel/stable/green) ![Channel: latest](https://badgen.net/badge/channel/latest/yellow)
### Enhancements ### Enhancements
@@ -84,6 +53,8 @@
## **5.34.0** (2019-04-30) ## **5.34.0** (2019-04-30)
![Channel: stable](https://badgen.net/badge/channel/stable/green)
### Highlights ### 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)) - [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))

View File

@@ -1,37 +1,28 @@
> This file contains all changes that have not been released yet. > 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 ### Enhancements
> Users must be able to say: “Nice enhancement, I'm eager to test it” - [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))
- [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)) - [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))
- [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)) - [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 ### Bug fixes
> Users must be able to say: “I had this issue, happy to know it's fixed” - [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))
- [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 ### Released packages
> 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 - xen-api v0.26.0
- xo-server v5.45.0 - xo-server-auth-saml v0.6.0
- xo-web v5.45.0 - xo-server-backup-reports v0.16.2
- xo-server v5.44.0
- xo-web v5.44.0

View File

@@ -6,8 +6,8 @@
"babel-eslint": "^10.0.1", "babel-eslint": "^10.0.1",
"babel-jest": "^24.1.0", "babel-jest": "^24.1.0",
"benchmark": "^2.1.4", "benchmark": "^2.1.4",
"eslint": "^6.0.1", "eslint": "^5.1.0",
"eslint-config-prettier": "^6.0.0", "eslint-config-prettier": "^4.1.0",
"eslint-config-standard": "12.0.0", "eslint-config-standard": "12.0.0",
"eslint-config-standard-jsx": "^6.0.2", "eslint-config-standard-jsx": "^6.0.2",
"eslint-plugin-eslint-comments": "^3.1.1", "eslint-plugin-eslint-comments": "^3.1.1",
@@ -17,13 +17,13 @@
"eslint-plugin-react": "^7.6.1", "eslint-plugin-react": "^7.6.1",
"eslint-plugin-standard": "^4.0.0", "eslint-plugin-standard": "^4.0.0",
"exec-promise": "^0.7.0", "exec-promise": "^0.7.0",
"flow-bin": "^0.102.0", "flow-bin": "^0.100.0",
"globby": "^10.0.0", "globby": "^9.0.0",
"husky": "^3.0.0", "husky": "^2.2.0",
"jest": "^24.1.0", "jest": "^24.1.0",
"lodash": "^4.17.4", "lodash": "^4.17.4",
"prettier": "^1.10.2", "prettier": "^1.10.2",
"promise-toolbox": "^0.13.0", "promise-toolbox": "^0.12.1",
"sorted-object": "^2.0.1" "sorted-object": "^2.0.1"
}, },
"engines": { "engines": {

View File

@@ -27,7 +27,7 @@
"node": ">=6" "node": ">=6"
}, },
"dependencies": { "dependencies": {
"@xen-orchestra/fs": "^0.10.0", "@xen-orchestra/fs": "^0.9.0",
"cli-progress": "^2.0.0", "cli-progress": "^2.0.0",
"exec-promise": "^0.7.0", "exec-promise": "^0.7.0",
"getopts": "^2.2.3", "getopts": "^2.2.3",
@@ -40,9 +40,9 @@
"@babel/preset-env": "^7.0.0", "@babel/preset-env": "^7.0.0",
"babel-plugin-lodash": "^3.3.2", "babel-plugin-lodash": "^3.3.2",
"cross-env": "^5.1.3", "cross-env": "^5.1.3",
"execa": "^2.0.2", "execa": "^1.0.0",
"index-modules": "^0.3.0", "index-modules": "^0.3.0",
"promise-toolbox": "^0.13.0", "promise-toolbox": "^0.12.1",
"rimraf": "^2.6.1", "rimraf": "^2.6.1",
"tmp": "^0.1.0" "tmp": "^0.1.0"
}, },

View File

@@ -26,7 +26,7 @@
"from2": "^2.3.0", "from2": "^2.3.0",
"fs-extra": "^8.0.1", "fs-extra": "^8.0.1",
"limit-concurrency-decorator": "^0.4.0", "limit-concurrency-decorator": "^0.4.0",
"promise-toolbox": "^0.13.0", "promise-toolbox": "^0.12.1",
"struct-fu": "^1.2.0", "struct-fu": "^1.2.0",
"uuid": "^3.0.1" "uuid": "^3.0.1"
}, },
@@ -35,10 +35,10 @@
"@babel/core": "^7.0.0", "@babel/core": "^7.0.0",
"@babel/preset-env": "^7.0.0", "@babel/preset-env": "^7.0.0",
"@babel/preset-flow": "^7.0.0", "@babel/preset-flow": "^7.0.0",
"@xen-orchestra/fs": "^0.10.0", "@xen-orchestra/fs": "^0.9.0",
"babel-plugin-lodash": "^3.3.2", "babel-plugin-lodash": "^3.3.2",
"cross-env": "^5.1.3", "cross-env": "^5.1.3",
"execa": "^2.0.2", "execa": "^1.0.0",
"fs-promise": "^2.0.0", "fs-promise": "^2.0.0",
"get-stream": "^5.1.0", "get-stream": "^5.1.0",
"index-modules": "^0.3.0", "index-modules": "^0.3.0",

View File

@@ -364,7 +364,9 @@ export default class Vhd {
const offset = blockAddr + this.sectorsOfBitmap + beginSectorId const offset = blockAddr + this.sectorsOfBitmap + beginSectorId
debug( 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) { for (let i = beginSectorId; i < endSectorId; ++i) {

View File

@@ -41,7 +41,7 @@
"human-format": "^0.10.0", "human-format": "^0.10.0",
"lodash": "^4.17.4", "lodash": "^4.17.4",
"pw": "^0.0.4", "pw": "^0.0.4",
"xen-api": "^0.26.0" "xen-api": "^0.25.2"
}, },
"devDependencies": { "devDependencies": {
"@babel/cli": "^7.1.5", "@babel/cli": "^7.1.5",

View File

@@ -1,6 +1,6 @@
{ {
"name": "xen-api", "name": "xen-api",
"version": "0.26.0", "version": "0.25.2",
"license": "ISC", "license": "ISC",
"description": "Connector to the Xen API", "description": "Connector to the Xen API",
"keywords": [ "keywords": [
@@ -46,7 +46,7 @@
"make-error": "^1.3.0", "make-error": "^1.3.0",
"minimist": "^1.2.0", "minimist": "^1.2.0",
"ms": "^2.1.1", "ms": "^2.1.1",
"promise-toolbox": "^0.13.0", "promise-toolbox": "^0.12.1",
"pw": "0.0.4", "pw": "0.0.4",
"xmlrpc": "^1.3.2", "xmlrpc": "^1.3.2",
"xo-collection": "^0.4.1" "xo-collection": "^0.4.1"

View File

@@ -99,8 +99,8 @@ export class Xapi extends EventEmitter {
this._sessionId = undefined this._sessionId = undefined
this._status = DISCONNECTED this._status = DISCONNECTED
this._watchEventsError = undefined this._lastCatchedEventError = null
this._lastEventFetchedTimestamp = undefined this._lastSuccessfulFetchTime = undefined
this._debounce = opts.debounce ?? 200 this._debounce = opts.debounce ?? 200
this._objects = new Collection() this._objects = new Collection()
@@ -171,6 +171,22 @@ export class Xapi extends EventEmitter {
try { try {
await this._sessionOpen() await this._sessionOpen()
// Uses introspection to list available types.
const types = (this._types = (await this._interruptOnDisconnect(
this._call('system.listMethods')
))
.filter(isGetAllRecordsMethod)
.map(method => method.slice(0, method.indexOf('.'))))
this._lcToTypes = { __proto__: null }
types.forEach(type => {
const lcType = type.toLowerCase()
if (lcType !== type) {
this._lcToTypes[lcType] = type
}
})
this._pool = (await this.getAllRecords('pool'))[0]
debug('%s: connected', this._humanId) debug('%s: connected', this._humanId)
this._status = CONNECTED this._status = CONNECTED
this._resolveConnected() this._resolveConnected()
@@ -482,14 +498,6 @@ export class Xapi extends EventEmitter {
return this._objectsFetched return this._objectsFetched
} }
get lastEventFetchedTimestamp() {
return this._lastEventFetchedTimestamp
}
get watchEventsError() {
return this._watchEventsError
}
// ensure we have received all events up to this call // ensure we have received all events up to this call
// //
// optionally returns the up to date object for the given ref // optionally returns the up to date object for the given ref
@@ -580,6 +588,14 @@ export class Xapi extends EventEmitter {
throw new Error('no object with UUID: ' + uuid) throw new Error('no object with UUID: ' + uuid)
} }
get lastCatchedEventError() {
return this._lastCatchedEventError
}
get lastSuccessfulFetchTime() {
return this._lastSuccessfulFetchTime
}
// manually run events watching if set to `false` in constructor // manually run events watching if set to `false` in constructor
watchEvents() { watchEvents() {
ignoreErrors.call(this._watchEvents()) ignoreErrors.call(this._watchEvents())
@@ -734,28 +750,6 @@ export class Xapi extends EventEmitter {
}, },
} }
) )
const oldPoolRef = this._pool?.$ref
this._pool = (await this.getAllRecords('pool'))[0]
// if the pool ref has changed, it means that the XAPI has been restarted or
// it's not the same XAPI, we need to refetch the available types and reset
// the event loop in that case
if (this._pool.$ref !== oldPoolRef) {
// Uses introspection to list available types.
const types = (this._types = (await this._interruptOnDisconnect(
this._call('system.listMethods')
))
.filter(isGetAllRecordsMethod)
.map(method => method.slice(0, method.indexOf('.'))))
this._lcToTypes = { __proto__: null }
types.forEach(type => {
const lcType = type.toLowerCase()
if (lcType !== type) {
this._lcToTypes[lcType] = type
}
})
}
} }
_setUrl(url) { _setUrl(url) {
@@ -953,28 +947,23 @@ export class Xapi extends EventEmitter {
let result let result
try { try {
// don't use _sessionCall because a session failure should break the result = await this._sessionCall(
// loop and trigger a complete refetch
result = await this._call(
'event.from', 'event.from',
[ [
this._sessionId,
types, types,
fromToken, fromToken,
EVENT_TIMEOUT + 0.1, // must be float for XML-RPC transport EVENT_TIMEOUT + 0.1, // must be float for XML-RPC transport
], ],
EVENT_TIMEOUT * 1e3 * 1.1 EVENT_TIMEOUT * 1e3 * 1.1
) )
this._lastEventFetchedTimestamp = Date.now() this._lastSuccessfulFetchTime = Date.now()
this._watchEventsError = undefined
} catch (error) { } catch (error) {
const code = error?.code if (error?.code === 'EVENTS_LOST') {
if (code === 'EVENTS_LOST' || code === 'SESSION_INVALID') {
// eslint-disable-next-line no-labels // eslint-disable-next-line no-labels
continue mainLoop continue mainLoop
} }
this._watchEventsError = error this._lastCatchedEventError = error
console.warn('_watchEvents', error) console.warn('_watchEvents', error)
await pDelay(this._eventPollDelay) await pDelay(this._eventPollDelay)
continue continue

View File

@@ -43,7 +43,7 @@
"nice-pipe": "0.0.0", "nice-pipe": "0.0.0",
"pretty-ms": "^4.0.0", "pretty-ms": "^4.0.0",
"progress-stream": "^2.0.0", "progress-stream": "^2.0.0",
"promise-toolbox": "^0.13.0", "promise-toolbox": "^0.12.1",
"pump": "^3.0.0", "pump": "^3.0.0",
"pw": "^0.0.4", "pw": "^0.0.4",
"strip-indent": "^2.0.0", "strip-indent": "^2.0.0",

View File

@@ -24,6 +24,7 @@ const nicePipe = require('nice-pipe')
const pairs = require('lodash/toPairs') const pairs = require('lodash/toPairs')
const pick = require('lodash/pick') const pick = require('lodash/pick')
const pump = require('pump') const pump = require('pump')
const startsWith = require('lodash/startsWith')
const prettyMs = require('pretty-ms') const prettyMs = require('pretty-ms')
const progressStream = require('progress-stream') const progressStream = require('progress-stream')
const pw = require('pw') const pw = require('pw')
@@ -80,7 +81,7 @@ function parseParameters(args) {
const name = matches[1] const name = matches[1]
let value = matches[2] let value = matches[2]
if (value.startsWith('json:')) { if (startsWith(value, 'json:')) {
value = JSON.parse(value.slice(5)) value = JSON.parse(value.slice(5))
} }

View File

@@ -1,5 +1,6 @@
import JsonRpcWebSocketClient, { OPEN, CLOSED } from 'jsonrpc-websocket-client' import JsonRpcWebSocketClient, { OPEN, CLOSED } from 'jsonrpc-websocket-client'
import { BaseError } from 'make-error' import { BaseError } from 'make-error'
import { startsWith } from 'lodash'
// =================================================================== // ===================================================================
@@ -34,7 +35,7 @@ export default class Xo extends JsonRpcWebSocketClient {
} }
call(method, args, i) { call(method, args, i) {
if (method.startsWith('session.')) { if (startsWith(method, 'session.')) {
return Promise.reject( return Promise.reject(
new XoError('session.*() methods are disabled from this interface') new XoError('session.*() methods are disabled from this interface')
) )

View File

@@ -1,6 +1,6 @@
{ {
"name": "xo-server-auth-ldap", "name": "xo-server-auth-ldap",
"version": "0.6.5", "version": "0.6.4",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"description": "LDAP authentication plugin for XO-Server", "description": "LDAP authentication plugin for XO-Server",
"keywords": [ "keywords": [
@@ -39,7 +39,7 @@
"inquirer": "^6.0.0", "inquirer": "^6.0.0",
"ldapjs": "^1.0.1", "ldapjs": "^1.0.1",
"lodash": "^4.17.4", "lodash": "^4.17.4",
"promise-toolbox": "^0.13.0" "promise-toolbox": "^0.12.1"
}, },
"devDependencies": { "devDependencies": {
"@babel/cli": "^7.0.0", "@babel/cli": "^7.0.0",

View File

@@ -230,9 +230,10 @@ class AuthLdap {
logger(`attempting to bind as ${entry.objectName}`) logger(`attempting to bind as ${entry.objectName}`)
await bind(entry.objectName, password) await bind(entry.objectName, password)
logger( logger(
`successfully bound as ${entry.objectName} => ${username} authenticated` `successfully bound as ${
entry.objectName
} => ${username} authenticated`
) )
logger(JSON.stringify(entry, null, 2))
return { username } return { username }
} catch (error) { } catch (error) {
logger(`failed to bind as ${entry.objectName}: ${error.message}`) logger(`failed to bind as ${entry.objectName}: ${error.message}`)

View File

@@ -1,6 +1,6 @@
{ {
"name": "xo-server-auth-saml", "name": "xo-server-auth-saml",
"version": "0.6.0", "version": "0.5.3",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"description": "SAML authentication plugin for XO-Server", "description": "SAML authentication plugin for XO-Server",
"keywords": [ "keywords": [

View File

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

View File

@@ -357,7 +357,9 @@ class BackupReportsXoPlugin {
nagiosStatus: log.status === 'success' ? 0 : 2, nagiosStatus: log.status === 'success' ? 0 : 2,
nagiosMarkdown: nagiosMarkdown:
log.status === 'success' 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 ${ : `[Xen Orchestra] [${log.status}] Metadata backup report for ${
log.jobName log.jobName
} - ${nagiosText.join(' ')}`, } - ${nagiosText.join(' ')}`,
@@ -391,7 +393,9 @@ class BackupReportsXoPlugin {
} Backup report for ${jobName} ${STATUS_ICON[log.status]}`, } Backup report for ${jobName} ${STATUS_ICON[log.status]}`,
markdown: toMarkdown(markdown), markdown: toMarkdown(markdown),
nagiosStatus: 2, 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}`,
}) })
} }
@@ -709,7 +713,9 @@ class BackupReportsXoPlugin {
subject: `[Xen Orchestra] ${globalStatus} ${icon}`, subject: `[Xen Orchestra] ${globalStatus} ${icon}`,
markdown, markdown,
nagiosStatus: 2, nagiosStatus: 2,
nagiosMarkdown: `[Xen Orchestra] [${globalStatus}] Error : ${error.message}`, nagiosMarkdown: `[Xen Orchestra] [${globalStatus}] Error : ${
error.message
}`,
}) })
} }

View File

@@ -189,7 +189,9 @@ export default class DensityPlan extends Plan {
const { vm, destination } = move const { vm, destination } = move
const xapiDest = this.xo.getXapi(destination) const xapiDest = this.xo.getXapi(destination)
debug( 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( return xapiDest.migrateVm(
vm._xapiId, vm._xapiId,

View File

@@ -126,7 +126,9 @@ export default class PerformancePlan extends Plan {
destinationAverages.memoryFree -= vmAverages.memory destinationAverages.memoryFree -= vmAverages.memory
debug( 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++ optimizationsCount++
@@ -141,7 +143,9 @@ export default class PerformancePlan extends Plan {
await Promise.all(promises) await Promise.all(promises)
debug( debug(
`Performance mode: ${optimizationsCount} optimizations for Host (${exceededHost.id}).` `Performance mode: ${optimizationsCount} optimizations for Host (${
exceededHost.id
}).`
) )
} }
} }

View File

@@ -183,7 +183,9 @@ export const configurationSchema = {
description: Object.keys(HOST_FUNCTIONS) description: Object.keys(HOST_FUNCTIONS)
.map( .map(
k => k =>
` * ${k} (${HOST_FUNCTIONS[k].unit}): ${HOST_FUNCTIONS[k].description}` ` * ${k} (${HOST_FUNCTIONS[k].unit}): ${
HOST_FUNCTIONS[k].description
}`
) )
.join('\n'), .join('\n'),
type: 'string', type: 'string',
@@ -231,7 +233,9 @@ export const configurationSchema = {
description: Object.keys(VM_FUNCTIONS) description: Object.keys(VM_FUNCTIONS)
.map( .map(
k => k =>
` * ${k} (${VM_FUNCTIONS[k].unit}): ${VM_FUNCTIONS[k].description}` ` * ${k} (${VM_FUNCTIONS[k].unit}): ${
VM_FUNCTIONS[k].description
}`
) )
.join('\n'), .join('\n'),
type: 'string', type: 'string',
@@ -280,7 +284,9 @@ export const configurationSchema = {
description: Object.keys(SR_FUNCTIONS) description: Object.keys(SR_FUNCTIONS)
.map( .map(
k => k =>
` * ${k} (${SR_FUNCTIONS[k].unit}): ${SR_FUNCTIONS[k].description}` ` * ${k} (${SR_FUNCTIONS[k].unit}): ${
SR_FUNCTIONS[k].description
}`
) )
.join('\n'), .join('\n'),
type: 'string', type: 'string',
@@ -408,7 +414,9 @@ ${monitorBodies.join('\n')}`
} }
_parseDefinition(definition) { _parseDefinition(definition) {
const alarmId = `${definition.objectType}|${definition.variableName}|${definition.alarmTriggerLevel}` const alarmId = `${definition.objectType}|${definition.variableName}|${
definition.alarmTriggerLevel
}`
const typeFunction = const typeFunction =
TYPE_FUNCTION_MAP[definition.objectType][definition.variableName] TYPE_FUNCTION_MAP[definition.objectType][definition.variableName]
const parseData = (result, uuid) => { const parseData = (result, uuid) => {
@@ -460,7 +468,9 @@ ${monitorBodies.join('\n')}`
...definition, ...definition,
alarmId, alarmId,
vmFunction: typeFunction, vmFunction: typeFunction,
title: `${typeFunction.name} ${definition.comparator} ${definition.alarmTriggerLevel}${typeFunction.unit}`, title: `${typeFunction.name} ${definition.comparator} ${
definition.alarmTriggerLevel
}${typeFunction.unit}`,
snapshot: async () => { snapshot: async () => {
return Promise.all( return Promise.all(
map(definition.uuids, async uuid => { map(definition.uuids, async uuid => {
@@ -654,7 +664,9 @@ ${entry.listItem}
subject: `[Xen Orchestra] Performance Alert ${subjectSuffix}`, subject: `[Xen Orchestra] Performance Alert ${subjectSuffix}`,
markdown: markdown:
markdownBody + 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 { } else {
throw new Error('The email alert system has a configuration issue.') throw new Error('The email alert system has a configuration issue.')

View File

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

View File

@@ -1,43 +0,0 @@
# xo-server-sdn-controller [![Build Status](https://travis-ci.org/vatesfr/xen-orchestra.png?branch=master)](https://travis-ci.org/vatesfr/xen-orchestra)
XO Server plugin that allows the creation of pool-wide private networks.
## Install
For installing XO and the plugins from the sources, please take a look at [the documentation](https://xen-orchestra.com/docs/from_the_sources.html).
## Usage
### Network creation
In the network creation view, select a `pool` and `Private network`.
Create the network.
Choice is offer between `GRE` and `VxLAN`, if `VxLAN` is chosen, then the port 4789 must be open for UDP traffic.
The following line needs to be added, if not already present, in `/etc/sysconfig/iptables` of all the hosts where `VxLAN` is wanted:
`-A xapi-INPUT -p udp -m conntrack --ctstate NEW -m udp --dport 4789 -j ACCEPT`
### Configuration
Like all other xo-server plugins, it can be configured directly via
the web interface, see [the plugin documentation](https://xen-orchestra.com/docs/plugins.html).
The plugin's configuration contains:
- `cert-dir`: A path where to find the certificates to create SSL connections with the hosts.
If none is provided, the plugin will create its own self-signed certificates.
- `override-certs:` Whether or not to uninstall an already existing SDN controller CA certificate in order to replace it by the plugin's one.
## Contributions
Contributions are *very* welcomed, either on the documentation or on
the code.
You may:
- report any [issue](https://github.com/vatesfr/xen-orchestra/issues)
you've encountered;
- fork and create a pull request.
## License
AGPL3 © [Vates SAS](http://vates.fr)

View File

@@ -1,36 +0,0 @@
{
"name": "xo-server-sdn-controller",
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/packages/xo-server-sdn-controller",
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
"repository": {
"directory": "packages/xo-server-sdn-controller",
"type": "git",
"url": "https://github.com/vatesfr/xen-orchestra.git"
},
"main": "./dist",
"scripts": {
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
"prebuild": "rimraf dist/",
"predev": "yarn run prebuild",
"prepublishOnly": "yarn run build"
},
"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.84",
"promise-toolbox": "^0.13.0"
},
"private": true
}

View File

@@ -1,780 +0,0 @@
import assert from 'assert'
import createLogger from '@xen-orchestra/log'
import NodeOpenssl from 'node-openssl-cert'
import { access, constants, readFile, writeFile } from 'fs'
import { EventEmitter } from 'events'
import { filter, find, forOwn, map } from 'lodash'
import { fromCallback, fromEvent } from 'promise-toolbox'
import { join } from 'path'
import { OvsdbClient } from './ovsdb-client'
// =============================================================================
const log = createLogger('xo:xo-server:sdn-controller')
const PROTOCOL = 'pssl'
const CA_CERT = 'ca-cert.pem'
const CLIENT_KEY = 'client-key.pem'
const CLIENT_CERT = 'client-cert.pem'
const SDN_CONTROLLER_CERT = 'sdn-controller-ca.pem'
const NB_DAYS = 9999
// =============================================================================
export const configurationSchema = {
type: 'object',
properties: {
'cert-dir': {
description: `Full path to a directory where to find: \`client-cert.pem\`,
\`client-key.pem\` and \`ca-cert.pem\` to create ssl connections with hosts.
If none is provided, the plugin will create its own self-signed certificates.`,
type: 'string',
},
'override-certs': {
description: `Replace already existing SDN controller CA certificate`,
type: 'boolean',
default: false,
},
},
}
// =============================================================================
async function fileWrite(path, data) {
await fromCallback(writeFile, path, data)
log.debug(`${path} successfully written`)
}
async function fileRead(path) {
const result = await fromCallback(readFile, path)
return result
}
async function fileExists(path) {
try {
await fromCallback(access, path, constants.F_OK)
} catch (error) {
if (error.code === 'ENOENT') {
return false
}
throw error
}
return true
}
// =============================================================================
class SDNController extends EventEmitter {
constructor({ xo, getDataDir }) {
super()
this._xo = xo
this._getDataDir = getDataDir
this._clientKey = null
this._clientCert = null
this._caCert = null
this._poolNetworks = []
this._ovsdbClients = []
this._newHosts = []
this._networks = new Map()
this._starCenters = new Map()
this._cleaners = []
this._objectsAdded = this._objectsAdded.bind(this)
this._objectsUpdated = this._objectsUpdated.bind(this)
this._overrideCerts = false
this._unsetApiMethod = null
}
// ---------------------------------------------------------------------------
async configure(configuration) {
this._overrideCerts = configuration['override-certs']
let certDirectory = configuration['cert-dir']
if (certDirectory == null) {
log.debug(`No cert-dir provided, using default self-signed certificates`)
certDirectory = await this._getDataDir()
if (!(await fileExists(join(certDirectory, CA_CERT)))) {
// If one certificate doesn't exist, none should
assert(
!(await fileExists(join(certDirectory, CLIENT_KEY))),
`${CLIENT_KEY} should not exist`
)
assert(
!(await fileExists(join(certDirectory, CLIENT_CERT))),
`${CLIENT_CERT} should not exist`
)
log.debug(`No default self-signed certificates exists, creating them`)
await this._generateCertificatesAndKey(certDirectory)
}
}
// TODO: verify certificates and create new certificates if needed
;[this._clientKey, this._clientCert, this._caCert] = await Promise.all([
fileRead(join(certDirectory, CLIENT_KEY)),
fileRead(join(certDirectory, CLIENT_CERT)),
fileRead(join(certDirectory, CA_CERT)),
])
this._ovsdbClients.forEach(client => {
client.updateCertificates(this._clientKey, this._clientCert, this._caCert)
})
const updatedPools = []
for (const poolNetwork of this._poolNetworks) {
if (updatedPools.includes(poolNetwork.pool)) {
continue
}
const xapi = this._xo.getXapi(poolNetwork.pool)
await this._installCaCertificateIfNeeded(xapi)
updatedPools.push(poolNetwork.pool)
}
}
async load() {
const createPrivateNetwork = this._createPrivateNetwork.bind(this)
createPrivateNetwork.description =
'Creates a pool-wide private network on a selected pool'
createPrivateNetwork.params = {
poolId: { type: 'string' },
networkName: { type: 'string' },
networkDescription: { type: 'string' },
encapsulation: { type: 'string' },
}
createPrivateNetwork.resolve = {
xoPool: ['poolId', 'pool', ''],
}
this._unsetApiMethod = this._xo.addApiMethod(
'plugin.SDNController.createPrivateNetwork',
createPrivateNetwork
)
// FIXME: we should monitor when xapis are added/removed
forOwn(this._xo.getAllXapis(), async xapi => {
await xapi.objectsFetched
if (this._setControllerNeeded(xapi) === false) {
this._cleaners.push(await this._manageXapi(xapi))
const hosts = filter(xapi.objects.all, { $type: 'host' })
await Promise.all(
map(hosts, async host => {
this._createOvsdbClient(host)
})
)
// Add already existing pool-wide private networks
const networks = filter(xapi.objects.all, { $type: 'network' })
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`
)
const center = await this._electNewCenter(network, true)
this._poolNetworks.push({
pool: network.$pool.$ref,
network: network.$ref,
starCenter: center?.$ref,
})
this._networks.set(network.$id, network.$ref)
if (center != null) {
this._starCenters.set(center.$id, center.$ref)
}
}
})
}
})
}
async unload() {
this._ovsdbClients = []
this._poolNetworks = []
this._newHosts = []
this._networks.clear()
this._starCenters.clear()
this._cleaners.forEach(cleaner => cleaner())
this._cleaners = []
this._unsetApiMethod()
}
// ===========================================================================
async _createPrivateNetwork({
xoPool,
networkName,
networkDescription,
encapsulation,
}) {
const pool = this._xo.getXapiObject(xoPool)
await this._setPoolControllerIfNeeded(pool)
// Create the private network
const privateNetworkRef = await pool.$xapi.call('network.create', {
name_label: networkName,
name_description: networkDescription,
MTU: 0,
other_config: {
automatic: 'false',
private_pool_wide: 'true',
encapsulation: encapsulation,
},
})
const privateNetwork = await pool.$xapi._getOrWaitObject(privateNetworkRef)
log.info(
`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
const hosts = filter(pool.$xapi.objects.all, { $type: 'host' })
await Promise.all(
map(hosts, async host => {
await this._createTunnel(host, privateNetwork)
this._createOvsdbClient(host)
})
)
const center = await this._electNewCenter(privateNetwork, false)
this._poolNetworks.push({
pool: pool.$ref,
network: privateNetwork.$ref,
starCenter: center?.$ref,
encapsulation: encapsulation,
})
this._networks.set(privateNetwork.$id, privateNetwork.$ref)
if (center != null) {
this._starCenters.set(center.$id, center.$ref)
}
}
// ---------------------------------------------------------------------------
async _manageXapi(xapi) {
const { objects } = xapi
const objectsRemovedXapi = this._objectsRemoved.bind(this, xapi)
objects.on('add', this._objectsAdded)
objects.on('update', this._objectsUpdated)
objects.on('remove', objectsRemovedXapi)
await this._installCaCertificateIfNeeded(xapi)
return () => {
objects.removeListener('add', this._objectsAdded)
objects.removeListener('update', this._objectsUpdated)
objects.removeListener('remove', objectsRemovedXapi)
}
}
async _objectsAdded(objects) {
await Promise.all(
map(objects, async object => {
const { $type } = object
if ($type === 'host') {
log.debug(
`New host: '${object.name_label}' in pool: '${object.$pool.name_label}'`
)
if (find(this._newHosts, { $ref: object.$ref }) == null) {
this._newHosts.push(object)
}
this._createOvsdbClient(object)
}
})
)
}
async _objectsUpdated(objects) {
await Promise.all(
map(objects, async (object, id) => {
const { $type } = object
if ($type === 'PIF') {
await this._pifUpdated(object)
} else if ($type === 'host') {
await this._hostUpdated(object)
}
})
)
}
async _objectsRemoved(xapi, objects) {
await Promise.all(
map(objects, async (object, id) => {
const client = find(this._ovsdbClients, { id: id })
if (client != null) {
this._ovsdbClients.splice(this._ovsdbClients.indexOf(client), 1)
}
// If a Star center host is removed: re-elect a new center where needed
const starCenterRef = this._starCenters.get(id)
if (starCenterRef != null) {
this._starCenters.delete(id)
const poolNetworks = filter(this._poolNetworks, {
starCenter: starCenterRef,
})
for (const poolNetwork of poolNetworks) {
const network = xapi.getObjectByRef(poolNetwork.network)
const newCenter = await this._electNewCenter(network, true)
poolNetwork.starCenter = newCenter?.$ref
if (newCenter != null) {
this._starCenters.set(newCenter.$id, newCenter.$ref)
}
}
return
}
// If a network is removed, clean this._poolNetworks from it
const networkRef = this._networks.get(id)
if (networkRef != null) {
this._networks.delete(id)
const poolNetwork = find(this._poolNetworks, {
network: networkRef,
})
if (poolNetwork != null) {
this._poolNetworks.splice(
this._poolNetworks.indexOf(poolNetwork),
1
)
}
}
})
)
}
async _pifUpdated(pif) {
// Only if PIF is in a private network
const poolNetwork = find(this._poolNetworks, { network: pif.network })
if (poolNetwork == null) {
return
}
if (!pif.currently_attached) {
if (poolNetwork.starCenter !== pif.host) {
return
}
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`
)
const newCenter = await this._electNewCenter(pif.$network, true)
poolNetwork.starCenter = newCenter?.$ref
this._starCenters.delete(pif.$host.$id)
if (newCenter != null) {
this._starCenters.set(newCenter.$id, newCenter.$ref)
}
} else {
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}'`
)
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`
)
const starCenter = pif.$xapi.getObjectByRef(poolNetwork.starCenter)
await this._addHostToNetwork(pif.$host, pif.$network, starCenter)
}
}
async _hostUpdated(host) {
const xapi = host.$xapi
if (host.enabled) {
if (host.PIFs.length === 0) {
return
}
const tunnels = filter(xapi.objects.all, { $type: 'tunnel' })
const newHost = find(this._newHosts, { $ref: host.$ref })
if (newHost != null) {
this._newHosts.splice(this._newHosts.indexOf(newHost), 1)
try {
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}`
)
}
}
for (const tunnel of tunnels) {
const accessPIF = xapi.getObjectByRef(tunnel.access_PIF)
if (accessPIF.host !== host.$ref) {
continue
}
const poolNetwork = find(this._poolNetworks, {
network: accessPIF.network,
})
if (poolNetwork == null) {
continue
}
if (accessPIF.currently_attached) {
continue
}
log.debug(
`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}'`
)
}
const starCenter = host.$xapi.getObjectByRef(poolNetwork.starCenter)
await this._addHostToNetwork(host, accessPIF.$network, starCenter)
}
} else {
const poolNetworks = filter(this._poolNetworks, { starCenter: host.$ref })
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`
)
const newCenter = await this._electNewCenter(network, true)
poolNetwork.starCenter = newCenter?.$ref
this._starCenters.delete(host.$id)
if (newCenter != null) {
this._starCenters.set(newCenter.$id, newCenter.$ref)
}
}
}
}
// ---------------------------------------------------------------------------
async _setPoolControllerIfNeeded(pool) {
if (!this._setControllerNeeded(pool.$xapi)) {
// Nothing to do
return
}
const controller = find(pool.$xapi.objects.all, { $type: 'SDN_controller' })
if (controller != null) {
await pool.$xapi.call('SDN_controller.forget', controller.$ref)
log.debug(`Remove old SDN controller from pool: '${pool.name_label}'`)
}
await pool.$xapi.call('SDN_controller.introduce', PROTOCOL)
log.debug(`Set SDN controller of pool: '${pool.name_label}'`)
this._cleaners.push(await this._manageXapi(pool.$xapi))
}
_setControllerNeeded(xapi) {
const controller = find(xapi.objects.all, { $type: 'SDN_controller' })
return !(
controller != null &&
controller.protocol === PROTOCOL &&
controller.address === '' &&
controller.port === 0
)
}
// ---------------------------------------------------------------------------
async _installCaCertificateIfNeeded(xapi) {
let needInstall = false
try {
const result = await xapi.call('pool.certificate_list')
if (!result.includes(SDN_CONTROLLER_CERT)) {
needInstall = true
} 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}'`
)
needInstall = true
}
} catch (error) {
log.error(
`Couldn't retrieve certificate list of pool: '${xapi.pool.name_label}'`
)
}
if (!needInstall) {
return
}
try {
await xapi.call(
'pool.certificate_install',
SDN_CONTROLLER_CERT,
this._caCert.toString()
)
await xapi.call('pool.certificate_sync')
log.debug(
`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}`
)
}
}
// ---------------------------------------------------------------------------
async _electNewCenter(network, resetNeeded) {
const pool = network.$pool
let newCenter = null
const hosts = filter(pool.$xapi.objects.all, { $type: 'host' })
await Promise.all(
map(hosts, async host => {
if (resetNeeded) {
// Clean old ports and interfaces
const hostClient = find(this._ovsdbClients, { host: host.$ref })
if (hostClient != null) {
try {
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}`
)
return
}
}
}
if (newCenter != null) {
return
}
const pif = find(host.$PIFs, { network: network.$ref })
if (pif != null && pif.currently_attached && host.enabled) {
newCenter = host
}
})
)
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`
)
return null
}
// Recreate star topology
await Promise.all(
await map(hosts, async host => {
await this._addHostToNetwork(host, network, newCenter)
})
)
log.info(
`New star center host elected: '${newCenter.name_label}' in network: '${network.name_label}'`
)
return newCenter
}
async _createTunnel(host, network) {
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}'`
)
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}'`
)
}
async _addHostToNetwork(host, network, starCenter) {
if (host.$ref === starCenter.$ref) {
// Nothing to do
return
}
const hostClient = find(this._ovsdbClients, {
host: host.$ref,
})
if (hostClient == null) {
log.error(`No OVSDB client found for host: '${host.name_label}'`)
return
}
const starCenterClient = find(this._ovsdbClients, {
host: starCenter.$ref,
})
if (starCenterClient == null) {
log.error(
`No OVSDB client found for star-center host: '${starCenter.name_label}'`
)
return
}
const encapsulation =
network.other_config.encapsulation != null
? network.other_config.encapsulation
: 'gre'
try {
await hostClient.addInterfaceAndPort(
network.uuid,
network.name_label,
starCenterClient.address,
encapsulation
)
await starCenterClient.addInterfaceAndPort(
network.uuid,
network.name_label,
hostClient.address,
encapsulation
)
} catch (error) {
log.error(
`Couldn't add host: '${host.name_label}' to network: '${network.name_label}' in pool: '${host.$pool.name_label}' because: ${error}`
)
}
}
// ---------------------------------------------------------------------------
_createOvsdbClient(host) {
const foundClient = find(this._ovsdbClients, { host: host.$ref })
if (foundClient != null) {
return foundClient
}
const client = new OvsdbClient(
host,
this._clientKey,
this._clientCert,
this._caCert
)
this._ovsdbClients.push(client)
return client
}
// ---------------------------------------------------------------------------
async _generateCertificatesAndKey(dataDir) {
const openssl = new NodeOpenssl()
const rsakeyoptions = {
rsa_keygen_bits: 4096,
format: 'PKCS8',
}
const subject = {
countryName: 'XX',
localityName: 'Default City',
organizationName: 'Default Company LTD',
}
const csroptions = {
hash: 'sha256',
startdate: new Date('1984-02-04 00:00:00'),
enddate: new Date('2143-06-04 04:16:23'),
subject: subject,
}
const cacsroptions = {
hash: 'sha256',
days: NB_DAYS,
subject: subject,
}
openssl.generateRSAPrivateKey(rsakeyoptions, (err, cakey, cmd) => {
if (err) {
log.error(`Error while generating CA private key: ${err}`)
return
}
openssl.generateCSR(cacsroptions, cakey, null, (err, csr, cmd) => {
if (err) {
log.error(`Error while generating CA certificate: ${err}`)
return
}
openssl.selfSignCSR(
csr,
cacsroptions,
cakey,
null,
async (err, cacrt, cmd) => {
if (err) {
log.error(`Error while signing CA certificate: ${err}`)
return
}
await fileWrite(join(dataDir, CA_CERT), cacrt)
openssl.generateRSAPrivateKey(
rsakeyoptions,
async (err, key, cmd) => {
if (err) {
log.error(`Error while generating private key: ${err}`)
return
}
await fileWrite(join(dataDir, CLIENT_KEY), key)
openssl.generateCSR(csroptions, key, null, (err, csr, cmd) => {
if (err) {
log.error(`Error while generating certificate: ${err}`)
return
}
openssl.CASignCSR(
csr,
cacsroptions,
false,
cacrt,
cakey,
null,
async (err, crt, cmd) => {
if (err) {
log.error(`Error while signing certificate: ${err}`)
return
}
await fileWrite(join(dataDir, CLIENT_CERT), crt)
this.emit('certWritten')
}
)
})
}
)
}
)
})
})
await fromEvent(this, 'certWritten', {})
log.debug('All certificates have been successfully written')
}
}
export default opts => new SDNController(opts)

View File

@@ -1,481 +0,0 @@
import assert from 'assert'
import createLogger from '@xen-orchestra/log'
import forOwn from 'lodash/forOwn'
import fromEvent from 'promise-toolbox/fromEvent'
import { connect } from 'tls'
const log = createLogger('xo:xo-server:sdn-controller:ovsdb-client')
const OVSDB_PORT = 6640
// =============================================================================
export class OvsdbClient {
constructor(host, clientKey, clientCert, caCert) {
this._host = host
this._numberOfPortAndInterface = 0
this._requestID = 0
this.updateCertificates(clientKey, clientCert, caCert)
log.debug(`[${this._host.name_label}] New OVSDB client`)
}
// ---------------------------------------------------------------------------
get address() {
return this._host.address
}
get host() {
return this._host.$ref
}
get id() {
return this._host.$id
}
updateCertificates(clientKey, clientCert, caCert) {
this._clientKey = clientKey
this._clientCert = clientCert
this._caCert = caCert
log.debug(`[${this._host.name_label}] Certificates have been updated`)
}
// ---------------------------------------------------------------------------
async addInterfaceAndPort(
networkUuid,
networkName,
remoteAddress,
encapsulation
) {
const socket = await this._connect()
const index = this._numberOfPortAndInterface
++this._numberOfPortAndInterface
const [bridgeUuid, bridgeName] = await this._getBridgeUuidForNetwork(
networkUuid,
networkName,
socket
)
if (bridgeUuid == null) {
socket.destroy()
return
}
const alreadyExist = await this._interfaceAndPortAlreadyExist(
bridgeUuid,
bridgeName,
remoteAddress,
socket
)
if (alreadyExist) {
socket.destroy()
return
}
const interfaceName = 'tunnel_iface' + index
const portName = 'tunnel_port' + index
// Add interface and port to the bridge
const options = ['map', [['remote_ip', remoteAddress]]]
const addInterfaceOperation = {
op: 'insert',
table: 'Interface',
row: {
type: encapsulation,
options: options,
name: interfaceName,
other_config: ['map', [['private_pool_wide', 'true']]],
},
'uuid-name': 'new_iface',
}
const addPortOperation = {
op: 'insert',
table: 'Port',
row: {
name: portName,
interfaces: ['set', [['named-uuid', 'new_iface']]],
other_config: ['map', [['private_pool_wide', 'true']]],
},
'uuid-name': 'new_port',
}
const mutateBridgeOperation = {
op: 'mutate',
table: 'Bridge',
where: [['_uuid', '==', ['uuid', bridgeUuid]]],
mutations: [['ports', 'insert', ['set', [['named-uuid', 'new_port']]]]],
}
const params = [
'Open_vSwitch',
addInterfaceOperation,
addPortOperation,
mutateBridgeOperation,
]
const jsonObjects = await this._sendOvsdbTransaction(params, socket)
if (jsonObjects == null) {
socket.destroy()
return
}
let error
let details
let i = 0
let opResult
do {
opResult = jsonObjects[0].result[i]
if (opResult != null && opResult.error != null) {
error = opResult.error
details = opResult.details
}
++i
} while (opResult && !error)
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}`
)
socket.destroy()
return
}
log.debug(
`[${this._host.name_label}] Port: '${portName}' and interface: '${interfaceName}' added to bridge: '${bridgeName}' on network: '${networkName}'`
)
socket.destroy()
}
async resetForNetwork(networkUuid, networkName) {
const socket = await this._connect()
const [bridgeUuid, bridgeName] = await this._getBridgeUuidForNetwork(
networkUuid,
networkName,
socket
)
if (bridgeUuid == null) {
socket.destroy()
return
}
// Delete old ports created by a SDN controller
const ports = await this._getBridgePorts(bridgeUuid, bridgeName, socket)
if (ports == null) {
socket.destroy()
return
}
const portsToDelete = []
for (const port of ports) {
const portUuid = port[1]
const where = [['_uuid', '==', ['uuid', portUuid]]]
const selectResult = await this._select(
'Port',
['name', 'other_config'],
where,
socket
)
if (selectResult == null) {
continue
}
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}'`
)
portsToDelete.push(['uuid', portUuid])
}
})
}
if (portsToDelete.length === 0) {
// Nothing to do
socket.destroy()
return
}
const mutateBridgeOperation = {
op: 'mutate',
table: 'Bridge',
where: [['_uuid', '==', ['uuid', bridgeUuid]]],
mutations: [['ports', 'delete', ['set', portsToDelete]]],
}
const params = ['Open_vSwitch', mutateBridgeOperation]
const jsonObjects = await this._sendOvsdbTransaction(params, socket)
if (jsonObjects == null) {
socket.destroy()
return
}
if (jsonObjects[0].error != null) {
log.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}'`
)
socket.destroy()
}
// ===========================================================================
_parseJson(chunk) {
let data = chunk.toString()
let buffer = ''
let depth = 0
let pos = 0
const objects = []
for (let i = pos; i < data.length; ++i) {
const c = data.charAt(i)
if (c === '{') {
depth++
} else if (c === '}') {
depth--
if (depth === 0) {
const object = JSON.parse(buffer + data.substr(0, i + 1))
objects.push(object)
buffer = ''
data = data.substr(i + 1)
pos = 0
i = -1
}
}
}
buffer += data
return objects
}
// ---------------------------------------------------------------------------
async _getBridgeUuidForNetwork(networkUuid, networkName, socket) {
const where = [
[
'external_ids',
'includes',
['map', [['xs-network-uuids', networkUuid]]],
],
]
const selectResult = await this._select(
'Bridge',
['_uuid', 'name'],
where,
socket
)
if (selectResult == null) {
return [null, null]
}
const bridgeUuid = selectResult._uuid[1]
const bridgeName = selectResult.name
log.debug(
`[${this._host.name_label}] Found bridge: '${bridgeName}' for network: '${networkName}'`
)
return [bridgeUuid, bridgeName]
}
async _interfaceAndPortAlreadyExist(
bridgeUuid,
bridgeName,
remoteAddress,
socket
) {
const ports = await this._getBridgePorts(bridgeUuid, bridgeName, socket)
if (ports == null) {
return
}
for (const port of ports) {
const portUuid = port[1]
const interfaces = await this._getPortInterfaces(portUuid, socket)
if (interfaces == null) {
continue
}
for (const iface of interfaces) {
const interfaceUuid = iface[1]
const hasRemote = await this._interfaceHasRemote(
interfaceUuid,
remoteAddress,
socket
)
if (hasRemote === true) {
return true
}
}
}
return false
}
async _getBridgePorts(bridgeUuid, bridgeName, socket) {
const where = [['_uuid', '==', ['uuid', bridgeUuid]]]
const selectResult = await this._select('Bridge', ['ports'], where, socket)
if (selectResult == null) {
return null
}
return selectResult.ports[0] === 'set'
? selectResult.ports[1]
: [selectResult.ports]
}
async _getPortInterfaces(portUuid, socket) {
const where = [['_uuid', '==', ['uuid', portUuid]]]
const selectResult = await this._select(
'Port',
['name', 'interfaces'],
where,
socket
)
if (selectResult == null) {
return null
}
return selectResult.interfaces[0] === 'set'
? selectResult.interfaces[1]
: [selectResult.interfaces]
}
async _interfaceHasRemote(interfaceUuid, remoteAddress, socket) {
const where = [['_uuid', '==', ['uuid', interfaceUuid]]]
const selectResult = await this._select(
'Interface',
['name', 'options'],
where,
socket
)
if (selectResult == null) {
return false
}
for (const option of selectResult.options[1]) {
if (option[0] === 'remote_ip' && option[1] === remoteAddress) {
return true
}
}
return false
}
// ---------------------------------------------------------------------------
async _select(table, columns, where, socket) {
const selectOperation = {
op: 'select',
table: table,
columns: columns,
where: where,
}
const params = ['Open_vSwitch', selectOperation]
const jsonObjects = await this._sendOvsdbTransaction(params, socket)
if (jsonObjects == null) {
return
}
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}`
)
return null
}
if (jsonResult.rows.length === 0) {
log.error(
`[${this._host.name_label}] No '${columns}' found in: '${table}' where: '${where}'`
)
return null
}
// 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}'`
)
return jsonResult.rows[0]
}
async _sendOvsdbTransaction(params, socket) {
const stream = socket
const requestId = this._requestID
++this._requestID
const req = {
id: requestId,
method: 'transact',
params: params,
}
try {
stream.write(JSON.stringify(req))
} catch (error) {
log.error(
`[${this._host.name_label}] Error while writing into stream: ${error}`
)
return null
}
let result
let jsonObjects
let resultRequestId
do {
try {
result = await fromEvent(stream, 'data', {})
} catch (error) {
log.error(
`[${this._host.name_label}] Error while waiting for stream data: ${error}`
)
return null
}
jsonObjects = this._parseJson(result)
resultRequestId = jsonObjects[0].id
} while (resultRequestId !== requestId)
return jsonObjects
}
// ---------------------------------------------------------------------------
async _connect() {
const options = {
ca: this._caCert,
key: this._clientKey,
cert: this._clientCert,
host: this._host.address,
port: OVSDB_PORT,
rejectUnauthorized: false,
requestCert: false,
}
const socket = connect(options)
try {
await fromEvent(socket, 'secureConnect', {})
} catch (error) {
log.error(
`[${this._host.name_label}] TLS connection failed because: ${error}: ${error.code}`
)
throw error
}
log.debug(`[${this._host.name_label}] TLS connection successful`)
socket.on('error', error => {
log.error(
`[${this._host.name_label}] OVSDB client socket error: ${error} with code: ${error.code}`
)
})
return socket
}
}

View File

@@ -34,7 +34,7 @@
"dependencies": { "dependencies": {
"nodemailer": "^6.1.0", "nodemailer": "^6.1.0",
"nodemailer-markdown": "^1.0.1", "nodemailer-markdown": "^1.0.1",
"promise-toolbox": "^0.13.0" "promise-toolbox": "^0.12.1"
}, },
"devDependencies": { "devDependencies": {
"@babel/cli": "^7.0.0", "@babel/cli": "^7.0.0",

View File

@@ -33,7 +33,7 @@
"node": ">=6" "node": ">=6"
}, },
"dependencies": { "dependencies": {
"promise-toolbox": "^0.13.0", "promise-toolbox": "^0.12.1",
"slack-node": "^0.1.8" "slack-node": "^0.1.8"
}, },
"devDependencies": { "devDependencies": {

View File

@@ -42,7 +42,7 @@
"html-minifier": "^4.0.0", "html-minifier": "^4.0.0",
"human-format": "^0.10.0", "human-format": "^0.10.0",
"lodash": "^4.17.4", "lodash": "^4.17.4",
"promise-toolbox": "^0.13.0" "promise-toolbox": "^0.12.1"
}, },
"devDependencies": { "devDependencies": {
"@babel/cli": "^7.0.0", "@babel/cli": "^7.0.0",

View File

@@ -29,9 +29,6 @@ guessVhdSizeOnImport = false
# be turned for investigation by the administrator. # be turned for investigation by the administrator.
verboseApiLogsOnErrors = false 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 # https:#github.com/websockets/ws#websocket-compression
[apiWebSocketOptions] [apiWebSocketOptions]
perMessageDeflate = { threshold = 524288 } # 512kiB perMessageDeflate = { threshold = 524288 } # 512kiB
@@ -90,5 +87,8 @@ mountsDir = '/run/xo-server/mounts'
# timeout in milliseconds (set to 0 to disable) # timeout in milliseconds (set to 0 to disable)
timeout = 600e3 timeout = 600e3
# timeout before reporting server issue
noEventsTimeout = '5 minutes'
# see https:#github.com/vatesfr/xen-orchestra/issues/3419 # see https:#github.com/vatesfr/xen-orchestra/issues/3419
# useSudo = false # useSudo = false

View File

@@ -1,7 +1,7 @@
{ {
"private": true, "private": true,
"name": "xo-server", "name": "xo-server",
"version": "5.44.0", "version": "5.43.0",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"description": "Server part of Xen-Orchestra", "description": "Server part of Xen-Orchestra",
"keywords": [ "keywords": [
@@ -38,7 +38,7 @@
"@xen-orchestra/cron": "^1.0.3", "@xen-orchestra/cron": "^1.0.3",
"@xen-orchestra/defined": "^0.0.0", "@xen-orchestra/defined": "^0.0.0",
"@xen-orchestra/emit-async": "^0.0.0", "@xen-orchestra/emit-async": "^0.0.0",
"@xen-orchestra/fs": "^0.10.0", "@xen-orchestra/fs": "^0.9.0",
"@xen-orchestra/log": "^0.1.4", "@xen-orchestra/log": "^0.1.4",
"@xen-orchestra/mixin": "^0.0.0", "@xen-orchestra/mixin": "^0.0.0",
"ajv": "^6.1.1", "ajv": "^6.1.1",
@@ -102,7 +102,7 @@
"passport": "^0.4.0", "passport": "^0.4.0",
"passport-local": "^1.0.0", "passport-local": "^1.0.0",
"pretty-format": "^24.0.0", "pretty-format": "^24.0.0",
"promise-toolbox": "^0.13.0", "promise-toolbox": "^0.12.1",
"proxy-agent": "^3.0.0", "proxy-agent": "^3.0.0",
"pug": "^2.0.0-rc.4", "pug": "^2.0.0-rc.4",
"pump": "^3.0.0", "pump": "^3.0.0",
@@ -123,7 +123,7 @@
"value-matcher": "^0.2.0", "value-matcher": "^0.2.0",
"vhd-lib": "^0.7.0", "vhd-lib": "^0.7.0",
"ws": "^6.0.0", "ws": "^6.0.0",
"xen-api": "^0.26.0", "xen-api": "^0.25.2",
"xml2js": "^0.4.19", "xml2js": "^0.4.19",
"xo-acl-resolver": "^0.4.1", "xo-acl-resolver": "^0.4.1",
"xo-collection": "^0.4.1", "xo-collection": "^0.4.1",

View File

@@ -123,14 +123,10 @@ getJob.params = {
export async function runJob({ export async function runJob({
id, id,
schedule, schedule,
settings,
vm, vm,
vms = vm !== undefined ? [vm] : undefined, vms = vm !== undefined ? [vm] : undefined,
}) { }) {
return this.runJobSequence([id], await this.getSchedule(schedule), { return this.runJobSequence([id], await this.getSchedule(schedule), vms)
settings,
vms,
})
} }
runJob.permission = 'admin' runJob.permission = 'admin'
@@ -142,13 +138,6 @@ runJob.params = {
schedule: { schedule: {
type: 'string', type: 'string',
}, },
settings: {
type: 'object',
properties: {
'*': { type: 'object' },
},
optional: true,
},
vm: { vm: {
type: 'string', type: 'string',
optional: true, optional: true,

View File

@@ -1,7 +1,5 @@
// TODO: too low level, move into host. // TODO: too low level, move into host.
import { filter, find } from 'lodash'
import { IPV4_CONFIG_MODES, IPV6_CONFIG_MODES } from '../xapi' import { IPV4_CONFIG_MODES, IPV6_CONFIG_MODES } from '../xapi'
export function getIpv4ConfigurationModes() { export function getIpv4ConfigurationModes() {
@@ -17,17 +15,7 @@ export function getIpv6ConfigurationModes() {
async function delete_({ pif }) { async function delete_({ pif }) {
// TODO: check if PIF is attached before // TODO: check if PIF is attached before
const xapi = this.getXapi(pif) await this.getXapi(pif).callAsync('PIF.destroy', pif._xapiRef)
const tunnels = filter(xapi.objects.all, { $type: 'tunnel' })
const tunnel = find(tunnels, { access_PIF: pif._xapiRef })
if (tunnel != null) {
await xapi.callAsync('PIF.unplug', pif._xapiRef)
await xapi.callAsync('tunnel.destroy', tunnel.$ref)
return
}
await xapi.callAsync('PIF.destroy', pif._xapiRef)
} }
export { delete_ as delete } export { delete_ as delete }

View File

@@ -168,7 +168,9 @@ export async function mergeInto({ source, target, force }) {
if (sourceHost.productBrand !== targetHost.productBrand) { if (sourceHost.productBrand !== targetHost.productBrand) {
throw new Error( 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`
) )
} }

View File

@@ -100,24 +100,20 @@ set.params = {
optional: true, optional: true,
type: 'boolean', type: 'boolean',
}, },
readOnly: {
optional: true,
type: 'boolean',
},
} }
// ------------------------------------------------------------------- // -------------------------------------------------------------------
export async function enable({ id }) { export async function connect({ id }) {
this.updateXenServer(id, { enabled: true })::ignoreErrors() this.updateXenServer(id, { enabled: true })::ignoreErrors()
await this.connectXenServer(id) await this.connectXenServer(id)
} }
enable.description = 'enable a Xen server' connect.description = 'connect a Xen server'
enable.permission = 'admin' connect.permission = 'admin'
enable.params = { connect.params = {
id: { id: {
type: 'string', type: 'string',
}, },
@@ -125,16 +121,16 @@ enable.params = {
// ------------------------------------------------------------------- // -------------------------------------------------------------------
export async function disable({ id }) { export async function disconnect({ id }) {
this.updateXenServer(id, { enabled: false })::ignoreErrors() this.updateXenServer(id, { enabled: false })::ignoreErrors()
await this.disconnectXenServer(id) await this.disconnectXenServer(id)
} }
disable.description = 'disable a Xen server' disconnect.description = 'disconnect a Xen server'
disable.permission = 'admin' disconnect.permission = 'admin'
disable.params = { disconnect.params = {
id: { id: {
type: 'string', type: 'string',
}, },

View File

@@ -1,6 +1,3 @@
import assert from 'assert'
import { fromEvent } from 'promise-toolbox'
export function getPermissionsForUser({ userId }) { export function getPermissionsForUser({ userId }) {
return this.getPermissionsForUser(userId) return this.getPermissionsForUser(userId)
} }
@@ -89,35 +86,3 @@ copyVm.resolve = {
vm: ['vm', 'VM'], vm: ['vm', 'VM'],
sr: ['sr', 'SR'], 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" },
}

View File

@@ -1134,10 +1134,7 @@ resume.resolve = {
// ------------------------------------------------------------------- // -------------------------------------------------------------------
export async function revert({ snapshot, snapshotBefore }) { export function revert({ snapshot, snapshotBefore }) {
await this.checkPermissions(this.user.id, [
[snapshot.$snapshot_of, 'operate'],
])
return this.getXapi(snapshot).revertVm(snapshot._xapiId, snapshotBefore) return this.getXapi(snapshot).revertVm(snapshot._xapiId, snapshotBefore)
} }
@@ -1147,7 +1144,7 @@ revert.params = {
} }
revert.resolve = { revert.resolve = {
snapshot: ['snapshot', 'VM-snapshot', 'view'], snapshot: ['snapshot', 'VM-snapshot', 'administrate'],
} }
// ------------------------------------------------------------------- // -------------------------------------------------------------------

View File

@@ -446,7 +446,9 @@ const createNetworkAndInsertHosts = defer(async function(
}) })
if (result.exit !== 0) { if (result.exit !== 0) {
throw invalidParameters( 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}`
) )
} }
}) })
@@ -1048,7 +1050,9 @@ export async function replaceBrick({
CURRENT_POOL_OPERATIONS[poolId] = { ...OPERATION_OBJECT, state: 1 } CURRENT_POOL_OPERATIONS[poolId] = { ...OPERATION_OBJECT, state: 1 }
await glusterCmd( await glusterCmd(
glusterEndpoint, 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) await glusterCmd(glusterEndpoint, 'peer detach ' + previousIp)
data.nodes.splice(nodeIndex, 1, { data.nodes.splice(nodeIndex, 1, {
@@ -1122,7 +1126,9 @@ async function _prepareGlusterVm(
} }
await newVM.add_tags('XOSAN') await newVM.add_tags('XOSAN')
await xapi.editVm(newVM, { 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', name_description: 'Xosan VM storage',
memory: memorySize, memory: memorySize,
}) })

View File

@@ -13,6 +13,7 @@ import includes from 'lodash/includes'
import proxyConsole from './proxy-console' import proxyConsole from './proxy-console'
import pw from 'pw' import pw from 'pw'
import serveStatic from 'serve-static' import serveStatic from 'serve-static'
import startsWith from 'lodash/startsWith'
import stoppable from 'stoppable' import stoppable from 'stoppable'
import WebServer from 'http-server-plus' import WebServer from 'http-server-plus'
import WebSocket from 'ws' import WebSocket from 'ws'
@@ -331,7 +332,7 @@ async function registerPluginsInPath(path) {
await Promise.all( await Promise.all(
mapToArray(files, name => { mapToArray(files, name => {
if (name.startsWith(PLUGIN_PREFIX)) { if (startsWith(name, PLUGIN_PREFIX)) {
return registerPluginWrapper.call( return registerPluginWrapper.call(
this, this,
`${path}/${name}`, `${path}/${name}`,
@@ -427,7 +428,7 @@ const setUpProxies = (express, opts, xo) => {
const { url } = req const { url } = req
for (const prefix in opts) { for (const prefix in opts) {
if (url.startsWith(prefix)) { if (startsWith(url, prefix)) {
const target = opts[prefix] const target = opts[prefix]
proxy.web(req, res, { proxy.web(req, res, {
@@ -451,7 +452,7 @@ const setUpProxies = (express, opts, xo) => {
const { url } = req const { url } = req
for (const prefix in opts) { for (const prefix in opts) {
if (url.startsWith(prefix)) { if (startsWith(url, prefix)) {
const target = opts[prefix] const target = opts[prefix]
proxy.ws(req, socket, head, { proxy.ws(req, socket, head, {

View File

@@ -1,6 +1,6 @@
import Collection from '../collection/redis' import Collection from '../collection/redis'
import Model from '../model' import Model from '../model'
import { forEach, serializeError } from '../utils' import { forEach } from '../utils'
import { parseProp } from './utils' import { parseProp } from './utils'
@@ -30,28 +30,13 @@ export class Servers extends Collection {
// Deserializes // Deserializes
forEach(servers, server => { forEach(servers, server => {
server.allowUnauthorized = server.allowUnauthorized === 'true'
server.enabled = server.enabled === 'true'
if (server.error) { if (server.error) {
server.error = parseProp('server', server, 'error', '') server.error = parseProp('server', server, 'error', '')
} else { } else {
delete server.error delete server.error
} }
server.readOnly = server.readOnly === 'true'
}) })
return servers 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)
}
} }

View File

@@ -13,7 +13,9 @@ export default function proxyConsole(ws, vmConsole, sessionId) {
hostname = address hostname = address
log.warn( 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`
) )
} }

View File

@@ -1,3 +1,5 @@
import { startsWith } from 'lodash'
import ensureArray from './_ensureArray' import ensureArray from './_ensureArray'
import { import {
extractProperty, extractProperty,
@@ -117,7 +119,7 @@ const TRANSFORMS = {
size: update.installation_size, size: update.installation_size,
} }
if (update.name_label.startsWith('XS')) { if (startsWith(update.name_label, 'XS')) {
// It's a patch update but for homogeneity, we're still using pool_patches // It's a patch update but for homogeneity, we're still using pool_patches
} else { } else {
supplementalPacks.push(formattedUpdate) supplementalPacks.push(formattedUpdate)
@@ -263,17 +265,6 @@ 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 = { const vm = {
// type is redefined after for controllers/, templates & // type is redefined after for controllers/, templates &
// snapshots. // snapshots.
@@ -290,7 +281,7 @@ const TRANSFORMS = {
? +metrics.VCPUs_number ? +metrics.VCPUs_number
: +obj.VCPUs_at_startup, : +obj.VCPUs_at_startup,
}, },
current_operations: currentOperations, current_operations: obj.current_operations,
docker: (function() { docker: (function() {
const monitor = otherConfig['xscontainer-monitor'] const monitor = otherConfig['xscontainer-monitor']
if (!monitor) { if (!monitor) {

View File

@@ -4,6 +4,7 @@ import synchronized from 'decorator-synchronized'
import { BaseError } from 'make-error' import { BaseError } from 'make-error'
import { import {
defaults, defaults,
endsWith,
findKey, findKey,
forEach, forEach,
identity, identity,
@@ -183,7 +184,7 @@ const STATS = {
transformValue: value => value * 1024, transformValue: value => value * 1024,
}, },
memory: { memory: {
test: metricType => metricType.endsWith('memory'), test: metricType => endsWith(metricType, 'memory'),
}, },
cpus: { cpus: {
test: /^cpu(\d+)$/, test: /^cpu(\d+)$/,

View File

@@ -22,8 +22,8 @@ import { forbiddenOperation } from 'xo-common/api-errors'
import { Xapi as XapiBase, NULL_REF } from 'xen-api' import { Xapi as XapiBase, NULL_REF } from 'xen-api'
import { import {
every, every,
filter,
find, find,
filter,
flatMap, flatMap,
flatten, flatten,
groupBy, groupBy,
@@ -31,6 +31,7 @@ import {
isEmpty, isEmpty,
noop, noop,
omit, omit,
startsWith,
uniq, uniq,
} from 'lodash' } from 'lodash'
import { satisfies as versionSatisfies } from 'semver' import { satisfies as versionSatisfies } from 'semver'
@@ -829,7 +830,7 @@ export default class Xapi extends XapiBase {
} }
// If the VDI name start with `[NOBAK]`, do not export it. // If the VDI name start with `[NOBAK]`, do not export it.
if (vdi.name_label.startsWith('[NOBAK]')) { if (startsWith(vdi.name_label, '[NOBAK]')) {
// FIXME: find a way to not create the VDI snapshot in the // FIXME: find a way to not create the VDI snapshot in the
// first time. // first time.
// //
@@ -1723,7 +1724,9 @@ export default class Xapi extends XapiBase {
} }
log.debug( 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 { try {
await pRetry( await pRetry(
@@ -2133,16 +2136,6 @@ export default class Xapi extends XapiBase {
mapToArray(bonds, bond => this.call('Bond.destroy', bond)) mapToArray(bonds, bond => this.call('Bond.destroy', bond))
) )
const tunnels = filter(this.objects.all, { $type: 'tunnel' })
await Promise.all(
map(pifs, async pif => {
const tunnel = find(tunnels, { access_PIF: pif.$ref })
if (tunnel != null) {
await this.callAsync('tunnel.destroy', tunnel.$ref)
}
})
)
await this.callAsync('network.destroy', network.$ref) await this.callAsync('network.destroy', network.$ref)
} }

View File

@@ -256,12 +256,16 @@ export default {
) { ) {
if (getAll) { if (getAll) {
log( log(
`patch ${patch.name} (${id}) conflicts with installed patch ${conflictId}` `patch ${
patch.name
} (${id}) conflicts with installed patch ${conflictId}`
) )
return return
} }
throw new Error( throw new Error(
`patch ${patch.name} (${id}) conflicts with installed patch ${conflictId}` `patch ${
patch.name
} (${id}) conflicts with installed patch ${conflictId}`
) )
} }
@@ -288,7 +292,9 @@ export default {
if (!installed[id] && find(installable, { id }) === undefined) { if (!installed[id] && find(installable, { id }) === undefined) {
if (requiredPatch.paid && freeHost) { if (requiredPatch.paid && freeHost) {
throw new Error( throw new Error(
`required patch ${requiredPatch.name} (${id}) requires a XenServer license` `required patch ${
requiredPatch.name
} (${id}) requires a XenServer license`
) )
} }
installable.push(requiredPatch) installable.push(requiredPatch)

View File

@@ -19,7 +19,6 @@ import {
isEmpty, isEmpty,
last, last,
mapValues, mapValues,
merge,
noop, noop,
some, some,
sum, sum,
@@ -69,7 +68,6 @@ export type Mode = 'full' | 'delta'
export type ReportWhen = 'always' | 'failure' | 'never' export type ReportWhen = 'always' | 'failure' | 'never'
type Settings = {| type Settings = {|
bypassVdiChainsCheck?: boolean,
concurrency?: number, concurrency?: number,
deleteFirst?: boolean, deleteFirst?: boolean,
copyRetention?: number, copyRetention?: number,
@@ -141,7 +139,6 @@ const getOldEntries = <T>(retention: number, entries?: T[]): T[] =>
: entries : entries
const defaultSettings: Settings = { const defaultSettings: Settings = {
bypassVdiChainsCheck: false,
concurrency: 0, concurrency: 0,
deleteFirst: false, deleteFirst: false,
exportRetention: 0, exportRetention: 0,
@@ -560,7 +557,7 @@ export default class BackupNg {
const executor: Executor = async ({ const executor: Executor = async ({
cancelToken, cancelToken,
data, data: vmsId,
job: job_, job: job_,
logger, logger,
runJobId, runJobId,
@@ -570,8 +567,6 @@ export default class BackupNg {
throw new Error('backup job cannot run without a schedule') throw new Error('backup job cannot run without a schedule')
} }
let vmsId = data?.vms
const job: BackupJob = (job_: any) const job: BackupJob = (job_: any)
const vmsPattern = job.vms const vmsPattern = job.vms
@@ -625,9 +620,7 @@ export default class BackupNg {
})) }))
) )
const settings = merge(job.settings, data?.settings) const timeout = getSetting(job.settings, 'timeout', [''])
const timeout = getSetting(settings, 'timeout', [''])
if (timeout !== 0) { if (timeout !== 0) {
const source = CancelToken.source([cancelToken]) const source = CancelToken.source([cancelToken])
cancelToken = source.token cancelToken = source.token
@@ -660,7 +653,6 @@ export default class BackupNg {
schedule, schedule,
logger, logger,
taskId, taskId,
settings,
srs, srs,
remotes remotes
) )
@@ -668,7 +660,7 @@ export default class BackupNg {
// 2018-07-20, JFT: vmTimeout is disabled for the time being until // 2018-07-20, JFT: vmTimeout is disabled for the time being until
// we figure out exactly how it should behave. // we figure out exactly how it should behave.
// //
// const vmTimeout: number = getSetting(settings, 'vmTimeout', [ // const vmTimeout: number = getSetting(job.settings, 'vmTimeout', [
// uuid, // uuid,
// scheduleId, // scheduleId,
// ]) // ])
@@ -697,7 +689,9 @@ export default class BackupNg {
} }
} }
const concurrency: number = getSetting(settings, 'concurrency', ['']) const concurrency: number = getSetting(job.settings, 'concurrency', [
'',
])
if (concurrency !== 0) { if (concurrency !== 0) {
handleVm = limitConcurrency(concurrency)(handleVm) handleVm = limitConcurrency(concurrency)(handleVm)
logger.notice('vms', { logger.notice('vms', {
@@ -936,7 +930,6 @@ export default class BackupNg {
schedule: Schedule, schedule: Schedule,
logger: any, logger: any,
taskId: string, taskId: string,
settings: Settings,
srs: any[], srs: any[],
remotes: any[] remotes: any[]
): Promise<void> { ): Promise<void> {
@@ -964,7 +957,7 @@ export default class BackupNg {
) )
} }
const { id: jobId, mode } = job const { id: jobId, mode, settings } = job
const { id: scheduleId } = schedule const { id: scheduleId } = schedule
let exportRetention: number = getSetting(settings, 'exportRetention', [ let exportRetention: number = getSetting(settings, 'exportRetention', [
@@ -1025,14 +1018,7 @@ export default class BackupNg {
.filter(_ => _.other_config['xo:backup:job'] === jobId) .filter(_ => _.other_config['xo:backup:job'] === jobId)
.sort(compareSnapshotTime) .sort(compareSnapshotTime)
const bypassVdiChainsCheck: boolean = getSetting(
settings,
'bypassVdiChainsCheck',
[vmUuid, '']
)
if (!bypassVdiChainsCheck) {
xapi._assertHealthyVdiChains(vm) xapi._assertHealthyVdiChains(vm)
}
const offlineSnapshot: boolean = getSetting(settings, 'offlineSnapshot', [ const offlineSnapshot: boolean = getSetting(settings, 'offlineSnapshot', [
vmUuid, vmUuid,

View File

@@ -10,7 +10,17 @@ import { createReadStream, readdir, stat } from 'fs'
import { satisfies as versionSatisfies } from 'semver' import { satisfies as versionSatisfies } from 'semver'
import { utcFormat } from 'd3-time-format' import { utcFormat } from 'd3-time-format'
import { basename, dirname } from 'path' import { basename, dirname } from 'path'
import { filter, find, includes, once, range, sortBy, trim } from 'lodash' import {
endsWith,
filter,
find,
includes,
once,
range,
sortBy,
startsWith,
trim,
} from 'lodash'
import { import {
chainVhd, chainVhd,
createSyntheticStream as createVhdReadStream, createSyntheticStream as createVhdReadStream,
@@ -94,7 +104,7 @@ const getVdiTimestamp = name => {
const getDeltaBackupNameWithoutExt = name => const getDeltaBackupNameWithoutExt = name =>
name.slice(0, -DELTA_BACKUP_EXT_LENGTH) name.slice(0, -DELTA_BACKUP_EXT_LENGTH)
const isDeltaBackup = name => name.endsWith(DELTA_BACKUP_EXT) const isDeltaBackup = name => endsWith(name, DELTA_BACKUP_EXT)
// ------------------------------------------------------------------- // -------------------------------------------------------------------
@@ -298,13 +308,13 @@ export default class {
const handler = await this._xo.getRemoteHandler(remoteId) const handler = await this._xo.getRemoteHandler(remoteId)
// List backups. (No delta) // List backups. (No delta)
const backupFilter = file => file.endsWith('.xva') const backupFilter = file => endsWith(file, '.xva')
const files = await handler.list('.') const files = await handler.list('.')
const backups = filter(files, backupFilter) const backups = filter(files, backupFilter)
// List delta backups. // List delta backups.
const deltaDirs = filter(files, file => file.startsWith('vm_delta_')) const deltaDirs = filter(files, file => startsWith(file, 'vm_delta_'))
for (const deltaDir of deltaDirs) { for (const deltaDir of deltaDirs) {
const files = await handler.list(deltaDir) const files = await handler.list(deltaDir)
@@ -326,12 +336,12 @@ export default class {
const backups = [] const backups = []
await asyncMap(handler.list('.'), entry => { await asyncMap(handler.list('.'), entry => {
if (entry.endsWith('.xva')) { if (endsWith(entry, '.xva')) {
backups.push(parseVmBackupPath(entry)) backups.push(parseVmBackupPath(entry))
} else if (entry.startsWith('vm_delta_')) { } else if (startsWith(entry, 'vm_delta_')) {
return handler.list(entry).then(children => return handler.list(entry).then(children =>
asyncMap(children, child => { asyncMap(children, child => {
if (child.endsWith('.json')) { if (endsWith(child, '.json')) {
const path = `${entry}/${child}` const path = `${entry}/${child}`
const record = parseVmBackupPath(path) const record = parseVmBackupPath(path)
@@ -401,7 +411,9 @@ export default class {
localBaseUuid, localBaseUuid,
{ {
bypassVdiChainsCheck: force, 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)) $defer.onFailure(() => srcXapi.deleteVm(delta.vm.uuid))
@@ -997,7 +1009,7 @@ export default class {
// Currently, the filenames of the VHD changes over time // Currently, the filenames of the VHD changes over time
// (delta → full), but the JSON is not updated, therefore the // (delta → full), but the JSON is not updated, therefore the
// VHD path may need to be fixed. // VHD path may need to be fixed.
return vhdPath.endsWith('_delta.vhd') return endsWith(vhdPath, '_delta.vhd')
? pFromCallback(cb => stat(vhdPath, cb)).then( ? pFromCallback(cb => stat(vhdPath, cb)).then(
() => vhdPath, () => vhdPath,
error => { error => {

View File

@@ -204,7 +204,9 @@ export default class metadataBackup {
await asyncMap(handlers, async (handler, remoteId) => { await asyncMap(handlers, async (handler, remoteId) => {
const subTaskId = logger.notice( 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: { data: {
id: remoteId, id: remoteId,
@@ -242,7 +244,9 @@ export default class metadataBackup {
) )
logger.notice( 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', event: 'task.end',
status: 'success', status: 'success',
@@ -261,7 +265,9 @@ export default class metadataBackup {
}) })
logger.error( 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', event: 'task.end',
result: serializeError(error), result: serializeError(error),
@@ -334,7 +340,9 @@ export default class metadataBackup {
await asyncMap(handlers, async (handler, remoteId) => { await asyncMap(handlers, async (handler, remoteId) => {
const subTaskId = logger.notice( 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: { data: {
id: remoteId, id: remoteId,
@@ -384,7 +392,9 @@ export default class metadataBackup {
) )
logger.notice( 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', event: 'task.end',
status: 'success', status: 'success',
@@ -406,7 +416,9 @@ export default class metadataBackup {
}) })
logger.error( 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', event: 'task.end',
result: serializeError(error), result: serializeError(error),

View File

@@ -1,4 +1,6 @@
import endsWith from 'lodash/endsWith'
import levelup from 'level-party' import levelup from 'level-party'
import startsWith from 'lodash/startsWith'
import sublevel from 'level-sublevel' import sublevel from 'level-sublevel'
import { ensureDir } from 'fs-extra' import { ensureDir } from 'fs-extra'
@@ -36,7 +38,7 @@ const levelPromise = db => {
return return
} }
if (name.endsWith('Stream') || name.startsWith('is')) { if (endsWith(name, 'Stream') || startsWith(name, 'is')) {
dbP[name] = db::value dbP[name] = db::value
} else { } else {
dbP[name] = promisify(value, db) dbP[name] = promisify(value, db)

View File

@@ -1,7 +1,6 @@
import createLogger from '@xen-orchestra/log' import createLogger from '@xen-orchestra/log'
import { BaseError } from 'make-error' import { BaseError } from 'make-error'
import { fibonacci } from 'iterable-backoff' import { fibonacci } from 'iterable-backoff'
import { findKey } from 'lodash'
import { noSuchObject } from 'xo-common/api-errors' import { noSuchObject } from 'xo-common/api-errors'
import { pDelay, ignoreErrors } from 'promise-toolbox' import { pDelay, ignoreErrors } from 'promise-toolbox'
@@ -16,6 +15,7 @@ import {
isEmpty, isEmpty,
isString, isString,
popProperty, popProperty,
serializeError,
} from '../utils' } from '../utils'
import { Servers } from '../models/server' import { Servers } from '../models/server'
@@ -42,10 +42,7 @@ const log = createLogger('xo:xo-mixins:xen-servers')
// - _xapis[server.id] id defined // - _xapis[server.id] id defined
// - _serverIdsByPool[xapi.pool.$id] is server.id // - _serverIdsByPool[xapi.pool.$id] is server.id
export default class { export default class {
constructor( constructor(xo, { guessVhdSizeOnImport, noEventsTimeout, xapiOptions }) {
xo,
{ guessVhdSizeOnImport, xapiMarkDisconnectedDelay, xapiOptions }
) {
this._objectConflicts = { __proto__: null } // TODO: clean when a server is disconnected. this._objectConflicts = { __proto__: null } // TODO: clean when a server is disconnected.
const serversDb = (this._servers = new Servers({ const serversDb = (this._servers = new Servers({
connection: xo._redis, connection: xo._redis,
@@ -60,7 +57,7 @@ export default class {
} }
this._xapis = { __proto__: null } this._xapis = { __proto__: null }
this._xo = xo this._xo = xo
this._xapiMarkDisconnectedDelay = parseDuration(xapiMarkDisconnectedDelay) this._noEventsTimeout = parseDuration(noEventsTimeout)
xo.on('clean', () => serversDb.rebuildIndexes()) xo.on('clean', () => serversDb.rebuildIndexes())
xo.on('start', async () => { xo.on('start', async () => {
@@ -99,23 +96,23 @@ export default class {
} }
async registerXenServer({ async registerXenServer({
allowUnauthorized = false, allowUnauthorized,
host, host,
label, label,
password, password,
readOnly = false, readOnly,
username, username,
}) { }) {
// FIXME: We are storing passwords which is bad! // FIXME: We are storing passwords which is bad!
// Could we use tokens instead? // Could we use tokens instead?
// TODO: use plain objects // TODO: use plain objects
const server = await this._servers.create({ const server = await this._servers.create({
allowUnauthorized, allowUnauthorized: allowUnauthorized ? 'true' : undefined,
enabled: true, enabled: 'true',
host, host,
label: label || undefined, label: label || undefined,
password, password,
readOnly, readOnly: readOnly ? 'true' : undefined,
username, username,
}) })
@@ -167,22 +164,22 @@ export default class {
if (password) server.set('password', password) if (password) server.set('password', password)
if (error !== undefined) { if (error !== undefined) {
server.set('error', error) server.set('error', error ? JSON.stringify(error) : '')
} }
if (enabled !== undefined) { if (enabled !== undefined) {
server.set('enabled', enabled) server.set('enabled', enabled ? 'true' : undefined)
} }
if (readOnly !== undefined) { if (readOnly !== undefined) {
server.set('readOnly', readOnly) server.set('readOnly', readOnly ? 'true' : undefined)
if (xapi !== undefined) { if (xapi !== undefined) {
xapi.readOnly = readOnly xapi.readOnly = readOnly
} }
} }
if (allowUnauthorized !== undefined) { if (allowUnauthorized !== undefined) {
server.set('allowUnauthorized', allowUnauthorized) server.set('allowUnauthorized', allowUnauthorized ? 'true' : undefined)
} }
await this._servers.update(server) await this._servers.update(server)
@@ -210,21 +207,7 @@ export default class {
const conflicts = this._objectConflicts const conflicts = this._objectConflicts
const objects = this._xo._objects const objects = this._xo._objects
const serverIdsByPool = this._serverIdsByPool
forEach(newXapiObjects, function handleObject(xapiObject, xapiId) { 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 { $ref } = xapiObject
const dependent = dependents[$ref] const dependent = dependents[$ref]
@@ -294,8 +277,8 @@ export default class {
const server = (await this._getXenServer(id)).properties const server = (await this._getXenServer(id)).properties
const xapi = (this._xapis[server.id] = new Xapi({ const xapi = (this._xapis[server.id] = new Xapi({
allowUnauthorized: server.allowUnauthorized, allowUnauthorized: Boolean(server.allowUnauthorized),
readOnly: server.readOnly, readOnly: Boolean(server.readOnly),
...this._xapiOptions, ...this._xapiOptions,
@@ -431,7 +414,7 @@ export default class {
} catch (error) { } catch (error) {
delete this._xapis[server.id] delete this._xapis[server.id]
xapi.disconnect()::ignoreErrors() xapi.disconnect()::ignoreErrors()
this.updateXenServer(id, { error })::ignoreErrors() this.updateXenServer(id, { error: serializeError(error) })::ignoreErrors()
throw error throw error
} }
} }
@@ -492,13 +475,10 @@ export default class {
const servers = await this._servers.get() const servers = await this._servers.get()
const xapis = this._xapis const xapis = this._xapis
forEach(servers, server => { forEach(servers, server => {
const lastEventFetchedTimestamp = const lastSuccessfulFetchTime = xapis[server.id]?.lastSuccessfulFetchTime
xapis[server.id]?.lastEventFetchedTimestamp const currentTime = new Date().getTime()
if ( if (currentTime > lastSuccessfulFetchTime + this._noEventsTimeout) {
lastEventFetchedTimestamp !== undefined && server.error = xapis[server.id].lastCatchedEventError
Date.now() > lastEventFetchedTimestamp + this._xapiMarkDisconnectedDelay
) {
server.error = xapis[server.id].watchEventsError
} }
server.status = this._getXenServerStatus(server.id) server.status = this._getXenServerStatus(server.id)
if (server.status === 'connected') { if (server.status === 'connected') {

View File

@@ -27,7 +27,7 @@
"child-process-promise": "^2.0.3", "child-process-promise": "^2.0.3",
"core-js": "^3.0.0", "core-js": "^3.0.0",
"pipette": "^0.9.3", "pipette": "^0.9.3",
"promise-toolbox": "^0.13.0", "promise-toolbox": "^0.12.1",
"tmp": "^0.1.0", "tmp": "^0.1.0",
"vhd-lib": "^0.7.0" "vhd-lib": "^0.7.0"
}, },
@@ -38,7 +38,7 @@
"babel-plugin-lodash": "^3.3.2", "babel-plugin-lodash": "^3.3.2",
"cross-env": "^5.1.3", "cross-env": "^5.1.3",
"event-to-promise": "^0.8.0", "event-to-promise": "^0.8.0",
"execa": "^2.0.2", "execa": "^1.0.0",
"fs-extra": "^8.0.1", "fs-extra": "^8.0.1",
"get-stream": "^5.1.0", "get-stream": "^5.1.0",
"index-modules": "^0.3.0", "index-modules": "^0.3.0",

View File

@@ -1,7 +1,7 @@
{ {
"private": true, "private": true,
"name": "xo-web", "name": "xo-web",
"version": "5.44.0", "version": "5.43.0",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"description": "Web interface client for Xen-Orchestra", "description": "Web interface client for Xen-Orchestra",
"keywords": [ "keywords": [
@@ -84,7 +84,7 @@
"human-format": "^0.10.0", "human-format": "^0.10.0",
"immutable": "^4.0.0-rc.9", "immutable": "^4.0.0-rc.9",
"index-modules": "^0.3.0", "index-modules": "^0.3.0",
"is-ip": "^3.1.0", "is-ip": "^2.0.0",
"jsonrpc-websocket-client": "^0.5.0", "jsonrpc-websocket-client": "^0.5.0",
"kindof": "^2.0.0", "kindof": "^2.0.0",
"lodash": "^4.6.1", "lodash": "^4.6.1",
@@ -96,7 +96,7 @@
"moment-timezone": "^0.5.14", "moment-timezone": "^0.5.14",
"notifyjs": "^3.0.0", "notifyjs": "^3.0.0",
"otplib": "^11.0.0", "otplib": "^11.0.0",
"promise-toolbox": "^0.13.0", "promise-toolbox": "^0.12.1",
"prop-types": "^15.6.0", "prop-types": "^15.6.0",
"qrcode": "^1.3.2", "qrcode": "^1.3.2",
"random-password": "^0.1.2", "random-password": "^0.1.2",

View File

@@ -1,6 +1,6 @@
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import React from 'react' import React from 'react'
import { isFunction } from 'lodash' import { isFunction, startsWith } from 'lodash'
import Button from './button' import Button from './button'
import Component from './base-component' import Component from './base-component'
@@ -73,7 +73,7 @@ export default class ActionButton extends Component {
let empty = true let empty = true
handlerParam = {} handlerParam = {}
Object.keys(props).forEach(key => { Object.keys(props).forEach(key => {
if (key.startsWith('data-')) { if (startsWith(key, 'data-')) {
empty = false empty = false
handlerParam[key.slice(5)] = props[key] handlerParam[key.slice(5)] = props[key]
} }

View File

@@ -1,7 +1,7 @@
import classNames from 'classnames' import classNames from 'classnames'
import React from 'react' import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { isEmpty, isFunction, isString, map, pick } from 'lodash' import { isEmpty, isFunction, isString, map, pick, startsWith } from 'lodash'
import _ from '../intl' import _ from '../intl'
import Component from '../base-component' import Component from '../base-component'
@@ -119,7 +119,7 @@ class Editable extends Component {
this.setState({ saving: true }) this.setState({ saving: true })
const params = Object.keys(props).reduce((res, val) => { const params = Object.keys(props).reduce((res, val) => {
if (val.startsWith('data-')) { if (startsWith(val, 'data-')) {
res[val.slice(5)] = props[val] res[val.slice(5)] = props[val]
} }
return res return res

View File

@@ -1,6 +1,7 @@
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import React from 'react' import React from 'react'
import { injectState, provideState } from 'reaclette' import { injectState, provideState } from 'reaclette'
import { startsWith } from 'lodash'
import decorate from '../apply-decorators' import decorate from '../apply-decorators'
@@ -22,7 +23,7 @@ const Number_ = decorate([
const params = {} const params = {}
let empty = true let empty = true
Object.keys(props).forEach(key => { Object.keys(props).forEach(key => {
if (key.startsWith('data-')) { if (startsWith(key, 'data-')) {
empty = false empty = false
params[key.slice(5)] = props[key] params[key.slice(5)] = props[key]
} }

View File

@@ -3058,6 +3058,9 @@ export default {
// Original text: "Enable it if your certificate is rejected, but it's not recommended because your connection will not be secured." // Original text: "Enable it if your certificate is rejected, but it's not recommended because your connection will not be secured."
serverUnauthorizedCertificatesInfo: undefined, serverUnauthorizedCertificatesInfo: undefined,
// Original text: 'Disconnect server'
serverDisconnect: undefined,
// Original text: 'username' // Original text: 'username'
serverPlaceHolderUser: undefined, serverPlaceHolderUser: undefined,
@@ -3088,6 +3091,12 @@ export default {
// Original text: 'Connecting…' // Original text: 'Connecting…'
serverConnecting: undefined, serverConnecting: undefined,
// Original text: 'Connected'
serverConnected: undefined,
// Original text: 'Disconnected'
serverDisconnected: undefined,
// Original text: 'Authentication error' // Original text: 'Authentication error'
serverAuthFailed: undefined, serverAuthFailed: undefined,

View File

@@ -3135,6 +3135,9 @@ export default {
serverUnauthorizedCertificatesInfo: serverUnauthorizedCertificatesInfo:
"Activez ceci si votre certificat est rejeté, mais ce n'est pas recommandé car votre connexion ne sera pas sécurisée.", "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" // Original text: "username"
serverPlaceHolderUser: "nom d'utilisateur", serverPlaceHolderUser: "nom d'utilisateur",
@@ -3166,6 +3169,12 @@ export default {
// Original text: "Connecting…" // Original text: "Connecting…"
serverConnecting: 'Connexion…', serverConnecting: 'Connexion…',
// Original text: "Connected"
serverConnected: 'Connecté',
// Original text: "Disconnected"
serverDisconnected: 'Déconnecté',
// Original text: "Authentication error" // Original text: "Authentication error"
serverAuthFailed: "Erreur d'authentification", serverAuthFailed: "Erreur d'authentification",

View File

@@ -2612,6 +2612,9 @@ export default {
// Original text: 'Read Only' // Original text: 'Read Only'
serverReadOnly: undefined, serverReadOnly: undefined,
// Original text: 'Disconnect server'
serverDisconnect: undefined,
// Original text: 'username' // Original text: 'username'
serverPlaceHolderUser: undefined, serverPlaceHolderUser: undefined,

View File

@@ -2909,6 +2909,9 @@ export default {
// Original text: "Read Only" // Original text: "Read Only"
serverReadOnly: 'Csak Olvasható', serverReadOnly: 'Csak Olvasható',
// Original text: "Disconnect server"
serverDisconnect: 'Szerver Lecsatlakozás',
// Original text: "username" // Original text: "username"
serverPlaceHolderUser: 'felhasználónév', serverPlaceHolderUser: 'felhasználónév',
@@ -2936,6 +2939,12 @@ export default {
// Original text: "Connecting…" // Original text: "Connecting…"
serverConnecting: 'Csatlakozás…', serverConnecting: 'Csatlakozás…',
// Original text: "Connected"
serverConnected: 'Kapcsolódva',
// Original text: "Disconnected"
serverDisconnected: 'Lekapcsolódva',
// Original text: "Authentication error" // Original text: "Authentication error"
serverAuthFailed: 'Bejelentkezési hiba', serverAuthFailed: 'Bejelentkezési hiba',

View File

@@ -2648,6 +2648,9 @@ export default {
// Original text: "Read Only" // Original text: "Read Only"
serverReadOnly: 'Tylko do odczytu', serverReadOnly: 'Tylko do odczytu',
// Original text: "Disconnect server"
serverDisconnect: 'Rozłącz serwer',
// Original text: "username" // Original text: "username"
serverPlaceHolderUser: 'Użytkownik', serverPlaceHolderUser: 'Użytkownik',

View File

@@ -2636,6 +2636,9 @@ export default {
// Original text: "Read Only" // Original text: "Read Only"
serverReadOnly: 'Modo Leitura', serverReadOnly: 'Modo Leitura',
// Original text: 'Disconnect server'
serverDisconnect: undefined,
// Original text: 'username' // Original text: 'username'
serverPlaceHolderUser: undefined, serverPlaceHolderUser: undefined,

View File

@@ -3916,6 +3916,9 @@ export default {
serverUnauthorizedCertificatesInfo: serverUnauthorizedCertificatesInfo:
'Sertifikanız reddedildiğinde bunu yapın ancak bağlantınız güvenli olmayacağı için tavsiye edilmez.', '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" // Original text: "username"
serverPlaceHolderUser: 'kullanıcı adı', serverPlaceHolderUser: 'kullanıcı adı',
@@ -3946,6 +3949,12 @@ export default {
// Original text: "Connecting…" // Original text: "Connecting…"
serverConnecting: 'Bağlanıyor...', serverConnecting: 'Bağlanıyor...',
// Original text: "Connected"
serverConnected: 'Bağlandı',
// Original text: "Disconnected"
serverDisconnected: 'Bağlantı kesildi',
// Original text: "Authentication error" // Original text: "Authentication error"
serverAuthFailed: 'Kimlik doğrulama hatası', serverAuthFailed: 'Kimlik doğrulama hatası',

View File

@@ -47,8 +47,6 @@ const messages = {
chooseBackup: 'Choose a backup', chooseBackup: 'Choose a backup',
clickToShowError: 'Click to show error', clickToShowError: 'Click to show error',
backupJobs: 'Backup jobs', backupJobs: 'Backup jobs',
iscsiSessions:
'({ nSessions, number }) iSCSI session{nSessions, plural, one {} other {s}}',
// ----- Modals ----- // ----- Modals -----
alertOk: 'OK', alertOk: 'OK',
@@ -808,7 +806,7 @@ const messages = {
hostNoIscsiSr: 'Not connected to an iSCSI SR', hostNoIscsiSr: 'Not connected to an iSCSI SR',
hostMultipathingSrs: 'Click to see concerned SRs', hostMultipathingSrs: 'Click to see concerned SRs',
hostMultipathingPaths: hostMultipathingPaths:
'{nActives, number} of {nPaths, number} path{nPaths, plural, one {} other {s}}', '{nActives, number} of {nPaths, number} path{nPaths, plural, one {} other {s}} ({ nSessions, number } iSCSI session{nSessions, plural, one {} other {s}})',
hostMultipathingRequiredState: hostMultipathingRequiredState:
'This action will not be fulfilled if a VM is in a running state. Please ensure that all VMs are evacuated or stopped before performing this action!', 'This action will not be fulfilled if a VM is in a running state. Please ensure that all VMs are evacuated or stopped before performing this action!',
hostMultipathingWarning: hostMultipathingWarning:
@@ -959,7 +957,6 @@ const messages = {
statDisk: 'Disk throughput', statDisk: 'Disk throughput',
statLastTenMinutes: 'Last 10 minutes', statLastTenMinutes: 'Last 10 minutes',
statLastTwoHours: 'Last 2 hours', statLastTwoHours: 'Last 2 hours',
statLastDay: 'Last day',
statLastWeek: 'Last week', statLastWeek: 'Last week',
statLastYear: 'Last year', statLastYear: 'Last year',
@@ -1662,6 +1659,7 @@ const messages = {
serverAllowUnauthorizedCertificates: 'Allow Unauthorized Certificates', serverAllowUnauthorizedCertificates: 'Allow Unauthorized Certificates',
serverUnauthorizedCertificatesInfo: serverUnauthorizedCertificatesInfo:
"Enable it if your certificate is rejected, but it's not recommended because your connection will not be secured.", "Enable it if your certificate is rejected, but it's not recommended because your connection will not be secured.",
serverDisconnect: 'Disconnect server',
serverPlaceHolderUser: 'username', serverPlaceHolderUser: 'username',
serverPlaceHolderPassword: 'password', serverPlaceHolderPassword: 'password',
serverPlaceHolderAddress: 'address[:port]', serverPlaceHolderAddress: 'address[:port]',
@@ -1671,12 +1669,13 @@ const messages = {
serverAddFailed: 'Adding server failed', serverAddFailed: 'Adding server failed',
serverStatus: 'Status', serverStatus: 'Status',
serverConnectionFailed: 'Connection failed. Click for more information.', serverConnectionFailed: 'Connection failed. Click for more information.',
serverConnected: 'Connected',
serverDisconnected: 'Disconnected',
serverAuthFailed: 'Authentication error', serverAuthFailed: 'Authentication error',
serverUnknownError: 'Unknown error', serverUnknownError: 'Unknown error',
serverSelfSignedCertError: 'Invalid self-signed certificate', serverSelfSignedCertError: 'Invalid self-signed certificate',
serverSelfSignedCertQuestion: serverSelfSignedCertQuestion:
'Do you want to accept self-signed certificate for this server even though it would decrease security?', 'Do you want to accept self-signed certificate for this server even though it would decrease security?',
serverEnable: 'Enable',
serverEnabled: 'Enabled', serverEnabled: 'Enabled',
serverDisabled: 'Disabled', serverDisabled: 'Disabled',
serverDisable: 'Disable server', serverDisable: 'Disable server',
@@ -1722,13 +1721,11 @@ const messages = {
newNetworkBondMode: 'Bond mode', newNetworkBondMode: 'Bond mode',
newNetworkInfo: 'Info', newNetworkInfo: 'Info',
newNetworkType: 'Type', newNetworkType: 'Type',
newNetworkEncapsulation: 'Encapsulation',
deleteNetwork: 'Delete network', deleteNetwork: 'Delete network',
deleteNetworkConfirm: 'Are you sure you want to delete this network?', deleteNetworkConfirm: 'Are you sure you want to delete this network?',
networkInUse: 'This network is currently in use', networkInUse: 'This network is currently in use',
pillBonded: 'Bonded', pillBonded: 'Bonded',
bondedNetwork: 'Bonded network', bondedNetwork: 'Bonded network',
privateNetwork: 'Private network',
// ----- Add host ----- // ----- Add host -----
addHostSelectHost: 'Host', addHostSelectHost: 'Host',

View File

@@ -3,7 +3,7 @@ import CopyToClipboard from 'react-copy-to-clipboard'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import React from 'react' import React from 'react'
import { get } from '@xen-orchestra/defined' import { get } from '@xen-orchestra/defined'
import { find } from 'lodash' import { find, startsWith } from 'lodash'
import decorate from './apply-decorators' import decorate from './apply-decorators'
import Icon from './icon' import Icon from './icon'
@@ -492,7 +492,7 @@ const xoItemToRender = {
gpuGroup: group => ( gpuGroup: group => (
<span> <span>
{group.name_label.startsWith('Group of ') {startsWith(group.name_label, 'Group of ')
? group.name_label.slice(9) ? group.name_label.slice(9)
: group.name_label} : group.name_label}
</span> </span>

View File

@@ -24,6 +24,7 @@ import {
isFunction, isFunction,
map, map,
sortBy, sortBy,
startsWith,
} from 'lodash' } from 'lodash'
import ActionRowButton from '../action-row-button' import ActionRowButton from '../action-row-button'
@@ -326,7 +327,7 @@ export default class SortedTable extends Component {
const { props } = this const { props } = this
const userData = {} const userData = {}
Object.keys(props).forEach(key => { Object.keys(props).forEach(key => {
if (key.startsWith('data-')) { if (startsWith(key, 'data-')) {
userData[key.slice(5)] = props[key] userData[key.slice(5)] = props[key]
} }
}) })

View File

@@ -1,68 +0,0 @@
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
}

View File

@@ -58,12 +58,6 @@ export class TooltipViewer extends Component {
// =================================================================== // ===================================================================
// Wrap disabled HTML element before wrapping it with Tooltip
// <Tooltip>
// <div>
// <MyComponent disabled />
// </div>
// </Tooltip>
export default class Tooltip extends Component { export default class Tooltip extends Component {
static propTypes = { static propTypes = {
children: PropTypes.oneOfType([PropTypes.element, PropTypes.string]), children: PropTypes.oneOfType([PropTypes.element, PropTypes.string]),

View File

@@ -22,6 +22,7 @@ import {
replace, replace,
sample, sample,
some, some,
startsWith,
} from 'lodash' } from 'lodash'
import _ from './intl' import _ from './intl'
@@ -476,7 +477,7 @@ export const compareVersions = makeNiceCompare((v1, v2) => {
return 0 return 0
}) })
export const isXosanPack = ({ name }) => name.startsWith('XOSAN') export const isXosanPack = ({ name }) => startsWith(name, 'XOSAN')
// =================================================================== // ===================================================================

View File

@@ -536,13 +536,13 @@ export const editServer = (server, props) =>
subscribeServers.forceRefresh subscribeServers.forceRefresh
) )
export const enableServer = server => export const connectServer = server =>
_call('server.enable', { id: resolveId(server) })::pFinally( _call('server.connect', { id: resolveId(server) })::pFinally(
subscribeServers.forceRefresh subscribeServers.forceRefresh
) )
export const disableServer = server => export const disconnectServer = server =>
_call('server.disable', { id: resolveId(server) })::tap( _call('server.disconnect', { id: resolveId(server) })::tap(
subscribeServers.forceRefresh subscribeServers.forceRefresh
) )
@@ -1648,8 +1648,6 @@ export const getBondModes = () => _call('network.getBondModes')
export const createNetwork = params => _call('network.create', params) export const createNetwork = params => _call('network.create', params)
export const createBondedNetwork = params => export const createBondedNetwork = params =>
_call('network.createBonded', params) _call('network.createBonded', params)
export const createPrivateNetwork = params =>
_call('plugin.SDNController.createPrivateNetwork', params)
export const deleteNetwork = network => export const deleteNetwork = network =>
confirm({ confirm({

View File

@@ -1,6 +1,7 @@
import _ from 'intl' import _ from 'intl'
import ActionButton from 'action-button' import ActionButton from 'action-button'
import Component from 'base-component' import Component from 'base-component'
import endsWith from 'lodash/endsWith'
import Icon from 'icon' import Icon from 'icon'
import React from 'react' import React from 'react'
import replace from 'lodash/replace' import replace from 'lodash/replace'
@@ -191,7 +192,7 @@ export default class RestoreFileModalBody extends Component {
select.blur() select.blur()
select.focus() select.focus()
const isFile = file.id !== '..' && !file.path.endsWith('/') const isFile = file.id !== '..' && !endsWith(file.path, '/')
if (isFile) { if (isFile) {
const { selectedFiles } = this.state const { selectedFiles } = this.state
if (!includes(selectedFiles, file)) { if (!includes(selectedFiles, file)) {
@@ -227,7 +228,7 @@ export default class RestoreFileModalBody extends Component {
_selectAllFolderFiles = () => { _selectAllFolderFiles = () => {
this.setState({ this.setState({
selectedFiles: (this.state.selectedFiles || []).concat( selectedFiles: (this.state.selectedFiles || []).concat(
filter(this._getSelectableFiles(), ({ path }) => !path.endsWith('/')) filter(this._getSelectableFiles(), ({ path }) => !endsWith(path, '/'))
), ),
}) })
} }

View File

@@ -10,7 +10,16 @@ import { dirname } from 'path'
import { Container, Col, Row } from 'grid' import { Container, Col, Row } from 'grid'
import { createSelector } from 'reselect' import { createSelector } from 'reselect'
import { formatSize } from 'utils' import { formatSize } from 'utils'
import { filter, find, forEach, includes, isEmpty, map } from 'lodash' import {
endsWith,
filter,
find,
forEach,
includes,
isEmpty,
map,
startsWith,
} from 'lodash'
import { getRenderXoItemOfType } from 'render-xo-item' import { getRenderXoItemOfType } from 'render-xo-item'
import { listPartitions, listFiles } from 'xo' import { listPartitions, listFiles } from 'xo'
@@ -37,7 +46,7 @@ const fileOptionRenderer = ({ isFile, name }) => (
</span> </span>
) )
const ensureTrailingSlash = path => path + (path.endsWith('/') ? '' : '/') const ensureTrailingSlash = path => path + (endsWith(path, '/') ? '' : '/')
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
@@ -57,7 +66,7 @@ const formatFilesOptions = (rawFiles, path) => {
return files.concat( return files.concat(
map(rawFiles, (_, name) => ({ map(rawFiles, (_, name) => ({
id: `${path}${name}`, id: `${path}${name}`,
isFile: !name.endsWith('/'), isFile: !endsWith(name, '/'),
name, name,
path: `${path}${name}`, path: `${path}${name}`,
})) }))
@@ -253,7 +262,7 @@ export default class RestoreFileModalBody extends Component {
redundantFiles[file.path] = redundantFiles[file.path] =
find( find(
files, files,
f => !f.isFile && f !== file && file.path.startsWith(f.path) f => !f.isFile && f !== file && startsWith(file.path, f.path)
) !== undefined ) !== undefined
}) })
return redundantFiles return redundantFiles

View File

@@ -155,11 +155,11 @@ export default class Restore extends Component {
}) })
// TODO: perf // TODO: perf
let first, last let first, last
let size = 0
forEach(backupDataByVm, (data, vmId) => { forEach(backupDataByVm, (data, vmId) => {
first = { timestamp: Infinity } first = { timestamp: Infinity }
last = { timestamp: 0 } last = { timestamp: 0 }
const count = {} const count = {}
let size = 0
forEach(data.backups, backup => { forEach(data.backups, backup => {
if (backup.timestamp > last.timestamp) { if (backup.timestamp > last.timestamp) {
last = backup last = backup

View File

@@ -22,7 +22,7 @@ import { createGetObjectsOfType, getUser } from 'selectors'
import { createSelector } from 'reselect' import { createSelector } from 'reselect'
import { generateUiSchema } from 'xo-json-schema-input' import { generateUiSchema } from 'xo-json-schema-input'
import { SelectSubject } from 'select-objects' import { SelectSubject } from 'select-objects'
import { forEach, isArray, map, mapValues, noop } from 'lodash' import { forEach, isArray, map, mapValues, noop, startsWith } from 'lodash'
import { createJob, createSchedule, getRemote, editJob, editSchedule } from 'xo' import { createJob, createSchedule, getRemote, editJob, editSchedule } from 'xo'
@@ -479,7 +479,7 @@ export default class New extends Component {
if (remoteId) { if (remoteId) {
const remote = await getRemote(remoteId) const remote = await getRemote(remoteId)
if (remote.url.startsWith('file:')) { if (startsWith(remote.url, 'file:')) {
await confirm({ await confirm({
title: _('localRemoteWarningTitle'), title: _('localRemoteWarningTitle'),
body: _('localRemoteWarningMessage'), body: _('localRemoteWarningMessage'),

View File

@@ -55,7 +55,6 @@ const MultipathableSrs = decorate([
<Container> <Container>
{map(pbds, pbd => { {map(pbds, pbd => {
const [nActives, nPaths] = getIscsiPaths(pbd) const [nActives, nPaths] = getIscsiPaths(pbd)
const nSessions = pbd.otherConfig.iscsi_sessions
return ( return (
<Row key={pbd.id}> <Row key={pbd.id}>
<Col> <Col>
@@ -65,8 +64,8 @@ const MultipathableSrs = decorate([
_('hostMultipathingPaths', { _('hostMultipathingPaths', {
nActives, nActives,
nPaths, nPaths,
})}{' '} nSessions: pbd.otherConfig.iscsi_sessions,
{nSessions !== undefined && _('iscsiSessions', { nSessions })} })}
</Col> </Col>
</Row> </Row>
) )

View File

@@ -4,8 +4,8 @@ import Icon from 'icon'
import React from 'react' import React from 'react'
import Tooltip from 'tooltip' import Tooltip from 'tooltip'
import { Container, Row, Col } from 'grid' import { Container, Row, Col } from 'grid'
import { DEFAULT_GRANULARITY, fetchStats, SelectGranularity } from 'stats'
import { Toggle } from 'form' import { Toggle } from 'form'
import { fetchHostStats } from 'xo'
import { import {
CpuLineChart, CpuLineChart,
MemoryLineChart, MemoryLineChart,
@@ -14,9 +14,9 @@ import {
} from 'xo-line-chart' } from 'xo-line-chart'
export default class HostStats extends Component { export default class HostStats extends Component {
state = { constructor(props) {
granularity: DEFAULT_GRANULARITY, super(props)
useCombinedValues: false, this.state.useCombinedValues = false
} }
loop(host = this.props.host) { loop(host = this.props.host) {
@@ -33,7 +33,7 @@ export default class HostStats extends Component {
cancelled = true cancelled = true
} }
fetchStats(host, 'host', this.state.granularity).then(stats => { fetchHostStats(host, this.state.granularity).then(stats => {
if (cancelled) { if (cancelled) {
return return
} }
@@ -80,7 +80,8 @@ export default class HostStats extends Component {
} }
} }
handleSelectStats(granularity) { handleSelectStats(event) {
const granularity = event.target.value
clearTimeout(this.timeout) clearTimeout(this.timeout)
this.setState( this.setState(
@@ -124,11 +125,26 @@ export default class HostStats extends Component {
)} )}
</Col> </Col>
<Col mediumSize={6}> <Col mediumSize={6}>
<SelectGranularity <div className='btn-tab'>
<select
className='form-control'
onChange={this.handleSelectStats} onChange={this.handleSelectStats}
required defaultValue={granularity}
value={granularity} >
/> {_('statLastTenMinutes', message => (
<option value='seconds'>{message}</option>
))}
{_('statLastTwoHours', message => (
<option value='minutes'>{message}</option>
))}
{_('statLastWeek', message => (
<option value='hours'>{message}</option>
))}
{_('statLastYear', message => (
<option value='days'>{message}</option>
))}
</select>
</div>
</Col> </Col>
</Row> </Row>
<Row> <Row>

View File

@@ -1175,8 +1175,10 @@ export default class NewVm extends BaseComponent {
</LineItem> </LineItem>
<br /> <br />
<LineItem> <LineItem>
<Tooltip content={CAN_CLOUD_INIT ? undefined : _('premiumOnly')}>
<label> <label>
<Tooltip
content={CAN_CLOUD_INIT ? undefined : _('premiumOnly')}
>
<input <input
checked={installMethod === 'SSH'} checked={installMethod === 'SSH'}
disabled={!CAN_CLOUD_INIT} disabled={!CAN_CLOUD_INIT}
@@ -1185,10 +1187,10 @@ export default class NewVm extends BaseComponent {
type='radio' type='radio'
value='SSH' value='SSH'
/> />
</Tooltip>
&nbsp; &nbsp;
{_('newVmSshKey')} {_('newVmSshKey')}
</label> </label>
</Tooltip>
&nbsp; &nbsp;
<span className={classNames('input-group', styles.fixedWidth)}> <span className={classNames('input-group', styles.fixedWidth)}>
<DebounceInput <DebounceInput
@@ -1216,8 +1218,10 @@ export default class NewVm extends BaseComponent {
</LineItem> </LineItem>
<br /> <br />
<LineItem> <LineItem>
<Tooltip content={CAN_CLOUD_INIT ? undefined : _('premiumOnly')}>
<label> <label>
<Tooltip
content={CAN_CLOUD_INIT ? undefined : _('premiumOnly')}
>
<input <input
checked={installMethod === 'customConfig'} checked={installMethod === 'customConfig'}
disabled={!CAN_CLOUD_INIT} disabled={!CAN_CLOUD_INIT}
@@ -1226,10 +1230,10 @@ export default class NewVm extends BaseComponent {
type='radio' type='radio'
value='customConfig' value='customConfig'
/> />
</Tooltip>
&nbsp; &nbsp;
{_('newVmCustomConfig')} {_('newVmCustomConfig')}
</label> </label>
</Tooltip>
&nbsp; &nbsp;
<AvailableTemplateVars /> <AvailableTemplateVars />
&nbsp; &nbsp;

View File

@@ -4,14 +4,8 @@ import decorate from 'apply-decorators'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import React, { Component } from 'react' import React, { Component } from 'react'
import Wizard, { Section } from 'wizard' import Wizard, { Section } from 'wizard'
import { addSubscriptions, connectStore } from 'utils' import { connectStore } from 'utils'
import { import { createBondedNetwork, createNetwork, getBondModes } from 'xo'
createBondedNetwork,
createNetwork,
createPrivateNetwork,
getBondModes,
subscribePlugins,
} from 'xo'
import { createGetObject, getIsPoolAdmin } from 'selectors' import { createGetObject, getIsPoolAdmin } from 'selectors'
import { injectIntl } from 'react-intl' import { injectIntl } from 'react-intl'
import { injectState, provideState } from 'reaclette' import { injectState, provideState } from 'reaclette'
@@ -27,8 +21,6 @@ const EMPTY = {
bonded: false, bonded: false,
bondMode: undefined, bondMode: undefined,
description: '', description: '',
encapsulation: 'gre',
isPrivate: false,
mtu: '', mtu: '',
name: '', name: '',
pif: undefined, pif: undefined,
@@ -37,9 +29,6 @@ const EMPTY = {
} }
const NewNetwork = decorate([ const NewNetwork = decorate([
addSubscriptions({
plugins: subscribePlugins,
}),
connectStore(() => ({ connectStore(() => ({
isPoolAdmin: getIsPoolAdmin, isPoolAdmin: getIsPoolAdmin,
pool: createGetObject((_, props) => props.location.query.pool), pool: createGetObject((_, props) => props.location.query.pool),
@@ -53,26 +42,11 @@ const NewNetwork = decorate([
onChangeMode: (_, bondMode) => ({ bondMode }), onChangeMode: (_, bondMode) => ({ bondMode }),
onChangePif: (_, value) => ({ bonded }) => onChangePif: (_, value) => ({ bonded }) =>
bonded ? { pifs: value } : { pif: value }, bonded ? { pifs: value } : { pif: value },
onChangeEncapsulation(_, encapsulation) {
return { encapsulation: encapsulation.value }
},
reset: () => EMPTY, reset: () => EMPTY,
toggleBonded() { toggleBonded: () => ({ bonded }) => ({
const { bonded, isPrivate } = this.state
return {
...EMPTY, ...EMPTY,
bonded: !bonded, bonded: !bonded,
isPrivate: bonded ? isPrivate : false, }),
}
},
togglePrivate() {
const { bonded, isPrivate } = this.state
return {
...EMPTY,
isPrivate: !isPrivate,
bonded: isPrivate ? bonded : false,
}
},
}, },
computed: { computed: {
modeOptions: ({ bondModes }) => modeOptions: ({ bondModes }) =>
@@ -84,10 +58,6 @@ const NewNetwork = decorate([
: [], : [],
pifPredicate: (_, { pool }) => pif => pifPredicate: (_, { pool }) => pif =>
pif.vlan === -1 && pif.$host === (pool && pool.master), pif.vlan === -1 && pif.$host === (pool && pool.master),
isSdnControllerLoaded: (state, { plugins = [] }) =>
plugins.some(
plugin => plugin.name === 'sdn-controller' && plugin.loaded
),
}, },
}), }),
injectState, injectState,
@@ -101,9 +71,7 @@ const NewNetwork = decorate([
const { const {
bonded, bonded,
bondMode, bondMode,
isPrivate,
description, description,
encapsulation,
mtu, mtu,
name, name,
pif, pif,
@@ -120,13 +88,6 @@ const NewNetwork = decorate([
pool: pool.id, pool: pool.id,
vlan, vlan,
}) })
: isPrivate
? createPrivateNetwork({
poolId: pool.id,
networkName: name,
networkDescription: description,
encapsulation: encapsulation,
})
: createNetwork({ : createNetwork({
description, description,
mtu, mtu,
@@ -171,9 +132,7 @@ const NewNetwork = decorate([
const { const {
bonded, bonded,
bondMode, bondMode,
isPrivate,
description, description,
encapsulation,
modeOptions, modeOptions,
mtu, mtu,
name, name,
@@ -181,7 +140,6 @@ const NewNetwork = decorate([
pifPredicate, pifPredicate,
pifs, pifs,
vlan, vlan,
isSdnControllerLoaded,
} = state } = state
const { formatMessage } = intl const { formatMessage } = intl
return ( return (
@@ -194,48 +152,8 @@ const NewNetwork = decorate([
<Toggle onChange={effects.toggleBonded} value={bonded} />{' '} <Toggle onChange={effects.toggleBonded} value={bonded} />{' '}
<label>{_('bondedNetwork')}</label> <label>{_('bondedNetwork')}</label>
</div> </div>
<div>
<Toggle
disabled={!isSdnControllerLoaded}
onChange={effects.togglePrivate}
value={isPrivate}
/>{' '}
<label>{_('privateNetwork')}</label>
</div>
</Section> </Section>
<Section icon='info' title='newNetworkInfo'> <Section icon='info' title='newNetworkInfo'>
{isPrivate ? (
<div className='form-group'>
<label>{_('newNetworkName')}</label>
<input
className='form-control'
name='name'
onChange={effects.linkState}
required
type='text'
value={name}
/>
<label>{_('newNetworkDescription')}</label>
<input
className='form-control'
name='description'
onChange={effects.linkState}
type='text'
value={description}
/>
<label>{_('newNetworkEncapsulation')}</label>
<Select
className='form-control'
name='encapsulation'
onChange={effects.onChangeEncapsulation}
options={[
{ label: 'GRE', value: 'gre' },
{ label: 'VxLAN', value: 'vxlan' },
]}
value={encapsulation}
/>
</div>
) : (
<div className='form-group'> <div className='form-group'>
<label>{_('newNetworkInterface')}</label> <label>{_('newNetworkInterface')}</label>
<SelectPif <SelectPif
@@ -267,9 +185,7 @@ const NewNetwork = decorate([
className='form-control' className='form-control'
name='mtu' name='mtu'
onChange={effects.linkState} onChange={effects.linkState}
placeholder={formatMessage( placeholder={formatMessage(messages.newNetworkDefaultMtu)}
messages.newNetworkDefaultMtu
)}
type='text' type='text'
value={mtu} value={mtu}
/> />
@@ -299,7 +215,6 @@ const NewNetwork = decorate([
</div> </div>
)} )}
</div> </div>
)}
</Section> </Section>
</Wizard> </Wizard>
<div className='form-group pull-right'> <div className='form-group pull-right'>

View File

@@ -1,14 +1,15 @@
import _ from 'intl' import _ from 'intl'
import Component from 'base-component' import Component from 'base-component'
import getEventValue from 'get-event-value'
import Icon from 'icon' import Icon from 'icon'
import React from 'react' import React from 'react'
import Tooltip from 'tooltip' import Tooltip from 'tooltip'
import { connectStore } from 'utils'
import { Container, Row, Col } from 'grid' 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 { Toggle } from 'form'
import { fetchHostStats } from 'xo'
import { createGetObjectsOfType, createSelector } from 'selectors'
import { map } from 'lodash'
import { connectStore } from 'utils'
import { import {
PoolCpuLineChart, PoolCpuLineChart,
PoolMemoryLineChart, PoolMemoryLineChart,
@@ -26,7 +27,6 @@ import {
}) })
export default class PoolStats extends Component { export default class PoolStats extends Component {
state = { state = {
granularity: DEFAULT_GRANULARITY,
useCombinedValues: false, useCombinedValues: false,
} }
@@ -42,7 +42,7 @@ export default class PoolStats extends Component {
Promise.all( Promise.all(
map(this.props.hosts, host => map(this.props.hosts, host =>
fetchStats(host, 'host', this.state.granularity).then(stats => ({ fetchHostStats(host, this.state.granularity).then(stats => ({
host: host.name_label, host: host.name_label,
...stats, ...stats,
})) }))
@@ -74,7 +74,8 @@ export default class PoolStats extends Component {
clearTimeout(this.timeout) clearTimeout(this.timeout)
} }
_handleSelectStats = granularity => { _handleSelectStats = event => {
const granularity = getEventValue(event)
clearTimeout(this.timeout) clearTimeout(this.timeout)
this.setState( this.setState(
@@ -115,11 +116,26 @@ export default class PoolStats extends Component {
)} )}
</Col> </Col>
<Col mediumSize={6}> <Col mediumSize={6}>
<SelectGranularity <div className='btn-tab'>
<select
className='form-control'
onChange={this._handleSelectStats} onChange={this._handleSelectStats}
required defaultValue={granularity}
value={granularity} >
/> {_('statLastTenMinutes', message => (
<option value='seconds'>{message}</option>
))}
{_('statLastTwoHours', message => (
<option value='minutes'>{message}</option>
))}
{_('statLastWeek', message => (
<option value='hours'>{message}</option>
))}
{_('statLastYear', message => (
<option value='days'>{message}</option>
))}
</select>
</div>
</Col> </Col>
</Row> </Row>
<Row> <Row>

View File

@@ -25,7 +25,12 @@ import Upgrade from 'xoa-upgrade'
import { Container, Row, Col } from 'grid' import { Container, Row, Col } from 'grid'
import { injectIntl } from 'react-intl' import { injectIntl } from 'react-intl'
import { SizeInput } from 'form' import { SizeInput } from 'form'
import { addSubscriptions, adminOnly, connectStore, resolveIds } from 'utils' import {
addSubscriptions,
adminOnly,
connectStore,
resolveIds
} from 'utils'
import { import {
createGetObjectsOfType, createGetObjectsOfType,
createSelector, createSelector,

View File

@@ -16,9 +16,9 @@ import { injectIntl } from 'react-intl'
import { noop } from 'lodash' import { noop } from 'lodash'
import { import {
addServer, addServer,
disableServer,
editServer, editServer,
enableServer, connectServer,
disconnectServer,
removeServer, removeServer,
subscribeServers, subscribeServers,
} from 'xo' } from 'xo'
@@ -38,7 +38,7 @@ const showServerError = server => {
}).then( }).then(
() => () =>
editServer(server, { allowUnauthorized: true }).then(() => editServer(server, { allowUnauthorized: true }).then(() =>
enableServer(server) connectServer(server)
), ),
noop noop
) )
@@ -101,15 +101,16 @@ const COLUMNS = [
<div> <div>
<StateButton <StateButton
disabledLabel={_('serverDisabled')} disabledLabel={_('serverDisabled')}
disabledHandler={enableServer} disabledHandler={connectServer}
disabledTooltip={_('serverEnable')} disabledTooltip={_('serverDisabled')}
enabledLabel={_('serverEnabled')} enabledLabel={_('serverEnabled')}
enabledHandler={disableServer} enabledHandler={disconnectServer}
enabledTooltip={_('serverDisable')} enabledTooltip={_('serverDisable')}
handlerParam={server} handlerParam={server}
state={server.enabled} pending={server.status === 'connecting'}
state={server.status === 'connected'}
/>{' '} />{' '}
{server.error != null && ( {server.status === 'connected' && server.error && (
<Tooltip content={_('serverConnectionFailed')}> <Tooltip content={_('serverConnectionFailed')}>
<a <a
className='text-danger btn btn-link btn-sm' className='text-danger btn btn-link btn-sm'
@@ -128,11 +129,11 @@ const COLUMNS = [
itemRenderer: server => ( itemRenderer: server => (
<Toggle <Toggle
onChange={readOnly => editServer(server, { readOnly })} onChange={readOnly => editServer(server, { readOnly })}
value={server.readOnly} value={!!server.readOnly}
/> />
), ),
name: _('serverReadOnly'), name: _('serverReadOnly'),
sortCriteria: _ => _.readOnly, sortCriteria: _ => !!_.readOnly,
}, },
{ {
itemRenderer: server => ( itemRenderer: server => (
@@ -153,7 +154,7 @@ const COLUMNS = [
</Tooltip> </Tooltip>
</span> </span>
), ),
sortCriteria: _ => _.allowUnauthorized, sortCriteria: _ => !!_.allowUnauthorized,
}, },
{ {
itemRenderer: ({ poolId }) => itemRenderer: ({ poolId }) =>

View File

@@ -111,17 +111,14 @@ const HOST_WITH_PATHS_COLUMNS = [
} }
const [nActives, nPaths] = getIscsiPaths(pbd) const [nActives, nPaths] = getIscsiPaths(pbd)
const nSessions = pbd.otherConfig.iscsi_sessions
return ( return (
<span> nActives !== undefined &&
{nActives !== undefined &&
nPaths !== undefined && nPaths !== undefined &&
_('hostMultipathingPaths', { _('hostMultipathingPaths', {
nActives, nActives,
nPaths, nPaths,
})}{' '} nSessions: pbd.otherConfig.iscsi_sessions,
{nSessions !== undefined && _('iscsiSessions', { nSessions })} })
</span>
) )
}, },
sortCriteria: (pbd, hosts) => get(() => hosts[pbd.host].multipathing), sortCriteria: (pbd, hosts) => get(() => hosts[pbd.host].multipathing),

View File

@@ -4,7 +4,7 @@ import Icon from 'icon'
import React from 'react' import React from 'react'
import Tooltip from 'tooltip' import Tooltip from 'tooltip'
import { Container, Row, Col } from 'grid' import { Container, Row, Col } from 'grid'
import { DEFAULT_GRANULARITY, fetchStats, SelectGranularity } from 'stats' import { fetchSrStats } from 'xo'
import { get } from 'lodash' import { get } from 'lodash'
import { Toggle } from 'form' import { Toggle } from 'form'
import { import {
@@ -16,7 +16,7 @@ import {
export default class SrStats extends Component { export default class SrStats extends Component {
state = { state = {
granularity: DEFAULT_GRANULARITY, granularity: 'seconds',
} }
_loop(sr = get(this.props, 'sr')) { _loop(sr = get(this.props, 'sr')) {
@@ -33,7 +33,7 @@ export default class SrStats extends Component {
cancelled = true cancelled = true
} }
fetchStats(sr, 'sr', this.state.granularity).then(data => { fetchSrStats(sr, this.state.granularity).then(data => {
if (cancelled) { if (cancelled) {
return return
} }
@@ -62,7 +62,7 @@ export default class SrStats extends Component {
clearTimeout(this.timeout) clearTimeout(this.timeout)
} }
_onGranularityChange = granularity => { _onGranularityChange = ({ target: { value: granularity } }) => {
clearTimeout(this.timeout) clearTimeout(this.timeout)
this.setState( this.setState(
{ {
@@ -104,11 +104,26 @@ export default class SrStats extends Component {
)} )}
</Col> </Col>
<Col mediumSize={6}> <Col mediumSize={6}>
<SelectGranularity <div className='btn-tab'>
<select
className='form-control'
onChange={this._onGranularityChange} onChange={this._onGranularityChange}
required defaultValue={granularity}
value={granularity} >
/> {_('statLastTenMinutes', message => (
<option value='seconds'>{message}</option>
))}
{_('statLastTwoHours', message => (
<option value='minutes'>{message}</option>
))}
{_('statLastWeek', message => (
<option value='hours'>{message}</option>
))}
{_('statLastYear', message => (
<option value='days'>{message}</option>
))}
</select>
</div>
</Col> </Col>
</Row> </Row>
<Row> <Row>

View File

@@ -861,7 +861,7 @@ export default class TabAdvanced extends Component {
<td> <td>
<Number <Number
value={vm.CPUs.number} value={vm.CPUs.number}
onChange={CPUs => editVm(vm, { CPUs })} onChange={cpus => editVm(vm, { cpus })}
/> />
/ /
{vm.power_state === 'Running' ? ( {vm.power_state === 'Running' ? (
@@ -869,7 +869,9 @@ export default class TabAdvanced extends Component {
) : ( ) : (
<Number <Number
value={vm.CPUs.max} value={vm.CPUs.max}
onChange={cpusMax => editVm(vm, { cpusMax })} onChange={cpusStaticMax =>
editVm(vm, { cpusStaticMax })
}
/> />
)} )}
</td> </td>

View File

@@ -14,7 +14,6 @@ import { FormattedRelative, FormattedDate } from 'react-intl'
import { Container, Row, Col } from 'grid' import { Container, Row, Col } from 'grid'
import { Number, Size } from 'editable' import { Number, Size } from 'editable'
import { import {
createCollectionWrapper,
createFinder, createFinder,
createGetObjectsOfType, createGetObjectsOfType,
createGetVmLastShutdownTime, createGetVmLastShutdownTime,
@@ -49,15 +48,6 @@ export default connectStore(() => {
return { return {
lastShutdownTime: createGetVmLastShutdownTime(), lastShutdownTime: createGetVmLastShutdownTime(),
tasks: createGetObjectsOfType('task')
.pick(
createSelector(
(_, { vm }) => vm.current_operations,
createCollectionWrapper(Object.keys)
)
)
.filter({ status: 'pending' })
.sort(),
vgpu: getAttachedVgpu, vgpu: getAttachedVgpu,
vgpuTypes: getVgpuTypes, vgpuTypes: getVgpuTypes,
} }
@@ -65,7 +55,6 @@ export default connectStore(() => {
({ ({
lastShutdownTime, lastShutdownTime,
statsOverview, statsOverview,
tasks,
vgpu, vgpu,
vgpuTypes, vgpuTypes,
vm, vm,
@@ -74,6 +63,7 @@ export default connectStore(() => {
const { const {
addresses, addresses,
CPUs: cpus, CPUs: cpus,
current_operations: currentOperations,
id, id,
installTime, installTime,
memory, memory,
@@ -231,18 +221,12 @@ export default connectStore(() => {
</h2> </h2>
</Col> </Col>
</Row> </Row>
{isEmpty(tasks) ? null : ( {isEmpty(currentOperations) ? null : (
<Row className='text-xs-center'> <Row className='text-xs-center'>
<Col> <Col>
<h4>{_('vmCurrentStatus')}</h4> <h4>
{map(tasks, task => ( {_('vmCurrentStatus')} {map(currentOperations)[0]}
<p> </h4>
<strong>{task.name_label}</strong>
{task.progress > 0 && (
<span>: {Math.round(task.progress * 100)}%</span>
)}
</p>
))}
</Col> </Col>
</Row> </Row>
)} )}

View File

@@ -1,11 +1,12 @@
import _ from 'intl' import _, { messages } from 'intl'
import Component from 'base-component' import Component from 'base-component'
import Icon from 'icon' import Icon from 'icon'
import React from 'react' import React from 'react'
import Tooltip from 'tooltip' import Tooltip from 'tooltip'
import { Container, Row, Col } from 'grid' import { fetchVmStats } from 'xo'
import { DEFAULT_GRANULARITY, fetchStats, SelectGranularity } from 'stats'
import { Toggle } from 'form' import { Toggle } from 'form'
import { injectIntl } from 'react-intl'
import { Container, Row, Col } from 'grid'
import { import {
CpuLineChart, CpuLineChart,
MemoryLineChart, MemoryLineChart,
@@ -13,10 +14,11 @@ import {
XvdLineChart, XvdLineChart,
} from 'xo-line-chart' } from 'xo-line-chart'
export default class VmStats extends Component { export default injectIntl(
state = { class VmStats extends Component {
granularity: DEFAULT_GRANULARITY, constructor(props) {
useCombinedValues: false, super(props)
this.state.useCombinedValues = false
} }
loop(vm = this.props.vm) { loop(vm = this.props.vm) {
@@ -33,7 +35,7 @@ export default class VmStats extends Component {
cancelled = true cancelled = true
} }
fetchStats(vm, 'vm', this.state.granularity).then(stats => { fetchVmStats(vm, this.state.granularity).then(stats => {
if (cancelled) { if (cancelled) {
return return
} }
@@ -77,7 +79,8 @@ export default class VmStats extends Component {
} }
} }
handleSelectStats(granularity) { handleSelectStats(event) {
const granularity = event.target.value
clearTimeout(this.timeout) clearTimeout(this.timeout)
this.setState( this.setState(
@@ -91,6 +94,7 @@ export default class VmStats extends Component {
handleSelectStats = ::this.handleSelectStats handleSelectStats = ::this.handleSelectStats
render() { render() {
const { intl } = this.props
const { const {
granularity, granularity,
selectStatsLoading, selectStatsLoading,
@@ -119,11 +123,26 @@ export default class VmStats extends Component {
)} )}
</Col> </Col>
<Col mediumSize={6}> <Col mediumSize={6}>
<SelectGranularity <div className='btn-tab'>
<select
className='form-control'
onChange={this.handleSelectStats} onChange={this.handleSelectStats}
required defaultValue={granularity}
value={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> </Col>
</Row> </Row>
<Row> <Row>
@@ -160,3 +179,4 @@ export default class VmStats extends Component {
) )
} }
} }
)

1364
yarn.lock

File diff suppressed because it is too large Load Diff