Compare commits
50 Commits
sdn-contro
...
nr-nbd-pro
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0a7af4b1c7 | ||
|
|
28559cde02 | ||
|
|
6970d48cc3 | ||
|
|
52801c5afc | ||
|
|
7797bce814 | ||
|
|
18762dc624 | ||
|
|
5a828a6465 | ||
|
|
eaa9f36478 | ||
|
|
2b63134bcf | ||
|
|
8dcff63aea | ||
|
|
c2777607be | ||
|
|
9ba2b18fdb | ||
|
|
4ebc10db6a | ||
|
|
610b6c7bb0 | ||
|
|
357333c4e4 | ||
|
|
723334a685 | ||
|
|
b2c218ff83 | ||
|
|
adabd6966d | ||
|
|
b3eb1270dd | ||
|
|
7659a195d3 | ||
|
|
8d2e23f4a8 | ||
|
|
539d7dab5d | ||
|
|
06d43cdb24 | ||
|
|
af7bcf19ab | ||
|
|
7ebeb37881 | ||
|
|
4911bbe3a2 | ||
|
|
e0b6ab3f8a | ||
|
|
8736c2cf9a | ||
|
|
d825c33b55 | ||
|
|
171ecaaf62 | ||
|
|
5e6d5d4eb0 | ||
|
|
3733a3c335 | ||
|
|
7fca6defd6 | ||
|
|
2a270b399e | ||
|
|
64109aee05 | ||
|
|
e1d9395128 | ||
|
|
32eec95c26 | ||
|
|
f41cca45aa | ||
|
|
48eeab974c | ||
|
|
eed44156ae | ||
|
|
1177d9bdd8 | ||
|
|
d151a94285 | ||
|
|
a7fe6453ee | ||
|
|
313eb136f4 | ||
|
|
98591ff83d | ||
|
|
0b9d78560b | ||
|
|
32a930e598 | ||
|
|
edd8512196 | ||
|
|
7a6aec34ae | ||
|
|
009a0c5703 |
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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> { }
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
"lodash": "^4.17.15",
|
||||
"promise-toolbox": "^0.15.0",
|
||||
"proper-lockfile": "^4.1.1",
|
||||
"vhd-lib": "^0.7.2"
|
||||
"vhd-lib": "^0.8.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",
|
||||
|
||||
@@ -25,7 +25,7 @@ export default class MountHandler extends LocalHandler {
|
||||
this._keeper = undefined
|
||||
this._params = {
|
||||
...params,
|
||||
options: [params.options, remote.options]
|
||||
options: [params.options, remote.options ?? params.defaultOptions]
|
||||
.filter(_ => _ !== undefined)
|
||||
.join(','),
|
||||
}
|
||||
|
||||
@@ -2,15 +2,13 @@ import { parse } from 'xo-remote-parser'
|
||||
|
||||
import MountHandler from './_mount'
|
||||
|
||||
const DEFAULT_NFS_OPTIONS = 'vers=3'
|
||||
|
||||
export default class NfsHandler extends MountHandler {
|
||||
constructor(remote, opts) {
|
||||
const { host, port, path } = parse(remote.url)
|
||||
super(remote, opts, {
|
||||
type: 'nfs',
|
||||
device: `${host}${port !== undefined ? ':' + port : ''}:${path}`,
|
||||
options: DEFAULT_NFS_OPTIONS,
|
||||
defaultOptions: 'vers=3',
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<!-- DO NOT EDIT MANUALLY, THIS FILE HAS BEEN GENERATED -->
|
||||
|
||||
# @xen-orchestra/openflow [](https://travis-ci.org/vatesfr/xen-orchestra)
|
||||
# @xen-orchestra/openflow
|
||||
|
||||
[](https://npmjs.org/package/@xen-orchestra/openflow)  [](https://bundlephobia.com/result?p=@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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -31,5 +31,10 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@vates/read-chunk": "^0.1.0"
|
||||
}
|
||||
},
|
||||
"author": {
|
||||
"name": "Vates SAS",
|
||||
"url": "https://vates.fr"
|
||||
},
|
||||
"license": "ISC"
|
||||
}
|
||||
|
||||
86
CHANGELOG.md
86
CHANGELOG.md
@@ -1,11 +1,81 @@
|
||||
# ChangeLog
|
||||
|
||||
## **5.51.0** (2020-09-30)
|
||||
## **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
|
||||
|
||||
- [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)
|
||||
|
||||

|
||||
|
||||
### 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
|
||||
|
||||
@@ -7,23 +7,14 @@
|
||||
|
||||
> 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))
|
||||
- [LDAP] Prevent LDAP-provided groups from being edited from XO [#1884](https://github.com/vatesfr/xen-orchestra/issues/1884) (PR [#5351](https://github.com/vatesfr/xen-orchestra/pull/5351))
|
||||
- [Licensing] Allow Free and Starter users to copy VMs and create a VM from snapshot on the same pool [#4890](https://github.com/vatesfr/xen-orchestra/issues/4890) (PR [5333](https://github.com/vatesfr/xen-orchestra/pull/5333))
|
||||
|
||||
### 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))
|
||||
- [Remotes/NFS] Only mount with `vers=3` when no other options [#4940](https://github.com/vatesfr/xen-orchestra/issues/4940) (PR [#5354](https://github.com/vatesfr/xen-orchestra/pull/5354))
|
||||
|
||||
### Packages to release
|
||||
|
||||
@@ -42,7 +33,6 @@
|
||||
>
|
||||
> 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 patch
|
||||
- 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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
"exec-promise": "^0.7.0",
|
||||
"getopts": "^2.2.3",
|
||||
"struct-fu": "^1.2.0",
|
||||
"vhd-lib": "^0.7.2"
|
||||
"vhd-lib": "^0.8.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.0.0",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": false,
|
||||
"name": "vhd-lib",
|
||||
"version": "0.7.2",
|
||||
"version": "0.8.0",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"description": "Primitives for VHD file handling",
|
||||
"keywords": [],
|
||||
|
||||
@@ -307,19 +307,15 @@ export default class Vhd {
|
||||
return this._write(blockTable.slice(i, i + 4), this.header.tableOffset + i)
|
||||
}
|
||||
|
||||
// Make a new empty block at vhd end.
|
||||
// Update block allocation table in context and in file.
|
||||
// Allocate a new uninitialized block in the BAT
|
||||
async _createBlock(blockId) {
|
||||
assert.strictEqual(this._getBatEntry(blockId), BLOCK_UNUSED)
|
||||
|
||||
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._setBatEntry(blockId, blockAddr),
|
||||
])
|
||||
await this._setBatEntry(blockId, blockAddr)
|
||||
|
||||
return blockAddr
|
||||
}
|
||||
|
||||
@@ -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.8.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",
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"predev": "yarn run prebuild",
|
||||
"prepublishOnly": "yarn run build"
|
||||
},
|
||||
"version": "1.0.3",
|
||||
"version": "1.0.4",
|
||||
"engines": {
|
||||
"node": ">=8.10"
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
|
||||
85
packages/xo-server/docs/file-restoration.md
Normal file
85
packages/xo-server/docs/file-restoration.md
Normal 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
|
||||
```
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "xo-server",
|
||||
"version": "5.67.0",
|
||||
"version": "5.70.0",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"description": "Server part of Xen-Orchestra",
|
||||
"keywords": [
|
||||
@@ -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.8.0",
|
||||
"ws": "^7.1.2",
|
||||
"xdg-basedir": "^4.0.0",
|
||||
"xen-api": "^0.29.0",
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { forbiddenOperation } from 'xo-common/api-errors'
|
||||
|
||||
export async function create({ name }) {
|
||||
return (await this.createGroup({ name })).id
|
||||
}
|
||||
@@ -51,6 +53,13 @@ setUsers.params = {
|
||||
|
||||
// adds the user id to group.users
|
||||
export async function addUser({ id, userId }) {
|
||||
const group = await this.getGroup(id)
|
||||
if (group.provider !== undefined) {
|
||||
throw forbiddenOperation(
|
||||
'add user',
|
||||
'cannot add user to synchronized group'
|
||||
)
|
||||
}
|
||||
await this.addUserToGroup(userId, id)
|
||||
}
|
||||
|
||||
@@ -65,6 +74,13 @@ addUser.params = {
|
||||
|
||||
// remove the user id from group.users
|
||||
export async function removeUser({ id, userId }) {
|
||||
const group = await this.getGroup(id)
|
||||
if (group.provider !== undefined) {
|
||||
throw forbiddenOperation(
|
||||
'remove user',
|
||||
'cannot remove user from synchronized group'
|
||||
)
|
||||
}
|
||||
await this.removeUserFromGroup(userId, id)
|
||||
}
|
||||
|
||||
@@ -80,6 +96,15 @@ removeUser.params = {
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export async function set({ id, name }) {
|
||||
if (name !== undefined) {
|
||||
const group = await this.getGroup(id)
|
||||
if (group.provider !== undefined) {
|
||||
throw forbiddenOperation(
|
||||
'set group name',
|
||||
'cannot edit synchronized group'
|
||||
)
|
||||
}
|
||||
}
|
||||
await this.updateGroup(id, { name })
|
||||
}
|
||||
|
||||
|
||||
@@ -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'],
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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],
|
||||
}
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
isEmpty,
|
||||
noop,
|
||||
omit,
|
||||
once,
|
||||
uniq,
|
||||
} from 'lodash'
|
||||
import { satisfies as versionSatisfies } from 'semver'
|
||||
@@ -50,7 +51,7 @@ import {
|
||||
pDelay,
|
||||
pFinally,
|
||||
promisifyAll,
|
||||
pSettle,
|
||||
pSettle, pFromCallback,
|
||||
} from '../utils'
|
||||
|
||||
import mixins from './mixins'
|
||||
@@ -70,6 +71,10 @@ import {
|
||||
prepareXapiParam,
|
||||
} from './utils'
|
||||
import { createVhdStreamWithLength } from 'vhd-lib'
|
||||
import * as tls from 'tls'
|
||||
import * as net from 'net'
|
||||
import readChunk from './readChunk'
|
||||
import { pingNbdServer } from './nbd-client'
|
||||
|
||||
const log = createLogger('xo:xapi')
|
||||
|
||||
@@ -342,6 +347,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 +1222,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 +1244,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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2014,7 +2049,7 @@ export default class Xapi extends XapiBase {
|
||||
}
|
||||
|
||||
@cancelable
|
||||
_exportVdi($cancelToken, vdi, base, format = VDI_FORMAT_VHD) {
|
||||
async _exportVdi($cancelToken, vdi, base, format = VDI_FORMAT_VHD) {
|
||||
const query = {
|
||||
format,
|
||||
vdi: vdi.$ref,
|
||||
@@ -2028,7 +2063,13 @@ export default class Xapi extends XapiBase {
|
||||
base ? ` (from base ${vdi.name_label})` : ''
|
||||
}`
|
||||
)
|
||||
|
||||
const nbds = await this.call('VDI.get_nbd_info', vdi.$ref)
|
||||
if (nbds.length > 0) {
|
||||
const nbd = nbds[0]
|
||||
console.log('NBD', nbd)
|
||||
await pingNbdServer(nbd)
|
||||
}
|
||||
throw Error('STOP')
|
||||
return this.getResource($cancelToken, '/export_raw_vdi/', {
|
||||
query,
|
||||
task: this.createTask('VDI Export', vdi.name_label),
|
||||
|
||||
138
packages/xo-server/src/xapi/nbd-client.js
Normal file
138
packages/xo-server/src/xapi/nbd-client.js
Normal file
@@ -0,0 +1,138 @@
|
||||
import net from 'net'
|
||||
import { pFromCallback } from '../utils'
|
||||
import readChunk from './readChunk'
|
||||
import tls from 'tls'
|
||||
import * as assert from 'assert'
|
||||
|
||||
const OK_REPLIES = {
|
||||
NBD_REP_ACK: 1,
|
||||
NBD_REP_SERVER: 2,
|
||||
NBD_REP_INFO: 3,
|
||||
NBD_REP_META_CONTEXT: 4
|
||||
}
|
||||
|
||||
const ERR_PREFIX = Math.pow(2, 31)
|
||||
|
||||
const ERR_REPLIES = {
|
||||
NBD_REP_ERR_UNSUP: ERR_PREFIX + 1,
|
||||
NBD_REP_ERR_POLICY: ERR_PREFIX + 2,
|
||||
NBD_REP_ERR_INVALID: ERR_PREFIX + 3,
|
||||
NBD_REP_ERR_PLATFORM: ERR_PREFIX + 4,
|
||||
NBD_REP_ERR_TLS_REQD: ERR_PREFIX + 5,
|
||||
NBD_REP_ERR_UNKNOWN: ERR_PREFIX + 6,
|
||||
NBD_REP_ERR_SHUTDOWN: ERR_PREFIX + 7,
|
||||
NBD_REP_ERR_BLOCK_SIZE_REQD: ERR_PREFIX + 8,
|
||||
NBD_REP_ERR_TOO_BIG: ERR_PREFIX + 9,
|
||||
}
|
||||
|
||||
const ALL_REPLIES = { ...OK_REPLIES, ...ERR_REPLIES }
|
||||
|
||||
const BACK_RESPONSES = []
|
||||
Object.entries(ALL_REPLIES).forEach(e => {BACK_RESPONSES[e[1]] = e[0]})
|
||||
|
||||
const OPTIONS_TYPES = {
|
||||
NBD_OPT_EXPORT_NAME: 1,
|
||||
NBD_OPT_ABORT: 2,
|
||||
NBD_OPT_LIST: 3,
|
||||
NBD_OPT_PEEK_EXPORT: 4,
|
||||
NBD_OPT_STARTTLS: 5,
|
||||
NBD_OPT_INFO: 6,
|
||||
NBD_OPT_GO: 7,
|
||||
NBD_OPT_STRUCTURED_REPLY: 8,
|
||||
NBD_OPT_LIST_META_CONTEXT: 9,
|
||||
NBD_OPT_SET_META_CONTEXT: 10,
|
||||
}
|
||||
|
||||
const BACK_OPTIONS = []
|
||||
Object.entries(OPTIONS_TYPES).forEach(e => {BACK_OPTIONS[e[1]] = e[0]})
|
||||
|
||||
async function sendOption(socket, optionCode, optionDataBuffer = Buffer.alloc(0)) {
|
||||
console.log('SEND OPTION', optionCode, BACK_OPTIONS[optionCode])
|
||||
await pFromCallback(cb => socket.write(Buffer.from('IHAVEOPT', 'ascii'), cb))
|
||||
const responseBuffer = Buffer.alloc(4)
|
||||
responseBuffer.writeInt32BE(optionCode)
|
||||
await pFromCallback(cb => socket.write(responseBuffer, cb))
|
||||
responseBuffer.writeInt32BE(optionDataBuffer.length)
|
||||
await pFromCallback(cb => socket.write(responseBuffer, cb))
|
||||
if (optionDataBuffer.length > 0)
|
||||
await pFromCallback(cb => socket.write(optionDataBuffer, cb))
|
||||
if (optionCode === OPTIONS_TYPES.NBD_OPT_EXPORT_NAME) {
|
||||
const exportLength = await readChunk(socket, 8)
|
||||
console.log('exportLength', exportLength.readBigInt64BE(0))
|
||||
const flags = await readChunk(socket, 2)
|
||||
console.log('transmission flags', flags)
|
||||
// read blank
|
||||
await readChunk(socket, 124)
|
||||
} else {
|
||||
const responseMagic = await readChunk(socket, 8)
|
||||
// compare as string to get around the bigInt issue
|
||||
assert.strictEqual(responseMagic.readBigUInt64BE(0).toString(16), 0x3e889045565a9.toString(16))
|
||||
const responseOption = await readChunk(socket, 4)
|
||||
assert.strictEqual(responseOption.readUInt32BE(0), optionCode)
|
||||
const responseCode = (await readChunk(socket, 4)).readUInt32BE(0)
|
||||
console.log('responseCode', responseCode)
|
||||
console.log('responseCode', BACK_RESPONSES[responseCode])
|
||||
const responseDataLen = (await readChunk(socket, 4)).readUInt32BE(0)
|
||||
|
||||
console.log('datalen', responseDataLen)
|
||||
|
||||
if (responseDataLen > 0) {
|
||||
const data = await readChunk(socket, responseDataLen)
|
||||
console.log('DATA', data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function sendGoOrInfoOption(socket, optionCode, exportName) {
|
||||
assert.strictEqual(optionCode === OPTIONS_TYPES.NBD_OPT_INFO || optionCode === OPTIONS_TYPES.NBD_OPT_GO, true)
|
||||
const nameBuffer = Buffer.from(exportName, 'ascii')
|
||||
const nameLenBuffer = Buffer.alloc(4)
|
||||
nameLenBuffer.writeUInt32BE(nameBuffer.length)
|
||||
const infoRequsetNumberBuffer = Buffer.alloc(2)
|
||||
const dataBuffer = Buffer.concat([nameLenBuffer, nameBuffer, infoRequsetNumberBuffer])
|
||||
return sendOption(socket, optionCode, dataBuffer)
|
||||
}
|
||||
|
||||
export async function pingNbdServer({ exportname, address, port = 10809, cert, subject }) {
|
||||
console.log('pingNbdServer', { exportname, address, port, cert, subject })
|
||||
// https://github.com/xenserver/xs-cbt-samples/blob/master/python2_nbd_client.py
|
||||
const socket = net.connect(port, address)
|
||||
// https://sourceforge.net/p/nbd/code/ci/cdb0bc57f3faefd7a5562d57ad57cd990781c415/
|
||||
socket.setNoDelay()
|
||||
socket.on('error', e => console.log('Socket ERROR', e))
|
||||
socket.on('close', e => console.log('Socket CLOSE', e))
|
||||
await pFromCallback(cb => socket.once('connect', cb))
|
||||
console.log('Connected')
|
||||
const MAGIC = 'NBDMAGIC'
|
||||
const magicChunk = await readChunk(socket, MAGIC.length)
|
||||
console.log('magic', magicChunk.toString('ascii'))
|
||||
const OPTIONS = 'IHAVEOPT'
|
||||
let optChunk = await readChunk(socket, OPTIONS.length)
|
||||
console.log('OPTIONS', optChunk.toString('ascii'))
|
||||
optChunk = await readChunk(socket, 2)
|
||||
const flags = optChunk.readUInt16BE(0)
|
||||
const hasFixedNewStyle = !!(flags & 1)
|
||||
|
||||
console.log('FLAGS New Style', hasFixedNewStyle)
|
||||
|
||||
const CLIENT_FLAGS = 1
|
||||
const responseBuffer = Buffer.alloc(4)
|
||||
responseBuffer.writeInt32BE(CLIENT_FLAGS)
|
||||
await pFromCallback(cb => socket.write(responseBuffer, cb))
|
||||
|
||||
await sendOption(socket, OPTIONS_TYPES.NBD_OPT_STARTTLS)
|
||||
|
||||
console.log('READY FOR TLS', 'readable', socket.readable, socket.readableLength, socket.read())
|
||||
const tlsSocket = tls.connect({ socket, ca: cert, host: subject })
|
||||
tlsSocket.on('error', e => console.log('TLS ERROR', e))
|
||||
await pFromCallback(cb => tlsSocket.once('secureConnect', cb))
|
||||
console.log('*******TLS DONE!', tlsSocket.readableLength, socket.read())
|
||||
|
||||
await sendOption(tlsSocket, OPTIONS_TYPES.NBD_OPT_EXPORT_NAME, Buffer.from(exportname, 'ascii'))
|
||||
//await sendGoOrInfoOption(tlsSocket, OPTIONS_TYPES.NBD_OPT_GO, exportname)
|
||||
for (let i = 0; i < 124; i++) {
|
||||
optChunk = await readChunk(tlsSocket, 1)
|
||||
console.log('byte', i, optChunk)
|
||||
}
|
||||
console.log('HANDSHAKE DONE', optChunk)
|
||||
}
|
||||
51
packages/xo-server/src/xapi/readChunk.js
Normal file
51
packages/xo-server/src/xapi/readChunk.js
Normal file
@@ -0,0 +1,51 @@
|
||||
|
||||
export default async function readChunk(stream, n) {
|
||||
if (n === 0) {
|
||||
return Buffer.alloc(0)
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
const chunks = []
|
||||
let i = 0
|
||||
|
||||
function clean() {
|
||||
stream.removeListener('readable', onReadable)
|
||||
stream.removeListener('end', onEnd)
|
||||
stream.removeListener('error', onError)
|
||||
}
|
||||
|
||||
function resolve2() {
|
||||
clean()
|
||||
resolve(Buffer.concat(chunks, i))
|
||||
}
|
||||
|
||||
const onEnd = resolve2
|
||||
|
||||
function onError(error) {
|
||||
reject(error)
|
||||
clean()
|
||||
}
|
||||
|
||||
function onReadable() {
|
||||
const chunk = stream.read(n - i)
|
||||
if (chunk === null) {
|
||||
return // wait for more data
|
||||
}
|
||||
i += chunk.length
|
||||
chunks.push(chunk)
|
||||
|
||||
if (i === n) {
|
||||
resolve2()
|
||||
} else if (i > n) {
|
||||
throw new RangeError(`read (${i}) more than expected (${n})`)
|
||||
}
|
||||
}
|
||||
|
||||
stream.on('end', onEnd)
|
||||
stream.on('error', onError)
|
||||
stream.on('readable', onReadable)
|
||||
|
||||
if (stream.readable) {
|
||||
onReadable()
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -55,6 +55,7 @@ export default class {
|
||||
})
|
||||
}
|
||||
|
||||
@synchronized
|
||||
async getRemoteHandler(remote) {
|
||||
if (typeof remote === 'string') {
|
||||
remote = await this._getRemote(remote)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -304,7 +304,9 @@ export default class {
|
||||
[providerId]: {
|
||||
id,
|
||||
data:
|
||||
data !== undefined ? data : user.authProviders?.[providerId]?.data,
|
||||
data !== undefined
|
||||
? data
|
||||
: user.authProviders?.[providerId]?.data,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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.8.0",
|
||||
"xml2js": "^0.4.23"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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?',
|
||||
@@ -725,6 +742,7 @@ const messages = {
|
||||
customizeFilters: 'Customize filters',
|
||||
|
||||
// ----- VM actions ------
|
||||
cantInterPoolCopy: 'Interpool copy requires at least Enterprise plan',
|
||||
startVmLabel: 'Start',
|
||||
startVmOnLabel: 'Start on…',
|
||||
startVmOnMissingHostTitle: 'No host selected',
|
||||
@@ -829,6 +847,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 +927,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 +1209,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 +1652,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 +2074,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 +2128,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 +2441,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 +2455,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}}',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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'
|
||||
)
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -1,25 +1,30 @@
|
||||
import _, { messages } from 'intl'
|
||||
import Icon from 'icon'
|
||||
import map from 'lodash/map'
|
||||
import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import { compileTemplate } from '@xen-orchestra/template'
|
||||
import { every } from 'lodash'
|
||||
import { injectIntl } from 'react-intl'
|
||||
|
||||
import BaseComponent from 'base-component'
|
||||
import SingleLineRow from 'single-line-row'
|
||||
import Upgrade from 'xoa-upgrade'
|
||||
import { Col } from 'grid'
|
||||
import { SelectSr } from 'select-objects'
|
||||
import { connectStore } from 'utils'
|
||||
import { isSrWritable } from 'xo'
|
||||
|
||||
import SelectCompression from '../../select-compression'
|
||||
import ZstdChecker from '../../zstd-checker'
|
||||
import { getXoaPlan, STARTER } from '../../xoa-plans'
|
||||
import {
|
||||
createGetObject,
|
||||
createGetObjectsOfType,
|
||||
createSelector,
|
||||
} from '../../selectors'
|
||||
|
||||
const CAN_INTERPOOL_COPY = getXoaPlan().value > STARTER.value
|
||||
|
||||
@connectStore(
|
||||
() => {
|
||||
const getVms = createGetObjectsOfType((_, props) => props.type).pick(
|
||||
@@ -77,6 +82,14 @@ class CopyVmsModalBody extends BaseComponent {
|
||||
})
|
||||
}
|
||||
|
||||
getSrPredicate = createSelector(
|
||||
() => this.props.resolvedVms,
|
||||
vms =>
|
||||
CAN_INTERPOOL_COPY
|
||||
? undefined
|
||||
: sr => isSrWritable(sr) && every(vms, { $poolId: sr.$pool })
|
||||
)
|
||||
|
||||
_onChangeSr = sr => this.setState({ sr })
|
||||
_onChangeNamePattern = event =>
|
||||
this.setState({ namePattern: event.target.value })
|
||||
@@ -89,7 +102,7 @@ class CopyVmsModalBody extends BaseComponent {
|
||||
} = this.props
|
||||
const { compression, copyMode, namePattern, sr } = this.state
|
||||
|
||||
return process.env.XOA_PLAN > 2 ? (
|
||||
return (
|
||||
<div>
|
||||
<SingleLineRow>
|
||||
<Col size={6}>{_('copyVmName')}</Col>
|
||||
@@ -124,8 +137,14 @@ class CopyVmsModalBody extends BaseComponent {
|
||||
<SelectSr
|
||||
disabled={copyMode !== 'fullCopy'}
|
||||
onChange={this.linkState('sr')}
|
||||
predicate={this.getSrPredicate()}
|
||||
value={sr}
|
||||
/>
|
||||
{!CAN_INTERPOOL_COPY && (
|
||||
<p className='text-muted'>
|
||||
<Icon icon='info' /> {_('cantInterPoolCopy')}
|
||||
</p>
|
||||
)}
|
||||
</Col>
|
||||
</SingleLineRow>
|
||||
<SingleLineRow className='mt-1'>
|
||||
@@ -160,10 +179,6 @@ class CopyVmsModalBody extends BaseComponent {
|
||||
</SingleLineRow>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<Upgrade place='vmCopy' available={3} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -141,6 +141,10 @@
|
||||
@extend .fa;
|
||||
@extend .fa-download;
|
||||
}
|
||||
&-upload {
|
||||
@extend .fa;
|
||||
@extend .fa-upload;
|
||||
}
|
||||
&-shortcuts {
|
||||
@extend .fa;
|
||||
@extend .fa-keyboard-o;
|
||||
|
||||
@@ -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>
|
||||
),
|
||||
|
||||
@@ -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'
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -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 -------------------------------------------------------------------
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -233,7 +233,8 @@ export default class PoolItem extends Component {
|
||||
<span>
|
||||
{hostMetrics.count}x <Icon icon='host' /> {nVms}x{' '}
|
||||
<Icon icon='vm' /> {nSrs}x <Icon icon='sr' /> {hostMetrics.cpus}
|
||||
x <Icon icon='cpu' /> {formatSizeShort(hostMetrics.memoryTotal)}
|
||||
x <Icon icon='cpu' /> {formatSizeShort(hostMetrics.memoryTotal)}{' '}
|
||||
<Icon icon='memory' />
|
||||
</span>
|
||||
</Col>
|
||||
<Col mediumSize={4} className={styles.itemExpanded}>
|
||||
|
||||
@@ -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' />
|
||||
}
|
||||
|
||||
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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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 }),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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: '',
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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 }
|
||||
@@ -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({
|
||||
@@ -40,7 +40,7 @@ class UserDisplay extends Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { id, users } = this.props
|
||||
const { id, users, canRemove } = this.props
|
||||
|
||||
return (
|
||||
<span>
|
||||
@@ -51,13 +51,15 @@ class UserDisplay extends Component {
|
||||
>
|
||||
</em>
|
||||
)}{' '}
|
||||
<ActionButton
|
||||
className='pull-right'
|
||||
btnStyle='primary'
|
||||
size='small'
|
||||
icon='remove'
|
||||
handler={this._removeUser}
|
||||
/>
|
||||
{canRemove && (
|
||||
<ActionButton
|
||||
className='pull-right'
|
||||
btnStyle='primary'
|
||||
size='small'
|
||||
icon='remove'
|
||||
handler={this._removeUser}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
@@ -88,7 +90,11 @@ class GroupMembersDisplay extends Component {
|
||||
<ul className='list-group'>
|
||||
{map(group.users, user => (
|
||||
<li className='list-group-item' key={user}>
|
||||
<UserDisplay id={user} group={group} />
|
||||
<UserDisplay
|
||||
id={user}
|
||||
group={group}
|
||||
canRemove={group.provider === undefined}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
@@ -107,9 +113,15 @@ const getPredicate = users => entity =>
|
||||
const GROUP_COLUMNS = [
|
||||
{
|
||||
name: _('groupNameColumn'),
|
||||
itemRenderer: group => (
|
||||
<Text value={group.name} onChange={value => setGroupName(group, value)} />
|
||||
),
|
||||
itemRenderer: group =>
|
||||
group.provider === undefined ? (
|
||||
<Text
|
||||
value={group.name}
|
||||
onChange={value => setGroupName(group, value)}
|
||||
/>
|
||||
) : (
|
||||
group.name
|
||||
),
|
||||
sortCriteria: group => group.name,
|
||||
},
|
||||
{
|
||||
@@ -118,13 +130,14 @@ const GROUP_COLUMNS = [
|
||||
},
|
||||
{
|
||||
name: _('addUserToGroupColumn'),
|
||||
itemRenderer: group => (
|
||||
<SelectSubject
|
||||
predicate={getPredicate(group.users)}
|
||||
onChange={user => user && addUserToGroup(user, group)}
|
||||
value={null}
|
||||
/>
|
||||
),
|
||||
itemRenderer: group =>
|
||||
group.provider === undefined ? (
|
||||
<SelectSubject
|
||||
predicate={getPredicate(group.users)}
|
||||
onChange={user => user && addUserToGroup(user, group)}
|
||||
value={null}
|
||||
/>
|
||||
) : null,
|
||||
},
|
||||
]
|
||||
|
||||
@@ -141,6 +154,7 @@ const ACTIONS = [
|
||||
|
||||
@addSubscriptions({
|
||||
groups: subscribeGroups,
|
||||
plugins: subscribePlugins,
|
||||
})
|
||||
@injectIntl
|
||||
export default class Groups extends Component {
|
||||
@@ -153,11 +167,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}
|
||||
|
||||
@@ -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`}>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user