feat(proxy): ability to set the deployed VM network configuration (#4810)

This commit is contained in:
badrAZ 2020-02-20 12:03:25 +01:00 committed by GitHub
parent 692d5be166
commit 18685d061a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 271 additions and 41 deletions

View File

@ -9,6 +9,7 @@
- [New VM] Ability to set VM max vCPUS [#4703](https://github.com/vatesfr/xen-orchestra/issues/4703) (PR [#4729](https://github.com/vatesfr/xen-orchestra/pull/4729))
- [SDN Controller] Automatically handle new, connected and disconnected servers (PR [#4677](https://github.com/vatesfr/xen-orchestra/pull/4677))
- [Proxy] Support network configuration for the deployed proxy (PR [#4810](https://github.com/vatesfr/xen-orchestra/pull/4810))
### Bug fixes

View File

@ -137,7 +137,7 @@ proxyName = 'Proxy {date}'
vmName = 'XOA Proxy {date}'
# The duration for which we can wait for the VM networks to be defined
vmNetworksTimeout = '1 min'
vmNetworksTimeout = '5 min'
vmTag = 'XOA Proxy'

View File

@ -96,8 +96,8 @@ update.params = {
},
}
export function deploy({ sr }) {
return this.deployProxy(sr._xapiId)
export function deploy({ network, sr }) {
return this.deployProxy(sr._xapiId, { network })
}
deploy.permission = 'admin'
@ -105,6 +105,24 @@ deploy.params = {
sr: {
type: 'string',
},
network: {
type: 'object',
optional: true,
properties: {
ip: {
type: 'string',
},
netmask: {
type: 'string',
},
gateway: {
type: 'string',
},
dns: {
type: 'string',
},
},
},
}
deploy.resolve = {
sr: ['sr', 'SR', 'administrate'],

View File

@ -8,7 +8,7 @@ import { compileTemplate } from '@xen-orchestra/template'
import { format, parse } from 'json-rpc-peer'
import { noSuchObject } from 'xo-common/api-errors'
import { NULL_REF } from 'xen-api'
import { omit } from 'lodash'
import { mapValues, omit } from 'lodash'
import { timeout } from 'promise-toolbox'
import Collection from '../collection/redis'
@ -133,7 +133,7 @@ export default class Proxy {
}
@defer
async deployProxy($defer, srId) {
async deployProxy($defer, srId, { network } = {}) {
const app = this._app
const xoProxyConf = this._xoProxyConf
@ -162,29 +162,34 @@ export default class Proxy {
app.getApplianceRegistration(),
])
const date = new Date()
const xenstoreData = {
'vm-data/system-account-xoa-password': password,
'vm-data/xo-proxy-authenticationToken': JSON.stringify(
proxyAuthenticationToken
),
'vm-data/xoa-updater-credentials': JSON.stringify({
email,
registrationToken,
}),
'vm-data/xoa-updater-channel': JSON.stringify(xoProxyConf.channel),
}
if (network !== undefined) {
xenstoreData['vm-data/ip'] = network.ip
xenstoreData['vm-data/gateway'] = network.gateway
xenstoreData['vm-data/netmask'] = network.netmask
xenstoreData['vm-data/dns'] = network.dns
}
await Promise.all([
vm.add_tags(xoProxyConf.vmTag),
vm.set_name_label(this._generateDefaultVmName(date)),
vm.update_xenstore_data({
'vm-data/system-account-xoa-password': password,
'vm-data/xo-proxy-authenticationToken': JSON.stringify(
proxyAuthenticationToken
),
'vm-data/xoa-updater-credentials': JSON.stringify({
email,
registrationToken,
}),
'vm-data/xoa-updater-channel': JSON.stringify(xoProxyConf.channel),
}),
vm.update_xenstore_data(xenstoreData),
])
await xapi.startVm(vm.$id)
await vm.update_xenstore_data({
'vm-data/system-account-xoa-password': null,
'vm-data/xo-proxy-authenticationToken': null,
'vm-data/xoa-updater-credentials': null,
})
await vm.update_xenstore_data(
mapValues(omit(xenstoreData, 'vm-data/xoa-updater-channel'), _ => null)
)
// ensure appliance has an IP address
const vmNetworksTimeout = parseDuration(xoProxyConf.vmNetworksTimeout)
@ -193,9 +198,10 @@ export default class Proxy {
vmNetworksTimeout
)
await timeout.call(
xapi._waitObjectState(
vm.guest_metrics,
guest_metrics => guest_metrics.networks['0/ip'] !== undefined
xapi._waitObjectState(vm.guest_metrics, guest_metrics =>
network === undefined
? guest_metrics.networks['0/ip'] !== undefined
: guest_metrics.networks['0/ip'] === network.ip
),
vmNetworksTimeout
)

View File

@ -59,6 +59,10 @@ const messages = {
name: 'Name',
address: 'Address',
vm: 'VM',
destinationSR: 'Destination SR',
dhcp: 'DHCP',
ip: 'IP',
static: 'Static',
// ----- Modals -----
alertOk: 'OK',
@ -2271,6 +2275,8 @@ const messages = {
proxyTestSuccessMessage: 'The proxy appears to work correctly',
proxyLinkedRemotes: 'Click to see linked remotes',
proxyLinkedBackups: 'Click to see linked backups',
proxyNetworkDnsPlaceHolder: 'Default to: {dns}',
proxyNetworkNetmaskPlaceHolder: 'Default to: {netmask}',
// ----- Utils -----
secondsFormat: '{seconds, plural, one {# second} other {# seconds}}',

View File

@ -3061,8 +3061,8 @@ export const getApplianceInfo = () => _call('xoa.getApplianceInfo')
// Proxy --------------------------------------------------------------------
export const deployProxyAppliance = sr =>
_call('proxy.deploy', { sr: resolveId(sr) })::tap(
export const deployProxyAppliance = (sr, props) =>
_call('proxy.deploy', { sr: resolveId(sr), ...props })::tap(
subscribeProxies.forceRefresh
)

View File

@ -0,0 +1,212 @@
import _, { messages } from 'intl'
import decorate from 'apply-decorators'
import Icon from 'icon'
import React from 'react'
import SingleLineRow from 'single-line-row'
import { Col, Container } from 'grid'
import { deployProxyAppliance } from 'xo'
import { form } from 'modal'
import { generateId } from 'reaclette-utils'
import { injectIntl } from 'react-intl'
import { provideState, injectState } from 'reaclette'
import { Select } from 'form'
import { SelectSr } from 'select-objects'
const Label = ({ children, ...props }) => (
<label {...props} style={{ cursor: 'pointer' }}>
<strong>{children}</strong>
</label>
)
const NETWORK_MODE_OPTIONS = [
{
label: _('dhcp'),
value: 'dhcp',
},
{
label: _('static'),
value: 'static',
},
]
const DEFAULT_DNS = '8.8.8.8'
const DEFAULT_NETMASK = '255.255.255.0'
const Modal = decorate([
provideState({
effects: {
onSrChange(_, sr) {
this.props.onChange({
...this.props.value,
sr,
})
},
onNetworkModeChange(_, networkMode) {
this.props.onChange({
...this.props.value,
networkMode,
})
},
onInputChange(_, { target: { name, value } }) {
this.props.onChange({
...this.props.value,
[name]: value,
})
},
},
computed: {
idDnsInput: generateId,
idGatewayInput: generateId,
idIpInput: generateId,
idNetmaskInput: generateId,
idSelectNetworkMode: generateId,
idSelectSr: generateId,
isStaticMode: (state, { value }) => value.networkMode === 'static',
},
}),
injectState,
injectIntl,
({ effects, state, value, intl: { formatMessage } }) => (
<Container>
<SingleLineRow>
<Col mediumSize={4}>
<Label htmlFor={state.idSelectSr}>{_('destinationSR')}</Label>
</Col>
<Col mediumSize={8}>
<SelectSr
id={state.idSelectSr}
onChange={effects.onSrChange}
required
value={value.sr}
/>
</Col>
</SingleLineRow>
<SingleLineRow className='mt-1'>
<Col mediumSize={4}>
<Label htmlFor={state.idSelectNetworkMode}>{_('network')}</Label>
</Col>
<Col mediumSize={8}>
<Select
id={state.idSelectNetworkMode}
onChange={effects.onNetworkModeChange}
options={NETWORK_MODE_OPTIONS}
required
simpleValue
value={value.networkMode}
/>
</Col>
</SingleLineRow>
{state.isStaticMode && (
<div>
<SingleLineRow className='mt-1'>
<Col mediumSize={4}>
<Label htmlFor={state.idIpInput}>{_('ip')}</Label>
</Col>
<Col mediumSize={8}>
<input
className='form-control'
id={state.idIpInput}
name='ip'
onChange={effects.onInputChange}
pattern='[^\s]+'
required={state.isStaticMode}
value={value.ip}
/>
</Col>
</SingleLineRow>
<SingleLineRow className='mt-1'>
<Col mediumSize={4}>
<Label htmlFor={state.idNetmaskInput}>{_('netmask')}</Label>
</Col>
<Col mediumSize={8}>
<input
className='form-control'
id={state.idNetmaskInput}
name='netmask'
onChange={effects.onInputChange}
placeholder={formatMessage(
messages.proxyNetworkNetmaskPlaceHolder,
{
netmask: DEFAULT_NETMASK,
}
)}
value={value.netmask}
/>
</Col>
</SingleLineRow>
<SingleLineRow className='mt-1'>
<Col mediumSize={4}>
<Label htmlFor={state.idGatewayInput}>{_('gateway')}</Label>
</Col>
<Col mediumSize={8}>
<input
className='form-control'
id={state.idGatewayInput}
name='gateway'
onChange={effects.onInputChange}
pattern='[^\s]+'
required={state.isStaticMode}
value={value.gateway}
/>
</Col>
</SingleLineRow>
<SingleLineRow className='mt-1'>
<Col mediumSize={4}>
<Label htmlFor={state.idDnsInput}>{_('dns')}</Label>
</Col>
<Col mediumSize={8}>
<input
className='form-control'
id={state.idDnsInput}
name='dns'
onChange={effects.onInputChange}
placeholder={formatMessage(
messages.proxyNetworkDnsPlaceHolder,
{
dns: DEFAULT_DNS,
}
)}
value={value.dns}
/>
</Col>
</SingleLineRow>
</div>
)}
</Container>
),
])
const deployProxy = () =>
form({
defaultValue: {
dns: '',
gateway: '',
ip: '',
netmask: '',
networkMode: 'dhcp',
},
render: props => <Modal {...props} />,
header: (
<span>
<Icon icon='proxy' /> {_('deployProxy')}
</span>
),
}).then(({ sr, networkMode, ip, netmask, gateway, dns }) =>
deployProxyAppliance(
sr,
networkMode === 'static'
? {
network: {
dns: (dns = dns.trim()) === '' ? DEFAULT_DNS : dns,
gateway,
ip,
netmask:
(netmask = netmask.trim()) === '' ? DEFAULT_NETMASK : netmask,
},
}
: undefined
)
)
export { deployProxy as default }

View File

@ -8,14 +8,11 @@ import NoObjects from 'no-objects'
import React from 'react'
import SortedTable from 'sorted-table'
import { adminOnly } from 'utils'
import { form } from 'modal'
import { SelectSr } from 'select-objects'
import { Text, XoSelect } from 'editable'
import { Vm } from 'render-xo-item'
import { withRouter } from 'react-router'
import {
checkProxyHealth,
deployProxyAppliance,
destroyProxyAppliances,
forgetProxyAppliances,
editProxyAppliance,
@ -25,17 +22,7 @@ import {
import Page from '../page'
const _deployProxy = () =>
form({
render: ({ onChange, value }) => (
<SelectSr onChange={onChange} value={value} required />
),
header: (
<span>
<Icon icon='proxy' /> {_('deployProxy')}
</span>
),
}).then(deployProxyAppliance)
import deployProxy from './deploy-proxy'
const _editProxy = (value, { name, proxy }) =>
editProxyAppliance(proxy, { [name]: value })
@ -172,7 +159,7 @@ export default decorate([
<div className='mt-1 mb-1'>
<ActionButton
btnStyle='success'
handler={_deployProxy}
handler={deployProxy}
icon='proxy'
size='large'
>