/* * Copyright (C) 2021 Red Hat, Inc. * SPDX-License-Identifier: LGPL-2.1-or-later */ import React from "react"; import { Alert } from "@patternfly/react-core/dist/esm/components/Alert/index.js"; import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js"; import { Checkbox } from "@patternfly/react-core/dist/esm/components/Checkbox/index.js"; import { Form } from "@patternfly/react-core/dist/esm/components/Form/index.js"; import { Flex, FlexItem } from "@patternfly/react-core/dist/esm/layouts/Flex/index.js"; import { Modal, ModalBody, ModalFooter, ModalHeader } from '@patternfly/react-core/dist/esm/components/Modal/index.js'; import { Radio } from "@patternfly/react-core/dist/esm/components/Radio/index.js"; import { Popover } from "@patternfly/react-core/dist/esm/components/Popover/index.js"; import { Spinner } from "@patternfly/react-core/dist/esm/components/Spinner/index.js"; import { Split, SplitItem } from "@patternfly/react-core/dist/esm/layouts/Split/index.js"; import { Stack } from "@patternfly/react-core/dist/esm/layouts/Stack/index.js"; import { Content, ContentVariants } from "@patternfly/react-core/dist/esm/components/Content/index.js"; import { InfoIcon, InfoCircleIcon } from "@patternfly/react-icons"; import { Icon } from "@patternfly/react-core/dist/esm/components/Icon/index.js"; import cockpit from "cockpit"; import { proxy as serviceProxy } from "service"; import { install_dialog } from "cockpit-components-install-dialog.jsx"; import { getPackageManager } from "packagemanager"; import type { PackageManager } from "_internal/packagemanager-abstract"; const _ = cockpit.gettext; type ServiceProxy = ReturnType & cockpit.EventSource<{ changed(): void }>; interface KpatchSettingsProps { privileged?: boolean; } interface KpatchSettingsState { loaded: boolean; auto: boolean | null; enabled: boolean | null; missing: string[]; unavailable: string[]; error: string; updating: boolean; showModal: boolean; applyCheckbox: boolean; justCurrent: boolean | null; kernelName: string; patchName: string | null; patchInstalled: boolean | null; patchUnavailable: boolean | null; packageManager: PackageManager | null; } export class KpatchSettings extends React.Component { kpatchService: ServiceProxy; constructor(props: KpatchSettingsProps) { super(props); this.state = { loaded: false, auto: null, // `dnf kpatch` is set to `auto` enabled: null, // kpatch.service is enabled missing: [], // missing packages from `kpatch`, `kpatch-dnf` unavailable: [], // unavailable packages from `kpatch`, `kpatch-dnf` // Modal states error: "", updating: false, showModal: false, applyCheckbox: false, // state of the checkbox justCurrent: null, // radio state kernelName: "", // uname -r patchName: null, // kpatch-patch name patchInstalled: null, // kpatch-patch installed patchUnavailable: null, // kpatch-patch available packageManager: null, }; this.kpatchService = serviceProxy("kpatch") as ServiceProxy; this.checkSetup = this.checkSetup.bind(this); this.handleChange = this.handleChange.bind(this); this.onClose = this.onClose.bind(this); this.handleInstall = this.handleInstall.bind(this); } // Only current patches or also future ones current(enabled: boolean | null, installed: boolean | null, unavailable: boolean | null) { return enabled && (installed || unavailable); } async componentDidMount() { this.kpatchService.addEventListener('changed', () => { this.setState(state => { const current = this.current(this.kpatchService.enabled, state.patchInstalled, state.patchUnavailable); return ({ enabled: this.kpatchService.enabled, justCurrent: current && !state.auto, applyCheckbox: !!current, }); }); }); const packageManager = await getPackageManager(); this.setState({ packageManager }); packageManager.check_missing_packages(["kpatch", "kpatch-dnf"]) .then(d => this.checkSetup().then(() => this.setState({ loaded: true, unavailable: d.unavailable_names || [], missing: d.missing_names || [], }) ) ) .catch((e: cockpit.BasicError) => console.log("Could not determine kpatch availability:", JSON.stringify(e))); } checkSetup() { // TODO - replace both with `dnf kpatch status` once https://github.com/dynup/kpatch-dnf/pull/8 lands const kpatch_promise = cockpit.file("/etc/dnf/plugins/kpatch.conf").read() .then(data => { if (data) { const auto = /autoupdate\s*=\s*True/i.test(data); this.setState((state, _) => { const current = this.current(state.enabled, state.patchInstalled, state.patchUnavailable); return ({ auto: !!auto, justCurrent: current && !auto, applyCheckbox: !!current, }); }); } }) .catch(() => true); // Ignore errors, most likely just does not exist const uname_promise = cockpit.spawn(["uname", "-r"]) .then(data => { const fields = data.split("-"); // if there's no release field, we don't have an official kernel if (!fields[1]) return; const kpp_kernel_version = fields[0].replaceAll(".", "_"); let release = fields[1].split("."); release = release.slice(0, release.length - 2); // remove el8.x86_64 const kpp_kernel_release = release.join("_"); const patch_name = ["kpatch-patch", kpp_kernel_version, kpp_kernel_release].join("-"); cockpit.assert(this.state.packageManager, "packageManager not initialised"); return this.state.packageManager.check_missing_packages([patch_name]) .then(d => this.setState((state, _) => { const installed = (d.unavailable_names || []).length === 0 && (d.missing_names || []).length === 0; const unavailable = (d.unavailable_names || []).length > 0; const current = this.current(state.enabled, installed, unavailable); return ({ kernelName: data, patchName: patch_name, patchInstalled: installed, patchUnavailable: unavailable, justCurrent: current && !state.auto, applyCheckbox: !!current, }); }) ); }) .catch(err => console.error("Could not determine kpatch packages:", JSON.stringify(err))); // not-covered: OS error return Promise.allSettled([kpatch_promise, uname_promise]); } handleInstall() { this.setState({ updating: true }); install_dialog(this.state.missing) .then(() => this.setState({ missing: [], updating: false })) .catch(() => this.setState({ updating: false })); } onClose() { this.setState((state, _) => { const current = this.current(state.enabled, state.patchInstalled, state.patchUnavailable); return ({ justCurrent: current && !state.auto, applyCheckbox: !!current, showModal: false, error: "", }); }); } handleChange() { this.setState({ updating: true }); if (this.state.applyCheckbox) { let install; if (this.state.justCurrent) { install = new Promise((resolve, reject) => { cockpit.spawn(["dnf", "-y", "kpatch", "manual"], { superuser: "require", err: "message" }) .then(() => { if (!this.state.patchUnavailable && !this.state.patchInstalled) // TODO - replace with `dnf kpatch install` once https://github.com/dynup/kpatch-dnf/pull/8 lands cockpit.spawn(["dnf", "-y", "install", this.state.patchName!], { superuser: "require", err: "message" }).then(() => resolve()) .catch(reject); else resolve(); }) .catch(reject); }); } else { install = cockpit.spawn(["dnf", "-y", "kpatch", "auto"], { superuser: "require", err: "message" }); } install .then(() => this.kpatchService.enable().then(() => this.kpatchService.start().then(() => this.setState({ showModal: false, error: "" }) ) ) ) .catch((e: cockpit.BasicError) => this.setState({ error: e.toString() })) .finally(() => this.checkSetup().then(() => this.setState({ updating: false }))); } else { cockpit.spawn(["dnf", "-y", "kpatch", "manual"], { superuser: "require", err: "message" }) .then(() => this.kpatchService.disable().then(() => this.kpatchService.stop().then(() => this.setState({ showModal: false, error: "" }) ) ) ) .catch((e: cockpit.BasicError) => this.setState({ error: e.toString() })) .finally(() => this.checkSetup().then(() => this.setState({ updating: false }))); } } render() { let state; let actionText = _("Edit"); let action = () => this.setState({ showModal: true }); if (this.state.loaded === false || this.state.patchName === null) { // Not yet recognized state = ; } else if (this.state.unavailable.length > 0) { state = { _("Not available") }   ; } else if (this.state.missing.length > 0) { state = _("Not installed"); actionText = _("Install"); action = this.handleInstall; } else if (!this.state.enabled) { state = _("Disabled"); actionText = _("Enable"); } else { state = _("Enabled"); } const kernel_name = this.state.kernelName ? " (" + this.state.kernelName + ")" : ""; const error = this.state.error ? : null; const body =
this.setState({ applyCheckbox: checked })} body={<> this.setState({ justCurrent: false })} isDisabled={!this.state.applyCheckbox} isChecked={!this.state.justCurrent} /> this.setState({ justCurrent: true })} isDisabled={!this.state.applyCheckbox} isChecked={!!this.state.justCurrent} /> } />; return (<>
{_("Kernel live patching")} {state}
{error} {body} ); } } interface KpatchStatusState { loaded: string[]; installed: string[]; changelog: string | null; } export class KpatchStatus extends React.Component, KpatchStatusState> { constructor(props: Record) { super(props); this.state = { loaded: [], installed: [], changelog: null, // FIXME - load changelog }; } componentDidMount() { cockpit.spawn(["kpatch", "list"], { superuser: "try", err: "ignore", environ: ["LC_MESSAGES=C"] }) .then(m => { const parts = m.trim().split("\n\n"); if (parts.length !== 2 || !parts[0].startsWith("Loaded patch modules:") || !parts[1].startsWith("Installed patch modules:")) { console.warn("Unexpected output from `kpatch list`", m); return; } const loaded = parts[0].split("\n") .slice(1) .map(i => i.split(" ")[0]); const installed = parts[1].split("\n") .slice(1) .map(i => i.split(" ")[0]); this.setState({ loaded, installed }); }) .catch(() => true); // Ignore errors } render() { let text: React.ReactNode[] = []; text = this.state.loaded.map(i => { cockpit.format(_("Kernel live patch $0 is active"), i) } ); if (text.length === 0) text = this.state.installed.map(i => { cockpit.format(_("Kernel live patch $0 is installed"), i) } ); if (text.length > 0) return ( {text} ); return null; } }