Compare commits

...

10 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
25 changed files with 643 additions and 647 deletions

View File

@@ -4,12 +4,16 @@
### 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-server v5.51.0
- xo-web v5.51.0
- xo-web v5.50.3
## **5.39.0** (2019-09-30)

View File

@@ -7,11 +7,14 @@
> Users must be able to say: “Nice enhancement, I'm eager to test it”
[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”
- [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

View File

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

@@ -8,7 +8,7 @@ import { safeDateFormat } from '../utils'
export function createJob({ schedules, ...job }) {
job.userId = this.user.id
return this.createBackupNgJob(job, schedules)
return this.createBackupNgJob(job, schedules).then(({ id }) => id)
}
createJob.permission = 'admin'

View File

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

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

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

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

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

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

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

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