Compare commits

..

1 Commits

Author SHA1 Message Date
Julien Fontanet
4c7c9f9156 WiP: feat(fs/glob): basic glob implementation 2019-10-06 23:15:04 +02:00
28 changed files with 696 additions and 635 deletions

View File

@@ -0,0 +1,38 @@
import escapeRegExp from 'lodash/escapeRegExp'
const compileFragment = pattern =>
new RegExp(
`^${pattern
.split('*')
.map(escapeRegExp)
.join('[^]*')}$`
)
export function parseGlob(pattern) {
const parts = []
while (pattern.length !== 0) {
const i = pattern.indexOf('*')
if (i === -1) {
parts.push(pattern)
break
}
let fragmentStart = pattern.lastIndexOf('/', i)
if (fragmentStart === -1) {
fragmentStart = 0
} else {
parts.push(pattern.slice(0, fragmentStart))
++fragmentStart
}
let fragmentEnd = pattern.indexOf('/', i)
if (fragmentEnd === -1) {
fragmentEnd = pattern.length
}
parts.push(compileFragment(pattern.slice(fragmentStart, fragmentEnd)))
pattern = pattern.slice(fragmentEnd + 1)
}
return parts
}

View File

@@ -0,0 +1,12 @@
/* eslint-env jest */
import { parseGlob } from './_parseGlob'
describe('parseGlob', () => {
it.each([['foo/*/bar*baz/qux', ['foo', /^[^]*$/, /^bar[^]*baz$/, 'qux']]])(
'parse %j correctly',
(pattern, result) => {
expect(parseGlob(pattern)).toEqual(result)
}
)
})

View File

@@ -14,6 +14,7 @@ import { type Readable, type Writable } from 'stream'
import normalizePath from './_normalizePath'
import { createChecksumStream, validChecksumOfReadStream } from './checksum'
import { parseGlob } from './_parseGlob'
const { dirname } = path.posix
@@ -258,6 +259,12 @@ export default class RemoteHandlerAbstract {
)
}
// basic glob support, only `*` is supported
async glob(pattern) {
const parts = parseGlob(pattern)
// TODO
}
async list(
dir: string,
{

View File

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

View File

@@ -7,14 +7,11 @@
> 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.14.0",
"promise-toolbox": "^0.13.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, promisify } from 'promise-toolbox'
import { fromCallback, fromEvent } from 'promise-toolbox'
import { join } from 'path'
import { OvsdbClient } from './ovsdb-client'
@@ -47,8 +47,15 @@ export const configurationSchema = {
// =============================================================================
const fileWrite = promisify(writeFile)
const fileRead = promisify(readFile)
async function fileWrite(path, data) {
await fromCallback(writeFile, path, data)
}
async function fileRead(path) {
const result = await fromCallback(readFile, path)
return result
}
async function fileExists(path) {
try {
await fromCallback(access, path, constants.F_OK)
@@ -67,8 +74,8 @@ async function fileExists(path) {
// 2019-09-03
// Compatibility code, to be removed in 1 year.
const updateNetworkOtherConfig = network =>
Promise.all(
function updateNetworkOtherConfig(network) {
return Promise.all(
map(
{
'cross-pool-network-uuid': 'cross_pool_network_uuid',
@@ -94,107 +101,14 @@ const updateNetworkOtherConfig = network =>
}
)
)
}
// -----------------------------------------------------------------------------
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')
function createPassword() {
const chars =
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789?!'
return Array.from({ length: 16 }, _ => sample(chars)).join('')
}
// =============================================================================
@@ -263,7 +177,7 @@ class SDNController extends EventEmitter {
)
log.debug(`No default self-signed certificates exists, creating them`)
await generateCertificatesAndKey(certDirectory)
await this._generateCertificatesAndKey(certDirectory)
}
}
// TODO: verify certificates and create new certificates if needed
@@ -317,11 +231,7 @@ 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'
@@ -1520,6 +1430,119 @@ 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

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

View File

@@ -2,11 +2,15 @@
import defer from 'golike-defer'
import Xo from 'xo-lib'
import XoCollection from 'xo-collection'
import { defaultsDeep, find, forOwn, pick } from 'lodash'
import { find, forOwn } from 'lodash'
import { fromEvent } from 'promise-toolbox'
import config from './_config'
import { getDefaultName } from './_defaultValues'
const getDefaultCredentials = () => {
const { email, password } = config.xoConnection
return { email, password }
}
class XoConnection extends Xo {
constructor(opts) {
@@ -68,10 +72,7 @@ class XoConnection extends Xo {
}
@defer
async connect(
$defer,
credentials = pick(config.xoConnection, 'email', 'password')
) {
async connect($defer, credentials = getDefaultCredentials()) {
await this.open()
$defer.onFailure(() => this.close())
@@ -110,26 +111,9 @@ class XoConnection extends Xo {
}
async createTempBackupNgJob(params) {
// 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 })
const job = await this.call('backupNg.createJob', params)
this._tempResourceDisposers.push('backupNg.deleteJob', { id: job.id })
return job
}
async createTempNetwork(params) {
@@ -144,7 +128,7 @@ class XoConnection extends Xo {
async createTempVm(params) {
const id = await this.call('vm.create', {
name_label: getDefaultName(),
name_label: 'XO Test',
template: config.templates.templateWithoutDisks,
...params,
})

View File

@@ -1,6 +1,61 @@
// 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>,
@@ -37,6 +92,23 @@ 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>,
@@ -56,6 +128,22 @@ 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>,
@@ -69,7 +157,7 @@ Object {
}
`;
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 2`] = `
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 3`] = `
Object {
"end": Any<Number>,
"id": Any<String>,
@@ -80,7 +168,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 4`] = `
Object {
"data": Object {
"id": Any<String>,
@@ -95,19 +183,6 @@ 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>,
@@ -122,6 +197,19 @@ 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>,
@@ -136,19 +224,6 @@ 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>,
@@ -164,13 +239,12 @@ Object {
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 9`] = `
Object {
"data": Object {
"id": Any<String>,
"type": "VM",
},
"end": Any<Number>,
"id": Any<String>,
"message": Any<String>,
"result": Object {
"size": Any<Number>,
},
"start": Any<Number>,
"status": "success",
}
@@ -178,10 +252,15 @@ Object {
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>,
"message": "snapshot",
"result": Any<String>,
"jobId": Any<String>,
"message": "backup",
"scheduleId": Any<String>,
"start": Any<Number>,
"status": "success",
}
@@ -191,8 +270,7 @@ exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retent
Object {
"data": Object {
"id": Any<String>,
"isFull": false,
"type": "remote",
"type": "VM",
},
"end": Any<Number>,
"id": Any<String>,
@@ -206,29 +284,14 @@ exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retent
Object {
"end": Any<Number>,
"id": Any<String>,
"message": Any<String>,
"result": Object {
"size": Any<Number>,
},
"message": "snapshot",
"result": 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 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>,
@@ -243,6 +306,19 @@ Object {
}
`;
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 14`] = `
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 15`] = `
Object {
"end": Any<Number>,
@@ -257,6 +333,21 @@ 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>,
"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>,
@@ -269,7 +360,36 @@ Object {
}
`;
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 17`] = `
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>,
@@ -283,7 +403,7 @@ Object {
}
`;
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 18`] = `
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>,
@@ -294,47 +414,6 @@ 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 {
@@ -376,7 +455,65 @@ 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>,
@@ -387,7 +524,7 @@ Object {
}
`;
exports[`backupNg execute three times a rolling snapshot with 2 as retention & revert to an old state 2`] = `
exports[`backupNg execute three times a rolling snapshot with 2 as retention & revert to an old state 3`] = `
Object {
"data": Object {
"id": Any<String>,

View File

@@ -6,44 +6,20 @@ 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 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 DEFAULT_SCHEDULE = {
name: 'scheduleTest',
cron: '0 * * * * *',
}
const validateRootTask = (log, expected) =>
expect(log).toEqual({
const validateRootTask = (log, props) =>
expect(log).toMatchSnapshot({
end: expect.any(Number),
id: expect.any(String),
message: 'backup',
jobId: expect.any(String),
scheduleId: expect.any(String),
start: expect.any(Number),
...expected,
...props,
})
const validateVmTask = (task, vmId, props) => {
@@ -90,55 +66,88 @@ 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 jobInput = {
mode: 'full',
vms: {
id: config.vms.default,
},
}
const jobOutput = await xo.createTempBackupNgJob(jobInput)
validateBackupJob(jobInput, jobOutput)
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)
})
it('creates a new backup job with schedules', async () => {
const scheduleTempId = randomId()
const jobInput = {
mode: 'full',
const { id: jobId } = await xo.createTempBackupNgJob({
...defaultBackupNg,
schedules: {
[scheduleTempId]: getDefaultSchedule(),
[scheduleTempId]: DEFAULT_SCHEDULE,
},
settings: {
...defaultBackupNg.settings,
[scheduleTempId]: { snapshotRetention: 1 },
},
vms: {
id: config.vms.default,
},
}
const jobOutput = await xo.createTempBackupNgJob(jobInput)
validateBackupJob(
jobInput,
jobOutput,
await xo.getSchedule({ jobId: jobOutput.id })
)
})
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),
})
})
})
describe('.delete() :', () => {
it('deletes a backup job', async () => {
const scheduleTempId = randomId()
const jobId = await xo.call('backupNg.createJob', {
mode: 'full',
name: getDefaultName(),
vms: {
id: config.vms.default,
},
const { id: jobId } = await xo.call('backupNg.createJob', {
...defaultBackupNg,
schedules: {
[scheduleTempId]: getDefaultSchedule(),
[scheduleTempId]: DEFAULT_SCHEDULE,
},
settings: {
...defaultBackupNg.settings,
[scheduleTempId]: { snapshotRetention: 1 },
},
})
@@ -164,19 +173,16 @@ describe('backupNg', () => {
describe('.runJob() :', () => {
it('fails trying to run a backup job without schedule', async () => {
const { id } = await xo.createTempBackupNgJob({
vms: {
id: config.vms.default,
},
})
const { id } = await xo.createTempBackupNgJob(defaultBackupNg)
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]: getDefaultSchedule(),
[scheduleTempId]: DEFAULT_SCHEDULE,
},
settings: {
[scheduleTempId]: { snapshotRetention: 1 },
@@ -199,8 +205,9 @@ describe('backupNg', () => {
jest.setTimeout(7e3)
const scheduleTempId = randomId()
const { id: jobId } = await xo.createTempBackupNgJob({
...defaultBackupNg,
schedules: {
[scheduleTempId]: getDefaultSchedule(),
[scheduleTempId]: DEFAULT_SCHEDULE,
},
settings: {
[scheduleTempId]: { snapshotRetention: 1 },
@@ -224,23 +231,25 @@ 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 jobInput = {
const { id: jobId } = await xo.createTempBackupNgJob({
...defaultBackupNg,
schedules: {
[scheduleTempId]: getDefaultSchedule(),
[scheduleTempId]: DEFAULT_SCHEDULE,
},
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')
@@ -255,16 +264,12 @@ describe('backupNg', () => {
jobId,
scheduleId: schedule.id,
})
validateRootTask(log, {
data: {
mode: jobInput.mode,
reportWhen: jobInput.settings[''].reportWhen,
},
jobId,
jobName: jobInput.name,
scheduleId: schedule.id,
status: 'skipped',
expect(log).toMatchSnapshot({
end: expect.any(Number),
id: expect.any(String),
jobId: expect.any(String),
scheduleId: expect.any(String),
start: expect.any(Number),
})
expect(vmTask).toMatchSnapshot({
@@ -288,24 +293,22 @@ describe('backupNg', () => {
const scheduleTempId = randomId()
await xo.createTempServer(config.servers.default)
const { id: remoteId } = await xo.createTempRemote(config.remotes.default)
const jobInput = {
const { id: jobId } = await xo.createTempBackupNgJob({
...defaultBackupNg,
remotes: {
id: remoteId,
},
schedules: {
[scheduleTempId]: getDefaultSchedule(),
[scheduleTempId]: DEFAULT_SCHEDULE,
},
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')
@@ -321,15 +324,12 @@ describe('backupNg', () => {
scheduleId: schedule.id,
})
validateRootTask(log, {
data: {
mode: jobInput.mode,
reportWhen: jobInput.settings[''].reportWhen,
},
jobId,
jobName: jobInput.name,
scheduleId: schedule.id,
status: 'failure',
expect(log).toMatchSnapshot({
end: expect.any(Number),
id: expect.any(String),
jobId: expect.any(String),
scheduleId: expect.any(String),
start: expect.any(Number),
})
expect(task).toMatchSnapshot({
@@ -352,6 +352,7 @@ 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: [
@@ -364,18 +365,22 @@ describe('backupNg', () => {
})
const scheduleTempId = randomId()
const jobInput = {
const { id: jobId } = await xo.createTempBackupNgJob({
...defaultBackupNg,
vms: {
id: vm.id,
},
schedules: {
[scheduleTempId]: getDefaultSchedule(),
[scheduleTempId]: DEFAULT_SCHEDULE,
},
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')
@@ -415,15 +420,12 @@ describe('backupNg', () => {
scheduleId: schedule.id,
})
validateRootTask(log, {
data: {
mode: jobInput.mode,
reportWhen: jobInput.settings[''].reportWhen,
},
jobId,
jobName: jobInput.name,
scheduleId: schedule.id,
status: 'success',
expect(log).toMatchSnapshot({
end: expect.any(Number),
id: expect.any(String),
jobId: expect.any(String),
scheduleId: expect.any(String),
start: expect.any(Number),
})
const subTaskSnapshot = subTasks.find(
@@ -468,7 +470,7 @@ describe('backupNg', () => {
const exportRetention = 2
const fullInterval = 2
const scheduleTempId = randomId()
const jobInput = {
const { id: jobId } = await xo.createTempBackupNgJob({
mode: 'delta',
remotes: {
id: {
@@ -476,11 +478,13 @@ describe('backupNg', () => {
},
},
schedules: {
[scheduleTempId]: getDefaultSchedule(),
[scheduleTempId]: DEFAULT_SCHEDULE,
},
settings: {
'': {
bypassVdiChainsCheck: true,
fullInterval,
reportWhen: 'never',
},
[remoteId1]: { deleteFirst: true },
[scheduleTempId]: { exportRetention },
@@ -488,8 +492,7 @@ describe('backupNg', () => {
vms: {
id: vmToBackup,
},
}
const { id: jobId } = await xo.createTempBackupNgJob(jobInput)
})
const schedule = await xo.getSchedule({ jobId })
expect(typeof schedule).toBe('object')
@@ -512,12 +515,10 @@ describe('backupNg', () => {
backupLogs.forEach(({ tasks = [], ...log }, key) => {
validateRootTask(log, {
data: {
mode: jobInput.mode,
reportWhen: jobInput.settings[''].reportWhen,
mode: 'delta',
reportWhen: 'never',
},
jobId,
jobName: jobInput.name,
scheduleId: schedule.id,
message: 'backup',
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).then(({ id }) => id)
return this.createBackupNgJob(job, schedules)
}
createJob.permission = 'admin'

View File

@@ -1,7 +1,7 @@
{
"private": true,
"name": "xo-web",
"version": "5.50.3",
"version": "5.50.2",
"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'
vmSocketsWithCoresPerSocket: undefined,
vmCoresPerSocket: 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"
vmSocketsWithCoresPerSocket:
vmCoresPerSocket:
'{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"
vmSocketsWithCoresPerSocket:
vmCoresPerSocket:
'{nSockets, number} soket ve her sokette {nCores, number} çekirdek',
// Original text: "None"

View File

@@ -50,7 +50,6 @@ const messages = {
backupJobs: 'Backup jobs',
iscsiSessions:
'({ nSessions, number }) iSCSI session{nSessions, plural, one {} other {s}}',
requiresAdminPermissions: 'Requires admin permissions',
// ----- Modals -----
alertOk: 'OK',
@@ -100,7 +99,6 @@ const messages = {
updatePage: 'Updates',
licensesPage: 'Licenses',
notificationsPage: 'Notifications',
supportPage: 'Support',
settingsPage: 'Settings',
settingsServersPage: 'Servers',
settingsUsersPage: 'Users',
@@ -155,9 +153,6 @@ 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',
@@ -1159,16 +1154,12 @@ const messages = {
vmCpuLimitsLabel: 'CPU limits',
vmCpuTopology: 'Topology',
vmChooseCoresPerSocket: 'Default behavior',
vmSocketsWithCoresPerSocket:
vmCoresPerSocket:
'{nSockets, number} socket{nSockets, plural, one {} other {s}} with {nCores, number} core{nCores, plural, one {} other {s}} per socket',
vmCoresPerSocketNone: 'None',
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})',
vmCoresPerSocketIncorrectValue: 'Incorrect cores per socket value',
vmCoresPerSocketIncorrectValueSolution:
'Please change the selected value to fix it.',
vmHaDisabled: 'Disabled',
vmMemoryLimitsLabel: 'Memory limits (min/max)',
vmVgpu: 'vGPU',

View File

@@ -1,119 +0,0 @@
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,6 +449,26 @@ 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,7 +2918,3 @@ 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,10 +830,6 @@
@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: (_, { namespace, templates }) =>
filter(templates, ['other.xo:resource:namespace', namespace]),
installedTemplates: (_, { id, templates }) =>
filter(templates, ['other.xo:resource:xva:id', id]),
isTemplateInstalledOnAllPools: ({ installedTemplates }, { pools }) =>
installedTemplates.length > 0 &&
pools.every(

View File

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

View File

@@ -11,7 +11,6 @@ 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'
@@ -81,6 +80,7 @@ import {
addSubscriptions,
connectStore,
formatSize,
getCoresPerSocketPossibilities,
generateReadableRandomString,
resolveIds,
} from 'utils'
@@ -347,7 +347,6 @@ export default class NewVm extends BaseComponent {
this._replaceState(
{
bootAfterCreate: true,
coresPerSocket: undefined,
CPUs: '',
cpuCap: '',
cpuWeight: '',
@@ -501,8 +500,7 @@ export default class NewVm extends BaseComponent {
VIFs: _VIFs,
resourceSet: resourceSet && resourceSet.id,
// vm.set parameters
coresPerSocket:
state.coresPerSocket === null ? undefined : state.coresPerSocket,
coresPerSocket: state.coresPerSocket,
CPUs: state.CPUs,
cpuWeight: state.cpuWeight === '' ? null : state.cpuWeight,
cpuCap: state.cpuCap === '' ? null : state.cpuCap,
@@ -763,6 +761,17 @@ 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'
@@ -1070,17 +1079,7 @@ 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
@@ -1115,13 +1114,29 @@ export default class NewVm extends BaseComponent {
)}
</Item>
<Item label={_('vmCpuTopology')}>
{pool !== undefined ? (
selectCoresPerSocket
) : (
<Tooltip content={_('requiresAdminPermissions')}>
{selectCoresPerSocket}
</Tooltip>
)}
<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>
</Item>
</SectionContent>
</Section>

View File

@@ -9,7 +9,6 @@ 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'
@@ -34,6 +33,7 @@ import {
addSubscriptions,
connectStore,
formatSize,
getCoresPerSocketPossibilities,
getVirtualizationModeLabel,
noop,
osFamily,
@@ -43,6 +43,7 @@ import {
every,
filter,
find,
includes,
isEmpty,
keyBy,
map,
@@ -248,30 +249,80 @@ class Vgpus extends Component {
}
class CoresPerSocket extends Component {
_onChange = coresPerSocket => editVm(this.props.vm, { coresPerSocket })
_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 })
render() {
const { container, vm } = this.props
const { coresPerSocket, CPUs: cpus } = vm
const selectedCoresPerSocket = vm.coresPerSocket
const options = this._getCoresPerSocketPossibilities()
return (
<div>
<form className='form-inline'>
{container != null ? (
<SelectCoresPerSocket
maxCores={container.cpus.cores}
maxVcpus={cpus.max}
onChange={this._onChange}
value={coresPerSocket}
/>
) : coresPerSocket !== undefined ? (
_('vmSocketsWithCoresPerSocket', {
nSockets: cpus.max / coresPerSocket,
nCores: coresPerSocket,
<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,
})
) : (
_('vmCoresPerSocketNone')
)}
</div>
</form>
)
}
}
@@ -349,12 +400,14 @@ 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: defined(users[subject], groups[subject]),
subject:
(users !== undefined && users[subject]) ||
(groups !== undefined && groups[subject]),
}))
},
},

View File

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

View File

@@ -1,61 +0,0 @@
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,13 +11103,6 @@ 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"