diff --git a/CHANGELOG.unreleased.md b/CHANGELOG.unreleased.md index bdc3d1ec2..fd46a7c31 100644 --- a/CHANGELOG.unreleased.md +++ b/CHANGELOG.unreleased.md @@ -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 diff --git a/packages/xo-server-usage-report/src/index.js b/packages/xo-server-usage-report/src/index.js index d6aa9ccad..52b0b5011 100644 --- a/packages/xo-server-usage-report/src/index.js +++ b/packages/xo-server-usage-report/src/index.js @@ -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:', diff --git a/packages/xo-server/src/api/host.js b/packages/xo-server/src/api/host.js index 49c9ecd55..8e10a4025 100644 --- a/packages/xo-server/src/api/host.js +++ b/packages/xo-server/src/api/host.js @@ -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) } diff --git a/packages/xo-server/src/api/pool.js b/packages/xo-server/src/api/pool.js index d391aa473..7df5355ee 100644 --- a/packages/xo-server/src/api/pool.js +++ b/packages/xo-server/src/api/pool.js @@ -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) } diff --git a/packages/xo-server/src/xapi-object-to-xo.js b/packages/xo-server/src/xapi-object-to-xo.js index 0b841394a..722ba69a3 100644 --- a/packages/xo-server/src/xapi-object-to-xo.js +++ b/packages/xo-server/src/xapi-object-to-xo.js @@ -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, diff --git a/packages/xo-server/src/xapi/index.js b/packages/xo-server/src/xapi/index.js index 0d9c46402..262620d0b 100644 --- a/packages/xo-server/src/xapi/index.js +++ b/packages/xo-server/src/xapi/index.js @@ -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() - diff --git a/packages/xo-server/src/xapi/mixins/patching.js b/packages/xo-server/src/xapi/mixins/patching.js index b02418b09..5249e61c0 100644 --- a/packages/xo-server/src/xapi/mixins/patching.js +++ b/packages/xo-server/src/xapi/mixins/patching.js @@ -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') }, } diff --git a/packages/xo-server/src/xo-mixins/patches.js b/packages/xo-server/src/xo-mixins/patches.js new file mode 100644 index 000000000..43b044bfe --- /dev/null +++ b/packages/xo-server/src/xo-mixins/patches.js @@ -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) + } +} diff --git a/packages/xo-web/src/common/hosts-patches-table.js b/packages/xo-web/src/common/hosts-patches-table.js index 157be8d7f..ac7e0567c 100644 --- a/packages/xo-web/src/common/hosts-patches-table.js +++ b/packages/xo-web/src/common/hosts-patches-table.js @@ -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 }) => ( - - ), - }, ] 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, }} diff --git a/packages/xo-web/src/common/intl/messages.js b/packages/xo-web/src/common/intl/messages.js index 7e42f3494..f4c9298a7 100644 --- a/packages/xo-web/src/common/intl/messages.js +++ b/packages/xo-web/src/common/intl/messages.js @@ -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?', diff --git a/packages/xo-web/src/common/selectors.js b/packages/xo-web/src/common/selectors.js index 3f508eaa7..0ea80247a 100644 --- a/packages/xo-web/src/common/selectors.js +++ b/packages/xo-web/src/common/selectors.js @@ -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, diff --git a/packages/xo-web/src/common/xo/add-host-modal/index.js b/packages/xo-web/src/common/xo/add-host-modal/index.js index cdea60b7e..a23d32c61 100644 --- a/packages/xo-web/src/common/xo/add-host-modal/index.js +++ b/packages/xo-web/src/common/xo/add-host-modal/index.js @@ -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 (
@@ -85,17 +96,39 @@ export default class AddHostModal extends BaseComponent {
- {nMissingPatches > 0 && ( - - - - {' '} - {process.env.XOA_PLAN > 1 - ? _('hostNeedsPatchUpdate', { patches: nMissingPatches }) - : _('hostNeedsPatchUpdateNoInstall')} - - - + {(nHostMissingPatches > 0 || nPoolMissingPatches > 0) && ( +
+ {process.env.XOA_PLAN > 1 ? ( +
+ {nPoolMissingPatches > 0 && ( + + + + {' '} + {_('missingPatchesPool', { + nMissingPatches: nPoolMissingPatches, + })} + + + + )} + {nHostMissingPatches > 0 && ( + + + + {' '} + {_('missingPatchesHost', { + nMissingPatches: nHostMissingPatches, + })} + + + + )} +
+ ) : ( + _('patchUpdateNoInstall') + )} +
)}
) diff --git a/packages/xo-web/src/common/xo/index.js b/packages/xo-web/src/common/xo/index.js index 730273269..20936a967 100644 --- a/packages/xo-web/src/common/xo/index.js +++ b/packages/xo-web/src/common/xo/index.js @@ -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: , @@ -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 diff --git a/packages/xo-web/src/xo-app/host/index.js b/packages/xo-web/src/xo-app/host/index.js index 10369c90f..5ea94c92a 100644 --- a/packages/xo-web/src/xo-app/host/index.js +++ b/packages/xo-web/src/xo-app/host/index.js @@ -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) diff --git a/packages/xo-web/src/xo-app/host/tab-patches.js b/packages/xo-web/src/xo-app/host/tab-patches.js index e2bdc609a..0b7cff295 100644 --- a/packages/xo-web/src/xo-app/host/tab-patches.js +++ b/packages/xo-web/src/xo-app/host/tab-patches.js @@ -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 ( @@ -287,7 +235,6 @@ class XenServerPatches extends Component {

{_('hostMissingPatches')}

@@ -296,14 +243,11 @@ class XenServerPatches extends Component { )} - {patches ? ( - -

{_('hostAppliedPatches')}

- -
- ) : ( -

{_('patchNothing')}

- )} +

{_('hostAppliedPatches')}

+
@@ -316,31 +260,22 @@ export default class TabPatches extends Component { router: PropTypes.object, } - _chooseActionPatch = async doInstall => { - const choice = await chooseAction({ - body:

{_('installPatchWarningContent')}

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

{_('installAllPatchesContent')}

, + 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 {_('updatePluginNotInstalled')} } - return this.props.host.productBrand === 'XCP-ng' ? ( - - ) : ( - + const Patches = + this.props.host.productBrand === 'XCP-ng' ? XcpPatches : XenServerPatches + return ( + ) } } diff --git a/packages/xo-web/src/xo-app/pool/tab-patches.js b/packages/xo-web/src/xo-app/pool/tab-patches.js index 3c8c44f0b..58962e149 100644 --- a/packages/xo-web/src/xo-app/pool/tab-patches.js +++ b/packages/xo-web/src/xo-app/pool/tab-patches.js @@ -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 }) => ( + + {description} + + ), + sortCriteria: 'description', + }, + { + name: _('patchReleaseDate'), + itemRenderer: ({ date }) => ( + + {' '} + () + + ), + 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'), + + + + {_('changelogPatch')} + + {name} + + + + {_('changelogDate')} + + + + + + + + {_('changelogAuthor')} + + {author} + + + + {_('changelogDescription')} + + {description} + + + ), + 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 ( + + {' '} + () + + ) + }, + 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 ( -
- - - - - + {productBrand === 'XCP-ng' ? ( + + +

{_('hostMissingPatches')}

+ + +
+ ) : ( +
+ + +

{_('hostMissingPatches')}

+ + +
+ + +

{_('hostAppliedPatches')}

+ + +
+
+ )} )