Compare commits
3 Commits
xo-server-
...
new-vm-aut
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cbdfccb716 | ||
|
|
d605e9740d | ||
|
|
00d2a88da3 |
@@ -35,9 +35,6 @@ module.exports = {
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
// disabled because XAPI objects are using camel case
|
||||
camelcase: ['off'],
|
||||
|
||||
'no-console': ['error', { allow: ['warn', 'error'] }],
|
||||
'no-var': 'error',
|
||||
'node/no-extraneous-import': 'error',
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"golike-defer": "^0.4.1",
|
||||
"xen-api": "^0.27.1"
|
||||
"xen-api": "^0.25.1"
|
||||
},
|
||||
"scripts": {
|
||||
"postversion": "npm publish"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@xen-orchestra/fs",
|
||||
"version": "0.10.1",
|
||||
"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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
90
CHANGELOG.md
90
CHANGELOG.md
@@ -1,106 +1,22 @@
|
||||
# ChangeLog
|
||||
|
||||
## **next**
|
||||
## **next** (2019-05-14)
|
||||
|
||||
### Enhancements
|
||||
|
||||
- [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))
|
||||
- [SR/General] Improve SR usage graph [#3608](https://github.com/vatesfr/xen-orchestra/issues/3608) (PR [#3830](https://github.com/vatesfr/xen-orchestra/pull/3830))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- [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))
|
||||
- [VM/Advanced] Fix virtualization mode switch (PV/HVM) (PR [#4349](https://github.com/vatesfr/xen-orchestra/pull/4349))
|
||||
|
||||
### Released packages
|
||||
|
||||
- @xen-orchestra/fs v0.10.0
|
||||
- xo-server-sdn-controller v0.1.1
|
||||
- xen-api v0.27.1
|
||||
- xo-server v5.45.2
|
||||
- xo-web v5.45.1
|
||||
|
||||
## **5.36.0** (2019-06-27)
|
||||
|
||||

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

|
||||
|
||||
### Enhancements
|
||||
|
||||
- [VM/general] Display 'Started... ago' instead of 'Halted... ago' for paused state [#3750](https://github.com/vatesfr/xen-orchestra/issues/3750) (PR [#4170](https://github.com/vatesfr/xen-orchestra/pull/4170))
|
||||
- [Metadata backup] Ability to define when the backup report will be sent (PR [#4149](https://github.com/vatesfr/xen-orchestra/pull/4149))
|
||||
- [XOA/Update] Ability to select release channel [#4200](https://github.com/vatesfr/xen-orchestra/issues/4200) (PR [#4202](https://github.com/vatesfr/xen-orchestra/pull/4202))
|
||||
- [User] Forget connection tokens on password change or on demand [#4214](https://github.com/vatesfr/xen-orchestra/issues/4214) (PR [#4224](https://github.com/vatesfr/xen-orchestra/pull/4224))
|
||||
- [Settings/Logs] LICENCE_RESTRICTION errors: suggest XCP-ng as an Open Source alternative [#3876](https://github.com/vatesfr/xen-orchestra/issues/3876) (PR [#4238](https://github.com/vatesfr/xen-orchestra/pull/4238))
|
||||
- [VM/Migrate] Display VDI size on migrate modal [#2534](https://github.com/vatesfr/xen-orchestra/issues/2534) (PR [#4250](https://github.com/vatesfr/xen-orchestra/pull/4250))
|
||||
- [Host] Display hyperthreading status on advanced tab [#4262](https://github.com/vatesfr/xen-orchestra/issues/4262) (PR [#4263](https://github.com/vatesfr/xen-orchestra/pull/4263))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- [Pool/Patches] Fix "an error has occurred" in "Applied patches" [#4192](https://github.com/vatesfr/xen-orchestra/issues/4192) (PR [#4193](https://github.com/vatesfr/xen-orchestra/pull/4193))
|
||||
- [Backup NG] Fix report sent even though "Never" is selected [#4092](https://github.com/vatesfr/xen-orchestra/issues/4092) (PR [#4178](https://github.com/vatesfr/xen-orchestra/pull/4178))
|
||||
- [Remotes] Fix issues after a config import (PR [#4197](https://github.com/vatesfr/xen-orchestra/pull/4197))
|
||||
- [Charts] Fixed the chart lines sometimes changing order/color (PR [#4221](https://github.com/vatesfr/xen-orchestra/pull/4221))
|
||||
- Prevent non-admin users to access admin pages with URL (PR [#4220](https://github.com/vatesfr/xen-orchestra/pull/4220))
|
||||
- [Upgrade] Fix alert before upgrade while running backup jobs [#4164](https://github.com/vatesfr/xen-orchestra/issues/4164) (PR [#4235](https://github.com/vatesfr/xen-orchestra/pull/4235))
|
||||
- [Import] Fix import OVA files (PR [#4232](https://github.com/vatesfr/xen-orchestra/pull/4232))
|
||||
- [VM/network] Fix duplicate IPv4 (PR [#4239](https://github.com/vatesfr/xen-orchestra/pull/4239))
|
||||
- [Remotes] Fix disconnected remotes which may appear to work
|
||||
- [Host] Fix incorrect hypervisor name [#4246](https://github.com/vatesfr/xen-orchestra/issues/4246) (PR [#4248](https://github.com/vatesfr/xen-orchestra/pull/4248))
|
||||
|
||||
### Released packages
|
||||
|
||||
- xo-server-backup-reports v0.16.1
|
||||
- @xen-orchestra/fs v0.9.0
|
||||
- vhd-lib v0.7.0
|
||||
- xo-server v5.42.1
|
||||
- xo-web v5.42.1
|
||||
- xo-server v5.41.0
|
||||
- xo-web v5.41.0
|
||||
|
||||
## **5.34.0** (2019-04-30)
|
||||
|
||||
|
||||
@@ -1,22 +1,20 @@
|
||||
> 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”
|
||||
- [VM/general] Display 'Started... ago' instead of 'Halted... ago' for paused state [#3750](https://github.com/vatesfr/xen-orchestra/issues/3750) (PR [#4170](https://github.com/vatesfr/xen-orchestra/pull/4170))
|
||||
- [Metadata backup] Ability to define when the backup report will be sent (PR [#4149](https://github.com/vatesfr/xen-orchestra/pull/4149))
|
||||
- [XOA/Update] Ability to select release channel [#4200](https://github.com/vatesfr/xen-orchestra/issues/4200) (PR [#4202](https://github.com/vatesfr/xen-orchestra/pull/4202))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
> Users must be able to say: “I had this issue, happy to know it's fixed”
|
||||
- [Charts] Fixed the chart lines sometimes changing order/color (PR [#4221](https://github.com/vatesfr/xen-orchestra/pull/4221))
|
||||
- Prevent non-admin users to access admin pages with URL
|
||||
- [Upgrade] Fix alert before upgrade while running backup jobs (PR [#4235](https://github.com/vatesfr/xen-orchestra/pull/4235))
|
||||
- [Import] Fix import OVA files (PR [#4232](https://github.com/vatesfr/xen-orchestra/pull/4232))
|
||||
- [VM/network] Fix duplicate IPv4 (PR [#4239](https://github.com/vatesfr/xen-orchestra/pull/4239))
|
||||
|
||||
### 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.
|
||||
|
||||
- xo-server v5.46.0
|
||||
- xo-web v5.46.0
|
||||
- xo-server v5.42.0
|
||||
- xo-web v5.42.0
|
||||
|
||||
12
package.json
12
package.json
@@ -6,8 +6,8 @@
|
||||
"babel-eslint": "^10.0.1",
|
||||
"babel-jest": "^24.1.0",
|
||||
"benchmark": "^2.1.4",
|
||||
"eslint": "^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.98.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": {
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
"node": ">=6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@xen-orchestra/fs": "^0.10.1",
|
||||
"@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"
|
||||
},
|
||||
|
||||
@@ -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,12 +35,12 @@
|
||||
"@babel/core": "^7.0.0",
|
||||
"@babel/preset-env": "^7.0.0",
|
||||
"@babel/preset-flow": "^7.0.0",
|
||||
"@xen-orchestra/fs": "^0.10.1",
|
||||
"@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",
|
||||
"get-stream": "^4.0.0",
|
||||
"index-modules": "^0.3.0",
|
||||
"readable-stream": "^3.0.6",
|
||||
"rimraf": "^2.6.2",
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
"human-format": "^0.10.0",
|
||||
"lodash": "^4.17.4",
|
||||
"pw": "^0.0.4",
|
||||
"xen-api": "^0.27.1"
|
||||
"xen-api": "^0.25.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.1.5",
|
||||
|
||||
@@ -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:
|
||||
|
||||
```
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "xen-api",
|
||||
"version": "0.27.1",
|
||||
"version": "0.25.1",
|
||||
"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"
|
||||
|
||||
@@ -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
|
||||
@@ -1083,14 +1059,9 @@ export class Xapi extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
props[`add_${field}`] = function(value) {
|
||||
props[`add_to_${field}`] = function(...values) {
|
||||
return xapi
|
||||
.call(`${type}.add_${field}`, this.$ref, value)
|
||||
.then(noop)
|
||||
}
|
||||
props[`remove_${field}`] = function(value) {
|
||||
return xapi
|
||||
.call(`${type}.remove_${field}`, this.$ref, value)
|
||||
.call(`${type}.add_${field}`, this.$ref, values)
|
||||
.then(noop)
|
||||
}
|
||||
} else if (value !== null && typeof value === 'object') {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
|
||||
@@ -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')
|
||||
)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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}`)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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
|
||||
}`,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}).`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.')
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
module.exports = require('../../@xen-orchestra/babel-config')(
|
||||
require('./package.json')
|
||||
)
|
||||
@@ -1,43 +0,0 @@
|
||||
# xo-server-sdn-controller [](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)
|
||||
@@ -1,36 +0,0 @@
|
||||
{
|
||||
"name": "xo-server-sdn-controller",
|
||||
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/packages/xo-server-sdn-controller",
|
||||
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
|
||||
"repository": {
|
||||
"directory": "packages/xo-server-sdn-controller",
|
||||
"type": "git",
|
||||
"url": "https://github.com/vatesfr/xen-orchestra.git"
|
||||
},
|
||||
"main": "./dist",
|
||||
"scripts": {
|
||||
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
|
||||
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
|
||||
"prebuild": "rimraf dist/",
|
||||
"predev": "yarn run prebuild",
|
||||
"prepublishOnly": "yarn run build"
|
||||
},
|
||||
"version": "0.1.1",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.4.4",
|
||||
"@babel/core": "^7.4.4",
|
||||
"@babel/plugin-proposal-optional-chaining": "^7.2.0",
|
||||
"@babel/preset-env": "^7.4.4",
|
||||
"cross-env": "^5.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@xen-orchestra/log": "^0.1.4",
|
||||
"lodash": "^4.17.11",
|
||||
"node-openssl-cert": "^0.0.84",
|
||||
"promise-toolbox": "^0.13.0"
|
||||
},
|
||||
"private": true
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
"node": ">=6"
|
||||
},
|
||||
"dependencies": {
|
||||
"promise-toolbox": "^0.13.0",
|
||||
"promise-toolbox": "^0.12.1",
|
||||
"slack-node": "^0.1.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
"html-minifier": "^4.0.0",
|
||||
"human-format": "^0.10.0",
|
||||
"lodash": "^4.17.4",
|
||||
"promise-toolbox": "^0.13.0"
|
||||
"promise-toolbox": "^0.12.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.0.0",
|
||||
|
||||
@@ -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
|
||||
@@ -52,11 +49,6 @@ maxTokenValidity = '0.5 year'
|
||||
# Delay for which backups listing on a remote is cached
|
||||
listingDebounce = '1 min'
|
||||
|
||||
# Duration for which we can wait for the backup size before returning
|
||||
#
|
||||
# It should be short to avoid blocking the display of the available backups.
|
||||
vmBackupSizeTimeout = '2 seconds'
|
||||
|
||||
# Helmet handles HTTP security via headers
|
||||
#
|
||||
# https://helmetjs.github.io/docs/
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "xo-server",
|
||||
"version": "5.45.3",
|
||||
"version": "5.41.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.1",
|
||||
"@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.27.1",
|
||||
"xen-api": "^0.25.1",
|
||||
"xml2js": "^0.4.19",
|
||||
"xo-acl-resolver": "^0.4.1",
|
||||
"xo-collection": "^0.4.1",
|
||||
|
||||
@@ -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,
|
||||
@@ -194,7 +183,6 @@ getLogs.params = {
|
||||
after: { type: ['number', 'string'], optional: true },
|
||||
before: { type: ['number', 'string'], optional: true },
|
||||
limit: { type: 'number', optional: true },
|
||||
'*': { type: 'any' },
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
@@ -4,19 +4,23 @@ import { format, JsonRpcError } from 'json-rpc-peer'
|
||||
|
||||
export async function set({
|
||||
host,
|
||||
|
||||
multipathing,
|
||||
|
||||
// TODO: use camel case.
|
||||
name_label: nameLabel,
|
||||
name_description: nameDescription,
|
||||
}) {
|
||||
host = this.getXapiObject(host)
|
||||
const xapi = this.getXapi(host)
|
||||
const hostId = host._xapiId
|
||||
|
||||
await Promise.all([
|
||||
nameDescription !== undefined && host.set_name_description(nameDescription),
|
||||
nameLabel !== undefined && host.set_name_label(nameLabel),
|
||||
multipathing !== undefined &&
|
||||
host.$xapi.setHostMultipathing(host.$id, multipathing),
|
||||
])
|
||||
if (multipathing !== undefined) {
|
||||
await xapi.setHostMultipathing(hostId, multipathing)
|
||||
}
|
||||
|
||||
return xapi.setHostProperties(hostId, {
|
||||
nameLabel,
|
||||
nameDescription,
|
||||
})
|
||||
}
|
||||
|
||||
set.description = 'changes the properties of an host'
|
||||
@@ -211,25 +215,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 +269,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'],
|
||||
}
|
||||
|
||||
@@ -85,26 +85,18 @@ createBonded.description =
|
||||
// ===================================================================
|
||||
|
||||
export async function set({
|
||||
network,
|
||||
|
||||
automatic,
|
||||
defaultIsLocked,
|
||||
name_description: nameDescription,
|
||||
name_label: nameLabel,
|
||||
network,
|
||||
}) {
|
||||
network = this.getXapiObject(network)
|
||||
|
||||
await Promise.all([
|
||||
automatic !== undefined &&
|
||||
network.update_other_config('automatic', automatic ? 'true' : null),
|
||||
defaultIsLocked !== undefined &&
|
||||
network.set_default_locking_mode(
|
||||
defaultIsLocked ? 'disabled' : 'unlocked'
|
||||
),
|
||||
nameDescription !== undefined &&
|
||||
network.set_name_description(nameDescription),
|
||||
nameLabel !== undefined && network.set_name_label(nameLabel),
|
||||
])
|
||||
await this.getXapi(network).setNetworkProperties(network._xapiId, {
|
||||
automatic,
|
||||
defaultIsLocked,
|
||||
nameDescription,
|
||||
nameLabel,
|
||||
})
|
||||
}
|
||||
|
||||
set.params = {
|
||||
|
||||
@@ -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 }
|
||||
|
||||
|
||||
@@ -5,15 +5,14 @@ import { format, JsonRPcError } from 'json-rpc-peer'
|
||||
export async function set({
|
||||
pool,
|
||||
|
||||
// TODO: use camel case.
|
||||
name_description: nameDescription,
|
||||
name_label: nameLabel,
|
||||
}) {
|
||||
pool = this.getXapiObject(pool)
|
||||
|
||||
await Promise.all([
|
||||
nameDescription !== undefined && pool.set_name_description(nameDescription),
|
||||
nameLabel !== undefined && pool.set_name_label(nameLabel),
|
||||
])
|
||||
await this.getXapi(pool).setPoolProperties({
|
||||
nameDescription,
|
||||
nameLabel,
|
||||
})
|
||||
}
|
||||
|
||||
set.params = {
|
||||
@@ -168,7 +167,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`
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
|
||||
@@ -10,15 +10,14 @@ import { forEach, parseXml } from '../utils'
|
||||
export async function set({
|
||||
sr,
|
||||
|
||||
// TODO: use camel case.
|
||||
name_description: nameDescription,
|
||||
name_label: nameLabel,
|
||||
}) {
|
||||
sr = this.getXapiObject(sr)
|
||||
|
||||
await Promise.all([
|
||||
nameDescription !== undefined && sr.set_name_description(nameDescription),
|
||||
nameLabel !== undefined && sr.set_name_label(nameLabel),
|
||||
])
|
||||
await this.getXapi(sr).setSrProperties(sr._xapiId, {
|
||||
nameDescription,
|
||||
nameLabel,
|
||||
})
|
||||
}
|
||||
|
||||
set.params = {
|
||||
@@ -180,35 +179,6 @@ createIso.resolve = {
|
||||
host: ['host', 'host', 'administrate'],
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export async function createFile({
|
||||
host,
|
||||
nameLabel,
|
||||
nameDescription,
|
||||
location,
|
||||
}) {
|
||||
const xapi = this.getXapi(host)
|
||||
return xapi.createSr({
|
||||
hostRef: host._xapiRef,
|
||||
name_label: nameLabel,
|
||||
name_description: nameDescription,
|
||||
type: 'file',
|
||||
device_config: { location },
|
||||
})
|
||||
}
|
||||
|
||||
createFile.params = {
|
||||
host: { type: 'string' },
|
||||
nameLabel: { type: 'string' },
|
||||
nameDescription: { type: 'string' },
|
||||
location: { type: 'string' },
|
||||
}
|
||||
|
||||
createFile.resolve = {
|
||||
host: ['host', 'host', 'administrate'],
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// NFS SR
|
||||
|
||||
@@ -391,58 +361,6 @@ createExt.resolve = {
|
||||
host: ['host', 'host', 'administrate'],
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// This function helps to detect all ZFS pools
|
||||
// Return a dict of pools with their parameters { <poolname>: {<paramdict>}}
|
||||
// example output (the parameter mountpoint is of interest):
|
||||
// {"tank":
|
||||
// {
|
||||
// "setuid": "on", "relatime": "off", "referenced": "24K", "written": "24K", "zoned": "off", "primarycache": "all",
|
||||
// "logbias": "latency", "creation": "Mon May 27 17:24 2019", "sync": "standard", "snapdev": "hidden",
|
||||
// "dedup": "off", "sharenfs": "off", "usedbyrefreservation": "0B", "sharesmb": "off", "createtxg": "1",
|
||||
// "canmount": "on", "mountpoint": "/tank", "casesensitivity": "sensitive", "utf8only": "off", "xattr": "on",
|
||||
// "dnodesize": "legacy", "mlslabel": "none", "objsetid": "54", "defcontext": "none", "rootcontext": "none",
|
||||
// "mounted": "yes", "compression": "off", "overlay": "off", "logicalused": "47K", "usedbysnapshots": "0B",
|
||||
// "filesystem_count": "none", "copies": "1", "snapshot_limit": "none", "aclinherit": "restricted",
|
||||
// "compressratio": "1.00x", "readonly": "off", "version": "5", "normalization": "none", "filesystem_limit": "none",
|
||||
// "type": "filesystem", "secondarycache": "all", "refreservation": "none", "available": "17.4G", "used": "129K",
|
||||
// "exec": "on", "refquota": "none", "refcompressratio": "1.00x", "quota": "none", "keylocation": "none",
|
||||
// "snapshot_count": "none", "fscontext": "none", "vscan": "off", "reservation": "none", "atime": "on",
|
||||
// "recordsize": "128K", "usedbychildren": "105K", "usedbydataset": "24K", "guid": "656061077639704004",
|
||||
// "pbkdf2iters": "0", "checksum": "on", "special_small_blocks": "0", "redundant_metadata": "all",
|
||||
// "volmode": "default", "devices": "on", "keyformat": "none", "logicalreferenced": "12K", "acltype": "off",
|
||||
// "nbmand": "off", "context": "none", "encryption": "off", "snapdir": "hidden"}}
|
||||
export async function probeZfs({ host }) {
|
||||
const xapi = this.getXapi(host)
|
||||
try {
|
||||
const result = await xapi.call(
|
||||
'host.call_plugin',
|
||||
host._xapiRef,
|
||||
'zfs.py',
|
||||
'list_zfs_pools',
|
||||
{}
|
||||
)
|
||||
return JSON.parse(result)
|
||||
} catch (error) {
|
||||
if (
|
||||
error.code === 'XENAPI_MISSING_PLUGIN' ||
|
||||
error.code === 'UNKNOWN_XENAPI_PLUGIN_FUNCTION'
|
||||
) {
|
||||
return {}
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
probeZfs.params = {
|
||||
host: { type: 'string' },
|
||||
}
|
||||
|
||||
probeZfs.resolve = {
|
||||
host: ['host', 'host', 'administrate'],
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// This function helps to detect all NFS shares (exports) on a NFS server
|
||||
// Return a table of exports with their paths and ACLs
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export async function add({ tag, object }) {
|
||||
await this.getXapiObject(object).add_tags(tag)
|
||||
await this.getXapi(object).addTag(object._xapiId, tag)
|
||||
}
|
||||
|
||||
add.description = 'add a new tag to an object'
|
||||
@@ -16,7 +16,7 @@ add.params = {
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export async function remove({ tag, object }) {
|
||||
await this.getXapiObject(object).remove_tags(tag)
|
||||
await this.getXapi(object).removeTag(object._xapiId, tag)
|
||||
}
|
||||
|
||||
remove.description = 'remove an existing tag from an object'
|
||||
|
||||
@@ -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" },
|
||||
}
|
||||
|
||||
@@ -34,25 +34,3 @@ delete_.permission = 'admin'
|
||||
delete_.params = {
|
||||
token: { type: 'string' },
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export async function deleteAll({ except }) {
|
||||
await this.deleteAuthenticationTokens({
|
||||
filter: {
|
||||
user_id: this.session.get('user_id'),
|
||||
id: {
|
||||
__not: except,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
deleteAll.description =
|
||||
'delete all tokens of the current user except the current one'
|
||||
|
||||
deleteAll.permission = ''
|
||||
|
||||
deleteAll.params = {
|
||||
except: { type: 'string', optional: true },
|
||||
}
|
||||
|
||||
@@ -320,11 +320,6 @@ create.params = {
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
hvmBootFirmware: { type: 'string', optional: true },
|
||||
|
||||
// other params are passed to `editVm`
|
||||
'*': { type: 'any' },
|
||||
}
|
||||
|
||||
create.resolve = {
|
||||
@@ -565,8 +560,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 },
|
||||
@@ -610,7 +603,7 @@ set.params = {
|
||||
// Switch from Cirrus video adaptor to VGA adaptor
|
||||
vga: { type: 'string', optional: true },
|
||||
|
||||
videoram: { type: 'number', optional: true },
|
||||
videoram: { type: ['string', 'number'], optional: true },
|
||||
|
||||
coresPerSocket: { type: ['string', 'number', 'null'], optional: true },
|
||||
|
||||
@@ -628,11 +621,6 @@ set.params = {
|
||||
|
||||
// set the VM network interface controller
|
||||
nicType: { type: ['string', 'null'], optional: true },
|
||||
|
||||
// set the VM boot firmware mode
|
||||
hvmBootFirmware: { type: ['string', 'null'], optional: true },
|
||||
|
||||
virtualizationMode: { type: 'string', optional: true },
|
||||
}
|
||||
|
||||
set.resolve = {
|
||||
@@ -1136,10 +1124,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)
|
||||
}
|
||||
|
||||
@@ -1149,7 +1134,7 @@ revert.params = {
|
||||
}
|
||||
|
||||
revert.resolve = {
|
||||
snapshot: ['snapshot', 'VM-snapshot', 'view'],
|
||||
snapshot: ['snapshot', 'VM-snapshot', 'administrate'],
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
@@ -1375,7 +1360,9 @@ createInterface.resolve = {
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export async function attachPci({ vm, pciId }) {
|
||||
await this.getXapiObject(vm).update_other_config('pci', pciId)
|
||||
const xapi = this.getXapi(vm)
|
||||
|
||||
await xapi.call('VM.add_to_other_config', vm._xapiRef, 'pci', pciId)
|
||||
}
|
||||
|
||||
attachPci.params = {
|
||||
@@ -1390,7 +1377,9 @@ attachPci.resolve = {
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export async function detachPci({ vm }) {
|
||||
await this.getXapiObject(vm).update_other_config('pci', null)
|
||||
const xapi = this.getXapi(vm)
|
||||
|
||||
await xapi.call('VM.remove_from_other_config', vm._xapiRef, 'pci')
|
||||
}
|
||||
|
||||
detachPci.params = {
|
||||
@@ -1427,7 +1416,7 @@ export async function setBootOrder({ vm, order }) {
|
||||
throw invalidParameters('You can only set the boot order on a HVM guest')
|
||||
}
|
||||
|
||||
await this.getXapiObject(vm).update_HVM_boot_params('order', order)
|
||||
await this.getXapiObject(vm).set_HVM_boot_params({ order })
|
||||
}
|
||||
|
||||
setBootOrder.params = {
|
||||
|
||||
@@ -55,7 +55,6 @@ getAllObjects.description = 'Returns all XO objects'
|
||||
getAllObjects.params = {
|
||||
filter: { type: 'object', optional: true },
|
||||
limit: { type: 'number', optional: true },
|
||||
ndjson: { type: 'boolean', optional: true },
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
@@ -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}`
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -885,10 +887,10 @@ async function createVDIOnLVMWithoutSizeLimit(xapi, lvmSr, diskSize) {
|
||||
await xapi.callAsync('SR.scan', xapi.getObject(lvmSr).$ref)
|
||||
const vdi = find(xapi.getObject(lvmSr).$VDIs, vdi => vdi.uuid === uuid)
|
||||
if (vdi != null) {
|
||||
await Promise.all([
|
||||
vdi.set_name_description('Created by XO'),
|
||||
vdi.set_name_label('xosan_data'),
|
||||
])
|
||||
await xapi.setSrProperties(vdi.$ref, {
|
||||
nameLabel: 'xosan_data',
|
||||
nameDescription: 'Created by XO',
|
||||
})
|
||||
return vdi
|
||||
}
|
||||
}
|
||||
@@ -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, {
|
||||
@@ -1120,9 +1124,11 @@ async function _prepareGlusterVm(
|
||||
}
|
||||
}
|
||||
}
|
||||
await newVM.add_tags('XOSAN')
|
||||
await xapi.addTag(newVM.$id, 'XOSAN')
|
||||
await xapi.editVm(newVM, {
|
||||
name_label: `XOSAN - ${lvmSr.name_label} - ${host.name_label} ${labelSuffix}`,
|
||||
name_label: `XOSAN - ${lvmSr.name_label} - ${
|
||||
host.name_label
|
||||
} ${labelSuffix}`,
|
||||
name_description: 'Xosan VM storage',
|
||||
memory: memorySize,
|
||||
})
|
||||
|
||||
@@ -13,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, {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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`
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
@@ -528,7 +519,6 @@ const TRANSFORMS = {
|
||||
|
||||
name_description: obj.name_description,
|
||||
name_label: obj.name_label,
|
||||
parent: obj.sm_config['vhd-parent'],
|
||||
size: +obj.virtual_size,
|
||||
snapshots: link(obj, 'snapshots'),
|
||||
tags: obj.tags,
|
||||
|
||||
@@ -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+)$/,
|
||||
|
||||
@@ -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'
|
||||
@@ -246,6 +247,69 @@ export default class Xapi extends XapiBase {
|
||||
)::ignoreErrors()
|
||||
}
|
||||
|
||||
async setHostProperties(id, { nameLabel, nameDescription }) {
|
||||
const host = this.getObject(id)
|
||||
await Promise.all([
|
||||
nameDescription !== undefined &&
|
||||
host.set_name_description(nameDescription),
|
||||
nameLabel !== undefined && host.set_name_label(nameLabel),
|
||||
])
|
||||
}
|
||||
|
||||
async setPoolProperties({ autoPoweron, nameLabel, nameDescription }) {
|
||||
const { pool } = this
|
||||
|
||||
await Promise.all([
|
||||
nameDescription !== undefined &&
|
||||
pool.set_name_description(nameDescription),
|
||||
nameLabel !== undefined && pool.set_name_label(nameLabel),
|
||||
autoPoweron != null &&
|
||||
pool.update_other_config('autoPoweron', autoPoweron ? 'true' : null),
|
||||
])
|
||||
}
|
||||
|
||||
async setSrProperties(id, { nameLabel, nameDescription }) {
|
||||
const sr = this.getObject(id)
|
||||
await Promise.all([
|
||||
nameDescription !== undefined && sr.set_name_description(nameDescription),
|
||||
nameLabel !== undefined && sr.set_name_label(nameLabel),
|
||||
])
|
||||
}
|
||||
|
||||
async setNetworkProperties(
|
||||
id,
|
||||
{ automatic, defaultIsLocked, nameDescription, nameLabel }
|
||||
) {
|
||||
let defaultLockingMode
|
||||
if (defaultIsLocked != null) {
|
||||
defaultLockingMode = defaultIsLocked ? 'disabled' : 'unlocked'
|
||||
}
|
||||
const network = this.getObject(id)
|
||||
await Promise.all([
|
||||
defaultLockingMode !== undefined &&
|
||||
network.set_default_locking_mode(defaultLockingMode),
|
||||
nameDescription !== undefined &&
|
||||
network.set_name_description(nameDescription),
|
||||
nameLabel !== undefined && network.set_name_label(nameLabel),
|
||||
automatic !== undefined &&
|
||||
network.update_other_config('automatic', automatic ? 'true' : null),
|
||||
])
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
|
||||
async addTag(id, tag) {
|
||||
const { $ref: ref, $type: type } = this.getObject(id)
|
||||
|
||||
await this.call(`${type}.add_tags`, ref, tag)
|
||||
}
|
||||
|
||||
async removeTag(id, tag) {
|
||||
const { $ref: ref, $type: type } = this.getObject(id)
|
||||
|
||||
await this.call(`${type}.remove_tags`, ref, tag)
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
|
||||
setDefaultSr(srId) {
|
||||
@@ -829,7 +893,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.
|
||||
//
|
||||
@@ -955,21 +1019,17 @@ export default class Xapi extends XapiBase {
|
||||
await this._createVmRecord({
|
||||
...delta.vm,
|
||||
affinity: null,
|
||||
blocked_operations: {
|
||||
...delta.vm.blocked_operations,
|
||||
start: 'Importing…',
|
||||
},
|
||||
ha_always_run: false,
|
||||
is_a_template: false,
|
||||
name_label: `[Importing…] ${name_label}`,
|
||||
other_config: {
|
||||
...delta.vm.other_config,
|
||||
[TAG_COPY_SRC]: delta.vm.uuid,
|
||||
},
|
||||
})
|
||||
)
|
||||
$defer.onFailure(() => this._deleteVm(vm))
|
||||
|
||||
await Promise.all([
|
||||
vm.set_name_label(`[Importing…] ${name_label}`),
|
||||
vm.update_blocked_operations('start', 'Importing…'),
|
||||
vm.update_other_config(TAG_COPY_SRC, delta.vm.uuid),
|
||||
])
|
||||
|
||||
// 2. Delete all VBDs which may have been created by the import.
|
||||
await asyncMap(vm.$VBDs, vbd => this._deleteVbd(vbd))::ignoreErrors()
|
||||
|
||||
@@ -1086,7 +1146,6 @@ export default class Xapi extends XapiBase {
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
delta.vm.ha_always_run && vm.set_ha_always_run(true),
|
||||
vm.set_name_label(name_label),
|
||||
// FIXME: move
|
||||
vm.update_blocked_operations(
|
||||
@@ -1317,7 +1376,11 @@ export default class Xapi extends XapiBase {
|
||||
$defer.onFailure(() => this._deleteVm(vm))
|
||||
// Disable start and change the VM name label during import.
|
||||
await Promise.all([
|
||||
vm.update_blocked_operations('start', 'OVA import in progress...'),
|
||||
this.addForbiddenOperationToVm(
|
||||
vm.$id,
|
||||
'start',
|
||||
'OVA import in progress...'
|
||||
),
|
||||
vm.set_name_label(`[Importing...] ${nameLabel}`),
|
||||
])
|
||||
|
||||
@@ -1378,7 +1441,7 @@ export default class Xapi extends XapiBase {
|
||||
|
||||
// Enable start and restore the VM name label after import.
|
||||
await Promise.all([
|
||||
vm.update_blocked_operations('start', null),
|
||||
this.removeForbiddenOperationFromVm(vm.$id, 'start'),
|
||||
vm.set_name_label(nameLabel),
|
||||
])
|
||||
return vm
|
||||
@@ -1511,6 +1574,13 @@ export default class Xapi extends XapiBase {
|
||||
return /* await */ this._snapshotVm(this.getObject(vmId), nameLabel)
|
||||
}
|
||||
|
||||
async setVcpuWeight(vmId, weight) {
|
||||
await this.getObject(vmId).update_VCPUs_params(
|
||||
'weight',
|
||||
weight || null // Take all falsy values as a removal (0 included)
|
||||
)
|
||||
}
|
||||
|
||||
async _startVm(vm, host, force) {
|
||||
log.debug(`Starting VM ${vm.name_label}`)
|
||||
|
||||
@@ -1608,6 +1678,24 @@ export default class Xapi extends XapiBase {
|
||||
}
|
||||
}
|
||||
|
||||
// vm_operations: http://xapi-project.github.io/xen-api/classes/vm.html
|
||||
async addForbiddenOperationToVm(vmId, operation, reason) {
|
||||
await this.call(
|
||||
'VM.add_to_blocked_operations',
|
||||
this.getObject(vmId).$ref,
|
||||
operation,
|
||||
`[XO] ${reason}`
|
||||
)
|
||||
}
|
||||
|
||||
async removeForbiddenOperationFromVm(vmId, operation) {
|
||||
await this.call(
|
||||
'VM.remove_from_blocked_operations',
|
||||
this.getObject(vmId).$ref,
|
||||
operation
|
||||
)
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
|
||||
async createVbd({
|
||||
@@ -1723,7 +1811,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 +2223,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 +2423,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 +2435,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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,6 +74,7 @@ declare export class Xapi {
|
||||
): Promise<void>;
|
||||
_snapshotVm(cancelToken: mixed, vm: Vm, nameLabel?: string): Promise<Vm>;
|
||||
|
||||
addTag(object: Id, tag: string): Promise<void>;
|
||||
barrier(): Promise<void>;
|
||||
barrier(ref: string): Promise<XapiObject>;
|
||||
deleteVm(vm: Id): Promise<void>;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -84,32 +84,4 @@ export default {
|
||||
})
|
||||
return unhealthyVdis
|
||||
},
|
||||
|
||||
async createSr({
|
||||
hostRef,
|
||||
|
||||
content_type = 'user', // recommended by Citrix
|
||||
device_config = {},
|
||||
name_description = '',
|
||||
name_label,
|
||||
shared = false,
|
||||
physical_size = 0,
|
||||
sm_config = {},
|
||||
type,
|
||||
}) {
|
||||
const srRef = await this.call(
|
||||
'SR.create',
|
||||
hostRef,
|
||||
device_config,
|
||||
physical_size,
|
||||
name_label,
|
||||
name_description,
|
||||
type,
|
||||
content_type,
|
||||
shared,
|
||||
sm_config
|
||||
)
|
||||
|
||||
return (await this.barrier(srRef)).uuid
|
||||
},
|
||||
}
|
||||
|
||||
@@ -107,12 +107,15 @@ export default {
|
||||
|
||||
if (isHvm) {
|
||||
if (!isEmpty(vdis) || installMethod === 'network') {
|
||||
const { order } = vm.HVM_boot_params
|
||||
const { HVM_boot_params: bootParams } = vm
|
||||
let order = bootParams.order
|
||||
if (order) {
|
||||
order = 'n' + order.replace('n', '')
|
||||
} else {
|
||||
order = 'ncd'
|
||||
}
|
||||
|
||||
vm.update_HVM_boot_params(
|
||||
'order',
|
||||
order ? 'n' + order.replace('n', '') : 'ncd'
|
||||
)
|
||||
vm.set_HVM_boot_params({ ...bootParams, order })
|
||||
}
|
||||
} else {
|
||||
// PV
|
||||
@@ -265,8 +268,11 @@ export default {
|
||||
autoPoweron: {
|
||||
set(value, vm) {
|
||||
return Promise.all([
|
||||
vm.update_other_config('auto_poweron', value ? 'true' : null),
|
||||
value && vm.$pool.update_other_config('auto_poweron', 'true'),
|
||||
vm.update_other_config('autoPoweron', value ? 'true' : null),
|
||||
value &&
|
||||
this.setPoolProperties({
|
||||
autoPoweron: true,
|
||||
}),
|
||||
])
|
||||
},
|
||||
},
|
||||
@@ -288,7 +294,7 @@ export default {
|
||||
|
||||
coresPerSocket: {
|
||||
set: (coresPerSocket, vm) =>
|
||||
vm.update_platform('cores-per-socket', String(coresPerSocket)),
|
||||
vm.update_platform('cores-per-socket', coresPerSocket),
|
||||
},
|
||||
|
||||
CPUs: 'cpus',
|
||||
@@ -312,7 +318,7 @@ export default {
|
||||
|
||||
cpuCap: {
|
||||
get: vm => vm.VCPUs_params.cap && +vm.VCPUs_params.cap,
|
||||
set: (cap, vm) => vm.update_VCPUs_params('cap', String(cap)),
|
||||
set: (cap, vm) => vm.update_VCPUs_params('cap', cap),
|
||||
},
|
||||
|
||||
cpuMask: {
|
||||
@@ -335,11 +341,7 @@ export default {
|
||||
|
||||
cpuWeight: {
|
||||
get: vm => vm.VCPUs_params.weight && +vm.VCPUs_params.weight,
|
||||
set: (weight, vm) =>
|
||||
vm.update_VCPUs_params(
|
||||
'weight',
|
||||
weight === null ? null : String(weight)
|
||||
),
|
||||
set: (weight, vm) => vm.update_VCPUs_params('weight', weight),
|
||||
},
|
||||
|
||||
highAvailability: {
|
||||
@@ -441,7 +443,7 @@ export default {
|
||||
`The different values that the video RAM can take are: ${XEN_VIDEORAM_VALUES}`
|
||||
)
|
||||
}
|
||||
return vm.update_platform('videoram', String(videoram))
|
||||
return vm.update_platform('videoram', videoram)
|
||||
},
|
||||
},
|
||||
|
||||
@@ -449,10 +451,6 @@ export default {
|
||||
get: vm => +vm.start_delay,
|
||||
set: (startDelay, vm) => vm.set_start_delay(startDelay),
|
||||
},
|
||||
|
||||
hvmBootFirmware: {
|
||||
set: (firmware, vm) => vm.update_HVM_boot_params('firmware', firmware),
|
||||
},
|
||||
}),
|
||||
|
||||
async editVm(id, props, checkLimits) {
|
||||
|
||||
@@ -60,9 +60,8 @@ function checkParams(method, params) {
|
||||
|
||||
const result = schemaInspector.validate(
|
||||
{
|
||||
properties: schema,
|
||||
strict: true,
|
||||
type: 'object',
|
||||
properties: schema,
|
||||
},
|
||||
params
|
||||
)
|
||||
@@ -262,15 +261,11 @@ export default class Api {
|
||||
//
|
||||
// The goal here is to standardize the calls by always providing
|
||||
// an id parameter when possible to simplify calls to the API.
|
||||
if (params?.id === undefined) {
|
||||
if (params != null && params.id === undefined) {
|
||||
const namespace = name.slice(0, name.indexOf('.'))
|
||||
const spec = method.params
|
||||
if (spec !== undefined && 'id' in spec && !(namespace in spec)) {
|
||||
const id = params[namespace]
|
||||
if (typeof id === 'string') {
|
||||
delete params[namespace]
|
||||
params.id = id
|
||||
}
|
||||
const id = params[namespace]
|
||||
if (typeof id === 'string') {
|
||||
params.id = id
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import createLogger from '@xen-orchestra/log'
|
||||
import { createPredicate } from 'value-matcher'
|
||||
import { ignoreErrors } from 'promise-toolbox'
|
||||
import { invalidCredentials, noSuchObject } from 'xo-common/api-errors'
|
||||
|
||||
@@ -194,14 +193,6 @@ export default class {
|
||||
}
|
||||
}
|
||||
|
||||
async deleteAuthenticationTokens({ filter }) {
|
||||
return Promise.all(
|
||||
(await this._tokens.get())
|
||||
.filter(createPredicate(filter))
|
||||
.map(({ id }) => this.deleteAuthenticationToken(id))
|
||||
)
|
||||
}
|
||||
|
||||
async getAuthenticationToken(id) {
|
||||
let token = await this._tokens.first(id)
|
||||
if (token === undefined) {
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -19,7 +19,6 @@ import {
|
||||
isEmpty,
|
||||
last,
|
||||
mapValues,
|
||||
merge,
|
||||
noop,
|
||||
some,
|
||||
sum,
|
||||
@@ -30,7 +29,6 @@ import {
|
||||
ignoreErrors,
|
||||
pFinally,
|
||||
pFromEvent,
|
||||
timeout,
|
||||
} from 'promise-toolbox'
|
||||
import Vhd, {
|
||||
chainVhd,
|
||||
@@ -43,7 +41,6 @@ import { type CallJob, type Executor, type Job } from '../jobs'
|
||||
import { type Schedule } from '../scheduling'
|
||||
|
||||
import createSizeStream from '../../size-stream'
|
||||
import parseDuration from '../../_parseDuration'
|
||||
import {
|
||||
type DeltaVmExport,
|
||||
type DeltaVmImport,
|
||||
@@ -69,7 +66,6 @@ export type Mode = 'full' | 'delta'
|
||||
export type ReportWhen = 'always' | 'failure' | 'never'
|
||||
|
||||
type Settings = {|
|
||||
bypassVdiChainsCheck?: boolean,
|
||||
concurrency?: number,
|
||||
deleteFirst?: boolean,
|
||||
copyRetention?: number,
|
||||
@@ -141,7 +137,6 @@ const getOldEntries = <T>(retention: number, entries?: T[]): T[] =>
|
||||
: entries
|
||||
|
||||
const defaultSettings: Settings = {
|
||||
bypassVdiChainsCheck: false,
|
||||
concurrency: 0,
|
||||
deleteFirst: false,
|
||||
exportRetention: 0,
|
||||
@@ -291,7 +286,7 @@ const importers: $Dict<
|
||||
xapi.importVm(xva, { srId: sr.$id })
|
||||
)
|
||||
await Promise.all([
|
||||
vm.add_tags('restored from backup'),
|
||||
xapi.addTag(vm.$id, 'restored from backup'),
|
||||
xapi.editVm(vm.$id, {
|
||||
name_label: `${metadata.vm.name_label} (${safeDateFormat(
|
||||
metadata.timestamp
|
||||
@@ -455,7 +450,7 @@ const disableVmHighAvailability = async (xapi: Xapi, vm: Vm) => {
|
||||
|
||||
return Promise.all([
|
||||
vm.set_ha_restart_priority(''),
|
||||
vm.add_tags('HA disabled'),
|
||||
xapi.addTag(vm.$ref, 'HA disabled'),
|
||||
])
|
||||
}
|
||||
|
||||
@@ -512,17 +507,9 @@ const disableVmHighAvailability = async (xapi: Xapi, vm: Vm) => {
|
||||
// │ │ ├─ task.start(message: 'transfer')
|
||||
// │ │ │ ├─ task.warning(message: string)
|
||||
// │ │ │ └─ task.end(result: { size: number })
|
||||
// │ │ │
|
||||
// │ │ │ // in case of full backup, DR and CR
|
||||
// │ │ ├─ task.start(message: 'clean')
|
||||
// │ │ │ ├─ task.warning(message: string)
|
||||
// │ │ │ └─ task.end
|
||||
// │ │ │
|
||||
// │ │ │ // in case of delta backup
|
||||
// │ │ ├─ task.start(message: 'merge')
|
||||
// │ │ │ ├─ task.warning(message: string)
|
||||
// │ │ │ └─ task.end(result: { size: number })
|
||||
// │ │ │
|
||||
// │ │ └─ task.end
|
||||
// │ └─ task.end
|
||||
// └─ job.end
|
||||
@@ -549,18 +536,17 @@ export default class BackupNg {
|
||||
return this._runningRestores
|
||||
}
|
||||
|
||||
constructor(app: any, { backup }) {
|
||||
constructor(app: any) {
|
||||
this._app = app
|
||||
this._logger = undefined
|
||||
this._runningRestores = new Set()
|
||||
this._backupOptions = backup
|
||||
|
||||
app.on('start', async () => {
|
||||
this._logger = await app.getLogger('restore')
|
||||
|
||||
const executor: Executor = async ({
|
||||
cancelToken,
|
||||
data,
|
||||
data: vmsId,
|
||||
job: job_,
|
||||
logger,
|
||||
runJobId,
|
||||
@@ -570,8 +556,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 +609,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 +642,6 @@ export default class BackupNg {
|
||||
schedule,
|
||||
logger,
|
||||
taskId,
|
||||
settings,
|
||||
srs,
|
||||
remotes
|
||||
)
|
||||
@@ -668,7 +649,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 +678,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 +919,6 @@ export default class BackupNg {
|
||||
schedule: Schedule,
|
||||
logger: any,
|
||||
taskId: string,
|
||||
settings: Settings,
|
||||
srs: any[],
|
||||
remotes: any[]
|
||||
): Promise<void> {
|
||||
@@ -964,7 +946,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 +1007,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,
|
||||
@@ -1216,20 +1191,11 @@ export default class BackupNg {
|
||||
)
|
||||
): any)
|
||||
|
||||
const deleteOldBackups = () =>
|
||||
wrapTask(
|
||||
{
|
||||
logger,
|
||||
message: 'clean',
|
||||
parentId: taskId,
|
||||
},
|
||||
this._deleteFullVmBackups(handler, oldBackups)
|
||||
)
|
||||
const deleteFirst = getSetting(settings, 'deleteFirst', [
|
||||
remoteId,
|
||||
])
|
||||
if (deleteFirst) {
|
||||
await deleteOldBackups()
|
||||
await this._deleteFullVmBackups(handler, oldBackups)
|
||||
}
|
||||
|
||||
await wrapTask(
|
||||
@@ -1245,7 +1211,7 @@ export default class BackupNg {
|
||||
await handler.outputFile(metadataFilename, jsonMetadata)
|
||||
|
||||
if (!deleteFirst) {
|
||||
await deleteOldBackups()
|
||||
await this._deleteFullVmBackups(handler, oldBackups)
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -1276,18 +1242,9 @@ export default class BackupNg {
|
||||
listReplicatedVms(xapi, scheduleId, srId, vmUuid)
|
||||
)
|
||||
|
||||
const deleteOldBackups = () =>
|
||||
wrapTask(
|
||||
{
|
||||
logger,
|
||||
message: 'clean',
|
||||
parentId: taskId,
|
||||
},
|
||||
this._deleteVms(xapi, oldVms)
|
||||
)
|
||||
const deleteFirst = getSetting(settings, 'deleteFirst', [srId])
|
||||
if (deleteFirst) {
|
||||
await deleteOldBackups()
|
||||
await this._deleteVms(xapi, oldVms)
|
||||
}
|
||||
|
||||
const vm = await xapi.barrier(
|
||||
@@ -1309,7 +1266,7 @@ export default class BackupNg {
|
||||
)
|
||||
|
||||
await Promise.all([
|
||||
vm.add_tags('Disaster Recovery'),
|
||||
xapi.addTag(vm.$ref, 'Disaster Recovery'),
|
||||
disableVmHighAvailability(xapi, vm),
|
||||
vm.update_blocked_operations(
|
||||
'start',
|
||||
@@ -1319,7 +1276,7 @@ export default class BackupNg {
|
||||
])
|
||||
|
||||
if (!deleteFirst) {
|
||||
await deleteOldBackups()
|
||||
await this._deleteVms(xapi, oldVms)
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -1645,19 +1602,9 @@ export default class BackupNg {
|
||||
listReplicatedVms(xapi, scheduleId, srId, vmUuid)
|
||||
)
|
||||
|
||||
const deleteOldBackups = () =>
|
||||
wrapTask(
|
||||
{
|
||||
logger,
|
||||
message: 'clean',
|
||||
parentId: taskId,
|
||||
},
|
||||
this._deleteVms(xapi, oldVms)
|
||||
)
|
||||
|
||||
const deleteFirst = getSetting(settings, 'deleteFirst', [srId])
|
||||
if (deleteFirst) {
|
||||
await deleteOldBackups()
|
||||
await this._deleteVms(xapi, oldVms)
|
||||
}
|
||||
|
||||
const { vm } = await wrapTask(
|
||||
@@ -1677,7 +1624,7 @@ export default class BackupNg {
|
||||
)
|
||||
|
||||
await Promise.all([
|
||||
vm.add_tags('Continuous Replication'),
|
||||
xapi.addTag(vm.$ref, 'Continuous Replication'),
|
||||
disableVmHighAvailability(xapi, vm),
|
||||
vm.update_blocked_operations(
|
||||
'start',
|
||||
@@ -1687,7 +1634,7 @@ export default class BackupNg {
|
||||
])
|
||||
|
||||
if (!deleteFirst) {
|
||||
await deleteOldBackups()
|
||||
await this._deleteVms(xapi, oldVms)
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -1814,16 +1761,6 @@ export default class BackupNg {
|
||||
const path = `${dir}/${file}`
|
||||
try {
|
||||
const metadata = JSON.parse(String(await handler.readFile(path)))
|
||||
if (metadata.mode === 'full') {
|
||||
metadata.size = await timeout
|
||||
.call(
|
||||
handler.getSize(resolveRelativeFromFile(path, metadata.xva)),
|
||||
parseDuration(this._backupOptions.vmBackupSizeTimeout)
|
||||
)
|
||||
.catch(err => {
|
||||
log.warn(`_listVmBackups, getSize`, { err })
|
||||
})
|
||||
}
|
||||
if (predicate === undefined || predicate(metadata)) {
|
||||
Object.defineProperty(metadata, '_filename', {
|
||||
value: path,
|
||||
|
||||
@@ -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)
|
||||
@@ -362,7 +372,7 @@ export default class {
|
||||
|
||||
const { datetime } = parseVmBackupPath(file)
|
||||
await Promise.all([
|
||||
vm.add_tags('restored from backup'),
|
||||
xapi.addTag(vm.$id, 'restored from backup'),
|
||||
xapi.editVm(vm.$id, {
|
||||
name_label: `${vm.name_label} (${shortDate(datetime * 1e3)})`,
|
||||
}),
|
||||
@@ -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))
|
||||
@@ -960,13 +972,12 @@ export default class {
|
||||
nameLabel: copyName,
|
||||
})
|
||||
|
||||
await Promise.all([
|
||||
data.vm.add_tags('Disaster Recovery'),
|
||||
data.vm.update_blocked_operations(
|
||||
'start',
|
||||
'Start operation for this vm is blocked, clone it if you want to use it.'
|
||||
),
|
||||
])
|
||||
data.vm.update_blocked_operations(
|
||||
'start',
|
||||
'Start operation for this vm is blocked, clone it if you want to use it.'
|
||||
)
|
||||
|
||||
await targetXapi.addTag(data.vm.$id, 'Disaster Recovery')
|
||||
|
||||
if (!deleteOldBackupsFirst) {
|
||||
await this._removeVms(targetXapi, vmsToRemove)
|
||||
@@ -997,7 +1008,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 => {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -67,7 +67,7 @@ export default class {
|
||||
const handlers = this._handlers
|
||||
let handler = handlers[id]
|
||||
if (handler === undefined) {
|
||||
handler = getHandler(remote, this._remoteOptions)
|
||||
handler = handlers[id] = getHandler(remote, this._remoteOptions)
|
||||
|
||||
try {
|
||||
await handler.sync()
|
||||
@@ -76,8 +76,6 @@ export default class {
|
||||
ignoreErrors.call(this._updateRemote(id, { error: error.message }))
|
||||
throw error
|
||||
}
|
||||
|
||||
handlers[id] = handler
|
||||
}
|
||||
|
||||
return handler
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,9 +38,9 @@
|
||||
"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",
|
||||
"get-stream": "^4.0.0",
|
||||
"index-modules": "^0.3.0",
|
||||
"rimraf": "^2.6.2"
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "xo-web",
|
||||
"version": "5.45.1",
|
||||
"version": "5.41.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",
|
||||
|
||||
@@ -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]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
@@ -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,
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
|
||||
@@ -2612,6 +2612,9 @@ export default {
|
||||
// Original text: 'Read Only'
|
||||
serverReadOnly: undefined,
|
||||
|
||||
// Original text: 'Disconnect server'
|
||||
serverDisconnect: undefined,
|
||||
|
||||
// Original text: 'username'
|
||||
serverPlaceHolderUser: undefined,
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
|
||||
@@ -2636,6 +2636,9 @@ export default {
|
||||
// Original text: "Read Only"
|
||||
serverReadOnly: 'Modo Leitura',
|
||||
|
||||
// Original text: 'Disconnect server'
|
||||
serverDisconnect: undefined,
|
||||
|
||||
// Original text: 'username'
|
||||
serverPlaceHolderUser: undefined,
|
||||
|
||||
|
||||
@@ -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ı',
|
||||
|
||||
|
||||
@@ -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',
|
||||
@@ -575,12 +571,9 @@ const messages = {
|
||||
newSrPasswordPlaceHolder: 'Password',
|
||||
newSrLvmDevicePlaceHolder: 'Device, e.g /dev/sda…',
|
||||
newSrLocalPathPlaceHolder: '/path/to/directory',
|
||||
newSrNfsDefaultVersion: 'Default NFS version',
|
||||
newSrUseNfs4: 'Use NFSv4',
|
||||
newSrNfsOptions: 'Comma delimited NFS options',
|
||||
newSrNfs: 'NFS version',
|
||||
noSharedZfsAvailable: 'No shared ZFS available',
|
||||
reattachNewSrTooltip: 'Reattach SR',
|
||||
srLocation: 'Storage location',
|
||||
|
||||
// ------ New Network -----
|
||||
createNewNetworkNoPermission:
|
||||
@@ -682,16 +675,6 @@ const messages = {
|
||||
cloneVmLabel: 'Clone',
|
||||
fastCloneVmLabel: 'Fast clone',
|
||||
vmConsoleLabel: 'Console',
|
||||
backupLabel: 'Backup',
|
||||
|
||||
// ----- SR general tab -----
|
||||
baseCopyTooltip:
|
||||
'{n, number} base cop{n, plural, one {y} other {ies}} ({usage})',
|
||||
diskTooltip: '{name} ({usage})',
|
||||
snapshotsTooltip:
|
||||
'{n, number} snapshot{n, plural, one {} other {s}} ({usage})',
|
||||
vdiOnVmTooltip: '{name} ({usage}) on {vmName}',
|
||||
vdisTooltip: '{n, number} VDI{n, plural, one {} other {s}} ({usage})',
|
||||
|
||||
// ----- SR advanced tab -----
|
||||
|
||||
@@ -807,9 +790,6 @@ const messages = {
|
||||
memoryHostState:
|
||||
'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',
|
||||
@@ -817,7 +797,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:
|
||||
@@ -873,7 +853,6 @@ const messages = {
|
||||
// ----- Host storage tabs -----
|
||||
addSrDeviceButton: 'Add a storage',
|
||||
srType: 'Type',
|
||||
pbdDetails: 'PBD details',
|
||||
pbdStatus: 'Status',
|
||||
pbdStatusConnected: 'Connected',
|
||||
pbdStatusDisconnected: 'Disconnected',
|
||||
@@ -968,7 +947,6 @@ const messages = {
|
||||
statDisk: 'Disk throughput',
|
||||
statLastTenMinutes: 'Last 10 minutes',
|
||||
statLastTwoHours: 'Last 2 hours',
|
||||
statLastDay: 'Last day',
|
||||
statLastWeek: 'Last week',
|
||||
statLastYear: 'Last year',
|
||||
|
||||
@@ -1047,7 +1025,6 @@ const messages = {
|
||||
vifMacLabel: 'MAC address',
|
||||
vifMtuLabel: 'MTU',
|
||||
vifNetworkLabel: 'Network',
|
||||
vifRateLimitLabel: 'Rate limit (kB/s)',
|
||||
vifStatusLabel: 'Status',
|
||||
vifStatusConnected: 'Connected',
|
||||
vifStatusDisconnected: 'Disconnected',
|
||||
@@ -1153,10 +1130,6 @@ const messages = {
|
||||
addAclsErrorMessage: 'User(s)/group(s) and role are required.',
|
||||
removeAcl: 'Delete',
|
||||
moreAcls: '{nAcls, number} more…',
|
||||
vmBootFirmware: 'Boot firmware',
|
||||
vmDefaultBootFirmwareLabel: 'default (bios)',
|
||||
vmBootFirmwareWarningMessage:
|
||||
"You're about to change your boot firmware. This is still experimental in CH/XCP-ng 8.0. Are you sure you want to continue?",
|
||||
|
||||
// ----- VM placeholders -----
|
||||
|
||||
@@ -1671,6 +1644,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]',
|
||||
@@ -1680,15 +1654,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',
|
||||
@@ -1731,13 +1703,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',
|
||||
@@ -1869,11 +1839,6 @@ const messages = {
|
||||
pwdChangeErrorBody:
|
||||
'The old password provided is incorrect. Your password has not been changed.',
|
||||
changePasswordOk: 'OK',
|
||||
forgetTokens: 'Forget all connection tokens',
|
||||
forgetTokensExplained:
|
||||
'This will prevent other clients from authenticating with existing tokens but will not kill active sessions',
|
||||
forgetTokensSuccess: 'Successfully forgot connection tokens',
|
||||
forgetTokensError: 'Error while forgetting connection tokens',
|
||||
sshKeys: 'SSH keys',
|
||||
newSshKey: 'New SSH key',
|
||||
deleteSshKey: 'Delete',
|
||||
@@ -1902,7 +1867,6 @@ const messages = {
|
||||
// ----- Logs -----
|
||||
logUser: 'User',
|
||||
logMessage: 'Message',
|
||||
logSuggestXcpNg: 'Use XCP-ng to get rid of restrictions',
|
||||
logError: 'Error',
|
||||
logTitle: 'Logs',
|
||||
logDisplayDetails: 'Display details',
|
||||
|
||||
@@ -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'
|
||||
@@ -283,7 +283,7 @@ export const Vdi = decorate([
|
||||
sr: getSr(state, props),
|
||||
})
|
||||
}),
|
||||
({ id, showSize, showSr, sr, vdi }) => {
|
||||
({ id, sr, vdi }) => {
|
||||
if (vdi === undefined) {
|
||||
return unknowItem(id, 'VDI')
|
||||
}
|
||||
@@ -291,12 +291,9 @@ export const Vdi = decorate([
|
||||
return (
|
||||
<span>
|
||||
<Icon icon='disk' /> {vdi.name_label}
|
||||
{sr !== undefined && showSr && (
|
||||
{sr !== undefined && (
|
||||
<span className='text-muted'> - {sr.name_label}</span>
|
||||
)}
|
||||
{showSize && (
|
||||
<span className='text-muted'> ({formatSize(vdi.size)})</span>
|
||||
)}
|
||||
</span>
|
||||
)
|
||||
},
|
||||
@@ -305,13 +302,10 @@ export const Vdi = decorate([
|
||||
Vdi.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
self: PropTypes.bool,
|
||||
showSize: PropTypes.bool,
|
||||
}
|
||||
|
||||
Vdi.defaultProps = {
|
||||
self: false,
|
||||
showSize: false,
|
||||
showSr: false,
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
@@ -438,8 +432,8 @@ const xoItemToRender = {
|
||||
// XO objects.
|
||||
pool: ({ id }) => <Pool id={id} />,
|
||||
|
||||
VDI: ({ id }) => <Vdi id={id} showSr />,
|
||||
'VDI-resourceSet': ({ id }) => <Vdi id={id} self showSr />,
|
||||
VDI: ({ id }) => <Vdi id={id} />,
|
||||
'VDI-resourceSet': ({ id }) => <Vdi id={id} self />,
|
||||
|
||||
// Pool objects.
|
||||
'VM-template': ({ id }) => <VmTemplate id={id} />,
|
||||
@@ -492,7 +486,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>
|
||||
@@ -504,9 +498,6 @@ const xoItemToRender = {
|
||||
{backup.mode}
|
||||
</span>{' '}
|
||||
<span className='tag tag-warning'>{backup.remote.name}</span>{' '}
|
||||
{backup.size !== undefined && (
|
||||
<span className='tag tag-info'>{formatSize(backup.size)}</span>
|
||||
)}{' '}
|
||||
<FormattedDate
|
||||
value={new Date(backup.timestamp)}
|
||||
month='long'
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
import _ from 'intl'
|
||||
import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import { confirm } from 'modal'
|
||||
import { injectState, provideState } from 'reaclette'
|
||||
import { noop } from 'utils'
|
||||
|
||||
// https://docs.citrix.com/en-us/citrix-hypervisor/whats-new/experimental.html
|
||||
// XAPI values should be lowercased
|
||||
const VM_BOOT_FIRMWARES = ['bios', 'uefi']
|
||||
|
||||
const withState = provideState({
|
||||
effects: {
|
||||
handleBootFirmwareChange(
|
||||
__,
|
||||
{
|
||||
target: { value },
|
||||
}
|
||||
) {
|
||||
if (value !== '') {
|
||||
// TODO: Confirm should be removed once the feature is stabilized
|
||||
confirm({
|
||||
title: _('vmBootFirmware'),
|
||||
body: _('vmBootFirmwareWarningMessage'),
|
||||
}).then(() => this.props.onChange(value), noop)
|
||||
} else {
|
||||
this.props.onChange(value)
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const SelectBootFirmware = ({ effects, value }) => (
|
||||
<select
|
||||
className='form-control'
|
||||
onChange={effects.handleBootFirmwareChange}
|
||||
value={value}
|
||||
>
|
||||
<option value=''>{_('vmDefaultBootFirmwareLabel')}</option>
|
||||
{VM_BOOT_FIRMWARES.map(val => (
|
||||
<option key={val} value={val}>
|
||||
{val}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
)
|
||||
|
||||
SelectBootFirmware.propTypes = {
|
||||
onChange: PropTypes.func.isRequired,
|
||||
value: PropTypes.string.isRequired,
|
||||
}
|
||||
|
||||
export default withState(injectState(SelectBootFirmware))
|
||||
@@ -59,7 +59,7 @@ export const constructSmartPattern = (
|
||||
|
||||
const valueToComplexMatcher = pattern => {
|
||||
if (typeof pattern === 'string') {
|
||||
return new CM.RegExpNode(`^${escapeRegExp(pattern)}$`, 'i')
|
||||
return new CM.RegExpNode(`^${escapeRegExp(pattern)}$`)
|
||||
}
|
||||
|
||||
if (Array.isArray(pattern)) {
|
||||
|
||||
@@ -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]
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -53,7 +53,3 @@ export const setXoaConfiguration = createAction(
|
||||
'XOA_CONFIGURATION',
|
||||
configuration => configuration
|
||||
)
|
||||
export const setHomeVmIdsSelection = createAction(
|
||||
'SET_HOME_VM_IDS_SELECTION',
|
||||
homeVmIdsSelection => homeVmIdsSelection
|
||||
)
|
||||
|
||||
@@ -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!
|
||||
|
||||
@@ -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]),
|
||||
|
||||
@@ -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'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ import Component from 'base-component'
|
||||
import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import { map } from 'lodash'
|
||||
import { Vdi } from 'render-xo-item'
|
||||
|
||||
import _ from '../../intl'
|
||||
import SingleLineRow from '../../single-line-row'
|
||||
@@ -86,9 +85,7 @@ export default class ChooseSrForEachVdisModal extends Component {
|
||||
</SingleLineRow>
|
||||
{map(props.vdis, vdi => (
|
||||
<SingleLineRow key={vdi.uuid}>
|
||||
<Col size={6}>
|
||||
<Vdi id={vdi.id} showSize />
|
||||
</Col>
|
||||
<Col size={6}>{vdi.name_label || vdi.name}</Col>
|
||||
<Col size={6}>
|
||||
<SelectSr
|
||||
onChange={sr =>
|
||||
|
||||
@@ -376,7 +376,7 @@ export const dismissNotification = id => {
|
||||
export const subscribeNotifications = createSubscription(async () => {
|
||||
const { user, xoaUpdaterState } = store.getState()
|
||||
if (
|
||||
+process.env.XOA_PLAN === 5 ||
|
||||
process.env.XOA_PLAN === 5 ||
|
||||
xoaUpdaterState === 'disconnected' ||
|
||||
xoaUpdaterState === 'error'
|
||||
) {
|
||||
@@ -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 => {
|
||||
@@ -2529,25 +2508,14 @@ export const editUser = (user, { email, password, permission }) =>
|
||||
subscribeUsers.forceRefresh
|
||||
)
|
||||
|
||||
const _signOutFromEverywhereElse = () =>
|
||||
_call('token.deleteAll', { except: cookies.get('token') })
|
||||
|
||||
export const signOutFromEverywhereElse = () =>
|
||||
_signOutFromEverywhereElse().then(
|
||||
() => success(_('forgetTokens'), _('forgetTokensSuccess')),
|
||||
() => error(_('forgetTokens'), _('forgetTokensError'))
|
||||
)
|
||||
|
||||
export const changePassword = (oldPassword, newPassword) =>
|
||||
_call('user.changePassword', {
|
||||
oldPassword,
|
||||
newPassword,
|
||||
})
|
||||
.then(_signOutFromEverywhereElse)
|
||||
.then(
|
||||
() => success(_('pwdChangeSuccess'), _('pwdChangeSuccessBody')),
|
||||
() => error(_('pwdChangeError'), _('pwdChangeErrorBody'))
|
||||
)
|
||||
}).then(
|
||||
() => success(_('pwdChangeSuccess'), _('pwdChangeSuccessBody')),
|
||||
() => error(_('pwdChangeError'), _('pwdChangeErrorBody'))
|
||||
)
|
||||
|
||||
const _setUserPreferences = preferences =>
|
||||
_call('user.set', {
|
||||
|
||||
@@ -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, '/'))
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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({
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user