Compare commits
43 Commits
complex-ma
...
nr-copy-fi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
58954b5dbc | ||
|
|
c0470140f1 | ||
|
|
b34b02e4b0 | ||
|
|
72eb7aed3f | ||
|
|
ecacc3a9e5 | ||
|
|
9ce6a6eb09 | ||
|
|
2f55ee9028 | ||
|
|
18762dc624 | ||
|
|
5a828a6465 | ||
|
|
d26c093fe1 | ||
|
|
eaa9f36478 | ||
|
|
2b63134bcf | ||
|
|
8dcff63aea | ||
|
|
c2777607be | ||
|
|
9ba2b18fdb | ||
|
|
4ebc10db6a | ||
|
|
610b6c7bb0 | ||
|
|
c953f34b01 | ||
|
|
69267d0d04 | ||
|
|
3dee6f4247 | ||
|
|
4b715d7d96 | ||
|
|
f3088dbafd | ||
|
|
357333c4e4 | ||
|
|
723334a685 | ||
|
|
b2c218ff83 | ||
|
|
adabd6966d | ||
|
|
b3eb1270dd | ||
|
|
7659a195d3 | ||
|
|
8d2e23f4a8 | ||
|
|
539d7dab5d | ||
|
|
06d43cdb24 | ||
|
|
af7bcf19ab | ||
|
|
7ebeb37881 | ||
|
|
6bafdf3827 | ||
|
|
663d6b4607 | ||
|
|
eeb8049ff5 | ||
|
|
898d787659 | ||
|
|
57c320eaf6 | ||
|
|
64ec631b21 | ||
|
|
79626a3e38 | ||
|
|
b10c5ca6e8 | ||
|
|
9beb9c3ac5 | ||
|
|
d2b06f3ee7 |
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
53
CHANGELOG.md
53
CHANGELOG.md
@@ -1,8 +1,43 @@
|
||||
# ChangeLog
|
||||
|
||||
## **5.52.0** (2020-10-30)
|
||||
|
||||

|
||||
|
||||
### 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)
|
||||
|
||||

|
||||

|
||||
|
||||
### Enhancements
|
||||
|
||||
@@ -30,7 +65,7 @@
|
||||
- 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))
|
||||
|
||||
### Packages to release
|
||||
### Released packages
|
||||
|
||||
- xo-server-sdn-controller 1.0.4
|
||||
- xo-server-backup-reports 0.16.7
|
||||
@@ -63,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
|
||||
@@ -73,9 +108,7 @@
|
||||
|
||||
## **5.50.3** (2020-09-17)
|
||||
|
||||

|
||||
|
||||
### Packages to release
|
||||
### Released packages
|
||||
|
||||
- xo-server-audit 0.8.0
|
||||
|
||||
@@ -91,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
|
||||
@@ -107,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
|
||||
@@ -137,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
|
||||
@@ -154,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
|
||||
|
||||
@@ -7,16 +7,12 @@
|
||||
|
||||
> Users must be able to say: “Nice enhancement, I'm eager to test it”
|
||||
|
||||
- [Host/Advanced] Display installed certificates [#5134](https://github.com/vatesfr/xen-orchestra/issues/5134) (PR [#5319](https://github.com/vatesfr/xen-orchestra/pull/5319))
|
||||
- [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))
|
||||
- [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”
|
||||
|
||||
- [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))
|
||||
|
||||
### Packages to release
|
||||
|
||||
> Packages will be released in the order they are here, therefore, they should
|
||||
@@ -34,8 +30,7 @@
|
||||
>
|
||||
> In case of conflict, the highest (lowest in previous list) `$version` wins.
|
||||
|
||||
- @xen-orchestra/fs minor
|
||||
- vhd-lib minor
|
||||
- @xen-orchestra/audit-core minor
|
||||
- xo-server-audit minor
|
||||
- xo-web minor
|
||||
- xo-server minor
|
||||
- xo-web minor
|
||||
|
||||
BIN
docs/assets/audit_log_configuration.png
Normal file
BIN
docs/assets/audit_log_configuration.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
@@ -278,6 +278,34 @@ Now, your authorized users can create VMs with their SSH keys, grow template dis
|
||||
|
||||

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

|
||||
|
||||
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.
|
||||
|
||||
@@ -40,7 +40,6 @@
|
||||
"jest": {
|
||||
"collectCoverage": true,
|
||||
"moduleNameMapper": {
|
||||
"^.": "./src",
|
||||
"^(@vates/[^/]+)": "$1/src",
|
||||
"^(@xen-orchestra/[^/]+)": "$1/src",
|
||||
"^(value-matcher)": "$1/src",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -307,15 +312,18 @@ export default class Vhd {
|
||||
return this._write(blockTable.slice(i, i + 4), this.header.tableOffset + i)
|
||||
}
|
||||
|
||||
// Allocate a new uninitialized block in the BAT
|
||||
async _createBlock(blockId) {
|
||||
assert.strictEqual(this._getBatEntry(blockId), BLOCK_UNUSED)
|
||||
|
||||
// Make a new empty block at vhd end.
|
||||
// Update block allocation table in context and in file.
|
||||
async _createBlock(blockId, fullBlock = Buffer.alloc(this.fullBlockSize)) {
|
||||
const blockAddr = Math.ceil(this._getEndOfData() / SECTOR_SIZE)
|
||||
|
||||
debug(`create block ${blockId} at ${blockAddr}`)
|
||||
|
||||
await this._setBatEntry(blockId, blockAddr)
|
||||
await Promise.all([
|
||||
// Write an empty block and addr in vhd file.
|
||||
this._write(fullBlock, sectorsToBytes(blockAddr)),
|
||||
this._setBatEntry(blockId, blockAddr),
|
||||
])
|
||||
|
||||
return blockAddr
|
||||
}
|
||||
@@ -338,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))
|
||||
}
|
||||
|
||||
@@ -377,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)) {
|
||||
@@ -403,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.)
|
||||
@@ -481,7 +496,7 @@ export default class Vhd {
|
||||
)
|
||||
const endInBuffer = Math.min(
|
||||
((currentBlock + 1) * this.sectorsPerBlock - offsetSectors) *
|
||||
SECTOR_SIZE,
|
||||
SECTOR_SIZE,
|
||||
buffer.length
|
||||
)
|
||||
let inputBuffer
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "xo-server",
|
||||
"version": "5.68.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",
|
||||
|
||||
@@ -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'],
|
||||
}
|
||||
|
||||
@@ -343,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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "xo-web",
|
||||
"version": "5.72.0",
|
||||
"version": "5.74.0",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"description": "Web interface client for Xen-Orchestra",
|
||||
"keywords": [
|
||||
|
||||
@@ -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',
|
||||
@@ -86,6 +90,11 @@ const messages = {
|
||||
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',
|
||||
@@ -574,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',
|
||||
@@ -656,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?',
|
||||
@@ -832,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',
|
||||
@@ -1193,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',
|
||||
@@ -2057,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',
|
||||
|
||||
@@ -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 />}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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({
|
||||
@@ -2868,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 =>
|
||||
@@ -3020,7 +3064,7 @@ export const setDefaultHomeFilter = (type, name) => {
|
||||
return _setUserPreferences({
|
||||
defaultHomeFilters: {
|
||||
...defaultFilters,
|
||||
[type]: name,
|
||||
[type]: name === null ? undefined : name,
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -3357,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
|
||||
)
|
||||
|
||||
@@ -141,6 +141,10 @@
|
||||
@extend .fa;
|
||||
@extend .fa-download;
|
||||
}
|
||||
&-upload {
|
||||
@extend .fa;
|
||||
@extend .fa-upload;
|
||||
}
|
||||
&-shortcuts {
|
||||
@extend .fa;
|
||||
@extend .fa-keyboard-o;
|
||||
|
||||
@@ -238,6 +238,7 @@ const SchedulePreviewBody = decorate([
|
||||
icon='run-schedule'
|
||||
key='run'
|
||||
size='small'
|
||||
tooltip={_('runBackupJob')}
|
||||
/>
|
||||
)}{' '}
|
||||
{lastRunLog !== undefined && (
|
||||
|
||||
112
packages/xo-web/src/xo-app/host/install-certificate.js
Normal file
112
packages/xo-web/src/xo-app/host/install-certificate.js
Normal 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 }
|
||||
@@ -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>
|
||||
@@ -454,7 +500,20 @@ export default class extends Component {
|
||||
]}
|
||||
{host.certificates !== undefined && (
|
||||
<div>
|
||||
<h3>{_('installedCertificates')}</h3>
|
||||
<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 }) => (
|
||||
|
||||
@@ -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: '',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
12
yarn.lock
12
yarn.lock
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user