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:
parent
fe1da4ea12
commit
cde9a02c32
@ -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
|
||||
|
@ -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:',
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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() -
|
||||
|
@ -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')
|
||||
},
|
||||
}
|
||||
|
18
packages/xo-server/src/xo-mixins/patches.js
Normal file
18
packages/xo-server/src/xo-mixins/patches.js
Normal 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)
|
||||
}
|
||||
}
|
@ -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,
|
||||
}}
|
||||
|
@ -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?',
|
||||
|
@ -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,
|
||||
|
@ -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>
|
||||
)
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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} />
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
)
|
||||
|
Loading…
Reference in New Issue
Block a user