Compare commits
82 Commits
xo-web-v5.
...
better-sor
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
af55e0341b | ||
|
|
3a4f1f78ce | ||
|
|
1d765ebf49 | ||
|
|
3d416d75e9 | ||
|
|
f8666ba367 | ||
|
|
9e80f76dd8 | ||
|
|
c76a5eaf67 | ||
|
|
cd378f0168 | ||
|
|
7d51ff0cf5 | ||
|
|
47819ea956 | ||
|
|
c7e3560c98 | ||
|
|
b24400b21d | ||
|
|
6c1d651687 | ||
|
|
e7757b53e7 | ||
|
|
a6d182e92d | ||
|
|
925eca1463 | ||
|
|
8b454f0d39 | ||
|
|
7c4d110353 | ||
|
|
6df55523b6 | ||
|
|
3ec6a24634 | ||
|
|
164b4218c4 | ||
|
|
56df8a6477 | ||
|
|
47a83b312d | ||
|
|
41a28ae088 | ||
|
|
436a8755ae | ||
|
|
960b179d95 | ||
|
|
0f0d0e1076 | ||
|
|
a8bd0d8075 | ||
|
|
986d3af685 | ||
|
|
1833f9ffdf | ||
|
|
30a6877f8a | ||
|
|
aaae2583c7 | ||
|
|
7f24afc2e7 | ||
|
|
0040923e12 | ||
|
|
844efb88d8 | ||
|
|
9efc3dd1fb | ||
|
|
67853bad8e | ||
|
|
faa8e1441a | ||
|
|
5c54611d1b | ||
|
|
dcf55e4385 | ||
|
|
2b0f1b6aab | ||
|
|
ae6cc8eea3 | ||
|
|
5279fa49a7 | ||
|
|
dcd8a62784 | ||
|
|
8c197b0e1a | ||
|
|
aed824b200 | ||
|
|
036b30212e | ||
|
|
3451ab3f50 | ||
|
|
0d0a92c2b1 | ||
|
|
aa19bc7bf5 | ||
|
|
347759b2e7 | ||
|
|
352230446c | ||
|
|
3eff8102e1 | ||
|
|
6693d845d9 | ||
|
|
4d79c462db | ||
|
|
c44ef6a1dc | ||
|
|
f0996fcfa7 | ||
|
|
54bc384d37 | ||
|
|
504fc1efe8 | ||
|
|
f4179b93fb | ||
|
|
564252c198 | ||
|
|
802a7a4463 | ||
|
|
3b3d6ba13c | ||
|
|
7350bf58e2 | ||
|
|
d37e29afc6 | ||
|
|
40de8c9e23 | ||
|
|
c81eac13c8 | ||
|
|
a6e1860f0d | ||
|
|
03eb2d81f0 | ||
|
|
171710b5e8 | ||
|
|
bed76429c2 | ||
|
|
d19f9b5062 | ||
|
|
38081d9822 | ||
|
|
54e278d3f7 | ||
|
|
181ed1b1a5 | ||
|
|
fb2d325ccb | ||
|
|
5f94a52537 | ||
|
|
c69b50c5d2 | ||
|
|
1c72f89178 | ||
|
|
14bd16da14 | ||
|
|
11a57f4618 | ||
|
|
57f35aff90 |
@@ -38,6 +38,8 @@ module.exports = {
|
||||
// disabled because XAPI objects are using camel case
|
||||
camelcase: ['off'],
|
||||
|
||||
'react/jsx-handler-names': 'off',
|
||||
|
||||
'no-console': ['error', { allow: ['warn', 'error'] }],
|
||||
'no-var': 'error',
|
||||
'node/no-extraneous-import': 'error',
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
"@babel/preset-flow": "^7.0.0",
|
||||
"babel-plugin-lodash": "^3.3.2",
|
||||
"cross-env": "^5.1.3",
|
||||
"rimraf": "^2.6.2"
|
||||
"rimraf": "^3.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@xen-orchestra/cron",
|
||||
"version": "1.0.3",
|
||||
"version": "1.0.4",
|
||||
"license": "ISC",
|
||||
"description": "Focused, well maintained, cron parser/scheduler",
|
||||
"keywords": [
|
||||
@@ -47,7 +47,7 @@
|
||||
"@babel/preset-env": "^7.0.0",
|
||||
"@babel/preset-flow": "^7.0.0",
|
||||
"cross-env": "^5.1.3",
|
||||
"rimraf": "^2.6.2"
|
||||
"rimraf": "^3.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
|
||||
|
||||
@@ -5,9 +5,16 @@ import parse from './parse'
|
||||
|
||||
const MAX_DELAY = 2 ** 31 - 1
|
||||
|
||||
function nextDelay(schedule) {
|
||||
const now = schedule._createDate()
|
||||
return next(schedule._schedule, now) - now
|
||||
}
|
||||
|
||||
class Job {
|
||||
constructor(schedule, fn) {
|
||||
const wrapper = () => {
|
||||
this._isRunning = true
|
||||
|
||||
let result
|
||||
try {
|
||||
result = fn()
|
||||
@@ -22,23 +29,34 @@ class Job {
|
||||
}
|
||||
}
|
||||
const scheduleNext = () => {
|
||||
const delay = schedule._nextDelay()
|
||||
this._timeout =
|
||||
delay < MAX_DELAY
|
||||
? setTimeout(wrapper, delay)
|
||||
: setTimeout(scheduleNext, MAX_DELAY)
|
||||
this._isRunning = false
|
||||
|
||||
if (this._isEnabled) {
|
||||
const delay = nextDelay(schedule)
|
||||
this._timeout =
|
||||
delay < MAX_DELAY
|
||||
? setTimeout(wrapper, delay)
|
||||
: setTimeout(scheduleNext, MAX_DELAY)
|
||||
}
|
||||
}
|
||||
|
||||
this._isEnabled = false
|
||||
this._isRunning = false
|
||||
this._scheduleNext = scheduleNext
|
||||
this._timeout = undefined
|
||||
}
|
||||
|
||||
start() {
|
||||
this.stop()
|
||||
this._scheduleNext()
|
||||
|
||||
this._isEnabled = true
|
||||
if (!this._isRunning) {
|
||||
this._scheduleNext()
|
||||
}
|
||||
}
|
||||
|
||||
stop() {
|
||||
this._isEnabled = false
|
||||
clearTimeout(this._timeout)
|
||||
}
|
||||
}
|
||||
@@ -68,11 +86,6 @@ class Schedule {
|
||||
return dates
|
||||
}
|
||||
|
||||
_nextDelay() {
|
||||
const now = this._createDate()
|
||||
return next(this._schedule, now) - now
|
||||
}
|
||||
|
||||
startJob(fn) {
|
||||
const job = this.createJob(fn)
|
||||
job.start()
|
||||
|
||||
62
@xen-orchestra/cron/src/index.spec.js
Normal file
62
@xen-orchestra/cron/src/index.spec.js
Normal file
@@ -0,0 +1,62 @@
|
||||
/* eslint-env jest */
|
||||
|
||||
import { createSchedule } from './'
|
||||
|
||||
describe('issues', () => {
|
||||
test('stop during async execution', async () => {
|
||||
let nCalls = 0
|
||||
let resolve, promise
|
||||
|
||||
const job = createSchedule('* * * * *').createJob(() => {
|
||||
++nCalls
|
||||
|
||||
// eslint-disable-next-line promise/param-names
|
||||
promise = new Promise(r => {
|
||||
resolve = r
|
||||
})
|
||||
return promise
|
||||
})
|
||||
|
||||
job.start()
|
||||
jest.runAllTimers()
|
||||
|
||||
expect(nCalls).toBe(1)
|
||||
|
||||
job.stop()
|
||||
|
||||
resolve()
|
||||
await promise
|
||||
|
||||
jest.runAllTimers()
|
||||
expect(nCalls).toBe(1)
|
||||
})
|
||||
|
||||
test('stop then start during async job execution', async () => {
|
||||
let nCalls = 0
|
||||
let resolve, promise
|
||||
|
||||
const job = createSchedule('* * * * *').createJob(() => {
|
||||
++nCalls
|
||||
|
||||
// eslint-disable-next-line promise/param-names
|
||||
promise = new Promise(r => {
|
||||
resolve = r
|
||||
})
|
||||
return promise
|
||||
})
|
||||
|
||||
job.start()
|
||||
jest.runAllTimers()
|
||||
|
||||
expect(nCalls).toBe(1)
|
||||
|
||||
job.stop()
|
||||
job.start()
|
||||
|
||||
resolve()
|
||||
await promise
|
||||
|
||||
jest.runAllTimers()
|
||||
expect(nCalls).toBe(2)
|
||||
})
|
||||
})
|
||||
@@ -35,7 +35,7 @@
|
||||
"@babel/preset-flow": "^7.0.0",
|
||||
"babel-plugin-lodash": "^3.3.2",
|
||||
"cross-env": "^5.1.3",
|
||||
"rimraf": "^2.6.2"
|
||||
"rimraf": "^3.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
"@babel/preset-env": "^7.0.0",
|
||||
"babel-plugin-lodash": "^3.3.2",
|
||||
"cross-env": "^5.1.3",
|
||||
"rimraf": "^2.6.2"
|
||||
"rimraf": "^3.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
"cross-env": "^5.1.3",
|
||||
"dotenv": "^8.0.0",
|
||||
"index-modules": "^0.3.0",
|
||||
"rimraf": "^2.6.2"
|
||||
"rimraf": "^3.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
"babel-plugin-lodash": "^3.3.2",
|
||||
"cross-env": "^5.1.3",
|
||||
"index-modules": "^0.3.0",
|
||||
"rimraf": "^2.6.2"
|
||||
"rimraf": "^3.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
"babel-plugin-dev": "^1.0.0",
|
||||
"babel-plugin-lodash": "^3.3.2",
|
||||
"cross-env": "^5.1.3",
|
||||
"rimraf": "^2.6.2"
|
||||
"rimraf": "^3.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
|
||||
|
||||
3
@xen-orchestra/template/.babelrc.js
Normal file
3
@xen-orchestra/template/.babelrc.js
Normal file
@@ -0,0 +1,3 @@
|
||||
module.exports = require('../../@xen-orchestra/babel-config')(
|
||||
require('./package.json')
|
||||
)
|
||||
62
@xen-orchestra/template/README.md
Normal file
62
@xen-orchestra/template/README.md
Normal file
@@ -0,0 +1,62 @@
|
||||
# @xen-orchestra/template [](https://travis-ci.org/vatesfr/xen-orchestra)
|
||||
|
||||
## Install
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/@xen-orchestra/template):
|
||||
|
||||
```
|
||||
> npm install --save @xen-orchestra/template
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Create a string replacer based on a pattern and a list of rules.
|
||||
|
||||
```js
|
||||
const myReplacer = compileTemplate('{name}_COPY_\{name}_{id}_%\%', {
|
||||
'{name}': vm => vm.name_label,
|
||||
'{id}': vm => vm.id,
|
||||
'%': (_, i) => i
|
||||
})
|
||||
|
||||
const newString = myReplacer({
|
||||
name_label: 'foo',
|
||||
id: 42,
|
||||
}, 32)
|
||||
|
||||
newString === 'foo_COPY_{name}_42_32%' // true
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
```
|
||||
# Install dependencies
|
||||
> yarn
|
||||
|
||||
# Run the tests
|
||||
> yarn test
|
||||
|
||||
# Continuously compile
|
||||
> yarn dev
|
||||
|
||||
# Continuously run the tests
|
||||
> yarn dev-test
|
||||
|
||||
# Build for production (automatically called by npm install)
|
||||
> yarn build
|
||||
```
|
||||
|
||||
## Contributions
|
||||
|
||||
Contributions are *very* welcomed, either on the documentation or on
|
||||
the code.
|
||||
|
||||
You may:
|
||||
|
||||
- report any [issue](https://github.com/vatesfr/xen-orchestra/issues)
|
||||
you've encountered;
|
||||
- fork and create a pull request.
|
||||
|
||||
## License
|
||||
|
||||
ISC © [Vates SAS](https://vates.fr)
|
||||
46
@xen-orchestra/template/package.json
Normal file
46
@xen-orchestra/template/package.json
Normal file
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"name": "@xen-orchestra/template",
|
||||
"version": "0.1.0",
|
||||
"license": "ISC",
|
||||
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/template",
|
||||
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
|
||||
"repository": {
|
||||
"directory": "@xen-orchestra/template",
|
||||
"type": "git",
|
||||
"url": "https://github.com/vatesfr/xen-orchestra.git"
|
||||
},
|
||||
"author": {
|
||||
"name": "Julien Fontanet",
|
||||
"email": "julien.fontanet@vates.fr"
|
||||
},
|
||||
"preferGlobal": false,
|
||||
"main": "dist/",
|
||||
"files": [
|
||||
"dist/"
|
||||
],
|
||||
"browserslist": [
|
||||
">2%"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.0.0",
|
||||
"@babel/core": "^7.0.0",
|
||||
"@babel/preset-env": "^7.0.0",
|
||||
"cross-env": "^5.1.3",
|
||||
"rimraf": "^3.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
|
||||
"clean": "rimraf dist/",
|
||||
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
|
||||
"prebuild": "yarn run clean",
|
||||
"predev": "yarn run prebuild",
|
||||
"prepublishOnly": "yarn run build",
|
||||
"postversion": "npm publish --access public"
|
||||
},
|
||||
"dependencies": {
|
||||
"lodash": "^4.17.15"
|
||||
}
|
||||
}
|
||||
19
@xen-orchestra/template/src/index.js
Normal file
19
@xen-orchestra/template/src/index.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import escapeRegExp from 'lodash/escapeRegExp'
|
||||
|
||||
const compareLengthDesc = (a, b) => b.length - a.length
|
||||
|
||||
export function compileTemplate(pattern, rules) {
|
||||
const matches = Object.keys(rules)
|
||||
.sort(compareLengthDesc)
|
||||
.map(escapeRegExp)
|
||||
.join('|')
|
||||
const regExp = new RegExp(`\\\\(?:\\\\|${matches})|${matches}`, 'g')
|
||||
return (...params) =>
|
||||
pattern.replace(regExp, match => {
|
||||
if (match[0] === '\\') {
|
||||
return match.slice(1)
|
||||
}
|
||||
const rule = rules[match]
|
||||
return typeof rule === 'function' ? rule(...params) : rule
|
||||
})
|
||||
}
|
||||
14
@xen-orchestra/template/src/index.spec.js
Normal file
14
@xen-orchestra/template/src/index.spec.js
Normal file
@@ -0,0 +1,14 @@
|
||||
/* eslint-env jest */
|
||||
import { compileTemplate } from '.'
|
||||
|
||||
it("correctly replaces the template's variables", () => {
|
||||
const replacer = compileTemplate(
|
||||
'{property}_\\{property}_\\\\{property}_{constant}_%_FOO',
|
||||
{
|
||||
'{property}': obj => obj.name,
|
||||
'{constant}': 1235,
|
||||
'%': (_, i) => i,
|
||||
}
|
||||
)
|
||||
expect(replacer({ name: 'bar' }, 5)).toBe('bar_{property}_\\bar_1235_5_FOO')
|
||||
})
|
||||
49
CHANGELOG.md
49
CHANGELOG.md
@@ -4,6 +4,49 @@
|
||||
|
||||
### Enhancements
|
||||
|
||||
- [SR/new] Clarify address formats [#4450](https://github.com/vatesfr/xen-orchestra/issues/4450) (PR [#4460](https://github.com/vatesfr/xen-orchestra/pull/4460))
|
||||
- [Backup NG/New] Show warning if zstd compression is not supported on a VM [#3892](https://github.com/vatesfr/xen-orchestra/issues/3892) (PRs [#4411](https://github.com/vatesfr/xen-orchestra/pull/4411))
|
||||
- [VM/disks] Don't hide disks that are attached to the same VM twice [#4400](https://github.com/vatesfr/xen-orchestra/issues/4400) (PR [#4414](https://github.com/vatesfr/xen-orchestra/pull/4414))
|
||||
- [VM/console] Add a button to connect to the VM via the local SSH client (PR [#4415](https://github.com/vatesfr/xen-orchestra/pull/4415))
|
||||
- [SDN Controller] Add possibility to encrypt private networks (PR [#4441](https://github.com/vatesfr/xen-orchestra/pull/4441))
|
||||
- [SDN Controller] Ability to configure MTU for private networks (PR [#4491](https://github.com/vatesfr/xen-orchestra/pull/4491))
|
||||
- [VM Export] Filenames are now prefixed with datetime [#4503](https://github.com/vatesfr/xen-orchestra/issues/4503)
|
||||
- [Backups] Improve performance by caching VM backups listing (PR [#4509](https://github.com/vatesfr/xen-orchestra/pull/4509))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- [PBD] Obfuscate cifs password from device config [#4384](https://github.com/vatesfr/xen-orchestra/issues/4384) (PR [#4401](https://github.com/vatesfr/xen-orchestra/pull/4401))
|
||||
- [XOSAN] Fix "invalid parameters" error on creating a SR (PR [#4478](https://github.com/vatesfr/xen-orchestra/pull/4478))
|
||||
- [Patching] Avoid overloading XCP-ng by reducing the frequency of yum update checks [#4358](https://github.com/vatesfr/xen-orchestra/issues/4358) (PR [#4477](https://github.com/vatesfr/xen-orchestra/pull/4477))
|
||||
- [Network] Fix inability to create a bonded network (PR [#4489](https://github.com/vatesfr/xen-orchestra/pull/4489))
|
||||
- [Backup restore & Replication] Don't copy `sm_config` to new VDIs which might leads to useless coalesces [#4482](https://github.com/vatesfr/xen-orchestra/issues/4482) (PR [#4484](https://github.com/vatesfr/xen-orchestra/pull/4484))
|
||||
- [Home] Fix intermediary "no results" display showed on filtering items [#4420](https://github.com/vatesfr/xen-orchestra/issues/4420) (PR [#4456](https://github.com/vatesfr/xen-orchestra/pull/4456)
|
||||
- [Backup NG/New schedule] Properly show user errors in the form [#3831](https://github.com/vatesfr/xen-orchestra/issues/3831) (PR [#4131](https://github.com/vatesfr/xen-orchestra/pull/4131))
|
||||
- [VM/Advanced] Fix `"vm.set_domain_type" is not a function` error on switching virtualization mode (PV/HVM) [#4348](https://github.com/vatesfr/xen-orchestra/issues/4348) (PR [#4504](https://github.com/vatesfr/xen-orchestra/pull/4504))
|
||||
- [Backup NG/logs] Show warning when zstd compression is selected but not supported [#3892](https://github.com/vatesfr/xen-orchestra/issues/3892) (PR [#4375](https://github.com/vatesfr/xen-orchestra/pull/4375)
|
||||
- [Patches] Fix patches installation for CH 8.0 (PR [#4511](https://github.com/vatesfr/xen-orchestra/pull/4511))
|
||||
- [Network] Fix inability to set a network name [#4514](https://github.com/vatesfr/xen-orchestra/issues/4514) (PR [4510](https://github.com/vatesfr/xen-orchestra/pull/4510))
|
||||
- [Backup NG] Fix race conditions that could lead to disabled jobs still running (PR [4510](https://github.com/vatesfr/xen-orchestra/pull/4510))
|
||||
- [XOA] Remove "Updates" and "Licenses" tabs for non admin users (PR [#4526](https://github.com/vatesfr/xen-orchestra/pull/4526))
|
||||
- [New VM] Ability to escape [cloud config template](https://xen-orchestra.com/blog/xen-orchestra-5-21/#cloudconfigtemplates) variables [#4486](https://github.com/vatesfr/xen-orchestra/issues/4486) (PR [#4501](https://github.com/vatesfr/xen-orchestra/pull/4501))
|
||||
- [Backup NG] Properly log and report if job is already running [#4497](https://github.com/vatesfr/xen-orchestra/issues/4497) (PR [4534](https://github.com/vatesfr/xen-orchestra/pull/4534))
|
||||
|
||||
|
||||
### Released packages
|
||||
|
||||
- @xen-orchestra/cron v1.0.4
|
||||
- xo-server-sdn-controller v0.3.0
|
||||
- @xen-orchestra/template v0.1.0
|
||||
- xo-server v5.50.0
|
||||
- xo-web v5.50.0
|
||||
|
||||
|
||||
## **5.38.0** (2019-08-29)
|
||||
|
||||

|
||||
|
||||
### Enhancements
|
||||
|
||||
- [VM/Attach disk] Display confirmation modal when VDI is already attached [#3381](https://github.com/vatesfr/xen-orchestra/issues/3381) (PR [#4366](https://github.com/vatesfr/xen-orchestra/pull/4366))
|
||||
- [Zstd]
|
||||
- [VM/copy, VM/export] Only show zstd option when it's supported [#3892](https://github.com/vatesfr/xen-orchestra/issues/3892) (PRs [#4326](https://github.com/vatesfr/xen-orchestra/pull/4326) [#4368](https://github.com/vatesfr/xen-orchestra/pull/4368))
|
||||
@@ -24,11 +67,11 @@
|
||||
- xo-server-sdn-controller v0.2.0
|
||||
- xo-server-usage-report v0.7.3
|
||||
- xo-server v5.48.0
|
||||
- xo-web v5.48.0
|
||||
- xo-web v5.48.1
|
||||
|
||||
## **5.37.1** (2019-08-06)
|
||||
|
||||

|
||||

|
||||
|
||||
### Enhancements
|
||||
|
||||
@@ -85,8 +128,6 @@
|
||||
|
||||
## **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))
|
||||
|
||||
@@ -7,12 +7,20 @@
|
||||
|
||||
> Users must be able to say: “Nice enhancement, I'm eager to test it”
|
||||
|
||||
- [Settings/Logs] Differenciate XS/XCP-ng errors from XO errors [#4101](https://github.com/vatesfr/xen-orchestra/issues/4101) (PR [#4385](https://github.com/vatesfr/xen-orchestra/pull/4385))
|
||||
- [Backups] Improve performance by caching logs consolidation (PR [#4541](https://github.com/vatesfr/xen-orchestra/pull/4541))
|
||||
- [New VM] Cloud Init available for all plans (PR [#4543](https://github.com/vatesfr/xen-orchestra/pull/4543))
|
||||
- [Servers] IPv6 addresses can be used [#4520](https://github.com/vatesfr/xen-orchestra/issues/4520) (PR [#4521](https://github.com/vatesfr/xen-orchestra/pull/4521)) \
|
||||
Note: They must enclosed in brackets to differentiate with the port, e.g.: `[2001:db8::7334]` or `[ 2001:db8::7334]:4343`
|
||||
- [HUB] VM template store [#1918](https://github.com/vatesfr/xen-orchestra/issues/1918) (PR [#4442](https://github.com/vatesfr/xen-orchestra/pull/4442))
|
||||
- [Home/bulk vm migration] Display same-pool hosts first in the selector [#4462](https://github.com/vatesfr/xen-orchestra/issues/4462) (PR [#4474](https://github.com/vatesfr/xen-orchestra/pull/4474))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- [PBD] Obfuscate cifs password from device config [#4384](https://github.com/vatesfr/xen-orchestra/issues/4384) (PR [#4401](https://github.com/vatesfr/xen-orchestra/pull/4401))
|
||||
|
||||
> Users must be able to say: “I had this issue, happy to know it's fixed”
|
||||
|
||||
- [Host] Fix an issue where host was wrongly reporting time inconsistency (PR [#4540](https://github.com/vatesfr/xen-orchestra/pull/4540))
|
||||
|
||||
### Released packages
|
||||
|
||||
> Packages will be released in the order they are here, therefore, they should
|
||||
@@ -20,5 +28,7 @@
|
||||
>
|
||||
> Rule of thumb: add packages on top.
|
||||
|
||||
- xo-server v5.49.0
|
||||
- xo-web v5.49.0
|
||||
- xen-api v0.27.2
|
||||
- xo-server-cloud v0.3.0
|
||||
- xo-server v5.51.0
|
||||
- xo-web v5.51.0
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
### Check list
|
||||
|
||||
> Check items when done or if not relevant
|
||||
> Check if done.
|
||||
>
|
||||
> Strikethrough if not relevant: ~~example~~ ([doc](https://help.github.com/en/articles/basic-writing-and-formatting-syntax)).
|
||||
|
||||
- [ ] PR reference the relevant issue (e.g. `Fixes #007`)
|
||||
- [ ] PR reference the relevant issue (e.g. `Fixes #007` or `See xoa-support#42`)
|
||||
- [ ] if UI changes, a screenshot has been added to the PR
|
||||
- [ ] if `xo-server` API changes, the corresponding test has been added to/updated on [`xo-server-test`](https://github.com/vatesfr/xen-orchestra/tree/master/packages/xo-server-test)
|
||||
- [ ] `CHANGELOG.unreleased.md`:
|
||||
- enhancement/bug fix entry added
|
||||
- list of packages to release updated (`${name} v${new version}`)
|
||||
- [ ] documentation updated
|
||||
- [ ] **I have tested added/updated features** (and impacted code)
|
||||
- `CHANGELOG.unreleased.md`:
|
||||
- [ ] enhancement/bug fix entry added
|
||||
- [ ] list of packages to release updated (`${name} v${new version}`)
|
||||
- **I have tested added/updated features** (and impacted code)
|
||||
- [ ] unit tests (e.g. [`cron/parse.spec.js`](https://github.com/vatesfr/xen-orchestra/blob/b24400b21de1ebafa1099c56bac1de5c988d9202/%40xen-orchestra/cron/src/parse.spec.js))
|
||||
- [ ] if `xo-server` API changes, the corresponding test has been added to/updated on [`xo-server-test`](https://github.com/vatesfr/xen-orchestra/tree/master/packages/xo-server-test)
|
||||
- [ ] at least manual testing
|
||||
|
||||
### Process
|
||||
|
||||
@@ -17,3 +21,10 @@
|
||||
1. mark it as `WiP:` (Work in Progress) if not ready to be merged
|
||||
1. when you want a review, add a reviewer (and only one)
|
||||
1. if necessary, update your PR, and re- add a reviewer
|
||||
|
||||
From [_the Four Agreements_](https://en.wikipedia.org/wiki/Don_Miguel_Ruiz#The_Four_Agreements):
|
||||
|
||||
1. Be impeccable with your word.
|
||||
1. Don't take anything personally.
|
||||
1. Don't make assumptions.
|
||||
1. Always do your best.
|
||||
|
||||
@@ -13,11 +13,11 @@ It aims to be easy to use on any device supporting modern web technologies (HTML
|
||||
|
||||
## XOA quick deploy
|
||||
|
||||
SSH to your XenServer, and execute the following:
|
||||
Log in to your account and use the deploy form available on [this page](https://xen-orchestra.com/#!/xoa)
|
||||
|
||||
```
|
||||
bash -c "$(curl -s http://xoa.io/deploy)"
|
||||
```
|
||||
> **Note:** no data will be sent to our servers, it's running only between your browser and your host!
|
||||
|
||||
[](https://xen-orchestra.com/#!/xoa)
|
||||
|
||||
### XOA credentials
|
||||
|
||||
|
||||
@@ -55,6 +55,7 @@
|
||||
* [Emergency Shutdown](emergency_shutdown.md)
|
||||
* [Auto scalability](auto_scalability.md)
|
||||
* [Forecaster](forecaster.md)
|
||||
* [SDN Controller](sdn_controller.md)
|
||||
* [Recipes](recipes.md)
|
||||
* [Reverse proxy](reverse_proxy.md)
|
||||
* [How to contribute?](contributing.md)
|
||||
|
||||
BIN
docs/assets/deploy_form.png
Normal file
BIN
docs/assets/deploy_form.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 41 KiB |
BIN
docs/assets/sdn-controller.png
Normal file
BIN
docs/assets/sdn-controller.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 85 KiB |
@@ -15,5 +15,6 @@ We've made multiple categories to help you to find what you need:
|
||||
* [Job Manager](scheduler.html)
|
||||
* [Alerts](alerts.html)
|
||||
* [Load balancing](load_balancing.html)
|
||||
* [SDN Controller](sdn_controller.html)
|
||||
|
||||

|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
|
||||
# Installation
|
||||
|
||||
SSH to your XenServer/XCP-ng host and execute the following:
|
||||
Log in to your account and use the deploy form available on [this page](https://xen-orchestra.com/#!/xoa)
|
||||
|
||||
```
|
||||
bash -c "$(curl -s http://xoa.io/deploy)"
|
||||
```
|
||||

|
||||
|
||||
This will automatically download/import/start the XOA appliance. Nothing is changed on your host itself, it's 100% safe.
|
||||
|
||||
## [More on XOA](xoa.md)
|
||||
## [More on XOA and alternate deploy](xoa.md)
|
||||
|
||||

|
||||
|
||||
60
docs/sdn_controller.md
Normal file
60
docs/sdn_controller.md
Normal file
@@ -0,0 +1,60 @@
|
||||
# SDN Controller
|
||||
|
||||
> SDN Controller is available in XOA 5.44 and higher
|
||||
|
||||
The SDN Controller enables a user to **create pool-wide and cross-pool** (since XOA 5.48.1) **private networks**.
|
||||
|
||||

|
||||
|
||||
## How does it work?
|
||||
|
||||
Please read the [dedicated devblog on the SDN Controller](https://xen-orchestra.com/blog/xo-sdn-controller/) and its [extension for cross-pool private networks](https://xen-orchestra.com/blog/devblog-3-extending-the-sdn-controller/).
|
||||
|
||||
## Usage
|
||||
|
||||
### Network creation
|
||||
|
||||
In the network creation view:
|
||||
- Select a `pool`
|
||||
- Select `Private network`
|
||||
- Select an interface on which to create the network's tunnels
|
||||
- Select the encapsulation: a choice is offered between `GRE` and `VxLAN`, if `VxLAN` is chosen, then port 4789 must be open for UDP traffic on all the network's hosts (see [the requirements](#vxlan))
|
||||
- Choose if the network should be encrypted or not (see [the requirements](#encryption) to use encryption)
|
||||
- Select other `pool`s to add them to the network if desired
|
||||
- For each added `pool`: select an interface on which to create the tunnels
|
||||
- Create the network
|
||||
- Have fun! ☺
|
||||
|
||||
***NB:***
|
||||
- All hosts in a private network must be able to reach the other hosts' management interface.
|
||||
> The term ‘management interface’ is used to indicate the IP-enabled NIC that carries the management traffic.
|
||||
- Only 1 encrypted GRE network and 1 encrypted VxLAN network per pool can exist at a time due to Open vSwitch limitation.
|
||||
|
||||
### 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`: The path where the plugin will look for the certificates to create SSL connections with the hosts.
|
||||
If none is provided, the plugin will create its own self-signed certificates.
|
||||
- `override-certs`: Enable to uninstall the existing SDN controller CA certificate in order to replace it with the plugin's one.
|
||||
|
||||
## Requirements
|
||||
|
||||
### VxLAN
|
||||
|
||||
- On XCP-ng prior to 7.6:
|
||||
- To be able to use `VxLAN`, 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`
|
||||
|
||||
### Encryption
|
||||
|
||||
> Encryption is not available prior to 8.0.
|
||||
|
||||
- On XCP-ng 8.0:
|
||||
- To be able to encrypt the networks, `openvswitch-ipsec` package must be installed on all the hosts:
|
||||
- `yum install openvswitch-ipsec --enablerepo=xcp-ng-testing`
|
||||
- `systemctl enable ipsec`
|
||||
- `systemctl enable openvswitch-ipsec`
|
||||
- `systemctl start ipsec`
|
||||
- `systemctl start openvswitch-ipsec`
|
||||
@@ -26,6 +26,8 @@ The **fastest and most secure way** to install Xen Orchestra is to use our web d
|
||||
|
||||
> **Note:** no data will be sent to our servers, it's running only between your browser and your host!
|
||||
|
||||

|
||||
|
||||
### Via a bash script
|
||||
|
||||
Alternatively, you can deploy it by connecting to your XenServer host and executing the following:
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
"benchmark": "^2.1.4",
|
||||
"eslint": "^6.0.1",
|
||||
"eslint-config-prettier": "^6.0.0",
|
||||
"eslint-config-standard": "12.0.0",
|
||||
"eslint-config-standard-jsx": "^6.0.2",
|
||||
"eslint-config-standard": "14.1.0",
|
||||
"eslint-config-standard-jsx": "^8.1.0",
|
||||
"eslint-plugin-eslint-comments": "^3.1.1",
|
||||
"eslint-plugin-import": "^2.8.0",
|
||||
"eslint-plugin-node": "^9.0.1",
|
||||
@@ -17,7 +17,7 @@
|
||||
"eslint-plugin-react": "^7.6.1",
|
||||
"eslint-plugin-standard": "^4.0.0",
|
||||
"exec-promise": "^0.7.0",
|
||||
"flow-bin": "^0.102.0",
|
||||
"flow-bin": "^0.106.3",
|
||||
"globby": "^10.0.0",
|
||||
"husky": "^3.0.0",
|
||||
"jest": "^24.1.0",
|
||||
@@ -46,6 +46,7 @@
|
||||
"/xo-web/"
|
||||
],
|
||||
"testRegex": "\\.spec\\.js$",
|
||||
"timers": "fake",
|
||||
"transform": {
|
||||
"\\.jsx?$": "babel-jest"
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
"@babel/preset-env": "^7.0.0",
|
||||
"babel-plugin-lodash": "^3.3.2",
|
||||
"cross-env": "^5.1.1",
|
||||
"rimraf": "^2.6.2"
|
||||
"rimraf": "^3.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
"@babel/preset-env": "^7.0.0",
|
||||
"@babel/preset-flow": "^7.0.0",
|
||||
"cross-env": "^5.1.3",
|
||||
"rimraf": "^2.6.2"
|
||||
"rimraf": "^3.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@xen-orchestra/fs": "^0.10.1",
|
||||
"cli-progress": "^2.0.0",
|
||||
"cli-progress": "^3.1.0",
|
||||
"exec-promise": "^0.7.0",
|
||||
"getopts": "^2.2.3",
|
||||
"struct-fu": "^1.2.0",
|
||||
@@ -43,7 +43,7 @@
|
||||
"execa": "^2.0.2",
|
||||
"index-modules": "^0.3.0",
|
||||
"promise-toolbox": "^0.13.0",
|
||||
"rimraf": "^2.6.1",
|
||||
"rimraf": "^3.0.0",
|
||||
"tmp": "^0.1.0"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
"get-stream": "^5.1.0",
|
||||
"index-modules": "^0.3.0",
|
||||
"readable-stream": "^3.0.6",
|
||||
"rimraf": "^2.6.2",
|
||||
"rimraf": "^3.0.0",
|
||||
"tmp": "^0.1.0"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
"@babel/preset-env": "^7.1.5",
|
||||
"babel-plugin-lodash": "^3.2.11",
|
||||
"cross-env": "^5.1.4",
|
||||
"rimraf": "^2.6.1"
|
||||
"rimraf": "^3.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
|
||||
|
||||
189
packages/xen-api/examples/package-lock.json
generated
Normal file
189
packages/xen-api/examples/package-lock.json
generated
Normal file
@@ -0,0 +1,189 @@
|
||||
{
|
||||
"requires": true,
|
||||
"lockfileVersion": 1,
|
||||
"dependencies": {
|
||||
"core-util-is": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
|
||||
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
|
||||
},
|
||||
"debug": {
|
||||
"version": "2.6.9",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
||||
"requires": {
|
||||
"ms": "2.0.0"
|
||||
}
|
||||
},
|
||||
"event-loop-delay": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/event-loop-delay/-/event-loop-delay-1.0.0.tgz",
|
||||
"integrity": "sha512-8YtyeIWHXrvTqlAhv+fmtaGGARmgStbvocERYzrZ3pwhnQULe5PuvMUTjIWw/emxssoaftfHZsJtkeY8xjiXCg==",
|
||||
"requires": {
|
||||
"napi-macros": "^1.8.2",
|
||||
"node-gyp-build": "^3.7.0"
|
||||
}
|
||||
},
|
||||
"getopts": {
|
||||
"version": "2.2.5",
|
||||
"resolved": "https://registry.npmjs.org/getopts/-/getopts-2.2.5.tgz",
|
||||
"integrity": "sha512-9jb7AW5p3in+IiJWhQiZmmwkpLaR/ccTWdWQCtZM66HJcHHLegowh4q4tSD7gouUyeNvFWRavfK9GXosQHDpFA=="
|
||||
},
|
||||
"golike-defer": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/golike-defer/-/golike-defer-0.4.1.tgz",
|
||||
"integrity": "sha512-x8cq/Fvu32T8cnco3CBDRF+/M2LFmfSIysKfecX09uIK3cFdHcEKBTPlPnEO6lwrdxfjkOIU6dIw3EIlEJeS1A=="
|
||||
},
|
||||
"human-format": {
|
||||
"version": "0.10.1",
|
||||
"resolved": "https://registry.npmjs.org/human-format/-/human-format-0.10.1.tgz",
|
||||
"integrity": "sha512-UzCHToSw3HI9MxH9tYzMr1JbHJbgzr6o0hZCun7sruv59S1leps21bmgpBkkwEvQon5n/2OWKH1iU7BEko02cg=="
|
||||
},
|
||||
"inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
|
||||
},
|
||||
"isarray": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
|
||||
},
|
||||
"make-error": {
|
||||
"version": "1.3.5",
|
||||
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.5.tgz",
|
||||
"integrity": "sha512-c3sIjNUow0+8swNwVpqoH4YCShKNFkMaw6oH1mNS2haDZQqkeZFlHS3dhoeEbKKmJB4vXpJucU6oH75aDYeE9g=="
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
|
||||
},
|
||||
"napi-macros": {
|
||||
"version": "1.8.2",
|
||||
"resolved": "https://registry.npmjs.org/napi-macros/-/napi-macros-1.8.2.tgz",
|
||||
"integrity": "sha512-Tr0DNY4RzTaBG2W2m3l7ZtFuJChTH6VZhXVhkGGjF/4cZTt+i8GcM9ozD+30Lmr4mDoZ5Xx34t2o4GJqYWDGcg=="
|
||||
},
|
||||
"node-gyp-build": {
|
||||
"version": "3.9.0",
|
||||
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-3.9.0.tgz",
|
||||
"integrity": "sha512-zLcTg6P4AbcHPq465ZMFNXx7XpKKJh+7kkN699NiQWisR2uWYOWNWqRHAmbnmKiL4e9aLSlmy5U7rEMUXV59+A=="
|
||||
},
|
||||
"prettier-bytes": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/prettier-bytes/-/prettier-bytes-1.0.4.tgz",
|
||||
"integrity": "sha1-mUsCqkb2mcULYle1+qp/4lV+YtY="
|
||||
},
|
||||
"process-nextick-args": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
|
||||
},
|
||||
"process-top": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/process-top/-/process-top-1.0.0.tgz",
|
||||
"integrity": "sha512-er8iSmBMslOt5cgIHg9m6zilTPsuUqpEb1yfQ4bDmO80zr/e/5hNn+Tay3CJM/FOBnJo8Bt3fFiDDH6GvIgeAg==",
|
||||
"requires": {
|
||||
"event-loop-delay": "^1.0.0",
|
||||
"prettier-bytes": "^1.0.4"
|
||||
}
|
||||
},
|
||||
"progress-stream": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/progress-stream/-/progress-stream-2.0.0.tgz",
|
||||
"integrity": "sha1-+sY6Cz0R3qy7CWmrzJOyFLzhntU=",
|
||||
"requires": {
|
||||
"speedometer": "~1.0.0",
|
||||
"through2": "~2.0.3"
|
||||
}
|
||||
},
|
||||
"promise-toolbox": {
|
||||
"version": "0.13.0",
|
||||
"resolved": "https://registry.npmjs.org/promise-toolbox/-/promise-toolbox-0.13.0.tgz",
|
||||
"integrity": "sha512-Z6u7EL9/QyY1zZqeqpEiKS7ygKwZyl0JL0ouno/en6vMliZZc4AmM0aFCrDAVxEyKqj2f3SpkW0lXEfAZsNWiQ==",
|
||||
"requires": {
|
||||
"make-error": "^1.3.2"
|
||||
}
|
||||
},
|
||||
"readable-stream": {
|
||||
"version": "3.4.0",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.4.0.tgz",
|
||||
"integrity": "sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ==",
|
||||
"requires": {
|
||||
"inherits": "^2.0.3",
|
||||
"string_decoder": "^1.1.1",
|
||||
"util-deprecate": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
|
||||
},
|
||||
"speedometer": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/speedometer/-/speedometer-1.0.0.tgz",
|
||||
"integrity": "sha1-zWccsGdSwivKM3Di8zREC+T8YuI="
|
||||
},
|
||||
"stream-parser": {
|
||||
"version": "0.3.1",
|
||||
"resolved": "https://registry.npmjs.org/stream-parser/-/stream-parser-0.3.1.tgz",
|
||||
"integrity": "sha1-FhhUhpRCACGhGC/wrxkRwSl2F3M=",
|
||||
"requires": {
|
||||
"debug": "2"
|
||||
}
|
||||
},
|
||||
"string_decoder": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||
"requires": {
|
||||
"safe-buffer": "~5.1.0"
|
||||
}
|
||||
},
|
||||
"throttle": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/throttle/-/throttle-1.0.3.tgz",
|
||||
"integrity": "sha1-ijLkoV8XY9mXlIMXxevjrYpB5Lc=",
|
||||
"requires": {
|
||||
"readable-stream": ">= 0.3.0",
|
||||
"stream-parser": ">= 0.0.2"
|
||||
}
|
||||
},
|
||||
"through2": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz",
|
||||
"integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==",
|
||||
"requires": {
|
||||
"readable-stream": "~2.3.6",
|
||||
"xtend": "~4.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"readable-stream": {
|
||||
"version": "2.3.6",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
|
||||
"integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
|
||||
"requires": {
|
||||
"core-util-is": "~1.0.0",
|
||||
"inherits": "~2.0.3",
|
||||
"isarray": "~1.0.0",
|
||||
"process-nextick-args": "~2.0.0",
|
||||
"safe-buffer": "~5.1.1",
|
||||
"string_decoder": "~1.1.1",
|
||||
"util-deprecate": "~1.0.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
|
||||
},
|
||||
"xtend": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
||||
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
"human-format": "^0.10.1",
|
||||
"process-top": "^1.0.0",
|
||||
"progress-stream": "^2.0.0",
|
||||
"promise-toolbox": "^0.11.0",
|
||||
"promise-toolbox": "^0.13.0",
|
||||
"readable-stream": "^3.1.1",
|
||||
"throttle": "^1.0.3"
|
||||
}
|
||||
|
||||
@@ -1,179 +0,0 @@
|
||||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
core-util-is@~1.0.0:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
|
||||
integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=
|
||||
|
||||
debug@2:
|
||||
version "2.6.9"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
|
||||
integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
|
||||
dependencies:
|
||||
ms "2.0.0"
|
||||
|
||||
event-loop-delay@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/event-loop-delay/-/event-loop-delay-1.0.0.tgz#5af6282549494fd0d868c499cbdd33e027978b8c"
|
||||
integrity sha512-8YtyeIWHXrvTqlAhv+fmtaGGARmgStbvocERYzrZ3pwhnQULe5PuvMUTjIWw/emxssoaftfHZsJtkeY8xjiXCg==
|
||||
dependencies:
|
||||
napi-macros "^1.8.2"
|
||||
node-gyp-build "^3.7.0"
|
||||
|
||||
getopts@^2.2.3:
|
||||
version "2.2.3"
|
||||
resolved "https://registry.yarnpkg.com/getopts/-/getopts-2.2.3.tgz#11d229775e2ec2067ed8be6fcc39d9b4bf39cf7d"
|
||||
integrity sha512-viEcb8TpgeG05+Nqo5EzZ8QR0hxdyrYDp6ZSTZqe2M/h53Bk036NmqG38Vhf5RGirC/Of9Xql+v66B2gp256SQ==
|
||||
|
||||
golike-defer@^0.4.1:
|
||||
version "0.4.1"
|
||||
resolved "https://registry.yarnpkg.com/golike-defer/-/golike-defer-0.4.1.tgz#7a1cd435d61e461305805d980b133a0f3db4e1cc"
|
||||
|
||||
human-format@^0.10.1:
|
||||
version "0.10.1"
|
||||
resolved "https://registry.yarnpkg.com/human-format/-/human-format-0.10.1.tgz#107793f355912e256148d5b5dcf66a0230187ee9"
|
||||
integrity sha512-UzCHToSw3HI9MxH9tYzMr1JbHJbgzr6o0hZCun7sruv59S1leps21bmgpBkkwEvQon5n/2OWKH1iU7BEko02cg==
|
||||
|
||||
inherits@^2.0.3, inherits@~2.0.3:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
|
||||
integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=
|
||||
|
||||
isarray@~1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
|
||||
integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=
|
||||
|
||||
make-error@^1.3.2:
|
||||
version "1.3.5"
|
||||
resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.5.tgz#efe4e81f6db28cadd605c70f29c831b58ef776c8"
|
||||
integrity sha512-c3sIjNUow0+8swNwVpqoH4YCShKNFkMaw6oH1mNS2haDZQqkeZFlHS3dhoeEbKKmJB4vXpJucU6oH75aDYeE9g==
|
||||
|
||||
ms@2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
|
||||
integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=
|
||||
|
||||
napi-macros@^1.8.2:
|
||||
version "1.8.2"
|
||||
resolved "https://registry.yarnpkg.com/napi-macros/-/napi-macros-1.8.2.tgz#299265c1d8aa401351ad0675107d751228c03eda"
|
||||
integrity sha512-Tr0DNY4RzTaBG2W2m3l7ZtFuJChTH6VZhXVhkGGjF/4cZTt+i8GcM9ozD+30Lmr4mDoZ5Xx34t2o4GJqYWDGcg==
|
||||
|
||||
node-gyp-build@^3.7.0:
|
||||
version "3.7.0"
|
||||
resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-3.7.0.tgz#daa77a4f547b9aed3e2aac779eaf151afd60ec8d"
|
||||
integrity sha512-L/Eg02Epx6Si2NXmedx+Okg+4UHqmaf3TNcxd50SF9NQGcJaON3AtU++kax69XV7YWz4tUspqZSAsVofhFKG2w==
|
||||
|
||||
prettier-bytes@^1.0.4:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/prettier-bytes/-/prettier-bytes-1.0.4.tgz#994b02aa46f699c50b6257b5faaa7fe2557e62d6"
|
||||
integrity sha1-mUsCqkb2mcULYle1+qp/4lV+YtY=
|
||||
|
||||
process-nextick-args@~2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa"
|
||||
integrity sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==
|
||||
|
||||
process-top@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/process-top/-/process-top-1.0.0.tgz#52892bedb581c5abf0df2d0aa5c429e34275cc7e"
|
||||
integrity sha512-er8iSmBMslOt5cgIHg9m6zilTPsuUqpEb1yfQ4bDmO80zr/e/5hNn+Tay3CJM/FOBnJo8Bt3fFiDDH6GvIgeAg==
|
||||
dependencies:
|
||||
event-loop-delay "^1.0.0"
|
||||
prettier-bytes "^1.0.4"
|
||||
|
||||
progress-stream@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/progress-stream/-/progress-stream-2.0.0.tgz#fac63a0b3d11deacbb0969abcc93b214bce19ed5"
|
||||
integrity sha1-+sY6Cz0R3qy7CWmrzJOyFLzhntU=
|
||||
dependencies:
|
||||
speedometer "~1.0.0"
|
||||
through2 "~2.0.3"
|
||||
|
||||
promise-toolbox@^0.11.0:
|
||||
version "0.11.0"
|
||||
resolved "https://registry.yarnpkg.com/promise-toolbox/-/promise-toolbox-0.11.0.tgz#9ed928355355395072dace3f879879504e07d1bc"
|
||||
integrity sha512-bjHk0kq+Ke3J3zbkbbJH6kXCyQZbFHwOTrE/Et7vS0uS0tluoV+PLqU/kEyxl8aARM7v04y2wFoDo/wWAEPvjA==
|
||||
dependencies:
|
||||
make-error "^1.3.2"
|
||||
|
||||
"readable-stream@>= 0.3.0", readable-stream@^3.1.1:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.1.1.tgz#ed6bbc6c5ba58b090039ff18ce670515795aeb06"
|
||||
integrity sha512-DkN66hPyqDhnIQ6Jcsvx9bFjhw214O4poMBcIMgPVpQvNy9a0e0Uhg5SqySyDKAmUlwt8LonTBz1ezOnM8pUdA==
|
||||
dependencies:
|
||||
inherits "^2.0.3"
|
||||
string_decoder "^1.1.1"
|
||||
util-deprecate "^1.0.1"
|
||||
|
||||
readable-stream@~2.3.6:
|
||||
version "2.3.6"
|
||||
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf"
|
||||
integrity sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==
|
||||
dependencies:
|
||||
core-util-is "~1.0.0"
|
||||
inherits "~2.0.3"
|
||||
isarray "~1.0.0"
|
||||
process-nextick-args "~2.0.0"
|
||||
safe-buffer "~5.1.1"
|
||||
string_decoder "~1.1.1"
|
||||
util-deprecate "~1.0.1"
|
||||
|
||||
safe-buffer@~5.1.0, safe-buffer@~5.1.1:
|
||||
version "5.1.2"
|
||||
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
|
||||
integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
|
||||
|
||||
speedometer@~1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/speedometer/-/speedometer-1.0.0.tgz#cd671cb06752c22bca3370e2f334440be4fc62e2"
|
||||
integrity sha1-zWccsGdSwivKM3Di8zREC+T8YuI=
|
||||
|
||||
"stream-parser@>= 0.0.2":
|
||||
version "0.3.1"
|
||||
resolved "https://registry.yarnpkg.com/stream-parser/-/stream-parser-0.3.1.tgz#1618548694420021a1182ff0af1911c129761773"
|
||||
integrity sha1-FhhUhpRCACGhGC/wrxkRwSl2F3M=
|
||||
dependencies:
|
||||
debug "2"
|
||||
|
||||
string_decoder@^1.1.1:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.2.0.tgz#fe86e738b19544afe70469243b2a1ee9240eae8d"
|
||||
integrity sha512-6YqyX6ZWEYguAxgZzHGL7SsCeGx3V2TtOTqZz1xSTSWnqsbWwbptafNyvf/ACquZUXV3DANr5BDIwNYe1mN42w==
|
||||
dependencies:
|
||||
safe-buffer "~5.1.0"
|
||||
|
||||
string_decoder@~1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8"
|
||||
integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==
|
||||
dependencies:
|
||||
safe-buffer "~5.1.0"
|
||||
|
||||
throttle@^1.0.3:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/throttle/-/throttle-1.0.3.tgz#8a32e4a15f1763d997948317c5ebe3ad8a41e4b7"
|
||||
integrity sha1-ijLkoV8XY9mXlIMXxevjrYpB5Lc=
|
||||
dependencies:
|
||||
readable-stream ">= 0.3.0"
|
||||
stream-parser ">= 0.0.2"
|
||||
|
||||
through2@~2.0.3:
|
||||
version "2.0.5"
|
||||
resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd"
|
||||
integrity sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==
|
||||
dependencies:
|
||||
readable-stream "~2.3.6"
|
||||
xtend "~4.0.1"
|
||||
|
||||
util-deprecate@^1.0.1, util-deprecate@~1.0.1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
|
||||
integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
|
||||
|
||||
xtend@~4.0.1:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af"
|
||||
integrity sha1-pcbVMr5lbiPbgg77lDofBJmNY68=
|
||||
@@ -61,7 +61,7 @@
|
||||
"@babel/preset-env": "^7.0.0",
|
||||
"babel-plugin-lodash": "^3.3.2",
|
||||
"cross-env": "^5.1.3",
|
||||
"rimraf": "^2.6.1"
|
||||
"rimraf": "^3.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const URL_RE = /^(?:(https?:)\/*)?(?:([^:]+):([^@]+)@)?([^/]+?)(?::([0-9]+))?\/?$/
|
||||
const URL_RE = /^(?:(https?:)\/*)?(?:([^:]+):([^@]+)@)?(?:\[([^\]]+)\]|([^:/]+))(?::([0-9]+))?\/?$/
|
||||
|
||||
export default url => {
|
||||
const matches = URL_RE.exec(url)
|
||||
@@ -6,7 +6,15 @@ export default url => {
|
||||
throw new Error('invalid URL: ' + url)
|
||||
}
|
||||
|
||||
const [, protocol = 'https:', username, password, hostname, port] = matches
|
||||
const [
|
||||
,
|
||||
protocol = 'https:',
|
||||
username,
|
||||
password,
|
||||
ipv6,
|
||||
hostname = ipv6,
|
||||
port,
|
||||
] = matches
|
||||
const parsedUrl = { protocol, hostname, port }
|
||||
if (username !== undefined) {
|
||||
parsedUrl.username = decodeURIComponent(username)
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
"@babel/preset-flow": "^7.0.0",
|
||||
"babel-plugin-lodash": "^3.3.2",
|
||||
"cross-env": "^5.1.3",
|
||||
"rimraf": "^2.6.2"
|
||||
"rimraf": "^3.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
|
||||
|
||||
@@ -199,7 +199,18 @@ function main(args) {
|
||||
return exports[fnName](args.slice(1))
|
||||
}
|
||||
|
||||
return exports.call(args)
|
||||
return exports.call(args).catch(error => {
|
||||
if (!(error != null && error.code === 10 && 'errors' in error.data)) {
|
||||
throw error
|
||||
}
|
||||
|
||||
const lines = [error.message]
|
||||
const { errors } = error.data
|
||||
errors.forEach(error => {
|
||||
lines.push(` property ${error.property}: ${error.message}`)
|
||||
})
|
||||
throw lines.join('\n')
|
||||
})
|
||||
}
|
||||
exports = module.exports = main
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
"@babel/preset-env": "^7.0.0",
|
||||
"cross-env": "^5.1.3",
|
||||
"event-to-promise": "^0.8.0",
|
||||
"rimraf": "^2.6.1"
|
||||
"rimraf": "^3.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
"@babel/preset-env": "^7.0.0",
|
||||
"babel-plugin-lodash": "^3.3.2",
|
||||
"cross-env": "^5.1.3",
|
||||
"rimraf": "^2.6.1"
|
||||
"rimraf": "^3.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
"@babel/preset-env": "^7.0.0",
|
||||
"babel-plugin-lodash": "^3.3.2",
|
||||
"cross-env": "^5.1.3",
|
||||
"rimraf": "^2.6.1"
|
||||
"rimraf": "^3.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
"babel-plugin-lodash": "^3.3.2",
|
||||
"cross-env": "^5.1.3",
|
||||
"deep-freeze": "^0.0.1",
|
||||
"rimraf": "^2.6.1"
|
||||
"rimraf": "^3.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
"@babel/preset-env": "^7.0.0",
|
||||
"babel-plugin-lodash": "^3.3.2",
|
||||
"cross-env": "^5.1.3",
|
||||
"rimraf": "^2.6.1"
|
||||
"rimraf": "^3.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
"dependencies": {
|
||||
"event-to-promise": "^0.8.0",
|
||||
"exec-promise": "^0.7.0",
|
||||
"inquirer": "^6.0.0",
|
||||
"inquirer": "^7.0.0",
|
||||
"ldapjs": "^1.0.1",
|
||||
"lodash": "^4.17.4",
|
||||
"promise-toolbox": "^0.13.0"
|
||||
@@ -47,7 +47,7 @@
|
||||
"@babel/preset-env": "^7.0.0",
|
||||
"babel-plugin-lodash": "^3.3.2",
|
||||
"cross-env": "^5.1.3",
|
||||
"rimraf": "^2.6.1"
|
||||
"rimraf": "^3.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
"@babel/preset-env": "^7.0.0",
|
||||
"babel-preset-env": "^1.6.1",
|
||||
"cross-env": "^5.1.3",
|
||||
"rimraf": "^2.6.1"
|
||||
"rimraf": "^3.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
"@babel/preset-env": "^7.0.0",
|
||||
"babel-plugin-lodash": "^3.3.2",
|
||||
"cross-env": "^5.1.3",
|
||||
"rimraf": "^2.6.1"
|
||||
"rimraf": "^3.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
"@babel/core": "^7.0.0",
|
||||
"@babel/preset-env": "^7.0.0",
|
||||
"cross-env": "^5.1.3",
|
||||
"rimraf": "^2.5.4"
|
||||
"rimraf": "^3.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
|
||||
|
||||
@@ -20,9 +20,13 @@ class XoServerCloud {
|
||||
}
|
||||
|
||||
async load() {
|
||||
const getResourceCatalog = () => this._getCatalog()
|
||||
getResourceCatalog.description = 'Get the list of all available resources'
|
||||
const getResourceCatalog = this._getCatalog.bind(this)
|
||||
getResourceCatalog.description =
|
||||
"Get the list of user's available resources"
|
||||
getResourceCatalog.permission = 'admin'
|
||||
getResourceCatalog.params = {
|
||||
filters: { type: 'object', optional: true },
|
||||
}
|
||||
|
||||
const registerResource = ({ namespace }) =>
|
||||
this._registerResource(namespace)
|
||||
@@ -34,8 +38,29 @@ class XoServerCloud {
|
||||
}
|
||||
registerResource.permission = 'admin'
|
||||
|
||||
const downloadAndInstallResource = this._downloadAndInstallResource.bind(
|
||||
this
|
||||
)
|
||||
|
||||
downloadAndInstallResource.description =
|
||||
'Download and install a resource via cloud plugin'
|
||||
|
||||
downloadAndInstallResource.params = {
|
||||
id: { type: 'string' },
|
||||
namespace: { type: 'string' },
|
||||
version: { type: 'string' },
|
||||
sr: { type: 'string' },
|
||||
}
|
||||
|
||||
downloadAndInstallResource.resolve = {
|
||||
sr: ['sr', 'SR', 'administrate'],
|
||||
}
|
||||
|
||||
downloadAndInstallResource.permission = 'admin'
|
||||
|
||||
this._unsetApiMethods = this._xo.addApiMethods({
|
||||
cloud: {
|
||||
downloadAndInstallResource,
|
||||
getResourceCatalog,
|
||||
registerResource,
|
||||
},
|
||||
@@ -66,8 +91,8 @@ class XoServerCloud {
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
async _getCatalog() {
|
||||
const catalog = await this._updater.call('getResourceCatalog')
|
||||
async _getCatalog({ filters } = {}) {
|
||||
const catalog = await this._updater.call('getResourceCatalog', { filters })
|
||||
|
||||
if (!catalog) {
|
||||
throw new Error('cannot get catalog')
|
||||
@@ -90,6 +115,26 @@ class XoServerCloud {
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
async _downloadAndInstallResource({ id, namespace, sr, version }) {
|
||||
const stream = await this._requestResource({
|
||||
hub: true,
|
||||
id,
|
||||
namespace,
|
||||
version,
|
||||
})
|
||||
const vm = await this._xo.getXapi(sr.$poolId).importVm(stream, {
|
||||
srId: sr.id,
|
||||
type: 'xva',
|
||||
})
|
||||
await vm.update_other_config({
|
||||
'xo:resource:namespace': namespace,
|
||||
'xo:resource:xva:version': version,
|
||||
'xo:resource:xva:id': id,
|
||||
})
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
async _registerResource(namespace) {
|
||||
const _namespace = (await this._getNamespaces())[namespace]
|
||||
|
||||
@@ -106,8 +151,10 @@ class XoServerCloud {
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
async _getNamespaceCatalog(namespace) {
|
||||
const namespaceCatalog = (await this._getCatalog())[namespace]
|
||||
async _getNamespaceCatalog({ hub, namespace }) {
|
||||
const namespaceCatalog = (await this._getCatalog({ filters: { hub } }))[
|
||||
namespace
|
||||
]
|
||||
|
||||
if (!namespaceCatalog) {
|
||||
throw new Error(`cannot get catalog: ${namespace} not registered`)
|
||||
@@ -118,14 +165,17 @@ class XoServerCloud {
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
async _requestResource(namespace, id, version) {
|
||||
async _requestResource({ hub = false, id, namespace, version }) {
|
||||
const _namespace = (await this._getNamespaces())[namespace]
|
||||
|
||||
if (!_namespace || !_namespace.registered) {
|
||||
if (!hub && (!_namespace || !_namespace.registered)) {
|
||||
throw new Error(`cannot get resource: ${namespace} not registered`)
|
||||
}
|
||||
|
||||
const { _token: token } = await this._getNamespaceCatalog(namespace)
|
||||
const { _token: token } = await this._getNamespaceCatalog({
|
||||
hub,
|
||||
namespace,
|
||||
})
|
||||
|
||||
// 2018-03-20 Extra check: getResourceDownloadToken seems to be called without a token in some cases
|
||||
if (token === undefined) {
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
"node": ">=6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@xen-orchestra/cron": "^1.0.3",
|
||||
"@xen-orchestra/cron": "^1.0.4",
|
||||
"lodash": "^4.16.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
"node": ">=6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@xen-orchestra/cron": "^1.0.3",
|
||||
"@xen-orchestra/cron": "^1.0.4",
|
||||
"d3-time-format": "^2.1.1",
|
||||
"json5": "^2.0.1",
|
||||
"lodash": "^4.17.4"
|
||||
@@ -33,7 +33,7 @@
|
||||
"@babel/preset-flow": "^7.0.0",
|
||||
"babel-plugin-lodash": "^3.3.2",
|
||||
"cross-env": "^5.1.3",
|
||||
"rimraf": "^2.6.2"
|
||||
"rimraf": "^3.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
|
||||
|
||||
@@ -6,29 +6,9 @@ XO Server plugin that allows the creation of pool-wide and cross-pool private ne
|
||||
|
||||
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
|
||||
## Documentation
|
||||
|
||||
### Network creation
|
||||
|
||||
In the network creation view:
|
||||
- Select a `pool` and `Private network`
|
||||
- Select on which interface to create the network's tunnels
|
||||
- Select other pools to add them to the network if wanted
|
||||
- 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.
|
||||
Please see the plugin's [official documentation](https://xen-orchestra.com/docs/sdn_controller.html).
|
||||
|
||||
## Contributions
|
||||
|
||||
|
||||
@@ -15,13 +15,14 @@
|
||||
"predev": "yarn run prebuild",
|
||||
"prepublishOnly": "yarn run build"
|
||||
},
|
||||
"version": "0.2.0",
|
||||
"version": "0.3.0",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.4.4",
|
||||
"@babel/core": "^7.4.4",
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.4.4",
|
||||
"@babel/plugin-proposal-optional-chaining": "^7.2.0",
|
||||
"@babel/preset-env": "^7.4.4",
|
||||
"cross-env": "^5.2.0"
|
||||
@@ -29,7 +30,7 @@
|
||||
"dependencies": {
|
||||
"@xen-orchestra/log": "^0.1.4",
|
||||
"lodash": "^4.17.11",
|
||||
"node-openssl-cert": "^0.0.84",
|
||||
"node-openssl-cert": "^0.0.97",
|
||||
"promise-toolbox": "^0.13.0",
|
||||
"uuid": "^3.3.2"
|
||||
},
|
||||
|
||||
@@ -4,7 +4,7 @@ import NodeOpenssl from 'node-openssl-cert'
|
||||
import uuidv4 from 'uuid/v4'
|
||||
import { access, constants, readFile, writeFile } from 'fs'
|
||||
import { EventEmitter } from 'events'
|
||||
import { filter, find, forEach, forOwn, map, omitBy } from 'lodash'
|
||||
import { filter, find, forOwn, map, omitBy, sample } from 'lodash'
|
||||
import { fromCallback, fromEvent } from 'promise-toolbox'
|
||||
import { join } from 'path'
|
||||
|
||||
@@ -60,7 +60,7 @@ async function fileExists(path) {
|
||||
try {
|
||||
await fromCallback(access, path, constants.F_OK)
|
||||
} catch (error) {
|
||||
if (error.code === 'ENOENT') {
|
||||
if (error?.code === 'ENOENT') {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -70,9 +70,67 @@ async function fileExists(path) {
|
||||
return true
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
// 2019-09-03
|
||||
// Compatibility code, to be removed in 1 year.
|
||||
function updateNetworkOtherConfig(network) {
|
||||
return Promise.all(
|
||||
map(
|
||||
{
|
||||
'cross-pool-network-uuid': 'cross_pool_network_uuid',
|
||||
encapsulation: 'encapsulation',
|
||||
'pif-device': 'pif_device',
|
||||
'private-pool-wide': 'private_pool_wide',
|
||||
vni: 'vni',
|
||||
},
|
||||
(oldKey, newKey) => {
|
||||
const namespacedKey = `xo:sdn-controller:${newKey}`
|
||||
if (network.other_config[namespacedKey] !== undefined) {
|
||||
// Nothing to do the update has been done already
|
||||
return
|
||||
}
|
||||
|
||||
const value = network.other_config[oldKey]
|
||||
if (value !== undefined) {
|
||||
return network.update_other_config({
|
||||
[oldKey]: null,
|
||||
[namespacedKey]: value,
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
function createPassword() {
|
||||
const chars =
|
||||
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789?!'
|
||||
return Array.from({ length: 16 }, _ => sample(chars)).join('')
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
|
||||
class SDNController extends EventEmitter {
|
||||
/*
|
||||
Attributes on created networks:
|
||||
- `other_config`:
|
||||
- `xo:sdn-controller:encapsulation` : encapsulation protocol used for tunneling (either `gre` or `vxlan`)
|
||||
- `xo:sdn-controller:encrypted` : `true` if the network is encrypted
|
||||
- `xo:sdn-controller:pif-device` : PIF device on which the tunnels are created, must be physical and have an IP configuration
|
||||
- `xo:sdn-controller:private-pool-wide`: `true` if the network is created (and so must be managed) by a SDN Controller
|
||||
- `xo:sdn-controller:vni` : VxLAN Network Identifier,
|
||||
it is used by OpenVSwitch to route traffic of different networks in a single tunnel
|
||||
See: https://tools.ietf.org/html/rfc7348
|
||||
|
||||
Attributes on created tunnels: See: https://xapi-project.github.io/xapi/design/tunnelling.html
|
||||
- `status`:
|
||||
- `active`: `true` if the corresponding OpenVSwitch bridge is correctly configured and working
|
||||
- `key` : Corresponding OpenVSwitch bridge name (missing if `active` is `false`)
|
||||
*/
|
||||
|
||||
constructor({ xo, getDataDir }) {
|
||||
super()
|
||||
|
||||
@@ -94,9 +152,6 @@ class SDNController extends EventEmitter {
|
||||
|
||||
this._overrideCerts = false
|
||||
|
||||
// VNI: VxLAN Network Identifier, it is used by OpenVSwitch
|
||||
// to route traffic of different networks in a single tunnel.
|
||||
// See: https://tools.ietf.org/html/rfc7348
|
||||
this._prevVni = 0
|
||||
}
|
||||
|
||||
@@ -150,7 +205,14 @@ class SDNController extends EventEmitter {
|
||||
|
||||
async load() {
|
||||
// Expose method to create pool-wide private network
|
||||
const createPrivateNetwork = this._createPrivateNetwork.bind(this)
|
||||
const createPrivateNetwork = params =>
|
||||
this._createPrivateNetwork({
|
||||
encrypted: false,
|
||||
mtu: 0,
|
||||
...params,
|
||||
vni: ++this._prevVni,
|
||||
})
|
||||
|
||||
createPrivateNetwork.description =
|
||||
'Creates a pool-wide private network on a selected pool'
|
||||
createPrivateNetwork.params = {
|
||||
@@ -159,6 +221,8 @@ class SDNController extends EventEmitter {
|
||||
networkDescription: { type: 'string' },
|
||||
encapsulation: { type: 'string' },
|
||||
pifId: { type: 'string' },
|
||||
encrypted: { type: 'boolean', optional: true },
|
||||
mtu: { type: 'integer', optional: true },
|
||||
}
|
||||
createPrivateNetwork.resolve = {
|
||||
xoPool: ['poolId', 'pool', ''],
|
||||
@@ -166,9 +230,9 @@ class SDNController extends EventEmitter {
|
||||
}
|
||||
|
||||
// Expose method to create cross-pool private network
|
||||
const createCrossPoolPrivateNetwork = this._createCrossPoolPrivateNetwork.bind(
|
||||
this
|
||||
)
|
||||
const createCrossPoolPrivateNetwork = params =>
|
||||
this._createCrossPoolPrivateNetwork({ encrypted: false, mtu: 0, ...params })
|
||||
|
||||
createCrossPoolPrivateNetwork.description =
|
||||
'Creates a cross-pool private network on selected pools'
|
||||
createCrossPoolPrivateNetwork.params = {
|
||||
@@ -187,6 +251,8 @@ class SDNController extends EventEmitter {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
encrypted: { type: 'boolean', optional: true },
|
||||
mtu: { type: 'integer', optional: true },
|
||||
}
|
||||
|
||||
this._unsetApiMethods = this._xo.addApiMethods({
|
||||
@@ -217,11 +283,17 @@ class SDNController extends EventEmitter {
|
||||
const noVniNetworks = []
|
||||
await Promise.all(
|
||||
map(networks, async network => {
|
||||
if (network.other_config.private_pool_wide !== 'true') {
|
||||
// 2019-09-03
|
||||
// Compatibility code, to be removed in 1 year.
|
||||
await updateNetworkOtherConfig(network)
|
||||
network = await network.$xapi.barrier(network.$ref)
|
||||
|
||||
const otherConfig = network.other_config
|
||||
if (otherConfig['xo:sdn-controller:private-pool-wide'] !== 'true') {
|
||||
return
|
||||
}
|
||||
|
||||
const { vni } = network.other_config
|
||||
const vni = otherConfig['xo:sdn-controller:vni']
|
||||
if (vni === undefined) {
|
||||
noVniNetworks.push(network)
|
||||
} else {
|
||||
@@ -239,10 +311,13 @@ class SDNController extends EventEmitter {
|
||||
// 2019-08-22
|
||||
// This is used to add the pif_device to networks created before this version. (v0.1.2)
|
||||
// This will be removed in 1 year.
|
||||
if (network.other_config.pif_device === undefined) {
|
||||
if (otherConfig['xo:sdn-controller:pif-device'] === undefined) {
|
||||
const tunnel = this._getHostTunnelForNetwork(center, network.$ref)
|
||||
const pif = xapi.getObjectByRef(tunnel.transport_PIF)
|
||||
await network.update_other_config('pif_device', pif.device)
|
||||
await network.update_other_config(
|
||||
'xo:sdn-controller:pif-device',
|
||||
pif.device
|
||||
)
|
||||
}
|
||||
|
||||
this._poolNetworks.push({
|
||||
@@ -256,7 +331,7 @@ class SDNController extends EventEmitter {
|
||||
}
|
||||
|
||||
const crossPoolNetworkUuid =
|
||||
network.other_config.cross_pool_network_uuid
|
||||
otherConfig['xo:sdn-controller:cross-pool-network-uuid']
|
||||
if (crossPoolNetworkUuid !== undefined) {
|
||||
let crossPoolNetwork = this._crossPoolNetworks[
|
||||
crossPoolNetworkUuid
|
||||
@@ -291,7 +366,10 @@ class SDNController extends EventEmitter {
|
||||
// This will be removed in 1 year.
|
||||
await Promise.all(
|
||||
map(noVniNetworks, async network => {
|
||||
await network.update_other_config('vni', String(++this._prevVni))
|
||||
await network.update_other_config(
|
||||
'xo:sdn-controller:vni',
|
||||
String(++this._prevVni)
|
||||
)
|
||||
|
||||
// Re-elect a center to apply the VNI
|
||||
const center = await this._electNewCenter(network, true)
|
||||
@@ -333,6 +411,9 @@ class SDNController extends EventEmitter {
|
||||
networkDescription,
|
||||
encapsulation,
|
||||
xoPif,
|
||||
vni,
|
||||
encrypted,
|
||||
mtu,
|
||||
}) {
|
||||
const pool = this._xo.getXapiObject(xoPool)
|
||||
await this._setPoolControllerIfNeeded(pool)
|
||||
@@ -343,13 +424,16 @@ class SDNController extends EventEmitter {
|
||||
const privateNetworkRef = await pool.$xapi.call('network.create', {
|
||||
name_label: networkName,
|
||||
name_description: networkDescription,
|
||||
MTU: 0,
|
||||
MTU: mtu,
|
||||
other_config: {
|
||||
// Set `automatic` to false so XenCenter does not get confused
|
||||
// See: https://citrix.github.io/xenserver-sdk/#network
|
||||
automatic: 'false',
|
||||
private_pool_wide: 'true',
|
||||
encapsulation: encapsulation,
|
||||
pif_device: pif.device,
|
||||
vni: String(++this._prevVni),
|
||||
'xo:sdn-controller:encapsulation': encapsulation,
|
||||
'xo:sdn-controller:encrypted': encrypted ? 'true' : undefined,
|
||||
'xo:sdn-controller:pif-device': pif.device,
|
||||
'xo:sdn-controller:private-pool-wide': 'true',
|
||||
'xo:sdn-controller:vni': String(vni),
|
||||
},
|
||||
})
|
||||
|
||||
@@ -390,6 +474,8 @@ class SDNController extends EventEmitter {
|
||||
networkDescription,
|
||||
encapsulation,
|
||||
xoPifIds,
|
||||
encrypted,
|
||||
mtu,
|
||||
}) {
|
||||
const uuid = uuidv4()
|
||||
const crossPoolNetwork = {
|
||||
@@ -400,6 +486,7 @@ class SDNController extends EventEmitter {
|
||||
|
||||
log.debug('New cross-pool network created', { uuid })
|
||||
|
||||
const vni = ++this._prevVni
|
||||
for (const xoPoolId of xoPoolIds) {
|
||||
const xoPool = this._xo.getObject(xoPoolId, 'pool')
|
||||
const pool = this._xo.getXapiObject(xoPool)
|
||||
@@ -416,10 +503,16 @@ class SDNController extends EventEmitter {
|
||||
networkDescription,
|
||||
encapsulation,
|
||||
xoPif,
|
||||
vni,
|
||||
encrypted,
|
||||
mtu,
|
||||
})
|
||||
|
||||
const network = pool.$xapi.getObjectByRef(poolNetwork.network)
|
||||
await network.update_other_config('cross_pool_network_uuid', uuid)
|
||||
await network.update_other_config(
|
||||
'xo:sdn-controller:cross-pool-network-uuid',
|
||||
uuid
|
||||
)
|
||||
|
||||
crossPoolNetwork.pools.push(poolNetwork.pool)
|
||||
crossPoolNetwork.networks.push(poolNetwork.network)
|
||||
@@ -454,7 +547,7 @@ class SDNController extends EventEmitter {
|
||||
}
|
||||
|
||||
_objectsAdded(objects) {
|
||||
forEach(objects, object => {
|
||||
forOwn(objects, object => {
|
||||
const { $type } = object
|
||||
|
||||
if ($type === 'host') {
|
||||
@@ -472,26 +565,28 @@ class SDNController extends EventEmitter {
|
||||
}
|
||||
|
||||
_objectsUpdated(objects) {
|
||||
return Promise.all(
|
||||
map(objects, object => {
|
||||
forOwn(objects, async object => {
|
||||
try {
|
||||
const { $type } = object
|
||||
|
||||
if ($type === 'PIF') {
|
||||
return this._pifUpdated(object)
|
||||
await this._pifUpdated(object)
|
||||
} else if ($type === 'host') {
|
||||
await this._hostUpdated(object)
|
||||
} else if ($type === 'host_metrics') {
|
||||
await this._hostMetricsUpdated(object)
|
||||
}
|
||||
if ($type === 'host') {
|
||||
return this._hostUpdated(object)
|
||||
}
|
||||
if ($type === 'host_metrics') {
|
||||
return this._hostMetricsUpdated(object)
|
||||
}
|
||||
})
|
||||
)
|
||||
} catch (error) {
|
||||
log.error('Error in _objectsUpdated', {
|
||||
error,
|
||||
object,
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
_objectsRemoved(xapi, objects) {
|
||||
return Promise.all(
|
||||
map(objects, async (object, id) => {
|
||||
forOwn(objects, async (object, id) => {
|
||||
try {
|
||||
this._ovsdbClients = this._ovsdbClients.filter(
|
||||
client => client.host.$id !== id
|
||||
)
|
||||
@@ -533,8 +628,13 @@ class SDNController extends EventEmitter {
|
||||
crossPoolNetwork => crossPoolNetwork.networks.length === 0
|
||||
)
|
||||
}
|
||||
})
|
||||
)
|
||||
} catch (error) {
|
||||
log.error('Error in _objectsRemoved', {
|
||||
error,
|
||||
object,
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async _pifUpdated(pif) {
|
||||
@@ -666,11 +766,12 @@ class SDNController extends EventEmitter {
|
||||
}
|
||||
|
||||
const network = host.$xapi.getObjectByRef(poolNetwork.network)
|
||||
const pifDevice = network.other_config.pif_device || 'eth0'
|
||||
this._createTunnel(host, network, pifDevice)
|
||||
const pifDevice =
|
||||
network.other_config['xo:sdn-controller:pif-device'] ?? 'eth0'
|
||||
await this._createTunnel(host, network, pifDevice)
|
||||
}
|
||||
|
||||
this._addHostToPoolNetworks(host)
|
||||
await this._addHostToPoolNetworks(host)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1054,7 +1155,15 @@ class SDNController extends EventEmitter {
|
||||
const network = client.host.$xapi.getObjectByRef(poolNetwork.network)
|
||||
|
||||
// Use centerNetwork VNI by convention
|
||||
const { encapsulation = 'gre', vni = '0' } = centerNetwork.other_config
|
||||
const otherConfig = centerNetwork.other_config
|
||||
const encapsulation =
|
||||
otherConfig['xo:sdn-controller:encapsulation'] ?? 'gre'
|
||||
const vni = otherConfig['xo:sdn-controller:vni'] ?? '0'
|
||||
const password =
|
||||
otherConfig['xo:sdn-controller:encrypted'] === 'true'
|
||||
? createPassword()
|
||||
: undefined
|
||||
|
||||
try {
|
||||
await Promise.all([
|
||||
client.addInterfaceAndPort(
|
||||
@@ -1063,6 +1172,7 @@ class SDNController extends EventEmitter {
|
||||
centerClient.host.address,
|
||||
encapsulation,
|
||||
vni,
|
||||
password,
|
||||
centerNetwork.uuid
|
||||
),
|
||||
centerClient.addInterfaceAndPort(
|
||||
@@ -1071,6 +1181,7 @@ class SDNController extends EventEmitter {
|
||||
client.host.address,
|
||||
encapsulation,
|
||||
vni,
|
||||
password,
|
||||
network.uuid
|
||||
),
|
||||
])
|
||||
@@ -1105,6 +1216,15 @@ class SDNController extends EventEmitter {
|
||||
}
|
||||
|
||||
const tunnel = this._getHostTunnelForNetwork(host, network.$ref)
|
||||
if (tunnel === undefined) {
|
||||
log.info('Unable to add host to network: no tunnel available', {
|
||||
network: network.name_label,
|
||||
host: host.name_label,
|
||||
pool: host.$pool.name_label,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const starCenterTunnel = this._getHostTunnelForNetwork(
|
||||
starCenter,
|
||||
network.$ref
|
||||
@@ -1135,7 +1255,16 @@ class SDNController extends EventEmitter {
|
||||
return
|
||||
}
|
||||
|
||||
const { encapsulation = 'gre', vni = '0' } = network.other_config
|
||||
const otherConfig = network.other_config
|
||||
const encapsulation =
|
||||
otherConfig['xo:sdn-controller:encapsulation'] ?? 'gre'
|
||||
const vni = otherConfig['xo:sdn-controller:vni'] ?? '0'
|
||||
|
||||
const password =
|
||||
otherConfig['xo:sdn-controller:encrypted'] === 'true'
|
||||
? createPassword()
|
||||
: undefined
|
||||
|
||||
let bridgeName
|
||||
try {
|
||||
;[bridgeName] = await Promise.all([
|
||||
@@ -1144,14 +1273,16 @@ class SDNController extends EventEmitter {
|
||||
network.name_label,
|
||||
starCenterClient.host.address,
|
||||
encapsulation,
|
||||
vni
|
||||
vni,
|
||||
password
|
||||
),
|
||||
starCenterClient.addInterfaceAndPort(
|
||||
network.uuid,
|
||||
network.name_label,
|
||||
hostClient.host.address,
|
||||
encapsulation,
|
||||
vni
|
||||
vni,
|
||||
password
|
||||
),
|
||||
])
|
||||
} catch (error) {
|
||||
@@ -1161,6 +1292,7 @@ class SDNController extends EventEmitter {
|
||||
host: host.name_label,
|
||||
pool: host.$pool.name_label,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (bridgeName !== undefined) {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
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'
|
||||
import { forOwn, toPairs } from 'lodash'
|
||||
|
||||
const log = createLogger('xo:xo-server:sdn-controller:ovsdb-client')
|
||||
|
||||
@@ -10,7 +10,33 @@ const OVSDB_PORT = 6640
|
||||
|
||||
// =============================================================================
|
||||
|
||||
function toMap(object) {
|
||||
return ['map', toPairs(object)]
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
|
||||
export class OvsdbClient {
|
||||
/*
|
||||
Create an SSL connection to an XCP-ng host.
|
||||
Interact with the host's OpenVSwitch (OVS) daemon to create and manage the virtual bridges
|
||||
corresponding to the private networks with OVSDB (OpenVSwitch DataBase) Protocol.
|
||||
See:
|
||||
- OVSDB Protocol: https://tools.ietf.org/html/rfc7047
|
||||
- OVS Tunneling : http://docs.openvswitch.org/en/latest/howto/tunneling/
|
||||
- OVS IPSEC : http://docs.openvswitch.org/en/latest/howto/ipsec/
|
||||
|
||||
Attributes on created OVS ports (corresponds to a XAPI `PIF` or `VIF`):
|
||||
- `other_config`:
|
||||
- `xo:sdn-controller:cross-pool` : UUID of the remote network connected by the tunnel
|
||||
- `xo:sdn-controller:private-pool-wide`: `true` if created (and managed) by a SDN Controller
|
||||
|
||||
Attributes on created OVS interfaces:
|
||||
- `options`:
|
||||
- `key` : Network's VNI
|
||||
- `remote_ip`: Remote IP of the tunnel
|
||||
*/
|
||||
|
||||
constructor(host, clientKey, clientCert, caCert) {
|
||||
this._numberOfPortAndInterface = 0
|
||||
this._requestId = 0
|
||||
@@ -46,6 +72,7 @@ export class OvsdbClient {
|
||||
remoteAddress,
|
||||
encapsulation,
|
||||
key,
|
||||
password,
|
||||
remoteNetwork
|
||||
) {
|
||||
if (
|
||||
@@ -91,33 +118,36 @@ export class OvsdbClient {
|
||||
const portName = bridgeName + '_port' + index
|
||||
|
||||
// Add interface and port to the bridge
|
||||
const options = ['map', [['remote_ip', remoteAddress], ['key', key]]]
|
||||
const otherConfig =
|
||||
remoteNetwork !== undefined
|
||||
? ['map', [['cross_pool', remoteNetwork]]]
|
||||
: ['map', [['private_pool_wide', 'true']]]
|
||||
|
||||
const options = { remote_ip: remoteAddress, key: key }
|
||||
if (password !== undefined) {
|
||||
options.psk = password
|
||||
}
|
||||
const addInterfaceOperation = {
|
||||
op: 'insert',
|
||||
table: 'Interface',
|
||||
row: {
|
||||
type: encapsulation,
|
||||
options: options,
|
||||
options: toMap(options),
|
||||
name: interfaceName,
|
||||
other_config: otherConfig,
|
||||
},
|
||||
'uuid-name': 'new_iface',
|
||||
}
|
||||
|
||||
const addPortOperation = {
|
||||
op: 'insert',
|
||||
table: 'Port',
|
||||
row: {
|
||||
name: portName,
|
||||
interfaces: ['set', [['named-uuid', 'new_iface']]],
|
||||
other_config: otherConfig,
|
||||
other_config: toMap(
|
||||
remoteNetwork !== undefined
|
||||
? { 'xo:sdn-controller:cross-pool': remoteNetwork }
|
||||
: { 'xo:sdn-controller:private-pool-wide': 'true' }
|
||||
),
|
||||
},
|
||||
'uuid-name': 'new_port',
|
||||
}
|
||||
|
||||
const mutateBridgeOperation = {
|
||||
op: 'mutate',
|
||||
table: 'Bridge',
|
||||
@@ -217,12 +247,20 @@ export class OvsdbClient {
|
||||
}
|
||||
|
||||
forOwn(selectResult.other_config[1], config => {
|
||||
const shouldDelete =
|
||||
// 2019-09-03
|
||||
// Compatibility code, to be removed in 1 year.
|
||||
const oldShouldDelete =
|
||||
(config[0] === 'private_pool_wide' && !crossPoolOnly) ||
|
||||
(config[0] === 'cross_pool' &&
|
||||
(remoteNetwork === undefined || remoteNetwork === config[1]))
|
||||
|
||||
if (shouldDelete) {
|
||||
const shouldDelete =
|
||||
(config[0] === 'xo:sdn-controller:private-pool-wide' &&
|
||||
!crossPoolOnly) ||
|
||||
(config[0] === 'xo:sdn-controller:cross-pool' &&
|
||||
(remoteNetwork === undefined || remoteNetwork === config[1]))
|
||||
|
||||
if (shouldDelete || oldShouldDelete) {
|
||||
portsToDelete.push(['uuid', portUuid])
|
||||
}
|
||||
})
|
||||
@@ -299,11 +337,7 @@ export class OvsdbClient {
|
||||
|
||||
async _getBridgeUuidForNetwork(networkUuid, networkName, socket) {
|
||||
const where = [
|
||||
[
|
||||
'external_ids',
|
||||
'includes',
|
||||
['map', [['xs-network-uuids', networkUuid]]],
|
||||
],
|
||||
['external_ids', 'includes', toMap({ 'xs-network-uuids': networkUuid })],
|
||||
]
|
||||
const selectResult = await this._select(
|
||||
'Bridge',
|
||||
|
||||
@@ -15,15 +15,20 @@ src
|
||||
| | └─ index.spec.js.snap
|
||||
| └─ index.spec.js
|
||||
├─ job
|
||||
| └─ index.spec.js
|
||||
├─ issues
|
||||
¦ └─ index.spec.js
|
||||
¦
|
||||
¦
|
||||
├─ _xoConnection.js
|
||||
└─ util.js
|
||||
```
|
||||
|
||||
The tests can describe xo methods or scenarios:
|
||||
```javascript
|
||||
The tests can describe:
|
||||
|
||||
- XO methods or scenarios:
|
||||
|
||||
`src/user/index.js`
|
||||
```js
|
||||
import xo from "../_xoConnection";
|
||||
|
||||
describe("user", () => {
|
||||
@@ -46,6 +51,16 @@ describe("user", () => {
|
||||
});
|
||||
|
||||
});
|
||||
```
|
||||
- issues
|
||||
|
||||
`src/issues/index.js`
|
||||
```js
|
||||
describe("issue", () => {
|
||||
test("5454", () => {
|
||||
/* some tests */
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### Best practices
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
email = ''
|
||||
password = ''
|
||||
|
||||
[pools]
|
||||
default = ''
|
||||
|
||||
[servers]
|
||||
[servers.default]
|
||||
username = ''
|
||||
|
||||
@@ -87,7 +87,7 @@ class XoConnection extends Xo {
|
||||
while (true) {
|
||||
try {
|
||||
await predicate(obj)
|
||||
return
|
||||
return obj
|
||||
} catch (_) {}
|
||||
// If failed, wait for next object state/update and retry.
|
||||
obj = await this.waitObject(id)
|
||||
@@ -116,13 +116,26 @@ class XoConnection extends Xo {
|
||||
return job
|
||||
}
|
||||
|
||||
async createTempNetwork(params) {
|
||||
const id = await this.call('network.create', {
|
||||
name: 'XO Test',
|
||||
pool: config.pools.default,
|
||||
...params,
|
||||
})
|
||||
this._tempResourceDisposers.push('network.delete', { id })
|
||||
return this.getOrWaitObject(id)
|
||||
}
|
||||
|
||||
async createTempVm(params) {
|
||||
const id = await this.call('vm.create', params)
|
||||
const id = await this.call('vm.create', {
|
||||
name_label: 'XO Test',
|
||||
template: config.templates.templateWithoutDisks,
|
||||
...params,
|
||||
})
|
||||
this._tempResourceDisposers.push('vm.delete', { id })
|
||||
await this.waitObjectState(id, vm => {
|
||||
return this.waitObjectState(id, vm => {
|
||||
if (vm.type !== 'VM') throw new Error('retry')
|
||||
})
|
||||
return id
|
||||
}
|
||||
|
||||
async createTempRemote(params) {
|
||||
@@ -197,6 +210,10 @@ class XoConnection extends Xo {
|
||||
return backups
|
||||
}
|
||||
|
||||
getBackupLogs(filter) {
|
||||
return this.call('backupNg.getLogs', { _forceRefresh: true, ...filter })
|
||||
}
|
||||
|
||||
async _cleanDisposers(disposers) {
|
||||
for (let n = disposers.length - 1; n > 0; ) {
|
||||
const params = disposers[n--]
|
||||
|
||||
@@ -66,6 +66,10 @@ const validateOperationTask = (task, props) => {
|
||||
})
|
||||
}
|
||||
|
||||
// Note: `bypassVdiChainsCheck` must be enabled because the XAPI might be not
|
||||
// able to coalesce VDIs as fast as the tests run.
|
||||
//
|
||||
// See https://xen-orchestra.com/docs/backup_troubleshooting.html#vdi-chain-protection
|
||||
describe('backupNg', () => {
|
||||
let defaultBackupNg
|
||||
|
||||
@@ -217,7 +221,7 @@ describe('backupNg', () => {
|
||||
expect(typeof schedule).toBe('object')
|
||||
|
||||
await xo.call('backupNg.runJob', { id: jobId, schedule: schedule.id })
|
||||
const [log] = await xo.call('backupNg.getLogs', {
|
||||
const [log] = await xo.getBackupLogs({
|
||||
scheduleId: schedule.id,
|
||||
})
|
||||
expect(log.warnings).toMatchSnapshot()
|
||||
@@ -226,7 +230,7 @@ describe('backupNg', () => {
|
||||
it('fails trying to run a backup job with a VM without disks', async () => {
|
||||
jest.setTimeout(8e3)
|
||||
await xo.createTempServer(config.servers.default)
|
||||
const vmIdWithoutDisks = await xo.createTempVm({
|
||||
const { id: vmIdWithoutDisks } = await xo.createTempVm({
|
||||
name_label: 'XO Test Without Disks',
|
||||
name_description: 'Creating a vm without disks',
|
||||
template: config.templates.templateWithoutDisks,
|
||||
@@ -256,7 +260,7 @@ describe('backupNg', () => {
|
||||
tasks: [vmTask],
|
||||
...log
|
||||
},
|
||||
] = await xo.call('backupNg.getLogs', {
|
||||
] = await xo.getBackupLogs({
|
||||
jobId,
|
||||
scheduleId: schedule.id,
|
||||
})
|
||||
@@ -315,7 +319,7 @@ describe('backupNg', () => {
|
||||
tasks: [task],
|
||||
...log
|
||||
},
|
||||
] = await xo.call('backupNg.getLogs', {
|
||||
] = await xo.getBackupLogs({
|
||||
jobId,
|
||||
scheduleId: schedule.id,
|
||||
})
|
||||
@@ -347,7 +351,7 @@ describe('backupNg', () => {
|
||||
test('execute three times a rolling snapshot with 2 as retention & revert to an old state', async () => {
|
||||
jest.setTimeout(6e4)
|
||||
await xo.createTempServer(config.servers.default)
|
||||
const vmId = await xo.createTempVm({
|
||||
let vm = await xo.createTempVm({
|
||||
name_label: 'XO Test Temp',
|
||||
name_description: 'Creating a temporary vm',
|
||||
template: config.templates.default,
|
||||
@@ -364,45 +368,46 @@ describe('backupNg', () => {
|
||||
const { id: jobId } = await xo.createTempBackupNgJob({
|
||||
...defaultBackupNg,
|
||||
vms: {
|
||||
id: vmId,
|
||||
id: vm.id,
|
||||
},
|
||||
schedules: {
|
||||
[scheduleTempId]: DEFAULT_SCHEDULE,
|
||||
},
|
||||
settings: {
|
||||
...defaultBackupNg.settings,
|
||||
'': {
|
||||
bypassVdiChainsCheck: true,
|
||||
reportWhen: 'never',
|
||||
},
|
||||
[scheduleTempId]: { snapshotRetention: 2 },
|
||||
},
|
||||
})
|
||||
|
||||
const schedule = await xo.getSchedule({ jobId })
|
||||
expect(typeof schedule).toBe('object')
|
||||
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const oldSnapshots = xo.objects.all[vmId].snapshots
|
||||
await xo.call('backupNg.runJob', { id: jobId, schedule: schedule.id })
|
||||
await xo.waitObjectState(vmId, ({ snapshots }) => {
|
||||
vm = await xo.waitObjectState(vm.id, ({ snapshots }) => {
|
||||
// Test on updating snapshots.
|
||||
expect(snapshots).not.toEqual(oldSnapshots)
|
||||
expect(snapshots).not.toEqual(vm.snapshots)
|
||||
})
|
||||
}
|
||||
|
||||
const { snapshots, videoram: oldVideoram } = xo.objects.all[vmId]
|
||||
|
||||
// Test on the retention, how many snapshots should be saved.
|
||||
expect(snapshots.length).toBe(2)
|
||||
expect(vm.snapshots.length).toBe(2)
|
||||
|
||||
const newVideoram = 16
|
||||
await xo.call('vm.set', { id: vmId, videoram: newVideoram })
|
||||
await xo.waitObjectState(vmId, ({ videoram }) => {
|
||||
await xo.call('vm.set', { id: vm.id, videoram: newVideoram })
|
||||
await xo.waitObjectState(vm.id, ({ videoram }) => {
|
||||
expect(videoram).toBe(newVideoram.toString())
|
||||
})
|
||||
|
||||
await xo.call('vm.revert', {
|
||||
snapshot: snapshots[0],
|
||||
snapshot: vm.snapshots[0],
|
||||
})
|
||||
|
||||
await xo.waitObjectState(vmId, ({ videoram }) => {
|
||||
expect(videoram).toBe(oldVideoram)
|
||||
await xo.waitObjectState(vm.id, ({ videoram }) => {
|
||||
expect(videoram).toBe(vm.videoram)
|
||||
})
|
||||
|
||||
const [
|
||||
@@ -410,7 +415,7 @@ describe('backupNg', () => {
|
||||
tasks: [{ tasks: subTasks, ...vmTask }],
|
||||
...log
|
||||
},
|
||||
] = await xo.call('backupNg.getLogs', {
|
||||
] = await xo.getBackupLogs({
|
||||
jobId,
|
||||
scheduleId: schedule.id,
|
||||
})
|
||||
@@ -442,7 +447,7 @@ describe('backupNg', () => {
|
||||
message: expect.any(String),
|
||||
start: expect.any(Number),
|
||||
})
|
||||
expect(vmTask.data.id).toBe(vmId)
|
||||
expect(vmTask.data.id).toBe(vm.id)
|
||||
})
|
||||
|
||||
test('execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval', async () => {
|
||||
@@ -477,8 +482,9 @@ describe('backupNg', () => {
|
||||
},
|
||||
settings: {
|
||||
'': {
|
||||
reportWhen: 'never',
|
||||
bypassVdiChainsCheck: true,
|
||||
fullInterval,
|
||||
reportWhen: 'never',
|
||||
},
|
||||
[remoteId1]: { deleteFirst: true },
|
||||
[scheduleTempId]: { exportRetention },
|
||||
@@ -500,7 +506,7 @@ describe('backupNg', () => {
|
||||
expect(backups.length).toBe(exportRetention)
|
||||
)
|
||||
|
||||
const backupLogs = await xo.call('backupNg.getLogs', {
|
||||
const backupLogs = await xo.getBackupLogs({
|
||||
jobId,
|
||||
scheduleId: schedule.id,
|
||||
})
|
||||
|
||||
51
packages/xo-server-test/src/issues/index.spec.js
Normal file
51
packages/xo-server-test/src/issues/index.spec.js
Normal file
@@ -0,0 +1,51 @@
|
||||
/* eslint-env jest */
|
||||
|
||||
import config from '../_config'
|
||||
import xo from '../_xoConnection'
|
||||
|
||||
describe('issue', () => {
|
||||
test('4507', async () => {
|
||||
await xo.createTempServer(config.servers.default)
|
||||
|
||||
const props = {
|
||||
coresPerSocket: 1,
|
||||
cpuCap: 1,
|
||||
}
|
||||
const vm = await xo.createTempVm(props)
|
||||
expect(vm).toMatchObject(props)
|
||||
|
||||
await xo.call('vm.set', {
|
||||
coresPerSocket: null,
|
||||
cpuCap: null,
|
||||
id: vm.id,
|
||||
})
|
||||
await xo.waitObjectState(vm.id, vm => {
|
||||
expect(vm.coresPerSocket).toBe(undefined)
|
||||
expect(vm.cpuCap).toBe(undefined)
|
||||
})
|
||||
})
|
||||
|
||||
test('4514', async () => {
|
||||
await xo.createTempServer(config.servers.default)
|
||||
|
||||
const oldName = 'Old XO Test name'
|
||||
const { id, name_label } = await xo.createTempNetwork({ name: oldName })
|
||||
expect(name_label).toBe(oldName)
|
||||
|
||||
const newName = 'New XO Test name'
|
||||
await xo.call('network.set', { id, name_label: newName })
|
||||
await xo.waitObjectState(id, ({ name_label }) => {
|
||||
expect(name_label).toBe(newName)
|
||||
})
|
||||
})
|
||||
|
||||
test('4523', async () => {
|
||||
const id = await xo.call('network.create', {
|
||||
name: 'XO Test',
|
||||
pool: config.pools.default,
|
||||
})
|
||||
expect(typeof id).toBe('string')
|
||||
|
||||
await xo.call('network.delete', { id })
|
||||
})
|
||||
})
|
||||
@@ -42,7 +42,7 @@
|
||||
"@babel/preset-env": "^7.0.0",
|
||||
"babel-plugin-lodash": "^3.3.2",
|
||||
"cross-env": "^5.1.3",
|
||||
"rimraf": "^2.6.1"
|
||||
"rimraf": "^3.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
"@babel/preset-env": "^7.0.0",
|
||||
"babel-preset-env": "^1.5.2",
|
||||
"cross-env": "^5.1.3",
|
||||
"rimraf": "^2.6.1"
|
||||
"rimraf": "^3.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
"@babel/core": "^7.0.0",
|
||||
"@babel/preset-env": "^7.0.0",
|
||||
"cross-env": "^5.1.3",
|
||||
"rimraf": "^2.5.4"
|
||||
"rimraf": "^3.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
"@babel/core": "^7.0.0",
|
||||
"@babel/preset-env": "^7.0.0",
|
||||
"cross-env": "^5.1.3",
|
||||
"rimraf": "^2.6.1"
|
||||
"rimraf": "^3.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@xen-orchestra/async-map": "^0.0.0",
|
||||
"@xen-orchestra/cron": "^1.0.3",
|
||||
"@xen-orchestra/cron": "^1.0.4",
|
||||
"@xen-orchestra/log": "^0.1.4",
|
||||
"handlebars": "^4.0.6",
|
||||
"html-minifier": "^4.0.0",
|
||||
@@ -50,7 +50,7 @@
|
||||
"@babel/preset-env": "^7.0.0",
|
||||
"babel-plugin-lodash": "^3.3.2",
|
||||
"cross-env": "^5.1.3",
|
||||
"rimraf": "^2.6.1"
|
||||
"rimraf": "^3.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "xo-server",
|
||||
"version": "5.48.0",
|
||||
"version": "5.50.0",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "Server part of Xen-Orchestra",
|
||||
"keywords": [
|
||||
@@ -35,7 +35,7 @@
|
||||
"dependencies": {
|
||||
"@iarna/toml": "^2.2.1",
|
||||
"@xen-orchestra/async-map": "^0.0.0",
|
||||
"@xen-orchestra/cron": "^1.0.3",
|
||||
"@xen-orchestra/cron": "^1.0.4",
|
||||
"@xen-orchestra/defined": "^0.0.0",
|
||||
"@xen-orchestra/emit-async": "^0.0.0",
|
||||
"@xen-orchestra/fs": "^0.10.1",
|
||||
@@ -46,6 +46,7 @@
|
||||
"archiver": "^3.0.0",
|
||||
"async-iterator-to-stream": "^1.0.1",
|
||||
"base64url": "^3.0.0",
|
||||
"bind-property-descriptor": "^1.0.0",
|
||||
"blocked": "^1.2.1",
|
||||
"bluebird": "^3.5.1",
|
||||
"body-parser": "^1.18.2",
|
||||
@@ -149,7 +150,7 @@
|
||||
"babel-plugin-transform-dev": "^2.0.1",
|
||||
"cross-env": "^5.1.3",
|
||||
"index-modules": "^0.3.0",
|
||||
"rimraf": "^2.6.2"
|
||||
"rimraf": "^3.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
|
||||
|
||||
34
packages/xo-server/src/_MultiKeyMap.spec.js
Normal file
34
packages/xo-server/src/_MultiKeyMap.spec.js
Normal file
@@ -0,0 +1,34 @@
|
||||
/* eslint-env jest */
|
||||
|
||||
import MultiKeyMap from './_MultiKeyMap'
|
||||
|
||||
describe('MultiKeyMap', () => {
|
||||
it('works', () => {
|
||||
const map = new MultiKeyMap()
|
||||
|
||||
const keys = [
|
||||
// null key
|
||||
[],
|
||||
// simple key
|
||||
['foo'],
|
||||
// composite key
|
||||
['foo', 'bar'],
|
||||
// reverse composite key
|
||||
['bar', 'foo'],
|
||||
]
|
||||
const values = keys.map(() => ({}))
|
||||
|
||||
// set all values first to make sure they are all stored and not only the
|
||||
// last one
|
||||
keys.forEach((key, i) => {
|
||||
map.set(key, values[i])
|
||||
})
|
||||
|
||||
keys.forEach((key, i) => {
|
||||
// copy the key to make sure the array itself is not the key
|
||||
expect(map.get(key.slice())).toBe(values[i])
|
||||
map.delete(key.slice())
|
||||
expect(map.get(key.slice())).toBe(undefined)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -16,13 +16,28 @@ function scheduleRemoveCacheEntry(keys, expires) {
|
||||
|
||||
const defaultKeyFn = () => []
|
||||
|
||||
const { slice } = Array.prototype
|
||||
|
||||
export const REMOVE_CACHE_ENTRY = {}
|
||||
|
||||
// debounce an async function so that all subsequent calls in a delay receive
|
||||
// the same result
|
||||
//
|
||||
// similar to `p-debounce` with `leading` set to `true` but with key support
|
||||
export default (fn, delay, keyFn = defaultKeyFn) => {
|
||||
//
|
||||
// - `delay`: number of milliseconds to cache the response, a function can be
|
||||
// passed to use a custom delay for a call based on its parameters
|
||||
export const debounceWithKey = (fn, delay, keyFn = defaultKeyFn) => {
|
||||
const cache = new MultiKeyMap()
|
||||
return function() {
|
||||
const delayFn = typeof delay === 'number' ? () => delay : delay
|
||||
return function(arg) {
|
||||
if (arg === REMOVE_CACHE_ENTRY) {
|
||||
return removeCacheEntry(
|
||||
cache,
|
||||
ensureArray(keyFn.apply(this, slice.call(arguments, 1)))
|
||||
)
|
||||
}
|
||||
|
||||
const keys = ensureArray(keyFn.apply(this, arguments))
|
||||
let promise = cache.get(keys)
|
||||
if (promise === undefined) {
|
||||
@@ -30,10 +45,15 @@ export default (fn, delay, keyFn = defaultKeyFn) => {
|
||||
const remove = scheduleRemoveCacheEntry.bind(
|
||||
cache,
|
||||
keys,
|
||||
Date.now() + delay
|
||||
Date.now() + delayFn.apply(this, arguments)
|
||||
)
|
||||
promise.then(remove, remove)
|
||||
}
|
||||
return promise
|
||||
}
|
||||
}
|
||||
|
||||
debounceWithKey.decorate = (...params) => (target, name, descriptor) => ({
|
||||
...descriptor,
|
||||
value: debounceWithKey(descriptor.value, ...params),
|
||||
})
|
||||
|
||||
29
packages/xo-server/src/_pDebounceWithKey.spec.js
Normal file
29
packages/xo-server/src/_pDebounceWithKey.spec.js
Normal file
@@ -0,0 +1,29 @@
|
||||
/* eslint-env jest */
|
||||
|
||||
import { debounceWithKey, REMOVE_CACHE_ENTRY } from './_pDebounceWithKey'
|
||||
|
||||
describe('REMOVE_CACHE_ENTRY', () => {
|
||||
it('clears the cache', async () => {
|
||||
let i = 0
|
||||
const debouncedFn = debounceWithKey(
|
||||
function() {
|
||||
return Promise.resolve(++i)
|
||||
},
|
||||
Infinity,
|
||||
id => id
|
||||
)
|
||||
|
||||
// not cached accross keys
|
||||
expect(await debouncedFn(1)).toBe(1)
|
||||
expect(await debouncedFn(2)).toBe(2)
|
||||
|
||||
// retrieve the already cached values
|
||||
expect(await debouncedFn(1)).toBe(1)
|
||||
expect(await debouncedFn(2)).toBe(2)
|
||||
|
||||
// an entry for a specific key can be removed
|
||||
debouncedFn(REMOVE_CACHE_ENTRY, 1)
|
||||
expect(await debouncedFn(1)).toBe(3)
|
||||
expect(await debouncedFn(2)).toBe(2)
|
||||
})
|
||||
})
|
||||
@@ -3,6 +3,7 @@ import { fromCallback } from 'promise-toolbox'
|
||||
import { pipeline } from 'readable-stream'
|
||||
|
||||
import createNdJsonStream from '../_createNdJsonStream'
|
||||
import { REMOVE_CACHE_ENTRY } from '../_pDebounceWithKey'
|
||||
import { safeDateFormat } from '../utils'
|
||||
|
||||
export function createJob({ schedules, ...job }) {
|
||||
@@ -184,7 +185,20 @@ getAllLogs.params = {
|
||||
ndjson: { type: 'boolean', optional: true },
|
||||
}
|
||||
|
||||
export function getLogs({ after, before, limit, ...filter }) {
|
||||
export function getLogs({
|
||||
after,
|
||||
before,
|
||||
limit,
|
||||
|
||||
// TODO: it's a temporary work-around which should be removed
|
||||
// when the consolidated logs will be stored in the DB
|
||||
_forceRefresh = false,
|
||||
|
||||
...filter
|
||||
}) {
|
||||
if (_forceRefresh) {
|
||||
this.getBackupNgLogs(REMOVE_CACHE_ENTRY)
|
||||
}
|
||||
return this.getBackupNgLogsSorted({ after, before, limit, filter })
|
||||
}
|
||||
|
||||
@@ -302,7 +316,7 @@ export async function fetchFiles(params) {
|
||||
filename += '.zip'
|
||||
|
||||
return this.registerHttpRequest(handleFetchFiles, params, {
|
||||
suffix: encodeURI(`/${filename}`),
|
||||
suffix: '/' + encodeURIComponent(filename),
|
||||
}).then(url => ({ $getFrom: url }))
|
||||
}
|
||||
|
||||
|
||||
@@ -93,7 +93,7 @@ export async function fetchFiles({ format = 'zip', ...params }) {
|
||||
handleFetchFiles,
|
||||
{ ...params, format },
|
||||
{
|
||||
suffix: encodeURI(`/${fileName}`),
|
||||
suffix: '/' + encodeURIComponent(fileName),
|
||||
}
|
||||
).then(url => ({ $getFrom: url }))
|
||||
}
|
||||
|
||||
@@ -221,12 +221,7 @@ emergencyShutdownHost.resolve = {
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export async function isHostServerTimeConsistent({ host }) {
|
||||
try {
|
||||
await this.getXapi(host).assertConsistentHostServerTime(host._xapiRef)
|
||||
return true
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
return this.getXapi(host).isHostServerTimeConsistent(host._xapiRef)
|
||||
}
|
||||
|
||||
isHostServerTimeConsistent.params = {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import xapiObjectToXo from '../xapi-object-to-xo'
|
||||
import { mapToArray } from '../utils'
|
||||
|
||||
export function getBondModes() {
|
||||
@@ -12,13 +13,15 @@ export async function create({
|
||||
mtu = 1500,
|
||||
vlan = 0,
|
||||
}) {
|
||||
return this.getXapi(pool).createNetwork({
|
||||
name,
|
||||
description,
|
||||
pifId: pif && this.getObject(pif, 'PIF')._xapiId,
|
||||
mtu: +mtu,
|
||||
vlan: +vlan,
|
||||
})
|
||||
return xapiObjectToXo(
|
||||
await this.getXapi(pool).createNetwork({
|
||||
name,
|
||||
description,
|
||||
pifId: pif && this.getObject(pif, 'PIF')._xapiId,
|
||||
mtu: +mtu,
|
||||
vlan: +vlan,
|
||||
})
|
||||
).id
|
||||
}
|
||||
|
||||
create.params = {
|
||||
@@ -116,6 +119,9 @@ set.params = {
|
||||
type: 'boolean',
|
||||
optional: true,
|
||||
},
|
||||
id: {
|
||||
type: 'string',
|
||||
},
|
||||
name_description: {
|
||||
type: 'string',
|
||||
optional: true,
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
unauthorized,
|
||||
} from 'xo-common/api-errors'
|
||||
|
||||
import { forEach, map, mapFilter, parseSize } from '../utils'
|
||||
import { forEach, map, mapFilter, parseSize, safeDateFormat } from '../utils'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@@ -1189,7 +1189,11 @@ async function export_({ vm, compress }) {
|
||||
|
||||
return {
|
||||
$getFrom: await this.registerHttpRequest(handleExport, data, {
|
||||
suffix: encodeURI(`/${vm.name_label}.xva`),
|
||||
suffix:
|
||||
'/' +
|
||||
encodeURIComponent(
|
||||
`${safeDateFormat(new Date())} - ${vm.name_label}.xva`
|
||||
),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -821,12 +821,14 @@ export const createSR = defer(async function(
|
||||
createSR.description = 'create gluster VM'
|
||||
createSR.permission = 'admin'
|
||||
createSR.params = {
|
||||
brickSize: { type: 'number', optional: true },
|
||||
srs: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
template: { type: 'object' },
|
||||
pif: {
|
||||
type: 'string',
|
||||
},
|
||||
@@ -1162,11 +1164,11 @@ async function _prepareGlusterVm(
|
||||
}
|
||||
|
||||
async function _importGlusterVM(xapi, template, lvmsrId) {
|
||||
const templateStream = await this.requestResource(
|
||||
'xosan',
|
||||
template.id,
|
||||
template.version
|
||||
)
|
||||
const templateStream = await this.requestResource({
|
||||
id: template.id,
|
||||
namespace: 'xosan',
|
||||
version: template.version,
|
||||
})
|
||||
const newVM = await xapi.importVm(templateStream, {
|
||||
srId: lvmsrId,
|
||||
type: 'xva',
|
||||
@@ -1533,8 +1535,11 @@ export async function downloadAndInstallXosanPack({ id, version, pool }) {
|
||||
}
|
||||
|
||||
const xapi = this.getXapi(pool.id)
|
||||
const res = await this.requestResource('xosan', id, version)
|
||||
|
||||
const res = await this.requestResource({
|
||||
id,
|
||||
namespace: 'xosan',
|
||||
version,
|
||||
})
|
||||
await xapi.installSupplementalPackOnAllHosts(res)
|
||||
await xapi.pool.update_other_config(
|
||||
'xosan_pack_installation_time',
|
||||
|
||||
@@ -486,7 +486,7 @@ const TRANSFORMS = {
|
||||
attached: Boolean(obj.currently_attached),
|
||||
host: link(obj, 'host'),
|
||||
SR: link(obj, 'SR'),
|
||||
deviceConfig: sensitiveValues.replace(
|
||||
device_config: sensitiveValues.replace(
|
||||
obj.device_config,
|
||||
'* obfuscated *'
|
||||
),
|
||||
|
||||
@@ -734,9 +734,19 @@ export default class Xapi extends XapiBase {
|
||||
const { SR } = vdi
|
||||
let childrenMap = cache[SR]
|
||||
if (childrenMap === undefined) {
|
||||
const xapi = vdi.$xapi
|
||||
childrenMap = cache[SR] = groupBy(
|
||||
vdi.$SR.$VDIs,
|
||||
_ => _.sm_config['vhd-parent']
|
||||
vdi.$SR.VDIs,
|
||||
|
||||
// if for any reasons, the VDI is undefined, simply ignores it instead
|
||||
// of failing
|
||||
ref => {
|
||||
try {
|
||||
return xapi.getObjectByRef(ref).sm_config['vhd-parent']
|
||||
} catch (error) {
|
||||
log.warn('missing VDI in _assertHealthyVdiChain', { error })
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1682,12 +1692,15 @@ export default class Xapi extends XapiBase {
|
||||
}
|
||||
|
||||
async createVdi({
|
||||
// blindly copying `sm_config` from another VDI can create problems,
|
||||
// therefore it is ignored by this method
|
||||
//
|
||||
// see https://github.com/vatesfr/xen-orchestra/issues/4482
|
||||
name_description,
|
||||
name_label,
|
||||
other_config = {},
|
||||
read_only = false,
|
||||
sharable = false,
|
||||
sm_config,
|
||||
SR,
|
||||
tags,
|
||||
type = 'user',
|
||||
@@ -1707,7 +1720,6 @@ export default class Xapi extends XapiBase {
|
||||
other_config,
|
||||
read_only: Boolean(read_only),
|
||||
sharable: Boolean(sharable),
|
||||
sm_config,
|
||||
SR: sr.$ref,
|
||||
tags,
|
||||
type,
|
||||
@@ -2029,6 +2041,7 @@ export default class Xapi extends XapiBase {
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@deferrable
|
||||
async createNetwork(
|
||||
$defer,
|
||||
@@ -2346,14 +2359,22 @@ export default class Xapi extends XapiBase {
|
||||
)
|
||||
}
|
||||
|
||||
async assertConsistentHostServerTime(hostRef) {
|
||||
const delta =
|
||||
async _getHostServerTimeShift(hostRef) {
|
||||
return Math.abs(
|
||||
parseDateTime(await this.call('host.get_servertime', hostRef)).getTime() -
|
||||
Date.now()
|
||||
if (Math.abs(delta) > 30e3) {
|
||||
Date.now()
|
||||
)
|
||||
}
|
||||
|
||||
async isHostServerTimeConsistent(hostRef) {
|
||||
return (await this._getHostServerTimeShift(hostRef)) < 30e3
|
||||
}
|
||||
|
||||
async assertConsistentHostServerTime(hostRef) {
|
||||
if (!(await this.isHostServerTimeConsistent(hostRef))) {
|
||||
throw new Error(
|
||||
`host server time and XOA date are not consistent with each other (${ms(
|
||||
delta
|
||||
await this._getHostServerTimeShift(hostRef)
|
||||
)})`
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { filter, find, pickBy, some } from 'lodash'
|
||||
|
||||
import ensureArray from '../../_ensureArray'
|
||||
import { debounce } from '../../decorators'
|
||||
import { debounceWithKey } from '../../_pDebounceWithKey'
|
||||
import { forEach, mapFilter, mapToArray, parseXml } from '../../utils'
|
||||
|
||||
import { extractOpaqueRef, useUpdateSystem } from '../utils'
|
||||
@@ -35,6 +36,28 @@ const log = createLogger('xo:xapi')
|
||||
|
||||
const _isXcp = host => host.software_version.product_brand === 'XCP-ng'
|
||||
|
||||
const XCP_NG_DEBOUNCE_TIME_MS = 60000
|
||||
|
||||
// list all yum updates available for a XCP-ng host
|
||||
// (hostObject) → { uuid: patchObject }
|
||||
async function _listXcpUpdates(host) {
|
||||
return JSON.parse(
|
||||
await this.call(
|
||||
'host.call_plugin',
|
||||
host.$ref,
|
||||
'updater.py',
|
||||
'check_update',
|
||||
{}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
const _listXcpUpdateDebounced = debounceWithKey(
|
||||
_listXcpUpdates,
|
||||
XCP_NG_DEBOUNCE_TIME_MS,
|
||||
host => host.$ref
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
|
||||
export default {
|
||||
@@ -141,19 +164,8 @@ export default {
|
||||
|
||||
// LIST ----------------------------------------------------------------------
|
||||
|
||||
// list all yum updates available for a XCP-ng host
|
||||
// (hostObject) → { uuid: patchObject }
|
||||
async _listXcpUpdates(host) {
|
||||
return JSON.parse(
|
||||
await this.call(
|
||||
'host.call_plugin',
|
||||
host.$ref,
|
||||
'updater.py',
|
||||
'check_update',
|
||||
{}
|
||||
)
|
||||
)
|
||||
},
|
||||
_listXcpUpdates,
|
||||
_listXcpUpdateDebounced,
|
||||
|
||||
// list all patches provided by Citrix for this host version regardless
|
||||
// of if they're installed or not
|
||||
@@ -306,7 +318,7 @@ export default {
|
||||
listMissingPatches(hostId) {
|
||||
const host = this.getObject(hostId)
|
||||
return _isXcp(host)
|
||||
? this._listXcpUpdates(host)
|
||||
? this._listXcpUpdateDebounced(host)
|
||||
: // TODO: list paid patches of free hosts as well so the UI can show them
|
||||
this._listInstallablePatches(host)
|
||||
},
|
||||
|
||||
@@ -276,19 +276,20 @@ export default {
|
||||
if (virtualizationMode !== 'pv' && virtualizationMode !== 'hvm') {
|
||||
throw new Error(`The virtualization mode must be 'pv' or 'hvm'`)
|
||||
}
|
||||
return vm
|
||||
.set_domain_type(virtualizationMode)
|
||||
::pCatch({ code: 'MESSAGE_METHOD_UNKNOWN' }, () =>
|
||||
vm.set_HVM_boot_policy(
|
||||
return vm.set_domain_type !== undefined
|
||||
? vm.set_domain_type(virtualizationMode)
|
||||
: vm.set_HVM_boot_policy(
|
||||
virtualizationMode === 'hvm' ? 'Boot order' : ''
|
||||
)
|
||||
)
|
||||
},
|
||||
},
|
||||
|
||||
coresPerSocket: {
|
||||
set: (coresPerSocket, vm) =>
|
||||
vm.update_platform('cores-per-socket', String(coresPerSocket)),
|
||||
vm.update_platform(
|
||||
'cores-per-socket',
|
||||
coresPerSocket !== null ? String(coresPerSocket) : null
|
||||
),
|
||||
},
|
||||
|
||||
CPUs: 'cpus',
|
||||
@@ -314,7 +315,8 @@ 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 !== null ? String(cap) : null),
|
||||
},
|
||||
|
||||
cpuMask: {
|
||||
|
||||
@@ -332,7 +332,7 @@ export const makeEditObject = specs => {
|
||||
|
||||
export const useUpdateSystem = host => {
|
||||
// Match Xen Center's condition: https://github.com/xenserver/xenadmin/blob/f3a64fc54bbff239ca6f285406d9034f57537d64/XenModel/Utils/Helpers.cs#L420
|
||||
return versionSatisfies(host.software_version.platform_version, '^2.1.1')
|
||||
return versionSatisfies(host.software_version.platform_version, '>=2.1.1')
|
||||
}
|
||||
|
||||
export const canSrHaveNewVdiOfSize = (sr, minSize) =>
|
||||
|
||||
@@ -3,6 +3,7 @@ import kindOf from 'kindof'
|
||||
import ms from 'ms'
|
||||
import schemaInspector from 'schema-inspector'
|
||||
import { forEach, isFunction } from 'lodash'
|
||||
import { getBoundPropertyDescriptor } from 'bind-property-descriptor'
|
||||
import { MethodNotFound } from 'json-rpc-peer'
|
||||
|
||||
import * as methods from '../api'
|
||||
@@ -219,17 +220,29 @@ export default class Api {
|
||||
throw new MethodNotFound(name)
|
||||
}
|
||||
|
||||
// FIXME: it can cause issues if there any property assignments in
|
||||
// XO methods called from the API.
|
||||
const context = Object.create(xo, {
|
||||
api: {
|
||||
// Used by system.*().
|
||||
value: this,
|
||||
},
|
||||
session: {
|
||||
value: session,
|
||||
},
|
||||
})
|
||||
// create the context which is an augmented XO
|
||||
const context = (() => {
|
||||
const descriptors = {
|
||||
api: {
|
||||
// Used by system.*().
|
||||
value: this,
|
||||
},
|
||||
session: {
|
||||
value: session,
|
||||
},
|
||||
}
|
||||
|
||||
let obj = xo
|
||||
do {
|
||||
Object.getOwnPropertyNames(obj).forEach(name => {
|
||||
if (!(name in descriptors)) {
|
||||
descriptors[name] = getBoundPropertyDescriptor(obj, name, xo)
|
||||
}
|
||||
})
|
||||
} while ((obj = Reflect.getPrototypeOf(obj)) !== null)
|
||||
|
||||
return Object.create(null, descriptors)
|
||||
})()
|
||||
|
||||
// Fetch and inject the current user.
|
||||
const userId = session.get('user_id', undefined)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import ms from 'ms'
|
||||
import { forEach, isEmpty, iteratee, sortedIndexBy } from 'lodash'
|
||||
|
||||
import { debounceWithKey } from '../_pDebounceWithKey'
|
||||
|
||||
const isSkippedError = error =>
|
||||
error.message === 'no disks found' ||
|
||||
error.message === 'no VMs match this pattern' ||
|
||||
@@ -64,131 +66,138 @@ const taskTimeComparator = ({ start: s1, end: e1 }, { start: s2, end: e2 }) => {
|
||||
// tasks?: Task[],
|
||||
// }
|
||||
export default {
|
||||
async getBackupNgLogs(runId?: string) {
|
||||
const [jobLogs, restoreLogs, restoreMetadataLogs] = await Promise.all([
|
||||
this.getLogs('jobs'),
|
||||
this.getLogs('restore'),
|
||||
this.getLogs('metadataRestore'),
|
||||
])
|
||||
getBackupNgLogs: debounceWithKey(
|
||||
async function getBackupNgLogs(runId?: string) {
|
||||
const [jobLogs, restoreLogs, restoreMetadataLogs] = await Promise.all([
|
||||
this.getLogs('jobs'),
|
||||
this.getLogs('restore'),
|
||||
this.getLogs('metadataRestore'),
|
||||
])
|
||||
|
||||
const { runningJobs, runningRestores, runningMetadataRestores } = this
|
||||
const consolidated = {}
|
||||
const started = {}
|
||||
const { runningJobs, runningRestores, runningMetadataRestores } = this
|
||||
const consolidated = {}
|
||||
const started = {}
|
||||
|
||||
const handleLog = ({ data, time, message }, id) => {
|
||||
const { event } = data
|
||||
if (event === 'job.start') {
|
||||
if (
|
||||
(data.type === 'backup' || data.key === undefined) &&
|
||||
(runId === undefined || runId === id)
|
||||
) {
|
||||
const { scheduleId, jobId } = data
|
||||
consolidated[id] = started[id] = {
|
||||
const handleLog = ({ data, time, message }, id) => {
|
||||
const { event } = data
|
||||
if (event === 'job.start') {
|
||||
if (
|
||||
(data.type === 'backup' || data.key === undefined) &&
|
||||
(runId === undefined || runId === id)
|
||||
) {
|
||||
const { scheduleId, jobId } = data
|
||||
consolidated[id] = started[id] = {
|
||||
data: data.data,
|
||||
id,
|
||||
jobId,
|
||||
jobName: data.jobName,
|
||||
message: 'backup',
|
||||
scheduleId,
|
||||
start: time,
|
||||
status: runningJobs[jobId] === id ? 'pending' : 'interrupted',
|
||||
}
|
||||
}
|
||||
} else if (event === 'job.end') {
|
||||
const { runJobId } = data
|
||||
const log = started[runJobId]
|
||||
if (log !== undefined) {
|
||||
delete started[runJobId]
|
||||
log.end = time
|
||||
log.status = computeStatusAndSortTasks(
|
||||
getStatus((log.result = data.error)),
|
||||
log.tasks
|
||||
)
|
||||
}
|
||||
} else if (event === 'task.start') {
|
||||
const task = {
|
||||
data: data.data,
|
||||
id,
|
||||
jobId,
|
||||
jobName: data.jobName,
|
||||
message: 'backup',
|
||||
scheduleId,
|
||||
message,
|
||||
start: time,
|
||||
status: runningJobs[jobId] === id ? 'pending' : 'interrupted',
|
||||
}
|
||||
const { parentId } = data
|
||||
let parent
|
||||
if (parentId === undefined && (runId === undefined || runId === id)) {
|
||||
// top level task
|
||||
task.status =
|
||||
(message === 'restore' && !runningRestores.has(id)) ||
|
||||
(message === 'metadataRestore' &&
|
||||
!runningMetadataRestores.has(id))
|
||||
? 'interrupted'
|
||||
: 'pending'
|
||||
consolidated[id] = started[id] = task
|
||||
} else if ((parent = started[parentId]) !== undefined) {
|
||||
// sub-task for which the parent exists
|
||||
task.status = parent.status
|
||||
started[id] = task
|
||||
;(parent.tasks || (parent.tasks = [])).push(task)
|
||||
}
|
||||
} else if (event === 'task.end') {
|
||||
const { taskId } = data
|
||||
const log = started[taskId]
|
||||
if (log !== undefined) {
|
||||
// TODO: merge/transfer work-around
|
||||
delete started[taskId]
|
||||
log.end = time
|
||||
log.status = computeStatusAndSortTasks(
|
||||
getStatus((log.result = data.result), data.status),
|
||||
log.tasks
|
||||
)
|
||||
}
|
||||
} else if (event === 'task.warning') {
|
||||
const parent = started[data.taskId]
|
||||
parent !== undefined &&
|
||||
(parent.warnings || (parent.warnings = [])).push({
|
||||
data: data.data,
|
||||
message,
|
||||
})
|
||||
} else if (event === 'task.info') {
|
||||
const parent = started[data.taskId]
|
||||
parent !== undefined &&
|
||||
(parent.infos || (parent.infos = [])).push({
|
||||
data: data.data,
|
||||
message,
|
||||
})
|
||||
} else if (event === 'jobCall.start') {
|
||||
const parent = started[data.runJobId]
|
||||
if (parent !== undefined) {
|
||||
;(parent.tasks || (parent.tasks = [])).push(
|
||||
(started[id] = {
|
||||
data: {
|
||||
type: 'VM',
|
||||
id: data.params.id,
|
||||
},
|
||||
id,
|
||||
start: time,
|
||||
status: parent.status,
|
||||
})
|
||||
)
|
||||
}
|
||||
} else if (event === 'jobCall.end') {
|
||||
const { runCallId } = data
|
||||
const log = started[runCallId]
|
||||
if (log !== undefined) {
|
||||
delete started[runCallId]
|
||||
log.end = time
|
||||
log.status = computeStatusAndSortTasks(
|
||||
getStatus((log.result = data.error)),
|
||||
log.tasks
|
||||
)
|
||||
}
|
||||
}
|
||||
} else if (event === 'job.end') {
|
||||
const { runJobId } = data
|
||||
const log = started[runJobId]
|
||||
if (log !== undefined) {
|
||||
delete started[runJobId]
|
||||
log.end = time
|
||||
log.status = computeStatusAndSortTasks(
|
||||
getStatus((log.result = data.error)),
|
||||
log.tasks
|
||||
)
|
||||
}
|
||||
} else if (event === 'task.start') {
|
||||
const task = {
|
||||
data: data.data,
|
||||
id,
|
||||
message,
|
||||
start: time,
|
||||
}
|
||||
const { parentId } = data
|
||||
let parent
|
||||
if (parentId === undefined && (runId === undefined || runId === id)) {
|
||||
// top level task
|
||||
task.status =
|
||||
(message === 'restore' && !runningRestores.has(id)) ||
|
||||
(message === 'metadataRestore' && !runningMetadataRestores.has(id))
|
||||
? 'interrupted'
|
||||
: 'pending'
|
||||
consolidated[id] = started[id] = task
|
||||
} else if ((parent = started[parentId]) !== undefined) {
|
||||
// sub-task for which the parent exists
|
||||
task.status = parent.status
|
||||
started[id] = task
|
||||
;(parent.tasks || (parent.tasks = [])).push(task)
|
||||
}
|
||||
} else if (event === 'task.end') {
|
||||
const { taskId } = data
|
||||
const log = started[taskId]
|
||||
if (log !== undefined) {
|
||||
// TODO: merge/transfer work-around
|
||||
delete started[taskId]
|
||||
log.end = time
|
||||
log.status = computeStatusAndSortTasks(
|
||||
getStatus((log.result = data.result), data.status),
|
||||
log.tasks
|
||||
)
|
||||
}
|
||||
} else if (event === 'task.warning') {
|
||||
const parent = started[data.taskId]
|
||||
parent !== undefined &&
|
||||
(parent.warnings || (parent.warnings = [])).push({
|
||||
data: data.data,
|
||||
message,
|
||||
})
|
||||
} else if (event === 'task.info') {
|
||||
const parent = started[data.taskId]
|
||||
parent !== undefined &&
|
||||
(parent.infos || (parent.infos = [])).push({
|
||||
data: data.data,
|
||||
message,
|
||||
})
|
||||
} else if (event === 'jobCall.start') {
|
||||
const parent = started[data.runJobId]
|
||||
if (parent !== undefined) {
|
||||
;(parent.tasks || (parent.tasks = [])).push(
|
||||
(started[id] = {
|
||||
data: {
|
||||
type: 'VM',
|
||||
id: data.params.id,
|
||||
},
|
||||
id,
|
||||
start: time,
|
||||
status: parent.status,
|
||||
})
|
||||
)
|
||||
}
|
||||
} else if (event === 'jobCall.end') {
|
||||
const { runCallId } = data
|
||||
const log = started[runCallId]
|
||||
if (log !== undefined) {
|
||||
delete started[runCallId]
|
||||
log.end = time
|
||||
log.status = computeStatusAndSortTasks(
|
||||
getStatus((log.result = data.error)),
|
||||
log.tasks
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
forEach(jobLogs, handleLog)
|
||||
forEach(restoreLogs, handleLog)
|
||||
forEach(restoreMetadataLogs, handleLog)
|
||||
|
||||
return runId === undefined ? consolidated : consolidated[runId]
|
||||
},
|
||||
10e3,
|
||||
function keyFn(runId) {
|
||||
return [this, runId]
|
||||
}
|
||||
|
||||
forEach(jobLogs, handleLog)
|
||||
forEach(restoreLogs, handleLog)
|
||||
forEach(restoreMetadataLogs, handleLog)
|
||||
|
||||
return runId === undefined ? consolidated : consolidated[runId]
|
||||
},
|
||||
),
|
||||
|
||||
async getBackupNgLogsSorted({ after, before, filter, limit }) {
|
||||
let logs = await this.getBackupNgLogs()
|
||||
|
||||
@@ -44,6 +44,7 @@ import { type Schedule } from '../scheduling'
|
||||
|
||||
import createSizeStream from '../../size-stream'
|
||||
import parseDuration from '../../_parseDuration'
|
||||
import { debounceWithKey } from '../../_pDebounceWithKey'
|
||||
import {
|
||||
type DeltaVmExport,
|
||||
type DeltaVmImport,
|
||||
@@ -821,56 +822,66 @@ export default class BackupNg {
|
||||
)()
|
||||
}
|
||||
|
||||
@debounceWithKey.decorate(10e3, function keyFn(remoteId) {
|
||||
return [this, remoteId]
|
||||
})
|
||||
async _listVmBackupsOnRemote(remoteId: string) {
|
||||
const app = this._app
|
||||
const backupsByVm = {}
|
||||
try {
|
||||
const handler = await app.getRemoteHandler(remoteId)
|
||||
|
||||
const entries = (await handler.list(BACKUP_DIR).catch(error => {
|
||||
if (error == null || error.code !== 'ENOENT') {
|
||||
throw error
|
||||
}
|
||||
return []
|
||||
})).filter(name => name !== 'index.json')
|
||||
|
||||
await Promise.all(
|
||||
entries.map(async vmUuid => {
|
||||
// $FlowFixMe don't know what is the problem (JFT)
|
||||
const backups = await this._listVmBackups(handler, vmUuid)
|
||||
|
||||
if (backups.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
// inject an id usable by importVmBackupNg()
|
||||
backups.forEach(backup => {
|
||||
backup.id = `${remoteId}/${backup._filename}`
|
||||
|
||||
const { vdis, vhds } = backup
|
||||
backup.disks =
|
||||
vhds === undefined
|
||||
? []
|
||||
: Object.keys(vhds).map(vdiId => {
|
||||
const vdi = vdis[vdiId]
|
||||
return {
|
||||
id: `${dirname(backup._filename)}/${vhds[vdiId]}`,
|
||||
name: vdi.name_label,
|
||||
uuid: vdi.uuid,
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
backupsByVm[vmUuid] = backups
|
||||
})
|
||||
)
|
||||
} catch (error) {
|
||||
log.warn(`listVmBackups for remote ${remoteId}:`, { error })
|
||||
}
|
||||
return backupsByVm
|
||||
}
|
||||
|
||||
async listVmBackupsNg(remotes: string[]) {
|
||||
const backupsByVmByRemote: $Dict<$Dict<Metadata[]>> = {}
|
||||
|
||||
const app = this._app
|
||||
await Promise.all(
|
||||
remotes.map(async remoteId => {
|
||||
try {
|
||||
const handler = await app.getRemoteHandler(remoteId)
|
||||
|
||||
const entries = (await handler.list(BACKUP_DIR).catch(error => {
|
||||
if (error == null || error.code !== 'ENOENT') {
|
||||
throw error
|
||||
}
|
||||
return []
|
||||
})).filter(name => name !== 'index.json')
|
||||
|
||||
const backupsByVm = (backupsByVmByRemote[remoteId] = {})
|
||||
await Promise.all(
|
||||
entries.map(async vmUuid => {
|
||||
// $FlowFixMe don't know what is the problem (JFT)
|
||||
const backups = await this._listVmBackups(handler, vmUuid)
|
||||
|
||||
if (backups.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
// inject an id usable by importVmBackupNg()
|
||||
backups.forEach(backup => {
|
||||
backup.id = `${remoteId}/${backup._filename}`
|
||||
|
||||
const { vdis, vhds } = backup
|
||||
backup.disks =
|
||||
vhds === undefined
|
||||
? []
|
||||
: Object.keys(vhds).map(vdiId => {
|
||||
const vdi = vdis[vdiId]
|
||||
return {
|
||||
id: `${dirname(backup._filename)}/${vhds[vdiId]}`,
|
||||
name: vdi.name_label,
|
||||
uuid: vdi.uuid,
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
backupsByVm[vmUuid] = backups
|
||||
})
|
||||
)
|
||||
} catch (error) {
|
||||
log.warn(`listVmBackups for remote ${remoteId}:`, { error })
|
||||
}
|
||||
backupsByVmByRemote[remoteId] = await this._listVmBackupsOnRemote(
|
||||
remoteId
|
||||
)
|
||||
})
|
||||
)
|
||||
|
||||
@@ -1146,6 +1157,21 @@ export default class BackupNg {
|
||||
$defer.call(xapi, 'deleteVm', snapshot)
|
||||
}
|
||||
|
||||
let compress = getJobCompression(job)
|
||||
const pool = snapshot.$pool
|
||||
if (
|
||||
compress === 'zstd' &&
|
||||
pool.restrictions.restrict_zstd_export !== 'false'
|
||||
) {
|
||||
compress = false
|
||||
logger.warning(
|
||||
`Zstd is not supported on the pool ${pool.name_label}, the VM will be exported without compression`,
|
||||
{
|
||||
event: 'task.warning',
|
||||
taskId,
|
||||
}
|
||||
)
|
||||
}
|
||||
let xva: any = await wrapTask(
|
||||
{
|
||||
logger,
|
||||
@@ -1153,7 +1179,7 @@ export default class BackupNg {
|
||||
parentId: taskId,
|
||||
},
|
||||
xapi.exportVm($cancelToken, snapshot, {
|
||||
compress: getJobCompression(job),
|
||||
compress,
|
||||
})
|
||||
)
|
||||
const exportTask = xva.task
|
||||
|
||||
@@ -243,38 +243,17 @@ export default class Jobs {
|
||||
}
|
||||
|
||||
async _runJob(job: Job, schedule?: Schedule, data_?: any) {
|
||||
const { id } = job
|
||||
|
||||
const runningJobs = this._runningJobs
|
||||
if (id in runningJobs) {
|
||||
throw new Error(`job ${id} is already running`)
|
||||
}
|
||||
|
||||
const { type } = job
|
||||
const executor = this._executors[type]
|
||||
if (executor === undefined) {
|
||||
throw new Error(`cannot run job ${id}: no executor for type ${type}`)
|
||||
}
|
||||
|
||||
let data
|
||||
if (type === 'backup') {
|
||||
// $FlowFixMe only defined for BackupJob
|
||||
const settings = job.settings['']
|
||||
data = {
|
||||
// $FlowFixMe only defined for BackupJob
|
||||
mode: job.mode,
|
||||
reportWhen: (settings && settings.reportWhen) || 'failure',
|
||||
}
|
||||
}
|
||||
if (type === 'metadataBackup') {
|
||||
data = {
|
||||
reportWhen: job.settings['']?.reportWhen ?? 'failure',
|
||||
}
|
||||
}
|
||||
|
||||
const logger = this._logger
|
||||
const { id, type } = job
|
||||
const runJobId = logger.notice(`Starting execution of ${id}.`, {
|
||||
data,
|
||||
data:
|
||||
type === 'backup' || type === 'metadataBackup'
|
||||
? {
|
||||
// $FlowFixMe only defined for BackupJob
|
||||
mode: job.mode,
|
||||
reportWhen: job.settings['']?.reportWhen ?? 'failure',
|
||||
}
|
||||
: undefined,
|
||||
event: 'job.start',
|
||||
userId: job.userId,
|
||||
jobId: id,
|
||||
@@ -285,44 +264,64 @@ export default class Jobs {
|
||||
type,
|
||||
})
|
||||
|
||||
// runId is a temporary property used to check if the report is sent after the server interruption
|
||||
this.updateJob({ id, runId: runJobId })::ignoreErrors()
|
||||
runningJobs[id] = runJobId
|
||||
|
||||
const runs = this._runs
|
||||
|
||||
const { cancel, token } = CancelToken.source()
|
||||
runs[runJobId] = { cancel }
|
||||
|
||||
let session
|
||||
const app = this._app
|
||||
try {
|
||||
session = app.createUserConnection()
|
||||
session.set('user_id', job.userId)
|
||||
const runningJobs = this._runningJobs
|
||||
|
||||
const status = await executor({
|
||||
app,
|
||||
cancelToken: token,
|
||||
data: data_,
|
||||
job,
|
||||
logger,
|
||||
runJobId,
|
||||
schedule,
|
||||
session,
|
||||
})
|
||||
await logger.notice(
|
||||
`Execution terminated for ${job.id}.`,
|
||||
{
|
||||
event: 'job.end',
|
||||
if (id in runningJobs) {
|
||||
throw new Error(`the job (${id}) is already running`)
|
||||
}
|
||||
|
||||
const executor = this._executors[type]
|
||||
if (executor === undefined) {
|
||||
throw new Error(`cannot run job (${id}): no executor for type ${type}`)
|
||||
}
|
||||
|
||||
// runId is a temporary property used to check if the report is sent after the server interruption
|
||||
this.updateJob({ id, runId: runJobId })::ignoreErrors()
|
||||
runningJobs[id] = runJobId
|
||||
|
||||
const runs = this._runs
|
||||
let session
|
||||
try {
|
||||
const { cancel, token } = CancelToken.source()
|
||||
runs[runJobId] = { cancel }
|
||||
|
||||
session = app.createUserConnection()
|
||||
session.set('user_id', job.userId)
|
||||
|
||||
const status = await executor({
|
||||
app,
|
||||
cancelToken: token,
|
||||
data: data_,
|
||||
job,
|
||||
logger,
|
||||
runJobId,
|
||||
},
|
||||
true
|
||||
)
|
||||
schedule,
|
||||
session,
|
||||
})
|
||||
|
||||
app.emit('job:terminated', runJobId, {
|
||||
type: job.type,
|
||||
status,
|
||||
})
|
||||
await logger.notice(
|
||||
`Execution terminated for ${job.id}.`,
|
||||
{
|
||||
event: 'job.end',
|
||||
runJobId,
|
||||
},
|
||||
true
|
||||
)
|
||||
|
||||
app.emit('job:terminated', runJobId, {
|
||||
type: job.type,
|
||||
status,
|
||||
})
|
||||
} finally {
|
||||
this.updateJob({ id, runId: null })::ignoreErrors()
|
||||
delete runningJobs[id]
|
||||
delete runs[runJobId]
|
||||
if (session !== undefined) {
|
||||
session.close()
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
await logger.error(
|
||||
`The execution of ${id} has failed.`,
|
||||
@@ -337,13 +336,6 @@ export default class Jobs {
|
||||
type: job.type,
|
||||
})
|
||||
throw error
|
||||
} finally {
|
||||
this.updateJob({ id, runId: null })::ignoreErrors()
|
||||
delete runningJobs[id]
|
||||
delete runs[runJobId]
|
||||
if (session !== undefined) {
|
||||
session.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import asyncMap from '@xen-orchestra/async-map'
|
||||
import createLogger from '@xen-orchestra/log'
|
||||
import { fromEvent, ignoreErrors } from 'promise-toolbox'
|
||||
|
||||
import debounceWithKey from '../_pDebounceWithKey'
|
||||
import { debounceWithKey } from '../_pDebounceWithKey'
|
||||
import parseDuration from '../_parseDuration'
|
||||
import { type Xapi } from '../xapi'
|
||||
import {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import asyncMap from '@xen-orchestra/async-map'
|
||||
import { createSchedule } from '@xen-orchestra/cron'
|
||||
import { ignoreErrors } from 'promise-toolbox'
|
||||
import { keyBy } from 'lodash'
|
||||
import { noSuchObject } from 'xo-common/api-errors'
|
||||
|
||||
@@ -155,7 +156,9 @@ export default class Scheduling {
|
||||
this._runs[id] = createSchedule(
|
||||
schedule.cron,
|
||||
schedule.timezone
|
||||
).startJob(() => this._app.runJobSequence([schedule.jobId], schedule))
|
||||
).startJob(() => {
|
||||
ignoreErrors.call(this._app.runJobSequence([schedule.jobId], schedule))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
"fs-extra": "^8.0.1",
|
||||
"get-stream": "^5.1.0",
|
||||
"index-modules": "^0.3.0",
|
||||
"rimraf": "^2.6.2"
|
||||
"rimraf": "^3.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
|
||||
|
||||
@@ -261,7 +261,11 @@ gulp.task(function buildScripts() {
|
||||
],
|
||||
}),
|
||||
require('gulp-sourcemaps').init({ loadMaps: true }),
|
||||
PRODUCTION && require('gulp-uglify/composer')(require('uglify-es'))(),
|
||||
PRODUCTION &&
|
||||
require('gulp-uglify/composer')(require('uglify-es'))({
|
||||
// 2019-09-04 Disabling inline optimization until https://github.com/mishoo/UglifyJS2/issues/2842 is fixed
|
||||
compress: { inline: false },
|
||||
}),
|
||||
dest()
|
||||
)
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "xo-web",
|
||||
"version": "5.48.1",
|
||||
"version": "5.50.0",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "Web interface client for Xen-Orchestra",
|
||||
"keywords": [
|
||||
@@ -32,8 +32,9 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nraynaud/novnc": "0.6.1",
|
||||
"@xen-orchestra/cron": "^1.0.3",
|
||||
"@xen-orchestra/cron": "^1.0.4",
|
||||
"@xen-orchestra/defined": "^0.0.0",
|
||||
"@xen-orchestra/template": "^0.1.0",
|
||||
"ansi_up": "^4.0.3",
|
||||
"asap": "^2.0.6",
|
||||
"babel-core": "^6.26.0",
|
||||
@@ -71,7 +72,7 @@
|
||||
"font-mfizz": "^2.4.1",
|
||||
"get-stream": "^4.0.0",
|
||||
"gulp": "^4.0.0",
|
||||
"gulp-autoprefixer": "^6.0.0",
|
||||
"gulp-autoprefixer": "^7.0.0",
|
||||
"gulp-csso": "^3.0.0",
|
||||
"gulp-embedlr": "^0.5.2",
|
||||
"gulp-plumber": "^1.1.0",
|
||||
@@ -90,7 +91,7 @@
|
||||
"lodash": "^4.6.1",
|
||||
"loose-envify": "^1.1.0",
|
||||
"make-error": "^1.3.2",
|
||||
"marked": "^0.6.0",
|
||||
"marked": "^0.7.0",
|
||||
"modular-cssify": "^12",
|
||||
"moment": "^2.20.1",
|
||||
"moment-timezone": "^0.5.14",
|
||||
@@ -127,7 +128,7 @@
|
||||
"redux": "^4.0.0",
|
||||
"redux-thunk": "^2.0.1",
|
||||
"reselect": "^2.5.4",
|
||||
"rimraf": "^2.6.2",
|
||||
"rimraf": "^3.0.0",
|
||||
"semver": "^6.0.0",
|
||||
"styled-components": "^3.1.5",
|
||||
"uglify-es": "^3.3.4",
|
||||
|
||||
@@ -14,11 +14,16 @@ const AVAILABLE_TEMPLATE_VARS = {
|
||||
const showAvailableTemplateVars = () =>
|
||||
alert(
|
||||
_('availableTemplateVarsTitle'),
|
||||
<ul>
|
||||
{map(AVAILABLE_TEMPLATE_VARS, (value, key) => (
|
||||
<li key={key}>{_.keyValue(key, _(value))}</li>
|
||||
))}
|
||||
</ul>
|
||||
<div>
|
||||
<ul>
|
||||
{map(AVAILABLE_TEMPLATE_VARS, (value, key) => (
|
||||
<li key={key}>{_.keyValue(key, _(value))}</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className='text-info'>
|
||||
<Icon icon='info' /> {_('templateEscape')}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
const showNetworkConfigInfo = () =>
|
||||
@@ -87,5 +92,3 @@ export const DEFAULT_NETWORK_CONFIG_TEMPLATE = `#network:
|
||||
# name: eth0
|
||||
# subnets:
|
||||
# - type: dhcp`
|
||||
|
||||
export const CAN_CLOUD_INIT = +process.env.XOA_PLAN > 3
|
||||
|
||||
@@ -570,7 +570,9 @@ const messages = {
|
||||
newSrCreate: 'Create',
|
||||
newSrNamePlaceHolder: 'Storage name',
|
||||
newSrDescPlaceHolder: 'Storage description',
|
||||
newSrAddressPlaceHolder: 'Address',
|
||||
newSrIscsiAddressPlaceHolder: 'e.g 93.184.216.34 or iscsi.example.net',
|
||||
newSrNfsAddressPlaceHolder: 'e.g 93.184.216.34 or nfs.example.net',
|
||||
newSrSmbAddressPlaceHolder: 'e.g \\\\server\\sharename',
|
||||
newSrPortPlaceHolder: '[port]',
|
||||
newSrUsernamePlaceHolder: 'Username',
|
||||
newSrPasswordPlaceHolder: 'Password',
|
||||
@@ -982,11 +984,18 @@ const messages = {
|
||||
// ----- VM console tab -----
|
||||
copyToClipboardLabel: 'Copy',
|
||||
ctrlAltDelButtonLabel: 'Ctrl+Alt+Del',
|
||||
ctrlAltDelConfirmation: 'Send Ctrl+Alt+Del to VM?',
|
||||
multilineCopyToClipboard: 'Multiline copy',
|
||||
tipLabel: 'Tip:',
|
||||
hideHeaderTooltip: 'Hide info',
|
||||
showHeaderTooltip: 'Show info',
|
||||
sendToClipboard: 'Send to clipboard',
|
||||
sshRootTooltip: 'Connect using external SSH tool as root',
|
||||
sshRootLabel: 'SSH',
|
||||
sshUserTooltip: 'Connect using external SSH tool as user…',
|
||||
sshUserLabel: 'SSH as…',
|
||||
sshUsernameLabel: 'SSH user name',
|
||||
sshNeedClientTools: 'No IP address reported by client tools',
|
||||
|
||||
// ----- VM container tab -----
|
||||
containerName: 'Name',
|
||||
@@ -1309,12 +1318,12 @@ const messages = {
|
||||
newVmSshKey: 'SSH key',
|
||||
noConfigDrive: 'No config drive',
|
||||
newVmCustomConfig: 'Custom config',
|
||||
premiumOnly: 'Only available in Premium',
|
||||
availableTemplateVarsInfo:
|
||||
'Click here to see the available template variables',
|
||||
availableTemplateVarsTitle: 'Available template variables',
|
||||
templateNameInfo: 'the VM\'s name. It must not contain "_"',
|
||||
templateIndexInfo: "the VM's index, it will take 0 in case of single VM",
|
||||
templateEscape: 'Tip: escape any variable with a preceding backslash (\\)',
|
||||
coreOsDefaultTemplateError:
|
||||
'Error on getting the default coreOS cloud template',
|
||||
newVmBootAfterCreate: 'Boot VM after creation',
|
||||
@@ -1746,6 +1755,11 @@ const messages = {
|
||||
newNetworkInfo: 'Info',
|
||||
newNetworkType: 'Type',
|
||||
newNetworkEncapsulation: 'Encapsulation',
|
||||
newNetworkEncrypted: 'Encrypted',
|
||||
encryptionWarning:
|
||||
'A pool can have 1 encrypted GRE network and 1 encrypted VxLAN network max',
|
||||
newNetworkSdnControllerTip:
|
||||
'Private networks work on up-to-date XCP-ng hosts, for other scenarios please see the requirements',
|
||||
deleteNetwork: 'Delete network',
|
||||
deleteNetworkConfirm: 'Are you sure you want to delete this network?',
|
||||
networkInUse: 'This network is currently in use',
|
||||
@@ -1917,6 +1931,7 @@ const messages = {
|
||||
logUser: 'User',
|
||||
logMessage: 'Message',
|
||||
logSuggestXcpNg: 'Use XCP-ng to get rid of restrictions',
|
||||
logXapiError: 'This is a XenServer/XCP-ng error',
|
||||
logError: 'Error',
|
||||
logTitle: 'Logs',
|
||||
logDisplayDetails: 'Display details',
|
||||
@@ -2128,6 +2143,21 @@ const messages = {
|
||||
xosanIssueHostNotInNetwork:
|
||||
'Will configure the host xosan network device with a static IP address and plug it in.',
|
||||
|
||||
// Hub
|
||||
hubPage: 'Hub',
|
||||
noDefaultSr: 'The selected pool has no default SR',
|
||||
successfulInstall: 'VM installed successfully',
|
||||
vmNoAvailable: 'No VMs available ',
|
||||
create: 'Create',
|
||||
hubResourceAlert: 'Resource alert',
|
||||
os: 'OS',
|
||||
version: 'Version',
|
||||
size: 'Size',
|
||||
totalDiskSize: 'Total disk size',
|
||||
hideInstalledPool: 'Already installed templates are hidden',
|
||||
hubSrErrorTitle: 'Missing property',
|
||||
hubImportNotificationTitle: 'XVA import',
|
||||
|
||||
// Licenses
|
||||
xosanUnregisteredDisclaimer:
|
||||
'You are not registered and therefore will not be able to create or manage your XOSAN SRs. {link}',
|
||||
|
||||
@@ -30,6 +30,7 @@ export const selectLang = createAction('SELECT_LANG', lang => lang)
|
||||
export const connected = createAction('CONNECTED')
|
||||
export const disconnected = createAction('DISCONNECTED')
|
||||
|
||||
export const markObjectsFetched = createAction('OBJECTS_FETCHED')
|
||||
export const updateObjects = createAction('UPDATE_OBJECTS', updates => updates)
|
||||
export const updatePermissions = createAction(
|
||||
'UPDATE_PERMISSIONS',
|
||||
@@ -57,3 +58,11 @@ export const setHomeVmIdsSelection = createAction(
|
||||
'SET_HOME_VM_IDS_SELECTION',
|
||||
homeVmIdsSelection => homeVmIdsSelection
|
||||
)
|
||||
export const markHubResourceAsInstalling = createAction(
|
||||
'MARK_HUB_RESOURCE_AS_INSTALLING',
|
||||
id => id
|
||||
)
|
||||
export const markHubResourceAsInstalled = createAction(
|
||||
'MARK_HUB_RESOURCE_AS_INSTALLED',
|
||||
id => id
|
||||
)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import cookies from 'cookies-js'
|
||||
import { omit } from 'lodash'
|
||||
|
||||
import invoke from '../invoke'
|
||||
|
||||
@@ -92,13 +93,30 @@ export default {
|
||||
homeVmIdsSelection,
|
||||
}),
|
||||
|
||||
// whether a resource is currently being installed: `hubInstallingResources[<template id>]`
|
||||
hubInstallingResources: combineActionHandlers(
|
||||
{},
|
||||
{
|
||||
[actions.markHubResourceAsInstalling]: (
|
||||
prevHubInstallingResources,
|
||||
id
|
||||
) => ({ ...prevHubInstallingResources, [id]: true }),
|
||||
[actions.markHubResourceAsInstalled]: (prevHubInstallingResources, id) =>
|
||||
omit(prevHubInstallingResources, id),
|
||||
}
|
||||
),
|
||||
|
||||
objects: combineActionHandlers(
|
||||
{
|
||||
all: {}, // Mutable for performance!
|
||||
byType: {},
|
||||
fetched: false,
|
||||
},
|
||||
{
|
||||
[actions.updateObjects]: ({ all, byType: prevByType }, updates) => {
|
||||
[actions.updateObjects]: (
|
||||
{ all, byType: prevByType, fetched },
|
||||
updates
|
||||
) => {
|
||||
const byType = { ...prevByType }
|
||||
const get = type => {
|
||||
const curr = byType[type]
|
||||
@@ -125,8 +143,12 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
return { all, byType, fetched: true }
|
||||
return { all, byType, fetched }
|
||||
},
|
||||
[actions.markObjectsFetched]: state => ({
|
||||
...state,
|
||||
fetched: true,
|
||||
}),
|
||||
}
|
||||
),
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import { connect } from 'react-redux'
|
||||
import { FormattedDate } from 'react-intl'
|
||||
import {
|
||||
clone,
|
||||
escapeRegExp,
|
||||
every,
|
||||
forEach,
|
||||
isArray,
|
||||
@@ -14,12 +13,9 @@ import {
|
||||
isFunction,
|
||||
isPlainObject,
|
||||
isString,
|
||||
join,
|
||||
keys,
|
||||
map,
|
||||
mapValues,
|
||||
pick,
|
||||
replace,
|
||||
sample,
|
||||
some,
|
||||
} from 'lodash'
|
||||
@@ -355,33 +351,6 @@ export const resolveResourceSet = resourceSet => {
|
||||
export const resolveResourceSets = resourceSets =>
|
||||
map(resourceSets, resolveResourceSet)
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
// Creates a string replacer based on a pattern and a list of rules
|
||||
//
|
||||
// ```js
|
||||
// const myReplacer = buildTemplate('{name}_COPY_{name}_{id}_%', {
|
||||
// '{name}': vm => vm.name_label,
|
||||
// '{id}': vm => vm.id,
|
||||
// '%': (_, i) => i
|
||||
// })
|
||||
//
|
||||
// const newString = myReplacer({
|
||||
// name_label: 'foo',
|
||||
// id: 42,
|
||||
// }, 32)
|
||||
//
|
||||
// newString === 'foo_COPY_foo_42_32'
|
||||
// ```
|
||||
export function buildTemplate(pattern, rules) {
|
||||
const regExp = new RegExp(join(map(keys(rules), escapeRegExp), '|'), 'g')
|
||||
return (...params) =>
|
||||
replace(pattern, regExp, match => {
|
||||
const rule = rules[match]
|
||||
return isFunction(rule) ? rule(...params) : rule
|
||||
})
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export const streamToString = getStream
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import _, { messages } from 'intl'
|
||||
import map from 'lodash/map'
|
||||
import React from 'react'
|
||||
import { compileTemplate } from '@xen-orchestra/template'
|
||||
import { injectIntl } from 'react-intl'
|
||||
|
||||
import BaseComponent from 'base-component'
|
||||
@@ -8,29 +9,17 @@ import SingleLineRow from 'single-line-row'
|
||||
import Upgrade from 'xoa-upgrade'
|
||||
import { Col } from 'grid'
|
||||
import { SelectSr } from 'select-objects'
|
||||
import { buildTemplate, connectStore } from 'utils'
|
||||
import { connectStore } from 'utils'
|
||||
|
||||
import constructQueryString from '../../construct-query-string'
|
||||
import Icon from '../../icon'
|
||||
import Link from '../../link'
|
||||
import SelectCompression from '../../select-compression'
|
||||
import Tooltip from '../../tooltip'
|
||||
import {
|
||||
createCollectionWrapper,
|
||||
createGetObjectsOfType,
|
||||
createSelector,
|
||||
} from '../../selectors'
|
||||
import ZstdChecker from '../../zstd-checker'
|
||||
import { createGetObjectsOfType } from '../../selectors'
|
||||
|
||||
@connectStore(
|
||||
() => {
|
||||
const getVms = createGetObjectsOfType('VM').pick((_, props) => props.vms)
|
||||
return {
|
||||
containers: createSelector(
|
||||
createGetObjectsOfType('pool'),
|
||||
createGetObjectsOfType('host'),
|
||||
(pools, hosts) => ({ ...pools, ...hosts })
|
||||
),
|
||||
vms: getVms,
|
||||
resolvedVms: getVms,
|
||||
}
|
||||
},
|
||||
{ withRef: true }
|
||||
@@ -41,18 +30,18 @@ class CopyVmsModalBody extends BaseComponent {
|
||||
if (!state || !state.sr) {
|
||||
return {}
|
||||
}
|
||||
const { vms } = this.props
|
||||
const { resolvedVms } = this.props
|
||||
const { namePattern } = state
|
||||
|
||||
const names = namePattern
|
||||
? map(
|
||||
vms,
|
||||
buildTemplate(namePattern, {
|
||||
resolvedVms,
|
||||
compileTemplate(namePattern, {
|
||||
'{name}': vm => vm.name_label,
|
||||
'{id}': vm => vm.id,
|
||||
})
|
||||
)
|
||||
: map(vms, vm => vm.name_label)
|
||||
: map(resolvedVms, vm => vm.name_label)
|
||||
return {
|
||||
compression:
|
||||
state.compression === 'zstd' ? 'zstd' : state.compression === 'native',
|
||||
@@ -72,41 +61,13 @@ class CopyVmsModalBody extends BaseComponent {
|
||||
_onChangeNamePattern = event =>
|
||||
this.setState({ namePattern: event.target.value })
|
||||
|
||||
_getVmsWithoutZstd = createSelector(
|
||||
() => this.props.vms,
|
||||
() => this.props.containers,
|
||||
createCollectionWrapper((vms, containers) => {
|
||||
const vmIds = []
|
||||
for (const id in vms) {
|
||||
const container = containers[vms[id].$container]
|
||||
if (container !== undefined && !container.zstdSupported) {
|
||||
vmIds.push(id)
|
||||
}
|
||||
}
|
||||
return vmIds
|
||||
})
|
||||
)
|
||||
|
||||
_getVmsWithoutZstdLink = createSelector(
|
||||
this._getVmsWithoutZstd,
|
||||
vms => ({
|
||||
pathname: '/home',
|
||||
query: {
|
||||
t: 'VM',
|
||||
s: constructQueryString({
|
||||
id: {
|
||||
__or: vms,
|
||||
},
|
||||
}),
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
render() {
|
||||
const { formatMessage } = this.props.intl
|
||||
const {
|
||||
intl: { formatMessage },
|
||||
vms,
|
||||
} = this.props
|
||||
const { compression, namePattern, sr } = this.state
|
||||
const nVmsWithoutZstd =
|
||||
compression === 'zstd' ? this._getVmsWithoutZstd().length : 0
|
||||
|
||||
return process.env.XOA_PLAN > 2 ? (
|
||||
<div>
|
||||
<SingleLineRow>
|
||||
@@ -136,20 +97,7 @@ class CopyVmsModalBody extends BaseComponent {
|
||||
onChange={this.linkState('compression')}
|
||||
value={compression}
|
||||
/>
|
||||
{compression === 'zstd' && nVmsWithoutZstd > 0 && (
|
||||
<Tooltip content={_('notSupportedZstdTooltip')}>
|
||||
<Link
|
||||
className='text-warning'
|
||||
target='_blank'
|
||||
to={this._getVmsWithoutZstdLink()}
|
||||
>
|
||||
<Icon icon='alarm' />{' '}
|
||||
{_('notSupportedZstdWarning', {
|
||||
nVms: nVmsWithoutZstd,
|
||||
})}
|
||||
</Link>
|
||||
</Tooltip>
|
||||
)}
|
||||
{compression === 'zstd' && <ZstdChecker vms={vms} />}
|
||||
</Col>
|
||||
</SingleLineRow>
|
||||
</div>
|
||||
|
||||
@@ -43,6 +43,7 @@ import { noop, resolveId, resolveIds } from '../utils'
|
||||
import {
|
||||
connected,
|
||||
disconnected,
|
||||
markObjectsFetched,
|
||||
signedIn,
|
||||
signedOut,
|
||||
updateObjects,
|
||||
@@ -150,6 +151,7 @@ export const connectStore = store => {
|
||||
objects[object.id] = object
|
||||
})
|
||||
store.dispatch(updateObjects(objects))
|
||||
store.dispatch(markObjectsFetched())
|
||||
})
|
||||
})
|
||||
xo.on('notification', notification => {
|
||||
@@ -347,6 +349,10 @@ export const subscribeResourceCatalog = createSubscription(() =>
|
||||
_call('cloud.getResourceCatalog')
|
||||
)
|
||||
|
||||
export const subscribeHubResourceCatalog = createSubscription(() =>
|
||||
_call('cloud.getResourceCatalog', { filters: { hub: true } })
|
||||
)
|
||||
|
||||
const getNotificationCookie = () => {
|
||||
const notificationCookie = cookies.get(
|
||||
`notifications:${store.getState().user.id}`
|
||||
@@ -741,7 +747,10 @@ export const stopHosts = hosts => {
|
||||
title: _('stopHostsModalTitle', { nHosts }),
|
||||
body: _('stopHostsModalMessage', { nHosts }),
|
||||
}).then(
|
||||
() => map(hosts, host => _call('host.stop', { id: resolveId(host) })),
|
||||
() =>
|
||||
Promise.all(
|
||||
map(hosts, host => _call('host.stop', { id: resolveId(host) }))
|
||||
),
|
||||
noop
|
||||
)
|
||||
}
|
||||
@@ -780,7 +789,10 @@ export const emergencyShutdownHosts = hosts => {
|
||||
return confirm({
|
||||
title: _('emergencyShutdownHostsModalTitle', { nHosts }),
|
||||
body: _('emergencyShutdownHostsModalMessage', { nHosts }),
|
||||
}).then(() => map(hosts, host => emergencyShutdownHost(host)), noop)
|
||||
}).then(
|
||||
() => Promise.all(map(hosts, host => emergencyShutdownHost(host))),
|
||||
noop
|
||||
)
|
||||
}
|
||||
|
||||
export const isHostTimeConsistentWithXoaTime = host =>
|
||||
@@ -1028,7 +1040,10 @@ export const stopVms = (vms, force = false) =>
|
||||
title: _('stopVmsModalTitle', { vms: vms.length }),
|
||||
body: _('stopVmsModalMessage', { vms: vms.length }),
|
||||
}).then(
|
||||
() => map(vms, vm => _call('vm.stop', { id: resolveId(vm), force })),
|
||||
() =>
|
||||
Promise.all(
|
||||
map(vms, vm => _call('vm.stop', { id: resolveId(vm), force }))
|
||||
),
|
||||
noop
|
||||
)
|
||||
|
||||
@@ -1628,7 +1643,10 @@ export const deleteVifs = vifs =>
|
||||
title: _('deleteVifsModalTitle', { nVifs: vifs.length }),
|
||||
body: _('deleteVifsModalMessage', { nVifs: vifs.length }),
|
||||
}).then(
|
||||
() => map(vifs, vif => _call('vif.delete', { id: resolveId(vif) })),
|
||||
() =>
|
||||
Promise.all(
|
||||
map(vifs, vif => _call('vif.delete', { id: resolveId(vif) }))
|
||||
),
|
||||
noop
|
||||
)
|
||||
|
||||
@@ -1921,9 +1939,11 @@ export const deleteSchedules = schedules =>
|
||||
title: _('deleteSchedulesModalTitle', { nSchedules: schedules.length }),
|
||||
body: _('deleteSchedulesModalMessage', { nSchedules: schedules.length }),
|
||||
}).then(() =>
|
||||
map(schedules, schedule =>
|
||||
_call('schedule.delete', { id: resolveId(schedule) })::tap(
|
||||
subscribeSchedules.forceRefresh
|
||||
Promise.all(
|
||||
map(schedules, schedule =>
|
||||
_call('schedule.delete', { id: resolveId(schedule) })::tap(
|
||||
subscribeSchedules.forceRefresh
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -2855,7 +2875,10 @@ export const fixHostNotInXosanNetwork = (xosanSr, host) =>
|
||||
|
||||
// XOSAN packs -----------------------------------------------------------------
|
||||
|
||||
export const getResourceCatalog = () => _call('cloud.getResourceCatalog')
|
||||
export const getResourceCatalog = ({ filters } = {}) =>
|
||||
_call('cloud.getResourceCatalog', { filters })
|
||||
|
||||
export const getAllResourceCatalog = () => _call('cloud.getAllResourceCatalog')
|
||||
|
||||
const downloadAndInstallXosanPack = (pack, pool, { version }) =>
|
||||
_call('xosan.downloadAndInstallXosanPack', {
|
||||
@@ -2864,6 +2887,14 @@ const downloadAndInstallXosanPack = (pack, pool, { version }) =>
|
||||
pool: resolveId(pool),
|
||||
})
|
||||
|
||||
export const downloadAndInstallResource = ({ namespace, id, version, sr }) =>
|
||||
_call('cloud.downloadAndInstallResource', {
|
||||
namespace,
|
||||
id,
|
||||
version,
|
||||
sr: resolveId(sr),
|
||||
})
|
||||
|
||||
import UpdateXosanPacksModal from './update-xosan-packs-modal' // eslint-disable-line import/first
|
||||
export const updateXosanPacks = pool =>
|
||||
confirm({
|
||||
|
||||
@@ -119,6 +119,11 @@ export default class MigrateVmsModalBody extends BaseComponent {
|
||||
return network => networks[network.id]
|
||||
}
|
||||
)
|
||||
|
||||
this._getSelectedVmsPoolIds = createSelector(
|
||||
() => this.props.vms,
|
||||
vms => map(vms, '$pool')
|
||||
)
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
@@ -262,6 +267,11 @@ export default class MigrateVmsModalBody extends BaseComponent {
|
||||
_toggleSmartVifMapping = () =>
|
||||
this.setState({ smartVifMapping: !this.state.smartVifMapping })
|
||||
|
||||
compareContainers = (pool1, pool2) => {
|
||||
const poolIds = this._getSelectedVmsPoolIds()
|
||||
return poolIds.includes(pool1.id) ? -1 : poolIds.includes(pool2.id) ? 1 : 0
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
defaultSrConnectedToHost,
|
||||
@@ -281,6 +291,7 @@ export default class MigrateVmsModalBody extends BaseComponent {
|
||||
<Col size={6}>{_('migrateVmSelectHost')}</Col>
|
||||
<Col size={6}>
|
||||
<SelectHost
|
||||
compareContainers={this.compareContainers}
|
||||
onChange={this._selectHost}
|
||||
predicate={this._getHostPredicate()}
|
||||
value={host}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import _ from 'intl'
|
||||
import React from 'react'
|
||||
import BaseComponent from 'base-component'
|
||||
import { forEach } from 'lodash'
|
||||
import { createGetObjectsOfType } from 'selectors'
|
||||
import { buildTemplate, connectStore } from 'utils'
|
||||
import { compileTemplate } from '@xen-orchestra/template'
|
||||
import { connectStore } from 'utils'
|
||||
import { Container, Col, Row } from 'grid'
|
||||
import { createGetObjectsOfType } from 'selectors'
|
||||
import { forEach } from 'lodash'
|
||||
|
||||
const RULES = {
|
||||
'{date}': () => new Date().toISOString(),
|
||||
@@ -30,8 +31,8 @@ export default class SnapshotVmModalBody extends BaseComponent {
|
||||
return { names: {}, descriptions: {}, saveMemory }
|
||||
}
|
||||
|
||||
const generateName = buildTemplate(namePattern, RULES)
|
||||
const generateDescription = buildTemplate(descriptionPattern, RULES)
|
||||
const generateName = compileTemplate(namePattern, RULES)
|
||||
const generateDescription = compileTemplate(descriptionPattern, RULES)
|
||||
const names = {}
|
||||
const descriptions = {}
|
||||
|
||||
|
||||
77
packages/xo-web/src/common/zstd-checker.js
Normal file
77
packages/xo-web/src/common/zstd-checker.js
Normal file
@@ -0,0 +1,77 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
|
||||
import _ from './intl'
|
||||
import Component from './base-component'
|
||||
import constructQueryString from './construct-query-string'
|
||||
import Icon from './icon'
|
||||
import Link from './link'
|
||||
import Tooltip from './tooltip'
|
||||
import { connectStore } from './utils'
|
||||
import {
|
||||
createCollectionWrapper,
|
||||
createGetObjectsOfType,
|
||||
createSelector,
|
||||
} from './selectors'
|
||||
|
||||
@connectStore({
|
||||
containers: createSelector(
|
||||
createGetObjectsOfType('pool'),
|
||||
createGetObjectsOfType('host'),
|
||||
(pools, hosts) => ({ ...pools, ...hosts })
|
||||
),
|
||||
vms: createGetObjectsOfType('VM').pick((_, props) => props.vms),
|
||||
})
|
||||
export default class ZstdChecker extends Component {
|
||||
static propTypes = {
|
||||
vms: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
}
|
||||
|
||||
_getVmsWithoutZstd = createSelector(
|
||||
() => this.props.vms,
|
||||
() => this.props.containers,
|
||||
createCollectionWrapper((vms, containers) => {
|
||||
const vmIds = []
|
||||
for (const id in vms) {
|
||||
const container = containers[vms[id].$container]
|
||||
if (container !== undefined && !container.zstdSupported) {
|
||||
vmIds.push(id)
|
||||
}
|
||||
}
|
||||
return vmIds
|
||||
})
|
||||
)
|
||||
|
||||
_getVmsWithoutZstdLink = createSelector(
|
||||
this._getVmsWithoutZstd,
|
||||
vms => ({
|
||||
pathname: '/home',
|
||||
query: {
|
||||
t: 'VM',
|
||||
s: constructQueryString({
|
||||
id: {
|
||||
__or: vms,
|
||||
},
|
||||
}),
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
render() {
|
||||
const nVmsWithoutZstd = this._getVmsWithoutZstd().length
|
||||
return nVmsWithoutZstd > 0 ? (
|
||||
<Tooltip content={_('notSupportedZstdTooltip')}>
|
||||
<Link
|
||||
className='text-warning'
|
||||
target='_blank'
|
||||
to={this._getVmsWithoutZstdLink()}
|
||||
>
|
||||
<Icon icon='alarm' />{' '}
|
||||
{_('notSupportedZstdWarning', {
|
||||
nVms: nVmsWithoutZstd,
|
||||
})}
|
||||
</Link>
|
||||
</Tooltip>
|
||||
) : null
|
||||
}
|
||||
}
|
||||
@@ -277,6 +277,10 @@
|
||||
@extend .fa;
|
||||
@extend .fa-thumbs-up;
|
||||
}
|
||||
&-deploy {
|
||||
@extend .fa;
|
||||
@extend .fa-rocket;
|
||||
}
|
||||
|
||||
// Backups
|
||||
&-backup {
|
||||
@@ -886,6 +890,10 @@
|
||||
@extend .fa;
|
||||
@extend .fa-database;
|
||||
}
|
||||
&-menu-hub {
|
||||
@extend .fa;
|
||||
@extend .fa-cubes;
|
||||
}
|
||||
// New VM
|
||||
&-new-vm {
|
||||
&-infos {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user