Compare commits

..

76 Commits

Author SHA1 Message Date
Mohamedox
28523e591a fix 2019-09-26 16:43:11 +02:00
Mohamedox
e935cf8283 Adapt PR to comments 2019-09-26 16:18:44 +02:00
Mohamedox
87db911e5a delete forgotten comment 2019-09-26 15:57:40 +02:00
Mohamedox
ec6d3fd128 fix 2019-09-26 15:50:06 +02:00
Mohamedox
3aaafef88e adapt PR to comments 2019-09-26 15:22:38 +02:00
Mohamedox
714e4f4ea2 update changelog 2019-09-26 11:05:08 +02:00
Mohamedox
77b0914e48 fix 2019-09-26 10:31:28 +02:00
Mohamedox
5c4a362529 adapt PR to comments 2019-09-26 10:29:09 +02:00
Mohamedox
d7bcdfac19 fix 2019-09-25 17:20:32 +02:00
Mohamedox
5a3e895ce5 fix 2019-09-25 17:19:31 +02:00
Mohamedox
a7d08ac91e fix 2019-09-25 17:11:27 +02:00
Mohamedox
7ff48f3aa9 last fix 2019-09-25 16:38:27 +02:00
Mohamedox
7f42ab15dd fix 2019-09-25 16:22:35 +02:00
Mohamedox
3b7d02de95 fix 2019-09-25 16:17:43 +02:00
Mohamedox
3fdac01eb8 fix 2019-09-25 15:18:09 +02:00
Mohamedox
5601740334 fix 2019-09-25 14:48:50 +02:00
Mohamedox
f824cbe710 adapt PR to comments 2019-09-25 14:38:41 +02:00
Mohamedox
510f30eb23 fix 2019-09-25 13:38:56 +02:00
Mohamedox
c84eded0aa Adapt PR to comments 2019-09-25 11:59:10 +02:00
Mohamedox
02ed02926b update changelog 2019-09-25 10:17:56 +02:00
Mohamedox
77c172fce0 adapt Pr to comments 2019-09-25 10:17:55 +02:00
Mohamedox
c48e017711 fix 2019-09-25 10:17:55 +02:00
Mohamedox
855ec06628 Adapt PR to comments 2019-09-25 10:17:54 +02:00
Mohamedox
9e4b606372 fix 2019-09-25 10:17:32 +02:00
Mohamedox
de1c8f4d4f adapt PR to comments 2019-09-25 10:17:32 +02:00
Mohamedox
876562570c Adapt PR to comments 2019-09-25 10:17:31 +02:00
Mohamedox
bab514ffc4 fix 2019-09-25 10:17:00 +02:00
Mohamedox
dc15965972 fix 2019-09-25 10:16:59 +02:00
Mohamedox
903b7fed50 adapt PR to comments 2019-09-25 10:16:58 +02:00
Mohamedox
f62bdeae1b adapt PR to comments 2019-09-25 10:16:58 +02:00
Mohamedox
e259e72c92 fix 2019-09-25 10:16:57 +02:00
Mohamedox
2a64a4810b adapt PR to comments 2019-09-25 10:16:57 +02:00
Mohamedox
a543e05561 adapt pr to comments 2019-09-25 10:16:56 +02:00
Mohamedox
2c7ca39a77 Display error when pool has no default SR 2019-09-25 10:16:56 +02:00
Mohamedox
9ada70a5ab fix 2019-09-25 10:16:55 +02:00
Mohamedox
a574f9d8dc initialize template when changing page 2019-09-25 10:16:32 +02:00
Mohamedox
9a4f9a8978 fix 2019-09-25 10:16:31 +02:00
Mohamedox
1cdf14e587 fix 2019-09-25 10:16:30 +02:00
Mohamedox
9272524264 xva-store v1 2019-09-25 10:16:29 +02:00
Mohamedox
027dfd262f adapt PR to new specs 2019-09-25 10:16:28 +02:00
Mohamedox
8967ad94dc version 1.0 2019-09-25 10:16:27 +02:00
Mohamedox
48a0684097 xva store with popularity 2019-09-25 10:16:27 +02:00
Mohamedox
1caa98ea8b wip 2019-09-25 10:16:26 +02:00
Mohamedox
01e15ae4ec fix 2019-09-25 10:16:26 +02:00
Mohamedox
5077de953d adapt to new specs 2019-09-25 10:16:25 +02:00
Mohamedox
fab3751734 wip 2019-09-25 10:16:24 +02:00
Mohamedox
97df5d9e32 adapt PR to comments 2019-09-25 10:16:24 +02:00
Mohamedox
e2180303da change routing 2019-09-25 10:16:23 +02:00
Mohamedox
e2ddb62e5f mutualize code 2019-09-25 10:16:23 +02:00
Mohamedox
9e4265ae72 update changelog 2019-09-25 10:16:22 +02:00
Mohamedox
ec30e20278 fix 2019-09-25 10:15:51 +02:00
Mohamedox
1bf41b915c fix 2019-09-25 10:15:50 +02:00
Mohamedox
03e8cd0f7b change permission 2019-09-25 10:15:49 +02:00
Mohamedox
bf03dbd8dc fix 2019-09-25 10:15:49 +02:00
Mohamedox
570c9f6e0f enhance code quality 2019-09-25 10:15:48 +02:00
Mohamedox
e7016a4a40 fix 2019-09-25 10:15:47 +02:00
Mohamedox
f980ad58d4 xva 2019-09-25 10:15:46 +02:00
Mohamedox
a8af5a166d must subscribe 2019-09-25 10:15:46 +02:00
Mohamedox
d0acefaa04 poc version 2019-09-25 10:15:45 +02:00
Mohamedox
54fcc2e0db progress bar 2019-09-25 10:15:44 +02:00
Mohamedox
2d3f02cbbd wip 2019-09-25 10:15:43 +02:00
Mohamedox
13dd57bad7 only registred customer can download 2019-09-25 10:15:42 +02:00
Mohamedox
d9e26d155c wip 2019-09-25 10:15:41 +02:00
Mohamedox
c9556c44c9 WIP: feat(xo-web/hub): XO Vms HUB 2019-09-25 10:15:41 +02:00
Mohamedox
916b3ec662 adapt PR to comments 2019-09-25 10:15:40 +02:00
Mohamedox
f1cff1275c feat(xo-web/new-vm): create new VM with predefined template ID in URL query string
Fixes #4494
2019-09-25 10:15:39 +02:00
Julien Fontanet
b24400b21d feat(xen-api): support IPv6 addresses (#4521)
Fixes #4520
2019-09-24 13:59:49 +02:00
Pierre Donias
6c1d651687 fix(xo-server/host.isHostServerTimeConsistent): dont return false on check failure (#4540) 2019-09-24 10:46:43 +02:00
Pierre Donias
e7757b53e7 feat(xo-web/new-vm): remove cloud init plan limitation (#4543) 2019-09-24 10:24:27 +02:00
Julien Fontanet
a6d182e92d feat(xo-server/getBackupNgLogs): implement debounce (#4509) (#4541)
Similar to #4509

Fixes xoa-support#1676

For now, the delay is set to 10s which is the duration used by xo-web's
subscription, which makes it enough to make it independent of the number of
clients.

In the future, this could be configurable, but we may simply do the
consolidation only once during the backup execution.
2019-09-23 16:20:17 +02:00
Julien Fontanet
925eca1463 chore(xo-server/api): context has methods bound to XO
This makes sure the correct context is used, which is necessary for properties write and for debouncing.
2019-09-23 15:59:45 +02:00
Julien Fontanet
8b454f0d39 chore(xo-server/MultiKeyMap): add basic tests 2019-09-23 15:14:54 +02:00
Pierre Donias
7c4d110353 feat(xo-web/settings/logs): identify XAPI errors (#4385)
Fixes #4101
2019-09-23 10:31:34 +02:00
Julien Fontanet
6df55523b6 feat(xo-web): display node in list of packages 2019-09-20 17:22:21 +02:00
badrAZ
3ec6a24634 chore(CHANGELOG): update next 2019-09-20 16:36:04 +02:00
badrAZ
164b4218c4 feat(xo-web): 5.50.0 2019-09-20 16:21:38 +02:00
26 changed files with 832 additions and 254 deletions

View File

@@ -6,6 +6,12 @@
- [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))
- [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))
### Bug fixes
@@ -15,12 +21,25 @@
- [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))
### Released packages
- xo-server-sdn-controller v0.2.1
- xo-server v5.49.0
- xo-web v5.49.0
- @xen-orchestra/cron v1.0.4
- xo-server-sdn-controller v0.3.0
- @xen-orchestra/template v0.1.0
- xo-server v5.50.0
- xo-web v5.50.0
## **5.38.0** (2019-08-29)

View File

@@ -7,27 +7,18 @@
> 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))
- [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`
- [HUB] VM template store [#1918](https://github.com/vatesfr/xen-orchestra/issues/1918) (PR [#4442](https://github.com/vatesfr/xen-orchestra/pull/4442))
### 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))
- [Host] Fix an issue where host was wrongly reporting time inconsistency (PR [#4540](https://github.com/vatesfr/xen-orchestra/pull/4540))
### Released packages
@@ -36,8 +27,7 @@
>
> 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
- xen-api v0.27.2
- xo-server-cloud v0.3.0
- xo-server v5.51.0
- xo-web v5.51.0

View File

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

View File

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

View File

@@ -46,6 +46,7 @@
"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",

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
{
"private": true,
"name": "xo-web",
"version": "5.49.0",
"version": "5.50.0",
"license": "AGPL-3.0",
"description": "Web interface client for Xen-Orchestra",
"keywords": [

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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'>
&nbsp; {_('vmNoAvailable')}
<Icon icon='alarm' color='yellow' />
</h2>
</Col>
) : (
resources.map(data => (
<Col key={data.namespace} mediumSize={3}>
<Resource {...data} />
</Col>
))
)}
</Row>
</Container>
</Page>
),
])

View 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')}
&nbsp;
{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>
),
])

View 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(+process.env.XOA_PLAN) === '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(+process.env.XOA_PLAN) === '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: (_, { 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,
({
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}
size='small'
style={{ border: 'none' }}
>
<Icon icon='delete' size='xs' />
</ActionButton>
<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>
),
])

View File

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

View File

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

View File

@@ -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,
@@ -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'
/>
&nbsp;
{_('newVmSshKey')}
</label>
</Tooltip>
<label>
<input
checked={installMethod === 'SSH'}
name='installMethod'
onChange={this._linkState('installMethod')}
type='radio'
value='SSH'
/>
&nbsp;
{_('newVmSshKey')}
</label>
&nbsp;
<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'
/>
&nbsp;
{_('newVmCustomConfig')}
</label>
</Tooltip>
<label>
<input
checked={installMethod === 'customConfig'}
name='installMethod'
onChange={this._linkState('installMethod')}
type='radio'
value='customConfig'
/>
&nbsp;
{_('newVmCustomConfig')}
</label>
&nbsp;
<AvailableTemplateVars />
&nbsp;

View File

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

View File

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