Compare commits
123 Commits
xo-server-
...
fs-glob
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4c7c9f9156 | ||
|
|
093fb7f959 | ||
|
|
f6472424ad | ||
|
|
31ed3767c6 | ||
|
|
366acb65ea | ||
|
|
7c6946931b | ||
|
|
5d971433a5 | ||
|
|
05264b326b | ||
|
|
fdd5c6bfd8 | ||
|
|
42c3528c2f | ||
|
|
18640714f1 | ||
|
|
cda4d3399b | ||
|
|
4da8af6e69 | ||
|
|
b535565612 | ||
|
|
bef39b8a96 | ||
|
|
fb2502a031 | ||
|
|
0b90befda1 | ||
|
|
f9800f104a | ||
|
|
99134cc381 | ||
|
|
66ca08da6d | ||
|
|
5eb7ece6ba | ||
|
|
6b3d334e76 | ||
|
|
14f5fd8f73 | ||
|
|
5f73aee0df | ||
|
|
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 | ||
|
|
60e63a307f | ||
|
|
175e878ea6 | ||
|
|
5c960a3213 | ||
|
|
5dfb299e37 | ||
|
|
3890d4d9d1 | ||
|
|
77c62d6e7d | ||
|
|
ba54b53194 | ||
|
|
b4ef7352f2 | ||
|
|
1ce3368530 | ||
|
|
a4b32f3cb7 | ||
|
|
ee9cc05ae0 | ||
|
|
b8ccf2b0d6 | ||
|
|
886b499b94 | ||
|
|
07924d5621 | ||
|
|
43f3367ae4 | ||
|
|
454c73f42f | ||
|
|
041df698d5 | ||
|
|
97081f1219 | ||
|
|
f6792bf080 | ||
|
|
88635f31d6 | ||
|
|
abd0f115fc |
@@ -38,6 +38,15 @@ module.exports = {
|
||||
// disabled because XAPI objects are using camel case
|
||||
camelcase: ['off'],
|
||||
|
||||
'react/jsx-handler-names': 'off',
|
||||
|
||||
// disabled because not always relevant, we might reconsider in the future
|
||||
//
|
||||
// enabled by https://github.com/standard/eslint-config-standard/commit/319b177750899d4525eb1210686f6aca96190b2f
|
||||
//
|
||||
// example: https://github.com/vatesfr/xen-orchestra/blob/31ed3767c67044ca445658eb6b560718972402f2/packages/xen-api/src/index.js#L156-L157
|
||||
'lines-between-class-members': '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/",
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"golike-defer": "^0.4.1",
|
||||
"xen-api": "^0.27.1"
|
||||
"xen-api": "^0.27.2"
|
||||
},
|
||||
"scripts": {
|
||||
"postversion": "npm publish"
|
||||
|
||||
@@ -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/",
|
||||
|
||||
38
@xen-orchestra/fs/src/_parseGlob.js
Normal file
38
@xen-orchestra/fs/src/_parseGlob.js
Normal file
@@ -0,0 +1,38 @@
|
||||
import escapeRegExp from 'lodash/escapeRegExp'
|
||||
|
||||
const compileFragment = pattern =>
|
||||
new RegExp(
|
||||
`^${pattern
|
||||
.split('*')
|
||||
.map(escapeRegExp)
|
||||
.join('[^]*')}$`
|
||||
)
|
||||
|
||||
export function parseGlob(pattern) {
|
||||
const parts = []
|
||||
while (pattern.length !== 0) {
|
||||
const i = pattern.indexOf('*')
|
||||
if (i === -1) {
|
||||
parts.push(pattern)
|
||||
break
|
||||
}
|
||||
|
||||
let fragmentStart = pattern.lastIndexOf('/', i)
|
||||
if (fragmentStart === -1) {
|
||||
fragmentStart = 0
|
||||
} else {
|
||||
parts.push(pattern.slice(0, fragmentStart))
|
||||
++fragmentStart
|
||||
}
|
||||
|
||||
let fragmentEnd = pattern.indexOf('/', i)
|
||||
if (fragmentEnd === -1) {
|
||||
fragmentEnd = pattern.length
|
||||
}
|
||||
|
||||
parts.push(compileFragment(pattern.slice(fragmentStart, fragmentEnd)))
|
||||
|
||||
pattern = pattern.slice(fragmentEnd + 1)
|
||||
}
|
||||
return parts
|
||||
}
|
||||
12
@xen-orchestra/fs/src/_parseGlob.spec.js
Normal file
12
@xen-orchestra/fs/src/_parseGlob.spec.js
Normal file
@@ -0,0 +1,12 @@
|
||||
/* eslint-env jest */
|
||||
|
||||
import { parseGlob } from './_parseGlob'
|
||||
|
||||
describe('parseGlob', () => {
|
||||
it.each([['foo/*/bar*baz/qux', ['foo', /^[^]*$/, /^bar[^]*baz$/, 'qux']]])(
|
||||
'parse %j correctly',
|
||||
(pattern, result) => {
|
||||
expect(parseGlob(pattern)).toEqual(result)
|
||||
}
|
||||
)
|
||||
})
|
||||
@@ -14,6 +14,7 @@ import { type Readable, type Writable } from 'stream'
|
||||
|
||||
import normalizePath from './_normalizePath'
|
||||
import { createChecksumStream, validChecksumOfReadStream } from './checksum'
|
||||
import { parseGlob } from './_parseGlob'
|
||||
|
||||
const { dirname } = path.posix
|
||||
|
||||
@@ -258,6 +259,12 @@ export default class RemoteHandlerAbstract {
|
||||
)
|
||||
}
|
||||
|
||||
// basic glob support, only `*` is supported
|
||||
async glob(pattern) {
|
||||
const parts = parseGlob(pattern)
|
||||
// TODO
|
||||
}
|
||||
|
||||
async list(
|
||||
dir: string,
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@xen-orchestra/log",
|
||||
"version": "0.1.4",
|
||||
"version": "0.2.0",
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
"keywords": [],
|
||||
@@ -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/",
|
||||
|
||||
@@ -19,7 +19,8 @@ const createTransport = config => {
|
||||
}
|
||||
}
|
||||
|
||||
let { filter, transport } = config
|
||||
let { filter } = config
|
||||
let transport = createTransport(config.transport)
|
||||
const level = resolve(config.level)
|
||||
|
||||
if (filter !== undefined) {
|
||||
@@ -51,11 +52,12 @@ const symbol =
|
||||
? Symbol.for('@xen-orchestra/log')
|
||||
: '@@@xen-orchestra/log'
|
||||
|
||||
const { env } = process
|
||||
global[symbol] = createTransport({
|
||||
// display warnings or above, and all that are enabled via DEBUG or
|
||||
// NODE_DEBUG env
|
||||
filter: process.env.DEBUG || process.env.NODE_DEBUG,
|
||||
level: LEVELS.INFO,
|
||||
filter: [env.DEBUG, env.NODE_DEBUG].filter(Boolean).join(','),
|
||||
level: resolve(env.LOG_LEVEL, LEVELS.INFO),
|
||||
|
||||
transport: createConsoleTransport(),
|
||||
})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import createTransport from './transports/console'
|
||||
import LEVELS from './levels'
|
||||
import LEVELS, { resolve } from './levels'
|
||||
|
||||
const symbol =
|
||||
typeof Symbol !== 'undefined'
|
||||
@@ -9,7 +9,8 @@ if (!(symbol in global)) {
|
||||
// the default behavior, without requiring `configure` is to avoid
|
||||
// logging anything unless it's a real error
|
||||
const transport = createTransport()
|
||||
global[symbol] = log => log.level > LEVELS.WARN && transport(log)
|
||||
const level = resolve(process.env.LOG_LEVEL, LEVELS.WARN)
|
||||
global[symbol] = log => log.level >= level && transport(log)
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
@@ -72,5 +73,5 @@ prototype.wrap = function(message, fn) {
|
||||
}
|
||||
}
|
||||
|
||||
const createLogger = namespace => new Logger(namespace)
|
||||
export const createLogger = namespace => new Logger(namespace)
|
||||
export { createLogger as default }
|
||||
|
||||
@@ -13,11 +13,22 @@ for (const name in LEVELS) {
|
||||
NAMES[LEVELS[name]] = name
|
||||
}
|
||||
|
||||
export const resolve = level => {
|
||||
if (typeof level === 'string') {
|
||||
level = LEVELS[level.toUpperCase()]
|
||||
// resolves to the number representation of a level
|
||||
//
|
||||
// returns `defaultLevel` if invalid
|
||||
export const resolve = (level, defaultLevel) => {
|
||||
const type = typeof level
|
||||
if (type === 'number') {
|
||||
if (level in NAMES) {
|
||||
return level
|
||||
}
|
||||
} else if (type === 'string') {
|
||||
const nLevel = LEVELS[level.toUpperCase()]
|
||||
if (nLevel !== undefined) {
|
||||
return nLevel
|
||||
}
|
||||
}
|
||||
return level
|
||||
return defaultLevel
|
||||
}
|
||||
|
||||
Object.freeze(LEVELS)
|
||||
|
||||
@@ -1,26 +1,23 @@
|
||||
import LEVELS, { NAMES } from '../levels'
|
||||
|
||||
// Bind console methods (necessary for browsers)
|
||||
/* eslint-disable no-console */
|
||||
const debugConsole = console.log.bind(console)
|
||||
const infoConsole = console.info.bind(console)
|
||||
const warnConsole = console.warn.bind(console)
|
||||
const errorConsole = console.error.bind(console)
|
||||
/* eslint-enable no-console */
|
||||
|
||||
const { ERROR, INFO, WARN } = LEVELS
|
||||
|
||||
const consoleTransport = ({ data, level, namespace, message, time }) => {
|
||||
const fn =
|
||||
/* eslint-disable no-console */
|
||||
level < INFO
|
||||
? debugConsole
|
||||
? console.log
|
||||
: level < WARN
|
||||
? infoConsole
|
||||
? console.info
|
||||
: level < ERROR
|
||||
? warnConsole
|
||||
: errorConsole
|
||||
? console.warn
|
||||
: console.error
|
||||
/* eslint-enable no-console */
|
||||
|
||||
fn('%s - %s - [%s] %s', time.toISOString(), namespace, NAMES[level], message)
|
||||
data != null && fn(data)
|
||||
const args = [time.toISOString(), namespace, NAMES[level], message]
|
||||
if (data != null) {
|
||||
args.push(data)
|
||||
}
|
||||
fn.apply(console, args)
|
||||
}
|
||||
export default () => consoleTransport
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
83
CHANGELOG.md
83
CHANGELOG.md
@@ -4,26 +4,99 @@
|
||||
|
||||
### Enhancements
|
||||
|
||||
### Bug fixes
|
||||
|
||||
### Released packages
|
||||
|
||||
- xo-server v5.51.0
|
||||
- xo-web v5.51.0
|
||||
|
||||
|
||||
## **5.39.0** (2019-09-30)
|
||||
|
||||

|
||||
|
||||
### Highlights
|
||||
|
||||
- [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))
|
||||
- [Backups] Improve performance by caching VM backups listing (PR [#4509](https://github.com/vatesfr/xen-orchestra/pull/4509))
|
||||
- [HUB] VM template store [#1918](https://github.com/vatesfr/xen-orchestra/issues/1918) (PR [#4442](https://github.com/vatesfr/xen-orchestra/pull/4442))
|
||||
|
||||
### 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))
|
||||
- [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)
|
||||
- [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`
|
||||
|
||||
### 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))
|
||||
- [Host] Fix an issue where host was wrongly reporting time inconsistency (PR [#4540](https://github.com/vatesfr/xen-orchestra/pull/4540))
|
||||
|
||||
|
||||
### Released packages
|
||||
|
||||
- xen-api v0.27.2
|
||||
- xo-server-cloud v0.3.0
|
||||
- @xen-orchestra/cron v1.0.4
|
||||
- xo-server-sdn-controller v0.3.0
|
||||
- @xen-orchestra/template v0.1.0
|
||||
- xo-server v5.50.1
|
||||
- xo-web v5.50.2
|
||||
|
||||
|
||||
## **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))
|
||||
- [VM/Bulk copy] Show warning if zstd compression is not supported on a VM [#3892](https://github.com/vatesfr/xen-orchestra/issues/3892) (PR [#4346](https://github.com/vatesfr/xen-orchestra/pull/4346))
|
||||
- [VM import & Continuous Replication] Enable `guessVhdSizeOnImport` by default, this fix some `VDI_IO_ERROR` with XenServer 7.1 and XCP-ng 8.0 (PR [#4436](https://github.com/vatesfr/xen-orchestra/pull/4436))
|
||||
- [SDN Controller] Add possibility to create multiple GRE networks and VxLAN networks within a same pool (PR [#4435](https://github.com/vatesfr/xen-orchestra/pull/4435))
|
||||
- [SDN Controller] Add possibility to create cross-pool private networks (PR [#4405](https://github.com/vatesfr/xen-orchestra/pull/4405))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- [SR/General] Display VDI VM name in SR usage graph (PR [#4370](https://github.com/vatesfr/xen-orchestra/pull/4370))
|
||||
- [VM/Attach disk] Fix checking VDI mode (PR [#4373](https://github.com/vatesfr/xen-orchestra/pull/4373))
|
||||
- [VM revert] Snapshot before: add admin ACLs on created snapshot [#4331](https://github.com/vatesfr/xen-orchestra/issues/4331) (PR [#4391](https://github.com/vatesfr/xen-orchestra/pull/4391))
|
||||
- [Network] Fixed "invalid parameters" error when creating bonded network [#4425](https://github.com/vatesfr/xen-orchestra/issues/4425) (PR [#4429](https://github.com/vatesfr/xen-orchestra/pull/4429))
|
||||
|
||||
### Released packages
|
||||
|
||||
- xo-server-sdn-controller v0.2.0
|
||||
- xo-server-usage-report v0.7.3
|
||||
- xo-server v5.47.0
|
||||
- xo-web v5.47.0
|
||||
- xo-server v5.48.0
|
||||
- xo-web v5.48.1
|
||||
|
||||
## **5.37.1** (2019-08-06)
|
||||
|
||||

|
||||
|
||||
### Enhancements
|
||||
|
||||
- [SDN Controller] Let the user choose on which PIF to create a private network (PR [#4379](https://github.com/vatesfr/xen-orchestra/pull/4379))
|
||||
@@ -79,8 +152,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,15 +7,11 @@
|
||||
|
||||
> Users must be able to say: “Nice enhancement, I'm eager to test it”
|
||||
|
||||
- [VM import & Continuous Replication] Enable `guessVhdSizeOnImport` by default, this fix some `VDI_IO_ERROR` with XenServer 7.1 and XCP-ng 8.0 (PR [#4436](https://github.com/vatesfr/xen-orchestra/pull/4436))
|
||||
- [SDN Controller] Add possibility to create multiple GRE networks and VxLAN networks within a same pool (PR [#4435](https://github.com/vatesfr/xen-orchestra/pull/4435))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
> Users must be able to say: “I had this issue, happy to know it's fixed”
|
||||
|
||||
- [VM revert] Snapshot before: add admin ACLs on created snapshot [#4331](https://github.com/vatesfr/xen-orchestra/issues/4331) (PR [#4391](https://github.com/vatesfr/xen-orchestra/pull/4391))
|
||||
- [Network] Fixed "invalid parameters" error when creating bonded network [#4425](https://github.com/vatesfr/xen-orchestra/issues/4425) (PR [#4429](https://github.com/vatesfr/xen-orchestra/pull/4429))
|
||||
- [VM/new-vm] Fix template selection on creating new VM for resource sets [#4565](https://github.com/vatesfr/xen-orchestra/issues/4565) (PR [#4568](https://github.com/vatesfr/xen-orchestra/pull/4568))
|
||||
|
||||
### Released packages
|
||||
|
||||
@@ -24,6 +20,5 @@
|
||||
>
|
||||
> Rule of thumb: add packages on top.
|
||||
|
||||
- xo-server-sdn-controller v0.1.3
|
||||
- xo-server v5.48.0
|
||||
- xo-web v5.48.0
|
||||
- xo-server v5.51.0
|
||||
- xo-web v5.51.0
|
||||
|
||||
@@ -1,14 +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
|
||||
- [ ] `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
|
||||
|
||||
@@ -16,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/release-channels.png
Normal file
BIN
docs/assets/release-channels.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 99 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`
|
||||
@@ -41,6 +41,20 @@ However, if you want to start a manual check, you can do it by clicking on the "
|
||||
|
||||

|
||||
|
||||
#### Release channel
|
||||
In Xen Orchestra, you can make a choice between two different release channels.
|
||||
|
||||
##### Stable
|
||||
The stable channel is intended to be a version of Xen Orchestra that is already **one month old** (and therefore will benefit from one month of community feedback and various fixes). This way, users more concerned with the stability of their appliance will have the option to stay on a slightly older (and tested) version of XO (still supported by our pro support).
|
||||
|
||||
##### Latest
|
||||
|
||||
The latest channel will include all the latest improvements available in Xen Orchestra. The version available in latest has already been QA'd by our team, but issues may still occur once deployed in vastly varying environments, such as our user base has.
|
||||
|
||||
> To select the release channel of your choice, go to the XOA > Updates view.
|
||||
|
||||

|
||||
|
||||
#### Upgrade
|
||||
|
||||
If a new version is found, you'll have an upgrade button and its tooltip displayed:
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
"human-format": "^0.10.0",
|
||||
"lodash": "^4.17.4",
|
||||
"pw": "^0.0.4",
|
||||
"xen-api": "^0.27.1"
|
||||
"xen-api": "^0.27.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.1.5",
|
||||
@@ -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=
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "xen-api",
|
||||
"version": "0.27.1",
|
||||
"version": "0.27.2",
|
||||
"license": "ISC",
|
||||
"description": "Connector to the Xen API",
|
||||
"keywords": [
|
||||
@@ -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/",
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
"node": ">=6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@xen-orchestra/log": "^0.1.4",
|
||||
"@xen-orchestra/log": "^0.2.0",
|
||||
"human-format": "^0.10.0",
|
||||
"lodash": "^4.13.1",
|
||||
"moment-timezone": "^0.5.13"
|
||||
@@ -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/",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "xo-server-cloud",
|
||||
"version": "0.2.4",
|
||||
"version": "0.3.0",
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
"keywords": [
|
||||
@@ -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/",
|
||||
|
||||
@@ -680,7 +680,7 @@ ${entry.listItem}
|
||||
},
|
||||
}
|
||||
if (xapiObject.$type === 'VM') {
|
||||
payload['vm_uuid'] = xapiObject.uuid
|
||||
payload.vm_uuid = xapiObject.uuid
|
||||
}
|
||||
// JSON is not well formed, can't use the default node parser
|
||||
return JSON5.parse(
|
||||
|
||||
@@ -1,31 +1,14 @@
|
||||
# xo-server-sdn-controller [](https://travis-ci.org/vatesfr/xen-orchestra)
|
||||
|
||||
XO Server plugin that allows the creation of pool-wide private networks.
|
||||
XO Server plugin that allows the creation of pool-wide and cross-pool private networks.
|
||||
|
||||
## Install
|
||||
|
||||
For installing XO and the plugins from the sources, please take a look at [the documentation](https://xen-orchestra.com/docs/from_the_sources.html).
|
||||
|
||||
## Usage
|
||||
## Documentation
|
||||
|
||||
### Network creation
|
||||
|
||||
In the network creation view, select a `pool` and `Private network`.
|
||||
Create the network.
|
||||
|
||||
Choice is offer between `GRE` and `VxLAN`, if `VxLAN` is chosen, then the port 4789 must be open for UDP traffic.
|
||||
The following line needs to be added, if not already present, in `/etc/sysconfig/iptables` of all the hosts where `VxLAN` is wanted:
|
||||
`-A xapi-INPUT -p udp -m conntrack --ctstate NEW -m udp --dport 4789 -j ACCEPT`
|
||||
|
||||
### Configuration
|
||||
|
||||
Like all other xo-server plugins, it can be configured directly via
|
||||
the web interface, see [the plugin documentation](https://xen-orchestra.com/docs/plugins.html).
|
||||
|
||||
The plugin's configuration contains:
|
||||
- `cert-dir`: A path where to find the certificates to create SSL connections with the hosts.
|
||||
If none is provided, the plugin will create its own self-signed certificates.
|
||||
- `override-certs`: Whether or not to uninstall an already existing SDN controller CA certificate in order to replace it by the plugin's one.
|
||||
Please see the plugin's [official documentation](https://xen-orchestra.com/docs/sdn_controller.html).
|
||||
|
||||
## Contributions
|
||||
|
||||
|
||||
@@ -15,22 +15,24 @@
|
||||
"predev": "yarn run prebuild",
|
||||
"prepublishOnly": "yarn run build"
|
||||
},
|
||||
"version": "0.1.3",
|
||||
"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"
|
||||
},
|
||||
"dependencies": {
|
||||
"@xen-orchestra/log": "^0.1.4",
|
||||
"@xen-orchestra/log": "^0.2.0",
|
||||
"lodash": "^4.17.11",
|
||||
"node-openssl-cert": "^0.0.84",
|
||||
"promise-toolbox": "^0.13.0"
|
||||
"node-openssl-cert": "^0.0.97",
|
||||
"promise-toolbox": "^0.13.0",
|
||||
"uuid": "^3.3.2"
|
||||
},
|
||||
"private": true
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import assert from 'assert'
|
||||
import createLogger from '@xen-orchestra/log'
|
||||
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, map } from 'lodash'
|
||||
import { filter, find, forOwn, map, omitBy, sample } from 'lodash'
|
||||
import { fromCallback, fromEvent } from 'promise-toolbox'
|
||||
import { join } from 'path'
|
||||
|
||||
@@ -59,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
|
||||
}
|
||||
|
||||
@@ -69,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()
|
||||
|
||||
@@ -80,6 +139,7 @@ class SDNController extends EventEmitter {
|
||||
this._getDataDir = getDataDir
|
||||
|
||||
this._poolNetworks = []
|
||||
this._crossPoolNetworks = {}
|
||||
this._ovsdbClients = []
|
||||
this._newHosts = []
|
||||
|
||||
@@ -92,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
|
||||
}
|
||||
|
||||
@@ -146,8 +203,16 @@ class SDNController extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
load() {
|
||||
const createPrivateNetwork = this._createPrivateNetwork.bind(this)
|
||||
async load() {
|
||||
// Expose method to create pool-wide private network
|
||||
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 = {
|
||||
@@ -156,20 +221,53 @@ 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', ''],
|
||||
xoPif: ['pifId', 'PIF', ''],
|
||||
}
|
||||
this._unsetApiMethod = this._xo.addApiMethod(
|
||||
'plugin.SDNController.createPrivateNetwork',
|
||||
createPrivateNetwork
|
||||
)
|
||||
|
||||
// Expose method to create cross-pool private network
|
||||
const createCrossPoolPrivateNetwork = params =>
|
||||
this._createCrossPoolPrivateNetwork({ encrypted: false, mtu: 0, ...params })
|
||||
|
||||
createCrossPoolPrivateNetwork.description =
|
||||
'Creates a cross-pool private network on selected pools'
|
||||
createCrossPoolPrivateNetwork.params = {
|
||||
xoPoolIds: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
networkName: { type: 'string' },
|
||||
networkDescription: { type: 'string' },
|
||||
encapsulation: { type: 'string' },
|
||||
xoPifIds: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
encrypted: { type: 'boolean', optional: true },
|
||||
mtu: { type: 'integer', optional: true },
|
||||
}
|
||||
|
||||
this._unsetApiMethods = this._xo.addApiMethods({
|
||||
sdnController: {
|
||||
createCrossPoolPrivateNetwork,
|
||||
createPrivateNetwork,
|
||||
},
|
||||
})
|
||||
|
||||
// FIXME: we should monitor when xapis are added/removed
|
||||
return Promise.all(
|
||||
map(this._xo.getAllXapis(), async xapi => {
|
||||
this._xapis = this._xo.getAllXapis()
|
||||
await Promise.all(
|
||||
map(this._xapis, async xapi => {
|
||||
await xapi.objectsFetched
|
||||
|
||||
if (this._setControllerNeeded(xapi)) {
|
||||
return
|
||||
}
|
||||
@@ -180,16 +278,22 @@ class SDNController extends EventEmitter {
|
||||
this._createOvsdbClient(host)
|
||||
}
|
||||
|
||||
// Add already existing pool-wide private networks
|
||||
// Add already existing pool-wide & cross-pool private networks
|
||||
const networks = filter(xapi.objects.all, { $type: 'network' })
|
||||
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 {
|
||||
@@ -207,13 +311,11 @@ 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 xapi.call(
|
||||
'network.add_to_other_config',
|
||||
network.$ref,
|
||||
'pif_device',
|
||||
await network.update_other_config(
|
||||
'xo:sdn-controller:pif-device',
|
||||
pif.device
|
||||
)
|
||||
}
|
||||
@@ -227,6 +329,33 @@ class SDNController extends EventEmitter {
|
||||
if (center !== undefined) {
|
||||
this._starCenters.set(center.$id, center.$ref)
|
||||
}
|
||||
|
||||
const crossPoolNetworkUuid =
|
||||
otherConfig['xo:sdn-controller:cross-pool-network-uuid']
|
||||
if (crossPoolNetworkUuid !== undefined) {
|
||||
let crossPoolNetwork = this._crossPoolNetworks[
|
||||
crossPoolNetworkUuid
|
||||
]
|
||||
if (crossPoolNetwork === undefined) {
|
||||
crossPoolNetwork = {
|
||||
pools: [],
|
||||
networks: [],
|
||||
uuid: crossPoolNetworkUuid,
|
||||
}
|
||||
log.debug('Adding cross-pool network', {
|
||||
uuid: crossPoolNetworkUuid,
|
||||
})
|
||||
this._crossPoolNetworks[crossPoolNetworkUuid] = crossPoolNetwork
|
||||
}
|
||||
|
||||
crossPoolNetwork.pools.push(network.$pool.$ref)
|
||||
crossPoolNetwork.networks.push(network.$ref)
|
||||
log.debug('Pool network added to cross-pool network', {
|
||||
network: network.name_label,
|
||||
pool: network.$pool.name_label,
|
||||
uuid: crossPoolNetworkUuid,
|
||||
})
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
@@ -237,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)
|
||||
@@ -248,11 +380,18 @@ class SDNController extends EventEmitter {
|
||||
)
|
||||
})
|
||||
)
|
||||
|
||||
await Promise.all(
|
||||
map(this._crossPoolNetworks, crossPoolNetwork =>
|
||||
this._electNewPoolCenter(crossPoolNetwork)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
async unload() {
|
||||
this._ovsdbClients = []
|
||||
this._poolNetworks = []
|
||||
this._crossPoolNetworks = {}
|
||||
this._newHosts = []
|
||||
|
||||
this._networks.clear()
|
||||
@@ -261,7 +400,7 @@ class SDNController extends EventEmitter {
|
||||
this._cleaners.forEach(cleaner => cleaner())
|
||||
this._cleaners = []
|
||||
|
||||
this._unsetApiMethod()
|
||||
this._unsetApiMethods()
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
@@ -272,6 +411,9 @@ class SDNController extends EventEmitter {
|
||||
networkDescription,
|
||||
encapsulation,
|
||||
xoPif,
|
||||
vni,
|
||||
encrypted,
|
||||
mtu,
|
||||
}) {
|
||||
const pool = this._xo.getXapiObject(xoPool)
|
||||
await this._setPoolControllerIfNeeded(pool)
|
||||
@@ -282,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),
|
||||
},
|
||||
})
|
||||
|
||||
@@ -309,15 +454,77 @@ class SDNController extends EventEmitter {
|
||||
)
|
||||
|
||||
const center = await this._electNewCenter(privateNetwork, false)
|
||||
this._poolNetworks.push({
|
||||
const poolNetwork = {
|
||||
pool: pool.$ref,
|
||||
network: privateNetwork.$ref,
|
||||
starCenter: center?.$ref,
|
||||
})
|
||||
}
|
||||
this._poolNetworks.push(poolNetwork)
|
||||
this._networks.set(privateNetwork.$id, privateNetwork.$ref)
|
||||
if (center !== undefined) {
|
||||
this._starCenters.set(center.$id, center.$ref)
|
||||
}
|
||||
|
||||
return poolNetwork
|
||||
}
|
||||
|
||||
async _createCrossPoolPrivateNetwork({
|
||||
xoPoolIds,
|
||||
networkName,
|
||||
networkDescription,
|
||||
encapsulation,
|
||||
xoPifIds,
|
||||
encrypted,
|
||||
mtu,
|
||||
}) {
|
||||
const uuid = uuidv4()
|
||||
const crossPoolNetwork = {
|
||||
pools: [],
|
||||
networks: [],
|
||||
uuid,
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
const xoPifId = find(xoPifIds, id => {
|
||||
const pif = this._xo.getXapiObject(this._xo.getObject(id, 'PIF'))
|
||||
return pif.$pool.$ref === pool.$ref
|
||||
})
|
||||
const xoPif = this._xo.getObject(xoPifId, 'PIF')
|
||||
|
||||
const poolNetwork = await this._createPrivateNetwork({
|
||||
xoPool,
|
||||
networkName,
|
||||
networkDescription,
|
||||
encapsulation,
|
||||
xoPif,
|
||||
vni,
|
||||
encrypted,
|
||||
mtu,
|
||||
})
|
||||
|
||||
const network = pool.$xapi.getObjectByRef(poolNetwork.network)
|
||||
await network.update_other_config(
|
||||
'xo:sdn-controller:cross-pool-network-uuid',
|
||||
uuid
|
||||
)
|
||||
|
||||
crossPoolNetwork.pools.push(poolNetwork.pool)
|
||||
crossPoolNetwork.networks.push(poolNetwork.network)
|
||||
log.debug('Pool network added to cross-pool network', {
|
||||
network: networkName,
|
||||
pool: pool.name_label,
|
||||
uuid,
|
||||
})
|
||||
}
|
||||
|
||||
await this._electNewPoolCenter(crossPoolNetwork)
|
||||
this._crossPoolNetworks[uuid] = crossPoolNetwork
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -340,7 +547,7 @@ class SDNController extends EventEmitter {
|
||||
}
|
||||
|
||||
_objectsAdded(objects) {
|
||||
forEach(objects, object => {
|
||||
forOwn(objects, object => {
|
||||
const { $type } = object
|
||||
|
||||
if ($type === 'host') {
|
||||
@@ -358,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
|
||||
)
|
||||
@@ -407,9 +616,25 @@ class SDNController extends EventEmitter {
|
||||
this._poolNetworks = this._poolNetworks.filter(
|
||||
poolNetwork => poolNetwork.network !== networkRef
|
||||
)
|
||||
|
||||
forOwn(this._crossPoolNetworks, crossPoolNetwork => {
|
||||
crossPoolNetwork.networks = crossPoolNetwork.networks.filter(
|
||||
ref => ref !== networkRef
|
||||
)
|
||||
})
|
||||
|
||||
this._crossPoolNetworks = omitBy(
|
||||
this._crossPoolNetworks,
|
||||
crossPoolNetwork => crossPoolNetwork.networks.length === 0
|
||||
)
|
||||
}
|
||||
})
|
||||
)
|
||||
} catch (error) {
|
||||
log.error('Error in _objectsRemoved', {
|
||||
error,
|
||||
object,
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async _pifUpdated(pif) {
|
||||
@@ -421,10 +646,7 @@ class SDNController extends EventEmitter {
|
||||
|
||||
if (!pif.currently_attached) {
|
||||
const tunnel = this._getHostTunnelForNetwork(pif.$host, pif.network)
|
||||
await pif.$xapi.call('tunnel.set_status', tunnel.$ref, {
|
||||
active: 'false',
|
||||
})
|
||||
|
||||
await tunnel.set_status({ active: 'false' })
|
||||
if (poolNetwork.starCenter !== pif.host) {
|
||||
return
|
||||
}
|
||||
@@ -454,6 +676,43 @@ class SDNController extends EventEmitter {
|
||||
})
|
||||
poolNetwork.starCenter = pif.host
|
||||
this._starCenters.set(host.$id, host.$ref)
|
||||
|
||||
const crossPoolNetwork = find(
|
||||
this._crossPoolNetworks,
|
||||
crossPoolNetwork => crossPoolNetwork.networks.includes(pif.network)
|
||||
)
|
||||
if (crossPoolNetwork !== undefined) {
|
||||
if (crossPoolNetwork.poolCenter !== undefined) {
|
||||
const centerPoolNetwork = this._getPoolNetwork(
|
||||
crossPoolNetwork.poolCenter,
|
||||
crossPoolNetwork
|
||||
)
|
||||
await this._resetCrossPoolConnections(
|
||||
poolNetwork,
|
||||
crossPoolNetwork.uuid
|
||||
)
|
||||
await this._clearCenterPoolFromNetwork(
|
||||
centerPoolNetwork,
|
||||
poolNetwork,
|
||||
crossPoolNetwork.uuid
|
||||
)
|
||||
await this._connectNetworks(
|
||||
poolNetwork,
|
||||
centerPoolNetwork,
|
||||
crossPoolNetwork.uuid
|
||||
)
|
||||
} else {
|
||||
crossPoolNetwork.poolCenter = host.$pool.$ref
|
||||
log.debug(
|
||||
'First available pool becomes pool center of cross-pool network',
|
||||
{
|
||||
network: pif.network,
|
||||
pool: host.$pool.name_label,
|
||||
uuid: crossPoolNetwork.uuid,
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.debug('PIF plugged', {
|
||||
@@ -507,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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -555,8 +815,7 @@ class SDNController extends EventEmitter {
|
||||
_setControllerNeeded(xapi) {
|
||||
const controller = find(xapi.objects.all, { $type: 'SDN_controller' })
|
||||
return !(
|
||||
controller !== undefined &&
|
||||
controller.protocol === PROTOCOL &&
|
||||
controller?.protocol === PROTOCOL &&
|
||||
controller.address === '' &&
|
||||
controller.port === 0
|
||||
)
|
||||
@@ -607,6 +866,50 @@ class SDNController extends EventEmitter {
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async _electNewPoolCenter(crossPoolNetwork) {
|
||||
delete crossPoolNetwork.poolCenter
|
||||
|
||||
let centerPoolNetwork
|
||||
const crossPoolCenter = crossPoolNetwork.pools.find(poolRef => {
|
||||
centerPoolNetwork = this._getPoolNetwork(poolRef, crossPoolNetwork)
|
||||
return centerPoolNetwork.starCenter !== undefined
|
||||
})
|
||||
if (crossPoolCenter !== undefined) {
|
||||
const xapi = find(this._xapis, xapi => xapi.pool.$ref === crossPoolCenter)
|
||||
const network = xapi.getObjectByRef(centerPoolNetwork.network)
|
||||
const pool = xapi.getObjectByRef(crossPoolCenter)
|
||||
crossPoolNetwork.poolCenter = crossPoolCenter
|
||||
log.debug('New pool center in cross-pool network', {
|
||||
network: network.name_label,
|
||||
poolCenter: pool.name_label,
|
||||
uuid: crossPoolNetwork.uuid,
|
||||
})
|
||||
} else {
|
||||
log.error('No available pool center for cross-pool network', {
|
||||
uuid: crossPoolNetwork.uuid,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
await this._resetCrossPoolConnections(
|
||||
centerPoolNetwork,
|
||||
crossPoolNetwork.uuid
|
||||
)
|
||||
for (const poolRef of crossPoolNetwork.pools) {
|
||||
if (poolRef === crossPoolNetwork.poolCenter) {
|
||||
continue
|
||||
}
|
||||
|
||||
const poolNetwork = this._getPoolNetwork(poolRef, crossPoolNetwork)
|
||||
await this._resetCrossPoolConnections(poolNetwork, crossPoolNetwork.uuid)
|
||||
await this._connectNetworks(
|
||||
poolNetwork,
|
||||
centerPoolNetwork,
|
||||
crossPoolNetwork.uuid
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
async _electNewCenter(network, resetNeeded) {
|
||||
const pool = network.$pool
|
||||
|
||||
@@ -615,7 +918,7 @@ class SDNController extends EventEmitter {
|
||||
|
||||
for (const host of hosts) {
|
||||
const pif = find(host.$PIFs, { network: network.$ref })
|
||||
if (pif !== undefined && pif.currently_attached && host.$metrics.live) {
|
||||
if (pif?.currently_attached && host.$metrics.live) {
|
||||
newCenter = host
|
||||
}
|
||||
}
|
||||
@@ -633,7 +936,11 @@ class SDNController extends EventEmitter {
|
||||
)
|
||||
if (hostClient !== undefined) {
|
||||
try {
|
||||
await hostClient.resetForNetwork(network.uuid, network.name_label)
|
||||
await hostClient.resetForNetwork(
|
||||
network.uuid,
|
||||
network.name_label,
|
||||
false
|
||||
)
|
||||
} catch (error) {
|
||||
log.error('Error while resetting private network', {
|
||||
error,
|
||||
@@ -646,11 +953,25 @@ class SDNController extends EventEmitter {
|
||||
})
|
||||
)
|
||||
|
||||
const crossPoolNetwork = find(this._crossPoolNetworks, crossPoolNetwork =>
|
||||
crossPoolNetwork.networks.includes(network.$ref)
|
||||
)
|
||||
if (newCenter === undefined) {
|
||||
log.error('No available host to elect new star-center', {
|
||||
network: network.name_label,
|
||||
pool: network.$pool.name_label,
|
||||
})
|
||||
|
||||
// Re-elect a cross-pool center if needed
|
||||
if (crossPoolNetwork?.poolCenter === network.$pool.$ref) {
|
||||
const centerPoolNetwork = this._getPoolNetwork(
|
||||
crossPoolNetwork.poolCenter,
|
||||
crossPoolNetwork
|
||||
)
|
||||
delete centerPoolNetwork.starCenter
|
||||
await this._electNewPoolCenter(crossPoolNetwork)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -665,6 +986,53 @@ class SDNController extends EventEmitter {
|
||||
pool: network.$pool.name_label,
|
||||
})
|
||||
|
||||
// If the network is cross-pool: reconnect to other networks
|
||||
if (crossPoolNetwork !== undefined) {
|
||||
const centerPoolNetwork = this._getPoolNetwork(
|
||||
crossPoolNetwork.poolCenter,
|
||||
crossPoolNetwork
|
||||
)
|
||||
if (crossPoolNetwork.poolCenter === newCenter.$pool.$ref) {
|
||||
centerPoolNetwork.starCenter = newCenter.$ref
|
||||
await this._resetCrossPoolConnections(
|
||||
centerPoolNetwork,
|
||||
crossPoolNetwork.uuid
|
||||
)
|
||||
for (const poolRef of crossPoolNetwork.pools) {
|
||||
if (poolRef === crossPoolNetwork.poolCenter) {
|
||||
continue
|
||||
}
|
||||
const poolNetwork = this._getPoolNetwork(poolRef, crossPoolNetwork)
|
||||
await this._resetCrossPoolConnections(
|
||||
poolNetwork,
|
||||
crossPoolNetwork.uuid
|
||||
)
|
||||
await this._connectNetworks(
|
||||
poolNetwork,
|
||||
centerPoolNetwork,
|
||||
crossPoolNetwork.uuid
|
||||
)
|
||||
}
|
||||
} else {
|
||||
const poolNetwork = this._getPoolNetwork(
|
||||
newCenter.$pool.$ref,
|
||||
crossPoolNetwork
|
||||
)
|
||||
poolNetwork.starCenter = newCenter.$ref
|
||||
await this._resetCrossPoolConnections(poolNetwork)
|
||||
await this._clearCenterPoolFromNetwork(
|
||||
centerPoolNetwork,
|
||||
poolNetwork,
|
||||
crossPoolNetwork.uuid
|
||||
)
|
||||
await this._connectNetworks(
|
||||
poolNetwork,
|
||||
centerPoolNetwork,
|
||||
crossPoolNetwork.uuid
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return newCenter
|
||||
}
|
||||
|
||||
@@ -701,19 +1069,167 @@ class SDNController extends EventEmitter {
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async _clearCenterPoolFromNetwork(
|
||||
centerPoolNetwork,
|
||||
poolNetwork,
|
||||
crossPoolNetworkUuid
|
||||
) {
|
||||
const centerClient = find(
|
||||
this._ovsdbClients,
|
||||
client => client.host.$ref === centerPoolNetwork.starCenter
|
||||
)
|
||||
if (centerClient === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
const client = find(
|
||||
this._ovsdbClients,
|
||||
client => client.host.$ref === poolNetwork.starCenter
|
||||
)
|
||||
if (client === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
const centerNetwork = centerClient.host.$xapi.getObjectByRef(
|
||||
centerPoolNetwork.network
|
||||
)
|
||||
const remoteNetwork = client.host.$xapi.getObjectByRef(poolNetwork.network)
|
||||
try {
|
||||
await centerClient.resetForNetwork(
|
||||
centerNetwork.uuid,
|
||||
centerNetwork.name_label,
|
||||
true,
|
||||
remoteNetwork.uuid
|
||||
)
|
||||
} catch (error) {
|
||||
log.error('Error while clearing pool center from network', {
|
||||
error,
|
||||
network: remoteNetwork.name_label,
|
||||
host: client.host.name_label,
|
||||
remotePool: remoteNetwork.$pool.name_label,
|
||||
uuid: crossPoolNetworkUuid,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async _resetCrossPoolConnections(poolNetwork, uuid) {
|
||||
const client = find(
|
||||
this._ovsdbClients,
|
||||
client => client.host.$ref === poolNetwork.starCenter
|
||||
)
|
||||
if (client === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
const network = client.host.$xapi.getObjectByRef(poolNetwork.network)
|
||||
try {
|
||||
await client.resetForNetwork(network.uuid, network.name_label, true)
|
||||
} catch (error) {
|
||||
log.error('Error while resetting cross pool connections', {
|
||||
error,
|
||||
network: network.name_label,
|
||||
host: client.host.name_label,
|
||||
uuid,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async _connectNetworks(poolNetwork, centerPoolNetwork, uuid) {
|
||||
const client = find(
|
||||
this._ovsdbClients,
|
||||
client => client.host.$ref === poolNetwork.starCenter
|
||||
)
|
||||
if (client === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
const centerClient = find(
|
||||
this._ovsdbClients,
|
||||
client => client.host.$ref === centerPoolNetwork.starCenter
|
||||
)
|
||||
const centerNetwork = centerClient.host.$xapi.getObjectByRef(
|
||||
centerPoolNetwork.network
|
||||
)
|
||||
const network = client.host.$xapi.getObjectByRef(poolNetwork.network)
|
||||
|
||||
// Use centerNetwork VNI by convention
|
||||
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(
|
||||
network.uuid,
|
||||
network.name_label,
|
||||
centerClient.host.address,
|
||||
encapsulation,
|
||||
vni,
|
||||
password,
|
||||
centerNetwork.uuid
|
||||
),
|
||||
centerClient.addInterfaceAndPort(
|
||||
centerNetwork.uuid,
|
||||
centerNetwork.name_label,
|
||||
client.host.address,
|
||||
encapsulation,
|
||||
vni,
|
||||
password,
|
||||
network.uuid
|
||||
),
|
||||
])
|
||||
} catch (error) {
|
||||
log.error('Error while connecting networks', {
|
||||
error,
|
||||
network: network.name_label,
|
||||
host: client.host.name_label,
|
||||
pool: client.host.$pool.name_label,
|
||||
centerHost: centerClient.host.name_label,
|
||||
centerPool: centerClient.host.$pool.name_label,
|
||||
uuid,
|
||||
})
|
||||
return
|
||||
}
|
||||
log.debug('Networks connected', {
|
||||
network: network.name_label,
|
||||
host: client.host.name_label,
|
||||
pool: client.host.$pool.name_label,
|
||||
centerHost: centerClient.host.name_label,
|
||||
centerPool: centerClient.host.$pool.name_label,
|
||||
uuid,
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async _addHostToNetwork(host, network, starCenter) {
|
||||
if (host.$ref === starCenter.$ref) {
|
||||
// Nothing to do
|
||||
return
|
||||
}
|
||||
|
||||
const xapi = host.$xapi
|
||||
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
|
||||
)
|
||||
await xapi.call('tunnel.set_status', tunnel.$ref, { active: 'false' })
|
||||
await tunnel.set_status({ active: 'false' })
|
||||
|
||||
const hostClient = find(
|
||||
this._ovsdbClients,
|
||||
@@ -739,24 +1255,36 @@ class SDNController extends EventEmitter {
|
||||
return
|
||||
}
|
||||
|
||||
const encapsulation = network.other_config.encapsulation || 'gre'
|
||||
const { 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 hostClient.addInterfaceAndPort(
|
||||
network.uuid,
|
||||
network.name_label,
|
||||
starCenterClient.host.address,
|
||||
encapsulation,
|
||||
vni
|
||||
)
|
||||
await starCenterClient.addInterfaceAndPort(
|
||||
network.uuid,
|
||||
network.name_label,
|
||||
hostClient.host.address,
|
||||
encapsulation,
|
||||
vni
|
||||
)
|
||||
;[bridgeName] = await Promise.all([
|
||||
hostClient.addInterfaceAndPort(
|
||||
network.uuid,
|
||||
network.name_label,
|
||||
starCenterClient.host.address,
|
||||
encapsulation,
|
||||
vni,
|
||||
password
|
||||
),
|
||||
starCenterClient.addInterfaceAndPort(
|
||||
network.uuid,
|
||||
network.name_label,
|
||||
hostClient.host.address,
|
||||
encapsulation,
|
||||
vni,
|
||||
password
|
||||
),
|
||||
])
|
||||
} catch (error) {
|
||||
log.error('Error while connecting host to private network', {
|
||||
error,
|
||||
@@ -764,13 +1292,14 @@ class SDNController extends EventEmitter {
|
||||
host: host.name_label,
|
||||
pool: host.$pool.name_label,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (bridgeName !== undefined) {
|
||||
const activeStatus = { active: 'true', key: bridgeName }
|
||||
await Promise.all([
|
||||
xapi.call('tunnel.set_status', tunnel.$ref, activeStatus),
|
||||
xapi.call('tunnel.set_status', starCenterTunnel.$ref, activeStatus),
|
||||
tunnel.set_status(activeStatus),
|
||||
starCenterTunnel.set_status(activeStatus),
|
||||
])
|
||||
}
|
||||
}
|
||||
@@ -818,7 +1347,7 @@ class SDNController extends EventEmitter {
|
||||
}
|
||||
|
||||
async _hostUnreachable(host) {
|
||||
const poolNetworks = filter(this._poolNetworks, { starCenter: host.$ref })
|
||||
let poolNetworks = filter(this._poolNetworks, { starCenter: host.$ref })
|
||||
for (const poolNetwork of poolNetworks) {
|
||||
const network = host.$xapi.getObjectByRef(poolNetwork.network)
|
||||
log.debug('Unreachable star-center, electing a new one', {
|
||||
@@ -835,16 +1364,38 @@ class SDNController extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
for (const poolNetwork of this._poolNetworks) {
|
||||
const tunnel = this._getHostTunnelForNetwork(host, poolNetwork.network)
|
||||
await host.$xapi.call('tunnel.set_status', tunnel.$ref, {
|
||||
active: 'false',
|
||||
poolNetworks = this._poolNetworks.filter(
|
||||
poolNetwork => poolNetwork.pool === host.$pool.$ref
|
||||
)
|
||||
await Promise.all(
|
||||
map(poolNetworks, poolNetwork => {
|
||||
const tunnel = this._getHostTunnelForNetwork(host, poolNetwork.network)
|
||||
if (tunnel !== undefined) {
|
||||
return tunnel.set_status({ active: 'false' })
|
||||
}
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
_getPoolNetwork(poolRef, crossPoolNetwork) {
|
||||
const xapi = find(this._xapis, xapi => xapi.pool.$ref === poolRef)
|
||||
const networkRef = find(crossPoolNetwork.networks, networkRef => {
|
||||
try {
|
||||
const network = xapi.getObjectByRef(networkRef)
|
||||
return network.$pool.$ref === poolRef
|
||||
} catch (error) {
|
||||
return false
|
||||
}
|
||||
})
|
||||
|
||||
return find(this._poolNetworks, {
|
||||
pool: poolRef,
|
||||
network: networkRef,
|
||||
})
|
||||
}
|
||||
|
||||
_getHostTunnelForNetwork(host, networkRef) {
|
||||
const pif = find(host.$PIFs, { network: networkRef })
|
||||
if (pif === 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,10 +10,36 @@ 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
|
||||
this._requestId = 0
|
||||
|
||||
this._adding = []
|
||||
|
||||
@@ -45,7 +71,9 @@ export class OvsdbClient {
|
||||
networkName,
|
||||
remoteAddress,
|
||||
encapsulation,
|
||||
key
|
||||
key,
|
||||
password,
|
||||
remoteNetwork
|
||||
) {
|
||||
if (
|
||||
this._adding.find(
|
||||
@@ -58,9 +86,6 @@ export class OvsdbClient {
|
||||
this._adding.push(adding)
|
||||
|
||||
const socket = await this._connect()
|
||||
const index = this._numberOfPortAndInterface
|
||||
++this._numberOfPortAndInterface
|
||||
|
||||
const [bridgeUuid, bridgeName] = await this._getBridgeUuidForNetwork(
|
||||
networkUuid,
|
||||
networkName,
|
||||
@@ -68,7 +93,9 @@ export class OvsdbClient {
|
||||
)
|
||||
if (bridgeUuid === undefined) {
|
||||
socket.destroy()
|
||||
this._adding = this._adding.slice(this._adding.indexOf(adding), 1)
|
||||
this._adding = this._adding.filter(
|
||||
elem => elem.id !== networkUuid || elem.addr !== remoteAddress
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -80,36 +107,47 @@ export class OvsdbClient {
|
||||
)
|
||||
if (alreadyExist) {
|
||||
socket.destroy()
|
||||
this._adding = this._adding.slice(this._adding.indexOf(adding), 1)
|
||||
this._adding = this._adding.filter(
|
||||
elem => elem.id !== networkUuid || elem.addr !== remoteAddress
|
||||
)
|
||||
return bridgeName
|
||||
}
|
||||
|
||||
const interfaceName = 'tunnel_iface' + index
|
||||
const portName = 'tunnel_port' + index
|
||||
const index = ++this._numberOfPortAndInterface
|
||||
const interfaceName = bridgeName + '_iface' + index
|
||||
const portName = bridgeName + '_port' + index
|
||||
|
||||
// Add interface and port to the bridge
|
||||
const options = ['map', [['remote_ip', remoteAddress], ['key', key]]]
|
||||
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: ['map', [['private_pool_wide', 'true']]],
|
||||
},
|
||||
'uuid-name': 'new_iface',
|
||||
}
|
||||
|
||||
const addPortOperation = {
|
||||
op: 'insert',
|
||||
table: 'Port',
|
||||
row: {
|
||||
name: portName,
|
||||
interfaces: ['set', [['named-uuid', 'new_iface']]],
|
||||
other_config: ['map', [['private_pool_wide', 'true']]],
|
||||
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',
|
||||
@@ -124,7 +162,9 @@ export class OvsdbClient {
|
||||
]
|
||||
const jsonObjects = await this._sendOvsdbTransaction(params, socket)
|
||||
|
||||
this._adding = this._adding.slice(this._adding.indexOf(adding), 1)
|
||||
this._adding = this._adding.filter(
|
||||
elem => elem.id !== networkUuid || elem.addr !== remoteAddress
|
||||
)
|
||||
if (jsonObjects === undefined) {
|
||||
socket.destroy()
|
||||
return
|
||||
@@ -136,7 +176,7 @@ export class OvsdbClient {
|
||||
let opResult
|
||||
do {
|
||||
opResult = jsonObjects[0].result[i]
|
||||
if (opResult !== undefined && opResult.error !== undefined) {
|
||||
if (opResult?.error !== undefined) {
|
||||
error = opResult.error
|
||||
details = opResult.details
|
||||
}
|
||||
@@ -168,7 +208,12 @@ export class OvsdbClient {
|
||||
return bridgeName
|
||||
}
|
||||
|
||||
async resetForNetwork(networkUuid, networkName) {
|
||||
async resetForNetwork(
|
||||
networkUuid,
|
||||
networkName,
|
||||
crossPoolOnly,
|
||||
remoteNetwork
|
||||
) {
|
||||
const socket = await this._connect()
|
||||
const [bridgeUuid, bridgeName] = await this._getBridgeUuidForNetwork(
|
||||
networkUuid,
|
||||
@@ -202,7 +247,20 @@ export class OvsdbClient {
|
||||
}
|
||||
|
||||
forOwn(selectResult.other_config[1], config => {
|
||||
if (config[0] === 'private_pool_wide' && config[1] === 'true') {
|
||||
// 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]))
|
||||
|
||||
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])
|
||||
}
|
||||
})
|
||||
@@ -257,9 +315,9 @@ export class OvsdbClient {
|
||||
for (let i = pos; i < data.length; ++i) {
|
||||
const c = data.charAt(i)
|
||||
if (c === '{') {
|
||||
depth++
|
||||
++depth
|
||||
} else if (c === '}') {
|
||||
depth--
|
||||
--depth
|
||||
if (depth === 0) {
|
||||
const object = JSON.parse(buffer + data.substr(0, i + 1))
|
||||
objects.push(object)
|
||||
@@ -279,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',
|
||||
@@ -330,7 +384,7 @@ export class OvsdbClient {
|
||||
remoteAddress,
|
||||
socket
|
||||
)
|
||||
if (hasRemote === true) {
|
||||
if (hasRemote) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -438,9 +492,7 @@ export class OvsdbClient {
|
||||
|
||||
async _sendOvsdbTransaction(params, socket) {
|
||||
const stream = socket
|
||||
|
||||
const requestId = this._requestID
|
||||
++this._requestID
|
||||
const requestId = ++this._requestId
|
||||
const req = {
|
||||
id: requestId,
|
||||
method: 'transact',
|
||||
|
||||
@@ -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
|
||||
@@ -126,7 +141,8 @@ describe("user", () => {
|
||||
After each run of the tests, check that snapshots are not inadvertently modified.
|
||||
|
||||
- ⚠ Jest known issue ⚠
|
||||
If a test timeout is triggered the next async tests can fail, it is due to an inadvertently modified snapshots.
|
||||
If a test timeout is triggered the next async tests can fail, it's due to an inadvertently modified snapshots.
|
||||
As a workaround, you can clean your git working tree and re-run jest using a large timeout: `> yarn test --testTimeout=100000`
|
||||
|
||||
## Contributions
|
||||
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
email = ''
|
||||
password = ''
|
||||
|
||||
[pools]
|
||||
default = ''
|
||||
|
||||
[servers]
|
||||
[servers.default]
|
||||
username = ''
|
||||
@@ -11,12 +14,16 @@
|
||||
|
||||
[vms]
|
||||
default = ''
|
||||
# vmToBackup = ''
|
||||
|
||||
[templates]
|
||||
default = ''
|
||||
templateWithoutDisks = ''
|
||||
|
||||
[srs]
|
||||
default = ''
|
||||
|
||||
[remotes]
|
||||
default = { name = '', url = '' }
|
||||
remote1 = { name = '', url = '' }
|
||||
# remote2 = { name = '', url = '' }
|
||||
|
||||
@@ -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) {
|
||||
@@ -157,6 +170,50 @@ class XoConnection extends Xo {
|
||||
return find(await this.call('schedule.getAll'), predicate)
|
||||
}
|
||||
|
||||
async runBackupJob(jobId, scheduleId, { remotes, nExecutions = 1 }) {
|
||||
for (let i = 0; i < nExecutions; i++) {
|
||||
await xo.call('backupNg.runJob', { id: jobId, schedule: scheduleId })
|
||||
}
|
||||
const backups = {}
|
||||
if (remotes !== undefined) {
|
||||
const backupsByRemote = await xo.call('backupNg.listVmBackups', {
|
||||
remotes,
|
||||
})
|
||||
forOwn(backupsByRemote, (backupsByVm, remoteId) => {
|
||||
backups[remoteId] = []
|
||||
forOwn(backupsByVm, vmBackups => {
|
||||
vmBackups.forEach(
|
||||
({ jobId: backupJobId, scheduleId: backupScheduleId, id }) => {
|
||||
if (jobId === backupJobId && scheduleId === backupScheduleId) {
|
||||
this._tempResourceDisposers.push('backupNg.deleteVmBackup', {
|
||||
id,
|
||||
})
|
||||
backups[remoteId].push(id)
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
forOwn(this.objects.all, (obj, id) => {
|
||||
if (
|
||||
obj.other !== undefined &&
|
||||
obj.other['xo:backup:job'] === jobId &&
|
||||
obj.other['xo:backup:schedule'] === scheduleId
|
||||
) {
|
||||
this._tempResourceDisposers.push('vm.delete', {
|
||||
id,
|
||||
})
|
||||
}
|
||||
})
|
||||
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--]
|
||||
|
||||
@@ -127,6 +127,375 @@ Object {
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 1`] = `
|
||||
Object {
|
||||
"data": Object {
|
||||
"mode": "delta",
|
||||
"reportWhen": "never",
|
||||
},
|
||||
"end": Any<Number>,
|
||||
"id": Any<String>,
|
||||
"jobId": Any<String>,
|
||||
"message": "backup",
|
||||
"scheduleId": Any<String>,
|
||||
"start": Any<Number>,
|
||||
"status": "success",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 2`] = `
|
||||
Object {
|
||||
"data": Object {
|
||||
"id": Any<String>,
|
||||
"type": "VM",
|
||||
},
|
||||
"end": Any<Number>,
|
||||
"id": Any<String>,
|
||||
"message": Any<String>,
|
||||
"start": Any<Number>,
|
||||
"status": "success",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 3`] = `
|
||||
Object {
|
||||
"end": Any<Number>,
|
||||
"id": Any<String>,
|
||||
"message": "snapshot",
|
||||
"result": Any<String>,
|
||||
"start": Any<Number>,
|
||||
"status": "success",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 4`] = `
|
||||
Object {
|
||||
"data": Object {
|
||||
"id": Any<String>,
|
||||
"isFull": true,
|
||||
"type": "remote",
|
||||
},
|
||||
"end": Any<Number>,
|
||||
"id": Any<String>,
|
||||
"message": Any<String>,
|
||||
"start": Any<Number>,
|
||||
"status": "success",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 5`] = `
|
||||
Object {
|
||||
"end": Any<Number>,
|
||||
"id": Any<String>,
|
||||
"message": Any<String>,
|
||||
"result": Object {
|
||||
"size": Any<Number>,
|
||||
},
|
||||
"start": Any<Number>,
|
||||
"status": "success",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 6`] = `
|
||||
Object {
|
||||
"end": Any<Number>,
|
||||
"id": Any<String>,
|
||||
"message": Any<String>,
|
||||
"result": Object {
|
||||
"size": Any<Number>,
|
||||
},
|
||||
"start": Any<Number>,
|
||||
"status": "success",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 7`] = `
|
||||
Object {
|
||||
"data": Object {
|
||||
"id": Any<String>,
|
||||
"isFull": true,
|
||||
"type": "remote",
|
||||
},
|
||||
"end": Any<Number>,
|
||||
"id": Any<String>,
|
||||
"message": Any<String>,
|
||||
"start": Any<Number>,
|
||||
"status": "success",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 8`] = `
|
||||
Object {
|
||||
"end": Any<Number>,
|
||||
"id": Any<String>,
|
||||
"message": Any<String>,
|
||||
"result": Object {
|
||||
"size": Any<Number>,
|
||||
},
|
||||
"start": Any<Number>,
|
||||
"status": "success",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 9`] = `
|
||||
Object {
|
||||
"end": Any<Number>,
|
||||
"id": Any<String>,
|
||||
"message": Any<String>,
|
||||
"result": Object {
|
||||
"size": Any<Number>,
|
||||
},
|
||||
"start": Any<Number>,
|
||||
"status": "success",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 10`] = `
|
||||
Object {
|
||||
"data": Object {
|
||||
"mode": "delta",
|
||||
"reportWhen": "never",
|
||||
},
|
||||
"end": Any<Number>,
|
||||
"id": Any<String>,
|
||||
"jobId": Any<String>,
|
||||
"message": "backup",
|
||||
"scheduleId": Any<String>,
|
||||
"start": Any<Number>,
|
||||
"status": "success",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 11`] = `
|
||||
Object {
|
||||
"data": Object {
|
||||
"id": Any<String>,
|
||||
"type": "VM",
|
||||
},
|
||||
"end": Any<Number>,
|
||||
"id": Any<String>,
|
||||
"message": Any<String>,
|
||||
"start": Any<Number>,
|
||||
"status": "success",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 12`] = `
|
||||
Object {
|
||||
"end": Any<Number>,
|
||||
"id": Any<String>,
|
||||
"message": "snapshot",
|
||||
"result": Any<String>,
|
||||
"start": Any<Number>,
|
||||
"status": "success",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 13`] = `
|
||||
Object {
|
||||
"data": Object {
|
||||
"id": Any<String>,
|
||||
"isFull": false,
|
||||
"type": "remote",
|
||||
},
|
||||
"end": Any<Number>,
|
||||
"id": Any<String>,
|
||||
"message": Any<String>,
|
||||
"start": Any<Number>,
|
||||
"status": "success",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 14`] = `
|
||||
Object {
|
||||
"end": Any<Number>,
|
||||
"id": Any<String>,
|
||||
"message": Any<String>,
|
||||
"result": Object {
|
||||
"size": Any<Number>,
|
||||
},
|
||||
"start": Any<Number>,
|
||||
"status": "success",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 15`] = `
|
||||
Object {
|
||||
"end": Any<Number>,
|
||||
"id": Any<String>,
|
||||
"message": Any<String>,
|
||||
"result": Object {
|
||||
"size": Any<Number>,
|
||||
},
|
||||
"start": Any<Number>,
|
||||
"status": "success",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 16`] = `
|
||||
Object {
|
||||
"data": Object {
|
||||
"id": Any<String>,
|
||||
"isFull": false,
|
||||
"type": "remote",
|
||||
},
|
||||
"end": Any<Number>,
|
||||
"id": Any<String>,
|
||||
"message": Any<String>,
|
||||
"start": Any<Number>,
|
||||
"status": "success",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 17`] = `
|
||||
Object {
|
||||
"end": Any<Number>,
|
||||
"id": Any<String>,
|
||||
"message": Any<String>,
|
||||
"result": Object {
|
||||
"size": Any<Number>,
|
||||
},
|
||||
"start": Any<Number>,
|
||||
"status": "success",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 18`] = `
|
||||
Object {
|
||||
"end": Any<Number>,
|
||||
"id": Any<String>,
|
||||
"message": Any<String>,
|
||||
"result": Object {
|
||||
"size": Any<Number>,
|
||||
},
|
||||
"start": Any<Number>,
|
||||
"status": "success",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 19`] = `
|
||||
Object {
|
||||
"data": Object {
|
||||
"mode": "delta",
|
||||
"reportWhen": "never",
|
||||
},
|
||||
"end": Any<Number>,
|
||||
"id": Any<String>,
|
||||
"jobId": Any<String>,
|
||||
"message": "backup",
|
||||
"scheduleId": Any<String>,
|
||||
"start": Any<Number>,
|
||||
"status": "success",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 20`] = `
|
||||
Object {
|
||||
"data": Object {
|
||||
"id": Any<String>,
|
||||
"type": "VM",
|
||||
},
|
||||
"end": Any<Number>,
|
||||
"id": Any<String>,
|
||||
"message": Any<String>,
|
||||
"start": Any<Number>,
|
||||
"status": "success",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 21`] = `
|
||||
Object {
|
||||
"end": Any<Number>,
|
||||
"id": Any<String>,
|
||||
"message": "snapshot",
|
||||
"result": Any<String>,
|
||||
"start": Any<Number>,
|
||||
"status": "success",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 22`] = `
|
||||
Object {
|
||||
"data": Object {
|
||||
"id": Any<String>,
|
||||
"isFull": true,
|
||||
"type": "remote",
|
||||
},
|
||||
"end": Any<Number>,
|
||||
"id": Any<String>,
|
||||
"message": Any<String>,
|
||||
"start": Any<Number>,
|
||||
"status": "success",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 23`] = `
|
||||
Object {
|
||||
"end": Any<Number>,
|
||||
"id": Any<String>,
|
||||
"message": Any<String>,
|
||||
"result": Object {
|
||||
"size": Any<Number>,
|
||||
},
|
||||
"start": Any<Number>,
|
||||
"status": "success",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 24`] = `
|
||||
Object {
|
||||
"end": Any<Number>,
|
||||
"id": Any<String>,
|
||||
"message": Any<String>,
|
||||
"result": Object {
|
||||
"size": Any<Number>,
|
||||
},
|
||||
"start": Any<Number>,
|
||||
"status": "success",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 25`] = `
|
||||
Object {
|
||||
"data": Object {
|
||||
"id": Any<String>,
|
||||
"isFull": true,
|
||||
"type": "remote",
|
||||
},
|
||||
"end": Any<Number>,
|
||||
"id": Any<String>,
|
||||
"message": Any<String>,
|
||||
"start": Any<Number>,
|
||||
"status": "success",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 26`] = `
|
||||
Object {
|
||||
"end": Any<Number>,
|
||||
"id": Any<String>,
|
||||
"message": Any<String>,
|
||||
"result": Object {
|
||||
"size": Any<Number>,
|
||||
},
|
||||
"start": Any<Number>,
|
||||
"status": "success",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 27`] = `
|
||||
Object {
|
||||
"end": Any<Number>,
|
||||
"id": Any<String>,
|
||||
"message": Any<String>,
|
||||
"result": Object {
|
||||
"size": Any<Number>,
|
||||
},
|
||||
"start": Any<Number>,
|
||||
"status": "success",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg execute three times a rolling snapshot with 2 as retention & revert to an old state 1`] = `
|
||||
Object {
|
||||
"data": Object {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/* eslint-env jest */
|
||||
|
||||
import { forOwn } from 'lodash'
|
||||
import { noSuchObject } from 'xo-common/api-errors'
|
||||
|
||||
import config from '../_config'
|
||||
@@ -11,6 +12,64 @@ const DEFAULT_SCHEDULE = {
|
||||
cron: '0 * * * * *',
|
||||
}
|
||||
|
||||
const validateRootTask = (log, props) =>
|
||||
expect(log).toMatchSnapshot({
|
||||
end: expect.any(Number),
|
||||
id: expect.any(String),
|
||||
jobId: expect.any(String),
|
||||
scheduleId: expect.any(String),
|
||||
start: expect.any(Number),
|
||||
...props,
|
||||
})
|
||||
|
||||
const validateVmTask = (task, vmId, props) => {
|
||||
expect(task).toMatchSnapshot({
|
||||
data: {
|
||||
id: expect.any(String),
|
||||
},
|
||||
end: expect.any(Number),
|
||||
id: expect.any(String),
|
||||
message: expect.any(String),
|
||||
start: expect.any(Number),
|
||||
...props,
|
||||
})
|
||||
expect(task.data.id).toBe(vmId)
|
||||
}
|
||||
|
||||
const validateSnapshotTask = (task, props) =>
|
||||
expect(task).toMatchSnapshot({
|
||||
end: expect.any(Number),
|
||||
id: expect.any(String),
|
||||
result: expect.any(String),
|
||||
start: expect.any(Number),
|
||||
...props,
|
||||
})
|
||||
|
||||
const validateExportTask = (task, srOrRemoteIds, props) => {
|
||||
expect(task).toMatchSnapshot({
|
||||
end: expect.any(Number),
|
||||
id: expect.any(String),
|
||||
message: expect.any(String),
|
||||
start: expect.any(Number),
|
||||
...props,
|
||||
})
|
||||
expect(srOrRemoteIds).toContain(task.data.id)
|
||||
}
|
||||
|
||||
const validateOperationTask = (task, props) => {
|
||||
expect(task).toMatchSnapshot({
|
||||
end: expect.any(Number),
|
||||
id: expect.any(String),
|
||||
message: expect.any(String),
|
||||
start: expect.any(Number),
|
||||
...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
|
||||
|
||||
@@ -162,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()
|
||||
@@ -171,10 +230,10 @@ 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.default,
|
||||
template: config.templates.templateWithoutDisks,
|
||||
})
|
||||
|
||||
const scheduleTempId = randomId()
|
||||
@@ -201,7 +260,7 @@ describe('backupNg', () => {
|
||||
tasks: [vmTask],
|
||||
...log
|
||||
},
|
||||
] = await xo.call('backupNg.getLogs', {
|
||||
] = await xo.getBackupLogs({
|
||||
jobId,
|
||||
scheduleId: schedule.id,
|
||||
})
|
||||
@@ -260,7 +319,7 @@ describe('backupNg', () => {
|
||||
tasks: [task],
|
||||
...log
|
||||
},
|
||||
] = await xo.call('backupNg.getLogs', {
|
||||
] = await xo.getBackupLogs({
|
||||
jobId,
|
||||
scheduleId: schedule.id,
|
||||
})
|
||||
@@ -292,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,
|
||||
@@ -309,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 [
|
||||
@@ -355,7 +415,7 @@ describe('backupNg', () => {
|
||||
tasks: [{ tasks: subTasks, ...vmTask }],
|
||||
...log
|
||||
},
|
||||
] = await xo.call('backupNg.getLogs', {
|
||||
] = await xo.getBackupLogs({
|
||||
jobId,
|
||||
scheduleId: schedule.id,
|
||||
})
|
||||
@@ -387,6 +447,142 @@ 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 () => {
|
||||
jest.setTimeout(12e5)
|
||||
const {
|
||||
vms: { default: defaultVm, vmToBackup = defaultVm },
|
||||
remotes: { default: defaultRemote, remote1, remote2 = defaultRemote },
|
||||
servers: { default: defaultServer },
|
||||
} = config
|
||||
|
||||
expect(vmToBackup).not.toBe(undefined)
|
||||
expect(remote1).not.toBe(undefined)
|
||||
expect(remote2).not.toBe(undefined)
|
||||
|
||||
await xo.createTempServer(defaultServer)
|
||||
const { id: remoteId1 } = await xo.createTempRemote(remote1)
|
||||
const { id: remoteId2 } = await xo.createTempRemote(remote2)
|
||||
const remotes = [remoteId1, remoteId2]
|
||||
|
||||
const exportRetention = 2
|
||||
const fullInterval = 2
|
||||
const scheduleTempId = randomId()
|
||||
const { id: jobId } = await xo.createTempBackupNgJob({
|
||||
mode: 'delta',
|
||||
remotes: {
|
||||
id: {
|
||||
__or: remotes,
|
||||
},
|
||||
},
|
||||
schedules: {
|
||||
[scheduleTempId]: DEFAULT_SCHEDULE,
|
||||
},
|
||||
settings: {
|
||||
'': {
|
||||
bypassVdiChainsCheck: true,
|
||||
fullInterval,
|
||||
reportWhen: 'never',
|
||||
},
|
||||
[remoteId1]: { deleteFirst: true },
|
||||
[scheduleTempId]: { exportRetention },
|
||||
},
|
||||
vms: {
|
||||
id: vmToBackup,
|
||||
},
|
||||
})
|
||||
|
||||
const schedule = await xo.getSchedule({ jobId })
|
||||
expect(typeof schedule).toBe('object')
|
||||
|
||||
const nExecutions = 3
|
||||
const backupsByRemote = await xo.runBackupJob(jobId, schedule.id, {
|
||||
remotes,
|
||||
nExecutions,
|
||||
})
|
||||
forOwn(backupsByRemote, backups =>
|
||||
expect(backups.length).toBe(exportRetention)
|
||||
)
|
||||
|
||||
const backupLogs = await xo.getBackupLogs({
|
||||
jobId,
|
||||
scheduleId: schedule.id,
|
||||
})
|
||||
expect(backupLogs.length).toBe(nExecutions)
|
||||
|
||||
backupLogs.forEach(({ tasks = [], ...log }, key) => {
|
||||
validateRootTask(log, {
|
||||
data: {
|
||||
mode: 'delta',
|
||||
reportWhen: 'never',
|
||||
},
|
||||
message: 'backup',
|
||||
status: 'success',
|
||||
})
|
||||
|
||||
const numberOfTasks = {
|
||||
export: 0,
|
||||
merge: 0,
|
||||
snapshot: 0,
|
||||
transfer: 0,
|
||||
vm: 0,
|
||||
}
|
||||
tasks.forEach(({ tasks = [], ...vmTask }) => {
|
||||
if (vmTask.data !== undefined && vmTask.data.type === 'VM') {
|
||||
validateVmTask(vmTask, vmToBackup, { status: 'success' })
|
||||
numberOfTasks.vm++
|
||||
tasks.forEach(({ tasks = [], ...subTask }) => {
|
||||
if (subTask.message === 'snapshot') {
|
||||
validateSnapshotTask(subTask, { status: 'success' })
|
||||
numberOfTasks.snapshot++
|
||||
}
|
||||
if (subTask.message === 'export') {
|
||||
validateExportTask(subTask, remotes, {
|
||||
data: {
|
||||
id: expect.any(String),
|
||||
isFull: key % fullInterval === 0,
|
||||
type: 'remote',
|
||||
},
|
||||
status: 'success',
|
||||
})
|
||||
numberOfTasks.export++
|
||||
let mergeTaskKey, transferTaskKey
|
||||
tasks.forEach((operationTask, key) => {
|
||||
if (
|
||||
operationTask.message === 'transfer' ||
|
||||
operationTask.message === 'merge'
|
||||
) {
|
||||
validateOperationTask(operationTask, {
|
||||
result: { size: expect.any(Number) },
|
||||
status: 'success',
|
||||
})
|
||||
if (operationTask.message === 'transfer') {
|
||||
mergeTaskKey = key
|
||||
numberOfTasks.merge++
|
||||
} else {
|
||||
transferTaskKey = key
|
||||
numberOfTasks.transfer++
|
||||
}
|
||||
}
|
||||
})
|
||||
expect(
|
||||
subTask.data.id === remoteId1
|
||||
? mergeTaskKey > transferTaskKey
|
||||
: mergeTaskKey < transferTaskKey
|
||||
).toBe(true)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
expect(numberOfTasks).toEqual({
|
||||
export: 2,
|
||||
merge: 2,
|
||||
snapshot: 1,
|
||||
transfer: 2,
|
||||
vm: 1,
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
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 })
|
||||
})
|
||||
})
|
||||
@@ -209,7 +209,7 @@ describe('job', () => {
|
||||
})
|
||||
|
||||
it('runs a job', async () => {
|
||||
jest.setTimeout(7e3)
|
||||
jest.setTimeout(7e4)
|
||||
await xo.createTempServer(config.servers.default)
|
||||
const jobId = await xo.createTempJob(defaultJob)
|
||||
const snapshots = xo.objects.all[config.vms.default].snapshots
|
||||
|
||||
@@ -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,8 +36,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@xen-orchestra/async-map": "^0.0.0",
|
||||
"@xen-orchestra/cron": "^1.0.3",
|
||||
"@xen-orchestra/log": "^0.1.4",
|
||||
"@xen-orchestra/cron": "^1.0.4",
|
||||
"@xen-orchestra/log": "^0.2.0",
|
||||
"handlebars": "^4.0.6",
|
||||
"html-minifier": "^4.0.0",
|
||||
"human-format": "^0.10.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.1",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "Server part of Xen-Orchestra",
|
||||
"keywords": [
|
||||
@@ -35,17 +35,18 @@
|
||||
"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",
|
||||
"@xen-orchestra/log": "^0.1.4",
|
||||
"@xen-orchestra/log": "^0.2.0",
|
||||
"@xen-orchestra/mixin": "^0.0.0",
|
||||
"ajv": "^6.1.1",
|
||||
"app-conf": "^0.7.0",
|
||||
"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",
|
||||
@@ -123,7 +124,7 @@
|
||||
"value-matcher": "^0.2.0",
|
||||
"vhd-lib": "^0.7.0",
|
||||
"ws": "^6.0.0",
|
||||
"xen-api": "^0.27.1",
|
||||
"xen-api": "^0.27.2",
|
||||
"xml2js": "^0.4.19",
|
||||
"xo-acl-resolver": "^0.4.1",
|
||||
"xo-collection": "^0.4.1",
|
||||
@@ -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,
|
||||
|
||||
@@ -777,7 +777,7 @@ export async function probeIscsiExists({
|
||||
)
|
||||
|
||||
const srs = []
|
||||
forEach(ensureArray(xml['SRlist'].SR), sr => {
|
||||
forEach(ensureArray(xml.SRlist.SR), sr => {
|
||||
// get the UUID of SR connected to this LUN
|
||||
srs.push({ uuid: sr.UUID.trim() })
|
||||
})
|
||||
@@ -845,7 +845,7 @@ export async function probeNfsExists({ host, server, serverPath }) {
|
||||
|
||||
const srs = []
|
||||
|
||||
forEach(ensureArray(xml['SRlist'].SR), sr => {
|
||||
forEach(ensureArray(xml.SRlist.SR), sr => {
|
||||
// get the UUID of SR connected to this LUN
|
||||
srs.push({ uuid: sr.UUID.trim() })
|
||||
})
|
||||
|
||||
@@ -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`
|
||||
),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ async function rateLimitedRetry(action, shouldRetry, retryCount = 20) {
|
||||
function createVolumeInfoTypes() {
|
||||
function parseHeal(parsed) {
|
||||
const bricks = []
|
||||
parsed['healInfo']['bricks']['brick'].forEach(brick => {
|
||||
parsed.healInfo.bricks.brick.forEach(brick => {
|
||||
bricks.push(brick)
|
||||
if (brick.file) {
|
||||
brick.file = ensureArray(brick.file)
|
||||
@@ -96,21 +96,21 @@ function createVolumeInfoTypes() {
|
||||
|
||||
function parseStatus(parsed) {
|
||||
const brickDictByUuid = {}
|
||||
const volume = parsed['volStatus']['volumes']['volume']
|
||||
volume['node'].forEach(node => {
|
||||
const volume = parsed.volStatus.volumes.volume
|
||||
volume.node.forEach(node => {
|
||||
brickDictByUuid[node.peerid] = brickDictByUuid[node.peerid] || []
|
||||
brickDictByUuid[node.peerid].push(node)
|
||||
})
|
||||
return {
|
||||
commandStatus: true,
|
||||
result: { nodes: brickDictByUuid, tasks: volume['tasks'] },
|
||||
result: { nodes: brickDictByUuid, tasks: volume.tasks },
|
||||
}
|
||||
}
|
||||
|
||||
async function parseInfo(parsed) {
|
||||
const volume = parsed['volInfo']['volumes']['volume']
|
||||
volume['bricks'] = volume['bricks']['brick']
|
||||
volume['options'] = volume['options']['option']
|
||||
const volume = parsed.volInfo.volumes.volume
|
||||
volume.bricks = volume.bricks.brick
|
||||
volume.options = volume.options.option
|
||||
return { commandStatus: true, result: volume }
|
||||
}
|
||||
|
||||
@@ -118,23 +118,23 @@ function createVolumeInfoTypes() {
|
||||
return async function(sr) {
|
||||
const glusterEndpoint = this::_getGlusterEndpoint(sr)
|
||||
const cmdShouldRetry = result =>
|
||||
!result['commandStatus'] &&
|
||||
((result.parsed && result.parsed['cliOutput']['opErrno'] === '30802') ||
|
||||
!result.commandStatus &&
|
||||
((result.parsed && result.parsed.cliOutput.opErrno === '30802') ||
|
||||
result.stderr.match(/Another transaction is in progress/))
|
||||
const runCmd = async () =>
|
||||
glusterCmd(glusterEndpoint, 'volume ' + command, true)
|
||||
const commandResult = await rateLimitedRetry(runCmd, cmdShouldRetry, 30)
|
||||
return commandResult['commandStatus']
|
||||
? this::handler(commandResult.parsed['cliOutput'], sr)
|
||||
return commandResult.commandStatus
|
||||
? this::handler(commandResult.parsed.cliOutput, sr)
|
||||
: commandResult
|
||||
}
|
||||
}
|
||||
|
||||
async function profileType(sr) {
|
||||
async function parseProfile(parsed) {
|
||||
const volume = parsed['volProfile']
|
||||
volume['bricks'] = ensureArray(volume['brick'])
|
||||
delete volume['brick']
|
||||
const volume = parsed.volProfile
|
||||
volume.bricks = ensureArray(volume.brick)
|
||||
delete volume.brick
|
||||
return { commandStatus: true, result: volume }
|
||||
}
|
||||
|
||||
@@ -143,9 +143,9 @@ function createVolumeInfoTypes() {
|
||||
|
||||
async function profileTopType(sr) {
|
||||
async function parseTop(parsed) {
|
||||
const volume = parsed['volTop']
|
||||
volume['bricks'] = ensureArray(volume['brick'])
|
||||
delete volume['brick']
|
||||
const volume = parsed.volTop
|
||||
volume.bricks = ensureArray(volume.brick)
|
||||
delete volume.brick
|
||||
return { commandStatus: true, result: volume }
|
||||
}
|
||||
|
||||
@@ -326,7 +326,7 @@ async function remoteSsh(glusterEndpoint, cmd, ignoreError = false) {
|
||||
}
|
||||
messageArray.push(`${key}: ${result[key]}`)
|
||||
}
|
||||
messageArray.push('command: ' + result['command'].join(' '))
|
||||
messageArray.push('command: ' + result.command.join(' '))
|
||||
messageKeys.splice(messageKeys.indexOf('command'), 1)
|
||||
for (const key of messageKeys) {
|
||||
messageArray.push(`${key}: ${JSON.stringify(result[key])}`)
|
||||
@@ -343,7 +343,7 @@ async function remoteSsh(glusterEndpoint, cmd, ignoreError = false) {
|
||||
})
|
||||
break
|
||||
} catch (exception) {
|
||||
if (exception['code'] !== 'HOST_OFFLINE') {
|
||||
if (exception.code !== 'HOST_OFFLINE') {
|
||||
throw exception
|
||||
}
|
||||
}
|
||||
@@ -370,19 +370,17 @@ async function remoteSsh(glusterEndpoint, cmd, ignoreError = false) {
|
||||
}
|
||||
|
||||
function findErrorMessage(commandResut) {
|
||||
if (commandResut['exit'] === 0 && commandResut.parsed) {
|
||||
const cliOut = commandResut.parsed['cliOutput']
|
||||
if (cliOut['opErrstr'] && cliOut['opErrstr'].length) {
|
||||
return cliOut['opErrstr']
|
||||
if (commandResut.exit === 0 && commandResut.parsed) {
|
||||
const cliOut = commandResut.parsed.cliOutput
|
||||
if (cliOut.opErrstr && cliOut.opErrstr.length) {
|
||||
return cliOut.opErrstr
|
||||
}
|
||||
// "peer probe" returns it's "already in peer" error in cliOutput/output
|
||||
if (cliOut['output'] && cliOut['output'].length) {
|
||||
return cliOut['output']
|
||||
if (cliOut.output && cliOut.output.length) {
|
||||
return cliOut.output
|
||||
}
|
||||
}
|
||||
return commandResut['stderr'].length
|
||||
? commandResut['stderr']
|
||||
: commandResut['stdout']
|
||||
return commandResut.stderr.length ? commandResut.stderr : commandResut.stdout
|
||||
}
|
||||
|
||||
async function glusterCmd(glusterEndpoint, cmd, ignoreError = false) {
|
||||
@@ -392,15 +390,15 @@ async function glusterCmd(glusterEndpoint, cmd, ignoreError = false) {
|
||||
true
|
||||
)
|
||||
try {
|
||||
result.parsed = parseXml(result['stdout'])
|
||||
result.parsed = parseXml(result.stdout)
|
||||
} catch (e) {
|
||||
// pass, we never know if a message can be parsed or not, so we just try
|
||||
}
|
||||
if (result['exit'] === 0) {
|
||||
const cliOut = result.parsed['cliOutput']
|
||||
if (result.exit === 0) {
|
||||
const cliOut = result.parsed.cliOutput
|
||||
// we have found cases where opErrno is !=0 and opRet was 0, albeit the operation was an error.
|
||||
result.commandStatus =
|
||||
cliOut['opRet'].trim() === '0' && cliOut['opErrno'].trim() === '0'
|
||||
cliOut.opRet.trim() === '0' && cliOut.opErrno.trim() === '0'
|
||||
result.error = findErrorMessage(result)
|
||||
} else {
|
||||
result.commandStatus = false
|
||||
@@ -793,7 +791,7 @@ export const createSR = defer(async function(
|
||||
host: param.host.$id,
|
||||
vm: { id: param.vm.$id, ip: param.address },
|
||||
underlyingSr: param.underlyingSr.$id,
|
||||
arbiter: !!param['arbiter'],
|
||||
arbiter: !!param.arbiter,
|
||||
}))
|
||||
await xapi.xo.setData(xosanSrRef, 'xosan_config', {
|
||||
version: 'beta2',
|
||||
@@ -821,12 +819,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 +1162,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',
|
||||
@@ -1298,7 +1298,7 @@ export const addBricks = defer(async function(
|
||||
underlyingSr: newSr,
|
||||
})
|
||||
}
|
||||
const arbiterNode = data.nodes.find(n => n['arbiter'])
|
||||
const arbiterNode = data.nodes.find(n => n.arbiter)
|
||||
if (arbiterNode) {
|
||||
await glusterCmd(
|
||||
glusterEndpoint,
|
||||
@@ -1533,8 +1533,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',
|
||||
|
||||
@@ -6,6 +6,7 @@ import ndjson from 'ndjson'
|
||||
import parseArgs from 'minimist'
|
||||
import sublevel from 'level-sublevel'
|
||||
import util from 'util'
|
||||
import { join as joinPath } from 'path'
|
||||
import { repair as repairDb } from 'level'
|
||||
|
||||
import { forEach } from './utils'
|
||||
@@ -174,6 +175,7 @@ export default async function main() {
|
||||
}
|
||||
|
||||
const config = await appConf.load('xo-server', {
|
||||
appDir: joinPath(__dirname, '..'),
|
||||
ignoreUnknownFormats: true,
|
||||
})
|
||||
|
||||
|
||||
@@ -26,7 +26,12 @@ export const merge = (newValue, oldValue) => {
|
||||
|
||||
export const obfuscate = value => replace(value, OBFUSCATED_VALUE)
|
||||
|
||||
const SENSITIVE_PARAMS = { __proto__: null, password: true, token: true }
|
||||
const SENSITIVE_PARAMS = {
|
||||
__proto__: null,
|
||||
cifspassword: true,
|
||||
password: true,
|
||||
token: true,
|
||||
}
|
||||
|
||||
export function replace(value, replacement) {
|
||||
function helper(value, name) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import * as sensitiveValues from './sensitive-values'
|
||||
import ensureArray from './_ensureArray'
|
||||
import {
|
||||
extractProperty,
|
||||
@@ -485,7 +486,10 @@ const TRANSFORMS = {
|
||||
attached: Boolean(obj.currently_attached),
|
||||
host: link(obj, 'host'),
|
||||
SR: link(obj, 'SR'),
|
||||
device_config: obj.device_config,
|
||||
device_config: sensitiveValues.replace(
|
||||
obj.device_config,
|
||||
'* obfuscated *'
|
||||
),
|
||||
otherConfig: obj.other_config,
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1566,7 +1576,7 @@ export default class Xapi extends XapiBase {
|
||||
}
|
||||
} else {
|
||||
// Find the original template by name (*sigh*).
|
||||
const templateNameLabel = vm.other_config['base_template_name']
|
||||
const templateNameLabel = vm.other_config.base_template_name
|
||||
const template =
|
||||
templateNameLabel &&
|
||||
find(
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user