diff --git a/CHANGELOG.unreleased.md b/CHANGELOG.unreleased.md index e6fd7326260..3607079bef6 100644 --- a/CHANGELOG.unreleased.md +++ b/CHANGELOG.unreleased.md @@ -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 @@ -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 diff --git a/packages/xapi-explore-sr/src/index.js b/packages/xapi-explore-sr/src/index.js index 0664a720811..71d9eb008a3 100755 --- a/packages/xapi-explore-sr/src/index.js +++ b/packages/xapi-explore-sr/src/index.js @@ -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' @@ -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, readOnly: true, url, watchEvents: false, diff --git a/packages/xen-api/README.md b/packages/xen-api/README.md index 6c4ef643a09..00b6d2577b7 100644 --- a/packages/xen-api/README.md +++ b/packages/xen-api/README.md @@ -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. diff --git a/packages/xen-api/src/index.js b/packages/xen-api/src/index.js index 0a9a3122877..2cdaffd9c72 100644 --- a/packages/xen-api/src/index.js +++ b/packages/xen-api/src/index.js @@ -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 => { @@ -851,6 +852,7 @@ export class Xapi extends EventEmitter { rejectUnauthorized: !this._allowUnauthorized, }, url, + httpProxy: this._httpProxy, }) this._url = url } diff --git a/packages/xen-api/src/transports/json-rpc.js b/packages/xen-api/src/transports/json-rpc.js index 1bda4d36136..7703433f51f 100644 --- a/packages/xen-api/src/transports/json-rpc.js +++ b/packages/xen-api/src/transports/json-rpc.js @@ -1,4 +1,5 @@ import httpRequestPlus from 'http-request-plus' +import ProxyAgent from 'proxy-agent' import { format, parse } from 'json-rpc-protocol' import XapiError from '../_XapiError' @@ -6,7 +7,11 @@ 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, { @@ -17,6 +22,7 @@ export default ({ secureOptions, url }) => { 'Content-Type': 'application/json', }, path: '/jsonrpc', + agent, }) .readAll('utf8') .then( diff --git a/packages/xen-api/src/transports/xml-rpc-json.js b/packages/xen-api/src/transports/xml-rpc-json.js index 3e098dcad72..40d483e320e 100644 --- a/packages/xen-api/src/transports/xml-rpc-json.js +++ b/packages/xen-api/src/transports/xml-rpc-json.js @@ -1,5 +1,6 @@ import { createClient, createSecureClient } from 'xmlrpc' import { promisify } from 'promise-toolbox' +import ProxyAgent from 'proxy-agent' import XapiError from '../_XapiError' @@ -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, host: hostname, path: '/json', port, diff --git a/packages/xen-api/src/transports/xml-rpc.js b/packages/xen-api/src/transports/xml-rpc.js index 884e0267309..f312d66e9d7 100644 --- a/packages/xen-api/src/transports/xml-rpc.js +++ b/packages/xen-api/src/transports/xml-rpc.js @@ -1,5 +1,6 @@ import { createClient, createSecureClient } from 'xmlrpc' import { promisify } from 'promise-toolbox' +import ProxyAgent from 'proxy-agent' import XapiError from '../_XapiError' @@ -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, }) diff --git a/packages/xo-server/src/api/server.mjs b/packages/xo-server/src/api/server.mjs index a9c68dc9a7e..194f3240396 100644 --- a/packages/xo-server/src/api/server.mjs +++ b/packages/xo-server/src/api/server.mjs @@ -36,6 +36,10 @@ add.params = { optional: true, type: 'boolean', }, + httpProxy: { + optional: true, + type: 'string', + }, } // ------------------------------------------------------------------- @@ -104,6 +108,10 @@ set.params = { optional: true, type: 'boolean', }, + httpProxy: { + optional: true, + type: ['string', 'null'], + }, } // ------------------------------------------------------------------- diff --git a/packages/xo-server/src/xo-mixins/xen-servers.mjs b/packages/xo-server/src/xo-mixins/xen-servers.mjs index 2f7c8f76775..d61cdaeeb90 100644 --- a/packages/xo-server/src/xo-mixins/xen-servers.mjs +++ b/packages/xo-server/src/xo-mixins/xen-servers.mjs @@ -94,7 +94,7 @@ 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 @@ -102,6 +102,7 @@ export default class { allowUnauthorized, enabled: true, host, + httpProxy, label: label || undefined, password, readOnly, @@ -119,11 +120,18 @@ export default class { } } - async updateXenServer(id, { allowUnauthorized, enabled, error, host, label, password, readOnly, username }) { + async updateXenServer( + 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') @@ -153,6 +161,10 @@ export default class { server.set('allowUnauthorized', allowUnauthorized) } + if (httpProxy !== undefined) { + // 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) } @@ -288,6 +300,7 @@ export default class { readOnly: server.readOnly, ...config.get('xapiOptions'), + httpProxy: server.httpProxy, guessVhdSizeOnImport: config.get('guessVhdSizeOnImport'), auth: { diff --git a/packages/xo-web/src/common/intl/messages.js b/packages/xo-web/src/common/intl/messages.js index 7589102c0c2..178a3a0a47e 100644 --- a/packages/xo-web/src/common/intl/messages.js +++ b/packages/xo-web/src/common/intl/messages.js @@ -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', diff --git a/packages/xo-web/src/common/xo/index.js b/packages/xo-web/src/common/xo/index.js index 7d61cc100f7..2acd1ab9cd6 100644 --- a/packages/xo-web/src/common/xo/index.js +++ b/packages/xo-web/src/common/xo/index.js @@ -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, diff --git a/packages/xo-web/src/xo-app/settings/servers/index.js b/packages/xo-web/src/xo-app/settings/servers/index.js index b3a4bb35a0b..651cebe6e62 100644 --- a/packages/xo-web/src/xo-app/settings/servers/index.js +++ b/packages/xo-web/src/xo-app/settings/servers/index.js @@ -132,6 +132,18 @@ const COLUMNS = [ itemRenderer: ({ poolId }) => poolId !== undefined && , name: _('pool'), }, + { + itemRenderer: (server, formatMessage) => ( + editServer(server, { httpProxy: httpProxy || null })} + placeholder={formatMessage(messages.serverHttpProxyPlaceHolder)} + /> + ), + name: _('serverHttpProxy'), + sortCriteria: _ => _.httpProxy, + }, ] const INDIVIDUAL_ACTIONS = [ { @@ -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: '', @@ -227,6 +239,15 @@ export default class Servers extends Component { {' '} +
+ +
{' '} {_('serverConnect')}