Compare commits

..

10 Commits

Author SHA1 Message Date
Mohamedox
d2951f617b fix label id 2019-06-18 11:34:31 +02:00
Mohamedox
0ea64bdca7 remove nfs version 3 2019-06-18 11:34:31 +02:00
Mohamedox
f06bee3737 fix 2019-06-18 11:34:30 +02:00
Mohamedox
2693598ac8 change key nfs label name 2019-06-18 11:34:30 +02:00
Mohamedox
19011ad372 change nfs label name 2019-06-18 11:34:29 +02:00
Mohamedox
86eb7744a1 fix 2019-06-18 11:34:29 +02:00
Mohamedox
fe13ef6ff9 fix 2019-06-18 11:34:28 +02:00
Mohamedox
5607d34719 Fix select
Fixes #3951
2019-06-18 11:34:27 +02:00
Mohamedox
4501018dd6 update changelog 2019-06-18 11:34:27 +02:00
Mohamedox
9be9007fde fix 2019-06-18 11:34:06 +02:00
106 changed files with 1305 additions and 3384 deletions

View File

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

View File

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

View File

@@ -4,7 +4,6 @@
import getStream from 'get-stream'
import asyncMap from '@xen-orchestra/async-map'
import limit from 'limit-concurrency-decorator'
import path from 'path'
import synchronized from 'decorator-synchronized'
import { fromCallback, fromEvent, ignoreErrors, timeout } from 'promise-toolbox'
@@ -32,7 +31,6 @@ const computeRate = (hrtime: number[], size: number) => {
}
const DEFAULT_TIMEOUT = 6e5 // 10 min
const DEFAULT_MAX_PARALLEL_OPERATIONS = 10
const ignoreEnoent = error => {
if (error == null || error.code !== 'ENOENT') {
@@ -85,25 +83,6 @@ export default class RemoteHandlerAbstract {
}
}
;({ timeout: this._timeout = DEFAULT_TIMEOUT } = options)
const sharedLimit = limit(
options.maxParallelOperations ?? DEFAULT_MAX_PARALLEL_OPERATIONS
)
this.closeFile = sharedLimit(this.closeFile)
this.getInfo = sharedLimit(this.getInfo)
this.getSize = sharedLimit(this.getSize)
this.list = sharedLimit(this.list)
this.mkdir = sharedLimit(this.mkdir)
this.openFile = sharedLimit(this.openFile)
this.outputFile = sharedLimit(this.outputFile)
this.read = sharedLimit(this.read)
this.readFile = sharedLimit(this.readFile)
this.rename = sharedLimit(this.rename)
this.rmdir = sharedLimit(this.rmdir)
this.truncate = sharedLimit(this.truncate)
this.unlink = sharedLimit(this.unlink)
this.write = sharedLimit(this.write)
this.writeFile = sharedLimit(this.writeFile)
}
// Public members

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.error('something went wrong')
log.fatal('service/app is going down')
// you can add contextual info
log.debug('new API request', {
method: 'foo',
params: [ 'bar', 'baz' ]
user: 'qux'
})
// by convention, errors go into the `error` field
log.error('could not join server', {
error,
server: 'example.org',
})
```
Then, at application level, configure the logs are handled:

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,37 +1,13 @@
> 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
> Users must be able to say: “Nice enhancement, I'm eager to test it”
- [Stats] Ability to display last day stats [#4160](https://github.com/vatesfr/xen-orchestra/issues/4160) (PR [#4168](https://github.com/vatesfr/xen-orchestra/pull/4168))
- [Settings/servers] Display servers connection issues [#4300](https://github.com/vatesfr/xen-orchestra/issues/4300) (PR [#4310](https://github.com/vatesfr/xen-orchestra/pull/4310))
- [VM] Permission to revert to any snapshot for VM operators [#3928](https://github.com/vatesfr/xen-orchestra/issues/3928) (PR [#4247](https://github.com/vatesfr/xen-orchestra/pull/4247))
- [VM] Show current operations and progress [#3811](https://github.com/vatesfr/xen-orchestra/issues/3811) (PR [#3982](https://github.com/vatesfr/xen-orchestra/pull/3982))
- [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))
### Bug fixes
> Users must be able to say: “I had this issue, happy to know it's fixed”
- [Settings/Servers] Fix read-only setting toggling
- [SDN Controller] Do not choose physical PIF without IP configuration for tunnels. (PR [#4319](https://github.com/vatesfr/xen-orchestra/pull/4319))
- [Xen servers] Fix `no connection found for object` error if pool master is reinstalled [#4299](https://github.com/vatesfr/xen-orchestra/issues/4299) (PR [#4302](https://github.com/vatesfr/xen-orchestra/pull/4302))
- [Backup-ng/restore] Display correct size for full VM backup [#4316](https://github.com/vatesfr/xen-orchestra/issues/4316) (PR [#4332](https://github.com/vatesfr/xen-orchestra/pull/4332))
- [VM/tab-advanced] Fix CPU limits edition (PR [#4337](https://github.com/vatesfr/xen-orchestra/pull/4337))
- [Remotes] Fix `EIO` errors due to massive parallel fs operations [#4323](https://github.com/vatesfr/xen-orchestra/issues/4323) (PR [#4330](https://github.com/vatesfr/xen-orchestra/pull/4330))
### Released packages
> Packages will be released in the order they are here, therefore, they should
> be listed by inverse order of dependency.
>
> Rule of thumb: add packages on top.
- @xen-orchestra/fs v0.10.0
- xo-server-sdn-controller v0.1.1
- xen-api v0.26.0
- xo-server v5.45.0
- xo-web v5.45.0
- xo-server v5.44.0
- xo-web v5.44.0

View File

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

View File

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

View File

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

View File

@@ -364,7 +364,9 @@ export default class Vhd {
const offset = blockAddr + this.sectorsOfBitmap + beginSectorId
debug(
`writeBlockSectors at ${offset} block=${block.id}, sectors=${beginSectorId}...${endSectorId}`
`writeBlockSectors at ${offset} block=${
block.id
}, sectors=${beginSectorId}...${endSectorId}`
)
for (let i = beginSectorId; i < endSectorId; ++i) {

View File

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

View File

@@ -82,7 +82,7 @@ console.log(xapi.pool.$master.$resident_VMs[0].name_label)
A CLI is provided to help exploration and discovery of the XAPI.
```
> xen-api xen1.company.net root
> xen-api https://xen1.company.net root
Password: ******
root@xen1.company.net> xapi.status
'connected'
@@ -92,14 +92,6 @@ root@xen1.company.net> xapi.pool.$master.name_label
'xen1'
```
You can optionally prefix the address by a protocol: `https://` (default) or `http://`.
In case of error due to invalid or self-signed certificates you can use the `--allow-unauthorized` flag (or `--au`):
```
> xen-api --au xen1.company.net root
```
To ease searches, `find()` and `findAll()` functions are available:
```

View File

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

View File

@@ -99,9 +99,6 @@ export class Xapi extends EventEmitter {
this._sessionId = undefined
this._status = DISCONNECTED
this._watchEventsError = undefined
this._lastEventFetchedTimestamp = undefined
this._debounce = opts.debounce ?? 200
this._objects = new Collection()
this._objectsByRef = { __proto__: null }
@@ -171,6 +168,22 @@ export class Xapi extends EventEmitter {
try {
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)
this._status = CONNECTED
this._resolveConnected()
@@ -482,14 +495,6 @@ export class Xapi extends EventEmitter {
return this._objectsFetched
}
get lastEventFetchedTimestamp() {
return this._lastEventFetchedTimestamp
}
get watchEventsError() {
return this._watchEventsError
}
// ensure we have received all events up to this call
//
// optionally returns the up to date object for the given ref
@@ -734,28 +739,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) {
@@ -953,28 +936,21 @@ export class Xapi extends EventEmitter {
let result
try {
// don't use _sessionCall because a session failure should break the
// loop and trigger a complete refetch
result = await this._call(
result = await this._sessionCall(
'event.from',
[
this._sessionId,
types,
fromToken,
EVENT_TIMEOUT + 0.1, // must be float for XML-RPC transport
],
EVENT_TIMEOUT * 1e3 * 1.1
)
this._lastEventFetchedTimestamp = Date.now()
this._watchEventsError = undefined
} catch (error) {
const code = error?.code
if (code === 'EVENTS_LOST' || code === 'SESSION_INVALID') {
if (error?.code === 'EVENTS_LOST') {
// eslint-disable-next-line no-labels
continue mainLoop
}
this._watchEventsError = error
console.warn('_watchEvents', error)
await pDelay(this._eventPollDelay)
continue

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "xo-server-auth-saml",
"version": "0.6.0",
"version": "0.5.3",
"license": "AGPL-3.0",
"description": "SAML authentication plugin for XO-Server",
"keywords": [
@@ -33,7 +33,7 @@
"node": ">=6"
},
"dependencies": {
"passport-saml": "^1.1.0"
"passport-saml": "^1.0.0"
},
"devDependencies": {
"@babel/cli": "^7.0.0",

View File

@@ -24,10 +24,7 @@ export const configurationSchema = {
},
usernameField: {
title: 'Username field',
description: `Field to use as the XO username
You should try \`http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress\` if you are using Microsoft Azure Active Directory.
`,
description: 'Field to use as the XO username',
type: 'string',
},
},

View File

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

View File

@@ -142,14 +142,12 @@ const getErrorMarkdown = task => {
const MARKDOWN_BY_TYPE = {
pool(task, { formatDate }) {
const { id, pool = {}, poolMaster = {} } = task.data
const { pool, poolMaster = {} } = task.data
const name = pool.name_label || poolMaster.name_label || UNKNOWN_ITEM
return {
body: [
pool.uuid !== undefined
? `- **UUID**: ${pool.uuid}`
: `- **ID**: ${id}`,
`- **UUID**: ${pool.uuid}`,
...getTemporalDataMarkdown(task.end, task.start, formatDate),
getErrorMarkdown(task),
],
@@ -357,7 +355,9 @@ class BackupReportsXoPlugin {
nagiosStatus: log.status === 'success' ? 0 : 2,
nagiosMarkdown:
log.status === 'success'
? `[Xen Orchestra] [Success] Metadata backup report for ${log.jobName}`
? `[Xen Orchestra] [Success] Metadata backup report for ${
log.jobName
}`
: `[Xen Orchestra] [${log.status}] Metadata backup report for ${
log.jobName
} - ${nagiosText.join(' ')}`,
@@ -391,7 +391,9 @@ class BackupReportsXoPlugin {
} Backup report for ${jobName} ${STATUS_ICON[log.status]}`,
markdown: toMarkdown(markdown),
nagiosStatus: 2,
nagiosMarkdown: `[Xen Orchestra] [${log.status}] Backup report for ${jobName} - Error : ${log.result.message}`,
nagiosMarkdown: `[Xen Orchestra] [${
log.status
}] Backup report for ${jobName} - Error : ${log.result.message}`,
})
}
@@ -709,7 +711,9 @@ class BackupReportsXoPlugin {
subject: `[Xen Orchestra] ${globalStatus} ${icon}`,
markdown,
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 xapiDest = this.xo.getXapi(destination)
debug(
`Migrate VM (${vm.id}) to Host (${destination.id}) from Host (${vm.$container}).`
`Migrate VM (${vm.id}) to Host (${destination.id}) from Host (${
vm.$container
}).`
)
return xapiDest.migrateVm(
vm._xapiId,

View File

@@ -126,7 +126,9 @@ export default class PerformancePlan extends Plan {
destinationAverages.memoryFree -= vmAverages.memory
debug(
`Migrate VM (${vm.id}) to Host (${destination.id}) from Host (${exceededHost.id}).`
`Migrate VM (${vm.id}) to Host (${destination.id}) from Host (${
exceededHost.id
}).`
)
optimizationsCount++
@@ -141,7 +143,9 @@ export default class PerformancePlan extends Plan {
await Promise.all(promises)
debug(
`Performance mode: ${optimizationsCount} optimizations for Host (${exceededHost.id}).`
`Performance mode: ${optimizationsCount} optimizations for Host (${
exceededHost.id
}).`
)
}
}

View File

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

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.0",
"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": {
"nodemailer": "^6.1.0",
"nodemailer-markdown": "^1.0.1",
"promise-toolbox": "^0.13.0"
"promise-toolbox": "^0.12.1"
},
"devDependencies": {
"@babel/cli": "^7.0.0",

View File

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

View File

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

View File

@@ -29,9 +29,6 @@ guessVhdSizeOnImport = false
# be turned for investigation by the administrator.
verboseApiLogsOnErrors = false
# if no events could be fetched during this delay, the server will be marked as disconnected
xapiMarkDisconnectedDelay = '5 minutes'
# https:#github.com/websockets/ws#websocket-compression
[apiWebSocketOptions]
perMessageDeflate = { threshold = 524288 } # 512kiB

View File

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

View File

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

View File

@@ -211,25 +211,6 @@ emergencyShutdownHost.resolve = {
// -------------------------------------------------------------------
export async function isHostServerTimeConsistent({ host }) {
try {
await this.getXapi(host).assertConsistentHostServerTime(host._xapiRef)
return true
} catch (e) {
return false
}
}
isHostServerTimeConsistent.params = {
host: { type: 'string' },
}
isHostServerTimeConsistent.resolve = {
host: ['host', 'host', 'administrate'],
}
// -------------------------------------------------------------------
export function stats({ host, granularity }) {
return this.getXapiHostStats(host._xapiId, granularity)
}
@@ -284,19 +265,3 @@ installSupplementalPack.params = {
installSupplementalPack.resolve = {
host: ['host', 'host', 'admin'],
}
// -------------------------------------------------------------------
export function isHyperThreadingEnabled({ host }) {
return this.getXapi(host).isHyperThreadingEnabled(host._xapiId)
}
isHyperThreadingEnabled.description = 'get hyper-threading information'
isHyperThreadingEnabled.params = {
id: { type: 'string' },
}
isHyperThreadingEnabled.resolve = {
host: ['id', 'host', 'administrate'],
}

View File

@@ -1,7 +1,5 @@
// TODO: too low level, move into host.
import { filter, find } from 'lodash'
import { IPV4_CONFIG_MODES, IPV6_CONFIG_MODES } from '../xapi'
export function getIpv4ConfigurationModes() {
@@ -17,17 +15,7 @@ export function getIpv6ConfigurationModes() {
async function delete_({ pif }) {
// TODO: check if PIF is attached before
const xapi = this.getXapi(pif)
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)
await this.getXapi(pif).callAsync('PIF.destroy', pif._xapiRef)
}
export { delete_ as delete }

View File

@@ -168,7 +168,9 @@ export async function mergeInto({ source, target, force }) {
if (sourceHost.productBrand !== targetHost.productBrand) {
throw new Error(
`a ${sourceHost.productBrand} pool cannot be merged into a ${targetHost.productBrand} pool`
`a ${sourceHost.productBrand} pool cannot be merged into a ${
targetHost.productBrand
} pool`
)
}

View File

@@ -100,24 +100,20 @@ set.params = {
optional: true,
type: 'boolean',
},
readOnly: {
optional: true,
type: 'boolean',
},
}
// -------------------------------------------------------------------
export async function enable({ id }) {
export async function connect({ id }) {
this.updateXenServer(id, { enabled: true })::ignoreErrors()
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: {
type: 'string',
},
@@ -125,16 +121,16 @@ enable.params = {
// -------------------------------------------------------------------
export async function disable({ id }) {
export async function disconnect({ id }) {
this.updateXenServer(id, { enabled: false })::ignoreErrors()
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: {
type: 'string',
},

View File

@@ -1,6 +1,3 @@
import assert from 'assert'
import { fromEvent } from 'promise-toolbox'
export function getPermissionsForUser({ userId }) {
return this.getPermissionsForUser(userId)
}
@@ -89,35 +86,3 @@ copyVm.resolve = {
vm: ['vm', 'VM'],
sr: ['sr', 'SR'],
}
// -------------------------------------------------------------------
export async function changeConnectedXapiHostname({
hostname,
newObject,
oldObject,
}) {
const xapi = this.getXapi(oldObject)
const { pool: currentPool } = xapi
xapi._setUrl({ ...xapi._url, hostname })
await fromEvent(xapi.objects, 'finish')
if (xapi.pool.$id === currentPool.$id) {
await fromEvent(xapi.objects, 'finish')
}
assert(xapi.pool.$id !== currentPool.$id)
assert.doesNotThrow(() => this.getXapi(newObject))
assert.throws(() => this.getXapi(oldObject))
}
changeConnectedXapiHostname.description =
'change the connected XAPI hostname and check if the pool and the local cache are updated'
changeConnectedXapiHostname.permission = 'admin'
changeConnectedXapiHostname.params = {
hostname: { type: 'string' },
newObject: { type: 'string', description: "new connection's XO object" },
oldObject: { type: 'string', description: "current connection's XO object" },
}

View File

@@ -565,8 +565,6 @@ set.params = {
// Identifier of the VM to update.
id: { type: 'string' },
auto_poweron: { type: 'boolean', optional: true },
name_label: { type: 'string', optional: true },
name_description: { type: 'string', optional: true },
@@ -1134,10 +1132,7 @@ resume.resolve = {
// -------------------------------------------------------------------
export async function revert({ snapshot, snapshotBefore }) {
await this.checkPermissions(this.user.id, [
[snapshot.$snapshot_of, 'operate'],
])
export function revert({ snapshot, snapshotBefore }) {
return this.getXapi(snapshot).revertVm(snapshot._xapiId, snapshotBefore)
}
@@ -1147,7 +1142,7 @@ revert.params = {
}
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) {
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 }
await glusterCmd(
glusterEndpoint,
`volume replace-brick xosan ${previousBrick} ${addressAndHost.brickName} commit force`
`volume replace-brick xosan ${previousBrick} ${
addressAndHost.brickName
} commit force`
)
await glusterCmd(glusterEndpoint, 'peer detach ' + previousIp)
data.nodes.splice(nodeIndex, 1, {
@@ -1122,7 +1126,9 @@ async function _prepareGlusterVm(
}
await newVM.add_tags('XOSAN')
await xapi.editVm(newVM, {
name_label: `XOSAN - ${lvmSr.name_label} - ${host.name_label} ${labelSuffix}`,
name_label: `XOSAN - ${lvmSr.name_label} - ${
host.name_label
} ${labelSuffix}`,
name_description: 'Xosan VM storage',
memory: memorySize,
})

View File

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

View File

@@ -1,6 +1,6 @@
import Collection from '../collection/redis'
import Model from '../model'
import { forEach, serializeError } from '../utils'
import { forEach } from '../utils'
import { parseProp } from './utils'
@@ -30,28 +30,13 @@ export class Servers extends Collection {
// Deserializes
forEach(servers, server => {
server.allowUnauthorized = server.allowUnauthorized === 'true'
server.enabled = server.enabled === 'true'
if (server.error) {
server.error = parseProp('server', server, 'error', '')
} else {
delete server.error
}
server.readOnly = server.readOnly === 'true'
})
return servers
}
_update(servers) {
servers.map(server => {
server.allowUnauthorized = server.allowUnauthorized ? 'true' : undefined
server.enabled = server.enabled ? 'true' : undefined
const { error } = server
server.error =
error != null ? JSON.stringify(serializeError(error)) : undefined
server.readOnly = server.readOnly ? 'true' : undefined
})
return super._update(servers)
}
}

View File

@@ -13,7 +13,9 @@ export default function proxyConsole(ws, vmConsole, sessionId) {
hostname = address
log.warn(
`host is missing in console (${vmConsole.uuid}) URI (${vmConsole.location}) using host address (${address}) as fallback`
`host is missing in console (${vmConsole.uuid}) URI (${
vmConsole.location
}) using host address (${address}) as fallback`
)
}

View File

@@ -1,3 +1,5 @@
import { startsWith } from 'lodash'
import ensureArray from './_ensureArray'
import {
extractProperty,
@@ -117,7 +119,7 @@ const TRANSFORMS = {
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
} else {
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 = {
// type is redefined after for controllers/, templates &
// snapshots.
@@ -290,7 +281,7 @@ const TRANSFORMS = {
? +metrics.VCPUs_number
: +obj.VCPUs_at_startup,
},
current_operations: currentOperations,
current_operations: obj.current_operations,
docker: (function() {
const monitor = otherConfig['xscontainer-monitor']
if (!monitor) {

View File

@@ -4,6 +4,7 @@ import synchronized from 'decorator-synchronized'
import { BaseError } from 'make-error'
import {
defaults,
endsWith,
findKey,
forEach,
identity,
@@ -183,7 +184,7 @@ const STATS = {
transformValue: value => value * 1024,
},
memory: {
test: metricType => metricType.endsWith('memory'),
test: metricType => endsWith(metricType, 'memory'),
},
cpus: {
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 {
every,
filter,
find,
filter,
flatMap,
flatten,
groupBy,
@@ -31,6 +31,7 @@ import {
isEmpty,
noop,
omit,
startsWith,
uniq,
} from 'lodash'
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 (vdi.name_label.startsWith('[NOBAK]')) {
if (startsWith(vdi.name_label, '[NOBAK]')) {
// FIXME: find a way to not create the VDI snapshot in the
// first time.
//
@@ -1723,7 +1724,9 @@ export default class Xapi extends XapiBase {
}
log.debug(
`Moving VDI ${vdi.name_label} from ${vdi.$SR.name_label} to ${sr.name_label}`
`Moving VDI ${vdi.name_label} from ${vdi.$SR.name_label} to ${
sr.name_label
}`
)
try {
await pRetry(
@@ -2133,16 +2136,6 @@ export default class Xapi extends XapiBase {
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)
}
@@ -2343,7 +2336,7 @@ export default class Xapi extends XapiBase {
)
}
async assertConsistentHostServerTime(hostRef) {
async _assertConsistentHostServerTime(hostRef) {
const delta =
parseDateTime(await this.call('host.get_servertime', hostRef)).getTime() -
Date.now()
@@ -2355,27 +2348,4 @@ export default class Xapi extends XapiBase {
)
}
}
async isHyperThreadingEnabled(hostId) {
try {
return (
(await this.call(
'host.call_plugin',
this.getObject(hostId).$ref,
'hyperthreading.py',
'get_hyperthreading',
{}
)) !== 'false'
)
} catch (error) {
if (
error.code === 'XENAPI_MISSING_PLUGIN' ||
error.code === 'UNKNOWN_XENAPI_PLUGIN_FUNCTION'
) {
return null
} else {
throw error
}
}
}
}

View File

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

View File

@@ -1,8 +1,10 @@
import ms from 'ms'
import { forEach, isEmpty, iteratee, sortedIndexBy } from 'lodash'
import { noSuchObject } from 'xo-common/api-errors'
const isSkippedError = error =>
error.message === 'no disks found' ||
noSuchObject.is(error) ||
error.message === 'no VMs match this pattern' ||
error.message === 'unhealthy VDI chain'

View File

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

View File

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

View File

@@ -43,20 +43,6 @@ type MetadataBackupJob = {
xoMetadata?: boolean,
}
const logInstantFailureTask = (logger, { data, error, message, parentId }) => {
const taskId = logger.notice(message, {
data,
event: 'task.start',
parentId,
})
logger.error(message, {
event: 'task.end',
result: serializeError(error),
status: 'failure',
taskId,
})
}
const createSafeReaddir = (handler, methodName) => (path, options) =>
handler.list(path, options).catch(error => {
if (error?.code !== 'ENOENT') {
@@ -111,7 +97,7 @@ const deleteOldBackups = (handler, dir, retention, handleError) =>
// Task logs emitted in a metadata backup execution:
//
// job.start(data: { reportWhen: ReportWhen })
// ├─ task.start(data: { type: 'pool', id: string, pool?: <Pool />, poolMaster?: <Host /> })
// ├─ task.start(data: { type: 'pool', id: string, pool: <Pool />, poolMaster: <Host /> })
// │ ├─ task.start(data: { type: 'remote', id: string })
// │ │ └─ task.end
// │ └─ task.end
@@ -204,7 +190,9 @@ export default class metadataBackup {
await asyncMap(handlers, async (handler, remoteId) => {
const subTaskId = logger.notice(
`Starting XO metadata backup for the remote (${remoteId}). (${job.id})`,
`Starting XO metadata backup for the remote (${remoteId}). (${
job.id
})`,
{
data: {
id: remoteId,
@@ -242,7 +230,9 @@ export default class metadataBackup {
)
logger.notice(
`Backuping XO metadata for the remote (${remoteId}) is a success. (${job.id})`,
`Backuping XO metadata for the remote (${remoteId}) is a success. (${
job.id
})`,
{
event: 'task.end',
status: 'success',
@@ -261,7 +251,9 @@ export default class metadataBackup {
})
logger.error(
`Backuping XO metadata for the remote (${remoteId}) has failed. (${job.id})`,
`Backuping XO metadata for the remote (${remoteId}) has failed. (${
job.id
})`,
{
event: 'task.end',
result: serializeError(error),
@@ -334,7 +326,9 @@ export default class metadataBackup {
await asyncMap(handlers, async (handler, remoteId) => {
const subTaskId = logger.notice(
`Starting metadata backup for the pool (${poolId}) for the remote (${remoteId}). (${job.id})`,
`Starting metadata backup for the pool (${poolId}) for the remote (${remoteId}). (${
job.id
})`,
{
data: {
id: remoteId,
@@ -384,7 +378,9 @@ export default class metadataBackup {
)
logger.notice(
`Backuping pool metadata (${poolId}) for the remote (${remoteId}) is a success. (${job.id})`,
`Backuping pool metadata (${poolId}) for the remote (${remoteId}) is a success. (${
job.id
})`,
{
event: 'task.end',
status: 'success',
@@ -406,7 +402,9 @@ export default class metadataBackup {
})
logger.error(
`Backuping pool metadata (${poolId}) for the remote (${remoteId}) has failed. (${job.id})`,
`Backuping pool metadata (${poolId}) for the remote (${remoteId}) has failed. (${
job.id
})`,
{
event: 'task.end',
result: serializeError(error),
@@ -529,15 +527,16 @@ export default class metadataBackup {
try {
xapi = this._app.getXapi(id)
} catch (error) {
logInstantFailureTask(logger, {
data: {
type: 'pool',
id,
},
error,
message: `unable to get the xapi associated to the pool (${id})`,
parentId: runJobId,
})
logger.warning(
`unable to get the xapi associated to the pool (${id})`,
{
event: 'task.warning',
taskId: runJobId,
data: {
error,
},
}
)
}
if (xapi !== undefined) {
promises.push(

View File

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

View File

@@ -1,12 +1,10 @@
import createLogger from '@xen-orchestra/log'
import { BaseError } from 'make-error'
import { fibonacci } from 'iterable-backoff'
import { findKey } from 'lodash'
import { noSuchObject } from 'xo-common/api-errors'
import { pDelay, ignoreErrors } from 'promise-toolbox'
import * as XenStore from '../_XenStore'
import parseDuration from '../_parseDuration'
import Xapi from '../xapi'
import xapiObjectToXo from '../xapi-object-to-xo'
import XapiStats from '../xapi-stats'
@@ -16,6 +14,7 @@ import {
isEmpty,
isString,
popProperty,
serializeError,
} from '../utils'
import { Servers } from '../models/server'
@@ -42,10 +41,7 @@ const log = createLogger('xo:xo-mixins:xen-servers')
// - _xapis[server.id] id defined
// - _serverIdsByPool[xapi.pool.$id] is server.id
export default class {
constructor(
xo,
{ guessVhdSizeOnImport, xapiMarkDisconnectedDelay, xapiOptions }
) {
constructor(xo, { guessVhdSizeOnImport, xapiOptions }) {
this._objectConflicts = { __proto__: null } // TODO: clean when a server is disconnected.
const serversDb = (this._servers = new Servers({
connection: xo._redis,
@@ -60,7 +56,6 @@ export default class {
}
this._xapis = { __proto__: null }
this._xo = xo
this._xapiMarkDisconnectedDelay = parseDuration(xapiMarkDisconnectedDelay)
xo.on('clean', () => serversDb.rebuildIndexes())
xo.on('start', async () => {
@@ -99,23 +94,23 @@ export default class {
}
async registerXenServer({
allowUnauthorized = false,
allowUnauthorized,
host,
label,
password,
readOnly = false,
readOnly,
username,
}) {
// FIXME: We are storing passwords which is bad!
// Could we use tokens instead?
// TODO: use plain objects
const server = await this._servers.create({
allowUnauthorized,
enabled: true,
allowUnauthorized: allowUnauthorized ? 'true' : undefined,
enabled: 'true',
host,
label: label || undefined,
password,
readOnly,
readOnly: readOnly ? 'true' : undefined,
username,
})
@@ -167,22 +162,22 @@ export default class {
if (password) server.set('password', password)
if (error !== undefined) {
server.set('error', error)
server.set('error', error ? JSON.stringify(error) : '')
}
if (enabled !== undefined) {
server.set('enabled', enabled)
server.set('enabled', enabled ? 'true' : undefined)
}
if (readOnly !== undefined) {
server.set('readOnly', readOnly)
server.set('readOnly', readOnly ? 'true' : undefined)
if (xapi !== undefined) {
xapi.readOnly = readOnly
}
}
if (allowUnauthorized !== undefined) {
server.set('allowUnauthorized', allowUnauthorized)
server.set('allowUnauthorized', allowUnauthorized ? 'true' : undefined)
}
await this._servers.update(server)
@@ -210,21 +205,7 @@ export default class {
const conflicts = this._objectConflicts
const objects = this._xo._objects
const serverIdsByPool = this._serverIdsByPool
forEach(newXapiObjects, function handleObject(xapiObject, xapiId) {
// handle pool UUID change
if (
xapiObject.$type === 'pool' &&
serverIdsByPool[xapiObject.$id] === undefined
) {
const obsoletePoolId = findKey(
serverIdsByPool,
serverId => serverId === conId
)
delete serverIdsByPool[obsoletePoolId]
serverIdsByPool[xapiObject.$id] = conId
}
const { $ref } = xapiObject
const dependent = dependents[$ref]
@@ -294,8 +275,8 @@ export default class {
const server = (await this._getXenServer(id)).properties
const xapi = (this._xapis[server.id] = new Xapi({
allowUnauthorized: server.allowUnauthorized,
readOnly: server.readOnly,
allowUnauthorized: Boolean(server.allowUnauthorized),
readOnly: Boolean(server.readOnly),
...this._xapiOptions,
@@ -431,7 +412,7 @@ export default class {
} catch (error) {
delete this._xapis[server.id]
xapi.disconnect()::ignoreErrors()
this.updateXenServer(id, { error })::ignoreErrors()
this.updateXenServer(id, { error: serializeError(error) })::ignoreErrors()
throw error
}
}
@@ -492,14 +473,6 @@ export default class {
const servers = await this._servers.get()
const xapis = this._xapis
forEach(servers, server => {
const lastEventFetchedTimestamp =
xapis[server.id]?.lastEventFetchedTimestamp
if (
lastEventFetchedTimestamp !== undefined &&
Date.now() > lastEventFetchedTimestamp + this._xapiMarkDisconnectedDelay
) {
server.error = xapis[server.id].watchEventsError
}
server.status = this._getXenServerStatus(server.id)
if (server.status === 'connected') {
server.poolId = xapis[server.id].pool.uuid

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
import classNames from 'classnames'
import React from 'react'
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 Component from '../base-component'
@@ -119,7 +119,7 @@ class Editable extends Component {
this.setState({ saving: true })
const params = Object.keys(props).reduce((res, val) => {
if (val.startsWith('data-')) {
if (startsWith(val, 'data-')) {
res[val.slice(5)] = props[val]
}
return res

View File

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

View File

@@ -1,30 +0,0 @@
import _ from 'intl'
import decorate from 'apply-decorators'
import Icon from 'icon'
import PropTypes from 'prop-types'
import React from 'react'
import Tooltip from 'tooltip'
import { injectState, provideState } from 'reaclette'
import { isHostTimeConsistentWithXoaTime } from 'xo'
const InconsistentHostTimeWarning = decorate([
provideState({
computed: {
isHostTimeConsistentWithXoaTime: (_, { hostId }) =>
isHostTimeConsistentWithXoaTime(hostId),
},
}),
injectState,
({ state: { isHostTimeConsistentWithXoaTime = true } }) =>
isHostTimeConsistentWithXoaTime ? null : (
<Tooltip content={_('warningHostTimeTooltip')}>
<Icon color='text-danger' icon='alarm' />
</Tooltip>
),
])
InconsistentHostTimeWarning.propTypes = {
hostId: PropTypes.string.isRequired,
}
export { InconsistentHostTimeWarning as default }

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."
serverUnauthorizedCertificatesInfo: undefined,
// Original text: 'Disconnect server'
serverDisconnect: undefined,
// Original text: 'username'
serverPlaceHolderUser: undefined,
@@ -3088,6 +3091,12 @@ export default {
// Original text: 'Connecting…'
serverConnecting: undefined,
// Original text: 'Connected'
serverConnected: undefined,
// Original text: 'Disconnected'
serverDisconnected: undefined,
// Original text: 'Authentication error'
serverAuthFailed: undefined,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -47,8 +47,6 @@ const messages = {
chooseBackup: 'Choose a backup',
clickToShowError: 'Click to show error',
backupJobs: 'Backup jobs',
iscsiSessions:
'({ nSessions, number }) iSCSI session{nSessions, plural, one {} other {s}}',
// ----- Modals -----
alertOk: 'OK',
@@ -220,8 +218,6 @@ const messages = {
homeResourceSet: 'Resource set: {resourceSet}',
highAvailability: 'High Availability',
srSharedType: 'Shared {type}',
warningHostTimeTooltip:
'Host time and XOA time are not consistent with each other',
// ----- Home snapshots -----
snapshotVmsName: 'Name',
@@ -578,9 +574,7 @@ const messages = {
newSrNfsDefaultVersion: 'Default NFS version',
newSrNfsOptions: 'Comma delimited NFS options',
newSrNfs: 'NFS version',
noSharedZfsAvailable: 'No shared ZFS available',
reattachNewSrTooltip: 'Reattach SR',
srLocation: 'Storage location',
// ------ New Network -----
createNewNetworkNoPermission:
@@ -682,7 +676,6 @@ const messages = {
cloneVmLabel: 'Clone',
fastCloneVmLabel: 'Fast clone',
vmConsoleLabel: 'Console',
backupLabel: 'Backup',
// ----- SR advanced tab -----
@@ -799,8 +792,6 @@ const messages = {
'RAM: {memoryUsed} used on {memoryTotal} ({memoryFree} free)',
hardwareHostSettingsLabel: 'Hardware',
hyperThreading: 'Hyper-threading (SMT)',
hyperThreadingNotAvailable:
'HT detection is only available on XCP-ng 7.6 and higher',
hostAddress: 'Address',
hostStatus: 'Status',
hostBuildNumber: 'Build number',
@@ -808,7 +799,7 @@ const messages = {
hostNoIscsiSr: 'Not connected to an iSCSI SR',
hostMultipathingSrs: 'Click to see concerned SRs',
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:
'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:
@@ -864,7 +855,6 @@ const messages = {
// ----- Host storage tabs -----
addSrDeviceButton: 'Add a storage',
srType: 'Type',
pbdDetails: 'PBD details',
pbdStatus: 'Status',
pbdStatusConnected: 'Connected',
pbdStatusDisconnected: 'Disconnected',
@@ -959,7 +949,6 @@ const messages = {
statDisk: 'Disk throughput',
statLastTenMinutes: 'Last 10 minutes',
statLastTwoHours: 'Last 2 hours',
statLastDay: 'Last day',
statLastWeek: 'Last week',
statLastYear: 'Last year',
@@ -1038,7 +1027,6 @@ const messages = {
vifMacLabel: 'MAC address',
vifMtuLabel: 'MTU',
vifNetworkLabel: 'Network',
vifRateLimitLabel: 'Rate limit (kB/s)',
vifStatusLabel: 'Status',
vifStatusConnected: 'Connected',
vifStatusDisconnected: 'Disconnected',
@@ -1662,6 +1650,7 @@ const messages = {
serverAllowUnauthorizedCertificates: 'Allow Unauthorized Certificates',
serverUnauthorizedCertificatesInfo:
"Enable it if your certificate is rejected, but it's not recommended because your connection will not be secured.",
serverDisconnect: 'Disconnect server',
serverPlaceHolderUser: 'username',
serverPlaceHolderPassword: 'password',
serverPlaceHolderAddress: 'address[:port]',
@@ -1671,15 +1660,13 @@ const messages = {
serverAddFailed: 'Adding server failed',
serverStatus: 'Status',
serverConnectionFailed: 'Connection failed. Click for more information.',
serverConnected: 'Connected',
serverDisconnected: 'Disconnected',
serverAuthFailed: 'Authentication error',
serverUnknownError: 'Unknown error',
serverSelfSignedCertError: 'Invalid self-signed certificate',
serverSelfSignedCertQuestion:
'Do you want to accept self-signed certificate for this server even though it would decrease security?',
serverEnable: 'Enable',
serverEnabled: 'Enabled',
serverDisabled: 'Disabled',
serverDisable: 'Disable server',
// ----- Copy VM -----
copyVm: 'Copy VM',
@@ -1722,13 +1709,11 @@ const messages = {
newNetworkBondMode: 'Bond mode',
newNetworkInfo: 'Info',
newNetworkType: 'Type',
newNetworkEncapsulation: 'Encapsulation',
deleteNetwork: 'Delete network',
deleteNetworkConfirm: 'Are you sure you want to delete this network?',
networkInUse: 'This network is currently in use',
pillBonded: 'Bonded',
bondedNetwork: 'Bonded network',
privateNetwork: 'Private network',
// ----- Add host -----
addHostSelectHost: 'Host',

View File

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

View File

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

@@ -53,7 +53,3 @@ export const setXoaConfiguration = createAction(
'XOA_CONFIGURATION',
configuration => configuration
)
export const setHomeVmIdsSelection = createAction(
'SET_HOME_VM_IDS_SELECTION',
homeVmIdsSelection => homeVmIdsSelection
)

View File

@@ -86,12 +86,6 @@ export default {
}
),
// These IDs are used temporarily to be preselected in backup-ng/new/vms
homeVmIdsSelection: combineActionHandlers([], {
[actions.setHomeVmIdsSelection]: (_, homeVmIdsSelection) =>
homeVmIdsSelection,
}),
objects: combineActionHandlers(
{
all: {}, // Mutable for performance!

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 {
static propTypes = {
children: PropTypes.oneOfType([PropTypes.element, PropTypes.string]),

View File

@@ -22,6 +22,7 @@ import {
replace,
sample,
some,
startsWith,
} from 'lodash'
import _ from './intl'
@@ -476,7 +477,7 @@ export const compareVersions = makeNiceCompare((v1, v2) => {
return 0
})
export const isXosanPack = ({ name }) => name.startsWith('XOSAN')
export const isXosanPack = ({ name }) => startsWith(name, 'XOSAN')
// ===================================================================
@@ -645,14 +646,10 @@ export const createCompare = criterias => (...items) => {
// ===================================================================
export const hasLicenseRestrictions = host => {
const licenseType = host.license_params.sku_type
return (
host.productBrand !== 'XCP-ng' &&
versionSatisfies(host.version, '>=7.3.0') &&
(licenseType === 'free' || licenseType === 'express')
)
}
export const hasLicenseRestrictions = host =>
host.productBrand !== 'XCP-ng' &&
versionSatisfies(host.version, '>=7.3.0') &&
host.license_params.sku_type === 'free'
// ===================================================================

View File

@@ -536,13 +536,13 @@ export const editServer = (server, props) =>
subscribeServers.forceRefresh
)
export const enableServer = server =>
_call('server.enable', { id: resolveId(server) })::pFinally(
export const connectServer = server =>
_call('server.connect', { id: resolveId(server) })::pFinally(
subscribeServers.forceRefresh
)
export const disableServer = server =>
_call('server.disable', { id: resolveId(server) })::tap(
export const disconnectServer = server =>
_call('server.disconnect', { id: resolveId(server) })::tap(
subscribeServers.forceRefresh
)
@@ -777,14 +777,6 @@ export const emergencyShutdownHosts = hosts => {
}).then(() => map(hosts, host => emergencyShutdownHost(host)), noop)
}
export const isHostTimeConsistentWithXoaTime = host =>
_call('host.isHostServerTimeConsistent', { host: resolveId(host) })
export const isHyperThreadingEnabledHost = host =>
_call('host.isHyperThreadingEnabled', {
id: resolveId(host),
})
// for XCP-ng now
export const installAllPatchesOnHost = ({ host }) =>
confirm({
@@ -1628,15 +1620,14 @@ export const deleteVifs = vifs =>
export const setVif = (
vif,
{ allowedIpv4Addresses, allowedIpv6Addresses, mac, network, rateLimit }
{ network, mac, allowedIpv4Addresses, allowedIpv6Addresses }
) =>
_call('vif.set', {
id: resolveId(vif),
network: resolveId(network),
mac,
allowedIpv4Addresses,
allowedIpv6Addresses,
id: resolveId(vif),
mac,
network: resolveId(network),
rateLimit,
})
// Network -----------------------------------------------------------
@@ -1648,8 +1639,6 @@ export const getBondModes = () => _call('network.getBondModes')
export const createNetwork = params => _call('network.create', params)
export const createBondedNetwork = params =>
_call('network.createBonded', params)
export const createPrivateNetwork = params =>
_call('plugin.SDNController.createPrivateNetwork', params)
export const deleteNetwork = network =>
confirm({
@@ -2294,8 +2283,6 @@ export const probeSrHba = host => _call('sr.probeHba', { host })
export const probeSrHbaExists = (host, scsiId) =>
_call('sr.probeHbaExists', { host, scsiId })
export const probeZfs = host => _call('sr.probeZfs', { host: resolveId(host) })
export const reattachSr = (host, uuid, nameLabel, nameDescription, type) =>
_call('sr.reattach', { host, uuid, nameLabel, nameDescription, type })
@@ -2359,14 +2346,6 @@ export const createSrLvm = (host, nameLabel, nameDescription, device) =>
export const createSrExt = (host, nameLabel, nameDescription, device) =>
_call('sr.createExt', { host, nameLabel, nameDescription, device })
export const createSrZfs = (host, nameLabel, nameDescription, location) =>
_call('sr.createFile', {
host: resolveId(host),
nameDescription,
nameLabel,
location,
})
// Job logs ----------------------------------------------------------
export const deleteJobsLogs = async ids => {

View File

@@ -1,6 +1,7 @@
import _ from 'intl'
import ActionButton from 'action-button'
import Component from 'base-component'
import endsWith from 'lodash/endsWith'
import Icon from 'icon'
import React from 'react'
import replace from 'lodash/replace'
@@ -191,7 +192,7 @@ export default class RestoreFileModalBody extends Component {
select.blur()
select.focus()
const isFile = file.id !== '..' && !file.path.endsWith('/')
const isFile = file.id !== '..' && !endsWith(file.path, '/')
if (isFile) {
const { selectedFiles } = this.state
if (!includes(selectedFiles, file)) {
@@ -227,7 +228,7 @@ export default class RestoreFileModalBody extends Component {
_selectAllFolderFiles = () => {
this.setState({
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 { createSelector } from 'reselect'
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 { listPartitions, listFiles } from 'xo'
@@ -37,7 +46,7 @@ const fileOptionRenderer = ({ isFile, name }) => (
</span>
)
const ensureTrailingSlash = path => path + (path.endsWith('/') ? '' : '/')
const ensureTrailingSlash = path => path + (endsWith(path, '/') ? '' : '/')
// -----------------------------------------------------------------------------
@@ -57,7 +66,7 @@ const formatFilesOptions = (rawFiles, path) => {
return files.concat(
map(rawFiles, (_, name) => ({
id: `${path}${name}`,
isFile: !name.endsWith('/'),
isFile: !endsWith(name, '/'),
name,
path: `${path}${name}`,
}))
@@ -253,7 +262,7 @@ export default class RestoreFileModalBody extends Component {
redundantFiles[file.path] =
find(
files,
f => !f.isFile && f !== file && file.path.startsWith(f.path)
f => !f.isFile && f !== file && startsWith(file.path, f.path)
) !== undefined
})
return redundantFiles

View File

@@ -120,31 +120,28 @@ const createDoesRetentionExist = name => {
return ({ propSettings, settings = propSettings }) => settings.some(predicate)
}
const getInitialState = ({ preSelectedVmIds, setHomeVmIdsSelection }) => {
setHomeVmIdsSelection([]) // Clear preselected vmIds
return {
_displayAdvancedSettings: undefined,
_vmsPattern: undefined,
backupMode: false,
compression: undefined,
crMode: false,
deltaMode: false,
drMode: false,
name: '',
paramsUpdated: false,
remotes: [],
schedules: {},
settings: undefined,
showErrors: false,
smartMode: false,
snapshotMode: false,
srs: [],
tags: {
notValues: ['Continuous Replication', 'Disaster Recovery', 'XOSAN'],
},
vms: preSelectedVmIds,
}
}
const getInitialState = () => ({
_displayAdvancedSettings: undefined,
_vmsPattern: undefined,
backupMode: false,
compression: undefined,
crMode: false,
deltaMode: false,
drMode: false,
name: '',
paramsUpdated: false,
remotes: [],
schedules: {},
settings: undefined,
showErrors: false,
smartMode: false,
snapshotMode: false,
srs: [],
tags: {
notValues: ['Continuous Replication', 'Disaster Recovery', 'XOSAN'],
},
vms: [],
})
const DeleteOldBackupsFirst = ({ handler, handlerParam, value }) => (
<ActionButton
@@ -172,7 +169,6 @@ export default decorate([
hostsById: createGetObjectsOfType('host'),
poolsById: createGetObjectsOfType('pool'),
srsById: createGetObjectsOfType('SR'),
preSelectedVmIds: state => state.homeVmIdsSelection,
})),
injectIntl,
provideState({

View File

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

View File

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

View File

@@ -1,6 +1,5 @@
import _ from 'intl'
import Component from 'base-component'
import InconsistentHostTimeWarning from 'inconsistent-host-time-warning'
import Ellipsis, { EllipsisContainer } from 'ellipsis'
import Icon from 'icon'
import isEmpty from 'lodash/isEmpty'
@@ -129,8 +128,6 @@ export default class HostItem extends Component {
</Tooltip>
)}
&nbsp;
<InconsistentHostTimeWarning hostId={host.id} />
&nbsp;
{hasLicenseRestrictions(host) && <LicenseWarning />}
</EllipsisContainer>
</Col>

View File

@@ -178,14 +178,6 @@ const OPTIONS = {
icon: 'vm-snapshot',
labelId: 'snapshotVmLabel',
},
{
handler: (vmIds, _, { setHomeVmIdsSelection }, { router }) => {
setHomeVmIdsSelection(vmIds)
router.push('backup-ng/new/vms')
},
icon: 'backup',
labelId: 'backupLabel',
},
{
handler: deleteVms,
icon: 'vm-delete',
@@ -1018,9 +1010,7 @@ export default class Home extends Component {
onClick={() => {
action.handler(
this._getSelectedItemsIds(),
action.params,
this.props,
this.context
action.params
)
}}
>

View File

@@ -1,5 +1,4 @@
import _ from 'intl'
import InconsistentHostTimeWarning from 'inconsistent-host-time-warning'
import Copiable from 'copiable'
import HostActionBar from './action-bar'
import Icon from 'icon'
@@ -255,8 +254,6 @@ export default class Host extends Component {
</Link>
</Tooltip>
)}
&nbsp;
<InconsistentHostTimeWarning hostId={host.id} />
</h2>
<Copiable tagName='pre' className='text-muted mb-0'>
{host.uuid}

View File

@@ -21,7 +21,6 @@ import {
disableHost,
enableHost,
forgetHost,
isHyperThreadingEnabledHost,
installSupplementalPack,
restartHost,
setHostsMultipathing,
@@ -55,7 +54,6 @@ const MultipathableSrs = decorate([
<Container>
{map(pbds, pbd => {
const [nActives, nPaths] = getIscsiPaths(pbd)
const nSessions = pbd.otherConfig.iscsi_sessions
return (
<Row key={pbd.id}>
<Col>
@@ -65,8 +63,8 @@ const MultipathableSrs = decorate([
_('hostMultipathingPaths', {
nActives,
nPaths,
})}{' '}
{nSessions !== undefined && _('iscsiSessions', { nSessions })}
nSessions: pbd.otherConfig.iscsi_sessions,
})}
</Col>
</Row>
)
@@ -97,12 +95,6 @@ MultipathableSrs.propTypes = {
}
})
export default class extends Component {
async componentDidMount() {
this.setState({
isHtEnabled: await isHyperThreadingEnabledHost(this.props.host),
})
}
_getPacks = createSelector(
() => this.props.host.supplementalPacks,
packs => {
@@ -120,12 +112,14 @@ export default class extends Component {
return uniqPacks
}
)
_isHtEnabled = createSelector(
() => this.props.host.CPUs.flags,
flags => /\bht\b/.test(flags)
)
_setRemoteSyslogHost = value => setRemoteSyslogHost(this.props.host, value)
render() {
const { host, pcis, pgpus } = this.props
const { isHtEnabled } = this.state
return (
<Container>
<Row>
@@ -285,9 +279,7 @@ export default class extends Component {
<tr>
<th>{_('hyperThreading')}</th>
<td>
{isHtEnabled === null
? _('hyperThreadingNotAvailable')
: isHtEnabled
{this._isHtEnabled()
? _('stateEnabled')
: _('stateDisabled')}
</td>

View File

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

View File

@@ -87,19 +87,6 @@ const SR_COLUMNS = [
storage.shared ? _('srShared') : _('srNotShared'),
sortCriteria: 'shared',
},
{
name: _('pbdDetails'),
itemRenderer: ({ pbdDeviceConfig }) => {
const keys = Object.keys(pbdDeviceConfig)
return (
<ul className='list-unstyled'>
{keys.map(key => (
<li key={key}>{_.keyValue(key, pbdDeviceConfig[key])}</li>
))}
</ul>
)
},
},
{
name: _('pbdStatus'),
itemRenderer: storage => (
@@ -150,7 +137,6 @@ export default connectStore(() => {
return {
attached: pbd.attached,
pbdDeviceConfig: pbd.device_config,
format: sr.SR_type,
free: size > 0 ? size - usage : 0,
id: sr.id,

View File

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

View File

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

View File

@@ -33,7 +33,6 @@ import {
createSrLvm,
createSrNfs,
createSrHba,
createSrZfs,
probeSrIscsiExists,
probeSrIscsiIqns,
probeSrIscsiLuns,
@@ -41,7 +40,6 @@ import {
probeSrNfsExists,
probeSrHba,
probeSrHbaExists,
probeZfs,
reattachSrIso,
reattachSr,
} from 'xo'
@@ -198,15 +196,14 @@ class SelectLun extends Component {
// ===================================================================
const SR_TYPE_TO_LABEL = {
ext: 'ext (local)',
ext: 'Local Ext',
hba: 'HBA',
iscsi: 'iSCSI',
local: 'Local',
lvm: 'LVM (local)',
lvm: 'Local LVM',
nfs: 'NFS',
nfsiso: 'NFS ISO',
smb: 'SMB',
zfs: 'ZFS (local)',
}
const SR_GROUP_TO_LABEL = {
@@ -215,7 +212,7 @@ const SR_GROUP_TO_LABEL = {
}
const typeGroups = {
vdisr: ['ext', 'hba', 'iscsi', 'lvm', 'nfs', 'zfs'],
vdisr: ['ext', 'hba', 'iscsi', 'lvm', 'nfs'],
isosr: ['local', 'nfsiso', 'smb'],
}
@@ -253,7 +250,6 @@ export default class New extends Component {
unused: undefined,
usage: undefined,
used: undefined,
zfsPools: undefined,
}
this.getHostSrs = createFilter(
() => this.props.srs,
@@ -275,7 +271,6 @@ export default class New extends Component {
port,
server,
username,
zfsLocation,
} = this.refs
const {
host,
@@ -349,8 +344,6 @@ export default class New extends Component {
createSrLvm(host.id, name.value, description.value, device.value),
ext: () =>
createSrExt(host.id, name.value, description.value, device.value),
zfs: () =>
createSrZfs(host.id, name.value, description.value, zfsLocation.value),
local: () =>
createSrIso(
host.id,
@@ -388,10 +381,7 @@ export default class New extends Component {
}
}
_handleSrHostSelection = async host => {
this.setState({ host })
await this._probe(host, this.state.type)
}
_handleSrHostSelection = host => this.setState({ host })
_handleNameChange = event => this.setState({ name: event.target.value })
_handleDescriptionChange = event =>
this.setState({ description: event.target.value })
@@ -402,13 +392,20 @@ export default class New extends Component {
hbaDevices: undefined,
iqns: undefined,
paths: undefined,
summary: includes(['ext', 'lvm', 'local', 'smb', 'hba', 'zfs'], type),
summary: includes(['ext', 'lvm', 'local', 'smb', 'hba'], type),
type,
unused: undefined,
usage: undefined,
used: undefined,
})
await this._probe(this.state.host, type)
if (type === 'hba' && this.state.host !== undefined) {
this.setState(({ loading }) => ({ loading: loading + 1 }))
const hbaDevices = await probeSrHba(this.state.host.id)::ignoreErrors()
this.setState(({ loading }) => ({
hbaDevices,
loading: loading - 1,
}))
}
}
_handleSrHbaSelection = async scsiId => {
@@ -545,25 +542,6 @@ export default class New extends Component {
})
}
_probe = async (host, type) => {
const probeMethodFactories = {
hba: async hostId => ({
hbaDevices: await probeSrHba(hostId)::ignoreErrors(),
}),
zfs: async hostId => ({
zfsPools: await probeZfs(hostId)::ignoreErrors(),
}),
}
if (probeMethodFactories[type] !== undefined && host != null) {
this.setState(({ loading }) => ({ loading: loading + 1 }))
const probeResult = await probeMethodFactories[type](host.id)
this.setState(({ loading }) => ({
loading: loading - 1,
...probeResult,
}))
}
}
_reattach = async uuid => {
const { host, type } = this.state
@@ -616,7 +594,6 @@ export default class New extends Component {
unused,
usage,
used,
zfsPools,
} = this.state
const { formatMessage } = this.props.intl
@@ -915,31 +892,6 @@ export default class New extends Component {
/>
</fieldset>
)}
{type === 'zfs' && (
<fieldset>
<label htmlFor='selectSrLocation'>
{_('srLocation')}
</label>
<select
className='form-control'
defaultValue=''
id='selectSrLocation'
ref='zfsLocation'
required
>
<option value=''>
{isEmpty(zfsPools)
? formatMessage(messages.noSharedZfsAvailable)
: formatMessage(messages.noSelectedValue)}
</option>
{map(zfsPools, (pool, poolName) => (
<option key={poolName} value={pool.mountpoint}>
{poolName} - {pool.mountpoint}
</option>
))}
</select>
</fieldset>
)}
</fieldset>
)}
{loading !== 0 && <Icon icon='loading' />}

View File

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

View File

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

View File

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

View File

@@ -55,19 +55,6 @@ const HOST_COLUMNS = [
},
sortCriteria: (pbd, hosts) => hosts[pbd.host].name_description,
},
{
name: _('pbdDetails'),
itemRenderer: ({ device_config: deviceConfig }) => {
const keys = Object.keys(deviceConfig)
return (
<ul className='list-unstyled'>
{keys.map(key => (
<li key={key}>{_.keyValue(key, deviceConfig[key])}</li>
))}
</ul>
)
},
},
{
name: _('pbdStatus'),
itemRenderer: pbd => (
@@ -111,17 +98,14 @@ const HOST_WITH_PATHS_COLUMNS = [
}
const [nActives, nPaths] = getIscsiPaths(pbd)
const nSessions = pbd.otherConfig.iscsi_sessions
return (
<span>
{nActives !== undefined &&
nPaths !== undefined &&
_('hostMultipathingPaths', {
nActives,
nPaths,
})}{' '}
{nSessions !== undefined && _('iscsiSessions', { nSessions })}
</span>
nActives !== undefined &&
nPaths !== undefined &&
_('hostMultipathingPaths', {
nActives,
nPaths,
nSessions: pbd.otherConfig.iscsi_sessions,
})
)
},
sortCriteria: (pbd, hosts) => get(() => hosts[pbd.host].multipathing),

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