Compare commits

..

68 Commits

Author SHA1 Message Date
Julien Fontanet
58954b5dbc feat(xo-server): 5.71.0-0 2020-11-03 17:27:34 +01:00
Julien Fontanet
c0470140f1 feat(vhd-lib): 0.9.0-0 2020-11-03 17:27:25 +01:00
Julien Fontanet
b34b02e4b0 feat(@xen-orchestra/fs): 0.12.0-0 2020-11-03 17:26:32 +01:00
Nicolas Raynaud
72eb7aed3f update changelog 2020-10-30 17:40:46 +01:00
Nicolas Raynaud
ecacc3a9e5 add changelog entry 2020-10-30 17:09:52 +01:00
Nicolas Raynaud
9ce6a6eb09 add changelog entry 2020-10-30 16:42:23 +01:00
Nicolas Raynaud
2f55ee9028 Merge branch 'master' into nr-copy-file-range-merge 2020-10-30 15:41:17 +01:00
Rajaa.BARHTAOUI
18762dc624 feat: release 5.52.0 (#5352) 2020-10-30 14:23:00 +01:00
Julien Fontanet
5a828a6465 fix(CHANGELOG): "Packages to release* → *Released packages* 2020-10-30 11:48:53 +01:00
Nicolas Raynaud
d26c093fe1 Merge branch 'master' into nr-copy-file-range-merge 2020-10-30 11:46:34 +01:00
Julien Fontanet
eaa9f36478 feat: technical release (#5350) 2020-10-30 10:09:37 +01:00
Julien Fontanet
2b63134bcf feat(xo-web): add API debug mode
Adding `?debug` in URL (as search param), log all API calls in browser console.
2020-10-30 09:34:17 +01:00
Julien Fontanet
8dcff63aea fix(xo-server/{CR,DR}): add tags at VM creation (#5341)
Fixes https://xcp-ng.org/forum/topic/3285/cr-tag-ignored-kind-of-in-smart-backup

Necessary to avoid smart mode from matching the replicated VMs currently being imported.
2020-10-29 17:27:04 +01:00
Pierre Donias
c2777607be feat(xo-server-auth-ldap,xo-web): LDAP group manual sync (#5343)
See #1884

When the auth-ldap plugin is configured to synchronize LDAP groups, a group will
only be created in XO when a user that belongs to that group logs into XO. This
commit adds a button that allows an admin to force the synchronization of all
LDAP groups.
This can be useful if an admin wants to configure ACLs for those groups without
having to wait for a user of each group to log into XO at least once.
2020-10-29 17:13:38 +01:00
Julien Fontanet
9ba2b18fdb feat(xo-server/backup): dont backup migrating VMs
Fixes xoa-support#2953
2020-10-29 16:27:38 +01:00
Mathieu
4ebc10db6a fix(xo-web/formatSize): don't throw if the size is undefined (#5348) 2020-10-29 15:52:23 +01:00
Mathieu
610b6c7bb0 feat(xo-server,xo-web/host): get/set scheduler granularity (#5320)
Fixes #5291
2020-10-29 14:58:09 +01:00
Nicolas Raynaud
c953f34b01 Merge branch 'master' into nr-copy-file-range-merge 2020-10-29 13:46:25 +01:00
Nicolas Raynaud
69267d0d04 remove writeBlankRange()
(keeping it for another branch)
2020-10-29 03:16:47 +01:00
Nicolas Raynaud
3dee6f4247 some cleanup 2020-10-29 00:57:26 +01:00
Nicolas Raynaud
4b715d7d96 some cleanup 2020-10-29 00:52:55 +01:00
Nicolas Raynaud
f3088dbafd make fAllocateSyscall() optional 2020-10-29 00:44:58 +01:00
Rajaa.BARHTAOUI
357333c4e4 feat: technical release (#5347) 2020-10-28 16:07:21 +01:00
Rajaa.BARHTAOUI
723334a685 feat(xo-web/pool/network): add tooltip for Automatic column (#5345)
See xoa-support#2978
2020-10-28 11:50:49 +01:00
Mathieu
b2c218ff83 fix(xo-web/home): error notification when bulk VM snapshot fails (#5323) 2020-10-28 10:55:42 +01:00
badrAZ
adabd6966d feat(xo-server, xo-web/host): ability to install a certificate (#5332)
Fixes #5134
2020-10-27 16:36:26 +01:00
Mathieu
b3eb1270dd fix(xo-web/user): use default filter when custom filter is unset (#5321) 2020-10-27 15:25:38 +01:00
Mathieu
7659a195d3 fix(xo-web/vm): show snapshot button for self service user (#5324)
Introduced by a88798cc22
2020-10-27 15:17:33 +01:00
Julien Fontanet
8d2e23f4a8 fix(xo-server): remove TLS config to use native settings
See https://xcp-ng.org/forum/topic/3747/xen-orchestra-tls-minimum-version-requirement-how-to-set
2020-10-26 15:54:29 +01:00
Mathieu
539d7dab5d fix(xo-web/backup): add tooltip on running backup job button (#5325)
See https://xcp-ng.org/forum/topic/3687/xo-interface-and-usability/5
2020-10-26 15:22:35 +01:00
Pierre Donias
06d43cdb24 chore(xo-server-auth-ldap): remove unused dependency (#5344) 2020-10-26 09:32:04 +01:00
badrAZ
af7bcf19ab docs(users): add audit log documentation (#5342)
* docs: audit log
* docs: grammar edit for audit log info
* Fixes #5340

Co-authored-by: Jon Sands <fohdeesha@gmail.com>
2020-10-24 12:53:38 +02:00
Rajaa.BARHTAOUI
7ebeb37881 fix(xo-web#addSshKey): SSH key must be unique (#5329)
Mostly needed by the SortedTable, which uses the key as the item's ID.
2020-10-23 14:30:15 +02:00
Nicolas Raynaud
6bafdf3827 add console.log 2020-10-23 13:32:51 +02:00
Julien Fontanet
4911bbe3a2 chore: normalize packages 2020-10-23 10:21:52 +02:00
Julien Fontanet
e0b6ab3f8a feat(value-matcher/README): document patterns 2020-10-23 10:21:52 +02:00
Rajaa.BARHTAOUI
8736c2cf9a feat(xo-server,xo-web/VIF): require network ACLs to change VIF locking mode (#5283)
See xoa-support#2929
2020-10-23 10:18:29 +02:00
Rajaa.BARHTAOUI
d825c33b55 feat(xo-server,xo-web/networks): ability to change a VIF's network for self users (#5203)
Fixes #5020
2020-10-23 10:05:03 +02:00
Nicolas Raynaud
663d6b4607 add console.log 2020-10-23 01:04:20 +02:00
Nicolas Raynaud
eeb8049ff5 Merge branch 'master' into nr-copy-file-range-merge 2020-10-22 22:24:59 +02:00
Nicolas Raynaud
898d787659 try to get the tests running. 2020-10-22 22:15:47 +02:00
Nicolas Raynaud
57c320eaf6 start wiring copyFileRange() in the merge function 2020-10-22 16:35:09 +02:00
badrAZ
171ecaaf62 feat(xo-server, xo-web/host): display installed certificates (#5319)
See #5134
2020-10-21 14:36:55 +02:00
Julien Fontanet
5e6d5d4eb0 docs(xo-server): file restoration (#5336) 2020-10-20 15:35:16 +02:00
Rajaa.BARHTAOUI
3733a3c335 fix(xo-web): component names must be in PascalCase (#5322) 2020-10-20 15:19:59 +02:00
badrAZ
7fca6defd6 fix(xo-web, xo-server): fix host power state (#5288)
Fixes #4919
2020-10-20 10:01:48 +02:00
Julien Fontanet
2a270b399e fix(xo-server/getRemoteHandler): synchronize to avoid race conds during sync 2020-10-19 22:35:13 +02:00
Julien Fontanet
64109aee05 chore(vhd-lib/Vhd#_createBlock): assert block not already allocated 2020-10-19 22:33:58 +02:00
Julien Fontanet
e1d9395128 feat(vhd-lib/Vhd#_createBlock): dont initialize block
It's unnecessary, the block is written right after creation.
2020-10-19 22:30:57 +02:00
Julien Fontanet
32eec95c26 feat(@xen-orchestra/backups-cli): 0.2.1 2020-10-19 17:15:00 +02:00
badrAZ
f41cca45aa feat(audit-core, xo-server-audit): ability to delete a range of records (#5277) 2020-10-19 13:09:08 +02:00
Rajaa.BARHTAOUI
48eeab974c feat(xo-web/backup/restore): load backups in parallel (#5278)
See #5176
2020-10-19 11:47:33 +02:00
Nicolas Raynaud
64ec631b21 added writeBlankRange() and fSync() 2020-10-17 06:44:33 +02:00
Nicolas Raynaud
79626a3e38 fix s3.write() to work on new files 2020-10-17 06:42:39 +02:00
Nicolas Raynaud
b10c5ca6e8 added the copy_file_range loop, tested NFS 2020-10-17 02:24:16 +02:00
Nicolas Raynaud
9beb9c3ac5 begin introducing copyFileRange() method to remotes. 2020-10-16 13:30:58 +02:00
Nicolas Raynaud
d2b06f3ee7 try to use copy_file_range for VHD merge 2020-10-15 04:38:17 +02:00
Pierre Donias
eed44156ae feat: release 5.51.1 (#5328) 2020-10-14 16:12:37 +02:00
Pierre Donias
1177d9bdd8 feat: technical release (#5327) 2020-10-14 15:23:00 +02:00
badrAZ
d151a94285 feat(xo-server, xo-web/proxy): ability to update http proxy cfg (#5148) 2020-10-14 14:43:33 +02:00
badrAZ
a7fe6453ee feat(xo-server#_migrateVmWithStorageMotion): improve migration error (#5306)
Fixes #5282
2020-10-14 13:17:55 +02:00
Rajaa.BARHTAOUI
313eb136f4 fix(xo-web): some methods call throw 'not enough permissions' (#5303)
See #5285
2020-10-13 15:30:06 +02:00
Mathieu
98591ff83d fix(xo-server/deleteResourceSet): detach VMs from resource set (#5312)
Fixes #4797
2020-10-12 11:33:29 +02:00
Mathieu
0b9d78560b feat(xo-web/home): better sort of bulk VM migrate selector (#5308)
Fixes #4462
2020-10-12 10:22:24 +02:00
Rajaa.BARHTAOUI
32a930e598 feat(xo-web/vm/network): remove unused method (#5316)
Introduced by c4d96fbc49
2020-10-12 10:05:57 +02:00
Rajaa.BARHTAOUI
edd8512196 feat(xo-web/vm): hide backup tab for non-admin users (#5317)
Fixes #5309
2020-10-12 10:02:08 +02:00
Mathieu
7a6aec34ae fix(xo-web/ips): fix the range formatting (#5314)
Fixes #3170
2020-10-09 17:15:50 +02:00
badrAZ
009a0c5703 feat(xo-web/backup/overview): add link from backup job/schedule to corresponding logs (#5260)
Fixes #4564
2020-10-09 14:54:23 +02:00
79 changed files with 1597 additions and 376 deletions

View File

@@ -17,7 +17,7 @@ Installation of the [npm package](https://npmjs.org/package/@vates/coalesce-call
## Usage
```js
import { coalesceCalls } from 'coalesce-calls'
import { coalesceCalls } from '@vates/coalesce-calls'
const connect = coalesceCalls(async function () {
// async operation

View File

@@ -7,7 +7,7 @@
"type": "git",
"url": "https://github.com/vatesfr/xen-orchestra.git"
},
"version": "0.1.1",
"version": "0.2.0",
"engines": {
"node": ">=8.10"
},
@@ -30,6 +30,7 @@
"rimraf": "^3.0.0"
},
"dependencies": {
"@xen-orchestra/log": "^0.2.0",
"core-js": "^3.6.4",
"golike-defer": "^0.4.1",
"lodash": "^4.17.15",

View File

@@ -2,9 +2,12 @@
import 'core-js/features/symbol/async-iterator'
import assert from 'assert'
import createLogger from '@xen-orchestra/log'
import defer from 'golike-defer'
import hash from 'object-hash'
const log = createLogger('xo:audit-core')
export class Storage {
constructor() {
this._lock = Promise.resolve()
@@ -65,8 +68,17 @@ export class AuditCore {
@defer
async add($defer, subject, event, data) {
const time = Date.now()
$defer(await this._storage.acquireLock())
return this._addUnsafe({
data,
event,
subject,
time,
})
}
async _addUnsafe({ data, event, subject, time }) {
const storage = this._storage
$defer(await storage.acquireLock())
// delete "undefined" properties and normalize data with JSON.stringify
const record = JSON.parse(
@@ -139,4 +151,45 @@ export class AuditCore {
await this._storage.del(id)
}
}
@defer
async deleteRangeAndRewrite($defer, newest, oldest) {
assert.notStrictEqual(newest, undefined)
assert.notStrictEqual(oldest, undefined)
const storage = this._storage
$defer(await storage.acquireLock())
assert.notStrictEqual(await storage.get(newest), undefined)
const oldestRecord = await storage.get(oldest)
assert.notStrictEqual(oldestRecord, undefined)
const lastId = await storage.getLastId()
const recentRecords = []
for await (const record of this.getFrom(lastId)) {
if (record.id === newest) {
break
}
recentRecords.push(record)
}
for await (const record of this.getFrom(newest)) {
await storage.del(record.id)
if (record.id === oldest) {
break
}
}
await storage.setLastId(oldestRecord.previousId)
for (const record of recentRecords) {
try {
await this._addUnsafe(record)
await storage.del(record.id)
} catch (error) {
log.error(error)
}
}
}
}

View File

@@ -17,9 +17,10 @@ interface Record {
}
export class AuditCore {
constructor(storage: Storage) {}
public add(subject: any, event: string, data: any): Promise<Record> {}
public checkIntegrity(oldest: string, newest: string): Promise<number> {}
public getFrom(newest?: string): AsyncIterator {}
public deleteFrom(newest: string): Promise<void> {}
constructor(storage: Storage) { }
public add(subject: any, event: string, data: any): Promise<Record> { }
public checkIntegrity(oldest: string, newest: string): Promise<number> { }
public getFrom(newest?: string): AsyncIterator { }
public deleteFrom(newest: string): Promise<void> { }
public deleteRangeAndRewrite(newest: string, oldest: string): Promise<void> { }
}

View File

@@ -7,14 +7,14 @@
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
"dependencies": {
"@xen-orchestra/backups": "^0.1.1",
"@xen-orchestra/fs": "^0.11.1",
"@xen-orchestra/fs": "^0.12.0-0",
"filenamify": "^4.1.0",
"getopts": "^2.2.5",
"limit-concurrency-decorator": "^0.4.0",
"lodash": "^4.17.15",
"promise-toolbox": "^0.15.0",
"proper-lockfile": "^4.1.1",
"vhd-lib": "^0.7.2"
"vhd-lib": "^0.9.0-0"
},
"engines": {
"node": ">=7.10.1"
@@ -33,7 +33,7 @@
"scripts": {
"postversion": "npm publish --access public"
},
"version": "0.2.0",
"version": "0.2.1",
"license": "AGPL-3.0-or-later",
"author": {
"name": "Vates SAS",

View File

@@ -1,7 +1,7 @@
{
"private": false,
"name": "@xen-orchestra/fs",
"version": "0.11.1",
"version": "0.12.0-0",
"license": "AGPL-3.0-or-later",
"description": "The File System for Xen Orchestra backups.",
"keywords": [],
@@ -36,6 +36,7 @@
"readable-stream": "^3.0.6",
"through2": "^4.0.2",
"tmp": "^0.2.1",
"syscall": "^0.2.0",
"xo-remote-parser": "^0.6.0"
},
"devDependencies": {

View File

@@ -311,6 +311,28 @@ export default class RemoteHandlerAbstract {
)
}
/**
* Copy a range from one file to the other, kernel side, server side or with a reflink if possible.
*
* Slightly different from the copy_file_range linux system call:
* - offsets are mandatory (because some remote handlers don't have a current pointer for files)
* - flags is fixed to 0
* - will not return until copy is finished.
*
* @param fdIn read open file descriptor
* @param offsetIn either start offset in the source file
* @param fdOut write open file descriptor (not append!)
* @param offsetOut offset in the target file
* @param dataLen how long to copy
* @returns {Promise<void>}
*/
async copyFileRange(fdIn, offsetIn, fdOut, offsetOut, dataLen) {
// default implementation goes through the network
const buffer = Buffer.alloc(dataLen)
await this._read(fdIn, buffer, offsetIn)
await this._write(fdOut, buffer, offsetOut)
}
async readFile(
file: string,
{ flags = 'r' }: { flags?: string } = {}
@@ -357,27 +379,51 @@ export default class RemoteHandlerAbstract {
}
async test(): Promise<Object> {
const SIZE = 1024 * 1024 * 10
const testFileName = normalizePath(`${Date.now()}.test`)
const data = await fromCallback(randomBytes, SIZE)
const SIZE = 1024 * 1024 * 100
const now = Date.now()
const testFileName = normalizePath(`${now}.test`)
const testFileName2 = normalizePath(`${now}__dup.test`)
// get random ASCII for easy debug
const data = Buffer.from((await fromCallback(randomBytes, SIZE)).toString('base64'), 'ascii').slice(0, SIZE)
let step = 'write'
try {
const writeStart = process.hrtime()
await this._outputFile(testFileName, data, { flags: 'wx' })
const writeDuration = process.hrtime(writeStart)
let cloneDuration
const fd1 = await this.openFile(testFileName, 'r+')
try {
const fd2 = await this.openFile(testFileName2, 'wx')
try {
step = 'duplicate'
const cloneStart = process.hrtime()
await this.copyFileRange(fd1, 0, fd2, 0, data.byteLength)
cloneDuration = process.hrtime(cloneStart)
console.log('cloneDuration', cloneDuration)
} finally {
await this._closeFile(fd2)
}
} finally {
await this._closeFile(fd1)
}
step = 'read'
const readStart = process.hrtime()
const read = await this._readFile(testFileName, { flags: 'r' })
const readDuration = process.hrtime(readStart)
if (!data.equals(read)) {
throw new Error('output and input did not match')
}
const read2 = await this._readFile(testFileName2, { flags: 'r' })
if (!data.equals(read2)) {
throw new Error('duplicated and input did not match')
}
return {
success: true,
writeRate: computeRate(writeDuration, SIZE),
readRate: computeRate(readDuration, SIZE),
cloneDuration: computeRate(cloneDuration, SIZE),
}
} catch (error) {
return {
@@ -388,6 +434,7 @@ export default class RemoteHandlerAbstract {
}
} finally {
ignoreErrors.call(this._unlink(testFileName))
ignoreErrors.call(this._unlink(testFileName2))
}
}
@@ -428,7 +475,7 @@ export default class RemoteHandlerAbstract {
// Methods that can be called by private methods to avoid parallel limit on public methods
async __closeFile(fd: FileDescriptor): Promise<void> {
await timeout.call(this._closeFile(fd.fd), this._timeout)
await timeout.call(this._closeFile(fd), this._timeout)
}
async __mkdir(dir: string): Promise<void> {

View File

@@ -1,10 +1,39 @@
import df from '@sindresorhus/df'
import fs from 'fs-extra'
import { fromEvent } from 'promise-toolbox'
import { Syscall6 } from 'syscall'
import RemoteHandlerAbstract from './abstract'
/**
* @returns the number of byte effectively copied, needs to be called in a loop!
* @throws Error if the syscall returned -1
*/
function copyFileRangeSyscall(fdIn, offsetIn, fdOut, offsetOut, dataLen, flags = 0) {
// we are stuck on linux x86_64 because of int64 representation and syscall numbers
function wrapOffset(offsetIn) {
if (offsetIn == null)
return 0
const offsetInBuffer = new Uint32Array(2)
new DataView(offsetInBuffer.buffer).setBigUint64(0, BigInt(offsetIn), true)
return offsetInBuffer
}
// https://man7.org/linux/man-pages/man2/copy_file_range.2.html
const SYS_copy_file_range = 326
const [copied, _, errno] = Syscall6(SYS_copy_file_range, fdIn, wrapOffset(offsetIn), fdOut, wrapOffset(offsetOut), dataLen, flags)
if (copied === -1) {
throw new Error('Error no ' + errno)
}
return copied
}
export default class LocalHandler extends RemoteHandlerAbstract {
constructor(remote: any, options: Object = {}) {
super(remote, options)
this._canFallocate = true
}
get type() {
return 'file'
}
@@ -18,7 +47,7 @@ export default class LocalHandler extends RemoteHandlerAbstract {
}
async _closeFile(fd) {
return fs.close(fd)
return fs.close(fd.fd)
}
async _createReadStream(file, options) {
@@ -81,6 +110,26 @@ export default class LocalHandler extends RemoteHandlerAbstract {
return fs.open(this._getFilePath(path), flags)
}
/**
* Slightly different from the linux system call:
* - offsets are mandatory (because some remote handlers don't have a current pointer for files)
* - flags is fixed to 0
* - will not return until copy is finished.
*
* @param fdIn read open file descriptor
* @param offsetIn either start offset in the source file
* @param fdOut write open file descriptor (not append!)
* @param offsetOut offset in the target file
* @param dataLen how long to copy
* @returns {Promise<void>}
*/
async copyFileRange(fdIn, offsetIn, fdOut, offsetOut, dataLen) {
let copied = 0
do {
copied += await copyFileRangeSyscall(fdIn.fd, offsetIn + copied, fdOut.fd, offsetOut + copied, dataLen - copied)
} while (dataLen - copied > 0)
}
async _read(file, buffer, position) {
const needsClose = typeof file === 'string'
file = needsClose ? await fs.open(this._getFilePath(file), 'r') : file.fd

View File

@@ -138,14 +138,21 @@ export default class S3Handler extends RemoteHandlerAbstract {
file = file.fd
}
const uploadParams = this._createParams(file)
const fileSize = +(await this._s3.headObject(uploadParams).promise())
.ContentLength
let fileSize
try {
fileSize = +(await this._s3.headObject(uploadParams).promise()).ContentLength
} catch (e) {
if (e.code === 'NotFound') {
fileSize = 0
} else {
throw e
}
}
if (fileSize < MIN_PART_SIZE) {
const resultBuffer = Buffer.alloc(
Math.max(fileSize, position + buffer.length)
)
const fileContent = (await this._s3.getObject(uploadParams).promise())
.Body
const fileContent = fileSize ? (await this._s3.getObject(uploadParams).promise()).Body : Buffer.alloc(0)
fileContent.copy(resultBuffer)
buffer.copy(resultBuffer, position)
await this._s3

View File

@@ -2,6 +2,7 @@ import { parse } from 'xo-remote-parser'
import MountHandler from './_mount'
import normalizePath from './_normalizePath'
import { fromEvent } from "promise-toolbox"
export default class SmbMountHandler extends MountHandler {
constructor(remote, opts) {
@@ -22,4 +23,21 @@ export default class SmbMountHandler extends MountHandler {
get type() {
return 'smb'
}
// nraynaud: in some circumstances, renaming the file triggers a bug where we can't re-open it afterwards in SMB2
// SMB linux client Linux xoa 4.19.0-12-amd64 #1 SMP Debian 4.19.152-1 (2020-10-18) x86_64 GNU/Linux
// server Windows 10 Family Edition 1909 (v18363.1139)
async _outputStream(input, path, { checksum }) {
const output = await this.createOutputStream(path, { checksum })
try {
input.pipe(output)
await fromEvent(output, 'finish')
await output.checksumWritten
// $FlowFixMe
await input.task
} catch (error) {
await this.unlink(path, { checksum })
throw error
}
}
}

View File

@@ -1,6 +1,8 @@
<!-- DO NOT EDIT MANUALLY, THIS FILE HAS BEEN GENERATED -->
# @xen-orchestra/openflow [![Build Status](https://travis-ci.org/vatesfr/xen-orchestra.png?branch=master)](https://travis-ci.org/vatesfr/xen-orchestra)
# @xen-orchestra/openflow
[![Package Version](https://badgen.net/npm/v/@xen-orchestra/openflow)](https://npmjs.org/package/@xen-orchestra/openflow) ![License](https://badgen.net/npm/license/@xen-orchestra/openflow) [![PackagePhobia](https://badgen.net/bundlephobia/minzip/@xen-orchestra/openflow)](https://bundlephobia.com/result?p=@xen-orchestra/openflow) [![Node compatibility](https://badgen.net/npm/node/@xen-orchestra/openflow)](https://npmjs.org/package/@xen-orchestra/openflow)
> Pack and unpack OpenFlow messages
@@ -136,4 +138,4 @@ You may:
## License
© [Vates SAS](https://vates.fr)
[ISC](https://spdx.org/licenses/ISC) © [Vates SAS](https://vates.fr)

View File

@@ -8,7 +8,7 @@ const version = openflow.versions.openFlow11
const ofProtocol = openflow.protocols[version]
function parseOpenFlowMessages(socket) {
for await (const msg from parse(socket)) {
for await (const msg of parse(socket)) {
if (msg.header !== undefined) {
const ofType = msg.header.type
switch (ofType) {

View File

@@ -31,5 +31,10 @@
},
"dependencies": {
"@vates/read-chunk": "^0.1.0"
}
},
"author": {
"name": "Vates SAS",
"url": "https://vates.fr"
},
"license": "ISC"
}

View File

@@ -1,11 +1,81 @@
# ChangeLog
## **5.51.0** (2020-09-30)
## **5.52.0** (2020-10-30)
![Channel: latest](https://badgen.net/badge/channel/latest/yellow)
### Highlights
- [Host/Advanced] Display installed certificates with ability to install a new certificate [#5134](https://github.com/vatesfr/xen-orchestra/issues/5134) (PRs [#5319](https://github.com/vatesfr/xen-orchestra/pull/5319) [#5332](https://github.com/vatesfr/xen-orchestra/pull/5332))
- [VM/network] Allow Self Service users to change a VIF's network [#5020](https://github.com/vatesfr/xen-orchestra/issues/5020) (PR [#5203](https://github.com/vatesfr/xen-orchestra/pull/5203))
- [Host/Advanced] Ability to change the scheduler granularity. Only available on XCP-ng >= 8.2 [#5291](https://github.com/vatesfr/xen-orchestra/issues/5291) (PR [#5320](https://github.com/vatesfr/xen-orchestra/pull/5320))
### Enhancements
- [New SSH key] Show warning when the SSH key already exists (PR [#5329](https://github.com/vatesfr/xen-orchestra/pull/5329))
- [Pool/Network] Add a tooltip to the `Automatic` column (PR [#5345](https://github.com/vatesfr/xen-orchestra/pull/5345))
- [LDAP] Ability to force group synchronization [#1884](https://github.com/vatesfr/xen-orchestra/issues/1884) (PR [#5343](https://github.com/vatesfr/xen-orchestra/pull/5343))
### Bug fixes
- [Host] Fix power state stuck on busy after power off [#4919](https://github.com/vatesfr/xen-orchestra/issues/4919) (PR [#5288](https://github.com/vatesfr/xen-orchestra/pull/5288))
- [VM/Network] Don't allow users to change a VIF's locking mode if they don't have permissions on the network (PR [#5283](https://github.com/vatesfr/xen-orchestra/pull/5283))
- [Backup/overview] Add tooltip on the running backup job button (PR [#5325 ](https://github.com/vatesfr/xen-orchestra/pull/5325))
- [VM] Show snapshot button in toolbar for Self Service users (PR [#5324](https://github.com/vatesfr/xen-orchestra/pull/5324))
- [User] Fallback to default filter on resetting customized filter (PR [#5321](https://github.com/vatesfr/xen-orchestra/pull/5321))
- [Home] Show error notification when bulk VM snapshot fails (PR [#5323](https://github.com/vatesfr/xen-orchestra/pull/5323))
- [Backup] Skip VMs currently migrating
### Released packages
- xo-server-auth-ldap 0.10.0
- vhd-lib 0.8.0
- @xen-orchestra/audit-core 0.2.0
- xo-server-audit 0.9.0
- xo-web 5.74.0
- xo-server 5.70.0
## **5.51.1** (2020-10-14)
![Channel: stable](https://badgen.net/badge/channel/stable/green)
### Enhancements
- [Host/Advanced] Add the field `IOMMU` if it is defined (PR [#5294](https://github.com/vatesfr/xen-orchestra/pull/5294))
- [Backup logs/report] Hide merge task when no merge is done (PR [#5263](https://github.com/vatesfr/xen-orchestra/pull/5263))
- [New backup] Enable created schedules by default (PR [#5280](https://github.com/vatesfr/xen-orchestra/pull/5280))
- [Backup/overview] Link backup jobs/schedules to their corresponding logs [#4564](https://github.com/vatesfr/xen-orchestra/issues/4564) (PR [#5260](https://github.com/vatesfr/xen-orchestra/pull/5260))
- [VM] Hide backup tab for non-admin users [#5309](https://github.com/vatesfr/xen-orchestra/issues/5309) (PR [#5317](https://github.com/vatesfr/xen-orchestra/pull/5317))
- [VM/Bulk migrate] Sort hosts in the select so that the hosts on the same pool are shown first [#4462](https://github.com/vatesfr/xen-orchestra/issues/4462) (PR [#5308](https://github.com/vatesfr/xen-orchestra/pull/5308))
- [Proxy] Ability to update HTTP proxy configuration on XOA proxy (PR [#5148](https://github.com/vatesfr/xen-orchestra/pull/5148))
### Bug fixes
- [XOA/Notifications] Don't show expired notifications (PR [#5304](https://github.com/vatesfr/xen-orchestra/pull/5304))
- [Backup/S3] Fix secret key edit form [#5233](https://github.com/vatesfr/xen-orchestra/issues/5233) (PR[#5305](https://github.com/vatesfr/xen-orchestra/pull/5305))
- [New network] Remove the possibility of creating a network on a bond member interface (PR [#5262](https://github.com/vatesfr/xen-orchestra/pull/5262))
- [User] Fix custom filters not showing up when selecting a default filter for templates (PR [#5298](https://github.com/vatesfr/xen-orchestra/pull/5298))
- [Self/VDI migration] Fix hidden VDI after migration (PR [#5296](https://github.com/vatesfr/xen-orchestra/pull/5296))
- [Self/VDI migration] Fix `not enough permissions` error (PR [#5299](https://github.com/vatesfr/xen-orchestra/pull/5299))
- [Home] Hide backup filter for non-admin users [#5285](https://github.com/vatesfr/xen-orchestra/issues/5285) (PR [#5264](https://github.com/vatesfr/xen-orchestra/pull/5264))
- [Backup/S3] Fix request signature error [#5253](https://github.com/vatesfr/xen-orchestra/issues/5253) (PR[#5315](https://github.com/vatesfr/xen-orchestra/pull/5315))
- [SDN Controller] Fix tunnel traffic going on the wrong NIC: see https://xcp-ng.org/forum/topic/3544/mtu-problems-with-vxlan. (PR [#5281](https://github.com/vatesfr/xen-orchestra/pull/5281))
- [Settings/IP Pools] Fix some IP ranges being split into multiple ranges in the UI [#3170](https://github.com/vatesfr/xen-orchestra/issues/3170) (PR [#5314](https://github.com/vatesfr/xen-orchestra/pull/5314))
- [Self/Delete] Detach VMs and remove their ACLs on removing a resource set [#4797](https://github.com/vatesfr/xen-orchestra/issues/4797) (PR [#5312](https://github.com/vatesfr/xen-orchestra/pull/5312))
- Fix `not enough permissions` error when accessing some pages as a Self Service user (PR [#5303](https://github.com/vatesfr/xen-orchestra/pull/5303))
- [VM] Explicit error when VM migration failed due to unset default SR on destination pool [#5282](https://github.com/vatesfr/xen-orchestra/issues/5282) (PR [#5306](https://github.com/vatesfr/xen-orchestra/pull/5306))
### Released packages
- xo-server-sdn-controller 1.0.4
- xo-server-backup-reports 0.16.7
- xo-server 5.68.0
- xo-web 5.72.0
## **5.51.0** (2020-09-30)
### Highlights
- [Self/VDI migration] Ability to migrate VDIs to other SRs within a resource set [#5020](https://github.com/vatesfr/xen-orchestra/issues/5020) (PR [#5201](https://github.com/vatesfr/xen-orchestra/pull/5201))
- [LDAP] Ability to import LDAP groups to XO [#1884](https://github.com/vatesfr/xen-orchestra/issues/1884) (PR [#5279](https://github.com/vatesfr/xen-orchestra/pull/5279))
- [Tasks] Show XO objects linked to pending/finished tasks [#4275](https://github.com/vatesfr/xen-orchestra/issues/4275) (PR [#5267](https://github.com/vatesfr/xen-orchestra/pull/5267))
@@ -28,7 +98,7 @@
- [Import OVA] Improve import speed of embedded gzipped VMDK disks (PR [#5275](https://github.com/vatesfr/xen-orchestra/pull/5275))
- [Remotes] Fix editing bucket and directory for S3 remotes [#5233](https://github.com/vatesfr/xen-orchestra/issues/5233) (PR [5276](https://github.com/vatesfr/xen-orchestra/pull/5276))
### Packages to release
### Released packages
- xo-server-auth-ldap 0.9.0
- @xen-orchestra/fs 0.11.1
@@ -38,9 +108,7 @@
## **5.50.3** (2020-09-17)
![Channel: stable](https://badgen.net/badge/channel/stable/green)
### Packages to release
### Released packages
- xo-server-audit 0.8.0
@@ -56,7 +124,7 @@
- [New SR] Fix `Cannot read property 'trim' of undefined` error (PR [#5212](https://github.com/vatesfr/xen-orchestra/pull/5212))
- [Dashboard/Health] Fix suspended VDIs considered as orphans [#5248](https://github.com/vatesfr/xen-orchestra/issues/5248) (PR [#5249](https://github.com/vatesfr/xen-orchestra/pull/5249))
### Packages to release
### Released packages
- xo-server-audit 0.7.2
- xo-web 5.70.0
@@ -72,7 +140,7 @@
- [VM/Network] Fix TX checksumming [#5234](https://github.com/vatesfr/xen-orchestra/issues/5234)
### Packages to release
### Released packages
- xo-server-usage-report 0.9.0
- xo-server-audit 0.7.1
@@ -102,7 +170,7 @@
- [Audit] Obfuscate sensitive data in `user.changePassword` action's records [#5219](https://github.com/vatesfr/xen-orchestra/issues/5219) (PR [#5220](https://github.com/vatesfr/xen-orchestra/pull/5220))
- [SDN Controller] Fix `Cannot read property '$network' of undefined` error at the network creation (PR [#5217](https://github.com/vatesfr/xen-orchestra/pull/5217))
### Packages to release
### Released packages
- xo-server-audit 0.7.0
- xo-server-sdn-controller 1.0.3
@@ -119,7 +187,7 @@
- [Patches] Don't log errors related to missing patches listing (Previous fix in 5.48.3 was not working)
### Packages to release
### Released packages
- xo-server 5.64.1
- xo-server-sdn-controller 1.0.2

View File

@@ -7,24 +7,12 @@
> Users must be able to say: “Nice enhancement, I'm eager to test it”
- [Host/Advanced] Add the field `IOMMU` if it is defined (PR [#5294](https://github.com/vatesfr/xen-orchestra/pull/5294))
- [Backup logs/report] Hide merge task when no merge is done (PR [#5263](https://github.com/vatesfr/xen-orchestra/pull/5263))
- [New backup] Enable created schedules by default (PR [#5280](https://github.com/vatesfr/xen-orchestra/pull/5280))
- [backup] improve merge speed after backup when using SMB3.1.1 or NFS4.2 (PR [#5331](https://github.com/vatesfr/xen-orchestra/pull/5331))
### Bug fixes
> Users must be able to say: “I had this issue, happy to know it's fixed”
- [XOA/Notifications] Don't show expired notifications (PR [#5304](https://github.com/vatesfr/xen-orchestra/pull/5304))
- [Backup/S3] Fix secret key edit form [#5233](https://github.com/vatesfr/xen-orchestra/issues/5233) (PR[#5305](https://github.com/vatesfr/xen-orchestra/pull/5305))
- [New network] Remove the possibility of creating a network on a bond member interface (PR [#5262](https://github.com/vatesfr/xen-orchestra/pull/5262))
- [User] Fix custom filters not showing up when selecting a default filter for templates (PR [#5298](https://github.com/vatesfr/xen-orchestra/pull/5298))
- [Self/VDI migration] Fix hidden VDI after migration (PR [#5296](https://github.com/vatesfr/xen-orchestra/pull/5296))
- [Self/VDI migration] Fix `not enough permissions` error (PR [#5299](https://github.com/vatesfr/xen-orchestra/pull/5299))
- [Home] Hide backup filter for non-admin users [#5285](https://github.com/vatesfr/xen-orchestra/issues/5285) (PR [#5264](https://github.com/vatesfr/xen-orchestra/pull/5264))
- [Backup/S3] Fix request signature error [#5253](https://github.com/vatesfr/xen-orchestra/issues/5253) (PR[#5315](https://github.com/vatesfr/xen-orchestra/pull/5315))
- [SDN Controller] Fix tunnel traffic going on the wrong NIC: see https://xcp-ng.org/forum/topic/3544/mtu-problems-with-vxlan. (PR [#5281](https://github.com/vatesfr/xen-orchestra/pull/5281))
### Packages to release
> Packages will be released in the order they are here, therefore, they should
@@ -42,7 +30,7 @@
>
> In case of conflict, the highest (lowest in previous list) `$version` wins.
- xo-server-sdn-controller patch
- xo-server-backup-reports patch
- @xen-orchestra/fs minor
- vhd-lib minor
- xo-server minor
- xo-web minor

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View File

@@ -278,6 +278,34 @@ Now, your authorized users can create VMs with their SSH keys, grow template dis
![](https://pbs.twimg.com/media/CYMt2cJUkAAWCPg.png)
## Audit log
XO Audit Log is a plugin that records all important actions performed by users and provides the administrators an overview of each action. This gives them an idea of the users behavior regarding their infrastructure in order to track suspicious activities.
### How does it work?
XO Audit Log listens to important actions performed by users and stores them in the XOA database using the [hash chain structure](https://en.wikipedia.org/wiki/Hash_chain).
### Trustability of the records
Stored records are secured by:
- structure: records are chained using the hash chain structure which means that each record is linked to its parent in a cryptographically secure way. This structure prevents the alteration of old records.
- hash upload: the hash chain structure has limits, it does not protect from the rewrite of recent/all records. To reduce this risk, the Audit log plugin regularly uploads the last record hash to our database after checking the integrity of the whole record chain. This functionality keeps the records safe by notifying users in case of alteration of the records.
### Configuration
The recording of the users' actions is disabled by default. To enable it:
1. go into `settings/plugins`
2. expand the `audit` configuration
3. toggle active and save the configuration
![](./assets/audit_log_configuration.png)
Now, the audit plugin will record users' actions and upload the last record in the chain every day at **06:00 AM (UTC)**.
## Debugging
If you can't log in, please check the logs of `xo-server` while you attempt to connect. It will give you hints about the error encountered. You can do that with a `tail -f /var/log/syslog -n 100` on your XOA.

View File

@@ -40,7 +40,6 @@
"jest": {
"collectCoverage": true,
"moduleNameMapper": {
"^.": "./src",
"^(@vates/[^/]+)": "$1/src",
"^(@xen-orchestra/[^/]+)": "$1/src",
"^(value-matcher)": "$1/src",

View File

@@ -31,6 +31,53 @@ import { createPredicate } from 'value-matcher'
// ]
```
## Supported predicates
### `any`
The value must be strictly equal to the pattern.
```js
const predicate = createPredicate(42)
predicate(42) // true
predicate('foo') // false
```
### `{ [property: string]: Pattern }`
The value must be an object with all pattern properties matching.
```js
const predicate = createPredicate({ foo: 'bar' })
predicate({ foo: 'bar', baz: 42 }) // true
predicate('foo') // false
```
### `Pattern[]`
The value must be an array with some of its items matching each of pattern items.
```js
const predicate = createPredicate([42, { foo: 'bar' }])
predicate([false, { foo: 'bar', baz: 42 }, null, 42]) // true
predicate('foo') // false
```
### `{ __all: Pattern[] }`
All patterns must match.
### `{ __or: Pattern[] }`
At least one pattern must match.
### `{ __not: Pattern }`
The pattern must not match.
## Contributions
Contributions are _very_ welcomed, either on the documentation or on

View File

@@ -14,3 +14,50 @@ import { createPredicate } from 'value-matcher'
// { user: 'barney', age: 36, active: true },
// ]
```
## Supported predicates
### `any`
The value must be strictly equal to the pattern.
```js
const predicate = createPredicate(42)
predicate(42) // true
predicate('foo') // false
```
### `{ [property: string]: Pattern }`
The value must be an object with all pattern properties matching.
```js
const predicate = createPredicate({ foo: 'bar' })
predicate({ foo: 'bar', baz: 42 }) // true
predicate('foo') // false
```
### `Pattern[]`
The value must be an array with some of its items matching each of pattern items.
```js
const predicate = createPredicate([42, { foo: 'bar' }])
predicate([false, { foo: 'bar', baz: 42 }, null, 42]) // true
predicate('foo') // false
```
### `{ __all: Pattern[] }`
All patterns must match.
### `{ __or: Pattern[] }`
At least one pattern must match.
### `{ __not: Pattern }`
The pattern must not match.

View File

@@ -28,12 +28,12 @@
"node": ">=8.10"
},
"dependencies": {
"@xen-orchestra/fs": "^0.11.1",
"@xen-orchestra/fs": "^0.12.0-0",
"cli-progress": "^3.1.0",
"exec-promise": "^0.7.0",
"getopts": "^2.2.3",
"struct-fu": "^1.2.0",
"vhd-lib": "^0.7.2"
"vhd-lib": "^0.9.0-0"
},
"devDependencies": {
"@babel/cli": "^7.0.0",

View File

@@ -11,7 +11,7 @@ import { pFromCallback } from 'promise-toolbox'
import { pipeline } from 'readable-stream'
import { randomBytes } from 'crypto'
import Vhd, { chainVhd, createSyntheticStream, mergeVhd as vhdMerge } from './'
import Vhd, { chainVhd, createSyntheticStream, mergeVhd as vhdMerge } from './src/index'
import { SECTOR_SIZE } from './src/_constants'

View File

@@ -1,7 +1,7 @@
{
"private": false,
"name": "vhd-lib",
"version": "0.7.2",
"version": "0.9.0-0",
"license": "AGPL-3.0-or-later",
"description": "Primitives for VHD file handling",
"keywords": [],
@@ -36,7 +36,7 @@
"@babel/core": "^7.0.0",
"@babel/preset-env": "^7.0.0",
"@babel/preset-flow": "^7.0.0",
"@xen-orchestra/fs": "^0.11.1",
"@xen-orchestra/fs": "^0.12.0-0",
"babel-plugin-lodash": "^3.3.2",
"cross-env": "^7.0.2",
"execa": "^4.0.2",

View File

@@ -17,6 +17,8 @@ export default concurrency(2)(async function merge(
childPath,
{ onProgress = noop } = {}
) {
// merges blocks
let mergedDataSize = 0
const parentFd = await parentHandler.openFile(parentPath, 'r+')
try {
const parentVhd = new Vhd(parentHandler, parentFd)
@@ -68,14 +70,12 @@ export default concurrency(2)(async function merge(
onProgress({ total: nBlocks, done: 0 })
// merges blocks
let mergedDataSize = 0
for (let i = 0, block = firstBlock; i < nBlocks; ++i, ++block) {
while (!childVhd.containsBlock(block)) {
++block
}
mergedDataSize += await parentVhd.coalesceBlock(childVhd, block)
mergedDataSize += await parentVhd.coalesceBlock(childVhd, block,childFd, parentFd )
onProgress({
total: nBlocks,
done: i + 1,
@@ -94,12 +94,11 @@ export default concurrency(2)(async function merge(
// necessary to update values and to recreate the footer after block
// creation
await parentVhd.writeFooter()
return mergedDataSize
} finally {
await childHandler.closeFile(childFd)
}
} finally {
await parentHandler.closeFile(parentFd)
}
return mergedDataSize
})

View File

@@ -199,30 +199,35 @@ export default class Vhd {
}
// return the first sector (bitmap) of a block
_getBatEntry(block) {
const i = block * 4
_getBatEntry(blockId) {
const i = blockId * 4
const { blockTable } = this
return i < blockTable.length ? blockTable.readUInt32BE(i) : BLOCK_UNUSED
}
_readBlock(blockId, onlyBitmap = false) {
// returns actual byt offset in the file or null
_getBlockOffsetBytes(blockId) {
const blockAddr = this._getBatEntry(blockId)
if (blockAddr === BLOCK_UNUSED) {
return blockAddr === BLOCK_UNUSED ? null : sectorsToBytes(blockAddr)
}
_readBlock(blockId, onlyBitmap = false) {
const blockAddr = this._getBlockOffsetBytes(blockId)
if (blockAddr === null) {
throw new Error(`no such block ${blockId}`)
}
return this._read(
sectorsToBytes(blockAddr),
return this._read(blockAddr,
onlyBitmap ? this.bitmapSize : this.fullBlockSize
).then(buf =>
onlyBitmap
? { id: blockId, bitmap: buf }
: {
id: blockId,
bitmap: buf.slice(0, this.bitmapSize),
data: buf.slice(this.bitmapSize),
buffer: buf,
}
id: blockId,
bitmap: buf.slice(0, this.bitmapSize),
data: buf.slice(this.bitmapSize),
buffer: buf,
}
)
}
@@ -309,15 +314,14 @@ export default class Vhd {
// Make a new empty block at vhd end.
// Update block allocation table in context and in file.
async _createBlock(blockId) {
async _createBlock(blockId, fullBlock = Buffer.alloc(this.fullBlockSize)) {
const blockAddr = Math.ceil(this._getEndOfData() / SECTOR_SIZE)
debug(`create block ${blockId} at ${blockAddr}`)
await Promise.all([
// Write an empty block and addr in vhd file.
this._write(Buffer.alloc(this.fullBlockSize), sectorsToBytes(blockAddr)),
this._write(fullBlock, sectorsToBytes(blockAddr)),
this._setBatEntry(blockId, blockAddr),
])
@@ -342,12 +346,13 @@ export default class Vhd {
await this._write(bitmap, sectorsToBytes(blockAddr))
}
async _writeEntireBlock(block) {
let blockAddr = this._getBatEntry(block.id)
async _getAddressOrAllocate(blockId) {
const blockAddr = this._getBlockOffsetBytes(blockId)
return blockAddr === null ? await this._createBlock(blockId) : blockAddr
}
if (blockAddr === BLOCK_UNUSED) {
blockAddr = await this._createBlock(block.id)
}
async _writeEntireBlock(block) {
const blockAddr = this._getAddressOrAllocate(block.id)
await this._write(block.buffer, sectorsToBytes(blockAddr))
}
@@ -381,15 +386,18 @@ export default class Vhd {
)
}
async coalesceBlock(child, blockId) {
const block = await child._readBlock(blockId)
const { bitmap, data } = block
async coalesceBlock(child, blockId, childFd, parentFd) {
const childBlockAddress = child._getBlockOffsetBytes(blockId)
const bitmap = (await child._readBlock(blockId, true)).bitmap
debug(`coalesceBlock block=${blockId}`)
// For each sector of block data...
const { sectorsPerBlock } = child
// lazily loaded
let parentBitmap = null
// lazily loaded
let childBlock = null
for (let i = 0; i < sectorsPerBlock; i++) {
// If no changes on one sector, skip.
if (!mapTestBit(bitmap, i)) {
@@ -407,19 +415,22 @@ export default class Vhd {
const isFullBlock = i === 0 && endSector === sectorsPerBlock
if (isFullBlock) {
await this._writeEntireBlock(block)
await this._handler.copyFileRange(childFd, childBlockAddress, parentFd, await this._getAddressOrAllocate(blockId), this.fullBlockSize)
} else {
if (parentBitmap === null) {
parentBitmap = (await this._readBlock(blockId, true)).bitmap
}
await this._writeBlockSectors(block, i, endSector, parentBitmap)
if (childBlock === null) {
childBlock = await child._readBlock(blockId)
}
await this._writeBlockSectors(childBlock, i, endSector, parentBitmap)
}
i = endSector
}
// Return the merged data size
return data.length
return this.fullBlockSize - this.bitmapSize
}
// Write a context footer. (At the end and beginning of a vhd file.)
@@ -485,7 +496,7 @@ export default class Vhd {
)
const endInBuffer = Math.min(
((currentBlock + 1) * this.sectorsPerBlock - offsetSectors) *
SECTOR_SIZE,
SECTOR_SIZE,
buffer.length
)
let inputBuffer

View File

@@ -8,6 +8,6 @@
"promise-toolbox": "^0.13.0",
"readable-stream": "^3.1.1",
"throttle": "^1.0.3",
"vhd-lib": "^0.7.2"
"vhd-lib": "^0.9.0-0"
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "xo-server-audit",
"version": "0.8.0",
"version": "0.9.0",
"license": "AGPL-3.0-or-later",
"description": "Audit plugin for XO-Server",
"keywords": [
@@ -49,7 +49,7 @@
"prepublishOnly": "yarn run build"
},
"dependencies": {
"@xen-orchestra/audit-core": "^0.1.1",
"@xen-orchestra/audit-core": "^0.2.0",
"@xen-orchestra/cron": "^1.0.6",
"@xen-orchestra/log": "^0.2.0",
"async-iterator-to-stream": "^1.1.0",

View File

@@ -20,6 +20,7 @@ const DEFAULT_BLOCKED_LIST = {
'acl.getCurrentPermissions': true,
'audit.checkIntegrity': true,
'audit.clean': true,
'audit.deleteRange': true,
'audit.generateFingerprint': true,
'audit.getRecords': true,
'backup.list': true,
@@ -238,11 +239,21 @@ class AuditXoPlugin {
clean.permission = 'admin'
clean.description = 'Clean audit database'
const deleteRange = this._deleteRangeAndRewrite.bind(this)
deleteRange.description =
'Delete a range of records and rewrite the records chain'
deleteRange.permission = 'admin'
deleteRange.params = {
newest: { type: 'string' },
oldest: { type: 'string', optional: true },
}
cleaners.push(
this._xo.addApiMethods({
audit: {
checkIntegrity,
clean,
deleteRange,
exportRecords,
generateFingerprint,
getRecords,
@@ -428,6 +439,13 @@ class AuditXoPlugin {
throw error
}
}
async _deleteRangeAndRewrite({ newest, oldest = newest }) {
await this._auditCore.deleteRangeAndRewrite(newest, oldest)
if (this._uploadLastHashJob !== undefined) {
await this._uploadLastHash()
}
}
}
AuditXoPlugin.prototype._getRecordsStream = asyncIteratorToStream(

View File

@@ -1,6 +1,6 @@
{
"name": "xo-server-auth-ldap",
"version": "0.9.0",
"version": "0.10.0",
"license": "AGPL-3.0-or-later",
"description": "LDAP authentication plugin for XO-Server",
"keywords": [
@@ -34,7 +34,6 @@
"node": ">=10"
},
"dependencies": {
"@babel/plugin-proposal-optional-chaining": "^7.11.0",
"exec-promise": "^0.7.0",
"inquirer": "^7.0.0",
"ldapts": "^2.2.1",

View File

@@ -250,10 +250,16 @@ class AuthLdap {
load() {
this._xo.registerAuthenticationProvider(this._authenticate)
this._removeApiMethods = this._xo.addApiMethods({
ldap: {
synchronizeGroups: () => this._synchronizeGroups(),
},
})
}
unload() {
this._xo.unregisterAuthenticationProvider(this._authenticate)
this._removeApiMethods()
}
test({ username, password }) {
@@ -330,7 +336,7 @@ class AuthLdap {
user,
entry[groupsConfig.membersMapping.userAttribute]
)
} catch(error) {
} catch (error) {
logger(`failed to synchronize groups: ${error.message}`)
}
}
@@ -382,7 +388,7 @@ class AuthLdap {
})
const xoUsers =
user !== undefined &&
user === undefined &&
(await this._xo.getAllUsers()).filter(
user =>
user.authProviders !== undefined && 'ldap' in user.authProviders

View File

@@ -1,6 +1,6 @@
{
"name": "xo-server-backup-reports",
"version": "0.16.6",
"version": "0.16.7",
"license": "AGPL-3.0-or-later",
"description": "Backup reports plugin for XO-Server",
"keywords": [

View File

@@ -16,7 +16,7 @@
"predev": "yarn run prebuild",
"prepublishOnly": "yarn run build"
},
"version": "1.0.3",
"version": "1.0.4",
"engines": {
"node": ">=8.10"
},

View File

@@ -1,4 +1,3 @@
import assert from 'assert'
import createLogger from '@xen-orchestra/log'
import ipaddr from 'ipaddr.js'
import openflow from '@xen-orchestra/openflow'
@@ -117,7 +116,7 @@ export class OpenFlowChannel extends EventEmitter {
const { dlType, nwProto } = dlAndNwProtocolFromString(protocol)
const mac = vif.MAC
await this._coalesceConnect(vif.network)
await this._coalesceConnect()
if (direction.includes('from')) {
this._addFlow(
{
@@ -177,9 +176,6 @@ export class OpenFlowChannel extends EventEmitter {
instructions
)
}
await this._tlsHelper.closeSocket(this._socket)
delete this._socket
}
async deleteRule(vif, protocol, port, ipRange, direction, ofport) {
@@ -194,7 +190,7 @@ export class OpenFlowChannel extends EventEmitter {
const { dlType, nwProto } = dlAndNwProtocolFromString(protocol)
const mac = vif.MAC
await this._coalesceConnect(vif.network)
await this._coalesceConnect()
if (direction.includes('from')) {
this._removeFlows({
type: ofProtocol.matchType.standard,
@@ -239,9 +235,6 @@ export class OpenFlowChannel extends EventEmitter {
tp_src: port,
})
}
await this._tlsHelper.closeSocket(this._socket)
delete this._socket
}
// ===========================================================================
@@ -376,15 +369,15 @@ export class OpenFlowChannel extends EventEmitter {
}
}
async _connect(networkUuid) {
async _connect() {
if (this._socket !== undefined) {
return
}
const pif = this.host.$PIFs.find(pif => pif?.network === networkUuid)
assert(pif !== undefined, 'No PIF available')
this._socket = await this._tlsHelper.connect(pif.IP, OPENFLOW_PORT)
this._socket = await this._tlsHelper.connect(
this.host.address,
OPENFLOW_PORT
)
const deleteSocket = () => {
this._socket = undefined

View File

@@ -51,18 +51,4 @@ export class TlsHelper {
return socket
}
async closeSocket(socket) {
socket.end()
try {
await fromEvent(socket, 'finish')
} catch (error) {
log.error('TLS socket closure failed', {
error,
socket,
})
}
log.debug('TLS socket closed')
}
}

View File

@@ -99,18 +99,6 @@ port = 80
# These options are applied to all listen entries.
[http.listenOptions]
# Ciphers to use.
#
# These are the default ciphers in Node 4.2.6, we are setting
# them explicitly for older Node versions.
ciphers = 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384:DHE-RSA-AES256-SHA384:ECDHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA256:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!SRP:!CAMELLIA'
# Tell Node to respect the cipher order.
honorCipherOrder = true
# Specify to use at least TLSv1.1.
# See: https:#github.com/certsimple/minimum-tls-version
secureOptions = 117440512
[http.mounts]
'/' = '../xo-web/dist'

View File

@@ -0,0 +1,85 @@
> This file describe how XO extract files from a VHD file.
## Expose VHD disk as block device
```
> mkdir /tmp/vhd-mount
> vhdimount 20201008T074128Z.vhd /tmp/vhd-mount
> ls /tmp/vhd-mount
vhdi1
vhdi2
```
> Each device (`vhdi1`, …) corresponds to a version of the disks in the used chain, ie one device per differencing VHDs plus one for the dynamic VHD.
When device no longer necessary:
```
> fusermount -uz /tmp/vhd-mount
> rmdir /tmp/vhd-mount
```
## List available partitions
```
> partx --bytes --output=NR,START,SIZE,NAME,UUID,TYPE --pairs /tmp/vhd-mount/vhdi2
NR="1" START="2048" SIZE="254803968" NAME="" UUID="c8d70417-01" TYPE="0x83"
NR="2" START="501758" SIZE="8331985920" NAME="" UUID="c8d70417-02" TYPE="0x5"
NR="5" START="501760" SIZE="8331984896" NAME="" UUID="c8d70417-05" TYPE="0x8e"
```
## Mount LVM physical volume (partition type equals to `0x8e`)
```
> losetup -o $(($START * 512)) --show -f /tmp/vhd-mount/vhdi2
/dev/loop0
> pvscan --cache /dev/loop0
```
When logical volumes no longer necessary:
```
> pvs --noheading --nosuffix --nameprefixes --unbuffered --units b -o vg_name /dev/loop0
LVM2_VG_NAME='debian-vg'
> vgchange -an debian-vg
> losetup -d /dev/loop0
```
## List available LVM logical volumes
```
> pvs --noheading --nosuffix --nameprefixes --unbuffered --units b -o lv_name,lv_path,lv_size,vg_name /dev/loop0
LVM2_LV_NAME='root' LVM2_LV_PATH='/dev/debian-vg/root' LVM2_LV_SIZE='7935623168' LVM2_VG_NAME='debian-vg'
LVM2_LV_NAME='swap_1' LVM2_LV_PATH='/dev/debian-vg/swap_1' LVM2_LV_SIZE='394264576' LVM2_VG_NAME='debian-vg'
```
## Mount LVM logical volume
```
> vgchange -ay debian-vg
> lvs --noheading --nosuffix --nameprefixes --unbuffered --units b -o lv_name,lv_path
LVM2_LV_NAME='root' LVM2_LV_PATH='/dev/debian-vg/root'
LVM2_LV_NAME='swap_1' LVM2_LV_PATH='/dev/debian-vg/swap_1'
```
When logical volume no longer necessary:
```
> vgchange -ay debian-vg
```
## Mount block device
```
> mkdir /tmp/block-mount
> mount --options=loop,ro,$((START * 512)) --source=/tmp/vhd-mount/vhdi2 --target=/tmp/block-mount
> ls /tmp/block-mount
bin boot dev etc home lib lib64 lost+found media mnt opt proc root run sbin srv sys @System.solv tmp usr var
```
When mountpoint no longer necessary:
```
> umount --lazy /tmp/block-mount
> rmdir /tmp/block-mount
```

View File

@@ -1,7 +1,7 @@
{
"private": true,
"name": "xo-server",
"version": "5.67.0",
"version": "5.71.0-0",
"license": "AGPL-3.0-or-later",
"description": "Server part of Xen-Orchestra",
"keywords": [
@@ -38,7 +38,7 @@
"@xen-orchestra/cron": "^1.0.6",
"@xen-orchestra/defined": "^0.0.0",
"@xen-orchestra/emit-async": "^0.0.0",
"@xen-orchestra/fs": "^0.11.1",
"@xen-orchestra/fs": "^0.12.0-0",
"@xen-orchestra/log": "^0.2.0",
"@xen-orchestra/mixin": "^0.0.0",
"@xen-orchestra/self-signed": "^0.1.0",
@@ -130,7 +130,7 @@
"unzipper": "^0.10.5",
"uuid": "^3.0.1",
"value-matcher": "^0.2.0",
"vhd-lib": "^0.7.2",
"vhd-lib": "^0.9.0-0",
"ws": "^7.1.2",
"xdg-basedir": "^4.0.0",
"xen-api": "^0.29.0",

View File

@@ -2,6 +2,58 @@ import { format } from 'json-rpc-peer'
// ===================================================================
export async function getSchedulerGranularity({ host }) {
try {
return await this.getXapi(host).getField(
'host',
host._xapiRef,
'sched_gran'
)
} catch (e) {
// This method is supported on XCP-ng >= 8.2 only.
if (e.code === 'MESSAGE_METHOD_UNKNOWN') {
return null
}
throw e
}
}
getSchedulerGranularity.description = 'get the scheduler granularity of a host'
getSchedulerGranularity.params = {
id: { type: 'string' },
}
getSchedulerGranularity.resolve = {
host: ['id', 'host', 'view'],
}
// ===================================================================
export async function setSchedulerGranularity({ host, schedulerGranularity }) {
await this.getXapi(host).setField(
'host',
host._xapiRef,
'sched_gran',
schedulerGranularity
)
}
setSchedulerGranularity.description = 'set scheduler granularity of a host'
setSchedulerGranularity.params = {
id: { type: 'string' },
schedulerGranularity: {
enum: ['cpu', 'core', 'socket'],
},
}
setSchedulerGranularity.resolve = {
host: ['id', 'host', 'operate'],
}
// ===================================================================
export async function set({
host,
@@ -314,3 +366,23 @@ scanPifs.params = {
scanPifs.resolve = {
host: ['id', 'host', 'administrate'],
}
// -------------------------------------------------------------------
export async function installCertificate({ host, ...props }) {
host = this.getXapiObject(host)
await host.$xapi.installCertificateOnHost(host.$id, props)
}
installCertificate.description = 'Install a certificate on a host'
installCertificate.params = {
id: { type: 'string' },
certificate: { type: 'string' },
chain: { type: 'string', optional: true },
privateKey: { type: 'string' },
}
installCertificate.resolve = {
host: ['id', 'host', 'administrate'],
}

View File

@@ -149,7 +149,7 @@ deploy.resolve = {
}
export function upgradeAppliance({ id }) {
return this.upgradeProxyAppliance(id)
return this.updateProxyAppliance(id, { upgrade: true })
}
upgradeAppliance.permission = 'admin'
@@ -192,3 +192,18 @@ checkHealth.params = {
type: 'string',
},
}
export function updateApplianceSettings({ id, ...props }) {
return this.updateProxyAppliance(id, props)
}
updateApplianceSettings.permission = 'admin'
updateApplianceSettings.params = {
id: {
type: 'string',
},
httpProxy: {
type: ['string', 'null'],
optional: true,
},
}

View File

@@ -73,6 +73,7 @@ export async function set({
mac,
network,
rateLimit,
resourceSet,
txChecksumming,
}) {
const oldIpAddresses = vif.allowedIpv4Addresses.concat(
@@ -85,12 +86,29 @@ export async function set({
push.apply(newIpAddresses, allowedIpv6Addresses || vif.allowedIpv6Addresses)
}
if (lockingMode !== undefined) {
await this.checkPermissions(this.user.id, [
[network?.id ?? vif.$network, 'operate'],
])
}
if (network || mac) {
const networkId = network?.id
if (networkId !== undefined && this.user.permission !== 'admin') {
if (resourceSet !== undefined) {
await this.checkResourceSetConstraints(resourceSet, this.user.id, [
networkId,
])
} else {
await this.checkPermissions(this.user.id, [[networkId, 'operate']])
}
}
const xapi = this.getXapi(vif)
const vm = xapi.getObject(vif.$VM)
mac == null && (mac = vif.MAC)
network = xapi.getObject((network && network.id) || vif.$network)
network = xapi.getObject(networkId ?? vif.$network)
attached == null && (attached = vif.attached)
await this.allocIpAddresses(vif.id, null, oldIpAddresses)
@@ -156,6 +174,7 @@ set.params = {
optional: true,
type: ['number', 'null'],
},
resourceSet: { type: 'string', optional: true },
txChecksumming: {
type: 'boolean',
optional: true,
@@ -164,5 +183,5 @@ set.params = {
set.resolve = {
vif: ['id', 'VIF', 'operate'],
network: ['network', 'network', 'operate'],
network: ['network', 'network', false],
}

View File

@@ -148,7 +148,9 @@ const TRANSFORMS = {
// -----------------------------------------------------------------
host(obj) {
host(obj, dependents) {
dependents[obj.metrics] = obj.$id
const {
$metrics: metrics,
other_config: otherConfig,
@@ -262,6 +264,12 @@ const TRANSFORMS = {
capability.startsWith('hvm')
),
// Only exists on XCP-ng/CH >= 8.2
certificates: obj.$certificates?.map(({ fingerprint, not_after }) => ({
fingerprint,
notAfter: toTimestamp(not_after),
})),
// TODO: dedupe.
PIFs: link(obj, 'PIFs'),
$PIFs: link(obj, 'PIFs'),

View File

@@ -33,6 +33,7 @@ import {
isEmpty,
noop,
omit,
once,
uniq,
} from 'lodash'
import { satisfies as versionSatisfies } from 'semver'
@@ -342,6 +343,26 @@ export default class Xapi extends XapiBase {
await this.call('host.enable', this.getObject(hostId).$ref)
}
async installCertificateOnHost(
hostId,
{ certificate, chain = '', privateKey }
) {
try {
await this.call(
'host.install_server_certificate',
this.getObject(hostId).$ref,
certificate,
privateKey,
chain
)
} catch (error) {
// CH/XCP-ng reset the connection on the certificate install
if (error.code !== 'ECONNRESET') {
throw error
}
}
}
// Resources:
// - Citrix XenServer ® 7.0 Administrator's Guide ch. 5.4
// - https://github.com/xcp-ng/xenadmin/blob/60dd70fc36faa0ec91654ec97e24b7af36acff9f/XenModel/Actions/Host/EditMultipathAction.cs
@@ -1197,9 +1218,21 @@ export default class Xapi extends XapiBase {
force = false,
}
) {
const getDefaultSrRef = once(() => {
if (sr !== undefined) {
return hostXapi.getObject(sr).$ref
}
const defaultSr = host.$pool.$default_SR
if (defaultSr === undefined) {
throw new Error(
`This operation requires a default SR to be set on the pool ${host.$pool.name_label}`
)
}
return defaultSr.$ref
})
// VDIs/SRs mapping
const vdis = {}
const defaultSr = host.$pool.$default_SR
const vbds = flatMap(vm.$snapshots, '$VBDs').concat(vm.$VBDs)
for (const vbd of vbds) {
const vdi = vbd.$VDI
@@ -1207,9 +1240,7 @@ export default class Xapi extends XapiBase {
vdis[vdi.$ref] =
mapVdisSrs && mapVdisSrs[vdi.$id]
? hostXapi.getObject(mapVdisSrs[vdi.$id]).$ref
: sr !== undefined
? hostXapi.getObject(sr).$ref
: defaultSr.$ref // Will error if there are no default SR.
: getDefaultSrRef()
}
}

View File

@@ -749,6 +749,14 @@ export default class BackupNg {
let vmCancel
try {
cancelToken.throwIfRequested()
const isMigrating = Object.values(vm.current_operations).some(
op => op === 'migrate_send' || op === 'pool_migrate'
)
if (isMigrating) {
throw new Error('VM is currently migrating')
}
vmCancel = CancelToken.source([cancelToken])
// $FlowFixMe injected $defer param
@@ -1541,17 +1549,19 @@ export default class BackupNg {
result: () => ({ size: xva.size }),
},
xapi._importVm($cancelToken, fork, sr, vm =>
vm.set_name_label(
`${metadata.vm.name_label} - ${
job.name
} - (${safeDateFormat(metadata.timestamp)})`
)
Promise.all([
vm.add_tags('Disaster Recovery'),
vm.set_name_label(
`${metadata.vm.name_label} - ${
job.name
} - (${safeDateFormat(metadata.timestamp)})`
),
])
)
)
)
await Promise.all([
vm.add_tags('Disaster Recovery'),
disableVmHighAvailability(xapi, vm),
vm.update_blocked_operations(
'start',
@@ -1928,17 +1938,25 @@ export default class BackupNg {
parentId: taskId,
result: ({ transferSize }) => ({ size: transferSize }),
},
xapi.importDeltaVm(fork, {
disableStartAfterImport: false, // we'll take care of that
name_label: `${metadata.vm.name_label} - ${
job.name
} - (${safeDateFormat(metadata.timestamp)})`,
srId: sr.$id,
})
xapi.importDeltaVm(
{
__proto__: fork,
vm: {
...fork.vm,
tags: [...fork.vm.tags, 'Continuous Replication'],
},
},
{
disableStartAfterImport: false, // we'll take care of that
name_label: `${metadata.vm.name_label} - ${
job.name
} - (${safeDateFormat(metadata.timestamp)})`,
srId: sr.$id,
}
)
)
await Promise.all([
vm.add_tags('Continuous Replication'),
disableVmHighAvailability(xapi, vm),
vm.update_blocked_operations(
'start',

View File

@@ -153,12 +153,20 @@ export default class Proxy {
return this._db.update(proxy).then(extractProperties).then(omitToken)
}
async upgradeProxyAppliance(id) {
async updateProxyAppliance(id, { httpProxy, upgrade = false }) {
const xenstoreData = {}
if (httpProxy !== undefined) {
xenstoreData['vm-data/xoa-updater-proxy-url'] = JSON.stringify(httpProxy)
}
if (upgrade) {
xenstoreData['vm-data/xoa-updater-channel'] = JSON.stringify(
this._xoProxyConf.channel
)
}
const { vmUuid } = await this._getProxy(id)
const xapi = this._app.getXapi(vmUuid)
await xapi.getObject(vmUuid).update_xenstore_data({
'vm-data/xoa-updater-channel': JSON.stringify(this._xoProxyConf.channel),
})
await xapi.getObject(vmUuid).update_xenstore_data(xenstoreData)
try {
await xapi.rebootVm(vmUuid)

View File

@@ -55,6 +55,7 @@ export default class {
})
}
@synchronized
async getRemoteHandler(remote) {
if (typeof remote === 'string') {
remote = await this._getRemote(remote)

View File

@@ -171,6 +171,11 @@ export default class {
const store = this._store
if (await store.has(id)) {
await Promise.all(
mapToArray(this._xo.getObjects({ filter: { resourceSet: id } }), vm =>
this.setVmResourceSet(vm.id, null, true)
)
)
return store.del(id)
}

View File

@@ -30,7 +30,7 @@
"lodash": "^4.17.15",
"pako": "^1.0.11",
"promise-toolbox": "^0.15.0",
"vhd-lib": "^0.7.2",
"vhd-lib": "^0.9.0-0",
"xml2js": "^0.4.23"
},
"devDependencies": {

View File

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

View File

@@ -18,6 +18,7 @@ import {
SelectProxy,
SelectRemote,
SelectResourceSetIp,
SelectResourceSetsNetwork,
SelectResourceSetsSr,
SelectSr,
SelectSubject,
@@ -435,6 +436,7 @@ const MAP_TYPE_SELECT = {
proxy: SelectProxy,
remote: SelectRemote,
resourceSetIp: SelectResourceSetIp,
resourceSetNetwork: SelectResourceSetsNetwork,
resourceSetSr: SelectResourceSetsSr,
SR: SelectSr,
subject: SelectSubject,

View File

@@ -31,6 +31,8 @@ const messages = {
showLogs: 'Show logs',
noValue: 'None',
compression: 'Compression',
core: 'Core',
cpu: 'CPU',
multipathing: 'Multipathing',
multipathingDisabled: 'Multipathing disabled',
enableMultipathing: 'Enable multipathing',
@@ -42,6 +44,8 @@ const messages = {
hasInactivePath: 'Has an inactive path',
pools: 'Pools',
remotes: 'Remotes',
schedulerGranularity: 'Scheduler granularity',
socket: 'Socket',
type: 'Type',
restore: 'Restore',
delete: 'Delete',
@@ -83,6 +87,14 @@ const messages = {
advancedSettings: 'Advanced settings',
txChecksumming: 'TX checksumming',
unknownSize: 'Unknown size',
installedCertificates: 'Installed certificates',
expiry: 'Expiry',
fingerprint: 'Fingerprint',
certificate: 'Certificate (PEM)',
certificateChain: 'Certificate chain (PEM)',
privateKey: 'Private key (PKCS#8)',
installNewCertificate: 'Install new certificate',
replaceExistingCertificate: 'Replace existing certificate',
// ----- Modals -----
alertOk: 'OK',
@@ -571,6 +583,7 @@ const messages = {
'Delete backup job{nJobs, plural, one {} other {s}}',
confirmDeleteBackupJobsBody:
'Are you sure you want to delete {nJobs, number} backup job{nJobs, plural, one {} other {s}}?',
runBackupJob: 'Run backup job once',
// ------ Remote -----
remoteName: 'Name',
@@ -653,6 +666,10 @@ const messages = {
aclCreate: 'Create',
newGroupName: 'New group name',
createGroup: 'Create group',
syncLdapGroups: 'Synchronize LDAP groups',
ldapPluginNotConfigured: 'Install and configure the auth-ldap plugin first',
syncLdapGroupsWarning:
'Are you sure you want to synchronize LDAP groups with XO? This may delete XO groups and their ACLs.',
createGroupButton: 'Create',
deleteGroup: 'Delete group',
deleteGroupConfirm: 'Are you sure you want to delete this group?',
@@ -829,6 +846,7 @@ const messages = {
poolNetworkPifDetached: 'Disconnected',
showPifs: 'Show PIFs',
hidePifs: 'Hide PIFs',
networkAutomaticTooltip: 'Network(s) selected by default for new VMs',
// ----- Pool stats tab -----
poolNoStats: 'No stats',
poolAllHosts: 'All hosts',
@@ -908,6 +926,7 @@ const messages = {
hostLicenseExpiry: 'Expiry',
hostRemoteSyslog: 'Remote syslog',
hostIommu: 'IOMMU',
hostNoCertificateInstalled: 'No certificates installed on this host',
supplementalPacks: 'Installed supplemental packs',
supplementalPackNew: 'Install new supplemental pack',
supplementalPackPoolNew: 'Install supplemental pack on every host',
@@ -1189,6 +1208,7 @@ const messages = {
copySnapshot: 'Create a VM from this snapshot',
exportSnapshot: 'Export this snapshot',
snapshotDate: 'Creation date',
snapshotError: 'Snapshot error',
snapshotName: 'Name',
snapshotDescription: 'Description',
snapshotQuiesce: 'Quiesced snapshot',
@@ -1631,6 +1651,8 @@ const messages = {
'delete {nMetadataBackups} metadata backup{nMetadataBackups, plural, one {} other {s}}',
remoteNotCompatibleWithSelectedProxy:
"The backup will not be run on this remote because it's not compatible with the selected proxy",
remoteLoadBackupsFailure: 'Loading backups failed',
remoteLoadBackupsFailureMessage: 'Failed to load backups from {name}.',
// ----- Restore files view -----
listRemoteBackups: 'List remote backups',
@@ -2051,6 +2073,7 @@ const messages = {
deleteSshKey: 'Delete',
deleteSshKeys: 'Delete selected SSH keys',
newSshKeyModalTitle: 'New SSH key',
sshKeyAlreadyExists: 'SSH key already exists!',
sshKeyErrorTitle: 'Invalid key',
sshKeyErrorMessage: 'An SSH key requires both a title and a key.',
title: 'Title',
@@ -2104,6 +2127,7 @@ const messages = {
backupForceRestartFailedVms: "Force restart failed VMs' backup",
clickForMoreInformation: 'Click for more information',
goToThisJob: 'Click to go to this job',
goToCorrespondingLogs: 'Click to see corresponding logs',
// ----- IPs ------
ipPoolName: 'Name',
@@ -2416,6 +2440,7 @@ const messages = {
redeployProxyWarning: 'This action will destroy the old proxy VM',
noProxiesAvailable: 'No proxies available',
checkProxyHealth: 'Test your proxy',
updateProxyApplianceSettings: 'Update appliance settings',
proxyTestSuccess: 'Test passed for {name}',
proxyTestSuccessMessage: 'The proxy appears to work correctly',
proxyTestFailed: 'Test failed for {name}',
@@ -2429,6 +2454,8 @@ const messages = {
httpProxy: 'HTTP proxy',
httpProxyPlaceholder: 'protocol://username:password@address:port',
proxyUpgradesError: 'Unable to check upgrades availability',
proxyApplianceSettingsInfo:
'Leave the field empty and click on OK to remove the existing configuration',
// ----- Utils -----
secondsFormat: '{seconds, plural, one {# second} other {# seconds}}',

View File

@@ -65,7 +65,7 @@ export const getNextIpV4 = ip => {
index = i
return false
}
splitIp[i] = 1
splitIp[i] = 0
})
if (index === 0 && +splitIp[0] === 255) {
return 0

View File

@@ -8,7 +8,6 @@ import Icon from 'icon'
import Tooltip from 'tooltip'
import { alert } from 'modal'
import { isAdmin } from 'selectors'
import { SelectVdi, SelectResourceSetsVdi } from './select-objects'
import { addSubscriptions, connectStore, resolveResourceSet } from './utils'
import { ejectCd, insertCd, subscribeResourceSets } from './xo'
import {
@@ -17,6 +16,10 @@ import {
createGetObject,
createSelector,
} from './selectors'
import {
SelectResourceSetsVdi,
SelectVdi as SelectAnyVdi,
} from './select-objects'
const vdiPredicate = vdi => !vdi.missing
@@ -92,11 +95,11 @@ export default class IsoDevice extends Component {
const { cdDrive, isAdmin, mountedIso } = this.props
const resourceSet = this._getResolvedResourceSet()
const useResourceSet = !(isAdmin || resourceSet === undefined)
const SelectVdi_ = useResourceSet ? SelectResourceSetsVdi : SelectVdi
const SelectVdi = useResourceSet ? SelectResourceSetsVdi : SelectAnyVdi
return (
<div className='input-group'>
<SelectVdi_
<SelectVdi
onChange={this._handleInsert}
predicate={vdiPredicate}
resourceSet={useResourceSet ? resourceSet : undefined}

View File

@@ -634,3 +634,18 @@ export const getResolvedResourceSets = create(
_getResolvedResourceSet(resourceSet, networks, srs, vms)
)
)
export const createGetHostState = getHost =>
create(
(state, props) => getHost(state, props).power_state,
(state, props) => getHost(state, props).enabled,
(state, props) => getHost(state, props).current_operations,
(powerState, enabled, operations) =>
powerState !== 'Running'
? powerState
: !isEmpty(operations)
? 'Busy'
: !enabled
? 'Disabled'
: 'Running'
)

View File

@@ -36,6 +36,7 @@ import SingleLineRow from '../single-line-row'
import TableFilter from '../search-bar'
import UserError from '../user-error'
import { BlockLink } from '../link'
import { conditionalTooltip } from '../tooltip'
import { Container, Col } from '../grid'
import { error as _error } from '../notification'
import { generateId } from '../reaclette-utils'
@@ -58,6 +59,7 @@ class ColumnHead extends Component {
name: PropTypes.node,
sort: PropTypes.func,
sortIcon: PropTypes.string,
tooltip: PropTypes.node,
}
_sort = () => {
@@ -66,15 +68,18 @@ class ColumnHead extends Component {
}
render() {
const { name, sortIcon, textAlign } = this.props
const { name, sortIcon, textAlign, tooltip } = this.props
if (!this.props.sort) {
return <th className={textAlign && `text-xs-${textAlign}`}>{name}</th>
return conditionalTooltip(
<th className={textAlign && `text-xs-${textAlign}`}>{name}</th>,
tooltip
)
}
const isSelected = sortIcon === 'asc' || sortIcon === 'desc'
return (
return conditionalTooltip(
<th
className={classNames(
textAlign && `text-xs-${textAlign}`,
@@ -87,7 +92,8 @@ class ColumnHead extends Component {
<span className='pull-right'>
<Icon icon={sortIcon} />
</span>
</th>
</th>,
tooltip
)
}
}
@@ -984,6 +990,7 @@ class SortedTable extends Component {
? this._getSortOrder()
: 'sort'
}
tooltip={column.tooltip}
/>
))}
{hasIndividualActions && <th />}

View File

@@ -166,3 +166,12 @@ export default class Tooltip extends Component {
return children
}
}
// ===================================================================
export const conditionalTooltip = (children, tooltip) =>
tooltip !== undefined && tooltip !== '' ? (
<Tooltip content={tooltip}>{children}</Tooltip>
) : (
children
)

View File

@@ -223,7 +223,7 @@ function safeHumanFormat(value, opts) {
}
export const formatSize = bytes =>
safeHumanFormat(bytes, { scale: 'binary', unit: 'B' })
bytes != null ? safeHumanFormat(bytes, { scale: 'binary', unit: 'B' }) : 'N/D'
export const formatSizeShort = bytes =>
safeHumanFormat(bytes, { scale: 'binary', unit: 'B', decimals: 0 })

View File

@@ -106,23 +106,22 @@ connect()
const _signIn = new Promise(resolve => xo.once('authenticated', resolve))
const _call = (method, params) => {
let promise = _signIn.then(() => xo.call(method, params))
if (process.env.NODE_ENV !== 'production') {
promise = promise::tap(null, error => {
console.error('XO error', {
method,
params,
code: error.code,
message: error.message,
data: error.data,
const _call = new URLSearchParams(window.location.search.slice(1)).has('debug')
? async (method, params) => {
await _signIn
// eslint-disable-next-line no-console
console.debug('API call', method, params)
return tap.call(xo.call(method, params), null, error => {
console.error('XO error', {
method,
params,
code: error.code,
message: error.message,
data: error.data,
})
})
})
}
return promise
}
}
: (method, params) => _signIn.then(() => xo.call(method, params))
// ===================================================================
@@ -443,6 +442,30 @@ export const subscribeNotifications = createSubscription(async () => {
)
})
const checkSchedulerGranularitySubscriptions = {}
export const subscribeSchedulerGranularity = (host, cb) => {
if (checkSchedulerGranularitySubscriptions[host] === undefined) {
checkSchedulerGranularitySubscriptions[host] = createSubscription(() =>
_call('host.getSchedulerGranularity', { host })
)
}
return checkSchedulerGranularitySubscriptions[host](cb)
}
subscribeSchedulerGranularity.forceRefresh = host => {
if (host === undefined) {
forEach(checkSchedulerGranularitySubscriptions, subscription =>
subscription.forceRefresh()
)
return
}
const subscription = checkSchedulerGranularitySubscriptions[host]
if (subscription !== undefined) {
subscription.forceRefresh()
}
}
const checkSrCurrentStateSubscriptions = {}
export const subscribeCheckSrCurrentState = (pool, cb) => {
const poolId = resolveId(pool)
@@ -736,6 +759,12 @@ export const setPoolMaster = host =>
// Host --------------------------------------------------------------
export const setSchedulerGranularity = (host, schedulerGranularity) =>
_call('host.setSchedulerGranularity', {
host,
schedulerGranularity,
})::tap(() => subscribeSchedulerGranularity.forceRefresh(host))
export const editHost = (host, props) =>
_call('host.set', { ...props, id: resolveId(host) })
@@ -899,6 +928,9 @@ export const isHyperThreadingEnabledHost = host =>
id: resolveId(host),
})
export const installCertificateOnHost = (id, props) =>
_call('host.installCertificate', { id, ...props })
// for XCP-ng now
export const installAllPatchesOnHost = ({ host }) =>
confirm({
@@ -1331,13 +1363,17 @@ export const snapshotVms = vms =>
icon: 'memory',
title: _('snapshotVmsModalTitle', { vms: vms.length }),
body: <SnapshotVmModalBody vms={vms} />,
}).then(
({ names, saveMemory, descriptions }) =>
Promise.all(
map(vms, vm => snapshotVm(vm, names[vm], saveMemory, descriptions[vm]))
),
noop
)
})
.then(
({ names, saveMemory, descriptions }) =>
Promise.all(
map(vms, vm =>
snapshotVm(vm, names[vm], saveMemory, descriptions[vm])
)
),
noop
)
.catch(e => error(_('snapshotError'), e.message))
export const deleteSnapshot = vm =>
confirm({
@@ -1825,6 +1861,7 @@ export const setVif = (
mac,
network,
rateLimit,
resourceSet,
txChecksumming,
}
) =>
@@ -1836,6 +1873,7 @@ export const setVif = (
mac,
network: resolveId(network),
rateLimit,
resourceSet,
txChecksumming,
})
@@ -2349,8 +2387,10 @@ export const configurePlugin = (id, configuration) =>
)
export const getPlugin = async id => {
const plugins = await _call('plugin.get')
return plugins.find(plugin => plugin.id === id)
const { user } = store.getState()
if (user != null && user.permission === 'admin') {
return (await _call('plugin.get')).find(plugin => plugin.id === id)
}
}
export const purgePluginConfiguration = async id => {
@@ -2864,27 +2904,35 @@ const _setUserPreferences = preferences =>
})::tap(subscribeCurrentUser.forceRefresh)
import NewSshKeyModalBody from './new-ssh-key-modal' // eslint-disable-line import/first
export const addSshKey = key => {
export const addSshKey = async key => {
const { preferences } = xo.user
const otherKeys = (preferences && preferences.sshKeys) || []
if (key) {
return _setUserPreferences({
sshKeys: [...otherKeys, key],
})
}
return confirm({
icon: 'ssh-key',
title: _('newSshKeyModalTitle'),
body: <NewSshKeyModalBody />,
}).then(newKey => {
if (!newKey.title || !newKey.key) {
if (key === undefined) {
try {
key = await confirm({
icon: 'ssh-key',
title: _('newSshKeyModalTitle'),
body: <NewSshKeyModalBody />,
})
} catch (err) {
return
}
if (!key.title || !key.key) {
error(_('sshKeyErrorTitle'), _('sshKeyErrorMessage'))
return
}
return _setUserPreferences({
sshKeys: [...otherKeys, newKey],
})
}, noop)
}
if (otherKeys.some(otherKey => otherKey.key === key.key)) {
error(_('sshKeyErrorTitle'), _('sshKeyAlreadyExists'))
return
}
return _setUserPreferences({
sshKeys: [...otherKeys, key],
})
}
export const deleteSshKey = key =>
@@ -3016,7 +3064,7 @@ export const setDefaultHomeFilter = (type, name) => {
return _setUserPreferences({
defaultHomeFilters: {
...defaultFilters,
[type]: name,
[type]: name === null ? undefined : name,
},
})
}
@@ -3287,6 +3335,9 @@ export const upgradeProxyAppliance = proxy =>
export const getProxyApplianceUpdaterState = id =>
_call('proxy.getApplianceUpdaterState', { id })
export const updateProxyApplianceSettings = (id, props) =>
_call('proxy.updateApplianceSettings', { id, ...props })
const PROXY_HEALTH_CHECK_COMMON_ERRORS_CODE = new Set([
'ECONNREFUSED',
'ECONNRESET',
@@ -3350,3 +3401,15 @@ export const checkAuditRecordsIntegrity = (oldest, newest) =>
export const generateAuditFingerprint = oldest =>
_call('audit.generateFingerprint', { oldest })
// LDAP ------------------------------------------------------------------------
export const synchronizeLdapGroups = () =>
confirm({
title: _('syncLdapGroups'),
body: _('syncLdapGroupsWarning'),
icon: 'refresh',
}).then(
() => _call('ldap.synchronizeGroups')::tap(subscribeGroups.forceRefresh),
noop
)

View File

@@ -19,7 +19,7 @@ import Tooltip from '../../tooltip'
import { Col } from '../../grid'
import { getDefaultNetworkForVif } from '../utils'
import { SelectHost, SelectNetwork, SelectSr } from '../../select-objects'
import { connectStore } from '../../utils'
import { connectStore, createCompare } from '../../utils'
import {
createGetObjectsOfType,
createPicker,
@@ -251,6 +251,12 @@ export default class MigrateVmsModalBody extends BaseComponent {
srId: defaultSrConnectedToHost ? defaultSrId : undefined,
})
}
getCompareContainers = createSelector(
() => this.props.vms,
vms => createCompare([pool => some(vms, vm => vm.$pool === pool.id)])
)
_selectMigrationNetwork = migrationNetwork =>
this.setState({ migrationNetworkId: migrationNetwork.id })
_selectNetwork = network => this.setState({ networkId: network.id })
@@ -277,6 +283,7 @@ export default class MigrateVmsModalBody extends BaseComponent {
<Col size={6}>{_('migrateVmSelectHost')}</Col>
<Col size={6}>
<SelectHost
compareContainers={this.getCompareContainers()}
onChange={this._selectHost}
predicate={this._getHostPredicate()}
value={host}

View File

@@ -141,6 +141,10 @@
@extend .fa;
@extend .fa-download;
}
&-upload {
@extend .fa;
@extend .fa-upload;
}
&-shortcuts {
@extend .fa;
@extend .fa-keyboard-o;

View File

@@ -27,6 +27,7 @@ const Overview = decorate([
provideState({
initialState: () => ({
scrollIntoJobs: undefined,
scrollIntoLogs: undefined,
}),
effects: {
handleJobsRef(_, ref) {
@@ -34,6 +35,11 @@ const Overview = decorate([
this.state.scrollIntoJobs = ref.scrollIntoView.bind(ref)
}
},
handleLogsRef(_, ref) {
if (ref !== null) {
this.state.scrollIntoLogs = ref.scrollIntoView.bind(ref)
}
},
},
computed: {
haveLegacyBackups: (_, { legacyJobs }) =>
@@ -41,7 +47,10 @@ const Overview = decorate([
},
}),
injectState,
({ effects, state: { haveLegacyBackups, scrollIntoJobs } }) => (
({
effects,
state: { haveLegacyBackups, scrollIntoJobs, scrollIntoLogs },
}) => (
<div>
{haveLegacyBackups && <LegacyOverview />}
<div className='mt-2 mb-1'>
@@ -52,11 +61,13 @@ const Overview = decorate([
</CardHeader>
<CardBlock>
<div ref={effects.handleJobsRef}>
<JobsTable />
<JobsTable scrollIntoLogs={scrollIntoLogs} />
</div>
</CardBlock>
</Card>
<LogsTable scrollIntoJobs={scrollIntoJobs} />
<div ref={effects.handleLogsRef}>
<LogsTable scrollIntoJobs={scrollIntoJobs} />
</div>
</div>
</div>
),

View File

@@ -18,7 +18,9 @@ import { createFilter, createGetObjectsOfType, createSelector } from 'selectors'
import { createPredicate } from 'value-matcher'
import { get } from '@xen-orchestra/defined'
import { groupBy, isEmpty, map, some } from 'lodash'
import { injectState, provideState } from 'reaclette'
import { Proxy } from 'render-xo-item'
import { withRouter } from 'react-router'
import {
cancelJob,
deleteBackupJobs,
@@ -110,6 +112,49 @@ const _runBackupJob = ({ id, name, nVms, schedule, type }) =>
: runMetadataBackupJob({ id, schedule })
)
const CURSOR_POINTER_STYLE = { cursor: 'pointer' }
const GoToLogs = decorate([
withRouter,
provideState({
effects: {
goTo() {
const {
jobId,
location,
router,
scheduleId,
scrollIntoLogs,
} = this.props
router.replace({
...location,
query: {
...location.query,
s_logs:
jobId !== undefined
? `jobId:${jobId}`
: `scheduleId:${scheduleId}`,
},
})
scrollIntoLogs()
},
},
}),
injectState,
({ effects, children }) => (
<Tooltip content={_('goToCorrespondingLogs')}>
<span onClick={effects.goTo} style={CURSOR_POINTER_STYLE}>
{children}
</span>
</Tooltip>
),
])
GoToLogs.propTypes = {
jobId: PropTypes.string,
scheduleId: PropTypes.string,
scrollIntoLogs: PropTypes.func.isRequired,
}
const SchedulePreviewBody = decorate([
addSubscriptions(({ schedule }) => ({
lastRunLog: cb =>
@@ -138,12 +183,14 @@ const SchedulePreviewBody = decorate([
.filter(createSelector((_, props) => props.job.vms, createPredicate))
.count(),
})),
({ job, schedule, lastRunLog, nVms }) => (
({ job, schedule, scrollIntoLogs, lastRunLog, nVms }) => (
<Ul>
<Li>
{schedule.name
? _.keyValue(_('scheduleName'), schedule.name)
: _.keyValue(_('scheduleCron'), schedule.cron)}{' '}
<GoToLogs scheduleId={schedule.id} scrollIntoLogs={scrollIntoLogs}>
{schedule.name
? _.keyValue(_('scheduleName'), schedule.name)
: _.keyValue(_('scheduleCron'), schedule.cron)}
</GoToLogs>{' '}
<Tooltip content={_('scheduleCopyId', { id: schedule.id.slice(4, 8) })}>
<CopyToClipboard text={schedule.id}>
<Button size='small'>
@@ -191,6 +238,7 @@ const SchedulePreviewBody = decorate([
icon='run-schedule'
key='run'
size='small'
tooltip={_('runBackupJob')}
/>
)}{' '}
{lastRunLog !== undefined && (
@@ -226,9 +274,11 @@ class JobsTable extends React.Component {
static tableProps = {
columns: [
{
itemRenderer: ({ id }) => (
itemRenderer: ({ id }, { scrollIntoLogs }) => (
<Copiable data={id} tagName='p'>
{id.slice(4, 8)}
<GoToLogs jobId={id} scrollIntoLogs={scrollIntoLogs}>
{id.slice(4, 8)}
</GoToLogs>
</Copiable>
),
name: _('jobId'),
@@ -250,7 +300,7 @@ class JobsTable extends React.Component {
name: _('jobModes'),
},
{
itemRenderer: (job, { schedulesByJob }) =>
itemRenderer: (job, { schedulesByJob, scrollIntoLogs }) =>
map(
get(() => schedulesByJob[job.id]),
schedule => (
@@ -258,6 +308,7 @@ class JobsTable extends React.Component {
job={job}
key={schedule.id}
schedule={schedule}
scrollIntoLogs={scrollIntoLogs}
/>
)
),
@@ -399,6 +450,7 @@ class JobsTable extends React.Component {
data-goToNewTab={this._goToNewTab}
data-main={this.props.main}
data-schedulesByJob={this.props.schedulesByJob}
data-scrollIntoLogs={this.props.scrollIntoLogs}
stateUrlParam='s'
/>
)

View File

@@ -11,15 +11,14 @@ import { confirm } from 'modal'
import { error } from 'notification'
import { FormattedDate } from 'react-intl'
import {
cloneDeep,
filter,
find,
flatMap,
forEach,
keyBy,
map,
reduce,
orderBy,
toArray,
} from 'lodash'
import {
deleteBackups,
@@ -125,38 +124,7 @@ export default class Restore extends Component {
}
}
_refreshBackupList = async (
_remotes = this.props.remotes,
jobs = this.props.jobs
) => {
const remotes = keyBy(filter(_remotes, { enabled: true }), 'id')
const backupsByRemote = await listVmBackups(toArray(remotes))
const backupDataByVm = {}
forEach(backupsByRemote, (backups, remoteId) => {
const remote = remotes[remoteId]
forEach(backups, (vmBackups, vmId) => {
if (vmBackups.length === 0) {
return
}
if (backupDataByVm[vmId] === undefined) {
backupDataByVm[vmId] = { backups: [] }
}
backupDataByVm[vmId].backups.push(
...map(vmBackups, bkp => {
const job = find(jobs, { id: bkp.jobId })
return {
...bkp,
remote,
jobName: job && job.name,
}
})
)
})
})
// TODO: perf
_sortBackupList = backupDataByVm => {
let first, last
forEach(backupDataByVm, (data, vmId) => {
first = { timestamp: Infinity }
@@ -184,7 +152,59 @@ export default class Restore extends Component {
backupDataByVm[vmId].backups = orderBy(backups, 'timestamp', 'desc')
})
this.setState({ backupDataByVm })
return backupDataByVm
}
_refreshBackupListOnRemote = async (remote, jobs) => {
const remoteId = remote.id
const backupsByRemote = await listVmBackups([remoteId])
const { backupDataByVm } = this.state
const remoteBackupDataByVm = {}
forEach(backupsByRemote[remoteId], (vmBackups, vmId) => {
if (vmBackups.length === 0) {
return
}
const backupData = backupDataByVm[vmId]
remoteBackupDataByVm[vmId] =
backupData === undefined ? { backups: [] } : cloneDeep(backupData)
remoteBackupDataByVm[vmId].backups.push(
...map(vmBackups, bkp => {
const job = find(jobs, { id: bkp.jobId })
return {
...bkp,
remote,
jobName: job && job.name,
}
})
)
})
this.setState(({ backupDataByVm }) => ({
backupDataByVm: {
...backupDataByVm,
...this._sortBackupList(remoteBackupDataByVm),
},
}))
}
_refreshBackupList = (
remotes = this.props.remotes,
jobs = this.props.jobs
) => {
this.setState({ backupDataByVm: {} }, () =>
Promise.all(
map(filter(remotes, { enabled: true }), remote =>
this._refreshBackupListOnRemote(remote, jobs).catch(() =>
error(
_('remoteLoadBackupsFailure'),
_('remoteLoadBackupsFailureMessage', { name: remote.name })
)
)
)
)
)
}
// Actions -------------------------------------------------------------------

View File

@@ -72,6 +72,7 @@ class PatchesCard extends Component {
@connectStore({
hosts: createGetObjectsOfType('host'),
isAdmin,
pools: createGetObjectsOfType('pool'),
srs: createGetObjectsOfType('SR').filter([isSrWritable]),
vms: createGetObjectsOfType('VM'),
@@ -82,10 +83,13 @@ class PatchesCard extends Component {
task => task.status === 'pending',
]),
})
@addSubscriptions({
plugins: subscribePlugins,
users: subscribeUsers,
})
@addSubscriptions(
({ isAdmin }) =>
isAdmin && {
plugins: subscribePlugins,
users: subscribeUsers,
}
)
@injectIntl
class DefaultCard extends Component {
_getPoolWisePredicate = createSelector(

View File

@@ -28,6 +28,7 @@ import {
} from 'utils'
import {
createDoesHostNeedRestart,
createGetHostState,
createGetObject,
createGetObjectsOfType,
createSelector,
@@ -46,6 +47,7 @@ import styles from './index.css'
hostId => obj => obj.$container === hostId
)
),
state: createGetHostState((_, props) => props.item),
}))
export default class HostItem extends Component {
get _isRunning() {
@@ -65,21 +67,15 @@ export default class HostItem extends Component {
_toggleExpanded = () => this.setState({ expanded: !this.state.expanded })
_onSelect = () => this.props.onSelect(this.props.item.id)
_getHostState = createSelector(
() => this.props.item.power_state,
() => this.props.item.enabled,
() => this.props.item.current_operations,
(powerState, enabled, operations) =>
!isEmpty(operations)
? 'Busy'
: powerState === 'Running' && !enabled
? 'Disabled'
: powerState
)
render() {
const { item: host, container, expandAll, selected, nVms } = this.props
const state = this._getHostState()
const {
container,
expandAll,
item: host,
nVms,
selected,
state,
} = this.props
return (
<div className={styles.item}>

View File

@@ -475,10 +475,6 @@ const NoObjects = props =>
<NoObjectsWithoutServers {...props} />
)
@addSubscriptions({
jobs: subscribeBackupNgJobs,
noResourceSets: cb => subscribeResourceSets(data => cb(isEmpty(data))),
})
@connectStore(() => {
const type = (_, props) => props.location.query.t || DEFAULT_TYPE
@@ -503,6 +499,15 @@ const NoObjects = props =>
user: getUser,
}
})
@addSubscriptions(({ isAdmin }) => {
const noResourceSets = cb => subscribeResourceSets(data => cb(isEmpty(data)))
return isAdmin
? {
jobs: subscribeBackupNgJobs,
noResourceSets,
}
: { noResourceSets }
})
export default class Home extends Component {
static contextTypes = {
router: PropTypes.object,

View File

@@ -17,6 +17,7 @@ import { connectStore, routes } from 'utils'
import {
createDoesHostNeedRestart,
createFilter,
createGetHostState,
createGetObject,
createGetObjectsOfType,
createSelector,
@@ -93,6 +94,8 @@ const isRunning = host => host && host.power_state === 'Running'
const doesNeedRestart = createDoesHostNeedRestart(getHost)
const getHostState = createGetHostState(getHost)
return (state, props) => {
const host = getHost(state, props)
if (!host) {
@@ -110,6 +113,7 @@ const isRunning = host => host && host.power_state === 'Running'
pifs: getPifs(state, props),
pool: getPool(state, props),
privateNetworks: getPrivateNetworks(state, props),
state: getHostState(state, props),
vmController: getVmController(state, props),
vms: getHostVms(state, props),
}
@@ -210,22 +214,9 @@ export default class Host extends Component {
_setNameLabel = nameLabel =>
editHost(this.props.host, { name_label: nameLabel })
_getHostState = createSelector(
() => this.props.host.power_state,
() => this.props.host.enabled,
() => this.props.host.current_operations,
(powerState, enabled, operations) =>
!isEmpty(operations)
? 'Busy'
: powerState === 'Running' && !enabled
? 'Disabled'
: powerState
)
header() {
const { host, pool } = this.props
const { host, pool, state } = this.props
const { missingPatches } = this.state || {}
const state = this._getHostState()
if (!host) {
return <Icon icon='loading' />
}

View File

@@ -0,0 +1,112 @@
import _ from 'intl'
import decorate from 'apply-decorators'
import Icon from 'icon'
import React from 'react'
import SingleLineRow from 'single-line-row'
import { Col, Container } from 'grid'
import { form } from 'modal'
import { generateId } from 'reaclette-utils'
import { installCertificateOnHost } from 'xo'
import { provideState, injectState } from 'reaclette'
import { Textarea as DebounceTextarea } from 'debounce-input-decorator'
const InstallCertificateModal = decorate([
provideState({
effects: {
onChange(_, { target: { name, value } }) {
const { props } = this
props.onChange({
...props.value,
[name]: value,
})
},
},
computed: {
inputCertificateChainId: generateId,
inputCertificateId: generateId,
inputPrivateKeyId: generateId,
},
}),
injectState,
({ state, effects, value }) => (
<Container>
<SingleLineRow>
<Col mediumSize={4}>
<label htmlFor={state.inputCertificateId}>
<strong>{_('certificate')}</strong>
</label>
</Col>
<Col mediumSize={8}>
<DebounceTextarea
className='form-control'
id={state.inputCertificateId}
name='certificate'
onChange={effects.onChange}
required
value={value.certificate}
/>
</Col>
</SingleLineRow>
<SingleLineRow className='mt-1'>
<Col mediumSize={4}>
<label htmlFor={state.inputPrivateKeyId}>
<strong>{_('privateKey')}</strong>
</label>
</Col>
<Col mediumSize={8}>
<DebounceTextarea
className='form-control'
id={state.inputPrivateKeyId}
name='privateKey'
onChange={effects.onChange}
required
value={value.privateKey}
/>
</Col>
</SingleLineRow>
<SingleLineRow className='mt-1'>
<Col mediumSize={4}>
<label htmlFor={state.inputCertificateChainId}>
<strong>{_('certificateChain')}</strong>
</label>
</Col>
<Col mediumSize={8}>
<DebounceTextarea
className='form-control'
id={state.inputCertificateChainId}
name='certificateChain'
onChange={effects.onChange}
value={value.certificateChain}
/>
</Col>
</SingleLineRow>
</Container>
),
])
const installCertificate = async ({ id, isNewInstallation = false }) => {
const { certificate, certificateChain, privateKey } = await form({
defaultValue: {
certificate: '',
certificateChain: '',
privateKey: '',
},
render: props => <InstallCertificateModal {...props} />,
header: (
<span>
<Icon icon='upload' />{' '}
{isNewInstallation
? _('installNewCertificate')
: _('replaceExistingCertificate')}
</span>
),
})
await installCertificateOnHost(id, {
certificate: certificate.trim(),
chain: certificateChain.trim(),
privateKey: privateKey.trim(),
})
}
export { installCertificate }

View File

@@ -1,4 +1,5 @@
import _ from 'intl'
import ActionButton from 'action-button'
import Component from 'base-component'
import Copiable from 'copiable'
import decorate from 'apply-decorators'
@@ -10,7 +11,12 @@ import StateButton from 'state-button'
import TabButton from 'tab-button'
import Tooltip from 'tooltip'
import Upgrade from 'xoa-upgrade'
import { compareVersions, connectStore, getIscsiPaths } from 'utils'
import {
addSubscriptions,
compareVersions,
connectStore,
getIscsiPaths,
} from 'utils'
import { confirm } from 'modal'
import { Container, Row, Col } from 'grid'
import { createGetObjectsOfType, createSelector } from 'selectors'
@@ -18,7 +24,7 @@ import { forEach, isEmpty, map, noop } from 'lodash'
import { FormattedRelative, FormattedTime } from 'react-intl'
import { Sr } from 'render-xo-item'
import { Text } from 'editable'
import { Toggle } from 'form'
import { Toggle, Select } from 'form'
import {
detachHost,
disableHost,
@@ -33,10 +39,29 @@ import {
restartHost,
setHostsMultipathing,
setRemoteSyslogHost,
setSchedulerGranularity,
subscribeSchedulerGranularity,
} from 'xo'
import { installCertificate } from './install-certificate'
const ALLOW_INSTALL_SUPP_PACK = process.env.XOA_PLAN > 1
const SCHED_GRAN_TYPE_OPTIONS = [
{
label: _('core'),
value: 'core',
},
{
label: _('cpu'),
value: 'cpu',
},
{
label: _('socket'),
value: 'socket',
},
]
const forceReboot = host => restartHost(host, true)
const formatPack = ({ name, author, description, version }, key) => (
@@ -86,6 +111,9 @@ MultipathableSrs.propTypes = {
hostId: PropTypes.string.isRequired,
}
@addSubscriptions(props => ({
schedGran: cb => subscribeSchedulerGranularity(props.host.id, cb),
}))
@connectStore(() => {
const getPgpus = createGetObjectsOfType('PGPU')
.pick((_, { host }) => host.$PGPUs)
@@ -136,6 +164,9 @@ export default class extends Component {
}
)
_setSchedulerGranularity = value =>
setSchedulerGranularity(this.props.host.id, value)
_setHostIscsiIqn = iscsiIqn =>
confirm({
icon: 'alarm',
@@ -165,7 +196,7 @@ export default class extends Component {
}
render() {
const { host, pcis, pgpus } = this.props
const { host, pcis, pgpus, schedGran } = this.props
const {
isHtEnabled,
isNetDataPluginInstalledOnHost,
@@ -346,6 +377,21 @@ export default class extends Component {
{host.multipathing && <MultipathableSrs hostId={host.id} />}
</td>
</tr>
{schedGran != null && (
<tr>
<th>{_('schedulerGranularity')}</th>
<td>
<Select
onChange={this._setSchedulerGranularity}
options={SCHED_GRAN_TYPE_OPTIONS}
required
simpleValue
value={schedGran}
/>
<small>{_('rebootUpdateHostLabel')}</small>
</td>
</tr>
)}
<tr>
<th>{_('hostRemoteSyslog')}</th>
<td>
@@ -452,6 +498,57 @@ export default class extends Component {
<Upgrade place='supplementalPacks' available={2} />
</Container>,
]}
{host.certificates !== undefined && (
<div>
<h3>
{_('installedCertificates')}{' '}
<ActionButton
btnStyle='success'
data-id={host.id}
data-isNewInstallation={host.certificates.length === 0}
handler={installCertificate}
icon='upload'
>
{host.certificates.length > 0
? _('replaceExistingCertificate')
: _('installNewCertificate')}
</ActionButton>
</h3>
{host.certificates.length > 0 ? (
<ul className='list-group'>
{host.certificates.map(({ fingerprint, notAfter }) => (
<li className='list-group-item' key={fingerprint}>
<Container>
<Row>
<Col mediumSize={2}>
<strong>{_('fingerprint')}</strong>
</Col>
<Col mediumSize={10}>
<Copiable tagName='pre'>{fingerprint}</Copiable>
</Col>
</Row>
<Row>
<Col mediumSize={2}>
<strong>{_('expiry')}</strong>
</Col>
<Col mediumSize={10}>
<FormattedTime
value={notAfter * 1e3}
day='numeric'
month='long'
year='numeric'
/>
</Col>
</Row>
</Container>
</li>
))}
</ul>
) : (
<span>{_('hostNoCertificateInstalled')}</span>
)}
</div>
)}
</Col>
</Row>
</Container>

View File

@@ -16,6 +16,7 @@ import {
subscribePlugins,
} from 'xo'
import {
isAdmin,
createGetObject,
createGetObjectsOfType,
getIsPoolAdmin,
@@ -80,14 +81,18 @@ const canSupportPrivateNetwork = (pool, pif) =>
pif.$host === pool.master
const NewNetwork = decorate([
addSubscriptions({
plugins: subscribePlugins,
}),
connectStore(() => ({
isAdmin,
isPoolAdmin: getIsPoolAdmin,
nPools: createGetObjectsOfType('pool').count(),
pool: createGetObject((_, props) => props.location.query.pool),
})),
addSubscriptions(
({ isAdmin }) =>
isAdmin && {
plugins: subscribePlugins,
}
),
injectIntl,
provideState({
initialState: () => ({ ...EMPTY, bondModes: undefined }),

View File

@@ -2,17 +2,21 @@ import _ from 'intl'
import ActionBar, { Action } from 'action-bar'
import Component from 'base-component'
import React from 'react'
import { createGetObjectsOfType, createSelector } from 'selectors'
import { createGetObjectsOfType, createSelector, isAdmin } from 'selectors'
import { find } from 'lodash'
import { addSubscriptions, connectStore, noop } from 'utils'
import { addHostsToPool, disconnectServer, subscribeServers } from 'xo'
@connectStore({
hosts: createGetObjectsOfType('host'),
isAdmin,
})
@addSubscriptions({
servers: subscribeServers,
})
@addSubscriptions(
({ isAdmin }) =>
isAdmin && {
servers: subscribeServers,
}
)
export default class PoolActionBar extends Component {
_getMasterAddress = createSelector(
() => this.props.pool && this.props.pool.master,

View File

@@ -10,7 +10,7 @@ import map from 'lodash/map'
import React, { Component } from 'react'
import some from 'lodash/some'
import SortedTable from 'sorted-table'
import Tooltip from 'tooltip'
import Tooltip, { conditionalTooltip } from 'tooltip'
import { connectStore } from 'utils'
import { Container, Row, Col } from 'grid'
import { TabButtonLink } from 'tab-button'
@@ -32,9 +32,6 @@ import {
// =============================================================================
const _conditionalTooltip = (component, tooltip) =>
tooltip ? <Tooltip content={tooltip}>{component}</Tooltip> : component
const _createGetPifs = () =>
createGetObjectsOfType('PIF').pick((_, props) => props.network.PIFs)
@@ -169,13 +166,13 @@ class ToggleDefaultLockingMode extends Component {
render() {
const { isInUse, network } = this.props
return _conditionalTooltip(
return conditionalTooltip(
<Toggle
disabled={isInUse}
onChange={this._editDefaultIsLocked}
value={network.defaultIsLocked}
/>,
isInUse && _('networkInUse')
isInUse ? _('networkInUse') : undefined
)
}
}
@@ -356,6 +353,7 @@ const NETWORKS_COLUMNS = [
{
name: _('poolNetworkAutomatic'),
itemRenderer: network => <AutomaticNetwork network={network} />,
tooltip: _('networkAutomaticTooltip'),
},
{
name: '',

View File

@@ -24,6 +24,7 @@ import {
import Page from '../page'
import deployProxy from './deploy-proxy'
import { updateApplianceSettings } from './update-appliance-settings'
const _editProxy = (value, { name, proxy }) =>
editProxyAppliance(proxy, { [name]: value })
@@ -65,6 +66,14 @@ const INDIVIDUAL_ACTIONS = [
label: _('checkProxyHealth'),
level: 'primary',
},
{
collapsed: true,
disabled: ({ vmUuid }) => vmUuid === undefined,
handler: proxy => updateApplianceSettings(proxy),
icon: 'settings',
label: _('updateProxyApplianceSettings'),
level: 'primary',
},
{
handler: ({ id }, { router }) =>
router.push({

View File

@@ -0,0 +1,74 @@
import _, { messages } from 'intl'
import decorate from 'apply-decorators'
import Icon from 'icon'
import React from 'react'
import SingleLineRow from 'single-line-row'
import { Col, Container } from 'grid'
import { form } from 'modal'
import { generateId } from 'reaclette-utils'
import { injectIntl } from 'react-intl'
import { provideState, injectState } from 'reaclette'
import { updateProxyApplianceSettings } from 'xo'
const UpdateApplianceSettingsModal = decorate([
provideState({
effects: {
onHttpProxyChange(_, { target: { value } }) {
this.props.onChange({
...this.props.value,
httpProxy: value,
})
},
},
computed: {
idHttpProxyInput: generateId,
},
}),
injectIntl,
injectState,
({ intl: { formatMessage }, effects, state, value }) => (
<Container>
<SingleLineRow>
<Col mediumSize={4}>
<label htmlFor={state.idHttpProxyInput}>
<strong>{_('httpProxy')}</strong>
</label>
</Col>
<Col mediumSize={8}>
<input
className='form-control'
id={state.idHttpProxyInput}
onChange={effects.onHttpProxyChange}
placeholder={formatMessage(messages.httpProxyPlaceholder)}
value={value.httpProxy}
/>
</Col>
</SingleLineRow>
<SingleLineRow className='mt-1'>
<Col className='text-info'>
<Icon icon='info' /> {_('proxyApplianceSettingsInfo')}
</Col>
</SingleLineRow>
</Container>
),
])
const updateApplianceSettings = async proxy => {
let { httpProxy } = await form({
defaultValue: {
httpProxy: '',
},
render: props => <UpdateApplianceSettingsModal {...props} />,
header: (
<span>
<Icon icon='settings' /> {_('settings')}
</span>
),
})
await updateProxyApplianceSettings(proxy.id, {
httpProxy: (httpProxy = httpProxy.trim()) !== '' ? httpProxy : null,
})
}
export { updateApplianceSettings }

View File

@@ -1,15 +1,13 @@
import _, { messages } from 'intl'
import ActionButton from 'action-button'
import Component from 'base-component'
import includes from 'lodash/includes'
import isEmpty from 'lodash/isEmpty'
import keyBy from 'lodash/keyBy'
import map from 'lodash/map'
import PropTypes from 'prop-types'
import React from 'react'
import size from 'lodash/size'
import SortedTable from 'sorted-table'
import { conditionalTooltip } from 'tooltip'
import { addSubscriptions } from 'utils'
import { createSelector } from 'selectors'
import { includes, isEmpty, keyBy, map, size } from 'lodash'
import { injectIntl } from 'react-intl'
import { SelectSubject } from 'select-objects'
import { Text } from 'editable'
@@ -22,7 +20,9 @@ import {
removeUserFromGroup,
setGroupName,
subscribeGroups,
subscribePlugins,
subscribeUsers,
synchronizeLdapGroups,
} from 'xo'
@addSubscriptions({
@@ -141,6 +141,7 @@ const ACTIONS = [
@addSubscriptions({
groups: subscribeGroups,
plugins: subscribePlugins,
})
@injectIntl
export default class Groups extends Component {
@@ -153,11 +154,40 @@ export default class Groups extends Component {
}
}
_isLdapGroupSyncConfigured = createSelector(
() => this.props.plugins,
plugins => {
if (plugins === undefined) {
return false
}
const ldapPlugin = plugins.find(({ name }) => name === 'auth-ldap')
if (ldapPlugin === undefined) {
return false
}
return ldapPlugin.loaded && ldapPlugin.configuration.groups !== undefined
}
)
render() {
const { groups, intl } = this.props
const disableLdapGroupSync = !this._isLdapGroupSyncConfigured()
return (
<div>
{conditionalTooltip(
<ActionButton
btnStyle='primary'
className='mr-1 mb-1'
disabled={disableLdapGroupSync}
handler={synchronizeLdapGroups}
icon='refresh'
>
{_('syncLdapGroups')}
</ActionButton>,
disableLdapGroupSync ? _('ldapPluginNotConfigured') : undefined
)}
<form id='newGroupForm' className='form-inline'>
<div className='form-group'>
<input

View File

@@ -42,14 +42,12 @@ const vmActionBarByState = {
}
/>
)}
{!isSelfUser && (
<Action
handler={snapshotVm}
icon='vm-snapshot'
label={_('snapshotVmLabel')}
pending={includes(vm.current_operations, 'snapshot')}
/>
)}
<Action
handler={snapshotVm}
icon='vm-snapshot'
label={_('snapshotVmLabel')}
pending={includes(vm.current_operations, 'snapshot')}
/>
{!isSelfUser && canAdministrate && (
<Action
handler={exportVm}

View File

@@ -178,7 +178,7 @@ export default class Vm extends BaseComponent {
)
header() {
const { vm, container, pool } = this.props
const { isAdmin, vm, container, pool } = this.props
if (!vm) {
return <Icon icon='loading' />
}
@@ -273,7 +273,9 @@ export default class Vm extends BaseComponent {
)}
</NavLink>
)}
<NavLink to={`/vms/${vm.id}/backups`}>{_('backup')}</NavLink>
{isAdmin && (
<NavLink to={`/vms/${vm.id}/backups`}>{_('backup')}</NavLink>
)}
<NavLink to={`/vms/${vm.id}/logs`}>{_('logsTabName')}</NavLink>
{vm.docker && (
<NavLink to={`/vms/${vm.id}/containers`}>

View File

@@ -32,7 +32,6 @@ import {
noop,
resolveResourceSet,
} from 'utils'
import { SelectSr, SelectVdi, SelectResourceSetsSr } from 'select-objects'
import { SizeInput, Toggle } from 'form'
import { XoSelect, Size, Text } from 'editable'
import { confirm } from 'modal'
@@ -68,6 +67,11 @@ import {
setBootableVbd,
subscribeResourceSets,
} from 'xo'
import {
SelectResourceSetsSr,
SelectSr as SelectAnySr,
SelectVdi,
} from 'select-objects'
const compareSrs = createCompare([isSrShared])
@@ -284,13 +288,13 @@ class NewDisk extends Component {
const diskLimit = this._getResourceSetDiskLimit()
const resourceSet = this._getResolvedResourceSet()
const SelectSr_ =
isAdmin || resourceSet == null ? SelectSr : SelectResourceSetsSr
const SelectSr =
isAdmin || resourceSet == null ? SelectAnySr : SelectResourceSetsSr
return (
<form id='newDiskForm'>
<div className='form-group'>
<SelectSr_
<SelectSr
onChange={this.linkState('sr')}
predicate={this._getSrPredicate()}
required

View File

@@ -29,7 +29,7 @@ import {
resolveResourceSet,
} from 'utils'
import {
SelectNetwork,
SelectNetwork as SelectAnyNetwork,
SelectIp,
SelectResourceSetIp,
SelectResourceSetsNetwork,
@@ -53,6 +53,8 @@ import {
createGetObject,
createGetObjectsOfType,
createSelector,
getCheckPermissions,
getResolvedResourceSet,
isAdmin,
} from 'selectors'
@@ -72,22 +74,42 @@ import {
subscribeResourceSets,
} from 'xo'
@addSubscriptions(props => ({
resourceSet: cb =>
subscribeResourceSets(resourceSets =>
cb(find(resourceSets, { id: props.resourceSet }))
),
}))
@connectStore((state, props) => ({
isAdmin: isAdmin(state, props),
resolvedResourceSet: getResolvedResourceSet(
state,
props,
props.resourceSet !== undefined // to get objects as a self user
),
}))
class VifNetwork extends BaseComponent {
_getNetworkPredicate = createSelector(
() => this.props.vif.$pool,
vifPoolId => network => network.$pool === vifPoolId
)
render() {
const { network } = this.props
_onChangeNetwork = network => {
const { resourceSet, vif } = this.props
return setVif(vif, { network, resourceSet: get(() => resourceSet.id) })
}
render() {
const { isAdmin, network, resolvedResourceSet } = this.props
const self = !isAdmin && resolvedResourceSet !== undefined
return (
network !== undefined && (
<XoSelect
onChange={network => setVif(this.props.vif, { network })}
onChange={this._onChangeNetwork}
predicate={this._getNetworkPredicate()}
resourceSet={self ? resolvedResourceSet : undefined}
value={network}
xoType='network'
xoType={self ? 'resourceSetNetwork' : 'network'}
>
{network.name_label}
</XoSelect>
@@ -272,6 +294,9 @@ class VifAllowedIps extends BaseComponent {
}
}
@connectStore(() => ({
checkPermissions: getCheckPermissions,
}))
class VifStatus extends BaseComponent {
componentDidMount() {
getLockingModeValues().then(lockingModeValues =>
@@ -279,6 +304,12 @@ class VifStatus extends BaseComponent {
)
}
_getCanEditVifLockingMode = createSelector(
() => this.props.checkPermissions,
() => this.props.vif.$network,
(checkPermissions, networkId) => checkPermissions(networkId, 'operate')
)
_getIps = createSelector(
() => this.props.vif.allowedIpv4Addresses || EMPTY_ARRAY,
() => this.props.vif.allowedIpv6Addresses || EMPTY_ARRAY,
@@ -369,28 +400,29 @@ class VifStatus extends BaseComponent {
state={vif.attached}
/>{' '}
{this._getNetworkStatus()}{' '}
{isLockingModeEdition ? (
<select
className='form-control'
onBlur={this.toggleState('isLockingModeEdition')}
onChange={this._onChangeVif}
value={vif.lockingMode}
>
{map(this.state.lockingModeValues, lockingMode => (
<option key={lockingMode} value={lockingMode}>
{lockingMode}
</option>
))}
</select>
) : (
<ActionButton
btnStyle='primary'
icon='edit'
handler={this.toggleState('isLockingModeEdition')}
size='small'
tooltip={_('editVifLockingMode')}
/>
)}
{this._getCanEditVifLockingMode() &&
(isLockingModeEdition ? (
<select
className='form-control'
onBlur={this.toggleState('isLockingModeEdition')}
onChange={this._onChangeVif}
value={vif.lockingMode}
>
{map(this.state.lockingModeValues, lockingMode => (
<option key={lockingMode} value={lockingMode}>
{lockingMode}
</option>
))}
</select>
) : (
<ActionButton
btnStyle='primary'
icon='edit'
handler={this.toggleState('isLockingModeEdition')}
size='small'
tooltip={_('editVifLockingMode')}
/>
))}
</div>
)
}
@@ -552,9 +584,8 @@ class NewAclRuleForm extends BaseComponent {
}
}
@addSubscriptions({
plugins: subscribePlugins,
})
@connectStore({ isAdmin })
@addSubscriptions(({ isAdmin }) => isAdmin && { plugins: subscribePlugins })
class AclRuleRow extends Component {
render() {
const { rule, vif, plugins } = this.props
@@ -589,9 +620,8 @@ class AclRuleRow extends Component {
}
}
@addSubscriptions({
plugins: subscribePlugins,
})
@connectStore({ isAdmin })
@addSubscriptions(({ isAdmin }) => isAdmin && { plugins: subscribePlugins })
class AclRulesRows extends BaseComponent {
_newAclRule(vif) {
return confirm({
@@ -700,8 +730,12 @@ const COLUMNS = [
sortCriteria: 'MTU',
},
{
itemRenderer: (vif, userData) => (
<VifNetwork vif={vif} network={userData.networks[vif.$network]} />
itemRenderer: (vif, { networks, resourceSet }) => (
<VifNetwork
vif={vif}
network={networks[vif.$network]}
resourceSet={resourceSet}
/>
),
name: _('vifNetworkLabel'),
sortCriteria: (vif, userData) => userData.networks[vif.$network].name_label,
@@ -871,13 +905,15 @@ class NewVif extends BaseComponent {
const { mac, network } = this.state
const resourceSet = this._getResolvedResourceSet()
const Select_ =
isAdmin || resourceSet == null ? SelectNetwork : SelectResourceSetsNetwork
const SelectNetwork =
isAdmin || resourceSet == null
? SelectAnyNetwork
: SelectResourceSetsNetwork
return (
<form id='newVifForm'>
<div className='form-group'>
<Select_
<SelectNetwork
onChange={this._selectNetwork}
predicate={this._getNetworkPredicate()}
required
@@ -923,7 +959,11 @@ class NewVif extends BaseComponent {
return (state, props) => ({
vifs: getVifs(state, props),
networks: getNetworks(state, props),
networks: getNetworks(
state,
props,
props.vm.resourceSet !== undefined // to get networks as a self user
),
})
})
export default class TabNetwork extends BaseComponent {
@@ -979,6 +1019,7 @@ export default class TabNetwork extends BaseComponent {
columns={COLUMNS}
data-ipsByDevice={this._getIpsByDevice()}
data-networks={networks}
data-resourceSet={vm.resourceSet}
filters={FILTERS}
groupedActions={GROUPED_ACTIONS}
individualActions={INDIVIDUAL_ACTIONS}

View File

@@ -3974,7 +3974,7 @@ bind-property-descriptor@^1.0.0:
dependencies:
lodash "^4.17.4"
bindings@^1.5.0:
bindings@^1.3.0, bindings@^1.5.0:
version "1.5.0"
resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df"
integrity sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==
@@ -12784,7 +12784,7 @@ node-gyp-build@~4.1.0:
resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.1.1.tgz#d7270b5d86717068d114cc57fff352f96d745feb"
integrity sha512-dSq1xmcPDKPZ2EED2S6zw/b9NKsqzXRE6dVr8TVQnI3FJOTteUMuqF3Qqs6LZg+mLGYJWqQzMbIjMtJqTv87nQ==
node-gyp@^3.8.0:
node-gyp@^3.7.0, node-gyp@^3.8.0:
version "3.8.0"
resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-3.8.0.tgz#540304261c330e80d0d5edce253a68cb3964218c"
integrity sha512-3g8lYefrRRzvGeSowdJKAKyks8oUpLEd/DyPV4eMhVlhJ0aNaZqIrNUIPuEWWTAoPqyFkfGrM67MC69baqn6vA==
@@ -17313,6 +17313,14 @@ syntax-error@^1.1.1:
dependencies:
acorn-node "^1.2.0"
syscall@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/syscall/-/syscall-0.2.0.tgz#9308898495dfb5c062ea7a60c46f81f29f532ac4"
integrity sha512-MLlgaLAMbOGKUVlqsLVYnJ4dBZmeE1nza4BVgVgGUr2dPV17tgR79JUPIUybX/EqGm1jywsXSXUPXpNbIXDVCw==
dependencies:
bindings "^1.3.0"
node-gyp "^3.7.0"
table@^5.2.3:
version "5.4.6"
resolved "https://registry.yarnpkg.com/table/-/table-5.4.6.tgz#1292d19500ce3f86053b05f0e8e7e4a3bb21079e"