Compare commits
57 Commits
fix-home-f
...
icinga2-te
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aa92f0fc93 | ||
|
|
b79605b692 | ||
|
|
ea0fc68a53 | ||
|
|
1ca5c32de3 | ||
|
|
f51bcfa05a | ||
|
|
e1bf68ab38 | ||
|
|
99e03b7ce5 | ||
|
|
cd70d3ea46 | ||
|
|
d387227cef | ||
|
|
2f4530e426 | ||
|
|
4db181d8bf | ||
|
|
9a7a1cc752 | ||
|
|
59ca6c6708 | ||
|
|
fe7901ca7f | ||
|
|
9351b4a5bb | ||
|
|
dfdd0a0496 | ||
|
|
cda39ec256 | ||
|
|
3720a46ff3 | ||
|
|
7ea50ea41e | ||
|
|
60a696916b | ||
|
|
b6a255d96f | ||
|
|
44a0cce7f2 | ||
|
|
f580e0d26f | ||
|
|
6beefe86e2 | ||
|
|
cbada35788 | ||
|
|
44ff2f872d | ||
|
|
2198853662 | ||
|
|
4636109081 | ||
|
|
1c042778b6 | ||
|
|
34b5962eac | ||
|
|
fc7af59eb7 | ||
|
|
7e557ca059 | ||
|
|
1d0cea8ad0 | ||
|
|
5c901d7c1e | ||
|
|
1dffab0bb8 | ||
|
|
ae89e14ea2 | ||
|
|
908255060c | ||
|
|
88278d0041 | ||
|
|
86bfd91c9d | ||
|
|
0ee412ccb9 | ||
|
|
b8bd6ea820 | ||
|
|
98a1ab3033 | ||
|
|
e360f53a40 | ||
|
|
237ec38003 | ||
|
|
30ea1bbf87 | ||
|
|
0d0aef6014 | ||
|
|
1b7441715c | ||
|
|
e3223b6124 | ||
|
|
41fb06187b | ||
|
|
adf0e8ae3b | ||
|
|
42dd1efb41 | ||
|
|
b6a6694abf | ||
|
|
04f2f50d6d | ||
|
|
6d1048e5c5 | ||
|
|
fe722c8b31 | ||
|
|
0326ce1d85 | ||
|
|
183ddb68d3 |
@@ -1,7 +1,6 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
#- stable # disable for now due to an issue of indirect dep upath with Node 9
|
||||
- 8
|
||||
- 12
|
||||
|
||||
# Use containers.
|
||||
# http://docs.travis-ci.com/user/workers/container-based-infrastructure/
|
||||
|
||||
46
@vates/coalesce-calls/README.md
Normal file
46
@vates/coalesce-calls/README.md
Normal file
@@ -0,0 +1,46 @@
|
||||
<!-- DO NOT EDIT MANUALLY, THIS FILE HAS BEEN GENERATED -->
|
||||
|
||||
# @vates/coalesce-calls
|
||||
|
||||
[](https://npmjs.org/package/@vates/coalesce-calls)  [](https://bundlephobia.com/result?p=@vates/coalesce-calls) [](https://npmjs.org/package/@vates/coalesce-calls)
|
||||
|
||||
> Wraps an async function so that concurrent calls will be coalesced
|
||||
|
||||
## Install
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/@vates/coalesce-calls):
|
||||
|
||||
```
|
||||
> npm install --save @vates/coalesce-calls
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```js
|
||||
import { coalesceCalls } from 'coalesce-calls'
|
||||
|
||||
const connect = coalesceCalls(async function () {
|
||||
// async operation
|
||||
})
|
||||
|
||||
connect()
|
||||
|
||||
// the previous promise result will be returned if the operation is not
|
||||
// complete yet
|
||||
connect()
|
||||
```
|
||||
|
||||
## Contributions
|
||||
|
||||
Contributions are _very_ welcomed, either on the documentation or on
|
||||
the code.
|
||||
|
||||
You may:
|
||||
|
||||
- report any [issue](https://github.com/vatesfr/xen-orchestra/issues)
|
||||
you've encountered;
|
||||
- fork and create a pull request.
|
||||
|
||||
## License
|
||||
|
||||
[ISC](https://spdx.org/licenses/ISC) © [Vates SAS](https://vates.fr)
|
||||
13
@vates/coalesce-calls/USAGE.md
Normal file
13
@vates/coalesce-calls/USAGE.md
Normal file
@@ -0,0 +1,13 @@
|
||||
```js
|
||||
import { coalesceCalls } from '@vates/coalesce-calls'
|
||||
|
||||
const connect = coalesceCalls(async function () {
|
||||
// async operation
|
||||
})
|
||||
|
||||
connect()
|
||||
|
||||
// the previous promise result will be returned if the operation is not
|
||||
// complete yet
|
||||
connect()
|
||||
```
|
||||
14
@vates/coalesce-calls/index.js
Normal file
14
@vates/coalesce-calls/index.js
Normal file
@@ -0,0 +1,14 @@
|
||||
exports.coalesceCalls = function (fn) {
|
||||
let promise
|
||||
const clean = () => {
|
||||
promise = undefined
|
||||
}
|
||||
return function () {
|
||||
if (promise !== undefined) {
|
||||
return promise
|
||||
}
|
||||
promise = fn.apply(this, arguments)
|
||||
promise.then(clean, clean)
|
||||
return promise
|
||||
}
|
||||
}
|
||||
33
@vates/coalesce-calls/index.spec.js
Normal file
33
@vates/coalesce-calls/index.spec.js
Normal file
@@ -0,0 +1,33 @@
|
||||
/* eslint-env jest */
|
||||
|
||||
const { coalesceCalls } = require('./')
|
||||
|
||||
const pDefer = () => {
|
||||
const r = {}
|
||||
r.promise = new Promise((resolve, reject) => {
|
||||
r.reject = reject
|
||||
r.resolve = resolve
|
||||
})
|
||||
return r
|
||||
}
|
||||
|
||||
describe('coalesceCalls', () => {
|
||||
it('decorates an async function', async () => {
|
||||
const fn = coalesceCalls(promise => promise)
|
||||
|
||||
const defer1 = pDefer()
|
||||
const promise1 = fn(defer1.promise)
|
||||
const defer2 = pDefer()
|
||||
const promise2 = fn(defer2.promise)
|
||||
|
||||
defer1.resolve('foo')
|
||||
expect(await promise1).toBe('foo')
|
||||
expect(await promise2).toBe('foo')
|
||||
|
||||
const defer3 = pDefer()
|
||||
const promise3 = fn(defer3.promise)
|
||||
|
||||
defer3.resolve('bar')
|
||||
expect(await promise3).toBe('bar')
|
||||
})
|
||||
})
|
||||
38
@vates/coalesce-calls/package.json
Normal file
38
@vates/coalesce-calls/package.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"private": false,
|
||||
"name": "@vates/coalesce-calls",
|
||||
"description": "Wraps an async function so that concurrent calls will be coalesced",
|
||||
"keywords": [
|
||||
"async",
|
||||
"calls",
|
||||
"coalesce",
|
||||
"decorate",
|
||||
"decorator",
|
||||
"merge",
|
||||
"promise",
|
||||
"wrap",
|
||||
"wrapper"
|
||||
],
|
||||
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@vates/coalesce-calls",
|
||||
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
|
||||
"repository": {
|
||||
"directory": "@vates/coalesce-calls",
|
||||
"type": "git",
|
||||
"url": "https://github.com/vatesfr/xen-orchestra.git"
|
||||
},
|
||||
"files": [
|
||||
"index.js"
|
||||
],
|
||||
"author": {
|
||||
"name": "Vates SAS",
|
||||
"url": "https://vates.fr"
|
||||
},
|
||||
"license": "ISC",
|
||||
"version": "0.1.0",
|
||||
"engines": {
|
||||
"node": ">=8.10"
|
||||
},
|
||||
"scripts": {
|
||||
"postversion": "npm publish --access public"
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,7 @@
|
||||
"preferGlobal": true,
|
||||
"dependencies": {
|
||||
"golike-defer": "^0.4.1",
|
||||
"xen-api": "^0.28.5"
|
||||
"xen-api": "^0.29.0"
|
||||
},
|
||||
"scripts": {
|
||||
"postversion": "npm publish"
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
"strip-indent": "^2.0.0",
|
||||
"xdg-basedir": "^3.0.0",
|
||||
"xo-lib": "^0.9.0",
|
||||
"xo-vmdk-to-vhd": "^1.2.0"
|
||||
"xo-vmdk-to-vhd": "^1.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.0.0",
|
||||
|
||||
106
CHANGELOG.md
106
CHANGELOG.md
@@ -1,8 +1,108 @@
|
||||
# ChangeLog
|
||||
|
||||
## **5.48.3** (2020-07-10)
|
||||
|
||||

|
||||
|
||||
### Enhancements
|
||||
|
||||
- [Audit] Logging user actions is now opt-in (PR [#5151](https://github.com/vatesfr/xen-orchestra/pull/5151))
|
||||
- [Settings/Audit] Warn if logging is inactive (PR [#5152](https://github.com/vatesfr/xen-orchestra/pull/5152))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- [Proxy] Don't use configured HTTP proxy to connect to XO proxy
|
||||
- [Backup with proxy] Correctly log job-level errors
|
||||
- [XO] Fix a few broken documentation links (PR [#5146](https://github.com/vatesfr/xen-orchestra/pull/5146))
|
||||
- [Patches] Don't log errors related to missing patches listing (PR [#5149](https://github.com/vatesfr/xen-orchestra/pull/5149))
|
||||
|
||||
### Released packages
|
||||
|
||||
- xo-server-audit 0.6.0
|
||||
- xo-web 5.64.0
|
||||
- xo-server 5.62.1
|
||||
|
||||
## **5.48.2** (2020-07-07)
|
||||
|
||||
### Enhancements
|
||||
|
||||
- [Backup] Better resolution of the "last run log" quick access (PR [#5141](https://github.com/vatesfr/xen-orchestra/pull/5141))
|
||||
- [Patches] Don't check patches on halted XCP-ng hosts (PR [#5140](https://github.com/vatesfr/xen-orchestra/pull/5140))
|
||||
- [XO] Don't check time consistency on halted hosts (PR [#5140](https://github.com/vatesfr/xen-orchestra/pull/5140))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- [Smart backup/edit] Fix "Excluded VMs tags" being reset to the default ones (PR [#5136](https://github.com/vatesfr/xen-orchestra/pull/5136))
|
||||
|
||||
### Released packages
|
||||
|
||||
- xo-web 5.63.0
|
||||
|
||||
## **5.48.1** (2020-07-03)
|
||||
|
||||
### Enhancements
|
||||
|
||||
- [Home] Remove 'tags' filter from the filter selector since tags have their own selector (PR [#5121](https://github.com/vatesfr/xen-orchestra/pull/5121))
|
||||
- [Backup/New] Add "XOA Proxy" to the excluded tags by default (PR [#5128](https://github.com/vatesfr/xen-orchestra/pull/5128))
|
||||
- [Backup/overview] Don't open backup job edition in a new tab (PR [#5130](https://github.com/vatesfr/xen-orchestra/pull/5130))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- [Restore legacy, File restore legacy] Fix mount error in case of existing proxy remotes (PR [#5124](https://github.com/vatesfr/xen-orchestra/pull/5124))
|
||||
- [File restore] Don't fail with `TypeError [ERR_INVALID_ARG_TYPE]` on LVM partitions
|
||||
- [Import/OVA] Fix import of bigger OVA files (>8GB .vmdk disk) (PR [#5129](https://github.com/vatesfr/xen-orchestra/pull/5129))
|
||||
|
||||
### Released packages
|
||||
|
||||
- xo-vmdk-to-vhd 1.2.1
|
||||
- xo-server 5.62.0
|
||||
- xo-web 5.62.0
|
||||
|
||||
## **5.48.0** (2020-06-30)
|
||||
|
||||
### Highlights
|
||||
|
||||
- [VM/Network] Show IP addresses in front of their VIFs [#4882](https://github.com/vatesfr/xen-orchestra/issues/4882) (PR [#5003](https://github.com/vatesfr/xen-orchestra/pull/5003))
|
||||
- [Home/Template] Ability to copy/clone VM templates [#4734](https://github.com/vatesfr/xen-orchestra/issues/4734) (PR [#5006](https://github.com/vatesfr/xen-orchestra/pull/5006))
|
||||
- [VM] Ability to protect VM from accidental deletion [#4773](https://github.com/vatesfr/xen-orchestra/issues/4773) (PR [#5045](https://github.com/vatesfr/xen-orchestra/pull/5045))
|
||||
- [VM] Differentiate PV drivers detection from management agent detection [#4783](https://github.com/vatesfr/xen-orchestra/issues/4783) (PR [#5007](https://github.com/vatesfr/xen-orchestra/pull/5007))
|
||||
- [SR/Advanced, SR selector] Show thin/thick provisioning [#2208](https://github.com/vatesfr/xen-orchestra/issues/2208) (PR [#5081](https://github.com/vatesfr/xen-orchestra/pull/5081))
|
||||
- [Backup/health] Show VM backups with missing jobs, schedules and VMs [#4716](https://github.com/vatesfr/xen-orchestra/issues/4716) (PR [#5062](https://github.com/vatesfr/xen-orchestra/pull/5062))
|
||||
|
||||
### Enhancements
|
||||
|
||||
- [Plugin] Disable test plugin action when the plugin is not loaded (PR [#5038](https://github.com/vatesfr/xen-orchestra/pull/5038))
|
||||
- [VM/bulk copy] Add fast clone option (PR [#5006](https://github.com/vatesfr/xen-orchestra/pull/5006))
|
||||
- [Home/VM] Homogenize the list of backed up VMs with the normal list (PR [#5046](https://github.com/vatesfr/xen-orchestra/pull/5046))
|
||||
- [SR/Disks] Add tooltip for disabled migration (PR [#4884](https://github.com/vatesfr/xen-orchestra/pull/4884))
|
||||
- [Licenses] Ability to move a license from another XOA to the current XOA (PR [#5110](https://github.com/vatesfr/xen-orchestra/pull/5110))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- [VM/Creation] Fix `insufficient space` which could happened when moving and resizing disks (PR [#5044](https://github.com/vatesfr/xen-orchestra/pull/5044))
|
||||
- [VM/General] Fix displayed IPV6 instead of IPV4 in case of an old version of XenServer (PR [#5036](https://github.com/vatesfr/xen-orchestra/pull/5036))
|
||||
- [Host/Load-balancer] Fix VM migration condition: free memory in the destination host must be greater or equal to used VM memory (PR [#5054](https://github.com/vatesfr/xen-orchestra/pull/5054))
|
||||
- [Home] Broken "Import VM" link [#5055](https://github.com/vatesfr/xen-orchestra/issues/5055) (PR [#5056](https://github.com/vatesfr/xen-orchestra/pull/5056))
|
||||
- [Home/SR] Fix inability to edit SRs' name [#5057](https://github.com/vatesfr/xen-orchestra/issues/5057) (PR [#5058](https://github.com/vatesfr/xen-orchestra/pull/5058))
|
||||
- [Backup] Fix huge logs in case of Continuous Replication or Disaster Recovery errors (PR [#5069](https://github.com/vatesfr/xen-orchestra/pull/5069))
|
||||
- [Notification] Fix same notification showing again as unread (PR [#5067](https://github.com/vatesfr/xen-orchestra/pull/5067))
|
||||
- [SDN Controller] Fix broken private network creation when specifiyng a preferred center [#5076](https://github.com/vatesfr/xen-orchestra/issues/5076) (PRs [#5079](https://github.com/vatesfr/xen-orchestra/pull/5079) & [#5080](https://github.com/vatesfr/xen-orchestra/pull/5080))
|
||||
- [Import/VMDK] Import of VMDK disks has been broken since 5.45.0 (PR [#5087](https://github.com/vatesfr/xen-orchestra/pull/5087))
|
||||
- [Remotes] Fix not displayed used/total disk (PR [#5093](https://github.com/vatesfr/xen-orchestra/pull/5093))
|
||||
- [Perf alert] Regroup items with missing stats in one email [#3137](https://github.com/vatesfr/xen-orchestra/issues/3137) (PR [#4413](https://github.com/vatesfr/xen-orchestra/pull/4413))
|
||||
|
||||
### Released packages
|
||||
|
||||
- xo-server-perf-alert 0.2.3
|
||||
- xo-server-audit 0.5.0
|
||||
- xo-server-sdn-controller 0.4.3
|
||||
- xo-server-load-balancer 0.3.3
|
||||
- xo-server 5.61.1
|
||||
- xo-web 5.61.1
|
||||
|
||||
## **5.47.1** (2020-06-02)
|
||||
|
||||

|
||||

|
||||
|
||||
### Bug fixes
|
||||
|
||||
@@ -60,8 +160,6 @@
|
||||
|
||||
## **5.46.0** (2020-04-30)
|
||||
|
||||

|
||||
|
||||
### Highlights
|
||||
|
||||
- [Internationalization] Italian translation (Thanks [@infodavide](https://github.com/infodavide)!) [#4908](https://github.com/vatesfr/xen-orchestra/issues/4908) (PRs [#4931](https://github.com/vatesfr/xen-orchestra/pull/4931) [#4932](https://github.com/vatesfr/xen-orchestra/pull/4932))
|
||||
@@ -318,7 +416,7 @@
|
||||
|
||||
- [Backup NG] Make report recipients configurable in the backup settings [#4581](https://github.com/vatesfr/xen-orchestra/issues/4581) (PR [#4646](https://github.com/vatesfr/xen-orchestra/pull/4646))
|
||||
- [Host] Advanced Live Telemetry (PR [#4680](https://github.com/vatesfr/xen-orchestra/pull/4680))
|
||||
- [Plugin][web hooks](https://xen-orchestra.com/docs/web-hooks.html) [#1946](https://github.com/vatesfr/xen-orchestra/issues/1946) (PR [#3155](https://github.com/vatesfr/xen-orchestra/pull/3155))
|
||||
- [Plugin][web hooks](https://xen-orchestra.com/docs/advanced.html#web-hooks) [#1946](https://github.com/vatesfr/xen-orchestra/issues/1946) (PR [#3155](https://github.com/vatesfr/xen-orchestra/pull/3155))
|
||||
|
||||
### Enhancements
|
||||
|
||||
|
||||
@@ -7,31 +7,10 @@
|
||||
|
||||
> Users must be able to say: “Nice enhancement, I'm eager to test it”
|
||||
|
||||
- [VM/Network] Show IP addresses in front of their VIFs [#4882](https://github.com/vatesfr/xen-orchestra/issues/4882) (PR [#5003](https://github.com/vatesfr/xen-orchestra/pull/5003))
|
||||
- [VM] Ability to protect VM from accidental deletion [#4773](https://github.com/vatesfr/xen-orchestra/issues/4773)
|
||||
- [Plugin] Disable test plugin action when the plugin is not loaded (PR [#5038](https://github.com/vatesfr/xen-orchestra/pull/5038))
|
||||
- [Home/Template] Ability to copy/clone VM templates [#4734](https://github.com/vatesfr/xen-orchestra/issues/4734) (PR [#5006](https://github.com/vatesfr/xen-orchestra/pull/5006))
|
||||
- [VM/bulk copy] Add fast clone option (PR [#5006](https://github.com/vatesfr/xen-orchestra/pull/5006))
|
||||
- [VM] Differentiate PV drivers detection from management agent detection [#4783](https://github.com/vatesfr/xen-orchestra/issues/4783) (PR [#5007](https://github.com/vatesfr/xen-orchestra/pull/5007))
|
||||
- [Home/VM] Homogenize the list of backed up VMs with the normal list (PR [#5046](https://github.com/vatesfr/xen-orchestra/pull/5046)
|
||||
- [SR/Disks] Add tooltip for disabled migration (PR [#4884](https://github.com/vatesfr/xen-orchestra/pull/4884))
|
||||
- [SR/Advanced, SR selector] Show thin/thick provisioning [#2208](https://github.com/vatesfr/xen-orchestra/issues/2208) (PR [#5081](https://github.com/vatesfr/xen-orchestra/pull/5081))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
> Users must be able to say: “I had this issue, happy to know it's fixed”
|
||||
|
||||
- [VM/Creation] Fix `insufficient space` which could happened when moving and resizing disks (PR [#5044](https://github.com/vatesfr/xen-orchestra/pull/5044))
|
||||
- [VM/General] Fix displayed IPV6 instead of IPV4 in case of an old version of XenServer (PR [#5036](https://github.com/vatesfr/xen-orchestra/pull/5036)))
|
||||
- [Host/Load-balancer] Fix VM migration condition: free memory in the destination host must be greater or equal to used VM memory (PR [#5054](https://github.com/vatesfr/xen-orchestra/pull/5054))
|
||||
- [Home] Broken "Import VM" link [#5055](https://github.com/vatesfr/xen-orchestra/issues/5055) (PR [#5056](https://github.com/vatesfr/xen-orchestra/pull/5056))
|
||||
- [Home/SR] Fix inability to edit SRs' name [#5057](https://github.com/vatesfr/xen-orchestra/issues/5057) (PR [#5058](https://github.com/vatesfr/xen-orchestra/pull/5058))
|
||||
- [Backup] Fix huge logs in case of Continuous Replication or Disaster Recovery errors (PR [#5069](https://github.com/vatesfr/xen-orchestra/pull/5069))
|
||||
- [Notification] Fix same notification showing again as unread (PR [#5067](https://github.com/vatesfr/xen-orchestra/pull/5067))
|
||||
- [SDN Controller] Fix broken private network creation when specifiyng a preferred center [#5076](https://github.com/vatesfr/xen-orchestra/issues/5076) (PRs [#5079](https://github.com/vatesfr/xen-orchestra/pull/5079) & [#5080](https://github.com/vatesfr/xen-orchestra/pull/5080))
|
||||
- [Import/VMDK] Import of VMDK disks has been broken since 5.45.0 (PR [#5087](https://github.com/vatesfr/xen-orchestra/pull/5087))
|
||||
- [Remotes] Fix not displayed used/total disk (PR [#5093](https://github.com/vatesfr/xen-orchestra/pull/5093))
|
||||
|
||||
### Packages to release
|
||||
|
||||
> Packages will be released in the order they are here, therefore, they should
|
||||
@@ -48,9 +27,3 @@
|
||||
> - major: if the change breaks compatibility
|
||||
>
|
||||
> In case of conflict, the highest (lowest in previous list) `$version` wins.
|
||||
|
||||
- xo-server-audit minor
|
||||
- xo-server-sdn-controller patch
|
||||
- xo-server-load-balancer patch
|
||||
- xo-server minor
|
||||
- xo-web minor
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 21 KiB |
@@ -16,7 +16,7 @@ Xen Orchestra should be fully functional with any version of these two virtualiz
|
||||
## XCP-ng
|
||||
|
||||
:::tip
|
||||
Xen Orchestra and XCP-ng are mainly edited by the same company ([Vates](https://vates.fr)). That's why you are sure to have the best compatibility with both XCP-ng and XO!
|
||||
Xen Orchestra and XCP-ng are mainly edited by the same company ([Vates](https://vates.fr)). That's why you are sure to have the best compatibility with both XCP-ng and XO! Also, we strongly suggest people to keep using the latest XCP-ng version as far as possible (or N-1).
|
||||
:::
|
||||
|
||||
- XCP-ng 8.1 ✅ 🚀
|
||||
@@ -25,14 +25,9 @@ Xen Orchestra and XCP-ng are mainly edited by the same company ([Vates](https://
|
||||
- XCP-ng 7.5 ✅ ❗
|
||||
- XCP-ng 7.4 ✅ ❗
|
||||
|
||||
:::tip
|
||||
We strongly suggest people to keep using the latest XCP-ng version as far as possible (or N-1).
|
||||
:::
|
||||
|
||||
## Citrix Hypervisor (formerly XenServer)
|
||||
|
||||
Backup restore for large VM disks (>1TiB usage) is [broken on old XenServer versions](https://bugs.xenserver.org/browse/XSO-868) (except 7.1 LTS up-to-date and superior to 7.6).
|
||||
|
||||
- Citrix Hypervisor 8.2 LTS ✅
|
||||
- Citrix Hypervisor 8.1 ✅
|
||||
- Citrix Hypervisor 8.0 ✅
|
||||
- XenServer 7.6 ✅ ❗
|
||||
@@ -46,9 +41,14 @@ Backup restore for large VM disks (>1TiB usage) is [broken on old XenServer vers
|
||||
- XenServer 6.5 ✅ ❗
|
||||
- Random Delta backup issues
|
||||
- XenServer 6.1 and 6.2 ❎ ❗
|
||||
- No Delta backup and CR support
|
||||
- **No official support** due to missing JSON-RPC (only XML, too CPU intensive)
|
||||
- Not compatible with Delta backup and CR
|
||||
- XenServer 5.x ❎ ❗
|
||||
- Basic administration features only
|
||||
- Basic administration features only, **no official support**
|
||||
|
||||
:::warning
|
||||
Backup restore for large VM disks (>1TiB usage) is [broken on old XenServer versions](https://bugs.xenserver.org/browse/XSO-868) (except 7.1 LTS up-to-date and superior to 7.6).
|
||||
:::
|
||||
|
||||
## Others
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
"human-format": "^0.10.0",
|
||||
"lodash": "^4.17.4",
|
||||
"pw": "^0.0.4",
|
||||
"xen-api": "^0.28.5"
|
||||
"xen-api": "^0.29.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.1.5",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": false,
|
||||
"name": "xen-api",
|
||||
"version": "0.28.5",
|
||||
"version": "0.29.0",
|
||||
"license": "ISC",
|
||||
"description": "Connector to the Xen API",
|
||||
"keywords": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "xo-server-audit",
|
||||
"version": "0.4.0",
|
||||
"version": "0.6.0",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"description": "Audit plugin for XO-Server",
|
||||
"keywords": [
|
||||
|
||||
@@ -20,21 +20,28 @@ const DEFAULT_BLOCKED_LIST = {
|
||||
'audit.checkIntegrity': true,
|
||||
'audit.generateFingerprint': true,
|
||||
'audit.getRecords': true,
|
||||
'backup.list': true,
|
||||
'backupNg.getAllJobs': true,
|
||||
'backupNg.getAllLogs': true,
|
||||
'backupNg.listVmBackups': true,
|
||||
'cloud.getResourceCatalog': true,
|
||||
'cloudConfig.getAll': true,
|
||||
'group.getAll': true,
|
||||
'host.isHostServerTimeConsistent': true,
|
||||
'host.isHyperThreadingEnabled': true,
|
||||
'host.stats': true,
|
||||
'ipPool.getAll': true,
|
||||
'job.getAll': true,
|
||||
'log.get': true,
|
||||
'metadataBackup.getAllJobs': true,
|
||||
'network.getBondModes': true,
|
||||
'pif.getIpv4ConfigurationModes': true,
|
||||
'plugin.get': true,
|
||||
'pool.listMissingPatches': true,
|
||||
'proxy.getAll': true,
|
||||
'remote.getAll': true,
|
||||
'remote.getAllInfo': true,
|
||||
'remote.list': true,
|
||||
'resourceSet.getAll': true,
|
||||
'role.getAll': true,
|
||||
'schedule.getAll': true,
|
||||
@@ -47,15 +54,36 @@ const DEFAULT_BLOCKED_LIST = {
|
||||
'system.getServerTimezone': true,
|
||||
'system.getServerVersion': true,
|
||||
'user.getAll': true,
|
||||
'vm.getHaValues': true,
|
||||
'vm.stats': true,
|
||||
'xo.getAllObjects': true,
|
||||
'xoa.getApplianceInfo': true,
|
||||
'xoa.licenses.get': true,
|
||||
'xoa.licenses.getAll': true,
|
||||
'xoa.licenses.getSelf': true,
|
||||
'xoa.supportTunnel.getState': true,
|
||||
'xosan.checkSrCurrentState': true,
|
||||
'xosan.computeXosanPossibleOptions': true,
|
||||
'xosan.getVolumeInfo': true,
|
||||
}
|
||||
|
||||
const LAST_ID = 'lastId'
|
||||
|
||||
// interface Db {
|
||||
// lastId: string
|
||||
// [RecordId: string]: {
|
||||
// data: object
|
||||
// event: string
|
||||
// id: strings
|
||||
// previousId: string
|
||||
// subject: {
|
||||
// userId: string
|
||||
// userIp: string
|
||||
// userName: string
|
||||
// }
|
||||
// time: number
|
||||
// }
|
||||
// }
|
||||
class Db extends Storage {
|
||||
constructor(db) {
|
||||
super()
|
||||
@@ -87,6 +115,16 @@ class Db extends Storage {
|
||||
}
|
||||
}
|
||||
|
||||
export const configurationSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
active: {
|
||||
description: 'Whether to save user actions in the audit log',
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const NAMESPACE = 'audit'
|
||||
class AuditXoPlugin {
|
||||
constructor({ staticConfig, xo }) {
|
||||
@@ -99,6 +137,19 @@ class AuditXoPlugin {
|
||||
|
||||
this._auditCore = undefined
|
||||
this._storage = undefined
|
||||
|
||||
this._listeners = {
|
||||
'xo:audit': this._handleEvent.bind(this),
|
||||
'xo:postCall': this._handleEvent.bind(this, 'apiCall'),
|
||||
}
|
||||
}
|
||||
|
||||
configure({ active = false }, { loaded }) {
|
||||
this._active = active
|
||||
|
||||
if (loaded) {
|
||||
this._addListeners()
|
||||
}
|
||||
}
|
||||
|
||||
async load() {
|
||||
@@ -111,8 +162,7 @@ class AuditXoPlugin {
|
||||
this._storage = undefined
|
||||
})
|
||||
|
||||
this._addListener('xo:postCall', this._handleEvent.bind(this, 'apiCall'))
|
||||
this._addListener('xo:audit', this._handleEvent.bind(this))
|
||||
this._addListeners()
|
||||
|
||||
const exportRecords = this._exportRecords.bind(this)
|
||||
exportRecords.permission = 'admin'
|
||||
@@ -156,34 +206,44 @@ class AuditXoPlugin {
|
||||
}
|
||||
|
||||
unload() {
|
||||
this._removeListeners()
|
||||
this._cleaners.forEach(cleaner => cleaner())
|
||||
this._cleaners.length = 0
|
||||
}
|
||||
|
||||
_addListener(event, listener_) {
|
||||
const listener = async (...args) => {
|
||||
try {
|
||||
await listener_(...args)
|
||||
} catch (error) {
|
||||
log.error(error)
|
||||
}
|
||||
_addListeners(event, listener_) {
|
||||
this._removeListeners()
|
||||
|
||||
if (this._active) {
|
||||
const listeners = this._listeners
|
||||
Object.keys(listeners).forEach(event => {
|
||||
this._xo.addListener(event, listeners[event])
|
||||
})
|
||||
}
|
||||
const xo = this._xo
|
||||
xo.on(event, listener)
|
||||
this._cleaners.push(() => xo.removeListener(event, listener))
|
||||
}
|
||||
|
||||
_handleEvent(event, { userId, userIp, userName, ...data }) {
|
||||
if (event !== 'apiCall' || !this._blockedList[data.method]) {
|
||||
return this._auditCore.add(
|
||||
{
|
||||
userId,
|
||||
userIp,
|
||||
userName,
|
||||
},
|
||||
event,
|
||||
data
|
||||
)
|
||||
_removeListeners() {
|
||||
const listeners = this._listeners
|
||||
Object.keys(listeners).forEach(event => {
|
||||
this._xo.removeListener(event, listeners[event])
|
||||
})
|
||||
}
|
||||
|
||||
async _handleEvent(event, { userId, userIp, userName, ...data }) {
|
||||
try {
|
||||
if (event !== 'apiCall' || !this._blockedList[data.method]) {
|
||||
return await this._auditCore.add(
|
||||
{
|
||||
userId,
|
||||
userIp,
|
||||
userName,
|
||||
},
|
||||
event,
|
||||
data
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
log.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -272,7 +272,7 @@ class BackupReportsXoPlugin {
|
||||
}
|
||||
}
|
||||
|
||||
async _metadataHandler(log, { name: jobName }, schedule, force) {
|
||||
async _metadataHandler(log, job, schedule, force) {
|
||||
const xo = this._xo
|
||||
|
||||
const formatDate = createDateFormatter(schedule?.timezone)
|
||||
@@ -290,7 +290,7 @@ class BackupReportsXoPlugin {
|
||||
`## Global status: ${log.status}`,
|
||||
'',
|
||||
`- **Job ID**: ${log.jobId}`,
|
||||
`- **Job name**: ${jobName}`,
|
||||
`- **Job name**: ${job.name}`,
|
||||
`- **Run ID**: ${log.id}`,
|
||||
...getTemporalDataMarkdown(log.end, log.start, formatDate),
|
||||
n !== 0 && `- **Successes**: ${nSuccesses} / ${n}`,
|
||||
@@ -349,10 +349,12 @@ class BackupReportsXoPlugin {
|
||||
markdown.push('---', '', `*${pkg.name} v${pkg.version}*`)
|
||||
|
||||
return this._sendReport({
|
||||
job,
|
||||
subject: `[Xen Orchestra] ${log.status} − Metadata backup report for ${
|
||||
log.jobName
|
||||
} ${STATUS_ICON[log.status]}`,
|
||||
markdown: toMarkdown(markdown),
|
||||
schedule,
|
||||
success: log.status === 'success',
|
||||
nagiosMarkdown:
|
||||
log.status === 'success'
|
||||
@@ -363,10 +365,10 @@ class BackupReportsXoPlugin {
|
||||
})
|
||||
}
|
||||
|
||||
async _ngVmHandler(log, { name: jobName, settings }, schedule, force) {
|
||||
async _ngVmHandler(log, job, schedule, force) {
|
||||
const xo = this._xo
|
||||
|
||||
const mailReceivers = get(() => settings[''].reportRecipients)
|
||||
const mailReceivers = get(() => job.settings[''].reportRecipients)
|
||||
const { reportWhen, mode } = log.data || {}
|
||||
|
||||
const formatDate = createDateFormatter(schedule?.timezone)
|
||||
@@ -385,12 +387,17 @@ class BackupReportsXoPlugin {
|
||||
'',
|
||||
`*${pkg.name} v${pkg.version}*`,
|
||||
]
|
||||
|
||||
const jobName = job.name
|
||||
|
||||
return this._sendReport({
|
||||
subject: `[Xen Orchestra] ${
|
||||
log.status
|
||||
} − Backup report for ${jobName} ${STATUS_ICON[log.status]}`,
|
||||
job,
|
||||
mailReceivers,
|
||||
markdown: toMarkdown(markdown),
|
||||
schedule,
|
||||
success: false,
|
||||
nagiosMarkdown: `[Xen Orchestra] [${
|
||||
log.status
|
||||
@@ -649,8 +656,10 @@ class BackupReportsXoPlugin {
|
||||
|
||||
markdown.push('---', '', `*${pkg.name} v${pkg.version}*`)
|
||||
return this._sendReport({
|
||||
job,
|
||||
mailReceivers,
|
||||
markdown: toMarkdown(markdown),
|
||||
schedule,
|
||||
subject: `[Xen Orchestra] ${log.status} − Backup report for ${jobName} ${
|
||||
STATUS_ICON[log.status]
|
||||
}`,
|
||||
@@ -724,7 +733,9 @@ class BackupReportsXoPlugin {
|
||||
markdown = markdown.join('\n')
|
||||
return this._sendReport({
|
||||
subject: `[Xen Orchestra] ${globalStatus} ${icon}`,
|
||||
job,
|
||||
markdown,
|
||||
schedule,
|
||||
success: false,
|
||||
nagiosMarkdown: `[Xen Orchestra] [${globalStatus}] Error : ${error.message}`,
|
||||
})
|
||||
@@ -913,6 +924,7 @@ class BackupReportsXoPlugin {
|
||||
markdown = markdown.join('\n')
|
||||
|
||||
return this._sendReport({
|
||||
job,
|
||||
markdown,
|
||||
subject: `[Xen Orchestra] ${globalStatus} − Backup report for ${tag} ${
|
||||
globalSuccess
|
||||
@@ -921,6 +933,7 @@ class BackupReportsXoPlugin {
|
||||
? ICON_FAILURE
|
||||
: ICON_SKIPPED
|
||||
}`,
|
||||
schedule,
|
||||
success: globalSuccess,
|
||||
nagiosMarkdown: globalSuccess
|
||||
? `[Xen Orchestra] [Success] Backup report for ${tag}`
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "xo-server-load-balancer",
|
||||
"version": "0.3.2",
|
||||
"version": "0.3.3",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"description": "Load balancer for XO-Server",
|
||||
"keywords": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "xo-server-perf-alert",
|
||||
"version": "0.2.2",
|
||||
"version": "0.2.3",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"description": "Sends alerts based on performance criteria",
|
||||
"keywords": [],
|
||||
|
||||
@@ -592,22 +592,11 @@ ${monitorBodies.join('\n')}`
|
||||
const monitors = this._getMonitors()
|
||||
for (const monitor of monitors) {
|
||||
const snapshot = await monitor.snapshot()
|
||||
for (const entry of snapshot) {
|
||||
raiseOrLowerAlarm(
|
||||
`${monitor.alarmId}|${entry.uuid}|RRD`,
|
||||
entry.value === undefined,
|
||||
() => {
|
||||
this._sendAlertEmail(
|
||||
'Secondary Issue',
|
||||
`
|
||||
## There was an issue when trying to check ${monitor.title}
|
||||
${entry.listItem}`
|
||||
)
|
||||
},
|
||||
() => {}
|
||||
)
|
||||
|
||||
const entriesWithMissingStats = []
|
||||
for (const entry of snapshot) {
|
||||
if (entry.value === undefined) {
|
||||
entriesWithMissingStats.push(entry)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -656,6 +645,23 @@ ${entry.listItem}
|
||||
lowerAlarm
|
||||
)
|
||||
}
|
||||
|
||||
raiseOrLowerAlarm(
|
||||
`${monitor.alarmId}|${entriesWithMissingStats
|
||||
.map(({ uuid }) => uuid)
|
||||
.sort()
|
||||
.join('|')}|RRD`,
|
||||
entriesWithMissingStats.length !== 0,
|
||||
() => {
|
||||
this._sendAlertEmail(
|
||||
'Secondary Issue',
|
||||
`
|
||||
## There was an issue when trying to check ${monitor.title}
|
||||
${entriesWithMissingStats.map(({ listItem }) => listItem).join('\n')}`
|
||||
)
|
||||
},
|
||||
() => {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"predev": "yarn run prebuild",
|
||||
"prepublishOnly": "yarn run build"
|
||||
},
|
||||
"version": "0.4.2",
|
||||
"version": "0.4.3",
|
||||
"engines": {
|
||||
"node": ">=8.10"
|
||||
},
|
||||
|
||||
8
packages/xo-server-test/config.toml
Normal file
8
packages/xo-server-test/config.toml
Normal file
@@ -0,0 +1,8 @@
|
||||
# Vendor config: DO NOT TOUCH!
|
||||
#
|
||||
# See sample.config.toml to override.
|
||||
|
||||
# After some executions we saw that `deleteTempResources` takes around `21s`.
|
||||
# Then, we chose a large timeout to be sure that all resources created by `xo-server-test`
|
||||
# will be deleted
|
||||
deleteTempResourcesTimeout = '2 minutes'
|
||||
@@ -31,6 +31,7 @@
|
||||
"@babel/plugin-proposal-decorators": "^7.4.0",
|
||||
"@babel/preset-env": "^7.1.6",
|
||||
"@iarna/toml": "^2.2.1",
|
||||
"@vates/parse-duration": "^0.1.0",
|
||||
"app-conf": "^0.7.0",
|
||||
"babel-plugin-lodash": "^3.2.11",
|
||||
"golike-defer": "^0.4.1",
|
||||
|
||||
@@ -4,6 +4,7 @@ import Xo from 'xo-lib'
|
||||
import XoCollection from 'xo-collection'
|
||||
import { defaultsDeep, find, forOwn, pick } from 'lodash'
|
||||
import { fromEvent } from 'promise-toolbox'
|
||||
import { parseDuration } from '@vates/parse-duration'
|
||||
|
||||
import config from './_config'
|
||||
import { getDefaultName } from './_defaultValues'
|
||||
@@ -278,7 +279,11 @@ afterAll(async () => {
|
||||
await xo.close()
|
||||
xo = null
|
||||
})
|
||||
afterEach(() => xo.deleteTempResources())
|
||||
afterEach(async () => {
|
||||
jest.setTimeout(parseDuration(config.deleteTempResourcesTimeout))
|
||||
|
||||
await xo.deleteTempResources()
|
||||
})
|
||||
|
||||
export { xo as default }
|
||||
|
||||
|
||||
@@ -22,16 +22,18 @@ Object {
|
||||
exports[`backupNg .runJob() : fails trying to run a backup job with no matching VMs 1`] = `[JsonRpcError: unknown error from the peer]`;
|
||||
|
||||
exports[`backupNg .runJob() : fails trying to run a backup job with non-existent vm 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"data": Object {
|
||||
"vms": Array [
|
||||
"non-existent-id",
|
||||
],
|
||||
},
|
||||
"message": "missingVms",
|
||||
Object {
|
||||
"data": Object {
|
||||
"id": Any<String>,
|
||||
"type": "VM",
|
||||
},
|
||||
]
|
||||
"end": Any<Number>,
|
||||
"id": Any<String>,
|
||||
"message": Any<String>,
|
||||
"result": Any<Object>,
|
||||
"start": Any<Number>,
|
||||
"status": "failure",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg .runJob() : fails trying to run a backup job without schedule 1`] = `[JsonRpcError: invalid parameters]`;
|
||||
|
||||
@@ -198,7 +198,7 @@ describe('backupNg', () => {
|
||||
it('fails trying to run a backup job with non-existent vm', async () => {
|
||||
jest.setTimeout(7e3)
|
||||
const scheduleTempId = randomId()
|
||||
const { id: jobId } = await xo.createTempBackupNgJob({
|
||||
const jobInput = {
|
||||
schedules: {
|
||||
[scheduleTempId]: getDefaultSchedule(),
|
||||
},
|
||||
@@ -208,16 +208,39 @@ describe('backupNg', () => {
|
||||
vms: {
|
||||
id: 'non-existent-id',
|
||||
},
|
||||
})
|
||||
}
|
||||
const { id: jobId } = await xo.createTempBackupNgJob(jobInput)
|
||||
|
||||
const schedule = await xo.getSchedule({ jobId })
|
||||
expect(typeof schedule).toBe('object')
|
||||
|
||||
await xo.call('backupNg.runJob', { id: jobId, schedule: schedule.id })
|
||||
const [log] = await xo.getBackupLogs({
|
||||
const [
|
||||
{
|
||||
tasks: [vmTask],
|
||||
...log
|
||||
},
|
||||
] = await xo.getBackupLogs({
|
||||
scheduleId: schedule.id,
|
||||
})
|
||||
expect(log.warnings).toMatchSnapshot()
|
||||
|
||||
validateRootTask(log, {
|
||||
data: {
|
||||
mode: jobInput.mode,
|
||||
reportWhen: jobInput.settings[''].reportWhen,
|
||||
},
|
||||
jobId,
|
||||
jobName: jobInput.name,
|
||||
scheduleId: schedule.id,
|
||||
status: 'failure',
|
||||
})
|
||||
|
||||
validateVmTask(vmTask, jobInput.vms.id, {
|
||||
status: 'failure',
|
||||
result: expect.any(Object),
|
||||
})
|
||||
|
||||
expect(noSuchObject.is(vmTask.result)).toBe(true)
|
||||
})
|
||||
|
||||
it('fails trying to run a backup job with a VM without disks', async () => {
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
import assert from 'assert'
|
||||
import assert, { match } from 'assert'
|
||||
import { URL } from 'url'
|
||||
|
||||
const RE = /(\\*)\{([^}]+)\}/
|
||||
const evalTemplate = (template, fn) =>
|
||||
template.replace(RE, ([, escape, key]) => {
|
||||
const n = escape.length
|
||||
const escaped = n % 2 !== 0
|
||||
return escaped ? match.slice(n - 1 / 2) : escaped.slice(n / 2) + fn(key)
|
||||
})
|
||||
|
||||
// =============================================================================
|
||||
|
||||
export const configurationSchema = {
|
||||
@@ -83,7 +91,14 @@ class XoServerIcinga2 {
|
||||
this._url = serverUrl.href
|
||||
|
||||
this._filter =
|
||||
configuration.filter !== undefined ? configuration.filter : ''
|
||||
configuration.filter !== undefined
|
||||
? compileTemplate(configuration.filter, {
|
||||
jobId: _ => _.job.id,
|
||||
jobName: _ => _.job.name,
|
||||
scheduleId: _ => _.schedule.id,
|
||||
scheduleName: _ => _.schedule.name,
|
||||
})
|
||||
: ''
|
||||
this._acceptUnauthorized = configuration.acceptUnauthorized
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "xo-server",
|
||||
"version": "5.60.0",
|
||||
"version": "5.62.1",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"description": "Server part of Xen-Orchestra",
|
||||
"keywords": [
|
||||
@@ -83,7 +83,7 @@
|
||||
"iterable-backoff": "^0.1.0",
|
||||
"jest-worker": "^24.0.0",
|
||||
"js-yaml": "^3.10.0",
|
||||
"json-rpc-peer": "^0.15.3",
|
||||
"json-rpc-peer": "^0.16.0",
|
||||
"json5": "^2.0.1",
|
||||
"kindof": "^2.0.0",
|
||||
"level-party": "^4.0.0",
|
||||
@@ -133,13 +133,13 @@
|
||||
"vhd-lib": "^0.7.2",
|
||||
"ws": "^7.1.2",
|
||||
"xdg-basedir": "^4.0.0",
|
||||
"xen-api": "^0.28.5",
|
||||
"xen-api": "^0.29.0",
|
||||
"xml2js": "^0.4.19",
|
||||
"xo-acl-resolver": "^0.4.1",
|
||||
"xo-collection": "^0.4.1",
|
||||
"xo-common": "^0.5.0",
|
||||
"xo-remote-parser": "^0.5.0",
|
||||
"xo-vmdk-to-vhd": "^1.2.0",
|
||||
"xo-vmdk-to-vhd": "^1.2.1",
|
||||
"yazl": "^2.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -48,6 +48,15 @@ createJob.params = {
|
||||
},
|
||||
}
|
||||
|
||||
export function getSuggestedExcludedTags() {
|
||||
return [
|
||||
'Continuous Replication',
|
||||
'Disaster Recovery',
|
||||
'XOSAN',
|
||||
this._config['xo-proxy'].vmTag,
|
||||
]
|
||||
}
|
||||
|
||||
export function migrateLegacyJob({ id }) {
|
||||
return this.migrateLegacyBackupJob(id)
|
||||
}
|
||||
|
||||
@@ -31,6 +31,10 @@ module.exports = function globMatcher(patterns, opts) {
|
||||
const nAny = anyMustMatch.length
|
||||
|
||||
return function (string) {
|
||||
if (typeof string !== 'string') {
|
||||
return false
|
||||
}
|
||||
|
||||
let i
|
||||
|
||||
for (i = 0; i < nNone; ++i) {
|
||||
|
||||
@@ -13,6 +13,58 @@ import globMatcher from './glob-matcher'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const getLogs = (db, args) => {
|
||||
let stream = highland(db.createReadStream({ reverse: true }))
|
||||
|
||||
if (args.since) {
|
||||
stream = stream.filter(({ value }) => value.time >= args.since)
|
||||
}
|
||||
|
||||
if (args.until) {
|
||||
stream = stream.filter(({ value }) => value.time <= args.until)
|
||||
}
|
||||
|
||||
const fields = Object.keys(args.matchers)
|
||||
|
||||
if (fields.length > 0) {
|
||||
stream = stream.filter(({ value }) => {
|
||||
for (const field of fields) {
|
||||
const fieldValue = get(value, field)
|
||||
if (!args.matchers[field](fieldValue)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
return stream.take(args.limit)
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const deleteLogs = (db, args) =>
|
||||
new Promise(resolve => {
|
||||
let count = 1
|
||||
const cb = () => {
|
||||
if (--count === 0) {
|
||||
resolve()
|
||||
}
|
||||
}
|
||||
|
||||
const deleteEntry = key => {
|
||||
++count
|
||||
db.del(key, cb)
|
||||
}
|
||||
|
||||
getLogs(db, args)
|
||||
.each(({ key }) => {
|
||||
deleteEntry(key)
|
||||
})
|
||||
.done(cb)
|
||||
})
|
||||
|
||||
const GC_KEEP = 2e4
|
||||
|
||||
const gc = (db, args) =>
|
||||
@@ -64,32 +116,7 @@ const gc = (db, args) =>
|
||||
})
|
||||
|
||||
async function printLogs(db, args) {
|
||||
let stream = highland(db.createReadStream({ reverse: true }))
|
||||
|
||||
if (args.since) {
|
||||
stream = stream.filter(({ value }) => value.time >= args.since)
|
||||
}
|
||||
|
||||
if (args.until) {
|
||||
stream = stream.filter(({ value }) => value.time <= args.until)
|
||||
}
|
||||
|
||||
const fields = Object.keys(args.matchers)
|
||||
|
||||
if (fields.length > 0) {
|
||||
stream = stream.filter(({ value }) => {
|
||||
for (const field of fields) {
|
||||
const fieldValue = get(value, field)
|
||||
if (fieldValue === undefined || !args.matchers[field](fieldValue)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
stream = stream.take(args.limit)
|
||||
let stream = getLogs(db, args)
|
||||
|
||||
if (args.json) {
|
||||
stream = highland(stream.pipe(ndjson.serialize())).each(value => {
|
||||
@@ -134,12 +161,18 @@ xo-server-logs [--json] [--limit=<limit>] [--since=<date>] [--until=<date>] [<pa
|
||||
<pattern>
|
||||
Patterns can be used to filter the entries.
|
||||
|
||||
Patterns have the following format \`<field>=<value>\`/\`<field>\`.
|
||||
Patterns have the following format \`<field>=<value>\`, \`<field>\` or \`!<field>\`.
|
||||
|
||||
xo-server-logs --gc
|
||||
|
||||
Remove all but the ${GC_KEEP}th most recent log entries.
|
||||
|
||||
xo-server-logs --delete <predicate>...
|
||||
|
||||
Delete all logs matching the passed predicates.
|
||||
|
||||
For more information on predicates, see the print usage.
|
||||
|
||||
xo-server-logs --repair
|
||||
|
||||
Repair/compact the database.
|
||||
@@ -154,7 +187,7 @@ function getArgs() {
|
||||
const stringArgs = ['since', 'until', 'limit']
|
||||
const args = parseArgs(process.argv.slice(2), {
|
||||
string: stringArgs,
|
||||
boolean: ['help', 'json', 'gc', 'repair'],
|
||||
boolean: ['delete', 'help', 'json', 'gc', 'repair'],
|
||||
default: {
|
||||
limit: 100,
|
||||
json: false,
|
||||
@@ -177,20 +210,41 @@ function getArgs() {
|
||||
const field = value.slice(0, i)
|
||||
const pattern = value.slice(i + 1)
|
||||
|
||||
patterns[pattern]
|
||||
? patterns[field].push(pattern)
|
||||
: (patterns[field] = [pattern])
|
||||
} else if (!patterns[value]) {
|
||||
patterns[value] = null
|
||||
const fieldPatterns = patterns[field]
|
||||
if (fieldPatterns === undefined) {
|
||||
patterns[field] = [pattern]
|
||||
} else if (Array.isArray(fieldPatterns)) {
|
||||
fieldPatterns.push(pattern)
|
||||
} else {
|
||||
throw new Error('cannot mix existence with equality patterns')
|
||||
}
|
||||
} else {
|
||||
const negate = value[0] === '!'
|
||||
if (negate) {
|
||||
value = value.slice(1)
|
||||
}
|
||||
|
||||
if (patterns[value]) {
|
||||
throw new Error('cannot mix existence with equality patterns')
|
||||
}
|
||||
|
||||
patterns[value] = !negate
|
||||
}
|
||||
}
|
||||
|
||||
const trueFunction = () => true
|
||||
const mustExists = value => value !== undefined
|
||||
const mustNotExists = value => value === undefined
|
||||
|
||||
args.matchers = {}
|
||||
|
||||
for (const field in patterns) {
|
||||
const values = patterns[field]
|
||||
args.matchers[field] = values === null ? trueFunction : globMatcher(values)
|
||||
args.matchers[field] =
|
||||
values === true
|
||||
? mustExists
|
||||
: values === false
|
||||
? mustNotExists
|
||||
: globMatcher(values)
|
||||
}
|
||||
|
||||
// Warning: minimist makes one array of values if the same option is used many times.
|
||||
@@ -258,5 +312,9 @@ export default async function main() {
|
||||
}
|
||||
)
|
||||
|
||||
return args.gc ? gc(db) : printLogs(db, args)
|
||||
return args.delete
|
||||
? deleteLogs(db, args)
|
||||
: args.gc
|
||||
? gc(db)
|
||||
: printLogs(db, args)
|
||||
}
|
||||
|
||||
@@ -333,11 +333,16 @@ export default class Api {
|
||||
data.params
|
||||
)}) [${ms(Date.now() - startTime)}] =!> ${error}`
|
||||
|
||||
this._logger.error(message, {
|
||||
...data,
|
||||
duration: Date.now() - startTime,
|
||||
error: serializedError,
|
||||
})
|
||||
// 2020-07-10: Work-around: many kinds of error can be triggered by this
|
||||
// method, which can generates a lot of logs due to the fact that xo-web
|
||||
// uses 5s active subscriptions to call it
|
||||
if (method !== 'pool.listMissingPatches') {
|
||||
this._logger.error(message, {
|
||||
...data,
|
||||
duration: Date.now() - startTime,
|
||||
error: serializedError,
|
||||
})
|
||||
}
|
||||
|
||||
if (xo._config.verboseLogsOnErrors) {
|
||||
log.warn(message, { error })
|
||||
|
||||
@@ -4,9 +4,10 @@ import { forEach, isEmpty, iteratee, sortedIndexBy } from 'lodash'
|
||||
import { debounceWithKey } from '../_pDebounceWithKey'
|
||||
|
||||
const isSkippedError = error =>
|
||||
error.message === 'no disks found' ||
|
||||
error.message === 'no VMs match this pattern' ||
|
||||
error.message === 'unhealthy VDI chain'
|
||||
error != null &&
|
||||
(error.message === 'no disks found' ||
|
||||
error.message === 'no VMs match this pattern' ||
|
||||
error.message === 'unhealthy VDI chain')
|
||||
|
||||
const getStatus = (
|
||||
error,
|
||||
|
||||
@@ -666,7 +666,7 @@ export default class BackupNg {
|
||||
}),
|
||||
])
|
||||
|
||||
return app.callProxyMethod(job.proxy, 'backup.run', {
|
||||
const params = {
|
||||
job: {
|
||||
...job,
|
||||
|
||||
@@ -677,8 +677,64 @@ export default class BackupNg {
|
||||
recordToXapi,
|
||||
remotes,
|
||||
schedule,
|
||||
streamLogs: true,
|
||||
xapis,
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
const logsStream = await app.callProxyMethod(
|
||||
job.proxy,
|
||||
'backup.run',
|
||||
params,
|
||||
{
|
||||
expectStream: true,
|
||||
}
|
||||
)
|
||||
|
||||
const localTaskIds = { __proto__: null }
|
||||
for await (const log of logsStream) {
|
||||
const { event, message, taskId } = log
|
||||
|
||||
const common = {
|
||||
data: log.data,
|
||||
event: 'task.' + event,
|
||||
result: log.result,
|
||||
status: log.status,
|
||||
}
|
||||
|
||||
if (event === 'start') {
|
||||
const { parentId } = log
|
||||
if (parentId === undefined) {
|
||||
// ignore root task (already handled by runJob)
|
||||
localTaskIds[taskId] = runJobId
|
||||
} else {
|
||||
common.parentId = localTaskIds[parentId]
|
||||
localTaskIds[taskId] = logger.notice(message, common)
|
||||
}
|
||||
} else {
|
||||
const localTaskId = localTaskIds[taskId]
|
||||
if (localTaskId === runJobId) {
|
||||
if (event === 'end') {
|
||||
if (log.status === 'failure') {
|
||||
throw log.result
|
||||
}
|
||||
return log.result
|
||||
}
|
||||
} else {
|
||||
common.taskId = localTaskId
|
||||
logger.notice(message, common)
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
} catch (error) {
|
||||
// XO API invalid parameters error
|
||||
if (error.code === 10) {
|
||||
delete params.streamLogs
|
||||
return app.callProxyMethod(job.proxy, 'backup.run', params)
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
const srs = srIds.map(id => app.getXapiObject(id, 'SR'))
|
||||
|
||||
@@ -366,7 +366,7 @@ export default class BackupNgFileRestore {
|
||||
).lv_path
|
||||
)
|
||||
unmountQueue.push(partition.unmount)
|
||||
return { ...partition, unmount }
|
||||
return { __proto__: partition, unmount }
|
||||
}
|
||||
|
||||
return mountPartition(
|
||||
|
||||
@@ -22,6 +22,10 @@ export default class Http {
|
||||
this.setHttpProxy(httpProxy)
|
||||
}
|
||||
|
||||
// TODO: find a way to decide for which addresses the proxy should be used
|
||||
//
|
||||
// For the moment, use this method only to access external resources (e.g.
|
||||
// patches, upgrades, support tunnel)
|
||||
httpRequest(...args) {
|
||||
return hrp(
|
||||
{
|
||||
|
||||
@@ -310,10 +310,7 @@ export default class Jobs {
|
||||
true
|
||||
)
|
||||
|
||||
app.emit('job:terminated', runJobId, {
|
||||
type: job.type,
|
||||
status,
|
||||
})
|
||||
app.emit('job:terminated', { job, runJobId, schedule, status })
|
||||
} finally {
|
||||
this.updateJob({ id, runId: null })::ignoreErrors()
|
||||
delete runningJobs[id]
|
||||
@@ -332,9 +329,7 @@ export default class Jobs {
|
||||
},
|
||||
true
|
||||
)
|
||||
app.emit('job:terminated', runJobId, {
|
||||
type: job.type,
|
||||
})
|
||||
app.emit('job:terminated', { job, runJobId, schedule })
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import cookie from 'cookie'
|
||||
import defer from 'golike-defer'
|
||||
import hrp from 'http-request-plus'
|
||||
import parseSetCookie from 'set-cookie-parser'
|
||||
import pumpify from 'pumpify'
|
||||
import split2 from 'split2'
|
||||
@@ -25,6 +26,16 @@ const synchronizedWrite = synchronized()
|
||||
|
||||
const log = createLogger('xo:proxy')
|
||||
|
||||
const assertProxyAddress = (proxy, address) => {
|
||||
if (address !== undefined) {
|
||||
return address
|
||||
}
|
||||
|
||||
const error = new Error('cannot get the proxy address')
|
||||
error.proxy = omit(proxy, 'authenticationToken')
|
||||
throw error
|
||||
}
|
||||
|
||||
export default class Proxy {
|
||||
constructor(app, conf) {
|
||||
this._app = app
|
||||
@@ -317,24 +328,10 @@ export default class Proxy {
|
||||
await this.callProxyMethod(id, 'system.getServerVersion')
|
||||
}
|
||||
|
||||
async callProxyMethod(id, method, params, expectStream = false) {
|
||||
async callProxyMethod(id, method, params, { expectStream = false } = {}) {
|
||||
const proxy = await this._getProxy(id)
|
||||
|
||||
let ipAddress
|
||||
if (proxy.vmUuid !== undefined) {
|
||||
const vm = this._app.getXapi(proxy.vmUuid).getObjectByUuid(proxy.vmUuid)
|
||||
ipAddress = extractIpFromVmNetworks(vm.$guest_metrics?.networks)
|
||||
} else {
|
||||
ipAddress = proxy.address
|
||||
}
|
||||
|
||||
if (ipAddress === undefined) {
|
||||
const error = new Error('cannot get the proxy IP')
|
||||
error.proxy = omit(proxy, 'authenticationToken')
|
||||
throw error
|
||||
}
|
||||
|
||||
const response = await this._app.httpRequest({
|
||||
const request = {
|
||||
body: format.request(0, method, params),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -343,13 +340,27 @@ export default class Proxy {
|
||||
proxy.authenticationToken
|
||||
),
|
||||
},
|
||||
hostname: ipAddress,
|
||||
method: 'POST',
|
||||
pathname: '/api/v1',
|
||||
protocol: 'https:',
|
||||
rejectUnauthorized: false,
|
||||
timeout: parseDuration(this._xoProxyConf.callTimeout),
|
||||
})
|
||||
}
|
||||
|
||||
if (proxy.vmUuid !== undefined) {
|
||||
const vm = this._app.getXapi(proxy.vmUuid).getObjectByUuid(proxy.vmUuid)
|
||||
|
||||
// use hostname field to avoid issues with IPv6 addresses
|
||||
request.hostname = assertProxyAddress(
|
||||
proxy,
|
||||
extractIpFromVmNetworks(vm.$guest_metrics?.networks)
|
||||
)
|
||||
} else {
|
||||
// use host field so that ports can be passed
|
||||
request.host = assertProxyAddress(proxy, proxy.address)
|
||||
}
|
||||
|
||||
const response = await hrp(request)
|
||||
|
||||
const authenticationToken = parseSetCookie(response, {
|
||||
map: true,
|
||||
@@ -358,15 +369,13 @@ export default class Proxy {
|
||||
await this.updateProxy(id, { authenticationToken })
|
||||
}
|
||||
|
||||
const lines = pumpify(response, split2())
|
||||
const lines = pumpify.obj(response, split2(JSON.parse))
|
||||
const firstLine = await readChunk(lines)
|
||||
|
||||
const { result, error } = parse(String(firstLine))
|
||||
if (error !== undefined) {
|
||||
throw error
|
||||
}
|
||||
const result = parse.result(firstLine)
|
||||
const isStream = result.$responseType === 'ndjson'
|
||||
if (isStream !== expectStream) {
|
||||
lines.destroy()
|
||||
throw new Error(
|
||||
`expect the result ${expectStream ? '' : 'not'} to be a stream`
|
||||
)
|
||||
|
||||
@@ -60,6 +60,10 @@ export default class {
|
||||
remote = await this._getRemote(remote)
|
||||
}
|
||||
|
||||
if (remote.proxy !== undefined) {
|
||||
throw new Error('cannot get handler to proxy remote')
|
||||
}
|
||||
|
||||
if (!remote.enabled) {
|
||||
throw new Error('remote is disabled')
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": false,
|
||||
"name": "xo-vmdk-to-vhd",
|
||||
"version": "1.2.0",
|
||||
"version": "1.2.1",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"description": "JS lib streaming a vmdk file to a vhd",
|
||||
"keywords": [
|
||||
|
||||
@@ -67,10 +67,21 @@ function parseTarHeader(header, stringDeserializer) {
|
||||
if (fileName.length === 0) {
|
||||
return null
|
||||
}
|
||||
const fileSize = parseInt(
|
||||
stringDeserializer(header.slice(124, 124 + 11), 'ascii'),
|
||||
8
|
||||
)
|
||||
const sizeBuffer = header.slice(124, 124 + 12)
|
||||
// size encoding: https://codeistry.wordpress.com/2014/08/14/how-to-parse-a-tar-file/
|
||||
let fileSize = 0
|
||||
// If the leading byte is 0x80 (128), the non-leading bytes of the field are concatenated in big-endian order, with the result being a positive number expressed in binary form.
|
||||
//
|
||||
// Source: https://www.gnu.org/software/tar/manual/html_node/Extensions.html
|
||||
if (new Uint8Array(sizeBuffer)[0] === 128) {
|
||||
for (const byte of new Uint8Array(sizeBuffer.slice(1))) {
|
||||
fileSize *= 256
|
||||
fileSize += byte
|
||||
}
|
||||
} else {
|
||||
fileSize = parseInt(stringDeserializer(sizeBuffer.slice(0, 11), 'ascii'), 8)
|
||||
}
|
||||
|
||||
return { fileName, fileSize }
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "xo-web",
|
||||
"version": "5.60.0",
|
||||
"version": "5.64.0",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"description": "Web interface client for Xen-Orchestra",
|
||||
"keywords": [
|
||||
@@ -145,7 +145,7 @@
|
||||
"xo-common": "^0.5.0",
|
||||
"xo-lib": "^0.8.0",
|
||||
"xo-remote-parser": "^0.5.0",
|
||||
"xo-vmdk-to-vhd": "^1.2.0"
|
||||
"xo-vmdk-to-vhd": "^1.2.1"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "NODE_ENV=production gulp build",
|
||||
|
||||
@@ -8,26 +8,21 @@ export const VM = {
|
||||
homeFilterNonRunningVms: '!power_state:running ',
|
||||
homeFilterHvmGuests: 'virtualizationMode:hvm ',
|
||||
homeFilterRunningVms: 'power_state:running ',
|
||||
homeFilterTags: 'tags:',
|
||||
}
|
||||
|
||||
export const host = {
|
||||
...common,
|
||||
homeFilterRunningHosts: 'power_state:running ',
|
||||
homeFilterTags: 'tags:',
|
||||
}
|
||||
|
||||
export const pool = {
|
||||
...common,
|
||||
homeFilterTags: 'tags:',
|
||||
}
|
||||
|
||||
export const vmTemplate = {
|
||||
...common,
|
||||
homeFilterTags: 'tags:',
|
||||
}
|
||||
|
||||
export const SR = {
|
||||
...common,
|
||||
homeFilterTags: 'tags:',
|
||||
}
|
||||
|
||||
@@ -10,8 +10,8 @@ import { isHostTimeConsistentWithXoaTime } from 'xo'
|
||||
const InconsistentHostTimeWarning = decorate([
|
||||
provideState({
|
||||
computed: {
|
||||
isHostTimeConsistentWithXoaTime: (_, { hostId }) =>
|
||||
isHostTimeConsistentWithXoaTime(hostId),
|
||||
isHostTimeConsistentWithXoaTime: (_, { host }) =>
|
||||
isHostTimeConsistentWithXoaTime(host),
|
||||
},
|
||||
}),
|
||||
injectState,
|
||||
@@ -24,7 +24,7 @@ const InconsistentHostTimeWarning = decorate([
|
||||
])
|
||||
|
||||
InconsistentHostTimeWarning.propTypes = {
|
||||
hostId: PropTypes.string.isRequired,
|
||||
host: PropTypes.object.isRequired,
|
||||
}
|
||||
|
||||
export { InconsistentHostTimeWarning as default }
|
||||
|
||||
@@ -4030,7 +4030,7 @@ export default {
|
||||
vmsToBackup: 'VMs per il backup',
|
||||
|
||||
// Original text: 'Refresh backup list'
|
||||
restoreResfreshList: "Aggiorna l'elenco di backup",
|
||||
refreshBackupList: "Aggiorna l'elenco di backup",
|
||||
|
||||
// Original text: 'Legacy restore'
|
||||
restoreLegacy: 'Ripristino legacy',
|
||||
|
||||
@@ -3430,7 +3430,7 @@ export default {
|
||||
vmsToBackup: "Yedeklenecek VM'ler",
|
||||
|
||||
// Original text: "Refresh backup list"
|
||||
restoreResfreshList: 'Yedek listesini yenile',
|
||||
refreshBackupList: 'Yedek listesini yenile',
|
||||
|
||||
// Original text: "Restore"
|
||||
restoreVmBackups: 'Geri yükle',
|
||||
|
||||
@@ -236,7 +236,6 @@ const messages = {
|
||||
homeFilterNonRunningVms: 'Non running VMs',
|
||||
homeFilterPendingVms: 'Pending VMs',
|
||||
homeFilterHvmGuests: 'HVM guests',
|
||||
homeFilterTags: 'Tags',
|
||||
homeSortBy: 'Sort by',
|
||||
homeSortByCpus: 'CPUs',
|
||||
homeSortByStartTime: 'Start time',
|
||||
@@ -540,6 +539,7 @@ const messages = {
|
||||
deleteOldBackupsFirstMessage:
|
||||
'Delete old backups before backing up the VMs. If the new backup fails, you will lose your old backups.',
|
||||
customTag: 'Custom tag',
|
||||
editJobNotFound: "The job you're trying to edit wasn't found",
|
||||
|
||||
// ------ New Remote -----
|
||||
newRemote: 'New file system remote',
|
||||
@@ -1324,6 +1324,16 @@ const messages = {
|
||||
metricsLoading: 'Loading…',
|
||||
|
||||
// ----- Health -----
|
||||
deleteBackups: 'Delete backup{nBackups, plural, one {} other {s}}',
|
||||
deleteBackupsMessage:
|
||||
'Are you sure you want to delete {nBackups, number} backup{nBackups, plural, one {} other {s}}?',
|
||||
detachedBackups: 'Detached backups',
|
||||
missingJob: 'Missing job',
|
||||
missingVm: 'Missing VM',
|
||||
missingVmInJob: 'This VM does not belong to this job',
|
||||
missingSchedule: 'Missing schedule',
|
||||
noDetachedBackups: 'No backups',
|
||||
reason: 'Reason',
|
||||
orphanedVdis: 'Orphaned snapshot VDIs',
|
||||
orphanedVms: 'Orphaned VMs snapshot',
|
||||
noOrphanedObject: 'No orphans',
|
||||
@@ -1542,7 +1552,7 @@ const messages = {
|
||||
importBackupTitle: 'Import VM',
|
||||
importBackupMessage: 'Starting your backup import',
|
||||
vmsToBackup: 'VMs to backup',
|
||||
restoreResfreshList: 'Refresh backup list',
|
||||
refreshBackupList: 'Refresh backup list',
|
||||
restoreLegacy: 'Legacy restore',
|
||||
restoreFileLegacy: 'Legacy file restore',
|
||||
restoreVmBackups: 'Restore',
|
||||
@@ -2291,6 +2301,8 @@ const messages = {
|
||||
displayAuditRecord: 'Display record',
|
||||
noAuditRecordAvailable: 'No audit record available',
|
||||
refreshAuditRecordsList: 'Refresh records list',
|
||||
auditInactiveUserActionsRecord:
|
||||
'User actions recording is currently inactive',
|
||||
|
||||
// Licenses
|
||||
xosanUnregisteredDisclaimer:
|
||||
@@ -2334,6 +2346,7 @@ const messages = {
|
||||
xosanInstallXoaPlugin: 'Install XOA plugin first',
|
||||
xosanLoadXoaPlugin: 'Load XOA plugin first',
|
||||
bindXoaLicense: 'Activate license',
|
||||
rebindXoaLicense: 'Move license to this XOA',
|
||||
bindXoaLicenseConfirm:
|
||||
'Are you sure you want to activate this license on your XOA? This action is not reversible!',
|
||||
bindXoaLicenseConfirmText: 'activate {licenseType} license',
|
||||
|
||||
@@ -14,6 +14,7 @@ import { createGetObject, createSelector } from './selectors'
|
||||
import { FormattedDate } from 'react-intl'
|
||||
import {
|
||||
isSrWritable,
|
||||
subscribeBackupNgJobs,
|
||||
subscribeProxies,
|
||||
subscribeRemotes,
|
||||
subscribeUsers,
|
||||
@@ -21,11 +22,13 @@ import {
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const unknowItem = (uuid, type) => (
|
||||
const unknowItem = (uuid, type, placeholder) => (
|
||||
<Tooltip content={_('copyUuid', { uuid })}>
|
||||
<CopyToClipboard text={uuid}>
|
||||
<span className='text-muted' style={{ cursor: 'pointer' }}>
|
||||
{_('errorUnknownItem', { type })}
|
||||
{placeholder === undefined
|
||||
? _('errorUnknownItem', { type })
|
||||
: placeholder}
|
||||
</span>
|
||||
</CopyToClipboard>
|
||||
</Tooltip>
|
||||
@@ -142,9 +145,9 @@ export const Vm = decorate([
|
||||
),
|
||||
}
|
||||
}),
|
||||
({ id, vm, container, link, newTab }) => {
|
||||
({ id, vm, container, link, newTab, name }) => {
|
||||
if (vm === undefined) {
|
||||
return unknowItem(id, 'VM')
|
||||
return unknowItem(id, 'VM', name)
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -159,6 +162,7 @@ export const Vm = decorate([
|
||||
Vm.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
link: PropTypes.bool,
|
||||
name: PropTypes.string,
|
||||
newTab: PropTypes.bool,
|
||||
}
|
||||
|
||||
@@ -425,6 +429,41 @@ Proxy.defaultProps = {
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export const BackupJob = decorate([
|
||||
addSubscriptions(({ id }) => ({
|
||||
job: cb =>
|
||||
subscribeBackupNgJobs(jobs => cb(jobs.find(job => job.id === id))),
|
||||
})),
|
||||
({ id, job, link, newTab }) => {
|
||||
if (job === undefined) {
|
||||
return unknowItem(id, 'job')
|
||||
}
|
||||
|
||||
return (
|
||||
<LinkWrapper
|
||||
link={link}
|
||||
newTab={newTab}
|
||||
to={`/backup/overview?s=id:${id}`}
|
||||
>
|
||||
{job.name}
|
||||
</LinkWrapper>
|
||||
)
|
||||
},
|
||||
])
|
||||
|
||||
BackupJob.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
link: PropTypes.bool,
|
||||
newTab: PropTypes.bool,
|
||||
}
|
||||
|
||||
BackupJob.defaultProps = {
|
||||
link: false,
|
||||
newTab: false,
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export const Vgpu = connectStore(() => ({
|
||||
vgpuType: createGetObject((_, props) => props.vgpu.vgpuType),
|
||||
}))(({ vgpu, vgpuType }) => (
|
||||
|
||||
@@ -106,7 +106,7 @@ class TableFilter extends Component {
|
||||
<Tooltip content={_('filterSyntaxLinkTooltip')}>
|
||||
<a
|
||||
className='input-group-addon'
|
||||
href='https://xen-orchestra.com/docs/search.html#filter-syntax'
|
||||
href='https://xen-orchestra.com/docs/manage_infrastructure.html#filter-syntax'
|
||||
rel='noopener noreferrer'
|
||||
target='_blank'
|
||||
>
|
||||
@@ -1001,4 +1001,5 @@ class SortedTable extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
export default withRouter(SortedTable)
|
||||
// withRouter is needed to trigger a render on filtering/sorting items
|
||||
export default withRouter(SortedTable, { withRef: true })
|
||||
|
||||
@@ -821,6 +821,9 @@ export const getHostMissingPatches = async host => {
|
||||
? patches
|
||||
: filter(patches, { paid: false })
|
||||
}
|
||||
if (host.power_state !== 'Running') {
|
||||
return []
|
||||
}
|
||||
try {
|
||||
return await _call('pool.listMissingPatches', { host: hostId })
|
||||
} catch (_) {
|
||||
@@ -847,8 +850,12 @@ export const emergencyShutdownHosts = hosts => {
|
||||
)
|
||||
}
|
||||
|
||||
export const isHostTimeConsistentWithXoaTime = host =>
|
||||
_call('host.isHostServerTimeConsistent', { host: resolveId(host) })
|
||||
export const isHostTimeConsistentWithXoaTime = host => {
|
||||
if (host.power_state !== 'Running') {
|
||||
return true
|
||||
}
|
||||
return _call('host.isHostServerTimeConsistent', { host: resolveId(host) })
|
||||
}
|
||||
|
||||
export const isHyperThreadingEnabledHost = host =>
|
||||
_call('host.isHyperThreadingEnabled', {
|
||||
@@ -2101,6 +2108,9 @@ export const subscribeMetadataBackupJobs = createSubscription(() =>
|
||||
export const createBackupNgJob = props =>
|
||||
_call('backupNg.createJob', props)::tap(subscribeBackupNgJobs.forceRefresh)
|
||||
|
||||
export const getSuggestedExcludedTags = () =>
|
||||
_call('backupNg.getSuggestedExcludedTags')
|
||||
|
||||
export const deleteBackupJobs = async ({
|
||||
backupIds = [],
|
||||
metadataBackupIds = [],
|
||||
@@ -2307,6 +2317,8 @@ export const getResourceSet = id =>
|
||||
|
||||
// Remote ------------------------------------------------------------
|
||||
|
||||
export const getRemotes = () => _call('remote.getAll')
|
||||
|
||||
export const getRemote = remote =>
|
||||
_call('remote.get', resolveIds({ id: remote }))::tap(null, err =>
|
||||
error(_('getRemote'), err.message || String(err))
|
||||
@@ -2363,18 +2375,21 @@ export const editRemote = (remote, { name, options, proxy, url }) =>
|
||||
testRemote(remote).catch(noop)
|
||||
})
|
||||
|
||||
export const listRemote = remote =>
|
||||
_call(
|
||||
'remote.list',
|
||||
resolveIds({ id: remote })
|
||||
)::tap(subscribeRemotes.forceRefresh, err =>
|
||||
error(_('listRemote'), err.message || String(err))
|
||||
)
|
||||
export const listRemote = async remote =>
|
||||
remote.proxy === undefined
|
||||
? _call('remote.list', {
|
||||
id: remote.id,
|
||||
})::tap(subscribeRemotes.forceRefresh, err =>
|
||||
error(_('listRemote'), err.message || String(err))
|
||||
)
|
||||
: []
|
||||
|
||||
export const listRemoteBackups = remote =>
|
||||
_call('backup.list', resolveIds({ remote }))::tap(null, err =>
|
||||
error(_('listRemote'), err.message || String(err))
|
||||
)
|
||||
export const listRemoteBackups = async remote =>
|
||||
remote.proxy === undefined
|
||||
? _call('backup.list', { remote: remote.id })::tap(null, err =>
|
||||
error(_('listRemote'), err.message || String(err))
|
||||
)
|
||||
: []
|
||||
|
||||
export const testRemote = remote =>
|
||||
_call('remote.test', resolveIds({ id: remote }))
|
||||
@@ -3072,7 +3087,7 @@ export const getLicense = (productId, boundObjectId) =>
|
||||
export const unlockXosan = (licenseId, srId) =>
|
||||
_call('xosan.unlock', { licenseId, sr: srId })
|
||||
|
||||
export const selfBindLicense = ({ id, plan }) =>
|
||||
export const selfBindLicense = ({ id, plan, oldXoaId }) =>
|
||||
confirm({
|
||||
title: _('bindXoaLicense'),
|
||||
body: _('bindXoaLicenseConfirm'),
|
||||
@@ -3082,7 +3097,10 @@ export const selfBindLicense = ({ id, plan }) =>
|
||||
},
|
||||
icon: 'unlock',
|
||||
})
|
||||
.then(() => _call('xoa.licenses.bindToSelf', { licenseId: id }), noop)
|
||||
.then(
|
||||
() => _call('xoa.licenses.bindToSelf', { licenseId: id, oldXoaId }),
|
||||
noop
|
||||
)
|
||||
::tap(subscribeSelfLicenses.forceRefresh)
|
||||
|
||||
export const subscribeSelfLicenses = createSubscription(() =>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import _ from 'intl'
|
||||
import addSubscriptions from 'add-subscriptions'
|
||||
import decorate from 'apply-decorators'
|
||||
import defined from '@xen-orchestra/defined'
|
||||
import Icon from 'icon'
|
||||
import React from 'react'
|
||||
import { injectState, provideState } from 'reaclette'
|
||||
import { find, groupBy, keyBy } from 'lodash'
|
||||
@@ -28,11 +30,21 @@ export default decorate([
|
||||
defined(find(jobs, { id }), find(metadataJobs, { id })),
|
||||
schedules: (_, { schedulesByJob, routeParams: { id } }) =>
|
||||
schedulesByJob && keyBy(schedulesByJob[id], 'id'),
|
||||
loading: (_, props) =>
|
||||
props.jobs === undefined ||
|
||||
props.metadataJobs === undefined ||
|
||||
props.schedulesByJob === undefined,
|
||||
},
|
||||
}),
|
||||
injectState,
|
||||
({ state: { job = {}, schedules } }) =>
|
||||
job.type === 'backup' ? (
|
||||
({ state: { job, loading, schedules } }) =>
|
||||
loading ? (
|
||||
_('statusLoading')
|
||||
) : job === undefined ? (
|
||||
<span className='text-danger'>
|
||||
<Icon icon='error' /> {_('editJobNotFound')}
|
||||
</span>
|
||||
) : job.type === 'backup' ? (
|
||||
<New job={job} schedules={schedules} />
|
||||
) : (
|
||||
<Metadata job={job} schedules={schedules} />
|
||||
|
||||
@@ -230,7 +230,7 @@ export default class Restore extends Component {
|
||||
handler={this._refreshBackupList}
|
||||
icon='refresh'
|
||||
>
|
||||
{_('restoreResfreshList')}
|
||||
{_('refreshBackupList')}
|
||||
</ActionButton>
|
||||
</div>
|
||||
<em>
|
||||
|
||||
@@ -1,17 +1,87 @@
|
||||
import _ from 'intl'
|
||||
import Component from 'base-component'
|
||||
import ActionButton from 'action-button'
|
||||
import decorate from 'apply-decorators'
|
||||
import defined, { get } from '@xen-orchestra/defined'
|
||||
import Icon from 'icon'
|
||||
import Link from 'link'
|
||||
import NoObjects from 'no-objects'
|
||||
import React from 'react'
|
||||
import renderXoItem from 'render-xo-item'
|
||||
import renderXoItem, { BackupJob, Vm } from 'render-xo-item'
|
||||
import SortedTable from 'sorted-table'
|
||||
import { addSubscriptions, connectStore } from 'utils'
|
||||
import { addSubscriptions, connectStore, noop } from 'utils'
|
||||
import { Card, CardHeader, CardBlock } from 'card'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import { confirm } from 'modal'
|
||||
import { createPredicate } from 'value-matcher'
|
||||
import { createGetLoneSnapshots, createGetObjectsOfType } from 'selectors'
|
||||
import { deleteSnapshot, deleteSnapshots, subscribeSchedules } from 'xo'
|
||||
import { FormattedRelative, FormattedTime } from 'react-intl'
|
||||
import { forEach, keyBy, omit, toArray } from 'lodash'
|
||||
import { FormattedDate, FormattedRelative, FormattedTime } from 'react-intl'
|
||||
import { injectState, provideState } from 'reaclette'
|
||||
import {
|
||||
deleteBackups,
|
||||
deleteSnapshot,
|
||||
deleteSnapshots,
|
||||
getRemotes,
|
||||
listVmBackups,
|
||||
subscribeBackupNgJobs,
|
||||
subscribeSchedules,
|
||||
} from 'xo'
|
||||
|
||||
const DETACHED_BACKUP_COLUMNS = [
|
||||
{
|
||||
name: _('date'),
|
||||
itemRenderer: backup => (
|
||||
<FormattedDate
|
||||
value={new Date(backup.timestamp)}
|
||||
month='long'
|
||||
day='numeric'
|
||||
year='numeric'
|
||||
hour='2-digit'
|
||||
minute='2-digit'
|
||||
second='2-digit'
|
||||
/>
|
||||
),
|
||||
sortCriteria: 'timestamp',
|
||||
sortOrder: 'desc',
|
||||
},
|
||||
{
|
||||
name: _('vm'),
|
||||
itemRenderer: ({ vm, vmId }) => <Vm id={vmId} link name={vm.name_label} />,
|
||||
sortCriteria: ({ vm, vmId }, { vms }) => defined(vms[vmId], vm).name_label,
|
||||
},
|
||||
{
|
||||
name: _('job'),
|
||||
itemRenderer: ({ jobId }) => <BackupJob id={jobId} link />,
|
||||
sortCriteria: ({ jobId }, { jobs }) => get(() => jobs[jobId].name),
|
||||
},
|
||||
{
|
||||
name: _('jobModes'),
|
||||
valuePath: 'mode',
|
||||
},
|
||||
{
|
||||
name: _('reason'),
|
||||
itemRenderer: ({ reason }) => _(reason),
|
||||
sortCriteria: 'reason',
|
||||
},
|
||||
]
|
||||
|
||||
const DETACHED_BACKUP_ACTIONS = [
|
||||
{
|
||||
handler: (backups, { fetchBackupList }) => {
|
||||
const nBackups = backups.length
|
||||
return confirm({
|
||||
title: _('deleteBackups', { nBackups }),
|
||||
body: _('deleteBackupsMessage', { nBackups }),
|
||||
icon: 'delete',
|
||||
})
|
||||
.then(() => deleteBackups(backups), noop)
|
||||
.then(fetchBackupList)
|
||||
},
|
||||
icon: 'delete',
|
||||
label: backups => _('deleteBackups', { nBackups: backups.length }),
|
||||
level: 'danger',
|
||||
},
|
||||
]
|
||||
|
||||
const SNAPSHOT_COLUMNS = [
|
||||
{
|
||||
@@ -66,69 +136,167 @@ const ACTIONS = [
|
||||
},
|
||||
]
|
||||
|
||||
@addSubscriptions({
|
||||
// used by createGetLoneSnapshots
|
||||
schedules: subscribeSchedules,
|
||||
})
|
||||
@connectStore({
|
||||
loneSnapshots: createGetLoneSnapshots,
|
||||
legacySnapshots: createGetObjectsOfType('VM-snapshot').filter([
|
||||
(() => {
|
||||
const RE = /^(?:XO_DELTA_EXPORT:|XO_DELTA_BASE_VM_SNAPSHOT_|rollingSnapshot_)/
|
||||
return (
|
||||
{ name_label } // eslint-disable-line camelcase
|
||||
) => RE.test(name_label)
|
||||
})(),
|
||||
]),
|
||||
vms: createGetObjectsOfType('VM'),
|
||||
})
|
||||
export default class Health extends Component {
|
||||
render() {
|
||||
return (
|
||||
<Container>
|
||||
<Row className='lone-snapshots'>
|
||||
<Col>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Icon icon='vm' /> {_('vmSnapshotsRelatedToNonExistentBackups')}
|
||||
</CardHeader>
|
||||
<CardBlock>
|
||||
<NoObjects
|
||||
actions={ACTIONS}
|
||||
collection={this.props.loneSnapshots}
|
||||
columns={SNAPSHOT_COLUMNS}
|
||||
component={SortedTable}
|
||||
data-vms={this.props.vms}
|
||||
emptyMessage={_('noSnapshots')}
|
||||
shortcutsTarget='.lone-snapshots'
|
||||
stateUrlParam='s_vm_snapshots'
|
||||
/>
|
||||
</CardBlock>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row className='legacy-snapshots'>
|
||||
<Col>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Icon icon='vm' /> {_('legacySnapshots')}
|
||||
</CardHeader>
|
||||
<CardBlock>
|
||||
<NoObjects
|
||||
actions={ACTIONS}
|
||||
collection={this.props.legacySnapshots}
|
||||
columns={SNAPSHOT_COLUMNS}
|
||||
component={SortedTable}
|
||||
data-vms={this.props.vms}
|
||||
emptyMessage={_('noSnapshots')}
|
||||
shortcutsTarget='.legacy-snapshots'
|
||||
stateUrlParam='s_legacy_vm_snapshots'
|
||||
/>
|
||||
</CardBlock>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
}
|
||||
const Health = decorate([
|
||||
addSubscriptions({
|
||||
// used by createGetLoneSnapshots
|
||||
schedules: cb =>
|
||||
subscribeSchedules(schedules => {
|
||||
cb(keyBy(schedules, 'id'))
|
||||
}),
|
||||
jobs: cb =>
|
||||
subscribeBackupNgJobs(jobs => {
|
||||
cb(keyBy(jobs, 'id'))
|
||||
}),
|
||||
}),
|
||||
connectStore({
|
||||
loneSnapshots: createGetLoneSnapshots,
|
||||
legacySnapshots: createGetObjectsOfType('VM-snapshot').filter([
|
||||
(() => {
|
||||
const RE = /^(?:XO_DELTA_EXPORT:|XO_DELTA_BASE_VM_SNAPSHOT_|rollingSnapshot_)/
|
||||
return (
|
||||
{ name_label } // eslint-disable-line camelcase
|
||||
) => RE.test(name_label)
|
||||
})(),
|
||||
]),
|
||||
vms: createGetObjectsOfType('VM'),
|
||||
}),
|
||||
provideState({
|
||||
initialState: () => ({
|
||||
backupsByRemote: undefined,
|
||||
}),
|
||||
effects: {
|
||||
initialize({ fetchBackupList }) {
|
||||
return fetchBackupList()
|
||||
},
|
||||
async fetchBackupList() {
|
||||
this.state.backupsByRemote = await listVmBackups(
|
||||
toArray(await getRemotes())
|
||||
)
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
detachedBackups: ({ backupsByRemote }, { jobs, vms, schedules }) => {
|
||||
if (jobs === undefined || schedules === undefined) {
|
||||
return []
|
||||
}
|
||||
|
||||
const detachedBackups = []
|
||||
let job
|
||||
forEach(backupsByRemote, backupsByVm => {
|
||||
forEach(backupsByVm, (vmBackups, vmId) => {
|
||||
const vm = vms[vmId]
|
||||
vmBackups.forEach(backup => {
|
||||
const reason =
|
||||
vm === undefined
|
||||
? 'missingVm'
|
||||
: (job = jobs[backup.jobId]) === undefined
|
||||
? 'missingJob'
|
||||
: schedules[backup.scheduleId] === undefined
|
||||
? 'missingSchedule'
|
||||
: !createPredicate(omit(job.vms, 'power_state'))(vm)
|
||||
? 'missingVmInJob'
|
||||
: undefined
|
||||
|
||||
if (reason !== undefined) {
|
||||
detachedBackups.push({
|
||||
...backup,
|
||||
vmId,
|
||||
reason,
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
return detachedBackups
|
||||
},
|
||||
},
|
||||
}),
|
||||
injectState,
|
||||
({
|
||||
effects: { fetchBackupList },
|
||||
jobs,
|
||||
legacySnapshots,
|
||||
loneSnapshots,
|
||||
state: { detachedBackups },
|
||||
vms,
|
||||
}) => (
|
||||
<Container>
|
||||
<Row className='detached-backups'>
|
||||
<Col>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Icon icon='backup' /> {_('detachedBackups')}
|
||||
</CardHeader>
|
||||
<CardBlock>
|
||||
<div className='mb-1'>
|
||||
<ActionButton
|
||||
btnStyle='primary'
|
||||
handler={fetchBackupList}
|
||||
icon='refresh'
|
||||
>
|
||||
{_('refreshBackupList')}
|
||||
</ActionButton>
|
||||
</div>
|
||||
<NoObjects
|
||||
actions={DETACHED_BACKUP_ACTIONS}
|
||||
collection={detachedBackups}
|
||||
columns={DETACHED_BACKUP_COLUMNS}
|
||||
component={SortedTable}
|
||||
data-fetchBackupList={fetchBackupList}
|
||||
data-jobs={jobs}
|
||||
data-vms={vms}
|
||||
emptyMessage={_('noDetachedBackups')}
|
||||
shortcutsTarget='.detached-backups'
|
||||
stateUrlParam='s_detached_backups'
|
||||
/>
|
||||
</CardBlock>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row className='lone-snapshots'>
|
||||
<Col>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Icon icon='vm' /> {_('vmSnapshotsRelatedToNonExistentBackups')}
|
||||
</CardHeader>
|
||||
<CardBlock>
|
||||
<NoObjects
|
||||
actions={ACTIONS}
|
||||
collection={loneSnapshots}
|
||||
columns={SNAPSHOT_COLUMNS}
|
||||
component={SortedTable}
|
||||
data-vms={vms}
|
||||
emptyMessage={_('noSnapshots')}
|
||||
shortcutsTarget='.lone-snapshots'
|
||||
stateUrlParam='s_vm_snapshots'
|
||||
/>
|
||||
</CardBlock>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row className='legacy-snapshots'>
|
||||
<Col>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Icon icon='vm' /> {_('legacySnapshots')}
|
||||
</CardHeader>
|
||||
<CardBlock>
|
||||
<NoObjects
|
||||
actions={ACTIONS}
|
||||
collection={legacySnapshots}
|
||||
columns={SNAPSHOT_COLUMNS}
|
||||
component={SortedTable}
|
||||
data-vms={vms}
|
||||
emptyMessage={_('noSnapshots')}
|
||||
shortcutsTarget='.legacy-snapshots'
|
||||
stateUrlParam='s_legacy_vm_snapshots'
|
||||
/>
|
||||
</CardBlock>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
),
|
||||
])
|
||||
|
||||
export default Health
|
||||
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
deleteSchedule,
|
||||
editBackupNgJob,
|
||||
editSchedule,
|
||||
getSuggestedExcludedTags,
|
||||
isSrWritable,
|
||||
subscribeRemotes,
|
||||
} from 'xo'
|
||||
@@ -152,9 +153,9 @@ const ReportRecipients = decorate([
|
||||
])
|
||||
|
||||
const SR_BACKEND_FAILURE_LINK =
|
||||
'https://xen-orchestra.com/docs/backup_troubleshooting.html#srbackendfailure44-insufficient-space'
|
||||
'https://xen-orchestra.com/docs/backup_troubleshooting.html#sr-backend-failure-44'
|
||||
|
||||
const BACKUP_NG_DOC_LINK = 'https://xen-orchestra.com/docs/backups.html'
|
||||
const BACKUP_NG_DOC_LINK = 'https://xen-orchestra.com/docs/backup.html'
|
||||
|
||||
const ThinProvisionedTip = ({ label }) => (
|
||||
<Tooltip content={_(label)}>
|
||||
@@ -215,7 +216,11 @@ const createDoesRetentionExist = name => {
|
||||
return ({ propSettings, settings = propSettings }) => settings.some(predicate)
|
||||
}
|
||||
|
||||
const getInitialState = ({ preSelectedVmIds, setHomeVmIdsSelection }) => {
|
||||
const getInitialState = ({
|
||||
preSelectedVmIds,
|
||||
setHomeVmIdsSelection,
|
||||
suggestedExcludedTags,
|
||||
}) => {
|
||||
setHomeVmIdsSelection([]) // Clear preselected vmIds
|
||||
return {
|
||||
_displayAdvancedSettings: undefined,
|
||||
@@ -227,7 +232,6 @@ const getInitialState = ({ preSelectedVmIds, setHomeVmIdsSelection }) => {
|
||||
deltaMode: false,
|
||||
drMode: false,
|
||||
name: '',
|
||||
paramsUpdated: false,
|
||||
remotes: [],
|
||||
schedules: {},
|
||||
settings: undefined,
|
||||
@@ -235,9 +239,7 @@ const getInitialState = ({ preSelectedVmIds, setHomeVmIdsSelection }) => {
|
||||
smartMode: false,
|
||||
snapshotMode: false,
|
||||
srs: [],
|
||||
tags: {
|
||||
notValues: ['Continuous Replication', 'Disaster Recovery', 'XOSAN'],
|
||||
},
|
||||
tags: { notValues: suggestedExcludedTags },
|
||||
vms: preSelectedVmIds,
|
||||
}
|
||||
}
|
||||
@@ -282,15 +284,12 @@ const DeleteOldBackupsFirst = ({ handler, handlerParam, value }) => (
|
||||
</ActionButton>
|
||||
)
|
||||
|
||||
export default decorate([
|
||||
const New = decorate([
|
||||
New => props => (
|
||||
<Upgrade place='newBackup' required={2}>
|
||||
<New {...props} />
|
||||
</Upgrade>
|
||||
),
|
||||
addSubscriptions({
|
||||
remotes: subscribeRemotes,
|
||||
}),
|
||||
connectStore(() => ({
|
||||
hostsById: createGetObjectsOfType('host'),
|
||||
poolsById: createGetObjectsOfType('pool'),
|
||||
@@ -301,6 +300,11 @@ export default decorate([
|
||||
provideState({
|
||||
initialState: getInitialState,
|
||||
effects: {
|
||||
initialize: function ({ updateParams }) {
|
||||
if (this.state.edition) {
|
||||
updateParams()
|
||||
}
|
||||
},
|
||||
createJob: () => async state => {
|
||||
if (state.isJobInvalid) {
|
||||
return {
|
||||
@@ -504,7 +508,6 @@ export default decorate([
|
||||
|
||||
return {
|
||||
name: job.name,
|
||||
paramsUpdated: true,
|
||||
smartMode: job.vms.id === undefined,
|
||||
snapshotMode: some(
|
||||
job.settings,
|
||||
@@ -708,8 +711,7 @@ export default decorate([
|
||||
type: 'VM',
|
||||
}
|
||||
),
|
||||
needUpdateParams: (state, { job, schedules }) =>
|
||||
job !== undefined && schedules !== undefined && !state.paramsUpdated,
|
||||
edition: (_, { job }) => job !== undefined,
|
||||
isJobInvalid: state =>
|
||||
state.missingName ||
|
||||
state.missingVms ||
|
||||
@@ -818,10 +820,6 @@ export default decorate([
|
||||
timeout,
|
||||
} = settings.get('') || {}
|
||||
|
||||
if (state.needUpdateParams) {
|
||||
effects.updateParams()
|
||||
}
|
||||
|
||||
return (
|
||||
<form id={state.formId}>
|
||||
<Container>
|
||||
@@ -1225,7 +1223,7 @@ export default decorate([
|
||||
<Row>
|
||||
<Card>
|
||||
<CardBlock>
|
||||
{state.paramsUpdated ? (
|
||||
{state.edition ? (
|
||||
<ActionButton
|
||||
btnStyle='primary'
|
||||
form={state.formId}
|
||||
@@ -1268,3 +1266,24 @@ export default decorate([
|
||||
)
|
||||
},
|
||||
])
|
||||
|
||||
export default decorate([
|
||||
addSubscriptions({
|
||||
remotes: subscribeRemotes,
|
||||
}),
|
||||
provideState({
|
||||
computed: {
|
||||
loading: (state, props) =>
|
||||
state.suggestedExcludedTags === undefined ||
|
||||
props.remotes === undefined,
|
||||
suggestedExcludedTags: () => getSuggestedExcludedTags(),
|
||||
},
|
||||
}),
|
||||
injectState,
|
||||
({ state: { loading, suggestedExcludedTags }, ...props }) =>
|
||||
loading ? (
|
||||
_('statusLoading')
|
||||
) : (
|
||||
<New suggestedExcludedTags={suggestedExcludedTags} {...props} />
|
||||
),
|
||||
])
|
||||
|
||||
@@ -117,11 +117,17 @@ const SchedulePreviewBody = decorate([
|
||||
let lastRunLog
|
||||
for (const runId in logs) {
|
||||
const log = logs[runId]
|
||||
if (
|
||||
log.scheduleId === schedule.id &&
|
||||
(lastRunLog === undefined || lastRunLog.start < log.start)
|
||||
) {
|
||||
lastRunLog = log
|
||||
if (log.scheduleId === schedule.id) {
|
||||
if (log.status === 'pending') {
|
||||
lastRunLog = log
|
||||
break
|
||||
}
|
||||
if (
|
||||
lastRunLog === undefined ||
|
||||
(lastRunLog.end || lastRunLog.start) < (log.end || log.start)
|
||||
) {
|
||||
lastRunLog = log
|
||||
}
|
||||
}
|
||||
}
|
||||
cb(lastRunLog)
|
||||
@@ -342,7 +348,8 @@ class JobsTable extends React.Component {
|
||||
icon: 'preview',
|
||||
},
|
||||
{
|
||||
handler: (job, { goToNewTab }) => goToNewTab(`/backup/${job.id}/edit`),
|
||||
handler: (job, { goTo, goToNewTab, main }) =>
|
||||
(main ? goTo : goToNewTab)(`/backup/${job.id}/edit`),
|
||||
label: _('formEdit'),
|
||||
icon: 'edit',
|
||||
level: 'primary',
|
||||
|
||||
@@ -259,8 +259,8 @@ export default class Restore extends Component {
|
||||
const remotes = filter(rawRemotes, 'enabled')
|
||||
const remotesInfo = await Promise.all(
|
||||
map(remotes, async remote => ({
|
||||
files: await listRemote(remote.id),
|
||||
backupsInfo: await listRemoteBackups(remote.id),
|
||||
files: await listRemote(remote),
|
||||
backupsInfo: await listRemoteBackups(remote),
|
||||
}))
|
||||
)
|
||||
|
||||
|
||||
@@ -280,7 +280,7 @@ export default class Restore extends Component {
|
||||
handler={this._refreshBackupList}
|
||||
icon='refresh'
|
||||
>
|
||||
{_('restoreResfreshList')}
|
||||
{_('refreshBackupList')}
|
||||
</ActionButton>{' '}
|
||||
<ButtonLink to='backup/restore/metadata'>
|
||||
<Icon icon='database' /> {_('metadata')}
|
||||
|
||||
@@ -133,7 +133,7 @@ export default class HostItem extends Component {
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
<InconsistentHostTimeWarning hostId={host.id} />
|
||||
<InconsistentHostTimeWarning host={host} />
|
||||
|
||||
{hasLicenseRestrictions(host) && <LicenseWarning />}
|
||||
</EllipsisContainer>
|
||||
|
||||
@@ -924,13 +924,13 @@ export default class Home extends Component {
|
||||
|
||||
_setBackupFilter = backupFilter => {
|
||||
const { pathname, query } = this.props.location
|
||||
const isAll = backupFilter === 'all'
|
||||
this.context.router.push({
|
||||
pathname,
|
||||
query: {
|
||||
...query,
|
||||
backup: isAll ? undefined : backupFilter === 'backedUpVms',
|
||||
p: isAll ? 1 : undefined,
|
||||
backup:
|
||||
backupFilter === 'all' ? undefined : backupFilter === 'backedUpVms',
|
||||
p: 1,
|
||||
s_backup: undefined,
|
||||
},
|
||||
})
|
||||
@@ -1016,7 +1016,7 @@ export default class Home extends Component {
|
||||
<Tooltip content={_('filterSyntaxLinkTooltip')}>
|
||||
<a
|
||||
className='input-group-addon'
|
||||
href='https://xen-orchestra.com/docs/search.html#filter-syntax'
|
||||
href='https://xen-orchestra.com/docs/manage_infrastructure.html#filter-syntax'
|
||||
rel='noopener noreferrer'
|
||||
target='_blank'
|
||||
>
|
||||
|
||||
@@ -260,7 +260,7 @@ export default class Host extends Component {
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
<InconsistentHostTimeWarning hostId={host.id} />
|
||||
<InconsistentHostTimeWarning host={host} />
|
||||
</h2>
|
||||
<Copiable tagName='pre' className='text-muted mb-0'>
|
||||
{host.uuid}
|
||||
|
||||
@@ -5,6 +5,7 @@ import Copiable from 'copiable'
|
||||
import CopyToClipboard from 'react-copy-to-clipboard'
|
||||
import decorate from 'apply-decorators'
|
||||
import Icon from 'icon'
|
||||
import Link from 'link'
|
||||
import NoObjects from 'no-objects'
|
||||
import React from 'react'
|
||||
import SortedTable from 'sorted-table'
|
||||
@@ -23,6 +24,7 @@ import {
|
||||
exportAuditRecords,
|
||||
fetchAuditRecords,
|
||||
generateAuditFingerprint,
|
||||
getPlugin,
|
||||
} from 'xo'
|
||||
|
||||
const getIntegrityErrorRender = ({ nValid, error }) => (
|
||||
@@ -287,7 +289,10 @@ export default decorate([
|
||||
this.state._records = await fetchAuditRecords()
|
||||
},
|
||||
handleRef(_, ref) {
|
||||
this.state.goTo = get(() => ref.goTo.bind(ref))
|
||||
if (ref !== null) {
|
||||
const component = ref.getWrappedInstance()
|
||||
this.state.goTo = component.goTo.bind(component)
|
||||
}
|
||||
},
|
||||
handleCheck(_, oldest, newest, error) {
|
||||
const { state } = this
|
||||
@@ -335,6 +340,11 @@ export default decorate([
|
||||
},
|
||||
]
|
||||
: _records,
|
||||
isUserActionsRecordInactive: async () => {
|
||||
const { configuration: { active } = {} } = await getPlugin('audit')
|
||||
|
||||
return !active
|
||||
},
|
||||
},
|
||||
}),
|
||||
injectState,
|
||||
@@ -368,6 +378,21 @@ export default decorate([
|
||||
{_('auditCheckIntegrity')}
|
||||
</ActionButton>
|
||||
</div>
|
||||
{state.isUserActionsRecordInactive && (
|
||||
<p>
|
||||
<Link
|
||||
className='text-warning'
|
||||
to={{
|
||||
pathname: '/settings/plugins',
|
||||
query: {
|
||||
s: 'id:/^audit$/',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Icon icon='alarm' /> {_('auditInactiveUserActionsRecord')}
|
||||
</Link>
|
||||
</p>
|
||||
)}
|
||||
<NoObjects
|
||||
collection={state.records}
|
||||
columns={COLUMNS}
|
||||
|
||||
@@ -93,7 +93,22 @@ const LicenseManager = ({ item, userData }) => {
|
||||
)
|
||||
}
|
||||
|
||||
return <span>{_('licenseBoundToOtherXoa')}</span>
|
||||
return (
|
||||
<span>
|
||||
{_('licenseBoundToOtherXoa')}
|
||||
<br />
|
||||
<ActionButton
|
||||
btnStyle='danger'
|
||||
data-id={item.id}
|
||||
data-plan={item.product}
|
||||
data-oldXoaId={item.xoaId}
|
||||
handler={selfBindLicense}
|
||||
icon='unlock'
|
||||
>
|
||||
{_('rebindXoaLicense')}
|
||||
</ActionButton>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
if (type === 'proxy') {
|
||||
|
||||
@@ -495,8 +495,8 @@ export default class NewXosan extends Component {
|
||||
<div className='alert alert-danger'>
|
||||
{_('xosanDisperseWarning', {
|
||||
link: (
|
||||
<a href='https://xen-orchestra.com/docs/xosan_types.html'>
|
||||
xen-orchestra.com/docs/xosan_types.html
|
||||
<a href='https://xen-orchestra.com/docs/xosan.html#xosan-types'>
|
||||
https://xen-orchestra.com/docs/xosan.html#xosan-types
|
||||
</a>
|
||||
),
|
||||
})}
|
||||
|
||||
18
yarn.lock
18
yarn.lock
@@ -943,6 +943,13 @@
|
||||
dependencies:
|
||||
regenerator-runtime "^0.13.4"
|
||||
|
||||
"@babel/runtime@^7.3.4":
|
||||
version "7.10.3"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.10.3.tgz#670d002655a7c366540c67f6fd3342cd09500364"
|
||||
integrity sha512-RzGO0RLSdokm9Ipe/YD+7ww8X2Ro79qiXZF3HU9ljrM+qnJmH1Vqth+hbiQZy761LnMJTMitHDuKVYTk3k4dLw==
|
||||
dependencies:
|
||||
regenerator-runtime "^0.13.4"
|
||||
|
||||
"@babel/template@^7.10.1", "@babel/template@^7.3.3", "@babel/template@^7.4.0":
|
||||
version "7.10.1"
|
||||
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.10.1.tgz#e167154a94cb5f14b28dc58f5356d2162f539811"
|
||||
@@ -10976,7 +10983,7 @@ json-rpc-peer@^0.13.1:
|
||||
lodash "^4.17.4"
|
||||
readable-stream "^2.2.9"
|
||||
|
||||
json-rpc-peer@^0.15.0, json-rpc-peer@^0.15.3:
|
||||
json-rpc-peer@^0.15.0:
|
||||
version "0.15.5"
|
||||
resolved "https://registry.yarnpkg.com/json-rpc-peer/-/json-rpc-peer-0.15.5.tgz#51bc04cd4ff1c71694d9d903ce3c250d34f2d97e"
|
||||
integrity sha512-jZUNbRmcMXTpAnp1WGY9o85IfdGLKp75lBFYOIgpKOT9ZwKDHQOc3UmxOJUUg1bBfI7D1dltR3FSA6D0ZpPMpw==
|
||||
@@ -10985,6 +10992,15 @@ json-rpc-peer@^0.15.0, json-rpc-peer@^0.15.3:
|
||||
json-rpc-protocol "^0.12.0"
|
||||
lodash "^4.17.4"
|
||||
|
||||
json-rpc-peer@^0.16.0:
|
||||
version "0.16.0"
|
||||
resolved "https://registry.yarnpkg.com/json-rpc-peer/-/json-rpc-peer-0.16.0.tgz#44ce9924cab354c3f263b315b8feb9ed644a06c2"
|
||||
integrity sha512-7T112z5S5xKpWBk1MRmYao6PDPXPFct9cWJbZBVGyg4Zle8EOdvd6y09n+dWFZNdh5qZShHlYQModXU+gHyWcQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.3.4"
|
||||
json-rpc-protocol "^0.13.1"
|
||||
lodash "^4.17.4"
|
||||
|
||||
json-rpc-protocol@^0.11.3:
|
||||
version "0.11.4"
|
||||
resolved "https://registry.yarnpkg.com/json-rpc-protocol/-/json-rpc-protocol-0.11.4.tgz#d1adbfa8e28e548f48d83c5d5c1bde3d2e406cac"
|
||||
|
||||
Reference in New Issue
Block a user