Compare commits

...

57 Commits

Author SHA1 Message Date
Julien Fontanet
aa92f0fc93 WiP: feat(xo-server-icinga2): template filter 2020-07-13 10:26:33 +02:00
Pierre Donias
b79605b692 feat: release 5.48.3 (#5154) 2020-07-10 18:12:03 +02:00
Pierre Donias
ea0fc68a53 feat: technical release (patch) (#5153) 2020-07-10 16:38:41 +02:00
badrAZ
1ca5c32de3 feat(xo-web/audit): show warning in case of disabled logs record (#5152) 2020-07-10 15:32:30 +02:00
Julien Fontanet
f51bcfa05a feat(xo-server-audit): add active setting (default false) (#5151) 2020-07-10 14:29:30 +02:00
Pierre Donias
e1bf68ab38 fix(xo-server/pool.listMissingPatches): don't log errors (#5149)
Work-around to avoid generating too many logs
2020-07-10 12:49:37 +02:00
Pierre Donias
99e03b7ce5 fix(xo-web): broken doc links (#5146)
Introduced by 30d69dadbb
2020-07-10 09:14:55 +02:00
Julien Fontanet
cd70d3ea46 fix(Travis CI): use Node 12 (#5147) 2020-07-09 15:51:22 +02:00
Julien Fontanet
d387227cef fix(xo-server/backups): dont ignore proxy job result/error 2020-07-09 09:40:02 +02:00
Pierre Donias
2f4530e426 feat: release 5.48.2 (#5143) 2020-07-07 15:49:34 +02:00
badrAZ
4db181d8bf chore(xo-server-audit): document audit DB strucure (#5078) 2020-07-07 15:22:41 +02:00
Pierre Donias
9a7a1cc752 feat: technical release (#5142) 2020-07-07 15:03:04 +02:00
Pierre Donias
59ca6c6708 feat(xo-web): prevent XO from checking time consistency of halted hosts 2020-07-07 14:51:06 +02:00
Pierre Donias
fe7901ca7f feat(xo-web): prevent XO from listing missing patches on halted XCP-ng hosts
Otherwise it triggers a lot of errors on XCP-ng
2020-07-07 14:51:06 +02:00
Pierre Donias
9351b4a5bb feat(xo-web/backup/logs): better resolution of last run log (#5141) 2020-07-07 13:47:32 +02:00
badrAZ
dfdd0a0496 fix(xo-server-test): extend timeout of deleteTempResources (#5117) 2020-07-07 13:31:13 +02:00
Pierre Donias
cda39ec256 fix(xo-web/backup/edit): tags overwritten by default ones (#5136)
Introduced by 1c042778b6
See xoa-support#2663

It was due to a race condition between the fetch of the default excluded tags
and the fetch of the job (and its schedules) which made the component populate
the form with the job's information.

Changes:
- `Edit` waits for the `job` and the `schedules` before rendering its child
- `New`'s wrapper waits for the `remotes` and the `suggestedExcludedTags` before
  rendering `New`
- `New`: `updateParams` is now called in `initialize` because we don't need to
  wait for the job and the schedules any more to be able to populate the form
2020-07-07 09:40:40 +02:00
Olivier Lambert
3720a46ff3 feat(docs/supported_hosts): no pro support for XS < 6.5 (#5137) 2020-07-06 20:03:47 +02:00
Julien Fontanet
7ea50ea41e fix(xo-server/callProxyMethod): dont use HTTP proxy 2020-07-06 17:06:37 +02:00
Pierre Donias
60a696916b feat: release 5.48.1 (#5133) 2020-07-03 15:25:39 +02:00
Pierre Donias
b6a255d96f feat: technical release (patch) (#5132) 2020-07-03 14:24:40 +02:00
marcpezin
44a0cce7f2 fix(docs/license_management): replace confusing screenshot about activation (#5131) 2020-07-03 12:03:07 +02:00
Nicolas Raynaud
f580e0d26f fix(import/OVA): fix big size parsing in OVA files (#5129) 2020-07-03 11:48:39 +02:00
Rajaa.BARHTAOUI
6beefe86e2 feat(xo-web/backup): don't open edition in new tab (#5130) 2020-07-03 11:45:06 +02:00
Julien Fontanet
cbada35788 fix(xo-server/file restore): dont fail on LVM partitions
Fixes xoa-support#2640

Introduced by 48ce7df43

The issue was due to object spreading copying only own properties but `path` is now an inherited property due to `dedupeUnmount`.
2020-07-03 11:05:04 +02:00
Julien Fontanet
44ff2f872d feat(xo-server/getBackupNgLogs): dont fail on undefined/null errors
See xoa-support#2663

This should almost never happen but if it does, it should not prevent logs from being consolidated.
2020-07-03 09:22:28 +02:00
Julien Fontanet
2198853662 feat(xo-server/logs-cli): can match on missing props 2020-07-03 09:22:28 +02:00
badrAZ
4636109081 fix(xo-web/(file-)restore-legacy): ignore proxy remotes (#5124)
Legacy restore doesn't support proxy remotes
2020-07-02 16:23:36 +02:00
Pierre Donias
1c042778b6 feat(xo-server,xo-web/smart backup): exclude XO Proxy VMs by default (#5128) 2020-07-02 15:06:47 +02:00
Rajaa.BARHTAOUI
34b5962eac fix(xo-web/backup/health): missing noop function (#5126)
Introduced by committing a suggestion https://github.com/vatesfr/xen-orchestra/pull/5062#discussion_r446135166 
without taking into account that the `noop` function is not already defined.
2020-07-02 15:05:55 +02:00
Rajaa.BARHTAOUI
fc7af59eb7 chore(xo-web/home): remove 'tags' filter from selector (#5121)
See https://github.com/vatesfr/xen-orchestra/pull/5118#discussion_r447586676
2020-07-02 14:52:59 +02:00
Olivier Lambert
7e557ca059 feat(docs/supported hosts): add CH 8.2 LTS in the list of supported hosts (#5127) 2020-07-02 09:30:16 +02:00
Julien Fontanet
1d0cea8ad0 feat(xo-server/logs-cli): add --delete command 2020-07-01 18:04:45 +02:00
Julien Fontanet
5c901d7c1e fix(xo-server/logs-cli): dont fail on non-string value 2020-07-01 18:01:34 +02:00
Julien Fontanet
1dffab0bb8 feat(xen-api): 0.29.0 2020-07-01 17:11:19 +02:00
Julien Fontanet
ae89e14ea2 feat(xo-server/getRemoteHandler): throw for proxy remotes 2020-07-01 11:46:28 +02:00
Pierre Donias
908255060c feat: release 5.48.0 (#5123) 2020-06-30 17:25:16 +02:00
Pierre Donias
88278d0041 feat: technical release (patch) (#5122) 2020-06-30 16:51:20 +02:00
Julien Fontanet
86bfd91c9d feat(xo-server/backup): logs proxy support 2020-06-30 15:45:40 +02:00
Julien Fontanet
0ee412ccb9 feat(xo-server/callProxyMethod): allow proxy.address to contain port 2020-06-30 15:20:58 +02:00
Julien Fontanet
b8bd6ea820 chore(xo-server/callProxyMethod): use parse.result to handle errors 2020-06-30 12:14:45 +02:00
Julien Fontanet
98a1ab3033 fix(xo-server/callProxyMethod): destroy lines stream in case of error 2020-06-30 12:14:45 +02:00
Julien Fontanet
e360f53a40 fix(xo-server/callProxyMethod): all lines should be JSON parsed 2020-06-30 12:14:45 +02:00
Julien Fontanet
237ec38003 fix(xo-server/callProxyMethod): lines is an object stream 2020-06-30 12:14:45 +02:00
Julien Fontanet
30ea1bbf87 feat(xo-server/callProxyMethod): expectStream as named option 2020-06-30 12:14:45 +02:00
Pierre Donias
0d0aef6014 feat: technical release (patch) (#5120) 2020-06-30 11:40:44 +02:00
badrAZ
1b7441715c feat(xo-server-perf-alert): regroup items with missing stats in one email (#4413)
Fixes #5104
2020-06-30 09:30:20 +02:00
badrAZ
e3223b6124 fix(xo-web/audit): ref not correctly forwarded (#5106)
Introduced by 9f29a047a7

The high level component (audit UI) need to call a `SortedTable` method. The only way to do it is to have access to its ref.

All components between the high level component and the `SortedTable` should forward this ref. Unfortunately, the commit  9f29a047a7 decorate the `StortedTable` with `withRouter` without forwarding its ref which breaks the audit check integrity feedback.
2020-06-29 15:44:33 +02:00
Julien Fontanet
41fb06187b fix(coalesce-calls/README): fix import 2020-06-29 15:39:14 +02:00
Julien Fontanet
adf0e8ae3b feat(@vates/coalesce-calls): 0.1.0 2020-06-29 15:33:53 +02:00
Rajaa.BARHTAOUI
42dd1efb41 feat(xo-web/home): remove unnecessary condition (#5116)
Introduced by f736381933

Backup filter: remove the needless condition to go to the first page.
2020-06-29 15:24:28 +02:00
badrAZ
b6a6694abf fix(xo-server-test/job with non-existent VM): logged as failed task (#5112)
Since c061505bf8, missing VMs are logged as a failure tasks
2020-06-29 14:04:23 +02:00
Pierre Donias
04f2f50d6d feat: technical release (#5115) 2020-06-26 15:56:40 +02:00
Rajaa.BARHTAOUI
6d1048e5c5 feat(xo-web/backup/health): show detached backups (#5062)
See #4716
2020-06-26 14:45:01 +02:00
Pierre Donias
fe722c8b31 feat(xo-web/licenses): rebind license to this XOA (#5110)
See xoa#55
Requires xoa#58

When an XOA license is bound to another XOA, allow the user to move it to the
current XOA (thus unbinding it from the other one).
2020-06-26 13:12:53 +02:00
badrAZ
0326ce1d85 feat(xo-server-audit): ignore common methods without side effects (#5109)
Ignoring these methods reduce the number of records in the audit which makes it easier to analyze and store.
2020-06-25 16:55:57 +02:00
Rajaa.BARHTAOUI
183ddb68d3 fix(changelog): missing a parenthesis (#5113) 2020-06-25 15:53:30 +02:00
64 changed files with 1172 additions and 341 deletions

View File

@@ -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/

View File

@@ -0,0 +1,46 @@
<!-- DO NOT EDIT MANUALLY, THIS FILE HAS BEEN GENERATED -->
# @vates/coalesce-calls
[![Package Version](https://badgen.net/npm/v/@vates/coalesce-calls)](https://npmjs.org/package/@vates/coalesce-calls) ![License](https://badgen.net/npm/license/@vates/coalesce-calls) [![PackagePhobia](https://badgen.net/bundlephobia/minzip/@vates/coalesce-calls)](https://bundlephobia.com/result?p=@vates/coalesce-calls) [![Node compatibility](https://badgen.net/npm/node/@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)

View 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()
```

View 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
}
}

View 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')
})
})

View 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"
}
}

View File

@@ -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"

View File

@@ -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",

View File

@@ -1,8 +1,108 @@
# ChangeLog
## **5.48.3** (2020-07-10)
![Channel: latest](https://badgen.net/badge/channel/latest/yellow)
### 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)
![Channel: latest](https://badgen.net/badge/channel/latest/yellow)
![Channel: stable](https://badgen.net/badge/channel/stable/green)
### Bug fixes
@@ -60,8 +160,6 @@
## **5.46.0** (2020-04-30)
![Channel: stable](https://badgen.net/badge/channel/stable/green)
### 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

View File

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

View File

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

View File

@@ -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",

View File

@@ -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": [

View File

@@ -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": [

View File

@@ -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)
}
}

View File

@@ -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}`

View File

@@ -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": [

View File

@@ -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": [],

View File

@@ -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')}`
)
},
() => {}
)
}
}

View File

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

View 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'

View File

@@ -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",

View File

@@ -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 }

View File

@@ -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]`;

View File

@@ -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 () => {

View File

@@ -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
}

View File

@@ -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": {

View File

@@ -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)
}

View File

@@ -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) {

View File

@@ -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)
}

View File

@@ -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 })

View File

@@ -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,

View File

@@ -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'))

View File

@@ -366,7 +366,7 @@ export default class BackupNgFileRestore {
).lv_path
)
unmountQueue.push(partition.unmount)
return { ...partition, unmount }
return { __proto__: partition, unmount }
}
return mountPartition(

View File

@@ -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(
{

View File

@@ -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
}
}

View File

@@ -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`
)

View File

@@ -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')
}

View File

@@ -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": [

View File

@@ -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 }
}

View File

@@ -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",

View File

@@ -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:',
}

View File

@@ -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 }

View File

@@ -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',

View File

@@ -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',

View File

@@ -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',

View File

@@ -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 }) => (

View File

@@ -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 })

View File

@@ -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(() =>

View File

@@ -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} />

View File

@@ -230,7 +230,7 @@ export default class Restore extends Component {
handler={this._refreshBackupList}
icon='refresh'
>
{_('restoreResfreshList')}
{_('refreshBackupList')}
</ActionButton>
</div>
<em>

View File

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

View File

@@ -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} />
),
])

View File

@@ -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',

View File

@@ -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),
}))
)

View File

@@ -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')}

View File

@@ -133,7 +133,7 @@ export default class HostItem extends Component {
</Tooltip>
)}
&nbsp;
<InconsistentHostTimeWarning hostId={host.id} />
<InconsistentHostTimeWarning host={host} />
&nbsp;
{hasLicenseRestrictions(host) && <LicenseWarning />}
</EllipsisContainer>

View File

@@ -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'
>

View File

@@ -260,7 +260,7 @@ export default class Host extends Component {
</Tooltip>
)}
&nbsp;
<InconsistentHostTimeWarning hostId={host.id} />
<InconsistentHostTimeWarning host={host} />
</h2>
<Copiable tagName='pre' className='text-muted mb-0'>
{host.uuid}

View File

@@ -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}

View File

@@ -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') {

View File

@@ -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>
),
})}

View File

@@ -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"