fix(xo-server,xo-web,xo-server-usage-report): patches (#4077)

See #2565
See #3655
Fixes #2188
Fixes #3777
Fixes #3783
Fixes #3934
Fixes support#1228
Fixes support#1338
Fixes support#1362

- mergeInto: fix auto-patching on XS < 7.2
- mergeInto: homogenize both the host and pool's patches
- correctly install specific patches
- XCP-ng: fix "xcp-ng-updater not installed" bug
This commit is contained in:
Pierre Donias 2019-03-28 17:05:04 +01:00 committed by GitHub
parent fe1da4ea12
commit cde9a02c32
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 815 additions and 735 deletions

View File

@ -17,10 +17,16 @@
- Properly redirect to sign in page instead of being stuck in a refresh loop - Properly redirect to sign in page instead of being stuck in a refresh loop
- [Backup-ng] No more false positives when list matching VMs on Home page [#4078](https://github.com/vatesfr/xen-orchestra/issues/4078) (PR [#4085](https://github.com/vatesfr/xen-orchestra/pull/4085)) - [Backup-ng] No more false positives when list matching VMs on Home page [#4078](https://github.com/vatesfr/xen-orchestra/issues/4078) (PR [#4085](https://github.com/vatesfr/xen-orchestra/pull/4085))
- [Plugins] Properly remove optional settings when unchecking _Fill information_ (PR [#4076](https://github.com/vatesfr/xen-orchestra/pull/4076)) - [Plugins] Properly remove optional settings when unchecking _Fill information_ (PR [#4076](https://github.com/vatesfr/xen-orchestra/pull/4076))
- [Patches] (PR [#4077](https://github.com/vatesfr/xen-orchestra/pull/4077))
- Add a host to a pool: fixes the auto-patching of the host on XenServer < 7.2 [#3783](https://github.com/vatesfr/xen-orchestra/issues/3783)
- Add a host to a pool: homogenizes both the host and **pool**'s patches [#2188](https://github.com/vatesfr/xen-orchestra/issues/2188)
- Safely install a subset of patches on a pool [#3777](https://github.com/vatesfr/xen-orchestra/issues/3777)
- XCP-ng: no longer requires to run `yum install xcp-ng-updater` when it's already installed [#3934](https://github.com/vatesfr/xen-orchestra/issues/3934)
### Released packages ### Released packages
- vhd-lib v0.6.0 - vhd-lib v0.6.0
- @xen-orchestra/fs v0.8.0 - @xen-orchestra/fs v0.8.0
- xo-server-usage-report v0.7.2
- xo-server v5.38.0 - xo-server v5.38.0
- xo-web v5.38.0 - xo-web v5.38.0

View File

@ -494,7 +494,7 @@ async function getHostsMissingPatches({ runningHosts, xo }) {
map(runningHosts, async host => { map(runningHosts, async host => {
let hostsPatches = await xo let hostsPatches = await xo
.getXapi(host) .getXapi(host)
.listMissingPoolPatchesOnHost(host._xapiId) .listMissingPatches(host._xapiId)
.catch(error => { .catch(error => {
console.error( console.error(
'[WARN] error on fetching hosts missing patches:', '[WARN] error on fetching hosts missing patches:',

View File

@ -199,59 +199,6 @@ forget.resolve = {
// ------------------------------------------------------------------- // -------------------------------------------------------------------
// Returns an array of missing new patches in the host
// Returns an empty array if up-to-date
// Throws an error if the host is not running the latest XS version
export function listMissingPatches({ host }) {
return this.getXapi(host).listMissingPoolPatchesOnHost(host._xapiId)
}
listMissingPatches.description =
'return an array of missing new patches in the host'
listMissingPatches.params = {
host: { type: 'string' },
}
listMissingPatches.resolve = {
host: ['host', 'host', 'view'],
}
// -------------------------------------------------------------------
export function installPatch({ host, patch: patchUuid }) {
return this.getXapi(host).installPoolPatchOnHost(patchUuid, host._xapiId)
}
installPatch.description = 'install a patch on an host'
installPatch.params = {
host: { type: 'string' },
patch: { type: 'string' },
}
installPatch.resolve = {
host: ['host', 'host', 'administrate'],
}
// -------------------------------------------------------------------
export function installAllPatches({ host }) {
return this.getXapi(host).installAllPoolPatchesOnHost(host._xapiId)
}
installAllPatches.description = 'install all the missing patches on a host'
installAllPatches.params = {
host: { type: 'string' },
}
installAllPatches.resolve = {
host: ['host', 'host', 'administrate'],
}
// -------------------------------------------------------------------
export function emergencyShutdownHost({ host }) { export function emergencyShutdownHost({ host }) {
return this.getXapi(host).emergencyShutdownHost(host._xapiId) return this.getXapi(host).emergencyShutdownHost(host._xapiId)
} }

View File

@ -1,6 +1,4 @@
import { format } from 'json-rpc-peer' import { format } from 'json-rpc-peer'
import { differenceBy } from 'lodash'
import { mapToArray } from '../utils'
// =================================================================== // ===================================================================
@ -75,40 +73,43 @@ setPoolMaster.resolve = {
// ------------------------------------------------------------------- // -------------------------------------------------------------------
export async function installPatch({ pool, patch: patchUuid }) { // Returns an array of missing new patches in the host
await this.getXapi(pool).installPoolPatchOnAllHosts(patchUuid) // Returns an empty array if up-to-date
export function listMissingPatches({ host }) {
return this.getXapi(host).listMissingPatches(host._xapiId)
} }
installPatch.params = { listMissingPatches.description =
pool: { 'return an array of missing new patches in the host'
type: 'string',
}, listMissingPatches.params = {
patch: { host: { type: 'string' },
type: 'string',
},
} }
installPatch.resolve = { listMissingPatches.resolve = {
pool: ['pool', 'pool', 'administrate'], host: ['host', 'host', 'view'],
} }
// ------------------------------------------------------------------- // -------------------------------------------------------------------
export async function installAllPatches({ pool }) { export async function installPatches({ pool, patches, hosts }) {
await this.getXapi(pool).installAllPoolPatchesOnAllHosts() await this.getXapi(hosts === undefined ? pool : hosts[0]).installPatches({
patches,
hosts,
})
} }
installAllPatches.params = { installPatches.params = {
pool: { pool: { type: 'string', optional: true },
type: 'string', patches: { type: 'array', optional: true },
}, hosts: { type: 'array', optional: true },
} }
installAllPatches.resolve = { installPatches.resolve = {
pool: ['pool', 'pool', 'administrate'], pool: ['pool', 'pool', 'administrate'],
} }
installAllPatches.description = installPatches.description = 'Install patches on hosts'
'Install automatically all patches for every hosts of a pool'
// ------------------------------------------------------------------- // -------------------------------------------------------------------
@ -144,6 +145,22 @@ export { uploadPatch as patch }
// ------------------------------------------------------------------- // -------------------------------------------------------------------
export async function getPatchesDifference({ source, target }) {
return this.getPatchesDifference(target.id, source.id)
}
getPatchesDifference.params = {
source: { type: 'string' },
target: { type: 'string' },
}
getPatchesDifference.resolve = {
source: ['source', 'host', 'view'],
target: ['target', 'host', 'view'],
}
// -------------------------------------------------------------------
export async function mergeInto({ source, target, force }) { export async function mergeInto({ source, target, force }) {
const sourceHost = this.getObject(source.master) const sourceHost = this.getObject(source.master)
const targetHost = this.getObject(target.master) const targetHost = this.getObject(target.master)
@ -156,21 +173,21 @@ export async function mergeInto({ source, target, force }) {
) )
} }
const sourcePatches = sourceHost.patches const counterDiff = this.getPatchesDifference(source.master, target.master)
const targetPatches = targetHost.patches
const counterDiff = differenceBy(sourcePatches, targetPatches, 'name')
if (counterDiff.length > 0) { if (counterDiff.length > 0) {
throw new Error('host has patches that are not applied on target pool') const targetXapi = this.getXapi(target)
await targetXapi.installPatches({
patches: await targetXapi.findPatches(counterDiff),
})
} }
const diff = differenceBy(targetPatches, sourcePatches, 'name') const diff = this.getPatchesDifference(target.master, source.master)
if (diff.length > 0) {
// TODO: compare UUIDs const sourceXapi = this.getXapi(source)
await this.getXapi(source).installSpecificPatchesOnHost( await sourceXapi.installPatches({
mapToArray(diff, 'name'), patches: await sourceXapi.findPatches(diff),
sourceHost._xapiId })
) }
await this.mergeXenPools(source._xapiId, target._xapiId, force) await this.mergeXenPools(source._xapiId, target._xapiId, force)
} }

View File

@ -102,11 +102,10 @@ const TRANSFORMS = {
} = obj } = obj
const isRunning = isHostRunning(obj) const isRunning = isHostRunning(obj)
let supplementalPacks, patches let supplementalPacks
if (useUpdateSystem(obj)) { if (useUpdateSystem(obj)) {
supplementalPacks = [] supplementalPacks = []
patches = []
forEach(obj.$updates, update => { forEach(obj.$updates, update => {
const formattedUpdate = { const formattedUpdate = {
@ -121,7 +120,7 @@ const TRANSFORMS = {
} }
if (startsWith(update.name_label, 'XS')) { if (startsWith(update.name_label, 'XS')) {
patches.push(formattedUpdate) // It's a patch update but for homogeneity, we're still using pool_patches
} else { } else {
supplementalPacks.push(formattedUpdate) supplementalPacks.push(formattedUpdate)
} }
@ -171,7 +170,7 @@ const TRANSFORMS = {
} }
})(), })(),
multipathing: otherConfig.multipathing === 'true', multipathing: otherConfig.multipathing === 'true',
patches: patches || link(obj, 'patches'), patches: link(obj, 'patches'),
powerOnMode: obj.power_on_mode, powerOnMode: obj.power_on_mode,
power_state: metrics ? (isRunning ? 'Running' : 'Halted') : 'Unknown', power_state: metrics ? (isRunning ? 'Running' : 'Halted') : 'Unknown',
startTime: toTimestamp(otherConfig.boot_time), startTime: toTimestamp(otherConfig.boot_time),
@ -625,10 +624,18 @@ const TRANSFORMS = {
// ----------------------------------------------------------------- // -----------------------------------------------------------------
host_patch(obj) { host_patch(obj) {
const poolPatch = obj.$pool_patch
return { return {
type: 'patch',
applied: Boolean(obj.applied), applied: Boolean(obj.applied),
enforceHomogeneity: poolPatch.pool_applied,
description: poolPatch.name_description,
name: poolPatch.name_label,
pool_patch: poolPatch.$ref,
size: poolPatch.size,
guidance: poolPatch.after_apply_guidance,
time: toTimestamp(obj.timestamp_applied), time: toTimestamp(obj.timestamp_applied),
pool_patch: link(obj, 'pool_patch', '$ref'),
$host: link(obj, 'host'), $host: link(obj, 'host'),
} }
@ -640,12 +647,15 @@ const TRANSFORMS = {
return { return {
id: obj.$ref, id: obj.$ref,
applied: Boolean(obj.pool_applied), dataUuid: obj.uuid, // UUID of the patch file as stated in Citrix's XML file
description: obj.name_description, description: obj.name_description,
guidance: obj.after_apply_guidance, guidance: obj.after_apply_guidance,
name: obj.name_label, name: obj.name_label,
size: +obj.size, size: +obj.size,
uuid: obj.uuid, uuid: obj.$ref,
// TODO: means that the patch must be applied on every host
// applied: Boolean(obj.pool_applied),
// TODO: what does it mean, should we handle it? // TODO: what does it mean, should we handle it?
// version: obj.version, // version: obj.version,

View File

@ -2472,6 +2472,15 @@ export default class Xapi extends XapiBase {
) )
} }
// Main purpose: upload update on VDI
// Is a local SR on a non master host OK?
findAvailableSr(minSize) {
return find(
this.objects.all,
obj => obj.$type === 'SR' && canSrHaveNewVdiOfSize(obj, minSize)
)
}
async _assertConsistentHostServerTime(hostRef) { async _assertConsistentHostServerTime(hostRef) {
const delta = const delta =
parseDateTime(await this.call('host.get_servertime', hostRef)).getTime() - parseDateTime(await this.call('host.get_servertime', hostRef)).getTime() -

View File

@ -1,16 +1,8 @@
import asyncMap from '@xen-orchestra/async-map' import asyncMap from '@xen-orchestra/async-map'
import createLogger from '@xen-orchestra/log' import createLogger from '@xen-orchestra/log'
import deferrable from 'golike-defer' import deferrable from 'golike-defer'
import every from 'lodash/every'
import filter from 'lodash/filter'
import find from 'lodash/find'
import includes from 'lodash/includes'
import isObject from 'lodash/isObject'
import pickBy from 'lodash/pickBy'
import some from 'lodash/some'
import sortBy from 'lodash/sortBy'
import assign from 'lodash/assign'
import unzip from 'julien-f-unzip' import unzip from 'julien-f-unzip'
import { filter, find, pickBy, some } from 'lodash'
import ensureArray from '../../_ensureArray' import ensureArray from '../../_ensureArray'
import { debounce } from '../../decorators' import { debounce } from '../../decorators'
@ -18,9 +10,35 @@ import { forEach, mapFilter, mapToArray, parseXml } from '../../utils'
import { extractOpaqueRef, useUpdateSystem } from '../utils' import { extractOpaqueRef, useUpdateSystem } from '../utils'
// TOC -------------------------------------------------------------------------
// # HELPERS
// _isXcp
// _ejectToolsIsos
// _getXenUpdates Map of Objects
// # LIST
// _listXcpUpdates XCP available updates - Array of Objects
// _listPatches XS patches (installed or not) - Map of Objects
// _listInstalledPatches XS installed patches on the host - Map of Booleans
// _listInstallablePatches XS (host, requested patches) → sorted patches that are not installed and not conflicting - Array of Objects
// listMissingPatches HL: installable patches (XS) or updates (XCP) - Array of Objects
// findPatches HL: get XS patches IDs from names
// # INSTALL
// _xcpUpdate XCP yum update
// _legacyUploadPatch XS legacy upload
// _uploadPatch XS upload on a dedicated VDI
// installPatches HL: install patches (XS) or yum update (XCP) on hosts
// HELPERS ---------------------------------------------------------------------
const log = createLogger('xo:xapi') const log = createLogger('xo:xapi')
const _isXcp = host => host.software_version.product_brand === 'XCP-ng'
// =============================================================================
export default { export default {
// raw { uuid: patch } map translated from updates.xensource.com/XenServer/updates.xml
// FIXME: should be static // FIXME: should be static
@debounce(24 * 60 * 60 * 1000) @debounce(24 * 60 * 60 * 1000)
async _getXenUpdates() { async _getXenUpdates() {
@ -43,13 +61,16 @@ export default {
guidance: patch['after-apply-guidance'], guidance: patch['after-apply-guidance'],
name: patch['name-label'], name: patch['name-label'],
url: patch['patch-url'], url: patch['patch-url'],
id: patch.uuid,
uuid: patch.uuid, uuid: patch.uuid,
conflicts: mapToArray(ensureArray(patch.conflictingpatches), patch => { conflicts: mapToArray(
return patch.conflictingpatch.uuid ensureArray(patch.conflictingpatches),
}), patch => patch.conflictingpatch.uuid
requirements: mapToArray(ensureArray(patch.requiredpatches), patch => { ),
return patch.requiredpatch.uuid requirements: mapToArray(
}), ensureArray(patch.requiredpatches),
patch => patch.requiredpatch.uuid
),
paid: patch['update-stream'] === 'premium', paid: patch['update-stream'] === 'premium',
upgrade: /^XS\d{2,}$/.test(patch['name-label']), upgrade: /^XS\d{2,}$/.test(patch['name-label']),
// TODO: what does it mean, should we handle it? // TODO: what does it mean, should we handle it?
@ -96,72 +117,12 @@ export default {
} }
}, },
// ================================================================= // eject all ISOs from all the host's VMs when installing patches
// if hostRef is not specified: eject ISOs on all the pool's VMs
// Returns installed and not installed patches for a given host.
async _getPoolPatchesForHost(host) {
const versions = (await this._getXenUpdates()).versions
const hostVersions = host.software_version
const version =
versions[hostVersions.product_version] ||
versions[hostVersions.product_version_text]
return version ? version.patches : []
},
_getInstalledPoolPatchesOnHost(host) {
const installed = { __proto__: null }
// platform_version < 2.1.1
forEach(host.$patches, hostPatch => {
installed[hostPatch.$pool_patch.uuid] = true
})
// platform_version >= 2.1.1
forEach(host.$updates, update => {
installed[update.uuid] = true // TODO: ignore packs
})
return installed
},
async _listMissingPoolPatchesOnHost(host) {
const all = await this._getPoolPatchesForHost(host)
const installed = this._getInstalledPoolPatchesOnHost(host)
const installable = { __proto__: null }
forEach(all, (patch, uuid) => {
if (installed[uuid]) {
return
}
for (const uuid of patch.conflicts) {
if (uuid in installed) {
return
}
}
installable[uuid] = patch
})
return installable
},
async listMissingPoolPatchesOnHost(hostId) {
const host = this.getObject(hostId)
// Returns an array to not break compatibility.
return mapToArray(
await (host.software_version.product_brand === 'XCP-ng'
? this._xcpListHostUpdates(host)
: this._listMissingPoolPatchesOnHost(host))
)
},
async _ejectToolsIsos(hostRef) { async _ejectToolsIsos(hostRef) {
return Promise.all( return Promise.all(
mapFilter(this.objects.all, vm => { mapFilter(this.objects.all, vm => {
if (vm.$type !== 'VM' || (hostRef && vm.resident_on !== hostRef)) { if (vm.$type !== 'vm' || (hostRef && vm.resident_on !== hostRef)) {
return return
} }
@ -178,54 +139,235 @@ export default {
) )
}, },
// ----------------------------------------------------------------- // LIST ----------------------------------------------------------------------
_isPoolPatchInstallableOnHost(patchUuid, host) { // list all yum updates available for a XCP-ng host
const installed = this._getInstalledPoolPatchesOnHost(host) // (hostObject) → { uuid: patchObject }
async _listXcpUpdates(host) {
return JSON.parse(
await this.call(
'host.call_plugin',
host.$ref,
'updater.py',
'check_update',
{}
)
)
},
if (installed[patchUuid]) { // list all patches provided by Citrix for this host version regardless
return false // of if they're installed or not
// ignores upgrade patches
// (hostObject) → { uuid: patchObject }
async _listPatches(host) {
const versions = (await this._getXenUpdates()).versions
const hostVersions = host.software_version
const version =
versions[hostVersions.product_version] ||
versions[hostVersions.product_version_text]
return version ? pickBy(version.patches, patch => !patch.upgrade) : {}
},
// list patches installed on the host
// (hostObject) → { uuid: boolean }
_listInstalledPatches(host) {
const installed = { __proto__: null }
// Legacy XS patches
if (!useUpdateSystem(host)) {
forEach(host.$patches, hostPatch => {
installed[hostPatch.$pool_patch.uuid] = true
})
return installed
}
// ----------
forEach(host.$updates, update => {
// ignore packs
if (update.name_label.startsWith('XS')) {
installed[update.uuid] = true
}
})
return installed
},
// TODO: handle upgrade patches
// (hostObject, [ patchId ]) → [ patchObject ]
async _listInstallablePatches(host, requestedPatches) {
const all = await this._listPatches(host)
const installed = this._listInstalledPatches(host)
let getAll = false
if (requestedPatches === undefined) {
getAll = true
requestedPatches = Object.keys(all)
}
const freeHost = this.pool.$master.license_params.sku_type === 'free'
// We assume:
// - no conflict transitivity (If A conflicts with B and B with C, Citrix should tell us explicitly that A conflicts with C)
// - no requirements transitivity (If A requires B and B requires C, Citrix should tell us explicitly that A requires C)
// - sorted requirements (If A requires B and C, then C cannot require B)
// For each requested patch:
// - throw if not found
// - throw if already installed
// - ignore if already in installable (may have been added because of requirements)
// - if paid patch on free host: either ignore (listing all the patches) or throw (patch is requested)
// - throw if conflicting patches installed
// - throw if conflicting patches in installable
// - throw if one of the requirements is not found
// - push its required patches in installable
// - push it in installable
const installable = []
forEach(requestedPatches, id => {
const patch = all[id]
if (patch === undefined) {
throw new Error(`patch not found: ${id}`)
} }
let installable = true if (installed[id] !== undefined) {
if (getAll) {
forEach(installed, patch => { return
if (includes(patch.conflicts, patchUuid)) {
installable = false
return false
} }
throw new Error(`patch already installed: ${patch.name} (${id})`)
}
if (find(installable, { id }) !== undefined) {
return
}
if (patch.paid && freeHost) {
if (getAll) {
return
}
throw new Error(
`requested patch ${patch.name} (${id}) requires a XenServer license`
)
}
let conflictId
if (
(conflictId = find(
patch.conflicts,
conflictId => installed[conflictId] !== undefined
)) !== undefined
) {
if (getAll) {
log(
`patch ${
patch.name
} (${id}) conflicts with installed patch ${conflictId}`
)
return
}
throw new Error(
`patch ${
patch.name
} (${id}) conflicts with installed patch ${conflictId}`
)
}
if (
(conflictId = find(patch.conflicts, conflictId =>
find(installable, { id: conflictId })
)) !== undefined
) {
if (getAll) {
log(`patches ${id} and ${conflictId} conflict with eachother`)
return
}
throw new Error(
`patches ${id} and ${conflictId} conflict with eachother`
)
}
// add requirements
forEach(patch.requirements, id => {
const requiredPatch = all[id]
if (requiredPatch === undefined) {
throw new Error(`required patch ${id} not found`)
}
if (!installed[id] && find(installable, { id }) === undefined) {
if (requiredPatch.paid && freeHost) {
throw new Error(
`required patch ${
requiredPatch.name
} (${id}) requires a XenServer license`
)
}
installable.push(requiredPatch)
}
})
// add itself
installable.push(patch)
}) })
return installable return installable
}, },
_isPoolPatchInstallableOnPool(patchUuid) { // high level
return every( listMissingPatches(hostId) {
this.objects.all, const host = this.getObject(hostId)
obj => return _isXcp(host)
obj.$type !== 'host' || ? this._listXcpUpdates(host)
this._isPoolPatchInstallableOnHost(patchUuid, obj) : // TODO: list paid patches of free hosts as well so the UI can show them
this._listInstallablePatches(host)
},
// convenient method to find which patches should be installed from a
// list of patch names
// e.g.: compare the installed patches of 2 hosts by their
// names (XS..E...) then find the patches global ID
// [ names ] → [ IDs ]
async findPatches(names) {
const all = await this._listPatches(this.pool.$master)
return filter(all, patch => names.includes(patch.name)).map(
patch => patch.id
) )
}, },
// ----------------------------------------------------------------- // INSTALL -------------------------------------------------------------------
// platform_version < 2.1.1 ---------------------------------------- _xcpUpdate(hosts) {
async uploadPoolPatch(stream, patchName) { if (hosts === undefined) {
const patchRef = await this.putResource(stream, '/pool_patch_upload', { hosts = filter(this.objects.all, { $type: 'host' })
task: this.createTask('Patch upload', patchName), } else {
}).then(extractOpaqueRef) hosts = filter(
this.objects.all,
obj => obj.$type === 'host' && hosts.includes(obj.$id)
)
}
return this._getOrWaitObject(patchRef) return asyncMap(hosts, async host => {
const update = await this.call(
'host.call_plugin',
host.$ref,
'updater.py',
'update',
{}
)
if (JSON.parse(update).exit !== 0) {
throw new Error('Update install failed')
} else {
await this._updateObjectMapProperty(host, 'other_config', {
rpm_patch_installation_time: String(Date.now() / 1000),
})
}
})
}, },
async _getOrUploadPoolPatch(uuid) { // Legacy XS patches: upload a patch on a pool before installing it
async _legacyUploadPatch(uuid) {
// check if the patch has already been uploaded
try { try {
return this.getObjectByUuid(uuid) return this.getObjectByUuid(uuid)
} catch (error) {} } catch (e) {}
log.debug(`downloading patch ${uuid}`) log.debug(`legacy downloading patch ${uuid}`)
const patchInfo = (await this._getXenUpdates()).patches[uuid] const patchInfo = (await this._getXenUpdates()).patches[uuid]
if (!patchInfo) { if (!patchInfo) {
@ -248,16 +390,21 @@ export default {
.on('error', reject) .on('error', reject)
}) })
return this.uploadPoolPatch(stream, patchInfo.name) const patchRef = await this.putResource(stream, '/pool_patch_upload', {
task: this.createTask('Patch upload', patchInfo.name),
}).then(extractOpaqueRef)
return this._getOrWaitObject(patchRef)
}, },
// ----------
// patform_version >= 2.1.1 ---------------------------------------- // upload patch on a VDI on a shared SR
async _getUpdateVdi($defer, patchUuid, hostId) { async _uploadPatch($defer, uuid) {
log.debug(`downloading patch ${patchUuid}`) log.debug(`downloading patch ${uuid}`)
const patchInfo = (await this._getXenUpdates()).patches[patchUuid] const patchInfo = (await this._getXenUpdates()).patches[uuid]
if (!patchInfo) { if (!patchInfo) {
throw new Error('no such patch ' + patchUuid) throw new Error('no such patch ' + uuid)
} }
let stream = await this.xo.httpRequest(patchInfo.url) let stream = await this.xo.httpRequest(patchInfo.url)
@ -271,315 +418,104 @@ export default {
.on('error', reject) .on('error', reject)
}) })
let vdi const sr = this.findAvailableSr(stream.length)
if (sr === undefined) {
// If no hostId provided, try and find a shared SR
if (!hostId) {
const sr = this.findAvailableSharedSr(stream.length)
if (!sr) {
return return
} }
vdi = await this.createTemporaryVdiOnSr( const vdi = await this.createTemporaryVdiOnSr(
stream, stream,
sr, sr,
'[XO] Patch ISO', '[XO] Patch ISO',
'small temporary VDI to store a patch ISO' 'small temporary VDI to store a patch ISO'
) )
} else {
vdi = await this.createTemporaryVdiOnHost(
stream,
hostId,
'[XO] Patch ISO',
'small temporary VDI to store a patch ISO'
)
}
$defer(() => this._deleteVdi(vdi.$ref)) $defer(() => this._deleteVdi(vdi.$ref))
return vdi return vdi
}, },
// ----------------------------------------------------------------- _poolWideInstall: deferrable(async function($defer, patches) {
// Legacy XS patches
// patform_version < 2.1.1 ----------------------------------------- if (!useUpdateSystem(this.pool.$master)) {
async _installPoolPatchOnHost(patchUuid, host) { // for each patch: pool_patch.pool_apply
for (const p of patches) {
const [patch] = await Promise.all([ const [patch] = await Promise.all([
this._getOrUploadPoolPatch(patchUuid), this._legacyUploadPatch(p.uuid),
this._ejectToolsIsos(host.$ref), this._ejectToolsIsos(this.pool.$master.$ref),
])
await this.call('pool_patch.apply', patch.$ref, host.$ref)
},
// patform_version >= 2.1.1
_installPatchUpdateOnHost: deferrable(async function(
$defer,
patchUuid,
host
) {
await this._assertConsistentHostServerTime(host.$ref)
const [vdi] = await Promise.all([
this._getUpdateVdi($defer, patchUuid, host.$id),
this._ejectToolsIsos(host.$ref),
])
const updateRef = await this.call('pool_update.introduce', vdi.$ref)
// TODO: check update status
// const precheck = await this.call('pool_update.precheck', updateRef, host.$ref)
// - ok_livepatch_complete An applicable live patch exists for every required component
// - ok_livepatch_incomplete An applicable live patch exists but it is not sufficient
// - ok There is no applicable live patch
return this.call('pool_update.apply', updateRef, host.$ref)
}),
// -----------------------------------------------------------------
async installPoolPatchOnHost(patchUuid, host) {
log.debug(`installing patch ${patchUuid}`)
if (!isObject(host)) {
host = this.getObject(host)
}
return useUpdateSystem(host)
? this._installPatchUpdateOnHost(patchUuid, host)
: this._installPoolPatchOnHost(patchUuid, host)
},
// -----------------------------------------------------------------
// platform_version < 2.1.1
async _installPoolPatchOnAllHosts(patchUuid) {
const [patch] = await Promise.all([
this._getOrUploadPoolPatch(patchUuid),
this._ejectToolsIsos(),
]) ])
await this.call('pool_patch.pool_apply', patch.$ref) await this.call('pool_patch.pool_apply', patch.$ref)
}, }
return
}
// ----------
// platform_version >= 2.1.1 // for each patch: pool_update.introduce → pool_update.pool_apply
_installPatchUpdateOnAllHosts: deferrable(async function($defer, patchUuid) { for (const p of patches) {
await this._assertConsistentHostServerTime(this.pool.master) const [vdi] = await Promise.all([
this._uploadPatch($defer, p.uuid),
let [vdi] = await Promise.all([
this._getUpdateVdi($defer, patchUuid),
this._ejectToolsIsos(), this._ejectToolsIsos(),
]) ])
if (vdi == null) { if (vdi === undefined) {
vdi = await this._getUpdateVdi($defer, patchUuid, this.pool.master) throw new Error('patch could not be uploaded')
} }
return this.call( log.debug(`installing patch ${p.uuid}`)
await this.call(
'pool_update.pool_apply', 'pool_update.pool_apply',
await this.call('pool_update.introduce', vdi.$ref) await this.call('pool_update.introduce', vdi.$ref)
) )
}
}), }),
async installPoolPatchOnAllHosts(patchUuid) { async _hostInstall(patches, host) {
log.debug(`installing patch ${patchUuid} on all hosts`) throw new Error('single host install not implemented')
// Legacy XS patches
return useUpdateSystem(this.pool.$master) // for each patch: pool_patch.apply
? this._installPatchUpdateOnAllHosts(patchUuid) // ----------
: this._installPoolPatchOnAllHosts(patchUuid) // for each patch: pool_update.introduce → pool_update.apply
}, },
// ----------------------------------------------------------------- // high level
// install specified patches on specified hosts
// If no host is provided, install on pool //
async _installPoolPatchAndRequirements(patch, patchesByUuid, host) { // no hosts specified: pool-wide install (only the pool master installed patches will be considered)
if ( // no patches specified: install either the pool master's missing patches (no hosts specified) or each host's missing patches
host == null //
? !this._isPoolPatchInstallableOnPool(patch.uuid) // patches will be ignored for XCP (always updates completely)
: !this._isPoolPatchInstallableOnHost(patch.uuid, host) // patches that are already installed will be ignored (XS only)
) { //
return // XS pool-wide optimization only works when no hosts are specified
// it may install more patches that specified if some of them require other patches
async installPatches({ patches, hosts }) {
// XCP
if (_isXcp(this.pool.$master)) {
return this._xcpUpdate(hosts)
} }
const { requirements } = patch // XS
// TODO: assert consistent time
if (requirements.length) { const poolWide = hosts === undefined
for (const requirementUuid of requirements) { if (poolWide) {
const requirement = patchesByUuid[requirementUuid] log.debug('patches that were requested to be installed', patches)
const installablePatches = await this._listInstallablePatches(
if (requirement != null) { this.pool.$master,
await this._installPoolPatchAndRequirements( patches
requirement,
patchesByUuid,
host
)
host = host && this.getObject(host.$id)
}
}
}
return host == null
? this.installPoolPatchOnAllHosts(patch.uuid)
: this.installPoolPatchOnHost(patch.uuid, host)
},
async installSpecificPatchesOnHost(patchNames, hostId) {
const host = this.getObject(hostId)
const missingPatches = await this._listMissingPoolPatchesOnHost(host)
const patchesToInstall = []
const addPatchesToList = patches => {
forEach(patches, patch => {
addPatchesToList(mapToArray(patch.requirements, { uuid: patch.uuid }))
if (!find(patchesToInstall, { name: patch.name })) {
patchesToInstall.push(patch)
}
})
}
addPatchesToList(
mapToArray(patchNames, name => find(missingPatches, { name }))
) )
for (let i = 0, n = patchesToInstall.length; i < n; i++) { log.debug(
await this._installPoolPatchAndRequirements( 'patches that will actually be installed',
patchesToInstall[i], installablePatches.map(patch => patch.uuid)
missingPatches,
host
)
}
},
async installAllPoolPatchesOnHost(hostId) {
const host = this.getObject(hostId)
if (host.software_version.product_brand === 'XCP-ng') {
return this._xcpInstallHostUpdates(host)
}
return this._installAllPoolPatchesOnHost(host)
},
async _installAllPoolPatchesOnHost(host) {
const installableByUuid =
host.license_params.sku_type !== 'free'
? pickBy(await this._listMissingPoolPatchesOnHost(host), {
upgrade: false,
})
: pickBy(await this._listMissingPoolPatchesOnHost(host), {
paid: false,
upgrade: false,
})
// List of all installable patches sorted from the newest to the
// oldest.
const installable = sortBy(
installableByUuid,
patch => -Date.parse(patch.date)
) )
for (let i = 0, n = installable.length; i < n; ++i) { return this._poolWideInstall(installablePatches)
const patch = installable[i]
if (this._isPoolPatchInstallableOnHost(patch.uuid, host)) {
await this._installPoolPatchAndRequirements(
patch,
installableByUuid,
host
).catch(error => {
if (
error.code !== 'PATCH_ALREADY_APPLIED' &&
error.code !== 'UPDATE_ALREADY_APPLIED'
) {
throw error
} }
})
host = this.getObject(host.$id)
}
}
},
async installAllPoolPatchesOnAllHosts() { // for each host
if (this.pool.$master.software_version.product_brand === 'XCP-ng') { // get installable patches
return this._xcpInstallAllPoolUpdatesOnHost() // filter patches that should be installed
} // sort patches
return this._installAllPoolPatchesOnAllHosts() // host-by-host install
}, throw new Error('non pool-wide install not implemented')
async _installAllPoolPatchesOnAllHosts() {
const installableByUuid = assign(
{},
...(await Promise.all(
mapFilter(this.objects.all, host => {
if (host.$type === 'host') {
return this._listMissingPoolPatchesOnHost(host).then(patches =>
host.license_params.sku_type !== 'free'
? pickBy(patches, { upgrade: false })
: pickBy(patches, { paid: false, upgrade: false })
)
}
})
))
)
// List of all installable patches sorted from the newest to the
// oldest.
const installable = sortBy(
installableByUuid,
patch => -Date.parse(patch.date)
)
for (let i = 0, n = installable.length; i < n; ++i) {
const patch = installable[i]
await this._installPoolPatchAndRequirements(
patch,
installableByUuid
).catch(error => {
if (
error.code !== 'PATCH_ALREADY_APPLIED' &&
error.code !== 'UPDATE_ALREADY_APPLIED_IN_POOL'
) {
throw error
}
})
}
},
// ----------------------------------
// XCP-ng dedicated zone for patching
// ----------------------------------
// list all yum updates available for a XCP-ng host
async _xcpListHostUpdates(host) {
return JSON.parse(
await this.call(
'host.call_plugin',
host.$ref,
'updater.py',
'check_update',
{}
)
)
},
// install all yum updates for a XCP-ng host
async _xcpInstallHostUpdates(host) {
const update = await this.call(
'host.call_plugin',
host.$ref,
'updater.py',
'update',
{}
)
if (JSON.parse(update).exit !== 0) {
throw new Error('Update install failed')
} else {
await this._updateObjectMapProperty(host, 'other_config', {
rpm_patch_installation_time: String(Date.now() / 1000),
})
}
},
// install all yum updates for all XCP-ng hosts in a give pool
async _xcpInstallAllPoolUpdatesOnHost() {
await asyncMap(filter(this.objects.all, { $type: 'host' }), host =>
this._xcpInstallHostUpdates(host)
)
}, },
} }

View File

@ -0,0 +1,18 @@
import { differenceBy } from 'lodash'
export default class {
constructor(xo) {
this._xo = xo
}
getPatchesDifference(hostA, hostB) {
const patchesA = this._xo
.getObject(hostA)
.patches.map(patchId => this._xo.getObject(patchId))
const patchesB = this._xo
.getObject(hostB)
.patches.map(patchId => this._xo.getObject(patchId))
return differenceBy(patchesA, patchesB, 'name').map(patch => patch.name)
}
}

View File

@ -15,11 +15,7 @@ import {
createFilter, createFilter,
createSelector, createSelector,
} from './selectors' } from './selectors'
import { import { installAllPatchesOnPool, subscribeHostMissingPatches } from './xo'
installAllHostPatches,
installAllPatchesOnPool,
subscribeHostMissingPatches,
} from './xo'
// =================================================================== // ===================================================================
@ -43,17 +39,6 @@ const MISSING_PATCHES_COLUMNS = [
), ),
sortCriteria: (host, { missingPatches }) => missingPatches[host.id], sortCriteria: (host, { missingPatches }) => missingPatches[host.id],
}, },
{
name: _('patchUpdateButton'),
itemRenderer: (host, { installAllHostPatches }) => (
<ActionButton
btnStyle='primary'
handler={installAllHostPatches}
handlerParam={host}
icon='host-patch-update'
/>
),
},
] ]
const POOLS_MISSING_PATCHES_COLUMNS = [ const POOLS_MISSING_PATCHES_COLUMNS = [
@ -115,7 +100,9 @@ class HostsPatchesTable extends Component {
pools[host.$pool] = true pools[host.$pool] = true
}) })
return Promise.all(map(keys(pools), installAllPatchesOnPool)) return Promise.all(
map(keys(pools), pool => installAllPatchesOnPool({ pool }))
)
} }
componentDidMount() { componentDidMount() {
@ -162,7 +149,6 @@ class HostsPatchesTable extends Component {
: MISSING_PATCHES_COLUMNS : MISSING_PATCHES_COLUMNS
} }
userData={{ userData={{
installAllHostPatches,
missingPatches: this.state.missingPatches, missingPatches: this.state.missingPatches,
pools, pools,
}} }}

View File

@ -756,10 +756,12 @@ const messages = {
addSrLabel: 'Add SR', addSrLabel: 'Add SR',
addVmLabel: 'Add VM', addVmLabel: 'Add VM',
addHostLabel: 'Add Host', addHostLabel: 'Add Host',
hostNeedsPatchUpdate: missingPatchesPool:
'This host needs to install {patches, number} patch{patches, plural, one {} other {es}} before it can be added to the pool. This operation may be long.', 'The pool needs to install {nMissingPatches, number} patch{nMissingPatches, plural, one {} other {es}}. This operation may be long.',
hostNeedsPatchUpdateNoInstall: missingPatchesHost:
"This host cannot be added to the pool because it's missing some patches.", 'This host needs to install {nMissingPatches, number} patch{nMissingPatches, plural, one {} other {es}}. This operation may be long.',
patchUpdateNoInstall:
'This host cannot be added to the pool because the patches are not homogeneous.',
addHostErrorTitle: 'Adding host failed', addHostErrorTitle: 'Adding host failed',
addHostNotHomogeneousErrorMessage: 'Host patches could not be homogenized.', addHostNotHomogeneousErrorMessage: 'Host patches could not be homogenized.',
disconnectServer: 'Disconnect', disconnectServer: 'Disconnect',
@ -886,14 +888,14 @@ const messages = {
hostAppliedPatches: 'Applied patches', hostAppliedPatches: 'Applied patches',
hostMissingPatches: 'Missing patches', hostMissingPatches: 'Missing patches',
hostUpToDate: 'Host up-to-date!', hostUpToDate: 'Host up-to-date!',
installPatchWarningTitle: 'Non-recommended patch install', installAllPatchesTitle: 'Install all patches',
installPatchWarningContent: installAllPatchesContent: 'To install all patches go to pool.',
'This will install a patch only on this host. This is NOT the recommended way: please go into the Pool patch view and follow instructions there. If you are sure about this, you can continue anyway', installAllPatchesRedirect: 'Go to pool',
installPatchWarningReject: 'Go to pool', installAllPatchesOnHostContent:
installPatchWarningResolve: 'Install', 'Are you sure you want to install all patches on this host?',
patchRelease: 'Release', patchRelease: 'Release',
updatePluginNotInstalled: updatePluginNotInstalled:
'Update plugin is not installed on this host. Please run `yum install xcp-ng-updater` first.', 'An error occurred while fetching the patches. Please make sure the updater plugin is installed by running `yum install xcp-ng-updater` on the host.',
showChangelog: 'Show changelog', showChangelog: 'Show changelog',
changelog: 'Changelog', changelog: 'Changelog',
changelogPatch: 'Patch', changelogPatch: 'Patch',
@ -902,6 +904,10 @@ const messages = {
changelogDescription: 'Description', changelogDescription: 'Description',
// ----- Pool patch tabs ----- // ----- Pool patch tabs -----
refreshPatches: 'Refresh patches', refreshPatches: 'Refresh patches',
install: 'Install',
installPatchesTitle: 'Install patch{nPatches, plural, one {} other {es}}',
installPatchesContent:
'Are you sure you want to install {nPatches, number} patch{nPatches, plural, one {} other {es}}?',
installPoolPatches: 'Install pool patches', installPoolPatches: 'Install pool patches',
confirmPoolPatch: confirmPoolPatch:
'Are you sure you want to install all the patches on this pool?', 'Are you sure you want to install all the patches on this pool?',

View File

@ -332,8 +332,8 @@ export const createSortForType = invoke(() => {
const iterateesByType = { const iterateesByType = {
message: message => message.time, message: message => message.time,
PIF: pif => pif.device, PIF: pif => pif.device,
patch: patch => patch.name,
pool: pool => pool.name_label, pool: pool => pool.name_label,
pool_patch: patch => patch.name,
tag: tag => tag, tag: tag => tag,
VBD: vbd => vbd.position, VBD: vbd => vbd.position,
'VDI-snapshot': snapshot => snapshot.snapshot_time, 'VDI-snapshot': snapshot => snapshot.snapshot_time,
@ -494,37 +494,18 @@ export const createGetObjectMessages = objectSelector =>
export const getObject = createGetObject((_, id) => id) export const getObject = createGetObject((_, id) => id)
export const createDoesHostNeedRestart = hostSelector => { export const createDoesHostNeedRestart = hostSelector => {
// XS < 7.1 const patchRequiresReboot = createGetObjectsOfType('patch')
const patchRequiresReboot = createGetObjectsOfType('pool_patch') .pick(create(hostSelector, host => host.patches))
.pick( .find(
// Returns the first patch of the host which requires it to be create(hostSelector, host => ({ guidance, time, upgrade }) =>
// restarted. time > host.startTime &&
create( (upgrade ||
createGetObjectsOfType('host_patch') some(
.pick((state, props) => {
const host = hostSelector(state, props)
return host && host.patches
})
.filter(
create(
(state, props) => {
const host = hostSelector(state, props)
return host && host.startTime
},
startTime => patch => patch.time > startTime
)
),
hostPatches => map(hostPatches, hostPatch => hostPatch.pool_patch)
)
)
.find([
({ guidance, upgrade }) =>
upgrade ||
find(
guidance, guidance,
action => action === 'restartHost' || action === 'restartXapi' action => action === 'restartHost' || action === 'restartXapi'
), ))
]) )
)
return create( return create(
hostSelector, hostSelector,

View File

@ -9,10 +9,10 @@ import {
createCollectionWrapper, createCollectionWrapper,
createGetObjectsOfType, createGetObjectsOfType,
createSelector, createSelector,
createGetObject,
} from 'selectors' } from 'selectors'
import { forEach } from 'lodash'
import { getPatchesDifference } from 'xo'
import { SelectHost } from 'select-objects' import { SelectHost } from 'select-objects'
import { differenceBy, forEach } from 'lodash'
@connectStore( @connectStore(
() => ({ () => ({
@ -38,20 +38,20 @@ import { differenceBy, forEach } from 'lodash'
return singleHosts return singleHosts
}) })
), ),
poolMasterPatches: createSelector(
createGetObject((_, props) => props.pool.master),
({ patches }) => patches
),
}), }),
{ withRef: true } { withRef: true }
) )
export default class AddHostModal extends BaseComponent { export default class AddHostModal extends BaseComponent {
get value() { get value() {
if (process.env.XOA_PLAN < 2 && this.state.nMissingPatches) { const { nHostMissingPatches, nPoolMissingPatches } = this.state
if (
process.env.XOA_PLAN < 2 &&
(nHostMissingPatches > 0 || nPoolMissingPatches > 0)
) {
return {} return {}
} }
return this.state return { host: this.state.host }
} }
_getHostPredicate = createSelector( _getHostPredicate = createSelector(
@ -59,18 +59,29 @@ export default class AddHostModal extends BaseComponent {
singleHosts => host => singleHosts[host.id] singleHosts => host => singleHosts[host.id]
) )
_onChangeHost = host => { _onChangeHost = async host => {
if (host === null) {
this.setState({ this.setState({
host, host,
nMissingPatches: host nHostMissingPatches: undefined,
? differenceBy(this.props.poolMasterPatches, host.patches, 'name') nPoolMissingPatches: undefined,
.length })
: undefined, return
}
const { master } = this.props.pool
const hostMissingPatches = await getPatchesDifference(host.id, master)
const poolMissingPatches = await getPatchesDifference(master, host.id)
this.setState({
host,
nHostMissingPatches: hostMissingPatches.length,
nPoolMissingPatches: poolMissingPatches.length,
}) })
} }
render() { render() {
const { nMissingPatches } = this.state const { nHostMissingPatches, nPoolMissingPatches } = this.state
return ( return (
<div> <div>
@ -85,19 +96,41 @@ export default class AddHostModal extends BaseComponent {
</Col> </Col>
</SingleLineRow> </SingleLineRow>
<br /> <br />
{nMissingPatches > 0 && ( {(nHostMissingPatches > 0 || nPoolMissingPatches > 0) && (
<div>
{process.env.XOA_PLAN > 1 ? (
<div>
{nPoolMissingPatches > 0 && (
<SingleLineRow> <SingleLineRow>
<Col> <Col>
<span className='text-danger'> <span className='text-danger'>
<Icon icon='error' />{' '} <Icon icon='error' />{' '}
{process.env.XOA_PLAN > 1 {_('missingPatchesPool', {
? _('hostNeedsPatchUpdate', { patches: nMissingPatches }) nMissingPatches: nPoolMissingPatches,
: _('hostNeedsPatchUpdateNoInstall')} })}
</span>
</Col>
</SingleLineRow>
)}
{nHostMissingPatches > 0 && (
<SingleLineRow>
<Col>
<span className='text-danger'>
<Icon icon='error' />{' '}
{_('missingPatchesHost', {
nMissingPatches: nHostMissingPatches,
})}
</span> </span>
</Col> </Col>
</SingleLineRow> </SingleLineRow>
)} )}
</div> </div>
) : (
_('patchUpdateNoInstall')
)}
</div>
)}
</div>
) )
} }
} }

View File

@ -556,6 +556,12 @@ export const removeServer = server =>
export const editPool = (pool, props) => export const editPool = (pool, props) =>
_call('pool.set', { id: resolveId(pool), ...props }) _call('pool.set', { id: resolveId(pool), ...props })
export const getPatchesDifference = (source, target) =>
_call('pool.getPatchesDifference', {
source: resolveId(source),
target: resolveId(target),
})
import AddHostModalBody from './add-host-modal' // eslint-disable-line import/first import AddHostModalBody from './add-host-modal' // eslint-disable-line import/first
export const addHostToPool = (pool, host) => { export const addHostToPool = (pool, host) => {
if (host) { if (host) {
@ -739,23 +745,18 @@ export const enableHost = host => _call('host.enable', { id: resolveId(host) })
export const disableHost = host => export const disableHost = host =>
_call('host.disable', { id: resolveId(host) }) _call('host.disable', { id: resolveId(host) })
const missingUpdatePluginByHost = { __proto__: null }
export const getHostMissingPatches = async host => { export const getHostMissingPatches = async host => {
const hostId = resolveId(host) const hostId = resolveId(host)
if (host.productBrand !== 'XCP-ng') { if (host.productBrand !== 'XCP-ng') {
const patches = await _call('host.listMissingPatches', { host: hostId }) const patches = await _call('pool.listMissingPatches', { host: hostId })
// Hide paid patches to XS-free users // Hide paid patches to XS-free users
return host.license_params.sku_type !== 'free' return host.license_params.sku_type !== 'free'
? patches ? patches
: filter(patches, { paid: false }) : filter(patches, { paid: false })
} }
if (missingUpdatePluginByHost[hostId]) {
return null
}
try { try {
return await _call('host.listMissingPatches', { host: hostId }) return await _call('pool.listMissingPatches', { host: hostId })
} catch (_) { } catch (_) {
missingUpdatePluginByHost[hostId] = true
return null return null
} }
} }
@ -776,18 +777,30 @@ export const emergencyShutdownHosts = hosts => {
}).then(() => map(hosts, host => emergencyShutdownHost(host)), noop) }).then(() => map(hosts, host => emergencyShutdownHost(host)), noop)
} }
export const installHostPatch = (host, { uuid }) => // for XCP-ng now
_call('host.installPatch', { host: resolveId(host), patch: uuid })::tap(() => export const installAllPatchesOnHost = ({ host }) =>
confirm({
body: _('installAllPatchesOnHostContent'),
title: _('installAllPatchesTitle'),
}).then(() =>
_call('pool.installPatches', { hosts: [resolveId(host)] })::tap(() =>
subscribeHostMissingPatches.forceRefresh(host) subscribeHostMissingPatches.forceRefresh(host)
) )
)
export const installAllHostPatches = host => export const installPatches = (patches, pool) =>
_call('host.installAllPatches', { host: resolveId(host) })::tap(() => confirm({
subscribeHostMissingPatches.forceRefresh(host) body: _('installPatchesContent', { nPatches: patches.length }),
title: _('installPatchesTitle', { nPatches: patches.length }),
}).then(() =>
_call('pool.installPatches', {
pool: resolveId(pool),
patches: resolveIds(patches),
})::tap(() => subscribeHostMissingPatches.forceRefresh())
) )
import InstallPoolPatchesModalBody from './install-pool-patches-modal' // eslint-disable-line import/first import InstallPoolPatchesModalBody from './install-pool-patches-modal' // eslint-disable-line import/first
export const installAllPatchesOnPool = pool => { export const installAllPatchesOnPool = ({ pool }) => {
const poolId = resolveId(pool) const poolId = resolveId(pool)
return confirm({ return confirm({
body: <InstallPoolPatchesModalBody pool={poolId} />, body: <InstallPoolPatchesModalBody pool={poolId} />,
@ -795,7 +808,7 @@ export const installAllPatchesOnPool = pool => {
icon: 'host-patch-update', icon: 'host-patch-update',
}).then( }).then(
() => () =>
_call('pool.installAllPatches', { pool: poolId })::tap(() => _call('pool.installPatches', { pool: poolId })::tap(() =>
subscribeHostMissingPatches.forceRefresh() subscribeHostMissingPatches.forceRefresh()
), ),
noop noop

View File

@ -20,7 +20,7 @@ import {
createGetObjectsOfType, createGetObjectsOfType,
createSelector, createSelector,
} from 'selectors' } from 'selectors'
import { assign, isEmpty, isString, map, pick, sortBy } from 'lodash' import { assign, isEmpty, map, pick, sortBy } from 'lodash'
import TabAdvanced from './tab-advanced' import TabAdvanced from './tab-advanced'
import TabConsole from './tab-console' import TabConsole from './tab-console'
@ -101,19 +101,11 @@ const isRunning = host => host && host.power_state === 'Running'
) )
) )
const getHostPatches = createSelector( const getHostPatches = createGetObjectsOfType('patch').pick(
createGetObjectsOfType('pool_patch'),
createGetObjectsOfType('host_patch').pick(
createSelector( createSelector(
getHost, getHost,
host => (isString(host.patches[0]) ? host.patches : []) host => host.patches
) )
),
(poolsPatches, hostsPatches) =>
map(hostsPatches, hostPatch => ({
...hostPatch,
poolPatch: poolsPatches[hostPatch.pool_patch],
}))
) )
const doesNeedRestart = createDoesHostNeedRestart(getHost) const doesNeedRestart = createDoesHostNeedRestart(getHost)

View File

@ -7,10 +7,10 @@ import Upgrade from 'xoa-upgrade'
import { alert, chooseAction } from 'modal' import { alert, chooseAction } from 'modal'
import { connectStore, formatSize } from 'utils' import { connectStore, formatSize } from 'utils'
import { Container, Row, Col } from 'grid' import { Container, Row, Col } from 'grid'
import { createDoesHostNeedRestart, createSelector } from 'selectors' import { createDoesHostNeedRestart } from 'selectors'
import { FormattedRelative, FormattedTime } from 'react-intl' import { FormattedRelative, FormattedTime } from 'react-intl'
import { restartHost, installAllHostPatches, installHostPatch } from 'xo' import { installAllPatchesOnHost, restartHost } from 'xo'
import { isEmpty, isString } from 'lodash' import { isEmpty } from 'lodash'
const MISSING_PATCH_COLUMNS = [ const MISSING_PATCH_COLUMNS = [
{ {
@ -124,13 +124,13 @@ const INDIVIDUAL_ACTIONS_XCP = [
const INSTALLED_PATCH_COLUMNS = [ const INSTALLED_PATCH_COLUMNS = [
{ {
name: _('patchNameLabel'), name: _('patchNameLabel'),
itemRenderer: patch => patch.poolPatch.name, itemRenderer: patch => patch.name,
sortCriteria: patch => patch.poolPatch.name, sortCriteria: patch => patch.name,
}, },
{ {
name: _('patchDescription'), name: _('patchDescription'),
itemRenderer: patch => patch.poolPatch.description, itemRenderer: patch => patch.description,
sortCriteria: patch => patch.poolPatch.description, sortCriteria: patch => patch.description,
}, },
{ {
default: true, default: true,
@ -152,26 +152,6 @@ const INSTALLED_PATCH_COLUMNS = [
sortCriteria: patch => patch.time, sortCriteria: patch => patch.time,
sortOrder: 'desc', sortOrder: 'desc',
}, },
{
name: _('patchSize'),
itemRenderer: patch => formatSize(patch.poolPatch.size),
sortCriteria: patch => patch.poolPatch.size,
},
]
// support for software_version.platform_version ^2.1.1
const INSTALLED_PATCH_COLUMNS_2 = [
{
default: true,
name: _('patchNameLabel'),
itemRenderer: patch => patch.name,
sortCriteria: patch => patch.name,
},
{
name: _('patchDescription'),
itemRenderer: patch => patch.description,
sortCriteria: patch => patch.description,
},
{ {
name: _('patchSize'), name: _('patchSize'),
itemRenderer: patch => formatSize(patch.size), itemRenderer: patch => formatSize(patch.size),
@ -225,40 +205,8 @@ class XcpPatches extends Component {
needsRestart: createDoesHostNeedRestart((_, props) => props.host), needsRestart: createDoesHostNeedRestart((_, props) => props.host),
})) }))
class XenServerPatches extends Component { class XenServerPatches extends Component {
_getPatches = createSelector(
() => this.props.host,
() => this.props.hostPatches,
(host, hostPatches) => {
if (isEmpty(host.patches) && isEmpty(hostPatches)) {
return { patches: null }
}
if (isString(host.patches[0])) {
return {
patches: hostPatches,
columns: INSTALLED_PATCH_COLUMNS,
}
}
return {
patches: host.patches,
columns: INSTALLED_PATCH_COLUMNS_2,
}
}
)
_individualActions = [
{
name: _('patchAction'),
level: 'primary',
handler: this.props.installPatch,
icon: 'host-patch-update',
},
]
render() { render() {
const { host, missingPatches, installAllPatches } = this.props const { host, missingPatches, installAllPatches, hostPatches } = this.props
const { patches, columns } = this._getPatches()
const hasMissingPatches = !isEmpty(missingPatches) const hasMissingPatches = !isEmpty(missingPatches)
return ( return (
<Container> <Container>
@ -287,7 +235,6 @@ class XenServerPatches extends Component {
<Col> <Col>
<h3>{_('hostMissingPatches')}</h3> <h3>{_('hostMissingPatches')}</h3>
<SortedTable <SortedTable
individualActions={this._individualActions}
collection={missingPatches} collection={missingPatches}
columns={MISSING_PATCH_COLUMNS} columns={MISSING_PATCH_COLUMNS}
/> />
@ -296,14 +243,11 @@ class XenServerPatches extends Component {
)} )}
<Row> <Row>
<Col> <Col>
{patches ? (
<span>
<h3>{_('hostAppliedPatches')}</h3> <h3>{_('hostAppliedPatches')}</h3>
<SortedTable collection={patches} columns={columns} /> <SortedTable
</span> collection={hostPatches}
) : ( columns={INSTALLED_PATCH_COLUMNS}
<h4 className='text-xs-center'>{_('patchNothing')}</h4> />
)}
</Col> </Col>
</Row> </Row>
</Container> </Container>
@ -316,30 +260,21 @@ export default class TabPatches extends Component {
router: PropTypes.object, router: PropTypes.object,
} }
_chooseActionPatch = async doInstall => { _installAllPatches = () => {
const choice = await chooseAction({ const { host } = this.props
body: <p>{_('installPatchWarningContent')}</p>, const { $pool: pool, productBrand } = host
buttons: [
{
label: _('installPatchWarningResolve'),
value: 'install',
btnStyle: 'primary',
},
{ label: _('installPatchWarningReject'), value: 'goToPool' },
],
title: _('installPatchWarningTitle'),
})
return choice === 'install' if (productBrand === 'XCP-ng') {
? doInstall() return installAllPatchesOnHost({ host })
: this.context.router.push(`/pools/${this.props.host.$pool}/patches`)
} }
_installAllPatches = () => return chooseAction({
this._chooseActionPatch(() => installAllHostPatches(this.props.host)) body: <p>{_('installAllPatchesContent')}</p>,
buttons: [{ label: _('installAllPatchesRedirect'), value: 'goToPool' }],
_installPatch = patch => icon: 'host-patch-update',
this._chooseActionPatch(() => installHostPatch(this.props.host, patch)) title: _('installAllPatchesTitle'),
}).then(() => this.context.router.push(`/pools/${pool}/patches`))
}
render() { render() {
if (process.env.XOA_PLAN < 2) { if (process.env.XOA_PLAN < 2) {
@ -352,14 +287,10 @@ export default class TabPatches extends Component {
if (this.props.missingPatches === null) { if (this.props.missingPatches === null) {
return <em>{_('updatePluginNotInstalled')}</em> return <em>{_('updatePluginNotInstalled')}</em>
} }
return this.props.host.productBrand === 'XCP-ng' ? ( const Patches =
<XcpPatches {...this.props} installAllPatches={this._installAllPatches} /> this.props.host.productBrand === 'XCP-ng' ? XcpPatches : XenServerPatches
) : ( return (
<XenServerPatches <Patches {...this.props} installAllPatches={this._installAllPatches} />
{...this.props}
installAllPatches={this._installAllPatches}
installPatch={this._installPatch}
/>
) )
} }
} }

View File

@ -1,43 +1,238 @@
import Component from 'base-component' import _ from 'intl'
import HostsPatchesTable from 'hosts-patches-table' import React, { Component } from 'react'
import React from 'react' import SortedTable from 'sorted-table'
import TabButton from 'tab-button'
import Upgrade from 'xoa-upgrade' import Upgrade from 'xoa-upgrade'
import { connectStore } from 'utils' import { addSubscriptions, connectStore, formatSize } from 'utils'
import { Container, Row, Col } from 'grid' import { alert } from 'modal'
import { Col, Container, Row } from 'grid'
import { createGetObjectsOfType } from 'selectors' import { createGetObjectsOfType } from 'selectors'
import { FormattedRelative, FormattedTime } from 'react-intl'
import {
installAllPatchesOnPool,
installPatches,
subscribeHostMissingPatches,
} from 'xo'
import { isEmpty } from 'lodash'
// =================================================================== const MISSING_PATCH_COLUMNS = [
{
name: _('patchNameLabel'),
itemRenderer: _ => _.name,
sortCriteria: 'name',
},
{
name: _('patchDescription'),
itemRenderer: ({ description, documentationUrl }) => (
<a href={documentationUrl} target='_blank'>
{description}
</a>
),
sortCriteria: 'description',
},
{
name: _('patchReleaseDate'),
itemRenderer: ({ date }) => (
<span>
<FormattedTime value={date} day='numeric' month='long' year='numeric' />{' '}
(<FormattedRelative value={date} />)
</span>
),
sortCriteria: 'date',
sortOrder: 'desc',
},
{
name: _('patchGuidance'),
itemRenderer: _ => _.guidance,
sortCriteria: 'guidance',
},
]
@connectStore(() => { const ACTIONS = [
const getHosts = createGetObjectsOfType('host').filter((_, props) => host => {
props.pool.id === host.$pool handler: (patches, { pool }) => installPatches(patches, pool),
icon: 'host-patch-update',
label: _('install'),
level: 'primary',
},
]
const MISSING_PATCH_COLUMNS_XCP = [
{
name: _('patchNameLabel'),
itemRenderer: _ => _.name,
sortCriteria: 'name',
},
{
name: _('patchDescription'),
itemRenderer: _ => _.description,
sortCriteria: 'description',
},
{
name: _('patchVersion'),
itemRenderer: _ => _.version,
},
{
name: _('patchRelease'),
itemRenderer: _ => _.release,
},
{
name: _('patchSize'),
itemRenderer: _ => formatSize(_.size),
sortCriteria: 'size',
},
]
const INDIVIDUAL_ACTIONS_XCP = [
{
disabled: _ => _.changelog === null,
handler: ({ name, changelog: { author, date, description } }) =>
alert(
_('changelog'),
<Container>
<Row className='mb-1'>
<Col size={3}>
<strong>{_('changelogPatch')}</strong>
</Col>
<Col size={9}>{name}</Col>
</Row>
<Row className='mb-1'>
<Col size={3}>
<strong>{_('changelogDate')}</strong>
</Col>
<Col size={9}>
<FormattedTime
value={date * 1000}
day='numeric'
month='long'
year='numeric'
/>
</Col>
</Row>
<Row className='mb-1'>
<Col size={3}>
<strong>{_('changelogAuthor')}</strong>
</Col>
<Col size={9}>{author}</Col>
</Row>
<Row>
<Col size={3}>
<strong>{_('changelogDescription')}</strong>
</Col>
<Col size={9}>{description}</Col>
</Row>
</Container>
),
icon: 'preview',
label: _('showChangelog'),
},
]
const INSTALLED_PATCH_COLUMNS = [
{
name: _('patchNameLabel'),
itemRenderer: _ => _.name,
sortCriteria: 'name',
},
{
name: _('patchDescription'),
itemRenderer: _ => _.description,
sortCriteria: 'description',
},
{
default: true,
name: _('patchApplied'),
itemRenderer: patch => {
const time = patch.time * 1000
return (
<span>
<FormattedTime
value={time}
day='numeric'
month='long'
year='numeric'
/>{' '}
(<FormattedRelative value={time} />)
</span>
) )
},
sortCriteria: 'time',
sortOrder: 'desc',
},
{
name: _('patchSize'),
itemRenderer: _ => formatSize(_.size),
sortCriteria: 'size',
},
]
return { @addSubscriptions(({ master }) => ({
hosts: getHosts, missingPatches: cb => subscribeHostMissingPatches(master, cb),
} }))
@connectStore({
hostPatches: createGetObjectsOfType('patch').pick(
(_, { master }) => master.patches
),
}) })
export default class TabPatches extends Component { export default class TabPatches extends Component {
_getContainer = () => this.refs.container
render() { render() {
const {
hostPatches,
missingPatches = [],
pool,
master: { productBrand },
} = this.props
return ( return (
<Upgrade place='poolPatches' required={2}> <Upgrade place='poolPatches' required={2}>
<Container> <Container>
<Row> <Row>
<Col className='text-xs-right'> <Col className='text-xs-right'>
<div ref='container' /> <TabButton
btnStyle='primary'
data-pool={pool}
disabled={isEmpty(missingPatches)}
handler={installAllPatchesOnPool}
icon='host-patch-update'
labelId='installPoolPatches'
/>
</Col>
</Row>
{productBrand === 'XCP-ng' ? (
<Row>
<Col>
<h3>{_('hostMissingPatches')}</h3>
<SortedTable
columns={MISSING_PATCH_COLUMNS_XCP}
collection={missingPatches}
individualActions={INDIVIDUAL_ACTIONS_XCP}
/>
</Col>
</Row>
) : (
<div>
<Row>
<Col>
<h3>{_('hostMissingPatches')}</h3>
<SortedTable
actions={ACTIONS}
collection={missingPatches}
columns={MISSING_PATCH_COLUMNS}
data-pool={pool}
/>
</Col> </Col>
</Row> </Row>
<Row> <Row>
<Col> <Col>
<HostsPatchesTable <h3>{_('hostAppliedPatches')}</h3>
buttonsGroupContainer={this._getContainer} <SortedTable
hosts={this.props.hosts} collection={hostPatches}
useTabButton columns={INSTALLED_PATCH_COLUMNS}
/> />
</Col> </Col>
</Row> </Row>
</div>
)}
</Container> </Container>
</Upgrade> </Upgrade>
) )