Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(xen-api): add an HTTP proxy support #5958

Merged
merged 9 commits into from
Oct 27, 2021
3 changes: 3 additions & 0 deletions CHANGELOG.unreleased.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
- [Jobs] Ability to copy a job ID (PR [#5951](https://github.com/vatesfr/xen-orchestra/pull/5951))
- [Host/advanced] Add button to enable/disable the host (PR [#5952](https://github.com/vatesfr/xen-orchestra/pull/5952))
- [VM/export] Ability to copy the export URL (PR [#5948](https://github.com/vatesfr/xen-orchestra/pull/5948))
- [Servers] Ability to use an HTTP proxy between XO and a server

### Bug fixes

Expand Down Expand Up @@ -42,8 +43,10 @@

- xo-server-netbox patch
- vhd-lib minor
- xen-api minor
- @xen-orchestra/backup minor
- @xen-orchestra/proxy minor
- vhd-cli minor
- xapi-explore-sr minor
- xo-server patch
- xo-web minor
3 changes: 3 additions & 0 deletions packages/xapi-explore-sr/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import archy from 'archy'
import chalk from 'chalk'
import execPromise from 'exec-promise'
import firstDefined from '@xen-orchestra/defined'
import humanFormat from 'human-format'
import pw from 'pw'
import { createClient } from 'xen-api'
Expand Down Expand Up @@ -69,11 +70,13 @@ execPromise(async args => {
url = required('Host URL'),
user = required('Host user'),
password = await askPassword('Host password'),
httpProxy = firstDefined(process.env.http_proxy, process.env.HTTP_PROXY),
] = args

const xapi = createClient({
allowUnauthorized: true,
auth: { user, password },
httpProxy,
julien-f marked this conversation as resolved.
Show resolved Hide resolved
readOnly: true,
url,
watchEvents: false,
Expand Down
1 change: 1 addition & 0 deletions packages/xen-api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ Options:
- `auth`: credentials used to sign in (can also be specified in the URL)
- `readOnly = false`: if true, no methods with side-effects can be called
- `callTimeout`: number of milliseconds after which a call is considered failed (can also be a map of timeouts by methods)
- `httpProxy`: URL of the HTTP/HTTPS proxy used to reach the host, can include credentials

```js
// Force connection.
Expand Down
2 changes: 2 additions & 0 deletions packages/xen-api/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ export class Xapi extends EventEmitter {
}

this._allowUnauthorized = opts.allowUnauthorized
this._httpProxy = opts.httpProxy
this._setUrl(url)

this._connected = new Promise(resolve => {
Expand Down Expand Up @@ -851,6 +852,7 @@ export class Xapi extends EventEmitter {
rejectUnauthorized: !this._allowUnauthorized,
},
url,
httpProxy: this._httpProxy,
})
this._url = url
}
Expand Down
8 changes: 7 additions & 1 deletion packages/xen-api/src/transports/json-rpc.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import httpRequestPlus from 'http-request-plus'
import ProxyAgent from 'proxy-agent'
import { format, parse } from 'json-rpc-protocol'

import XapiError from '../_XapiError'

import UnsupportedTransport from './_UnsupportedTransport'

// https://github.com/xenserver/xenadmin/blob/0df39a9d83cd82713f32d24704852a0fd57b8a64/XenModel/XenAPI/Session.cs#L403-L433
export default ({ secureOptions, url }) => {
export default ({ secureOptions, url, httpProxy }) => {
let agent
if (httpProxy !== undefined) {
agent = new ProxyAgent(httpProxy)
}
return (method, args) =>
httpRequestPlus
.post(url, {
Expand All @@ -17,6 +22,7 @@ export default ({ secureOptions, url }) => {
'Content-Type': 'application/json',
},
path: '/jsonrpc',
agent,
})
.readAll('utf8')
.then(
Expand Down
8 changes: 7 additions & 1 deletion packages/xen-api/src/transports/xml-rpc-json.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createClient, createSecureClient } from 'xmlrpc'
import { promisify } from 'promise-toolbox'
import ProxyAgent from 'proxy-agent'

import XapiError from '../_XapiError'

Expand Down Expand Up @@ -70,10 +71,15 @@ const parseResult = result => {
throw new UnsupportedTransport()
}

export default ({ secureOptions, url: { hostname, port, protocol } }) => {
export default ({ secureOptions, url: { hostname, port, protocol }, httpProxy }) => {
const secure = protocol === 'https:'
let agent
if (httpProxy !== undefined) {
agent = new ProxyAgent(httpProxy)
}
const client = (secure ? createSecureClient : createClient)({
...(secure ? secureOptions : undefined),
agent,
julien-f marked this conversation as resolved.
Show resolved Hide resolved
host: hostname,
path: '/json',
port,
Expand Down
8 changes: 7 additions & 1 deletion packages/xen-api/src/transports/xml-rpc.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createClient, createSecureClient } from 'xmlrpc'
import { promisify } from 'promise-toolbox'
import ProxyAgent from 'proxy-agent'

import XapiError from '../_XapiError'

Expand Down Expand Up @@ -30,10 +31,15 @@ const parseResult = result => {
return result.Value
}

export default ({ secureOptions, url: { hostname, port, protocol } }) => {
export default ({ secureOptions, url: { hostname, port, protocol, httpProxy } }) => {
const secure = protocol === 'https:'
let agent
if (httpProxy !== undefined) {
agent = new ProxyAgent(httpProxy)
}
const client = (secure ? createSecureClient : createClient)({
...(secure ? secureOptions : undefined),
agent,
host: hostname,
port,
})
Expand Down
8 changes: 8 additions & 0 deletions packages/xo-server/src/api/server.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ add.params = {
optional: true,
type: 'boolean',
},
httpProxy: {
optional: true,
type: 'string',
},
}

// -------------------------------------------------------------------
Expand Down Expand Up @@ -104,6 +108,10 @@ set.params = {
optional: true,
type: 'boolean',
},
httpProxy: {
optional: true,
type: ['string', 'null'],
},
}

// -------------------------------------------------------------------
Expand Down
19 changes: 16 additions & 3 deletions packages/xo-server/src/xo-mixins/xen-servers.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -94,14 +94,15 @@ export default class {
// TODO: disconnect servers on stop.
}

async registerXenServer({ allowUnauthorized = false, host, label, password, readOnly = false, username }) {
async registerXenServer({ allowUnauthorized = false, host, label, password, readOnly = false, username, httpProxy }) {
// FIXME: We are storing passwords which is bad!
// Could we use tokens instead?
// TODO: use plain objects
const server = await this._servers.create({
allowUnauthorized,
enabled: true,
host,
httpProxy,
label: label || undefined,
password,
readOnly,
Expand All @@ -119,11 +120,18 @@ export default class {
}
}

async updateXenServer(id, { allowUnauthorized, enabled, error, host, label, password, readOnly, username }) {
async updateXenServer(
julien-f marked this conversation as resolved.
Show resolved Hide resolved
id,
{ allowUnauthorized, enabled, error, host, label, password, readOnly, username, httpProxy }
) {
const server = await this._getXenServer(id)
const xapi = this._xapis[id]
const requireDisconnected =
allowUnauthorized !== undefined || host !== undefined || password !== undefined || username !== undefined
allowUnauthorized !== undefined ||
host !== undefined ||
password !== undefined ||
username !== undefined ||
httpProxy !== undefined

if (requireDisconnected && xapi !== undefined && xapi.status !== 'disconnected') {
throw new Error('this entry require disconnecting the server to update it')
Expand Down Expand Up @@ -153,6 +161,10 @@ export default class {
server.set('allowUnauthorized', allowUnauthorized)
}

if (httpProxy !== undefined) {
julien-f marked this conversation as resolved.
Show resolved Hide resolved
// if value is null, pass undefined to the model , so it will delete this optionnal property from the Server object
server.set('httpProxy', httpProxy === null ? undefined : httpProxy)
}
await this._servers.update(server)
}

Expand Down Expand Up @@ -288,6 +300,7 @@ export default class {
readOnly: server.readOnly,

...config.get('xapiOptions'),
httpProxy: server.httpProxy,
guessVhdSizeOnImport: config.get('guessVhdSizeOnImport'),

auth: {
Expand Down
2 changes: 2 additions & 0 deletions packages/xo-web/src/common/intl/messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -1826,6 +1826,8 @@ const messages = {
serverEnabled: 'Enabled',
serverDisabled: 'Disabled',
serverDisable: 'Disable server',
serverHttpProxy: ' HTTP proxy URL',
serverHttpProxyPlaceHolder: ' HTTP proxy URL',

// ----- Copy VM -----
copyVm: 'Copy VM',
Expand Down
3 changes: 2 additions & 1 deletion packages/xo-web/src/common/xo/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -560,10 +560,11 @@ export const exportConfig = () =>

// Server ------------------------------------------------------------

export const addServer = (host, username, password, label, allowUnauthorized) =>
export const addServer = (host, username, password, label, allowUnauthorized, httpProxy) =>
_call('server.add', {
allowUnauthorized,
host,
httpProxy,
label,
password,
username,
Expand Down
27 changes: 24 additions & 3 deletions packages/xo-web/src/xo-app/settings/servers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,18 @@ const COLUMNS = [
itemRenderer: ({ poolId }) => poolId !== undefined && <Pool id={poolId} link />,
name: _('pool'),
},
{
itemRenderer: (server, formatMessage) => (
<Text
value={server.httpProxy || ''}
// force a null value for falsish value to ensure the value is removed from object if set to ''
onChange={httpProxy => editServer(server, { httpProxy: httpProxy || null })}
placeholder={formatMessage(messages.serverHttpProxyPlaceHolder)}
/>
),
name: _('serverHttpProxy'),
sortCriteria: _ => _.httpProxy,
},
]
const INDIVIDUAL_ACTIONS = [
{
Expand All @@ -152,13 +164,13 @@ export default class Servers extends Component {
}

_addServer = async () => {
const { label, host, password, username, allowUnauthorized } = this.state

await addServer(host, username, password, label, allowUnauthorized)
const { label, host, password, username, allowUnauthorized, httpProxy } = this.state
await addServer(host, username, password, label, allowUnauthorized, httpProxy)

this.setState({
allowUnauthorized: false,
host: '',
httpProxy: '',
label: '',
password: '',
username: '',
Expand Down Expand Up @@ -227,6 +239,15 @@ export default class Servers extends Component {
<Toggle onChange={this.linkState('allowUnauthorized')} value={state.allowUnauthorized} />
</Tooltip>
</div>{' '}
<div className='form-group'>
<input
className='form-control'
onChange={this.linkState('httpProxy')}
placeholder={formatMessage(messages.serverHttpProxy)}
type='text'
value={state.httpProxy || ''}
/>
</div>{' '}
<ActionButton btnStyle='primary' form='form-add-server' handler={this._addServer} icon='save'>
{_('serverConnect')}
</ActionButton>
Expand Down