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
4 changes: 3 additions & 1 deletion errata/sources/distros/alma.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,9 @@ def process_alma_erratum(release, advisory):
def add_alma_erratum_osreleases(e, release):
""" Update OS Release for Alma Linux errata
"""
osrelease = get_or_create_osrelease(name=f'Alma Linux {release}')
from operatingsystems.utils import normalize_el_osrelease
osrelease_name = normalize_el_osrelease(f'Alma Linux {release}')
osrelease = get_or_create_osrelease(name=osrelease_name)
e.osreleases.add(osrelease)


Expand Down
3 changes: 2 additions & 1 deletion errata/sources/distros/centos.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,13 +117,14 @@ def add_centos_erratum_references(e, references):
def parse_centos_errata_children(e, children):
""" Parse errata children to obtain architecture, release and packages
"""
from operatingsystems.utils import normalize_el_osrelease
fixed_packages = set()
for c in children:
if c.tag == 'os_arch':
pass
elif c.tag == 'os_release':
if accepted_centos_release([c.text]):
osrelease_name = f'CentOS {c.text}'
osrelease_name = normalize_el_osrelease(f'CentOS {c.text}')
osrelease = get_or_create_osrelease(name=osrelease_name)
e.osreleases.add(osrelease)
elif c.tag == 'packages':
Expand Down
36 changes: 27 additions & 9 deletions errata/sources/repos/yum.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@
from defusedxml import ElementTree
from django.db import connections

from operatingsystems.utils import get_or_create_osrelease
from operatingsystems.utils import (
get_or_create_osrelease, normalize_el_osrelease,
)
from packages.models import Package
from packages.utils import get_or_create_package
from patchman.signals import pbar_start, pbar_update
Expand Down Expand Up @@ -184,24 +186,40 @@ def get_osrelease_names(e, update):
return osreleases


def get_existing_el_osreleases(major_version):
""" Returns existing OSReleases for EL-based distros matching the major version
"""
from operatingsystems.models import OSRelease
el_patterns = [
f'Red Hat Enterprise Linux {major_version}',
f'CentOS Stream {major_version}',
f'CentOS {major_version}',
f'Rocky Linux {major_version}',
f'Alma Linux {major_version}',
f'Oracle Linux {major_version}',
]
return list(OSRelease.objects.filter(name__in=el_patterns))


def add_updateinfo_osreleases(e, collection, osrelease_names):
""" Adds OSRelease objects to an Erratum
rocky and alma need some renaming
EPEL maps to existing EL-based OSReleases only
"""
if not osrelease_names:
collection_name = collection.find('name')
if collection_name is not None:
osrelease_name = collection_name.text
osrelease_names.append(osrelease_name)
for osrelease_name in osrelease_names:
if osrelease_name.startswith('almalinux'):
version = osrelease_name.split('-')[1]
osrelease_name = 'Alma Linux ' + version
elif osrelease_name.startswith('rocky-linux'):
version = osrelease_name.split('-')[2]
osrelease_name = 'Rocky Linux ' + version
elif osrelease_name in ['Amazon Linux', 'Amazon Linux AMI']:
osrelease_name = 'Amazon Linux 1'
if osrelease_name.startswith('Fedora EPEL'):
# "Fedora EPEL 10.0" → map to existing EL 10 OSReleases
version_str = osrelease_name.split()[-1] # "10.0"
major_version = version_str.split('.')[0] # "10"
for osrelease in get_existing_el_osreleases(major_version):
e.osreleases.add(osrelease)
continue
osrelease_name = normalize_el_osrelease(osrelease_name)
osrelease = get_or_create_osrelease(name=osrelease_name)
e.osreleases.add(osrelease)

Expand Down
144 changes: 144 additions & 0 deletions operatingsystems/tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,150 @@
from django.test import TestCase, override_settings

from operatingsystems.models import OSRelease, OSVariant
from operatingsystems.utils import normalize_el_osrelease


@override_settings(
CELERY_TASK_ALWAYS_EAGER=True,
CACHES={'default': {'BACKEND': 'django.core.cache.backends.locmem.LocMemCache'}}
)
class NormalizeELOSReleaseTests(TestCase):
"""Tests for normalize_el_osrelease() function.

This function normalizes EL-based distro names to major version only,
ensuring consistent OSRelease naming across errata and reports.

Regression notes - OLD behavior that caused duplicate OSReleases:
- 'rocky-linux-10.1' -> 'Rocky Linux 10.1' (should be 'Rocky Linux 10')
- 'almalinux-10.1' -> 'Alma Linux 10.1' (should be 'Alma Linux 10')
- 'Rocky Linux 10.1' -> passed through unchanged (should be 'Rocky Linux 10')
- 'CentOS 7.9' -> passed through unchanged (should be 'CentOS 7')
"""

# ===========================================
# REGRESSION TESTS - These were bugs before
# ===========================================

def test_regression_rocky_dash_format_minor_stripped(self):
"""REGRESSION: rocky-linux-10.1 was creating 'Rocky Linux 10.1'

Old behavior: version = osrelease_name.split('-')[2] -> '10.1'
result = 'Rocky Linux 10.1'
New behavior: major_version = '10.1'.split('.')[0] -> '10'
result = 'Rocky Linux 10'
"""
# OLD (wrong): 'Rocky Linux 10.1'
# NEW (correct): 'Rocky Linux 10'
self.assertEqual(normalize_el_osrelease('rocky-linux-10.1'), 'Rocky Linux 10')
self.assertNotEqual(normalize_el_osrelease('rocky-linux-10.1'), 'Rocky Linux 10.1')

def test_regression_alma_dash_format_minor_stripped(self):
"""REGRESSION: almalinux-10.1 was creating 'Alma Linux 10.1'

Old behavior: version = osrelease_name.split('-')[1] -> '10.1'
result = 'Alma Linux 10.1'
New behavior: major_version = '10.1'.split('.')[0] -> '10'
result = 'Alma Linux 10'
"""
# OLD (wrong): 'Alma Linux 10.1'
# NEW (correct): 'Alma Linux 10'
self.assertEqual(normalize_el_osrelease('almalinux-10.1'), 'Alma Linux 10')
self.assertNotEqual(normalize_el_osrelease('almalinux-10.1'), 'Alma Linux 10.1')

def test_regression_rocky_human_format_minor_stripped(self):
"""REGRESSION: 'Rocky Linux 10.1' was passed through unchanged

Old behavior: no handling, passed through as 'Rocky Linux 10.1'
New behavior: normalized to 'Rocky Linux 10'
"""
# OLD (wrong): 'Rocky Linux 10.1'
# NEW (correct): 'Rocky Linux 10'
self.assertEqual(normalize_el_osrelease('Rocky Linux 10.1'), 'Rocky Linux 10')
self.assertNotEqual(normalize_el_osrelease('Rocky Linux 10.1'), 'Rocky Linux 10.1')

def test_regression_centos_minor_stripped(self):
"""REGRESSION: 'CentOS 7.9' was passed through unchanged

Old behavior: no handling for human-readable format
New behavior: normalized to 'CentOS 7'
"""
# OLD (wrong): 'CentOS 7.9'
# NEW (correct): 'CentOS 7'
self.assertEqual(normalize_el_osrelease('CentOS 7.9'), 'CentOS 7')
self.assertNotEqual(normalize_el_osrelease('CentOS 7.9'), 'CentOS 7.9')

# ===========================================
# STANDARD TESTS - Expected behavior
# ===========================================

def test_rocky_linux_with_minor_version(self):
"""Test Rocky Linux X.Y -> Rocky Linux X"""
self.assertEqual(normalize_el_osrelease('Rocky Linux 10.1'), 'Rocky Linux 10')
self.assertEqual(normalize_el_osrelease('Rocky Linux 9.3'), 'Rocky Linux 9')

def test_rocky_linux_dash_format(self):
"""Test rocky-linux-X.Y -> Rocky Linux X"""
self.assertEqual(normalize_el_osrelease('rocky-linux-10.1'), 'Rocky Linux 10')
self.assertEqual(normalize_el_osrelease('rocky-linux-9.3'), 'Rocky Linux 9')

def test_alma_linux_with_minor_version(self):
"""Test Alma Linux X.Y -> Alma Linux X"""
self.assertEqual(normalize_el_osrelease('Alma Linux 10.1'), 'Alma Linux 10')
self.assertEqual(normalize_el_osrelease('Alma Linux 9.3'), 'Alma Linux 9')

def test_almalinux_dash_format(self):
"""Test almalinux-X.Y -> Alma Linux X"""
self.assertEqual(normalize_el_osrelease('almalinux-10.1'), 'Alma Linux 10')
self.assertEqual(normalize_el_osrelease('almalinux-9.3'), 'Alma Linux 9')

def test_almalinux_no_space(self):
"""Test AlmaLinux X.Y -> AlmaLinux X"""
self.assertEqual(normalize_el_osrelease('AlmaLinux 10.1'), 'AlmaLinux 10')

def test_centos_with_minor_version(self):
"""Test CentOS X.Y -> CentOS X"""
self.assertEqual(normalize_el_osrelease('CentOS 7.9'), 'CentOS 7')
self.assertEqual(normalize_el_osrelease('CentOS 8.5'), 'CentOS 8')

def test_rhel_with_minor_version(self):
"""Test RHEL X.Y -> RHEL X"""
self.assertEqual(normalize_el_osrelease('RHEL 8.2'), 'RHEL 8')
self.assertEqual(normalize_el_osrelease('RHEL 9.1'), 'RHEL 9')

def test_red_hat_enterprise_linux_with_minor_version(self):
"""Test Red Hat Enterprise Linux X.Y -> Red Hat Enterprise Linux X"""
self.assertEqual(
normalize_el_osrelease('Red Hat Enterprise Linux 8.2'),
'Red Hat Enterprise Linux 8'
)

def test_oracle_linux_with_minor_version(self):
"""Test Oracle Linux X.Y -> Oracle Linux X"""
self.assertEqual(normalize_el_osrelease('Oracle Linux 8.1'), 'Oracle Linux 8')

def test_amazon_linux_normalization(self):
"""Test Amazon Linux -> Amazon Linux 1"""
self.assertEqual(normalize_el_osrelease('Amazon Linux'), 'Amazon Linux 1')
self.assertEqual(normalize_el_osrelease('Amazon Linux AMI'), 'Amazon Linux 1')

# ===========================================
# NO-OP TESTS - Should remain unchanged
# ===========================================

def test_major_version_only_unchanged(self):
"""Test that major-version-only names are unchanged (no regression)"""
self.assertEqual(normalize_el_osrelease('Rocky Linux 10'), 'Rocky Linux 10')
self.assertEqual(normalize_el_osrelease('CentOS 7'), 'CentOS 7')
self.assertEqual(normalize_el_osrelease('RHEL 9'), 'RHEL 9')
self.assertEqual(normalize_el_osrelease('Alma Linux 9'), 'Alma Linux 9')

def test_non_el_distros_unchanged(self):
"""Test that non-EL distros are unchanged (no false positives)"""
self.assertEqual(normalize_el_osrelease('Ubuntu 22.04'), 'Ubuntu 22.04')
self.assertEqual(normalize_el_osrelease('Debian 12'), 'Debian 12')
self.assertEqual(normalize_el_osrelease('Fedora 39'), 'Fedora 39')
self.assertEqual(normalize_el_osrelease('Arch Linux'), 'Arch Linux')
self.assertEqual(normalize_el_osrelease('openSUSE Leap 15.5'), 'openSUSE Leap 15.5')


@override_settings(
Expand Down
33 changes: 33 additions & 0 deletions operatingsystems/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,39 @@
from django.db import IntegrityError


def normalize_el_osrelease(osrelease_name):
"""Normalize EL-based distros to major version only.
e.g. 'Rocky Linux 10.1' -> 'Rocky Linux 10'
'rocky-linux-10.1' -> 'Rocky Linux 10'
'almalinux-10.1' -> 'Alma Linux 10'
"""
if osrelease_name.startswith('rocky-linux-'):
major_version = osrelease_name.split('-')[2].split('.')[0]
return f'Rocky Linux {major_version}'
elif osrelease_name.startswith('almalinux-'):
major_version = osrelease_name.split('-')[1].split('.')[0]
return f'Alma Linux {major_version}'
elif osrelease_name in ['Amazon Linux', 'Amazon Linux AMI']:
return 'Amazon Linux 1'

el_distro_prefixes = [
'Rocky Linux',
'Alma Linux',
'AlmaLinux',
'CentOS',
'RHEL',
'Red Hat Enterprise Linux',
'Oracle Linux',
]
for prefix in el_distro_prefixes:
if osrelease_name.startswith(prefix):
version_part = osrelease_name[len(prefix):].strip()
if '.' in version_part:
major_version = version_part.split('.')[0]
return f'{prefix} {major_version}'
return osrelease_name


def get_or_create_osrelease(name, cpe_name=None, codename=None):
""" Get or create OSRelease from OS details
"""
Expand Down
Loading