Compare commits
39 Commits
xo-web-v5.
...
hub-xva-de
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a4b966aa96 | ||
|
|
7459c9e2cf | ||
|
|
4edaf67f0a | ||
|
|
876c1130e1 | ||
|
|
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 |
@@ -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
|
||||
|
||||
55
CHANGELOG.md
55
CHANGELOG.md
@@ -4,8 +4,37 @@
|
||||
|
||||
### Enhancements
|
||||
|
||||
### Bug fixes
|
||||
|
||||
### Released packages
|
||||
|
||||
- xo-server v5.51.0
|
||||
- xo-web v5.51.0
|
||||
|
||||
|
||||
## **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 +44,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 +97,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,13 @@
|
||||
|
||||
> 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))
|
||||
- [HUB] Display template description (PR [#4575](https://github.com/vatesfr/xen-orchestra/pull/4575))
|
||||
|
||||
### 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))
|
||||
|
||||
### Released packages
|
||||
|
||||
@@ -36,8 +22,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.
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
"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",
|
||||
|
||||
@@ -210,6 +210,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--]
|
||||
|
||||
@@ -221,7 +221,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()
|
||||
@@ -260,7 +260,7 @@ describe('backupNg', () => {
|
||||
tasks: [vmTask],
|
||||
...log
|
||||
},
|
||||
] = await xo.call('backupNg.getLogs', {
|
||||
] = await xo.getBackupLogs({
|
||||
jobId,
|
||||
scheduleId: schedule.id,
|
||||
})
|
||||
@@ -319,7 +319,7 @@ describe('backupNg', () => {
|
||||
tasks: [task],
|
||||
...log
|
||||
},
|
||||
] = await xo.call('backupNg.getLogs', {
|
||||
] = await xo.getBackupLogs({
|
||||
jobId,
|
||||
scheduleId: schedule.id,
|
||||
})
|
||||
@@ -415,7 +415,7 @@ describe('backupNg', () => {
|
||||
tasks: [{ tasks: subTasks, ...vmTask }],
|
||||
...log
|
||||
},
|
||||
] = await xo.call('backupNg.getLogs', {
|
||||
] = await xo.getBackupLogs({
|
||||
jobId,
|
||||
scheduleId: schedule.id,
|
||||
})
|
||||
@@ -506,7 +506,7 @@ describe('backupNg', () => {
|
||||
expect(backups.length).toBe(exportRetention)
|
||||
)
|
||||
|
||||
const backupLogs = await xo.call('backupNg.getLogs', {
|
||||
const backupLogs = await xo.getBackupLogs({
|
||||
jobId,
|
||||
scheduleId: schedule.id,
|
||||
})
|
||||
|
||||
@@ -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,6 +3,7 @@ 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 }) {
|
||||
@@ -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 = {
|
||||
|
||||
@@ -1164,11 +1164,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',
|
||||
@@ -1535,8 +1535,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',
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "xo-web",
|
||||
"version": "5.50.0",
|
||||
"version": "5.50.2",
|
||||
"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
|
||||
|
||||
@@ -1318,7 +1318,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 +1931,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 +2143,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}',
|
||||
|
||||
@@ -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 = {
|
||||
@@ -629,7 +629,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({
|
||||
|
||||
@@ -277,6 +277,10 @@
|
||||
@extend .fa;
|
||||
@extend .fa-thumbs-up;
|
||||
}
|
||||
&-deploy {
|
||||
@extend .fa;
|
||||
@extend .fa-rocket;
|
||||
}
|
||||
|
||||
// Backups
|
||||
&-backup {
|
||||
@@ -886,6 +890,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>
|
||||
),
|
||||
])
|
||||
279
packages/xo-web/src/xo-app/hub/resource.js
Normal file
279
packages/xo-web/src/xo-app/hub/resource.js
Normal file
@@ -0,0 +1,279 @@
|
||||
import _ from 'intl'
|
||||
import ActionButton from 'action-button'
|
||||
import Button from 'button'
|
||||
import decorate from 'apply-decorators'
|
||||
import Icon from 'icon'
|
||||
import marked from 'marked'
|
||||
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')
|
||||
},
|
||||
showDescription() {
|
||||
alert(
|
||||
this.props.name,
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: marked(this.props.description),
|
||||
}}
|
||||
/>
|
||||
)
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
installedTemplates: (_, { id, templates }) =>
|
||||
filter(templates, ['other.xo:resource:xva:id', id]),
|
||||
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,
|
||||
({
|
||||
description,
|
||||
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'>
|
||||
<Row>
|
||||
<Col mediumSize={6}>
|
||||
<span className='text-muted'>{_('os')}</span> <strong>{os}</strong>
|
||||
</Col>
|
||||
<Col mediumSize={6}>
|
||||
{description !== undefined && (
|
||||
<div className='pull-right'>
|
||||
<Button onClick={effects.showDescription}>
|
||||
<Icon icon='info' /> {_('vmNameDescription')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
<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 {
|
||||
|
||||
@@ -354,6 +354,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',
|
||||
|
||||
@@ -21,7 +21,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,
|
||||
@@ -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,31 @@ 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,
|
||||
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 = () => {
|
||||
@@ -1189,20 +1196,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 +1234,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 />
|
||||
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user