Compare commits
19 Commits
server-bas
...
ldap-origi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9c78664426 | ||
|
|
5142bf4338 | ||
|
|
e3532612ff | ||
|
|
d25e403233 | ||
|
|
8a5580eae5 | ||
|
|
cf1251ad7b | ||
|
|
4b1d0e8786 | ||
|
|
b6e99ce4a6 | ||
|
|
920def30d7 | ||
|
|
3839aa7419 | ||
|
|
8fde720f02 | ||
|
|
c6dfaa30b5 | ||
|
|
0d4975ba0f | ||
|
|
77325c98a6 | ||
|
|
01dc088a6f | ||
|
|
c20e9820fe | ||
|
|
d255c116dd | ||
|
|
2c1da3458a | ||
|
|
8017e42797 |
7
@xen-orchestra/backups-cli/_asyncMap.js
Normal file
7
@xen-orchestra/backups-cli/_asyncMap.js
Normal file
@@ -0,0 +1,7 @@
|
||||
const curryRight = require('lodash/curryRight')
|
||||
|
||||
module.exports = curryRight((iterable, fn) =>
|
||||
Promise.all(
|
||||
Array.isArray(iterable) ? iterable.map(fn) : Array.from(iterable, fn)
|
||||
)
|
||||
)
|
||||
57
@xen-orchestra/backups-cli/_fs.js
Normal file
57
@xen-orchestra/backups-cli/_fs.js
Normal file
@@ -0,0 +1,57 @@
|
||||
const { dirname } = require('path')
|
||||
|
||||
const fs = require('promise-toolbox/promisifyAll')(require('fs'))
|
||||
module.exports = fs
|
||||
|
||||
fs.mktree = async function mkdirp(path) {
|
||||
try {
|
||||
await fs.mkdir(path)
|
||||
} catch (error) {
|
||||
const { code } = error
|
||||
if (code === 'EEXIST') {
|
||||
await fs.readdir(path)
|
||||
return
|
||||
}
|
||||
if (code === 'ENOENT') {
|
||||
await mkdirp(dirname(path))
|
||||
return mkdirp(path)
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// - easier:
|
||||
// - single param for direct use in `Array#map`
|
||||
// - files are prefixed with directory path
|
||||
// - safer: returns empty array if path is missing or not a directory
|
||||
fs.readdir2 = path =>
|
||||
fs.readdir(path).then(
|
||||
entries => {
|
||||
entries.forEach((entry, i) => {
|
||||
entries[i] = `${path}/${entry}`
|
||||
})
|
||||
|
||||
return entries
|
||||
},
|
||||
error => {
|
||||
if (
|
||||
error != null &&
|
||||
(error.code === 'ENOENT' || error.code === 'ENOTDIR')
|
||||
) {
|
||||
console.warn('WARN: readdir(%s)', path, error)
|
||||
return []
|
||||
}
|
||||
throw error
|
||||
}
|
||||
)
|
||||
|
||||
fs.symlink2 = async (target, path) => {
|
||||
try {
|
||||
await fs.symlink(target, path)
|
||||
} catch (error) {
|
||||
if (error.code === 'EEXIST' && (await fs.readlink(path)) === target) {
|
||||
return
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -6,27 +6,21 @@ let force
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
const assert = require('assert')
|
||||
const flatten = require('lodash/flatten')
|
||||
const getopts = require('getopts')
|
||||
const lockfile = require('proper-lockfile')
|
||||
const pipe = require('promise-toolbox/pipe')
|
||||
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 asyncMap = require('../_asyncMap')
|
||||
const fs = 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)
|
||||
|
||||
const isGzipFile = async fd => {
|
||||
// https://tools.ietf.org/html/rfc1952.html#page-5
|
||||
const magicNumber = Buffer.allocUnsafe(2)
|
||||
@@ -89,24 +83,6 @@ const isValidXva = async path => {
|
||||
|
||||
const noop = Function.prototype
|
||||
|
||||
const readDir = path =>
|
||||
fs.readdir(path).then(
|
||||
entries => {
|
||||
entries.forEach((entry, i) => {
|
||||
entries[i] = `${path}/${entry}`
|
||||
})
|
||||
|
||||
return entries
|
||||
},
|
||||
error => {
|
||||
// a missing dir is by definition empty
|
||||
if (error != null && error.code === 'ENOENT') {
|
||||
return []
|
||||
}
|
||||
throw error
|
||||
}
|
||||
)
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
// chain is an array of VHDs from child to parent
|
||||
@@ -157,12 +133,12 @@ async function mergeVhdChain(chain) {
|
||||
|
||||
const listVhds = pipe([
|
||||
vmDir => vmDir + '/vdis',
|
||||
readDir,
|
||||
asyncMap(readDir),
|
||||
fs.readdir2,
|
||||
asyncMap(fs.readdir2),
|
||||
flatten,
|
||||
asyncMap(readDir),
|
||||
asyncMap(fs.readdir2),
|
||||
flatten,
|
||||
filter(_ => _.endsWith('.vhd')),
|
||||
_ => _.filter(_ => _.endsWith('.vhd')),
|
||||
])
|
||||
|
||||
async function handleVm(vmDir) {
|
||||
@@ -239,10 +215,12 @@ async function handleVm(vmDir) {
|
||||
await Promise.all(deletions)
|
||||
}
|
||||
|
||||
const [jsons, xvas] = await readDir(vmDir).then(entries => [
|
||||
entries.filter(_ => _.endsWith('.json')),
|
||||
new Set(entries.filter(_ => _.endsWith('.xva'))),
|
||||
])
|
||||
const [jsons, xvas] = await fs
|
||||
.readdir2(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
|
||||
|
||||
28
@xen-orchestra/backups-cli/commands/create-symlink-index.js
Normal file
28
@xen-orchestra/backups-cli/commands/create-symlink-index.js
Normal file
@@ -0,0 +1,28 @@
|
||||
const filenamify = require('filenamify')
|
||||
const get = require('lodash/get')
|
||||
const { dirname, join, relative } = require('path')
|
||||
|
||||
const asyncMap = require('../_asyncMap')
|
||||
const { mktree, readdir2, readFile, symlink2 } = require('../_fs')
|
||||
|
||||
module.exports = async function createSymlinkIndex([backupDir, fieldPath]) {
|
||||
const indexDir = join(backupDir, 'indexes', filenamify(fieldPath))
|
||||
await mktree(indexDir)
|
||||
|
||||
await asyncMap(await readdir2(backupDir), async vmDir =>
|
||||
asyncMap(
|
||||
(await readdir2(vmDir)).filter(_ => _.endsWith('.json')),
|
||||
async json => {
|
||||
const metadata = JSON.parse(await readFile(json))
|
||||
const value = get(metadata, fieldPath)
|
||||
if (value !== undefined) {
|
||||
const target = relative(indexDir, dirname(json))
|
||||
const path = join(indexDir, filenamify(String(value)))
|
||||
await symlink2(target, path).catch(error => {
|
||||
console.warn('symlink(%s, %s)', target, path, error)
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -7,6 +7,12 @@ require('./_composeCommands')({
|
||||
},
|
||||
usage: '[--force] xo-vm-backups/*',
|
||||
},
|
||||
'create-symlink-index': {
|
||||
get main() {
|
||||
return require('./commands/create-symlink-index')
|
||||
},
|
||||
usage: 'xo-vm-backups <field path>',
|
||||
},
|
||||
})(process.argv.slice(2), 'xo-backups').catch(error => {
|
||||
console.error('main', error)
|
||||
process.exitCode = 1
|
||||
|
||||
@@ -5,9 +5,10 @@
|
||||
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
|
||||
"dependencies": {
|
||||
"@xen-orchestra/fs": "^0.10.2",
|
||||
"filenamify": "^4.1.0",
|
||||
"getopts": "^2.2.5",
|
||||
"lodash": "^4.17.15",
|
||||
"promise-toolbox": "^0.14.0",
|
||||
"promise-toolbox": "^0.15.0",
|
||||
"proper-lockfile": "^4.1.1",
|
||||
"vhd-lib": "^0.7.2"
|
||||
},
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
"node": ">=8.10"
|
||||
},
|
||||
"dependencies": {
|
||||
"@marsaud/smb2": "^0.14.0",
|
||||
"@marsaud/smb2": "^0.15.0",
|
||||
"@sindresorhus/df": "^3.1.1",
|
||||
"@xen-orchestra/async-map": "^0.0.0",
|
||||
"decorator-synchronized": "^0.5.0",
|
||||
@@ -30,7 +30,7 @@
|
||||
"get-stream": "^5.1.0",
|
||||
"limit-concurrency-decorator": "^0.4.0",
|
||||
"lodash": "^4.17.4",
|
||||
"promise-toolbox": "^0.14.0",
|
||||
"promise-toolbox": "^0.15.0",
|
||||
"readable-stream": "^3.0.6",
|
||||
"through2": "^3.0.0",
|
||||
"tmp": "^0.1.0",
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"lodash": "^4.17.4",
|
||||
"promise-toolbox": "^0.14.0"
|
||||
"promise-toolbox": "^0.15.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.0.0",
|
||||
|
||||
23
CHANGELOG.md
23
CHANGELOG.md
@@ -8,12 +8,31 @@
|
||||
|
||||
### Released packages
|
||||
|
||||
## **5.41.0** (2019-11-29)
|
||||
|
||||
## **5.42.0** (2019-12-20)
|
||||
|
||||

|
||||
|
||||
### Highlights
|
||||
|
||||
- [SDN Controller] Allow private network creation on bond and VLAN (PR [#4682](https://github.com/vatesfr/xen-orchestra/pull/4682))
|
||||
- [Hub/recipes] [Ability to create a kubernetes cluster](https://xen-orchestra.com/blog/devblog-5-kubernetes-clutser-on-xo/) (PR [#4695](https://github.com/vatesfr/xen-orchestra/pull/4695))
|
||||
|
||||
### Enhancements
|
||||
|
||||
- [XOA] Display XOA build number [#4693](https://github.com/vatesfr/xen-orchestra/issues/4693) (PR [#4694](https://github.com/vatesfr/xen-orchestra/pull/4694))
|
||||
|
||||
### Released packages
|
||||
|
||||
- xo-server v5.54.0
|
||||
- xo-web v5.54.0
|
||||
|
||||
## **5.41.0** (2019-11-29)
|
||||
|
||||

|
||||
|
||||
### Highlights
|
||||
|
||||
- [Backup NG] Make report recipients configurable in the backup settings [#4581](https://github.com/vatesfr/xen-orchestra/issues/4581) (PR [#4646](https://github.com/vatesfr/xen-orchestra/pull/4646))
|
||||
- [Host] Advanced Live Telemetry (PR [#4680](https://github.com/vatesfr/xen-orchestra/pull/4680))
|
||||
- [Plugin] [Web hooks](https://xen-orchestra.com/docs/web-hooks.html) [#1946](https://github.com/vatesfr/xen-orchestra/issues/1946) (PR [#3155](https://github.com/vatesfr/xen-orchestra/pull/3155))
|
||||
@@ -44,8 +63,6 @@
|
||||
|
||||
## **5.40.2** (2019-11-22)
|
||||
|
||||

|
||||
|
||||
### Enhancements
|
||||
|
||||
- [Logs] Ability to report a bug with attached log (PR [#4201](https://github.com/vatesfr/xen-orchestra/pull/4201))
|
||||
|
||||
@@ -7,8 +7,6 @@
|
||||
|
||||
> Users must be able to say: “Nice enhancement, I'm eager to test it”
|
||||
|
||||
- [SDN Controller] Allow private network creation on bond and VLAN (PR [#4682](https://github.com/vatesfr/xen-orchestra/pull/4682))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
> Users must be able to say: “I had this issue, happy to know it's fixed”
|
||||
@@ -20,5 +18,5 @@
|
||||
>
|
||||
> Rule of thumb: add packages on top.
|
||||
|
||||
- xo-server v5.54.0
|
||||
- xo-web v5.54.0
|
||||
- xo-server v5.55.0
|
||||
- xo-web v5.55.0
|
||||
|
||||
@@ -17,14 +17,14 @@
|
||||
"eslint-plugin-react": "^7.6.1",
|
||||
"eslint-plugin-standard": "^4.0.0",
|
||||
"exec-promise": "^0.7.0",
|
||||
"flow-bin": "^0.113.0",
|
||||
"flow-bin": "^0.114.0",
|
||||
"globby": "^10.0.0",
|
||||
"husky": "^3.0.0",
|
||||
"jest": "^24.1.0",
|
||||
"lint-staged": "^9.5.0",
|
||||
"lodash": "^4.17.4",
|
||||
"prettier": "^1.10.2",
|
||||
"promise-toolbox": "^0.14.0",
|
||||
"promise-toolbox": "^0.15.0",
|
||||
"sorted-object": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
"cross-env": "^6.0.3",
|
||||
"execa": "^3.2.0",
|
||||
"index-modules": "^0.3.0",
|
||||
"promise-toolbox": "^0.14.0",
|
||||
"promise-toolbox": "^0.15.0",
|
||||
"rimraf": "^3.0.0",
|
||||
"tmp": "^0.1.0"
|
||||
},
|
||||
|
||||
@@ -27,8 +27,8 @@
|
||||
"from2": "^2.3.0",
|
||||
"fs-extra": "^8.0.1",
|
||||
"limit-concurrency-decorator": "^0.4.0",
|
||||
"promise-toolbox": "^0.14.0",
|
||||
"lodash": "^4.17.4",
|
||||
"promise-toolbox": "^0.15.0",
|
||||
"struct-fu": "^1.2.0",
|
||||
"uuid": "^3.0.1"
|
||||
},
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
"make-error": "^1.3.0",
|
||||
"minimist": "^1.2.0",
|
||||
"ms": "^2.1.1",
|
||||
"promise-toolbox": "^0.14.0",
|
||||
"promise-toolbox": "^0.15.0",
|
||||
"pw": "0.0.4",
|
||||
"xmlrpc": "^1.3.2",
|
||||
"xo-collection": "^0.4.1"
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
"nice-pipe": "0.0.0",
|
||||
"pretty-ms": "^5.0.0",
|
||||
"progress-stream": "^2.0.0",
|
||||
"promise-toolbox": "^0.14.0",
|
||||
"promise-toolbox": "^0.15.0",
|
||||
"pump": "^3.0.0",
|
||||
"pw": "^0.0.4",
|
||||
"strip-indent": "^3.0.0",
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
module.exports = require('../../@xen-orchestra/babel-config')(
|
||||
require('./package.json')
|
||||
)
|
||||
@@ -1,24 +0,0 @@
|
||||
/benchmark/
|
||||
/benchmarks/
|
||||
*.bench.js
|
||||
*.bench.js.map
|
||||
|
||||
/examples/
|
||||
example.js
|
||||
example.js.map
|
||||
*.example.js
|
||||
*.example.js.map
|
||||
|
||||
/fixture/
|
||||
/fixtures/
|
||||
*.fixture.js
|
||||
*.fixture.js.map
|
||||
*.fixtures.js
|
||||
*.fixtures.js.map
|
||||
|
||||
/test/
|
||||
/tests/
|
||||
*.spec.js
|
||||
*.spec.js.map
|
||||
|
||||
__snapshots__/
|
||||
@@ -1,48 +0,0 @@
|
||||
{
|
||||
"name": "xo-server-auth-http",
|
||||
"version": "0.0.0",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "Basic HTTP authentication plugin for XO-Server",
|
||||
"keywords": [
|
||||
"authorization",
|
||||
"basic",
|
||||
"http",
|
||||
"orchestra",
|
||||
"plugin",
|
||||
"xen-orchestra",
|
||||
"xen",
|
||||
"xo-server"
|
||||
],
|
||||
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/packages/xo-server-auth-http",
|
||||
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
|
||||
"repository": {
|
||||
"directory": "packages/xo-server-auth-http",
|
||||
"type": "git",
|
||||
"url": "https://github.com/vatesfr/xen-orchestra.git"
|
||||
},
|
||||
"main": "dist/",
|
||||
"files": [
|
||||
"dist/"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=8.10"
|
||||
},
|
||||
"dependencies": {
|
||||
"passport-http": "^0.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.7.4",
|
||||
"@babel/core": "^7.7.4",
|
||||
"@babel/preset-env": "^7.7.4",
|
||||
"cross-env": "^6.0.3",
|
||||
"rimraf": "^3.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
|
||||
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
|
||||
"prebuild": "rimraf dist/",
|
||||
"predev": "yarn run prebuild",
|
||||
"prepublishOnly": "yarn run build"
|
||||
},
|
||||
"private": true
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
import { BasicStrategy } from 'passport-http'
|
||||
|
||||
export const configurationSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
realm: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
required: ['realm'],
|
||||
}
|
||||
|
||||
class Plugin {
|
||||
constructor({ xo }) {
|
||||
this._configuration = undefined
|
||||
this._unregisterPassportStrategy = undefined
|
||||
this._xo = xo
|
||||
}
|
||||
|
||||
configure(configuration) {
|
||||
this._configuration = configuration
|
||||
}
|
||||
|
||||
load() {
|
||||
const xo = this._xo
|
||||
this._unregisterPassportStrategy = xo.registerPassportStrategy(
|
||||
new BasicStrategy(
|
||||
this._configuration,
|
||||
async (username, password, done) => {
|
||||
try {
|
||||
const { user } = await xo.authenticateUser({ username, password })
|
||||
done(null, user)
|
||||
} catch (error) {
|
||||
done(null, false, { message: error.message })
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
unload() {
|
||||
this._unregisterPassportStrategy()
|
||||
}
|
||||
}
|
||||
|
||||
export default opts => new Plugin(opts)
|
||||
@@ -39,7 +39,7 @@
|
||||
"inquirer": "^7.0.0",
|
||||
"ldapjs": "^1.0.1",
|
||||
"lodash": "^4.17.4",
|
||||
"promise-toolbox": "^0.14.0"
|
||||
"promise-toolbox": "^0.15.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.0.0",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* eslint no-throw-literal: 0 */
|
||||
|
||||
import eventToPromise from 'event-to-promise'
|
||||
import noop from 'lodash/noop'
|
||||
import { find, identity, noop } from 'lodash'
|
||||
import { createClient } from 'ldapjs'
|
||||
import { escape } from 'ldapjs/lib/filters/escape'
|
||||
import { promisify } from 'promise-toolbox'
|
||||
@@ -25,6 +25,22 @@ const evalFilter = (filter, vars) =>
|
||||
|
||||
return escape(value)
|
||||
})
|
||||
const makeEvalFormat = format =>
|
||||
format === undefined
|
||||
? identity
|
||||
: (input, record) =>
|
||||
format.replace(VAR_RE, (_, name) => {
|
||||
if (name === 'input') {
|
||||
return input
|
||||
}
|
||||
|
||||
let tmp = find(record.attributes, _ => _.type === name)
|
||||
if (tmp !== undefined && (tmp = tmp.vals).length !== 0) {
|
||||
return tmp[0]
|
||||
}
|
||||
|
||||
throw new Error(`invalid entry ${name}`)
|
||||
})
|
||||
|
||||
export const configurationSchema = {
|
||||
type: 'object',
|
||||
@@ -100,6 +116,12 @@ Or something like this if you also want to filter by group:
|
||||
type: 'string',
|
||||
default: DEFAULTS.filter,
|
||||
},
|
||||
usernameFormat: {
|
||||
description: `
|
||||
|
||||
`.trim(),
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
required: ['uri', 'base'],
|
||||
}
|
||||
@@ -157,15 +179,10 @@ class AuthLdap {
|
||||
}
|
||||
}
|
||||
|
||||
const {
|
||||
bind: credentials,
|
||||
base: searchBase,
|
||||
filter: searchFilter = DEFAULTS.filter,
|
||||
} = conf
|
||||
|
||||
this._credentials = credentials
|
||||
this._searchBase = searchBase
|
||||
this._searchFilter = searchFilter
|
||||
this._credentials = conf.bind
|
||||
this._formatUsername = makeEvalFormat(conf.usernameFormat)
|
||||
this._searchBase = conf.base
|
||||
;({ filter: this._searchFilter = DEFAULTS.filter } = conf)
|
||||
}
|
||||
|
||||
load() {
|
||||
@@ -242,6 +259,9 @@ class AuthLdap {
|
||||
try {
|
||||
logger(`attempting to bind as ${entry.objectName}`)
|
||||
await bind(entry.objectName, password)
|
||||
|
||||
username = this._formatUsername(username, entry)
|
||||
|
||||
logger(
|
||||
`successfully bound as ${entry.objectName} => ${username} authenticated`
|
||||
)
|
||||
|
||||
@@ -157,6 +157,8 @@ const promptByType = {
|
||||
defaultValue && defaultValue[name],
|
||||
subpath
|
||||
)
|
||||
} else {
|
||||
value[name] = schema.default
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -30,8 +30,8 @@
|
||||
"dependencies": {
|
||||
"@xen-orchestra/log": "^0.2.0",
|
||||
"lodash": "^4.17.11",
|
||||
"node-openssl-cert": "^0.0.103",
|
||||
"promise-toolbox": "^0.14.0",
|
||||
"node-openssl-cert": "^0.0.116",
|
||||
"promise-toolbox": "^0.15.0",
|
||||
"uuid": "^3.3.2"
|
||||
},
|
||||
"private": true
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
"golike-defer": "^0.4.1",
|
||||
"jest": "^24.8.0",
|
||||
"lodash": "^4.17.11",
|
||||
"promise-toolbox": "^0.14.0",
|
||||
"promise-toolbox": "^0.15.0",
|
||||
"xo-collection": "^0.4.1",
|
||||
"xo-common": "^0.2.0",
|
||||
"xo-lib": "^0.9.0"
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
"dependencies": {
|
||||
"nodemailer": "^6.1.0",
|
||||
"nodemailer-markdown": "^1.0.1",
|
||||
"promise-toolbox": "^0.14.0"
|
||||
"promise-toolbox": "^0.15.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.0.0",
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
"node": ">=6"
|
||||
},
|
||||
"dependencies": {
|
||||
"promise-toolbox": "^0.14.0",
|
||||
"promise-toolbox": "^0.15.0",
|
||||
"slack-node": "^0.1.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
"html-minifier": "^4.0.0",
|
||||
"human-format": "^0.10.0",
|
||||
"lodash": "^4.17.4",
|
||||
"promise-toolbox": "^0.14.0"
|
||||
"promise-toolbox": "^0.15.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.0.0",
|
||||
|
||||
@@ -72,6 +72,10 @@ vmBackupSizeTimeout = '2 seconds'
|
||||
|
||||
poolMetadataTimeout = '10 minutes'
|
||||
|
||||
# https://github.com/naugtur/blocked-at#params-and-return-value
|
||||
[blockedAtOptions]
|
||||
threshold = 1000
|
||||
|
||||
# Helmet handles HTTP security via headers
|
||||
#
|
||||
# https://helmetjs.github.io/docs/
|
||||
@@ -108,5 +112,11 @@ timeout = 600e3
|
||||
# see https:#github.com/vatesfr/xen-orchestra/issues/3419
|
||||
# useSudo = false
|
||||
|
||||
|
||||
# https://github.com/facebook/jest/blob/master/packages/jest-worker/README.md#options-object-optional
|
||||
#
|
||||
#[workerOptions]
|
||||
#numWorkers = 2
|
||||
|
||||
[xapiOptions]
|
||||
maxUncoalescedVdis = 1
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "xo-server",
|
||||
"version": "5.53.0",
|
||||
"version": "5.54.0",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "Server part of Xen-Orchestra",
|
||||
"keywords": [
|
||||
@@ -47,7 +47,7 @@
|
||||
"async-iterator-to-stream": "^1.0.1",
|
||||
"base64url": "^3.0.0",
|
||||
"bind-property-descriptor": "^1.0.0",
|
||||
"blocked": "^1.2.1",
|
||||
"blocked-at": "^1.2.0",
|
||||
"bluebird": "^3.5.1",
|
||||
"body-parser": "^1.18.2",
|
||||
"compression": "^1.7.3",
|
||||
@@ -84,8 +84,8 @@
|
||||
"julien-f-source-map-support": "0.1.0",
|
||||
"julien-f-unzip": "^0.2.1",
|
||||
"kindof": "^2.0.0",
|
||||
"level": "^4.0.0",
|
||||
"level-party": "^3.0.4",
|
||||
"level": "^6.0.0",
|
||||
"level-party": "^4.0.0",
|
||||
"level-sublevel": "^6.6.1",
|
||||
"limit-concurrency-decorator": "^0.4.0",
|
||||
"lodash": "^4.17.4",
|
||||
@@ -102,7 +102,7 @@
|
||||
"passport": "^0.4.0",
|
||||
"passport-local": "^1.0.0",
|
||||
"pretty-format": "^24.0.0",
|
||||
"promise-toolbox": "^0.14.0",
|
||||
"promise-toolbox": "^0.15.0",
|
||||
"proxy-agent": "^3.0.0",
|
||||
"pug": "^2.0.0-rc.4",
|
||||
"pump": "^3.0.0",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import appConf from 'app-conf'
|
||||
import assert from 'assert'
|
||||
import authenticator from 'otplib/authenticator'
|
||||
import blocked from 'blocked'
|
||||
import blocked from 'blocked-at'
|
||||
import compression from 'compression'
|
||||
import createExpress from 'express'
|
||||
import createLogger from '@xen-orchestra/log'
|
||||
@@ -9,6 +9,7 @@ import crypto from 'crypto'
|
||||
import has from 'lodash/has'
|
||||
import helmet from 'helmet'
|
||||
import includes from 'lodash/includes'
|
||||
import ms from 'ms'
|
||||
import proxyConsole from './proxy-console'
|
||||
import pw from 'pw'
|
||||
import serveStatic from 'serve-static'
|
||||
@@ -124,10 +125,6 @@ async function setUpPassport(express, xo, { authentication: authCfg }) {
|
||||
strategy,
|
||||
{ label = strategy.label, name = strategy.name } = {}
|
||||
) => {
|
||||
if (name in strategies) {
|
||||
throw new TypeError('duplicate passport strategy ' + name)
|
||||
}
|
||||
|
||||
passport.use(name, strategy)
|
||||
if (name !== 'local') {
|
||||
strategies[name] = label ?? name
|
||||
@@ -179,9 +176,7 @@ async function setUpPassport(express, xo, { authentication: authCfg }) {
|
||||
}
|
||||
|
||||
if (authenticator.check(req.body.otp, user.preferences.otp)) {
|
||||
setToken(req, res, next).then(() =>
|
||||
res.redirect(303, req.flash('return-url')[0] || '/')
|
||||
)
|
||||
setToken(req, res, next)
|
||||
} else {
|
||||
req.flash('error', 'Invalid code')
|
||||
res.redirect(303, '/signin-otp')
|
||||
@@ -193,7 +188,7 @@ async function setUpPassport(express, xo, { authentication: authCfg }) {
|
||||
parseDuration
|
||||
)
|
||||
const SESSION_VALIDITY = ifDef(authCfg.sessionCookieValidity, parseDuration)
|
||||
const setToken = async (req, res) => {
|
||||
const setToken = async (req, res, next) => {
|
||||
const { user, isPersistent } = req.session
|
||||
const token = await xo.createAuthenticationToken({
|
||||
expiresIn: isPersistent ? PERMANENT_VALIDITY : SESSION_VALIDITY,
|
||||
@@ -210,6 +205,7 @@ async function setUpPassport(express, xo, { authentication: authCfg }) {
|
||||
|
||||
delete req.session.isPersistent
|
||||
delete req.session.user
|
||||
res.redirect(303, req.flash('return-url')[0] || '/')
|
||||
}
|
||||
|
||||
const SIGNIN_STRATEGY_RE = /^\/signin\/([^/]+)(\/callback)?(:?\?.*)?$/
|
||||
@@ -229,12 +225,6 @@ async function setUpPassport(express, xo, { authentication: authCfg }) {
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
if (typeof info === 'string') {
|
||||
res.statusCode = 401
|
||||
res.setHeader('WWW-Authenticate', info)
|
||||
return res.end('unauthorized')
|
||||
}
|
||||
|
||||
req.flash('error', info ? info.message : 'Invalid credentials')
|
||||
return res.redirect(303, '/signin')
|
||||
}
|
||||
@@ -247,17 +237,16 @@ async function setUpPassport(express, xo, { authentication: authCfg }) {
|
||||
return res.redirect(303, '/signin-otp')
|
||||
}
|
||||
|
||||
await setToken(req, res)
|
||||
res.redirect(303, req.flash('return-url')[0] || '/')
|
||||
setToken(req, res, next)
|
||||
})(req, res, next)
|
||||
}
|
||||
|
||||
if (req.cookies.token) {
|
||||
return next()
|
||||
next()
|
||||
} else {
|
||||
req.flash('return-url', url)
|
||||
res.redirect(authCfg.defaultSignInPage)
|
||||
}
|
||||
|
||||
req.flash('return-url', url)
|
||||
return res.redirect(authCfg.defaultSignInPage)
|
||||
})
|
||||
|
||||
// Install the local strategy.
|
||||
@@ -654,20 +643,18 @@ export default async function main(args) {
|
||||
return USAGE
|
||||
}
|
||||
|
||||
const config = await loadConfiguration()
|
||||
|
||||
{
|
||||
const logPerf = createLogger('xo:perf')
|
||||
blocked(
|
||||
ms => {
|
||||
logPerf.info(`blocked for ${ms | 0}ms`)
|
||||
},
|
||||
{
|
||||
threshold: 500,
|
||||
}
|
||||
)
|
||||
blocked((time, stack) => {
|
||||
logPerf.info(`blocked for ${ms(time)}`, {
|
||||
time,
|
||||
stack,
|
||||
})
|
||||
}, config.blockedAtOptions)
|
||||
}
|
||||
|
||||
const config = await loadConfiguration()
|
||||
|
||||
const webServer = await createWebServer(config.http)
|
||||
|
||||
// Now the web server is listening, drop privileges.
|
||||
|
||||
@@ -9,7 +9,10 @@ export default class Workers {
|
||||
app.on('start', () => {
|
||||
process.env.XO_CONFIG = JSON.stringify(config)
|
||||
|
||||
this._worker = new Worker(require.resolve('./worker'))
|
||||
this._worker = new Worker(
|
||||
require.resolve('./worker'),
|
||||
config.workerOptions
|
||||
)
|
||||
})
|
||||
app.on('stop', () => this._worker.end())
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
"child-process-promise": "^2.0.3",
|
||||
"core-js": "^3.0.0",
|
||||
"pipette": "^0.9.3",
|
||||
"promise-toolbox": "^0.14.0",
|
||||
"promise-toolbox": "^0.15.0",
|
||||
"tmp": "^0.1.0",
|
||||
"vhd-lib": "^0.7.2"
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "xo-web",
|
||||
"version": "5.53.3",
|
||||
"version": "5.54.0",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "Web interface client for Xen-Orchestra",
|
||||
"keywords": [
|
||||
@@ -98,7 +98,7 @@
|
||||
"moment-timezone": "^0.5.14",
|
||||
"notifyjs": "^3.0.0",
|
||||
"otplib": "^11.0.0",
|
||||
"promise-toolbox": "^0.14.0",
|
||||
"promise-toolbox": "^0.15.0",
|
||||
"prop-types": "^15.6.0",
|
||||
"qrcode": "^1.3.2",
|
||||
"random-password": "^0.1.2",
|
||||
|
||||
@@ -897,13 +897,13 @@ export default {
|
||||
remoteNamePlaceHolder: 'nom distant*',
|
||||
|
||||
// Original text: "Name *"
|
||||
remoteMyNamePlaceHolder: 'Nom *',
|
||||
remoteMyNamePlaceHolder: 'Nom',
|
||||
|
||||
// Original text: "/path/to/backup"
|
||||
remoteLocalPlaceHolderPath: '/chemin/de/la/sauvegarde',
|
||||
|
||||
// Original text: "host *"
|
||||
remoteNfsPlaceHolderHost: 'hôte *',
|
||||
remoteNfsPlaceHolderHost: 'hôte',
|
||||
|
||||
// Original text: "path/to/backup"
|
||||
remoteNfsPlaceHolderPath: 'chemin/de/la/sauvegarde',
|
||||
@@ -922,7 +922,7 @@ export default {
|
||||
remoteSmbPlaceHolderDomain: 'Domaine',
|
||||
|
||||
// Original text: "<address>\\<share> *"
|
||||
remoteSmbPlaceHolderAddressShare: '<adresse>\\<partage> *',
|
||||
remoteSmbPlaceHolderAddressShare: '<adresse>\\<partage>',
|
||||
|
||||
// Original text: "password(fill to edit)"
|
||||
remotePlaceHolderPassword: 'mot de passe (saisir ici pour éditer)',
|
||||
|
||||
@@ -840,13 +840,13 @@ export default {
|
||||
remoteNamePlaceHolder: 'távoli név *',
|
||||
|
||||
// Original text: "Name *"
|
||||
remoteMyNamePlaceHolder: 'Név *',
|
||||
remoteMyNamePlaceHolder: 'Név',
|
||||
|
||||
// Original text: "/path/to/backup"
|
||||
remoteLocalPlaceHolderPath: '/path/to/backup',
|
||||
|
||||
// Original text: "host *"
|
||||
remoteNfsPlaceHolderHost: 'kiszolgáló *',
|
||||
remoteNfsPlaceHolderHost: 'kiszolgáló',
|
||||
|
||||
// Original text: "path/to/backup"
|
||||
remoteNfsPlaceHolderPath: 'path/to/backup',
|
||||
@@ -864,7 +864,7 @@ export default {
|
||||
remoteSmbPlaceHolderDomain: 'Domain',
|
||||
|
||||
// Original text: "<address>\\<share> *"
|
||||
remoteSmbPlaceHolderAddressShare: '<address>\\<share> *',
|
||||
remoteSmbPlaceHolderAddressShare: '<address>\\<share>',
|
||||
|
||||
// Original text: "password(fill to edit)"
|
||||
remotePlaceHolderPassword: 'jelszó(kattintson a módosításhoz)',
|
||||
|
||||
@@ -722,13 +722,13 @@ export default {
|
||||
remoteNamePlaceHolder: 'Nazwa zdalna*',
|
||||
|
||||
// Original text: "Name *"
|
||||
remoteMyNamePlaceHolder: 'Nazwa *',
|
||||
remoteMyNamePlaceHolder: 'Nazwa',
|
||||
|
||||
// Original text: "/path/to/backup"
|
||||
remoteLocalPlaceHolderPath: '/ścieżka/do/kopii/zapasowej',
|
||||
|
||||
// Original text: "host *"
|
||||
remoteNfsPlaceHolderHost: 'Host *',
|
||||
remoteNfsPlaceHolderHost: 'Host',
|
||||
|
||||
// Original text: "/path/to/backup"
|
||||
remoteNfsPlaceHolderPath: '/ścieżka/do/kopii/zapasowej',
|
||||
@@ -746,7 +746,7 @@ export default {
|
||||
remoteSmbPlaceHolderDomain: 'Domena',
|
||||
|
||||
// Original text: "<address>\\<share> *"
|
||||
remoteSmbPlaceHolderAddressShare: '<adres>\\<udział> *',
|
||||
remoteSmbPlaceHolderAddressShare: '<adres>\\<udział>',
|
||||
|
||||
// Original text: "password(fill to edit)"
|
||||
remotePlaceHolderPassword: 'Hasło (wypełnij)',
|
||||
|
||||
@@ -1255,13 +1255,13 @@ export default {
|
||||
remoteNamePlaceHolder: 'hedef adı *',
|
||||
|
||||
// Original text: "Name *"
|
||||
remoteMyNamePlaceHolder: 'Ad *',
|
||||
remoteMyNamePlaceHolder: 'Ad',
|
||||
|
||||
// Original text: "/path/to/backup"
|
||||
remoteLocalPlaceHolderPath: '/yedek/için/yol',
|
||||
|
||||
// Original text: "host *"
|
||||
remoteNfsPlaceHolderHost: 'sunucu *',
|
||||
remoteNfsPlaceHolderHost: 'sunucu',
|
||||
|
||||
// Original text: 'Port'
|
||||
remoteNfsPlaceHolderPort: undefined,
|
||||
@@ -1282,7 +1282,7 @@ export default {
|
||||
remoteSmbPlaceHolderDomain: 'Domain',
|
||||
|
||||
// Original text: "<address>\\\\<share> *"
|
||||
remoteSmbPlaceHolderAddressShare: '<paylaşım>\\\\<adresi> *',
|
||||
remoteSmbPlaceHolderAddressShare: '<paylaşım>\\\\<adresi>',
|
||||
|
||||
// Original text: "password(fill to edit)"
|
||||
remotePlaceHolderPassword: 'parola(düzenlemek için doldurun)',
|
||||
|
||||
@@ -542,9 +542,9 @@ const messages = {
|
||||
remoteAuth: 'Auth',
|
||||
remoteDeleteTip: 'Delete',
|
||||
remoteDeleteSelected: 'Delete selected remotes',
|
||||
remoteMyNamePlaceHolder: 'Name *',
|
||||
remoteMyNamePlaceHolder: 'Name',
|
||||
remoteLocalPlaceHolderPath: '/path/to/backup',
|
||||
remoteNfsPlaceHolderHost: 'Host *',
|
||||
remoteNfsPlaceHolderHost: 'Host',
|
||||
remoteNfsPlaceHolderPort: 'Port',
|
||||
remoteNfsPlaceHolderPath: 'path/to/backup',
|
||||
remoteNfsPlaceHolderOptions: 'Custom mount options. Default: vers=3',
|
||||
@@ -552,7 +552,7 @@ const messages = {
|
||||
remoteSmbPlaceHolderUsername: 'Username',
|
||||
remoteSmbPlaceHolderPassword: 'Password',
|
||||
remoteSmbPlaceHolderDomain: 'Domain',
|
||||
remoteSmbPlaceHolderAddressShare: '<address>\\\\<share> *',
|
||||
remoteSmbPlaceHolderAddressShare: '<address>\\\\<share>',
|
||||
remoteSmbPlaceHolderOptions: 'Custom mount options',
|
||||
remotePlaceHolderPassword: 'Password(fill to edit)',
|
||||
|
||||
@@ -1880,6 +1880,7 @@ const messages = {
|
||||
changeChannel: 'Change channel',
|
||||
updaterCommunity:
|
||||
'The Web updater, the release channels and the proxy settings are available in XOA.',
|
||||
xoaBuild: 'XOA build:',
|
||||
|
||||
// ----- OS Disclaimer -----
|
||||
disclaimerTitle: 'Xen Orchestra from the sources',
|
||||
@@ -2174,6 +2175,7 @@ const messages = {
|
||||
|
||||
// Hub
|
||||
hubPage: 'Hub',
|
||||
hubCommunity: 'Hub is available in XOA',
|
||||
noDefaultSr: 'The selected pool has no default SR',
|
||||
successfulInstall: 'VM installed successfully',
|
||||
vmNoAvailable: 'No VMs available ',
|
||||
@@ -2187,6 +2189,15 @@ const messages = {
|
||||
hubImportNotificationTitle: 'XVA import',
|
||||
hubTemplateDescriptionNotAvailable:
|
||||
'No description available for this template',
|
||||
recipeCreatedSuccessfully: 'Recipe created successfully',
|
||||
recipeViewCreatedVms: 'View created VMs',
|
||||
templatesLabel: 'Templates',
|
||||
recipesLabel: 'Recipes',
|
||||
network: 'Network',
|
||||
recipeMasterNameLabel: 'Master name',
|
||||
recipeNumberOfNodesLabel: 'Number of nodes',
|
||||
recipeSshKeyLabel: 'SSH key',
|
||||
recipeNetworkCidr: 'Network CIDR',
|
||||
|
||||
// Licenses
|
||||
xosanUnregisteredDisclaimer:
|
||||
|
||||
@@ -40,7 +40,7 @@ export class Notification extends Component {
|
||||
return
|
||||
}
|
||||
|
||||
error = (title, body) =>
|
||||
error = (title, body, autoCloseTimeout = 6e3) =>
|
||||
notification.error(
|
||||
title,
|
||||
this.props.isAdmin ? (
|
||||
@@ -58,10 +58,12 @@ export class Notification extends Component {
|
||||
) : (
|
||||
body
|
||||
),
|
||||
6e3
|
||||
autoCloseTimeout
|
||||
)
|
||||
info = (title, body) => notification.info(title, body, 3e3)
|
||||
success = (title, body) => notification.success(title, body, 3e3)
|
||||
info = (title, body, autoCloseTimeout = 3e3) =>
|
||||
notification.info(title, body, autoCloseTimeout)
|
||||
success = (title, body, autoCloseTimeout = 3e3) =>
|
||||
notification.success(title, body, autoCloseTimeout)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -66,3 +66,8 @@ export const markHubResourceAsInstalled = createAction(
|
||||
'MARK_HUB_RESOURCE_AS_INSTALLED',
|
||||
id => id
|
||||
)
|
||||
export const markRecipeAsCreating = createAction(
|
||||
'MARK_RECIPE_AS_CREATING',
|
||||
id => id
|
||||
)
|
||||
export const markRecipeAsDone = createAction('MARK_RECIPE_AS_DONE', id => id)
|
||||
|
||||
@@ -106,6 +106,19 @@ export default {
|
||||
}
|
||||
),
|
||||
|
||||
// whether a resource is currently being created: `recipeCreatingResources[<recipe id>]`
|
||||
recipeCreatingResources: combineActionHandlers(
|
||||
{},
|
||||
{
|
||||
[actions.markRecipeAsCreating]: (prevRecipeCreatingResources, id) => ({
|
||||
...prevRecipeCreatingResources,
|
||||
[id]: true,
|
||||
}),
|
||||
[actions.markRecipeAsDone]: (prevRecipeCreatedResources, id) =>
|
||||
omit(prevRecipeCreatedResources, id),
|
||||
}
|
||||
),
|
||||
|
||||
objects: combineActionHandlers(
|
||||
{
|
||||
all: {}, // Mutable for performance!
|
||||
|
||||
@@ -1188,6 +1188,9 @@ export const changeVirtualizationMode = vm =>
|
||||
})
|
||||
)
|
||||
|
||||
export const createKubernetesCluster = params =>
|
||||
_call('xoa.recipe.createKubernetesCluster', params)
|
||||
|
||||
export const deleteTemplates = templates =>
|
||||
confirm({
|
||||
title: _('templateDeleteModalTitle', { templates: templates.length }),
|
||||
@@ -2284,6 +2287,7 @@ export const disableRemote = remote =>
|
||||
|
||||
export const editRemote = (remote, { name, url, options }) =>
|
||||
_call('remote.set', resolveIds({ remote, name, url, options }))::tap(() => {
|
||||
subscribeRemotes.forceRefresh()
|
||||
testRemote(remote).catch(noop)
|
||||
})
|
||||
|
||||
@@ -3013,3 +3017,5 @@ export const openTunnel = () =>
|
||||
export const subscribeTunnelState = createSubscription(() =>
|
||||
_call('xoa.supportTunnel.getState')
|
||||
)
|
||||
|
||||
export const getApplianceInfo = () => _call('xoa.getApplianceInfo')
|
||||
|
||||
@@ -91,6 +91,14 @@
|
||||
@extend .fa;
|
||||
@extend .fa-chevron-down;
|
||||
}
|
||||
&-hub-recipe {
|
||||
@extend .fa;
|
||||
@extend .fa-wpforms;
|
||||
}
|
||||
&-hub-template {
|
||||
@extend .fa;
|
||||
@extend .fa-cube;
|
||||
}
|
||||
|
||||
&-grab {
|
||||
@extend .fa;
|
||||
|
||||
@@ -1,63 +1,55 @@
|
||||
import _ from 'intl'
|
||||
import decorate from 'apply-decorators'
|
||||
import Icon from 'icon'
|
||||
import React from 'react'
|
||||
import { addSubscriptions, adminOnly } from 'utils'
|
||||
|
||||
import { Container, Col, Row } from 'grid'
|
||||
import { injectState, provideState } from 'reaclette'
|
||||
import { isEmpty, map, omit, orderBy } from 'lodash'
|
||||
import { subscribeHubResourceCatalog } from 'xo'
|
||||
import { getXoaPlan, routes, TryXoa } from 'utils'
|
||||
import { NavLink, NavTabs } from 'nav'
|
||||
|
||||
import Page from '../page'
|
||||
import Resource from './resource'
|
||||
import Recipes from './recipes'
|
||||
import Templates from './templates'
|
||||
|
||||
// ==================================================================
|
||||
|
||||
const HEADER = (
|
||||
<h2>
|
||||
<Icon icon='menu-hub' /> {_('hubPage')}
|
||||
</h2>
|
||||
const Header = (
|
||||
<Container>
|
||||
<Row>
|
||||
<Col mediumSize={3}>
|
||||
<h2>
|
||||
<Icon icon='menu-hub' /> {_('hubPage')}
|
||||
</h2>
|
||||
</Col>
|
||||
<Col mediumSize={9}>
|
||||
<NavTabs className='pull-right'>
|
||||
<NavLink to='/hub/templates'>
|
||||
<Icon icon='hub-template' /> {_('templatesLabel')}
|
||||
</NavLink>
|
||||
<NavLink to='/hub/recipes'>
|
||||
<Icon icon='hub-recipe' /> {_('recipesLabel')}
|
||||
</NavLink>
|
||||
</NavTabs>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
)
|
||||
|
||||
export default decorate([
|
||||
adminOnly,
|
||||
addSubscriptions({
|
||||
catalog: subscribeHubResourceCatalog,
|
||||
}),
|
||||
provideState({
|
||||
computed: {
|
||||
resources: (_, { catalog }) =>
|
||||
orderBy(
|
||||
map(omit(catalog, '_namespaces'), (entry, namespace) => ({
|
||||
namespace,
|
||||
...entry.xva,
|
||||
})),
|
||||
'name',
|
||||
'asc'
|
||||
),
|
||||
},
|
||||
}),
|
||||
injectState,
|
||||
({ state: { resources } }) => (
|
||||
<Page header={HEADER} title='hubPage' formatTitle>
|
||||
const Hub = routes('hub', {
|
||||
templates: Templates,
|
||||
recipes: Recipes,
|
||||
})(({ children }) => (
|
||||
<Page header={Header} title='hubPage' formatTitle>
|
||||
{getXoaPlan() === 'Community' ? (
|
||||
<Container>
|
||||
<Row>
|
||||
{isEmpty(resources) ? (
|
||||
<Col>
|
||||
<h2 className='text-muted'>
|
||||
{_('vmNoAvailable')}
|
||||
<Icon icon='alarm' color='yellow' />
|
||||
</h2>
|
||||
</Col>
|
||||
) : (
|
||||
resources.map(data => (
|
||||
<Col key={data.namespace} mediumSize={6} largeSize={4}>
|
||||
<Resource {...data} />
|
||||
</Col>
|
||||
))
|
||||
)}
|
||||
</Row>
|
||||
<h2 className='text-info'>{_('hubCommunity')}</h2>
|
||||
<p>
|
||||
<TryXoa page='hub' />
|
||||
</p>
|
||||
</Container>
|
||||
</Page>
|
||||
),
|
||||
])
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</Page>
|
||||
))
|
||||
|
||||
export default Hub
|
||||
|
||||
21
packages/xo-web/src/xo-app/hub/recipes/index.js
Normal file
21
packages/xo-web/src/xo-app/hub/recipes/index.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import decorate from 'apply-decorators'
|
||||
import React from 'react'
|
||||
import { adminOnly } from 'utils'
|
||||
import { Container, Col, Row } from 'grid'
|
||||
|
||||
import Recipe from './recipe'
|
||||
|
||||
// ==================================================================
|
||||
|
||||
export default decorate([
|
||||
adminOnly,
|
||||
() => (
|
||||
<Container>
|
||||
<Row>
|
||||
<Col mediumSize={4}>
|
||||
<Recipe />
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
),
|
||||
])
|
||||
135
packages/xo-web/src/xo-app/hub/recipes/recipe-form.js
Normal file
135
packages/xo-web/src/xo-app/hub/recipes/recipe-form.js
Normal file
@@ -0,0 +1,135 @@
|
||||
import * as FormGrid from 'form-grid'
|
||||
import _, { messages } from 'intl'
|
||||
import decorate from 'apply-decorators'
|
||||
import React from 'react'
|
||||
import { Container } from 'grid'
|
||||
import { get } from '@xen-orchestra/defined'
|
||||
import { injectIntl } from 'react-intl'
|
||||
import { injectState, provideState } from 'reaclette'
|
||||
import { isSrWritable } from 'xo'
|
||||
import { SelectPool, SelectNetwork, SelectSr } from 'select-objects'
|
||||
|
||||
export default decorate([
|
||||
injectIntl,
|
||||
provideState({
|
||||
effects: {
|
||||
onChangePool(__, pool) {
|
||||
const { onChange, value } = this.props
|
||||
onChange({
|
||||
...value,
|
||||
pool,
|
||||
})
|
||||
},
|
||||
onChangeSr(__, sr) {
|
||||
const { onChange, value } = this.props
|
||||
onChange({
|
||||
...value,
|
||||
sr,
|
||||
})
|
||||
},
|
||||
onChangeNetwork(__, network) {
|
||||
const { onChange, value } = this.props
|
||||
onChange({
|
||||
...value,
|
||||
network,
|
||||
})
|
||||
},
|
||||
onChangeValue(__, ev) {
|
||||
const { name, value } = ev.target
|
||||
const { onChange, value: prevValue } = this.props
|
||||
onChange({
|
||||
...prevValue,
|
||||
[name]: value,
|
||||
})
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
networkPredicate: (_, { value: { pool } }) => network =>
|
||||
pool.id === network.$pool,
|
||||
srPredicate: (_, { value }) => sr =>
|
||||
sr.$pool === get(() => value.pool.id) && isSrWritable(sr),
|
||||
},
|
||||
}),
|
||||
injectState,
|
||||
({ effects, install, intl: { formatMessage }, state, value }) => (
|
||||
<Container>
|
||||
<FormGrid.Row>
|
||||
<label>{_('vmImportToPool')}</label>
|
||||
<SelectPool
|
||||
className='mb-1'
|
||||
onChange={effects.onChangePool}
|
||||
required
|
||||
value={value.pool}
|
||||
/>
|
||||
</FormGrid.Row>
|
||||
<FormGrid.Row>
|
||||
<label>{_('vmImportToSr')}</label>
|
||||
<SelectSr
|
||||
onChange={effects.onChangeSr}
|
||||
predicate={state.srPredicate}
|
||||
required
|
||||
value={value.sr}
|
||||
/>
|
||||
</FormGrid.Row>
|
||||
<FormGrid.Row>
|
||||
<label>{_('network')}</label>
|
||||
<SelectNetwork
|
||||
className='mb-1'
|
||||
onChange={effects.onChangeNetwork}
|
||||
required
|
||||
value={value.network}
|
||||
predicate={state.networkPredicate}
|
||||
/>
|
||||
</FormGrid.Row>
|
||||
<FormGrid.Row>
|
||||
<label>{_('recipeMasterNameLabel')}</label>
|
||||
<input
|
||||
className='form-control'
|
||||
name='masterName'
|
||||
onChange={effects.onChangeValue}
|
||||
placeholder={formatMessage(messages.recipeMasterNameLabel)}
|
||||
required
|
||||
type='text'
|
||||
value={value.masterName}
|
||||
/>
|
||||
</FormGrid.Row>
|
||||
<FormGrid.Row>
|
||||
<label>{_('recipeNumberOfNodesLabel')}</label>
|
||||
<input
|
||||
className='form-control'
|
||||
name='nbNodes'
|
||||
min='1'
|
||||
onChange={effects.onChangeValue}
|
||||
placeholder={formatMessage(messages.recipeNumberOfNodesLabel)}
|
||||
required
|
||||
type='number'
|
||||
value={value.nbNodes}
|
||||
/>
|
||||
</FormGrid.Row>
|
||||
<FormGrid.Row>
|
||||
<label>{_('recipeSshKeyLabel')}</label>
|
||||
<input
|
||||
className='form-control'
|
||||
name='sshKey'
|
||||
onChange={effects.onChangeValue}
|
||||
placeholder={formatMessage(messages.recipeSshKeyLabel)}
|
||||
required
|
||||
type='text'
|
||||
value={value.sshKey}
|
||||
/>
|
||||
</FormGrid.Row>
|
||||
<FormGrid.Row>
|
||||
<label>{_('recipeNetworkCidr')}</label>
|
||||
<input
|
||||
className='form-control'
|
||||
name='cidr'
|
||||
onChange={effects.onChangeValue}
|
||||
placeholder={formatMessage(messages.recipeNetworkCidr)}
|
||||
required
|
||||
type='text'
|
||||
value={value.cidr}
|
||||
/>
|
||||
</FormGrid.Row>
|
||||
</Container>
|
||||
),
|
||||
])
|
||||
109
packages/xo-web/src/xo-app/hub/recipes/recipe.js
Normal file
109
packages/xo-web/src/xo-app/hub/recipes/recipe.js
Normal file
@@ -0,0 +1,109 @@
|
||||
import * as ComplexMatcher from 'complex-matcher'
|
||||
import _ from 'intl'
|
||||
import ActionButton from 'action-button'
|
||||
import ButtonLink from 'button-link'
|
||||
import decorate from 'apply-decorators'
|
||||
import Icon from 'icon'
|
||||
import marked from 'marked'
|
||||
import React from 'react'
|
||||
import { Card, CardBlock, CardHeader } from 'card'
|
||||
import { escapeRegExp } from 'lodash'
|
||||
import { form } from 'modal'
|
||||
import { connectStore } from 'utils'
|
||||
import { createGetObjectsOfType } from 'selectors'
|
||||
import { createKubernetesCluster } from 'xo'
|
||||
import { injectState, provideState } from 'reaclette'
|
||||
import { success } from 'notification'
|
||||
import { withRouter } from 'react-router'
|
||||
|
||||
import RecipeForm from './recipe-form'
|
||||
|
||||
const RECIPE_INFO = {
|
||||
id: '05abc8a8-ebf4-41a6-b1ed-efcb2dbf893d',
|
||||
name: 'Kubernetes cluster',
|
||||
description:
|
||||
'Creates a Kubernetes cluster composed of 1 master and a configurable number of nodes working for the master.',
|
||||
}
|
||||
|
||||
export default decorate([
|
||||
withRouter,
|
||||
connectStore(() => ({
|
||||
pools: createGetObjectsOfType('pool'),
|
||||
recipeCreatingResources: state => state.recipeCreatingResources,
|
||||
})),
|
||||
provideState({
|
||||
initialState: () => ({
|
||||
selectedInstallPools: [],
|
||||
}),
|
||||
effects: {
|
||||
async create() {
|
||||
const { markRecipeAsCreating, markRecipeAsDone } = this.props
|
||||
const recipeParams = await form({
|
||||
defaultValue: {
|
||||
pool: {},
|
||||
},
|
||||
render: props => <RecipeForm {...props} />,
|
||||
header: (
|
||||
<span>
|
||||
<Icon icon='hub-recipe' /> {RECIPE_INFO.name}
|
||||
</span>
|
||||
),
|
||||
size: 'medium',
|
||||
})
|
||||
|
||||
const { cidr, masterName, nbNodes, network, sr, sshKey } = recipeParams
|
||||
|
||||
markRecipeAsCreating(RECIPE_INFO.id)
|
||||
const tag = await createKubernetesCluster({
|
||||
cidr,
|
||||
masterName,
|
||||
nbNodes: +nbNodes,
|
||||
network: network.id,
|
||||
sr: sr.id,
|
||||
sshKey,
|
||||
})
|
||||
markRecipeAsDone(RECIPE_INFO.id)
|
||||
|
||||
const filter = new ComplexMatcher.Property(
|
||||
'tags',
|
||||
new ComplexMatcher.RegExp(`^${escapeRegExp(tag)}$`, 'i')
|
||||
)
|
||||
|
||||
success(
|
||||
_('recipeCreatedSuccessfully'),
|
||||
<ButtonLink
|
||||
btnStyle='success'
|
||||
size='small'
|
||||
to={`/home?s=${encodeURIComponent(filter)}`}
|
||||
>
|
||||
{_('recipeViewCreatedVms')}
|
||||
</ButtonLink>,
|
||||
8e3
|
||||
)
|
||||
},
|
||||
},
|
||||
}),
|
||||
injectState,
|
||||
({ effects, recipeCreatingResources }) => (
|
||||
<Card shadow>
|
||||
<CardHeader>{RECIPE_INFO.name}</CardHeader>
|
||||
<CardBlock>
|
||||
<div
|
||||
className='text-muted'
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: marked(RECIPE_INFO.description),
|
||||
}}
|
||||
/>
|
||||
<hr />
|
||||
<ActionButton
|
||||
block
|
||||
handler={effects.create}
|
||||
icon='deploy'
|
||||
pending={recipeCreatingResources[RECIPE_INFO.id]}
|
||||
>
|
||||
{_('create')}
|
||||
</ActionButton>
|
||||
</CardBlock>
|
||||
</Card>
|
||||
),
|
||||
])
|
||||
54
packages/xo-web/src/xo-app/hub/templates/index.js
Normal file
54
packages/xo-web/src/xo-app/hub/templates/index.js
Normal file
@@ -0,0 +1,54 @@
|
||||
import _ from 'intl'
|
||||
import decorate from 'apply-decorators'
|
||||
import Icon from 'icon'
|
||||
import React from 'react'
|
||||
import { addSubscriptions, adminOnly } from 'utils'
|
||||
import { Container, Col, Row } from 'grid'
|
||||
import { injectState, provideState } from 'reaclette'
|
||||
import { isEmpty, map, omit, orderBy } from 'lodash'
|
||||
import { subscribeHubResourceCatalog } from 'xo'
|
||||
|
||||
import Resource from './resource'
|
||||
|
||||
// ==================================================================
|
||||
|
||||
export default decorate([
|
||||
adminOnly,
|
||||
addSubscriptions({
|
||||
catalog: subscribeHubResourceCatalog,
|
||||
}),
|
||||
provideState({
|
||||
computed: {
|
||||
resources: (_, { catalog }) =>
|
||||
orderBy(
|
||||
map(omit(catalog, '_namespaces'), (entry, namespace) => ({
|
||||
namespace,
|
||||
...entry.xva,
|
||||
})),
|
||||
'name',
|
||||
'asc'
|
||||
),
|
||||
},
|
||||
}),
|
||||
injectState,
|
||||
({ state: { resources } }) => (
|
||||
<Container>
|
||||
<Row>
|
||||
{isEmpty(resources) ? (
|
||||
<Col>
|
||||
<h2 className='text-muted'>
|
||||
{_('vmNoAvailable')}
|
||||
<Icon icon='alarm' color='yellow' />
|
||||
</h2>
|
||||
</Col>
|
||||
) : (
|
||||
resources.map(data => (
|
||||
<Col key={data.namespace} mediumSize={6} largeSize={4}>
|
||||
<Resource {...data} />
|
||||
</Col>
|
||||
))
|
||||
)}
|
||||
</Row>
|
||||
</Container>
|
||||
),
|
||||
])
|
||||
@@ -8,7 +8,7 @@ import React from 'react'
|
||||
import { alert, form } from 'modal'
|
||||
import { Card, CardBlock, CardHeader } from 'card'
|
||||
import { Col, Row } from 'grid'
|
||||
import { connectStore, formatSize, getXoaPlan } from 'utils'
|
||||
import { connectStore, formatSize } from 'utils'
|
||||
import { createGetObjectsOfType } from 'selectors'
|
||||
import { deleteTemplates, downloadAndInstallResource, pureDeleteVm } from 'xo'
|
||||
import { error, success } from 'notification'
|
||||
@@ -30,18 +30,6 @@ const EXCLUSIVE_FIELDS = ['longDescription'] // These fields will not have a lab
|
||||
const MARKDOWN_FIELDS = ['longDescription', 'description']
|
||||
const STATIC_FIELDS = [...EXCLUSIVE_FIELDS, ...BANNED_FIELDS] // These fields will not be displayed with dynamic fields
|
||||
|
||||
const subscribeAlert = () =>
|
||||
alert(
|
||||
_('hubResourceAlert'),
|
||||
<div>
|
||||
<p>
|
||||
{_('considerSubscribe', {
|
||||
link: 'https://xen-orchestra.com',
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
|
||||
export default decorate([
|
||||
withRouter,
|
||||
connectStore(() => {
|
||||
@@ -69,10 +57,6 @@ export default decorate([
|
||||
version,
|
||||
} = this.props
|
||||
const { isTemplateInstalled } = this.state
|
||||
if (getXoaPlan() === 'Community') {
|
||||
subscribeAlert()
|
||||
return
|
||||
}
|
||||
const resourceParams = await form({
|
||||
defaultValue: {
|
||||
mapPoolsSrs: {},
|
||||
@@ -127,10 +111,6 @@ export default decorate([
|
||||
async create() {
|
||||
const { isPoolCreated, installedTemplates } = this.state
|
||||
const { name } = this.props
|
||||
if (getXoaPlan() === 'Community') {
|
||||
subscribeAlert()
|
||||
return
|
||||
}
|
||||
const resourceParams = await form({
|
||||
defaultValue: {
|
||||
pool: undefined,
|
||||
@@ -330,9 +330,21 @@ export default class Menu extends Component {
|
||||
],
|
||||
},
|
||||
isAdmin && {
|
||||
to: '/hub',
|
||||
to: '/hub/templates',
|
||||
icon: 'menu-hub',
|
||||
label: 'hubPage',
|
||||
subMenu: [
|
||||
{
|
||||
to: '/hub/templates',
|
||||
icon: 'hub-template',
|
||||
label: 'templatesLabel',
|
||||
},
|
||||
{
|
||||
to: '/hub/recipes',
|
||||
icon: 'hub-recipe',
|
||||
label: 'recipesLabel',
|
||||
},
|
||||
],
|
||||
},
|
||||
isAdmin && { to: '/about', icon: 'menu-about', label: 'aboutPage' },
|
||||
!noOperatablePools && {
|
||||
|
||||
@@ -312,7 +312,10 @@ const INDIVIDUAL_ACTIONS = [
|
||||
level: 'primary',
|
||||
},
|
||||
{
|
||||
handler: (remote, { editRemote }) => editRemote(remote),
|
||||
handler: (remote, { reset, editRemote }) => {
|
||||
reset()
|
||||
editRemote(remote)
|
||||
},
|
||||
icon: 'edit',
|
||||
label: _('formEdit'),
|
||||
level: 'primary',
|
||||
@@ -372,6 +375,7 @@ export default decorate([
|
||||
columns={COLUMNS_LOCAL_REMOTE}
|
||||
data-editRemote={effects.editRemote}
|
||||
data-formatMessage={formatMessage}
|
||||
data-reset={effects.reset}
|
||||
filters={FILTERS}
|
||||
groupedActions={GROUPED_ACTIONS}
|
||||
individualActions={INDIVIDUAL_ACTIONS}
|
||||
@@ -388,6 +392,7 @@ export default decorate([
|
||||
columns={COLUMNS_NFS_REMOTE}
|
||||
data-editRemote={effects.editRemote}
|
||||
data-formatMessage={formatMessage}
|
||||
data-reset={effects.reset}
|
||||
filters={FILTERS}
|
||||
groupedActions={GROUPED_ACTIONS}
|
||||
individualActions={INDIVIDUAL_ACTIONS}
|
||||
@@ -404,6 +409,7 @@ export default decorate([
|
||||
columns={COLUMNS_SMB_REMOTE}
|
||||
data-editRemote={effects.editRemote}
|
||||
data-formatMessage={formatMessage}
|
||||
data-reset={effects.reset}
|
||||
filters={FILTERS}
|
||||
groupedActions={GROUPED_ACTIONS}
|
||||
individualActions={INDIVIDUAL_ACTIONS}
|
||||
|
||||
@@ -43,7 +43,7 @@ export default decorate([
|
||||
editRemote: ({ reset }) => state => {
|
||||
const {
|
||||
remote,
|
||||
domain = remote.domain,
|
||||
domain = remote.domain || '',
|
||||
host = remote.host,
|
||||
name,
|
||||
options = remote.options || '',
|
||||
@@ -242,7 +242,7 @@ export default decorate([
|
||||
className='form-control'
|
||||
name='host'
|
||||
onChange={effects.linkState}
|
||||
pattern='^([^\\/]+)\\([^\\/]+)$'
|
||||
pattern='^[^\\/]+\\[^\\/]+$'
|
||||
placeholder={formatMessage(
|
||||
messages.remoteSmbPlaceHolderAddressShare
|
||||
)}
|
||||
@@ -255,7 +255,7 @@ export default decorate([
|
||||
className='form-control'
|
||||
name='path'
|
||||
onChange={effects.linkState}
|
||||
pattern='^(([^\\/]+)+(\\[^\\/]+)*)?$'
|
||||
pattern='^([^\\/]+(\\[^\\/]+)*)?$'
|
||||
placeholder={formatMessage(
|
||||
messages.remoteSmbPlaceHolderRemotePath
|
||||
)}
|
||||
|
||||
@@ -15,12 +15,12 @@ import { confirm } from 'modal'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import { error } from 'notification'
|
||||
import { generateId, linkState, toggleState } from 'reaclette-utils'
|
||||
import { getApplianceInfo, subscribeBackupNgJobs, subscribeJobs } from 'xo'
|
||||
import { injectIntl } from 'react-intl'
|
||||
import { injectState, provideState } from 'reaclette'
|
||||
import { Input as DebounceInput } from 'debounce-input-decorator'
|
||||
import { isEmpty, map, pick, some, zipObject } from 'lodash'
|
||||
import { Password, Select } from 'form'
|
||||
import { subscribeBackupNgJobs, subscribeJobs } from 'xo'
|
||||
|
||||
import { getXoaPlan, TryXoa } from '../../../common/utils'
|
||||
|
||||
@@ -258,6 +258,10 @@ const Updates = decorate([
|
||||
.map(name => `- ${name}: ${installedPackages[name]}`)
|
||||
.join('\n'),
|
||||
proxyFormId: generateId,
|
||||
xoaBuild: async () => {
|
||||
const { build = 'unknown' } = await getApplianceInfo()
|
||||
return build
|
||||
},
|
||||
},
|
||||
}),
|
||||
injectState,
|
||||
@@ -290,7 +294,7 @@ const Updates = decorate([
|
||||
<CardBlock>
|
||||
<fieldset disabled={COMMUNITY}>
|
||||
<p>
|
||||
{_('currentVersion')}{' '}
|
||||
{_('xoaBuild')} {state.xoaBuild} - {_('currentVersion')}{' '}
|
||||
{defined(
|
||||
() => state.installedPackages['xen-orchestra'],
|
||||
'unknown'
|
||||
|
||||
Reference in New Issue
Block a user