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
- [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))
- [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
- vhd-lib v0.6.0
- @xen-orchestra/fs v0.8.0
- xo-server-usage-report v0.7.2
- xo-server v5.38.0
- xo-web v5.38.0

View File

@ -494,7 +494,7 @@ async function getHostsMissingPatches({ runningHosts, xo }) {
map(runningHosts, async host => {
let hostsPatches = await xo
.getXapi(host)
.listMissingPoolPatchesOnHost(host._xapiId)
.listMissingPatches(host._xapiId)
.catch(error => {
console.error(
'[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 }) {
return this.getXapi(host).emergencyShutdownHost(host._xapiId)
}

View File

@ -1,6 +1,4 @@
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 }) {
await this.getXapi(pool).installPoolPatchOnAllHosts(patchUuid)
// Returns an array of missing new patches in the host
// Returns an empty array if up-to-date
export function listMissingPatches({ host }) {
return this.getXapi(host).listMissingPatches(host._xapiId)
}
installPatch.params = {
pool: {
type: 'string',
},
patch: {
type: 'string',
},
listMissingPatches.description =
'return an array of missing new patches in the host'
listMissingPatches.params = {
host: { type: 'string' },
}
installPatch.resolve = {
pool: ['pool', 'pool', 'administrate'],
listMissingPatches.resolve = {
host: ['host', 'host', 'view'],
}
// -------------------------------------------------------------------
export async function installAllPatches({ pool }) {
await this.getXapi(pool).installAllPoolPatchesOnAllHosts()
export async function installPatches({ pool, patches, hosts }) {
await this.getXapi(hosts === undefined ? pool : hosts[0]).installPatches({
patches,
hosts,
})
}
installAllPatches.params = {
pool: {
type: 'string',
},
installPatches.params = {
pool: { type: 'string', optional: true },
patches: { type: 'array', optional: true },
hosts: { type: 'array', optional: true },
}
installAllPatches.resolve = {
installPatches.resolve = {
pool: ['pool', 'pool', 'administrate'],
}
installAllPatches.description =
'Install automatically all patches for every hosts of a pool'
installPatches.description = 'Install patches on hosts'
// -------------------------------------------------------------------
@ -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 }) {
const sourceHost = this.getObject(source.master)
const targetHost = this.getObject(target.master)
@ -156,21 +173,21 @@ export async function mergeInto({ source, target, force }) {
)
}
const sourcePatches = sourceHost.patches
const targetPatches = targetHost.patches
const counterDiff = differenceBy(sourcePatches, targetPatches, 'name')
const counterDiff = this.getPatchesDifference(source.master, target.master)
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')
// TODO: compare UUIDs
await this.getXapi(source).installSpecificPatchesOnHost(
mapToArray(diff, 'name'),
sourceHost._xapiId
)
const diff = this.getPatchesDifference(target.master, source.master)
if (diff.length > 0) {
const sourceXapi = this.getXapi(source)
await sourceXapi.installPatches({
patches: await sourceXapi.findPatches(diff),
})
}
await this.mergeXenPools(source._xapiId, target._xapiId, force)
}

View File

@ -102,11 +102,10 @@ const TRANSFORMS = {
} = obj
const isRunning = isHostRunning(obj)
let supplementalPacks, patches
let supplementalPacks
if (useUpdateSystem(obj)) {
supplementalPacks = []
patches = []
forEach(obj.$updates, update => {
const formattedUpdate = {
@ -121,7 +120,7 @@ const TRANSFORMS = {
}
if (startsWith(update.name_label, 'XS')) {
patches.push(formattedUpdate)
// It's a patch update but for homogeneity, we're still using pool_patches
} else {
supplementalPacks.push(formattedUpdate)
}
@ -171,7 +170,7 @@ const TRANSFORMS = {
}
})(),
multipathing: otherConfig.multipathing === 'true',
patches: patches || link(obj, 'patches'),
patches: link(obj, 'patches'),
powerOnMode: obj.power_on_mode,
power_state: metrics ? (isRunning ? 'Running' : 'Halted') : 'Unknown',
startTime: toTimestamp(otherConfig.boot_time),
@ -625,10 +624,18 @@ const TRANSFORMS = {
// -----------------------------------------------------------------
host_patch(obj) {
const poolPatch = obj.$pool_patch
return {
type: 'patch',
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),
pool_patch: link(obj, 'pool_patch', '$ref'),
$host: link(obj, 'host'),
}
@ -640,12 +647,15 @@ const TRANSFORMS = {
return {
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,
guidance: obj.after_apply_guidance,
name: obj.name_label,
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?
// 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) {
const delta =
parseDateTime(await this.call('host.get_servertime', hostRef)).getTime() -

View File

@ -1,16 +1,8 @@
import asyncMap from '@xen-orchestra/async-map'
import createLogger from '@xen-orchestra/log'
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 { filter, find, pickBy, some } from 'lodash'
import ensureArray from '../../_ensureArray'
import { debounce } from '../../decorators'
@ -18,9 +10,35 @@ import { forEach, mapFilter, mapToArray, parseXml } 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 _isXcp = host => host.software_version.product_brand === 'XCP-ng'
// =============================================================================
export default {
// raw { uuid: patch } map translated from updates.xensource.com/XenServer/updates.xml
// FIXME: should be static
@debounce(24 * 60 * 60 * 1000)
async _getXenUpdates() {
@ -43,13 +61,16 @@ export default {
guidance: patch['after-apply-guidance'],
name: patch['name-label'],
url: patch['patch-url'],
id: patch.uuid,
uuid: patch.uuid,
conflicts: mapToArray(ensureArray(patch.conflictingpatches), patch => {
return patch.conflictingpatch.uuid
}),
requirements: mapToArray(ensureArray(patch.requiredpatches), patch => {
return patch.requiredpatch.uuid
}),
conflicts: mapToArray(
ensureArray(patch.conflictingpatches),
patch => patch.conflictingpatch.uuid
),
requirements: mapToArray(
ensureArray(patch.requiredpatches),
patch => patch.requiredpatch.uuid
),
paid: patch['update-stream'] === 'premium',
upgrade: /^XS\d{2,}$/.test(patch['name-label']),
// TODO: what does it mean, should we handle it?
@ -96,72 +117,12 @@ export default {
}
},
// =================================================================
// 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))
)
},
// 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
async _ejectToolsIsos(hostRef) {
return Promise.all(
mapFilter(this.objects.all, vm => {
if (vm.$type !== 'VM' || (hostRef && vm.resident_on !== hostRef)) {
if (vm.$type !== 'vm' || (hostRef && vm.resident_on !== hostRef)) {
return
}
@ -178,54 +139,235 @@ export default {
)
},
// -----------------------------------------------------------------
// LIST ----------------------------------------------------------------------
_isPoolPatchInstallableOnHost(patchUuid, host) {
const installed = this._getInstalledPoolPatchesOnHost(host)
// list all yum updates available for a XCP-ng 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]) {
return false
// list all patches provided by Citrix for this host version regardless
// 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
}
// ----------
let installable = true
forEach(installed, patch => {
if (includes(patch.conflicts, patchUuid)) {
installable = false
return false
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}`)
}
if (installed[id] !== undefined) {
if (getAll) {
return
}
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
},
_isPoolPatchInstallableOnPool(patchUuid) {
return every(
this.objects.all,
obj =>
obj.$type !== 'host' ||
this._isPoolPatchInstallableOnHost(patchUuid, obj)
// high level
listMissingPatches(hostId) {
const host = this.getObject(hostId)
return _isXcp(host)
? this._listXcpUpdates(host)
: // 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 ----------------------------------------
async uploadPoolPatch(stream, patchName) {
const patchRef = await this.putResource(stream, '/pool_patch_upload', {
task: this.createTask('Patch upload', patchName),
}).then(extractOpaqueRef)
_xcpUpdate(hosts) {
if (hosts === undefined) {
hosts = filter(this.objects.all, { $type: 'host' })
} else {
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 {
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]
if (!patchInfo) {
@ -248,16 +390,21 @@ export default {
.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 ----------------------------------------
async _getUpdateVdi($defer, patchUuid, hostId) {
log.debug(`downloading patch ${patchUuid}`)
// upload patch on a VDI on a shared SR
async _uploadPatch($defer, uuid) {
log.debug(`downloading patch ${uuid}`)
const patchInfo = (await this._getXenUpdates()).patches[patchUuid]
const patchInfo = (await this._getXenUpdates()).patches[uuid]
if (!patchInfo) {
throw new Error('no such patch ' + patchUuid)
throw new Error('no such patch ' + uuid)
}
let stream = await this.xo.httpRequest(patchInfo.url)
@ -271,315 +418,104 @@ export default {
.on('error', reject)
})
let vdi
// If no hostId provided, try and find a shared SR
if (!hostId) {
const sr = this.findAvailableSharedSr(stream.length)
if (!sr) {
return
}
vdi = await this.createTemporaryVdiOnSr(
stream,
sr,
'[XO] 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'
)
const sr = this.findAvailableSr(stream.length)
if (sr === undefined) {
return
}
const vdi = await this.createTemporaryVdiOnSr(
stream,
sr,
'[XO] Patch ISO',
'small temporary VDI to store a patch ISO'
)
$defer(() => this._deleteVdi(vdi.$ref))
return vdi
},
// -----------------------------------------------------------------
_poolWideInstall: deferrable(async function($defer, patches) {
// Legacy XS patches
if (!useUpdateSystem(this.pool.$master)) {
// for each patch: pool_patch.pool_apply
for (const p of patches) {
const [patch] = await Promise.all([
this._legacyUploadPatch(p.uuid),
this._ejectToolsIsos(this.pool.$master.$ref),
])
// patform_version < 2.1.1 -----------------------------------------
async _installPoolPatchOnHost(patchUuid, host) {
const [patch] = await Promise.all([
this._getOrUploadPoolPatch(patchUuid),
this._ejectToolsIsos(host.$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)
},
// platform_version >= 2.1.1
_installPatchUpdateOnAllHosts: deferrable(async function($defer, patchUuid) {
await this._assertConsistentHostServerTime(this.pool.master)
let [vdi] = await Promise.all([
this._getUpdateVdi($defer, patchUuid),
this._ejectToolsIsos(),
])
if (vdi == null) {
vdi = await this._getUpdateVdi($defer, patchUuid, this.pool.master)
}
return this.call(
'pool_update.pool_apply',
await this.call('pool_update.introduce', vdi.$ref)
)
}),
async installPoolPatchOnAllHosts(patchUuid) {
log.debug(`installing patch ${patchUuid} on all hosts`)
return useUpdateSystem(this.pool.$master)
? this._installPatchUpdateOnAllHosts(patchUuid)
: this._installPoolPatchOnAllHosts(patchUuid)
},
// -----------------------------------------------------------------
// If no host is provided, install on pool
async _installPoolPatchAndRequirements(patch, patchesByUuid, host) {
if (
host == null
? !this._isPoolPatchInstallableOnPool(patch.uuid)
: !this._isPoolPatchInstallableOnHost(patch.uuid, host)
) {
await this.call('pool_patch.pool_apply', patch.$ref)
}
return
}
// ----------
const { requirements } = patch
if (requirements.length) {
for (const requirementUuid of requirements) {
const requirement = patchesByUuid[requirementUuid]
if (requirement != null) {
await this._installPoolPatchAndRequirements(
requirement,
patchesByUuid,
host
)
host = host && this.getObject(host.$id)
}
// for each patch: pool_update.introduce → pool_update.pool_apply
for (const p of patches) {
const [vdi] = await Promise.all([
this._uploadPatch($defer, p.uuid),
this._ejectToolsIsos(),
])
if (vdi === undefined) {
throw new Error('patch could not be uploaded')
}
}
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++) {
await this._installPoolPatchAndRequirements(
patchesToInstall[i],
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) {
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() {
if (this.pool.$master.software_version.product_brand === 'XCP-ng') {
return this._xcpInstallAllPoolUpdatesOnHost()
}
return this._installAllPoolPatchesOnAllHosts()
},
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(
log.debug(`installing patch ${p.uuid}`)
await this.call(
'host.call_plugin',
host.$ref,
'updater.py',
'check_update',
{}
'pool_update.pool_apply',
await this.call('pool_update.introduce', vdi.$ref)
)
)
},
// 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),
})
}
}),
async _hostInstall(patches, host) {
throw new Error('single host install not implemented')
// Legacy XS patches
// for each patch: pool_patch.apply
// ----------
// for each patch: pool_update.introduce → pool_update.apply
},
// 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)
)
// high level
// install specified patches on specified hosts
//
// no hosts specified: pool-wide install (only the pool master installed patches will be considered)
// no patches specified: install either the pool master's missing patches (no hosts specified) or each host's missing patches
//
// patches will be ignored for XCP (always updates completely)
// patches that are already installed will be ignored (XS only)
//
// 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)
}
// XS
// TODO: assert consistent time
const poolWide = hosts === undefined
if (poolWide) {
log.debug('patches that were requested to be installed', patches)
const installablePatches = await this._listInstallablePatches(
this.pool.$master,
patches
)
log.debug(
'patches that will actually be installed',
installablePatches.map(patch => patch.uuid)
)
return this._poolWideInstall(installablePatches)
}
// for each host
// get installable patches
// filter patches that should be installed
// sort patches
// host-by-host install
throw new Error('non pool-wide install not implemented')
},
}

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,
createSelector,
} from './selectors'
import {
installAllHostPatches,
installAllPatchesOnPool,
subscribeHostMissingPatches,
} from './xo'
import { installAllPatchesOnPool, subscribeHostMissingPatches } from './xo'
// ===================================================================
@ -43,17 +39,6 @@ const MISSING_PATCHES_COLUMNS = [
),
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 = [
@ -115,7 +100,9 @@ class HostsPatchesTable extends Component {
pools[host.$pool] = true
})
return Promise.all(map(keys(pools), installAllPatchesOnPool))
return Promise.all(
map(keys(pools), pool => installAllPatchesOnPool({ pool }))
)
}
componentDidMount() {
@ -162,7 +149,6 @@ class HostsPatchesTable extends Component {
: MISSING_PATCHES_COLUMNS
}
userData={{
installAllHostPatches,
missingPatches: this.state.missingPatches,
pools,
}}

View File

@ -756,10 +756,12 @@ const messages = {
addSrLabel: 'Add SR',
addVmLabel: 'Add VM',
addHostLabel: 'Add Host',
hostNeedsPatchUpdate:
'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.',
hostNeedsPatchUpdateNoInstall:
"This host cannot be added to the pool because it's missing some patches.",
missingPatchesPool:
'The pool needs to install {nMissingPatches, number} patch{nMissingPatches, plural, one {} other {es}}. This operation may be long.',
missingPatchesHost:
'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',
addHostNotHomogeneousErrorMessage: 'Host patches could not be homogenized.',
disconnectServer: 'Disconnect',
@ -886,14 +888,14 @@ const messages = {
hostAppliedPatches: 'Applied patches',
hostMissingPatches: 'Missing patches',
hostUpToDate: 'Host up-to-date!',
installPatchWarningTitle: 'Non-recommended patch install',
installPatchWarningContent:
'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',
installPatchWarningReject: 'Go to pool',
installPatchWarningResolve: 'Install',
installAllPatchesTitle: 'Install all patches',
installAllPatchesContent: 'To install all patches go to pool.',
installAllPatchesRedirect: 'Go to pool',
installAllPatchesOnHostContent:
'Are you sure you want to install all patches on this host?',
patchRelease: 'Release',
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',
changelog: 'Changelog',
changelogPatch: 'Patch',
@ -902,6 +904,10 @@ const messages = {
changelogDescription: 'Description',
// ----- Pool patch tabs -----
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',
confirmPoolPatch:
'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 = {
message: message => message.time,
PIF: pif => pif.device,
patch: patch => patch.name,
pool: pool => pool.name_label,
pool_patch: patch => patch.name,
tag: tag => tag,
VBD: vbd => vbd.position,
'VDI-snapshot': snapshot => snapshot.snapshot_time,
@ -494,37 +494,18 @@ export const createGetObjectMessages = objectSelector =>
export const getObject = createGetObject((_, id) => id)
export const createDoesHostNeedRestart = hostSelector => {
// XS < 7.1
const patchRequiresReboot = createGetObjectsOfType('pool_patch')
.pick(
// Returns the first patch of the host which requires it to be
// restarted.
create(
createGetObjectsOfType('host_patch')
.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)
const patchRequiresReboot = createGetObjectsOfType('patch')
.pick(create(hostSelector, host => host.patches))
.find(
create(hostSelector, host => ({ guidance, time, upgrade }) =>
time > host.startTime &&
(upgrade ||
some(
guidance,
action => action === 'restartHost' || action === 'restartXapi'
))
)
)
.find([
({ guidance, upgrade }) =>
upgrade ||
find(
guidance,
action => action === 'restartHost' || action === 'restartXapi'
),
])
return create(
hostSelector,

View File

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

View File

@ -556,6 +556,12 @@ export const removeServer = server =>
export const editPool = (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
export const addHostToPool = (pool, host) => {
if (host) {
@ -739,23 +745,18 @@ export const enableHost = host => _call('host.enable', { id: resolveId(host) })
export const disableHost = host =>
_call('host.disable', { id: resolveId(host) })
const missingUpdatePluginByHost = { __proto__: null }
export const getHostMissingPatches = async host => {
const hostId = resolveId(host)
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
return host.license_params.sku_type !== 'free'
? patches
: filter(patches, { paid: false })
}
if (missingUpdatePluginByHost[hostId]) {
return null
}
try {
return await _call('host.listMissingPatches', { host: hostId })
return await _call('pool.listMissingPatches', { host: hostId })
} catch (_) {
missingUpdatePluginByHost[hostId] = true
return null
}
}
@ -776,18 +777,30 @@ export const emergencyShutdownHosts = hosts => {
}).then(() => map(hosts, host => emergencyShutdownHost(host)), noop)
}
export const installHostPatch = (host, { uuid }) =>
_call('host.installPatch', { host: resolveId(host), patch: uuid })::tap(() =>
subscribeHostMissingPatches.forceRefresh(host)
// for XCP-ng now
export const installAllPatchesOnHost = ({ host }) =>
confirm({
body: _('installAllPatchesOnHostContent'),
title: _('installAllPatchesTitle'),
}).then(() =>
_call('pool.installPatches', { hosts: [resolveId(host)] })::tap(() =>
subscribeHostMissingPatches.forceRefresh(host)
)
)
export const installAllHostPatches = host =>
_call('host.installAllPatches', { host: resolveId(host) })::tap(() =>
subscribeHostMissingPatches.forceRefresh(host)
export const installPatches = (patches, pool) =>
confirm({
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
export const installAllPatchesOnPool = pool => {
export const installAllPatchesOnPool = ({ pool }) => {
const poolId = resolveId(pool)
return confirm({
body: <InstallPoolPatchesModalBody pool={poolId} />,
@ -795,7 +808,7 @@ export const installAllPatchesOnPool = pool => {
icon: 'host-patch-update',
}).then(
() =>
_call('pool.installAllPatches', { pool: poolId })::tap(() =>
_call('pool.installPatches', { pool: poolId })::tap(() =>
subscribeHostMissingPatches.forceRefresh()
),
noop

View File

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

View File

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

View File

@ -1,43 +1,238 @@
import Component from 'base-component'
import HostsPatchesTable from 'hosts-patches-table'
import React from 'react'
import _ from 'intl'
import React, { Component } from 'react'
import SortedTable from 'sorted-table'
import TabButton from 'tab-button'
import Upgrade from 'xoa-upgrade'
import { connectStore } from 'utils'
import { Container, Row, Col } from 'grid'
import { addSubscriptions, connectStore, formatSize } from 'utils'
import { alert } from 'modal'
import { Col, Container, Row } from 'grid'
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 getHosts = createGetObjectsOfType('host').filter((_, props) => host =>
props.pool.id === host.$pool
)
const ACTIONS = [
{
handler: (patches, { pool }) => installPatches(patches, pool),
icon: 'host-patch-update',
label: _('install'),
level: 'primary',
},
]
return {
hosts: getHosts,
}
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',
},
]
@addSubscriptions(({ master }) => ({
missingPatches: cb => subscribeHostMissingPatches(master, cb),
}))
@connectStore({
hostPatches: createGetObjectsOfType('patch').pick(
(_, { master }) => master.patches
),
})
export default class TabPatches extends Component {
_getContainer = () => this.refs.container
render() {
const {
hostPatches,
missingPatches = [],
pool,
master: { productBrand },
} = this.props
return (
<Upgrade place='poolPatches' required={2}>
<Container>
<Row>
<Col className='text-xs-right'>
<div ref='container' />
</Col>
</Row>
<Row>
<Col>
<HostsPatchesTable
buttonsGroupContainer={this._getContainer}
hosts={this.props.hosts}
useTabButton
<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>
</Row>
<Row>
<Col>
<h3>{_('hostAppliedPatches')}</h3>
<SortedTable
collection={hostPatches}
columns={INSTALLED_PATCH_COLUMNS}
/>
</Col>
</Row>
</div>
)}
</Container>
</Upgrade>
)