Compare commits

..

2 Commits

Author SHA1 Message Date
Pierre Donias
0fad24d757 Handle clone and copy 2019-07-23 17:05:02 +02:00
Pierre Donias
3f0878940f WiP: VM owner 2019-07-23 17:05:01 +02:00
44 changed files with 502 additions and 707 deletions

View File

@@ -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)
![Channel: latest](https://badgen.net/badge/channel/latest/yellow)
### 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)
![Channel: stable](https://badgen.net/badge/channel/stable/green)
![Channel: latest](https://badgen.net/badge/channel/latest/yellow)
### Highlights
@@ -109,6 +78,8 @@
## **5.35.0** (2019-05-29)
![Channel: stable](https://badgen.net/badge/channel/stable/green)
### 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))

View File

@@ -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

View File

@@ -42,7 +42,6 @@
"testEnvironment": "node",
"testPathIgnorePatterns": [
"/dist/",
"/xo-server-test/",
"/xo-web/"
],
"testRegex": "\\.spec\\.js$",

View File

@@ -15,7 +15,7 @@
"predev": "yarn run prebuild",
"prepublishOnly": "yarn run build"
},
"version": "0.1.2",
"version": "0.1.1",
"engines": {
"node": ">=6"
},

View File

@@ -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,
})

View File

@@ -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,
})
})

View File

@@ -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

View File

@@ -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$"
}
}

View File

@@ -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 = '' }

View File

@@ -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())

View File

@@ -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',

View File

@@ -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] })

View File

@@ -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({

View File

@@ -1,6 +1,6 @@
{
"name": "xo-server-usage-report",
"version": "0.7.3",
"version": "0.7.2",
"license": "AGPL-3.0",
"description": "",
"keywords": [

View File

@@ -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,

View File

@@ -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": [

View File

@@ -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 = {

View File

@@ -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(

View File

@@ -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,

View File

@@ -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": [

View File

@@ -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 ''
}
}

View File

@@ -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',

View File

@@ -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',

View File

@@ -1557,7 +1557,7 @@ export default {
vdiAction: undefined,
// Original text: 'Attach disk'
vdiAttachDevice: undefined,
vdiAttachDeviceButton: undefined,
// Original text: 'New disk'
vbdCreateDeviceButton: undefined,

View File

@@ -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',

View File

@@ -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',

View File

@@ -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',

View File

@@ -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',

View File

@@ -1176,7 +1176,7 @@ export default {
vdiAction: '操作',
// Original text: "Attach disk"
vdiAttachDevice: '附加磁盘',
vdiAttachDeviceButton: '附加磁盘',
// Original text: "New disk"
vbdCreateDeviceButton: '新建磁盘',

View File

@@ -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',

View File

@@ -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 => (

View File

@@ -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'

View File

@@ -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

View File

@@ -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 }

View File

@@ -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>

View File

@@ -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,
}

View File

@@ -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 => {

View File

@@ -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'

View File

@@ -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'

View File

@@ -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}>
&times;
</button>
</div>
)}
<div style={CONTAINER_STYLE}>

View File

@@ -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'>

View File

@@ -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.

View File

@@ -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 />

View File

@@ -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>