Compare commits
66 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
28523e591a | ||
|
|
e935cf8283 | ||
|
|
87db911e5a | ||
|
|
ec6d3fd128 | ||
|
|
3aaafef88e | ||
|
|
714e4f4ea2 | ||
|
|
77b0914e48 | ||
|
|
5c4a362529 | ||
|
|
d7bcdfac19 | ||
|
|
5a3e895ce5 | ||
|
|
a7d08ac91e | ||
|
|
7ff48f3aa9 | ||
|
|
7f42ab15dd | ||
|
|
3b7d02de95 | ||
|
|
3fdac01eb8 | ||
|
|
5601740334 | ||
|
|
f824cbe710 | ||
|
|
510f30eb23 | ||
|
|
c84eded0aa | ||
|
|
02ed02926b | ||
|
|
77c172fce0 | ||
|
|
c48e017711 | ||
|
|
855ec06628 | ||
|
|
9e4b606372 | ||
|
|
de1c8f4d4f | ||
|
|
876562570c | ||
|
|
bab514ffc4 | ||
|
|
dc15965972 | ||
|
|
903b7fed50 | ||
|
|
f62bdeae1b | ||
|
|
e259e72c92 | ||
|
|
2a64a4810b | ||
|
|
a543e05561 | ||
|
|
2c7ca39a77 | ||
|
|
9ada70a5ab | ||
|
|
a574f9d8dc | ||
|
|
9a4f9a8978 | ||
|
|
1cdf14e587 | ||
|
|
9272524264 | ||
|
|
027dfd262f | ||
|
|
8967ad94dc | ||
|
|
48a0684097 | ||
|
|
1caa98ea8b | ||
|
|
01e15ae4ec | ||
|
|
5077de953d | ||
|
|
fab3751734 | ||
|
|
97df5d9e32 | ||
|
|
e2180303da | ||
|
|
e2ddb62e5f | ||
|
|
9e4265ae72 | ||
|
|
ec30e20278 | ||
|
|
1bf41b915c | ||
|
|
03e8cd0f7b | ||
|
|
bf03dbd8dc | ||
|
|
570c9f6e0f | ||
|
|
e7016a4a40 | ||
|
|
f980ad58d4 | ||
|
|
a8af5a166d | ||
|
|
d0acefaa04 | ||
|
|
54fcc2e0db | ||
|
|
2d3f02cbbd | ||
|
|
13dd57bad7 | ||
|
|
d9e26d155c | ||
|
|
c9556c44c9 | ||
|
|
916b3ec662 | ||
|
|
f1cff1275c |
@@ -12,6 +12,7 @@
|
|||||||
- [New VM] Cloud Init available for all plans (PR [#4543](https://github.com/vatesfr/xen-orchestra/pull/4543))
|
- [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)) \
|
- [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`
|
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
|
### Bug fixes
|
||||||
|
|
||||||
@@ -27,5 +28,6 @@
|
|||||||
> Rule of thumb: add packages on top.
|
> Rule of thumb: add packages on top.
|
||||||
|
|
||||||
- xen-api v0.27.2
|
- xen-api v0.27.2
|
||||||
|
- xo-server-cloud v0.3.0
|
||||||
- xo-server v5.51.0
|
- xo-server v5.51.0
|
||||||
- xo-web v5.51.0
|
- xo-web v5.51.0
|
||||||
|
|||||||
@@ -20,9 +20,13 @@ class XoServerCloud {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async load() {
|
async load() {
|
||||||
const getResourceCatalog = () => this._getCatalog()
|
const getResourceCatalog = this._getCatalog.bind(this)
|
||||||
getResourceCatalog.description = 'Get the list of all available resources'
|
getResourceCatalog.description =
|
||||||
|
"Get the list of user's available resources"
|
||||||
getResourceCatalog.permission = 'admin'
|
getResourceCatalog.permission = 'admin'
|
||||||
|
getResourceCatalog.params = {
|
||||||
|
filters: { type: 'object', optional: true },
|
||||||
|
}
|
||||||
|
|
||||||
const registerResource = ({ namespace }) =>
|
const registerResource = ({ namespace }) =>
|
||||||
this._registerResource(namespace)
|
this._registerResource(namespace)
|
||||||
@@ -34,8 +38,29 @@ class XoServerCloud {
|
|||||||
}
|
}
|
||||||
registerResource.permission = 'admin'
|
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({
|
this._unsetApiMethods = this._xo.addApiMethods({
|
||||||
cloud: {
|
cloud: {
|
||||||
|
downloadAndInstallResource,
|
||||||
getResourceCatalog,
|
getResourceCatalog,
|
||||||
registerResource,
|
registerResource,
|
||||||
},
|
},
|
||||||
@@ -66,8 +91,8 @@ class XoServerCloud {
|
|||||||
|
|
||||||
// ----------------------------------------------------------------
|
// ----------------------------------------------------------------
|
||||||
|
|
||||||
async _getCatalog() {
|
async _getCatalog({ filters } = {}) {
|
||||||
const catalog = await this._updater.call('getResourceCatalog')
|
const catalog = await this._updater.call('getResourceCatalog', { filters })
|
||||||
|
|
||||||
if (!catalog) {
|
if (!catalog) {
|
||||||
throw new Error('cannot get 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) {
|
async _registerResource(namespace) {
|
||||||
const _namespace = (await this._getNamespaces())[namespace]
|
const _namespace = (await this._getNamespaces())[namespace]
|
||||||
|
|
||||||
@@ -106,8 +151,10 @@ class XoServerCloud {
|
|||||||
|
|
||||||
// ----------------------------------------------------------------
|
// ----------------------------------------------------------------
|
||||||
|
|
||||||
async _getNamespaceCatalog(namespace) {
|
async _getNamespaceCatalog({ hub, namespace }) {
|
||||||
const namespaceCatalog = (await this._getCatalog())[namespace]
|
const namespaceCatalog = (await this._getCatalog({ filters: { hub } }))[
|
||||||
|
namespace
|
||||||
|
]
|
||||||
|
|
||||||
if (!namespaceCatalog) {
|
if (!namespaceCatalog) {
|
||||||
throw new Error(`cannot get catalog: ${namespace} not registered`)
|
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]
|
const _namespace = (await this._getNamespaces())[namespace]
|
||||||
|
|
||||||
if (!_namespace || !_namespace.registered) {
|
if (!hub && (!_namespace || !_namespace.registered)) {
|
||||||
throw new Error(`cannot get resource: ${namespace} not 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
|
// 2018-03-20 Extra check: getResourceDownloadToken seems to be called without a token in some cases
|
||||||
if (token === undefined) {
|
if (token === undefined) {
|
||||||
|
|||||||
@@ -1164,11 +1164,11 @@ async function _prepareGlusterVm(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function _importGlusterVM(xapi, template, lvmsrId) {
|
async function _importGlusterVM(xapi, template, lvmsrId) {
|
||||||
const templateStream = await this.requestResource(
|
const templateStream = await this.requestResource({
|
||||||
'xosan',
|
id: template.id,
|
||||||
template.id,
|
namespace: 'xosan',
|
||||||
template.version
|
version: template.version,
|
||||||
)
|
})
|
||||||
const newVM = await xapi.importVm(templateStream, {
|
const newVM = await xapi.importVm(templateStream, {
|
||||||
srId: lvmsrId,
|
srId: lvmsrId,
|
||||||
type: 'xva',
|
type: 'xva',
|
||||||
@@ -1535,8 +1535,11 @@ export async function downloadAndInstallXosanPack({ id, version, pool }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const xapi = this.getXapi(pool.id)
|
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.installSupplementalPackOnAllHosts(res)
|
||||||
await xapi.pool.update_other_config(
|
await xapi.pool.update_other_config(
|
||||||
'xosan_pack_installation_time',
|
'xosan_pack_installation_time',
|
||||||
|
|||||||
@@ -2143,6 +2143,21 @@ const messages = {
|
|||||||
xosanIssueHostNotInNetwork:
|
xosanIssueHostNotInNetwork:
|
||||||
'Will configure the host xosan network device with a static IP address and plug it in.',
|
'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
|
// Licenses
|
||||||
xosanUnregisteredDisclaimer:
|
xosanUnregisteredDisclaimer:
|
||||||
'You are not registered and therefore will not be able to create or manage your XOSAN SRs. {link}',
|
'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',
|
'SET_HOME_VM_IDS_SELECTION',
|
||||||
homeVmIdsSelection => homeVmIdsSelection
|
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 cookies from 'cookies-js'
|
||||||
|
import { omit } from 'lodash'
|
||||||
|
|
||||||
import invoke from '../invoke'
|
import invoke from '../invoke'
|
||||||
|
|
||||||
@@ -92,6 +93,19 @@ export default {
|
|||||||
homeVmIdsSelection,
|
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(
|
objects: combineActionHandlers(
|
||||||
{
|
{
|
||||||
all: {}, // Mutable for performance!
|
all: {}, // Mutable for performance!
|
||||||
|
|||||||
@@ -349,6 +349,10 @@ export const subscribeResourceCatalog = createSubscription(() =>
|
|||||||
_call('cloud.getResourceCatalog')
|
_call('cloud.getResourceCatalog')
|
||||||
)
|
)
|
||||||
|
|
||||||
|
export const subscribeHubResourceCatalog = createSubscription(() =>
|
||||||
|
_call('cloud.getResourceCatalog', { filters: { hub: true } })
|
||||||
|
)
|
||||||
|
|
||||||
const getNotificationCookie = () => {
|
const getNotificationCookie = () => {
|
||||||
const notificationCookie = cookies.get(
|
const notificationCookie = cookies.get(
|
||||||
`notifications:${store.getState().user.id}`
|
`notifications:${store.getState().user.id}`
|
||||||
@@ -2871,7 +2875,10 @@ export const fixHostNotInXosanNetwork = (xosanSr, host) =>
|
|||||||
|
|
||||||
// XOSAN packs -----------------------------------------------------------------
|
// 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 }) =>
|
const downloadAndInstallXosanPack = (pack, pool, { version }) =>
|
||||||
_call('xosan.downloadAndInstallXosanPack', {
|
_call('xosan.downloadAndInstallXosanPack', {
|
||||||
@@ -2880,6 +2887,14 @@ const downloadAndInstallXosanPack = (pack, pool, { version }) =>
|
|||||||
pool: resolveId(pool),
|
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
|
import UpdateXosanPacksModal from './update-xosan-packs-modal' // eslint-disable-line import/first
|
||||||
export const updateXosanPacks = pool =>
|
export const updateXosanPacks = pool =>
|
||||||
confirm({
|
confirm({
|
||||||
|
|||||||
@@ -277,6 +277,10 @@
|
|||||||
@extend .fa;
|
@extend .fa;
|
||||||
@extend .fa-thumbs-up;
|
@extend .fa-thumbs-up;
|
||||||
}
|
}
|
||||||
|
&-deploy {
|
||||||
|
@extend .fa;
|
||||||
|
@extend .fa-rocket;
|
||||||
|
}
|
||||||
|
|
||||||
// Backups
|
// Backups
|
||||||
&-backup {
|
&-backup {
|
||||||
@@ -886,6 +890,10 @@
|
|||||||
@extend .fa;
|
@extend .fa;
|
||||||
@extend .fa-database;
|
@extend .fa-database;
|
||||||
}
|
}
|
||||||
|
&-menu-hub {
|
||||||
|
@extend .fa;
|
||||||
|
@extend .fa-cubes;
|
||||||
|
}
|
||||||
// New VM
|
// New VM
|
||||||
&-new-vm {
|
&-new-vm {
|
||||||
&-infos {
|
&-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={3}>
|
||||||
|
<Resource {...data} />
|
||||||
|
</Col>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</Row>
|
||||||
|
</Container>
|
||||||
|
</Page>
|
||||||
|
),
|
||||||
|
])
|
||||||
60
packages/xo-web/src/xo-app/hub/resource-form.js
Normal file
60
packages/xo-web/src/xo-app/hub/resource-form.js
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import * as FormGrid from 'form-grid'
|
||||||
|
import _ from 'intl'
|
||||||
|
import decorate from 'apply-decorators'
|
||||||
|
import Icon from 'icon'
|
||||||
|
import React from 'react'
|
||||||
|
import Tooltip from 'tooltip'
|
||||||
|
import { Container } from 'grid'
|
||||||
|
import { SelectPool } from 'select-objects'
|
||||||
|
import { error } from 'notification'
|
||||||
|
import { injectState, provideState } from 'reaclette'
|
||||||
|
|
||||||
|
export default decorate([
|
||||||
|
provideState({
|
||||||
|
initialState: ({ multi }) => ({
|
||||||
|
pools: multi ? [] : undefined,
|
||||||
|
}),
|
||||||
|
effects: {
|
||||||
|
onChangePool(__, pools) {
|
||||||
|
const noDefaultSr = Array.isArray(pools)
|
||||||
|
? pools.some(pool => pool.default_SR === undefined)
|
||||||
|
: pools.default_SR === undefined
|
||||||
|
if (noDefaultSr) {
|
||||||
|
error(_('hubSrErrorTitle'), _('noDefaultSr'))
|
||||||
|
} else {
|
||||||
|
this.props.onChange({
|
||||||
|
pools,
|
||||||
|
pool: pools,
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
pools,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
injectState,
|
||||||
|
({ effects, install, multi, state, poolPredicate }) => (
|
||||||
|
<Container>
|
||||||
|
<FormGrid.Row>
|
||||||
|
<label>
|
||||||
|
{_('vmImportToPool')}
|
||||||
|
|
||||||
|
{install && (
|
||||||
|
<Tooltip content={_('hideInstalledPool')}>
|
||||||
|
<Icon icon='info' />
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</label>
|
||||||
|
<SelectPool
|
||||||
|
className='mb-1'
|
||||||
|
multi={multi}
|
||||||
|
onChange={effects.onChangePool}
|
||||||
|
predicate={poolPredicate}
|
||||||
|
required
|
||||||
|
value={state.pools}
|
||||||
|
/>
|
||||||
|
</FormGrid.Row>
|
||||||
|
</Container>
|
||||||
|
),
|
||||||
|
])
|
||||||
255
packages/xo-web/src/xo-app/hub/resource.js
Normal file
255
packages/xo-web/src/xo-app/hub/resource.js
Normal file
@@ -0,0 +1,255 @@
|
|||||||
|
import _ from 'intl'
|
||||||
|
import ActionButton from 'action-button'
|
||||||
|
import decorate from 'apply-decorators'
|
||||||
|
import Icon from 'icon'
|
||||||
|
import React from 'react'
|
||||||
|
import { Card, CardBlock, CardHeader } from 'card'
|
||||||
|
import { Col, Row } from 'grid'
|
||||||
|
import { alert, form } from 'modal'
|
||||||
|
import { connectStore, formatSize, getXoaPlan } from 'utils'
|
||||||
|
import { createGetObjectsOfType } from 'selectors'
|
||||||
|
import { downloadAndInstallResource, deleteTemplates } from 'xo'
|
||||||
|
import { error, success } from 'notification'
|
||||||
|
import { find, filter } from 'lodash'
|
||||||
|
import { injectState, provideState } from 'reaclette'
|
||||||
|
import { withRouter } from 'react-router'
|
||||||
|
|
||||||
|
import ResourceForm from './resource-form'
|
||||||
|
|
||||||
|
const subscribeAlert = () =>
|
||||||
|
alert(
|
||||||
|
_('hubResourceAlert'),
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
{_('considerSubscribe', {
|
||||||
|
link: 'https://xen-orchestra.com',
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default decorate([
|
||||||
|
withRouter,
|
||||||
|
connectStore(() => {
|
||||||
|
const getTemplates = createGetObjectsOfType('VM-template').sort()
|
||||||
|
const getPools = createGetObjectsOfType('pool')
|
||||||
|
return {
|
||||||
|
templates: getTemplates,
|
||||||
|
pools: getPools,
|
||||||
|
hubInstallingResources: state => state.hubInstallingResources,
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
provideState({
|
||||||
|
initialState: () => ({
|
||||||
|
selectedInstallPools: [],
|
||||||
|
}),
|
||||||
|
effects: {
|
||||||
|
async install() {
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
namespace,
|
||||||
|
markHubResourceAsInstalled,
|
||||||
|
markHubResourceAsInstalling,
|
||||||
|
version,
|
||||||
|
} = this.props
|
||||||
|
const { isTemplateInstalled } = this.state
|
||||||
|
if (getXoaPlan(+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>
|
||||||
|
),
|
||||||
|
])
|
||||||
@@ -27,6 +27,7 @@ import BackupNg from './backup-ng'
|
|||||||
import Dashboard from './dashboard'
|
import Dashboard from './dashboard'
|
||||||
import Home from './home'
|
import Home from './home'
|
||||||
import Host from './host'
|
import Host from './host'
|
||||||
|
import Hub from './hub'
|
||||||
import Jobs from './jobs'
|
import Jobs from './jobs'
|
||||||
import Menu from './menu'
|
import Menu from './menu'
|
||||||
import Modal, { alert, FormModal } from 'modal'
|
import Modal, { alert, FormModal } from 'modal'
|
||||||
@@ -93,6 +94,7 @@ const BODY_STYLE = {
|
|||||||
'vms/:id': Vm,
|
'vms/:id': Vm,
|
||||||
xoa: Xoa,
|
xoa: Xoa,
|
||||||
xosan: Xosan,
|
xosan: Xosan,
|
||||||
|
hub: Hub,
|
||||||
})
|
})
|
||||||
@connectStore(state => {
|
@connectStore(state => {
|
||||||
return {
|
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' },
|
isAdmin && { to: '/about', icon: 'menu-about', label: 'aboutPage' },
|
||||||
!noOperatablePools && {
|
!noOperatablePools && {
|
||||||
to: '/tasks',
|
to: '/tasks',
|
||||||
|
|||||||
@@ -280,7 +280,12 @@ export default class NewVm extends BaseComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this._reset()
|
this._reset(() => {
|
||||||
|
const { template } = this.props
|
||||||
|
if (template !== undefined) {
|
||||||
|
this._initTemplate(this.props.template)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
@@ -338,8 +343,9 @@ export default class NewVm extends BaseComponent {
|
|||||||
|
|
||||||
// Actions ---------------------------------------------------------------------
|
// Actions ---------------------------------------------------------------------
|
||||||
|
|
||||||
_reset = () => {
|
_reset = callback => {
|
||||||
this._replaceState({
|
this._replaceState(
|
||||||
|
{
|
||||||
bootAfterCreate: true,
|
bootAfterCreate: true,
|
||||||
CPUs: '',
|
CPUs: '',
|
||||||
cpuCap: '',
|
cpuCap: '',
|
||||||
@@ -359,7 +365,9 @@ export default class NewVm extends BaseComponent {
|
|||||||
seqStart: 1,
|
seqStart: 1,
|
||||||
share: false,
|
share: false,
|
||||||
tags: [],
|
tags: [],
|
||||||
})
|
},
|
||||||
|
callback
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
_selfCreate = () => {
|
_selfCreate = () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user