Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions hosts/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
# You should have received a copy of the GNU General Public License
# along with Patchman. If not, see <http://www.gnu.org/licenses/>

import re

from django.db import models
from django.db.models import Q
from django.urls import reverse
Expand Down Expand Up @@ -363,6 +365,21 @@ def get_running_kernel_flavour(self):
'linux-tools-',
]

def get_deb_kernel_series(self, pkg_name):
"""Extract kernel major.minor series from a DEB kernel package name.

e.g. 'linux-image-6.8.0-51-generic' → '6.8'
'linux-image-6.17.0-19-generic' → '6.17'
'linux-modules-extra-6.1.0-28-cloud-amd64' → '6.1'
Returns None if the series cannot be determined.
"""
for prefix in self.deb_kernel_prefixes:
if pkg_name.startswith(prefix):
remainder = pkg_name[len(prefix):]
m = re.match(r'(\d+\.\d+)', remainder)
return m.group(1) if m else None
return None

def find_kernel_updates(self, kernel_packages, repo_packages):

update_ids = set()
Expand Down Expand Up @@ -561,6 +578,10 @@ def find_deb_kernel_updates(self, kernel_packages, repo_packages, hostrepos):
continue
processed_prefixes.add(prefix)

# extract kernel series (e.g. '6.8') to avoid cross-track
# comparisons (GA 6.8 vs HWE 6.17 in the same repo)
installed_series = self.get_deb_kernel_series(pkg_name)

# build endswith filter for flavoured kernels
name_filter = Q(
name__name__startswith=prefix,
Expand All @@ -570,8 +591,13 @@ def find_deb_kernel_updates(self, kernel_packages, repo_packages, hostrepos):
name_filter &= Q(name__name__endswith=f'-{running_flavour}')

# find repo highest for this prefix+flavour, respecting priority
# and kernel series (GA vs HWE)
repo_highest = None
for rp in repo_packages.filter(name_filter):
if installed_series is not None:
rp_series = self.get_deb_kernel_series(rp.name.name)
if rp_series != installed_series:
continue
if priority is not None:
rp_best_repo = find_best_repo(rp, hostrepos)
if not rp_best_repo or rp_best_repo.priority < priority:
Expand Down
106 changes: 97 additions & 9 deletions hosts/tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -574,6 +574,92 @@ def test_deb_running_kernel_flavour(self):
host.kernel = '5.14.0-503.el9'
self.assertIsNone(host.get_running_kernel_flavour())

def test_deb_kernel_series_extraction(self):
"""Test get_deb_kernel_series helper."""
host = self._create_host('6.8.0-51-generic', [self.img_51])
self.assertEqual(
host.get_deb_kernel_series('linux-image-6.8.0-51-generic'), '6.8'
)
self.assertEqual(
host.get_deb_kernel_series('linux-image-6.17.0-19-generic'), '6.17'
)
self.assertEqual(
host.get_deb_kernel_series('linux-modules-extra-6.1.0-28-cloud-amd64'), '6.1'
)
self.assertEqual(
host.get_deb_kernel_series('linux-image-unsigned-6.8.0-51-generic'), '6.8'
)
self.assertIsNone(host.get_deb_kernel_series('not-a-kernel'))

def test_deb_hwe_not_offered_to_ga_host(self):
"""HWE kernels (6.17) should not be offered as updates to GA (6.8) hosts.

Reproduces GitHub issue: both GA and HWE kernels in the same repo
(noble-updates), same priority. Without series filtering, 6.17 would
be incorrectly picked as an update for a 6.8 host.
"""
# add HWE kernel packages to the same repo/mirror
hwe_img_name = PackageName.objects.create(
name='linux-image-6.17.0-19-generic'
)
hwe_img = Package.objects.create(
name=hwe_img_name, arch=self.pkg_arch, epoch='',
version='6.17.0-19.19~24.04.2', release='', packagetype='D'
)
self.mirror.packages.add(hwe_img)

host = self._create_host(
'6.8.0-51-generic',
[self.img_49, self.img_51],
)
repo_packages = Package.objects.filter(mirror=self.mirror)
kernel_packages = host.packages.filter(
name__name__startswith='linux-image-'
)

host.find_kernel_updates(kernel_packages, repo_packages)

# should get GA update (6.8.0-51 → 6.8.0-53), NOT HWE (6.17)
self.assertEqual(host.updates.count(), 1)
update = host.updates.first()
self.assertEqual(update.oldpackage, self.img_51)
self.assertEqual(update.newpackage, self.img_53)

def test_deb_hwe_host_gets_hwe_updates(self):
"""HWE host (6.17) should get HWE updates, not GA."""
hwe_img_19_name = PackageName.objects.create(
name='linux-image-6.17.0-19-generic'
)
hwe_img_19 = Package.objects.create(
name=hwe_img_19_name, arch=self.pkg_arch, epoch='',
version='6.17.0-19.19~24.04.2', release='', packagetype='D'
)
hwe_img_21_name = PackageName.objects.create(
name='linux-image-6.17.0-21-generic'
)
hwe_img_21 = Package.objects.create(
name=hwe_img_21_name, arch=self.pkg_arch, epoch='',
version='6.17.0-21.21~24.04.2', release='', packagetype='D'
)
self.mirror.packages.add(hwe_img_19, hwe_img_21)

host = self._create_host(
'6.17.0-19-generic',
[hwe_img_19],
)
repo_packages = Package.objects.filter(mirror=self.mirror)
kernel_packages = host.packages.filter(
name__name__startswith='linux-image-'
)

host.find_kernel_updates(kernel_packages, repo_packages)

# should get HWE update only
self.assertEqual(host.updates.count(), 1)
update = host.updates.first()
self.assertEqual(update.oldpackage, hwe_img_19)
self.assertEqual(update.newpackage, hwe_img_21)


@override_settings(
CELERY_TASK_ALWAYS_EAGER=True,
Expand Down Expand Up @@ -791,8 +877,10 @@ def test_deb_backports_lower_priority_no_update(self):

self.assertEqual(host.updates.count(), 0)

def test_deb_backports_equal_priority_shows_update(self):
"""DEB: backports kernel with equal priority SHOULD be flagged."""
def test_deb_backports_equal_priority_no_update(self):
"""DEB: backports kernel with equal priority but different series
should NOT be flagged — series filtering prevents cross-track updates.
"""
host = self._create_host(main_priority=500, bp_priority=500)
repo_packages = Package.objects.filter(
mirror__in=[self.main_mirror, self.bp_mirror]
Expand All @@ -803,10 +891,10 @@ def test_deb_backports_equal_priority_shows_update(self):

host.find_kernel_updates(kernel_packages, repo_packages)

self.assertEqual(host.updates.count(), 1)
self.assertEqual(host.updates.count(), 0)

def test_deb_priority_zero_no_filtering(self):
"""DEB: priority 0 (unset) means no filtering — backward compat."""
def test_deb_priority_zero_no_cross_series_update(self):
"""DEB: priority 0 (unset) still respects series filtering."""
host = self._create_host(main_priority=0, bp_priority=0)
repo_packages = Package.objects.filter(
mirror__in=[self.main_mirror, self.bp_mirror]
Expand All @@ -817,10 +905,10 @@ def test_deb_priority_zero_no_filtering(self):

host.find_kernel_updates(kernel_packages, repo_packages)

self.assertEqual(host.updates.count(), 1)
self.assertEqual(host.updates.count(), 0)

def test_deb_host_repos_only_false_no_filtering(self):
"""DEB: host_repos_only=False skips priority filtering entirely."""
def test_deb_host_repos_only_false_no_cross_series_update(self):
"""DEB: host_repos_only=False still respects series filtering."""
host = self._create_host(
main_priority=500, bp_priority=100, host_repos_only=False,
)
Expand All @@ -833,7 +921,7 @@ def test_deb_host_repos_only_false_no_filtering(self):

host.find_kernel_updates(kernel_packages, repo_packages)

self.assertEqual(host.updates.count(), 1)
self.assertEqual(host.updates.count(), 0)

def test_rpm_backports_lower_priority_no_update(self):
"""RPM: kernel from lower-priority repo should NOT be flagged."""
Expand Down
Loading