Compare commits
10 Commits
xo-server-
...
select-nfs
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d2951f617b | ||
|
|
0ea64bdca7 | ||
|
|
f06bee3737 | ||
|
|
2693598ac8 | ||
|
|
19011ad372 | ||
|
|
86eb7744a1 | ||
|
|
fe13ef6ff9 | ||
|
|
5607d34719 | ||
|
|
4501018dd6 | ||
|
|
9be9007fde |
@@ -16,7 +16,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"golike-defer": "^0.4.1",
|
||||
"xen-api": "^0.26.0"
|
||||
"xen-api": "^0.25.2"
|
||||
},
|
||||
"scripts": {
|
||||
"postversion": "npm publish"
|
||||
|
||||
@@ -3,30 +3,11 @@
|
||||
### Enhancements
|
||||
|
||||
- [Backup-ng/restore] Display size for full VM backup [#4009](https://github.com/vatesfr/xen-orchestra/issues/4009) (PR [#4245](https://github.com/vatesfr/xen-orchestra/pull/4245))
|
||||
- [Sr/new] Ability to select NFS version when creating NFS storage [#3951](https://github.com/vatesfr/xen-orchestra/issues/3951) (PR [#4277](https://github.com/vatesfr/xen-orchestra/pull/4277))
|
||||
- [auth-saml] Improve compatibility with Microsoft Azure Active Directory (PR [#4294](https://github.com/vatesfr/xen-orchestra/pull/4294))
|
||||
- [Host] Display warning when "Citrix Hypervisor" license has restrictions [#4251](https://github.com/vatesfr/xen-orchestra/issues/4164) (PR [#4235](https://github.com/vatesfr/xen-orchestra/pull/4279))
|
||||
- [VM/Backup] Create backup bulk action [#2573](https://github.com/vatesfr/xen-orchestra/issues/2573) (PR [#4257](https://github.com/vatesfr/xen-orchestra/pull/4257))
|
||||
- [Sr/new] Ability to select NFS version when creating NFS storage [#3951](https://github.com/vatesfr/xen-orchestra/issues/#3951) (PR [#4277](https://github.com/vatesfr/xen-orchestra/pull/4277))
|
||||
- [SR/new] Create ZFS storage [#4260](https://github.com/vatesfr/xen-orchestra/issues/4260) (PR [#4266](https://github.com/vatesfr/xen-orchestra/pull/4266))
|
||||
- [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
|
||||
- xo-web v5.44.0
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
"human-format": "^0.10.0",
|
||||
"lodash": "^4.17.4",
|
||||
"pw": "^0.0.4",
|
||||
"xen-api": "^0.26.0"
|
||||
"xen-api": "^0.25.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.1.5",
|
||||
|
||||
@@ -82,7 +82,7 @@ console.log(xapi.pool.$master.$resident_VMs[0].name_label)
|
||||
A CLI is provided to help exploration and discovery of the XAPI.
|
||||
|
||||
```
|
||||
> xen-api xen1.company.net root
|
||||
> xen-api https://xen1.company.net root
|
||||
Password: ******
|
||||
root@xen1.company.net> xapi.status
|
||||
'connected'
|
||||
@@ -92,14 +92,6 @@ root@xen1.company.net> xapi.pool.$master.name_label
|
||||
'xen1'
|
||||
```
|
||||
|
||||
You can optionally prefix the address by a protocol: `https://` (default) or `http://`.
|
||||
|
||||
In case of error due to invalid or self-signed certificates you can use the `--allow-unauthorized` flag (or `--au`):
|
||||
|
||||
```
|
||||
> xen-api --au xen1.company.net root
|
||||
```
|
||||
|
||||
To ease searches, `find()` and `findAll()` functions are available:
|
||||
|
||||
```
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "xen-api",
|
||||
"version": "0.26.0",
|
||||
"version": "0.25.2",
|
||||
"license": "ISC",
|
||||
"description": "Connector to the Xen API",
|
||||
"keywords": [
|
||||
|
||||
@@ -168,6 +168,22 @@ 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()
|
||||
@@ -723,28 +739,6 @@ 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) {
|
||||
@@ -942,12 +936,9 @@ export class Xapi extends EventEmitter {
|
||||
|
||||
let result
|
||||
try {
|
||||
// don't use _sessionCall because a session failure should break the
|
||||
// loop and trigger a complete refetch
|
||||
result = await this._call(
|
||||
result = await this._sessionCall(
|
||||
'event.from',
|
||||
[
|
||||
this._sessionId,
|
||||
types,
|
||||
fromToken,
|
||||
EVENT_TIMEOUT + 0.1, // must be float for XML-RPC transport
|
||||
@@ -955,8 +946,7 @@ export class Xapi extends EventEmitter {
|
||||
EVENT_TIMEOUT * 1e3 * 1.1
|
||||
)
|
||||
} catch (error) {
|
||||
const code = error?.code
|
||||
if (code === 'EVENTS_LOST' || code === 'SESSION_INVALID') {
|
||||
if (error?.code === 'EVENTS_LOST') {
|
||||
// eslint-disable-next-line no-labels
|
||||
continue mainLoop
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "xo-server-auth-ldap",
|
||||
"version": "0.6.5",
|
||||
"version": "0.6.4",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "LDAP authentication plugin for XO-Server",
|
||||
"keywords": [
|
||||
|
||||
@@ -234,7 +234,6 @@ 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}`)
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
"node": ">=6"
|
||||
},
|
||||
"dependencies": {
|
||||
"passport-saml": "^1.1.0"
|
||||
"passport-saml": "^1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.0.0",
|
||||
|
||||
@@ -24,10 +24,7 @@ export const configurationSchema = {
|
||||
},
|
||||
usernameField: {
|
||||
title: 'Username field',
|
||||
description: `Field to use as the XO username
|
||||
|
||||
You should try \`http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress\` if you are using Microsoft Azure Active Directory.
|
||||
`,
|
||||
description: 'Field to use as the XO username',
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -142,14 +142,12 @@ const getErrorMarkdown = task => {
|
||||
|
||||
const MARKDOWN_BY_TYPE = {
|
||||
pool(task, { formatDate }) {
|
||||
const { id, pool = {}, poolMaster = {} } = task.data
|
||||
const { pool, poolMaster = {} } = task.data
|
||||
const name = pool.name_label || poolMaster.name_label || UNKNOWN_ITEM
|
||||
|
||||
return {
|
||||
body: [
|
||||
pool.uuid !== undefined
|
||||
? `- **UUID**: ${pool.uuid}`
|
||||
: `- **ID**: ${id}`,
|
||||
`- **UUID**: ${pool.uuid}`,
|
||||
...getTemporalDataMarkdown(task.end, task.start, formatDate),
|
||||
getErrorMarkdown(task),
|
||||
],
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
module.exports = require('../../@xen-orchestra/babel-config')(
|
||||
require('./package.json')
|
||||
)
|
||||
@@ -1,43 +0,0 @@
|
||||
# xo-server-sdn-controller [](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)
|
||||
@@ -1,35 +0,0 @@
|
||||
{
|
||||
"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
|
||||
}
|
||||
@@ -1,830 +0,0 @@
|
||||
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)
|
||||
@@ -1,511 +0,0 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -123,7 +123,7 @@
|
||||
"value-matcher": "^0.2.0",
|
||||
"vhd-lib": "^0.7.0",
|
||||
"ws": "^6.0.0",
|
||||
"xen-api": "^0.26.0",
|
||||
"xen-api": "^0.25.2",
|
||||
"xml2js": "^0.4.19",
|
||||
"xo-acl-resolver": "^0.4.1",
|
||||
"xo-collection": "^0.4.1",
|
||||
|
||||
@@ -211,25 +211,6 @@ emergencyShutdownHost.resolve = {
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export async function isHostServerTimeConsistent({ host }) {
|
||||
try {
|
||||
await this.getXapi(host).assertConsistentHostServerTime(host._xapiRef)
|
||||
return true
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
isHostServerTimeConsistent.params = {
|
||||
host: { type: 'string' },
|
||||
}
|
||||
|
||||
isHostServerTimeConsistent.resolve = {
|
||||
host: ['host', 'host', 'administrate'],
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export function stats({ host, granularity }) {
|
||||
return this.getXapiHostStats(host._xapiId, granularity)
|
||||
}
|
||||
@@ -284,19 +265,3 @@ installSupplementalPack.params = {
|
||||
installSupplementalPack.resolve = {
|
||||
host: ['host', 'host', 'admin'],
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export function isHyperThreadingEnabled({ host }) {
|
||||
return this.getXapi(host).isHyperThreadingEnabled(host._xapiId)
|
||||
}
|
||||
|
||||
isHyperThreadingEnabled.description = 'get hyper-threading information'
|
||||
|
||||
isHyperThreadingEnabled.params = {
|
||||
id: { type: 'string' },
|
||||
}
|
||||
|
||||
isHyperThreadingEnabled.resolve = {
|
||||
host: ['id', 'host', 'administrate'],
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
// TODO: too low level, move into host.
|
||||
|
||||
import { filter, find } from 'lodash'
|
||||
|
||||
import { IPV4_CONFIG_MODES, IPV6_CONFIG_MODES } from '../xapi'
|
||||
|
||||
export function getIpv4ConfigurationModes() {
|
||||
@@ -17,17 +15,7 @@ export function getIpv6ConfigurationModes() {
|
||||
|
||||
async function delete_({ pif }) {
|
||||
// TODO: check if PIF is attached before
|
||||
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)
|
||||
await this.getXapi(pif).callAsync('PIF.destroy', pif._xapiRef)
|
||||
}
|
||||
export { delete_ as delete }
|
||||
|
||||
|
||||
@@ -565,8 +565,6 @@ set.params = {
|
||||
// Identifier of the VM to update.
|
||||
id: { type: 'string' },
|
||||
|
||||
auto_poweron: { type: 'boolean', optional: true },
|
||||
|
||||
name_label: { type: 'string', optional: true },
|
||||
|
||||
name_description: { type: 'string', optional: true },
|
||||
|
||||
@@ -22,8 +22,8 @@ import { forbiddenOperation } from 'xo-common/api-errors'
|
||||
import { Xapi as XapiBase, NULL_REF } from 'xen-api'
|
||||
import {
|
||||
every,
|
||||
filter,
|
||||
find,
|
||||
filter,
|
||||
flatMap,
|
||||
flatten,
|
||||
groupBy,
|
||||
@@ -2136,16 +2136,6 @@ 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)
|
||||
}
|
||||
|
||||
@@ -2346,7 +2336,7 @@ export default class Xapi extends XapiBase {
|
||||
)
|
||||
}
|
||||
|
||||
async assertConsistentHostServerTime(hostRef) {
|
||||
async _assertConsistentHostServerTime(hostRef) {
|
||||
const delta =
|
||||
parseDateTime(await this.call('host.get_servertime', hostRef)).getTime() -
|
||||
Date.now()
|
||||
@@ -2358,27 +2348,4 @@ export default class Xapi extends XapiBase {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
async isHyperThreadingEnabled(hostId) {
|
||||
try {
|
||||
return (
|
||||
(await this.call(
|
||||
'host.call_plugin',
|
||||
this.getObject(hostId).$ref,
|
||||
'hyperthreading.py',
|
||||
'get_hyperthreading',
|
||||
{}
|
||||
)) !== 'false'
|
||||
)
|
||||
} catch (error) {
|
||||
if (
|
||||
error.code === 'XENAPI_MISSING_PLUGIN' ||
|
||||
error.code === 'UNKNOWN_XENAPI_PLUGIN_FUNCTION'
|
||||
) {
|
||||
return null
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import ms from 'ms'
|
||||
import { forEach, isEmpty, iteratee, sortedIndexBy } from 'lodash'
|
||||
import { noSuchObject } from 'xo-common/api-errors'
|
||||
|
||||
const isSkippedError = error =>
|
||||
error.message === 'no disks found' ||
|
||||
noSuchObject.is(error) ||
|
||||
error.message === 'no VMs match this pattern' ||
|
||||
error.message === 'unhealthy VDI chain'
|
||||
|
||||
|
||||
@@ -43,20 +43,6 @@ type MetadataBackupJob = {
|
||||
xoMetadata?: boolean,
|
||||
}
|
||||
|
||||
const logInstantFailureTask = (logger, { data, error, message, parentId }) => {
|
||||
const taskId = logger.notice(message, {
|
||||
data,
|
||||
event: 'task.start',
|
||||
parentId,
|
||||
})
|
||||
logger.error(message, {
|
||||
event: 'task.end',
|
||||
result: serializeError(error),
|
||||
status: 'failure',
|
||||
taskId,
|
||||
})
|
||||
}
|
||||
|
||||
const createSafeReaddir = (handler, methodName) => (path, options) =>
|
||||
handler.list(path, options).catch(error => {
|
||||
if (error?.code !== 'ENOENT') {
|
||||
@@ -111,7 +97,7 @@ const deleteOldBackups = (handler, dir, retention, handleError) =>
|
||||
// Task logs emitted in a metadata backup execution:
|
||||
//
|
||||
// job.start(data: { reportWhen: ReportWhen })
|
||||
// ├─ task.start(data: { type: 'pool', id: string, pool?: <Pool />, poolMaster?: <Host /> })
|
||||
// ├─ task.start(data: { type: 'pool', id: string, pool: <Pool />, poolMaster: <Host /> })
|
||||
// │ ├─ task.start(data: { type: 'remote', id: string })
|
||||
// │ │ └─ task.end
|
||||
// │ └─ task.end
|
||||
@@ -541,15 +527,16 @@ export default class metadataBackup {
|
||||
try {
|
||||
xapi = this._app.getXapi(id)
|
||||
} catch (error) {
|
||||
logInstantFailureTask(logger, {
|
||||
data: {
|
||||
type: 'pool',
|
||||
id,
|
||||
},
|
||||
error,
|
||||
message: `unable to get the xapi associated to the pool (${id})`,
|
||||
parentId: runJobId,
|
||||
})
|
||||
logger.warning(
|
||||
`unable to get the xapi associated to the pool (${id})`,
|
||||
{
|
||||
event: 'task.warning',
|
||||
taskId: runJobId,
|
||||
data: {
|
||||
error,
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
if (xapi !== undefined) {
|
||||
promises.push(
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
import _ from 'intl'
|
||||
import decorate from 'apply-decorators'
|
||||
import Icon from 'icon'
|
||||
import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import Tooltip from 'tooltip'
|
||||
import { injectState, provideState } from 'reaclette'
|
||||
import { isHostTimeConsistentWithXoaTime } from 'xo'
|
||||
|
||||
const InconsistentHostTimeWarning = decorate([
|
||||
provideState({
|
||||
computed: {
|
||||
isHostTimeConsistentWithXoaTime: (_, { hostId }) =>
|
||||
isHostTimeConsistentWithXoaTime(hostId),
|
||||
},
|
||||
}),
|
||||
injectState,
|
||||
({ state: { isHostTimeConsistentWithXoaTime = true } }) =>
|
||||
isHostTimeConsistentWithXoaTime ? null : (
|
||||
<Tooltip content={_('warningHostTimeTooltip')}>
|
||||
<Icon color='text-danger' icon='alarm' />
|
||||
</Tooltip>
|
||||
),
|
||||
])
|
||||
|
||||
InconsistentHostTimeWarning.propTypes = {
|
||||
hostId: PropTypes.string.isRequired,
|
||||
}
|
||||
|
||||
export { InconsistentHostTimeWarning as default }
|
||||
@@ -47,8 +47,6 @@ 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',
|
||||
@@ -220,8 +218,6 @@ const messages = {
|
||||
homeResourceSet: 'Resource set: {resourceSet}',
|
||||
highAvailability: 'High Availability',
|
||||
srSharedType: 'Shared {type}',
|
||||
warningHostTimeTooltip:
|
||||
'Host time and XOA time are not consistent with each other',
|
||||
|
||||
// ----- Home snapshots -----
|
||||
snapshotVmsName: 'Name',
|
||||
@@ -578,9 +574,7 @@ const messages = {
|
||||
newSrNfsDefaultVersion: 'Default NFS version',
|
||||
newSrNfsOptions: 'Comma delimited NFS options',
|
||||
newSrNfs: 'NFS version',
|
||||
noSharedZfsAvailable: 'No shared ZFS available',
|
||||
reattachNewSrTooltip: 'Reattach SR',
|
||||
srLocation: 'Storage location',
|
||||
|
||||
// ------ New Network -----
|
||||
createNewNetworkNoPermission:
|
||||
@@ -682,7 +676,6 @@ const messages = {
|
||||
cloneVmLabel: 'Clone',
|
||||
fastCloneVmLabel: 'Fast clone',
|
||||
vmConsoleLabel: 'Console',
|
||||
backupLabel: 'Backup',
|
||||
|
||||
// ----- SR advanced tab -----
|
||||
|
||||
@@ -799,8 +792,6 @@ const messages = {
|
||||
'RAM: {memoryUsed} used on {memoryTotal} ({memoryFree} free)',
|
||||
hardwareHostSettingsLabel: 'Hardware',
|
||||
hyperThreading: 'Hyper-threading (SMT)',
|
||||
hyperThreadingNotAvailable:
|
||||
'HT detection is only available on XCP-ng 7.6 and higher',
|
||||
hostAddress: 'Address',
|
||||
hostStatus: 'Status',
|
||||
hostBuildNumber: 'Build number',
|
||||
@@ -808,7 +799,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}}',
|
||||
'{nActives, number} of {nPaths, number} path{nPaths, plural, one {} other {s}} ({ nSessions, number } iSCSI session{nSessions, 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:
|
||||
@@ -864,7 +855,6 @@ const messages = {
|
||||
// ----- Host storage tabs -----
|
||||
addSrDeviceButton: 'Add a storage',
|
||||
srType: 'Type',
|
||||
pbdDetails: 'PBD details',
|
||||
pbdStatus: 'Status',
|
||||
pbdStatusConnected: 'Connected',
|
||||
pbdStatusDisconnected: 'Disconnected',
|
||||
@@ -1037,7 +1027,6 @@ const messages = {
|
||||
vifMacLabel: 'MAC address',
|
||||
vifMtuLabel: 'MTU',
|
||||
vifNetworkLabel: 'Network',
|
||||
vifRateLimitLabel: 'Rate limit (kB/s)',
|
||||
vifStatusLabel: 'Status',
|
||||
vifStatusConnected: 'Connected',
|
||||
vifStatusDisconnected: 'Disconnected',
|
||||
@@ -1720,13 +1709,11 @@ 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',
|
||||
|
||||
@@ -53,7 +53,3 @@ export const setXoaConfiguration = createAction(
|
||||
'XOA_CONFIGURATION',
|
||||
configuration => configuration
|
||||
)
|
||||
export const setHomeVmIdsSelection = createAction(
|
||||
'SET_HOME_VM_IDS_SELECTION',
|
||||
homeVmIdsSelection => homeVmIdsSelection
|
||||
)
|
||||
|
||||
@@ -86,12 +86,6 @@ export default {
|
||||
}
|
||||
),
|
||||
|
||||
// These IDs are used temporarily to be preselected in backup-ng/new/vms
|
||||
homeVmIdsSelection: combineActionHandlers([], {
|
||||
[actions.setHomeVmIdsSelection]: (_, homeVmIdsSelection) =>
|
||||
homeVmIdsSelection,
|
||||
}),
|
||||
|
||||
objects: combineActionHandlers(
|
||||
{
|
||||
all: {}, // Mutable for performance!
|
||||
|
||||
@@ -58,12 +58,6 @@ 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]),
|
||||
|
||||
@@ -646,14 +646,10 @@ export const createCompare = criterias => (...items) => {
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export const hasLicenseRestrictions = host => {
|
||||
const licenseType = host.license_params.sku_type
|
||||
return (
|
||||
host.productBrand !== 'XCP-ng' &&
|
||||
versionSatisfies(host.version, '>=7.3.0') &&
|
||||
(licenseType === 'free' || licenseType === 'express')
|
||||
)
|
||||
}
|
||||
export const hasLicenseRestrictions = host =>
|
||||
host.productBrand !== 'XCP-ng' &&
|
||||
versionSatisfies(host.version, '>=7.3.0') &&
|
||||
host.license_params.sku_type === 'free'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
|
||||
@@ -777,14 +777,6 @@ export const emergencyShutdownHosts = hosts => {
|
||||
}).then(() => map(hosts, host => emergencyShutdownHost(host)), noop)
|
||||
}
|
||||
|
||||
export const isHostTimeConsistentWithXoaTime = host =>
|
||||
_call('host.isHostServerTimeConsistent', { host: resolveId(host) })
|
||||
|
||||
export const isHyperThreadingEnabledHost = host =>
|
||||
_call('host.isHyperThreadingEnabled', {
|
||||
id: resolveId(host),
|
||||
})
|
||||
|
||||
// for XCP-ng now
|
||||
export const installAllPatchesOnHost = ({ host }) =>
|
||||
confirm({
|
||||
@@ -1628,15 +1620,14 @@ export const deleteVifs = vifs =>
|
||||
|
||||
export const setVif = (
|
||||
vif,
|
||||
{ allowedIpv4Addresses, allowedIpv6Addresses, mac, network, rateLimit }
|
||||
{ network, mac, allowedIpv4Addresses, allowedIpv6Addresses }
|
||||
) =>
|
||||
_call('vif.set', {
|
||||
id: resolveId(vif),
|
||||
network: resolveId(network),
|
||||
mac,
|
||||
allowedIpv4Addresses,
|
||||
allowedIpv6Addresses,
|
||||
id: resolveId(vif),
|
||||
mac,
|
||||
network: resolveId(network),
|
||||
rateLimit,
|
||||
})
|
||||
|
||||
// Network -----------------------------------------------------------
|
||||
@@ -1648,8 +1639,6 @@ 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({
|
||||
@@ -2294,8 +2283,6 @@ export const probeSrHba = host => _call('sr.probeHba', { host })
|
||||
export const probeSrHbaExists = (host, scsiId) =>
|
||||
_call('sr.probeHbaExists', { host, scsiId })
|
||||
|
||||
export const probeZfs = host => _call('sr.probeZfs', { host: resolveId(host) })
|
||||
|
||||
export const reattachSr = (host, uuid, nameLabel, nameDescription, type) =>
|
||||
_call('sr.reattach', { host, uuid, nameLabel, nameDescription, type })
|
||||
|
||||
@@ -2359,14 +2346,6 @@ export const createSrLvm = (host, nameLabel, nameDescription, device) =>
|
||||
export const createSrExt = (host, nameLabel, nameDescription, device) =>
|
||||
_call('sr.createExt', { host, nameLabel, nameDescription, device })
|
||||
|
||||
export const createSrZfs = (host, nameLabel, nameDescription, location) =>
|
||||
_call('sr.createFile', {
|
||||
host: resolveId(host),
|
||||
nameDescription,
|
||||
nameLabel,
|
||||
location,
|
||||
})
|
||||
|
||||
// Job logs ----------------------------------------------------------
|
||||
|
||||
export const deleteJobsLogs = async ids => {
|
||||
|
||||
@@ -120,31 +120,28 @@ const createDoesRetentionExist = name => {
|
||||
return ({ propSettings, settings = propSettings }) => settings.some(predicate)
|
||||
}
|
||||
|
||||
const getInitialState = ({ preSelectedVmIds, setHomeVmIdsSelection }) => {
|
||||
setHomeVmIdsSelection([]) // Clear preselected vmIds
|
||||
return {
|
||||
_displayAdvancedSettings: undefined,
|
||||
_vmsPattern: undefined,
|
||||
backupMode: false,
|
||||
compression: undefined,
|
||||
crMode: false,
|
||||
deltaMode: false,
|
||||
drMode: false,
|
||||
name: '',
|
||||
paramsUpdated: false,
|
||||
remotes: [],
|
||||
schedules: {},
|
||||
settings: undefined,
|
||||
showErrors: false,
|
||||
smartMode: false,
|
||||
snapshotMode: false,
|
||||
srs: [],
|
||||
tags: {
|
||||
notValues: ['Continuous Replication', 'Disaster Recovery', 'XOSAN'],
|
||||
},
|
||||
vms: preSelectedVmIds,
|
||||
}
|
||||
}
|
||||
const getInitialState = () => ({
|
||||
_displayAdvancedSettings: undefined,
|
||||
_vmsPattern: undefined,
|
||||
backupMode: false,
|
||||
compression: undefined,
|
||||
crMode: false,
|
||||
deltaMode: false,
|
||||
drMode: false,
|
||||
name: '',
|
||||
paramsUpdated: false,
|
||||
remotes: [],
|
||||
schedules: {},
|
||||
settings: undefined,
|
||||
showErrors: false,
|
||||
smartMode: false,
|
||||
snapshotMode: false,
|
||||
srs: [],
|
||||
tags: {
|
||||
notValues: ['Continuous Replication', 'Disaster Recovery', 'XOSAN'],
|
||||
},
|
||||
vms: [],
|
||||
})
|
||||
|
||||
const DeleteOldBackupsFirst = ({ handler, handlerParam, value }) => (
|
||||
<ActionButton
|
||||
@@ -172,7 +169,6 @@ export default decorate([
|
||||
hostsById: createGetObjectsOfType('host'),
|
||||
poolsById: createGetObjectsOfType('pool'),
|
||||
srsById: createGetObjectsOfType('SR'),
|
||||
preSelectedVmIds: state => state.homeVmIdsSelection,
|
||||
})),
|
||||
injectIntl,
|
||||
provideState({
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import _ from 'intl'
|
||||
import Component from 'base-component'
|
||||
import InconsistentHostTimeWarning from 'inconsistent-host-time-warning'
|
||||
import Ellipsis, { EllipsisContainer } from 'ellipsis'
|
||||
import Icon from 'icon'
|
||||
import isEmpty from 'lodash/isEmpty'
|
||||
@@ -129,8 +128,6 @@ export default class HostItem extends Component {
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
<InconsistentHostTimeWarning hostId={host.id} />
|
||||
|
||||
{hasLicenseRestrictions(host) && <LicenseWarning />}
|
||||
</EllipsisContainer>
|
||||
</Col>
|
||||
|
||||
@@ -178,14 +178,6 @@ const OPTIONS = {
|
||||
icon: 'vm-snapshot',
|
||||
labelId: 'snapshotVmLabel',
|
||||
},
|
||||
{
|
||||
handler: (vmIds, _, { setHomeVmIdsSelection }, { router }) => {
|
||||
setHomeVmIdsSelection(vmIds)
|
||||
router.push('backup-ng/new/vms')
|
||||
},
|
||||
icon: 'backup',
|
||||
labelId: 'backupLabel',
|
||||
},
|
||||
{
|
||||
handler: deleteVms,
|
||||
icon: 'vm-delete',
|
||||
@@ -1018,9 +1010,7 @@ export default class Home extends Component {
|
||||
onClick={() => {
|
||||
action.handler(
|
||||
this._getSelectedItemsIds(),
|
||||
action.params,
|
||||
this.props,
|
||||
this.context
|
||||
action.params
|
||||
)
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import _ from 'intl'
|
||||
import InconsistentHostTimeWarning from 'inconsistent-host-time-warning'
|
||||
import Copiable from 'copiable'
|
||||
import HostActionBar from './action-bar'
|
||||
import Icon from 'icon'
|
||||
@@ -255,8 +254,6 @@ export default class Host extends Component {
|
||||
</Link>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
<InconsistentHostTimeWarning hostId={host.id} />
|
||||
</h2>
|
||||
<Copiable tagName='pre' className='text-muted mb-0'>
|
||||
{host.uuid}
|
||||
|
||||
@@ -21,7 +21,6 @@ import {
|
||||
disableHost,
|
||||
enableHost,
|
||||
forgetHost,
|
||||
isHyperThreadingEnabledHost,
|
||||
installSupplementalPack,
|
||||
restartHost,
|
||||
setHostsMultipathing,
|
||||
@@ -55,7 +54,6 @@ const MultipathableSrs = decorate([
|
||||
<Container>
|
||||
{map(pbds, pbd => {
|
||||
const [nActives, nPaths] = getIscsiPaths(pbd)
|
||||
const nSessions = pbd.otherConfig.iscsi_sessions
|
||||
return (
|
||||
<Row key={pbd.id}>
|
||||
<Col>
|
||||
@@ -65,8 +63,8 @@ const MultipathableSrs = decorate([
|
||||
_('hostMultipathingPaths', {
|
||||
nActives,
|
||||
nPaths,
|
||||
})}{' '}
|
||||
{nSessions !== undefined && _('iscsiSessions', { nSessions })}
|
||||
nSessions: pbd.otherConfig.iscsi_sessions,
|
||||
})}
|
||||
</Col>
|
||||
</Row>
|
||||
)
|
||||
@@ -97,12 +95,6 @@ MultipathableSrs.propTypes = {
|
||||
}
|
||||
})
|
||||
export default class extends Component {
|
||||
async componentDidMount() {
|
||||
this.setState({
|
||||
isHtEnabled: await isHyperThreadingEnabledHost(this.props.host),
|
||||
})
|
||||
}
|
||||
|
||||
_getPacks = createSelector(
|
||||
() => this.props.host.supplementalPacks,
|
||||
packs => {
|
||||
@@ -120,12 +112,14 @@ export default class extends Component {
|
||||
return uniqPacks
|
||||
}
|
||||
)
|
||||
|
||||
_isHtEnabled = createSelector(
|
||||
() => this.props.host.CPUs.flags,
|
||||
flags => /\bht\b/.test(flags)
|
||||
)
|
||||
_setRemoteSyslogHost = value => setRemoteSyslogHost(this.props.host, value)
|
||||
|
||||
render() {
|
||||
const { host, pcis, pgpus } = this.props
|
||||
const { isHtEnabled } = this.state
|
||||
return (
|
||||
<Container>
|
||||
<Row>
|
||||
@@ -285,9 +279,7 @@ export default class extends Component {
|
||||
<tr>
|
||||
<th>{_('hyperThreading')}</th>
|
||||
<td>
|
||||
{isHtEnabled === null
|
||||
? _('hyperThreadingNotAvailable')
|
||||
: isHtEnabled
|
||||
{this._isHtEnabled()
|
||||
? _('stateEnabled')
|
||||
: _('stateDisabled')}
|
||||
</td>
|
||||
|
||||
@@ -87,19 +87,6 @@ const SR_COLUMNS = [
|
||||
storage.shared ? _('srShared') : _('srNotShared'),
|
||||
sortCriteria: 'shared',
|
||||
},
|
||||
{
|
||||
name: _('pbdDetails'),
|
||||
itemRenderer: ({ pbdDeviceConfig }) => {
|
||||
const keys = Object.keys(pbdDeviceConfig)
|
||||
return (
|
||||
<ul className='list-unstyled'>
|
||||
{keys.map(key => (
|
||||
<li key={key}>{_.keyValue(key, pbdDeviceConfig[key])}</li>
|
||||
))}
|
||||
</ul>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: _('pbdStatus'),
|
||||
itemRenderer: storage => (
|
||||
@@ -150,7 +137,6 @@ export default connectStore(() => {
|
||||
|
||||
return {
|
||||
attached: pbd.attached,
|
||||
pbdDeviceConfig: pbd.device_config,
|
||||
format: sr.SR_type,
|
||||
free: size > 0 ? size - usage : 0,
|
||||
id: sr.id,
|
||||
|
||||
@@ -1175,8 +1175,10 @@ export default class NewVm extends BaseComponent {
|
||||
</LineItem>
|
||||
<br />
|
||||
<LineItem>
|
||||
<Tooltip content={CAN_CLOUD_INIT ? undefined : _('premiumOnly')}>
|
||||
<label>
|
||||
<label>
|
||||
<Tooltip
|
||||
content={CAN_CLOUD_INIT ? undefined : _('premiumOnly')}
|
||||
>
|
||||
<input
|
||||
checked={installMethod === 'SSH'}
|
||||
disabled={!CAN_CLOUD_INIT}
|
||||
@@ -1185,10 +1187,10 @@ export default class NewVm extends BaseComponent {
|
||||
type='radio'
|
||||
value='SSH'
|
||||
/>
|
||||
|
||||
{_('newVmSshKey')}
|
||||
</label>
|
||||
</Tooltip>
|
||||
</Tooltip>
|
||||
|
||||
{_('newVmSshKey')}
|
||||
</label>
|
||||
|
||||
<span className={classNames('input-group', styles.fixedWidth)}>
|
||||
<DebounceInput
|
||||
@@ -1216,8 +1218,10 @@ export default class NewVm extends BaseComponent {
|
||||
</LineItem>
|
||||
<br />
|
||||
<LineItem>
|
||||
<Tooltip content={CAN_CLOUD_INIT ? undefined : _('premiumOnly')}>
|
||||
<label>
|
||||
<label>
|
||||
<Tooltip
|
||||
content={CAN_CLOUD_INIT ? undefined : _('premiumOnly')}
|
||||
>
|
||||
<input
|
||||
checked={installMethod === 'customConfig'}
|
||||
disabled={!CAN_CLOUD_INIT}
|
||||
@@ -1226,10 +1230,10 @@ export default class NewVm extends BaseComponent {
|
||||
type='radio'
|
||||
value='customConfig'
|
||||
/>
|
||||
|
||||
{_('newVmCustomConfig')}
|
||||
</label>
|
||||
</Tooltip>
|
||||
</Tooltip>
|
||||
|
||||
{_('newVmCustomConfig')}
|
||||
</label>
|
||||
|
||||
<AvailableTemplateVars />
|
||||
|
||||
|
||||
@@ -4,14 +4,8 @@ import decorate from 'apply-decorators'
|
||||
import PropTypes from 'prop-types'
|
||||
import React, { Component } from 'react'
|
||||
import Wizard, { Section } from 'wizard'
|
||||
import { addSubscriptions, connectStore } from 'utils'
|
||||
import {
|
||||
createBondedNetwork,
|
||||
createNetwork,
|
||||
createPrivateNetwork,
|
||||
getBondModes,
|
||||
subscribePlugins,
|
||||
} from 'xo'
|
||||
import { connectStore } from 'utils'
|
||||
import { createBondedNetwork, createNetwork, getBondModes } from 'xo'
|
||||
import { createGetObject, getIsPoolAdmin } from 'selectors'
|
||||
import { injectIntl } from 'react-intl'
|
||||
import { injectState, provideState } from 'reaclette'
|
||||
@@ -27,8 +21,6 @@ const EMPTY = {
|
||||
bonded: false,
|
||||
bondMode: undefined,
|
||||
description: '',
|
||||
encapsulation: 'gre',
|
||||
isPrivate: false,
|
||||
mtu: '',
|
||||
name: '',
|
||||
pif: undefined,
|
||||
@@ -37,9 +29,6 @@ const EMPTY = {
|
||||
}
|
||||
|
||||
const NewNetwork = decorate([
|
||||
addSubscriptions({
|
||||
plugins: subscribePlugins,
|
||||
}),
|
||||
connectStore(() => ({
|
||||
isPoolAdmin: getIsPoolAdmin,
|
||||
pool: createGetObject((_, props) => props.location.query.pool),
|
||||
@@ -53,26 +42,11 @@ const NewNetwork = decorate([
|
||||
onChangeMode: (_, bondMode) => ({ bondMode }),
|
||||
onChangePif: (_, value) => ({ bonded }) =>
|
||||
bonded ? { pifs: value } : { pif: value },
|
||||
onChangeEncapsulation(_, encapsulation) {
|
||||
return { encapsulation: encapsulation.value }
|
||||
},
|
||||
reset: () => EMPTY,
|
||||
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,
|
||||
}
|
||||
},
|
||||
toggleBonded: () => ({ bonded }) => ({
|
||||
...EMPTY,
|
||||
bonded: !bonded,
|
||||
}),
|
||||
},
|
||||
computed: {
|
||||
modeOptions: ({ bondModes }) =>
|
||||
@@ -84,10 +58,6 @@ 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,
|
||||
@@ -101,9 +71,7 @@ const NewNetwork = decorate([
|
||||
const {
|
||||
bonded,
|
||||
bondMode,
|
||||
isPrivate,
|
||||
description,
|
||||
encapsulation,
|
||||
mtu,
|
||||
name,
|
||||
pif,
|
||||
@@ -120,13 +88,6 @@ const NewNetwork = decorate([
|
||||
pool: pool.id,
|
||||
vlan,
|
||||
})
|
||||
: isPrivate
|
||||
? createPrivateNetwork({
|
||||
poolId: pool.id,
|
||||
networkName: name,
|
||||
networkDescription: description,
|
||||
encapsulation: encapsulation,
|
||||
})
|
||||
: createNetwork({
|
||||
description,
|
||||
mtu,
|
||||
@@ -171,9 +132,7 @@ const NewNetwork = decorate([
|
||||
const {
|
||||
bonded,
|
||||
bondMode,
|
||||
isPrivate,
|
||||
description,
|
||||
encapsulation,
|
||||
modeOptions,
|
||||
mtu,
|
||||
name,
|
||||
@@ -181,7 +140,6 @@ const NewNetwork = decorate([
|
||||
pifPredicate,
|
||||
pifs,
|
||||
vlan,
|
||||
isSdnControllerLoaded,
|
||||
} = state
|
||||
const { formatMessage } = intl
|
||||
return (
|
||||
@@ -194,112 +152,69 @@ 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'>
|
||||
{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>
|
||||
)}
|
||||
<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'>
|
||||
|
||||
@@ -33,7 +33,6 @@ import {
|
||||
createSrLvm,
|
||||
createSrNfs,
|
||||
createSrHba,
|
||||
createSrZfs,
|
||||
probeSrIscsiExists,
|
||||
probeSrIscsiIqns,
|
||||
probeSrIscsiLuns,
|
||||
@@ -41,7 +40,6 @@ import {
|
||||
probeSrNfsExists,
|
||||
probeSrHba,
|
||||
probeSrHbaExists,
|
||||
probeZfs,
|
||||
reattachSrIso,
|
||||
reattachSr,
|
||||
} from 'xo'
|
||||
@@ -198,15 +196,14 @@ class SelectLun extends Component {
|
||||
// ===================================================================
|
||||
|
||||
const SR_TYPE_TO_LABEL = {
|
||||
ext: 'ext (local)',
|
||||
ext: 'Local Ext',
|
||||
hba: 'HBA',
|
||||
iscsi: 'iSCSI',
|
||||
local: 'Local',
|
||||
lvm: 'LVM (local)',
|
||||
lvm: 'Local LVM',
|
||||
nfs: 'NFS',
|
||||
nfsiso: 'NFS ISO',
|
||||
smb: 'SMB',
|
||||
zfs: 'ZFS (local)',
|
||||
}
|
||||
|
||||
const SR_GROUP_TO_LABEL = {
|
||||
@@ -215,7 +212,7 @@ const SR_GROUP_TO_LABEL = {
|
||||
}
|
||||
|
||||
const typeGroups = {
|
||||
vdisr: ['ext', 'hba', 'iscsi', 'lvm', 'nfs', 'zfs'],
|
||||
vdisr: ['ext', 'hba', 'iscsi', 'lvm', 'nfs'],
|
||||
isosr: ['local', 'nfsiso', 'smb'],
|
||||
}
|
||||
|
||||
@@ -253,7 +250,6 @@ export default class New extends Component {
|
||||
unused: undefined,
|
||||
usage: undefined,
|
||||
used: undefined,
|
||||
zfsPools: undefined,
|
||||
}
|
||||
this.getHostSrs = createFilter(
|
||||
() => this.props.srs,
|
||||
@@ -275,7 +271,6 @@ export default class New extends Component {
|
||||
port,
|
||||
server,
|
||||
username,
|
||||
zfsLocation,
|
||||
} = this.refs
|
||||
const {
|
||||
host,
|
||||
@@ -349,8 +344,6 @@ export default class New extends Component {
|
||||
createSrLvm(host.id, name.value, description.value, device.value),
|
||||
ext: () =>
|
||||
createSrExt(host.id, name.value, description.value, device.value),
|
||||
zfs: () =>
|
||||
createSrZfs(host.id, name.value, description.value, zfsLocation.value),
|
||||
local: () =>
|
||||
createSrIso(
|
||||
host.id,
|
||||
@@ -388,10 +381,7 @@ export default class New extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
_handleSrHostSelection = async host => {
|
||||
this.setState({ host })
|
||||
await this._probe(host, this.state.type)
|
||||
}
|
||||
_handleSrHostSelection = host => this.setState({ host })
|
||||
_handleNameChange = event => this.setState({ name: event.target.value })
|
||||
_handleDescriptionChange = event =>
|
||||
this.setState({ description: event.target.value })
|
||||
@@ -402,13 +392,20 @@ export default class New extends Component {
|
||||
hbaDevices: undefined,
|
||||
iqns: undefined,
|
||||
paths: undefined,
|
||||
summary: includes(['ext', 'lvm', 'local', 'smb', 'hba', 'zfs'], type),
|
||||
summary: includes(['ext', 'lvm', 'local', 'smb', 'hba'], type),
|
||||
type,
|
||||
unused: undefined,
|
||||
usage: undefined,
|
||||
used: undefined,
|
||||
})
|
||||
await this._probe(this.state.host, type)
|
||||
if (type === 'hba' && this.state.host !== undefined) {
|
||||
this.setState(({ loading }) => ({ loading: loading + 1 }))
|
||||
const hbaDevices = await probeSrHba(this.state.host.id)::ignoreErrors()
|
||||
this.setState(({ loading }) => ({
|
||||
hbaDevices,
|
||||
loading: loading - 1,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
_handleSrHbaSelection = async scsiId => {
|
||||
@@ -545,25 +542,6 @@ export default class New extends Component {
|
||||
})
|
||||
}
|
||||
|
||||
_probe = async (host, type) => {
|
||||
const probeMethodFactories = {
|
||||
hba: async hostId => ({
|
||||
hbaDevices: await probeSrHba(hostId)::ignoreErrors(),
|
||||
}),
|
||||
zfs: async hostId => ({
|
||||
zfsPools: await probeZfs(hostId)::ignoreErrors(),
|
||||
}),
|
||||
}
|
||||
if (probeMethodFactories[type] !== undefined && host != null) {
|
||||
this.setState(({ loading }) => ({ loading: loading + 1 }))
|
||||
const probeResult = await probeMethodFactories[type](host.id)
|
||||
this.setState(({ loading }) => ({
|
||||
loading: loading - 1,
|
||||
...probeResult,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
_reattach = async uuid => {
|
||||
const { host, type } = this.state
|
||||
|
||||
@@ -616,7 +594,6 @@ export default class New extends Component {
|
||||
unused,
|
||||
usage,
|
||||
used,
|
||||
zfsPools,
|
||||
} = this.state
|
||||
const { formatMessage } = this.props.intl
|
||||
|
||||
@@ -915,31 +892,6 @@ export default class New extends Component {
|
||||
/>
|
||||
</fieldset>
|
||||
)}
|
||||
{type === 'zfs' && (
|
||||
<fieldset>
|
||||
<label htmlFor='selectSrLocation'>
|
||||
{_('srLocation')}
|
||||
</label>
|
||||
<select
|
||||
className='form-control'
|
||||
defaultValue=''
|
||||
id='selectSrLocation'
|
||||
ref='zfsLocation'
|
||||
required
|
||||
>
|
||||
<option value=''>
|
||||
{isEmpty(zfsPools)
|
||||
? formatMessage(messages.noSharedZfsAvailable)
|
||||
: formatMessage(messages.noSelectedValue)}
|
||||
</option>
|
||||
{map(zfsPools, (pool, poolName) => (
|
||||
<option key={poolName} value={pool.mountpoint}>
|
||||
{poolName} - {pool.mountpoint}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</fieldset>
|
||||
)}
|
||||
</fieldset>
|
||||
)}
|
||||
{loading !== 0 && <Icon icon='loading' />}
|
||||
|
||||
@@ -55,19 +55,6 @@ const HOST_COLUMNS = [
|
||||
},
|
||||
sortCriteria: (pbd, hosts) => hosts[pbd.host].name_description,
|
||||
},
|
||||
{
|
||||
name: _('pbdDetails'),
|
||||
itemRenderer: ({ device_config: deviceConfig }) => {
|
||||
const keys = Object.keys(deviceConfig)
|
||||
return (
|
||||
<ul className='list-unstyled'>
|
||||
{keys.map(key => (
|
||||
<li key={key}>{_.keyValue(key, deviceConfig[key])}</li>
|
||||
))}
|
||||
</ul>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: _('pbdStatus'),
|
||||
itemRenderer: pbd => (
|
||||
@@ -111,17 +98,14 @@ const HOST_WITH_PATHS_COLUMNS = [
|
||||
}
|
||||
|
||||
const [nActives, nPaths] = getIscsiPaths(pbd)
|
||||
const nSessions = pbd.otherConfig.iscsi_sessions
|
||||
return (
|
||||
<span>
|
||||
{nActives !== undefined &&
|
||||
nPaths !== undefined &&
|
||||
_('hostMultipathingPaths', {
|
||||
nActives,
|
||||
nPaths,
|
||||
})}{' '}
|
||||
{nSessions !== undefined && _('iscsiSessions', { nSessions })}
|
||||
</span>
|
||||
nActives !== undefined &&
|
||||
nPaths !== undefined &&
|
||||
_('hostMultipathingPaths', {
|
||||
nActives,
|
||||
nPaths,
|
||||
nSessions: pbd.otherConfig.iscsi_sessions,
|
||||
})
|
||||
)
|
||||
},
|
||||
sortCriteria: (pbd, hosts) => get(() => hosts[pbd.host].multipathing),
|
||||
|
||||
@@ -13,7 +13,7 @@ import Tooltip from 'tooltip'
|
||||
import { isIp, isIpV4 } from 'ip-utils'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import { injectIntl } from 'react-intl'
|
||||
import { Number, Text, XoSelect } from 'editable'
|
||||
import { XoSelect, Text } from 'editable'
|
||||
import {
|
||||
addSubscriptions,
|
||||
connectStore,
|
||||
@@ -331,17 +331,6 @@ const COLUMNS = [
|
||||
name: _('vifNetworkLabel'),
|
||||
sortCriteria: (vif, userData) => userData.networks[vif.$network].name_label,
|
||||
},
|
||||
{
|
||||
itemRenderer: ({ id, rateLimit }) => (
|
||||
<Number
|
||||
nullable
|
||||
onChange={rateLimit => setVif(id, { rateLimit })}
|
||||
value={rateLimit === undefined ? '' : rateLimit}
|
||||
/>
|
||||
),
|
||||
name: _('vifRateLimitLabel'),
|
||||
sortCriteria: 'rateLimit',
|
||||
},
|
||||
{
|
||||
component: VifAllowedIps,
|
||||
name: _('vifAllowedIps'),
|
||||
|
||||
27
yarn.lock
27
yarn.lock
@@ -2,7 +2,7 @@
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
"@babel/cli@^7.0.0", "@babel/cli@^7.1.5", "@babel/cli@^7.4.4":
|
||||
"@babel/cli@^7.0.0", "@babel/cli@^7.1.5":
|
||||
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.4.4":
|
||||
"@babel/core@^7.0.0", "@babel/core@^7.1.0", "@babel/core@^7.1.5":
|
||||
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.4.4":
|
||||
"@babel/preset-env@^7.0.0", "@babel/preset-env@^7.1.5":
|
||||
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.2.0:
|
||||
cross-env@^5.1.1, cross-env@^5.1.3, cross-env@^5.1.4:
|
||||
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.22.1:
|
||||
"moment@>= 2.9.0", moment@^2.10.6, moment@^2.20.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,14 +9682,6 @@ 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"
|
||||
@@ -10464,7 +10456,7 @@ passport-oauth2@1.x.x:
|
||||
uid2 "0.0.x"
|
||||
utils-merge "1.x.x"
|
||||
|
||||
passport-saml@^1.1.0:
|
||||
passport-saml@^1.0.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/passport-saml/-/passport-saml-1.1.0.tgz#0fdd639b905b8f5db172bc07f2e73819a043c321"
|
||||
integrity sha512-T2dKp++HQDg9dyJvb+5dyhIVVdIb5FX6DKMex0RALU16D65fiIbZ01vKZ2qtD+nW5nSOkbkfKJUolr9U689EdQ==
|
||||
@@ -10894,13 +10886,6 @@ 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"
|
||||
|
||||
Reference in New Issue
Block a user