Compare commits
2 Commits
xo-web-v5.
...
pierre-vm-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0fad24d757 | ||
|
|
3f0878940f |
45
CHANGELOG.md
45
CHANGELOG.md
@@ -4,47 +4,16 @@
|
||||
|
||||
### Enhancements
|
||||
|
||||
### Bug fixes
|
||||
|
||||
### Released packages
|
||||
|
||||
- xo-server v5.47.0
|
||||
- xo-web v5.47.0
|
||||
|
||||
## **5.37.1** (2019-08-06)
|
||||
|
||||

|
||||
|
||||
### Enhancements
|
||||
|
||||
- [SDN Controller] Let the user choose on which PIF to create a private network (PR [#4379](https://github.com/vatesfr/xen-orchestra/pull/4379))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- [SDN Controller] Better detect host shutting down to adapt network topology (PR [#4314](https://github.com/vatesfr/xen-orchestra/pull/4314))
|
||||
- [SDN Controller] Add new hosts to pool's private networks (PR [#4382](https://github.com/vatesfr/xen-orchestra/pull/4382))
|
||||
|
||||
### Released packages
|
||||
|
||||
- xo-server-sdn-controller v0.1.2
|
||||
|
||||
## **5.37.0** (2019-07-25)
|
||||
|
||||
### Highlights
|
||||
|
||||
- [Pool] Ability to add multiple hosts on the pool [#2402](https://github.com/vatesfr/xen-orchestra/issues/2402) (PR [#3716](https://github.com/vatesfr/xen-orchestra/pull/3716))
|
||||
- [SR/General] Improve SR usage graph [#3608](https://github.com/vatesfr/xen-orchestra/issues/3608) (PR [#3830](https://github.com/vatesfr/xen-orchestra/pull/3830))
|
||||
- [VM] Permission to revert to any snapshot for VM operators [#3928](https://github.com/vatesfr/xen-orchestra/issues/3928) (PR [#4247](https://github.com/vatesfr/xen-orchestra/pull/4247))
|
||||
- [Backup NG] Ability to bypass unhealthy VDI chains check [#4324](https://github.com/vatesfr/xen-orchestra/issues/4324) (PR [#4340](https://github.com/vatesfr/xen-orchestra/pull/4340))
|
||||
- [VM/console] Multiline copy/pasting [#4261](https://github.com/vatesfr/xen-orchestra/issues/4261) (PR [#4341](https://github.com/vatesfr/xen-orchestra/pull/4341))
|
||||
|
||||
### Enhancements
|
||||
|
||||
- [Stats] Ability to display last day stats [#4160](https://github.com/vatesfr/xen-orchestra/issues/4160) (PR [#4168](https://github.com/vatesfr/xen-orchestra/pull/4168))
|
||||
- [Settings/servers] Display servers connection issues [#4300](https://github.com/vatesfr/xen-orchestra/issues/4300) (PR [#4310](https://github.com/vatesfr/xen-orchestra/pull/4310))
|
||||
- [VM] Permission to revert to any snapshot for VM operators [#3928](https://github.com/vatesfr/xen-orchestra/issues/3928) (PR [#4247](https://github.com/vatesfr/xen-orchestra/pull/4247))
|
||||
- [VM] Show current operations and progress [#3811](https://github.com/vatesfr/xen-orchestra/issues/3811) (PR [#3982](https://github.com/vatesfr/xen-orchestra/pull/3982))
|
||||
- [SR/General] Improve SR usage graph [#3608](https://github.com/vatesfr/xen-orchestra/issues/3608) (PR [#3830](https://github.com/vatesfr/xen-orchestra/pull/3830))
|
||||
- [Backup NG/New] Generate default schedule if no schedule is specified [#4036](https://github.com/vatesfr/xen-orchestra/issues/4036) (PR [#4183](https://github.com/vatesfr/xen-orchestra/pull/4183))
|
||||
- [Host/Advanced] Ability to edit iSCSI IQN [#4048](https://github.com/vatesfr/xen-orchestra/issues/4048) (PR [#4208](https://github.com/vatesfr/xen-orchestra/pull/4208))
|
||||
- [Backup NG] Ability to bypass unhealthy VDI chains check [#4324](https://github.com/vatesfr/xen-orchestra/issues/4324) (PR [#4340](https://github.com/vatesfr/xen-orchestra/pull/4340))
|
||||
- [Pool] Ability to add multiple hosts on the pool [#2402](https://github.com/vatesfr/xen-orchestra/issues/2402) (PR [#3716](https://github.com/vatesfr/xen-orchestra/pull/3716))
|
||||
- [VM/console] Multiline copy/pasting [#4261](https://github.com/vatesfr/xen-orchestra/issues/4261) (PR [#4341](https://github.com/vatesfr/xen-orchestra/pull/4341))
|
||||
- [VM,host] Improved state icons/pills (colors and tooltips) (PR [#4363](https://github.com/vatesfr/xen-orchestra/pull/4363))
|
||||
|
||||
### Bug fixes
|
||||
@@ -70,7 +39,7 @@
|
||||
|
||||
## **5.36.0** (2019-06-27)
|
||||
|
||||

|
||||

|
||||
|
||||
### Highlights
|
||||
|
||||
@@ -109,6 +78,8 @@
|
||||
|
||||
## **5.35.0** (2019-05-29)
|
||||
|
||||

|
||||
|
||||
### Enhancements
|
||||
|
||||
- [VM/general] Display 'Started... ago' instead of 'Halted... ago' for paused state [#3750](https://github.com/vatesfr/xen-orchestra/issues/3750) (PR [#4170](https://github.com/vatesfr/xen-orchestra/pull/4170))
|
||||
|
||||
@@ -7,17 +7,13 @@
|
||||
|
||||
> Users must be able to say: “Nice enhancement, I'm eager to test it”
|
||||
|
||||
- [VM/Attach disk] Display confirmation modal when VDI is already attached [#3381](https://github.com/vatesfr/xen-orchestra/issues/3381) (PR [#4366](https://github.com/vatesfr/xen-orchestra/pull/4366))
|
||||
- [Zstd]
|
||||
- [VM/copy, VM/export] Only show zstd option when it's supported [#3892](https://github.com/vatesfr/xen-orchestra/issues/3892) (PRs [#4326](https://github.com/vatesfr/xen-orchestra/pull/4326) [#4368](https://github.com/vatesfr/xen-orchestra/pull/4368))
|
||||
- [VM/Bulk copy] Show warning if zstd compression is not supported on a VM [#3892](https://github.com/vatesfr/xen-orchestra/issues/3892) (PR [#4346](https://github.com/vatesfr/xen-orchestra/pull/4346))
|
||||
- [VM/copy] Only show zstd option when it's supported [#3892](https://github.com/vatesfr/xen-orchestra/issues/3892) (PR [#4326](https://github.com/vatesfr/xen-orchestra/pull/4326))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
> Users must be able to say: “I had this issue, happy to know it's fixed”
|
||||
|
||||
- [SR/General] Display VDI VM name in SR usage graph (PR [#4370](https://github.com/vatesfr/xen-orchestra/pull/4370))
|
||||
- [VM/Attach disk] Fix checking VDI mode (PR [#4373](https://github.com/vatesfr/xen-orchestra/pull/4373))
|
||||
- [SDN Controller] Better detect host shutting down to adapt network topology (PR [#4314](https://github.com/vatesfr/xen-orchestra/pull/4314))
|
||||
|
||||
### Released packages
|
||||
|
||||
@@ -26,6 +22,6 @@
|
||||
>
|
||||
> Rule of thumb: add packages on top.
|
||||
|
||||
- xo-server-usage-report v0.7.3
|
||||
- xo-server-sdn-controller v0.1.2
|
||||
- xo-server v5.47.0
|
||||
- xo-web v5.47.0
|
||||
|
||||
@@ -42,7 +42,6 @@
|
||||
"testEnvironment": "node",
|
||||
"testPathIgnorePatterns": [
|
||||
"/dist/",
|
||||
"/xo-server-test/",
|
||||
"/xo-web/"
|
||||
],
|
||||
"testRegex": "\\.spec\\.js$",
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
"predev": "yarn run prebuild",
|
||||
"prepublishOnly": "yarn run build"
|
||||
},
|
||||
"version": "0.1.2",
|
||||
"version": "0.1.1",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
},
|
||||
|
||||
@@ -3,7 +3,7 @@ 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, forEach, map } from 'lodash'
|
||||
import { filter, find, forOwn, map } from 'lodash'
|
||||
import { fromCallback, fromEvent } from 'promise-toolbox'
|
||||
import { join } from 'path'
|
||||
|
||||
@@ -79,6 +79,10 @@ class SDNController extends EventEmitter {
|
||||
|
||||
this._getDataDir = getDataDir
|
||||
|
||||
this._clientKey = null
|
||||
this._clientCert = null
|
||||
this._caCert = null
|
||||
|
||||
this._poolNetworks = []
|
||||
this._ovsdbClients = []
|
||||
this._newHosts = []
|
||||
@@ -91,6 +95,8 @@ class SDNController extends EventEmitter {
|
||||
this._objectsUpdated = this._objectsUpdated.bind(this)
|
||||
|
||||
this._overrideCerts = false
|
||||
|
||||
this._unsetApiMethod = null
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -99,7 +105,7 @@ class SDNController extends EventEmitter {
|
||||
this._overrideCerts = configuration['override-certs']
|
||||
let certDirectory = configuration['cert-dir']
|
||||
|
||||
if (certDirectory === undefined) {
|
||||
if (certDirectory == null) {
|
||||
log.debug(`No cert-dir provided, using default self-signed certificates`)
|
||||
certDirectory = await this._getDataDir()
|
||||
|
||||
@@ -141,7 +147,7 @@ class SDNController extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
load() {
|
||||
async load() {
|
||||
const createPrivateNetwork = this._createPrivateNetwork.bind(this)
|
||||
createPrivateNetwork.description =
|
||||
'Creates a pool-wide private network on a selected pool'
|
||||
@@ -150,11 +156,9 @@ class SDNController extends EventEmitter {
|
||||
networkName: { type: 'string' },
|
||||
networkDescription: { type: 'string' },
|
||||
encapsulation: { type: 'string' },
|
||||
pifId: { type: 'string' },
|
||||
}
|
||||
createPrivateNetwork.resolve = {
|
||||
xoPool: ['poolId', 'pool', ''],
|
||||
xoPif: ['pifId', 'PIF', ''],
|
||||
}
|
||||
this._unsetApiMethod = this._xo.addApiMethod(
|
||||
'plugin.SDNController.createPrivateNetwork',
|
||||
@@ -162,58 +166,41 @@ class SDNController extends EventEmitter {
|
||||
)
|
||||
|
||||
// FIXME: we should monitor when xapis are added/removed
|
||||
return Promise.all(
|
||||
map(this._xo.getAllXapis(), async xapi => {
|
||||
await xapi.objectsFetched
|
||||
if (this._setControllerNeeded(xapi)) {
|
||||
return
|
||||
}
|
||||
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' })
|
||||
for (const host of hosts) {
|
||||
this._createOvsdbClient(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' })
|
||||
await Promise.all(
|
||||
map(networks, async network => {
|
||||
if (network.other_config.private_pool_wide !== 'true') {
|
||||
return
|
||||
}
|
||||
|
||||
forOwn(networks, async network => {
|
||||
if (network.other_config.private_pool_wide === 'true') {
|
||||
log.debug('Adding network to managed networks', {
|
||||
network: network.name_label,
|
||||
pool: network.$pool.name_label,
|
||||
})
|
||||
const center = await this._electNewCenter(network, true)
|
||||
|
||||
// Previously created network didn't store `pif_device`
|
||||
if (network.other_config.pif_device === undefined) {
|
||||
const tunnel = this._getHostTunnelForNetwork(center, network.$ref)
|
||||
const pif = xapi.getObjectByRef(tunnel.transport_PIF)
|
||||
await xapi.call(
|
||||
'network.add_to_other_config',
|
||||
network.$ref,
|
||||
'pif_device',
|
||||
pif.device
|
||||
)
|
||||
}
|
||||
|
||||
this._poolNetworks.push({
|
||||
pool: network.$pool.$ref,
|
||||
network: network.$ref,
|
||||
starCenter: center?.$ref,
|
||||
})
|
||||
this._networks.set(network.$id, network.$ref)
|
||||
if (center !== undefined) {
|
||||
if (center != null) {
|
||||
this._starCenters.set(center.$id, center.$ref)
|
||||
}
|
||||
})
|
||||
)
|
||||
})
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async unload() {
|
||||
@@ -237,13 +224,10 @@ class SDNController extends EventEmitter {
|
||||
networkName,
|
||||
networkDescription,
|
||||
encapsulation,
|
||||
xoPif,
|
||||
}) {
|
||||
const pool = this._xo.getXapiObject(xoPool)
|
||||
await this._setPoolControllerIfNeeded(pool)
|
||||
|
||||
const pif = this._xo.getXapiObject(xoPif)
|
||||
|
||||
// Create the private network
|
||||
const privateNetworkRef = await pool.$xapi.call('network.create', {
|
||||
name_label: networkName,
|
||||
@@ -253,7 +237,6 @@ class SDNController extends EventEmitter {
|
||||
automatic: 'false',
|
||||
private_pool_wide: 'true',
|
||||
encapsulation: encapsulation,
|
||||
pif_device: pif.device,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -268,7 +251,7 @@ class SDNController extends EventEmitter {
|
||||
const hosts = filter(pool.$xapi.objects.all, { $type: 'host' })
|
||||
await Promise.all(
|
||||
map(hosts, async host => {
|
||||
await this._createTunnel(host, privateNetwork, pif.device)
|
||||
await this._createTunnel(host, privateNetwork)
|
||||
this._createOvsdbClient(host)
|
||||
})
|
||||
)
|
||||
@@ -278,9 +261,10 @@ class SDNController extends EventEmitter {
|
||||
pool: pool.$ref,
|
||||
network: privateNetwork.$ref,
|
||||
starCenter: center?.$ref,
|
||||
encapsulation: encapsulation,
|
||||
})
|
||||
this._networks.set(privateNetwork.$id, privateNetwork.$ref)
|
||||
if (center !== undefined) {
|
||||
if (center != null) {
|
||||
this._starCenters.set(center.$id, center.$ref)
|
||||
}
|
||||
}
|
||||
@@ -304,52 +288,53 @@ class SDNController extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
_objectsAdded(objects) {
|
||||
forEach(objects, object => {
|
||||
const { $type } = object
|
||||
|
||||
if ($type === 'host') {
|
||||
log.debug('New host', {
|
||||
host: object.name_label,
|
||||
pool: object.$pool.name_label,
|
||||
})
|
||||
|
||||
if (find(this._newHosts, { $ref: object.$ref }) === undefined) {
|
||||
this._newHosts.push(object)
|
||||
}
|
||||
this._createOvsdbClient(object)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
_objectsUpdated(objects) {
|
||||
return Promise.all(
|
||||
map(objects, object => {
|
||||
async _objectsAdded(objects) {
|
||||
await Promise.all(
|
||||
map(objects, async object => {
|
||||
const { $type } = object
|
||||
|
||||
if ($type === 'PIF') {
|
||||
return this._pifUpdated(object)
|
||||
}
|
||||
if ($type === 'host') {
|
||||
return this._hostUpdated(object)
|
||||
}
|
||||
if ($type === 'host_metrics') {
|
||||
return this._hostMetricsUpdated(object)
|
||||
log.debug('New host', {
|
||||
host: object.name_label,
|
||||
pool: object.$pool.name_label,
|
||||
})
|
||||
|
||||
if (find(this._newHosts, { $ref: object.$ref }) == null) {
|
||||
this._newHosts.push(object)
|
||||
}
|
||||
this._createOvsdbClient(object)
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
_objectsRemoved(xapi, objects) {
|
||||
return Promise.all(
|
||||
async _objectsUpdated(objects) {
|
||||
await Promise.all(
|
||||
map(objects, async (object, id) => {
|
||||
this._ovsdbClients = this._ovsdbClients.filter(
|
||||
client => client.host.$id !== id
|
||||
)
|
||||
const { $type } = object
|
||||
|
||||
if ($type === 'PIF') {
|
||||
await this._pifUpdated(object)
|
||||
} else if ($type === 'host') {
|
||||
await this._hostUpdated(object)
|
||||
} else if ($type === 'host_metrics') {
|
||||
await this._hostMetricsUpdated(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 !== undefined) {
|
||||
if (starCenterRef != null) {
|
||||
this._starCenters.delete(id)
|
||||
const poolNetworks = filter(this._poolNetworks, {
|
||||
starCenter: starCenterRef,
|
||||
@@ -358,7 +343,7 @@ class SDNController extends EventEmitter {
|
||||
const network = xapi.getObjectByRef(poolNetwork.network)
|
||||
const newCenter = await this._electNewCenter(network, true)
|
||||
poolNetwork.starCenter = newCenter?.$ref
|
||||
if (newCenter !== undefined) {
|
||||
if (newCenter != null) {
|
||||
this._starCenters.set(newCenter.$id, newCenter.$ref)
|
||||
}
|
||||
}
|
||||
@@ -367,11 +352,17 @@ class SDNController extends EventEmitter {
|
||||
|
||||
// If a network is removed, clean this._poolNetworks from it
|
||||
const networkRef = this._networks.get(id)
|
||||
if (networkRef !== undefined) {
|
||||
if (networkRef != null) {
|
||||
this._networks.delete(id)
|
||||
this._poolNetworks = this._poolNetworks.filter(
|
||||
poolNetwork => poolNetwork.network !== networkRef
|
||||
)
|
||||
const poolNetwork = find(this._poolNetworks, {
|
||||
network: networkRef,
|
||||
})
|
||||
if (poolNetwork != null) {
|
||||
this._poolNetworks.splice(
|
||||
this._poolNetworks.indexOf(poolNetwork),
|
||||
1
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
@@ -380,16 +371,11 @@ class SDNController extends EventEmitter {
|
||||
async _pifUpdated(pif) {
|
||||
// Only if PIF is in a private network
|
||||
const poolNetwork = find(this._poolNetworks, { network: pif.network })
|
||||
if (poolNetwork === undefined) {
|
||||
if (poolNetwork == null) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!pif.currently_attached) {
|
||||
const tunnel = this._getHostTunnelForNetwork(pif.$host, pif.network)
|
||||
await pif.$xapi.call('tunnel.set_status', tunnel.$ref, {
|
||||
active: 'false',
|
||||
})
|
||||
|
||||
if (poolNetwork.starCenter !== pif.host) {
|
||||
return
|
||||
}
|
||||
@@ -406,11 +392,11 @@ class SDNController extends EventEmitter {
|
||||
const newCenter = await this._electNewCenter(pif.$network, true)
|
||||
poolNetwork.starCenter = newCenter?.$ref
|
||||
this._starCenters.delete(pif.$host.$id)
|
||||
if (newCenter !== undefined) {
|
||||
if (newCenter != null) {
|
||||
this._starCenters.set(newCenter.$id, newCenter.$ref)
|
||||
}
|
||||
} else {
|
||||
if (poolNetwork.starCenter === undefined) {
|
||||
if (poolNetwork.starCenter == null) {
|
||||
const host = pif.$host
|
||||
log.debug('First available host becomes star center of network', {
|
||||
host: host.name_label,
|
||||
@@ -434,64 +420,39 @@ class SDNController extends EventEmitter {
|
||||
}
|
||||
|
||||
async _hostUpdated(host) {
|
||||
const xapi = host.$xapi
|
||||
|
||||
if (host.enabled) {
|
||||
if (host.PIFs.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const newHost = find(this._newHosts, { $ref: host.$ref })
|
||||
if (newHost !== undefined) {
|
||||
this._newHosts = this._newHosts.slice(
|
||||
this._newHosts.indexOf(newHost),
|
||||
1
|
||||
)
|
||||
|
||||
log.debug('Sync pool certificates', {
|
||||
newHost: host.name_label,
|
||||
pool: host.$pool.name_label,
|
||||
})
|
||||
if (newHost != null) {
|
||||
this._newHosts.splice(this._newHosts.indexOf(newHost), 1)
|
||||
try {
|
||||
await host.$xapi.call('pool.certificate_sync')
|
||||
await xapi.call('pool.certificate_sync')
|
||||
} catch (error) {
|
||||
log.error('Error while syncing SDN controller CA certificate', {
|
||||
error,
|
||||
pool: host.$pool.name_label,
|
||||
})
|
||||
}
|
||||
|
||||
const poolNetworks = filter(this._poolNetworks, {
|
||||
pool: host.$pool.$ref,
|
||||
})
|
||||
for (const poolNetwork of poolNetworks) {
|
||||
const tunnel = this._getHostTunnelForNetwork(
|
||||
host,
|
||||
poolNetwork.network
|
||||
)
|
||||
if (tunnel !== undefined) {
|
||||
continue
|
||||
}
|
||||
|
||||
const network = host.$xapi.getObjectByRef(poolNetwork.network)
|
||||
const pifDevice = network.other_config.pif_device || 'eth0'
|
||||
this._createTunnel(host, network, pifDevice)
|
||||
}
|
||||
|
||||
this._addHostToPoolNetworks(host)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_hostMetricsUpdated(hostMetrics) {
|
||||
const ovsdbClient = find(
|
||||
this._ovsdbClients,
|
||||
client => client.host.metrics === hostMetrics.$ref
|
||||
)
|
||||
async _hostMetricsUpdated(hostMetrics) {
|
||||
const ovsdbClient = find(this._ovsdbClients, {
|
||||
hostMetricsRef: hostMetrics.$ref,
|
||||
})
|
||||
const host = ovsdbClient._host
|
||||
|
||||
if (hostMetrics.live) {
|
||||
return this._addHostToPoolNetworks(ovsdbClient.host)
|
||||
await this._addHostToPoolNetworks(host)
|
||||
} else {
|
||||
await this._hostUnreachable(host)
|
||||
}
|
||||
|
||||
return this._hostUnreachable(ovsdbClient.host)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -503,7 +464,7 @@ class SDNController extends EventEmitter {
|
||||
}
|
||||
|
||||
const controller = find(pool.$xapi.objects.all, { $type: 'SDN_controller' })
|
||||
if (controller !== undefined) {
|
||||
if (controller != null) {
|
||||
await pool.$xapi.call('SDN_controller.forget', controller.$ref)
|
||||
log.debug('Old SDN controller removed', {
|
||||
pool: pool.name_label,
|
||||
@@ -520,7 +481,7 @@ class SDNController extends EventEmitter {
|
||||
_setControllerNeeded(xapi) {
|
||||
const controller = find(xapi.objects.all, { $type: 'SDN_controller' })
|
||||
return !(
|
||||
controller !== undefined &&
|
||||
controller != null &&
|
||||
controller.protocol === PROTOCOL &&
|
||||
controller.address === '' &&
|
||||
controller.port === 0
|
||||
@@ -575,7 +536,7 @@ class SDNController extends EventEmitter {
|
||||
async _electNewCenter(network, resetNeeded) {
|
||||
const pool = network.$pool
|
||||
|
||||
let newCenter
|
||||
let newCenter = null
|
||||
const hosts = filter(pool.$xapi.objects.all, { $type: 'host' })
|
||||
|
||||
for (const host of hosts) {
|
||||
@@ -592,11 +553,8 @@ class SDNController extends EventEmitter {
|
||||
}
|
||||
|
||||
// Clean old ports and interfaces
|
||||
const hostClient = find(
|
||||
this._ovsdbClients,
|
||||
client => client.host.$ref === host.$ref
|
||||
)
|
||||
if (hostClient !== undefined) {
|
||||
const hostClient = find(this._ovsdbClients, { host: host.$ref })
|
||||
if (hostClient != null) {
|
||||
try {
|
||||
await hostClient.resetForNetwork(network.uuid, network.name_label)
|
||||
} catch (error) {
|
||||
@@ -611,17 +569,19 @@ class SDNController extends EventEmitter {
|
||||
})
|
||||
)
|
||||
|
||||
if (newCenter === undefined) {
|
||||
if (newCenter == null) {
|
||||
log.error('No available host to elect new star-center', {
|
||||
network: network.name_label,
|
||||
pool: network.$pool.name_label,
|
||||
})
|
||||
return
|
||||
return null
|
||||
}
|
||||
|
||||
// Recreate star topology
|
||||
await Promise.all(
|
||||
map(hosts, host => this._addHostToNetwork(host, network, newCenter))
|
||||
await map(hosts, async host => {
|
||||
await this._addHostToNetwork(host, network, newCenter)
|
||||
})
|
||||
)
|
||||
|
||||
log.info('New star-center elected', {
|
||||
@@ -633,33 +593,20 @@ class SDNController extends EventEmitter {
|
||||
return newCenter
|
||||
}
|
||||
|
||||
async _createTunnel(host, network, pifDevice) {
|
||||
const hostPif = find(host.$PIFs, { device: pifDevice })
|
||||
if (hostPif === undefined) {
|
||||
log.error("Can't create tunnel: no available PIF", {
|
||||
pif: pifDevice,
|
||||
network: network.name_label,
|
||||
async _createTunnel(host, network) {
|
||||
const pif = host.$PIFs.find(
|
||||
pif => pif.physical && pif.ip_configuration_mode !== 'None'
|
||||
)
|
||||
if (pif == null) {
|
||||
log.error('No PIF found to create tunnel', {
|
||||
host: host.name_label,
|
||||
pool: host.$pool.name_label,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await host.$xapi.call('tunnel.create', hostPif.$ref, network.$ref)
|
||||
} catch (error) {
|
||||
log.error('Error while creating tunnel', {
|
||||
error,
|
||||
pif: pifDevice,
|
||||
network: network.name_label,
|
||||
host: host.name_label,
|
||||
pool: host.$pool.name_label,
|
||||
network: network.name_label,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
await host.$xapi.call('tunnel.create', pif.$ref, network.$ref)
|
||||
log.debug('New tunnel added', {
|
||||
pif: pifDevice,
|
||||
network: network.name_label,
|
||||
host: host.name_label,
|
||||
pool: host.$pool.name_label,
|
||||
@@ -672,19 +619,10 @@ class SDNController extends EventEmitter {
|
||||
return
|
||||
}
|
||||
|
||||
const xapi = host.$xapi
|
||||
const tunnel = this._getHostTunnelForNetwork(host, network.$ref)
|
||||
const starCenterTunnel = this._getHostTunnelForNetwork(
|
||||
starCenter,
|
||||
network.$ref
|
||||
)
|
||||
await xapi.call('tunnel.set_status', tunnel.$ref, { active: 'false' })
|
||||
|
||||
const hostClient = find(
|
||||
this._ovsdbClients,
|
||||
client => client.host.$ref === host.$ref
|
||||
)
|
||||
if (hostClient === undefined) {
|
||||
const hostClient = find(this._ovsdbClients, {
|
||||
host: host.$ref,
|
||||
})
|
||||
if (hostClient == null) {
|
||||
log.error('No OVSDB client found', {
|
||||
host: host.name_label,
|
||||
pool: host.$pool.name_label,
|
||||
@@ -692,11 +630,10 @@ class SDNController extends EventEmitter {
|
||||
return
|
||||
}
|
||||
|
||||
const starCenterClient = find(
|
||||
this._ovsdbClients,
|
||||
client => client.host.$ref === starCenter.$ref
|
||||
)
|
||||
if (starCenterClient === undefined) {
|
||||
const starCenterClient = find(this._ovsdbClients, {
|
||||
host: starCenter.$ref,
|
||||
})
|
||||
if (starCenterClient == null) {
|
||||
log.error('No OVSDB client found for star-center', {
|
||||
host: starCenter.name_label,
|
||||
pool: starCenter.$pool.name_label,
|
||||
@@ -704,37 +641,32 @@ class SDNController extends EventEmitter {
|
||||
return
|
||||
}
|
||||
|
||||
const encapsulation = network.other_config.encapsulation || 'gre'
|
||||
let bridgeName
|
||||
const encapsulation =
|
||||
network.other_config.encapsulation != null
|
||||
? network.other_config.encapsulation
|
||||
: 'gre'
|
||||
|
||||
try {
|
||||
bridgeName = await hostClient.addInterfaceAndPort(
|
||||
await hostClient.addInterfaceAndPort(
|
||||
network.uuid,
|
||||
network.name_label,
|
||||
starCenterClient.host.address,
|
||||
starCenterClient.address,
|
||||
encapsulation
|
||||
)
|
||||
await starCenterClient.addInterfaceAndPort(
|
||||
network.uuid,
|
||||
network.name_label,
|
||||
hostClient.host.address,
|
||||
hostClient.address,
|
||||
encapsulation
|
||||
)
|
||||
} catch (error) {
|
||||
log.error('Error while connecting host to private network', {
|
||||
log.error('Error while connection host to private network', {
|
||||
error,
|
||||
network: network.name_label,
|
||||
host: host.name_label,
|
||||
pool: host.$pool.name_label,
|
||||
})
|
||||
}
|
||||
|
||||
if (bridgeName !== undefined) {
|
||||
const activeStatus = { active: 'true', key: bridgeName }
|
||||
await Promise.all([
|
||||
xapi.call('tunnel.set_status', tunnel.$ref, activeStatus),
|
||||
xapi.call('tunnel.set_status', starCenterTunnel.$ref, activeStatus),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
async _addHostToPoolNetworks(host) {
|
||||
@@ -750,7 +682,7 @@ class SDNController extends EventEmitter {
|
||||
const poolNetwork = find(this._poolNetworks, {
|
||||
network: accessPif.network,
|
||||
})
|
||||
if (poolNetwork === undefined || accessPif.currently_attached) {
|
||||
if (poolNetwork == null || accessPif.currently_attached) {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -792,43 +724,17 @@ class SDNController extends EventEmitter {
|
||||
const newCenter = await this._electNewCenter(network, true)
|
||||
poolNetwork.starCenter = newCenter?.$ref
|
||||
this._starCenters.delete(host.$id)
|
||||
if (newCenter !== undefined) {
|
||||
if (newCenter !== null) {
|
||||
this._starCenters.set(newCenter.$id, newCenter.$ref)
|
||||
}
|
||||
}
|
||||
|
||||
for (const poolNetwork of this._poolNetworks) {
|
||||
const tunnel = this._getHostTunnelForNetwork(host, poolNetwork.network)
|
||||
await host.$xapi.call('tunnel.set_status', tunnel.$ref, {
|
||||
active: 'false',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
_getHostTunnelForNetwork(host, networkRef) {
|
||||
const pif = find(host.$PIFs, { network: networkRef })
|
||||
if (pif === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
const tunnel = find(host.$xapi.objects.all, {
|
||||
$type: 'tunnel',
|
||||
access_PIF: pif.$ref,
|
||||
})
|
||||
|
||||
return tunnel
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
_createOvsdbClient(host) {
|
||||
const foundClient = find(
|
||||
this._ovsdbClients,
|
||||
client => client.host.$ref === host.$ref
|
||||
)
|
||||
if (foundClient !== undefined) {
|
||||
const foundClient = find(this._ovsdbClients, { host: host.$ref })
|
||||
if (foundClient != null) {
|
||||
return foundClient
|
||||
}
|
||||
|
||||
@@ -868,11 +774,8 @@ class SDNController extends EventEmitter {
|
||||
subject: subject,
|
||||
}
|
||||
|
||||
// In all the following callbacks, `error` is:
|
||||
// - either an error object if there was an error
|
||||
// - or a boolean set to `false` if no error occurred
|
||||
openssl.generateRSAPrivateKey(rsakeyoptions, (error, cakey, cmd) => {
|
||||
if (error !== false) {
|
||||
if (error !== undefined) {
|
||||
log.error('Error while generating CA private key', {
|
||||
error,
|
||||
})
|
||||
@@ -880,7 +783,7 @@ class SDNController extends EventEmitter {
|
||||
}
|
||||
|
||||
openssl.generateCSR(cacsroptions, cakey, null, (error, csr, cmd) => {
|
||||
if (error !== false) {
|
||||
if (error !== undefined) {
|
||||
log.error('Error while generating CA certificate', {
|
||||
error,
|
||||
})
|
||||
@@ -893,7 +796,7 @@ class SDNController extends EventEmitter {
|
||||
cakey,
|
||||
null,
|
||||
async (error, cacrt, cmd) => {
|
||||
if (error !== false) {
|
||||
if (error !== undefined) {
|
||||
log.error('Error while signing CA certificate', {
|
||||
error,
|
||||
})
|
||||
@@ -904,7 +807,7 @@ class SDNController extends EventEmitter {
|
||||
openssl.generateRSAPrivateKey(
|
||||
rsakeyoptions,
|
||||
async (error, key, cmd) => {
|
||||
if (error !== false) {
|
||||
if (error !== undefined) {
|
||||
log.error('Error while generating private key', {
|
||||
error,
|
||||
})
|
||||
@@ -917,7 +820,7 @@ class SDNController extends EventEmitter {
|
||||
key,
|
||||
null,
|
||||
(error, csr, cmd) => {
|
||||
if (error !== false) {
|
||||
if (error !== undefined) {
|
||||
log.error('Error while generating certificate', {
|
||||
error,
|
||||
})
|
||||
@@ -931,7 +834,7 @@ class SDNController extends EventEmitter {
|
||||
cakey,
|
||||
null,
|
||||
async (error, crt, cmd) => {
|
||||
if (error !== false) {
|
||||
if (error !== undefined) {
|
||||
log.error('Error while signing certificate', {
|
||||
error,
|
||||
})
|
||||
|
||||
@@ -12,29 +12,42 @@ const OVSDB_PORT = 6640
|
||||
|
||||
export class OvsdbClient {
|
||||
constructor(host, clientKey, clientCert, caCert) {
|
||||
this._host = host
|
||||
this._numberOfPortAndInterface = 0
|
||||
this._requestID = 0
|
||||
|
||||
this._adding = []
|
||||
|
||||
this.host = host
|
||||
|
||||
this.updateCertificates(clientKey, clientCert, caCert)
|
||||
|
||||
log.debug('New OVSDB client', {
|
||||
host: this.host.name_label,
|
||||
host: this._host.name_label,
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
get address() {
|
||||
return this._host.address
|
||||
}
|
||||
|
||||
get host() {
|
||||
return this._host.$ref
|
||||
}
|
||||
|
||||
get id() {
|
||||
return this._host.$id
|
||||
}
|
||||
|
||||
get hostMetricsRef() {
|
||||
return this._host.metrics
|
||||
}
|
||||
|
||||
updateCertificates(clientKey, clientCert, caCert) {
|
||||
this._clientKey = clientKey
|
||||
this._clientCert = clientCert
|
||||
this._caCert = caCert
|
||||
|
||||
log.debug('Certificates have been updated', {
|
||||
host: this.host.name_label,
|
||||
host: this._host.name_label,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -46,16 +59,6 @@ export class OvsdbClient {
|
||||
remoteAddress,
|
||||
encapsulation
|
||||
) {
|
||||
if (
|
||||
this._adding.find(
|
||||
elem => elem.id === networkUuid && elem.addr === remoteAddress
|
||||
) !== undefined
|
||||
) {
|
||||
return
|
||||
}
|
||||
const adding = { id: networkUuid, addr: remoteAddress }
|
||||
this._adding.push(adding)
|
||||
|
||||
const socket = await this._connect()
|
||||
const index = this._numberOfPortAndInterface
|
||||
++this._numberOfPortAndInterface
|
||||
@@ -65,9 +68,8 @@ export class OvsdbClient {
|
||||
networkName,
|
||||
socket
|
||||
)
|
||||
if (bridgeUuid === undefined) {
|
||||
if (bridgeUuid == null) {
|
||||
socket.destroy()
|
||||
this._adding = this._adding.slice(this._adding.indexOf(adding), 1)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -79,8 +81,7 @@ export class OvsdbClient {
|
||||
)
|
||||
if (alreadyExist) {
|
||||
socket.destroy()
|
||||
this._adding = this._adding.slice(this._adding.indexOf(adding), 1)
|
||||
return bridgeName
|
||||
return
|
||||
}
|
||||
|
||||
const interfaceName = 'tunnel_iface' + index
|
||||
@@ -122,9 +123,7 @@ export class OvsdbClient {
|
||||
mutateBridgeOperation,
|
||||
]
|
||||
const jsonObjects = await this._sendOvsdbTransaction(params, socket)
|
||||
|
||||
this._adding = this._adding.slice(this._adding.indexOf(adding), 1)
|
||||
if (jsonObjects === undefined) {
|
||||
if (jsonObjects == null) {
|
||||
socket.destroy()
|
||||
return
|
||||
}
|
||||
@@ -135,14 +134,14 @@ export class OvsdbClient {
|
||||
let opResult
|
||||
do {
|
||||
opResult = jsonObjects[0].result[i]
|
||||
if (opResult !== undefined && opResult.error !== undefined) {
|
||||
if (opResult != null && opResult.error != null) {
|
||||
error = opResult.error
|
||||
details = opResult.details
|
||||
}
|
||||
++i
|
||||
} while (opResult !== undefined && error === undefined)
|
||||
} while (opResult && !error)
|
||||
|
||||
if (error !== undefined) {
|
||||
if (error != null) {
|
||||
log.error('Error while adding port and interface to bridge', {
|
||||
error,
|
||||
details,
|
||||
@@ -150,7 +149,7 @@ export class OvsdbClient {
|
||||
interface: interfaceName,
|
||||
bridge: bridgeName,
|
||||
network: networkName,
|
||||
host: this.host.name_label,
|
||||
host: this._host.name_label,
|
||||
})
|
||||
socket.destroy()
|
||||
return
|
||||
@@ -161,10 +160,9 @@ export class OvsdbClient {
|
||||
interface: interfaceName,
|
||||
bridge: bridgeName,
|
||||
network: networkName,
|
||||
host: this.host.name_label,
|
||||
host: this._host.name_label,
|
||||
})
|
||||
socket.destroy()
|
||||
return bridgeName
|
||||
}
|
||||
|
||||
async resetForNetwork(networkUuid, networkName) {
|
||||
@@ -174,14 +172,14 @@ export class OvsdbClient {
|
||||
networkName,
|
||||
socket
|
||||
)
|
||||
if (bridgeUuid === undefined) {
|
||||
if (bridgeUuid == null) {
|
||||
socket.destroy()
|
||||
return
|
||||
}
|
||||
|
||||
// Delete old ports created by a SDN controller
|
||||
const ports = await this._getBridgePorts(bridgeUuid, bridgeName, socket)
|
||||
if (ports === undefined) {
|
||||
if (ports == null) {
|
||||
socket.destroy()
|
||||
return
|
||||
}
|
||||
@@ -196,7 +194,7 @@ export class OvsdbClient {
|
||||
where,
|
||||
socket
|
||||
)
|
||||
if (selectResult === undefined) {
|
||||
if (selectResult == null) {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -222,15 +220,15 @@ export class OvsdbClient {
|
||||
|
||||
const params = ['Open_vSwitch', mutateBridgeOperation]
|
||||
const jsonObjects = await this._sendOvsdbTransaction(params, socket)
|
||||
if (jsonObjects === undefined) {
|
||||
if (jsonObjects == null) {
|
||||
socket.destroy()
|
||||
return
|
||||
}
|
||||
if (jsonObjects[0].error != null) {
|
||||
log.error('Error while deleting ports from bridge', {
|
||||
error: jsonObjects[0].error,
|
||||
error: jsonObjects.error,
|
||||
bridge: bridgeName,
|
||||
host: this.host.name_label,
|
||||
host: this._host.name_label,
|
||||
})
|
||||
socket.destroy()
|
||||
return
|
||||
@@ -239,7 +237,7 @@ export class OvsdbClient {
|
||||
log.debug('Ports deleted from bridge', {
|
||||
nPorts: jsonObjects[0].result[0].count,
|
||||
bridge: bridgeName,
|
||||
host: this.host.name_label,
|
||||
host: this._host.name_label,
|
||||
})
|
||||
socket.destroy()
|
||||
}
|
||||
@@ -290,12 +288,12 @@ export class OvsdbClient {
|
||||
where,
|
||||
socket
|
||||
)
|
||||
if (selectResult === undefined) {
|
||||
if (selectResult == null) {
|
||||
log.error('No bridge found for network', {
|
||||
network: networkName,
|
||||
host: this.host.name_label,
|
||||
host: this._host.name_label,
|
||||
})
|
||||
return []
|
||||
return [null, null]
|
||||
}
|
||||
|
||||
const bridgeUuid = selectResult._uuid[1]
|
||||
@@ -311,14 +309,14 @@ export class OvsdbClient {
|
||||
socket
|
||||
) {
|
||||
const ports = await this._getBridgePorts(bridgeUuid, bridgeName, socket)
|
||||
if (ports === undefined) {
|
||||
return false
|
||||
if (ports == null) {
|
||||
return
|
||||
}
|
||||
|
||||
for (const port of ports) {
|
||||
const portUuid = port[1]
|
||||
const interfaces = await this._getPortInterfaces(portUuid, socket)
|
||||
if (interfaces === undefined) {
|
||||
if (interfaces == null) {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -341,8 +339,8 @@ export class OvsdbClient {
|
||||
async _getBridgePorts(bridgeUuid, bridgeName, socket) {
|
||||
const where = [['_uuid', '==', ['uuid', bridgeUuid]]]
|
||||
const selectResult = await this._select('Bridge', ['ports'], where, socket)
|
||||
if (selectResult === undefined) {
|
||||
return
|
||||
if (selectResult == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
return selectResult.ports[0] === 'set'
|
||||
@@ -358,8 +356,8 @@ export class OvsdbClient {
|
||||
where,
|
||||
socket
|
||||
)
|
||||
if (selectResult === undefined) {
|
||||
return
|
||||
if (selectResult == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
return selectResult.interfaces[0] === 'set'
|
||||
@@ -375,7 +373,7 @@ export class OvsdbClient {
|
||||
where,
|
||||
socket
|
||||
)
|
||||
if (selectResult === undefined) {
|
||||
if (selectResult == null) {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -400,20 +398,20 @@ export class OvsdbClient {
|
||||
|
||||
const params = ['Open_vSwitch', selectOperation]
|
||||
const jsonObjects = await this._sendOvsdbTransaction(params, socket)
|
||||
if (jsonObjects === undefined) {
|
||||
if (jsonObjects == null) {
|
||||
return
|
||||
}
|
||||
const jsonResult = jsonObjects[0].result[0]
|
||||
if (jsonResult.error !== undefined) {
|
||||
if (jsonResult.error != null) {
|
||||
log.error('Error while selecting columns', {
|
||||
error: jsonResult.error,
|
||||
details: jsonResult.details,
|
||||
columns,
|
||||
table,
|
||||
where,
|
||||
host: this.host.name_label,
|
||||
host: this._host.name_label,
|
||||
})
|
||||
return
|
||||
return null
|
||||
}
|
||||
|
||||
if (jsonResult.rows.length === 0) {
|
||||
@@ -421,15 +419,15 @@ export class OvsdbClient {
|
||||
columns,
|
||||
table,
|
||||
where,
|
||||
host: this.host.name_label,
|
||||
host: this._host.name_label,
|
||||
})
|
||||
return
|
||||
return null
|
||||
}
|
||||
|
||||
// For now all select operations should return only 1 row
|
||||
assert(
|
||||
jsonResult.rows.length === 1,
|
||||
`[${this.host.name_label}] There should be exactly 1 row when searching: '${columns}' in: '${table}' where: '${where}'`
|
||||
`[${this._host.name_label}] There should exactly 1 row when searching: '${columns}' in: '${table}' where: '${where}'`
|
||||
)
|
||||
|
||||
return jsonResult.rows[0]
|
||||
@@ -451,9 +449,9 @@ export class OvsdbClient {
|
||||
} catch (error) {
|
||||
log.error('Error while writing into stream', {
|
||||
error,
|
||||
host: this.host.name_label,
|
||||
host: this._host.name_label,
|
||||
})
|
||||
return
|
||||
return null
|
||||
}
|
||||
|
||||
let result
|
||||
@@ -465,9 +463,9 @@ export class OvsdbClient {
|
||||
} catch (error) {
|
||||
log.error('Error while waiting for stream data', {
|
||||
error,
|
||||
host: this.host.name_label,
|
||||
host: this._host.name_label,
|
||||
})
|
||||
return
|
||||
return null
|
||||
}
|
||||
|
||||
jsonObjects = this._parseJson(result)
|
||||
@@ -484,7 +482,7 @@ export class OvsdbClient {
|
||||
ca: this._caCert,
|
||||
key: this._clientKey,
|
||||
cert: this._clientCert,
|
||||
host: this.host.address,
|
||||
host: this._host.address,
|
||||
port: OVSDB_PORT,
|
||||
rejectUnauthorized: false,
|
||||
requestCert: false,
|
||||
@@ -497,7 +495,7 @@ export class OvsdbClient {
|
||||
log.error('TLS connection failed', {
|
||||
error,
|
||||
code: error.code,
|
||||
host: this.host.name_label,
|
||||
host: this._host.name_label,
|
||||
})
|
||||
throw error
|
||||
}
|
||||
@@ -506,7 +504,7 @@ export class OvsdbClient {
|
||||
log.error('Socket error', {
|
||||
error,
|
||||
code: error.code,
|
||||
host: this.host.name_label,
|
||||
host: this._host.name_label,
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
> Test client for Xo-Server
|
||||
|
||||
Tests are ran sequentially to avoid concurrency issues.
|
||||
|
||||
## Adding a test
|
||||
|
||||
### Organization
|
||||
@@ -122,12 +120,6 @@ describe("user", () => {
|
||||
|
||||
- You can run only tests related to changed files, and review the failed output by using: `> yarn test --watch`
|
||||
|
||||
- ⚠ Warning: snapshots ⚠
|
||||
After each run of the tests, check that snapshots are not inadvertently modified.
|
||||
|
||||
- ⚠ Jest known issue ⚠
|
||||
If a test timeout is triggered the next async tests can fail, it is due to an inadvertently modified snapshots.
|
||||
|
||||
## Contributions
|
||||
|
||||
Contributions are *very* welcomed, either on the documentation or on
|
||||
|
||||
@@ -36,7 +36,6 @@
|
||||
"golike-defer": "^0.4.1",
|
||||
"jest": "^24.8.0",
|
||||
"lodash": "^4.17.11",
|
||||
"promise-toolbox": "^0.13.0",
|
||||
"xo-collection": "^0.4.1",
|
||||
"xo-common": "^0.2.0",
|
||||
"xo-lib": "^0.9.0"
|
||||
@@ -50,7 +49,6 @@
|
||||
"<rootDir>/src/old-tests"
|
||||
],
|
||||
"testEnvironment": "node",
|
||||
"testRegex": "\\.spec\\.js$",
|
||||
"maxConcurrency": 1
|
||||
"testRegex": "\\.spec\\.js$"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,12 +3,6 @@
|
||||
email = ''
|
||||
password = ''
|
||||
|
||||
[servers]
|
||||
[servers.default]
|
||||
username = ''
|
||||
password = ''
|
||||
host = ''
|
||||
|
||||
[vms]
|
||||
default = ''
|
||||
|
||||
@@ -18,5 +12,7 @@
|
||||
[srs]
|
||||
default = ''
|
||||
|
||||
[remotes]
|
||||
default = { name = '', url = '' }
|
||||
# resources created before all tests and deleted at the end.
|
||||
[preCreatedResources]
|
||||
[preCreatedResources.remotes]
|
||||
default = { name = '', url = '' }
|
||||
|
||||
@@ -3,10 +3,16 @@ import defer from 'golike-defer'
|
||||
import Xo from 'xo-lib'
|
||||
import XoCollection from 'xo-collection'
|
||||
import { find, forOwn } from 'lodash'
|
||||
import { fromEvent } from 'promise-toolbox'
|
||||
|
||||
import config from './_config'
|
||||
|
||||
const ARGS_BY_TYPE = {
|
||||
remotes: {
|
||||
getCreationArgs: conf => ['remote.create', conf],
|
||||
getDeletionArgs: res => ['remote.delete', { id: res.id }],
|
||||
},
|
||||
}
|
||||
|
||||
const getDefaultCredentials = () => {
|
||||
const { email, password } = config.xoConnection
|
||||
return { email, password }
|
||||
@@ -125,32 +131,22 @@ class XoConnection extends Xo {
|
||||
return id
|
||||
}
|
||||
|
||||
async createTempRemote(params) {
|
||||
const remote = await this.call('remote.create', params)
|
||||
this._tempResourceDisposers.push('remote.delete', { id: remote.id })
|
||||
return remote
|
||||
}
|
||||
|
||||
async createTempServer(params) {
|
||||
const servers = await this.call('server.getAll')
|
||||
const server = servers.find(server => server.host === params.host)
|
||||
if (server !== undefined) {
|
||||
if (server.status === 'disconnected') {
|
||||
await this.call('server.enable', { id: server.id })
|
||||
this._durableResourceDisposers.push('server.disable', { id: server.id })
|
||||
await fromEvent(this._objects, 'finish')
|
||||
async createRequiredResources() {
|
||||
const requiredResources = {}
|
||||
const resourcesToCreate = config.preCreatedResources
|
||||
for (const typeOfResources in resourcesToCreate) {
|
||||
const { getCreationArgs, getDeletionArgs } = ARGS_BY_TYPE[typeOfResources]
|
||||
const resources = resourcesToCreate[typeOfResources]
|
||||
for (const resource in resources) {
|
||||
const result = await this.call(...getCreationArgs(resources[resource]))
|
||||
this._durableResourceDisposers.push(...getDeletionArgs(result))
|
||||
requiredResources[typeOfResources] = {
|
||||
...requiredResources[typeOfResources],
|
||||
[resource]: result,
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const id = await this.call('server.add', {
|
||||
...params,
|
||||
allowUnauthorized: true,
|
||||
autoConnect: false,
|
||||
})
|
||||
this._durableResourceDisposers.push('server.remove', { id })
|
||||
await this.call('server.enable', { id })
|
||||
await fromEvent(this._objects, 'finish')
|
||||
return requiredResources
|
||||
}
|
||||
|
||||
async getSchedule(predicate) {
|
||||
@@ -162,7 +158,7 @@ class XoConnection extends Xo {
|
||||
const params = disposers[n--]
|
||||
const method = disposers[n--]
|
||||
await this.call(method, params).catch(error => {
|
||||
console.warn('deleteTempResources', method, params, error)
|
||||
console.warn('_cleanDisposers', method, params, error)
|
||||
})
|
||||
}
|
||||
disposers.length = 0
|
||||
@@ -183,9 +179,10 @@ const getConnection = credentials => {
|
||||
}
|
||||
|
||||
let xo
|
||||
let resources
|
||||
beforeAll(async () => {
|
||||
// TOFIX: stop tests if the connection is not established properly and show the error
|
||||
xo = await getConnection()
|
||||
resources = await xo.createRequiredResources()
|
||||
})
|
||||
afterAll(async () => {
|
||||
await xo.deleteDurableResources()
|
||||
@@ -194,7 +191,7 @@ afterAll(async () => {
|
||||
})
|
||||
afterEach(() => xo.deleteTempResources())
|
||||
|
||||
export { xo as default }
|
||||
export { xo as default, resources }
|
||||
|
||||
export const testConnection = ({ credentials }) =>
|
||||
getConnection(credentials).then(connection => connection.close())
|
||||
|
||||
@@ -4,7 +4,7 @@ import { noSuchObject } from 'xo-common/api-errors'
|
||||
|
||||
import config from '../_config'
|
||||
import randomId from '../_randomId'
|
||||
import xo from '../_xoConnection'
|
||||
import xo, { resources } from '../_xoConnection'
|
||||
|
||||
const DEFAULT_SCHEDULE = {
|
||||
name: 'scheduleTest',
|
||||
@@ -143,7 +143,6 @@ describe('backupNg', () => {
|
||||
})
|
||||
|
||||
it('fails trying to run a backup job with non-existent vm', async () => {
|
||||
jest.setTimeout(7e3)
|
||||
const scheduleTempId = randomId()
|
||||
const { id: jobId } = await xo.createTempBackupNgJob({
|
||||
...defaultBackupNg,
|
||||
@@ -169,8 +168,6 @@ describe('backupNg', () => {
|
||||
})
|
||||
|
||||
it('fails trying to run a backup job with a VM without disks', async () => {
|
||||
jest.setTimeout(8e3)
|
||||
await xo.createTempServer(config.servers.default)
|
||||
const vmIdWithoutDisks = await xo.createTempVm({
|
||||
name_label: 'XO Test Without Disks',
|
||||
name_description: 'Creating a vm without disks',
|
||||
@@ -230,14 +227,11 @@ describe('backupNg', () => {
|
||||
})
|
||||
|
||||
it('fails trying to run backup job without retentions', async () => {
|
||||
jest.setTimeout(7e3)
|
||||
const scheduleTempId = randomId()
|
||||
await xo.createTempServer(config.servers.default)
|
||||
const { id: remoteId } = await xo.createTempRemote(config.remotes.default)
|
||||
const { id: jobId } = await xo.createTempBackupNgJob({
|
||||
...defaultBackupNg,
|
||||
remotes: {
|
||||
id: remoteId,
|
||||
id: resources.remotes.default.id,
|
||||
},
|
||||
schedules: {
|
||||
[scheduleTempId]: DEFAULT_SCHEDULE,
|
||||
@@ -290,8 +284,7 @@ describe('backupNg', () => {
|
||||
})
|
||||
|
||||
test('execute three times a rolling snapshot with 2 as retention & revert to an old state', async () => {
|
||||
jest.setTimeout(6e4)
|
||||
await xo.createTempServer(config.servers.default)
|
||||
jest.setTimeout(7e4)
|
||||
const vmId = await xo.createTempVm({
|
||||
name_label: 'XO Test Temp',
|
||||
name_description: 'Creating a temporary vm',
|
||||
|
||||
@@ -40,7 +40,6 @@ describe('job', () => {
|
||||
|
||||
describe('.create() :', () => {
|
||||
it('creates a new job', async () => {
|
||||
jest.setTimeout(6e3)
|
||||
const userId = await xo.createTempUser(ADMIN_USER)
|
||||
const { email, password } = ADMIN_USER
|
||||
await testWithOtherConnection({ email, password }, async xo => {
|
||||
@@ -209,8 +208,6 @@ describe('job', () => {
|
||||
})
|
||||
|
||||
it('runs a job', async () => {
|
||||
jest.setTimeout(7e3)
|
||||
await xo.createTempServer(config.servers.default)
|
||||
const jobId = await xo.createTempJob(defaultJob)
|
||||
const snapshots = xo.objects.all[config.vms.default].snapshots
|
||||
await xo.call('job.runSequence', { idSequence: [jobId] })
|
||||
|
||||
@@ -35,7 +35,6 @@ describe('user', () => {
|
||||
},
|
||||
},
|
||||
async data => {
|
||||
jest.setTimeout(6e3)
|
||||
const userId = await xo.createTempUser(data)
|
||||
expect(typeof userId).toBe('string')
|
||||
expect(await xo.getUser(userId)).toMatchSnapshot({
|
||||
@@ -70,7 +69,6 @@ describe('user', () => {
|
||||
|
||||
describe('.changePassword() :', () => {
|
||||
it('changes the actual user password', async () => {
|
||||
jest.setTimeout(7e3)
|
||||
const user = {
|
||||
email: 'wayne7@vates.fr',
|
||||
password: 'batman',
|
||||
@@ -151,7 +149,6 @@ describe('user', () => {
|
||||
},
|
||||
},
|
||||
async data => {
|
||||
jest.setTimeout(6e3)
|
||||
data.id = await xo.createTempUser(SIMPLE_USER)
|
||||
expect(await xo.call('user.set', data)).toBe(true)
|
||||
expect(await xo.getUser(data.id)).toMatchSnapshot({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "xo-server-usage-report",
|
||||
"version": "0.7.3",
|
||||
"version": "0.7.2",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "",
|
||||
"keywords": [
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
values,
|
||||
zipObject,
|
||||
} from 'lodash'
|
||||
import { ignoreErrors, promisify } from 'promise-toolbox'
|
||||
import { promisify } from 'promise-toolbox'
|
||||
import { readFile, writeFile } from 'fs'
|
||||
|
||||
// ===================================================================
|
||||
@@ -759,22 +759,14 @@ class UsageReportPlugin {
|
||||
}
|
||||
|
||||
async _sendReport(storeData) {
|
||||
const xo = this._xo
|
||||
if (xo.sendEmail === undefined) {
|
||||
ignoreErrors.call(xo.unloadPlugin('usage-report'))
|
||||
throw new Error(
|
||||
'The plugin usage-report requires the plugin transport-email to be loaded'
|
||||
)
|
||||
}
|
||||
|
||||
const data = await dataBuilder({
|
||||
xo,
|
||||
xo: this._xo,
|
||||
storedStatsPath: this._storedStatsPath,
|
||||
all: this._conf.all,
|
||||
})
|
||||
|
||||
await Promise.all([
|
||||
xo.sendEmail({
|
||||
this._xo.sendEmail({
|
||||
to: this._conf.emails,
|
||||
subject: `[Xen Orchestra] Xo Report - ${currDate}`,
|
||||
markdown: `Hi there,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "xo-server",
|
||||
"version": "5.47.0",
|
||||
"version": "5.46.0",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "Server part of Xen-Orchestra",
|
||||
"keywords": [
|
||||
|
||||
@@ -150,6 +150,8 @@ export async function create(params) {
|
||||
}
|
||||
|
||||
const xapiVm = await xapi.createVm(template._xapiId, params, checkLimits)
|
||||
await xapiVm.update_other_config('owner', user.id)
|
||||
|
||||
const vm = xapi.xo.addObject(xapiVm)
|
||||
|
||||
if (resourceSet) {
|
||||
@@ -663,15 +665,16 @@ export const clone = defer(async function(
|
||||
await checkPermissionOnSrs.call(this, vm)
|
||||
const xapi = this.getXapi(vm)
|
||||
|
||||
const { $id: cloneId } = await xapi.cloneVm(vm._xapiRef, {
|
||||
const xapiVm = await xapi.cloneVm(vm._xapiRef, {
|
||||
nameLabel: name,
|
||||
fast: !fullCopy,
|
||||
})
|
||||
$defer.onFailure(() => xapi.deleteVm(cloneId))
|
||||
$defer.onFailure(() => xapi.deleteVm(xapiVm.$id))
|
||||
await xapiVm.update_other_config('owner', this.user.id)
|
||||
|
||||
const isAdmin = this.user.permission === 'admin'
|
||||
if (!isAdmin) {
|
||||
await this.addAcl(this.user.id, cloneId, 'admin')
|
||||
await this.addAcl(this.user.id, xapiVm.$id, 'admin')
|
||||
}
|
||||
|
||||
if (vm.resourceSet !== undefined) {
|
||||
@@ -682,7 +685,7 @@ export const clone = defer(async function(
|
||||
)
|
||||
}
|
||||
|
||||
return cloneId
|
||||
return xapiVm.$id
|
||||
})
|
||||
|
||||
clone.params = {
|
||||
@@ -704,19 +707,26 @@ export async function copy({ compress, name: nameLabel, sr, vm }) {
|
||||
await checkPermissionOnSrs.call(this, vm)
|
||||
}
|
||||
|
||||
return this.getXapi(vm)
|
||||
.copyVm(vm._xapiId, sr._xapiId, {
|
||||
nameLabel,
|
||||
})
|
||||
.then(vm => vm.$id)
|
||||
}
|
||||
|
||||
return this.getXapi(vm)
|
||||
.remoteCopyVm(vm._xapiId, this.getXapi(sr), sr._xapiId, {
|
||||
compress,
|
||||
const xapiVm = await this.getXapi(vm).copyVm(vm._xapiId, sr._xapiId, {
|
||||
nameLabel,
|
||||
})
|
||||
.then(({ vm }) => vm.$id)
|
||||
await xapiVm.update_other_config('owner', this.user.id)
|
||||
|
||||
return xapiVm.$id
|
||||
}
|
||||
|
||||
const { vm: xapiVm } = await this.getXapi(vm).remoteCopyVm(
|
||||
vm._xapiId,
|
||||
this.getXapi(sr),
|
||||
sr._xapiId,
|
||||
{
|
||||
compress,
|
||||
nameLabel,
|
||||
}
|
||||
)
|
||||
await xapiVm.update_other_config('owner', this.user.id)
|
||||
|
||||
return xapiVm.$id
|
||||
}
|
||||
|
||||
copy.params = {
|
||||
|
||||
@@ -255,7 +255,7 @@ export default {
|
||||
)) !== undefined
|
||||
) {
|
||||
if (getAll) {
|
||||
log.debug(
|
||||
log(
|
||||
`patch ${patch.name} (${id}) conflicts with installed patch ${conflictId}`
|
||||
)
|
||||
return
|
||||
@@ -271,7 +271,7 @@ export default {
|
||||
)) !== undefined
|
||||
) {
|
||||
if (getAll) {
|
||||
log.debug(`patches ${id} and ${conflictId} conflict with eachother`)
|
||||
log(`patches ${id} and ${conflictId} conflict with eachother`)
|
||||
return
|
||||
}
|
||||
throw new Error(
|
||||
|
||||
@@ -293,10 +293,6 @@ export default class {
|
||||
async connectXenServer(id) {
|
||||
const server = (await this._getXenServer(id)).properties
|
||||
|
||||
if (this._getXenServerStatus(id) !== 'disconnected') {
|
||||
throw new Error('the server is already connected')
|
||||
}
|
||||
|
||||
const xapi = (this._xapis[server.id] = new Xapi({
|
||||
allowUnauthorized: server.allowUnauthorized,
|
||||
readOnly: server.readOnly,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "xo-web",
|
||||
"version": "5.47.0",
|
||||
"version": "5.46.0",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "Web interface client for Xen-Orchestra",
|
||||
"keywords": [
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
import * as CM from 'complex-matcher'
|
||||
import { escapeRegExp } from 'lodash'
|
||||
|
||||
const valueToComplexMatcher = pattern => {
|
||||
if (typeof pattern === 'string') {
|
||||
return new CM.RegExpNode(`^${escapeRegExp(pattern)}$`, 'i')
|
||||
}
|
||||
|
||||
if (Array.isArray(pattern)) {
|
||||
return new CM.And(pattern.map(valueToComplexMatcher))
|
||||
}
|
||||
|
||||
if (pattern !== null && typeof pattern === 'object') {
|
||||
const keys = Object.keys(pattern)
|
||||
const { length } = keys
|
||||
|
||||
if (length === 1) {
|
||||
const [key] = keys
|
||||
if (key === '__and') {
|
||||
return new CM.And(pattern.__and.map(valueToComplexMatcher))
|
||||
}
|
||||
if (key === '__or') {
|
||||
return new CM.Or(pattern.__or.map(valueToComplexMatcher))
|
||||
}
|
||||
if (key === '__not') {
|
||||
return new CM.Not(valueToComplexMatcher(pattern.__not))
|
||||
}
|
||||
}
|
||||
|
||||
const children = []
|
||||
Object.keys(pattern).forEach(property => {
|
||||
const subpattern = pattern[property]
|
||||
if (subpattern !== undefined) {
|
||||
children.push(
|
||||
new CM.Property(property, valueToComplexMatcher(subpattern))
|
||||
)
|
||||
}
|
||||
})
|
||||
return children.length === 0 ? new CM.Null() : new CM.And(children)
|
||||
}
|
||||
|
||||
throw new Error('could not transform this pattern')
|
||||
}
|
||||
|
||||
export default pattern => {
|
||||
try {
|
||||
return valueToComplexMatcher(pattern).toString()
|
||||
} catch (error) {
|
||||
console.warn('constructQueryString', pattern, error)
|
||||
return ''
|
||||
}
|
||||
}
|
||||
@@ -1827,7 +1827,7 @@ export default {
|
||||
vdiAction: 'Acción',
|
||||
|
||||
// Original text: "Attach disk"
|
||||
vdiAttachDevice: 'Adjuntar disco',
|
||||
vdiAttachDeviceButton: 'Adjuntar disco',
|
||||
|
||||
// Original text: "New disk"
|
||||
vbdCreateDeviceButton: 'Nuevo disco',
|
||||
|
||||
@@ -1865,7 +1865,7 @@ export default {
|
||||
vdiAction: 'Action',
|
||||
|
||||
// Original text: "Attach disk"
|
||||
vdiAttachDevice: 'Attacher un disque',
|
||||
vdiAttachDeviceButton: 'Attacher un disque',
|
||||
|
||||
// Original text: "New disk"
|
||||
vbdCreateDeviceButton: 'Nouveau disque',
|
||||
|
||||
@@ -1557,7 +1557,7 @@ export default {
|
||||
vdiAction: undefined,
|
||||
|
||||
// Original text: 'Attach disk'
|
||||
vdiAttachDevice: undefined,
|
||||
vdiAttachDeviceButton: undefined,
|
||||
|
||||
// Original text: 'New disk'
|
||||
vbdCreateDeviceButton: undefined,
|
||||
|
||||
@@ -1773,7 +1773,7 @@ export default {
|
||||
vdiAction: 'Művelet',
|
||||
|
||||
// Original text: "Attach disk"
|
||||
vdiAttachDevice: 'Diszk Hozzácsatolás',
|
||||
vdiAttachDeviceButton: 'Diszk Hozzácsatolás',
|
||||
|
||||
// Original text: "New disk"
|
||||
vbdCreateDeviceButton: 'Új diszk',
|
||||
|
||||
@@ -1567,7 +1567,7 @@ export default {
|
||||
vdiAction: 'Akcja',
|
||||
|
||||
// Original text: "Attach disk"
|
||||
vdiAttachDevice: 'Dołącz dysk',
|
||||
vdiAttachDeviceButton: 'Dołącz dysk',
|
||||
|
||||
// Original text: "New disk"
|
||||
vbdCreateDeviceButton: 'Nowy dysk',
|
||||
|
||||
@@ -1564,7 +1564,7 @@ export default {
|
||||
vdiAction: 'Ação',
|
||||
|
||||
// Original text: "Attach disk"
|
||||
vdiAttachDevice: 'Anexar disco',
|
||||
vdiAttachDeviceButton: 'Anexar disco',
|
||||
|
||||
// Original text: "New disk"
|
||||
vbdCreateDeviceButton: 'Novo disco',
|
||||
|
||||
@@ -2303,7 +2303,7 @@ export default {
|
||||
vdiAction: 'Aksiyon',
|
||||
|
||||
// Original text: "Attach disk"
|
||||
vdiAttachDevice: 'Disk tak',
|
||||
vdiAttachDeviceButton: 'Disk tak',
|
||||
|
||||
// Original text: "New disk"
|
||||
vbdCreateDeviceButton: 'Yeni disk',
|
||||
|
||||
@@ -1176,7 +1176,7 @@ export default {
|
||||
vdiAction: '操作',
|
||||
|
||||
// Original text: "Attach disk"
|
||||
vdiAttachDevice: '附加磁盘',
|
||||
vdiAttachDeviceButton: '附加磁盘',
|
||||
|
||||
// Original text: "New disk"
|
||||
vbdCreateDeviceButton: '新建磁盘',
|
||||
|
||||
@@ -49,6 +49,7 @@ const messages = {
|
||||
backupJobs: 'Backup jobs',
|
||||
iscsiSessions:
|
||||
'({ nSessions, number }) iSCSI session{nSessions, plural, one {} other {s}}',
|
||||
owner: 'Owner',
|
||||
|
||||
// ----- Modals -----
|
||||
alertOk: 'OK',
|
||||
@@ -1001,10 +1002,8 @@ const messages = {
|
||||
containerRestart: 'Restart this container',
|
||||
|
||||
// ----- VM disk tab -----
|
||||
vdiAttachDeviceButton: 'Attach disk',
|
||||
vbdCreateDeviceButton: 'New disk',
|
||||
vdiAttachDevice: 'Attach disk',
|
||||
vdiAttachDeviceConfirm:
|
||||
'The selected VDI is already attached to this VM. Are you sure you want to continue?',
|
||||
vdiBootOrder: 'Boot order',
|
||||
vdiNameLabel: 'Name',
|
||||
vdiNameDescription: 'Description',
|
||||
@@ -1708,9 +1707,6 @@ const messages = {
|
||||
copyVmSelectSr: 'Select SR',
|
||||
copyVmsNoTargetSr: 'No target SR',
|
||||
copyVmsNoTargetSrMessage: 'A target SR is required to copy a VM',
|
||||
notSupportedZstdWarning:
|
||||
'Zstd is not supported on {nVms, number} VM{nVms, plural, one {} other {s}}',
|
||||
notSupportedZstdTooltip: 'Click to see the concerned VMs',
|
||||
fastCloneMode: 'Fast clone',
|
||||
fullCopyMode: 'Full copy',
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import Tooltip from './tooltip'
|
||||
import { addSubscriptions, connectStore, formatSize } from './utils'
|
||||
import { createGetObject, createSelector } from './selectors'
|
||||
import { FormattedDate } from 'react-intl'
|
||||
import { isSrWritable, subscribeRemotes } from './xo'
|
||||
import { isSrWritable, subscribeRemotes, subscribeUsers } from './xo'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@@ -392,6 +392,40 @@ Vgpu.propTypes = {
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export const User = decorate([
|
||||
addSubscriptions(({ id }) => ({
|
||||
user: cb => subscribeUsers(users => cb(find(users, { id }))),
|
||||
})),
|
||||
({ id, user, link, newTab }) => {
|
||||
if (user === undefined) {
|
||||
return unknowItem(id, 'user')
|
||||
}
|
||||
|
||||
return (
|
||||
<LinkWrapper
|
||||
link={link}
|
||||
newTab={newTab}
|
||||
to={`/settings/acls?s=subject:id:${id}`}
|
||||
>
|
||||
<Icon icon='user' /> {user.email}
|
||||
</LinkWrapper>
|
||||
)
|
||||
},
|
||||
])
|
||||
|
||||
User.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
link: PropTypes.bool,
|
||||
newTab: PropTypes.bool,
|
||||
}
|
||||
|
||||
User.defaultProps = {
|
||||
link: false,
|
||||
newTab: false,
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const xoItemToRender = {
|
||||
// Subscription objects.
|
||||
cloudConfig: template => (
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { get, identity, isEmpty } from 'lodash'
|
||||
import * as CM from 'complex-matcher'
|
||||
import { escapeRegExp, get, identity, isEmpty } from 'lodash'
|
||||
|
||||
import { EMPTY_OBJECT } from './../utils'
|
||||
|
||||
@@ -56,4 +57,56 @@ export const constructSmartPattern = (
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const valueToComplexMatcher = pattern => {
|
||||
if (typeof pattern === 'string') {
|
||||
return new CM.RegExpNode(`^${escapeRegExp(pattern)}$`, 'i')
|
||||
}
|
||||
|
||||
if (Array.isArray(pattern)) {
|
||||
return new CM.And(pattern.map(valueToComplexMatcher))
|
||||
}
|
||||
|
||||
if (pattern !== null && typeof pattern === 'object') {
|
||||
const keys = Object.keys(pattern)
|
||||
const { length } = keys
|
||||
|
||||
if (length === 1) {
|
||||
const [key] = keys
|
||||
if (key === '__and') {
|
||||
return new CM.And(pattern.__and.map(valueToComplexMatcher))
|
||||
}
|
||||
if (key === '__or') {
|
||||
return new CM.Or(pattern.__or.map(valueToComplexMatcher))
|
||||
}
|
||||
if (key === '__not') {
|
||||
return new CM.Not(valueToComplexMatcher(pattern.__not))
|
||||
}
|
||||
}
|
||||
|
||||
const children = []
|
||||
Object.keys(pattern).forEach(property => {
|
||||
const subpattern = pattern[property]
|
||||
if (subpattern !== undefined) {
|
||||
children.push(
|
||||
new CM.Property(property, valueToComplexMatcher(subpattern))
|
||||
)
|
||||
}
|
||||
})
|
||||
return children.length === 0 ? new CM.Null() : new CM.And(children)
|
||||
}
|
||||
|
||||
throw new Error('could not transform this pattern')
|
||||
}
|
||||
|
||||
export const constructQueryString = pattern => {
|
||||
try {
|
||||
return valueToComplexMatcher(pattern).toString()
|
||||
} catch (error) {
|
||||
console.warn('constructQueryString', pattern, error)
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export default from './preview'
|
||||
|
||||
@@ -6,12 +6,12 @@ import { createSelector } from 'reselect'
|
||||
import { filter, map, pickBy } from 'lodash'
|
||||
|
||||
import Component from './../base-component'
|
||||
import constructQueryString from '../construct-query-string'
|
||||
import Icon from './../icon'
|
||||
import Link from './../link'
|
||||
import renderXoItem from './../render-xo-item'
|
||||
import Tooltip from './../tooltip'
|
||||
import { Card, CardBlock, CardHeader } from './../card'
|
||||
import { constructQueryString } from './index'
|
||||
|
||||
const SAMPLE_SIZE_OF_MATCHING_VMS = 3
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ import { SelectSr } from '../../select-objects'
|
||||
{
|
||||
isZstdSupported: createSelector(
|
||||
createGetObject((_, { vm }) => vm.$container),
|
||||
container => container === undefined || container.zstdSupported
|
||||
container => container.zstdSupported
|
||||
),
|
||||
},
|
||||
{ withRef: true }
|
||||
|
||||
@@ -7,29 +7,16 @@ import BaseComponent from 'base-component'
|
||||
import SingleLineRow from 'single-line-row'
|
||||
import Upgrade from 'xoa-upgrade'
|
||||
import { Col } from 'grid'
|
||||
import { createGetObjectsOfType } from 'selectors'
|
||||
import { SelectSr } from 'select-objects'
|
||||
import { buildTemplate, connectStore } from 'utils'
|
||||
|
||||
import constructQueryString from '../../construct-query-string'
|
||||
import Icon from '../../icon'
|
||||
import Link from '../../link'
|
||||
import SelectCompression from '../../select-compression'
|
||||
import Tooltip from '../../tooltip'
|
||||
import {
|
||||
createCollectionWrapper,
|
||||
createGetObjectsOfType,
|
||||
createSelector,
|
||||
} from '../../selectors'
|
||||
|
||||
@connectStore(
|
||||
() => {
|
||||
const getVms = createGetObjectsOfType('VM').pick((_, props) => props.vms)
|
||||
return {
|
||||
containers: createSelector(
|
||||
createGetObjectsOfType('pool'),
|
||||
createGetObjectsOfType('host'),
|
||||
(pools, hosts) => ({ ...pools, ...hosts })
|
||||
),
|
||||
vms: getVms,
|
||||
}
|
||||
},
|
||||
@@ -72,41 +59,9 @@ class CopyVmsModalBody extends BaseComponent {
|
||||
_onChangeNamePattern = event =>
|
||||
this.setState({ namePattern: event.target.value })
|
||||
|
||||
_getVmsWithoutZstd = createSelector(
|
||||
() => this.props.vms,
|
||||
() => this.props.containers,
|
||||
createCollectionWrapper((vms, containers) => {
|
||||
const vmIds = []
|
||||
for (const id in vms) {
|
||||
const container = containers[vms[id].$container]
|
||||
if (container !== undefined && !container.zstdSupported) {
|
||||
vmIds.push(id)
|
||||
}
|
||||
}
|
||||
return vmIds
|
||||
})
|
||||
)
|
||||
|
||||
_getVmsWithoutZstdLink = createSelector(
|
||||
this._getVmsWithoutZstd,
|
||||
vms => ({
|
||||
pathname: '/home',
|
||||
query: {
|
||||
t: 'VM',
|
||||
s: constructQueryString({
|
||||
id: {
|
||||
__or: vms,
|
||||
},
|
||||
}),
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
render() {
|
||||
const { formatMessage } = this.props.intl
|
||||
const { compression, namePattern, sr } = this.state
|
||||
const nVmsWithoutZstd =
|
||||
compression === 'zstd' ? this._getVmsWithoutZstd().length : 0
|
||||
return process.env.XOA_PLAN > 2 ? (
|
||||
<div>
|
||||
<SingleLineRow>
|
||||
@@ -136,20 +91,6 @@ class CopyVmsModalBody extends BaseComponent {
|
||||
onChange={this.linkState('compression')}
|
||||
value={compression}
|
||||
/>
|
||||
{compression === 'zstd' && nVmsWithoutZstd > 0 && (
|
||||
<Tooltip content={_('notSupportedZstdTooltip')}>
|
||||
<Link
|
||||
className='text-warning'
|
||||
target='_blank'
|
||||
to={this._getVmsWithoutZstdLink()}
|
||||
>
|
||||
<Icon icon='alarm' />{' '}
|
||||
{_('notSupportedZstdWarning', {
|
||||
nVms: nVmsWithoutZstd,
|
||||
})}
|
||||
</Link>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Col>
|
||||
</SingleLineRow>
|
||||
</div>
|
||||
|
||||
@@ -1,22 +1,10 @@
|
||||
import BaseComponent from 'base-component'
|
||||
import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
|
||||
import _ from '../../intl'
|
||||
import SelectCompression from '../../select-compression'
|
||||
import { connectStore } from '../../utils'
|
||||
import { Container, Row, Col } from '../../grid'
|
||||
import { createGetObject, createSelector } from '../../selectors'
|
||||
|
||||
@connectStore(
|
||||
{
|
||||
isZstdSupported: createSelector(
|
||||
createGetObject((_, { vm }) => vm.$container),
|
||||
container => container === undefined || container.zstdSupported
|
||||
),
|
||||
},
|
||||
{ withRef: true }
|
||||
)
|
||||
export default class ExportVmModalBody extends BaseComponent {
|
||||
state = {
|
||||
compression: '',
|
||||
@@ -37,7 +25,6 @@ export default class ExportVmModalBody extends BaseComponent {
|
||||
<Col mediumSize={6}>
|
||||
<SelectCompression
|
||||
onChange={this.linkState('compression')}
|
||||
showZstd={this.props.isZstdSupported}
|
||||
value={this.state.compression}
|
||||
/>
|
||||
</Col>
|
||||
@@ -46,7 +33,3 @@ export default class ExportVmModalBody extends BaseComponent {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
ExportVmModalBody.propTypes = {
|
||||
vm: PropTypes.object.isRequired,
|
||||
}
|
||||
|
||||
@@ -1457,7 +1457,7 @@ export const importVms = (vms, sr) =>
|
||||
import ExportVmModalBody from './export-vm-modal' // eslint-disable-line import/first
|
||||
export const exportVm = vm =>
|
||||
confirm({
|
||||
body: <ExportVmModalBody vm={vm} />,
|
||||
body: <ExportVmModalBody />,
|
||||
icon: 'export',
|
||||
title: _('exportVmLabel'),
|
||||
}).then(compress => {
|
||||
|
||||
@@ -3,7 +3,6 @@ import ActionButton from 'action-button'
|
||||
import addSubscriptions from 'add-subscriptions'
|
||||
import Button from 'button'
|
||||
import ButtonLink from 'button-link'
|
||||
import constructQueryString from 'construct-query-string'
|
||||
import Copiable from 'copiable'
|
||||
import CopyToClipboard from 'react-copy-to-clipboard'
|
||||
import decorate from 'apply-decorators'
|
||||
@@ -16,6 +15,7 @@ import Tooltip from 'tooltip'
|
||||
import { adminOnly, connectStore, routes } from 'utils'
|
||||
import { Card, CardHeader, CardBlock } from 'card'
|
||||
import { confirm } from 'modal'
|
||||
import { constructQueryString } from 'smart-backup'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import { createGetLoneSnapshots, createSelector } from 'selectors'
|
||||
import { get } from '@xen-orchestra/defined'
|
||||
|
||||
@@ -2,7 +2,6 @@ import _ from 'intl'
|
||||
import ActionRowButton from 'action-row-button'
|
||||
import ButtonGroup from 'button-group'
|
||||
import Component from 'base-component'
|
||||
import constructQueryString from 'construct-query-string'
|
||||
import Icon from 'icon'
|
||||
import Link from 'link'
|
||||
import LogList from '../../logs'
|
||||
@@ -14,6 +13,7 @@ import StateButton from 'state-button'
|
||||
import Tooltip from 'tooltip'
|
||||
import { confirm } from 'modal'
|
||||
import { addSubscriptions } from 'utils'
|
||||
import { constructQueryString } from 'smart-backup'
|
||||
import { createSelector } from 'selectors'
|
||||
import { Card, CardHeader, CardBlock } from 'card'
|
||||
import { filter, find, forEach, get, keyBy, map, orderBy } from 'lodash'
|
||||
|
||||
@@ -11,7 +11,7 @@ import Shortcuts from 'shortcuts'
|
||||
import themes from 'themes'
|
||||
import _, { IntlProvider } from 'intl'
|
||||
import { blockXoaAccess } from 'xoa-updater'
|
||||
import { connectStore, getXoaPlan, routes } from 'utils'
|
||||
import { connectStore, routes } from 'utils'
|
||||
import { Notification } from 'notification'
|
||||
import { ShortcutManager } from 'react-shortcuts'
|
||||
import { ThemeProvider } from 'styled-components'
|
||||
@@ -132,8 +132,6 @@ export default class XoApp extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
dismissSourceBanner = () => this.setState({ dismissedSourceBanner: true })
|
||||
|
||||
componentDidMount() {
|
||||
this.refs.bodyWrapper.style.minHeight =
|
||||
this.refs.menu.getWrappedInstance().height + 'px'
|
||||
@@ -203,14 +201,13 @@ export default class XoApp extends Component {
|
||||
render() {
|
||||
const { signedUp, trial, registerNeeded } = this.props
|
||||
const blocked = signedUp && blockXoaAccess(trial) // If we are under expired or unstable trial (signed up only)
|
||||
const plan = getXoaPlan()
|
||||
|
||||
return (
|
||||
<IntlProvider>
|
||||
<ThemeProvider theme={themes.base}>
|
||||
<DocumentTitle title='Xen Orchestra'>
|
||||
<div>
|
||||
{plan !== 'Community' && registerNeeded && (
|
||||
{process.env.XOA_PLAN < 5 && registerNeeded && (
|
||||
<div className='alert alert-danger mb-0'>
|
||||
{_('notRegisteredDisclaimerInfo')}{' '}
|
||||
<a
|
||||
@@ -225,7 +222,7 @@ export default class XoApp extends Component {
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
{plan === 'Community' && !this.state.dismissedSourceBanner && (
|
||||
{+process.env.XOA_PLAN === 5 && (
|
||||
<div className='alert alert-danger mb-0'>
|
||||
<a
|
||||
href='https://xen-orchestra.com/#!/xoa?pk_campaign=xo_source_banner'
|
||||
@@ -234,9 +231,6 @@ export default class XoApp extends Component {
|
||||
>
|
||||
{_('disclaimerText3')}
|
||||
</a>
|
||||
<button className='close' onClick={this.dismissSourceBanner}>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div style={CONTAINER_STYLE}>
|
||||
|
||||
@@ -84,10 +84,6 @@ const NewNetwork = decorate([
|
||||
: [],
|
||||
pifPredicate: (_, { pool }) => pif =>
|
||||
pif.vlan === -1 && pif.$host === (pool && pool.master),
|
||||
pifPredicateSdnController: (_, { pool }) => pif =>
|
||||
pif.physical &&
|
||||
pif.ip_configuration_mode !== 'None' &&
|
||||
pif.$host === (pool && pool.master),
|
||||
isSdnControllerLoaded: (state, { plugins = [] }) =>
|
||||
plugins.some(
|
||||
plugin => plugin.name === 'sdn-controller' && plugin.loaded
|
||||
@@ -130,7 +126,6 @@ const NewNetwork = decorate([
|
||||
networkName: name,
|
||||
networkDescription: description,
|
||||
encapsulation: encapsulation,
|
||||
pifId: pif.id,
|
||||
})
|
||||
: createNetwork({
|
||||
description,
|
||||
@@ -184,7 +179,6 @@ const NewNetwork = decorate([
|
||||
name,
|
||||
pif,
|
||||
pifPredicate,
|
||||
pifPredicateSdnController,
|
||||
pifs,
|
||||
vlan,
|
||||
isSdnControllerLoaded,
|
||||
@@ -210,89 +204,102 @@ const NewNetwork = decorate([
|
||||
</div>
|
||||
</Section>
|
||||
<Section icon='info' title='newNetworkInfo'>
|
||||
<div className='form-group'>
|
||||
<label>{_('newNetworkInterface')}</label>
|
||||
<SelectPif
|
||||
multi={bonded}
|
||||
onChange={effects.onChangePif}
|
||||
predicate={
|
||||
isPrivate ? pifPredicateSdnController : pifPredicate
|
||||
}
|
||||
required={bonded || isPrivate}
|
||||
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}
|
||||
/>
|
||||
{isPrivate ? (
|
||||
<div>
|
||||
<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>
|
||||
<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>
|
||||
{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
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
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'>
|
||||
|
||||
@@ -63,7 +63,7 @@ const UsageTooltip = decorate([
|
||||
snapshotsUsage: (_, { group: { snapshots } }) =>
|
||||
formatSize(sumBy(snapshots, 'usage')),
|
||||
vmNamesByVdi: createCollectionWrapper(({ vdis }, { vbds, vms }) =>
|
||||
mapValues(vdis, vdi => get(() => vms[vbds[vdi.$VBDs[0]].VM].name_label))
|
||||
mapValues(vdis, vdi => get(() => vms[vbds[vdi.VBD].VM]))
|
||||
),
|
||||
},
|
||||
}),
|
||||
@@ -183,7 +183,7 @@ export default class TabGeneral extends Component {
|
||||
)
|
||||
}
|
||||
// search root base copy for each VDI
|
||||
const vdisInfo = vdis.map(({ id, parent, name_label, usage, $VBDs }) => {
|
||||
const vdisInfo = vdis.map(({ id, parent, name_label, usage }) => {
|
||||
const baseCopies = new Set()
|
||||
let baseCopy
|
||||
let root = id
|
||||
@@ -212,7 +212,6 @@ export default class TabGeneral extends Component {
|
||||
root,
|
||||
snapshots: snapshots === undefined ? [] : snapshots,
|
||||
usage,
|
||||
$VBDs,
|
||||
}
|
||||
})
|
||||
// group VDIs by their root base copy.
|
||||
|
||||
@@ -449,25 +449,14 @@ class AttachDisk extends Component {
|
||||
|
||||
const _isFreeForWriting = vdi =>
|
||||
vdi.$VBDs.length === 0 ||
|
||||
every(vdi.$VBDs, id => {
|
||||
some(vdi.$VBDs, id => {
|
||||
const vbd = vbds[id]
|
||||
return !vbd || !vbd.attached || vbd.read_only
|
||||
})
|
||||
|
||||
const _attachDisk = () =>
|
||||
attachDiskToVm(vdi, vm, {
|
||||
bootable,
|
||||
mode: readOnly || !_isFreeForWriting(vdi) ? 'RO' : 'RW',
|
||||
}).then(onClose)
|
||||
|
||||
// check if the selected VDI is already attached to this VM.
|
||||
return some(vbds, { VDI: vdi.id, VM: vm.id })
|
||||
? confirm({
|
||||
body: _('vdiAttachDeviceConfirm'),
|
||||
icon: 'alarm',
|
||||
title: _('vdiAttachDevice'),
|
||||
}).then(_attachDisk)
|
||||
: _attachDisk()
|
||||
return attachDiskToVm(vdi, vm, {
|
||||
bootable,
|
||||
mode: readOnly || !_isFreeForWriting(vdi) ? 'RO' : 'RW',
|
||||
}).then(onClose)
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -878,7 +867,7 @@ export default class TabDisks extends Component {
|
||||
]
|
||||
|
||||
render() {
|
||||
const { allVbds, srs, vdis, vm } = this.props
|
||||
const { srs, vbds, vdis, vm } = this.props
|
||||
|
||||
const { attachDisk, bootOrder, newDisk } = this.state
|
||||
|
||||
@@ -897,7 +886,7 @@ export default class TabDisks extends Component {
|
||||
btnStyle={attachDisk ? 'info' : 'primary'}
|
||||
handler={this._toggleAttachDisk}
|
||||
icon='disk'
|
||||
labelId='vdiAttachDevice'
|
||||
labelId='vdiAttachDeviceButton'
|
||||
/>
|
||||
)}
|
||||
{vm.virtualizationMode !== 'pv' && (
|
||||
@@ -927,7 +916,7 @@ export default class TabDisks extends Component {
|
||||
<AttachDisk
|
||||
checkSr={this._getCheckSr()}
|
||||
vm={vm}
|
||||
vbds={allVbds}
|
||||
vbds={vbds}
|
||||
onClose={this._toggleAttachDisk}
|
||||
/>
|
||||
<hr />
|
||||
|
||||
@@ -6,7 +6,7 @@ import isEmpty from 'lodash/isEmpty'
|
||||
import map from 'lodash/map'
|
||||
import React from 'react'
|
||||
import HomeTags from 'home-tags'
|
||||
import renderXoItem from 'render-xo-item'
|
||||
import renderXoItem, { User } from 'render-xo-item'
|
||||
import Tooltip from 'tooltip'
|
||||
import { addTag, editVm, removeTag } from 'xo'
|
||||
import { BlockLink } from 'link'
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
createGetObjectsOfType,
|
||||
createGetVmLastShutdownTime,
|
||||
createSelector,
|
||||
isAdmin,
|
||||
} from 'selectors'
|
||||
import {
|
||||
connectStore,
|
||||
@@ -48,6 +49,7 @@ export default connectStore(() => {
|
||||
)
|
||||
|
||||
return {
|
||||
isAdmin,
|
||||
lastShutdownTime: createGetVmLastShutdownTime(),
|
||||
tasks: createGetObjectsOfType('task')
|
||||
.pick(
|
||||
@@ -63,6 +65,7 @@ export default connectStore(() => {
|
||||
}
|
||||
})(
|
||||
({
|
||||
isAdmin,
|
||||
lastShutdownTime,
|
||||
statsOverview,
|
||||
tasks,
|
||||
@@ -78,6 +81,7 @@ export default connectStore(() => {
|
||||
installTime,
|
||||
memory,
|
||||
os_version: osVersion,
|
||||
other,
|
||||
power_state: powerState,
|
||||
startTime,
|
||||
tags,
|
||||
@@ -209,6 +213,18 @@ export default connectStore(() => {
|
||||
</BlockLink>
|
||||
</Col>
|
||||
</Row>
|
||||
{isAdmin && other.owner !== undefined && (
|
||||
<Row className='text-xs-center'>
|
||||
<Col>
|
||||
{_('keyValue', {
|
||||
key: _('owner'),
|
||||
value: (
|
||||
<User id={other.owner} link={process.env.XOA_PLAN > 2} />
|
||||
),
|
||||
})}
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
{!xenTools && powerState === 'Running' && (
|
||||
<Row className='text-xs-center'>
|
||||
<Col>
|
||||
|
||||
Reference in New Issue
Block a user