Compare commits
49 Commits
xo-web-v5.
...
58bfde62b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7d72165997 | ||
|
|
58bfde62bd | ||
|
|
7c734168d0 | ||
|
|
1e7bfec2ce | ||
|
|
1eb0603b4e | ||
|
|
4b32730ce8 | ||
|
|
ad083c1d9b | ||
|
|
b4f84c2de2 | ||
|
|
fc17443ce4 | ||
|
|
342ae06b21 | ||
|
|
093fb7f959 | ||
|
|
f6472424ad | ||
|
|
31ed3767c6 | ||
|
|
366acb65ea | ||
|
|
7c6946931b | ||
|
|
5d971433a5 | ||
|
|
05264b326b | ||
|
|
fdd5c6bfd8 | ||
|
|
42c3528c2f | ||
|
|
18640714f1 | ||
|
|
cda4d3399b | ||
|
|
4da8af6e69 | ||
|
|
b535565612 | ||
|
|
bef39b8a96 | ||
|
|
fb2502a031 | ||
|
|
0b90befda1 | ||
|
|
f9800f104a | ||
|
|
99134cc381 | ||
|
|
66ca08da6d | ||
|
|
5eb7ece6ba | ||
|
|
6b3d334e76 | ||
|
|
14f5fd8f73 | ||
|
|
5f73aee0df | ||
|
|
f8666ba367 | ||
|
|
9e80f76dd8 | ||
|
|
c76a5eaf67 | ||
|
|
cd378f0168 | ||
|
|
7d51ff0cf5 | ||
|
|
47819ea956 | ||
|
|
c7e3560c98 | ||
|
|
b24400b21d | ||
|
|
6c1d651687 | ||
|
|
e7757b53e7 | ||
|
|
a6d182e92d | ||
|
|
925eca1463 | ||
|
|
8b454f0d39 | ||
|
|
7c4d110353 | ||
|
|
6df55523b6 | ||
|
|
3ec6a24634 |
@@ -40,6 +40,13 @@ module.exports = {
|
||||
|
||||
'react/jsx-handler-names': 'off',
|
||||
|
||||
// disabled because not always relevant, we might reconsider in the future
|
||||
//
|
||||
// enabled by https://github.com/standard/eslint-config-standard/commit/319b177750899d4525eb1210686f6aca96190b2f
|
||||
//
|
||||
// example: https://github.com/vatesfr/xen-orchestra/blob/31ed3767c67044ca445658eb6b560718972402f2/packages/xen-api/src/index.js#L156-L157
|
||||
'lines-between-class-members': 'off',
|
||||
|
||||
'no-console': ['error', { allow: ['warn', 'error'] }],
|
||||
'no-var': 'error',
|
||||
'node/no-extraneous-import': 'error',
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"golike-defer": "^0.4.1",
|
||||
"xen-api": "^0.27.1"
|
||||
"xen-api": "^0.27.2"
|
||||
},
|
||||
"scripts": {
|
||||
"postversion": "npm publish"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@xen-orchestra/log",
|
||||
"version": "0.1.4",
|
||||
"version": "0.2.0",
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
"keywords": [],
|
||||
|
||||
@@ -19,7 +19,8 @@ const createTransport = config => {
|
||||
}
|
||||
}
|
||||
|
||||
let { filter, transport } = config
|
||||
let { filter } = config
|
||||
let transport = createTransport(config.transport)
|
||||
const level = resolve(config.level)
|
||||
|
||||
if (filter !== undefined) {
|
||||
@@ -51,11 +52,12 @@ const symbol =
|
||||
? Symbol.for('@xen-orchestra/log')
|
||||
: '@@@xen-orchestra/log'
|
||||
|
||||
const { env } = process
|
||||
global[symbol] = createTransport({
|
||||
// display warnings or above, and all that are enabled via DEBUG or
|
||||
// NODE_DEBUG env
|
||||
filter: process.env.DEBUG || process.env.NODE_DEBUG,
|
||||
level: LEVELS.INFO,
|
||||
filter: [env.DEBUG, env.NODE_DEBUG].filter(Boolean).join(','),
|
||||
level: resolve(env.LOG_LEVEL, LEVELS.INFO),
|
||||
|
||||
transport: createConsoleTransport(),
|
||||
})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import createTransport from './transports/console'
|
||||
import LEVELS from './levels'
|
||||
import LEVELS, { resolve } from './levels'
|
||||
|
||||
const symbol =
|
||||
typeof Symbol !== 'undefined'
|
||||
@@ -9,7 +9,8 @@ if (!(symbol in global)) {
|
||||
// the default behavior, without requiring `configure` is to avoid
|
||||
// logging anything unless it's a real error
|
||||
const transport = createTransport()
|
||||
global[symbol] = log => log.level > LEVELS.WARN && transport(log)
|
||||
const level = resolve(process.env.LOG_LEVEL, LEVELS.WARN)
|
||||
global[symbol] = log => log.level >= level && transport(log)
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
@@ -72,5 +73,5 @@ prototype.wrap = function(message, fn) {
|
||||
}
|
||||
}
|
||||
|
||||
const createLogger = namespace => new Logger(namespace)
|
||||
export const createLogger = namespace => new Logger(namespace)
|
||||
export { createLogger as default }
|
||||
|
||||
@@ -13,11 +13,22 @@ for (const name in LEVELS) {
|
||||
NAMES[LEVELS[name]] = name
|
||||
}
|
||||
|
||||
export const resolve = level => {
|
||||
if (typeof level === 'string') {
|
||||
level = LEVELS[level.toUpperCase()]
|
||||
// resolves to the number representation of a level
|
||||
//
|
||||
// returns `defaultLevel` if invalid
|
||||
export const resolve = (level, defaultLevel) => {
|
||||
const type = typeof level
|
||||
if (type === 'number') {
|
||||
if (level in NAMES) {
|
||||
return level
|
||||
}
|
||||
} else if (type === 'string') {
|
||||
const nLevel = LEVELS[level.toUpperCase()]
|
||||
if (nLevel !== undefined) {
|
||||
return nLevel
|
||||
}
|
||||
}
|
||||
return level
|
||||
return defaultLevel
|
||||
}
|
||||
|
||||
Object.freeze(LEVELS)
|
||||
|
||||
@@ -1,26 +1,23 @@
|
||||
import LEVELS, { NAMES } from '../levels'
|
||||
|
||||
// Bind console methods (necessary for browsers)
|
||||
/* eslint-disable no-console */
|
||||
const debugConsole = console.log.bind(console)
|
||||
const infoConsole = console.info.bind(console)
|
||||
const warnConsole = console.warn.bind(console)
|
||||
const errorConsole = console.error.bind(console)
|
||||
/* eslint-enable no-console */
|
||||
|
||||
const { ERROR, INFO, WARN } = LEVELS
|
||||
|
||||
const consoleTransport = ({ data, level, namespace, message, time }) => {
|
||||
const fn =
|
||||
/* eslint-disable no-console */
|
||||
level < INFO
|
||||
? debugConsole
|
||||
? console.log
|
||||
: level < WARN
|
||||
? infoConsole
|
||||
? console.info
|
||||
: level < ERROR
|
||||
? warnConsole
|
||||
: errorConsole
|
||||
? console.warn
|
||||
: console.error
|
||||
/* eslint-enable no-console */
|
||||
|
||||
fn('%s - %s - [%s] %s', time.toISOString(), namespace, NAMES[level], message)
|
||||
data != null && fn(data)
|
||||
const args = [time.toISOString(), namespace, NAMES[level], message]
|
||||
if (data != null) {
|
||||
args.push(data)
|
||||
}
|
||||
fn.apply(console, args)
|
||||
}
|
||||
export default () => consoleTransport
|
||||
|
||||
59
CHANGELOG.md
59
CHANGELOG.md
@@ -4,8 +4,41 @@
|
||||
|
||||
### Enhancements
|
||||
|
||||
- [Support] Ability to check the XOA on the user interface [#4513](https://github.com/vatesfr/xen-orchestra/issues/4513) (PR [#4574](https://github.com/vatesfr/xen-orchestra/pull/4574))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- [VM/new-vm] Fix template selection on creating new VM for resource sets [#4565](https://github.com/vatesfr/xen-orchestra/issues/4565) (PR [#4568](https://github.com/vatesfr/xen-orchestra/pull/4568))
|
||||
- [VM] Clearer invalid cores per socket error [#4120](https://github.com/vatesfr/xen-orchestra/issues/4120) (PR [#4187](https://github.com/vatesfr/xen-orchestra/pull/4187))
|
||||
|
||||
### Released packages
|
||||
|
||||
- xo-web v5.50.3
|
||||
|
||||
|
||||
## **5.39.0** (2019-09-30)
|
||||
|
||||

|
||||
|
||||
### Highlights
|
||||
|
||||
- [VM/console] Add a button to connect to the VM via the local SSH client (PR [#4415](https://github.com/vatesfr/xen-orchestra/pull/4415))
|
||||
- [SDN Controller] Add possibility to encrypt private networks (PR [#4441](https://github.com/vatesfr/xen-orchestra/pull/4441))
|
||||
- [Backups] Improve performance by caching VM backups listing (PR [#4509](https://github.com/vatesfr/xen-orchestra/pull/4509))
|
||||
- [HUB] VM template store [#1918](https://github.com/vatesfr/xen-orchestra/issues/1918) (PR [#4442](https://github.com/vatesfr/xen-orchestra/pull/4442))
|
||||
|
||||
### Enhancements
|
||||
|
||||
- [SR/new] Clarify address formats [#4450](https://github.com/vatesfr/xen-orchestra/issues/4450) (PR [#4460](https://github.com/vatesfr/xen-orchestra/pull/4460))
|
||||
- [Backup NG/New] Show warning if zstd compression is not supported on a VM [#3892](https://github.com/vatesfr/xen-orchestra/issues/3892) (PRs [#4411](https://github.com/vatesfr/xen-orchestra/pull/4411))
|
||||
- [VM/disks] Don't hide disks that are attached to the same VM twice [#4400](https://github.com/vatesfr/xen-orchestra/issues/4400) (PR [#4414](https://github.com/vatesfr/xen-orchestra/pull/4414))
|
||||
- [SDN Controller] Ability to configure MTU for private networks (PR [#4491](https://github.com/vatesfr/xen-orchestra/pull/4491))
|
||||
- [VM Export] Filenames are now prefixed with datetime [#4503](https://github.com/vatesfr/xen-orchestra/issues/4503)
|
||||
- [Settings/Logs] Differenciate XS/XCP-ng errors from XO errors [#4101](https://github.com/vatesfr/xen-orchestra/issues/4101) (PR [#4385](https://github.com/vatesfr/xen-orchestra/pull/4385))
|
||||
- [Backups] Improve performance by caching logs consolidation (PR [#4541](https://github.com/vatesfr/xen-orchestra/pull/4541))
|
||||
- [New VM] Cloud Init available for all plans (PR [#4543](https://github.com/vatesfr/xen-orchestra/pull/4543))
|
||||
- [Servers] IPv6 addresses can be used [#4520](https://github.com/vatesfr/xen-orchestra/issues/4520) (PR [#4521](https://github.com/vatesfr/xen-orchestra/pull/4521)) \
|
||||
Note: They must enclosed in brackets to differentiate with the port, e.g.: `[2001:db8::7334]` or `[ 2001:db8::7334]:4343`
|
||||
|
||||
### Bug fixes
|
||||
|
||||
@@ -15,16 +48,32 @@
|
||||
- [Network] Fix inability to create a bonded network (PR [#4489](https://github.com/vatesfr/xen-orchestra/pull/4489))
|
||||
- [Backup restore & Replication] Don't copy `sm_config` to new VDIs which might leads to useless coalesces [#4482](https://github.com/vatesfr/xen-orchestra/issues/4482) (PR [#4484](https://github.com/vatesfr/xen-orchestra/pull/4484))
|
||||
- [Home] Fix intermediary "no results" display showed on filtering items [#4420](https://github.com/vatesfr/xen-orchestra/issues/4420) (PR [#4456](https://github.com/vatesfr/xen-orchestra/pull/4456)
|
||||
- [Backup NG/New schedule] Properly show user errors in the form [#3831](https://github.com/vatesfr/xen-orchestra/issues/3831) (PR [#4131](https://github.com/vatesfr/xen-orchestra/pull/4131))
|
||||
- [VM/Advanced] Fix `"vm.set_domain_type" is not a function` error on switching virtualization mode (PV/HVM) [#4348](https://github.com/vatesfr/xen-orchestra/issues/4348) (PR [#4504](https://github.com/vatesfr/xen-orchestra/pull/4504))
|
||||
- [Backup NG/logs] Show warning when zstd compression is selected but not supported [#3892](https://github.com/vatesfr/xen-orchestra/issues/3892) (PR [#4375](https://github.com/vatesfr/xen-orchestra/pull/4375)
|
||||
- [Patches] Fix patches installation for CH 8.0 (PR [#4511](https://github.com/vatesfr/xen-orchestra/pull/4511))
|
||||
- [Network] Fix inability to set a network name [#4514](https://github.com/vatesfr/xen-orchestra/issues/4514) (PR [4510](https://github.com/vatesfr/xen-orchestra/pull/4510))
|
||||
- [Backup NG] Fix race conditions that could lead to disabled jobs still running (PR [4510](https://github.com/vatesfr/xen-orchestra/pull/4510))
|
||||
- [XOA] Remove "Updates" and "Licenses" tabs for non admin users (PR [#4526](https://github.com/vatesfr/xen-orchestra/pull/4526))
|
||||
- [New VM] Ability to escape [cloud config template](https://xen-orchestra.com/blog/xen-orchestra-5-21/#cloudconfigtemplates) variables [#4486](https://github.com/vatesfr/xen-orchestra/issues/4486) (PR [#4501](https://github.com/vatesfr/xen-orchestra/pull/4501))
|
||||
- [Backup NG] Properly log and report if job is already running [#4497](https://github.com/vatesfr/xen-orchestra/issues/4497) (PR [4534](https://github.com/vatesfr/xen-orchestra/pull/4534))
|
||||
- [Host] Fix an issue where host was wrongly reporting time inconsistency (PR [#4540](https://github.com/vatesfr/xen-orchestra/pull/4540))
|
||||
|
||||
|
||||
### Released packages
|
||||
|
||||
- xo-server-sdn-controller v0.2.1
|
||||
- xo-server v5.49.0
|
||||
- xo-web v5.49.0
|
||||
- xen-api v0.27.2
|
||||
- xo-server-cloud v0.3.0
|
||||
- @xen-orchestra/cron v1.0.4
|
||||
- xo-server-sdn-controller v0.3.0
|
||||
- @xen-orchestra/template v0.1.0
|
||||
- xo-server v5.50.1
|
||||
- xo-web v5.50.2
|
||||
|
||||
|
||||
## **5.38.0** (2019-08-29)
|
||||
|
||||

|
||||

|
||||
|
||||
### Enhancements
|
||||
|
||||
@@ -52,8 +101,6 @@
|
||||
|
||||
## **5.37.1** (2019-08-06)
|
||||
|
||||

|
||||
|
||||
### Enhancements
|
||||
|
||||
- [SDN Controller] Let the user choose on which PIF to create a private network (PR [#4379](https://github.com/vatesfr/xen-orchestra/pull/4379))
|
||||
|
||||
@@ -7,27 +7,14 @@
|
||||
|
||||
> Users must be able to say: “Nice enhancement, I'm eager to test it”
|
||||
|
||||
- [VM/disks] Don't hide disks that are attached to the same VM twice [#4400](https://github.com/vatesfr/xen-orchestra/issues/4400) (PR [#4414](https://github.com/vatesfr/xen-orchestra/pull/4414))
|
||||
- [VM/console] Add a button to connect to the VM via the local SSH client (PR [#4415](https://github.com/vatesfr/xen-orchestra/pull/4415))
|
||||
- [SDN Controller] Add possibility to encrypt private networks (PR [#4441](https://github.com/vatesfr/xen-orchestra/pull/4441))
|
||||
- [SDN Controller] Ability to configure MTU for private networks (PR [#4491](https://github.com/vatesfr/xen-orchestra/pull/4491))
|
||||
- [VM Export] Filenames are now prefixed with datetime [#4503](https://github.com/vatesfr/xen-orchestra/issues/4503)
|
||||
- [Backups] Improve performance by caching VM backups listing (PR [#4509](https://github.com/vatesfr/xen-orchestra/pull/4509))
|
||||
[Support] Ability to check the XOA on the user interface [#4513](https://github.com/vatesfr/xen-orchestra/issues/4513) (PR [#4574](https://github.com/vatesfr/xen-orchestra/pull/4574))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- [Backup NG/New schedule] Properly show user errors in the form [#3831](https://github.com/vatesfr/xen-orchestra/issues/3831) (PR [#4131](https://github.com/vatesfr/xen-orchestra/pull/4131))
|
||||
|
||||
> Users must be able to say: “I had this issue, happy to know it's fixed”
|
||||
|
||||
- [VM/Advanced] Fix `"vm.set_domain_type" is not a function` error on switching virtualization mode (PV/HVM) [#4348](https://github.com/vatesfr/xen-orchestra/issues/4348) (PR [#4504](https://github.com/vatesfr/xen-orchestra/pull/4504))
|
||||
- [Backup NG/logs] Show warning when zstd compression is selected but not supported [#3892](https://github.com/vatesfr/xen-orchestra/issues/3892) (PR [#4375](https://github.com/vatesfr/xen-orchestra/pull/4375)
|
||||
- [Patches] Fix patches installation for CH 8.0 (PR [#4511](https://github.com/vatesfr/xen-orchestra/pull/4511))
|
||||
- [Network] Fix inability to set a network name [#4514](https://github.com/vatesfr/xen-orchestra/issues/4514) (PR [4510](https://github.com/vatesfr/xen-orchestra/pull/4510))
|
||||
- [Backup NG] Fix race conditions that could lead to disabled jobs still running (PR [4510](https://github.com/vatesfr/xen-orchestra/pull/4510))
|
||||
- [XOA] Remove "Updates" and "Licenses" tabs for non admin users (PR [#4526](https://github.com/vatesfr/xen-orchestra/pull/4526))
|
||||
- [New VM] Ability to escape [cloud config template](https://xen-orchestra.com/blog/xen-orchestra-5-21/#cloudconfigtemplates) variables [#4486](https://github.com/vatesfr/xen-orchestra/issues/4486) (PR [#4501](https://github.com/vatesfr/xen-orchestra/pull/4501))
|
||||
- [Backup NG] Properly log and report if job is already running [#4497](https://github.com/vatesfr/xen-orchestra/issues/4497) (PR [4534](https://github.com/vatesfr/xen-orchestra/pull/4534))
|
||||
- [VM/new-vm] Fix template selection on creating new VM for resource sets [#4565](https://github.com/vatesfr/xen-orchestra/issues/4565) (PR [#4568](https://github.com/vatesfr/xen-orchestra/pull/4568))
|
||||
- [VM] Clearer invalid cores per socket error [#4120](https://github.com/vatesfr/xen-orchestra/issues/4120) (PR [#4187](https://github.com/vatesfr/xen-orchestra/pull/4187))
|
||||
|
||||
### Released packages
|
||||
|
||||
@@ -36,8 +23,5 @@
|
||||
>
|
||||
> Rule of thumb: add packages on top.
|
||||
|
||||
- @xen-orchestra/template v0.0.0
|
||||
- @xen-orchestra/cron v1.0.4
|
||||
- xo-server-sdn-controller v0.3.0
|
||||
- xo-server v5.50.0
|
||||
- xo-web v5.50.0
|
||||
- xo-server v5.51.0
|
||||
- xo-web v5.51.0
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
### Check list
|
||||
|
||||
> Check items when done or if not relevant
|
||||
> Check if done.
|
||||
>
|
||||
> Strikethrough if not relevant: ~~example~~ ([doc](https://help.github.com/en/articles/basic-writing-and-formatting-syntax)).
|
||||
|
||||
- [ ] PR reference the relevant issue (e.g. `Fixes #007`)
|
||||
- [ ] PR reference the relevant issue (e.g. `Fixes #007` or `See xoa-support#42`)
|
||||
- [ ] if UI changes, a screenshot has been added to the PR
|
||||
- [ ] if `xo-server` API changes, the corresponding test has been added to/updated on [`xo-server-test`](https://github.com/vatesfr/xen-orchestra/tree/master/packages/xo-server-test)
|
||||
- [ ] `CHANGELOG.unreleased.md`:
|
||||
- enhancement/bug fix entry added
|
||||
- list of packages to release updated (`${name} v${new version}`)
|
||||
- [ ] documentation updated
|
||||
- [ ] **I have tested added/updated features** (and impacted code)
|
||||
- `CHANGELOG.unreleased.md`:
|
||||
- [ ] enhancement/bug fix entry added
|
||||
- [ ] list of packages to release updated (`${name} v${new version}`)
|
||||
- **I have tested added/updated features** (and impacted code)
|
||||
- [ ] unit tests (e.g. [`cron/parse.spec.js`](https://github.com/vatesfr/xen-orchestra/blob/b24400b21de1ebafa1099c56bac1de5c988d9202/%40xen-orchestra/cron/src/parse.spec.js))
|
||||
- [ ] if `xo-server` API changes, the corresponding test has been added to/updated on [`xo-server-test`](https://github.com/vatesfr/xen-orchestra/tree/master/packages/xo-server-test)
|
||||
- [ ] at least manual testing
|
||||
|
||||
### Process
|
||||
|
||||
@@ -17,3 +21,10 @@
|
||||
1. mark it as `WiP:` (Work in Progress) if not ready to be merged
|
||||
1. when you want a review, add a reviewer (and only one)
|
||||
1. if necessary, update your PR, and re- add a reviewer
|
||||
|
||||
From [_the Four Agreements_](https://en.wikipedia.org/wiki/Don_Miguel_Ruiz#The_Four_Agreements):
|
||||
|
||||
1. Be impeccable with your word.
|
||||
1. Don't take anything personally.
|
||||
1. Don't make assumptions.
|
||||
1. Always do your best.
|
||||
|
||||
BIN
docs/assets/release-channels.png
Normal file
BIN
docs/assets/release-channels.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 99 KiB |
@@ -41,6 +41,20 @@ However, if you want to start a manual check, you can do it by clicking on the "
|
||||
|
||||

|
||||
|
||||
#### Release channel
|
||||
In Xen Orchestra, you can make a choice between two different release channels.
|
||||
|
||||
##### Stable
|
||||
The stable channel is intended to be a version of Xen Orchestra that is already **one month old** (and therefore will benefit from one month of community feedback and various fixes). This way, users more concerned with the stability of their appliance will have the option to stay on a slightly older (and tested) version of XO (still supported by our pro support).
|
||||
|
||||
##### Latest
|
||||
|
||||
The latest channel will include all the latest improvements available in Xen Orchestra. The version available in latest has already been QA'd by our team, but issues may still occur once deployed in vastly varying environments, such as our user base has.
|
||||
|
||||
> To select the release channel of your choice, go to the XOA > Updates view.
|
||||
|
||||

|
||||
|
||||
#### Upgrade
|
||||
|
||||
If a new version is found, you'll have an upgrade button and its tooltip displayed:
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
"human-format": "^0.10.0",
|
||||
"lodash": "^4.17.4",
|
||||
"pw": "^0.0.4",
|
||||
"xen-api": "^0.27.1"
|
||||
"xen-api": "^0.27.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.1.5",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "xen-api",
|
||||
"version": "0.27.1",
|
||||
"version": "0.27.2",
|
||||
"license": "ISC",
|
||||
"description": "Connector to the Xen API",
|
||||
"keywords": [
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const URL_RE = /^(?:(https?:)\/*)?(?:([^:]+):([^@]+)@)?([^/]+?)(?::([0-9]+))?\/?$/
|
||||
const URL_RE = /^(?:(https?:)\/*)?(?:([^:]+):([^@]+)@)?(?:\[([^\]]+)\]|([^:/]+))(?::([0-9]+))?\/?$/
|
||||
|
||||
export default url => {
|
||||
const matches = URL_RE.exec(url)
|
||||
@@ -6,7 +6,15 @@ export default url => {
|
||||
throw new Error('invalid URL: ' + url)
|
||||
}
|
||||
|
||||
const [, protocol = 'https:', username, password, hostname, port] = matches
|
||||
const [
|
||||
,
|
||||
protocol = 'https:',
|
||||
username,
|
||||
password,
|
||||
ipv6,
|
||||
hostname = ipv6,
|
||||
port,
|
||||
] = matches
|
||||
const parsedUrl = { protocol, hostname, port }
|
||||
if (username !== undefined) {
|
||||
parsedUrl.username = decodeURIComponent(username)
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
"node": ">=6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@xen-orchestra/log": "^0.1.4",
|
||||
"@xen-orchestra/log": "^0.2.0",
|
||||
"human-format": "^0.10.0",
|
||||
"lodash": "^4.13.1",
|
||||
"moment-timezone": "^0.5.13"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "xo-server-cloud",
|
||||
"version": "0.2.4",
|
||||
"version": "0.3.0",
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
"keywords": [
|
||||
|
||||
@@ -20,9 +20,13 @@ class XoServerCloud {
|
||||
}
|
||||
|
||||
async load() {
|
||||
const getResourceCatalog = () => this._getCatalog()
|
||||
getResourceCatalog.description = 'Get the list of all available resources'
|
||||
const getResourceCatalog = this._getCatalog.bind(this)
|
||||
getResourceCatalog.description =
|
||||
"Get the list of user's available resources"
|
||||
getResourceCatalog.permission = 'admin'
|
||||
getResourceCatalog.params = {
|
||||
filters: { type: 'object', optional: true },
|
||||
}
|
||||
|
||||
const registerResource = ({ namespace }) =>
|
||||
this._registerResource(namespace)
|
||||
@@ -34,8 +38,29 @@ class XoServerCloud {
|
||||
}
|
||||
registerResource.permission = 'admin'
|
||||
|
||||
const downloadAndInstallResource = this._downloadAndInstallResource.bind(
|
||||
this
|
||||
)
|
||||
|
||||
downloadAndInstallResource.description =
|
||||
'Download and install a resource via cloud plugin'
|
||||
|
||||
downloadAndInstallResource.params = {
|
||||
id: { type: 'string' },
|
||||
namespace: { type: 'string' },
|
||||
version: { type: 'string' },
|
||||
sr: { type: 'string' },
|
||||
}
|
||||
|
||||
downloadAndInstallResource.resolve = {
|
||||
sr: ['sr', 'SR', 'administrate'],
|
||||
}
|
||||
|
||||
downloadAndInstallResource.permission = 'admin'
|
||||
|
||||
this._unsetApiMethods = this._xo.addApiMethods({
|
||||
cloud: {
|
||||
downloadAndInstallResource,
|
||||
getResourceCatalog,
|
||||
registerResource,
|
||||
},
|
||||
@@ -66,8 +91,8 @@ class XoServerCloud {
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
async _getCatalog() {
|
||||
const catalog = await this._updater.call('getResourceCatalog')
|
||||
async _getCatalog({ filters } = {}) {
|
||||
const catalog = await this._updater.call('getResourceCatalog', { filters })
|
||||
|
||||
if (!catalog) {
|
||||
throw new Error('cannot get catalog')
|
||||
@@ -90,6 +115,26 @@ class XoServerCloud {
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
async _downloadAndInstallResource({ id, namespace, sr, version }) {
|
||||
const stream = await this._requestResource({
|
||||
hub: true,
|
||||
id,
|
||||
namespace,
|
||||
version,
|
||||
})
|
||||
const vm = await this._xo.getXapi(sr.$poolId).importVm(stream, {
|
||||
srId: sr.id,
|
||||
type: 'xva',
|
||||
})
|
||||
await vm.update_other_config({
|
||||
'xo:resource:namespace': namespace,
|
||||
'xo:resource:xva:version': version,
|
||||
'xo:resource:xva:id': id,
|
||||
})
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
async _registerResource(namespace) {
|
||||
const _namespace = (await this._getNamespaces())[namespace]
|
||||
|
||||
@@ -106,8 +151,10 @@ class XoServerCloud {
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
async _getNamespaceCatalog(namespace) {
|
||||
const namespaceCatalog = (await this._getCatalog())[namespace]
|
||||
async _getNamespaceCatalog({ hub, namespace }) {
|
||||
const namespaceCatalog = (await this._getCatalog({ filters: { hub } }))[
|
||||
namespace
|
||||
]
|
||||
|
||||
if (!namespaceCatalog) {
|
||||
throw new Error(`cannot get catalog: ${namespace} not registered`)
|
||||
@@ -118,14 +165,17 @@ class XoServerCloud {
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
async _requestResource(namespace, id, version) {
|
||||
async _requestResource({ hub = false, id, namespace, version }) {
|
||||
const _namespace = (await this._getNamespaces())[namespace]
|
||||
|
||||
if (!_namespace || !_namespace.registered) {
|
||||
if (!hub && (!_namespace || !_namespace.registered)) {
|
||||
throw new Error(`cannot get resource: ${namespace} not registered`)
|
||||
}
|
||||
|
||||
const { _token: token } = await this._getNamespaceCatalog(namespace)
|
||||
const { _token: token } = await this._getNamespaceCatalog({
|
||||
hub,
|
||||
namespace,
|
||||
})
|
||||
|
||||
// 2018-03-20 Extra check: getResourceDownloadToken seems to be called without a token in some cases
|
||||
if (token === undefined) {
|
||||
|
||||
@@ -680,7 +680,7 @@ ${entry.listItem}
|
||||
},
|
||||
}
|
||||
if (xapiObject.$type === 'VM') {
|
||||
payload['vm_uuid'] = xapiObject.uuid
|
||||
payload.vm_uuid = xapiObject.uuid
|
||||
}
|
||||
// JSON is not well formed, can't use the default node parser
|
||||
return JSON5.parse(
|
||||
|
||||
@@ -28,10 +28,10 @@
|
||||
"cross-env": "^5.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@xen-orchestra/log": "^0.1.4",
|
||||
"@xen-orchestra/log": "^0.2.0",
|
||||
"lodash": "^4.17.11",
|
||||
"node-openssl-cert": "^0.0.97",
|
||||
"promise-toolbox": "^0.13.0",
|
||||
"promise-toolbox": "^0.14.0",
|
||||
"uuid": "^3.3.2"
|
||||
},
|
||||
"private": true
|
||||
|
||||
@@ -5,7 +5,7 @@ import uuidv4 from 'uuid/v4'
|
||||
import { access, constants, readFile, writeFile } from 'fs'
|
||||
import { EventEmitter } from 'events'
|
||||
import { filter, find, forOwn, map, omitBy, sample } from 'lodash'
|
||||
import { fromCallback, fromEvent } from 'promise-toolbox'
|
||||
import { fromCallback, promisify } from 'promise-toolbox'
|
||||
import { join } from 'path'
|
||||
|
||||
import { OvsdbClient } from './ovsdb-client'
|
||||
@@ -47,15 +47,8 @@ export const configurationSchema = {
|
||||
|
||||
// =============================================================================
|
||||
|
||||
async function fileWrite(path, data) {
|
||||
await fromCallback(writeFile, path, data)
|
||||
}
|
||||
|
||||
async function fileRead(path) {
|
||||
const result = await fromCallback(readFile, path)
|
||||
return result
|
||||
}
|
||||
|
||||
const fileWrite = promisify(writeFile)
|
||||
const fileRead = promisify(readFile)
|
||||
async function fileExists(path) {
|
||||
try {
|
||||
await fromCallback(access, path, constants.F_OK)
|
||||
@@ -74,8 +67,8 @@ async function fileExists(path) {
|
||||
|
||||
// 2019-09-03
|
||||
// Compatibility code, to be removed in 1 year.
|
||||
function updateNetworkOtherConfig(network) {
|
||||
return Promise.all(
|
||||
const updateNetworkOtherConfig = network =>
|
||||
Promise.all(
|
||||
map(
|
||||
{
|
||||
'cross-pool-network-uuid': 'cross_pool_network_uuid',
|
||||
@@ -101,14 +94,107 @@ function updateNetworkOtherConfig(network) {
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
function createPassword() {
|
||||
const chars =
|
||||
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789?!'
|
||||
return Array.from({ length: 16 }, _ => sample(chars)).join('')
|
||||
const CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789?!'
|
||||
const createPassword = () =>
|
||||
Array.from({ length: 16 }, _ => sample(CHARS)).join('')
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
async function generateCertificatesAndKey(dataDir) {
|
||||
const openssl = new NodeOpenssl()
|
||||
const rsaKeyOptions = {
|
||||
rsa_keygen_bits: 4096,
|
||||
format: 'PKCS8',
|
||||
}
|
||||
const subject = {
|
||||
countryName: 'XX',
|
||||
localityName: 'Default City',
|
||||
organizationName: 'Default Company LTD',
|
||||
}
|
||||
const csrOptions = {
|
||||
hash: 'sha256',
|
||||
startdate: new Date('1984-02-04 00:00:00'),
|
||||
enddate: new Date('2143-06-04 04:16:23'),
|
||||
subject: subject,
|
||||
}
|
||||
const caCsrOptions = {
|
||||
hash: 'sha256',
|
||||
days: NB_DAYS,
|
||||
subject: subject,
|
||||
}
|
||||
|
||||
let operation
|
||||
try {
|
||||
// CA Cert
|
||||
operation = 'Generating CA private key'
|
||||
const caKey = await fromCallback.call(
|
||||
openssl,
|
||||
'generateRSAPrivateKey',
|
||||
rsaKeyOptions
|
||||
)
|
||||
|
||||
operation = 'Generating CA certificate'
|
||||
const caCsr = await fromCallback.call(
|
||||
openssl,
|
||||
'generateCSR',
|
||||
caCsrOptions,
|
||||
caKey,
|
||||
null
|
||||
)
|
||||
|
||||
operation = 'Signing CA certificate'
|
||||
const caCrt = await fromCallback.call(
|
||||
openssl,
|
||||
'selfSignCSR',
|
||||
caCsr,
|
||||
caCsrOptions,
|
||||
caKey,
|
||||
null
|
||||
)
|
||||
await fileWrite(join(dataDir, CA_CERT), caCrt)
|
||||
|
||||
// Cert
|
||||
operation = 'Generating private key'
|
||||
const key = await fromCallback.call(
|
||||
openssl,
|
||||
'generateRSAPrivateKey',
|
||||
rsaKeyOptions
|
||||
)
|
||||
await fileWrite(join(dataDir, CLIENT_KEY), key)
|
||||
|
||||
operation = 'Generating certificate'
|
||||
const csr = await fromCallback.call(
|
||||
openssl,
|
||||
'generateCSR',
|
||||
csrOptions,
|
||||
key,
|
||||
null
|
||||
)
|
||||
|
||||
operation = 'Signing certificate'
|
||||
const crt = await fromCallback.call(
|
||||
openssl,
|
||||
'CASignCSR',
|
||||
csr,
|
||||
caCsrOptions,
|
||||
false,
|
||||
caCrt,
|
||||
caKey,
|
||||
null
|
||||
)
|
||||
await fileWrite(join(dataDir, CLIENT_CERT), crt)
|
||||
} catch (error) {
|
||||
log.error('Error while generating certificates and keys', {
|
||||
operation,
|
||||
error,
|
||||
})
|
||||
throw error
|
||||
}
|
||||
|
||||
log.debug('All certificates have been successfully written')
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
@@ -177,7 +263,7 @@ class SDNController extends EventEmitter {
|
||||
)
|
||||
|
||||
log.debug(`No default self-signed certificates exists, creating them`)
|
||||
await this._generateCertificatesAndKey(certDirectory)
|
||||
await generateCertificatesAndKey(certDirectory)
|
||||
}
|
||||
}
|
||||
// TODO: verify certificates and create new certificates if needed
|
||||
@@ -231,7 +317,11 @@ class SDNController extends EventEmitter {
|
||||
|
||||
// Expose method to create cross-pool private network
|
||||
const createCrossPoolPrivateNetwork = params =>
|
||||
this._createCrossPoolPrivateNetwork({ encrypted: false, mtu: 0, ...params })
|
||||
this._createCrossPoolPrivateNetwork({
|
||||
encrypted: false,
|
||||
mtu: 0,
|
||||
...params,
|
||||
})
|
||||
|
||||
createCrossPoolPrivateNetwork.description =
|
||||
'Creates a cross-pool private network on selected pools'
|
||||
@@ -1430,119 +1520,6 @@ class SDNController extends EventEmitter {
|
||||
this._ovsdbClients.push(client)
|
||||
return client
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async _generateCertificatesAndKey(dataDir) {
|
||||
const openssl = new NodeOpenssl()
|
||||
|
||||
const rsakeyoptions = {
|
||||
rsa_keygen_bits: 4096,
|
||||
format: 'PKCS8',
|
||||
}
|
||||
const subject = {
|
||||
countryName: 'XX',
|
||||
localityName: 'Default City',
|
||||
organizationName: 'Default Company LTD',
|
||||
}
|
||||
const csroptions = {
|
||||
hash: 'sha256',
|
||||
startdate: new Date('1984-02-04 00:00:00'),
|
||||
enddate: new Date('2143-06-04 04:16:23'),
|
||||
subject: subject,
|
||||
}
|
||||
const cacsroptions = {
|
||||
hash: 'sha256',
|
||||
days: NB_DAYS,
|
||||
subject: subject,
|
||||
}
|
||||
|
||||
// In all the following callbacks, `error` is:
|
||||
// - either an error object if there was an error
|
||||
// - or a boolean set to `false` if no error occurred
|
||||
openssl.generateRSAPrivateKey(rsakeyoptions, (error, cakey, cmd) => {
|
||||
if (error !== false) {
|
||||
log.error('Error while generating CA private key', {
|
||||
error,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
openssl.generateCSR(cacsroptions, cakey, null, (error, csr, cmd) => {
|
||||
if (error !== false) {
|
||||
log.error('Error while generating CA certificate', {
|
||||
error,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
openssl.selfSignCSR(
|
||||
csr,
|
||||
cacsroptions,
|
||||
cakey,
|
||||
null,
|
||||
async (error, cacrt, cmd) => {
|
||||
if (error !== false) {
|
||||
log.error('Error while signing CA certificate', {
|
||||
error,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
await fileWrite(join(dataDir, CA_CERT), cacrt)
|
||||
openssl.generateRSAPrivateKey(
|
||||
rsakeyoptions,
|
||||
async (error, key, cmd) => {
|
||||
if (error !== false) {
|
||||
log.error('Error while generating private key', {
|
||||
error,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
await fileWrite(join(dataDir, CLIENT_KEY), key)
|
||||
openssl.generateCSR(
|
||||
csroptions,
|
||||
key,
|
||||
null,
|
||||
(error, csr, cmd) => {
|
||||
if (error !== false) {
|
||||
log.error('Error while generating certificate', {
|
||||
error,
|
||||
})
|
||||
return
|
||||
}
|
||||
openssl.CASignCSR(
|
||||
csr,
|
||||
cacsroptions,
|
||||
false,
|
||||
cacrt,
|
||||
cakey,
|
||||
null,
|
||||
async (error, crt, cmd) => {
|
||||
if (error !== false) {
|
||||
log.error('Error while signing certificate', {
|
||||
error,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
await fileWrite(join(dataDir, CLIENT_CERT), crt)
|
||||
this.emit('certWritten')
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
await fromEvent(this, 'certWritten', {})
|
||||
log.debug('All certificates have been successfully written')
|
||||
}
|
||||
}
|
||||
|
||||
export default opts => new SDNController(opts)
|
||||
|
||||
6
packages/xo-server-test/src/_defaultValues.js
Normal file
6
packages/xo-server-test/src/_defaultValues.js
Normal file
@@ -0,0 +1,6 @@
|
||||
export const getDefaultName = () => `xo-server-test ${new Date().toISOString()}`
|
||||
|
||||
export const getDefaultSchedule = () => ({
|
||||
name: getDefaultName(),
|
||||
cron: '0 * * * * *',
|
||||
})
|
||||
@@ -2,15 +2,11 @@
|
||||
import defer from 'golike-defer'
|
||||
import Xo from 'xo-lib'
|
||||
import XoCollection from 'xo-collection'
|
||||
import { find, forOwn } from 'lodash'
|
||||
import { defaultsDeep, find, forOwn, pick } from 'lodash'
|
||||
import { fromEvent } from 'promise-toolbox'
|
||||
|
||||
import config from './_config'
|
||||
|
||||
const getDefaultCredentials = () => {
|
||||
const { email, password } = config.xoConnection
|
||||
return { email, password }
|
||||
}
|
||||
import { getDefaultName } from './_defaultValues'
|
||||
|
||||
class XoConnection extends Xo {
|
||||
constructor(opts) {
|
||||
@@ -72,7 +68,10 @@ class XoConnection extends Xo {
|
||||
}
|
||||
|
||||
@defer
|
||||
async connect($defer, credentials = getDefaultCredentials()) {
|
||||
async connect(
|
||||
$defer,
|
||||
credentials = pick(config.xoConnection, 'email', 'password')
|
||||
) {
|
||||
await this.open()
|
||||
$defer.onFailure(() => this.close())
|
||||
|
||||
@@ -111,9 +110,26 @@ class XoConnection extends Xo {
|
||||
}
|
||||
|
||||
async createTempBackupNgJob(params) {
|
||||
const job = await this.call('backupNg.createJob', params)
|
||||
this._tempResourceDisposers.push('backupNg.deleteJob', { id: job.id })
|
||||
return job
|
||||
// mutate and inject default values
|
||||
defaultsDeep(params, {
|
||||
mode: 'full',
|
||||
name: getDefaultName(),
|
||||
settings: {
|
||||
'': {
|
||||
// it must be enabled because the XAPI might be not able to coalesce VDIs
|
||||
// as fast as the tests run
|
||||
//
|
||||
// see https://xen-orchestra.com/docs/backup_troubleshooting.html#vdi-chain-protection
|
||||
bypassVdiChainsCheck: true,
|
||||
|
||||
// it must be 'never' to avoid race conditions with the plugin `backup-reports`
|
||||
reportWhen: 'never',
|
||||
},
|
||||
},
|
||||
})
|
||||
const id = await this.call('backupNg.createJob', params)
|
||||
this._tempResourceDisposers.push('backupNg.deleteJob', { id })
|
||||
return this.call('backupNg.getJob', { id })
|
||||
}
|
||||
|
||||
async createTempNetwork(params) {
|
||||
@@ -128,7 +144,7 @@ class XoConnection extends Xo {
|
||||
|
||||
async createTempVm(params) {
|
||||
const id = await this.call('vm.create', {
|
||||
name_label: 'XO Test',
|
||||
name_label: getDefaultName(),
|
||||
template: config.templates.templateWithoutDisks,
|
||||
...params,
|
||||
})
|
||||
@@ -210,6 +226,10 @@ class XoConnection extends Xo {
|
||||
return backups
|
||||
}
|
||||
|
||||
getBackupLogs(filter) {
|
||||
return this.call('backupNg.getLogs', { _forceRefresh: true, ...filter })
|
||||
}
|
||||
|
||||
async _cleanDisposers(disposers) {
|
||||
for (let n = disposers.length - 1; n > 0; ) {
|
||||
const params = disposers[n--]
|
||||
|
||||
@@ -1,61 +1,6 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`backupNg .createJob() : creates a new backup job with schedules 1`] = `
|
||||
Object {
|
||||
"id": Any<String>,
|
||||
"mode": "full",
|
||||
"name": "default-backupNg",
|
||||
"settings": Any<Object>,
|
||||
"type": "backup",
|
||||
"userId": Any<String>,
|
||||
"vms": Any<Object>,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg .createJob() : creates a new backup job with schedules 2`] = `
|
||||
Object {
|
||||
"cron": "0 * * * * *",
|
||||
"enabled": false,
|
||||
"id": Any<String>,
|
||||
"jobId": Any<String>,
|
||||
"name": "scheduleTest",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg .createJob() : creates a new backup job without schedules 1`] = `
|
||||
Object {
|
||||
"id": Any<String>,
|
||||
"mode": "full",
|
||||
"name": "default-backupNg",
|
||||
"settings": Object {
|
||||
"": Object {
|
||||
"reportWhen": "never",
|
||||
},
|
||||
},
|
||||
"type": "backup",
|
||||
"userId": Any<String>,
|
||||
"vms": Any<Object>,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg .runJob() : fails trying to run a backup job with a VM without disks 1`] = `
|
||||
Object {
|
||||
"data": Object {
|
||||
"mode": "full",
|
||||
"reportWhen": "never",
|
||||
},
|
||||
"end": Any<Number>,
|
||||
"id": Any<String>,
|
||||
"jobId": Any<String>,
|
||||
"jobName": "default-backupNg",
|
||||
"message": "backup",
|
||||
"scheduleId": Any<String>,
|
||||
"start": Any<Number>,
|
||||
"status": "skipped",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg .runJob() : fails trying to run a backup job with a VM without disks 2`] = `
|
||||
Object {
|
||||
"data": Object {
|
||||
"id": Any<String>,
|
||||
@@ -92,23 +37,6 @@ Array [
|
||||
exports[`backupNg .runJob() : fails trying to run a backup job without schedule 1`] = `[JsonRpcError: invalid parameters]`;
|
||||
|
||||
exports[`backupNg .runJob() : fails trying to run backup job without retentions 1`] = `
|
||||
Object {
|
||||
"data": Object {
|
||||
"mode": "full",
|
||||
"reportWhen": "never",
|
||||
},
|
||||
"end": Any<Number>,
|
||||
"id": Any<String>,
|
||||
"jobId": Any<String>,
|
||||
"jobName": "default-backupNg",
|
||||
"message": "backup",
|
||||
"scheduleId": Any<String>,
|
||||
"start": Any<Number>,
|
||||
"status": "failure",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg .runJob() : fails trying to run backup job without retentions 2`] = `
|
||||
Object {
|
||||
"data": Object {
|
||||
"id": Any<String>,
|
||||
@@ -128,22 +56,6 @@ Object {
|
||||
`;
|
||||
|
||||
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 1`] = `
|
||||
Object {
|
||||
"data": Object {
|
||||
"mode": "delta",
|
||||
"reportWhen": "never",
|
||||
},
|
||||
"end": Any<Number>,
|
||||
"id": Any<String>,
|
||||
"jobId": Any<String>,
|
||||
"message": "backup",
|
||||
"scheduleId": Any<String>,
|
||||
"start": Any<Number>,
|
||||
"status": "success",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 2`] = `
|
||||
Object {
|
||||
"data": Object {
|
||||
"id": Any<String>,
|
||||
@@ -157,7 +69,7 @@ Object {
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 3`] = `
|
||||
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 2`] = `
|
||||
Object {
|
||||
"end": Any<Number>,
|
||||
"id": Any<String>,
|
||||
@@ -168,7 +80,7 @@ Object {
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 4`] = `
|
||||
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 3`] = `
|
||||
Object {
|
||||
"data": Object {
|
||||
"id": Any<String>,
|
||||
@@ -183,6 +95,19 @@ Object {
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 4`] = `
|
||||
Object {
|
||||
"end": Any<Number>,
|
||||
"id": Any<String>,
|
||||
"message": Any<String>,
|
||||
"result": Object {
|
||||
"size": Any<Number>,
|
||||
},
|
||||
"start": Any<Number>,
|
||||
"status": "success",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 5`] = `
|
||||
Object {
|
||||
"end": Any<Number>,
|
||||
@@ -197,19 +122,6 @@ Object {
|
||||
`;
|
||||
|
||||
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 6`] = `
|
||||
Object {
|
||||
"end": Any<Number>,
|
||||
"id": Any<String>,
|
||||
"message": Any<String>,
|
||||
"result": Object {
|
||||
"size": Any<Number>,
|
||||
},
|
||||
"start": Any<Number>,
|
||||
"status": "success",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 7`] = `
|
||||
Object {
|
||||
"data": Object {
|
||||
"id": Any<String>,
|
||||
@@ -224,6 +136,19 @@ Object {
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 7`] = `
|
||||
Object {
|
||||
"end": Any<Number>,
|
||||
"id": Any<String>,
|
||||
"message": Any<String>,
|
||||
"result": Object {
|
||||
"size": Any<Number>,
|
||||
},
|
||||
"start": Any<Number>,
|
||||
"status": "success",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 8`] = `
|
||||
Object {
|
||||
"end": Any<Number>,
|
||||
@@ -238,35 +163,6 @@ Object {
|
||||
`;
|
||||
|
||||
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 9`] = `
|
||||
Object {
|
||||
"end": Any<Number>,
|
||||
"id": Any<String>,
|
||||
"message": Any<String>,
|
||||
"result": Object {
|
||||
"size": Any<Number>,
|
||||
},
|
||||
"start": Any<Number>,
|
||||
"status": "success",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 10`] = `
|
||||
Object {
|
||||
"data": Object {
|
||||
"mode": "delta",
|
||||
"reportWhen": "never",
|
||||
},
|
||||
"end": Any<Number>,
|
||||
"id": Any<String>,
|
||||
"jobId": Any<String>,
|
||||
"message": "backup",
|
||||
"scheduleId": Any<String>,
|
||||
"start": Any<Number>,
|
||||
"status": "success",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 11`] = `
|
||||
Object {
|
||||
"data": Object {
|
||||
"id": Any<String>,
|
||||
@@ -280,7 +176,7 @@ Object {
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 12`] = `
|
||||
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 10`] = `
|
||||
Object {
|
||||
"end": Any<Number>,
|
||||
"id": Any<String>,
|
||||
@@ -291,7 +187,7 @@ Object {
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 13`] = `
|
||||
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 11`] = `
|
||||
Object {
|
||||
"data": Object {
|
||||
"id": Any<String>,
|
||||
@@ -306,7 +202,7 @@ Object {
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 14`] = `
|
||||
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 12`] = `
|
||||
Object {
|
||||
"end": Any<Number>,
|
||||
"id": Any<String>,
|
||||
@@ -319,6 +215,34 @@ Object {
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 13`] = `
|
||||
Object {
|
||||
"end": Any<Number>,
|
||||
"id": Any<String>,
|
||||
"message": Any<String>,
|
||||
"result": Object {
|
||||
"size": Any<Number>,
|
||||
},
|
||||
"start": Any<Number>,
|
||||
"status": "success",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 14`] = `
|
||||
Object {
|
||||
"data": Object {
|
||||
"id": Any<String>,
|
||||
"isFull": false,
|
||||
"type": "remote",
|
||||
},
|
||||
"end": Any<Number>,
|
||||
"id": Any<String>,
|
||||
"message": Any<String>,
|
||||
"start": Any<Number>,
|
||||
"status": "success",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 15`] = `
|
||||
Object {
|
||||
"end": Any<Number>,
|
||||
@@ -334,62 +258,18 @@ Object {
|
||||
|
||||
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 16`] = `
|
||||
Object {
|
||||
"data": Object {
|
||||
"id": Any<String>,
|
||||
"isFull": false,
|
||||
"type": "remote",
|
||||
},
|
||||
"end": Any<Number>,
|
||||
"id": Any<String>,
|
||||
"message": Any<String>,
|
||||
"result": Object {
|
||||
"size": Any<Number>,
|
||||
},
|
||||
"start": Any<Number>,
|
||||
"status": "success",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 17`] = `
|
||||
Object {
|
||||
"end": Any<Number>,
|
||||
"id": Any<String>,
|
||||
"message": Any<String>,
|
||||
"result": Object {
|
||||
"size": Any<Number>,
|
||||
},
|
||||
"start": Any<Number>,
|
||||
"status": "success",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 18`] = `
|
||||
Object {
|
||||
"end": Any<Number>,
|
||||
"id": Any<String>,
|
||||
"message": Any<String>,
|
||||
"result": Object {
|
||||
"size": Any<Number>,
|
||||
},
|
||||
"start": Any<Number>,
|
||||
"status": "success",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 19`] = `
|
||||
Object {
|
||||
"data": Object {
|
||||
"mode": "delta",
|
||||
"reportWhen": "never",
|
||||
},
|
||||
"end": Any<Number>,
|
||||
"id": Any<String>,
|
||||
"jobId": Any<String>,
|
||||
"message": "backup",
|
||||
"scheduleId": Any<String>,
|
||||
"start": Any<Number>,
|
||||
"status": "success",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 20`] = `
|
||||
Object {
|
||||
"data": Object {
|
||||
"id": Any<String>,
|
||||
@@ -403,7 +283,7 @@ Object {
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 21`] = `
|
||||
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 18`] = `
|
||||
Object {
|
||||
"end": Any<Number>,
|
||||
"id": Any<String>,
|
||||
@@ -414,6 +294,47 @@ Object {
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 19`] = `
|
||||
Object {
|
||||
"data": Object {
|
||||
"id": Any<String>,
|
||||
"isFull": true,
|
||||
"type": "remote",
|
||||
},
|
||||
"end": Any<Number>,
|
||||
"id": Any<String>,
|
||||
"message": Any<String>,
|
||||
"start": Any<Number>,
|
||||
"status": "success",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 20`] = `
|
||||
Object {
|
||||
"end": Any<Number>,
|
||||
"id": Any<String>,
|
||||
"message": Any<String>,
|
||||
"result": Object {
|
||||
"size": Any<Number>,
|
||||
},
|
||||
"start": Any<Number>,
|
||||
"status": "success",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 21`] = `
|
||||
Object {
|
||||
"end": Any<Number>,
|
||||
"id": Any<String>,
|
||||
"message": Any<String>,
|
||||
"result": Object {
|
||||
"size": Any<Number>,
|
||||
},
|
||||
"start": Any<Number>,
|
||||
"status": "success",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 22`] = `
|
||||
Object {
|
||||
"data": Object {
|
||||
@@ -455,65 +376,7 @@ Object {
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 25`] = `
|
||||
Object {
|
||||
"data": Object {
|
||||
"id": Any<String>,
|
||||
"isFull": true,
|
||||
"type": "remote",
|
||||
},
|
||||
"end": Any<Number>,
|
||||
"id": Any<String>,
|
||||
"message": Any<String>,
|
||||
"start": Any<Number>,
|
||||
"status": "success",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 26`] = `
|
||||
Object {
|
||||
"end": Any<Number>,
|
||||
"id": Any<String>,
|
||||
"message": Any<String>,
|
||||
"result": Object {
|
||||
"size": Any<Number>,
|
||||
},
|
||||
"start": Any<Number>,
|
||||
"status": "success",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg execute three times a delta backup with 2 remotes, 2 as retention and 2 as fullInterval 27`] = `
|
||||
Object {
|
||||
"end": Any<Number>,
|
||||
"id": Any<String>,
|
||||
"message": Any<String>,
|
||||
"result": Object {
|
||||
"size": Any<Number>,
|
||||
},
|
||||
"start": Any<Number>,
|
||||
"status": "success",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg execute three times a rolling snapshot with 2 as retention & revert to an old state 1`] = `
|
||||
Object {
|
||||
"data": Object {
|
||||
"mode": "full",
|
||||
"reportWhen": "never",
|
||||
},
|
||||
"end": Any<Number>,
|
||||
"id": Any<String>,
|
||||
"jobId": Any<String>,
|
||||
"jobName": "default-backupNg",
|
||||
"message": "backup",
|
||||
"scheduleId": Any<String>,
|
||||
"start": Any<Number>,
|
||||
"status": "success",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg execute three times a rolling snapshot with 2 as retention & revert to an old state 2`] = `
|
||||
Object {
|
||||
"end": Any<Number>,
|
||||
"id": Any<String>,
|
||||
@@ -524,7 +387,7 @@ Object {
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`backupNg execute three times a rolling snapshot with 2 as retention & revert to an old state 3`] = `
|
||||
exports[`backupNg execute three times a rolling snapshot with 2 as retention & revert to an old state 2`] = `
|
||||
Object {
|
||||
"data": Object {
|
||||
"id": Any<String>,
|
||||
|
||||
@@ -6,20 +6,44 @@ import { noSuchObject } from 'xo-common/api-errors'
|
||||
import config from '../_config'
|
||||
import randomId from '../_randomId'
|
||||
import xo from '../_xoConnection'
|
||||
import { getDefaultName, getDefaultSchedule } from '../_defaultValues'
|
||||
|
||||
const DEFAULT_SCHEDULE = {
|
||||
name: 'scheduleTest',
|
||||
cron: '0 * * * * *',
|
||||
const validateBackupJob = (jobInput, jobOutput, createdSchedule) => {
|
||||
const expectedObj = {
|
||||
id: expect.any(String),
|
||||
mode: jobInput.mode,
|
||||
name: jobInput.name,
|
||||
type: 'backup',
|
||||
settings: {
|
||||
'': jobInput.settings[''],
|
||||
},
|
||||
userId: xo._user.id,
|
||||
vms: jobInput.vms,
|
||||
}
|
||||
|
||||
const schedules = jobInput.schedules
|
||||
if (schedules !== undefined) {
|
||||
const scheduleTmpId = Object.keys(schedules)[0]
|
||||
expect(createdSchedule).toEqual({
|
||||
...schedules[scheduleTmpId],
|
||||
enabled: false,
|
||||
id: expect.any(String),
|
||||
jobId: jobOutput.id,
|
||||
})
|
||||
|
||||
expectedObj.settings[createdSchedule.id] = jobInput.settings[scheduleTmpId]
|
||||
}
|
||||
|
||||
expect(jobOutput).toEqual(expectedObj)
|
||||
}
|
||||
|
||||
const validateRootTask = (log, props) =>
|
||||
expect(log).toMatchSnapshot({
|
||||
const validateRootTask = (log, expected) =>
|
||||
expect(log).toEqual({
|
||||
end: expect.any(Number),
|
||||
id: expect.any(String),
|
||||
jobId: expect.any(String),
|
||||
scheduleId: expect.any(String),
|
||||
message: 'backup',
|
||||
start: expect.any(Number),
|
||||
...props,
|
||||
...expected,
|
||||
})
|
||||
|
||||
const validateVmTask = (task, vmId, props) => {
|
||||
@@ -66,88 +90,55 @@ const validateOperationTask = (task, props) => {
|
||||
})
|
||||
}
|
||||
|
||||
// Note: `bypassVdiChainsCheck` must be enabled because the XAPI might be not
|
||||
// able to coalesce VDIs as fast as the tests run.
|
||||
//
|
||||
// See https://xen-orchestra.com/docs/backup_troubleshooting.html#vdi-chain-protection
|
||||
describe('backupNg', () => {
|
||||
let defaultBackupNg
|
||||
|
||||
beforeAll(() => {
|
||||
defaultBackupNg = {
|
||||
name: 'default-backupNg',
|
||||
mode: 'full',
|
||||
vms: {
|
||||
id: config.vms.default,
|
||||
},
|
||||
settings: {
|
||||
'': {
|
||||
reportWhen: 'never',
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
describe('.createJob() :', () => {
|
||||
it('creates a new backup job without schedules', async () => {
|
||||
const backupNg = await xo.createTempBackupNgJob(defaultBackupNg)
|
||||
expect(backupNg).toMatchSnapshot({
|
||||
id: expect.any(String),
|
||||
userId: expect.any(String),
|
||||
vms: expect.any(Object),
|
||||
})
|
||||
expect(backupNg.vms).toEqual(defaultBackupNg.vms)
|
||||
expect(backupNg.userId).toBe(xo._user.id)
|
||||
const jobInput = {
|
||||
mode: 'full',
|
||||
vms: {
|
||||
id: config.vms.default,
|
||||
},
|
||||
}
|
||||
const jobOutput = await xo.createTempBackupNgJob(jobInput)
|
||||
validateBackupJob(jobInput, jobOutput)
|
||||
})
|
||||
|
||||
it('creates a new backup job with schedules', async () => {
|
||||
const scheduleTempId = randomId()
|
||||
const { id: jobId } = await xo.createTempBackupNgJob({
|
||||
...defaultBackupNg,
|
||||
const jobInput = {
|
||||
mode: 'full',
|
||||
schedules: {
|
||||
[scheduleTempId]: DEFAULT_SCHEDULE,
|
||||
[scheduleTempId]: getDefaultSchedule(),
|
||||
},
|
||||
settings: {
|
||||
...defaultBackupNg.settings,
|
||||
[scheduleTempId]: { snapshotRetention: 1 },
|
||||
},
|
||||
})
|
||||
|
||||
const backupNgJob = await xo.call('backupNg.getJob', { id: jobId })
|
||||
|
||||
expect(backupNgJob).toMatchSnapshot({
|
||||
id: expect.any(String),
|
||||
userId: expect.any(String),
|
||||
settings: expect.any(Object),
|
||||
vms: expect.any(Object),
|
||||
})
|
||||
expect(backupNgJob.vms).toEqual(defaultBackupNg.vms)
|
||||
expect(backupNgJob.userId).toBe(xo._user.id)
|
||||
|
||||
expect(Object.keys(backupNgJob.settings).length).toBe(2)
|
||||
const schedule = await xo.getSchedule({ jobId })
|
||||
expect(typeof schedule).toBe('object')
|
||||
expect(backupNgJob.settings[schedule.id]).toEqual({
|
||||
snapshotRetention: 1,
|
||||
})
|
||||
|
||||
expect(schedule).toMatchSnapshot({
|
||||
id: expect.any(String),
|
||||
jobId: expect.any(String),
|
||||
})
|
||||
vms: {
|
||||
id: config.vms.default,
|
||||
},
|
||||
}
|
||||
const jobOutput = await xo.createTempBackupNgJob(jobInput)
|
||||
validateBackupJob(
|
||||
jobInput,
|
||||
jobOutput,
|
||||
await xo.getSchedule({ jobId: jobOutput.id })
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('.delete() :', () => {
|
||||
it('deletes a backup job', async () => {
|
||||
const scheduleTempId = randomId()
|
||||
const { id: jobId } = await xo.call('backupNg.createJob', {
|
||||
...defaultBackupNg,
|
||||
const jobId = await xo.call('backupNg.createJob', {
|
||||
mode: 'full',
|
||||
name: getDefaultName(),
|
||||
vms: {
|
||||
id: config.vms.default,
|
||||
},
|
||||
schedules: {
|
||||
[scheduleTempId]: DEFAULT_SCHEDULE,
|
||||
[scheduleTempId]: getDefaultSchedule(),
|
||||
},
|
||||
settings: {
|
||||
...defaultBackupNg.settings,
|
||||
[scheduleTempId]: { snapshotRetention: 1 },
|
||||
},
|
||||
})
|
||||
@@ -173,16 +164,19 @@ describe('backupNg', () => {
|
||||
|
||||
describe('.runJob() :', () => {
|
||||
it('fails trying to run a backup job without schedule', async () => {
|
||||
const { id } = await xo.createTempBackupNgJob(defaultBackupNg)
|
||||
const { id } = await xo.createTempBackupNgJob({
|
||||
vms: {
|
||||
id: config.vms.default,
|
||||
},
|
||||
})
|
||||
await expect(xo.call('backupNg.runJob', { id })).rejects.toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('fails trying to run a backup job with no matching VMs', async () => {
|
||||
const scheduleTempId = randomId()
|
||||
const { id: jobId } = await xo.createTempBackupNgJob({
|
||||
...defaultBackupNg,
|
||||
schedules: {
|
||||
[scheduleTempId]: DEFAULT_SCHEDULE,
|
||||
[scheduleTempId]: getDefaultSchedule(),
|
||||
},
|
||||
settings: {
|
||||
[scheduleTempId]: { snapshotRetention: 1 },
|
||||
@@ -205,9 +199,8 @@ describe('backupNg', () => {
|
||||
jest.setTimeout(7e3)
|
||||
const scheduleTempId = randomId()
|
||||
const { id: jobId } = await xo.createTempBackupNgJob({
|
||||
...defaultBackupNg,
|
||||
schedules: {
|
||||
[scheduleTempId]: DEFAULT_SCHEDULE,
|
||||
[scheduleTempId]: getDefaultSchedule(),
|
||||
},
|
||||
settings: {
|
||||
[scheduleTempId]: { snapshotRetention: 1 },
|
||||
@@ -221,7 +214,7 @@ describe('backupNg', () => {
|
||||
expect(typeof schedule).toBe('object')
|
||||
|
||||
await xo.call('backupNg.runJob', { id: jobId, schedule: schedule.id })
|
||||
const [log] = await xo.call('backupNg.getLogs', {
|
||||
const [log] = await xo.getBackupLogs({
|
||||
scheduleId: schedule.id,
|
||||
})
|
||||
expect(log.warnings).toMatchSnapshot()
|
||||
@@ -231,25 +224,23 @@ describe('backupNg', () => {
|
||||
jest.setTimeout(8e3)
|
||||
await xo.createTempServer(config.servers.default)
|
||||
const { id: vmIdWithoutDisks } = await xo.createTempVm({
|
||||
name_label: 'XO Test Without Disks',
|
||||
name_description: 'Creating a vm without disks',
|
||||
template: config.templates.templateWithoutDisks,
|
||||
})
|
||||
|
||||
const scheduleTempId = randomId()
|
||||
const { id: jobId } = await xo.createTempBackupNgJob({
|
||||
...defaultBackupNg,
|
||||
const jobInput = {
|
||||
schedules: {
|
||||
[scheduleTempId]: DEFAULT_SCHEDULE,
|
||||
[scheduleTempId]: getDefaultSchedule(),
|
||||
},
|
||||
settings: {
|
||||
...defaultBackupNg.settings,
|
||||
[scheduleTempId]: { snapshotRetention: 1 },
|
||||
},
|
||||
vms: {
|
||||
id: vmIdWithoutDisks,
|
||||
},
|
||||
})
|
||||
}
|
||||
const { id: jobId } = await xo.createTempBackupNgJob(jobInput)
|
||||
|
||||
const schedule = await xo.getSchedule({ jobId })
|
||||
expect(typeof schedule).toBe('object')
|
||||
@@ -260,16 +251,20 @@ describe('backupNg', () => {
|
||||
tasks: [vmTask],
|
||||
...log
|
||||
},
|
||||
] = await xo.call('backupNg.getLogs', {
|
||||
] = await xo.getBackupLogs({
|
||||
jobId,
|
||||
scheduleId: schedule.id,
|
||||
})
|
||||
expect(log).toMatchSnapshot({
|
||||
end: expect.any(Number),
|
||||
id: expect.any(String),
|
||||
jobId: expect.any(String),
|
||||
scheduleId: expect.any(String),
|
||||
start: expect.any(Number),
|
||||
|
||||
validateRootTask(log, {
|
||||
data: {
|
||||
mode: jobInput.mode,
|
||||
reportWhen: jobInput.settings[''].reportWhen,
|
||||
},
|
||||
jobId,
|
||||
jobName: jobInput.name,
|
||||
scheduleId: schedule.id,
|
||||
status: 'skipped',
|
||||
})
|
||||
|
||||
expect(vmTask).toMatchSnapshot({
|
||||
@@ -293,22 +288,24 @@ describe('backupNg', () => {
|
||||
const scheduleTempId = randomId()
|
||||
await xo.createTempServer(config.servers.default)
|
||||
const { id: remoteId } = await xo.createTempRemote(config.remotes.default)
|
||||
const { id: jobId } = await xo.createTempBackupNgJob({
|
||||
...defaultBackupNg,
|
||||
const jobInput = {
|
||||
remotes: {
|
||||
id: remoteId,
|
||||
},
|
||||
schedules: {
|
||||
[scheduleTempId]: DEFAULT_SCHEDULE,
|
||||
[scheduleTempId]: getDefaultSchedule(),
|
||||
},
|
||||
settings: {
|
||||
...defaultBackupNg.settings,
|
||||
[scheduleTempId]: {},
|
||||
},
|
||||
srs: {
|
||||
id: config.srs.default,
|
||||
},
|
||||
})
|
||||
vms: {
|
||||
id: config.vms.default,
|
||||
},
|
||||
}
|
||||
const { id: jobId } = await xo.createTempBackupNgJob(jobInput)
|
||||
|
||||
const schedule = await xo.getSchedule({ jobId })
|
||||
expect(typeof schedule).toBe('object')
|
||||
@@ -319,17 +316,20 @@ describe('backupNg', () => {
|
||||
tasks: [task],
|
||||
...log
|
||||
},
|
||||
] = await xo.call('backupNg.getLogs', {
|
||||
] = await xo.getBackupLogs({
|
||||
jobId,
|
||||
scheduleId: schedule.id,
|
||||
})
|
||||
|
||||
expect(log).toMatchSnapshot({
|
||||
end: expect.any(Number),
|
||||
id: expect.any(String),
|
||||
jobId: expect.any(String),
|
||||
scheduleId: expect.any(String),
|
||||
start: expect.any(Number),
|
||||
validateRootTask(log, {
|
||||
data: {
|
||||
mode: jobInput.mode,
|
||||
reportWhen: jobInput.settings[''].reportWhen,
|
||||
},
|
||||
jobId,
|
||||
jobName: jobInput.name,
|
||||
scheduleId: schedule.id,
|
||||
status: 'failure',
|
||||
})
|
||||
|
||||
expect(task).toMatchSnapshot({
|
||||
@@ -352,7 +352,6 @@ describe('backupNg', () => {
|
||||
jest.setTimeout(6e4)
|
||||
await xo.createTempServer(config.servers.default)
|
||||
let vm = await xo.createTempVm({
|
||||
name_label: 'XO Test Temp',
|
||||
name_description: 'Creating a temporary vm',
|
||||
template: config.templates.default,
|
||||
VDIs: [
|
||||
@@ -365,22 +364,18 @@ describe('backupNg', () => {
|
||||
})
|
||||
|
||||
const scheduleTempId = randomId()
|
||||
const { id: jobId } = await xo.createTempBackupNgJob({
|
||||
...defaultBackupNg,
|
||||
const jobInput = {
|
||||
vms: {
|
||||
id: vm.id,
|
||||
},
|
||||
schedules: {
|
||||
[scheduleTempId]: DEFAULT_SCHEDULE,
|
||||
[scheduleTempId]: getDefaultSchedule(),
|
||||
},
|
||||
settings: {
|
||||
'': {
|
||||
bypassVdiChainsCheck: true,
|
||||
reportWhen: 'never',
|
||||
},
|
||||
[scheduleTempId]: { snapshotRetention: 2 },
|
||||
},
|
||||
})
|
||||
}
|
||||
const { id: jobId } = await xo.createTempBackupNgJob(jobInput)
|
||||
|
||||
const schedule = await xo.getSchedule({ jobId })
|
||||
expect(typeof schedule).toBe('object')
|
||||
@@ -415,17 +410,20 @@ describe('backupNg', () => {
|
||||
tasks: [{ tasks: subTasks, ...vmTask }],
|
||||
...log
|
||||
},
|
||||
] = await xo.call('backupNg.getLogs', {
|
||||
] = await xo.getBackupLogs({
|
||||
jobId,
|
||||
scheduleId: schedule.id,
|
||||
})
|
||||
|
||||
expect(log).toMatchSnapshot({
|
||||
end: expect.any(Number),
|
||||
id: expect.any(String),
|
||||
jobId: expect.any(String),
|
||||
scheduleId: expect.any(String),
|
||||
start: expect.any(Number),
|
||||
validateRootTask(log, {
|
||||
data: {
|
||||
mode: jobInput.mode,
|
||||
reportWhen: jobInput.settings[''].reportWhen,
|
||||
},
|
||||
jobId,
|
||||
jobName: jobInput.name,
|
||||
scheduleId: schedule.id,
|
||||
status: 'success',
|
||||
})
|
||||
|
||||
const subTaskSnapshot = subTasks.find(
|
||||
@@ -470,7 +468,7 @@ describe('backupNg', () => {
|
||||
const exportRetention = 2
|
||||
const fullInterval = 2
|
||||
const scheduleTempId = randomId()
|
||||
const { id: jobId } = await xo.createTempBackupNgJob({
|
||||
const jobInput = {
|
||||
mode: 'delta',
|
||||
remotes: {
|
||||
id: {
|
||||
@@ -478,13 +476,11 @@ describe('backupNg', () => {
|
||||
},
|
||||
},
|
||||
schedules: {
|
||||
[scheduleTempId]: DEFAULT_SCHEDULE,
|
||||
[scheduleTempId]: getDefaultSchedule(),
|
||||
},
|
||||
settings: {
|
||||
'': {
|
||||
bypassVdiChainsCheck: true,
|
||||
fullInterval,
|
||||
reportWhen: 'never',
|
||||
},
|
||||
[remoteId1]: { deleteFirst: true },
|
||||
[scheduleTempId]: { exportRetention },
|
||||
@@ -492,7 +488,8 @@ describe('backupNg', () => {
|
||||
vms: {
|
||||
id: vmToBackup,
|
||||
},
|
||||
})
|
||||
}
|
||||
const { id: jobId } = await xo.createTempBackupNgJob(jobInput)
|
||||
|
||||
const schedule = await xo.getSchedule({ jobId })
|
||||
expect(typeof schedule).toBe('object')
|
||||
@@ -506,7 +503,7 @@ describe('backupNg', () => {
|
||||
expect(backups.length).toBe(exportRetention)
|
||||
)
|
||||
|
||||
const backupLogs = await xo.call('backupNg.getLogs', {
|
||||
const backupLogs = await xo.getBackupLogs({
|
||||
jobId,
|
||||
scheduleId: schedule.id,
|
||||
})
|
||||
@@ -515,10 +512,12 @@ describe('backupNg', () => {
|
||||
backupLogs.forEach(({ tasks = [], ...log }, key) => {
|
||||
validateRootTask(log, {
|
||||
data: {
|
||||
mode: 'delta',
|
||||
reportWhen: 'never',
|
||||
mode: jobInput.mode,
|
||||
reportWhen: jobInput.settings[''].reportWhen,
|
||||
},
|
||||
message: 'backup',
|
||||
jobId,
|
||||
jobName: jobInput.name,
|
||||
scheduleId: schedule.id,
|
||||
status: 'success',
|
||||
})
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
"dependencies": {
|
||||
"@xen-orchestra/async-map": "^0.0.0",
|
||||
"@xen-orchestra/cron": "^1.0.4",
|
||||
"@xen-orchestra/log": "^0.1.4",
|
||||
"@xen-orchestra/log": "^0.2.0",
|
||||
"handlebars": "^4.0.6",
|
||||
"html-minifier": "^4.0.0",
|
||||
"human-format": "^0.10.0",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "xo-server",
|
||||
"version": "5.50.0",
|
||||
"version": "5.50.1",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "Server part of Xen-Orchestra",
|
||||
"keywords": [
|
||||
@@ -39,13 +39,14 @@
|
||||
"@xen-orchestra/defined": "^0.0.0",
|
||||
"@xen-orchestra/emit-async": "^0.0.0",
|
||||
"@xen-orchestra/fs": "^0.10.1",
|
||||
"@xen-orchestra/log": "^0.1.4",
|
||||
"@xen-orchestra/log": "^0.2.0",
|
||||
"@xen-orchestra/mixin": "^0.0.0",
|
||||
"ajv": "^6.1.1",
|
||||
"app-conf": "^0.7.0",
|
||||
"archiver": "^3.0.0",
|
||||
"async-iterator-to-stream": "^1.0.1",
|
||||
"base64url": "^3.0.0",
|
||||
"bind-property-descriptor": "^1.0.0",
|
||||
"blocked": "^1.2.1",
|
||||
"bluebird": "^3.5.1",
|
||||
"body-parser": "^1.18.2",
|
||||
@@ -123,7 +124,7 @@
|
||||
"value-matcher": "^0.2.0",
|
||||
"vhd-lib": "^0.7.0",
|
||||
"ws": "^6.0.0",
|
||||
"xen-api": "^0.27.1",
|
||||
"xen-api": "^0.27.2",
|
||||
"xml2js": "^0.4.19",
|
||||
"xo-acl-resolver": "^0.4.1",
|
||||
"xo-collection": "^0.4.1",
|
||||
|
||||
34
packages/xo-server/src/_MultiKeyMap.spec.js
Normal file
34
packages/xo-server/src/_MultiKeyMap.spec.js
Normal file
@@ -0,0 +1,34 @@
|
||||
/* eslint-env jest */
|
||||
|
||||
import MultiKeyMap from './_MultiKeyMap'
|
||||
|
||||
describe('MultiKeyMap', () => {
|
||||
it('works', () => {
|
||||
const map = new MultiKeyMap()
|
||||
|
||||
const keys = [
|
||||
// null key
|
||||
[],
|
||||
// simple key
|
||||
['foo'],
|
||||
// composite key
|
||||
['foo', 'bar'],
|
||||
// reverse composite key
|
||||
['bar', 'foo'],
|
||||
]
|
||||
const values = keys.map(() => ({}))
|
||||
|
||||
// set all values first to make sure they are all stored and not only the
|
||||
// last one
|
||||
keys.forEach((key, i) => {
|
||||
map.set(key, values[i])
|
||||
})
|
||||
|
||||
keys.forEach((key, i) => {
|
||||
// copy the key to make sure the array itself is not the key
|
||||
expect(map.get(key.slice())).toBe(values[i])
|
||||
map.delete(key.slice())
|
||||
expect(map.get(key.slice())).toBe(undefined)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -16,6 +16,10 @@ function scheduleRemoveCacheEntry(keys, expires) {
|
||||
|
||||
const defaultKeyFn = () => []
|
||||
|
||||
const { slice } = Array.prototype
|
||||
|
||||
export const REMOVE_CACHE_ENTRY = {}
|
||||
|
||||
// debounce an async function so that all subsequent calls in a delay receive
|
||||
// the same result
|
||||
//
|
||||
@@ -26,7 +30,14 @@ const defaultKeyFn = () => []
|
||||
export const debounceWithKey = (fn, delay, keyFn = defaultKeyFn) => {
|
||||
const cache = new MultiKeyMap()
|
||||
const delayFn = typeof delay === 'number' ? () => delay : delay
|
||||
return function() {
|
||||
return function(arg) {
|
||||
if (arg === REMOVE_CACHE_ENTRY) {
|
||||
return removeCacheEntry(
|
||||
cache,
|
||||
ensureArray(keyFn.apply(this, slice.call(arguments, 1)))
|
||||
)
|
||||
}
|
||||
|
||||
const keys = ensureArray(keyFn.apply(this, arguments))
|
||||
let promise = cache.get(keys)
|
||||
if (promise === undefined) {
|
||||
|
||||
29
packages/xo-server/src/_pDebounceWithKey.spec.js
Normal file
29
packages/xo-server/src/_pDebounceWithKey.spec.js
Normal file
@@ -0,0 +1,29 @@
|
||||
/* eslint-env jest */
|
||||
|
||||
import { debounceWithKey, REMOVE_CACHE_ENTRY } from './_pDebounceWithKey'
|
||||
|
||||
describe('REMOVE_CACHE_ENTRY', () => {
|
||||
it('clears the cache', async () => {
|
||||
let i = 0
|
||||
const debouncedFn = debounceWithKey(
|
||||
function() {
|
||||
return Promise.resolve(++i)
|
||||
},
|
||||
Infinity,
|
||||
id => id
|
||||
)
|
||||
|
||||
// not cached accross keys
|
||||
expect(await debouncedFn(1)).toBe(1)
|
||||
expect(await debouncedFn(2)).toBe(2)
|
||||
|
||||
// retrieve the already cached values
|
||||
expect(await debouncedFn(1)).toBe(1)
|
||||
expect(await debouncedFn(2)).toBe(2)
|
||||
|
||||
// an entry for a specific key can be removed
|
||||
debouncedFn(REMOVE_CACHE_ENTRY, 1)
|
||||
expect(await debouncedFn(1)).toBe(3)
|
||||
expect(await debouncedFn(2)).toBe(2)
|
||||
})
|
||||
})
|
||||
@@ -3,11 +3,12 @@ import { fromCallback } from 'promise-toolbox'
|
||||
import { pipeline } from 'readable-stream'
|
||||
|
||||
import createNdJsonStream from '../_createNdJsonStream'
|
||||
import { REMOVE_CACHE_ENTRY } from '../_pDebounceWithKey'
|
||||
import { safeDateFormat } from '../utils'
|
||||
|
||||
export function createJob({ schedules, ...job }) {
|
||||
job.userId = this.user.id
|
||||
return this.createBackupNgJob(job, schedules)
|
||||
return this.createBackupNgJob(job, schedules).then(({ id }) => id)
|
||||
}
|
||||
|
||||
createJob.permission = 'admin'
|
||||
@@ -184,7 +185,20 @@ getAllLogs.params = {
|
||||
ndjson: { type: 'boolean', optional: true },
|
||||
}
|
||||
|
||||
export function getLogs({ after, before, limit, ...filter }) {
|
||||
export function getLogs({
|
||||
after,
|
||||
before,
|
||||
limit,
|
||||
|
||||
// TODO: it's a temporary work-around which should be removed
|
||||
// when the consolidated logs will be stored in the DB
|
||||
_forceRefresh = false,
|
||||
|
||||
...filter
|
||||
}) {
|
||||
if (_forceRefresh) {
|
||||
this.getBackupNgLogs(REMOVE_CACHE_ENTRY)
|
||||
}
|
||||
return this.getBackupNgLogsSorted({ after, before, limit, filter })
|
||||
}
|
||||
|
||||
|
||||
@@ -221,12 +221,7 @@ emergencyShutdownHost.resolve = {
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export async function isHostServerTimeConsistent({ host }) {
|
||||
try {
|
||||
await this.getXapi(host).assertConsistentHostServerTime(host._xapiRef)
|
||||
return true
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
return this.getXapi(host).isHostServerTimeConsistent(host._xapiRef)
|
||||
}
|
||||
|
||||
isHostServerTimeConsistent.params = {
|
||||
|
||||
@@ -777,7 +777,7 @@ export async function probeIscsiExists({
|
||||
)
|
||||
|
||||
const srs = []
|
||||
forEach(ensureArray(xml['SRlist'].SR), sr => {
|
||||
forEach(ensureArray(xml.SRlist.SR), sr => {
|
||||
// get the UUID of SR connected to this LUN
|
||||
srs.push({ uuid: sr.UUID.trim() })
|
||||
})
|
||||
@@ -845,7 +845,7 @@ export async function probeNfsExists({ host, server, serverPath }) {
|
||||
|
||||
const srs = []
|
||||
|
||||
forEach(ensureArray(xml['SRlist'].SR), sr => {
|
||||
forEach(ensureArray(xml.SRlist.SR), sr => {
|
||||
// get the UUID of SR connected to this LUN
|
||||
srs.push({ uuid: sr.UUID.trim() })
|
||||
})
|
||||
|
||||
@@ -85,7 +85,7 @@ async function rateLimitedRetry(action, shouldRetry, retryCount = 20) {
|
||||
function createVolumeInfoTypes() {
|
||||
function parseHeal(parsed) {
|
||||
const bricks = []
|
||||
parsed['healInfo']['bricks']['brick'].forEach(brick => {
|
||||
parsed.healInfo.bricks.brick.forEach(brick => {
|
||||
bricks.push(brick)
|
||||
if (brick.file) {
|
||||
brick.file = ensureArray(brick.file)
|
||||
@@ -96,21 +96,21 @@ function createVolumeInfoTypes() {
|
||||
|
||||
function parseStatus(parsed) {
|
||||
const brickDictByUuid = {}
|
||||
const volume = parsed['volStatus']['volumes']['volume']
|
||||
volume['node'].forEach(node => {
|
||||
const volume = parsed.volStatus.volumes.volume
|
||||
volume.node.forEach(node => {
|
||||
brickDictByUuid[node.peerid] = brickDictByUuid[node.peerid] || []
|
||||
brickDictByUuid[node.peerid].push(node)
|
||||
})
|
||||
return {
|
||||
commandStatus: true,
|
||||
result: { nodes: brickDictByUuid, tasks: volume['tasks'] },
|
||||
result: { nodes: brickDictByUuid, tasks: volume.tasks },
|
||||
}
|
||||
}
|
||||
|
||||
async function parseInfo(parsed) {
|
||||
const volume = parsed['volInfo']['volumes']['volume']
|
||||
volume['bricks'] = volume['bricks']['brick']
|
||||
volume['options'] = volume['options']['option']
|
||||
const volume = parsed.volInfo.volumes.volume
|
||||
volume.bricks = volume.bricks.brick
|
||||
volume.options = volume.options.option
|
||||
return { commandStatus: true, result: volume }
|
||||
}
|
||||
|
||||
@@ -118,23 +118,23 @@ function createVolumeInfoTypes() {
|
||||
return async function(sr) {
|
||||
const glusterEndpoint = this::_getGlusterEndpoint(sr)
|
||||
const cmdShouldRetry = result =>
|
||||
!result['commandStatus'] &&
|
||||
((result.parsed && result.parsed['cliOutput']['opErrno'] === '30802') ||
|
||||
!result.commandStatus &&
|
||||
((result.parsed && result.parsed.cliOutput.opErrno === '30802') ||
|
||||
result.stderr.match(/Another transaction is in progress/))
|
||||
const runCmd = async () =>
|
||||
glusterCmd(glusterEndpoint, 'volume ' + command, true)
|
||||
const commandResult = await rateLimitedRetry(runCmd, cmdShouldRetry, 30)
|
||||
return commandResult['commandStatus']
|
||||
? this::handler(commandResult.parsed['cliOutput'], sr)
|
||||
return commandResult.commandStatus
|
||||
? this::handler(commandResult.parsed.cliOutput, sr)
|
||||
: commandResult
|
||||
}
|
||||
}
|
||||
|
||||
async function profileType(sr) {
|
||||
async function parseProfile(parsed) {
|
||||
const volume = parsed['volProfile']
|
||||
volume['bricks'] = ensureArray(volume['brick'])
|
||||
delete volume['brick']
|
||||
const volume = parsed.volProfile
|
||||
volume.bricks = ensureArray(volume.brick)
|
||||
delete volume.brick
|
||||
return { commandStatus: true, result: volume }
|
||||
}
|
||||
|
||||
@@ -143,9 +143,9 @@ function createVolumeInfoTypes() {
|
||||
|
||||
async function profileTopType(sr) {
|
||||
async function parseTop(parsed) {
|
||||
const volume = parsed['volTop']
|
||||
volume['bricks'] = ensureArray(volume['brick'])
|
||||
delete volume['brick']
|
||||
const volume = parsed.volTop
|
||||
volume.bricks = ensureArray(volume.brick)
|
||||
delete volume.brick
|
||||
return { commandStatus: true, result: volume }
|
||||
}
|
||||
|
||||
@@ -326,7 +326,7 @@ async function remoteSsh(glusterEndpoint, cmd, ignoreError = false) {
|
||||
}
|
||||
messageArray.push(`${key}: ${result[key]}`)
|
||||
}
|
||||
messageArray.push('command: ' + result['command'].join(' '))
|
||||
messageArray.push('command: ' + result.command.join(' '))
|
||||
messageKeys.splice(messageKeys.indexOf('command'), 1)
|
||||
for (const key of messageKeys) {
|
||||
messageArray.push(`${key}: ${JSON.stringify(result[key])}`)
|
||||
@@ -343,7 +343,7 @@ async function remoteSsh(glusterEndpoint, cmd, ignoreError = false) {
|
||||
})
|
||||
break
|
||||
} catch (exception) {
|
||||
if (exception['code'] !== 'HOST_OFFLINE') {
|
||||
if (exception.code !== 'HOST_OFFLINE') {
|
||||
throw exception
|
||||
}
|
||||
}
|
||||
@@ -370,19 +370,17 @@ async function remoteSsh(glusterEndpoint, cmd, ignoreError = false) {
|
||||
}
|
||||
|
||||
function findErrorMessage(commandResut) {
|
||||
if (commandResut['exit'] === 0 && commandResut.parsed) {
|
||||
const cliOut = commandResut.parsed['cliOutput']
|
||||
if (cliOut['opErrstr'] && cliOut['opErrstr'].length) {
|
||||
return cliOut['opErrstr']
|
||||
if (commandResut.exit === 0 && commandResut.parsed) {
|
||||
const cliOut = commandResut.parsed.cliOutput
|
||||
if (cliOut.opErrstr && cliOut.opErrstr.length) {
|
||||
return cliOut.opErrstr
|
||||
}
|
||||
// "peer probe" returns it's "already in peer" error in cliOutput/output
|
||||
if (cliOut['output'] && cliOut['output'].length) {
|
||||
return cliOut['output']
|
||||
if (cliOut.output && cliOut.output.length) {
|
||||
return cliOut.output
|
||||
}
|
||||
}
|
||||
return commandResut['stderr'].length
|
||||
? commandResut['stderr']
|
||||
: commandResut['stdout']
|
||||
return commandResut.stderr.length ? commandResut.stderr : commandResut.stdout
|
||||
}
|
||||
|
||||
async function glusterCmd(glusterEndpoint, cmd, ignoreError = false) {
|
||||
@@ -392,15 +390,15 @@ async function glusterCmd(glusterEndpoint, cmd, ignoreError = false) {
|
||||
true
|
||||
)
|
||||
try {
|
||||
result.parsed = parseXml(result['stdout'])
|
||||
result.parsed = parseXml(result.stdout)
|
||||
} catch (e) {
|
||||
// pass, we never know if a message can be parsed or not, so we just try
|
||||
}
|
||||
if (result['exit'] === 0) {
|
||||
const cliOut = result.parsed['cliOutput']
|
||||
if (result.exit === 0) {
|
||||
const cliOut = result.parsed.cliOutput
|
||||
// we have found cases where opErrno is !=0 and opRet was 0, albeit the operation was an error.
|
||||
result.commandStatus =
|
||||
cliOut['opRet'].trim() === '0' && cliOut['opErrno'].trim() === '0'
|
||||
cliOut.opRet.trim() === '0' && cliOut.opErrno.trim() === '0'
|
||||
result.error = findErrorMessage(result)
|
||||
} else {
|
||||
result.commandStatus = false
|
||||
@@ -793,7 +791,7 @@ export const createSR = defer(async function(
|
||||
host: param.host.$id,
|
||||
vm: { id: param.vm.$id, ip: param.address },
|
||||
underlyingSr: param.underlyingSr.$id,
|
||||
arbiter: !!param['arbiter'],
|
||||
arbiter: !!param.arbiter,
|
||||
}))
|
||||
await xapi.xo.setData(xosanSrRef, 'xosan_config', {
|
||||
version: 'beta2',
|
||||
@@ -1164,11 +1162,11 @@ async function _prepareGlusterVm(
|
||||
}
|
||||
|
||||
async function _importGlusterVM(xapi, template, lvmsrId) {
|
||||
const templateStream = await this.requestResource(
|
||||
'xosan',
|
||||
template.id,
|
||||
template.version
|
||||
)
|
||||
const templateStream = await this.requestResource({
|
||||
id: template.id,
|
||||
namespace: 'xosan',
|
||||
version: template.version,
|
||||
})
|
||||
const newVM = await xapi.importVm(templateStream, {
|
||||
srId: lvmsrId,
|
||||
type: 'xva',
|
||||
@@ -1300,7 +1298,7 @@ export const addBricks = defer(async function(
|
||||
underlyingSr: newSr,
|
||||
})
|
||||
}
|
||||
const arbiterNode = data.nodes.find(n => n['arbiter'])
|
||||
const arbiterNode = data.nodes.find(n => n.arbiter)
|
||||
if (arbiterNode) {
|
||||
await glusterCmd(
|
||||
glusterEndpoint,
|
||||
@@ -1535,8 +1533,11 @@ export async function downloadAndInstallXosanPack({ id, version, pool }) {
|
||||
}
|
||||
|
||||
const xapi = this.getXapi(pool.id)
|
||||
const res = await this.requestResource('xosan', id, version)
|
||||
|
||||
const res = await this.requestResource({
|
||||
id,
|
||||
namespace: 'xosan',
|
||||
version,
|
||||
})
|
||||
await xapi.installSupplementalPackOnAllHosts(res)
|
||||
await xapi.pool.update_other_config(
|
||||
'xosan_pack_installation_time',
|
||||
|
||||
@@ -6,6 +6,7 @@ import ndjson from 'ndjson'
|
||||
import parseArgs from 'minimist'
|
||||
import sublevel from 'level-sublevel'
|
||||
import util from 'util'
|
||||
import { join as joinPath } from 'path'
|
||||
import { repair as repairDb } from 'level'
|
||||
|
||||
import { forEach } from './utils'
|
||||
@@ -174,6 +175,7 @@ export default async function main() {
|
||||
}
|
||||
|
||||
const config = await appConf.load('xo-server', {
|
||||
appDir: joinPath(__dirname, '..'),
|
||||
ignoreUnknownFormats: true,
|
||||
})
|
||||
|
||||
|
||||
@@ -1576,7 +1576,7 @@ export default class Xapi extends XapiBase {
|
||||
}
|
||||
} else {
|
||||
// Find the original template by name (*sigh*).
|
||||
const templateNameLabel = vm.other_config['base_template_name']
|
||||
const templateNameLabel = vm.other_config.base_template_name
|
||||
const template =
|
||||
templateNameLabel &&
|
||||
find(
|
||||
@@ -2041,6 +2041,7 @@ export default class Xapi extends XapiBase {
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@deferrable
|
||||
async createNetwork(
|
||||
$defer,
|
||||
@@ -2358,14 +2359,22 @@ export default class Xapi extends XapiBase {
|
||||
)
|
||||
}
|
||||
|
||||
async assertConsistentHostServerTime(hostRef) {
|
||||
const delta =
|
||||
async _getHostServerTimeShift(hostRef) {
|
||||
return Math.abs(
|
||||
parseDateTime(await this.call('host.get_servertime', hostRef)).getTime() -
|
||||
Date.now()
|
||||
if (Math.abs(delta) > 30e3) {
|
||||
Date.now()
|
||||
)
|
||||
}
|
||||
|
||||
async isHostServerTimeConsistent(hostRef) {
|
||||
return (await this._getHostServerTimeShift(hostRef)) < 30e3
|
||||
}
|
||||
|
||||
async assertConsistentHostServerTime(hostRef) {
|
||||
if (!(await this.isHostServerTimeConsistent(hostRef))) {
|
||||
throw new Error(
|
||||
`host server time and XOA date are not consistent with each other (${ms(
|
||||
delta
|
||||
await this._getHostServerTimeShift(hostRef)
|
||||
)})`
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import kindOf from 'kindof'
|
||||
import ms from 'ms'
|
||||
import schemaInspector from 'schema-inspector'
|
||||
import { forEach, isFunction } from 'lodash'
|
||||
import { getBoundPropertyDescriptor } from 'bind-property-descriptor'
|
||||
import { MethodNotFound } from 'json-rpc-peer'
|
||||
|
||||
import * as methods from '../api'
|
||||
@@ -219,17 +220,29 @@ export default class Api {
|
||||
throw new MethodNotFound(name)
|
||||
}
|
||||
|
||||
// FIXME: it can cause issues if there any property assignments in
|
||||
// XO methods called from the API.
|
||||
const context = Object.create(xo, {
|
||||
api: {
|
||||
// Used by system.*().
|
||||
value: this,
|
||||
},
|
||||
session: {
|
||||
value: session,
|
||||
},
|
||||
})
|
||||
// create the context which is an augmented XO
|
||||
const context = (() => {
|
||||
const descriptors = {
|
||||
api: {
|
||||
// Used by system.*().
|
||||
value: this,
|
||||
},
|
||||
session: {
|
||||
value: session,
|
||||
},
|
||||
}
|
||||
|
||||
let obj = xo
|
||||
do {
|
||||
Object.getOwnPropertyNames(obj).forEach(name => {
|
||||
if (!(name in descriptors)) {
|
||||
descriptors[name] = getBoundPropertyDescriptor(obj, name, xo)
|
||||
}
|
||||
})
|
||||
} while ((obj = Reflect.getPrototypeOf(obj)) !== null)
|
||||
|
||||
return Object.create(null, descriptors)
|
||||
})()
|
||||
|
||||
// Fetch and inject the current user.
|
||||
const userId = session.get('user_id', undefined)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import ms from 'ms'
|
||||
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' ||
|
||||
@@ -64,131 +66,138 @@ const taskTimeComparator = ({ start: s1, end: e1 }, { start: s2, end: e2 }) => {
|
||||
// tasks?: Task[],
|
||||
// }
|
||||
export default {
|
||||
async getBackupNgLogs(runId?: string) {
|
||||
const [jobLogs, restoreLogs, restoreMetadataLogs] = await Promise.all([
|
||||
this.getLogs('jobs'),
|
||||
this.getLogs('restore'),
|
||||
this.getLogs('metadataRestore'),
|
||||
])
|
||||
getBackupNgLogs: debounceWithKey(
|
||||
async function getBackupNgLogs(runId?: string) {
|
||||
const [jobLogs, restoreLogs, restoreMetadataLogs] = await Promise.all([
|
||||
this.getLogs('jobs'),
|
||||
this.getLogs('restore'),
|
||||
this.getLogs('metadataRestore'),
|
||||
])
|
||||
|
||||
const { runningJobs, runningRestores, runningMetadataRestores } = this
|
||||
const consolidated = {}
|
||||
const started = {}
|
||||
const { runningJobs, runningRestores, runningMetadataRestores } = this
|
||||
const consolidated = {}
|
||||
const started = {}
|
||||
|
||||
const handleLog = ({ data, time, message }, id) => {
|
||||
const { event } = data
|
||||
if (event === 'job.start') {
|
||||
if (
|
||||
(data.type === 'backup' || data.key === undefined) &&
|
||||
(runId === undefined || runId === id)
|
||||
) {
|
||||
const { scheduleId, jobId } = data
|
||||
consolidated[id] = started[id] = {
|
||||
const handleLog = ({ data, time, message }, id) => {
|
||||
const { event } = data
|
||||
if (event === 'job.start') {
|
||||
if (
|
||||
(data.type === 'backup' || data.key === undefined) &&
|
||||
(runId === undefined || runId === id)
|
||||
) {
|
||||
const { scheduleId, jobId } = data
|
||||
consolidated[id] = started[id] = {
|
||||
data: data.data,
|
||||
id,
|
||||
jobId,
|
||||
jobName: data.jobName,
|
||||
message: 'backup',
|
||||
scheduleId,
|
||||
start: time,
|
||||
status: runningJobs[jobId] === id ? 'pending' : 'interrupted',
|
||||
}
|
||||
}
|
||||
} else if (event === 'job.end') {
|
||||
const { runJobId } = data
|
||||
const log = started[runJobId]
|
||||
if (log !== undefined) {
|
||||
delete started[runJobId]
|
||||
log.end = time
|
||||
log.status = computeStatusAndSortTasks(
|
||||
getStatus((log.result = data.error)),
|
||||
log.tasks
|
||||
)
|
||||
}
|
||||
} else if (event === 'task.start') {
|
||||
const task = {
|
||||
data: data.data,
|
||||
id,
|
||||
jobId,
|
||||
jobName: data.jobName,
|
||||
message: 'backup',
|
||||
scheduleId,
|
||||
message,
|
||||
start: time,
|
||||
status: runningJobs[jobId] === id ? 'pending' : 'interrupted',
|
||||
}
|
||||
const { parentId } = data
|
||||
let parent
|
||||
if (parentId === undefined && (runId === undefined || runId === id)) {
|
||||
// top level task
|
||||
task.status =
|
||||
(message === 'restore' && !runningRestores.has(id)) ||
|
||||
(message === 'metadataRestore' &&
|
||||
!runningMetadataRestores.has(id))
|
||||
? 'interrupted'
|
||||
: 'pending'
|
||||
consolidated[id] = started[id] = task
|
||||
} else if ((parent = started[parentId]) !== undefined) {
|
||||
// sub-task for which the parent exists
|
||||
task.status = parent.status
|
||||
started[id] = task
|
||||
;(parent.tasks || (parent.tasks = [])).push(task)
|
||||
}
|
||||
} else if (event === 'task.end') {
|
||||
const { taskId } = data
|
||||
const log = started[taskId]
|
||||
if (log !== undefined) {
|
||||
// TODO: merge/transfer work-around
|
||||
delete started[taskId]
|
||||
log.end = time
|
||||
log.status = computeStatusAndSortTasks(
|
||||
getStatus((log.result = data.result), data.status),
|
||||
log.tasks
|
||||
)
|
||||
}
|
||||
} else if (event === 'task.warning') {
|
||||
const parent = started[data.taskId]
|
||||
parent !== undefined &&
|
||||
(parent.warnings || (parent.warnings = [])).push({
|
||||
data: data.data,
|
||||
message,
|
||||
})
|
||||
} else if (event === 'task.info') {
|
||||
const parent = started[data.taskId]
|
||||
parent !== undefined &&
|
||||
(parent.infos || (parent.infos = [])).push({
|
||||
data: data.data,
|
||||
message,
|
||||
})
|
||||
} else if (event === 'jobCall.start') {
|
||||
const parent = started[data.runJobId]
|
||||
if (parent !== undefined) {
|
||||
;(parent.tasks || (parent.tasks = [])).push(
|
||||
(started[id] = {
|
||||
data: {
|
||||
type: 'VM',
|
||||
id: data.params.id,
|
||||
},
|
||||
id,
|
||||
start: time,
|
||||
status: parent.status,
|
||||
})
|
||||
)
|
||||
}
|
||||
} else if (event === 'jobCall.end') {
|
||||
const { runCallId } = data
|
||||
const log = started[runCallId]
|
||||
if (log !== undefined) {
|
||||
delete started[runCallId]
|
||||
log.end = time
|
||||
log.status = computeStatusAndSortTasks(
|
||||
getStatus((log.result = data.error)),
|
||||
log.tasks
|
||||
)
|
||||
}
|
||||
}
|
||||
} else if (event === 'job.end') {
|
||||
const { runJobId } = data
|
||||
const log = started[runJobId]
|
||||
if (log !== undefined) {
|
||||
delete started[runJobId]
|
||||
log.end = time
|
||||
log.status = computeStatusAndSortTasks(
|
||||
getStatus((log.result = data.error)),
|
||||
log.tasks
|
||||
)
|
||||
}
|
||||
} else if (event === 'task.start') {
|
||||
const task = {
|
||||
data: data.data,
|
||||
id,
|
||||
message,
|
||||
start: time,
|
||||
}
|
||||
const { parentId } = data
|
||||
let parent
|
||||
if (parentId === undefined && (runId === undefined || runId === id)) {
|
||||
// top level task
|
||||
task.status =
|
||||
(message === 'restore' && !runningRestores.has(id)) ||
|
||||
(message === 'metadataRestore' && !runningMetadataRestores.has(id))
|
||||
? 'interrupted'
|
||||
: 'pending'
|
||||
consolidated[id] = started[id] = task
|
||||
} else if ((parent = started[parentId]) !== undefined) {
|
||||
// sub-task for which the parent exists
|
||||
task.status = parent.status
|
||||
started[id] = task
|
||||
;(parent.tasks || (parent.tasks = [])).push(task)
|
||||
}
|
||||
} else if (event === 'task.end') {
|
||||
const { taskId } = data
|
||||
const log = started[taskId]
|
||||
if (log !== undefined) {
|
||||
// TODO: merge/transfer work-around
|
||||
delete started[taskId]
|
||||
log.end = time
|
||||
log.status = computeStatusAndSortTasks(
|
||||
getStatus((log.result = data.result), data.status),
|
||||
log.tasks
|
||||
)
|
||||
}
|
||||
} else if (event === 'task.warning') {
|
||||
const parent = started[data.taskId]
|
||||
parent !== undefined &&
|
||||
(parent.warnings || (parent.warnings = [])).push({
|
||||
data: data.data,
|
||||
message,
|
||||
})
|
||||
} else if (event === 'task.info') {
|
||||
const parent = started[data.taskId]
|
||||
parent !== undefined &&
|
||||
(parent.infos || (parent.infos = [])).push({
|
||||
data: data.data,
|
||||
message,
|
||||
})
|
||||
} else if (event === 'jobCall.start') {
|
||||
const parent = started[data.runJobId]
|
||||
if (parent !== undefined) {
|
||||
;(parent.tasks || (parent.tasks = [])).push(
|
||||
(started[id] = {
|
||||
data: {
|
||||
type: 'VM',
|
||||
id: data.params.id,
|
||||
},
|
||||
id,
|
||||
start: time,
|
||||
status: parent.status,
|
||||
})
|
||||
)
|
||||
}
|
||||
} else if (event === 'jobCall.end') {
|
||||
const { runCallId } = data
|
||||
const log = started[runCallId]
|
||||
if (log !== undefined) {
|
||||
delete started[runCallId]
|
||||
log.end = time
|
||||
log.status = computeStatusAndSortTasks(
|
||||
getStatus((log.result = data.error)),
|
||||
log.tasks
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
forEach(jobLogs, handleLog)
|
||||
forEach(restoreLogs, handleLog)
|
||||
forEach(restoreMetadataLogs, handleLog)
|
||||
|
||||
return runId === undefined ? consolidated : consolidated[runId]
|
||||
},
|
||||
10e3,
|
||||
function keyFn(runId) {
|
||||
return [this, runId]
|
||||
}
|
||||
|
||||
forEach(jobLogs, handleLog)
|
||||
forEach(restoreLogs, handleLog)
|
||||
forEach(restoreMetadataLogs, handleLog)
|
||||
|
||||
return runId === undefined ? consolidated : consolidated[runId]
|
||||
},
|
||||
),
|
||||
|
||||
async getBackupNgLogsSorted({ after, before, filter, limit }) {
|
||||
let logs = await this.getBackupNgLogs()
|
||||
|
||||
@@ -43,6 +43,6 @@ test('VMDKDirectParser reads OK', async () => {
|
||||
}
|
||||
expect(harvested.length).toEqual(2)
|
||||
expect(harvested[0].offsetBytes).toEqual(0)
|
||||
expect(harvested[0].data.length).toEqual(header['grainSizeSectors'] * 512)
|
||||
expect(harvested[1].offsetBytes).toEqual(header['grainSizeSectors'] * 512)
|
||||
expect(harvested[0].data.length).toEqual(header.grainSizeSectors * 512)
|
||||
expect(harvested[1].offsetBytes).toEqual(header.grainSizeSectors * 512)
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "xo-web",
|
||||
"version": "5.50.0",
|
||||
"version": "5.50.3",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "Web interface client for Xen-Orchestra",
|
||||
"keywords": [
|
||||
|
||||
@@ -92,5 +92,3 @@ export const DEFAULT_NETWORK_CONFIG_TEMPLATE = `#network:
|
||||
# name: eth0
|
||||
# subnets:
|
||||
# - type: dhcp`
|
||||
|
||||
export const CAN_CLOUD_INIT = +process.env.XOA_PLAN > 3
|
||||
|
||||
@@ -2142,7 +2142,7 @@ export default {
|
||||
vmChooseCoresPerSocket: undefined,
|
||||
|
||||
// Original text: '{nSockets, number} socket{nSockets, plural, one {} other {s}} with {nCores, number} core{nCores, plural, one {} other {s}} per socket'
|
||||
vmCoresPerSocket: undefined,
|
||||
vmSocketsWithCoresPerSocket: undefined,
|
||||
|
||||
// Original text: 'Incorrect cores per socket value'
|
||||
vmCoresPerSocketIncorrectValue: undefined,
|
||||
|
||||
@@ -2185,7 +2185,7 @@ export default {
|
||||
vmChooseCoresPerSocket: 'Comportement par défaut',
|
||||
|
||||
// Original text: "{nSockets, number} socket{nSockets, plural, one {} other {s}} with {nCores, number} core{nCores, plural, one {} other {s}} per socket"
|
||||
vmCoresPerSocket:
|
||||
vmSocketsWithCoresPerSocket:
|
||||
'{nSockets, number} socket{nSockets, plural, one {} other {s}} avec {nCores, number} cœur{nCores, plural, one {} other {s}} par socket',
|
||||
|
||||
// Original text: "Incorrect cores per socket value"
|
||||
|
||||
@@ -2660,7 +2660,7 @@ export default {
|
||||
vmChooseCoresPerSocket: 'Varsayılan davranış',
|
||||
|
||||
// Original text: "{nSockets, number} socket{nSockets, plural, one {} other {s}} with {nCores, number} core{nCores, plural, one {} other {s}} per socket"
|
||||
vmCoresPerSocket:
|
||||
vmSocketsWithCoresPerSocket:
|
||||
'{nSockets, number} soket ve her sokette {nCores, number} çekirdek',
|
||||
|
||||
// Original text: "None"
|
||||
|
||||
@@ -50,6 +50,7 @@ const messages = {
|
||||
backupJobs: 'Backup jobs',
|
||||
iscsiSessions:
|
||||
'({ nSessions, number }) iSCSI session{nSessions, plural, one {} other {s}}',
|
||||
requiresAdminPermissions: 'Requires admin permissions',
|
||||
|
||||
// ----- Modals -----
|
||||
alertOk: 'OK',
|
||||
@@ -99,6 +100,7 @@ const messages = {
|
||||
updatePage: 'Updates',
|
||||
licensesPage: 'Licenses',
|
||||
notificationsPage: 'Notifications',
|
||||
supportPage: 'Support',
|
||||
settingsPage: 'Settings',
|
||||
settingsServersPage: 'Servers',
|
||||
settingsUsersPage: 'Users',
|
||||
@@ -153,6 +155,9 @@ const messages = {
|
||||
// ----- Support -----
|
||||
noSupport: 'No support',
|
||||
freeUpgrade: 'Free upgrade!',
|
||||
checkXoa: 'Check XOA',
|
||||
xoaCheck: 'XOA check',
|
||||
checkXoaCommunity: 'XOA check is available in XOA.',
|
||||
|
||||
// ----- Sign out -----
|
||||
signOut: 'Sign out',
|
||||
@@ -1154,12 +1159,16 @@ const messages = {
|
||||
vmCpuLimitsLabel: 'CPU limits',
|
||||
vmCpuTopology: 'Topology',
|
||||
vmChooseCoresPerSocket: 'Default behavior',
|
||||
vmCoresPerSocket:
|
||||
vmSocketsWithCoresPerSocket:
|
||||
'{nSockets, number} socket{nSockets, plural, one {} other {s}} with {nCores, number} core{nCores, plural, one {} other {s}} per socket',
|
||||
vmCoresPerSocketNone: 'None',
|
||||
vmCoresPerSocketIncorrectValue: 'Incorrect cores per socket value',
|
||||
vmCoresPerSocketIncorrectValueSolution:
|
||||
'Please change the selected value to fix it.',
|
||||
vmCoresPerSocket:
|
||||
'{nCores, number} core{nCores, plural, one {} other {s}} per socket',
|
||||
vmCoresPerSocketNotDivisor: "Not a divisor of the VM's max CPUs",
|
||||
vmCoresPerSocketExceedsCoresLimit:
|
||||
'The selected value exceeds the cores limit ({maxCores, number})',
|
||||
vmCoresPerSocketExceedsSocketsLimit:
|
||||
'The selected value exceeds the sockets limit ({maxSockets, number})',
|
||||
vmHaDisabled: 'Disabled',
|
||||
vmMemoryLimitsLabel: 'Memory limits (min/max)',
|
||||
vmVgpu: 'vGPU',
|
||||
@@ -1318,7 +1327,6 @@ const messages = {
|
||||
newVmSshKey: 'SSH key',
|
||||
noConfigDrive: 'No config drive',
|
||||
newVmCustomConfig: 'Custom config',
|
||||
premiumOnly: 'Only available in Premium',
|
||||
availableTemplateVarsInfo:
|
||||
'Click here to see the available template variables',
|
||||
availableTemplateVarsTitle: 'Available template variables',
|
||||
@@ -1932,6 +1940,7 @@ const messages = {
|
||||
logUser: 'User',
|
||||
logMessage: 'Message',
|
||||
logSuggestXcpNg: 'Use XCP-ng to get rid of restrictions',
|
||||
logXapiError: 'This is a XenServer/XCP-ng error',
|
||||
logError: 'Error',
|
||||
logTitle: 'Logs',
|
||||
logDisplayDetails: 'Display details',
|
||||
@@ -2143,6 +2152,21 @@ const messages = {
|
||||
xosanIssueHostNotInNetwork:
|
||||
'Will configure the host xosan network device with a static IP address and plug it in.',
|
||||
|
||||
// Hub
|
||||
hubPage: 'Hub',
|
||||
noDefaultSr: 'The selected pool has no default SR',
|
||||
successfulInstall: 'VM installed successfully',
|
||||
vmNoAvailable: 'No VMs available ',
|
||||
create: 'Create',
|
||||
hubResourceAlert: 'Resource alert',
|
||||
os: 'OS',
|
||||
version: 'Version',
|
||||
size: 'Size',
|
||||
totalDiskSize: 'Total disk size',
|
||||
hideInstalledPool: 'Already installed templates are hidden',
|
||||
hubSrErrorTitle: 'Missing property',
|
||||
hubImportNotificationTitle: 'XVA import',
|
||||
|
||||
// Licenses
|
||||
xosanUnregisteredDisclaimer:
|
||||
'You are not registered and therefore will not be able to create or manage your XOSAN SRs. {link}',
|
||||
|
||||
@@ -468,14 +468,14 @@ export default class Scheduler extends Component {
|
||||
optionRenderer={getMonthName}
|
||||
options={MONTHS}
|
||||
onChange={this._monthChange}
|
||||
value={cronPatternArr[PICKTIME_TO_ID['month']]}
|
||||
value={cronPatternArr[PICKTIME_TO_ID.month]}
|
||||
/>
|
||||
</Col>
|
||||
<Col largeSize={6}>
|
||||
<DayPicker
|
||||
onChange={this._dayChange}
|
||||
monthDayPattern={cronPatternArr[PICKTIME_TO_ID['monthDay']]}
|
||||
weekDayPattern={cronPatternArr[PICKTIME_TO_ID['weekDay']]}
|
||||
monthDayPattern={cronPatternArr[PICKTIME_TO_ID.monthDay]}
|
||||
weekDayPattern={cronPatternArr[PICKTIME_TO_ID.weekDay]}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
@@ -485,7 +485,7 @@ export default class Scheduler extends Component {
|
||||
labelId='Hour'
|
||||
options={HOURS}
|
||||
onChange={this._hourChange}
|
||||
value={cronPatternArr[PICKTIME_TO_ID['hour']]}
|
||||
value={cronPatternArr[PICKTIME_TO_ID.hour]}
|
||||
/>
|
||||
</Col>
|
||||
<Col largeSize={6}>
|
||||
@@ -493,7 +493,7 @@ export default class Scheduler extends Component {
|
||||
labelId='Minute'
|
||||
options={MINS}
|
||||
onChange={this._minuteChange}
|
||||
value={cronPatternArr[PICKTIME_TO_ID['minute']]}
|
||||
value={cronPatternArr[PICKTIME_TO_ID.minute]}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
119
packages/xo-web/src/common/select-cores-per-socket.js
Normal file
119
packages/xo-web/src/common/select-cores-per-socket.js
Normal file
@@ -0,0 +1,119 @@
|
||||
import _ from 'intl'
|
||||
import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import { injectState, provideState } from 'reaclette'
|
||||
import { omit } from 'lodash'
|
||||
|
||||
import decorate from './apply-decorators'
|
||||
import Icon from './icon'
|
||||
import Tooltip from './tooltip'
|
||||
import { Select } from './form'
|
||||
|
||||
const PROP_TYPES = {
|
||||
maxCores: PropTypes.number,
|
||||
maxVcpus: PropTypes.number,
|
||||
value: PropTypes.number,
|
||||
}
|
||||
|
||||
const SELECT_STYLE = {
|
||||
display: 'inline-block',
|
||||
fontSize: '1rem',
|
||||
width: '20em',
|
||||
}
|
||||
|
||||
const LINE_ITEM_STYLE = {
|
||||
alignItems: 'center',
|
||||
display: 'flex',
|
||||
}
|
||||
|
||||
// https://github.com/xcp-ng/xenadmin/blob/0160cd0119fae3b871eef656c23e2b76fcc04cb5/XenModel/XenAPI-Extensions/VM.cs#L62
|
||||
const MAX_VM_SOCKETS = 16
|
||||
|
||||
// This algorithm was inspired from: https://github.com/xcp-ng/xenadmin/blob/master/XenAdmin/Controls/ComboBoxes/CPUTopologyComboBox.cs#L116
|
||||
const SelectCoresPerSocket = decorate([
|
||||
provideState({
|
||||
computed: {
|
||||
isValidValue: (state, { maxVcpus, value }) =>
|
||||
value == null ||
|
||||
(maxVcpus % value === 0 &&
|
||||
!state.valueExceedsCoresLimit &&
|
||||
!state.valueExceedsSocketsLimit),
|
||||
valueExceedsCoresLimit: (state, { maxCores, value }) => value > maxCores,
|
||||
valueExceedsSocketsLimit: (state, { maxCores, maxVcpus, value }) =>
|
||||
maxVcpus / value > MAX_VM_SOCKETS,
|
||||
options: ({ isValidValue }, { maxCores, maxVcpus, value }) => {
|
||||
const options = []
|
||||
|
||||
if (maxCores === undefined || maxVcpus === undefined) {
|
||||
return options
|
||||
}
|
||||
|
||||
const minCores = maxVcpus / MAX_VM_SOCKETS
|
||||
|
||||
// cores per socket must be a divisor of the max vCPUs and must not exceed the cores and sockets limit
|
||||
// e.g: with maxCores = 4, maxSockets = 16 and maxVCPUS = 6
|
||||
// 2 cores per socket is a valid value and 4 cores per socket isn't a valid value
|
||||
for (
|
||||
let coresPerSocket = maxCores;
|
||||
coresPerSocket >= minCores;
|
||||
coresPerSocket--
|
||||
) {
|
||||
if (maxVcpus % coresPerSocket === 0) {
|
||||
options.push({
|
||||
label: _('vmSocketsWithCoresPerSocket', {
|
||||
nSockets: maxVcpus / coresPerSocket,
|
||||
nCores: coresPerSocket,
|
||||
}),
|
||||
value: coresPerSocket,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (!isValidValue) {
|
||||
options.push({
|
||||
label: _('vmCoresPerSocket', {
|
||||
nCores: value,
|
||||
}),
|
||||
value,
|
||||
})
|
||||
}
|
||||
return options
|
||||
},
|
||||
selectProps: (_, props) => omit(props, Object.keys(PROP_TYPES)),
|
||||
},
|
||||
}),
|
||||
injectState,
|
||||
({ maxCores, state, value }) => (
|
||||
<div style={LINE_ITEM_STYLE}>
|
||||
<span style={SELECT_STYLE}>
|
||||
<Select
|
||||
options={state.options}
|
||||
placeholder={_('vmChooseCoresPerSocket')}
|
||||
simpleValue
|
||||
value={value}
|
||||
{...state.selectProps}
|
||||
/>
|
||||
</span>
|
||||
|
||||
{!state.isValidValue && (
|
||||
<Tooltip
|
||||
content={
|
||||
state.valueExceedsCoresLimit
|
||||
? _('vmCoresPerSocketExceedsCoresLimit', { maxCores })
|
||||
: state.valueExceedsSocketsLimit
|
||||
? _('vmCoresPerSocketExceedsSocketsLimit', {
|
||||
maxSockets: MAX_VM_SOCKETS,
|
||||
})
|
||||
: _('vmCoresPerSocketNotDivisor')
|
||||
}
|
||||
>
|
||||
<Icon icon='error' size='lg' />
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
])
|
||||
|
||||
SelectCoresPerSocket.propTypes = PROP_TYPES
|
||||
|
||||
export { SelectCoresPerSocket as default }
|
||||
@@ -916,7 +916,7 @@ export class SelectResourceSetsVdi extends React.PureComponent {
|
||||
() => this.props.resourceSet,
|
||||
({ objectsByType }) => {
|
||||
const { srPredicate } = this.props
|
||||
const srs = objectsByType['SR']
|
||||
const srs = objectsByType.SR
|
||||
return srPredicate ? filter(srs, srPredicate) : srs
|
||||
}
|
||||
)
|
||||
|
||||
@@ -58,3 +58,11 @@ export const setHomeVmIdsSelection = createAction(
|
||||
'SET_HOME_VM_IDS_SELECTION',
|
||||
homeVmIdsSelection => homeVmIdsSelection
|
||||
)
|
||||
export const markHubResourceAsInstalling = createAction(
|
||||
'MARK_HUB_RESOURCE_AS_INSTALLING',
|
||||
id => id
|
||||
)
|
||||
export const markHubResourceAsInstalled = createAction(
|
||||
'MARK_HUB_RESOURCE_AS_INSTALLED',
|
||||
id => id
|
||||
)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import cookies from 'cookies-js'
|
||||
import { omit } from 'lodash'
|
||||
|
||||
import invoke from '../invoke'
|
||||
|
||||
@@ -92,6 +93,19 @@ export default {
|
||||
homeVmIdsSelection,
|
||||
}),
|
||||
|
||||
// whether a resource is currently being installed: `hubInstallingResources[<template id>]`
|
||||
hubInstallingResources: combineActionHandlers(
|
||||
{},
|
||||
{
|
||||
[actions.markHubResourceAsInstalling]: (
|
||||
prevHubInstallingResources,
|
||||
id
|
||||
) => ({ ...prevHubInstallingResources, [id]: true }),
|
||||
[actions.markHubResourceAsInstalled]: (prevHubInstallingResources, id) =>
|
||||
omit(prevHubInstallingResources, id),
|
||||
}
|
||||
),
|
||||
|
||||
objects: combineActionHandlers(
|
||||
{
|
||||
all: {}, // Mutable for performance!
|
||||
|
||||
@@ -252,7 +252,7 @@ export const parseSize = size => {
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
const _NotFound = () => <h1>{_('errorPageNotFound')}</h1>
|
||||
const NotFound = () => <h1>{_('errorPageNotFound')}</h1>
|
||||
|
||||
// Decorator to declare routes on a component.
|
||||
//
|
||||
@@ -286,7 +286,7 @@ export const routes = (indexRoute, childRoutes) => target => {
|
||||
}
|
||||
|
||||
if (childRoutes) {
|
||||
childRoutes.push({ component: _NotFound, path: '*' })
|
||||
childRoutes.push({ component: NotFound, path: '*' })
|
||||
}
|
||||
|
||||
target.route = {
|
||||
@@ -449,26 +449,6 @@ export const isXosanPack = ({ name }) => name.startsWith('XOSAN')
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export const getCoresPerSocketPossibilities = (maxCoresPerSocket, vCPUs) => {
|
||||
// According to : https://www.citrix.com/blogs/2014/03/11/citrix-xenserver-setting-more-than-one-vcpu-per-vm-to-improve-application-performance-and-server-consolidation-e-g-for-cad3-d-graphical-applications/
|
||||
const maxVCPUs = 16
|
||||
|
||||
const options = []
|
||||
if (maxCoresPerSocket !== undefined && vCPUs !== '') {
|
||||
const ratio = vCPUs / maxVCPUs
|
||||
|
||||
for (
|
||||
let coresPerSocket = maxCoresPerSocket;
|
||||
coresPerSocket >= ratio;
|
||||
coresPerSocket--
|
||||
) {
|
||||
if (vCPUs % coresPerSocket === 0) options.push(coresPerSocket)
|
||||
}
|
||||
}
|
||||
|
||||
return options
|
||||
}
|
||||
|
||||
// Generates a random human-readable string of length `length`
|
||||
// Useful to generate random default names intended for the UI user
|
||||
export const generateReadableRandomString = (() => {
|
||||
@@ -629,7 +609,7 @@ export const adminOnly = Component =>
|
||||
connectStore({
|
||||
_isAdmin: isAdmin,
|
||||
})(({ _isAdmin, ...props }) =>
|
||||
_isAdmin ? <Component {...props} /> : <_NotFound />
|
||||
_isAdmin ? <Component {...props} /> : <NotFound />
|
||||
)
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@@ -349,6 +349,10 @@ export const subscribeResourceCatalog = createSubscription(() =>
|
||||
_call('cloud.getResourceCatalog')
|
||||
)
|
||||
|
||||
export const subscribeHubResourceCatalog = createSubscription(() =>
|
||||
_call('cloud.getResourceCatalog', { filters: { hub: true } })
|
||||
)
|
||||
|
||||
const getNotificationCookie = () => {
|
||||
const notificationCookie = cookies.get(
|
||||
`notifications:${store.getState().user.id}`
|
||||
@@ -2871,7 +2875,10 @@ export const fixHostNotInXosanNetwork = (xosanSr, host) =>
|
||||
|
||||
// XOSAN packs -----------------------------------------------------------------
|
||||
|
||||
export const getResourceCatalog = () => _call('cloud.getResourceCatalog')
|
||||
export const getResourceCatalog = ({ filters } = {}) =>
|
||||
_call('cloud.getResourceCatalog', { filters })
|
||||
|
||||
export const getAllResourceCatalog = () => _call('cloud.getAllResourceCatalog')
|
||||
|
||||
const downloadAndInstallXosanPack = (pack, pool, { version }) =>
|
||||
_call('xosan.downloadAndInstallXosanPack', {
|
||||
@@ -2880,6 +2887,14 @@ const downloadAndInstallXosanPack = (pack, pool, { version }) =>
|
||||
pool: resolveId(pool),
|
||||
})
|
||||
|
||||
export const downloadAndInstallResource = ({ namespace, id, version, sr }) =>
|
||||
_call('cloud.downloadAndInstallResource', {
|
||||
namespace,
|
||||
id,
|
||||
version,
|
||||
sr: resolveId(sr),
|
||||
})
|
||||
|
||||
import UpdateXosanPacksModal from './update-xosan-packs-modal' // eslint-disable-line import/first
|
||||
export const updateXosanPacks = pool =>
|
||||
confirm({
|
||||
@@ -2903,3 +2918,7 @@ export const getLicense = (productId, boundObjectId) =>
|
||||
|
||||
export const unlockXosan = (licenseId, srId) =>
|
||||
_call('xosan.unlock', { licenseId, sr: srId })
|
||||
|
||||
// Support --------------------------------------------------------------------
|
||||
|
||||
export const checkXoa = () => _call('xoa.check')
|
||||
|
||||
@@ -277,6 +277,10 @@
|
||||
@extend .fa;
|
||||
@extend .fa-thumbs-up;
|
||||
}
|
||||
&-deploy {
|
||||
@extend .fa;
|
||||
@extend .fa-rocket;
|
||||
}
|
||||
|
||||
// Backups
|
||||
&-backup {
|
||||
@@ -826,6 +830,10 @@
|
||||
@extend .fa;
|
||||
@extend .fa-bell;
|
||||
}
|
||||
&-menu-support {
|
||||
@extend .fa;
|
||||
@extend .fa-support;
|
||||
}
|
||||
&-menu-settings {
|
||||
@extend .fa;
|
||||
@extend .fa-cog;
|
||||
@@ -886,6 +894,10 @@
|
||||
@extend .fa;
|
||||
@extend .fa-database;
|
||||
}
|
||||
&-menu-hub {
|
||||
@extend .fa;
|
||||
@extend .fa-cubes;
|
||||
}
|
||||
// New VM
|
||||
&-new-vm {
|
||||
&-infos {
|
||||
|
||||
63
packages/xo-web/src/xo-app/hub/index.js
Normal file
63
packages/xo-web/src/xo-app/hub/index.js
Normal file
@@ -0,0 +1,63 @@
|
||||
import _ from 'intl'
|
||||
import decorate from 'apply-decorators'
|
||||
import Icon from 'icon'
|
||||
import React from 'react'
|
||||
import { addSubscriptions, adminOnly } from 'utils'
|
||||
import { Container, Col, Row } from 'grid'
|
||||
import { injectState, provideState } from 'reaclette'
|
||||
import { isEmpty, map, omit, orderBy } from 'lodash'
|
||||
import { subscribeHubResourceCatalog } from 'xo'
|
||||
|
||||
import Page from '../page'
|
||||
import Resource from './resource'
|
||||
|
||||
// ==================================================================
|
||||
|
||||
const HEADER = (
|
||||
<h2>
|
||||
<Icon icon='menu-hub' /> {_('hubPage')}
|
||||
</h2>
|
||||
)
|
||||
|
||||
export default decorate([
|
||||
adminOnly,
|
||||
addSubscriptions({
|
||||
catalog: subscribeHubResourceCatalog,
|
||||
}),
|
||||
provideState({
|
||||
computed: {
|
||||
resources: (_, { catalog }) =>
|
||||
orderBy(
|
||||
map(omit(catalog, '_namespaces'), (entry, namespace) => ({
|
||||
namespace,
|
||||
...entry.xva,
|
||||
})),
|
||||
'name',
|
||||
'asc'
|
||||
),
|
||||
},
|
||||
}),
|
||||
injectState,
|
||||
({ state: { resources } }) => (
|
||||
<Page header={HEADER} title='hubPage' formatTitle>
|
||||
<Container>
|
||||
<Row>
|
||||
{isEmpty(resources) ? (
|
||||
<Col>
|
||||
<h2 className='text-muted'>
|
||||
{_('vmNoAvailable')}
|
||||
<Icon icon='alarm' color='yellow' />
|
||||
</h2>
|
||||
</Col>
|
||||
) : (
|
||||
resources.map(data => (
|
||||
<Col key={data.namespace} mediumSize={6} largeSize={4}>
|
||||
<Resource {...data} />
|
||||
</Col>
|
||||
))
|
||||
)}
|
||||
</Row>
|
||||
</Container>
|
||||
</Page>
|
||||
),
|
||||
])
|
||||
60
packages/xo-web/src/xo-app/hub/resource-form.js
Normal file
60
packages/xo-web/src/xo-app/hub/resource-form.js
Normal file
@@ -0,0 +1,60 @@
|
||||
import * as FormGrid from 'form-grid'
|
||||
import _ from 'intl'
|
||||
import decorate from 'apply-decorators'
|
||||
import Icon from 'icon'
|
||||
import React from 'react'
|
||||
import Tooltip from 'tooltip'
|
||||
import { Container } from 'grid'
|
||||
import { SelectPool } from 'select-objects'
|
||||
import { error } from 'notification'
|
||||
import { injectState, provideState } from 'reaclette'
|
||||
|
||||
export default decorate([
|
||||
provideState({
|
||||
initialState: ({ multi }) => ({
|
||||
pools: multi ? [] : undefined,
|
||||
}),
|
||||
effects: {
|
||||
onChangePool(__, pools) {
|
||||
const noDefaultSr = Array.isArray(pools)
|
||||
? pools.some(pool => pool.default_SR === undefined)
|
||||
: pools.default_SR === undefined
|
||||
if (noDefaultSr) {
|
||||
error(_('hubSrErrorTitle'), _('noDefaultSr'))
|
||||
} else {
|
||||
this.props.onChange({
|
||||
pools,
|
||||
pool: pools,
|
||||
})
|
||||
return {
|
||||
pools,
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}),
|
||||
injectState,
|
||||
({ effects, install, multi, state, poolPredicate }) => (
|
||||
<Container>
|
||||
<FormGrid.Row>
|
||||
<label>
|
||||
{_('vmImportToPool')}
|
||||
|
||||
{install && (
|
||||
<Tooltip content={_('hideInstalledPool')}>
|
||||
<Icon icon='info' />
|
||||
</Tooltip>
|
||||
)}
|
||||
</label>
|
||||
<SelectPool
|
||||
className='mb-1'
|
||||
multi={multi}
|
||||
onChange={effects.onChangePool}
|
||||
predicate={poolPredicate}
|
||||
required
|
||||
value={state.pools}
|
||||
/>
|
||||
</FormGrid.Row>
|
||||
</Container>
|
||||
),
|
||||
])
|
||||
255
packages/xo-web/src/xo-app/hub/resource.js
Normal file
255
packages/xo-web/src/xo-app/hub/resource.js
Normal file
@@ -0,0 +1,255 @@
|
||||
import _ from 'intl'
|
||||
import ActionButton from 'action-button'
|
||||
import decorate from 'apply-decorators'
|
||||
import Icon from 'icon'
|
||||
import React from 'react'
|
||||
import { Card, CardBlock, CardHeader } from 'card'
|
||||
import { Col, Row } from 'grid'
|
||||
import { alert, form } from 'modal'
|
||||
import { connectStore, formatSize, getXoaPlan } from 'utils'
|
||||
import { createGetObjectsOfType } from 'selectors'
|
||||
import { downloadAndInstallResource, deleteTemplates } from 'xo'
|
||||
import { error, success } from 'notification'
|
||||
import { find, filter } from 'lodash'
|
||||
import { injectState, provideState } from 'reaclette'
|
||||
import { withRouter } from 'react-router'
|
||||
|
||||
import ResourceForm from './resource-form'
|
||||
|
||||
const subscribeAlert = () =>
|
||||
alert(
|
||||
_('hubResourceAlert'),
|
||||
<div>
|
||||
<p>
|
||||
{_('considerSubscribe', {
|
||||
link: 'https://xen-orchestra.com',
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
|
||||
export default decorate([
|
||||
withRouter,
|
||||
connectStore(() => {
|
||||
const getTemplates = createGetObjectsOfType('VM-template').sort()
|
||||
const getPools = createGetObjectsOfType('pool')
|
||||
return {
|
||||
templates: getTemplates,
|
||||
pools: getPools,
|
||||
hubInstallingResources: state => state.hubInstallingResources,
|
||||
}
|
||||
}),
|
||||
provideState({
|
||||
initialState: () => ({
|
||||
selectedInstallPools: [],
|
||||
}),
|
||||
effects: {
|
||||
async install() {
|
||||
const {
|
||||
id,
|
||||
name,
|
||||
namespace,
|
||||
markHubResourceAsInstalled,
|
||||
markHubResourceAsInstalling,
|
||||
version,
|
||||
} = this.props
|
||||
const { isTemplateInstalled } = this.state
|
||||
if (getXoaPlan() === 'Community') {
|
||||
subscribeAlert()
|
||||
return
|
||||
}
|
||||
const resourceParams = await form({
|
||||
render: props => (
|
||||
<ResourceForm
|
||||
install
|
||||
multi
|
||||
poolPredicate={isTemplateInstalled}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
header: (
|
||||
<span>
|
||||
<Icon icon='add-vm' /> {name}
|
||||
</span>
|
||||
),
|
||||
size: 'medium',
|
||||
})
|
||||
|
||||
markHubResourceAsInstalling(id)
|
||||
try {
|
||||
await Promise.all(
|
||||
resourceParams.pools.map(pool =>
|
||||
downloadAndInstallResource({
|
||||
namespace,
|
||||
id,
|
||||
version,
|
||||
sr: pool.default_SR,
|
||||
})
|
||||
)
|
||||
)
|
||||
success(_('hubImportNotificationTitle'), _('successfulInstall'))
|
||||
} catch (_error) {
|
||||
error(_('hubImportNotificationTitle'), _error.message)
|
||||
}
|
||||
markHubResourceAsInstalled(id)
|
||||
},
|
||||
async create() {
|
||||
const { isPoolCreated, installedTemplates } = this.state
|
||||
const { name } = this.props
|
||||
if (getXoaPlan() === 'Community') {
|
||||
subscribeAlert()
|
||||
return
|
||||
}
|
||||
const resourceParams = await form({
|
||||
render: props => (
|
||||
<ResourceForm poolPredicate={isPoolCreated} {...props} />
|
||||
),
|
||||
header: (
|
||||
<span>
|
||||
<Icon icon='add-vm' /> {name}
|
||||
</span>
|
||||
),
|
||||
size: 'medium',
|
||||
})
|
||||
const { $pool } = resourceParams.pool
|
||||
const template = find(installedTemplates, { $pool })
|
||||
if (template !== undefined) {
|
||||
this.props.router.push(
|
||||
`/vms/new?pool=${$pool}&template=${template.id}`
|
||||
)
|
||||
} else {
|
||||
throw new Error(`can't find template for pool: ${$pool}`)
|
||||
}
|
||||
},
|
||||
async deleteTemplates(__, { name }) {
|
||||
const { isPoolCreated } = this.state
|
||||
const resourceParams = await form({
|
||||
render: props => (
|
||||
<ResourceForm
|
||||
delete
|
||||
multi
|
||||
poolPredicate={isPoolCreated}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
header: (
|
||||
<span>
|
||||
<Icon icon='vm-delete' /> {name}
|
||||
</span>
|
||||
),
|
||||
size: 'medium',
|
||||
})
|
||||
const _templates = filter(this.state.installedTemplates, template =>
|
||||
find(resourceParams.pools, { $pool: template.$pool })
|
||||
)
|
||||
await deleteTemplates(_templates)
|
||||
},
|
||||
updateSelectedInstallPools(_, selectedInstallPools) {
|
||||
return {
|
||||
selectedInstallPools,
|
||||
}
|
||||
},
|
||||
updateSelectedCreatePool(_, selectedCreatePool) {
|
||||
return {
|
||||
selectedCreatePool,
|
||||
}
|
||||
},
|
||||
redirectToTaskPage() {
|
||||
this.props.router.push('/tasks')
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
installedTemplates: (_, { namespace, templates }) =>
|
||||
filter(templates, ['other.xo:resource:namespace', namespace]),
|
||||
isTemplateInstalledOnAllPools: ({ installedTemplates }, { pools }) =>
|
||||
installedTemplates.length > 0 &&
|
||||
pools.every(
|
||||
pool =>
|
||||
installedTemplates.find(template => template.$pool === pool.id) !==
|
||||
undefined
|
||||
),
|
||||
isTemplateInstalled: ({ installedTemplates }) => pool =>
|
||||
installedTemplates.find(template => template.$pool === pool.id) ===
|
||||
undefined,
|
||||
isPoolCreated: ({ installedTemplates }) => pool =>
|
||||
installedTemplates.find(template => template.$pool === pool.id) !==
|
||||
undefined,
|
||||
},
|
||||
}),
|
||||
injectState,
|
||||
({
|
||||
effects,
|
||||
hubInstallingResources,
|
||||
id,
|
||||
name,
|
||||
os,
|
||||
size,
|
||||
state,
|
||||
totalDiskSize,
|
||||
version,
|
||||
}) => (
|
||||
<Card shadow>
|
||||
<CardHeader>
|
||||
{name}
|
||||
<ActionButton
|
||||
className='pull-right'
|
||||
color='light'
|
||||
data-name={name}
|
||||
disabled={state.installedTemplates.length === 0}
|
||||
handler={effects.deleteTemplates}
|
||||
icon='delete'
|
||||
size='small'
|
||||
style={{ border: 'none' }}
|
||||
tooltip={_('remove')}
|
||||
/>
|
||||
<br />
|
||||
</CardHeader>
|
||||
<CardBlock className='text-center'>
|
||||
<div>
|
||||
<span className='text-muted'>{_('os')}</span> <strong>{os}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span className='text-muted'>{_('version')}</span>
|
||||
{' '}
|
||||
<strong>{version}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span className='text-muted'>{_('size')}</span>
|
||||
{' '}
|
||||
<strong>{formatSize(size)}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span className='text-muted'>{_('totalDiskSize')}</span>
|
||||
{' '}
|
||||
<strong>{formatSize(totalDiskSize)}</strong>
|
||||
</div>
|
||||
<hr />
|
||||
<Row>
|
||||
<Col mediumSize={6}>
|
||||
<ActionButton
|
||||
block
|
||||
disabled={state.isTemplateInstalledOnAllPools}
|
||||
form={state.idInstallForm}
|
||||
handler={effects.install}
|
||||
icon='add'
|
||||
pending={hubInstallingResources[id]}
|
||||
>
|
||||
{_('install')}
|
||||
</ActionButton>
|
||||
</Col>
|
||||
<Col mediumSize={6}>
|
||||
<ActionButton
|
||||
block
|
||||
disabled={state.installedTemplates.length === 0}
|
||||
form={state.idCreateForm}
|
||||
handler={effects.create}
|
||||
icon='deploy'
|
||||
>
|
||||
{_('create')}
|
||||
</ActionButton>
|
||||
</Col>
|
||||
</Row>
|
||||
</CardBlock>
|
||||
</Card>
|
||||
),
|
||||
])
|
||||
@@ -27,6 +27,7 @@ import BackupNg from './backup-ng'
|
||||
import Dashboard from './dashboard'
|
||||
import Home from './home'
|
||||
import Host from './host'
|
||||
import Hub from './hub'
|
||||
import Jobs from './jobs'
|
||||
import Menu from './menu'
|
||||
import Modal, { alert, FormModal } from 'modal'
|
||||
@@ -93,6 +94,7 @@ const BODY_STYLE = {
|
||||
'vms/:id': Vm,
|
||||
xoa: Xoa,
|
||||
xosan: Xosan,
|
||||
hub: Hub,
|
||||
})
|
||||
@connectStore(state => {
|
||||
return {
|
||||
|
||||
@@ -281,6 +281,11 @@ export default class Menu extends Component {
|
||||
label: 'notificationsPage',
|
||||
extra: <NotificationTag />,
|
||||
},
|
||||
isAdmin && {
|
||||
to: 'xoa/support',
|
||||
icon: 'menu-support',
|
||||
label: 'supportPage',
|
||||
},
|
||||
],
|
||||
},
|
||||
isAdmin && {
|
||||
@@ -354,6 +359,11 @@ export default class Menu extends Component {
|
||||
},
|
||||
],
|
||||
},
|
||||
isAdmin && {
|
||||
to: '/hub',
|
||||
icon: 'menu-hub',
|
||||
label: 'hubPage',
|
||||
},
|
||||
isAdmin && { to: '/about', icon: 'menu-about', label: 'aboutPage' },
|
||||
!noOperatablePools && {
|
||||
to: '/tasks',
|
||||
|
||||
@@ -11,6 +11,7 @@ import Page from '../page'
|
||||
import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import SelectBootFirmware from 'select-boot-firmware'
|
||||
import SelectCoresPerSocket from 'select-cores-per-socket'
|
||||
import store from 'store'
|
||||
import Tags from 'tags'
|
||||
import Tooltip from 'tooltip'
|
||||
@@ -21,7 +22,6 @@ import { Container, Row, Col } from 'grid'
|
||||
import { injectIntl } from 'react-intl'
|
||||
import {
|
||||
AvailableTemplateVars,
|
||||
CAN_CLOUD_INIT,
|
||||
DEFAULT_CLOUD_CONFIG_TEMPLATE,
|
||||
DEFAULT_NETWORK_CONFIG_TEMPLATE,
|
||||
NetworkConfigInfo,
|
||||
@@ -81,7 +81,6 @@ import {
|
||||
addSubscriptions,
|
||||
connectStore,
|
||||
formatSize,
|
||||
getCoresPerSocketPossibilities,
|
||||
generateReadableRandomString,
|
||||
resolveIds,
|
||||
} from 'utils'
|
||||
@@ -260,7 +259,7 @@ class Vif extends BaseComponent {
|
||||
props.pool === undefined // to get objects as a self user
|
||||
),
|
||||
srs: getSrs(state, props),
|
||||
template: getTemplate(state, props),
|
||||
template: getTemplate(state, props, props.pool === undefined),
|
||||
templates: getTemplates(state, props),
|
||||
userSshKeys: getUserSshKeys(state, props),
|
||||
})
|
||||
@@ -281,7 +280,12 @@ export default class NewVm extends BaseComponent {
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this._reset()
|
||||
this._reset(() => {
|
||||
const { template } = this.props
|
||||
if (template !== undefined) {
|
||||
this._initTemplate(this.props.template)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
@@ -339,28 +343,32 @@ export default class NewVm extends BaseComponent {
|
||||
|
||||
// Actions ---------------------------------------------------------------------
|
||||
|
||||
_reset = () => {
|
||||
this._replaceState({
|
||||
bootAfterCreate: true,
|
||||
CPUs: '',
|
||||
cpuCap: '',
|
||||
cpuWeight: '',
|
||||
existingDisks: {},
|
||||
fastClone: true,
|
||||
hvmBootFirmware: '',
|
||||
installMethod: 'noConfigDrive',
|
||||
multipleVms: false,
|
||||
name_label: '',
|
||||
name_description: '',
|
||||
nameLabels: map(Array(NB_VMS_MIN), (_, index) => `VM_${index + 1}`),
|
||||
namePattern: '{name}%',
|
||||
nbVms: NB_VMS_MIN,
|
||||
VDIs: [],
|
||||
VIFs: [],
|
||||
seqStart: 1,
|
||||
share: false,
|
||||
tags: [],
|
||||
})
|
||||
_reset = callback => {
|
||||
this._replaceState(
|
||||
{
|
||||
bootAfterCreate: true,
|
||||
coresPerSocket: undefined,
|
||||
CPUs: '',
|
||||
cpuCap: '',
|
||||
cpuWeight: '',
|
||||
existingDisks: {},
|
||||
fastClone: true,
|
||||
hvmBootFirmware: '',
|
||||
installMethod: 'noConfigDrive',
|
||||
multipleVms: false,
|
||||
name_label: '',
|
||||
name_description: '',
|
||||
nameLabels: map(Array(NB_VMS_MIN), (_, index) => `VM_${index + 1}`),
|
||||
namePattern: '{name}%',
|
||||
nbVms: NB_VMS_MIN,
|
||||
VDIs: [],
|
||||
VIFs: [],
|
||||
seqStart: 1,
|
||||
share: false,
|
||||
tags: [],
|
||||
},
|
||||
callback
|
||||
)
|
||||
}
|
||||
|
||||
_selfCreate = () => {
|
||||
@@ -493,7 +501,8 @@ export default class NewVm extends BaseComponent {
|
||||
VIFs: _VIFs,
|
||||
resourceSet: resourceSet && resourceSet.id,
|
||||
// vm.set parameters
|
||||
coresPerSocket: state.coresPerSocket,
|
||||
coresPerSocket:
|
||||
state.coresPerSocket === null ? undefined : state.coresPerSocket,
|
||||
CPUs: state.CPUs,
|
||||
cpuWeight: state.cpuWeight === '' ? null : state.cpuWeight,
|
||||
cpuCap: state.cpuCap === '' ? null : state.cpuCap,
|
||||
@@ -754,17 +763,6 @@ export default class NewVm extends BaseComponent {
|
||||
pool => vgpuType => pool !== undefined && pool.id === vgpuType.$pool
|
||||
)
|
||||
|
||||
_getCoresPerSocketPossibilities = createSelector(
|
||||
() => {
|
||||
const { pool } = this.props
|
||||
if (pool !== undefined) {
|
||||
return pool.cpus.cores
|
||||
}
|
||||
},
|
||||
() => this.state.state.CPUs,
|
||||
getCoresPerSocketPossibilities
|
||||
)
|
||||
|
||||
_isCoreOs = createSelector(
|
||||
() => this.props.template,
|
||||
template => template && template.name_label === 'CoreOS'
|
||||
@@ -1072,7 +1070,17 @@ export default class NewVm extends BaseComponent {
|
||||
_renderPerformances = () => {
|
||||
const { coresPerSocket, CPUs, memoryDynamicMax } = this.state.state
|
||||
const { template } = this.props
|
||||
const { pool } = this.props
|
||||
const memoryThreshold = get(() => template.memory.static[0])
|
||||
const selectCoresPerSocket = (
|
||||
<SelectCoresPerSocket
|
||||
disabled={pool === undefined}
|
||||
maxCores={get(() => pool.cpus.cores)}
|
||||
maxVcpus={get(() => template.CPUs.max)}
|
||||
onChange={this._linkState('coresPerSocket')}
|
||||
value={coresPerSocket}
|
||||
/>
|
||||
)
|
||||
|
||||
return (
|
||||
<Section
|
||||
@@ -1107,29 +1115,13 @@ export default class NewVm extends BaseComponent {
|
||||
)}
|
||||
</Item>
|
||||
<Item label={_('vmCpuTopology')}>
|
||||
<select
|
||||
className='form-control'
|
||||
onChange={this._linkState('coresPerSocket')}
|
||||
value={coresPerSocket}
|
||||
>
|
||||
{_('vmChooseCoresPerSocket', message => (
|
||||
<option value=''>{message}</option>
|
||||
))}
|
||||
{map(this._getCoresPerSocketPossibilities(), coresPerSocket =>
|
||||
_(
|
||||
'vmCoresPerSocket',
|
||||
{
|
||||
nSockets: CPUs / coresPerSocket,
|
||||
nCores: coresPerSocket,
|
||||
},
|
||||
message => (
|
||||
<option key={coresPerSocket} value={coresPerSocket}>
|
||||
{message}
|
||||
</option>
|
||||
)
|
||||
)
|
||||
)}
|
||||
</select>
|
||||
{pool !== undefined ? (
|
||||
selectCoresPerSocket
|
||||
) : (
|
||||
<Tooltip content={_('requiresAdminPermissions')}>
|
||||
{selectCoresPerSocket}
|
||||
</Tooltip>
|
||||
)}
|
||||
</Item>
|
||||
</SectionContent>
|
||||
</Section>
|
||||
@@ -1189,20 +1181,17 @@ export default class NewVm extends BaseComponent {
|
||||
</LineItem>
|
||||
<br />
|
||||
<LineItem>
|
||||
<Tooltip content={CAN_CLOUD_INIT ? undefined : _('premiumOnly')}>
|
||||
<label>
|
||||
<input
|
||||
checked={installMethod === 'SSH'}
|
||||
disabled={!CAN_CLOUD_INIT}
|
||||
name='installMethod'
|
||||
onChange={this._linkState('installMethod')}
|
||||
type='radio'
|
||||
value='SSH'
|
||||
/>
|
||||
|
||||
{_('newVmSshKey')}
|
||||
</label>
|
||||
</Tooltip>
|
||||
<label>
|
||||
<input
|
||||
checked={installMethod === 'SSH'}
|
||||
name='installMethod'
|
||||
onChange={this._linkState('installMethod')}
|
||||
type='radio'
|
||||
value='SSH'
|
||||
/>
|
||||
|
||||
{_('newVmSshKey')}
|
||||
</label>
|
||||
|
||||
<span className={classNames('input-group', styles.fixedWidth)}>
|
||||
<DebounceInput
|
||||
@@ -1230,20 +1219,17 @@ export default class NewVm extends BaseComponent {
|
||||
</LineItem>
|
||||
<br />
|
||||
<LineItem>
|
||||
<Tooltip content={CAN_CLOUD_INIT ? undefined : _('premiumOnly')}>
|
||||
<label>
|
||||
<input
|
||||
checked={installMethod === 'customConfig'}
|
||||
disabled={!CAN_CLOUD_INIT}
|
||||
name='installMethod'
|
||||
onChange={this._linkState('installMethod')}
|
||||
type='radio'
|
||||
value='customConfig'
|
||||
/>
|
||||
|
||||
{_('newVmCustomConfig')}
|
||||
</label>
|
||||
</Tooltip>
|
||||
<label>
|
||||
<input
|
||||
checked={installMethod === 'customConfig'}
|
||||
name='installMethod'
|
||||
onChange={this._linkState('installMethod')}
|
||||
type='radio'
|
||||
value='customConfig'
|
||||
/>
|
||||
|
||||
{_('newVmCustomConfig')}
|
||||
</label>
|
||||
|
||||
<AvailableTemplateVars />
|
||||
|
||||
|
||||
@@ -475,7 +475,7 @@ export default class New extends Component {
|
||||
}
|
||||
|
||||
_handleAuthChoice = () => {
|
||||
const auth = this.refs['auth'].checked
|
||||
const auth = this.refs.auth.checked
|
||||
this.setState({
|
||||
auth,
|
||||
})
|
||||
|
||||
@@ -154,7 +154,7 @@ class IpsCell extends BaseComponent {
|
||||
value={newIps || ''}
|
||||
/>{' '}
|
||||
<ActionButton
|
||||
form={`newIpForm`}
|
||||
form='newIpForm'
|
||||
icon='save'
|
||||
btnStyle='primary'
|
||||
handler={this._addIps}
|
||||
|
||||
@@ -12,6 +12,7 @@ import { addSubscriptions, downloadLog } from 'utils'
|
||||
import { alert } from 'modal'
|
||||
import { createSelector } from 'selectors'
|
||||
import { CAN_REPORT_BUG, reportBug } from 'report-bug-button'
|
||||
import { get } from '@xen-orchestra/defined'
|
||||
import {
|
||||
deleteApiLog,
|
||||
deleteApiLogs,
|
||||
@@ -33,6 +34,22 @@ const formatLog = log =>
|
||||
2
|
||||
)}\n${JSON.stringify(log.data.error, null, 2).replace(/\\n/g, '\n')}`
|
||||
|
||||
const LogMessage = ({ item: log }) => {
|
||||
const { error } = log.data
|
||||
return (
|
||||
<span>
|
||||
<pre className={styles.widthLimit}>{get(() => error.message)}</pre>
|
||||
{get(() => error.code) === 'LICENCE_RESTRICTION' ? (
|
||||
<a href='https://xcp-ng.org/' rel='noopener noreferrer' target='_blank'>
|
||||
{_('logSuggestXcpNg')}
|
||||
</a>
|
||||
) : get(() => error.name) === 'XapiError' ? (
|
||||
_('logXapiError')
|
||||
) : null}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
const COLUMNS = [
|
||||
{
|
||||
name: _('logUser'),
|
||||
@@ -50,22 +67,7 @@ const COLUMNS = [
|
||||
},
|
||||
{
|
||||
name: _('logMessage'),
|
||||
itemRenderer: log => (
|
||||
<span>
|
||||
<pre className={styles.widthLimit}>
|
||||
{log.data.error && log.data.error.message}
|
||||
</pre>
|
||||
{log.data.error && log.data.error.code === 'LICENCE_RESTRICTION' && (
|
||||
<a
|
||||
href='https://xcp-ng.org/'
|
||||
rel='noopener noreferrer'
|
||||
target='_blank'
|
||||
>
|
||||
{_('logSuggestXcpNg')}
|
||||
</a>
|
||||
)}
|
||||
</span>
|
||||
),
|
||||
component: LogMessage,
|
||||
sortCriteria: log => log.data.error && log.data.error.message,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -9,6 +9,7 @@ import Link from 'link'
|
||||
import React from 'react'
|
||||
import renderXoItem from 'render-xo-item'
|
||||
import SelectBootFirmware from 'select-boot-firmware'
|
||||
import SelectCoresPerSocket from 'select-cores-per-socket'
|
||||
import TabButton from 'tab-button'
|
||||
import Tooltip from 'tooltip'
|
||||
import { error } from 'notification'
|
||||
@@ -33,7 +34,6 @@ import {
|
||||
addSubscriptions,
|
||||
connectStore,
|
||||
formatSize,
|
||||
getCoresPerSocketPossibilities,
|
||||
getVirtualizationModeLabel,
|
||||
noop,
|
||||
osFamily,
|
||||
@@ -43,7 +43,6 @@ import {
|
||||
every,
|
||||
filter,
|
||||
find,
|
||||
includes,
|
||||
isEmpty,
|
||||
keyBy,
|
||||
map,
|
||||
@@ -249,80 +248,30 @@ class Vgpus extends Component {
|
||||
}
|
||||
|
||||
class CoresPerSocket extends Component {
|
||||
_getCoresPerSocketPossibilities = createSelector(
|
||||
() => {
|
||||
const { container } = this.props
|
||||
if (container != null) {
|
||||
return container.cpus.cores
|
||||
}
|
||||
},
|
||||
() => this.props.vm.CPUs.number,
|
||||
getCoresPerSocketPossibilities
|
||||
)
|
||||
|
||||
_selectedValueIsNotInOptions = createSelector(
|
||||
() => this.props.vm.coresPerSocket,
|
||||
this._getCoresPerSocketPossibilities,
|
||||
(selectedCoresPerSocket, options) =>
|
||||
selectedCoresPerSocket !== undefined &&
|
||||
!includes(options, selectedCoresPerSocket)
|
||||
)
|
||||
|
||||
_onChange = event =>
|
||||
editVm(this.props.vm, { coresPerSocket: getEventValue(event) || null })
|
||||
_onChange = coresPerSocket => editVm(this.props.vm, { coresPerSocket })
|
||||
|
||||
render() {
|
||||
const { container, vm } = this.props
|
||||
const selectedCoresPerSocket = vm.coresPerSocket
|
||||
const options = this._getCoresPerSocketPossibilities()
|
||||
const { coresPerSocket, CPUs: cpus } = vm
|
||||
|
||||
return (
|
||||
<form className='form-inline'>
|
||||
<div>
|
||||
{container != null ? (
|
||||
<span>
|
||||
<select
|
||||
className='form-control'
|
||||
onChange={this._onChange}
|
||||
value={selectedCoresPerSocket || ''}
|
||||
>
|
||||
{_({ key: 'none' }, 'vmChooseCoresPerSocket', message => (
|
||||
<option value=''>{message}</option>
|
||||
))}
|
||||
{this._selectedValueIsNotInOptions() &&
|
||||
_(
|
||||
{ key: 'incorrect' },
|
||||
'vmCoresPerSocketIncorrectValue',
|
||||
message => (
|
||||
<option value={selectedCoresPerSocket}> {message}</option>
|
||||
)
|
||||
)}
|
||||
{map(options, coresPerSocket =>
|
||||
_(
|
||||
{ key: coresPerSocket },
|
||||
'vmCoresPerSocket',
|
||||
{
|
||||
nSockets: vm.CPUs.number / coresPerSocket,
|
||||
nCores: coresPerSocket,
|
||||
},
|
||||
message => <option value={coresPerSocket}>{message}</option>
|
||||
)
|
||||
)}
|
||||
</select>{' '}
|
||||
{this._selectedValueIsNotInOptions() && (
|
||||
<Tooltip content={_('vmCoresPerSocketIncorrectValueSolution')}>
|
||||
<Icon icon='error' size='lg' />
|
||||
</Tooltip>
|
||||
)}
|
||||
</span>
|
||||
) : selectedCoresPerSocket != null ? (
|
||||
_('vmCoresPerSocket', {
|
||||
nSockets: vm.CPUs.number / selectedCoresPerSocket,
|
||||
nCores: selectedCoresPerSocket,
|
||||
<SelectCoresPerSocket
|
||||
maxCores={container.cpus.cores}
|
||||
maxVcpus={cpus.max}
|
||||
onChange={this._onChange}
|
||||
value={coresPerSocket}
|
||||
/>
|
||||
) : coresPerSocket !== undefined ? (
|
||||
_('vmSocketsWithCoresPerSocket', {
|
||||
nSockets: cpus.max / coresPerSocket,
|
||||
nCores: coresPerSocket,
|
||||
})
|
||||
) : (
|
||||
_('vmCoresPerSocketNone')
|
||||
)}
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -400,14 +349,12 @@ const Acls = decorate([
|
||||
computed: {
|
||||
rawAcls: (_, { acls, vm }) => filter(acls, { object: vm }),
|
||||
resolvedAcls: ({ rawAcls }, { users, groups }) => {
|
||||
if (users === undefined && groups === undefined) {
|
||||
if (users === undefined || groups === undefined) {
|
||||
return []
|
||||
}
|
||||
return rawAcls.map(({ subject, ...acl }) => ({
|
||||
...acl,
|
||||
subject:
|
||||
(users !== undefined && users[subject]) ||
|
||||
(groups !== undefined && groups[subject]),
|
||||
subject: defined(users[subject], groups[subject]),
|
||||
}))
|
||||
},
|
||||
},
|
||||
|
||||
@@ -7,9 +7,10 @@ import { Container, Row, Col } from 'grid'
|
||||
import { isAdmin } from 'selectors'
|
||||
import { NavLink, NavTabs } from 'nav'
|
||||
|
||||
import Update from './update'
|
||||
import Licenses from './licenses'
|
||||
import Notifications, { NotificationTag } from './notifications'
|
||||
import Support from './support'
|
||||
import Update from './update'
|
||||
|
||||
const Header = ({ isAdmin }) => (
|
||||
<Container>
|
||||
@@ -35,6 +36,11 @@ const Header = ({ isAdmin }) => (
|
||||
<Icon icon='menu-notification' /> {_('notificationsPage')}{' '}
|
||||
<NotificationTag />
|
||||
</NavLink>
|
||||
{isAdmin && (
|
||||
<NavLink to='/xoa/support'>
|
||||
<Icon icon='menu-support' /> {_('supportPage')}
|
||||
</NavLink>
|
||||
)}
|
||||
</NavTabs>
|
||||
</Col>
|
||||
</Row>
|
||||
@@ -42,9 +48,10 @@ const Header = ({ isAdmin }) => (
|
||||
)
|
||||
|
||||
const Xoa = routes('xoa', {
|
||||
update: Update,
|
||||
licenses: Licenses,
|
||||
notifications: Notifications,
|
||||
support: Support,
|
||||
update: Update,
|
||||
})(
|
||||
connectStore({
|
||||
isAdmin,
|
||||
|
||||
61
packages/xo-web/src/xo-app/xoa/support/index.js
Normal file
61
packages/xo-web/src/xo-app/xoa/support/index.js
Normal file
@@ -0,0 +1,61 @@
|
||||
import _ from 'intl'
|
||||
import ActionButton from 'action-button'
|
||||
import AnsiUp from 'ansi_up'
|
||||
import decorate from 'apply-decorators'
|
||||
import React from 'react'
|
||||
import { adminOnly, getXoaPlan } from 'utils'
|
||||
import { Card, CardBlock, CardHeader } from 'card'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import { injectState, provideState } from 'reaclette'
|
||||
import { checkXoa } from 'xo'
|
||||
|
||||
const ansiUp = new AnsiUp()
|
||||
const COMMUNITY = getXoaPlan() === 'Community'
|
||||
|
||||
const Support = decorate([
|
||||
adminOnly,
|
||||
provideState({
|
||||
initialState: () => ({ stdoutCheckXoa: '' }),
|
||||
effects: {
|
||||
initialize: async () => ({
|
||||
stdoutCheckXoa: COMMUNITY ? '' : await checkXoa(),
|
||||
}),
|
||||
checkXoa: async () => ({ stdoutCheckXoa: await checkXoa() }),
|
||||
},
|
||||
}),
|
||||
injectState,
|
||||
({ effects, state: { stdoutCheckXoa } }) => (
|
||||
<Container>
|
||||
<Row>
|
||||
<Col mediumSize={6}>
|
||||
<Card>
|
||||
<CardHeader>{_('xoaCheck')}</CardHeader>
|
||||
{COMMUNITY ? (
|
||||
<CardBlock>
|
||||
<span className='text-info'>{_('checkXoaCommunity')}</span>
|
||||
</CardBlock>
|
||||
) : (
|
||||
<CardBlock>
|
||||
<ActionButton
|
||||
btnStyle='success'
|
||||
handler={effects.checkXoa}
|
||||
icon='diagnosis'
|
||||
>
|
||||
{_('checkXoa')}
|
||||
</ActionButton>
|
||||
<hr />
|
||||
<pre
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: ansiUp.ansi_to_html(stdoutCheckXoa),
|
||||
}}
|
||||
/>
|
||||
</CardBlock>
|
||||
)}
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
),
|
||||
])
|
||||
|
||||
export default Support
|
||||
@@ -221,11 +221,12 @@ const Updates = decorate([
|
||||
? () => ({ 'xen-orchestra': 'sources' })
|
||||
: async function() {
|
||||
const {
|
||||
engine,
|
||||
installer,
|
||||
updater,
|
||||
npm,
|
||||
} = await xoaUpdater.getLocalManifest()
|
||||
return { ...installer, ...updater, ...npm }
|
||||
return { ...engine, ...installer, ...updater, ...npm }
|
||||
},
|
||||
isDisconnected: (_, { xoaUpdaterState }) =>
|
||||
xoaUpdater === 'disconnected' || xoaUpdaterState === 'error',
|
||||
|
||||
@@ -65,7 +65,7 @@ require('exec-promise')(() =>
|
||||
const originalScripts = scripts
|
||||
|
||||
if (!pkg.private && !('postversion' in scripts)) {
|
||||
scripts = { ...scripts, postversion: 'npm publish' }
|
||||
scripts = { ...scripts, postversion: 'npm publish --access public' }
|
||||
}
|
||||
|
||||
const prepublish = scripts.prepublish
|
||||
|
||||
@@ -11103,6 +11103,13 @@ promise-toolbox@^0.13.0:
|
||||
dependencies:
|
||||
make-error "^1.3.2"
|
||||
|
||||
promise-toolbox@^0.14.0:
|
||||
version "0.14.0"
|
||||
resolved "https://registry.yarnpkg.com/promise-toolbox/-/promise-toolbox-0.14.0.tgz#b2f8bd90fce6709b290b58fc06d89280375a98b4"
|
||||
integrity sha512-VV5lXK4lXaPB9oBO50ope1qd0AKN8N3nK14jYvV9/qFmfZW2Px/bJjPZBniGjXcIJf6J5Y/coNgJtPHDyiUV/g==
|
||||
dependencies:
|
||||
make-error "^1.3.2"
|
||||
|
||||
promise-toolbox@^0.8.0:
|
||||
version "0.8.3"
|
||||
resolved "https://registry.yarnpkg.com/promise-toolbox/-/promise-toolbox-0.8.3.tgz#b757232a21d246d8702df50da6784932dd0f5348"
|
||||
|
||||
Reference in New Issue
Block a user