* 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<typeof serviceProxy> & 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<KpatchSettingsProps, KpatchSettingsState> {
kpatchService: ServiceProxy;
constructor(props: KpatchSettingsProps) {
super(props);
this.state = {
loaded: false,
auto: null,
enabled: null,
missing: [],
unavailable: [],
error: "",
updating: false,
showModal: false,
applyCheckbox: false,
justCurrent: null,
kernelName: "",
patchName: null,
patchInstalled: null,
patchUnavailable: null,
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);
}
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() {
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);
const uname_promise = cockpit.spawn(["uname", "-r"])
.then(data => {
const fields = data.split("-");
if (!fields[1])
return;
const kpp_kernel_version = fields[0].replaceAll(".", "_");
let release = fields[1].split(".");
release = release.slice(0, release.length - 2);
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)));
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<void>((resolve, reject) => {
cockpit.spawn(["dnf", "-y", "kpatch", "manual"], { superuser: "require", err: "message" })
.then(() => {
if (!this.state.patchUnavailable && !this.state.patchInstalled)
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) {
state = <Spinner size="md" />;
} else if (this.state.unavailable.length > 0) {
state = <Popover headerContent={ _("Unavailable packages") } bodyContent={ this.state.unavailable.join(", ") }>
<span>
{ _("Not available") }
<Icon status="info">
<InfoCircleIcon className="ct-info-circle" />
</Icon>
</span>
</Popover>;
} 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 ? <Alert variant='danger' isInline title={this.state.error} /> : null;
const body = <Form><Checkbox id="apply-kpatch"
isChecked={this.state.applyCheckbox}
label={_("Apply kernel live patches")}
onChange={(_event, checked) => this.setState({ applyCheckbox: checked })}
body={<>
<Radio id="current-future"
name="policy"
label={_("for current and future kernels")}
onChange={() => this.setState({ justCurrent: false })}
isDisabled={!this.state.applyCheckbox}
isChecked={!this.state.justCurrent} />
<Radio id="current-only"
name="policy"
label={_("for current kernel only") + kernel_name}
onChange={() => this.setState({ justCurrent: true })}
isDisabled={!this.state.applyCheckbox}
isChecked={!!this.state.justCurrent} />
</>}
/></Form>;
return (<>
<div id="kpatch-settings">
<Flex alignItems={{ default: 'alignItemsCenter' }}>
<Flex grow={{ default: 'grow' }} alignItems={{ default: 'alignItemsBaseline' }}>
<FlexItem>
<b>{_("Kernel live patching")}</b>
</FlexItem>
<FlexItem>
{state}
</FlexItem>
</Flex>
<Flex>
<Button variant="secondary"
size="sm"
isDisabled={!this.props.privileged || this.state.updating || !this.state.loaded || this.state.unavailable.length > 0}
onClick={action}>
{actionText}
</Button>
</Flex>
</Flex>
</div>
<Modal position="top" variant="small" id="kpatch-setup" isOpen={this.state.showModal}
onClose={ this.onClose }
>
<ModalHeader title={_("Kernel live patch settings")} />
<ModalBody>
{error}
{body}
</ModalBody>
<ModalFooter>
<Button variant="primary"
isLoading={ this.state.updating }
isDisabled={ this.state.updating }
onClick={ this.handleChange }>
{_("Save")}
</Button>
<Button variant="link"
isDisabled={ this.state.updating }
onClick={ this.onClose }>
{_("Cancel")}
</Button>
</ModalFooter>
</Modal>
</>);
}
}
interface KpatchStatusState {
loaded: string[];
installed: string[];
changelog: string | null;
}
export class KpatchStatus extends React.Component<Record<string, never>, KpatchStatusState> {
constructor(props: Record<string, never>) {
super(props);
this.state = {
loaded: [],
installed: [],
changelog: null,
};
}
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);
}
render() {
let text: React.ReactNode[] = [];
text = this.state.loaded.map(i =>
<Content key={i} component={ContentVariants.p}>
{ cockpit.format(_("Kernel live patch $0 is active"), i) }
</Content>
);
if (text.length === 0)
text = this.state.installed.map(i =>
<Content key={i} component={ContentVariants.p}>
{ cockpit.format(_("Kernel live patch $0 is installed"), i) }
</Content>
);
if (text.length > 0)
return (
<Split hasGutter>
<SplitItem>
<InfoIcon />
</SplitItem>
<SplitItem isFilled>
<Stack>
{text}
</Stack>
</SplitItem>
</Split>
);
return null;
}
}