Skip to content

Commit b96e4f7

Browse files
committed
filter deb kernel updates by major.minor series
prevent HWE kernels (e.g. 6.17) from being offered as updates to GA kernel hosts (e.g. 6.8) when both tracks ship in the same repository at the same priority. extract major.minor series from the deb kernel package name and only compare within the same series.
1 parent de53ae7 commit b96e4f7

File tree

2 files changed

+123
-9
lines changed

2 files changed

+123
-9
lines changed

hosts/models.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
# You should have received a copy of the GNU General Public License
1616
# along with Patchman. If not, see <http://www.gnu.org/licenses/>
1717

18+
import re
19+
1820
from django.db import models
1921
from django.db.models import Q
2022
from django.urls import reverse
@@ -363,6 +365,21 @@ def get_running_kernel_flavour(self):
363365
'linux-tools-',
364366
]
365367

368+
def get_deb_kernel_series(self, pkg_name):
369+
"""Extract kernel major.minor series from a DEB kernel package name.
370+
371+
e.g. 'linux-image-6.8.0-51-generic' → '6.8'
372+
'linux-image-6.17.0-19-generic' → '6.17'
373+
'linux-modules-extra-6.1.0-28-cloud-amd64' → '6.1'
374+
Returns None if the series cannot be determined.
375+
"""
376+
for prefix in self.deb_kernel_prefixes:
377+
if pkg_name.startswith(prefix):
378+
remainder = pkg_name[len(prefix):]
379+
m = re.match(r'(\d+\.\d+)', remainder)
380+
return m.group(1) if m else None
381+
return None
382+
366383
def find_kernel_updates(self, kernel_packages, repo_packages):
367384

368385
update_ids = set()
@@ -561,6 +578,10 @@ def find_deb_kernel_updates(self, kernel_packages, repo_packages, hostrepos):
561578
continue
562579
processed_prefixes.add(prefix)
563580

581+
# extract kernel series (e.g. '6.8') to avoid cross-track
582+
# comparisons (GA 6.8 vs HWE 6.17 in the same repo)
583+
installed_series = self.get_deb_kernel_series(pkg_name)
584+
564585
# build endswith filter for flavoured kernels
565586
name_filter = Q(
566587
name__name__startswith=prefix,
@@ -570,8 +591,13 @@ def find_deb_kernel_updates(self, kernel_packages, repo_packages, hostrepos):
570591
name_filter &= Q(name__name__endswith=f'-{running_flavour}')
571592

572593
# find repo highest for this prefix+flavour, respecting priority
594+
# and kernel series (GA vs HWE)
573595
repo_highest = None
574596
for rp in repo_packages.filter(name_filter):
597+
if installed_series is not None:
598+
rp_series = self.get_deb_kernel_series(rp.name.name)
599+
if rp_series != installed_series:
600+
continue
575601
if priority is not None:
576602
rp_best_repo = find_best_repo(rp, hostrepos)
577603
if not rp_best_repo or rp_best_repo.priority < priority:

hosts/tests/test_models.py

Lines changed: 97 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -574,6 +574,92 @@ def test_deb_running_kernel_flavour(self):
574574
host.kernel = '5.14.0-503.el9'
575575
self.assertIsNone(host.get_running_kernel_flavour())
576576

577+
def test_deb_kernel_series_extraction(self):
578+
"""Test get_deb_kernel_series helper."""
579+
host = self._create_host('6.8.0-51-generic', [self.img_51])
580+
self.assertEqual(
581+
host.get_deb_kernel_series('linux-image-6.8.0-51-generic'), '6.8'
582+
)
583+
self.assertEqual(
584+
host.get_deb_kernel_series('linux-image-6.17.0-19-generic'), '6.17'
585+
)
586+
self.assertEqual(
587+
host.get_deb_kernel_series('linux-modules-extra-6.1.0-28-cloud-amd64'), '6.1'
588+
)
589+
self.assertEqual(
590+
host.get_deb_kernel_series('linux-image-unsigned-6.8.0-51-generic'), '6.8'
591+
)
592+
self.assertIsNone(host.get_deb_kernel_series('not-a-kernel'))
593+
594+
def test_deb_hwe_not_offered_to_ga_host(self):
595+
"""HWE kernels (6.17) should not be offered as updates to GA (6.8) hosts.
596+
597+
Reproduces GitHub issue: both GA and HWE kernels in the same repo
598+
(noble-updates), same priority. Without series filtering, 6.17 would
599+
be incorrectly picked as an update for a 6.8 host.
600+
"""
601+
# add HWE kernel packages to the same repo/mirror
602+
hwe_img_name = PackageName.objects.create(
603+
name='linux-image-6.17.0-19-generic'
604+
)
605+
hwe_img = Package.objects.create(
606+
name=hwe_img_name, arch=self.pkg_arch, epoch='',
607+
version='6.17.0-19.19~24.04.2', release='', packagetype='D'
608+
)
609+
self.mirror.packages.add(hwe_img)
610+
611+
host = self._create_host(
612+
'6.8.0-51-generic',
613+
[self.img_49, self.img_51],
614+
)
615+
repo_packages = Package.objects.filter(mirror=self.mirror)
616+
kernel_packages = host.packages.filter(
617+
name__name__startswith='linux-image-'
618+
)
619+
620+
host.find_kernel_updates(kernel_packages, repo_packages)
621+
622+
# should get GA update (6.8.0-51 → 6.8.0-53), NOT HWE (6.17)
623+
self.assertEqual(host.updates.count(), 1)
624+
update = host.updates.first()
625+
self.assertEqual(update.oldpackage, self.img_51)
626+
self.assertEqual(update.newpackage, self.img_53)
627+
628+
def test_deb_hwe_host_gets_hwe_updates(self):
629+
"""HWE host (6.17) should get HWE updates, not GA."""
630+
hwe_img_19_name = PackageName.objects.create(
631+
name='linux-image-6.17.0-19-generic'
632+
)
633+
hwe_img_19 = Package.objects.create(
634+
name=hwe_img_19_name, arch=self.pkg_arch, epoch='',
635+
version='6.17.0-19.19~24.04.2', release='', packagetype='D'
636+
)
637+
hwe_img_21_name = PackageName.objects.create(
638+
name='linux-image-6.17.0-21-generic'
639+
)
640+
hwe_img_21 = Package.objects.create(
641+
name=hwe_img_21_name, arch=self.pkg_arch, epoch='',
642+
version='6.17.0-21.21~24.04.2', release='', packagetype='D'
643+
)
644+
self.mirror.packages.add(hwe_img_19, hwe_img_21)
645+
646+
host = self._create_host(
647+
'6.17.0-19-generic',
648+
[hwe_img_19],
649+
)
650+
repo_packages = Package.objects.filter(mirror=self.mirror)
651+
kernel_packages = host.packages.filter(
652+
name__name__startswith='linux-image-'
653+
)
654+
655+
host.find_kernel_updates(kernel_packages, repo_packages)
656+
657+
# should get HWE update only
658+
self.assertEqual(host.updates.count(), 1)
659+
update = host.updates.first()
660+
self.assertEqual(update.oldpackage, hwe_img_19)
661+
self.assertEqual(update.newpackage, hwe_img_21)
662+
577663

578664
@override_settings(
579665
CELERY_TASK_ALWAYS_EAGER=True,
@@ -791,8 +877,10 @@ def test_deb_backports_lower_priority_no_update(self):
791877

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

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

804892
host.find_kernel_updates(kernel_packages, repo_packages)
805893

806-
self.assertEqual(host.updates.count(), 1)
894+
self.assertEqual(host.updates.count(), 0)
807895

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

818906
host.find_kernel_updates(kernel_packages, repo_packages)
819907

820-
self.assertEqual(host.updates.count(), 1)
908+
self.assertEqual(host.updates.count(), 0)
821909

822-
def test_deb_host_repos_only_false_no_filtering(self):
823-
"""DEB: host_repos_only=False skips priority filtering entirely."""
910+
def test_deb_host_repos_only_false_no_cross_series_update(self):
911+
"""DEB: host_repos_only=False still respects series filtering."""
824912
host = self._create_host(
825913
main_priority=500, bp_priority=100, host_repos_only=False,
826914
)
@@ -833,7 +921,7 @@ def test_deb_host_repos_only_false_no_filtering(self):
833921

834922
host.find_kernel_updates(kernel_packages, repo_packages)
835923

836-
self.assertEqual(host.updates.count(), 1)
924+
self.assertEqual(host.updates.count(), 0)
837925

838926
def test_rpm_backports_lower_priority_no_update(self):
839927
"""RPM: kernel from lower-priority repo should NOT be flagged."""

0 commit comments

Comments
 (0)