* Copyright (C) 2017 Red Hat, Inc.
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
import cockpit from "cockpit";
import React, { useState } 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 { Flex, FlexItem } from "@patternfly/react-core/dist/esm/layouts/Flex/index.js";
import { Form, FormGroup } from "@patternfly/react-core/dist/esm/components/Form/index.js";
import { FormSelect, FormSelectOption } from "@patternfly/react-core/dist/esm/components/FormSelect/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 { TimePicker } from "@patternfly/react-core/dist/esm/components/TimePicker/index.js";
import { install_dialog } from "cockpit-components-install-dialog.jsx";
import { useDialogs } from "dialogs.jsx";
import { debug } from "./utils";
const _ = cockpit.gettext;
* Package manager specific implementations; PackageKit does not cover
* automatic updates, so we have to implement dnf-automatic and
* unattended-upgrades configuration ourselves
*/
class ImplBase {
constructor() {
this.supported = true;
this.enabled = null;
this.type = null;
this.day = null;
this.time = null;
this.installed = null;
this.packageName = null;
}
async getConfig() {
throw new Error("abstract method");
}
async setConfig(enabled, type, day, time) {
throw new Error("abstract method", enabled, type, day, time);
}
parseCalendar(spec) {
const daysOfWeek = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"];
const validTime = /^((|0|1)[0-9]|2[0-3]):[0-5][0-9]$/;
const words = spec.trim().toLowerCase()
.split(/\s+/);
if (daysOfWeek.indexOf(words[0]) >= 0) {
this.day = words.shift();
} else if (words[0] === '*-*-*') {
this.day = "";
words.shift();
} else {
this.day = "";
}
if (words.length === 1 && validTime.test(words[0]))
this.time = words[0].replace(/^0+/, "");
else
this.supported = false;
}
}
class Dnf4Impl extends ImplBase {
async getConfig() {
this.packageName = "dnf-automatic";
try {
const output = await cockpit.script(
"set -e; if rpm -q " + this.packageName + " >/dev/null; then echo installed; fi; " +
"if grep -q '^[ \\t]*upgrade_type[ \\t]*=[ \\t]*security' /etc/dnf/automatic.conf; then echo security; fi; " +
"TIMER=dnf-automatic-install.timer; " +
"if systemctl --quiet is-enabled dnf-automatic-install.timer 2>/dev/null; then echo enabled; " +
"elif systemctl --quiet is-enabled dnf-automatic.timer 2>/dev/null && grep -q '^[ \t]*apply_updates[ \t]*=[ \t]*yes' " +
" /etc/dnf/automatic.conf; then echo enabled; TIMER=dnf-automatic.timer; " +
"fi; " +
'OUT=$(systemctl cat $TIMER 2>/dev/null || true); ' +
'echo "$OUT" | grep "^OnUnitInactiveSec= *[^ ]" | tail -n1; ' +
'echo "$OUT" | grep "^OnCalendar= *[^ ]" | tail -n1; ',
[], { err: "message" });
this.installed = (output.indexOf("installed\n") >= 0);
this.enabled = (output.indexOf("enabled\n") >= 0);
this.type = (output.indexOf("security\n") >= 0) ? "security" : "all";
const calIdx = output.indexOf("OnCalendar=");
if (calIdx >= 0) {
this.parseCalendar(output.substring(calIdx).split('\n')[0].split("=")[1]);
} else {
if (output.indexOf("InactiveSec=1d\n") >= 0)
this.day = this.time = "";
else if (this.installed)
this.supported = false;
}
debug(`dnf4 getConfig: supported ${this.supported}, enabled ${this.enabled}, type ${this.type}, day ${this.day}, time ${this.time}, installed ${this.installed}; raw response '${output}'`);
} catch (error) {
console.error("dnf4 getConfig failed:", error);
this.supported = false;
}
}
async setConfig(enabled, type, day, time) {
const timerConf = "/etc/systemd/system/dnf-automatic-install.timer.d/time.conf";
let script = "set -e; ";
if (type !== null) {
const value = (type === "security") ? "security" : "default";
script += "sed -i '/\\bupgrade_type\\b[ \\t]*=/ { h; s/^.*$/upgrade_type = " + value + "/ }; " +
"$ { x; /^$/ { s//upgrade_type = " + value + "/; H }; x }' /etc/dnf/automatic.conf; ";
}
if (enabled && !this.enabled && !this.time && !this.day)
time = "6:00";
if (time !== null || day !== null) {
if (day === "" && time === "") {
script += "rm -f " + timerConf + "; ";
} else {
if (day === null)
day = this.day;
if (time === null)
time = this.time;
script += "mkdir -p /etc/systemd/system/dnf-automatic-install.timer.d; ";
script += "printf '[Timer]\\nOnBootSec=\\nOnCalendar=" + day + " " + time + "\\n' > " + timerConf + "; ";
script += "systemctl daemon-reload; ";
}
}
if (enabled !== null) {
const rebootConf = "/etc/systemd/system/dnf-automatic-install.service.d/autoreboot.conf";
script += "systemctl " + (enabled ? "enable" : "disable") + " --now dnf-automatic-install.timer; ";
if (enabled) {
default to "when-needed"; but be strict about the format, to avoid changing a customized setting */
script += "if grep '^[[:space:]]*reboot\\b' /etc/dnf/automatic.conf; then ";
script += " sed -i 's/^reboot = never$/reboot = when-needed/' /etc/dnf/automatic.conf; ";
script += " rm -f " + rebootConf + "; ";
file behind for deciding whether it actually installed anything, so resort to grepping the journal
for the last run (https://bugzilla.redhat.com/show_bug.cgi?id=1491190) */
script += "else ";
script += " mkdir -p /etc/systemd/system/dnf-automatic-install.service.d; ";
script += " printf '[Service]\\nExecStartPost=/bin/sh -ec \"" +
"if systemctl status --no-pager --lines=100 dnf-automatic-install.service| grep -q ===========$$; then " +
"shutdown -r +5 rebooting after applying package updates; fi\"\\n' > " + rebootConf + "; ";
script += " systemctl daemon-reload; ";
script += "fi";
} else {
script += "systemctl disable --now dnf-automatic.timer 2>/dev/null || true; ";
script += "rm -f " + rebootConf + "; ";
}
}
debug(`dnf4 setConfig(${enabled}, "${type}", "${day}", "${time}"): script "${script}"`);
try {
await cockpit.script(script, [], { superuser: "require" });
debug("dnf4 setConfig: configuration updated successfully");
if (enabled !== null)
this.enabled = enabled;
if (type !== null)
this.type = type;
if (day !== null)
this.day = day;
if (time !== null)
this.time = time;
} catch (error) {
console.error("dnf4 setConfig failed:", error.toString());
}
}
}
class Dnf5Impl extends ImplBase {
async getConfig() {
this.packageName = "dnf5-plugin-automatic";
this.configFile = "/etc/dnf/dnf5-plugins/automatic.conf";
try {
await cockpit.spawn(["rpm", "-q", this.packageName], { err: "ignore" });
this.installed = true;
} catch (ex) {
this.installed = false;
debug("dnf5 getConfig: not installed:", ex);
return;
}
try {
const output = await cockpit.script(
"set -eu;" +
"if grep -q '^[ \\t]*upgrade_type[ \\t]*=[ \\t]*security' " + this.configFile + "; then echo security; fi; " +
"if systemctl --quiet is-enabled dnf5-automatic.timer && " +
" grep -q '^[ \t]*apply_updates[ \t]*=[ \t]*yes' " + this.configFile + "; then echo enabled; fi; " +
'OUT=$(systemctl cat dnf5-automatic.timer || true); ' +
'echo "$OUT" | grep "^OnUnitInactiveSec= *[^ ]" | tail -n1; ' +
'echo "$OUT" | grep "^OnCalendar= *[^ ]" | tail -n1; ',
[], { err: "message" });
this.enabled = (output.indexOf("enabled\n") >= 0);
this.type = (output.indexOf("security\n") >= 0) ? "security" : "all";
const calIdx = output.indexOf("OnCalendar=");
if (calIdx >= 0) {
this.parseCalendar(output.substring(calIdx).split('\n')[0].split("=")[1]);
} else {
if (output.indexOf("InactiveSec=1d\n") >= 0)
this.day = this.time = "";
else if (this.installed)
this.supported = false;
}
debug(`dnf5 getConfig: supported ${this.supported}, enabled ${this.enabled}, type ${this.type}, day ${this.day}, time ${this.time}, installed ${this.installed}; raw response '${output}'`);
} catch (error) {
console.error("dnf5 getConfig failed:", error);
this.supported = false;
}
}
async setConfig(enabled, type, day, time) {
const timerConfD = "/etc/systemd/system/dnf5-automatic.timer.d";
const timerConf = timerConfD + "/time.conf";
let script = "set -e; ";
const settings = [];
if (type !== null)
settings.push(["upgrade_type", (type === "security") ? "security" : "default"]);
if (time !== null || day !== null) {
if (day === "" && time === "") {
script += "rm -f " + timerConf + "; ";
} else {
if (day === null)
day = this.day;
if (time === null)
time = this.time;
script += "mkdir -p " + timerConfD + "; ";
script += "printf '[Timer]\\nOnBootSec=\\nOnCalendar=" + day + " " + time + "\\n' > " + timerConf + "; ";
script += "systemctl daemon-reload; ";
}
}
if (enabled !== null) {
script += "systemctl " + (enabled ? "enable" : "disable") + " --now dnf5-automatic.timer; ";
if (enabled) {
settings.push(["apply_updates", "yes"]);
settings.push(["reboot", "when-needed"]);
}
}
debug(`dnf5 setConfig(${enabled}, "${type}", "${day}", "${time}"): script "${script}", settings ${settings}`);
try {
if (settings.length > 0) {
await cockpit.file(this.configFile, { superuser: "require" }).modify(content => {
const lines = content ? content.split('\n') : [];
settings.forEach(([key, value]) => {
const idx = lines.findIndex(line => line.startsWith(key));
if (idx >= 0)
lines[idx] = key + " = " + value;
else
lines.push(`[commands]\n${key} = ${value}`);
});
return lines.join('\n');
});
}
await cockpit.script(script, [], { superuser: "require" });
debug("dnf5 setConfig: configuration updated successfully");
if (enabled !== null)
this.enabled = enabled;
if (type !== null)
this.type = type;
if (day !== null)
this.day = day;
if (time !== null)
this.time = time;
} catch (error) {
console.error("dnf5 setConfig failed:", error.toString());
}
}
}
export function getBackend(packagekit_backend, forceReinit) {
if (!getBackend.promise || forceReinit) {
debug("getBackend() called first time or forceReinit passed, initializing promise");
getBackend.promise = new Promise((resolve, reject) => {
if (packagekit_backend === "dnf5") {
const backend = new Dnf5Impl();
backend.getConfig().then(() => resolve(backend));
} else if (packagekit_backend === "dnf") {
cockpit.spawn(["dnf", "--version"], { err: "message" })
.then(version => {
const backend = version.includes("dnf5") ? new Dnf5Impl() : new Dnf4Impl();
backend.getConfig().then(() => resolve(backend));
})
.catch(ex => {
console.error("failed to run dnf --version:", ex);
resolve(null);
});
} else {
resolve(null);
}
});
}
return getBackend.promise;
}
const AutoUpdatesDialog = ({ backend }) => {
const Dialogs = useDialogs();
const [pending, setPending] = useState(false);
const [enabled, setEnabled] = useState(backend.enabled);
const [type, setType] = useState(backend.type);
const [day, setDay] = useState(backend.day);
const [time, setTime] = useState(backend.time && backend.time.padStart(5, "0"));
function save(event) {
setPending(true);
backend.setConfig(enabled, type, day, time)
.finally(Dialogs.close);
if (event)
event.preventDefault();
return false;
}
return (
<Modal position="top" variant="small" id="automatic-updates-dialog" isOpen
onClose={Dialogs.close}>
<ModalHeader title={_("Automatic updates")} />
<ModalBody>
<Form isHorizontal onSubmit={save}>
<FormGroup fieldId="type" label={_("Type")} hasNoPaddingTop>
<Radio isChecked={!enabled}
onChange={() => { setEnabled(false); setType(null) }}
isDisabled={pending}
label={_("No updates")}
id="no-updates"
name="type" />
<Radio isChecked={enabled && type === "security"}
onChange={() => { setEnabled(true); setType("security") }}
isDisabled={pending}
label={_("Security updates only")}
id="security-updates"
name="type" />
<Radio isChecked={enabled && type === "all"}
onChange={() => { setEnabled(true); setType("all") }}
isDisabled={pending}
label={_("All updates")}
id="all-updates"
name="type" />
</FormGroup>
{enabled &&
<>
<FormGroup fieldId="when" label={_("When")}>
<Flex className="auto-update-group">
<FormSelect id="auto-update-day"
isDisabled={pending}
value={day === "" ? "everyday" : day}
onChange={(_, d) => setDay(d === "everyday" ? "" : d) }>
<FormSelectOption value="everyday" label={_("every day")} />
<FormSelectOption value="mon" label={_("Mondays")} />
<FormSelectOption value="tue" label={_("Tuesdays")} />
<FormSelectOption value="wed" label={_("Wednesdays")} />
<FormSelectOption value="thu" label={_("Thursdays")} />
<FormSelectOption value="fri" label={_("Fridays")} />
<FormSelectOption value="sat" label={_("Saturdays")} />
<FormSelectOption value="sun" label={_("Sundays")} />
</FormSelect>
<span className="auto-conf-text">{_("at")}</span>
<TimePicker time={time} is24Hour
menuAppendTo={() => document.body}
id="auto-update-time" isDisabled={pending}
invalidFormatErrorMessage={_("Invalid time format")}
onChange={(_, time) => setTime(time)} />
</Flex>
</FormGroup>
<Alert variant="info" title={_("This host will reboot after updates are installed.")} isInline />
</>}
</Form>
</ModalBody>
<ModalFooter>
<Button variant="primary"
isLoading={pending}
isDisabled={pending}
onClick={save}>
{_("Save changes")}
</Button>
<Button variant="link"
isDisabled={pending}
onClick={Dialogs.close}>
{_("Cancel")}
</Button>
</ModalFooter>
</Modal>);
};
export const AutoUpdates = ({ privileged, packagekit_backend, initial_backend }) => {
const Dialogs = useDialogs();
const [backend, setBackend] = useState(initial_backend);
if (!backend)
return null;
let state = null;
if (!backend.enabled)
state = _("Disabled");
if (!backend.installed)
state = _("Not set up");
const days = {
"": _("every day"),
mon: _("every Monday"),
tue: _("every Tuesday"),
wed: _("every Wednesday"),
thu: _("every Thursday"),
fri: _("every Friday"),
sat: _("every Saturday"),
sun: _("every Sunday")
};
let desc = null;
if (backend.enabled && backend.supported) {
const day = days[backend.day];
const time = backend.time;
desc = backend.type === "security"
? cockpit.format(_("Security updates will be applied $0 at $1"), day, time)
: cockpit.format(_("Updates will be applied $0 at $1"), day, time);
}
if (privileged && backend.installed && !backend.supported)
return (
<div id="autoupdates-settings">
<Alert isInline
variant="info"
className="autoupdates-card-error"
title={_("Failed to parse unit files for dnf-automatic.timer or dnf-automatic-install.timer. Please remove custom overrides to configure automatic updates.")} />
</div>
);
return (
<div id="autoupdates-settings">
<Flex alignItems={{ default: 'alignItemsCenter' }}>
<Flex grow={{ default: 'grow' }} alignItems={{ default: 'alignItemsBaseline' }}>
<FlexItem>
<b>{_("Automatic updates")}</b>
</FlexItem>
<FlexItem>
{state}
</FlexItem>
</Flex>
<Flex>
<Button variant="secondary"
isDisabled={!privileged}
size="sm"
onClick={() => {
if (!backend.installed) {
install_dialog(backend.packageName)
.then(() => {
getBackend(packagekit_backend, true).then(b => {
setBackend(b);
Dialogs.show(<AutoUpdatesDialog backend={b} />);
});
}, () => null);
} else {
Dialogs.show(<AutoUpdatesDialog backend={backend} />);
}
}}>
{!backend.installed ? _("Enable") : _("Edit")}
</Button>
</Flex>
</Flex>
{desc}
</div>);
};