Skip to content

Conversation

@mchlwbr
Copy link

@mchlwbr mchlwbr commented Oct 16, 2025

This is currently based on cockpit version 337 as we (Team LinOTP) develop/run on bookworm-backports and resolves #15269.

I will happily rebase on a branch of your choice to get the feature to bookworm-backports.

#18595 seems to be stale and instead of parsing the config files on our own, we rely on apt-config dump doing the heavy lifting.

@mchlwbr mchlwbr force-pushed the feature/unattended-updates-LVA-156 branch from 83b983c to 5430d60 Compare October 22, 2025 05:10
@tomasmatus
Copy link
Member

Hey @mchlwbr ! Thanks for the PR, we'll have a look once you think it's ready and help you with tests if necessary.

Just a quick heads up - translations are done through Fedora Weblate and are periodically synced to here. Please don't directly edit translation files here.

@mchlwbr
Copy link
Author

mchlwbr commented Oct 22, 2025

Thanks, will do!

Please don't directly edit translation files here.

I will drop the commit. I added it because of the documented translation process:

Translations can also be done using any other tool or platform and be submitted via pull requests that update the po/XX.po files.

@mchlwbr mchlwbr force-pushed the feature/unattended-updates-LVA-156 branch from 5430d60 to 745c9be Compare October 22, 2025 07:57
@tomasmatus
Copy link
Member

Ah, my bad then, sorry! Though it may get annoying to deal with rebases every week when we sync translations.

@mchlwbr mchlwbr changed the title Draft: feauture: automatic apt updates via unattended-upgrades feauture: automatic apt updates via unattended-upgrades Oct 22, 2025
@mchlwbr mchlwbr force-pushed the feature/unattended-updates-LVA-156 branch 2 times, most recently from 8466912 to 3d3635e Compare October 22, 2025 12:01
@mchlwbr
Copy link
Author

mchlwbr commented Oct 22, 2025

The feature itself is ready for review.

I have rebased the PR on main and extended the tests in the WIP commit - albeit they are shot in the dark, as I don't get them to run locally. Is the test coverage enough? Do I need to extend testWithAvailableUpdates?

Though it may get annoying to deal with rebases every week when we sync translations.

I agree^^

Copy link
Member

@jelly jelly left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you very much for picking this up, this has been a long awaited feature from the community. Sorry for the long delay for an initial review.

} else {
if (timerOutput.indexOf("InactiveSec=1d\n") >= 0)
this.day = this.time = "";
else if (this.installed)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be unreachable, we return directly if it is not installed. (same issue is in the dnf5 implementation)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

}

// Get unattended-upgrades configuration
const aptConfigOutput = await cockpit.script('apt-config dump | grep "Unattended-Upgrade"', [], { superuser: "require", err: "message" });
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For me, verify that this is always installed on Debian.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

apt-config (part of apt) and grep should always be available as they both have required as priority and grep is marked as essential:

$ apt-cache show apt | grep -E "(Essential|Priority):"
Priority: required
$ apt-cache show grep | grep -E "(Essential|Priority):"
Essential: yes
Priority: required

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that's correct!

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll leave it to you to resolve threads once you're satisfied, ok?

script += ' "origin=Debian,codename=${distro_codename}-security,label=Debian-Security";\n';
} else {
// All updates (security + regular)
script += ' "origin=Debian,codename=${distro_codename},label=Debian";\n';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this correct? Reading the periodic updates wiki, it says the default is only security updates and that is:

Unattended-Upgrade::Origins-Pattern {
        // Codename based matching:
        // This will follow the migration of a release through different
        // archives (e.g. from testing to stable and later oldstable).
        // Software will be the latest available for the named release,
        // but the Debian release itself will not be automatically upgraded.
//      "origin=Debian,codename=${distro_codename}-updates";
//      "origin=Debian,codename=${distro_codename}-proposed-updates";
        "origin=Debian,codename=${distro_codename},label=Debian";
        "origin=Debian,codename=${distro_codename},label=Debian-Security";
        "origin=Debian,codename=${distro_codename}-security,label=Debian-Security";
//      "o=Debian Backports,n=${distro_codename}-backports,l=Debian Backports";

        // Archive or Suite based matching:
        // Note that this will silently match a different release after
        // migration to the specified archive (e.g. testing becomes the
        // new stable).
//      "o=Debian,a=stable";
//      "o=Debian,a=stable-updates";
//      "o=Debian,a=proposed-updates";
//      "o=Debian Backports,a=stable-backports,l=Debian Backports";
};

So I think if you want "all updates" you also have to enable:

"origin=Debian,codename=${distro_codename}-updates";

And we also have to handle if a user has enabled backports I suppose and enable those updates.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are correct.

We propose to set it to "origin=*"; when the user selects all. This way, all defined apt sources get used.

For security it'll be set to the quoted default:

        "origin=Debian,codename=${distro_codename},label=Debian";
        "origin=Debian,codename=${distro_codename},label=Debian-Security";
        "origin=Debian,codename=${distro_codename}-security,label=Debian-Security";

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

// Determine upgrades type based on Origins-Pattern
const regularUpgrades = aptConfigOutput.includes('Unattended-Upgrade::Origins-Pattern:: "origin=Debian,codename=${distro_codename},label=Debian"');
const securityUpgrades = aptConfigOutput.includes('Unattended-Upgrade::Origins-Pattern:: "origin=Debian,codename=${distro_codename},label=Debian-Security"') ||
aptConfigOutput.includes('Unattended-Upgrade::Origins-Pattern:: "origin=Debian,codename=${distro_codename}-security,label=Debian-Security"');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are very long lines and not to readable, I have some questions about the difference between all / security updates below. Hopefully we can simplify things after that is cleared up.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've suggested to set origins=* for all updates below.

I will think about how the parsing of a given config can be refactored.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

script += "systemctl enable --now apt-daily.timer; ";
} else {
script += "systemctl disable --now apt-daily-upgrade.timer; ";
script += "systemctl disable --now apt-daily.timer; ";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two timers unfortunately but not both are overridden, when I configured them to update every Friday at 6:00.

Thu 2025-11-06 20:09:51 UTC    10h Fri 2025-10-31 13:36:59 UTC        - apt-daily.timer              apt-daily.service
Fri 2025-11-07 06:13:11 UTC    20h Thu 2025-11-06 10:00:39 UTC  47s ago apt-daily-upgrade.timer      apt-daily-upgrade.service
root@debian-trixie-127-0-0-2-2201:~# systemctl cat apt-daily-upgrade.timer
# /usr/lib/systemd/system/apt-daily-upgrade.timer
[Unit]
Description=Daily apt upgrade and clean activities
After=apt-daily.timer

[Timer]
OnCalendar=*-*-* 6:00
RandomizedDelaySec=60m
Persistent=true

[Install]
WantedBy=timers.target

# /etc/systemd/system/apt-daily-upgrade.timer.d/time.conf
[Timer]
OnCalendar=
OnCalendar=fri 06:00
root@debian-trixie-127-0-0-2-2201:~# systemctl cat apt-daily.timer
# /usr/lib/systemd/system/apt-daily.timer
[Unit]
Description=Daily apt download activities

[Timer]
OnCalendar=*-*-* 6,18:00
RandomizedDelaySec=12h
Persistent=true

[Install]
WantedBy=timers.target

Or is this expected? And does apt-daily just download updates so this wouldn't be an issue.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

apt-daily perfoms apt update as far as I understood it: https://salsa.debian.org/apt-team/apt/-/blob/main/debian/apt-daily.service

For consistency, they should likely both be set to the defined value.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Copy link
Author

@mchlwbr mchlwbr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for your review and sorry about the delay - I was on vacation :)

I will implement your suggestions.

Can you give me the OK for setting "origin=*"; when the user selects all updates?

script += ' "origin=Debian,codename=${distro_codename}-security,label=Debian-Security";\n';
} else {
// All updates (security + regular)
script += ' "origin=Debian,codename=${distro_codename},label=Debian";\n';
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are correct.

We propose to set it to "origin=*"; when the user selects all. This way, all defined apt sources get used.

For security it'll be set to the quoted default:

        "origin=Debian,codename=${distro_codename},label=Debian";
        "origin=Debian,codename=${distro_codename},label=Debian-Security";
        "origin=Debian,codename=${distro_codename}-security,label=Debian-Security";

}

// Get unattended-upgrades configuration
const aptConfigOutput = await cockpit.script('apt-config dump | grep "Unattended-Upgrade"', [], { superuser: "require", err: "message" });
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

apt-config (part of apt) and grep should always be available as they both have required as priority and grep is marked as essential:

$ apt-cache show apt | grep -E "(Essential|Priority):"
Priority: required
$ apt-cache show grep | grep -E "(Essential|Priority):"
Essential: yes
Priority: required

script += "systemctl enable --now apt-daily.timer; ";
} else {
script += "systemctl disable --now apt-daily-upgrade.timer; ";
script += "systemctl disable --now apt-daily.timer; ";
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

apt-daily perfoms apt update as far as I understood it: https://salsa.debian.org/apt-team/apt/-/blob/main/debian/apt-daily.service

For consistency, they should likely both be set to the defined value.

// Determine upgrades type based on Origins-Pattern
const regularUpgrades = aptConfigOutput.includes('Unattended-Upgrade::Origins-Pattern:: "origin=Debian,codename=${distro_codename},label=Debian"');
const securityUpgrades = aptConfigOutput.includes('Unattended-Upgrade::Origins-Pattern:: "origin=Debian,codename=${distro_codename},label=Debian-Security"') ||
aptConfigOutput.includes('Unattended-Upgrade::Origins-Pattern:: "origin=Debian,codename=${distro_codename}-security,label=Debian-Security"');
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've suggested to set origins=* for all updates below.

I will think about how the parsing of a given config can be refactored.

@mchlwbr mchlwbr force-pushed the feature/unattended-updates-LVA-156 branch from 19a3e67 to 04682ea Compare November 19, 2025 12:51
@mchlwbr mchlwbr force-pushed the feature/unattended-updates-LVA-156 branch from 04682ea to acb2e3f Compare November 24, 2025 10:02
Copy link
Member

@jelly jelly left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the fixes!

Copy link
Contributor

@cockpituous cockpituous left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are more than 10 code coverage comments, see the full report here.

@mchlwbr
Copy link
Author

mchlwbr commented Nov 26, 2025

Hi @jelly
Thanks to the tests, I just noticed that the current implementation does not support ubuntu.

I'll get onto that and also extend the tests.

@@ -1460,14 +1466,16 @@ class TestAutoUpdates(NoSubManCase):

def setUp(self):
super().setUp()
# not implemented for apt yet, only dnf
self.supported_backend = "dnf" in self.backend
self.supported_backend = "dnf" in self.backend or self.backend == "apt"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One missing converted test is testWithAvailableUpdates it now fails with NotImplementedError.

@jelly
Copy link
Member

jelly commented Nov 26, 2025

Hi @jelly Thanks to the tests, I just noticed that the current implementation does not support ubuntu.

I'll get onto that and also extend the tests.

Thanks! Feel free to ask for help

Copy link
Contributor

@cockpituous cockpituous left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are more than 10 code coverage comments, see the full report here.

Comment on lines +98 to +99
if (time !== null || day !== null) {
if (day === "" && time === "") {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These 2 added lines are not executed by any test. Details

if (time !== null || day !== null) {
if (day === "" && time === "") {
// restore defaults
script += `rm -f ${configFile}; `;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This added line is not executed by any test. Details

Comment on lines +103 to +104
if (day == null) day = this.day;
if (time == null) time = this.time;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These 2 added lines are not executed by any test. Details

}
}
// Configure timer
script += this.generateTimerConfigScript(timerConf, day, time);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This added line is not executed by any test. Details

}
}

class AptImpl extends ImplBase {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This added line is not executed by any test. Details

Comment on lines 341 to 345
originsPatterns = {
all: '"origin=*";',
stable: '"origin=Debian,codename=${distro_codename},label=Debian";',
security: '"origin=Debian,codename=${distro_codename}-security,label=Debian-Security";',
securityAlt: '"origin=Debian,codename=${distro_codename},label=Debian-Security";'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These 5 added lines are not executed by any test. Details

Comment on lines +348 to +349
async getConfig() {
this.packageName = "unattended-upgrades";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These 2 added lines are not executed by any test. Details

Comment on lines +351 to +357
try {
await cockpit.spawn(["dpkg", "-l", this.packageName], { err: "ignore" });
this.installed = true;
} catch (ex) {
this.installed = false;
debug("apt getConfig: not installed:", ex);
return;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These 7 added lines are not executed by any test. Details

return;
}

try {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This added line is not executed by any test. Details

Comment on lines +362 to +363
const timerOutput = await cockpit.script(
"set -eu; " +
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These 2 added lines are not executed by any test. Details

@mchlwbr
Copy link
Author

mchlwbr commented Dec 4, 2025

Hi @jelly

I have added a WIP commit for ubuntu support based on unattended-upgrades config on ubuntu:

$ cat /etc/apt/apt.conf.d/50unattended-upgrades
// Automatically upgrade packages from these (origin:archive) pairs
//
// Note that in Ubuntu security updates may pull in new dependencies
// from non-security sources (e.g. chromium). By allowing the release
// pocket these get automatically pulled in.
Unattended-Upgrade::Allowed-Origins {
        "${distro_id}:${distro_codename}";
        "${distro_id}:${distro_codename}-security";
        // Extended Security Maintenance; doesn't necessarily exist for
        // every release and this system may not have it installed, but if
        // available, the policy for updates is such that unattended-upgrades
        // should also install from here by default.
        "${distro_id}ESMApps:${distro_codename}-apps-security";
        "${distro_id}ESM:${distro_codename}-infra-security";
//      "${distro_id}:${distro_codename}-updates";
//      "${distro_id}:${distro_codename}-proposed";
//      "${distro_id}:${distro_codename}-backports";
};

...

For security updates i have opted to require all patterns.

Could you please test it on ubuntu, @jelly ?
For me, a manual run of unattended-upgrade works with the configs set by cockpit.
But I currently have a weird network issue regarding the timer/service on my VM... The service and apt think the VM is offline, hence the service does not start due to ExecStartPre=-/usr/lib/apt/apt-helper wait-online. But even with that removed, it wont apply the upgrades :/
I will still try to get it to work on my VM, but it would be great if you can confirm that it's working on ubuntu.

Once that's sorted, I'll continue updating/fixing the test suite.

Edit 1: I got the timer to work as expected (I had to remove /var/lib/apt/periodic/upgrade-stamp manually though, otherwise the executed /usr/lib/apt/apt.systemd.daily install will skip the upgrades)

@mchlwbr
Copy link
Author

mchlwbr commented Dec 4, 2025

I pushed the updated tests - could you please run the test-suite again, @jelly ? I dont find the test report with the failing tests anymore :/

FYI: I'll rebase and merge both WIP commits with the others once it's running.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Enable automatic software updates on Debian

4 participants