Compare commits

...

33 Commits

Author SHA1 Message Date
BARHTAOUI
7d72165997 chore(CHANGELOG): update next 2019-10-10 15:56:19 +02:00
BARHTAOUI
58bfde62bd feat(xo-web): 5.50.3 2019-10-10 15:53:52 +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
Julien Fontanet
093fb7f959 fix(xo-server-logs): explicit appDir
May fix #4576
2019-10-03 16:02:10 +02:00
Julien Fontanet
f6472424ad fix(eslint): disable lines-between-class-members rule 2019-10-02 15:52:51 +02:00
Julien Fontanet
31ed3767c6 chore: fix some lint
Mainly: `obj['prop']` → `obj.prop`
2019-10-02 15:45:32 +02:00
marcpezin
366acb65ea doc(updater): release channels (#4572) 2019-10-02 15:24:45 +02:00
Pierre Donias
7c6946931b chore(xo-web): PascalCase NotFound component (#4567)
https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/jsx-pascal-case.md
2019-10-01 17:21:00 +02:00
HamadaBrest
5d971433a5 fix(xo-web/new-vm/self): template selection (#4568)
Fixes #4565
Introduced by 9efc3dd1fb
2019-10-01 16:59:59 +02:00
Rajaa.BARHTAOUI
05264b326b chore(CHANGELOG): 5.39.0 (#4561) 2019-09-30 16:08:56 +02:00
BARHTAOUI
fdd5c6bfd8 chore(CHANGELOG): update next 2019-09-30 15:13:05 +02:00
BARHTAOUI
42c3528c2f feat(xo-web): 5.50.2 2019-09-30 15:13:05 +02:00
HamadaBrest
18640714f1 fix(xo-web/hub/resource): "remove" button icon (#4559) 2019-09-30 14:43:31 +02:00
HamadaBrest
cda4d3399b fix(xo-web/hub): responsive hub VM templates (#4558)
Fixes #4557
2019-09-30 14:42:01 +02:00
Julien Fontanet
4da8af6e69 fix(log/configure): process nested transport 2019-09-27 14:55:26 +02:00
Julien Fontanet
b535565612 feat(@xen-orchestra/log): 0.2.0 2019-09-27 14:29:34 +02:00
Julien Fontanet
bef39b8a96 fix(log): support DEBUG and NODE_DEBUG when both defined 2019-09-27 14:26:19 +02:00
Julien Fontanet
fb2502a031 feat(log): support env var LOG_LEVEL 2019-09-27 14:23:27 +02:00
Julien Fontanet
0b90befda1 feat(log): also export createLogger symbol 2019-09-27 13:59:44 +02:00
Julien Fontanet
f9800f104a feat(log/console): display data inline 2019-09-27 13:59:03 +02:00
Julien Fontanet
99134cc381 Merge pull request #4554 from vatesfr/release
Technical release
2019-09-27 10:22:49 +02:00
BARHTAOUI
66ca08da6d chore(CHANGELOG): update next 2019-09-27 10:15:46 +02:00
BARHTAOUI
5eb7ece6ba feat(xo-web): 5.50.1 2019-09-27 09:59:05 +02:00
BARHTAOUI
6b3d334e76 feat(xo-server): 5.50.1 2019-09-27 09:58:51 +02:00
badrAZ
14f5fd8f73 feat(xen-api): 0.27.2 2019-09-27 09:54:47 +02:00
BARHTAOUI
5f73aee0df feat(xo-server-cloud): 0.3.0 2019-09-27 09:47:16 +02:00
51 changed files with 796 additions and 753 deletions

View File

@@ -40,6 +40,13 @@ module.exports = {
'react/jsx-handler-names': 'off',
// disabled because not always relevant, we might reconsider in the future
//
// enabled by https://github.com/standard/eslint-config-standard/commit/319b177750899d4525eb1210686f6aca96190b2f
//
// example: https://github.com/vatesfr/xen-orchestra/blob/31ed3767c67044ca445658eb6b560718972402f2/packages/xen-api/src/index.js#L156-L157
'lines-between-class-members': 'off',
'no-console': ['error', { allow: ['warn', 'error'] }],
'no-var': 'error',
'node/no-extraneous-import': 'error',

View File

@@ -16,7 +16,7 @@
},
"dependencies": {
"golike-defer": "^0.4.1",
"xen-api": "^0.27.1"
"xen-api": "^0.27.2"
},
"scripts": {
"postversion": "npm publish"

View File

@@ -1,6 +1,6 @@
{
"name": "@xen-orchestra/log",
"version": "0.1.4",
"version": "0.2.0",
"license": "ISC",
"description": "",
"keywords": [],

View File

@@ -19,7 +19,8 @@ const createTransport = config => {
}
}
let { filter, transport } = config
let { filter } = config
let transport = createTransport(config.transport)
const level = resolve(config.level)
if (filter !== undefined) {
@@ -51,11 +52,12 @@ const symbol =
? Symbol.for('@xen-orchestra/log')
: '@@@xen-orchestra/log'
const { env } = process
global[symbol] = createTransport({
// display warnings or above, and all that are enabled via DEBUG or
// NODE_DEBUG env
filter: process.env.DEBUG || process.env.NODE_DEBUG,
level: LEVELS.INFO,
filter: [env.DEBUG, env.NODE_DEBUG].filter(Boolean).join(','),
level: resolve(env.LOG_LEVEL, LEVELS.INFO),
transport: createConsoleTransport(),
})

View File

@@ -1,5 +1,5 @@
import createTransport from './transports/console'
import LEVELS from './levels'
import LEVELS, { resolve } from './levels'
const symbol =
typeof Symbol !== 'undefined'
@@ -9,7 +9,8 @@ if (!(symbol in global)) {
// the default behavior, without requiring `configure` is to avoid
// logging anything unless it's a real error
const transport = createTransport()
global[symbol] = log => log.level > LEVELS.WARN && transport(log)
const level = resolve(process.env.LOG_LEVEL, LEVELS.WARN)
global[symbol] = log => log.level >= level && transport(log)
}
// -------------------------------------------------------------------
@@ -72,5 +73,5 @@ prototype.wrap = function(message, fn) {
}
}
const createLogger = namespace => new Logger(namespace)
export const createLogger = namespace => new Logger(namespace)
export { createLogger as default }

View File

@@ -13,11 +13,22 @@ for (const name in LEVELS) {
NAMES[LEVELS[name]] = name
}
export const resolve = level => {
if (typeof level === 'string') {
level = LEVELS[level.toUpperCase()]
// resolves to the number representation of a level
//
// returns `defaultLevel` if invalid
export const resolve = (level, defaultLevel) => {
const type = typeof level
if (type === 'number') {
if (level in NAMES) {
return level
}
} else if (type === 'string') {
const nLevel = LEVELS[level.toUpperCase()]
if (nLevel !== undefined) {
return nLevel
}
}
return level
return defaultLevel
}
Object.freeze(LEVELS)

View File

@@ -1,26 +1,23 @@
import LEVELS, { NAMES } from '../levels'
// Bind console methods (necessary for browsers)
/* eslint-disable no-console */
const debugConsole = console.log.bind(console)
const infoConsole = console.info.bind(console)
const warnConsole = console.warn.bind(console)
const errorConsole = console.error.bind(console)
/* eslint-enable no-console */
const { ERROR, INFO, WARN } = LEVELS
const consoleTransport = ({ data, level, namespace, message, time }) => {
const fn =
/* eslint-disable no-console */
level < INFO
? debugConsole
? console.log
: level < WARN
? infoConsole
? console.info
: level < ERROR
? warnConsole
: errorConsole
? console.warn
: console.error
/* eslint-enable no-console */
fn('%s - %s - [%s] %s', time.toISOString(), namespace, NAMES[level], message)
data != null && fn(data)
const args = [time.toISOString(), namespace, NAMES[level], message]
if (data != null) {
args.push(data)
}
fn.apply(console, args)
}
export default () => consoleTransport

View File

@@ -4,14 +4,41 @@
### 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))
- [SDN Controller] Add possibility to encrypt private networks (PR [#4441](https://github.com/vatesfr/xen-orchestra/pull/4441))
- [Backups] Improve performance by caching VM backups listing (PR [#4509](https://github.com/vatesfr/xen-orchestra/pull/4509))
- [HUB] VM template store [#1918](https://github.com/vatesfr/xen-orchestra/issues/1918) (PR [#4442](https://github.com/vatesfr/xen-orchestra/pull/4442))
### Enhancements
- [SR/new] Clarify address formats [#4450](https://github.com/vatesfr/xen-orchestra/issues/4450) (PR [#4460](https://github.com/vatesfr/xen-orchestra/pull/4460))
- [Backup NG/New] Show warning if zstd compression is not supported on a VM [#3892](https://github.com/vatesfr/xen-orchestra/issues/3892) (PRs [#4411](https://github.com/vatesfr/xen-orchestra/pull/4411))
- [VM/disks] Don't hide disks that are attached to the same VM twice [#4400](https://github.com/vatesfr/xen-orchestra/issues/4400) (PR [#4414](https://github.com/vatesfr/xen-orchestra/pull/4414))
- [VM/console] Add a button to connect to the VM via the local SSH client (PR [#4415](https://github.com/vatesfr/xen-orchestra/pull/4415))
- [SDN Controller] Add possibility to encrypt private networks (PR [#4441](https://github.com/vatesfr/xen-orchestra/pull/4441))
- [SDN Controller] Ability to configure MTU for private networks (PR [#4491](https://github.com/vatesfr/xen-orchestra/pull/4491))
- [VM Export] Filenames are now prefixed with datetime [#4503](https://github.com/vatesfr/xen-orchestra/issues/4503)
- [Backups] Improve performance by caching VM backups listing (PR [#4509](https://github.com/vatesfr/xen-orchestra/pull/4509))
- [Settings/Logs] Differenciate XS/XCP-ng errors from XO errors [#4101](https://github.com/vatesfr/xen-orchestra/issues/4101) (PR [#4385](https://github.com/vatesfr/xen-orchestra/pull/4385))
- [Backups] Improve performance by caching logs consolidation (PR [#4541](https://github.com/vatesfr/xen-orchestra/pull/4541))
- [New VM] Cloud Init available for all plans (PR [#4543](https://github.com/vatesfr/xen-orchestra/pull/4543))
- [Servers] IPv6 addresses can be used [#4520](https://github.com/vatesfr/xen-orchestra/issues/4520) (PR [#4521](https://github.com/vatesfr/xen-orchestra/pull/4521)) \
Note: They must enclosed in brackets to differentiate with the port, e.g.: `[2001:db8::7334]` or `[ 2001:db8::7334]:4343`
### Bug fixes
@@ -30,20 +57,23 @@
- [XOA] Remove "Updates" and "Licenses" tabs for non admin users (PR [#4526](https://github.com/vatesfr/xen-orchestra/pull/4526))
- [New VM] Ability to escape [cloud config template](https://xen-orchestra.com/blog/xen-orchestra-5-21/#cloudconfigtemplates) variables [#4486](https://github.com/vatesfr/xen-orchestra/issues/4486) (PR [#4501](https://github.com/vatesfr/xen-orchestra/pull/4501))
- [Backup NG] Properly log and report if job is already running [#4497](https://github.com/vatesfr/xen-orchestra/issues/4497) (PR [4534](https://github.com/vatesfr/xen-orchestra/pull/4534))
- [Host] Fix an issue where host was wrongly reporting time inconsistency (PR [#4540](https://github.com/vatesfr/xen-orchestra/pull/4540))
### Released packages
- xen-api v0.27.2
- xo-server-cloud v0.3.0
- @xen-orchestra/cron v1.0.4
- xo-server-sdn-controller v0.3.0
- @xen-orchestra/template v0.1.0
- xo-server v5.50.0
- xo-web v5.50.0
- xo-server v5.50.1
- xo-web v5.50.2
## **5.38.0** (2019-08-29)
![Channel: latest](https://badgen.net/badge/channel/latest/yellow)
![Channel: stable](https://badgen.net/badge/channel/stable/green)
### Enhancements
@@ -71,8 +101,6 @@
## **5.37.1** (2019-08-06)
![Channel: stable](https://badgen.net/badge/channel/stable/green)
### Enhancements
- [SDN Controller] Let the user choose on which PIF to create a private network (PR [#4379](https://github.com/vatesfr/xen-orchestra/pull/4379))

View File

@@ -7,18 +7,14 @@
> Users must be able to say: “Nice enhancement, I'm eager to test it”
- [Settings/Logs] Differenciate XS/XCP-ng errors from XO errors [#4101](https://github.com/vatesfr/xen-orchestra/issues/4101) (PR [#4385](https://github.com/vatesfr/xen-orchestra/pull/4385))
- [Backups] Improve performance by caching logs consolidation (PR [#4541](https://github.com/vatesfr/xen-orchestra/pull/4541))
- [New VM] Cloud Init available for all plans (PR [#4543](https://github.com/vatesfr/xen-orchestra/pull/4543))
- [Servers] IPv6 addresses can be used [#4520](https://github.com/vatesfr/xen-orchestra/issues/4520) (PR [#4521](https://github.com/vatesfr/xen-orchestra/pull/4521)) \
Note: They must enclosed in brackets to differentiate with the port, e.g.: `[2001:db8::7334]` or `[ 2001:db8::7334]:4343`
- [HUB] VM template store [#1918](https://github.com/vatesfr/xen-orchestra/issues/1918) (PR [#4442](https://github.com/vatesfr/xen-orchestra/pull/4442))
[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
> Users must be able to say: “I had this issue, happy to know it's fixed”
- [Host] Fix an issue where host was wrongly reporting time inconsistency (PR [#4540](https://github.com/vatesfr/xen-orchestra/pull/4540))
- [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
@@ -27,7 +23,5 @@
>
> Rule of thumb: add packages on top.
- xen-api v0.27.2
- xo-server-cloud v0.3.0
- xo-server v5.51.0
- xo-web v5.51.0

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

View File

@@ -41,6 +41,20 @@ However, if you want to start a manual check, you can do it by clicking on the "
![](./assets/xo5updatebutton.png)
#### Release channel
In Xen Orchestra, you can make a choice between two different release channels.
##### Stable
The stable channel is intended to be a version of Xen Orchestra that is already **one month old** (and therefore will benefit from one month of community feedback and various fixes). This way, users more concerned with the stability of their appliance will have the option to stay on a slightly older (and tested) version of XO (still supported by our pro support).
##### Latest
The latest channel will include all the latest improvements available in Xen Orchestra. The version available in latest has already been QA'd by our team, but issues may still occur once deployed in vastly varying environments, such as our user base has.
> To select the release channel of your choice, go to the XOA > Updates view.
![](./assets/release-channels.png)
#### Upgrade
If a new version is found, you'll have an upgrade button and its tooltip displayed:

View File

@@ -41,7 +41,7 @@
"human-format": "^0.10.0",
"lodash": "^4.17.4",
"pw": "^0.0.4",
"xen-api": "^0.27.1"
"xen-api": "^0.27.2"
},
"devDependencies": {
"@babel/cli": "^7.1.5",

View File

@@ -1,6 +1,6 @@
{
"name": "xen-api",
"version": "0.27.1",
"version": "0.27.2",
"license": "ISC",
"description": "Connector to the Xen API",
"keywords": [

View File

@@ -36,7 +36,7 @@
"node": ">=6"
},
"dependencies": {
"@xen-orchestra/log": "^0.1.4",
"@xen-orchestra/log": "^0.2.0",
"human-format": "^0.10.0",
"lodash": "^4.13.1",
"moment-timezone": "^0.5.13"

View File

@@ -1,6 +1,6 @@
{
"name": "xo-server-cloud",
"version": "0.2.4",
"version": "0.3.0",
"license": "ISC",
"description": "",
"keywords": [

View File

@@ -680,7 +680,7 @@ ${entry.listItem}
},
}
if (xapiObject.$type === 'VM') {
payload['vm_uuid'] = xapiObject.uuid
payload.vm_uuid = xapiObject.uuid
}
// JSON is not well formed, can't use the default node parser
return JSON5.parse(

View File

@@ -28,10 +28,10 @@
"cross-env": "^5.2.0"
},
"dependencies": {
"@xen-orchestra/log": "^0.1.4",
"@xen-orchestra/log": "^0.2.0",
"lodash": "^4.17.11",
"node-openssl-cert": "^0.0.97",
"promise-toolbox": "^0.13.0",
"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

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

@@ -37,7 +37,7 @@
"dependencies": {
"@xen-orchestra/async-map": "^0.0.0",
"@xen-orchestra/cron": "^1.0.4",
"@xen-orchestra/log": "^0.1.4",
"@xen-orchestra/log": "^0.2.0",
"handlebars": "^4.0.6",
"html-minifier": "^4.0.0",
"human-format": "^0.10.0",

View File

@@ -1,7 +1,7 @@
{
"private": true,
"name": "xo-server",
"version": "5.50.0",
"version": "5.50.1",
"license": "AGPL-3.0",
"description": "Server part of Xen-Orchestra",
"keywords": [
@@ -39,7 +39,7 @@
"@xen-orchestra/defined": "^0.0.0",
"@xen-orchestra/emit-async": "^0.0.0",
"@xen-orchestra/fs": "^0.10.1",
"@xen-orchestra/log": "^0.1.4",
"@xen-orchestra/log": "^0.2.0",
"@xen-orchestra/mixin": "^0.0.0",
"ajv": "^6.1.1",
"app-conf": "^0.7.0",
@@ -124,7 +124,7 @@
"value-matcher": "^0.2.0",
"vhd-lib": "^0.7.0",
"ws": "^6.0.0",
"xen-api": "^0.27.1",
"xen-api": "^0.27.2",
"xml2js": "^0.4.19",
"xo-acl-resolver": "^0.4.1",
"xo-collection": "^0.4.1",

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

@@ -777,7 +777,7 @@ export async function probeIscsiExists({
)
const srs = []
forEach(ensureArray(xml['SRlist'].SR), sr => {
forEach(ensureArray(xml.SRlist.SR), sr => {
// get the UUID of SR connected to this LUN
srs.push({ uuid: sr.UUID.trim() })
})
@@ -845,7 +845,7 @@ export async function probeNfsExists({ host, server, serverPath }) {
const srs = []
forEach(ensureArray(xml['SRlist'].SR), sr => {
forEach(ensureArray(xml.SRlist.SR), sr => {
// get the UUID of SR connected to this LUN
srs.push({ uuid: sr.UUID.trim() })
})

View File

@@ -85,7 +85,7 @@ async function rateLimitedRetry(action, shouldRetry, retryCount = 20) {
function createVolumeInfoTypes() {
function parseHeal(parsed) {
const bricks = []
parsed['healInfo']['bricks']['brick'].forEach(brick => {
parsed.healInfo.bricks.brick.forEach(brick => {
bricks.push(brick)
if (brick.file) {
brick.file = ensureArray(brick.file)
@@ -96,21 +96,21 @@ function createVolumeInfoTypes() {
function parseStatus(parsed) {
const brickDictByUuid = {}
const volume = parsed['volStatus']['volumes']['volume']
volume['node'].forEach(node => {
const volume = parsed.volStatus.volumes.volume
volume.node.forEach(node => {
brickDictByUuid[node.peerid] = brickDictByUuid[node.peerid] || []
brickDictByUuid[node.peerid].push(node)
})
return {
commandStatus: true,
result: { nodes: brickDictByUuid, tasks: volume['tasks'] },
result: { nodes: brickDictByUuid, tasks: volume.tasks },
}
}
async function parseInfo(parsed) {
const volume = parsed['volInfo']['volumes']['volume']
volume['bricks'] = volume['bricks']['brick']
volume['options'] = volume['options']['option']
const volume = parsed.volInfo.volumes.volume
volume.bricks = volume.bricks.brick
volume.options = volume.options.option
return { commandStatus: true, result: volume }
}
@@ -118,23 +118,23 @@ function createVolumeInfoTypes() {
return async function(sr) {
const glusterEndpoint = this::_getGlusterEndpoint(sr)
const cmdShouldRetry = result =>
!result['commandStatus'] &&
((result.parsed && result.parsed['cliOutput']['opErrno'] === '30802') ||
!result.commandStatus &&
((result.parsed && result.parsed.cliOutput.opErrno === '30802') ||
result.stderr.match(/Another transaction is in progress/))
const runCmd = async () =>
glusterCmd(glusterEndpoint, 'volume ' + command, true)
const commandResult = await rateLimitedRetry(runCmd, cmdShouldRetry, 30)
return commandResult['commandStatus']
? this::handler(commandResult.parsed['cliOutput'], sr)
return commandResult.commandStatus
? this::handler(commandResult.parsed.cliOutput, sr)
: commandResult
}
}
async function profileType(sr) {
async function parseProfile(parsed) {
const volume = parsed['volProfile']
volume['bricks'] = ensureArray(volume['brick'])
delete volume['brick']
const volume = parsed.volProfile
volume.bricks = ensureArray(volume.brick)
delete volume.brick
return { commandStatus: true, result: volume }
}
@@ -143,9 +143,9 @@ function createVolumeInfoTypes() {
async function profileTopType(sr) {
async function parseTop(parsed) {
const volume = parsed['volTop']
volume['bricks'] = ensureArray(volume['brick'])
delete volume['brick']
const volume = parsed.volTop
volume.bricks = ensureArray(volume.brick)
delete volume.brick
return { commandStatus: true, result: volume }
}
@@ -326,7 +326,7 @@ async function remoteSsh(glusterEndpoint, cmd, ignoreError = false) {
}
messageArray.push(`${key}: ${result[key]}`)
}
messageArray.push('command: ' + result['command'].join(' '))
messageArray.push('command: ' + result.command.join(' '))
messageKeys.splice(messageKeys.indexOf('command'), 1)
for (const key of messageKeys) {
messageArray.push(`${key}: ${JSON.stringify(result[key])}`)
@@ -343,7 +343,7 @@ async function remoteSsh(glusterEndpoint, cmd, ignoreError = false) {
})
break
} catch (exception) {
if (exception['code'] !== 'HOST_OFFLINE') {
if (exception.code !== 'HOST_OFFLINE') {
throw exception
}
}
@@ -370,19 +370,17 @@ async function remoteSsh(glusterEndpoint, cmd, ignoreError = false) {
}
function findErrorMessage(commandResut) {
if (commandResut['exit'] === 0 && commandResut.parsed) {
const cliOut = commandResut.parsed['cliOutput']
if (cliOut['opErrstr'] && cliOut['opErrstr'].length) {
return cliOut['opErrstr']
if (commandResut.exit === 0 && commandResut.parsed) {
const cliOut = commandResut.parsed.cliOutput
if (cliOut.opErrstr && cliOut.opErrstr.length) {
return cliOut.opErrstr
}
// "peer probe" returns it's "already in peer" error in cliOutput/output
if (cliOut['output'] && cliOut['output'].length) {
return cliOut['output']
if (cliOut.output && cliOut.output.length) {
return cliOut.output
}
}
return commandResut['stderr'].length
? commandResut['stderr']
: commandResut['stdout']
return commandResut.stderr.length ? commandResut.stderr : commandResut.stdout
}
async function glusterCmd(glusterEndpoint, cmd, ignoreError = false) {
@@ -392,15 +390,15 @@ async function glusterCmd(glusterEndpoint, cmd, ignoreError = false) {
true
)
try {
result.parsed = parseXml(result['stdout'])
result.parsed = parseXml(result.stdout)
} catch (e) {
// pass, we never know if a message can be parsed or not, so we just try
}
if (result['exit'] === 0) {
const cliOut = result.parsed['cliOutput']
if (result.exit === 0) {
const cliOut = result.parsed.cliOutput
// we have found cases where opErrno is !=0 and opRet was 0, albeit the operation was an error.
result.commandStatus =
cliOut['opRet'].trim() === '0' && cliOut['opErrno'].trim() === '0'
cliOut.opRet.trim() === '0' && cliOut.opErrno.trim() === '0'
result.error = findErrorMessage(result)
} else {
result.commandStatus = false
@@ -793,7 +791,7 @@ export const createSR = defer(async function(
host: param.host.$id,
vm: { id: param.vm.$id, ip: param.address },
underlyingSr: param.underlyingSr.$id,
arbiter: !!param['arbiter'],
arbiter: !!param.arbiter,
}))
await xapi.xo.setData(xosanSrRef, 'xosan_config', {
version: 'beta2',
@@ -1300,7 +1298,7 @@ export const addBricks = defer(async function(
underlyingSr: newSr,
})
}
const arbiterNode = data.nodes.find(n => n['arbiter'])
const arbiterNode = data.nodes.find(n => n.arbiter)
if (arbiterNode) {
await glusterCmd(
glusterEndpoint,

View File

@@ -6,6 +6,7 @@ import ndjson from 'ndjson'
import parseArgs from 'minimist'
import sublevel from 'level-sublevel'
import util from 'util'
import { join as joinPath } from 'path'
import { repair as repairDb } from 'level'
import { forEach } from './utils'
@@ -174,6 +175,7 @@ export default async function main() {
}
const config = await appConf.load('xo-server', {
appDir: joinPath(__dirname, '..'),
ignoreUnknownFormats: true,
})

View File

@@ -1576,7 +1576,7 @@ export default class Xapi extends XapiBase {
}
} else {
// Find the original template by name (*sigh*).
const templateNameLabel = vm.other_config['base_template_name']
const templateNameLabel = vm.other_config.base_template_name
const template =
templateNameLabel &&
find(

View File

@@ -43,6 +43,6 @@ test('VMDKDirectParser reads OK', async () => {
}
expect(harvested.length).toEqual(2)
expect(harvested[0].offsetBytes).toEqual(0)
expect(harvested[0].data.length).toEqual(header['grainSizeSectors'] * 512)
expect(harvested[1].offsetBytes).toEqual(header['grainSizeSectors'] * 512)
expect(harvested[0].data.length).toEqual(header.grainSizeSectors * 512)
expect(harvested[1].offsetBytes).toEqual(header.grainSizeSectors * 512)
})

View File

@@ -1,7 +1,7 @@
{
"private": true,
"name": "xo-web",
"version": "5.50.0",
"version": "5.50.3",
"license": "AGPL-3.0",
"description": "Web interface client for Xen-Orchestra",
"keywords": [

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

@@ -468,14 +468,14 @@ export default class Scheduler extends Component {
optionRenderer={getMonthName}
options={MONTHS}
onChange={this._monthChange}
value={cronPatternArr[PICKTIME_TO_ID['month']]}
value={cronPatternArr[PICKTIME_TO_ID.month]}
/>
</Col>
<Col largeSize={6}>
<DayPicker
onChange={this._dayChange}
monthDayPattern={cronPatternArr[PICKTIME_TO_ID['monthDay']]}
weekDayPattern={cronPatternArr[PICKTIME_TO_ID['weekDay']]}
monthDayPattern={cronPatternArr[PICKTIME_TO_ID.monthDay]}
weekDayPattern={cronPatternArr[PICKTIME_TO_ID.weekDay]}
/>
</Col>
</Row>
@@ -485,7 +485,7 @@ export default class Scheduler extends Component {
labelId='Hour'
options={HOURS}
onChange={this._hourChange}
value={cronPatternArr[PICKTIME_TO_ID['hour']]}
value={cronPatternArr[PICKTIME_TO_ID.hour]}
/>
</Col>
<Col largeSize={6}>
@@ -493,7 +493,7 @@ export default class Scheduler extends Component {
labelId='Minute'
options={MINS}
onChange={this._minuteChange}
value={cronPatternArr[PICKTIME_TO_ID['minute']]}
value={cronPatternArr[PICKTIME_TO_ID.minute]}
/>
</Col>
</Row>

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

@@ -916,7 +916,7 @@ export class SelectResourceSetsVdi extends React.PureComponent {
() => this.props.resourceSet,
({ objectsByType }) => {
const { srPredicate } = this.props
const srs = objectsByType['SR']
const srs = objectsByType.SR
return srPredicate ? filter(srs, srPredicate) : srs
}
)

View File

@@ -252,7 +252,7 @@ export const parseSize = size => {
// -------------------------------------------------------------------
const _NotFound = () => <h1>{_('errorPageNotFound')}</h1>
const NotFound = () => <h1>{_('errorPageNotFound')}</h1>
// Decorator to declare routes on a component.
//
@@ -286,7 +286,7 @@ export const routes = (indexRoute, childRoutes) => target => {
}
if (childRoutes) {
childRoutes.push({ component: _NotFound, path: '*' })
childRoutes.push({ component: NotFound, path: '*' })
}
target.route = {
@@ -449,26 +449,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 = (() => {
@@ -629,7 +609,7 @@ export const adminOnly = Component =>
connectStore({
_isAdmin: isAdmin,
})(({ _isAdmin, ...props }) =>
_isAdmin ? <Component {...props} /> : <_NotFound />
_isAdmin ? <Component {...props} /> : <NotFound />
)
// ===================================================================

View File

@@ -2918,3 +2918,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

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

@@ -51,7 +51,7 @@ export default decorate([
</Col>
) : (
resources.map(data => (
<Col key={data.namespace} mediumSize={3}>
<Col key={data.namespace} mediumSize={6} largeSize={4}>
<Resource {...data} />
</Col>
))

View File

@@ -159,8 +159,8 @@ export default decorate([
},
},
computed: {
installedTemplates: (_, { id, templates }) =>
filter(templates, ['other.xo:resource:xva:id', id]),
installedTemplates: (_, { namespace, templates }) =>
filter(templates, ['other.xo:resource:namespace', namespace]),
isTemplateInstalledOnAllPools: ({ installedTemplates }, { pools }) =>
installedTemplates.length > 0 &&
pools.every(
@@ -197,11 +197,11 @@ export default decorate([
data-name={name}
disabled={state.installedTemplates.length === 0}
handler={effects.deleteTemplates}
icon='delete'
size='small'
style={{ border: 'none' }}
>
<Icon icon='delete' size='xs' />
</ActionButton>
tooltip={_('remove')}
/>
<br />
</CardHeader>
<CardBlock className='text-center'>

View File

@@ -281,6 +281,11 @@ export default class Menu extends Component {
label: 'notificationsPage',
extra: <NotificationTag />,
},
isAdmin && {
to: 'xoa/support',
icon: 'menu-support',
label: 'supportPage',
},
],
},
isAdmin && {

View File

@@ -11,6 +11,7 @@ import Page from '../page'
import PropTypes from 'prop-types'
import React from 'react'
import SelectBootFirmware from 'select-boot-firmware'
import SelectCoresPerSocket from 'select-cores-per-socket'
import store from 'store'
import Tags from 'tags'
import Tooltip from 'tooltip'
@@ -80,7 +81,6 @@ import {
addSubscriptions,
connectStore,
formatSize,
getCoresPerSocketPossibilities,
generateReadableRandomString,
resolveIds,
} from 'utils'
@@ -259,7 +259,7 @@ class Vif extends BaseComponent {
props.pool === undefined // to get objects as a self user
),
srs: getSrs(state, props),
template: getTemplate(state, props),
template: getTemplate(state, props, props.pool === undefined),
templates: getTemplates(state, props),
userSshKeys: getUserSshKeys(state, props),
})
@@ -347,6 +347,7 @@ export default class NewVm extends BaseComponent {
this._replaceState(
{
bootAfterCreate: true,
coresPerSocket: undefined,
CPUs: '',
cpuCap: '',
cpuWeight: '',
@@ -500,7 +501,8 @@ export default class NewVm extends BaseComponent {
VIFs: _VIFs,
resourceSet: resourceSet && resourceSet.id,
// vm.set parameters
coresPerSocket: state.coresPerSocket,
coresPerSocket:
state.coresPerSocket === null ? undefined : state.coresPerSocket,
CPUs: state.CPUs,
cpuWeight: state.cpuWeight === '' ? null : state.cpuWeight,
cpuCap: state.cpuCap === '' ? null : state.cpuCap,
@@ -761,17 +763,6 @@ export default class NewVm extends BaseComponent {
pool => vgpuType => pool !== undefined && pool.id === vgpuType.$pool
)
_getCoresPerSocketPossibilities = createSelector(
() => {
const { pool } = this.props
if (pool !== undefined) {
return pool.cpus.cores
}
},
() => this.state.state.CPUs,
getCoresPerSocketPossibilities
)
_isCoreOs = createSelector(
() => this.props.template,
template => template && template.name_label === 'CoreOS'
@@ -1079,7 +1070,17 @@ export default class NewVm extends BaseComponent {
_renderPerformances = () => {
const { coresPerSocket, CPUs, memoryDynamicMax } = this.state.state
const { template } = this.props
const { pool } = this.props
const memoryThreshold = get(() => template.memory.static[0])
const selectCoresPerSocket = (
<SelectCoresPerSocket
disabled={pool === undefined}
maxCores={get(() => pool.cpus.cores)}
maxVcpus={get(() => template.CPUs.max)}
onChange={this._linkState('coresPerSocket')}
value={coresPerSocket}
/>
)
return (
<Section
@@ -1114,29 +1115,13 @@ export default class NewVm extends BaseComponent {
)}
</Item>
<Item label={_('vmCpuTopology')}>
<select
className='form-control'
onChange={this._linkState('coresPerSocket')}
value={coresPerSocket}
>
{_('vmChooseCoresPerSocket', message => (
<option value=''>{message}</option>
))}
{map(this._getCoresPerSocketPossibilities(), coresPerSocket =>
_(
'vmCoresPerSocket',
{
nSockets: CPUs / coresPerSocket,
nCores: coresPerSocket,
},
message => (
<option key={coresPerSocket} value={coresPerSocket}>
{message}
</option>
)
)
)}
</select>
{pool !== undefined ? (
selectCoresPerSocket
) : (
<Tooltip content={_('requiresAdminPermissions')}>
{selectCoresPerSocket}
</Tooltip>
)}
</Item>
</SectionContent>
</Section>

View File

@@ -475,7 +475,7 @@ export default class New extends Component {
}
_handleAuthChoice = () => {
const auth = this.refs['auth'].checked
const auth = this.refs.auth.checked
this.setState({
auth,
})

View File

@@ -154,7 +154,7 @@ class IpsCell extends BaseComponent {
value={newIps || ''}
/>{' '}
<ActionButton
form={`newIpForm`}
form='newIpForm'
icon='save'
btnStyle='primary'
handler={this._addIps}

View File

@@ -9,6 +9,7 @@ import Link from 'link'
import React from 'react'
import renderXoItem from 'render-xo-item'
import SelectBootFirmware from 'select-boot-firmware'
import SelectCoresPerSocket from 'select-cores-per-socket'
import TabButton from 'tab-button'
import Tooltip from 'tooltip'
import { error } from 'notification'
@@ -33,7 +34,6 @@ import {
addSubscriptions,
connectStore,
formatSize,
getCoresPerSocketPossibilities,
getVirtualizationModeLabel,
noop,
osFamily,
@@ -43,7 +43,6 @@ import {
every,
filter,
find,
includes,
isEmpty,
keyBy,
map,
@@ -249,80 +248,30 @@ class Vgpus extends Component {
}
class CoresPerSocket extends Component {
_getCoresPerSocketPossibilities = createSelector(
() => {
const { container } = this.props
if (container != null) {
return container.cpus.cores
}
},
() => this.props.vm.CPUs.number,
getCoresPerSocketPossibilities
)
_selectedValueIsNotInOptions = createSelector(
() => this.props.vm.coresPerSocket,
this._getCoresPerSocketPossibilities,
(selectedCoresPerSocket, options) =>
selectedCoresPerSocket !== undefined &&
!includes(options, selectedCoresPerSocket)
)
_onChange = event =>
editVm(this.props.vm, { coresPerSocket: getEventValue(event) || null })
_onChange = coresPerSocket => editVm(this.props.vm, { coresPerSocket })
render() {
const { container, vm } = this.props
const selectedCoresPerSocket = vm.coresPerSocket
const options = this._getCoresPerSocketPossibilities()
const { coresPerSocket, CPUs: cpus } = vm
return (
<form className='form-inline'>
<div>
{container != null ? (
<span>
<select
className='form-control'
onChange={this._onChange}
value={selectedCoresPerSocket || ''}
>
{_({ key: 'none' }, 'vmChooseCoresPerSocket', message => (
<option value=''>{message}</option>
))}
{this._selectedValueIsNotInOptions() &&
_(
{ key: 'incorrect' },
'vmCoresPerSocketIncorrectValue',
message => (
<option value={selectedCoresPerSocket}> {message}</option>
)
)}
{map(options, coresPerSocket =>
_(
{ key: coresPerSocket },
'vmCoresPerSocket',
{
nSockets: vm.CPUs.number / coresPerSocket,
nCores: coresPerSocket,
},
message => <option value={coresPerSocket}>{message}</option>
)
)}
</select>{' '}
{this._selectedValueIsNotInOptions() && (
<Tooltip content={_('vmCoresPerSocketIncorrectValueSolution')}>
<Icon icon='error' size='lg' />
</Tooltip>
)}
</span>
) : selectedCoresPerSocket != null ? (
_('vmCoresPerSocket', {
nSockets: vm.CPUs.number / selectedCoresPerSocket,
nCores: selectedCoresPerSocket,
<SelectCoresPerSocket
maxCores={container.cpus.cores}
maxVcpus={cpus.max}
onChange={this._onChange}
value={coresPerSocket}
/>
) : coresPerSocket !== undefined ? (
_('vmSocketsWithCoresPerSocket', {
nSockets: cpus.max / coresPerSocket,
nCores: coresPerSocket,
})
) : (
_('vmCoresPerSocketNone')
)}
</form>
</div>
)
}
}
@@ -400,14 +349,12 @@ const Acls = decorate([
computed: {
rawAcls: (_, { acls, vm }) => filter(acls, { object: vm }),
resolvedAcls: ({ rawAcls }, { users, groups }) => {
if (users === undefined && groups === undefined) {
if (users === undefined || groups === undefined) {
return []
}
return rawAcls.map(({ subject, ...acl }) => ({
...acl,
subject:
(users !== undefined && users[subject]) ||
(groups !== undefined && groups[subject]),
subject: defined(users[subject], groups[subject]),
}))
},
},

View File

@@ -7,9 +7,10 @@ import { Container, Row, Col } from 'grid'
import { isAdmin } from 'selectors'
import { NavLink, NavTabs } from 'nav'
import Update from './update'
import Licenses from './licenses'
import Notifications, { NotificationTag } from './notifications'
import Support from './support'
import Update from './update'
const Header = ({ isAdmin }) => (
<Container>
@@ -35,6 +36,11 @@ const Header = ({ isAdmin }) => (
<Icon icon='menu-notification' /> {_('notificationsPage')}{' '}
<NotificationTag />
</NavLink>
{isAdmin && (
<NavLink to='/xoa/support'>
<Icon icon='menu-support' /> {_('supportPage')}
</NavLink>
)}
</NavTabs>
</Col>
</Row>
@@ -42,9 +48,10 @@ const Header = ({ isAdmin }) => (
)
const Xoa = routes('xoa', {
update: Update,
licenses: Licenses,
notifications: Notifications,
support: Support,
update: Update,
})(
connectStore({
isAdmin,

View File

@@ -0,0 +1,61 @@
import _ from 'intl'
import ActionButton from 'action-button'
import AnsiUp from 'ansi_up'
import decorate from 'apply-decorators'
import React from 'react'
import { adminOnly, getXoaPlan } from 'utils'
import { Card, CardBlock, CardHeader } from 'card'
import { Container, Row, Col } from 'grid'
import { injectState, provideState } from 'reaclette'
import { checkXoa } from 'xo'
const ansiUp = new AnsiUp()
const COMMUNITY = getXoaPlan() === 'Community'
const Support = decorate([
adminOnly,
provideState({
initialState: () => ({ stdoutCheckXoa: '' }),
effects: {
initialize: async () => ({
stdoutCheckXoa: COMMUNITY ? '' : await checkXoa(),
}),
checkXoa: async () => ({ stdoutCheckXoa: await checkXoa() }),
},
}),
injectState,
({ effects, state: { stdoutCheckXoa } }) => (
<Container>
<Row>
<Col mediumSize={6}>
<Card>
<CardHeader>{_('xoaCheck')}</CardHeader>
{COMMUNITY ? (
<CardBlock>
<span className='text-info'>{_('checkXoaCommunity')}</span>
</CardBlock>
) : (
<CardBlock>
<ActionButton
btnStyle='success'
handler={effects.checkXoa}
icon='diagnosis'
>
{_('checkXoa')}
</ActionButton>
<hr />
<pre
dangerouslySetInnerHTML={{
__html: ansiUp.ansi_to_html(stdoutCheckXoa),
}}
/>
</CardBlock>
)}
</Card>
</Col>
</Row>
</Container>
),
])
export default Support

View File

@@ -11103,6 +11103,13 @@ promise-toolbox@^0.13.0:
dependencies:
make-error "^1.3.2"
promise-toolbox@^0.14.0:
version "0.14.0"
resolved "https://registry.yarnpkg.com/promise-toolbox/-/promise-toolbox-0.14.0.tgz#b2f8bd90fce6709b290b58fc06d89280375a98b4"
integrity sha512-VV5lXK4lXaPB9oBO50ope1qd0AKN8N3nK14jYvV9/qFmfZW2Px/bJjPZBniGjXcIJf6J5Y/coNgJtPHDyiUV/g==
dependencies:
make-error "^1.3.2"
promise-toolbox@^0.8.0:
version "0.8.3"
resolved "https://registry.yarnpkg.com/promise-toolbox/-/promise-toolbox-0.8.3.tgz#b757232a21d246d8702df50da6784932dd0f5348"