Compare commits

..

18 Commits

Author SHA1 Message Date
Julien Fontanet
5602477891 fix(server/backup/writeStream): use pipeline instead of .pipe 2019-10-11 17:26:12 +02:00
Julien Fontanet
3483e7d9e0 chore: update dependencies 2019-10-11 17:15:25 +02:00
Julien Fontanet
56cb20a1af chore: use typeof instead of lodash/isFunction (#4587) 2019-10-11 15:16:37 +02:00
Rajaa.BARHTAOUI
64929653dd feat(xo-server): upgrade to execa@2.0.5 (#4584) 2019-10-11 15:01:56 +02:00
Julien Fontanet
c955da9bc6 feat(backups-cli): lowlevel tool to help with backups (#4556) 2019-10-11 14:48:37 +02:00
Julien Fontanet
291354fa8e chore: use Object.assign instead of lodash/assign (#4585)
Support is good enough:
- https://node.green/#ES2015-built-in-extensions-Object-static-methods-Object-assign
- https://kangax.github.io/compat-table/es6/#test-Object_static_methods
2019-10-11 14:43:38 +02:00
Julien Fontanet
905d736512 chore: use Array.isArray instead of lodash/isArray (#4586)
Support is good enough:
- https://node.green/#ES2015-built-ins-Proxy-Array-isArray-support
- https://kangax.github.io/compat-table/es5/#test-Array_methods
2019-10-11 14:29:01 +02:00
Rajaa.BARHTAOUI
3406d6e2a9 chore(CHANGELOG): 5.39.1 (#4597) 2019-10-11 10:33:18 +02:00
BARHTAOUI
fc10b5ffb9 chore(CHANGELOG): update next 2019-10-10 16:03:20 +02:00
BARHTAOUI
f89c313166 feat(xo-web): 5.50.3 2019-10-10 16:03:20 +02:00
Rajaa.BARHTAOUI
7c734168d0 feat(xo-web/xoa): expose 'xoa check' on the UI (#4574)
See #4513
2019-10-10 13:49:17 +02:00
HamadaBrest
1e7bfec2ce feat(xo-web/hub): delete template by namespace instead of ID (#4594) 2019-10-10 10:36:07 +02:00
badrAZ
1eb0603b4e chore(xo-server-test/backup-ng): consolidate default values (#4544)
Required for #4470
2019-10-08 14:34:11 +02:00
badrAZ
4b32730ce8 feat(xo-web/vm): improve invalid cores per socket feedback (#4187)
Fixes #4120
2019-10-08 11:05:11 +02:00
BenjiReis
ad083c1d9b chore(xo-server-sdn-controller): better cert creation code (#4582) 2019-10-07 12:12:01 +02:00
BenjiReis
b4f84c2de2 chore(xo-server-sdn-controller): arrow functions when possible (#4583) 2019-10-07 11:29:30 +02:00
badrAZ
fc17443ce4 fix(xo-web/vm/advanced): error on displaying ACL users (#4578) 2019-10-07 11:08:11 +02:00
Julien Fontanet
342ae06b21 chore(xo-sdn-controller): minor formatting fix 2019-10-07 10:19:34 +02:00
115 changed files with 1986 additions and 1549 deletions

View File

@@ -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',
},

View File

@@ -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": {

View 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))

View 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"
}

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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"

View File

@@ -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
}

View File

@@ -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)
}
)
})

View File

@@ -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,
{

View File

@@ -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"
},

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -8,14 +8,27 @@
### Released packages
- xo-server v5.51.0
- xo-web v5.51.0
## **5.39.1** (2019-10-11)
![Channel: latest](https://badgen.net/badge/channel/latest/yellow)
### 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)
![Channel: latest](https://badgen.net/badge/channel/latest/yellow)
### 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))

View File

@@ -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

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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"
},

View File

@@ -26,7 +26,7 @@
"from2": "^2.3.0",
"fs-extra": "^8.0.1",
"limit-concurrency-decorator": "^0.4.0",
"promise-toolbox": "^0.13.0",
"promise-toolbox": "^0.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",

View File

@@ -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

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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)

View File

@@ -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]

View File

@@ -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": {

View File

@@ -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))
})
}

View File

@@ -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,
'>'
)

View File

@@ -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"
},

View File

@@ -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": {

View File

@@ -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
}

View File

@@ -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": {

View File

@@ -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"
},

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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(),
})

View File

@@ -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

View File

@@ -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)

View File

@@ -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"

View File

@@ -0,0 +1,6 @@
export const getDefaultName = () => `xo-server-test ${new Date().toISOString()}`
export const getDefaultSchedule = () => ({
name: getDefaultName(),
cron: '0 * * * * *',
})

View File

@@ -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,
})

View File

@@ -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>,

View File

@@ -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',
})

View File

@@ -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')

View File

@@ -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,

View File

@@ -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')

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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',

View File

@@ -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"
},

View File

@@ -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'

View File

@@ -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]}`)
}
}

View File

@@ -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]
}

View File

@@ -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]
}

View File

@@ -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')

View File

@@ -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.
//

View File

@@ -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'

View File

@@ -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)
}

View File

@@ -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 => {

View File

@@ -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')
}

View File

@@ -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
}

View File

@@ -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

View File

@@ -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)

View File

@@ -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',

View File

@@ -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)
}

View File

@@ -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,
})

View File

@@ -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)
)

View File

@@ -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
}

View File

@@ -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)

View File

@@ -27,7 +27,7 @@
"child-process-promise": "^2.0.3",
"core-js": "^3.0.0",
"pipette": "^0.9.3",
"promise-toolbox": "^0.13.0",
"promise-toolbox": "^0.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",

View File

@@ -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",

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -1,7 +1,7 @@
import classNames from 'classnames'
import React from 'react'
import PropTypes from 'prop-types'
import { isEmpty, isFunction, isString, map, pick } from 'lodash'
import { isEmpty, 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
)
}

View File

@@ -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
}

View File

@@ -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,

View File

@@ -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"

View File

@@ -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"

View File

@@ -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',

View File

@@ -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) {

View File

@@ -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'
}

View File

@@ -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
}

View File

@@ -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}`

View 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>
&nbsp;
{!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 }

View File

@@ -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',
])

View File

@@ -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),

View File

@@ -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}

View File

@@ -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 = (() => {

View File

@@ -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')

View File

@@ -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))
}
}
}

View File

@@ -830,6 +830,10 @@
@extend .fa;
@extend .fa-bell;
}
&-menu-support {
@extend .fa;
@extend .fa-support;
}
&-menu-settings {
@extend .fa;
@extend .fa-cog;

View File

@@ -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