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 }) => (
-
{_('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' ? ( -