Compare commits
18 Commits
fs-glob
...
backup-pip
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5602477891 | ||
|
|
3483e7d9e0 | ||
|
|
56cb20a1af | ||
|
|
64929653dd | ||
|
|
c955da9bc6 | ||
|
|
291354fa8e | ||
|
|
905d736512 | ||
|
|
3406d6e2a9 | ||
|
|
fc10b5ffb9 | ||
|
|
f89c313166 | ||
|
|
7c734168d0 | ||
|
|
1e7bfec2ce | ||
|
|
1eb0603b4e | ||
|
|
4b32730ce8 | ||
|
|
ad083c1d9b | ||
|
|
b4f84c2de2 | ||
|
|
fc17443ce4 | ||
|
|
342ae06b21 |
@@ -21,7 +21,7 @@ module.exports = {
|
||||
|
||||
overrides: [
|
||||
{
|
||||
files: ['cli.js', '*-cli.js', 'packages/*cli*/**/*.js'],
|
||||
files: ['cli.js', '*-cli.js', '**/*cli*/**/*.js'],
|
||||
rules: {
|
||||
'no-console': 'off',
|
||||
},
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
"@babel/preset-env": "^7.0.0",
|
||||
"@babel/preset-flow": "^7.0.0",
|
||||
"babel-plugin-lodash": "^3.3.2",
|
||||
"cross-env": "^5.1.3",
|
||||
"cross-env": "^6.0.3",
|
||||
"rimraf": "^3.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
369
@xen-orchestra/backups-cli/index.js
Executable file
369
@xen-orchestra/backups-cli/index.js
Executable file
@@ -0,0 +1,369 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const args = process.argv.slice(2)
|
||||
|
||||
if (
|
||||
args.length === 0 ||
|
||||
/^(?:-h|--help)$/.test(args[0]) ||
|
||||
args[0] !== 'clean-vms'
|
||||
) {
|
||||
console.log('Usage: xo-backups clean-vms [--force] xo-vm-backups/*')
|
||||
// eslint-disable-next-line no-process-exit
|
||||
return process.exit(1)
|
||||
}
|
||||
|
||||
// remove `clean-vms` arg which is the only available command ATM
|
||||
args.splice(0, 1)
|
||||
|
||||
// only act (ie delete files) if `--force` is present
|
||||
const force = args[0] === '--force'
|
||||
if (force) {
|
||||
args.splice(0, 1)
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
const assert = require('assert')
|
||||
const lockfile = require('proper-lockfile')
|
||||
const { default: Vhd } = require('vhd-lib')
|
||||
const { curryRight, flatten } = require('lodash')
|
||||
const { dirname, resolve } = require('path')
|
||||
const { DISK_TYPE_DIFFERENCING } = require('vhd-lib/dist/_constants')
|
||||
const { pipe, promisifyAll } = require('promise-toolbox')
|
||||
|
||||
const fs = promisifyAll(require('fs'))
|
||||
const handler = require('@xen-orchestra/fs').getHandler({ url: 'file://' })
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
const asyncMap = curryRight((iterable, fn) =>
|
||||
Promise.all(
|
||||
Array.isArray(iterable) ? iterable.map(fn) : Array.from(iterable, fn)
|
||||
)
|
||||
)
|
||||
|
||||
const filter = (...args) => thisArg => thisArg.filter(...args)
|
||||
|
||||
// TODO: better check?
|
||||
|
||||
// our heuristic is not good enough, there has been some false positives
|
||||
// (detected as invalid by us but valid by `tar` and imported with success),
|
||||
// either:
|
||||
// - these files were normal but the check is incorrect
|
||||
// - these files were invalid but without data loss
|
||||
// - these files were invalid but with silent data loss
|
||||
//
|
||||
// FIXME: the heuristic does not work if the XVA is compressed, we need to
|
||||
// implement a specific test for it
|
||||
//
|
||||
// maybe reading the end of the file looking for a file named
|
||||
// /^Ref:\d+/\d+\.checksum$/ and then validating the tar structure from it
|
||||
//
|
||||
// https://github.com/npm/node-tar/issues/234#issuecomment-538190295
|
||||
const isValidTar = async path => {
|
||||
try {
|
||||
const fd = await fs.open(path, 'r')
|
||||
try {
|
||||
const { size } = await fs.fstat(fd)
|
||||
if (size <= 1024 || size % 512 !== 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
const buf = Buffer.allocUnsafe(1024)
|
||||
assert.strictEqual(
|
||||
await fs.read(fd, buf, 0, buf.length, size - buf.length),
|
||||
buf.length
|
||||
)
|
||||
return buf.every(_ => _ === 0)
|
||||
} finally {
|
||||
fs.close(fd).catch(noop)
|
||||
}
|
||||
} catch (error) {
|
||||
// never throw, log and report as valid to avoid side effects
|
||||
console.error('isValidTar', path, error)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
const noop = Function.prototype
|
||||
|
||||
const readDir = path =>
|
||||
fs.readdir(path).then(entries => {
|
||||
entries.forEach((entry, i) => {
|
||||
entries[i] = `${path}/${entry}`
|
||||
})
|
||||
|
||||
return entries
|
||||
})
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
// chain is an array of VHDs from child to parent
|
||||
//
|
||||
// the whole chain will be merged into parent, parent will be renamed to child
|
||||
// and all the others will deleted
|
||||
async function mergeVhdChain(chain) {
|
||||
assert(chain.length >= 2)
|
||||
|
||||
const child = chain[0]
|
||||
const parent = chain[chain.length - 1]
|
||||
const children = chain.slice(0, -1).reverse()
|
||||
|
||||
console.warn('Unused parents of VHD', child)
|
||||
chain
|
||||
.slice(1)
|
||||
.reverse()
|
||||
.forEach(parent => {
|
||||
console.warn(' ', parent)
|
||||
})
|
||||
force && console.warn(' merging…')
|
||||
console.warn('')
|
||||
if (force) {
|
||||
// `mergeVhd` does not work with a stream, either
|
||||
// - make it accept a stream
|
||||
// - or create synthetic VHD which is not a stream
|
||||
return console.warn('TODO: implement merge')
|
||||
// await mergeVhd(
|
||||
// handler,
|
||||
// parent,
|
||||
// handler,
|
||||
// children.length === 1
|
||||
// ? child
|
||||
// : await createSyntheticStream(handler, children)
|
||||
// )
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
force && fs.rename(parent, child),
|
||||
asyncMap(children.slice(0, -1), child => {
|
||||
console.warn('Unused VHD', child)
|
||||
force && console.warn(' deleting…')
|
||||
console.warn('')
|
||||
return force && handler.unlink(child)
|
||||
}),
|
||||
])
|
||||
}
|
||||
|
||||
const listVhds = pipe([
|
||||
vmDir => vmDir + '/vdis',
|
||||
readDir,
|
||||
asyncMap(readDir),
|
||||
flatten,
|
||||
asyncMap(readDir),
|
||||
flatten,
|
||||
filter(_ => _.endsWith('.vhd')),
|
||||
])
|
||||
|
||||
async function handleVm(vmDir) {
|
||||
const vhds = new Set()
|
||||
const vhdParents = { __proto__: null }
|
||||
const vhdChildren = { __proto__: null }
|
||||
|
||||
// remove broken VHDs
|
||||
await asyncMap(await listVhds(vmDir), async path => {
|
||||
try {
|
||||
const vhd = new Vhd(handler, path)
|
||||
await vhd.readHeaderAndFooter()
|
||||
vhds.add(path)
|
||||
if (vhd.footer.diskType === DISK_TYPE_DIFFERENCING) {
|
||||
const parent = resolve(dirname(path), vhd.header.parentUnicodeName)
|
||||
vhdParents[path] = parent
|
||||
if (parent in vhdChildren) {
|
||||
const error = new Error(
|
||||
'this script does not support multiple VHD children'
|
||||
)
|
||||
error.parent = parent
|
||||
error.child1 = vhdChildren[parent]
|
||||
error.child2 = path
|
||||
throw error // should we throw?
|
||||
}
|
||||
vhdChildren[parent] = path
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Error while checking VHD', path)
|
||||
console.warn(' ', error)
|
||||
if (error != null && error.code === 'ERR_ASSERTION') {
|
||||
force && console.warn(' deleting…')
|
||||
console.warn('')
|
||||
force && (await handler.unlink(path))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// remove VHDs with missing ancestors
|
||||
{
|
||||
const deletions = []
|
||||
|
||||
// return true if the VHD has been deleted or is missing
|
||||
const deleteIfOrphan = vhd => {
|
||||
const parent = vhdParents[vhd]
|
||||
if (parent === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
// no longer needs to be checked
|
||||
delete vhdParents[vhd]
|
||||
|
||||
deleteIfOrphan(parent)
|
||||
|
||||
if (!vhds.has(parent)) {
|
||||
vhds.delete(vhd)
|
||||
|
||||
console.warn('Error while checking VHD', vhd)
|
||||
console.warn(' missing parent', parent)
|
||||
force && console.warn(' deleting…')
|
||||
console.warn('')
|
||||
force && deletions.push(handler.unlink(vhd))
|
||||
}
|
||||
}
|
||||
|
||||
// > A property that is deleted before it has been visited will not be
|
||||
// > visited later.
|
||||
// >
|
||||
// > -- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...in#Deleted_added_or_modified_properties
|
||||
for (const child in vhdParents) {
|
||||
deleteIfOrphan(child)
|
||||
}
|
||||
|
||||
await Promise.all(deletions)
|
||||
}
|
||||
|
||||
const [jsons, xvas] = await readDir(vmDir).then(entries => [
|
||||
entries.filter(_ => _.endsWith('.json')),
|
||||
new Set(entries.filter(_ => _.endsWith('.xva'))),
|
||||
])
|
||||
|
||||
await asyncMap(xvas, async path => {
|
||||
// check is not good enough to delete the file, the best we can do is report
|
||||
// it
|
||||
if (!(await isValidTar(path))) {
|
||||
console.warn('Potential broken XVA', path)
|
||||
console.warn('')
|
||||
}
|
||||
})
|
||||
|
||||
const unusedVhds = new Set(vhds)
|
||||
const unusedXvas = new Set(xvas)
|
||||
|
||||
// compile the list of unused XVAs and VHDs, and remove backup metadata which
|
||||
// reference a missing XVA/VHD
|
||||
await asyncMap(jsons, async json => {
|
||||
const metadata = JSON.parse(await fs.readFile(json))
|
||||
const { mode } = metadata
|
||||
if (mode === 'full') {
|
||||
const linkedXva = resolve(vmDir, metadata.xva)
|
||||
|
||||
if (xvas.has(linkedXva)) {
|
||||
unusedXvas.delete(linkedXva)
|
||||
} else {
|
||||
console.warn('Error while checking backup', json)
|
||||
console.warn(' missing file', linkedXva)
|
||||
force && console.warn(' deleting…')
|
||||
console.warn('')
|
||||
force && (await handler.unlink(json))
|
||||
}
|
||||
} else if (mode === 'delta') {
|
||||
const linkedVhds = (() => {
|
||||
const { vhds } = metadata
|
||||
return Object.keys(vhds).map(key => resolve(vmDir, vhds[key]))
|
||||
})()
|
||||
|
||||
// FIXME: find better approach by keeping as much of the backup as
|
||||
// possible (existing disks) even if one disk is missing
|
||||
if (linkedVhds.every(_ => vhds.has(_))) {
|
||||
linkedVhds.forEach(_ => unusedVhds.delete(_))
|
||||
} else {
|
||||
console.warn('Error while checking backup', json)
|
||||
const missingVhds = linkedVhds.filter(_ => !vhds.has(_))
|
||||
console.warn(
|
||||
' %i/%i missing VHDs',
|
||||
missingVhds.length,
|
||||
linkedVhds.length
|
||||
)
|
||||
missingVhds.forEach(vhd => {
|
||||
console.warn(' ', vhd)
|
||||
})
|
||||
force && console.warn(' deleting…')
|
||||
console.warn('')
|
||||
force && (await handler.unlink(json))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// TODO: parallelize by vm/job/vdi
|
||||
const unusedVhdsDeletion = []
|
||||
{
|
||||
// VHD chains (as list from child to ancestor) to merge indexed by last
|
||||
// ancestor
|
||||
const vhdChainsToMerge = { __proto__: null }
|
||||
|
||||
const toCheck = new Set(unusedVhds)
|
||||
|
||||
const getUsedChildChainOrDelete = vhd => {
|
||||
if (vhd in vhdChainsToMerge) {
|
||||
const chain = vhdChainsToMerge[vhd]
|
||||
delete vhdChainsToMerge[vhd]
|
||||
return chain
|
||||
}
|
||||
|
||||
if (!unusedVhds.has(vhd)) {
|
||||
return [vhd]
|
||||
}
|
||||
|
||||
// no longer needs to be checked
|
||||
toCheck.delete(vhd)
|
||||
|
||||
const child = vhdChildren[vhd]
|
||||
if (child !== undefined) {
|
||||
const chain = getUsedChildChainOrDelete(child)
|
||||
if (chain !== undefined) {
|
||||
chain.push(vhd)
|
||||
return chain
|
||||
}
|
||||
}
|
||||
|
||||
console.warn('Unused VHD', vhd)
|
||||
force && console.warn(' deleting…')
|
||||
console.warn('')
|
||||
force && unusedVhdsDeletion.push(handler.unlink(vhd))
|
||||
}
|
||||
|
||||
toCheck.forEach(vhd => {
|
||||
vhdChainsToMerge[vhd] = getUsedChildChainOrDelete(vhd)
|
||||
})
|
||||
|
||||
Object.keys(vhdChainsToMerge).forEach(key => {
|
||||
const chain = vhdChainsToMerge[key]
|
||||
if (chain !== undefined) {
|
||||
unusedVhdsDeletion.push(mergeVhdChain(chain))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
unusedVhdsDeletion,
|
||||
asyncMap(unusedXvas, path => {
|
||||
console.warn('Unused XVA', path)
|
||||
force && console.warn(' deleting…')
|
||||
console.warn('')
|
||||
return force && handler.unlink(path)
|
||||
}),
|
||||
])
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
asyncMap(args, async vmDir => {
|
||||
vmDir = resolve(vmDir)
|
||||
|
||||
// TODO: implement this in `xo-server`, not easy because not compatible with
|
||||
// `@xen-orchestra/fs`.
|
||||
const release = await lockfile.lock(vmDir)
|
||||
try {
|
||||
await handleVm(vmDir)
|
||||
} catch (error) {
|
||||
console.error('handleVm', vmDir, error)
|
||||
} finally {
|
||||
await release()
|
||||
}
|
||||
}).catch(error => console.error('main', error))
|
||||
27
@xen-orchestra/backups-cli/package.json
Normal file
27
@xen-orchestra/backups-cli/package.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"bin": {
|
||||
"xo-backups": "index.js"
|
||||
},
|
||||
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
|
||||
"dependencies": {
|
||||
"@xen-orchestra/fs": "^0.10.1",
|
||||
"lodash": "^4.17.15",
|
||||
"promise-toolbox": "^0.14.0",
|
||||
"proper-lockfile": "^4.1.1",
|
||||
"vhd-lib": "^0.7.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.16.1"
|
||||
},
|
||||
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/backups-cli",
|
||||
"name": "@xen-orchestra/backups-cli",
|
||||
"repository": {
|
||||
"directory": "@xen-orchestra/backups-cli",
|
||||
"type": "git",
|
||||
"url": "https://github.com/vatesfr/xen-orchestra.git"
|
||||
},
|
||||
"scripts": {
|
||||
"postversion": "npm publish --access public"
|
||||
},
|
||||
"version": "0.0.0"
|
||||
}
|
||||
@@ -46,7 +46,7 @@
|
||||
"@babel/core": "^7.0.0",
|
||||
"@babel/preset-env": "^7.0.0",
|
||||
"@babel/preset-flow": "^7.0.0",
|
||||
"cross-env": "^5.1.3",
|
||||
"cross-env": "^6.0.3",
|
||||
"rimraf": "^3.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
"@babel/preset-env": "^7.0.0",
|
||||
"@babel/preset-flow": "^7.0.0",
|
||||
"babel-plugin-lodash": "^3.3.2",
|
||||
"cross-env": "^5.1.3",
|
||||
"cross-env": "^6.0.3",
|
||||
"rimraf": "^3.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
"@babel/core": "^7.0.0",
|
||||
"@babel/preset-env": "^7.0.0",
|
||||
"babel-plugin-lodash": "^3.3.2",
|
||||
"cross-env": "^5.1.3",
|
||||
"cross-env": "^6.0.3",
|
||||
"rimraf": "^3.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
"get-stream": "^4.0.0",
|
||||
"limit-concurrency-decorator": "^0.4.0",
|
||||
"lodash": "^4.17.4",
|
||||
"promise-toolbox": "^0.13.0",
|
||||
"promise-toolbox": "^0.14.0",
|
||||
"readable-stream": "^3.0.6",
|
||||
"through2": "^3.0.0",
|
||||
"tmp": "^0.1.0",
|
||||
@@ -46,7 +46,7 @@
|
||||
"@babel/preset-flow": "^7.0.0",
|
||||
"async-iterator-to-stream": "^1.1.0",
|
||||
"babel-plugin-lodash": "^3.3.2",
|
||||
"cross-env": "^5.1.3",
|
||||
"cross-env": "^6.0.3",
|
||||
"dotenv": "^8.0.0",
|
||||
"index-modules": "^0.3.0",
|
||||
"rimraf": "^3.0.0"
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
/* 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,7 +14,6 @@ import { type Readable, type Writable } from 'stream'
|
||||
|
||||
import normalizePath from './_normalizePath'
|
||||
import { createChecksumStream, validChecksumOfReadStream } from './checksum'
|
||||
import { parseGlob } from './_parseGlob'
|
||||
|
||||
const { dirname } = path.posix
|
||||
|
||||
@@ -259,12 +258,6 @@ export default class RemoteHandlerAbstract {
|
||||
)
|
||||
}
|
||||
|
||||
// basic glob support, only `*` is supported
|
||||
async glob(pattern) {
|
||||
const parts = parseGlob(pattern)
|
||||
// TODO
|
||||
}
|
||||
|
||||
async list(
|
||||
dir: string,
|
||||
{
|
||||
|
||||
@@ -31,14 +31,14 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"lodash": "^4.17.4",
|
||||
"promise-toolbox": "^0.13.0"
|
||||
"promise-toolbox": "^0.14.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.0.0",
|
||||
"@babel/core": "^7.0.0",
|
||||
"@babel/preset-env": "^7.0.0",
|
||||
"babel-plugin-lodash": "^3.3.2",
|
||||
"cross-env": "^5.1.3",
|
||||
"cross-env": "^6.0.3",
|
||||
"index-modules": "^0.3.0",
|
||||
"rimraf": "^3.0.0"
|
||||
},
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
"@babel/preset-env": "^7.0.0",
|
||||
"babel-plugin-dev": "^1.0.0",
|
||||
"babel-plugin-lodash": "^3.3.2",
|
||||
"cross-env": "^5.1.3",
|
||||
"cross-env": "^6.0.3",
|
||||
"rimraf": "^3.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
"@babel/cli": "^7.0.0",
|
||||
"@babel/core": "^7.0.0",
|
||||
"@babel/preset-env": "^7.0.0",
|
||||
"cross-env": "^5.1.3",
|
||||
"cross-env": "^6.0.3",
|
||||
"rimraf": "^3.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
21
CHANGELOG.md
21
CHANGELOG.md
@@ -8,14 +8,27 @@
|
||||
|
||||
### Released packages
|
||||
|
||||
- xo-server v5.51.0
|
||||
- xo-web v5.51.0
|
||||
|
||||
## **5.39.1** (2019-10-11)
|
||||
|
||||

|
||||
|
||||
### Enhancements
|
||||
|
||||
- [Support] Ability to check the XOA on the user interface [#4513](https://github.com/vatesfr/xen-orchestra/issues/4513) (PR [#4574](https://github.com/vatesfr/xen-orchestra/pull/4574))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- [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))
|
||||
- [VM] Clearer invalid cores per socket error [#4120](https://github.com/vatesfr/xen-orchestra/issues/4120) (PR [#4187](https://github.com/vatesfr/xen-orchestra/pull/4187))
|
||||
|
||||
### Released packages
|
||||
|
||||
- xo-web v5.50.3
|
||||
|
||||
|
||||
## **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))
|
||||
|
||||
@@ -3,15 +3,19 @@
|
||||
> Keep in mind the changelog is addressed to **users** and should be
|
||||
> understandable by them.
|
||||
|
||||
### Breaking changes
|
||||
|
||||
- `xo-server` requires Node 8.
|
||||
|
||||
### Enhancements
|
||||
|
||||
> Users must be able to say: “Nice enhancement, I'm eager to test it”
|
||||
|
||||
|
||||
### Bug fixes
|
||||
|
||||
> Users must be able to say: “I had this issue, happy to know it's fixed”
|
||||
|
||||
- [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
|
||||
|
||||
|
||||
@@ -12,18 +12,18 @@
|
||||
"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",
|
||||
"eslint-plugin-node": "^10.0.0",
|
||||
"eslint-plugin-promise": "^4.0.0",
|
||||
"eslint-plugin-react": "^7.6.1",
|
||||
"eslint-plugin-standard": "^4.0.0",
|
||||
"exec-promise": "^0.7.0",
|
||||
"flow-bin": "^0.106.3",
|
||||
"flow-bin": "^0.109.0",
|
||||
"globby": "^10.0.0",
|
||||
"husky": "^3.0.0",
|
||||
"jest": "^24.1.0",
|
||||
"lodash": "^4.17.4",
|
||||
"prettier": "^1.10.2",
|
||||
"promise-toolbox": "^0.13.0",
|
||||
"promise-toolbox": "^0.14.0",
|
||||
"sorted-object": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
"@babel/core": "^7.0.0",
|
||||
"@babel/preset-env": "^7.0.0",
|
||||
"babel-plugin-lodash": "^3.3.2",
|
||||
"cross-env": "^5.1.1",
|
||||
"cross-env": "^6.0.3",
|
||||
"rimraf": "^3.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
"@babel/core": "^7.0.0",
|
||||
"@babel/preset-env": "^7.0.0",
|
||||
"@babel/preset-flow": "^7.0.0",
|
||||
"cross-env": "^5.1.3",
|
||||
"cross-env": "^6.0.3",
|
||||
"rimraf": "^3.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -39,10 +39,10 @@
|
||||
"@babel/core": "^7.0.0",
|
||||
"@babel/preset-env": "^7.0.0",
|
||||
"babel-plugin-lodash": "^3.3.2",
|
||||
"cross-env": "^5.1.3",
|
||||
"cross-env": "^6.0.3",
|
||||
"execa": "^2.0.2",
|
||||
"index-modules": "^0.3.0",
|
||||
"promise-toolbox": "^0.13.0",
|
||||
"promise-toolbox": "^0.14.0",
|
||||
"rimraf": "^3.0.0",
|
||||
"tmp": "^0.1.0"
|
||||
},
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
"from2": "^2.3.0",
|
||||
"fs-extra": "^8.0.1",
|
||||
"limit-concurrency-decorator": "^0.4.0",
|
||||
"promise-toolbox": "^0.13.0",
|
||||
"promise-toolbox": "^0.14.0",
|
||||
"struct-fu": "^1.2.0",
|
||||
"uuid": "^3.0.1"
|
||||
},
|
||||
@@ -37,7 +37,7 @@
|
||||
"@babel/preset-flow": "^7.0.0",
|
||||
"@xen-orchestra/fs": "^0.10.1",
|
||||
"babel-plugin-lodash": "^3.3.2",
|
||||
"cross-env": "^5.1.3",
|
||||
"cross-env": "^6.0.3",
|
||||
"execa": "^2.0.2",
|
||||
"fs-promise": "^2.0.0",
|
||||
"get-stream": "^5.1.0",
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
import { fuFooter, fuHeader, checksumStruct } from './_structs'
|
||||
import { test as mapTestBit } from './_bitmap'
|
||||
|
||||
export default async function createSyntheticStream(handler, path) {
|
||||
export default async function createSyntheticStream(handler, paths) {
|
||||
const fds = []
|
||||
const cleanup = () => {
|
||||
for (let i = 0, n = fds.length; i < n; ++i) {
|
||||
@@ -24,7 +24,7 @@ export default async function createSyntheticStream(handler, path) {
|
||||
}
|
||||
try {
|
||||
const vhds = []
|
||||
while (true) {
|
||||
const open = async path => {
|
||||
const fd = await handler.openFile(path, 'r')
|
||||
fds.push(fd)
|
||||
const vhd = new Vhd(handler, fd)
|
||||
@@ -32,11 +32,18 @@ export default async function createSyntheticStream(handler, path) {
|
||||
await vhd.readHeaderAndFooter()
|
||||
await vhd.readBlockAllocationTable()
|
||||
|
||||
if (vhd.footer.diskType === DISK_TYPE_DYNAMIC) {
|
||||
break
|
||||
return vhd
|
||||
}
|
||||
if (typeof paths === 'string') {
|
||||
let path = paths
|
||||
let vhd
|
||||
while ((vhd = await open(path)).footer.diskType !== DISK_TYPE_DYNAMIC) {
|
||||
path = resolveRelativeFromFile(path, vhd.header.parentUnicodeName)
|
||||
}
|
||||
} else {
|
||||
for (const path of paths) {
|
||||
await open(path)
|
||||
}
|
||||
|
||||
path = resolveRelativeFromFile(path, vhd.header.parentUnicodeName)
|
||||
}
|
||||
const nVhds = vhds.length
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
"@babel/core": "^7.1.5",
|
||||
"@babel/preset-env": "^7.1.5",
|
||||
"babel-plugin-lodash": "^3.2.11",
|
||||
"cross-env": "^5.1.4",
|
||||
"cross-env": "^6.0.3",
|
||||
"rimraf": "^3.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
"make-error": "^1.3.0",
|
||||
"minimist": "^1.2.0",
|
||||
"ms": "^2.1.1",
|
||||
"promise-toolbox": "^0.13.0",
|
||||
"promise-toolbox": "^0.14.0",
|
||||
"pw": "0.0.4",
|
||||
"xmlrpc": "^1.3.2",
|
||||
"xo-collection": "^0.4.1"
|
||||
@@ -60,7 +60,7 @@
|
||||
"@babel/plugin-proposal-optional-chaining": "^7.2.0",
|
||||
"@babel/preset-env": "^7.0.0",
|
||||
"babel-plugin-lodash": "^3.3.2",
|
||||
"cross-env": "^5.1.3",
|
||||
"cross-env": "^6.0.3",
|
||||
"rimraf": "^3.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -8,7 +8,7 @@ import execPromise from 'exec-promise'
|
||||
import minimist from 'minimist'
|
||||
import pw from 'pw'
|
||||
import { asCallback, fromCallback } from 'promise-toolbox'
|
||||
import { filter, find, isArray } from 'lodash'
|
||||
import { filter, find } from 'lodash'
|
||||
import { getBoundPropertyDescriptor } from 'bind-property-descriptor'
|
||||
import { start as createRepl } from 'repl'
|
||||
|
||||
@@ -110,7 +110,7 @@ const main = async args => {
|
||||
asCallback.call(
|
||||
fromCallback(cb => {
|
||||
evaluate.call(repl, cmd, context, filename, cb)
|
||||
}).then(value => (isArray(value) ? Promise.all(value) : value)),
|
||||
}).then(value => (Array.isArray(value) ? Promise.all(value) : value)),
|
||||
cb
|
||||
)
|
||||
})(repl.eval)
|
||||
|
||||
@@ -4,7 +4,7 @@ import kindOf from 'kindof'
|
||||
import ms from 'ms'
|
||||
import httpRequest from 'http-request-plus'
|
||||
import { EventEmitter } from 'events'
|
||||
import { isArray, map, noop, omit } from 'lodash'
|
||||
import { map, noop, omit } from 'lodash'
|
||||
import {
|
||||
cancelable,
|
||||
defer,
|
||||
@@ -113,7 +113,7 @@ export class Xapi extends EventEmitter {
|
||||
this._watchedTypes = undefined
|
||||
const { watchEvents } = opts
|
||||
if (watchEvents !== false) {
|
||||
if (isArray(watchEvents)) {
|
||||
if (Array.isArray(watchEvents)) {
|
||||
this._watchedTypes = watchEvents
|
||||
}
|
||||
this.watchEvents()
|
||||
@@ -1075,7 +1075,7 @@ export class Xapi extends EventEmitter {
|
||||
const $field = (field in RESERVED_FIELDS ? '$$' : '$') + field
|
||||
|
||||
const value = data[field]
|
||||
if (isArray(value)) {
|
||||
if (Array.isArray(value)) {
|
||||
if (value.length === 0 || isOpaqueRef(value[0])) {
|
||||
getters[$field] = function() {
|
||||
const value = this[field]
|
||||
|
||||
@@ -38,16 +38,16 @@
|
||||
"human-format": "^0.10.0",
|
||||
"l33teral": "^3.0.3",
|
||||
"lodash": "^4.17.4",
|
||||
"micromatch": "^3.1.3",
|
||||
"micromatch": "^4.0.2",
|
||||
"mkdirp": "^0.5.1",
|
||||
"nice-pipe": "0.0.0",
|
||||
"pretty-ms": "^4.0.0",
|
||||
"pretty-ms": "^5.0.0",
|
||||
"progress-stream": "^2.0.0",
|
||||
"promise-toolbox": "^0.13.0",
|
||||
"promise-toolbox": "^0.14.0",
|
||||
"pump": "^3.0.0",
|
||||
"pw": "^0.0.4",
|
||||
"strip-indent": "^2.0.0",
|
||||
"xdg-basedir": "^3.0.0",
|
||||
"strip-indent": "^3.0.0",
|
||||
"xdg-basedir": "^4.0.0",
|
||||
"xo-lib": "^0.9.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -56,7 +56,7 @@
|
||||
"@babel/preset-env": "^7.0.0",
|
||||
"@babel/preset-flow": "^7.0.0",
|
||||
"babel-plugin-lodash": "^3.3.2",
|
||||
"cross-env": "^5.1.3",
|
||||
"cross-env": "^6.0.3",
|
||||
"rimraf": "^3.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -7,7 +7,6 @@ const promisify = require('bluebird').promisify
|
||||
const readFile = promisify(require('fs').readFile)
|
||||
const writeFile = promisify(require('fs').writeFile)
|
||||
|
||||
const assign = require('lodash/assign')
|
||||
const l33t = require('l33teral')
|
||||
const mkdirp = promisify(require('mkdirp'))
|
||||
const xdgBasedir = require('xdg-basedir')
|
||||
@@ -41,7 +40,7 @@ const save = (exports.save = function(config) {
|
||||
|
||||
exports.set = function(data) {
|
||||
return load().then(function(config) {
|
||||
return save(assign(config, data))
|
||||
return save(Object.assign(config, data))
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@ const getKeys = require('lodash/keys')
|
||||
const hrp = require('http-request-plus').default
|
||||
const humanFormat = require('human-format')
|
||||
const identity = require('lodash/identity')
|
||||
const isArray = require('lodash/isArray')
|
||||
const isObject = require('lodash/isObject')
|
||||
const micromatch = require('micromatch')
|
||||
const nicePipe = require('nice-pipe')
|
||||
@@ -298,7 +297,11 @@ async function listCommands(args) {
|
||||
str.push(
|
||||
name,
|
||||
'=<',
|
||||
type == null ? 'unknown type' : isArray(type) ? type.join('|') : type,
|
||||
type == null
|
||||
? 'unknown type'
|
||||
: Array.isArray(type)
|
||||
? type.join('|')
|
||||
: type,
|
||||
'>'
|
||||
)
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
"@babel/cli": "^7.0.0",
|
||||
"@babel/core": "^7.0.0",
|
||||
"@babel/preset-env": "^7.0.0",
|
||||
"cross-env": "^5.1.3",
|
||||
"cross-env": "^6.0.3",
|
||||
"event-to-promise": "^0.8.0",
|
||||
"rimraf": "^3.0.0"
|
||||
},
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
"@babel/core": "^7.0.0",
|
||||
"@babel/preset-env": "^7.0.0",
|
||||
"babel-plugin-lodash": "^3.3.2",
|
||||
"cross-env": "^5.1.3",
|
||||
"cross-env": "^6.0.3",
|
||||
"rimraf": "^3.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { BaseError } from 'make-error'
|
||||
import { isArray, iteratee } from 'lodash'
|
||||
import { iteratee } from 'lodash'
|
||||
|
||||
class XoError extends BaseError {
|
||||
constructor({ code, message, data }) {
|
||||
@@ -77,7 +77,7 @@ export const serverUnreachable = create(9, objectId => ({
|
||||
}))
|
||||
|
||||
export const invalidParameters = create(10, (message, errors) => {
|
||||
if (isArray(message)) {
|
||||
if (Array.isArray(message)) {
|
||||
errors = message
|
||||
message = undefined
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
"@babel/core": "^7.0.0",
|
||||
"@babel/preset-env": "^7.0.0",
|
||||
"babel-plugin-lodash": "^3.3.2",
|
||||
"cross-env": "^5.1.3",
|
||||
"cross-env": "^6.0.3",
|
||||
"rimraf": "^3.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
"@babel/core": "^7.0.0",
|
||||
"@babel/preset-env": "^7.0.0",
|
||||
"babel-plugin-lodash": "^3.3.2",
|
||||
"cross-env": "^5.1.3",
|
||||
"cross-env": "^6.0.3",
|
||||
"deep-freeze": "^0.0.1",
|
||||
"rimraf": "^3.0.0"
|
||||
},
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
"@babel/core": "^7.0.0",
|
||||
"@babel/preset-env": "^7.0.0",
|
||||
"babel-plugin-lodash": "^3.3.2",
|
||||
"cross-env": "^5.1.3",
|
||||
"cross-env": "^6.0.3",
|
||||
"rimraf": "^3.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -39,14 +39,14 @@
|
||||
"inquirer": "^7.0.0",
|
||||
"ldapjs": "^1.0.1",
|
||||
"lodash": "^4.17.4",
|
||||
"promise-toolbox": "^0.13.0"
|
||||
"promise-toolbox": "^0.14.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.0.0",
|
||||
"@babel/core": "^7.0.0",
|
||||
"@babel/preset-env": "^7.0.0",
|
||||
"babel-plugin-lodash": "^3.3.2",
|
||||
"cross-env": "^5.1.3",
|
||||
"cross-env": "^6.0.3",
|
||||
"rimraf": "^3.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
"@babel/core": "^7.0.0",
|
||||
"@babel/preset-env": "^7.0.0",
|
||||
"babel-preset-env": "^1.6.1",
|
||||
"cross-env": "^5.1.3",
|
||||
"cross-env": "^6.0.3",
|
||||
"rimraf": "^3.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
"@babel/plugin-proposal-optional-chaining": "^7.2.0",
|
||||
"@babel/preset-env": "^7.0.0",
|
||||
"babel-plugin-lodash": "^3.3.2",
|
||||
"cross-env": "^5.1.3",
|
||||
"cross-env": "^6.0.3",
|
||||
"rimraf": "^3.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
"@babel/cli": "^7.0.0",
|
||||
"@babel/core": "^7.0.0",
|
||||
"@babel/preset-env": "^7.0.0",
|
||||
"cross-env": "^5.1.3",
|
||||
"cross-env": "^6.0.3",
|
||||
"rimraf": "^3.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
"@babel/preset-env": "^7.0.0",
|
||||
"@babel/preset-flow": "^7.0.0",
|
||||
"babel-plugin-lodash": "^3.3.2",
|
||||
"cross-env": "^5.1.3",
|
||||
"cross-env": "^6.0.3",
|
||||
"rimraf": "^3.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import JSON5 from 'json5'
|
||||
import { createSchedule } from '@xen-orchestra/cron'
|
||||
import { assign, forOwn, map, mean } from 'lodash'
|
||||
import { forOwn, map, mean } from 'lodash'
|
||||
import { utcParse } from 'd3-time-format'
|
||||
|
||||
const COMPARATOR_FN = {
|
||||
@@ -483,7 +483,7 @@ ${monitorBodies.join('\n')}`
|
||||
result.rrd = await this.getRrd(result.object, observationPeriod)
|
||||
if (result.rrd !== null) {
|
||||
const data = parseData(result.rrd, result.object.uuid)
|
||||
assign(result, {
|
||||
Object.assign(result, {
|
||||
data,
|
||||
value: data.getDisplayableValue(),
|
||||
shouldAlarm: data.shouldAlarm(),
|
||||
@@ -496,7 +496,7 @@ ${monitorBodies.join('\n')}`
|
||||
definition.alarmTriggerLevel
|
||||
)
|
||||
const data = getter(result.object)
|
||||
assign(result, {
|
||||
Object.assign(result, {
|
||||
value: data.getDisplayableValue(),
|
||||
shouldAlarm: data.shouldAlarm(),
|
||||
})
|
||||
|
||||
@@ -25,13 +25,13 @@
|
||||
"@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"
|
||||
"cross-env": "^6.0.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@xen-orchestra/log": "^0.2.0",
|
||||
"lodash": "^4.17.11",
|
||||
"node-openssl-cert": "^0.0.97",
|
||||
"promise-toolbox": "^0.13.0",
|
||||
"node-openssl-cert": "^0.0.98",
|
||||
"promise-toolbox": "^0.14.0",
|
||||
"uuid": "^3.3.2"
|
||||
},
|
||||
"private": true
|
||||
|
||||
@@ -5,7 +5,7 @@ import uuidv4 from 'uuid/v4'
|
||||
import { access, constants, readFile, writeFile } from 'fs'
|
||||
import { EventEmitter } from 'events'
|
||||
import { filter, find, forOwn, map, omitBy, sample } from 'lodash'
|
||||
import { fromCallback, fromEvent } from 'promise-toolbox'
|
||||
import { fromCallback, promisify } from 'promise-toolbox'
|
||||
import { join } from 'path'
|
||||
|
||||
import { OvsdbClient } from './ovsdb-client'
|
||||
@@ -47,15 +47,8 @@ export const configurationSchema = {
|
||||
|
||||
// =============================================================================
|
||||
|
||||
async function fileWrite(path, data) {
|
||||
await fromCallback(writeFile, path, data)
|
||||
}
|
||||
|
||||
async function fileRead(path) {
|
||||
const result = await fromCallback(readFile, path)
|
||||
return result
|
||||
}
|
||||
|
||||
const fileWrite = promisify(writeFile)
|
||||
const fileRead = promisify(readFile)
|
||||
async function fileExists(path) {
|
||||
try {
|
||||
await fromCallback(access, path, constants.F_OK)
|
||||
@@ -74,8 +67,8 @@ async function fileExists(path) {
|
||||
|
||||
// 2019-09-03
|
||||
// Compatibility code, to be removed in 1 year.
|
||||
function updateNetworkOtherConfig(network) {
|
||||
return Promise.all(
|
||||
const updateNetworkOtherConfig = network =>
|
||||
Promise.all(
|
||||
map(
|
||||
{
|
||||
'cross-pool-network-uuid': 'cross_pool_network_uuid',
|
||||
@@ -101,14 +94,107 @@ function updateNetworkOtherConfig(network) {
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
function createPassword() {
|
||||
const chars =
|
||||
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789?!'
|
||||
return Array.from({ length: 16 }, _ => sample(chars)).join('')
|
||||
const CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789?!'
|
||||
const createPassword = () =>
|
||||
Array.from({ length: 16 }, _ => sample(CHARS)).join('')
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
async function generateCertificatesAndKey(dataDir) {
|
||||
const openssl = new NodeOpenssl()
|
||||
const rsaKeyOptions = {
|
||||
rsa_keygen_bits: 4096,
|
||||
format: 'PKCS8',
|
||||
}
|
||||
const subject = {
|
||||
countryName: 'XX',
|
||||
localityName: 'Default City',
|
||||
organizationName: 'Default Company LTD',
|
||||
}
|
||||
const csrOptions = {
|
||||
hash: 'sha256',
|
||||
startdate: new Date('1984-02-04 00:00:00'),
|
||||
enddate: new Date('2143-06-04 04:16:23'),
|
||||
subject: subject,
|
||||
}
|
||||
const caCsrOptions = {
|
||||
hash: 'sha256',
|
||||
days: NB_DAYS,
|
||||
subject: subject,
|
||||
}
|
||||
|
||||
let operation
|
||||
try {
|
||||
// CA Cert
|
||||
operation = 'Generating CA private key'
|
||||
const caKey = await fromCallback.call(
|
||||
openssl,
|
||||
'generateRSAPrivateKey',
|
||||
rsaKeyOptions
|
||||
)
|
||||
|
||||
operation = 'Generating CA certificate'
|
||||
const caCsr = await fromCallback.call(
|
||||
openssl,
|
||||
'generateCSR',
|
||||
caCsrOptions,
|
||||
caKey,
|
||||
null
|
||||
)
|
||||
|
||||
operation = 'Signing CA certificate'
|
||||
const caCrt = await fromCallback.call(
|
||||
openssl,
|
||||
'selfSignCSR',
|
||||
caCsr,
|
||||
caCsrOptions,
|
||||
caKey,
|
||||
null
|
||||
)
|
||||
await fileWrite(join(dataDir, CA_CERT), caCrt)
|
||||
|
||||
// Cert
|
||||
operation = 'Generating private key'
|
||||
const key = await fromCallback.call(
|
||||
openssl,
|
||||
'generateRSAPrivateKey',
|
||||
rsaKeyOptions
|
||||
)
|
||||
await fileWrite(join(dataDir, CLIENT_KEY), key)
|
||||
|
||||
operation = 'Generating certificate'
|
||||
const csr = await fromCallback.call(
|
||||
openssl,
|
||||
'generateCSR',
|
||||
csrOptions,
|
||||
key,
|
||||
null
|
||||
)
|
||||
|
||||
operation = 'Signing certificate'
|
||||
const crt = await fromCallback.call(
|
||||
openssl,
|
||||
'CASignCSR',
|
||||
csr,
|
||||
caCsrOptions,
|
||||
false,
|
||||
caCrt,
|
||||
caKey,
|
||||
null
|
||||
)
|
||||
await fileWrite(join(dataDir, CLIENT_CERT), crt)
|
||||
} catch (error) {
|
||||
log.error('Error while generating certificates and keys', {
|
||||
operation,
|
||||
error,
|
||||
})
|
||||
throw error
|
||||
}
|
||||
|
||||
log.debug('All certificates have been successfully written')
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
@@ -177,7 +263,7 @@ class SDNController extends EventEmitter {
|
||||
)
|
||||
|
||||
log.debug(`No default self-signed certificates exists, creating them`)
|
||||
await this._generateCertificatesAndKey(certDirectory)
|
||||
await generateCertificatesAndKey(certDirectory)
|
||||
}
|
||||
}
|
||||
// TODO: verify certificates and create new certificates if needed
|
||||
@@ -231,7 +317,11 @@ class SDNController extends EventEmitter {
|
||||
|
||||
// Expose method to create cross-pool private network
|
||||
const createCrossPoolPrivateNetwork = params =>
|
||||
this._createCrossPoolPrivateNetwork({ encrypted: false, mtu: 0, ...params })
|
||||
this._createCrossPoolPrivateNetwork({
|
||||
encrypted: false,
|
||||
mtu: 0,
|
||||
...params,
|
||||
})
|
||||
|
||||
createCrossPoolPrivateNetwork.description =
|
||||
'Creates a cross-pool private network on selected pools'
|
||||
@@ -1430,119 +1520,6 @@ class SDNController extends EventEmitter {
|
||||
this._ovsdbClients.push(client)
|
||||
return client
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async _generateCertificatesAndKey(dataDir) {
|
||||
const openssl = new NodeOpenssl()
|
||||
|
||||
const rsakeyoptions = {
|
||||
rsa_keygen_bits: 4096,
|
||||
format: 'PKCS8',
|
||||
}
|
||||
const subject = {
|
||||
countryName: 'XX',
|
||||
localityName: 'Default City',
|
||||
organizationName: 'Default Company LTD',
|
||||
}
|
||||
const csroptions = {
|
||||
hash: 'sha256',
|
||||
startdate: new Date('1984-02-04 00:00:00'),
|
||||
enddate: new Date('2143-06-04 04:16:23'),
|
||||
subject: subject,
|
||||
}
|
||||
const cacsroptions = {
|
||||
hash: 'sha256',
|
||||
days: NB_DAYS,
|
||||
subject: subject,
|
||||
}
|
||||
|
||||
// In all the following callbacks, `error` is:
|
||||
// - either an error object if there was an error
|
||||
// - or a boolean set to `false` if no error occurred
|
||||
openssl.generateRSAPrivateKey(rsakeyoptions, (error, cakey, cmd) => {
|
||||
if (error !== false) {
|
||||
log.error('Error while generating CA private key', {
|
||||
error,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
openssl.generateCSR(cacsroptions, cakey, null, (error, csr, cmd) => {
|
||||
if (error !== false) {
|
||||
log.error('Error while generating CA certificate', {
|
||||
error,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
openssl.selfSignCSR(
|
||||
csr,
|
||||
cacsroptions,
|
||||
cakey,
|
||||
null,
|
||||
async (error, cacrt, cmd) => {
|
||||
if (error !== false) {
|
||||
log.error('Error while signing CA certificate', {
|
||||
error,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
await fileWrite(join(dataDir, CA_CERT), cacrt)
|
||||
openssl.generateRSAPrivateKey(
|
||||
rsakeyoptions,
|
||||
async (error, key, cmd) => {
|
||||
if (error !== false) {
|
||||
log.error('Error while generating private key', {
|
||||
error,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
await fileWrite(join(dataDir, CLIENT_KEY), key)
|
||||
openssl.generateCSR(
|
||||
csroptions,
|
||||
key,
|
||||
null,
|
||||
(error, csr, cmd) => {
|
||||
if (error !== false) {
|
||||
log.error('Error while generating certificate', {
|
||||
error,
|
||||
})
|
||||
return
|
||||
}
|
||||
openssl.CASignCSR(
|
||||
csr,
|
||||
cacsroptions,
|
||||
false,
|
||||
cacrt,
|
||||
cakey,
|
||||
null,
|
||||
async (error, crt, cmd) => {
|
||||
if (error !== false) {
|
||||
log.error('Error while signing certificate', {
|
||||
error,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
await fileWrite(join(dataDir, CLIENT_CERT), crt)
|
||||
this.emit('certWritten')
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
await fromEvent(this, 'certWritten', {})
|
||||
log.debug('All certificates have been successfully written')
|
||||
}
|
||||
}
|
||||
|
||||
export default opts => new SDNController(opts)
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
"golike-defer": "^0.4.1",
|
||||
"jest": "^24.8.0",
|
||||
"lodash": "^4.17.11",
|
||||
"promise-toolbox": "^0.13.0",
|
||||
"promise-toolbox": "^0.14.0",
|
||||
"xo-collection": "^0.4.1",
|
||||
"xo-common": "^0.2.0",
|
||||
"xo-lib": "^0.9.0"
|
||||
|
||||
6
packages/xo-server-test/src/_defaultValues.js
Normal file
6
packages/xo-server-test/src/_defaultValues.js
Normal file
@@ -0,0 +1,6 @@
|
||||
export const getDefaultName = () => `xo-server-test ${new Date().toISOString()}`
|
||||
|
||||
export const getDefaultSchedule = () => ({
|
||||
name: getDefaultName(),
|
||||
cron: '0 * * * * *',
|
||||
})
|
||||
@@ -2,15 +2,11 @@
|
||||
import defer from 'golike-defer'
|
||||
import Xo from 'xo-lib'
|
||||
import XoCollection from 'xo-collection'
|
||||
import { find, forOwn } from 'lodash'
|
||||
import { defaultsDeep, find, forOwn, pick } from 'lodash'
|
||||
import { fromEvent } from 'promise-toolbox'
|
||||
|
||||
import config from './_config'
|
||||
|
||||
const getDefaultCredentials = () => {
|
||||
const { email, password } = config.xoConnection
|
||||
return { email, password }
|
||||
}
|
||||
import { getDefaultName } from './_defaultValues'
|
||||
|
||||
class XoConnection extends Xo {
|
||||
constructor(opts) {
|
||||
@@ -72,7 +68,10 @@ class XoConnection extends Xo {
|
||||
}
|
||||
|
||||
@defer
|
||||
async connect($defer, credentials = getDefaultCredentials()) {
|
||||
async connect(
|
||||
$defer,
|
||||
credentials = pick(config.xoConnection, 'email', 'password')
|
||||
) {
|
||||
await this.open()
|
||||
$defer.onFailure(() => this.close())
|
||||
|
||||
@@ -111,9 +110,26 @@ class XoConnection extends Xo {
|
||||
}
|
||||
|
||||
async createTempBackupNgJob(params) {
|
||||
const job = await this.call('backupNg.createJob', params)
|
||||
this._tempResourceDisposers.push('backupNg.deleteJob', { id: job.id })
|
||||
return job
|
||||
// mutate and inject default values
|
||||
defaultsDeep(params, {
|
||||
mode: 'full',
|
||||
name: getDefaultName(),
|
||||
settings: {
|
||||
'': {
|
||||
// it 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
|
||||
bypassVdiChainsCheck: true,
|
||||
|
||||
// it must be 'never' to avoid race conditions with the plugin `backup-reports`
|
||||
reportWhen: 'never',
|
||||
},
|
||||
},
|
||||
})
|
||||
const id = await this.call('backupNg.createJob', params)
|
||||
this._tempResourceDisposers.push('backupNg.deleteJob', { id })
|
||||
return this.call('backupNg.getJob', { id })
|
||||
}
|
||||
|
||||
async createTempNetwork(params) {
|
||||
@@ -128,7 +144,7 @@ class XoConnection extends Xo {
|
||||
|
||||
async createTempVm(params) {
|
||||
const id = await this.call('vm.create', {
|
||||
name_label: 'XO Test',
|
||||
name_label: getDefaultName(),
|
||||
template: config.templates.templateWithoutDisks,
|
||||
...params,
|
||||
})
|
||||
|
||||
@@ -1,61 +1,6 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`backupNg .createJob() : creates a new backup job with schedules 1`] = `
|
||||
Object {
|
||||
"id": Any<String>,
|
||||
"mode": "full",
|
||||
"name": "default-backupNg",
|
||||
"settings": Any<Object>,
|
||||
"type": "backup",
|
||||
"userId": Any<String>,
|
||||
"vms": Any<Object>,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg .createJob() : creates a new backup job with schedules 2`] = `
|
||||
Object {
|
||||
"cron": "0 * * * * *",
|
||||
"enabled": false,
|
||||
"id": Any<String>,
|
||||
"jobId": Any<String>,
|
||||
"name": "scheduleTest",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg .createJob() : creates a new backup job without schedules 1`] = `
|
||||
Object {
|
||||
"id": Any<String>,
|
||||
"mode": "full",
|
||||
"name": "default-backupNg",
|
||||
"settings": Object {
|
||||
"": Object {
|
||||
"reportWhen": "never",
|
||||
},
|
||||
},
|
||||
"type": "backup",
|
||||
"userId": Any<String>,
|
||||
"vms": Any<Object>,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg .runJob() : fails trying to run a backup job with a VM without disks 1`] = `
|
||||
Object {
|
||||
"data": Object {
|
||||
"mode": "full",
|
||||
"reportWhen": "never",
|
||||
},
|
||||
"end": Any<Number>,
|
||||
"id": Any<String>,
|
||||
"jobId": Any<String>,
|
||||
"jobName": "default-backupNg",
|
||||
"message": "backup",
|
||||
"scheduleId": Any<String>,
|
||||
"start": Any<Number>,
|
||||
"status": "skipped",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg .runJob() : fails trying to run a backup job with a VM without disks 2`] = `
|
||||
Object {
|
||||
"data": Object {
|
||||
"id": Any<String>,
|
||||
@@ -92,23 +37,6 @@ Array [
|
||||
exports[`backupNg .runJob() : fails trying to run a backup job without schedule 1`] = `[JsonRpcError: invalid parameters]`;
|
||||
|
||||
exports[`backupNg .runJob() : fails trying to run backup job without retentions 1`] = `
|
||||
Object {
|
||||
"data": Object {
|
||||
"mode": "full",
|
||||
"reportWhen": "never",
|
||||
},
|
||||
"end": Any<Number>,
|
||||
"id": Any<String>,
|
||||
"jobId": Any<String>,
|
||||
"jobName": "default-backupNg",
|
||||
"message": "backup",
|
||||
"scheduleId": Any<String>,
|
||||
"start": Any<Number>,
|
||||
"status": "failure",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg .runJob() : fails trying to run backup job without retentions 2`] = `
|
||||
Object {
|
||||
"data": Object {
|
||||
"id": Any<String>,
|
||||
@@ -128,22 +56,6 @@ 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>,
|
||||
@@ -157,7 +69,7 @@ Object {
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 3`] = `
|
||||
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 2`] = `
|
||||
Object {
|
||||
"end": Any<Number>,
|
||||
"id": Any<String>,
|
||||
@@ -168,7 +80,7 @@ Object {
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 4`] = `
|
||||
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 3`] = `
|
||||
Object {
|
||||
"data": Object {
|
||||
"id": Any<String>,
|
||||
@@ -183,6 +95,19 @@ Object {
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 4`] = `
|
||||
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 5`] = `
|
||||
Object {
|
||||
"end": Any<Number>,
|
||||
@@ -197,19 +122,6 @@ Object {
|
||||
`;
|
||||
|
||||
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>,
|
||||
@@ -224,6 +136,19 @@ Object {
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 7`] = `
|
||||
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 8`] = `
|
||||
Object {
|
||||
"end": Any<Number>,
|
||||
@@ -238,35 +163,6 @@ Object {
|
||||
`;
|
||||
|
||||
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>,
|
||||
@@ -280,7 +176,7 @@ Object {
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 12`] = `
|
||||
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 10`] = `
|
||||
Object {
|
||||
"end": Any<Number>,
|
||||
"id": Any<String>,
|
||||
@@ -291,7 +187,7 @@ Object {
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 13`] = `
|
||||
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>,
|
||||
@@ -306,7 +202,7 @@ Object {
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 14`] = `
|
||||
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>,
|
||||
@@ -319,6 +215,34 @@ Object {
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 13`] = `
|
||||
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 14`] = `
|
||||
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 15`] = `
|
||||
Object {
|
||||
"end": Any<Number>,
|
||||
@@ -334,62 +258,18 @@ Object {
|
||||
|
||||
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>,
|
||||
"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 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>,
|
||||
@@ -403,7 +283,7 @@ Object {
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 21`] = `
|
||||
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>,
|
||||
@@ -414,6 +294,47 @@ Object {
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 19`] = `
|
||||
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 20`] = `
|
||||
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 21`] = `
|
||||
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 22`] = `
|
||||
Object {
|
||||
"data": Object {
|
||||
@@ -455,65 +376,7 @@ Object {
|
||||
}
|
||||
`;
|
||||
|
||||
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 {
|
||||
"mode": "full",
|
||||
"reportWhen": "never",
|
||||
},
|
||||
"end": Any<Number>,
|
||||
"id": Any<String>,
|
||||
"jobId": Any<String>,
|
||||
"jobName": "default-backupNg",
|
||||
"message": "backup",
|
||||
"scheduleId": Any<String>,
|
||||
"start": Any<Number>,
|
||||
"status": "success",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg execute three times a rolling snapshot with 2 as retention & revert to an old state 2`] = `
|
||||
Object {
|
||||
"end": Any<Number>,
|
||||
"id": Any<String>,
|
||||
@@ -524,7 +387,7 @@ Object {
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg execute three times a rolling snapshot with 2 as retention & revert to an old state 3`] = `
|
||||
exports[`backupNg execute three times a rolling snapshot with 2 as retention & revert to an old state 2`] = `
|
||||
Object {
|
||||
"data": Object {
|
||||
"id": Any<String>,
|
||||
|
||||
@@ -6,20 +6,44 @@ import { noSuchObject } from 'xo-common/api-errors'
|
||||
import config from '../_config'
|
||||
import randomId from '../_randomId'
|
||||
import xo from '../_xoConnection'
|
||||
import { getDefaultName, getDefaultSchedule } from '../_defaultValues'
|
||||
|
||||
const DEFAULT_SCHEDULE = {
|
||||
name: 'scheduleTest',
|
||||
cron: '0 * * * * *',
|
||||
const validateBackupJob = (jobInput, jobOutput, createdSchedule) => {
|
||||
const expectedObj = {
|
||||
id: expect.any(String),
|
||||
mode: jobInput.mode,
|
||||
name: jobInput.name,
|
||||
type: 'backup',
|
||||
settings: {
|
||||
'': jobInput.settings[''],
|
||||
},
|
||||
userId: xo._user.id,
|
||||
vms: jobInput.vms,
|
||||
}
|
||||
|
||||
const schedules = jobInput.schedules
|
||||
if (schedules !== undefined) {
|
||||
const scheduleTmpId = Object.keys(schedules)[0]
|
||||
expect(createdSchedule).toEqual({
|
||||
...schedules[scheduleTmpId],
|
||||
enabled: false,
|
||||
id: expect.any(String),
|
||||
jobId: jobOutput.id,
|
||||
})
|
||||
|
||||
expectedObj.settings[createdSchedule.id] = jobInput.settings[scheduleTmpId]
|
||||
}
|
||||
|
||||
expect(jobOutput).toEqual(expectedObj)
|
||||
}
|
||||
|
||||
const validateRootTask = (log, props) =>
|
||||
expect(log).toMatchSnapshot({
|
||||
const validateRootTask = (log, expected) =>
|
||||
expect(log).toEqual({
|
||||
end: expect.any(Number),
|
||||
id: expect.any(String),
|
||||
jobId: expect.any(String),
|
||||
scheduleId: expect.any(String),
|
||||
message: 'backup',
|
||||
start: expect.any(Number),
|
||||
...props,
|
||||
...expected,
|
||||
})
|
||||
|
||||
const validateVmTask = (task, vmId, props) => {
|
||||
@@ -66,88 +90,55 @@ const validateOperationTask = (task, props) => {
|
||||
})
|
||||
}
|
||||
|
||||
// Note: `bypassVdiChainsCheck` must be enabled because the XAPI might be not
|
||||
// able to coalesce VDIs as fast as the tests run.
|
||||
//
|
||||
// See https://xen-orchestra.com/docs/backup_troubleshooting.html#vdi-chain-protection
|
||||
describe('backupNg', () => {
|
||||
let defaultBackupNg
|
||||
|
||||
beforeAll(() => {
|
||||
defaultBackupNg = {
|
||||
name: 'default-backupNg',
|
||||
mode: 'full',
|
||||
vms: {
|
||||
id: config.vms.default,
|
||||
},
|
||||
settings: {
|
||||
'': {
|
||||
reportWhen: 'never',
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
describe('.createJob() :', () => {
|
||||
it('creates a new backup job without schedules', async () => {
|
||||
const backupNg = await xo.createTempBackupNgJob(defaultBackupNg)
|
||||
expect(backupNg).toMatchSnapshot({
|
||||
id: expect.any(String),
|
||||
userId: expect.any(String),
|
||||
vms: expect.any(Object),
|
||||
})
|
||||
expect(backupNg.vms).toEqual(defaultBackupNg.vms)
|
||||
expect(backupNg.userId).toBe(xo._user.id)
|
||||
const jobInput = {
|
||||
mode: 'full',
|
||||
vms: {
|
||||
id: config.vms.default,
|
||||
},
|
||||
}
|
||||
const jobOutput = await xo.createTempBackupNgJob(jobInput)
|
||||
validateBackupJob(jobInput, jobOutput)
|
||||
})
|
||||
|
||||
it('creates a new backup job with schedules', async () => {
|
||||
const scheduleTempId = randomId()
|
||||
const { id: jobId } = await xo.createTempBackupNgJob({
|
||||
...defaultBackupNg,
|
||||
const jobInput = {
|
||||
mode: 'full',
|
||||
schedules: {
|
||||
[scheduleTempId]: DEFAULT_SCHEDULE,
|
||||
[scheduleTempId]: getDefaultSchedule(),
|
||||
},
|
||||
settings: {
|
||||
...defaultBackupNg.settings,
|
||||
[scheduleTempId]: { snapshotRetention: 1 },
|
||||
},
|
||||
})
|
||||
|
||||
const backupNgJob = await xo.call('backupNg.getJob', { id: jobId })
|
||||
|
||||
expect(backupNgJob).toMatchSnapshot({
|
||||
id: expect.any(String),
|
||||
userId: expect.any(String),
|
||||
settings: expect.any(Object),
|
||||
vms: expect.any(Object),
|
||||
})
|
||||
expect(backupNgJob.vms).toEqual(defaultBackupNg.vms)
|
||||
expect(backupNgJob.userId).toBe(xo._user.id)
|
||||
|
||||
expect(Object.keys(backupNgJob.settings).length).toBe(2)
|
||||
const schedule = await xo.getSchedule({ jobId })
|
||||
expect(typeof schedule).toBe('object')
|
||||
expect(backupNgJob.settings[schedule.id]).toEqual({
|
||||
snapshotRetention: 1,
|
||||
})
|
||||
|
||||
expect(schedule).toMatchSnapshot({
|
||||
id: expect.any(String),
|
||||
jobId: expect.any(String),
|
||||
})
|
||||
vms: {
|
||||
id: config.vms.default,
|
||||
},
|
||||
}
|
||||
const jobOutput = await xo.createTempBackupNgJob(jobInput)
|
||||
validateBackupJob(
|
||||
jobInput,
|
||||
jobOutput,
|
||||
await xo.getSchedule({ jobId: jobOutput.id })
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('.delete() :', () => {
|
||||
it('deletes a backup job', async () => {
|
||||
const scheduleTempId = randomId()
|
||||
const { id: jobId } = await xo.call('backupNg.createJob', {
|
||||
...defaultBackupNg,
|
||||
const jobId = await xo.call('backupNg.createJob', {
|
||||
mode: 'full',
|
||||
name: getDefaultName(),
|
||||
vms: {
|
||||
id: config.vms.default,
|
||||
},
|
||||
schedules: {
|
||||
[scheduleTempId]: DEFAULT_SCHEDULE,
|
||||
[scheduleTempId]: getDefaultSchedule(),
|
||||
},
|
||||
settings: {
|
||||
...defaultBackupNg.settings,
|
||||
[scheduleTempId]: { snapshotRetention: 1 },
|
||||
},
|
||||
})
|
||||
@@ -173,16 +164,19 @@ describe('backupNg', () => {
|
||||
|
||||
describe('.runJob() :', () => {
|
||||
it('fails trying to run a backup job without schedule', async () => {
|
||||
const { id } = await xo.createTempBackupNgJob(defaultBackupNg)
|
||||
const { id } = await xo.createTempBackupNgJob({
|
||||
vms: {
|
||||
id: config.vms.default,
|
||||
},
|
||||
})
|
||||
await expect(xo.call('backupNg.runJob', { id })).rejects.toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('fails trying to run a backup job with no matching VMs', async () => {
|
||||
const scheduleTempId = randomId()
|
||||
const { id: jobId } = await xo.createTempBackupNgJob({
|
||||
...defaultBackupNg,
|
||||
schedules: {
|
||||
[scheduleTempId]: DEFAULT_SCHEDULE,
|
||||
[scheduleTempId]: getDefaultSchedule(),
|
||||
},
|
||||
settings: {
|
||||
[scheduleTempId]: { snapshotRetention: 1 },
|
||||
@@ -205,9 +199,8 @@ describe('backupNg', () => {
|
||||
jest.setTimeout(7e3)
|
||||
const scheduleTempId = randomId()
|
||||
const { id: jobId } = await xo.createTempBackupNgJob({
|
||||
...defaultBackupNg,
|
||||
schedules: {
|
||||
[scheduleTempId]: DEFAULT_SCHEDULE,
|
||||
[scheduleTempId]: getDefaultSchedule(),
|
||||
},
|
||||
settings: {
|
||||
[scheduleTempId]: { snapshotRetention: 1 },
|
||||
@@ -231,25 +224,23 @@ describe('backupNg', () => {
|
||||
jest.setTimeout(8e3)
|
||||
await xo.createTempServer(config.servers.default)
|
||||
const { id: vmIdWithoutDisks } = await xo.createTempVm({
|
||||
name_label: 'XO Test Without Disks',
|
||||
name_description: 'Creating a vm without disks',
|
||||
template: config.templates.templateWithoutDisks,
|
||||
})
|
||||
|
||||
const scheduleTempId = randomId()
|
||||
const { id: jobId } = await xo.createTempBackupNgJob({
|
||||
...defaultBackupNg,
|
||||
const jobInput = {
|
||||
schedules: {
|
||||
[scheduleTempId]: DEFAULT_SCHEDULE,
|
||||
[scheduleTempId]: getDefaultSchedule(),
|
||||
},
|
||||
settings: {
|
||||
...defaultBackupNg.settings,
|
||||
[scheduleTempId]: { snapshotRetention: 1 },
|
||||
},
|
||||
vms: {
|
||||
id: vmIdWithoutDisks,
|
||||
},
|
||||
})
|
||||
}
|
||||
const { id: jobId } = await xo.createTempBackupNgJob(jobInput)
|
||||
|
||||
const schedule = await xo.getSchedule({ jobId })
|
||||
expect(typeof schedule).toBe('object')
|
||||
@@ -264,12 +255,16 @@ describe('backupNg', () => {
|
||||
jobId,
|
||||
scheduleId: schedule.id,
|
||||
})
|
||||
expect(log).toMatchSnapshot({
|
||||
end: expect.any(Number),
|
||||
id: expect.any(String),
|
||||
jobId: expect.any(String),
|
||||
scheduleId: expect.any(String),
|
||||
start: expect.any(Number),
|
||||
|
||||
validateRootTask(log, {
|
||||
data: {
|
||||
mode: jobInput.mode,
|
||||
reportWhen: jobInput.settings[''].reportWhen,
|
||||
},
|
||||
jobId,
|
||||
jobName: jobInput.name,
|
||||
scheduleId: schedule.id,
|
||||
status: 'skipped',
|
||||
})
|
||||
|
||||
expect(vmTask).toMatchSnapshot({
|
||||
@@ -293,22 +288,24 @@ describe('backupNg', () => {
|
||||
const scheduleTempId = randomId()
|
||||
await xo.createTempServer(config.servers.default)
|
||||
const { id: remoteId } = await xo.createTempRemote(config.remotes.default)
|
||||
const { id: jobId } = await xo.createTempBackupNgJob({
|
||||
...defaultBackupNg,
|
||||
const jobInput = {
|
||||
remotes: {
|
||||
id: remoteId,
|
||||
},
|
||||
schedules: {
|
||||
[scheduleTempId]: DEFAULT_SCHEDULE,
|
||||
[scheduleTempId]: getDefaultSchedule(),
|
||||
},
|
||||
settings: {
|
||||
...defaultBackupNg.settings,
|
||||
[scheduleTempId]: {},
|
||||
},
|
||||
srs: {
|
||||
id: config.srs.default,
|
||||
},
|
||||
})
|
||||
vms: {
|
||||
id: config.vms.default,
|
||||
},
|
||||
}
|
||||
const { id: jobId } = await xo.createTempBackupNgJob(jobInput)
|
||||
|
||||
const schedule = await xo.getSchedule({ jobId })
|
||||
expect(typeof schedule).toBe('object')
|
||||
@@ -324,12 +321,15 @@ describe('backupNg', () => {
|
||||
scheduleId: schedule.id,
|
||||
})
|
||||
|
||||
expect(log).toMatchSnapshot({
|
||||
end: expect.any(Number),
|
||||
id: expect.any(String),
|
||||
jobId: expect.any(String),
|
||||
scheduleId: expect.any(String),
|
||||
start: expect.any(Number),
|
||||
validateRootTask(log, {
|
||||
data: {
|
||||
mode: jobInput.mode,
|
||||
reportWhen: jobInput.settings[''].reportWhen,
|
||||
},
|
||||
jobId,
|
||||
jobName: jobInput.name,
|
||||
scheduleId: schedule.id,
|
||||
status: 'failure',
|
||||
})
|
||||
|
||||
expect(task).toMatchSnapshot({
|
||||
@@ -352,7 +352,6 @@ describe('backupNg', () => {
|
||||
jest.setTimeout(6e4)
|
||||
await xo.createTempServer(config.servers.default)
|
||||
let vm = await xo.createTempVm({
|
||||
name_label: 'XO Test Temp',
|
||||
name_description: 'Creating a temporary vm',
|
||||
template: config.templates.default,
|
||||
VDIs: [
|
||||
@@ -365,22 +364,18 @@ describe('backupNg', () => {
|
||||
})
|
||||
|
||||
const scheduleTempId = randomId()
|
||||
const { id: jobId } = await xo.createTempBackupNgJob({
|
||||
...defaultBackupNg,
|
||||
const jobInput = {
|
||||
vms: {
|
||||
id: vm.id,
|
||||
},
|
||||
schedules: {
|
||||
[scheduleTempId]: DEFAULT_SCHEDULE,
|
||||
[scheduleTempId]: getDefaultSchedule(),
|
||||
},
|
||||
settings: {
|
||||
'': {
|
||||
bypassVdiChainsCheck: true,
|
||||
reportWhen: 'never',
|
||||
},
|
||||
[scheduleTempId]: { snapshotRetention: 2 },
|
||||
},
|
||||
})
|
||||
}
|
||||
const { id: jobId } = await xo.createTempBackupNgJob(jobInput)
|
||||
|
||||
const schedule = await xo.getSchedule({ jobId })
|
||||
expect(typeof schedule).toBe('object')
|
||||
@@ -420,12 +415,15 @@ describe('backupNg', () => {
|
||||
scheduleId: schedule.id,
|
||||
})
|
||||
|
||||
expect(log).toMatchSnapshot({
|
||||
end: expect.any(Number),
|
||||
id: expect.any(String),
|
||||
jobId: expect.any(String),
|
||||
scheduleId: expect.any(String),
|
||||
start: expect.any(Number),
|
||||
validateRootTask(log, {
|
||||
data: {
|
||||
mode: jobInput.mode,
|
||||
reportWhen: jobInput.settings[''].reportWhen,
|
||||
},
|
||||
jobId,
|
||||
jobName: jobInput.name,
|
||||
scheduleId: schedule.id,
|
||||
status: 'success',
|
||||
})
|
||||
|
||||
const subTaskSnapshot = subTasks.find(
|
||||
@@ -470,7 +468,7 @@ describe('backupNg', () => {
|
||||
const exportRetention = 2
|
||||
const fullInterval = 2
|
||||
const scheduleTempId = randomId()
|
||||
const { id: jobId } = await xo.createTempBackupNgJob({
|
||||
const jobInput = {
|
||||
mode: 'delta',
|
||||
remotes: {
|
||||
id: {
|
||||
@@ -478,13 +476,11 @@ describe('backupNg', () => {
|
||||
},
|
||||
},
|
||||
schedules: {
|
||||
[scheduleTempId]: DEFAULT_SCHEDULE,
|
||||
[scheduleTempId]: getDefaultSchedule(),
|
||||
},
|
||||
settings: {
|
||||
'': {
|
||||
bypassVdiChainsCheck: true,
|
||||
fullInterval,
|
||||
reportWhen: 'never',
|
||||
},
|
||||
[remoteId1]: { deleteFirst: true },
|
||||
[scheduleTempId]: { exportRetention },
|
||||
@@ -492,7 +488,8 @@ describe('backupNg', () => {
|
||||
vms: {
|
||||
id: vmToBackup,
|
||||
},
|
||||
})
|
||||
}
|
||||
const { id: jobId } = await xo.createTempBackupNgJob(jobInput)
|
||||
|
||||
const schedule = await xo.getSchedule({ jobId })
|
||||
expect(typeof schedule).toBe('object')
|
||||
@@ -515,10 +512,12 @@ describe('backupNg', () => {
|
||||
backupLogs.forEach(({ tasks = [], ...log }, key) => {
|
||||
validateRootTask(log, {
|
||||
data: {
|
||||
mode: 'delta',
|
||||
reportWhen: 'never',
|
||||
mode: jobInput.mode,
|
||||
reportWhen: jobInput.settings[''].reportWhen,
|
||||
},
|
||||
message: 'backup',
|
||||
jobId,
|
||||
jobName: jobInput.name,
|
||||
scheduleId: schedule.id,
|
||||
status: 'success',
|
||||
})
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import expect from 'must'
|
||||
// ===================================================================
|
||||
|
||||
import { getConfig, getMainConnection, getSrId, waitObjectState } from './util'
|
||||
import { map, assign } from 'lodash'
|
||||
import { map } from 'lodash'
|
||||
import eventToPromise from 'event-to-promise'
|
||||
|
||||
// ===================================================================
|
||||
@@ -27,7 +27,7 @@ describe('disk', () => {
|
||||
const config = await getConfig()
|
||||
serverId = await xo.call(
|
||||
'server.add',
|
||||
assign({ autoConnect: false }, config.xenServer1)
|
||||
Object.assign({ autoConnect: false }, config.xenServer1)
|
||||
)
|
||||
await xo.call('server.connect', { id: serverId })
|
||||
await eventToPromise(xo.objects, 'finish')
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-env jest */
|
||||
|
||||
import { assign, find, map } from 'lodash'
|
||||
import { find, map } from 'lodash'
|
||||
|
||||
import { config, rejectionOf, xo } from './util'
|
||||
|
||||
@@ -151,7 +151,7 @@ describe('server', () => {
|
||||
|
||||
it('connects to a Xen server', async () => {
|
||||
const serverId = await addServer(
|
||||
assign({ autoConnect: false }, config.xenServer1)
|
||||
Object.assign({ autoConnect: false }, config.xenServer1)
|
||||
)
|
||||
|
||||
await xo.call('server.connect', {
|
||||
@@ -184,7 +184,7 @@ describe('server', () => {
|
||||
let serverId
|
||||
beforeEach(async () => {
|
||||
serverId = await addServer(
|
||||
assign({ autoConnect: false }, config.xenServer1)
|
||||
Object.assign({ autoConnect: false }, config.xenServer1)
|
||||
)
|
||||
await xo.call('server.connect', {
|
||||
id: serverId,
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
getOneHost,
|
||||
waitObjectState,
|
||||
} from './util'
|
||||
import { assign, map } from 'lodash'
|
||||
import { map } from 'lodash'
|
||||
import eventToPromise from 'event-to-promise'
|
||||
|
||||
// ===================================================================
|
||||
@@ -33,7 +33,7 @@ describe('vbd', () => {
|
||||
|
||||
serverId = await xo.call(
|
||||
'server.add',
|
||||
assign({ autoConnect: false }, config.xenServer1)
|
||||
Object.assign({ autoConnect: false }, config.xenServer1)
|
||||
)
|
||||
await xo.call('server.connect', { id: serverId })
|
||||
await eventToPromise(xo.objects, 'finish')
|
||||
|
||||
@@ -34,14 +34,14 @@
|
||||
"dependencies": {
|
||||
"nodemailer": "^6.1.0",
|
||||
"nodemailer-markdown": "^1.0.1",
|
||||
"promise-toolbox": "^0.13.0"
|
||||
"promise-toolbox": "^0.14.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.0.0",
|
||||
"@babel/core": "^7.0.0",
|
||||
"@babel/preset-env": "^7.0.0",
|
||||
"babel-plugin-lodash": "^3.3.2",
|
||||
"cross-env": "^5.1.3",
|
||||
"cross-env": "^6.0.3",
|
||||
"rimraf": "^3.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
"@babel/core": "^7.0.0",
|
||||
"@babel/preset-env": "^7.0.0",
|
||||
"babel-preset-env": "^1.5.2",
|
||||
"cross-env": "^5.1.3",
|
||||
"cross-env": "^6.0.3",
|
||||
"rimraf": "^3.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -33,14 +33,14 @@
|
||||
"node": ">=6"
|
||||
},
|
||||
"dependencies": {
|
||||
"promise-toolbox": "^0.13.0",
|
||||
"promise-toolbox": "^0.14.0",
|
||||
"slack-node": "^0.1.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.0.0",
|
||||
"@babel/core": "^7.0.0",
|
||||
"@babel/preset-env": "^7.0.0",
|
||||
"cross-env": "^5.1.3",
|
||||
"cross-env": "^6.0.3",
|
||||
"rimraf": "^3.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
"@babel/cli": "^7.0.0",
|
||||
"@babel/core": "^7.0.0",
|
||||
"@babel/preset-env": "^7.0.0",
|
||||
"cross-env": "^5.1.3",
|
||||
"cross-env": "^6.0.3",
|
||||
"rimraf": "^3.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -42,14 +42,14 @@
|
||||
"html-minifier": "^4.0.0",
|
||||
"human-format": "^0.10.0",
|
||||
"lodash": "^4.17.4",
|
||||
"promise-toolbox": "^0.13.0"
|
||||
"promise-toolbox": "^0.14.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.0.0",
|
||||
"@babel/core": "^7.0.0",
|
||||
"@babel/preset-env": "^7.0.0",
|
||||
"babel-plugin-lodash": "^3.3.2",
|
||||
"cross-env": "^5.1.3",
|
||||
"cross-env": "^6.0.3",
|
||||
"rimraf": "^3.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -5,7 +5,6 @@ import humanFormat from 'human-format'
|
||||
import { createSchedule } from '@xen-orchestra/cron'
|
||||
import { minify } from 'html-minifier'
|
||||
import {
|
||||
assign,
|
||||
concat,
|
||||
differenceBy,
|
||||
filter,
|
||||
@@ -418,7 +417,7 @@ function computeGlobalVmsStats({ haltedVms, vmsStats, xo }) {
|
||||
}))
|
||||
)
|
||||
|
||||
return assign(
|
||||
return Object.assign(
|
||||
computeMeans(vmsStats, [
|
||||
'cpu',
|
||||
'ram',
|
||||
@@ -446,7 +445,7 @@ function computeGlobalHostsStats({ haltedHosts, hostsStats, xo }) {
|
||||
}))
|
||||
)
|
||||
|
||||
return assign(
|
||||
return Object.assign(
|
||||
computeMeans(hostsStats, [
|
||||
'cpu',
|
||||
'ram',
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
"bin": "bin"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
"node": ">=8"
|
||||
},
|
||||
"dependencies": {
|
||||
"@iarna/toml": "^2.2.1",
|
||||
@@ -58,16 +58,15 @@
|
||||
"debug": "^4.0.1",
|
||||
"decorator-synchronized": "^0.5.0",
|
||||
"deptree": "^1.0.0",
|
||||
"escape-string-regexp": "^1.0.5",
|
||||
"event-to-promise": "^0.8.0",
|
||||
"exec-promise": "^0.7.0",
|
||||
"execa": "^1.0.0",
|
||||
"execa": "^2.0.5",
|
||||
"express": "^4.16.2",
|
||||
"express-session": "^1.15.6",
|
||||
"fatfs": "^0.10.4",
|
||||
"from2": "^2.3.0",
|
||||
"fs-extra": "^8.0.1",
|
||||
"get-stream": "^4.0.0",
|
||||
"get-stream": "^5.1.0",
|
||||
"golike-defer": "^0.4.1",
|
||||
"hashy": "^0.7.1",
|
||||
"helmet": "^3.9.0",
|
||||
@@ -91,7 +90,7 @@
|
||||
"limit-concurrency-decorator": "^0.4.0",
|
||||
"lodash": "^4.17.4",
|
||||
"make-error": "^1",
|
||||
"micromatch": "^3.1.4",
|
||||
"micromatch": "^4.0.2",
|
||||
"minimist": "^1.2.0",
|
||||
"moment-timezone": "^0.5.14",
|
||||
"ms": "^2.1.1",
|
||||
@@ -103,7 +102,7 @@
|
||||
"passport": "^0.4.0",
|
||||
"passport-local": "^1.0.0",
|
||||
"pretty-format": "^24.0.0",
|
||||
"promise-toolbox": "^0.13.0",
|
||||
"promise-toolbox": "^0.14.0",
|
||||
"proxy-agent": "^3.0.0",
|
||||
"pug": "^2.0.0-rc.4",
|
||||
"pump": "^3.0.0",
|
||||
@@ -123,7 +122,7 @@
|
||||
"uuid": "^3.0.1",
|
||||
"value-matcher": "^0.2.0",
|
||||
"vhd-lib": "^0.7.0",
|
||||
"ws": "^6.0.0",
|
||||
"ws": "^7.1.2",
|
||||
"xen-api": "^0.27.2",
|
||||
"xml2js": "^0.4.19",
|
||||
"xo-acl-resolver": "^0.4.1",
|
||||
@@ -148,7 +147,7 @@
|
||||
"@babel/preset-flow": "^7.0.0",
|
||||
"babel-plugin-lodash": "^3.3.2",
|
||||
"babel-plugin-transform-dev": "^2.0.1",
|
||||
"cross-env": "^5.1.3",
|
||||
"cross-env": "^6.0.3",
|
||||
"index-modules": "^0.3.0",
|
||||
"rimraf": "^3.0.0"
|
||||
},
|
||||
|
||||
@@ -8,7 +8,7 @@ import { safeDateFormat } from '../utils'
|
||||
|
||||
export function createJob({ schedules, ...job }) {
|
||||
job.userId = this.user.id
|
||||
return this.createBackupNgJob(job, schedules)
|
||||
return this.createBackupNgJob(job, schedules).then(({ id }) => id)
|
||||
}
|
||||
|
||||
createJob.permission = 'admin'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// FIXME: rename to disk.*
|
||||
|
||||
import { invalidParameters } from 'xo-common/api-errors'
|
||||
import { isArray, reduce } from 'lodash'
|
||||
import { reduce } from 'lodash'
|
||||
|
||||
import { parseSize } from '../utils'
|
||||
|
||||
@@ -85,7 +85,7 @@ export async function set(params) {
|
||||
continue
|
||||
}
|
||||
|
||||
for (const field of isArray(fields) ? fields : [fields]) {
|
||||
for (const field of Array.isArray(fields) ? fields : [fields]) {
|
||||
await xapi.call(`VDI.set_${field}`, ref, `${params[param]}`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Model from './model'
|
||||
import { BaseError } from 'make-error'
|
||||
import { EventEmitter } from 'events'
|
||||
import { isArray, isObject, map } from './utils'
|
||||
import { isObject, map } from './utils'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@@ -30,7 +30,7 @@ export default class Collection extends EventEmitter {
|
||||
}
|
||||
|
||||
async add(models, opts) {
|
||||
const array = isArray(models)
|
||||
const array = Array.isArray(models)
|
||||
if (!array) {
|
||||
models = [models]
|
||||
}
|
||||
@@ -66,7 +66,7 @@ export default class Collection extends EventEmitter {
|
||||
}
|
||||
|
||||
async remove(ids) {
|
||||
if (!isArray(ids)) {
|
||||
if (!Array.isArray(ids)) {
|
||||
ids = [ids]
|
||||
}
|
||||
|
||||
@@ -77,8 +77,8 @@ export default class Collection extends EventEmitter {
|
||||
}
|
||||
|
||||
async update(models) {
|
||||
const array = isArray(models)
|
||||
if (!isArray(models)) {
|
||||
const array = Array.isArray(models)
|
||||
if (!array) {
|
||||
models = [models]
|
||||
}
|
||||
|
||||
|
||||
@@ -29,13 +29,7 @@ import { ensureDir, readdir, readFile } from 'fs-extra'
|
||||
|
||||
import parseDuration from './_parseDuration'
|
||||
import Xo from './xo'
|
||||
import {
|
||||
forEach,
|
||||
isArray,
|
||||
isFunction,
|
||||
mapToArray,
|
||||
pFromCallback,
|
||||
} from './utils'
|
||||
import { forEach, mapToArray, pFromCallback } from './utils'
|
||||
|
||||
import bodyParser from 'body-parser'
|
||||
import connectFlash from 'connect-flash'
|
||||
@@ -281,15 +275,16 @@ async function registerPlugin(pluginPath, pluginName) {
|
||||
|
||||
// The default export can be either a factory or directly a plugin
|
||||
// instance.
|
||||
const instance = isFunction(factory)
|
||||
? factory({
|
||||
xo: this,
|
||||
getDataDir: () => {
|
||||
const dir = `${this._config.datadir}/${pluginName}`
|
||||
return ensureDir(dir).then(() => dir)
|
||||
},
|
||||
})
|
||||
: factory
|
||||
const instance =
|
||||
typeof factory === 'function'
|
||||
? factory({
|
||||
xo: this,
|
||||
getDataDir: () => {
|
||||
const dir = `${this._config.datadir}/${pluginName}`
|
||||
return ensureDir(dir).then(() => dir)
|
||||
},
|
||||
})
|
||||
: factory
|
||||
|
||||
await this.registerPlugin(
|
||||
pluginName,
|
||||
@@ -468,7 +463,7 @@ const setUpProxies = (express, opts, xo) => {
|
||||
|
||||
const setUpStaticFiles = (express, opts) => {
|
||||
forEach(opts, (paths, url) => {
|
||||
if (!isArray(paths)) {
|
||||
if (!Array.isArray(paths)) {
|
||||
paths = [paths]
|
||||
}
|
||||
|
||||
|
||||
@@ -8,19 +8,21 @@ const parse = createParser({
|
||||
keyTransform: key => key.slice(5).toLowerCase(),
|
||||
})
|
||||
const makeFunction = command => async (fields, ...args) => {
|
||||
return splitLines(
|
||||
await execa.stdout(command, [
|
||||
'--noheading',
|
||||
'--nosuffix',
|
||||
'--nameprefixes',
|
||||
'--unbuffered',
|
||||
'--units',
|
||||
'b',
|
||||
'-o',
|
||||
String(fields),
|
||||
...args,
|
||||
])
|
||||
).map(Array.isArray(fields) ? parse : line => parse(line)[fields])
|
||||
const { stdout } = await execa(command, [
|
||||
'--noheading',
|
||||
'--nosuffix',
|
||||
'--nameprefixes',
|
||||
'--unbuffered',
|
||||
'--units',
|
||||
'b',
|
||||
'-o',
|
||||
String(fields),
|
||||
...args,
|
||||
])
|
||||
|
||||
return splitLines(stdout).map(
|
||||
Array.isArray(fields) ? parse : line => parse(line)[fields]
|
||||
)
|
||||
}
|
||||
|
||||
export const lvs = makeFunction('lvs')
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import assign from 'lodash/assign'
|
||||
|
||||
const _combine = (vectors, n, cb) => {
|
||||
if (!n) {
|
||||
return
|
||||
@@ -35,7 +33,7 @@ export const combine = vectors => cb => _combine(vectors, vectors.length, cb)
|
||||
// Merge the properties of an objects set in one object.
|
||||
//
|
||||
// Ex: mergeObjects([ { a: 1 }, { b: 2 } ]) => { a: 1, b: 2 }
|
||||
export const mergeObjects = objects => assign({}, ...objects)
|
||||
export const mergeObjects = objects => Object.assign({}, ...objects)
|
||||
|
||||
// Compute a cross product between vectors.
|
||||
//
|
||||
|
||||
@@ -256,10 +256,8 @@ export const safeDateParse = utcParse('%Y%m%dT%H%M%SZ')
|
||||
//
|
||||
// Exports them from here to avoid direct dependencies on lodash/
|
||||
export { default as forEach } from 'lodash/forEach'
|
||||
export { default as isArray } from 'lodash/isArray'
|
||||
export { default as isBoolean } from 'lodash/isBoolean'
|
||||
export { default as isEmpty } from 'lodash/isEmpty'
|
||||
export { default as isFunction } from 'lodash/isFunction'
|
||||
export { default as isInteger } from 'lodash/isInteger'
|
||||
export { default as isObject } from 'lodash/isObject'
|
||||
export { default as isString } from 'lodash/isString'
|
||||
|
||||
@@ -3,7 +3,6 @@ import ensureArray from './_ensureArray'
|
||||
import {
|
||||
extractProperty,
|
||||
forEach,
|
||||
isArray,
|
||||
isEmpty,
|
||||
mapFilter,
|
||||
mapToArray,
|
||||
@@ -27,7 +26,7 @@ function link(obj, prop, idField = '$id') {
|
||||
return dynamicValue // Properly handles null and undefined.
|
||||
}
|
||||
|
||||
if (isArray(dynamicValue)) {
|
||||
if (Array.isArray(dynamicValue)) {
|
||||
return mapToArray(dynamicValue, idField)
|
||||
}
|
||||
|
||||
|
||||
@@ -42,7 +42,6 @@ import pRetry from '../_pRetry'
|
||||
import {
|
||||
camelToSnakeCase,
|
||||
forEach,
|
||||
isFunction,
|
||||
map,
|
||||
mapToArray,
|
||||
pAll,
|
||||
@@ -82,7 +81,7 @@ export const TAG_COPY_SRC = 'xo:copy_of'
|
||||
|
||||
// FIXME: remove this work around when fixed, https://phabricator.babeljs.io/T2877
|
||||
// export * from './utils'
|
||||
require('lodash/assign')(module.exports, require('./utils'))
|
||||
Object.assign(module.exports, require('./utils'))
|
||||
|
||||
// VDI formats. (Raw is not available for delta vdi.)
|
||||
export const VDI_FORMAT_VHD = 'vhd'
|
||||
@@ -174,7 +173,7 @@ export default class Xapi extends XapiBase {
|
||||
//
|
||||
// TODO: implements a timeout.
|
||||
_waitObject(predicate) {
|
||||
if (isFunction(predicate)) {
|
||||
if (typeof predicate === 'function') {
|
||||
const { promise, resolve } = defer()
|
||||
|
||||
const unregister = this._registerGenericWatcher(obj => {
|
||||
|
||||
@@ -9,9 +9,7 @@ import { satisfies as versionSatisfies } from 'semver'
|
||||
import {
|
||||
camelToSnakeCase,
|
||||
forEach,
|
||||
isArray,
|
||||
isBoolean,
|
||||
isFunction,
|
||||
isInteger,
|
||||
isString,
|
||||
map,
|
||||
@@ -48,7 +46,7 @@ export const prepareXapiParam = param => {
|
||||
if (isBoolean(param)) {
|
||||
return asBoolean(param)
|
||||
}
|
||||
if (isArray(param)) {
|
||||
if (Array.isArray(param)) {
|
||||
return map(param, prepareXapiParam)
|
||||
}
|
||||
if (isPlainObject(param)) {
|
||||
@@ -142,7 +140,7 @@ export const makeEditObject = specs => {
|
||||
return get
|
||||
}
|
||||
const normalizeSet = (set, name) => {
|
||||
if (isFunction(set)) {
|
||||
if (typeof set === 'function') {
|
||||
return set
|
||||
}
|
||||
|
||||
@@ -176,7 +174,7 @@ export const makeEditObject = specs => {
|
||||
}
|
||||
}
|
||||
|
||||
if (!isArray(set)) {
|
||||
if (!Array.isArray(set)) {
|
||||
throw new Error('must be an array, a function or a string')
|
||||
}
|
||||
|
||||
@@ -212,7 +210,7 @@ export const makeEditObject = specs => {
|
||||
}
|
||||
|
||||
forEach(spec.constraints, (constraint, constraintName) => {
|
||||
if (!isFunction(constraint)) {
|
||||
if (typeof constraint !== 'function') {
|
||||
throw new Error('constraint must be a function')
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import createLogger from '@xen-orchestra/log'
|
||||
import kindOf from 'kindof'
|
||||
import ms from 'ms'
|
||||
import schemaInspector from 'schema-inspector'
|
||||
import { forEach, isFunction } from 'lodash'
|
||||
import { forEach } from 'lodash'
|
||||
import { getBoundPropertyDescriptor } from 'bind-property-descriptor'
|
||||
import { MethodNotFound } from 'json-rpc-peer'
|
||||
|
||||
@@ -183,7 +183,7 @@ export default class Api {
|
||||
const addMethod = (method, name) => {
|
||||
name = base + name
|
||||
|
||||
if (isFunction(method)) {
|
||||
if (typeof method === 'function') {
|
||||
removes.push(this.addApiMethod(name, method))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { type Pattern, createPredicate } from 'value-matcher'
|
||||
import { type Readable, PassThrough } from 'stream'
|
||||
import { AssertionError } from 'assert'
|
||||
import { basename, dirname } from 'path'
|
||||
import { pipeline } from 'readable-stream'
|
||||
import {
|
||||
countBy,
|
||||
findLast,
|
||||
@@ -29,7 +30,7 @@ import {
|
||||
CancelToken,
|
||||
ignoreErrors,
|
||||
pFinally,
|
||||
pFromEvent,
|
||||
pFromCallback,
|
||||
timeout,
|
||||
} from 'promise-toolbox'
|
||||
import Vhd, {
|
||||
@@ -343,8 +344,7 @@ const writeStream = async (
|
||||
const tmpPath = `${dirname(path)}/.${basename(path)}`
|
||||
const output = await handler.createOutputStream(tmpPath, { checksum })
|
||||
try {
|
||||
input.pipe(output)
|
||||
await pFromEvent(output, 'finish')
|
||||
await pFromCallback(pipeline, input, output)
|
||||
await output.checksumWritten
|
||||
// $FlowFixMe
|
||||
await input.task
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import asyncMap from '@xen-orchestra/async-map'
|
||||
import createLogger from '@xen-orchestra/log'
|
||||
import deferrable from 'golike-defer'
|
||||
import escapeStringRegexp from 'escape-string-regexp'
|
||||
import execa from 'execa'
|
||||
import splitLines from 'split-lines'
|
||||
import { CancelToken, fromEvent, ignoreErrors } from 'promise-toolbox'
|
||||
@@ -10,7 +9,16 @@ import { createReadStream, readdir, stat } from 'fs'
|
||||
import { satisfies as versionSatisfies } from 'semver'
|
||||
import { utcFormat } from 'd3-time-format'
|
||||
import { basename, dirname } from 'path'
|
||||
import { filter, find, includes, once, range, sortBy, trim } from 'lodash'
|
||||
import {
|
||||
escapeRegExp,
|
||||
filter,
|
||||
find,
|
||||
includes,
|
||||
once,
|
||||
range,
|
||||
sortBy,
|
||||
trim,
|
||||
} from 'lodash'
|
||||
import {
|
||||
chainVhd,
|
||||
createSyntheticStream as createVhdReadStream,
|
||||
@@ -139,22 +147,20 @@ const listPartitions = (() => {
|
||||
})
|
||||
|
||||
return device =>
|
||||
execa
|
||||
.stdout('partx', [
|
||||
'--bytes',
|
||||
'--output=NR,START,SIZE,NAME,UUID,TYPE',
|
||||
'--pairs',
|
||||
device.path,
|
||||
])
|
||||
.then(stdout =>
|
||||
mapFilter(splitLines(stdout), line => {
|
||||
const partition = parseLine(line)
|
||||
const { type } = partition
|
||||
if (type != null && !IGNORED[+type]) {
|
||||
return partition
|
||||
}
|
||||
})
|
||||
)
|
||||
execa('partx', [
|
||||
'--bytes',
|
||||
'--output=NR,START,SIZE,NAME,UUID,TYPE',
|
||||
'--pairs',
|
||||
device.path,
|
||||
]).then(({ stdout }) =>
|
||||
mapFilter(splitLines(stdout), line => {
|
||||
const partition = parseLine(line)
|
||||
const { type } = partition
|
||||
if (type != null && !IGNORED[+type]) {
|
||||
return partition
|
||||
}
|
||||
})
|
||||
)
|
||||
})()
|
||||
|
||||
// handle LVM logical volumes automatically
|
||||
@@ -271,7 +277,7 @@ const mountLvmPv = (device, partition) => {
|
||||
}
|
||||
args.push('--show', '-f', device.path)
|
||||
|
||||
return execa.stdout('losetup', args).then(stdout => {
|
||||
return execa('losetup', args).then(({ stdout }) => {
|
||||
const path = trim(stdout)
|
||||
return {
|
||||
path,
|
||||
@@ -862,7 +868,7 @@ export default class {
|
||||
const files = await handler.list('.')
|
||||
|
||||
const reg = new RegExp(
|
||||
'^[^_]+_' + escapeStringRegexp(`${tag}_${vm.name_label}.xva`)
|
||||
'^[^_]+_' + escapeRegExp(`${tag}_${vm.name_label}.xva`)
|
||||
)
|
||||
const backups = sortBy(filter(files, fileName => reg.test(fileName)))
|
||||
|
||||
@@ -887,9 +893,7 @@ export default class {
|
||||
|
||||
xapi._assertHealthyVdiChains(vm)
|
||||
|
||||
const reg = new RegExp(
|
||||
'^rollingSnapshot_[^_]+_' + escapeStringRegexp(tag) + '_'
|
||||
)
|
||||
const reg = new RegExp('^rollingSnapshot_[^_]+_' + escapeRegExp(tag) + '_')
|
||||
const snapshots = sortBy(
|
||||
filter(vm.$snapshots, snapshot => reg.test(snapshot.name_label)),
|
||||
'name_label'
|
||||
@@ -926,9 +930,7 @@ export default class {
|
||||
const transferStart = Date.now()
|
||||
tag = 'DR_' + tag
|
||||
const reg = new RegExp(
|
||||
'^' +
|
||||
escapeStringRegexp(`${vm.name_label}_${tag}_`) +
|
||||
'[0-9]{8}T[0-9]{6}Z$'
|
||||
'^' + escapeRegExp(`${vm.name_label}_${tag}_`) + '[0-9]{8}T[0-9]{6}Z$'
|
||||
)
|
||||
|
||||
const targetXapi = this._xo.getXapi(sr)
|
||||
|
||||
@@ -87,7 +87,7 @@ async function mountLvmPhysicalVolume(devicePath, partition) {
|
||||
args.push('-o', partition.start * 512)
|
||||
}
|
||||
args.push('--show', '-f', devicePath)
|
||||
const path = (await execa.stdout('losetup', args)).trim()
|
||||
const path = (await execa('losetup', args)).stdout.trim()
|
||||
await execa('pvscan', ['--cache', path])
|
||||
|
||||
return {
|
||||
@@ -251,7 +251,7 @@ export default class BackupNgFileRestore {
|
||||
}
|
||||
|
||||
async _listPartitions(devicePath, inspectLvmPv = true) {
|
||||
const stdout = await execa.stdout('partx', [
|
||||
const { stdout } = await execa('partx', [
|
||||
'--bytes',
|
||||
'--output=NR,START,SIZE,NAME,UUID,TYPE',
|
||||
'--pairs',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import asyncMap from '@xen-orchestra/async-map'
|
||||
import { createPredicate } from 'value-matcher'
|
||||
import { timeout } from 'promise-toolbox'
|
||||
import { assign, filter, isEmpty, map, mapValues } from 'lodash'
|
||||
import { filter, isEmpty, map, mapValues } from 'lodash'
|
||||
|
||||
import { crossProduct } from '../../math'
|
||||
import { serializeError, thunkToArray } from '../../utils'
|
||||
@@ -82,7 +82,11 @@ export default async function executeJobCall({
|
||||
params,
|
||||
start: Date.now(),
|
||||
})
|
||||
let promise = app.callApiMethod(session, job.method, assign({}, params))
|
||||
let promise = app.callApiMethod(
|
||||
session,
|
||||
job.method,
|
||||
Object.assign({}, params)
|
||||
)
|
||||
if (job.timeout) {
|
||||
promise = promise::timeout(job.timeout)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { invalidParameters, noSuchObject } from 'xo-common/api-errors'
|
||||
|
||||
import * as sensitiveValues from '../sensitive-values'
|
||||
import { PluginsMetadata } from '../models/plugin-metadata'
|
||||
import { isFunction, mapToArray } from '../utils'
|
||||
import { mapToArray } from '../utils'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@@ -65,9 +65,9 @@ export default class {
|
||||
id,
|
||||
instance,
|
||||
name,
|
||||
testable: isFunction(instance.test),
|
||||
testable: typeof instance.test === 'function',
|
||||
testSchema,
|
||||
unloadable: isFunction(instance.unload),
|
||||
unloadable: typeof instance.unload === 'function',
|
||||
version,
|
||||
})
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import asyncMap from '@xen-orchestra/async-map'
|
||||
import synchronized from 'decorator-synchronized'
|
||||
import {
|
||||
assign,
|
||||
every,
|
||||
forEach,
|
||||
isObject,
|
||||
@@ -123,7 +122,7 @@ export default class {
|
||||
}
|
||||
|
||||
async computeVmResourcesUsage(vm) {
|
||||
return assign(
|
||||
return Object.assign(
|
||||
computeVmResourcesUsage(this._xo.getXapi(vm).getObject(vm._xapiId)),
|
||||
await this._xo.computeVmIpPoolsUsage(vm)
|
||||
)
|
||||
|
||||
@@ -2,7 +2,7 @@ import levelup from 'level-party'
|
||||
import sublevel from 'level-sublevel'
|
||||
import { ensureDir } from 'fs-extra'
|
||||
|
||||
import { forEach, isFunction, promisify } from '../utils'
|
||||
import { forEach, promisify } from '../utils'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@@ -32,7 +32,7 @@ const levelHas = db => {
|
||||
const levelPromise = db => {
|
||||
const dbP = {}
|
||||
forEach(db, (value, name) => {
|
||||
if (!isFunction(value)) {
|
||||
if (typeof value !== 'function') {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
forEach,
|
||||
includes,
|
||||
isEmpty,
|
||||
isFunction,
|
||||
isString,
|
||||
iteratee,
|
||||
map as mapToArray,
|
||||
@@ -210,7 +209,7 @@ export default class Xo extends EventEmitter {
|
||||
}
|
||||
|
||||
// For security, prevent from accessing `this`.
|
||||
if (isFunction(value)) {
|
||||
if (typeof value === 'function') {
|
||||
value = (value =>
|
||||
function() {
|
||||
return value.apply(thisArg, arguments)
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
"child-process-promise": "^2.0.3",
|
||||
"core-js": "^3.0.0",
|
||||
"pipette": "^0.9.3",
|
||||
"promise-toolbox": "^0.13.0",
|
||||
"promise-toolbox": "^0.14.0",
|
||||
"tmp": "^0.1.0",
|
||||
"vhd-lib": "^0.7.0"
|
||||
},
|
||||
@@ -36,7 +36,7 @@
|
||||
"@babel/core": "^7.0.0",
|
||||
"@babel/preset-env": "^7.0.0",
|
||||
"babel-plugin-lodash": "^3.3.2",
|
||||
"cross-env": "^5.1.3",
|
||||
"cross-env": "^6.0.3",
|
||||
"event-to-promise": "^0.8.0",
|
||||
"execa": "^2.0.2",
|
||||
"fs-extra": "^8.0.1",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "xo-web",
|
||||
"version": "5.50.2",
|
||||
"version": "5.50.3",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "Web interface client for Xen-Orchestra",
|
||||
"keywords": [
|
||||
@@ -97,7 +97,7 @@
|
||||
"moment-timezone": "^0.5.14",
|
||||
"notifyjs": "^3.0.0",
|
||||
"otplib": "^11.0.0",
|
||||
"promise-toolbox": "^0.13.0",
|
||||
"promise-toolbox": "^0.14.0",
|
||||
"prop-types": "^15.6.0",
|
||||
"qrcode": "^1.3.2",
|
||||
"random-password": "^0.1.2",
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import { isFunction } from 'lodash'
|
||||
|
||||
import Button from './button'
|
||||
import Component from './base-component'
|
||||
@@ -93,9 +92,10 @@ export default class ActionButton extends Component {
|
||||
|
||||
const { redirectOnSuccess } = props
|
||||
if (redirectOnSuccess !== undefined) {
|
||||
const to = isFunction(redirectOnSuccess)
|
||||
? redirectOnSuccess(result, handlerParam)
|
||||
: redirectOnSuccess
|
||||
const to =
|
||||
typeof redirectOnSuccess === 'function'
|
||||
? redirectOnSuccess(result, handlerParam)
|
||||
: redirectOnSuccess
|
||||
if (to !== undefined) {
|
||||
return this.context.router.push(to)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { PureComponent } from 'react'
|
||||
import { cowSet } from 'utils'
|
||||
import { includes, isArray, forEach, map } from 'lodash'
|
||||
import { includes, forEach, map } from 'lodash'
|
||||
|
||||
import getEventValue from './get-event-value'
|
||||
|
||||
@@ -15,7 +15,7 @@ const get = (object, path, depth) => {
|
||||
}
|
||||
|
||||
const prop = path[depth++]
|
||||
return isArray(object) && prop === '*'
|
||||
return Array.isArray(object) && prop === '*'
|
||||
? map(object, value => get(value, path, depth))
|
||||
: get(object[prop], path, depth)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import classNames from 'classnames'
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { isEmpty, isFunction, isString, map, pick } from 'lodash'
|
||||
import { isEmpty, isString, map, pick } from 'lodash'
|
||||
|
||||
import _ from '../intl'
|
||||
import Component from '../base-component'
|
||||
@@ -100,7 +100,7 @@ class Editable extends Component {
|
||||
|
||||
return this.__save(
|
||||
() => this.state.previous,
|
||||
isFunction(onUndo) ? onUndo : props.onChange
|
||||
typeof onUndo === 'function' ? onUndo : props.onChange
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types'
|
||||
import React, { Component } from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import { FormattedMessage, IntlProvider as IntlProvider_ } from 'react-intl'
|
||||
import { every, isFunction, isString } from 'lodash'
|
||||
import { every, isString } from 'lodash'
|
||||
|
||||
import locales from './locales'
|
||||
import messages from './messages'
|
||||
@@ -32,7 +32,7 @@ const getMessage = (props, messageId, values, render) => {
|
||||
throw new Error(`no message defined for ${messageId}`)
|
||||
}
|
||||
|
||||
if (isFunction(values)) {
|
||||
if (typeof values === 'function') {
|
||||
render = values
|
||||
values = undefined
|
||||
}
|
||||
|
||||
@@ -2142,7 +2142,7 @@ export default {
|
||||
vmChooseCoresPerSocket: undefined,
|
||||
|
||||
// Original text: '{nSockets, number} socket{nSockets, plural, one {} other {s}} with {nCores, number} core{nCores, plural, one {} other {s}} per socket'
|
||||
vmCoresPerSocket: undefined,
|
||||
vmSocketsWithCoresPerSocket: undefined,
|
||||
|
||||
// Original text: 'Incorrect cores per socket value'
|
||||
vmCoresPerSocketIncorrectValue: undefined,
|
||||
|
||||
@@ -2185,7 +2185,7 @@ export default {
|
||||
vmChooseCoresPerSocket: 'Comportement par défaut',
|
||||
|
||||
// Original text: "{nSockets, number} socket{nSockets, plural, one {} other {s}} with {nCores, number} core{nCores, plural, one {} other {s}} per socket"
|
||||
vmCoresPerSocket:
|
||||
vmSocketsWithCoresPerSocket:
|
||||
'{nSockets, number} socket{nSockets, plural, one {} other {s}} avec {nCores, number} cœur{nCores, plural, one {} other {s}} par socket',
|
||||
|
||||
// Original text: "Incorrect cores per socket value"
|
||||
|
||||
@@ -2660,7 +2660,7 @@ export default {
|
||||
vmChooseCoresPerSocket: 'Varsayılan davranış',
|
||||
|
||||
// Original text: "{nSockets, number} socket{nSockets, plural, one {} other {s}} with {nCores, number} core{nCores, plural, one {} other {s}} per socket"
|
||||
vmCoresPerSocket:
|
||||
vmSocketsWithCoresPerSocket:
|
||||
'{nSockets, number} soket ve her sokette {nCores, number} çekirdek',
|
||||
|
||||
// Original text: "None"
|
||||
|
||||
@@ -50,6 +50,7 @@ const messages = {
|
||||
backupJobs: 'Backup jobs',
|
||||
iscsiSessions:
|
||||
'({ nSessions, number }) iSCSI session{nSessions, plural, one {} other {s}}',
|
||||
requiresAdminPermissions: 'Requires admin permissions',
|
||||
|
||||
// ----- Modals -----
|
||||
alertOk: 'OK',
|
||||
@@ -99,6 +100,7 @@ const messages = {
|
||||
updatePage: 'Updates',
|
||||
licensesPage: 'Licenses',
|
||||
notificationsPage: 'Notifications',
|
||||
supportPage: 'Support',
|
||||
settingsPage: 'Settings',
|
||||
settingsServersPage: 'Servers',
|
||||
settingsUsersPage: 'Users',
|
||||
@@ -153,6 +155,9 @@ const messages = {
|
||||
// ----- Support -----
|
||||
noSupport: 'No support',
|
||||
freeUpgrade: 'Free upgrade!',
|
||||
checkXoa: 'Check XOA',
|
||||
xoaCheck: 'XOA check',
|
||||
checkXoaCommunity: 'XOA check is available in XOA.',
|
||||
|
||||
// ----- Sign out -----
|
||||
signOut: 'Sign out',
|
||||
@@ -1154,12 +1159,16 @@ const messages = {
|
||||
vmCpuLimitsLabel: 'CPU limits',
|
||||
vmCpuTopology: 'Topology',
|
||||
vmChooseCoresPerSocket: 'Default behavior',
|
||||
vmCoresPerSocket:
|
||||
vmSocketsWithCoresPerSocket:
|
||||
'{nSockets, number} socket{nSockets, plural, one {} other {s}} with {nCores, number} core{nCores, plural, one {} other {s}} per socket',
|
||||
vmCoresPerSocketNone: 'None',
|
||||
vmCoresPerSocketIncorrectValue: 'Incorrect cores per socket value',
|
||||
vmCoresPerSocketIncorrectValueSolution:
|
||||
'Please change the selected value to fix it.',
|
||||
vmCoresPerSocket:
|
||||
'{nCores, number} core{nCores, plural, one {} other {s}} per socket',
|
||||
vmCoresPerSocketNotDivisor: "Not a divisor of the VM's max CPUs",
|
||||
vmCoresPerSocketExceedsCoresLimit:
|
||||
'The selected value exceeds the cores limit ({maxCores, number})',
|
||||
vmCoresPerSocketExceedsSocketsLimit:
|
||||
'The selected value exceeds the sockets limit ({maxSockets, number})',
|
||||
vmHaDisabled: 'Disabled',
|
||||
vmMemoryLimitsLabel: 'Memory limits (min/max)',
|
||||
vmVgpu: 'vGPU',
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import forEachRight from 'lodash/forEachRight'
|
||||
import forEach from 'lodash/forEach'
|
||||
import isArray from 'lodash/isArray'
|
||||
import isIp from 'is-ip'
|
||||
import some from 'lodash/some'
|
||||
|
||||
@@ -76,7 +75,7 @@ export const getNextIpV4 = ip => {
|
||||
}
|
||||
|
||||
export const formatIps = ips => {
|
||||
if (!isArray(ips)) {
|
||||
if (!Array.isArray(ips)) {
|
||||
throw new Error('ips must be an array')
|
||||
}
|
||||
if (ips.length === 0) {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React from 'react'
|
||||
import includes from 'lodash/includes'
|
||||
import isArray from 'lodash/isArray'
|
||||
import marked from 'marked'
|
||||
|
||||
import { Col, Row } from 'grid'
|
||||
@@ -14,7 +13,7 @@ export const getType = schema => {
|
||||
|
||||
const type = schema.type
|
||||
|
||||
if (isArray(type)) {
|
||||
if (Array.isArray(type)) {
|
||||
if (includes(type, 'integer')) {
|
||||
return 'integer'
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import React, { Component, cloneElement } from 'react'
|
||||
import { createSelector } from 'selectors'
|
||||
import { identity, isArray, isString, map } from 'lodash'
|
||||
import { identity, isString, map } from 'lodash'
|
||||
import { injectIntl } from 'react-intl'
|
||||
import { injectState, provideState } from 'reaclette'
|
||||
import { Modal as ReactModal } from 'react-bootstrap-4/lib'
|
||||
@@ -35,7 +35,7 @@ const modal = (content, onClose, props) => {
|
||||
}
|
||||
|
||||
const _addRef = (component, ref) => {
|
||||
if (isString(component) || isArray(component)) {
|
||||
if (isString(component) || Array.isArray(component)) {
|
||||
return component
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import React from 'react'
|
||||
import { createSchedule } from '@xen-orchestra/cron'
|
||||
import { FormattedDate, FormattedTime } from 'react-intl'
|
||||
import { injectState, provideState } from 'reaclette'
|
||||
import { flatten, forEach, identity, isArray, map, sortedIndex } from 'lodash'
|
||||
import { flatten, forEach, identity, map, sortedIndex } from 'lodash'
|
||||
|
||||
import _ from './intl'
|
||||
import Button from './button'
|
||||
@@ -262,7 +262,7 @@ const TimePicker = decorate([
|
||||
provideState({
|
||||
effects: {
|
||||
onChange: (_, value) => ({ optionsValues }, { onChange }) => {
|
||||
if (isArray(value)) {
|
||||
if (Array.isArray(value)) {
|
||||
value = value.length === optionsValues.length ? '*' : value.join(',')
|
||||
} else {
|
||||
value = `*/${value}`
|
||||
|
||||
119
packages/xo-web/src/common/select-cores-per-socket.js
Normal file
119
packages/xo-web/src/common/select-cores-per-socket.js
Normal file
@@ -0,0 +1,119 @@
|
||||
import _ from 'intl'
|
||||
import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import { injectState, provideState } from 'reaclette'
|
||||
import { omit } from 'lodash'
|
||||
|
||||
import decorate from './apply-decorators'
|
||||
import Icon from './icon'
|
||||
import Tooltip from './tooltip'
|
||||
import { Select } from './form'
|
||||
|
||||
const PROP_TYPES = {
|
||||
maxCores: PropTypes.number,
|
||||
maxVcpus: PropTypes.number,
|
||||
value: PropTypes.number,
|
||||
}
|
||||
|
||||
const SELECT_STYLE = {
|
||||
display: 'inline-block',
|
||||
fontSize: '1rem',
|
||||
width: '20em',
|
||||
}
|
||||
|
||||
const LINE_ITEM_STYLE = {
|
||||
alignItems: 'center',
|
||||
display: 'flex',
|
||||
}
|
||||
|
||||
// https://github.com/xcp-ng/xenadmin/blob/0160cd0119fae3b871eef656c23e2b76fcc04cb5/XenModel/XenAPI-Extensions/VM.cs#L62
|
||||
const MAX_VM_SOCKETS = 16
|
||||
|
||||
// This algorithm was inspired from: https://github.com/xcp-ng/xenadmin/blob/master/XenAdmin/Controls/ComboBoxes/CPUTopologyComboBox.cs#L116
|
||||
const SelectCoresPerSocket = decorate([
|
||||
provideState({
|
||||
computed: {
|
||||
isValidValue: (state, { maxVcpus, value }) =>
|
||||
value == null ||
|
||||
(maxVcpus % value === 0 &&
|
||||
!state.valueExceedsCoresLimit &&
|
||||
!state.valueExceedsSocketsLimit),
|
||||
valueExceedsCoresLimit: (state, { maxCores, value }) => value > maxCores,
|
||||
valueExceedsSocketsLimit: (state, { maxCores, maxVcpus, value }) =>
|
||||
maxVcpus / value > MAX_VM_SOCKETS,
|
||||
options: ({ isValidValue }, { maxCores, maxVcpus, value }) => {
|
||||
const options = []
|
||||
|
||||
if (maxCores === undefined || maxVcpus === undefined) {
|
||||
return options
|
||||
}
|
||||
|
||||
const minCores = maxVcpus / MAX_VM_SOCKETS
|
||||
|
||||
// cores per socket must be a divisor of the max vCPUs and must not exceed the cores and sockets limit
|
||||
// e.g: with maxCores = 4, maxSockets = 16 and maxVCPUS = 6
|
||||
// 2 cores per socket is a valid value and 4 cores per socket isn't a valid value
|
||||
for (
|
||||
let coresPerSocket = maxCores;
|
||||
coresPerSocket >= minCores;
|
||||
coresPerSocket--
|
||||
) {
|
||||
if (maxVcpus % coresPerSocket === 0) {
|
||||
options.push({
|
||||
label: _('vmSocketsWithCoresPerSocket', {
|
||||
nSockets: maxVcpus / coresPerSocket,
|
||||
nCores: coresPerSocket,
|
||||
}),
|
||||
value: coresPerSocket,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (!isValidValue) {
|
||||
options.push({
|
||||
label: _('vmCoresPerSocket', {
|
||||
nCores: value,
|
||||
}),
|
||||
value,
|
||||
})
|
||||
}
|
||||
return options
|
||||
},
|
||||
selectProps: (_, props) => omit(props, Object.keys(PROP_TYPES)),
|
||||
},
|
||||
}),
|
||||
injectState,
|
||||
({ maxCores, state, value }) => (
|
||||
<div style={LINE_ITEM_STYLE}>
|
||||
<span style={SELECT_STYLE}>
|
||||
<Select
|
||||
options={state.options}
|
||||
placeholder={_('vmChooseCoresPerSocket')}
|
||||
simpleValue
|
||||
value={value}
|
||||
{...state.selectProps}
|
||||
/>
|
||||
</span>
|
||||
|
||||
{!state.isValidValue && (
|
||||
<Tooltip
|
||||
content={
|
||||
state.valueExceedsCoresLimit
|
||||
? _('vmCoresPerSocketExceedsCoresLimit', { maxCores })
|
||||
: state.valueExceedsSocketsLimit
|
||||
? _('vmCoresPerSocketExceedsSocketsLimit', {
|
||||
maxSockets: MAX_VM_SOCKETS,
|
||||
})
|
||||
: _('vmCoresPerSocketNotDivisor')
|
||||
}
|
||||
>
|
||||
<Icon icon='error' size='lg' />
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
])
|
||||
|
||||
SelectCoresPerSocket.propTypes = PROP_TYPES
|
||||
|
||||
export { SelectCoresPerSocket as default }
|
||||
@@ -2,13 +2,11 @@ import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { parse as parseRemote } from 'xo-remote-parser'
|
||||
import {
|
||||
assign,
|
||||
filter,
|
||||
flatten,
|
||||
forEach,
|
||||
groupBy,
|
||||
includes,
|
||||
isArray,
|
||||
isEmpty,
|
||||
isInteger,
|
||||
isString,
|
||||
@@ -62,7 +60,7 @@ const ADDON_BUTTON_STYLE = { lineHeight: '1.4' }
|
||||
const getIds = value =>
|
||||
value == null || isString(value) || isInteger(value)
|
||||
? value
|
||||
: isArray(value)
|
||||
: Array.isArray(value)
|
||||
? map(value, getIds)
|
||||
: value.id
|
||||
|
||||
@@ -87,7 +85,7 @@ const options = props => ({
|
||||
})
|
||||
|
||||
const getObjectsById = objects =>
|
||||
keyBy(isArray(objects) ? objects : flatten(toArray(objects)), 'id')
|
||||
keyBy(Array.isArray(objects) ? objects : flatten(toArray(objects)), 'id')
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@@ -155,7 +153,7 @@ class GenericSelect extends React.Component {
|
||||
})
|
||||
}
|
||||
}
|
||||
if (isArray(ids)) {
|
||||
if (Array.isArray(ids)) {
|
||||
ids.forEach(addIfMissing)
|
||||
} else {
|
||||
addIfMissing(ids)
|
||||
@@ -188,7 +186,7 @@ class GenericSelect extends React.Component {
|
||||
|
||||
let options
|
||||
if (containers === undefined) {
|
||||
if (__DEV__ && !isArray(objects)) {
|
||||
if (__DEV__ && !Array.isArray(objects)) {
|
||||
throw new Error(
|
||||
`${name}: without xoContainers, xoObjects must be an array`
|
||||
)
|
||||
@@ -199,7 +197,7 @@ class GenericSelect extends React.Component {
|
||||
: objects
|
||||
).map(getOption)
|
||||
} else {
|
||||
if (__DEV__ && isArray(objects)) {
|
||||
if (__DEV__ && Array.isArray(objects)) {
|
||||
throw new Error(
|
||||
`${name}: with xoContainers, xoObjects must be an object`
|
||||
)
|
||||
@@ -249,7 +247,7 @@ class GenericSelect extends React.Component {
|
||||
this._getObjectsById,
|
||||
value => value,
|
||||
(objectsById, value) =>
|
||||
isArray(value)
|
||||
Array.isArray(value)
|
||||
? map(value, value => objectsById[value.value])
|
||||
: objectsById[value.value]
|
||||
)
|
||||
@@ -642,7 +640,7 @@ export const SelectHighLevelObject = makeStoreSelect(
|
||||
getSrs,
|
||||
getVms,
|
||||
(hosts, networks, pools, srs, vms) =>
|
||||
sortBy(assign({}, hosts, networks, pools, srs, vms), [
|
||||
sortBy(Object.assign({}, hosts, networks, pools, srs, vms), [
|
||||
'type',
|
||||
'name_label',
|
||||
])
|
||||
|
||||
@@ -8,10 +8,8 @@ import {
|
||||
forEach,
|
||||
groupBy,
|
||||
identity,
|
||||
isArray,
|
||||
isArrayLike,
|
||||
isEmpty,
|
||||
isFunction,
|
||||
keys,
|
||||
map,
|
||||
orderBy,
|
||||
@@ -71,7 +69,7 @@ const _SELECTOR_PLACEHOLDER = Symbol('selector placeholder')
|
||||
const _create2 = (...inputs) => {
|
||||
const resultFn = inputs.pop()
|
||||
|
||||
if (inputs.length === 1 && isArray(inputs[0])) {
|
||||
if (inputs.length === 1 && Array.isArray(inputs[0])) {
|
||||
inputs = inputs[0]
|
||||
}
|
||||
|
||||
@@ -81,10 +79,10 @@ const _create2 = (...inputs) => {
|
||||
for (let i = 0; i < n; ++i) {
|
||||
const input = inputs[i]
|
||||
|
||||
if (isFunction(input)) {
|
||||
if (typeof input === 'function') {
|
||||
inputSelectors.push(input)
|
||||
inputs[i] = _SELECTOR_PLACEHOLDER
|
||||
} else if (isArray(input) && input.length === 1) {
|
||||
} else if (Array.isArray(input) && input.length === 1) {
|
||||
inputs[i] = input[0]
|
||||
}
|
||||
}
|
||||
@@ -352,7 +350,9 @@ export const createSortForType = invoke(() => {
|
||||
const getOrders = type => ordersByType[type]
|
||||
|
||||
const autoSelector = (type, fn) =>
|
||||
isFunction(type) ? (state, props) => fn(type(state, props)) : [fn(type)]
|
||||
typeof type === 'function'
|
||||
? (state, props) => fn(type(state, props))
|
||||
: [fn(type)]
|
||||
|
||||
return (type, collection) =>
|
||||
createSort(
|
||||
@@ -423,9 +423,11 @@ const _extendCollectionSelector = (selector, objectsType) => {
|
||||
// - sort: returns a selector which returns the objects appropriately
|
||||
// sorted (groupBy can be chained)
|
||||
export const createGetObjectsOfType = type => {
|
||||
const getObjects = isFunction(type)
|
||||
? (state, props) => state.objects.byType[type(state, props)] || EMPTY_OBJECT
|
||||
: state => state.objects.byType[type] || EMPTY_OBJECT
|
||||
const getObjects =
|
||||
typeof type === 'function'
|
||||
? (state, props) =>
|
||||
state.objects.byType[type(state, props)] || EMPTY_OBJECT
|
||||
: state => state.objects.byType[type] || EMPTY_OBJECT
|
||||
|
||||
return _extendCollectionSelector(
|
||||
createFilter(getObjects, _getPermissionsPredicate),
|
||||
|
||||
@@ -19,9 +19,7 @@ import {
|
||||
findIndex,
|
||||
forEach,
|
||||
get as getProperty,
|
||||
isArray,
|
||||
isEmpty,
|
||||
isFunction,
|
||||
map,
|
||||
sortBy,
|
||||
} from 'lodash'
|
||||
@@ -215,17 +213,17 @@ const Action = decorate([
|
||||
provideState({
|
||||
computed: {
|
||||
disabled: ({ items }, { disabled, userData }) =>
|
||||
isFunction(disabled) ? disabled(items, userData) : disabled,
|
||||
typeof disabled === 'function' ? disabled(items, userData) : disabled,
|
||||
handler: ({ items }, { handler, userData }) => () =>
|
||||
handler(items, userData),
|
||||
icon: ({ items }, { icon, userData }) =>
|
||||
isFunction(icon) ? icon(items, userData) : icon,
|
||||
typeof icon === 'function' ? icon(items, userData) : icon,
|
||||
items: (_, { items, grouped }) =>
|
||||
isArray(items) || !grouped ? items : [items],
|
||||
Array.isArray(items) || !grouped ? items : [items],
|
||||
label: ({ items }, { label, userData }) =>
|
||||
isFunction(label) ? label(items, userData) : label,
|
||||
typeof label === 'function' ? label(items, userData) : label,
|
||||
level: ({ items }, { level, userData }) =>
|
||||
isFunction(level) ? level(items, userData) : level,
|
||||
typeof level === 'function' ? level(items, userData) : level,
|
||||
},
|
||||
}),
|
||||
injectState,
|
||||
@@ -493,7 +491,9 @@ export default class SortedTable extends Component {
|
||||
if (item !== undefined) {
|
||||
if (rowLink !== undefined) {
|
||||
this.context.router.push(
|
||||
isFunction(rowLink) ? rowLink(item, userData) : rowLink
|
||||
typeof rowLink === 'function'
|
||||
? rowLink(item, userData)
|
||||
: rowLink
|
||||
)
|
||||
} else if (rowAction !== undefined) {
|
||||
rowAction(item, userData)
|
||||
@@ -785,7 +785,7 @@ export default class SortedTable extends Component {
|
||||
className={state.highlighted === i ? styles.highlight : undefined}
|
||||
key={id}
|
||||
tagName='tr'
|
||||
to={isFunction(rowLink) ? rowLink(item, userData) : rowLink}
|
||||
to={typeof rowLink === 'function' ? rowLink(item, userData) : rowLink}
|
||||
>
|
||||
{selectionColumn}
|
||||
{columns}
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
clone,
|
||||
every,
|
||||
forEach,
|
||||
isArray,
|
||||
isEmpty,
|
||||
isFunction,
|
||||
isPlainObject,
|
||||
@@ -81,12 +80,12 @@ const _normalizeMapStateToProps = mapper => {
|
||||
return state => pick(state, mapper)
|
||||
}
|
||||
|
||||
if (isFunction(mapper)) {
|
||||
if (typeof mapper === 'function') {
|
||||
const factoryOrMapper = (state, props) => {
|
||||
const result = mapper(state, props)
|
||||
|
||||
// Properly handles factory pattern.
|
||||
if (isFunction(result)) {
|
||||
if (typeof result === 'function') {
|
||||
mapper = result
|
||||
return factoryOrMapper
|
||||
}
|
||||
@@ -258,10 +257,10 @@ const NotFound = () => <h1>{_('errorPageNotFound')}</h1>
|
||||
//
|
||||
// TODO: add support for function childRoutes (getChildRoutes).
|
||||
export const routes = (indexRoute, childRoutes) => target => {
|
||||
if (isArray(indexRoute)) {
|
||||
if (Array.isArray(indexRoute)) {
|
||||
childRoutes = indexRoute
|
||||
indexRoute = undefined
|
||||
} else if (isFunction(indexRoute)) {
|
||||
} else if (typeof indexRoute === 'function') {
|
||||
indexRoute = {
|
||||
component: indexRoute,
|
||||
}
|
||||
@@ -449,26 +448,6 @@ export const isXosanPack = ({ name }) => name.startsWith('XOSAN')
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export const getCoresPerSocketPossibilities = (maxCoresPerSocket, vCPUs) => {
|
||||
// According to : https://www.citrix.com/blogs/2014/03/11/citrix-xenserver-setting-more-than-one-vcpu-per-vm-to-improve-application-performance-and-server-consolidation-e-g-for-cad3-d-graphical-applications/
|
||||
const maxVCPUs = 16
|
||||
|
||||
const options = []
|
||||
if (maxCoresPerSocket !== undefined && vCPUs !== '') {
|
||||
const ratio = vCPUs / maxVCPUs
|
||||
|
||||
for (
|
||||
let coresPerSocket = maxCoresPerSocket;
|
||||
coresPerSocket >= ratio;
|
||||
coresPerSocket--
|
||||
) {
|
||||
if (vCPUs % coresPerSocket === 0) options.push(coresPerSocket)
|
||||
}
|
||||
}
|
||||
|
||||
return options
|
||||
}
|
||||
|
||||
// Generates a random human-readable string of length `length`
|
||||
// Useful to generate random default names intended for the UI user
|
||||
export const generateReadableRandomString = (() => {
|
||||
|
||||
@@ -11,7 +11,6 @@ import Xo from 'xo-lib'
|
||||
import { createBackoff } from 'jsonrpc-websocket-client'
|
||||
import { SelectHost } from 'select-objects'
|
||||
import {
|
||||
assign,
|
||||
filter,
|
||||
forEach,
|
||||
get,
|
||||
@@ -159,7 +158,7 @@ export const connectStore = store => {
|
||||
return
|
||||
}
|
||||
|
||||
assign(updates, notification.params.items)
|
||||
Object.assign(updates, notification.params.items)
|
||||
sendUpdates()
|
||||
})
|
||||
subscribePermissions(permissions =>
|
||||
@@ -2918,3 +2917,7 @@ export const getLicense = (productId, boundObjectId) =>
|
||||
|
||||
export const unlockXosan = (licenseId, srId) =>
|
||||
_call('xosan.unlock', { licenseId, sr: srId })
|
||||
|
||||
// Support --------------------------------------------------------------------
|
||||
|
||||
export const checkXoa = () => _call('xoa.check')
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import assign from 'lodash/assign'
|
||||
import Client, {
|
||||
AbortedConnection,
|
||||
ConnectionError,
|
||||
@@ -298,7 +297,7 @@ class XoaUpdater extends EventEmitter {
|
||||
} catch (error) {
|
||||
return this._xoaStateError(error)
|
||||
} finally {
|
||||
this.emit('trialState', assign({}, this._xoaState))
|
||||
this.emit('trialState', Object.assign({}, this._xoaState))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -367,7 +366,7 @@ class XoaUpdater extends EventEmitter {
|
||||
while (this._log.length > 10) {
|
||||
this._log.shift()
|
||||
}
|
||||
this.emit('log', map(this._log, item => assign({}, item)))
|
||||
this.emit('log', map(this._log, item => Object.assign({}, item)))
|
||||
}
|
||||
|
||||
async getConfiguration() {
|
||||
@@ -377,7 +376,7 @@ class XoaUpdater extends EventEmitter {
|
||||
} catch (error) {
|
||||
this._configuration = {}
|
||||
} finally {
|
||||
this.emit('configuration', assign({}, this._configuration))
|
||||
this.emit('configuration', Object.assign({}, this._configuration))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -406,7 +405,7 @@ class XoaUpdater extends EventEmitter {
|
||||
} catch (error) {
|
||||
this._configuration = {}
|
||||
} finally {
|
||||
this.emit('configuration', assign({}, this._configuration))
|
||||
this.emit('configuration', Object.assign({}, this._configuration))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -830,6 +830,10 @@
|
||||
@extend .fa;
|
||||
@extend .fa-bell;
|
||||
}
|
||||
&-menu-support {
|
||||
@extend .fa;
|
||||
@extend .fa-support;
|
||||
}
|
||||
&-menu-settings {
|
||||
@extend .fa;
|
||||
@extend .fa-cog;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import logError from 'log-error'
|
||||
import React from 'react'
|
||||
import { assign, isFunction } from 'lodash'
|
||||
|
||||
// Avoid global breakage if a component fails to render.
|
||||
//
|
||||
@@ -32,18 +31,18 @@ React.createElement = (createElement => {
|
||||
}
|
||||
|
||||
return function(Component) {
|
||||
if (isFunction(Component)) {
|
||||
if (typeof Component === 'function') {
|
||||
const patched = Component._patched
|
||||
if (patched) {
|
||||
arguments[0] = patched
|
||||
} else {
|
||||
const { prototype } = Component
|
||||
let render
|
||||
if (prototype && isFunction((render = prototype.render))) {
|
||||
if (prototype && typeof (render = prototype.render) === 'function') {
|
||||
prototype.render = wrapRender(render)
|
||||
Component._patched = Component // itself
|
||||
} else {
|
||||
arguments[0] = Component._patched = assign(
|
||||
arguments[0] = Component._patched = Object.assign(
|
||||
wrapRender(Component),
|
||||
Component
|
||||
)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user