diff --git a/errata/models.py b/errata/models.py
index 4b09deeb..8f6a6f74 100644
--- a/errata/models.py
+++ b/errata/models.py
@@ -21,7 +21,7 @@
from errata.managers import ErratumManager
from packages.models import Package, PackageUpdate
-from packages.utils import find_evr, get_matching_packages
+from packages.utils import find_evr, get_matching_packages_q
from security.models import CVE, Reference
from security.utils import get_or_create_cve, get_or_create_reference
from util import get_url
@@ -64,34 +64,32 @@ def get_absolute_url(self):
def scan_for_security_updates(self):
if self.e_type == 'security':
- for package in self.fixed_packages.all():
- affected_updates = PackageUpdate.objects.filter(
- newpackage=package,
- security=False,
+ fixed_pks = list(self.fixed_packages.values_list('pk', flat=True))
+ if fixed_pks:
+ self._mark_updates_security(
+ PackageUpdate.objects.filter(newpackage__in=fixed_pks, security=False)
)
- for affected_update in affected_updates:
- affected_update.security = True
- try:
- affected_update.save()
- except IntegrityError as e:
- error_message(text=e)
- # a version of this update already exists that is
- # marked as a security update, so delete this one
- affected_update.delete()
- for package in self.affected_packages.all():
- affected_updates = PackageUpdate.objects.filter(
- oldpackage=package,
- security=False,
+ affected_pks = list(self.affected_packages.values_list('pk', flat=True))
+ if affected_pks:
+ self._mark_updates_security(
+ PackageUpdate.objects.filter(oldpackage__in=affected_pks, security=False)
)
- for affected_update in affected_updates:
- affected_update.security = True
- try:
- affected_update.save()
- except IntegrityError as e:
- error_message(text=e)
- # a version of this update already exists that is
- # marked as a security update, so delete this one
- affected_update.delete()
+
+ def _mark_updates_security(self, updates):
+ """ Mark a queryset of PackageUpdates as security updates.
+ Handles IntegrityError by deleting duplicates.
+ """
+ try:
+ updates.update(security=True)
+ except IntegrityError:
+ # fall back to individual saves to handle duplicates
+ for update in updates:
+ update.security = True
+ try:
+ update.save()
+ except IntegrityError as e:
+ error_message(text=e)
+ update.delete()
def fetch_osv_dev_data(self):
osv_dev_url = f'https://api.osv.dev/v1/vulns/{self.name}'
@@ -104,15 +102,19 @@ def fetch_osv_dev_data(self):
self.parse_osv_dev_data(osv_dev_json)
def parse_osv_dev_data(self, osv_dev_json):
+ from django.db.models import Q
name = osv_dev_json.get('id')
if name != self.name:
error_message(text=f'Erratum name mismatch - {self.name} != {name}')
return
related = osv_dev_json.get('related')
if related:
+ cves = []
for vuln in related:
if vuln.startswith('CVE'):
- self.add_cve(vuln)
+ cves.append(vuln)
+ for cve_id in cves:
+ self.add_cve(cve_id)
affected = osv_dev_json.get('affected')
if not affected:
return
@@ -129,30 +131,29 @@ def parse_osv_dev_data(self, osv_dev_json):
for match in matching_packages:
fixed_packages.add(match)
affected_versions = package.get('versions')
- if not affected_versions:
+ if not affected_versions or not fixed_packages:
continue
- for package in fixed_packages:
+ for fp in fixed_packages:
+ q = Q()
for version in affected_versions:
epoch, ver, rel = find_evr(version)
- matching_packages = get_matching_packages(
- name=package.name,
- epoch=epoch,
- version=ver,
- release=rel,
- arch=package.arch,
- p_type=package.packagetype,
- )
- for match in matching_packages:
- affected_packages.add(match)
+ q |= Q(epoch=epoch, version=ver, release=rel)
+ matching_packages = get_matching_packages_q(
+ name=fp.name,
+ q=q,
+ arch=fp.arch,
+ p_type=fp.packagetype,
+ )
+ affected_packages.update(matching_packages)
self.add_affected_packages(affected_packages)
def add_fixed_packages(self, packages):
- for package in packages:
- self.fixed_packages.add(package)
+ if packages:
+ self.fixed_packages.add(*packages)
def add_affected_packages(self, packages):
- for package in packages:
- self.affected_packages.add(package)
+ if packages:
+ self.affected_packages.add(*packages)
def add_cve(self, cve_id):
""" Add a CVE to an Erratum object
diff --git a/errata/sources/repos/yum.py b/errata/sources/repos/yum.py
index 992830c4..72813436 100644
--- a/errata/sources/repos/yum.py
+++ b/errata/sources/repos/yum.py
@@ -136,6 +136,8 @@ def add_updateinfo_erratum_references(e, update, ref_type, urls):
for url in urls:
e.add_reference(ref_type, url)
references = update.find('references')
+ if references is None:
+ return
for reference in references.findall('reference'):
if reference.attrib.get('type') == 'cve':
cve_id = reference.attrib.get('id')
diff --git a/etc/systemd/system/patchman-celery-beat.service b/etc/systemd/system/patchman-celery-beat.service
index c9bce722..79a5bc5c 100644
--- a/etc/systemd/system/patchman-celery-beat.service
+++ b/etc/systemd/system/patchman-celery-beat.service
@@ -5,6 +5,8 @@ After=network-online.target
[Service]
Type=simple
+Restart=on-failure
+RestartSec=10
User=patchman
Group=patchman
Environment="REDIS_HOST=127.0.0.1"
diff --git a/etc/systemd/system/patchman-celery-worker@.service b/etc/systemd/system/patchman-celery-worker@.service
index b2d6f6b7..7e10bd85 100644
--- a/etc/systemd/system/patchman-celery-worker@.service
+++ b/etc/systemd/system/patchman-celery-worker@.service
@@ -5,6 +5,8 @@ After=network-online.target
[Service]
Type=simple
+Restart=on-failure
+RestartSec=10
User=patchman
Group=patchman
Environment="REDIS_HOST=127.0.0.1"
@@ -19,6 +21,7 @@ ExecStart=/usr/bin/celery \
--task-events \
--pool ${CELERY_POOL_TYPE} \
--concurrency ${CELERY_CONCURRENCY} \
+ --loglevel info \
--hostname patchman-celery-worker%i@%%h
[Install]
diff --git a/hosts/models.py b/hosts/models.py
index 38c33ff1..b682d301 100644
--- a/hosts/models.py
+++ b/hosts/models.py
@@ -15,6 +15,8 @@
# You should have received a copy of the GNU General Public License
# along with Patchman. If not, see
+import re
+
from django.db import models
from django.db.models import Q
from django.urls import reverse
@@ -206,14 +208,11 @@ def find_updates(self):
errata_ids = set()
- if self.host_repos_only:
- update_ids = self.find_host_repo_updates(host_packages, repo_packages, errata_ids)
- else:
- update_ids = self.find_osrelease_repo_updates(host_packages, repo_packages, errata_ids)
+ update_ids = self.find_repo_updates(host_packages, repo_packages, errata_ids)
kernel_update_ids = self.find_kernel_updates(kernel_packages, repo_packages)
for ku_id in kernel_update_ids:
- update_ids.append(ku_id)
+ update_ids.add(ku_id)
for update in self.updates.all():
if update.id not in update_ids:
@@ -223,9 +222,9 @@ def find_updates(self):
if erratum.id not in errata_ids:
self.errata.remove(erratum)
- def find_host_repo_updates(self, host_packages, repo_packages, errata_ids):
+ def find_repo_updates(self, host_packages, repo_packages, errata_ids):
- update_ids = []
+ update_ids = set()
hostrepos_q = Q(repo__mirror__enabled=True,
repo__mirror__refresh=True,
repo__mirror__repo__enabled=True,
@@ -280,45 +279,7 @@ def find_host_repo_updates(self, host_packages, repo_packages, errata_ids):
if highest_package != package:
uid = self.process_update(package, highest_package)
if uid is not None:
- update_ids.append(uid)
- return update_ids
-
- def find_osrelease_repo_updates(self, host_packages, repo_packages, errata_ids):
-
- update_ids = []
- for package in host_packages:
- highest_package = package
-
- # find the packages that are potential updates
- pu_q = Q(name=package.name,
- arch=package.arch,
- packagetype=package.packagetype)
- potential_updates = repo_packages.filter(pu_q)
- for pu in potential_updates:
- pu_is_module_package = False
- pu_in_enabled_modules = False
- if pu.module_set.exists():
- pu_is_module_package = True
- for module in pu.module_set.all():
- if module in self.modules.all():
- pu_in_enabled_modules = True
- if pu_is_module_package:
- if not pu_in_enabled_modules:
- continue
- if package.compare_version(pu) == -1:
- # package updates that are fixed by erratum (may already be superceded by another update)
- errata = pu.provides_fix_in_erratum.all()
- if errata:
- for erratum in errata:
- self.errata.add(erratum)
- errata_ids.add(erratum.id)
- if highest_package.compare_version(pu) == -1:
- highest_package = pu
-
- if highest_package != package:
- uid = self.process_update(package, highest_package)
- if uid is not None:
- update_ids.append(uid)
+ update_ids.add(uid)
return update_ids
def check_if_reboot_required(self, host_highest):
@@ -350,7 +311,7 @@ def check_if_reboot_required(self, host_highest):
else:
self.reboot_required = False
- def _get_deb_kernel_flavour(self, pkg_name):
+ def get_deb_kernel_flavour(self, pkg_name):
"""Extract the flavour suffix from a DEB kernel package name.
e.g. 'linux-image-6.8.0-51-generic' → 'generic'
@@ -359,7 +320,7 @@ def _get_deb_kernel_flavour(self, pkg_name):
'linux-modules-extra-6.8.0-51-generic' → 'generic'
Returns None if the flavour cannot be determined.
"""
- for prefix in self._deb_kernel_prefixes:
+ for prefix in self.deb_kernel_prefixes:
if pkg_name.startswith(prefix):
# strip prefix, then split version from flavour
# e.g. '6.8.0-51-generic' or '6.1.0-28-cloud-amd64'
@@ -374,7 +335,7 @@ def _get_deb_kernel_flavour(self, pkg_name):
return None
return None
- def _get_running_kernel_flavour(self):
+ def get_running_kernel_flavour(self):
"""Extract the flavour from the running kernel string.
e.g. '6.8.0-51-generic' → 'generic'
@@ -391,7 +352,7 @@ def _get_running_kernel_flavour(self):
return None
# longest prefixes first to avoid linux-modules- matching linux-modules-extra-
- _deb_kernel_prefixes = [
+ deb_kernel_prefixes = [
'linux-image-unsigned-',
'linux-modules-extra-',
'linux-cloud-tools-',
@@ -404,12 +365,27 @@ 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 = []
+ update_ids = set()
self.reboot_required = False
- # build hostrepos for priority filtering (same as find_host_repo_updates)
+ # build hostrepos for priority filtering (same as find_repo_updates)
hostrepos = None
if self.host_repos_only:
hostrepos_q = Q(repo__mirror__enabled=True,
@@ -423,16 +399,16 @@ def find_kernel_updates(self, kernel_packages, repo_packages):
rpm_kernels = kernel_packages.filter(packagetype='R')
arch_kernels = kernel_packages.filter(packagetype='A')
- update_ids.extend(self._find_rpm_kernel_updates(rpm_kernels, repo_packages, hostrepos))
- update_ids.extend(self._find_deb_kernel_updates(deb_kernels, repo_packages, hostrepos))
- update_ids.extend(self._find_arch_kernel_updates(arch_kernels, repo_packages, hostrepos))
+ update_ids.update(self.find_rpm_kernel_updates(rpm_kernels, repo_packages, hostrepos))
+ update_ids.update(self.find_deb_kernel_updates(deb_kernels, repo_packages, hostrepos))
+ update_ids.update(self.find_arch_kernel_updates(arch_kernels, repo_packages, hostrepos))
self.save(update_fields=['reboot_required'])
return update_ids
- def _find_rpm_kernel_updates(self, kernel_packages, repo_packages, hostrepos):
+ def find_rpm_kernel_updates(self, kernel_packages, repo_packages, hostrepos):
- update_ids = []
+ update_ids = set()
# parse running kernel version for comparison
parts = self.kernel.split('-')
@@ -498,7 +474,7 @@ def _find_rpm_kernel_updates(self, kernel_packages, repo_packages, hostrepos):
if base_package and base_package.compare_version(repo_highest) == -1:
uid = self.process_update(base_package, repo_highest)
if uid is not None:
- update_ids.append(uid)
+ update_ids.add(uid)
# reboot check only on primary kernel packages
if host_highest and package.name.name in (
@@ -511,9 +487,9 @@ def _find_rpm_kernel_updates(self, kernel_packages, repo_packages, hostrepos):
return update_ids
- def _find_arch_kernel_updates(self, kernel_packages, repo_packages, hostrepos):
+ def find_arch_kernel_updates(self, kernel_packages, repo_packages, hostrepos):
- update_ids = []
+ update_ids = set()
for package in kernel_packages:
pu_q = Q(name=package.name)
@@ -540,7 +516,7 @@ def _find_arch_kernel_updates(self, kernel_packages, repo_packages, hostrepos):
if package.compare_version(repo_highest) == -1:
uid = self.process_update(package, repo_highest)
if uid is not None:
- update_ids.append(uid)
+ update_ids.add(uid)
# reboot check for main kernel packages (not -headers)
# Arch uname -r format varies by flavour:
@@ -562,10 +538,10 @@ def _find_arch_kernel_updates(self, kernel_packages, repo_packages, hostrepos):
return update_ids
- def _find_deb_kernel_updates(self, kernel_packages, repo_packages, hostrepos):
+ def find_deb_kernel_updates(self, kernel_packages, repo_packages, hostrepos):
- update_ids = []
- running_flavour = self._get_running_kernel_flavour()
+ update_ids = set()
+ running_flavour = self.get_running_kernel_flavour()
# find the linux-image package matching the running kernel
running_kernel_pkg = None
@@ -585,7 +561,7 @@ def _find_deb_kernel_updates(self, kernel_packages, repo_packages, hostrepos):
processed_prefixes = set()
for package in kernel_packages:
pkg_name = package.name.name
- flavour = self._get_deb_kernel_flavour(pkg_name)
+ flavour = self.get_deb_kernel_flavour(pkg_name)
# if we know the running flavour, only process matching packages
# if we don't (unflavoured kernel), process all kernel packages
@@ -594,7 +570,7 @@ def _find_deb_kernel_updates(self, kernel_packages, repo_packages, hostrepos):
# determine the prefix (e.g. 'linux-image-')
prefix = None
- for p in self._deb_kernel_prefixes:
+ for p in self.deb_kernel_prefixes:
if pkg_name.startswith(p):
prefix = p
break
@@ -602,6 +578,15 @@ 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 vs HWE); meta-packages like linux-image-generic
+ # yield None, so fall back to the running kernel's series
+ installed_series = self.get_deb_kernel_series(pkg_name)
+ if installed_series is None and self.kernel:
+ m = re.match(r'(\d+\.\d+)', self.kernel)
+ if m:
+ installed_series = m.group(1)
+
# build endswith filter for flavoured kernels
name_filter = Q(
name__name__startswith=prefix,
@@ -611,8 +596,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:
@@ -638,14 +628,14 @@ def _find_deb_kernel_updates(self, kernel_packages, repo_packages, hostrepos):
if base_package.compare_version(repo_highest) == -1:
uid = self.process_update(base_package, repo_highest)
if uid is not None:
- update_ids.append(uid)
+ update_ids.add(uid)
# reboot check: see if a newer linux-image is installed but not running
# use compare_version (DEB semantics) instead of labelCompare
if running_kernel_pkg:
for package in kernel_packages:
if package.name.name.startswith('linux-image-'):
- flavour = self._get_deb_kernel_flavour(package.name.name)
+ flavour = self.get_deb_kernel_flavour(package.name.name)
if running_flavour is None or flavour == running_flavour:
if running_kernel_pkg.compare_version(package) == -1:
self.reboot_required = True
diff --git a/hosts/tables.py b/hosts/tables.py
index b34b7d61..296b8df3 100644
--- a/hosts/tables.py
+++ b/hosts/tables.py
@@ -22,12 +22,14 @@
HOSTNAME_TEMPLATE = '{{ record.hostname }}'
SEC_UPDATES_TEMPLATE = (
'{% with count=record.get_num_security_updates %}'
- '{% if count != 0 %}{{ count }}{% else %}{% endif %}'
+ '{% if count != 0 %}'
+ '{{ count }}{% endif %}'
'{% endwith %}'
)
BUG_UPDATES_TEMPLATE = (
'{% with count=record.get_num_bugfix_updates %}'
- '{% if count != 0 %}{{ count }}{% else %}{% endif %}'
+ '{% if count != 0 %}'
+ '{{ count }}{% endif %}'
'{% endwith %}'
)
AFFECTED_ERRATA_TEMPLATE = (
diff --git a/hosts/tests/test_models.py b/hosts/tests/test_models.py
index 86a846bf..72198ca6 100644
--- a/hosts/tests/test_models.py
+++ b/hosts/tests/test_models.py
@@ -544,35 +544,121 @@ def test_deb_latest_not_installed(self):
self.assertEqual(update.newpackage, self.img_53)
def test_deb_flavour_extraction(self):
- """Test _get_deb_kernel_flavour helper."""
+ """Test get_deb_kernel_flavour helper."""
host = self._create_host('6.8.0-51-generic', [self.img_51])
self.assertEqual(
- host._get_deb_kernel_flavour('linux-image-6.8.0-51-generic'),
+ host.get_deb_kernel_flavour('linux-image-6.8.0-51-generic'),
'generic'
)
self.assertEqual(
- host._get_deb_kernel_flavour('linux-modules-extra-6.8.0-51-lowlatency'),
+ host.get_deb_kernel_flavour('linux-modules-extra-6.8.0-51-lowlatency'),
'lowlatency'
)
self.assertEqual(
- host._get_deb_kernel_flavour('linux-image-6.1.0-28-cloud-amd64'),
+ host.get_deb_kernel_flavour('linux-image-6.1.0-28-cloud-amd64'),
'cloud-amd64'
)
self.assertEqual(
- host._get_deb_kernel_flavour('linux-image-unsigned-6.8.0-51-generic'),
+ host.get_deb_kernel_flavour('linux-image-unsigned-6.8.0-51-generic'),
'generic'
)
def test_deb_running_kernel_flavour(self):
- """Test _get_running_kernel_flavour helper."""
+ """Test get_running_kernel_flavour helper."""
host = self._create_host('6.8.0-51-generic', [self.img_51])
- self.assertEqual(host._get_running_kernel_flavour(), 'generic')
+ self.assertEqual(host.get_running_kernel_flavour(), 'generic')
host.kernel = '6.1.0-28-cloud-amd64'
- self.assertEqual(host._get_running_kernel_flavour(), 'cloud-amd64')
+ self.assertEqual(host.get_running_kernel_flavour(), 'cloud-amd64')
host.kernel = '5.14.0-503.el9'
- self.assertIsNone(host._get_running_kernel_flavour())
+ 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(
@@ -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]
@@ -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]
@@ -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,
)
@@ -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."""
diff --git a/hosts/views.py b/hosts/views.py
index f940c3a4..b4e002dd 100644
--- a/hosts/views.py
+++ b/hosts/views.py
@@ -90,6 +90,9 @@ def host_list(request):
if 'package' in request.GET:
hosts = hosts.filter(packages__name__name=request.GET['package'])
+ if 'update_id' in request.GET:
+ hosts = hosts.filter(updates=request.GET['update_id'])
+
if 'repo_id' in request.GET:
hosts = hosts.filter(repos=request.GET['repo_id'])
diff --git a/operatingsystems/views.py b/operatingsystems/views.py
index 1a6b3fda..40824d0d 100644
--- a/operatingsystems/views.py
+++ b/operatingsystems/views.py
@@ -269,7 +269,7 @@ def osvariant_bulk_action(request):
action = request.POST.get('action', '')
select_all_filtered = request.POST.get('select_all_filtered') == '1'
- filter_params = request.POST.get('filter_params', '')
+ filter_params = sanitize_filter_params(request.POST.get('filter_params', ''))
if not action:
messages.warning(request, 'Please select an action')
@@ -310,7 +310,7 @@ def osrelease_bulk_action(request):
action = request.POST.get('action', '')
select_all_filtered = request.POST.get('select_all_filtered') == '1'
- filter_params = request.POST.get('filter_params', '')
+ filter_params = sanitize_filter_params(request.POST.get('filter_params', ''))
if not action:
messages.warning(request, 'Please select an action')
diff --git a/packages/tables.py b/packages/tables.py
index 633c79a2..6b41ac03 100644
--- a/packages/tables.py
+++ b/packages/tables.py
@@ -14,7 +14,7 @@
import django_tables2 as tables
-from packages.models import Package, PackageName
+from packages.models import Package, PackageName, PackageUpdate
from util.tables import BaseTable
PACKAGE_NAME_TEMPLATE = '{{ record }}'
@@ -24,15 +24,15 @@
)
PACKAGE_HOSTS_TEMPLATE = (
''
- 'Installed on {{ record.host_set.count }} Hosts'
+ 'Installed on {{ record.host_count }} Hosts'
)
AFFECTED_TEMPLATE = (
''
- 'Affected by {{ record.affected_by_erratum.count }} Errata'
+ 'Affected by {{ record.affected_count }} Errata'
)
FIXED_TEMPLATE = (
''
- 'Provides fix in {{ record.provides_fix_in_erratum.count }} Errata'
+ 'Provides fix in {{ record.fixed_count }} Errata'
)
@@ -71,25 +71,25 @@ class PackageTable(BaseTable):
package_repos = tables.TemplateColumn(
PACKAGE_REPOS_TEMPLATE,
verbose_name='Repositories',
- orderable=False,
+ order_by='repo_count',
attrs={'th': {'class': 'col-sm-auto'}, 'td': {'class': 'col-sm-auto'}},
)
package_hosts = tables.TemplateColumn(
PACKAGE_HOSTS_TEMPLATE,
verbose_name='Hosts',
- orderable=False,
+ order_by='host_count',
attrs={'th': {'class': 'col-sm-auto'}, 'td': {'class': 'col-sm-auto'}},
)
affected = tables.TemplateColumn(
AFFECTED_TEMPLATE,
verbose_name='Affected',
- orderable=False,
+ order_by='affected_count',
attrs={'th': {'class': 'col-sm-1'}, 'td': {'class': 'col-sm-1'}},
)
fixed = tables.TemplateColumn(
FIXED_TEMPLATE,
verbose_name='Fixed',
- orderable=False,
+ order_by='fixed_count',
attrs={'th': {'class': 'col-sm-1'}, 'td': {'class': 'col-sm-1'}},
)
@@ -101,20 +101,103 @@ class Meta(BaseTable.Meta):
)
+PACKAGE_NAME_HOSTS_TEMPLATE = (
+ ''
+ '{{ record.host_count }}'
+)
+
+
class PackageNameTable(BaseTable):
packagename_name = tables.TemplateColumn(
PACKAGE_NAME_TEMPLATE,
order_by='name',
verbose_name='Package',
- attrs={'th': {'class': 'col-sm-6'}, 'td': {'class': 'col-sm-6'}},
+ attrs={'th': {'class': 'col-sm-5'}, 'td': {'class': 'col-sm-5'}},
)
versions = tables.TemplateColumn(
'{{ record.package_set.count }}',
orderable=False,
- verbose_name='Versions available',
- attrs={'th': {'class': 'col-sm-6'}, 'td': {'class': 'col-sm-6'}},
+ verbose_name='Versions',
+ attrs={'th': {'class': 'col-sm-1'}, 'td': {'class': 'col-sm-1'}},
+ )
+ hosts = tables.TemplateColumn(
+ PACKAGE_NAME_HOSTS_TEMPLATE,
+ verbose_name='Hosts',
+ order_by='host_count',
+ attrs={'th': {'class': 'col-sm-1'}, 'td': {'class': 'col-sm-1'}},
)
class Meta(BaseTable.Meta):
model = PackageName
- fields = ('packagename_name', 'versions')
+ fields = ('packagename_name', 'versions', 'hosts')
+
+
+UPDATE_OLD_TEMPLATE = (
+ ''
+ '{{ record.oldpackage }}'
+)
+UPDATE_NEW_TEMPLATE = (
+ ''
+ '{{ record.newpackage }}'
+)
+UPDATE_HOSTS_TEMPLATE = (
+ ''
+ '{{ record.host_count }}'
+)
+UPDATE_AFFECTED_TEMPLATE = (
+ ''
+ '{{ record.affected_count }}'
+)
+UPDATE_FIXED_TEMPLATE = (
+ ''
+ '{{ record.fixed_count }}'
+)
+
+
+UPDATE_TYPE_TEMPLATE = (
+ '{% if record.security %}'
+ 'Security'
+ '{% else %}'
+ 'Bugfix'
+ '{% endif %}'
+)
+
+
+class PackageUpdateTable(BaseTable):
+ oldpackage = tables.TemplateColumn(
+ UPDATE_OLD_TEMPLATE,
+ verbose_name='Installed',
+ attrs={'th': {'class': 'col-sm-3'}, 'td': {'class': 'col-sm-3'}},
+ )
+ newpackage = tables.TemplateColumn(
+ UPDATE_NEW_TEMPLATE,
+ verbose_name='Available',
+ attrs={'th': {'class': 'col-sm-3'}, 'td': {'class': 'col-sm-3'}},
+ )
+ security = tables.TemplateColumn(
+ UPDATE_TYPE_TEMPLATE,
+ verbose_name='Type',
+ attrs={'th': {'class': 'col-sm-1'}, 'td': {'class': 'col-sm-1'}},
+ )
+ hosts = tables.TemplateColumn(
+ UPDATE_HOSTS_TEMPLATE,
+ verbose_name='Hosts',
+ order_by='host_count',
+ attrs={'th': {'class': 'col-sm-1'}, 'td': {'class': 'col-sm-1'}},
+ )
+ affected = tables.TemplateColumn(
+ UPDATE_AFFECTED_TEMPLATE,
+ verbose_name='Affected by Errata',
+ order_by='affected_count',
+ attrs={'th': {'class': 'col-sm-1'}, 'td': {'class': 'col-sm-1'}},
+ )
+ fixed = tables.TemplateColumn(
+ UPDATE_FIXED_TEMPLATE,
+ verbose_name='Fixed in Errata',
+ order_by='fixed_count',
+ attrs={'th': {'class': 'col-sm-1'}, 'td': {'class': 'col-sm-1'}},
+ )
+
+ class Meta(BaseTable.Meta):
+ model = PackageUpdate
+ fields = ('oldpackage', 'newpackage', 'security', 'hosts', 'affected', 'fixed')
diff --git a/packages/templates/packages/package_name_detail.html b/packages/templates/packages/package_name_detail.html
index 5cef00ec..2dfc8d29 100644
--- a/packages/templates/packages/package_name_detail.html
+++ b/packages/templates/packages/package_name_detail.html
@@ -1,4 +1,5 @@
{% extends "base.html" %}
+{% load django_tables2 %}
{% block page_title %}Package - {{ package }} {% endblock %}
@@ -9,32 +10,8 @@
{% block content %}
- {% if allversions %}
-
+ {% if table.rows %}
+ {% render_table table %}
{% else %}
No versions of this Package exist.
{% endif %}
diff --git a/packages/templates/packages/package_update_list.html b/packages/templates/packages/package_update_list.html
new file mode 100644
index 00000000..62c2557d
--- /dev/null
+++ b/packages/templates/packages/package_update_list.html
@@ -0,0 +1,7 @@
+{% extends "objectlist.html" %}
+
+{% block page_title %}Package Updates{% endblock %}
+
+{% block breadcrumbs %} {{ block.super }}
Package Updates{% endblock %}
+
+{% block content_title %} Package Updates {% endblock %}
diff --git a/packages/tests/test_views.py b/packages/tests/test_views.py
new file mode 100644
index 00000000..9fc86abd
--- /dev/null
+++ b/packages/tests/test_views.py
@@ -0,0 +1,72 @@
+from django.contrib.auth.models import User
+from django.test import TestCase, override_settings
+from django.urls import reverse
+
+from arch.models import PackageArchitecture
+from packages.models import Package, PackageName, PackageUpdate
+
+
+@override_settings(
+ CELERY_TASK_ALWAYS_EAGER=True,
+ CACHES={'default': {'BACKEND': 'django.core.cache.backends.locmem.LocMemCache'}}
+)
+class PackageUpdateViewTests(TestCase):
+
+ def setUp(self):
+ self.user = User.objects.create_user(
+ username='testuser', password='testpass'
+ )
+ self.client.login(username='testuser', password='testpass')
+ self.arch = PackageArchitecture.objects.create(name='x86_64')
+ self.name = PackageName.objects.create(name='openssl')
+ self.old = Package.objects.create(
+ name=self.name, arch=self.arch, epoch='',
+ version='1.1.1', release='1', packagetype='R',
+ )
+ self.new = Package.objects.create(
+ name=self.name, arch=self.arch, epoch='',
+ version='1.1.2', release='1', packagetype='R',
+ )
+ self.sec_update = PackageUpdate.objects.create(
+ oldpackage=self.old, newpackage=self.new, security=True,
+ )
+ self.bug_update = PackageUpdate.objects.create(
+ oldpackage=self.old, newpackage=self.new, security=False,
+ )
+
+ def test_update_list(self):
+ resp = self.client.get(reverse('packages:package_update_list'))
+ self.assertEqual(resp.status_code, 200)
+ self.assertContains(resp, 'openssl')
+
+ def test_update_list_filter_security(self):
+ resp = self.client.get(
+ reverse('packages:package_update_list'), {'security': 'true'}
+ )
+ self.assertEqual(resp.status_code, 200)
+ self.assertContains(resp, 'Security')
+
+ def test_update_list_filter_bugfix(self):
+ resp = self.client.get(
+ reverse('packages:package_update_list'), {'security': 'false'}
+ )
+ self.assertEqual(resp.status_code, 200)
+ self.assertContains(resp, 'Bugfix')
+
+ def test_update_list_search(self):
+ resp = self.client.get(
+ reverse('packages:package_update_list'), {'search': 'openssl'}
+ )
+ self.assertEqual(resp.status_code, 200)
+ self.assertContains(resp, 'openssl')
+
+ def test_update_list_search_no_results(self):
+ resp = self.client.get(
+ reverse('packages:package_update_list'), {'search': 'nonexistent'}
+ )
+ self.assertEqual(resp.status_code, 200)
+
+ def test_update_list_requires_login(self):
+ self.client.logout()
+ resp = self.client.get(reverse('packages:package_update_list'))
+ self.assertEqual(resp.status_code, 302)
diff --git a/packages/urls.py b/packages/urls.py
index bc027807..75273450 100644
--- a/packages/urls.py
+++ b/packages/urls.py
@@ -27,4 +27,5 @@
path('name/
/', views.package_name_detail, name='package_name_detail'),
path('id/', views.package_list, name='package_list'),
path('id//', views.package_detail, name='package_detail'),
+ path('updates/', views.package_update_list, name='package_update_list'),
]
diff --git a/packages/utils.py b/packages/utils.py
index c7f86b4c..54d56273 100644
--- a/packages/utils.py
+++ b/packages/utils.py
@@ -277,6 +277,22 @@ def get_matching_packages(name, epoch, version, release, p_type, arch=None):
return packages
+def get_matching_packages_q(name, q, p_type, arch=None):
+ """ Get packages matching a compound Q filter for batch version lookups.
+ Returns the matching packages or an empty queryset.
+ """
+ try:
+ package_name = PackageName.objects.get(name=name)
+ except PackageName.DoesNotExist:
+ return Package.objects.none()
+ base_filter = {'name': package_name, 'packagetype': p_type}
+ if arch:
+ if not isinstance(arch, PackageArchitecture):
+ arch, _ = PackageArchitecture.objects.get_or_create(name=arch)
+ base_filter['arch'] = arch
+ return Package.objects.filter(q, **base_filter)
+
+
def clean_packageupdates():
""" Removes PackageUpdate objects that are no longer linked to any hosts
"""
diff --git a/packages/views.py b/packages/views.py
index 9f75b415..0f2aaecb 100644
--- a/packages/views.py
+++ b/packages/views.py
@@ -16,7 +16,7 @@
# along with Patchman. If not, see
from django.contrib.auth.decorators import login_required
-from django.db.models import Q
+from django.db.models import Count, Q
from django.shortcuts import get_object_or_404, render
from django_tables2 import RequestConfig
from rest_framework import viewsets
@@ -26,7 +26,7 @@
from packages.serializers import (
PackageNameSerializer, PackageSerializer, PackageUpdateSerializer,
)
-from packages.tables import PackageNameTable, PackageTable
+from packages.tables import PackageNameTable, PackageTable, PackageUpdateTable
from util.filterspecs import Filter, FilterBar
@@ -109,6 +109,13 @@ def package_list(request):
filter_list.append(Filter(request, 'Architecture', 'arch_id', PackageArchitecture.objects.all()))
filter_bar = FilterBar(request, filter_list)
+ packages = packages.annotate(
+ host_count=Count('host', distinct=True),
+ repo_count=Count('mirror__repo', distinct=True),
+ affected_count=Count('affected_by_erratum', distinct=True),
+ fixed_count=Count('provides_fix_in_erratum', distinct=True),
+ )
+
table = PackageTable(packages)
RequestConfig(request, paginate={'per_page': 50}).configure(table)
@@ -144,6 +151,8 @@ def package_name_list(request):
filter_list.append(Filter(request, 'Architecture', 'arch_id', PackageArchitecture.objects.all()))
filter_bar = FilterBar(request, filter_list)
+ packages = packages.annotate(host_count=Count('package__host', distinct=True))
+
table = PackageNameTable(packages)
RequestConfig(request, paginate={'per_page': 50}).configure(table)
@@ -165,11 +174,62 @@ def package_detail(request, package_id):
@login_required
def package_name_detail(request, packagename):
package = get_object_or_404(PackageName, name=packagename)
- allversions = Package.objects.select_related('name', 'arch').filter(name=package.id)
+ allversions = Package.objects.select_related(
+ 'name', 'arch',
+ ).filter(name=package.id).annotate(
+ host_count=Count('host', distinct=True),
+ repo_count=Count('mirror__repo', distinct=True),
+ affected_count=Count('affected_by_erratum', distinct=True),
+ fixed_count=Count('provides_fix_in_erratum', distinct=True),
+ )
+ table = PackageTable(allversions)
+ RequestConfig(request, paginate={'per_page': 50}).configure(table)
return render(request,
'packages/package_name_detail.html',
{'package': package,
- 'allversions': allversions})
+ 'table': table})
+
+
+@login_required
+def package_update_list(request):
+ updates = PackageUpdate.objects.select_related(
+ 'oldpackage__name', 'oldpackage__arch',
+ 'newpackage__name', 'newpackage__arch',
+ ).annotate(
+ host_count=Count('host', distinct=True),
+ affected_count=Count('oldpackage__affected_by_erratum', distinct=True),
+ fixed_count=Count('newpackage__provides_fix_in_erratum', distinct=True),
+ )
+
+ if 'security' in request.GET:
+ security = request.GET['security'] == 'true'
+ updates = updates.filter(security=security)
+ if 'host_id' in request.GET:
+ updates = updates.filter(host=request.GET['host_id'])
+ if 'search' in request.GET:
+ terms = request.GET['search'].lower()
+ query = Q()
+ for term in terms.split(' '):
+ q = (Q(oldpackage__name__name__icontains=term) |
+ Q(newpackage__name__name__icontains=term))
+ query = query & q
+ updates = updates.filter(query)
+ else:
+ terms = ''
+
+ filter_list = []
+ filter_list.append(Filter(request, 'Type', 'security',
+ {'true': 'Security', 'false': 'Bugfix'}))
+ filter_bar = FilterBar(request, filter_list)
+
+ table = PackageUpdateTable(updates.distinct())
+ RequestConfig(request, paginate={'per_page': 50}).configure(table)
+
+ return render(request,
+ 'packages/package_update_list.html',
+ {'table': table,
+ 'filter_bar': filter_bar,
+ 'terms': terms})
class PackageNameViewSet(viewsets.ModelViewSet):
diff --git a/patchman/celery.py b/patchman/celery.py
index c47f994d..95c0c4fe 100644
--- a/patchman/celery.py
+++ b/patchman/celery.py
@@ -17,10 +17,24 @@
import os
from celery import Celery
+from celery.signals import task_prerun
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'patchman.settings') # noqa
+from django import db # noqa
from django.conf import settings # noqa
app = Celery('patchman')
app.config_from_object('django.conf:settings', namespace='CELERY')
app.autodiscover_tasks()
+
+
+@task_prerun.connect
+def close_stale_connections(**kwargs):
+ """Close stale DB connections before each task.
+
+ Django does this automatically for HTTP requests but not for Celery
+ tasks. Without this, long-lived workers hit 'server has gone away'
+ (MySQL) or 'server closed the connection unexpectedly' (PostgreSQL)
+ when the DB server drops idle connections.
+ """
+ db.close_old_connections()
diff --git a/patchman/receivers.py b/patchman/receivers.py
index 9393d891..99a544c9 100644
--- a/patchman/receivers.py
+++ b/patchman/receivers.py
@@ -15,9 +15,12 @@
# You should have received a copy of the GNU General Public License
# along with Patchman. If not, see
+import sys
+
from colorama import Fore, Style, init
from django.conf import settings
from django.dispatch import receiver
+from tqdm import tqdm
from tqdm.contrib.logging import logging_redirect_tqdm
from patchman.signals import (
@@ -54,9 +57,7 @@ def print_info_message(**kwargs):
"""
text = str(kwargs.get('text'))
if not get_quiet_mode():
- with logging_redirect_tqdm(loggers=[logger]):
- for line in text.splitlines():
- logger.info(Style.RESET_ALL + Fore.RESET + line)
+ tqdm.write(text, file=sys.stdout)
@receiver(warning_message_s)
diff --git a/patchman/sqlite3/base.py b/patchman/sqlite3/base.py
index c7ba0c6f..8e1ea030 100644
--- a/patchman/sqlite3/base.py
+++ b/patchman/sqlite3/base.py
@@ -20,6 +20,11 @@
class DatabaseWrapper(base.DatabaseWrapper):
+ def get_new_connection(self, conn_params):
+ conn = super().get_new_connection(conn_params)
+ conn.execute('PRAGMA journal_mode=WAL')
+ return conn
+
def _start_transaction_under_autocommit(self):
# Acquire a write lock immediately for transactions
self.cursor().execute('BEGIN IMMEDIATE')
diff --git a/reports/models.py b/reports/models.py
index a0f2bbef..578e2c46 100644
--- a/reports/models.py
+++ b/reports/models.py
@@ -46,7 +46,7 @@ class Report(models.Model):
reboot = models.TextField(null=True, blank=True)
class Meta:
- verbose_name_plural = 'Report'
+ verbose_name = 'Report'
verbose_name_plural = 'Reports'
ordering = ['-created']
diff --git a/reports/tests/test_utils.py b/reports/tests/test_utils.py
index b139669b..0f99c83a 100644
--- a/reports/tests/test_utils.py
+++ b/reports/tests/test_utils.py
@@ -227,6 +227,35 @@ def test_process_repo_json_rpm(self):
# RPM priority is negated
self.assertEqual(priority, -99)
+ def test_process_repo_no_rename_on_existing_mirror(self):
+ """Repo found by mirror URL should keep its original name.
+
+ Repo names are set at creation and should not be overwritten by
+ client reports — the admin may have renamed the repo in the UI.
+ """
+ repo, _ = process_repo(
+ r_type=Repository.RPM,
+ r_name='Zabbix Official Repository - x86_64 x86_64',
+ r_id='zabbix',
+ r_priority=-99,
+ urls=['https://repo.zabbix.com/zabbix/6.0/rhel/9/x86_64'],
+ arch='x86_64',
+ )
+ self.assertEqual(repo.name, 'Zabbix Official Repository - x86_64 x86_64')
+
+ # updated client reports same URL with corrected name
+ repo2, _ = process_repo(
+ r_type=Repository.RPM,
+ r_name='Zabbix Official Repository - x86_64',
+ r_id='zabbix',
+ r_priority=-99,
+ urls=['https://repo.zabbix.com/zabbix/6.0/rhel/9/x86_64'],
+ arch='x86_64',
+ )
+ # should return the same repo, name unchanged
+ self.assertEqual(repo2.id, repo.id)
+ self.assertEqual(repo2.name, 'Zabbix Official Repository - x86_64 x86_64')
+
@override_settings(
CELERY_TASK_ALWAYS_EAGER=True,
diff --git a/reports/utils.py b/reports/utils.py
index 9b2a4196..4fea67d3 100644
--- a/reports/utils.py
+++ b/reports/utils.py
@@ -258,9 +258,6 @@ def process_repo(r_type, r_name, r_id, r_priority, urls, arch):
if r_id and repository.repo_id != r_id:
repository.repo_id = r_id
- if r_name and repository.name != r_name:
- repository.name = r_name
-
for url in unknown:
Mirror.objects.create(repo=repository, url=url.rstrip('/'))
diff --git a/reports/views.py b/reports/views.py
index e5d1d438..2aab3e54 100644
--- a/reports/views.py
+++ b/reports/views.py
@@ -220,7 +220,7 @@ def report_bulk_action(request):
action = request.POST.get('action', '')
select_all_filtered = request.POST.get('select_all_filtered') == '1'
- filter_params = request.POST.get('filter_params', '')
+ filter_params = sanitize_filter_params(request.POST.get('filter_params', ''))
if not action:
messages.warning(request, 'Please select an action')
diff --git a/repos/repo_types/yum.py b/repos/repo_types/yum.py
index 1e96db39..24584fe9 100644
--- a/repos/repo_types/yum.py
+++ b/repos/repo_types/yum.py
@@ -68,6 +68,7 @@ def extract_module_metadata(data, url, repo):
modules_yaml = yaml.safe_load_all(extracted)
except yaml.YAMLError as e:
error_message(text=f'Error parsing modules.yaml: {e}')
+ return modules
mlen = len(re.findall(r'---', yaml.dump(extracted.decode())))
pbar_start.send(sender=None, ptext=f'Extracting {mlen} Modules ', plen=mlen)
diff --git a/repos/views.py b/repos/views.py
index 1a796c11..7a804a19 100644
--- a/repos/views.py
+++ b/repos/views.py
@@ -452,7 +452,7 @@ def repo_bulk_action(request):
action = request.POST.get('action', '')
select_all_filtered = request.POST.get('select_all_filtered') == '1'
- filter_params = request.POST.get('filter_params', '')
+ filter_params = sanitize_filter_params(request.POST.get('filter_params', ''))
if not action:
messages.warning(request, 'Please select an action')
@@ -531,7 +531,7 @@ def mirror_bulk_action(request):
action = request.POST.get('action', '')
select_all_filtered = request.POST.get('select_all_filtered') == '1'
- filter_params = request.POST.get('filter_params', '')
+ filter_params = sanitize_filter_params(request.POST.get('filter_params', ''))
if not action:
messages.warning(request, 'Please select an action')
diff --git a/requirements.txt b/requirements.txt
index 3f358230..62441847 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,11 +1,11 @@
-Django==4.2.29
+Django==4.2.30
django-taggit==4.0.0
django-extensions==3.2.3
django-bootstrap3==23.1
python-debian==1.0.1
defusedxml==0.7.1
PyYAML==6.0.2
-requests==2.32.4
+requests==2.33.0
colorama==0.4.6
djangorestframework==3.15.2
djangorestframework-api-key==3.0.0
diff --git a/security/models.py b/security/models.py
index 4a19f9ca..aab22882 100644
--- a/security/models.py
+++ b/security/models.py
@@ -179,8 +179,10 @@ def parse_osv_dev_cve_data(self, cve_json):
references = cve_json.get('references')
if references:
for reference in references:
- ref_type = reference.get('type').capitalize()
url = reference.get('url')
+ if not url:
+ continue
+ ref_type = reference.get('type').capitalize()
get_or_create_reference(ref_type, url)
scores = cve_json.get('severity')
if scores:
diff --git a/security/utils.py b/security/utils.py
index 745cf1f8..4033fc68 100644
--- a/security/utils.py
+++ b/security/utils.py
@@ -95,15 +95,19 @@ def fixup_reference(ref):
""" Fix up a Security Reference object to normalize the URL and type
"""
url = urlparse(ref.get('url'))
+ if not url.hostname:
+ return ref
ref_type = ref.get('ref_type')
- if 'lists' in url.hostname or 'lists' in url.path:
+ hostname = url.hostname
+ if 'lists' in hostname or 'lists' in url.path:
ref_type = 'Mailing List'
- if ref_type == 'bugzilla' or 'bug' in url.hostname or 'bugs' in url.path:
+ if ref_type == 'bugzilla' or 'bug' in hostname or 'bugs' in url.path:
ref_type = 'Bug Tracker'
url = fixup_ubuntu_usn_url(url)
- if url.hostname == 'ubuntu.com' and url.path.startswith('/security/notices/USN'):
+ hostname = url.hostname
+ if hostname == 'ubuntu.com' and url.path.startswith('/security/notices/USN'):
ref_type = 'USN'
- if 'launchpad.net' in url.hostname:
+ if 'launchpad.net' in hostname:
ref_type = 'Bug Tracker'
netloc = url.netloc.replace('bugs.', '')
bug = url.path.split('/')[-1]
diff --git a/util/templates/navbar.html b/util/templates/navbar.html
index 263be6a4..d07c40d5 100644
--- a/util/templates/navbar.html
+++ b/util/templates/navbar.html
@@ -12,7 +12,13 @@
Mirrors
- Packages
+
Errata