Compare commits

...

12 Commits

Author SHA1 Message Date
badrAZ
738c55bad0 feat(xo-server-backup-reports): 0.16.2 2019-06-27 14:08:11 +02:00
badrAZ
4b09bc85f5 feat(xo-server-auth-saml): 0.6.0 2019-06-27 14:07:44 +02:00
badrAZ
5bc67d3570 feat(xo-server-sdn-controller): 0.1.0 2019-06-27 14:07:23 +02:00
badrAZ
f7ae6222b7 feat(xen-api): 0.26.0 2019-06-27 14:06:14 +02:00
badrAZ
1e50dab093 feat(xo-server-auth-ldap): 0.6.5 2019-06-27 14:05:20 +02:00
Julien Fontanet
d1935bf778 feat(xo-server-auth-ldap/cli): display the whole entry on success
This can help while debugging filters.
2019-06-27 11:31:24 +02:00
Julien Fontanet
70a346d11e feat(CHANGELOG.unreleased): add xen-api v0.26.0 2019-06-27 11:12:23 +02:00
Julien Fontanet
fd39a2063d fix(xen-api): refresh pool on new session 2019-06-27 11:08:45 +02:00
Julien Fontanet
682512fffe fix(xen-api/events): full refresh on new session 2019-06-27 11:08:45 +02:00
HamadaBrest
b13f91ec8d fix(xo-web/new-vm): tooltips not showing up on disabled inputs (#4309)
Fixes #4304

mouseenter event not supported on disabled inputs in some browsers
https://devdocs.io/dom_events/mouseenter
2019-06-27 10:24:47 +02:00
BenjiReis
a140fc09ac feat(xo-server-sdn-controller): new plugin wich enables private pool-wide networkks (#4269)
See xcp-ng/xcp#175
2019-06-27 09:49:58 +02:00
badrAZ
f403a7e753 fix(xo-web): fix fibre channel paths display (#4303)
Fixes #4291
2019-06-26 16:42:56 +02:00
25 changed files with 1698 additions and 125 deletions

View File

@@ -16,7 +16,7 @@
},
"dependencies": {
"golike-defer": "^0.4.1",
"xen-api": "^0.25.2"
"xen-api": "^0.26.0"
},
"scripts": {
"postversion": "npm publish"

View File

@@ -12,14 +12,20 @@
- [Host] Display warning when host's time differs too much from XOA's time [#4113](https://github.com/vatesfr/xen-orchestra/issues/4113) (PR [#4173](https://github.com/vatesfr/xen-orchestra/pull/4173))
- [Host/storages, SR/hosts] Display PBD details [#4264](https://github.com/vatesfr/xen-orchestra/issues/4161) (PR [#4268](https://github.com/vatesfr/xen-orchestra/pull/4284))
- [VM/network] Display and set bandwidth rate-limit of a VIF [#4215](https://github.com/vatesfr/xen-orchestra/issues/4215) (PR [#4293](https://github.com/vatesfr/xen-orchestra/pull/4293))
- [SDN Controller] New plugin which enables creating pool-wide private networks [xcp-ng/xcp#175](https://github.com/xcp-ng/xcp/issues/175) (PR [#4269](https://github.com/vatesfr/xen-orchestra/pull/4269))
### Bug fixes
- [Metadata backup] Missing XAPIs should trigger a failure job [#4281](https://github.com/vatesfr/xen-orchestra/issues/4281) (PR [#4283](https://github.com/vatesfr/xen-orchestra/pull/4283))
- [Host/advanced] Fix host CPU hyperthreading detection [#4262](https://github.com/vatesfr/xen-orchestra/issues/4262) (PR [#4285](https://github.com/vatesfr/xen-orchestra/pull/4285))
- [iSCSI] Fix fibre channel paths display [#4291](https://github.com/vatesfr/xen-orchestra/issues/4291) (PR [#4303](https://github.com/vatesfr/xen-orchestra/pull/4303))
- [New VM] Fix tooltips not displayed on disabled elements in some browsers (e.g. Google Chrome) [#4304](https://github.com/vatesfr/xen-orchestra/issues/4304) (PR [#4309](https://github.com/vatesfr/xen-orchestra/pull/4309))
### Released packages
- xo-server-auth-ldap v0.6.5
- xen-api v0.26.0
- xo-server-sdn-controller v0.1
- xo-server-auth-saml v0.6.0
- xo-server-backup-reports v0.16.2
- xo-server v5.44.0

View File

@@ -41,7 +41,7 @@
"human-format": "^0.10.0",
"lodash": "^4.17.4",
"pw": "^0.0.4",
"xen-api": "^0.25.2"
"xen-api": "^0.26.0"
},
"devDependencies": {
"@babel/cli": "^7.1.5",

View File

@@ -1,6 +1,6 @@
{
"name": "xen-api",
"version": "0.25.2",
"version": "0.26.0",
"license": "ISC",
"description": "Connector to the Xen API",
"keywords": [

View File

@@ -168,22 +168,6 @@ export class Xapi extends EventEmitter {
try {
await this._sessionOpen()
// Uses introspection to list available types.
const types = (this._types = (await this._interruptOnDisconnect(
this._call('system.listMethods')
))
.filter(isGetAllRecordsMethod)
.map(method => method.slice(0, method.indexOf('.'))))
this._lcToTypes = { __proto__: null }
types.forEach(type => {
const lcType = type.toLowerCase()
if (lcType !== type) {
this._lcToTypes[lcType] = type
}
})
this._pool = (await this.getAllRecords('pool'))[0]
debug('%s: connected', this._humanId)
this._status = CONNECTED
this._resolveConnected()
@@ -739,6 +723,28 @@ export class Xapi extends EventEmitter {
},
}
)
const oldPoolRef = this._pool?.$ref
this._pool = (await this.getAllRecords('pool'))[0]
// if the pool ref has changed, it means that the XAPI has been restarted or
// it's not the same XAPI, we need to refetch the available types and reset
// the event loop in that case
if (this._pool.$ref !== oldPoolRef) {
// Uses introspection to list available types.
const types = (this._types = (await this._interruptOnDisconnect(
this._call('system.listMethods')
))
.filter(isGetAllRecordsMethod)
.map(method => method.slice(0, method.indexOf('.'))))
this._lcToTypes = { __proto__: null }
types.forEach(type => {
const lcType = type.toLowerCase()
if (lcType !== type) {
this._lcToTypes[lcType] = type
}
})
}
}
_setUrl(url) {
@@ -936,9 +942,12 @@ export class Xapi extends EventEmitter {
let result
try {
result = await this._sessionCall(
// don't use _sessionCall because a session failure should break the
// loop and trigger a complete refetch
result = await this._call(
'event.from',
[
this._sessionId,
types,
fromToken,
EVENT_TIMEOUT + 0.1, // must be float for XML-RPC transport
@@ -946,7 +955,8 @@ export class Xapi extends EventEmitter {
EVENT_TIMEOUT * 1e3 * 1.1
)
} catch (error) {
if (error?.code === 'EVENTS_LOST') {
const code = error?.code
if (code === 'EVENTS_LOST' || code === 'SESSION_INVALID') {
// eslint-disable-next-line no-labels
continue mainLoop
}

View File

@@ -1,6 +1,6 @@
{
"name": "xo-server-auth-ldap",
"version": "0.6.4",
"version": "0.6.5",
"license": "AGPL-3.0",
"description": "LDAP authentication plugin for XO-Server",
"keywords": [

View File

@@ -234,6 +234,7 @@ class AuthLdap {
entry.objectName
} => ${username} authenticated`
)
logger(JSON.stringify(entry, null, 2))
return { username }
} catch (error) {
logger(`failed to bind as ${entry.objectName}: ${error.message}`)

View File

@@ -1,6 +1,6 @@
{
"name": "xo-server-auth-saml",
"version": "0.5.3",
"version": "0.6.0",
"license": "AGPL-3.0",
"description": "SAML authentication plugin for XO-Server",
"keywords": [

View File

@@ -1,6 +1,6 @@
{
"name": "xo-server-backup-reports",
"version": "0.16.1",
"version": "0.16.2",
"license": "AGPL-3.0",
"description": "Backup reports plugin for XO-Server",
"keywords": [

View File

@@ -0,0 +1,3 @@
module.exports = require('../../@xen-orchestra/babel-config')(
require('./package.json')
)

View File

@@ -0,0 +1,43 @@
# xo-server-sdn-controller [![Build Status](https://travis-ci.org/vatesfr/xen-orchestra.png?branch=master)](https://travis-ci.org/vatesfr/xen-orchestra)
XO Server plugin that allows the creation of pool-wide private networks.
## Install
For installing XO and the plugins from the sources, please take a look at [the documentation](https://xen-orchestra.com/docs/from_the_sources.html).
## Usage
### Network creation
In the network creation view, select a `pool` and `Private network`.
Create the network.
Choice is offer between `GRE` and `VxLAN`, if `VxLAN` is chosen, then the port 4789 must be open for UDP traffic.
The following line needs to be added, if not already present, in `/etc/sysconfig/iptables` of all the hosts where `VxLAN` is wanted:
`-A xapi-INPUT -p udp -m conntrack --ctstate NEW -m udp --dport 4789 -j ACCEPT`
### Configuration
Like all other xo-server plugins, it can be configured directly via
the web interface, see [the plugin documentation](https://xen-orchestra.com/docs/plugins.html).
The plugin's configuration contains:
- `cert-dir`: A path where to find the certificates to create SSL connections with the hosts.
If none is provided, the plugin will create its own self-signed certificates.
- `override-certs:` Whether or not to uninstall an already existing SDN controller CA certificate in order to replace it by the plugin's one.
## Contributions
Contributions are *very* welcomed, either on the documentation or on
the code.
You may:
- report any [issue](https://github.com/vatesfr/xen-orchestra/issues)
you've encountered;
- fork and create a pull request.
## License
AGPL3 © [Vates SAS](http://vates.fr)

View File

@@ -0,0 +1,35 @@
{
"name": "xo-server-sdn-controller",
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/packages/xo-server-sdn-controller",
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
"repository": {
"directory": "packages/xo-server-sdn-controller",
"type": "git",
"url": "https://github.com/vatesfr/xen-orchestra.git"
},
"main": "./dist",
"scripts": {
"build": "cross-env NODE_ENV=production babel --source-maps --out-dir=dist/ src/",
"dev": "cross-env NODE_ENV=development babel --watch --source-maps --out-dir=dist/ src/",
"prebuild": "rimraf dist/",
"predev": "yarn run prebuild",
"prepublishOnly": "yarn run build"
},
"version": "0.1.0",
"engines": {
"node": ">=6"
},
"devDependencies": {
"@babel/cli": "^7.4.4",
"@babel/core": "^7.4.4",
"@babel/preset-env": "^7.4.4",
"cross-env": "^5.2.0"
},
"dependencies": {
"@xen-orchestra/log": "^0.1.4",
"lodash": "^4.17.11",
"node-openssl-cert": "^0.0.81",
"promise-toolbox": "^0.13.0"
},
"private": true
}

View File

@@ -0,0 +1,830 @@
import assert from 'assert'
import createLogger from '@xen-orchestra/log'
import NodeOpenssl from 'node-openssl-cert'
import { access, constants, readFile, writeFile } from 'fs'
import { EventEmitter } from 'events'
import { filter, find, forOwn, map } from 'lodash'
import { fromCallback, fromEvent } from 'promise-toolbox'
import { join } from 'path'
import { OvsdbClient } from './ovsdb-client'
// =============================================================================
const log = createLogger('xo:xo-server:sdn-controller')
const PROTOCOL = 'pssl'
const CA_CERT = 'ca-cert.pem'
const CLIENT_KEY = 'client-key.pem'
const CLIENT_CERT = 'client-cert.pem'
const SDN_CONTROLLER_CERT = 'sdn-controller-ca.pem'
const NB_DAYS = 9999
// =============================================================================
export const configurationSchema = {
type: 'object',
properties: {
'cert-dir': {
description: `Full path to a directory where to find: \`client-cert.pem\`,
\`client-key.pem\` and \`ca-cert.pem\` to create ssl connections with hosts.
If none is provided, the plugin will create its own self-signed certificates.`,
type: 'string',
},
'override-certs': {
description: `Replace already existing SDN controller CA certificate`,
type: 'boolean',
default: false,
},
},
}
// =============================================================================
async function fileWrite(path, data) {
await fromCallback(writeFile, path, data)
log.debug(`${path} successfully written`)
}
async function fileRead(path) {
const result = await fromCallback(readFile, path)
return result
}
async function fileExists(path) {
try {
await fromCallback(access, path, constants.F_OK)
} catch (error) {
if (error.code === 'ENOENT') {
return false
}
throw error
}
return true
}
// =============================================================================
class SDNController extends EventEmitter {
constructor({ xo, getDataDir }) {
super()
this._xo = xo
this._getDataDir = getDataDir
this._clientKey = null
this._clientCert = null
this._caCert = null
this._poolNetworks = []
this._ovsdbClients = []
this._newHosts = []
this._networks = new Map()
this._starCenters = new Map()
this._cleaners = []
this._objectsAdded = this._objectsAdded.bind(this)
this._objectsUpdated = this._objectsUpdated.bind(this)
this._overrideCerts = false
this._unsetApiMethod = null
}
// ---------------------------------------------------------------------------
async configure(configuration) {
this._overrideCerts = configuration['override-certs']
let certDirectory = configuration['cert-dir']
if (certDirectory == null) {
log.debug(`No cert-dir provided, using default self-signed certificates`)
certDirectory = await this._getDataDir()
if (!(await fileExists(join(certDirectory, CA_CERT)))) {
// If one certificate doesn't exist, none should
assert(
!(await fileExists(join(certDirectory, CLIENT_KEY))),
`${CLIENT_KEY} should not exist`
)
assert(
!(await fileExists(join(certDirectory, CLIENT_CERT))),
`${CLIENT_CERT} should not exist`
)
log.debug(`No default self-signed certificates exists, creating them`)
await this._generateCertificatesAndKey(certDirectory)
}
}
// TODO: verify certificates and create new certificates if needed
;[this._clientKey, this._clientCert, this._caCert] = await Promise.all([
fileRead(join(certDirectory, CLIENT_KEY)),
fileRead(join(certDirectory, CLIENT_CERT)),
fileRead(join(certDirectory, CA_CERT)),
])
this._ovsdbClients.forEach(client => {
client.updateCertificates(this._clientKey, this._clientCert, this._caCert)
})
const updatedPools = []
for (let i = 0; i < this._poolNetworks.length; ++i) {
const poolNetwork = this._poolNetworks[i]
if (updatedPools.includes(poolNetwork.pool)) {
continue
}
const xapi = this._xo.getXapi(poolNetwork.pool)
await this._installCaCertificateIfNeeded(xapi)
updatedPools.push(poolNetwork.pool)
}
}
async load() {
const createPrivateNetwork = this._createPrivateNetwork.bind(this)
createPrivateNetwork.description =
'Creates a pool-wide private network on a selected pool'
createPrivateNetwork.params = {
poolId: { type: 'string' },
networkName: { type: 'string' },
networkDescription: { type: 'string' },
encapsulation: { type: 'string' },
}
createPrivateNetwork.resolve = {
xoPool: ['poolId', 'pool', ''],
}
this._unsetApiMethod = this._xo.addApiMethod(
'plugin.SDNController.createPrivateNetwork',
createPrivateNetwork
)
// FIXME: we should monitor when xapis are added/removed
forOwn(this._xo.getAllXapis(), async xapi => {
await xapi.objectsFetched
if (this._setControllerNeeded(xapi) === false) {
this._cleaners.push(await this._manageXapi(xapi))
const hosts = filter(xapi.objects.all, { $type: 'host' })
await Promise.all(
map(hosts, async host => {
this._createOvsdbClient(host)
})
)
// Add already existing pool-wide private networks
const networks = filter(xapi.objects.all, { $type: 'network' })
forOwn(networks, async network => {
if (network.other_config.private_pool_wide === 'true') {
log.debug(
`Adding network: '${network.name_label}' for pool: '${
network.$pool.name_label
}' to managed networks`
)
const center = await this._electNewCenter(network, true)
this._poolNetworks.push({
pool: network.$pool.$ref,
network: network.$ref,
starCenter: center ? center.$ref : null,
})
this._networks.set(network.$id, network.$ref)
if (center != null) {
this._starCenters.set(center.$id, center.$ref)
}
}
})
}
})
}
async unload() {
this._ovsdbClients = []
this._poolNetworks = []
this._newHosts = []
this._networks.clear()
this._starCenters.clear()
this._cleaners.forEach(cleaner => cleaner())
this._cleaners = []
this._unsetApiMethod()
}
// ===========================================================================
async _createPrivateNetwork({
xoPool,
networkName,
networkDescription,
encapsulation,
}) {
const pool = this._xo.getXapiObject(xoPool)
await this._setPoolControllerIfNeeded(pool)
// Create the private network
const privateNetworkRef = await pool.$xapi.call('network.create', {
name_label: networkName,
name_description: networkDescription,
MTU: 0,
other_config: {
automatic: 'false',
private_pool_wide: 'true',
encapsulation: encapsulation,
},
})
const privateNetwork = await pool.$xapi._getOrWaitObject(privateNetworkRef)
log.info(
`Private network '${
privateNetwork.name_label
}' has been created for pool '${pool.name_label}'`
)
// For each pool's host, create a tunnel to the private network
const hosts = filter(pool.$xapi.objects.all, { $type: 'host' })
await Promise.all(
map(hosts, async host => {
await this._createTunnel(host, privateNetwork)
this._createOvsdbClient(host)
})
)
const center = await this._electNewCenter(privateNetwork, false)
this._poolNetworks.push({
pool: pool.$ref,
network: privateNetwork.$ref,
starCenter: center ? center.$ref : null,
encapsulation: encapsulation,
})
this._networks.set(privateNetwork.$id, privateNetwork.$ref)
if (center != null) {
this._starCenters.set(center.$id, center.$ref)
}
}
// ---------------------------------------------------------------------------
async _manageXapi(xapi) {
const { objects } = xapi
const objectsRemovedXapi = this._objectsRemoved.bind(this, xapi)
objects.on('add', this._objectsAdded)
objects.on('update', this._objectsUpdated)
objects.on('remove', objectsRemovedXapi)
await this._installCaCertificateIfNeeded(xapi)
return () => {
objects.removeListener('add', this._objectsAdded)
objects.removeListener('update', this._objectsUpdated)
objects.removeListener('remove', objectsRemovedXapi)
}
}
async _objectsAdded(objects) {
await Promise.all(
map(objects, async object => {
const { $type } = object
if ($type === 'host') {
log.debug(
`New host: '${object.name_label}' in pool: '${
object.$pool.name_label
}'`
)
if (find(this._newHosts, { $ref: object.$ref }) == null) {
this._newHosts.push(object)
}
this._createOvsdbClient(object)
}
})
)
}
async _objectsUpdated(objects) {
await Promise.all(
map(objects, async (object, id) => {
const { $type } = object
if ($type === 'PIF') {
await this._pifUpdated(object)
} else if ($type === 'host') {
await this._hostUpdated(object)
}
})
)
}
async _objectsRemoved(xapi, objects) {
await Promise.all(
map(objects, async (object, id) => {
const client = find(this._ovsdbClients, { id: id })
if (client != null) {
this._ovsdbClients.splice(this._ovsdbClients.indexOf(client), 1)
}
// If a Star center host is removed: re-elect a new center where needed
const starCenterRef = this._starCenters.get(id)
if (starCenterRef != null) {
this._starCenters.delete(id)
const poolNetworks = filter(this._poolNetworks, {
starCenter: starCenterRef,
})
for (let i = 0; i < poolNetworks.length; ++i) {
const poolNetwork = poolNetworks[i]
const network = await xapi._getOrWaitObject(poolNetwork.network)
const newCenter = await this._electNewCenter(network, true)
poolNetwork.starCenter = newCenter ? newCenter.$ref : null
if (newCenter != null) {
this._starCenters.set(newCenter.$id, newCenter.$ref)
}
}
return
}
// If a network is removed, clean this._poolNetworks from it
const networkRef = this._networks.get(id)
if (networkRef != null) {
this._networks.delete(id)
const poolNetwork = find(this._poolNetworks, {
network: networkRef,
})
if (poolNetwork != null) {
this._poolNetworks.splice(
this._poolNetworks.indexOf(poolNetwork),
1
)
}
}
})
)
}
async _pifUpdated(pif) {
// Only if PIF is in a private network
const poolNetwork = find(this._poolNetworks, { network: pif.network })
if (poolNetwork == null) {
return
}
if (!pif.currently_attached) {
if (poolNetwork.starCenter !== pif.host) {
return
}
log.debug(
`PIF: '${pif.device}' of network: '${
pif.$network.name_label
}' star-center host: '${
pif.$host.name_label
}' has been unplugged, electing a new host`
)
const newCenter = await this._electNewCenter(pif.$network, true)
poolNetwork.starCenter = newCenter ? newCenter.$ref : null
this._starCenters.delete(pif.$host.$id)
if (newCenter != null) {
this._starCenters.set(newCenter.$id, newCenter.$ref)
}
} else {
if (poolNetwork.starCenter == null) {
const host = pif.$host
log.debug(
`First available host: '${
host.name_label
}' becomes star center of network: '${pif.$network.name_label}'`
)
poolNetwork.starCenter = pif.host
this._starCenters.set(host.$id, host.$ref)
}
log.debug(
`PIF: '${pif.device}' of network: '${pif.$network.name_label}' host: '${
pif.$host.name_label
}' has been plugged`
)
const starCenter = await pif.$xapi._getOrWaitObject(
poolNetwork.starCenter
)
await this._addHostToNetwork(pif.$host, pif.$network, starCenter)
}
}
async _hostUpdated(host) {
const xapi = host.$xapi
if (host.enabled) {
if (host.PIFs.length === 0) {
return
}
const tunnels = filter(xapi.objects.all, { $type: 'tunnel' })
const newHost = find(this._newHosts, { $ref: host.$ref })
if (newHost != null) {
this._newHosts.splice(this._newHosts.indexOf(newHost), 1)
try {
await xapi.call('pool.certificate_sync')
} catch (error) {
log.error(
`Couldn't sync SDN controller ca certificate in pool: '${
host.$pool.name_label
}' because: ${error}`
)
}
}
for (let i = 0; i < tunnels.length; ++i) {
const tunnel = tunnels[i]
const accessPIF = await xapi._getOrWaitObject(tunnel.access_PIF)
if (accessPIF.host !== host.$ref) {
continue
}
const poolNetwork = find(this._poolNetworks, {
network: accessPIF.network,
})
if (poolNetwork == null) {
continue
}
if (accessPIF.currently_attached) {
continue
}
log.debug(
`Pluging PIF: '${accessPIF.device}' for host: '${
host.name_label
}' on network: '${accessPIF.$network.name_label}'`
)
try {
await xapi.call('PIF.plug', accessPIF.$ref)
} catch (error) {
log.error(
`XAPI error while pluging PIF: '${accessPIF.device}' on host: '${
host.name_label
}' for network: '${accessPIF.$network.name_label}'`
)
}
const starCenter = await host.$xapi._getOrWaitObject(
poolNetwork.starCenter
)
await this._addHostToNetwork(host, accessPIF.$network, starCenter)
}
} else {
const poolNetworks = filter(this._poolNetworks, { starCenter: host.$ref })
for (let i = 0; i < poolNetworks.length; ++i) {
const poolNetwork = poolNetworks[i]
const network = await host.$xapi._getOrWaitObject(poolNetwork.network)
log.debug(
`Star center host: '${host.name_label}' of network: '${
network.name_label
}' in pool: '${
host.$pool.name_label
}' is no longer reachable, electing a new host`
)
const newCenter = await this._electNewCenter(network, true)
poolNetwork.starCenter = newCenter ? newCenter.$ref : null
this._starCenters.delete(host.$id)
if (newCenter != null) {
this._starCenters.set(newCenter.$id, newCenter.$ref)
}
}
}
}
// ---------------------------------------------------------------------------
async _setPoolControllerIfNeeded(pool) {
if (!this._setControllerNeeded(pool.$xapi)) {
// Nothing to do
return
}
const controller = find(pool.$xapi.objects.all, { $type: 'SDN_controller' })
if (controller != null) {
await pool.$xapi.call('SDN_controller.forget', controller.$ref)
log.debug(`Remove old SDN controller from pool: '${pool.name_label}'`)
}
await pool.$xapi.call('SDN_controller.introduce', PROTOCOL)
log.debug(`Set SDN controller of pool: '${pool.name_label}'`)
this._cleaners.push(await this._manageXapi(pool.$xapi))
}
_setControllerNeeded(xapi) {
const controller = find(xapi.objects.all, { $type: 'SDN_controller' })
return !(
controller != null &&
controller.protocol === PROTOCOL &&
controller.address === '' &&
controller.port === 0
)
}
// ---------------------------------------------------------------------------
async _installCaCertificateIfNeeded(xapi) {
let needInstall = false
try {
const result = await xapi.call('pool.certificate_list')
if (!result.includes(SDN_CONTROLLER_CERT)) {
needInstall = true
} else if (this._overrideCerts) {
await xapi.call('pool.certificate_uninstall', SDN_CONTROLLER_CERT)
log.debug(
`Old SDN Controller CA certificate uninstalled on pool: '${
xapi.pool.name_label
}'`
)
needInstall = true
}
} catch (error) {
log.error(
`Couldn't retrieve certificate list of pool: '${xapi.pool.name_label}'`
)
}
if (!needInstall) {
return
}
try {
await xapi.call(
'pool.certificate_install',
SDN_CONTROLLER_CERT,
this._caCert.toString()
)
await xapi.call('pool.certificate_sync')
log.debug(
`SDN controller CA certificate install in pool: '${
xapi.pool.name_label
}'`
)
} catch (error) {
log.error(
`Couldn't install SDN controller CA certificate in pool: '${
xapi.pool.name_label
}' because: ${error}`
)
}
}
// ---------------------------------------------------------------------------
async _electNewCenter(network, resetNeeded) {
const pool = network.$pool
let newCenter = null
const hosts = filter(pool.$xapi.objects.all, { $type: 'host' })
await Promise.all(
map(hosts, async host => {
if (resetNeeded) {
// Clean old ports and interfaces
const hostClient = find(this._ovsdbClients, { host: host.$ref })
if (hostClient != null) {
try {
await hostClient.resetForNetwork(network.uuid, network.name_label)
} catch (error) {
log.error(
`Couldn't reset network: '${network.name_label}' for host: '${
host.name_label
}' in pool: '${network.$pool.name_label}' because: ${error}`
)
return
}
}
}
if (newCenter != null) {
return
}
const pif = find(host.$PIFs, { network: network.$ref })
if (pif != null && pif.currently_attached && host.enabled) {
newCenter = host
}
})
)
if (newCenter == null) {
log.error(
`Unable to elect a new star-center host to network: '${
network.name_label
}' for pool: '${
network.$pool.name_label
}' because there's no available host`
)
return null
}
// Recreate star topology
await Promise.all(
await map(hosts, async host => {
await this._addHostToNetwork(host, network, newCenter)
})
)
log.info(
`New star center host elected: '${newCenter.name_label}' in network: '${
network.name_label
}'`
)
return newCenter
}
async _createTunnel(host, network) {
const pif = find(host.$PIFs, { physical: true })
if (pif == null) {
log.error(
`No PIF found to create tunnel on host: '${
host.name_label
}' for network: '${network.name_label}'`
)
return
}
await host.$xapi.call('tunnel.create', pif.$ref, network.$ref)
log.debug(
`Tunnel added on host '${host.name_label}' for network '${
network.name_label
}'`
)
}
async _addHostToNetwork(host, network, starCenter) {
if (host.$ref === starCenter.$ref) {
// Nothing to do
return
}
const hostClient = find(this._ovsdbClients, {
host: host.$ref,
})
if (hostClient == null) {
log.error(`No OVSDB client found for host: '${host.name_label}'`)
return
}
const starCenterClient = find(this._ovsdbClients, {
host: starCenter.$ref,
})
if (starCenterClient == null) {
log.error(
`No OVSDB client found for star-center host: '${starCenter.name_label}'`
)
return
}
const encapsulation =
network.other_config.encapsulation != null
? network.other_config.encapsulation
: 'gre'
try {
await hostClient.addInterfaceAndPort(
network.uuid,
network.name_label,
starCenterClient.address,
encapsulation
)
await starCenterClient.addInterfaceAndPort(
network.uuid,
network.name_label,
hostClient.address,
encapsulation
)
} catch (error) {
log.error(
`Couldn't add host: '${host.name_label}' to network: '${
network.name_label
}' in pool: '${host.$pool.name_label}' because: ${error}`
)
}
}
// ---------------------------------------------------------------------------
_createOvsdbClient(host) {
const foundClient = find(this._ovsdbClients, { host: host.$ref })
if (foundClient != null) {
return foundClient
}
const client = new OvsdbClient(
host,
this._clientKey,
this._clientCert,
this._caCert
)
this._ovsdbClients.push(client)
return client
}
// ---------------------------------------------------------------------------
async _generateCertificatesAndKey(dataDir) {
const openssl = new NodeOpenssl()
const rsakeyoptions = {
rsa_keygen_bits: 4096,
format: 'PKCS8',
}
const subject = {
countryName: 'XX',
localityName: 'Default City',
organizationName: 'Default Company LTD',
}
const csroptions = {
hash: 'sha256',
startdate: new Date('1984-02-04 00:00:00'),
enddate: new Date('2143-06-04 04:16:23'),
subject: subject,
}
const cacsroptions = {
hash: 'sha256',
days: NB_DAYS,
subject: subject,
}
openssl.generateRSAPrivateKey(rsakeyoptions, (err, cakey, cmd) => {
if (err) {
log.error(`Error while generating CA private key: ${err}`)
return
}
openssl.generateCSR(cacsroptions, cakey, null, (err, csr, cmd) => {
if (err) {
log.error(`Error while generating CA certificate: ${err}`)
return
}
openssl.selfSignCSR(
csr,
cacsroptions,
cakey,
null,
async (err, cacrt, cmd) => {
if (err) {
log.error(`Error while signing CA certificate: ${err}`)
return
}
await fileWrite(join(dataDir, CA_CERT), cacrt)
openssl.generateRSAPrivateKey(
rsakeyoptions,
async (err, key, cmd) => {
if (err) {
log.error(`Error while generating private key: ${err}`)
return
}
await fileWrite(join(dataDir, CLIENT_KEY), key)
openssl.generateCSR(csroptions, key, null, (err, csr, cmd) => {
if (err) {
log.error(`Error while generating certificate: ${err}`)
return
}
openssl.CASignCSR(
csr,
cacsroptions,
false,
cacrt,
cakey,
null,
async (err, crt, cmd) => {
if (err) {
log.error(`Error while signing certificate: ${err}`)
return
}
await fileWrite(join(dataDir, CLIENT_CERT), crt)
this.emit('certWritten')
}
)
})
}
)
}
)
})
})
await fromEvent(this, 'certWritten', {})
log.debug('All certificates have been successfully written')
}
}
export default opts => new SDNController(opts)

View File

@@ -0,0 +1,511 @@
import assert from 'assert'
import createLogger from '@xen-orchestra/log'
import forOwn from 'lodash/forOwn'
import fromEvent from 'promise-toolbox/fromEvent'
import { connect } from 'tls'
const log = createLogger('xo:xo-server:sdn-controller:ovsdb-client')
const OVSDB_PORT = 6640
// =============================================================================
export class OvsdbClient {
constructor(host, clientKey, clientCert, caCert) {
this._host = host
this._numberOfPortAndInterface = 0
this._requestID = 0
this.updateCertificates(clientKey, clientCert, caCert)
log.debug(`[${this._host.name_label}] New OVSDB client`)
}
// ---------------------------------------------------------------------------
get address() {
return this._host.address
}
get host() {
return this._host.$ref
}
get id() {
return this._host.$id
}
updateCertificates(clientKey, clientCert, caCert) {
this._clientKey = clientKey
this._clientCert = clientCert
this._caCert = caCert
log.debug(`[${this._host.name_label}] Certificates have been updated`)
}
// ---------------------------------------------------------------------------
async addInterfaceAndPort(
networkUuid,
networkName,
remoteAddress,
encapsulation
) {
const socket = await this._connect()
const index = this._numberOfPortAndInterface
++this._numberOfPortAndInterface
const [bridgeUuid, bridgeName] = await this._getBridgeUuidForNetwork(
networkUuid,
networkName,
socket
)
if (bridgeUuid == null) {
socket.destroy()
return
}
const alreadyExist = await this._interfaceAndPortAlreadyExist(
bridgeUuid,
bridgeName,
remoteAddress,
socket
)
if (alreadyExist) {
socket.destroy()
return
}
const interfaceName = 'tunnel_iface' + index
const portName = 'tunnel_port' + index
// Add interface and port to the bridge
const options = ['map', [['remote_ip', remoteAddress]]]
const addInterfaceOperation = {
op: 'insert',
table: 'Interface',
row: {
type: encapsulation,
options: options,
name: interfaceName,
other_config: ['map', [['private_pool_wide', 'true']]],
},
'uuid-name': 'new_iface',
}
const addPortOperation = {
op: 'insert',
table: 'Port',
row: {
name: portName,
interfaces: ['set', [['named-uuid', 'new_iface']]],
other_config: ['map', [['private_pool_wide', 'true']]],
},
'uuid-name': 'new_port',
}
const mutateBridgeOperation = {
op: 'mutate',
table: 'Bridge',
where: [['_uuid', '==', ['uuid', bridgeUuid]]],
mutations: [['ports', 'insert', ['set', [['named-uuid', 'new_port']]]]],
}
const params = [
'Open_vSwitch',
addInterfaceOperation,
addPortOperation,
mutateBridgeOperation,
]
const jsonObjects = await this._sendOvsdbTransaction(params, socket)
if (jsonObjects == null) {
socket.destroy()
return
}
let error
let details
let i = 0
let opResult
do {
opResult = jsonObjects[0].result[i]
if (opResult != null && opResult.error != null) {
error = opResult.error
details = opResult.details
}
++i
} while (opResult && !error)
if (error != null) {
log.error(
`[${
this._host.name_label
}] Error while adding port: '${portName}' and interface: '${interfaceName}' to bridge: '${bridgeName}' on network: '${networkName}' because: ${error}: ${details}`
)
socket.destroy()
return
}
log.debug(
`[${
this._host.name_label
}] Port: '${portName}' and interface: '${interfaceName}' added to bridge: '${bridgeName}' on network: '${networkName}'`
)
socket.destroy()
}
async resetForNetwork(networkUuid, networkName) {
const socket = await this._connect()
const [bridgeUuid, bridgeName] = await this._getBridgeUuidForNetwork(
networkUuid,
networkName,
socket
)
if (bridgeUuid == null) {
socket.destroy()
return
}
// Delete old ports created by a SDN controller
const ports = await this._getBridgePorts(bridgeUuid, bridgeName, socket)
if (ports == null) {
socket.destroy()
return
}
const portsToDelete = []
for (let i = 0; i < ports.length; ++i) {
const portUuid = ports[i][1]
const where = [['_uuid', '==', ['uuid', portUuid]]]
const selectResult = await this._select(
'Port',
['name', 'other_config'],
where,
socket
)
if (selectResult == null) {
continue
}
forOwn(selectResult.other_config[1], config => {
if (config[0] === 'private_pool_wide' && config[1] === 'true') {
log.debug(
`[${this._host.name_label}] Adding port: '${
selectResult.name
}' to delete list from bridge: '${bridgeName}'`
)
portsToDelete.push(['uuid', portUuid])
}
})
}
if (portsToDelete.length === 0) {
// Nothing to do
socket.destroy()
return
}
const mutateBridgeOperation = {
op: 'mutate',
table: 'Bridge',
where: [['_uuid', '==', ['uuid', bridgeUuid]]],
mutations: [['ports', 'delete', ['set', portsToDelete]]],
}
const params = ['Open_vSwitch', mutateBridgeOperation]
const jsonObjects = await this._sendOvsdbTransaction(params, socket)
if (jsonObjects == null) {
socket.destroy()
return
}
if (jsonObjects[0].error != null) {
log.error(
`[${
this._host.name_label
}] Couldn't delete ports from bridge: '${bridgeName}' because: ${
jsonObjects.error
}`
)
socket.destroy()
return
}
log.debug(
`[${this._host.name_label}] Deleted ${
jsonObjects[0].result[0].count
} ports from bridge: '${bridgeName}'`
)
socket.destroy()
}
// ===========================================================================
_parseJson(chunk) {
let data = chunk.toString()
let buffer = ''
let depth = 0
let pos = 0
const objects = []
for (let i = pos; i < data.length; ++i) {
const c = data.charAt(i)
if (c === '{') {
depth++
} else if (c === '}') {
depth--
if (depth === 0) {
const object = JSON.parse(buffer + data.substr(0, i + 1))
objects.push(object)
buffer = ''
data = data.substr(i + 1)
pos = 0
i = -1
}
}
}
buffer += data
return objects
}
// ---------------------------------------------------------------------------
async _getBridgeUuidForNetwork(networkUuid, networkName, socket) {
const where = [
[
'external_ids',
'includes',
['map', [['xs-network-uuids', networkUuid]]],
],
]
const selectResult = await this._select(
'Bridge',
['_uuid', 'name'],
where,
socket
)
if (selectResult == null) {
return [null, null]
}
const bridgeUuid = selectResult._uuid[1]
const bridgeName = selectResult.name
log.debug(
`[${
this._host.name_label
}] Found bridge: '${bridgeName}' for network: '${networkName}'`
)
return [bridgeUuid, bridgeName]
}
async _interfaceAndPortAlreadyExist(
bridgeUuid,
bridgeName,
remoteAddress,
socket
) {
const ports = await this._getBridgePorts(bridgeUuid, bridgeName, socket)
if (ports == null) {
return
}
for (let i = 0; i < ports.length; ++i) {
const portUuid = ports[i][1]
const interfaces = await this._getPortInterfaces(portUuid, socket)
if (interfaces == null) {
continue
}
let j
for (j = 0; j < interfaces.length; ++j) {
const interfaceUuid = interfaces[j][1]
const hasRemote = await this._interfaceHasRemote(
interfaceUuid,
remoteAddress,
socket
)
if (hasRemote === true) {
return true
}
}
}
return false
}
async _getBridgePorts(bridgeUuid, bridgeName, socket) {
const where = [['_uuid', '==', ['uuid', bridgeUuid]]]
const selectResult = await this._select('Bridge', ['ports'], where, socket)
if (selectResult == null) {
return null
}
return selectResult.ports[0] === 'set'
? selectResult.ports[1]
: [selectResult.ports]
}
async _getPortInterfaces(portUuid, socket) {
const where = [['_uuid', '==', ['uuid', portUuid]]]
const selectResult = await this._select(
'Port',
['name', 'interfaces'],
where,
socket
)
if (selectResult == null) {
return null
}
return selectResult.interfaces[0] === 'set'
? selectResult.interfaces[1]
: [selectResult.interfaces]
}
async _interfaceHasRemote(interfaceUuid, remoteAddress, socket) {
const where = [['_uuid', '==', ['uuid', interfaceUuid]]]
const selectResult = await this._select(
'Interface',
['name', 'options'],
where,
socket
)
if (selectResult == null) {
return false
}
for (let i = 0; i < selectResult.options[1].length; ++i) {
const option = selectResult.options[1][i]
if (option[0] === 'remote_ip' && option[1] === remoteAddress) {
return true
}
}
return false
}
// ---------------------------------------------------------------------------
async _select(table, columns, where, socket) {
const selectOperation = {
op: 'select',
table: table,
columns: columns,
where: where,
}
const params = ['Open_vSwitch', selectOperation]
const jsonObjects = await this._sendOvsdbTransaction(params, socket)
if (jsonObjects == null) {
return
}
const jsonResult = jsonObjects[0].result[0]
if (jsonResult.error != null) {
log.error(
`[${
this._host.name_label
}] Couldn't retrieve: '${columns}' in: '${table}' because: ${
jsonResult.error
}: ${jsonResult.details}`
)
return null
}
if (jsonResult.rows.length === 0) {
log.error(
`[${
this._host.name_label
}] No '${columns}' found in: '${table}' where: '${where}'`
)
return null
}
// For now all select operations should return only 1 row
assert(
jsonResult.rows.length === 1,
`[${
this._host.name_label
}] There should exactly 1 row when searching: '${columns}' in: '${table}' where: '${where}'`
)
return jsonResult.rows[0]
}
async _sendOvsdbTransaction(params, socket) {
const stream = socket
const requestId = this._requestID
++this._requestID
const req = {
id: requestId,
method: 'transact',
params: params,
}
try {
stream.write(JSON.stringify(req))
} catch (error) {
log.error(
`[${this._host.name_label}] Error while writing into stream: ${error}`
)
return null
}
let result
let jsonObjects
let resultRequestId
do {
try {
result = await fromEvent(stream, 'data', {})
} catch (error) {
log.error(
`[${
this._host.name_label
}] Error while waiting for stream data: ${error}`
)
return null
}
jsonObjects = this._parseJson(result)
resultRequestId = jsonObjects[0].id
} while (resultRequestId !== requestId)
return jsonObjects
}
// ---------------------------------------------------------------------------
async _connect() {
const options = {
ca: this._caCert,
key: this._clientKey,
cert: this._clientCert,
host: this._host.address,
port: OVSDB_PORT,
rejectUnauthorized: false,
requestCert: false,
}
const socket = connect(options)
try {
await fromEvent(socket, 'secureConnect', {})
} catch (error) {
log.error(
`[${this._host.name_label}] TLS connection failed because: ${error}: ${
error.code
}`
)
throw error
}
log.debug(`[${this._host.name_label}] TLS connection successful`)
socket.on('error', error => {
log.error(
`[${
this._host.name_label
}] OVSDB client socket error: ${error} with code: ${error.code}`
)
})
return socket
}
}

View File

@@ -123,7 +123,7 @@
"value-matcher": "^0.2.0",
"vhd-lib": "^0.7.0",
"ws": "^6.0.0",
"xen-api": "^0.25.2",
"xen-api": "^0.26.0",
"xml2js": "^0.4.19",
"xo-acl-resolver": "^0.4.1",
"xo-collection": "^0.4.1",

View File

@@ -1,5 +1,7 @@
// TODO: too low level, move into host.
import { filter, find } from 'lodash'
import { IPV4_CONFIG_MODES, IPV6_CONFIG_MODES } from '../xapi'
export function getIpv4ConfigurationModes() {
@@ -15,7 +17,17 @@ export function getIpv6ConfigurationModes() {
async function delete_({ pif }) {
// TODO: check if PIF is attached before
await this.getXapi(pif).callAsync('PIF.destroy', pif._xapiRef)
const xapi = this.getXapi(pif)
const tunnels = filter(xapi.objects.all, { $type: 'tunnel' })
const tunnel = find(tunnels, { access_PIF: pif._xapiRef })
if (tunnel != null) {
await xapi.callAsync('PIF.unplug', pif._xapiRef)
await xapi.callAsync('tunnel.destroy', tunnel.$ref)
return
}
await xapi.callAsync('PIF.destroy', pif._xapiRef)
}
export { delete_ as delete }

View File

@@ -22,8 +22,8 @@ import { forbiddenOperation } from 'xo-common/api-errors'
import { Xapi as XapiBase, NULL_REF } from 'xen-api'
import {
every,
find,
filter,
find,
flatMap,
flatten,
groupBy,
@@ -2136,6 +2136,16 @@ export default class Xapi extends XapiBase {
mapToArray(bonds, bond => this.call('Bond.destroy', bond))
)
const tunnels = filter(this.objects.all, { $type: 'tunnel' })
await Promise.all(
map(pifs, async pif => {
const tunnel = find(tunnels, { access_PIF: pif.$ref })
if (tunnel != null) {
await this.callAsync('tunnel.destroy', tunnel.$ref)
}
})
)
await this.callAsync('network.destroy', network.$ref)
}

View File

@@ -47,6 +47,8 @@ const messages = {
chooseBackup: 'Choose a backup',
clickToShowError: 'Click to show error',
backupJobs: 'Backup jobs',
iscsiSessions:
'({ nSessions, number }) iSCSI session{nSessions, plural, one {} other {s}}',
// ----- Modals -----
alertOk: 'OK',
@@ -806,7 +808,7 @@ const messages = {
hostNoIscsiSr: 'Not connected to an iSCSI SR',
hostMultipathingSrs: 'Click to see concerned SRs',
hostMultipathingPaths:
'{nActives, number} of {nPaths, number} path{nPaths, plural, one {} other {s}} ({ nSessions, number } iSCSI session{nSessions, plural, one {} other {s}})',
'{nActives, number} of {nPaths, number} path{nPaths, plural, one {} other {s}}',
hostMultipathingRequiredState:
'This action will not be fulfilled if a VM is in a running state. Please ensure that all VMs are evacuated or stopped before performing this action!',
hostMultipathingWarning:
@@ -1718,11 +1720,13 @@ const messages = {
newNetworkBondMode: 'Bond mode',
newNetworkInfo: 'Info',
newNetworkType: 'Type',
newNetworkEncapsulation: 'Encapsulation',
deleteNetwork: 'Delete network',
deleteNetworkConfirm: 'Are you sure you want to delete this network?',
networkInUse: 'This network is currently in use',
pillBonded: 'Bonded',
bondedNetwork: 'Bonded network',
privateNetwork: 'Private network',
// ----- Add host -----
addHostSelectHost: 'Host',

View File

@@ -58,6 +58,12 @@ export class TooltipViewer extends Component {
// ===================================================================
// Wrap disabled HTML element before wrapping it with Tooltip
// <Tooltip>
// <div>
// <MyComponent disabled />
// </div>
// </Tooltip>
export default class Tooltip extends Component {
static propTypes = {
children: PropTypes.oneOfType([PropTypes.element, PropTypes.string]),

View File

@@ -1648,6 +1648,8 @@ export const getBondModes = () => _call('network.getBondModes')
export const createNetwork = params => _call('network.create', params)
export const createBondedNetwork = params =>
_call('network.createBonded', params)
export const createPrivateNetwork = params =>
_call('plugin.SDNController.createPrivateNetwork', params)
export const deleteNetwork = network =>
confirm({

View File

@@ -55,6 +55,7 @@ const MultipathableSrs = decorate([
<Container>
{map(pbds, pbd => {
const [nActives, nPaths] = getIscsiPaths(pbd)
const nSessions = pbd.otherConfig.iscsi_sessions
return (
<Row key={pbd.id}>
<Col>
@@ -64,8 +65,8 @@ const MultipathableSrs = decorate([
_('hostMultipathingPaths', {
nActives,
nPaths,
nSessions: pbd.otherConfig.iscsi_sessions,
})}
})}{' '}
{nSessions !== undefined && _('iscsiSessions', { nSessions })}
</Col>
</Row>
)

View File

@@ -1175,10 +1175,8 @@ export default class NewVm extends BaseComponent {
</LineItem>
<br />
<LineItem>
<label>
<Tooltip
content={CAN_CLOUD_INIT ? undefined : _('premiumOnly')}
>
<Tooltip content={CAN_CLOUD_INIT ? undefined : _('premiumOnly')}>
<label>
<input
checked={installMethod === 'SSH'}
disabled={!CAN_CLOUD_INIT}
@@ -1187,10 +1185,10 @@ export default class NewVm extends BaseComponent {
type='radio'
value='SSH'
/>
</Tooltip>
&nbsp;
{_('newVmSshKey')}
</label>
&nbsp;
{_('newVmSshKey')}
</label>
</Tooltip>
&nbsp;
<span className={classNames('input-group', styles.fixedWidth)}>
<DebounceInput
@@ -1218,10 +1216,8 @@ export default class NewVm extends BaseComponent {
</LineItem>
<br />
<LineItem>
<label>
<Tooltip
content={CAN_CLOUD_INIT ? undefined : _('premiumOnly')}
>
<Tooltip content={CAN_CLOUD_INIT ? undefined : _('premiumOnly')}>
<label>
<input
checked={installMethod === 'customConfig'}
disabled={!CAN_CLOUD_INIT}
@@ -1230,10 +1226,10 @@ export default class NewVm extends BaseComponent {
type='radio'
value='customConfig'
/>
</Tooltip>
&nbsp;
{_('newVmCustomConfig')}
</label>
&nbsp;
{_('newVmCustomConfig')}
</label>
</Tooltip>
&nbsp;
<AvailableTemplateVars />
&nbsp;

View File

@@ -4,8 +4,14 @@ import decorate from 'apply-decorators'
import PropTypes from 'prop-types'
import React, { Component } from 'react'
import Wizard, { Section } from 'wizard'
import { connectStore } from 'utils'
import { createBondedNetwork, createNetwork, getBondModes } from 'xo'
import { addSubscriptions, connectStore } from 'utils'
import {
createBondedNetwork,
createNetwork,
createPrivateNetwork,
getBondModes,
subscribePlugins,
} from 'xo'
import { createGetObject, getIsPoolAdmin } from 'selectors'
import { injectIntl } from 'react-intl'
import { injectState, provideState } from 'reaclette'
@@ -21,6 +27,8 @@ const EMPTY = {
bonded: false,
bondMode: undefined,
description: '',
encapsulation: 'gre',
isPrivate: false,
mtu: '',
name: '',
pif: undefined,
@@ -29,6 +37,9 @@ const EMPTY = {
}
const NewNetwork = decorate([
addSubscriptions({
plugins: subscribePlugins,
}),
connectStore(() => ({
isPoolAdmin: getIsPoolAdmin,
pool: createGetObject((_, props) => props.location.query.pool),
@@ -42,11 +53,26 @@ const NewNetwork = decorate([
onChangeMode: (_, bondMode) => ({ bondMode }),
onChangePif: (_, value) => ({ bonded }) =>
bonded ? { pifs: value } : { pif: value },
onChangeEncapsulation(_, encapsulation) {
return { encapsulation: encapsulation.value }
},
reset: () => EMPTY,
toggleBonded: () => ({ bonded }) => ({
...EMPTY,
bonded: !bonded,
}),
toggleBonded() {
const { bonded, isPrivate } = this.state
return {
...EMPTY,
bonded: !bonded,
isPrivate: bonded ? isPrivate : false,
}
},
togglePrivate() {
const { bonded, isPrivate } = this.state
return {
...EMPTY,
isPrivate: !isPrivate,
bonded: isPrivate ? bonded : false,
}
},
},
computed: {
modeOptions: ({ bondModes }) =>
@@ -58,6 +84,10 @@ const NewNetwork = decorate([
: [],
pifPredicate: (_, { pool }) => pif =>
pif.vlan === -1 && pif.$host === (pool && pool.master),
isSdnControllerLoaded: (state, { plugins = [] }) =>
plugins.some(
plugin => plugin.name === 'sdn-controller' && plugin.loaded
),
},
}),
injectState,
@@ -71,7 +101,9 @@ const NewNetwork = decorate([
const {
bonded,
bondMode,
isPrivate,
description,
encapsulation,
mtu,
name,
pif,
@@ -88,6 +120,13 @@ const NewNetwork = decorate([
pool: pool.id,
vlan,
})
: isPrivate
? createPrivateNetwork({
poolId: pool.id,
networkName: name,
networkDescription: description,
encapsulation: encapsulation,
})
: createNetwork({
description,
mtu,
@@ -132,7 +171,9 @@ const NewNetwork = decorate([
const {
bonded,
bondMode,
isPrivate,
description,
encapsulation,
modeOptions,
mtu,
name,
@@ -140,6 +181,7 @@ const NewNetwork = decorate([
pifPredicate,
pifs,
vlan,
isSdnControllerLoaded,
} = state
const { formatMessage } = intl
return (
@@ -152,69 +194,112 @@ const NewNetwork = decorate([
<Toggle onChange={effects.toggleBonded} value={bonded} />{' '}
<label>{_('bondedNetwork')}</label>
</div>
<div>
<Toggle
disabled={!isSdnControllerLoaded}
onChange={effects.togglePrivate}
value={isPrivate}
/>{' '}
<label>{_('privateNetwork')}</label>
</div>
</Section>
<Section icon='info' title='newNetworkInfo'>
<div className='form-group'>
<label>{_('newNetworkInterface')}</label>
<SelectPif
multi={bonded}
onChange={effects.onChangePif}
predicate={pifPredicate}
required={bonded}
value={bonded ? pifs : pif}
/>
<label>{_('newNetworkName')}</label>
<input
className='form-control'
name='name'
onChange={effects.linkState}
required
type='text'
value={name}
/>
<label>{_('newNetworkDescription')}</label>
<input
className='form-control'
name='description'
onChange={effects.linkState}
type='text'
value={description}
/>
<label>{_('newNetworkMtu')}</label>
<input
className='form-control'
name='mtu'
onChange={effects.linkState}
placeholder={formatMessage(messages.newNetworkDefaultMtu)}
type='text'
value={mtu}
/>
{bonded ? (
<div>
<label>{_('newNetworkBondMode')}</label>
<Select
onChange={effects.onChangeMode}
options={modeOptions}
required
value={bondMode}
/>
</div>
) : (
<div>
<label>{_('newNetworkVlan')}</label>
<input
className='form-control'
name='vlan'
onChange={effects.linkState}
placeholder={formatMessage(
messages.newNetworkDefaultVlan
)}
type='text'
value={vlan}
/>
</div>
)}
</div>
{isPrivate ? (
<div className='form-group'>
<label>{_('newNetworkName')}</label>
<input
className='form-control'
name='name'
onChange={effects.linkState}
required
type='text'
value={name}
/>
<label>{_('newNetworkDescription')}</label>
<input
className='form-control'
name='description'
onChange={effects.linkState}
type='text'
value={description}
/>
<label>{_('newNetworkEncapsulation')}</label>
<Select
className='form-control'
name='encapsulation'
onChange={effects.onChangeEncapsulation}
options={[
{ label: 'GRE', value: 'gre' },
{ label: 'VxLAN', value: 'vxlan' },
]}
value={encapsulation}
/>
</div>
) : (
<div className='form-group'>
<label>{_('newNetworkInterface')}</label>
<SelectPif
multi={bonded}
onChange={effects.onChangePif}
predicate={pifPredicate}
required={bonded}
value={bonded ? pifs : pif}
/>
<label>{_('newNetworkName')}</label>
<input
className='form-control'
name='name'
onChange={effects.linkState}
required
type='text'
value={name}
/>
<label>{_('newNetworkDescription')}</label>
<input
className='form-control'
name='description'
onChange={effects.linkState}
type='text'
value={description}
/>
<label>{_('newNetworkMtu')}</label>
<input
className='form-control'
name='mtu'
onChange={effects.linkState}
placeholder={formatMessage(
messages.newNetworkDefaultMtu
)}
type='text'
value={mtu}
/>
{bonded ? (
<div>
<label>{_('newNetworkBondMode')}</label>
<Select
onChange={effects.onChangeMode}
options={modeOptions}
required
value={bondMode}
/>
</div>
) : (
<div>
<label>{_('newNetworkVlan')}</label>
<input
className='form-control'
name='vlan'
onChange={effects.linkState}
placeholder={formatMessage(
messages.newNetworkDefaultVlan
)}
type='text'
value={vlan}
/>
</div>
)}
</div>
)}
</Section>
</Wizard>
<div className='form-group pull-right'>

View File

@@ -111,14 +111,17 @@ const HOST_WITH_PATHS_COLUMNS = [
}
const [nActives, nPaths] = getIscsiPaths(pbd)
const nSessions = pbd.otherConfig.iscsi_sessions
return (
nActives !== undefined &&
nPaths !== undefined &&
_('hostMultipathingPaths', {
nActives,
nPaths,
nSessions: pbd.otherConfig.iscsi_sessions,
})
<span>
{nActives !== undefined &&
nPaths !== undefined &&
_('hostMultipathingPaths', {
nActives,
nPaths,
})}{' '}
{nSessions !== undefined && _('iscsiSessions', { nSessions })}
</span>
)
},
sortCriteria: (pbd, hosts) => get(() => hosts[pbd.host].multipathing),

View File

@@ -2,7 +2,7 @@
# yarn lockfile v1
"@babel/cli@^7.0.0", "@babel/cli@^7.1.5":
"@babel/cli@^7.0.0", "@babel/cli@^7.1.5", "@babel/cli@^7.4.4":
version "7.4.4"
resolved "https://registry.yarnpkg.com/@babel/cli/-/cli-7.4.4.tgz#5454bb7112f29026a4069d8e6f0e1794e651966c"
integrity sha512-XGr5YjQSjgTa6OzQZY57FAJsdeVSAKR/u/KA5exWIz66IKtv/zXtHy+fIZcMry/EgYegwuHE7vzGnrFhjdIAsQ==
@@ -26,7 +26,7 @@
dependencies:
"@babel/highlight" "^7.0.0"
"@babel/core@^7.0.0", "@babel/core@^7.1.0", "@babel/core@^7.1.5":
"@babel/core@^7.0.0", "@babel/core@^7.1.0", "@babel/core@^7.1.5", "@babel/core@^7.4.4":
version "7.4.5"
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.4.5.tgz#081f97e8ffca65a9b4b0fdc7e274e703f000c06a"
integrity sha512-OvjIh6aqXtlsA8ujtGKfC7LYWksYSX8yQcM8Ay3LuvVeQ63lcOKgoZWVqcpFwkd29aYU9rVx7jxhfhiEDV9MZA==
@@ -712,7 +712,7 @@
core-js "^2.6.5"
regenerator-runtime "^0.13.2"
"@babel/preset-env@^7.0.0", "@babel/preset-env@^7.1.5":
"@babel/preset-env@^7.0.0", "@babel/preset-env@^7.1.5", "@babel/preset-env@^7.4.4":
version "7.4.5"
resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.4.5.tgz#2fad7f62983d5af563b5f3139242755884998a58"
integrity sha512-f2yNVXM+FsR5V8UwcFeIHzHWgnhXg3NpRmy0ADvALpnhB0SLbCvrCRr4BLOUYbQNLS+Z0Yer46x9dJXpXewI7w==
@@ -4020,7 +4020,7 @@ create-react-class@^15.5.1, create-react-class@^15.6.0:
loose-envify "^1.3.1"
object-assign "^4.1.1"
cross-env@^5.1.1, cross-env@^5.1.3, cross-env@^5.1.4:
cross-env@^5.1.1, cross-env@^5.1.3, cross-env@^5.1.4, cross-env@^5.2.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-5.2.0.tgz#6ecd4c015d5773e614039ee529076669b9d126f2"
integrity sha512-jtdNFfFW1hB7sMhr/H6rW1Z45LFqyI431m3qU6bFXcQ3Eh7LtBuG3h74o7ohHZ3crrRkkqHlo4jYHFPcjroANg==
@@ -9413,7 +9413,7 @@ moment-timezone@^0.5.13, moment-timezone@^0.5.14:
dependencies:
moment ">= 2.9.0"
"moment@>= 2.9.0", moment@^2.10.6, moment@^2.20.1:
"moment@>= 2.9.0", moment@^2.10.6, moment@^2.20.1, moment@^2.22.1:
version "2.24.0"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b"
integrity sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==
@@ -9682,6 +9682,14 @@ node-notifier@^5.2.1:
shellwords "^0.1.1"
which "^1.3.0"
node-openssl-cert@^0.0.81:
version "0.0.81"
resolved "https://registry.yarnpkg.com/node-openssl-cert/-/node-openssl-cert-0.0.81.tgz#79b5c0d9767116799bc74c4bd94b965a4789f8ad"
integrity sha512-KPDU2mx+dL9Ryj9Pse2KkcwGQXoXmg8FrSluIMcw/l6l2PiOYd6k+91SON359XWIS/x32WFmeFGUOCRHEuoQtQ==
dependencies:
moment "^2.22.1"
tmp "^0.0.33"
node-pre-gyp@0.9.1:
version "0.9.1"
resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.9.1.tgz#f11c07516dd92f87199dbc7e1838eab7cd56c9e0"
@@ -10886,6 +10894,13 @@ promise-toolbox@^0.12.1:
dependencies:
make-error "^1.3.2"
promise-toolbox@^0.13.0:
version "0.13.0"
resolved "https://registry.yarnpkg.com/promise-toolbox/-/promise-toolbox-0.13.0.tgz#f4c73167be3f3b51d92167e9db888f1718a75b59"
integrity sha512-Z6u7EL9/QyY1zZqeqpEiKS7ygKwZyl0JL0ouno/en6vMliZZc4AmM0aFCrDAVxEyKqj2f3SpkW0lXEfAZsNWiQ==
dependencies:
make-error "^1.3.2"
promise-toolbox@^0.8.0:
version "0.8.3"
resolved "https://registry.yarnpkg.com/promise-toolbox/-/promise-toolbox-0.8.3.tgz#b757232a21d246d8702df50da6784932dd0f5348"