diff --git a/.github/workflows/lint-and-test.yml b/.github/workflows/lint-and-test.yml
index 20b221aa..436698be 100644
--- a/.github/workflows/lint-and-test.yml
+++ b/.github/workflows/lint-and-test.yml
@@ -28,6 +28,10 @@ jobs:
pip install flake8
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
flake8 . --count --max-line-length=120 --show-source --statistics
+ - name: Check isort
+ run: |
+ pip install isort
+ isort --check --profile=django .
- name: Set secret key
run: ./sbin/patchman-set-secret-key
- name: Test with django
diff --git a/arch/admin.py b/arch/admin.py
index 624a3720..5224711c 100644
--- a/arch/admin.py
+++ b/arch/admin.py
@@ -16,7 +16,8 @@
# along with Patchman. If not, see
from django.contrib import admin
-from arch.models import PackageArchitecture, MachineArchitecture
+
+from arch.models import MachineArchitecture, PackageArchitecture
admin.site.register(PackageArchitecture)
admin.site.register(MachineArchitecture)
diff --git a/arch/serializers.py b/arch/serializers.py
index 5319e796..a5765128 100644
--- a/arch/serializers.py
+++ b/arch/serializers.py
@@ -16,7 +16,7 @@
from rest_framework import serializers
-from arch.models import PackageArchitecture, MachineArchitecture
+from arch.models import MachineArchitecture, PackageArchitecture
class PackageArchitectureSerializer(serializers.HyperlinkedModelSerializer):
diff --git a/arch/utils.py b/arch/utils.py
index 1498fdec..3db6ac70 100644
--- a/arch/utils.py
+++ b/arch/utils.py
@@ -14,8 +14,8 @@
# You should have received a copy of the GNU General Public License
# along with Patchman. If not, see
-from arch.models import PackageArchitecture, MachineArchitecture
-from patchman.signals import info_message
+from arch.models import MachineArchitecture, PackageArchitecture
+from util.logging import info_message
def clean_package_architectures():
@@ -24,9 +24,9 @@ def clean_package_architectures():
parches = PackageArchitecture.objects.filter(package__isnull=True)
plen = parches.count()
if plen == 0:
- info_message.send(sender=None, text='No orphaned PackageArchitectures found.')
+ info_message(text='No orphaned PackageArchitectures found.')
else:
- info_message.send(sender=None, text=f'Removing {plen} orphaned PackageArchitectures')
+ info_message(text=f'Removing {plen} orphaned PackageArchitectures')
parches.delete()
@@ -39,9 +39,9 @@ def clean_machine_architectures():
)
mlen = marches.count()
if mlen == 0:
- info_message.send(sender=None, text='No orphaned MachineArchitectures found.')
+ info_message(text='No orphaned MachineArchitectures found.')
else:
- info_message.send(sender=None, text=f'Removing {mlen} orphaned MachineArchitectures')
+ info_message(text=f'Removing {mlen} orphaned MachineArchitectures')
marches.delete()
diff --git a/arch/views.py b/arch/views.py
index 56a2a150..21f6b7c7 100644
--- a/arch/views.py
+++ b/arch/views.py
@@ -16,9 +16,10 @@
from rest_framework import viewsets
-from arch.models import PackageArchitecture, MachineArchitecture
-from arch.serializers import PackageArchitectureSerializer, \
- MachineArchitectureSerializer
+from arch.models import MachineArchitecture, PackageArchitecture
+from arch.serializers import (
+ MachineArchitectureSerializer, PackageArchitectureSerializer,
+)
class PackageArchitectureViewSet(viewsets.ModelViewSet):
diff --git a/debian/control b/debian/control
index 67026269..7bfe320e 100644
--- a/debian/control
+++ b/debian/control
@@ -20,7 +20,7 @@ Depends: ${misc:Depends}, python3 (>= 3.11), python3-django (>= 4.2),
python3-requests, python3-colorama, python3-magic, python3-humanize,
python3-yaml, libapache2-mod-wsgi-py3, apache2, sqlite3,
celery, python3-celery, python3-django-celery-beat, redis-server,
- python3-redis, python3-git, python3-django-taggit
+ python3-redis, python3-git, python3-django-taggit, python3-zstandard
Suggests: python3-mysqldb, python3-psycopg2, python3-pymemcache, memcached
Description: Django-based patch status monitoring tool for linux systems.
.
diff --git a/debian/copyright b/debian/copyright
index ab051037..5202ff0e 100644
--- a/debian/copyright
+++ b/debian/copyright
@@ -6,7 +6,7 @@ Source: https://github.com/furlongm/patchman
Files: *
Copyright: 2011-2012 VPAC http://www.vpac.org
2013-2021 Marcus Furlong
-License: GPL-3.0
+License: GPL-3.0-only
This package is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; version 3 only.
diff --git a/debian/python3-patchman.cron.daily b/debian/python3-patchman.cron.daily
deleted file mode 100644
index d4752f75..00000000
--- a/debian/python3-patchman.cron.daily
+++ /dev/null
@@ -1,3 +0,0 @@
-#!/bin/sh
-
-/usr/bin/patchman -a -q
diff --git a/domains/admin.py b/domains/admin.py
index 2ef883e3..5cb0fee3 100644
--- a/domains/admin.py
+++ b/domains/admin.py
@@ -16,6 +16,7 @@
# along with Patchman. If not, see
from django.contrib import admin
+
from domains.models import Domain
admin.site.register(Domain)
diff --git a/errata/admin.py b/errata/admin.py
index 88190ff6..ac4b8a50 100644
--- a/errata/admin.py
+++ b/errata/admin.py
@@ -15,6 +15,7 @@
# along with Patchman. If not, see
from django.contrib import admin
+
from errata.models import Erratum
diff --git a/errata/migrations/0001_initial.py b/errata/migrations/0001_initial.py
index 85fe88b4..d02a7dc8 100644
--- a/errata/migrations/0001_initial.py
+++ b/errata/migrations/0001_initial.py
@@ -19,7 +19,7 @@ class Migration(migrations.Migration):
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('er_type', models.CharField(max_length=255)),
- ('url', models.URLField(max_length=2000)),
+ ('url', models.URLField(max_length=765)),
],
),
migrations.CreateModel(
diff --git a/errata/models.py b/errata/models.py
index b10daf4d..8c21bcfa 100644
--- a/errata/models.py
+++ b/errata/models.py
@@ -16,17 +16,16 @@
import json
-from django.db import models
+from django.db import IntegrityError, models
from django.urls import reverse
-from django.db import IntegrityError
+from errata.managers import ErratumManager
from packages.models import Package, PackageUpdate
from packages.utils import find_evr, get_matching_packages
-from errata.managers import ErratumManager
from security.models import CVE, Reference
from security.utils import get_or_create_cve, get_or_create_reference
-from patchman.signals import error_message
from util import get_url
+from util.logging import error_message
class Erratum(models.Model):
@@ -70,7 +69,7 @@ def scan_for_security_updates(self):
try:
affected_update.save()
except IntegrityError as e:
- error_message.send(sender=None, text=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()
@@ -84,7 +83,7 @@ def scan_for_security_updates(self):
try:
affected_update.save()
except IntegrityError as e:
- error_message.send(sender=None, text=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()
@@ -93,7 +92,7 @@ def fetch_osv_dev_data(self):
osv_dev_url = f'https://api.osv.dev/v1/vulns/{self.name}'
res = get_url(osv_dev_url)
if res.status_code == 404:
- error_message.send(sender=None, text=f'404 - Skipping {self.name} - {osv_dev_url}')
+ error_message(text=f'404 - Skipping {self.name} - {osv_dev_url}')
return
data = res.content
osv_dev_json = json.loads(data)
@@ -102,7 +101,7 @@ def fetch_osv_dev_data(self):
def parse_osv_dev_data(self, osv_dev_json):
name = osv_dev_json.get('id')
if name != self.name:
- error_message.send(sender=None, text=f'Erratum name mismatch - {self.name} != {name}')
+ error_message(text=f'Erratum name mismatch - {self.name} != {name}')
return
related = osv_dev_json.get('related')
if related:
@@ -155,7 +154,7 @@ def add_cve(self, cve_id):
""" Add a CVE to an Erratum object
"""
if not cve_id.startswith('CVE') or not cve_id.split('-')[1].isdigit():
- error_message.send(sender=None, text=f'Not a CVE ID: {cve_id}')
+ error_message(text=f'Not a CVE ID: {cve_id}')
return
self.cves.add(get_or_create_cve(cve_id))
diff --git a/errata/sources/distros/alma.py b/errata/sources/distros/alma.py
index e0f2d4ae..0091b8bf 100644
--- a/errata/sources/distros/alma.py
+++ b/errata/sources/distros/alma.py
@@ -22,8 +22,8 @@
from operatingsystems.utils import get_or_create_osrelease
from packages.models import Package
from packages.utils import get_or_create_package, parse_package_string
-from util import get_url, fetch_content, get_setting_of_type
from patchman.signals import pbar_start, pbar_update
+from util import fetch_content, get_setting_of_type, get_url
def update_alma_errata(concurrent_processing=True):
diff --git a/errata/sources/distros/arch.py b/errata/sources/distros/arch.py
index 40d0dada..e22de403 100644
--- a/errata/sources/distros/arch.py
+++ b/errata/sources/distros/arch.py
@@ -20,10 +20,13 @@
from django.db import connections
from operatingsystems.utils import get_or_create_osrelease
-from patchman.signals import error_message, pbar_start, pbar_update
from packages.models import Package
-from packages.utils import find_evr, get_matching_packages, get_or_create_package
-from util import get_url, fetch_content
+from packages.utils import (
+ find_evr, get_matching_packages, get_or_create_package,
+)
+from patchman.signals import pbar_start, pbar_update
+from util import fetch_content, get_url
+from util.logging import error_message
def update_arch_errata(concurrent_processing=False):
@@ -99,7 +102,7 @@ def process_arch_erratum(advisory, osrelease):
add_arch_erratum_references(e, advisory)
add_arch_erratum_packages(e, advisory)
except Exception as exc:
- error_message.send(sender=None, text=exc)
+ error_message(text=exc)
def add_arch_linux_osrelease():
diff --git a/errata/sources/distros/centos.py b/errata/sources/distros/centos.py
index eefb2b88..8f4aa4a1 100644
--- a/errata/sources/distros/centos.py
+++ b/errata/sources/distros/centos.py
@@ -15,13 +15,15 @@
# along with Patchman. If not, see
import re
+
from defusedxml import ElementTree
from operatingsystems.utils import get_or_create_osrelease
from packages.models import Package
-from packages.utils import parse_package_string, get_or_create_package
-from patchman.signals import error_message, pbar_start, pbar_update
-from util import bunzip2, get_url, fetch_content, get_sha1, get_setting_of_type
+from packages.utils import get_or_create_package, parse_package_string
+from patchman.signals import pbar_start, pbar_update
+from util import bunzip2, fetch_content, get_setting_of_type, get_sha1, get_url
+from util.logging import error_message
def update_centos_errata():
@@ -34,7 +36,7 @@ def update_centos_errata():
if actual_checksum != expected_checksum:
e = 'CEFS checksum mismatch, skipping CentOS errata parsing\n'
e += f'{actual_checksum} (actual) != {expected_checksum} (expected)'
- error_message.send(sender=None, text=e)
+ error_message(text=e)
else:
if data:
parse_centos_errata(bunzip2(data))
diff --git a/errata/sources/distros/debian.py b/errata/sources/distros/debian.py
index 93ae2bd5..8025b1bf 100644
--- a/errata/sources/distros/debian.py
+++ b/errata/sources/distros/debian.py
@@ -18,17 +18,18 @@
import csv
import re
from datetime import datetime
-from debian.deb822 import Dsc
from io import StringIO
+from debian.deb822 import Dsc
from django.db import connections
from operatingsystems.models import OSRelease
from operatingsystems.utils import get_or_create_osrelease
from packages.models import Package
-from packages.utils import get_or_create_package, find_evr
-from patchman.signals import error_message, pbar_start, pbar_update, warning_message
-from util import get_url, fetch_content, get_setting_of_type, extract
+from packages.utils import find_evr, get_or_create_package
+from patchman.signals import pbar_start, pbar_update
+from util import extract, fetch_content, get_setting_of_type, get_url
+from util.logging import error_message, warning_message
DSCs = {}
@@ -217,7 +218,7 @@ def process_debian_erratum(erratum, accepted_codenames):
for package in packages:
process_debian_erratum_fixed_packages(e, package)
except Exception as exc:
- error_message.send(sender=None, text=exc)
+ error_message(text=exc)
def parse_debian_erratum_package(line, accepted_codenames):
@@ -249,7 +250,7 @@ def fetch_debian_dsc_package_list(package, version):
""" Fetch the package list from a DSC file for a given source package/version
"""
if not DSCs.get(package) or not DSCs[package].get(version):
- warning_message.send(sender=None, text=f'No DSC found for {package} {version}')
+ warning_message(text=f'No DSC found for {package} {version}')
return
source_url = DSCs[package][version]['url']
res = get_url(source_url)
@@ -263,7 +264,7 @@ def get_accepted_debian_codenames():
""" Get acceptable Debian OS codenames
Can be overridden by specifying DEBIAN_CODENAMES in settings
"""
- default_codenames = ['bookworm', 'bullseye']
+ default_codenames = ['bookworm', 'trixie']
accepted_codenames = get_setting_of_type(
setting_name='DEBIAN_CODENAMES',
setting_type=list,
diff --git a/errata/sources/distros/rocky.py b/errata/sources/distros/rocky.py
index 693d7b0c..2d805985 100644
--- a/errata/sources/distros/rocky.py
+++ b/errata/sources/distros/rocky.py
@@ -14,18 +14,21 @@
# You should have received a copy of the GNU General Public License
# along with Patchman. If not, see
-import json
import concurrent.futures
-from tenacity import retry, retry_if_exception_type, stop_after_attempt, wait_exponential
+import json
from django.db import connections
from django.db.utils import OperationalError
+from tenacity import (
+ retry, retry_if_exception_type, stop_after_attempt, wait_exponential,
+)
from operatingsystems.utils import get_or_create_osrelease
from packages.models import Package
-from packages.utils import parse_package_string, get_or_create_package
+from packages.utils import get_or_create_package, parse_package_string
from patchman.signals import pbar_start, pbar_update
-from util import get_url, fetch_content, info_message, error_message
+from util import fetch_content, get_url
+from util.logging import error_message, info_message
def update_rocky_errata(concurrent_processing=True):
@@ -50,16 +53,16 @@ def check_rocky_errata_endpoint_health(rocky_errata_api_host):
health = json.loads(data)
if health.get('status') == 'ok':
s = f'Rocky Errata API healthcheck OK: {rocky_errata_healthcheck_url}'
- info_message.send(sender=None, text=s)
+ info_message(text=s)
return True
else:
s = f'Rocky Errata API healthcheck FAILED: {rocky_errata_healthcheck_url}'
- error_message.send(sender=None, text=s)
+ error_message(text=s)
return False
except Exception as e:
s = f'Rocky Errata API healthcheck exception occured: {rocky_errata_healthcheck_url}\n'
s += str(e)
- error_message.send(sender=None, text=s)
+ error_message(text=s)
return False
@@ -194,7 +197,7 @@ def process_rocky_erratum(advisory):
add_rocky_erratum_oses(e, advisory)
add_rocky_erratum_packages(e, advisory)
except Exception as exc:
- error_message.send(sender=None, text=exc)
+ error_message(text=exc)
def add_rocky_erratum_references(e, advisory):
diff --git a/errata/sources/distros/ubuntu.py b/errata/sources/distros/ubuntu.py
index 7f50962c..5616331f 100644
--- a/errata/sources/distros/ubuntu.py
+++ b/errata/sources/distros/ubuntu.py
@@ -16,8 +16,8 @@
import concurrent.futures
import csv
-import os
import json
+import os
from io import StringIO
from urllib.parse import urlparse
@@ -26,9 +26,15 @@
from operatingsystems.models import OSRelease, OSVariant
from operatingsystems.utils import get_or_create_osrelease
from packages.models import Package
-from packages.utils import get_or_create_package, parse_package_string, find_evr, get_matching_packages
-from util import get_url, fetch_content, get_sha256, bunzip2, get_setting_of_type
-from patchman.signals import error_message, pbar_start, pbar_update
+from packages.utils import (
+ find_evr, get_matching_packages, get_or_create_package,
+ parse_package_string,
+)
+from patchman.signals import pbar_start, pbar_update
+from util import (
+ bunzip2, fetch_content, get_setting_of_type, get_sha256, get_url,
+)
+from util.logging import error_message
def update_ubuntu_errata(concurrent_processing=False):
@@ -45,7 +51,7 @@ def update_ubuntu_errata(concurrent_processing=False):
else:
e = 'Ubuntu USN DB checksum mismatch, skipping Ubuntu errata parsing\n'
e += f'{actual_checksum} (actual) != {expected_checksum} (expected)'
- error_message.send(sender=None, text=e)
+ error_message(text=e)
def fetch_ubuntu_usn_db():
@@ -126,7 +132,7 @@ def process_usn(usn_id, advisory, accepted_releases):
add_ubuntu_erratum_references(e, usn_id, advisory)
add_ubuntu_erratum_packages(e, advisory)
except Exception as exc:
- error_message.send(sender=None, text=exc)
+ error_message(text=exc)
def add_ubuntu_erratum_osreleases(e, affected_releases, accepted_releases):
@@ -202,7 +208,7 @@ def get_accepted_ubuntu_codenames():
""" Get acceptable Ubuntu OS codenames
Can be overridden by specifying UBUNTU_CODENAMES in settings
"""
- default_codenames = ['focal', 'jammy', 'noble']
+ default_codenames = ['jammy', 'noble']
accepted_codenames = get_setting_of_type(
setting_name='UBUNTU_CODENAMES',
setting_type=list,
diff --git a/errata/sources/repos/yum.py b/errata/sources/repos/yum.py
index dfeed879..8b6732c4 100644
--- a/errata/sources/repos/yum.py
+++ b/errata/sources/repos/yum.py
@@ -16,16 +16,17 @@
import concurrent.futures
from io import BytesIO
-from defusedxml import ElementTree
+from defusedxml import ElementTree
from django.db import connections
from operatingsystems.utils import get_or_create_osrelease
from packages.models import Package
from packages.utils import get_or_create_package
-from patchman.signals import pbar_start, pbar_update, error_message
+from patchman.signals import pbar_start, pbar_update
from security.models import Reference
from util import extract, get_url
+from util.logging import error_message
def extract_updateinfo(data, url, concurrent_processing=True):
@@ -38,7 +39,7 @@ def extract_updateinfo(data, url, concurrent_processing=True):
elen = root.__len__()
updates = root.findall('update')
except ElementTree.ParseError as e:
- error_message.send(sender=None, text=f'Error parsing updateinfo file from {url} : {e}')
+ error_message(text=f'Error parsing updateinfo file from {url} : {e}')
if concurrent_processing:
extract_updateinfo_concurrently(updates, elen)
else:
diff --git a/errata/tasks.py b/errata/tasks.py
index fe53b415..8ded3a79 100644
--- a/errata/tasks.py
+++ b/errata/tasks.py
@@ -15,17 +15,18 @@
# along with Patchman. If not, see
from celery import shared_task
+from django.core.cache import cache
-from errata.sources.distros.arch import update_arch_errata
from errata.sources.distros.alma import update_alma_errata
-from errata.sources.distros.debian import update_debian_errata
+from errata.sources.distros.arch import update_arch_errata
from errata.sources.distros.centos import update_centos_errata
+from errata.sources.distros.debian import update_debian_errata
from errata.sources.distros.rocky import update_rocky_errata
from errata.sources.distros.ubuntu import update_ubuntu_errata
-from patchman.signals import error_message
from repos.models import Repository
from security.tasks import update_cves, update_cwes
from util import get_setting_of_type
+from util.logging import error_message, warning_message
@shared_task
@@ -44,34 +45,44 @@ def update_yum_repo_errata(repo_id=None, force=False):
def update_errata(erratum_type=None, force=False, repo=None):
""" Update all distros errata
"""
- errata_os_updates = []
- erratum_types = ['yum', 'rocky', 'alma', 'arch', 'ubuntu', 'debian', 'centos']
- erratum_type_defaults = ['yum', 'rocky', 'alma', 'arch', 'ubuntu', 'debian']
- if erratum_type:
- if erratum_type not in erratum_types:
- error_message.send(sender=None, text=f'Erratum type `{erratum_type}` not in {erratum_types}')
- else:
- errata_os_updates = erratum_type
+ lock_key = 'update_errata_lock'
+ # lock will expire after 48 hours
+ lock_expire = 60 * 60 * 48
+
+ if cache.add(lock_key, 'true', lock_expire):
+ try:
+ errata_os_updates = []
+ erratum_types = ['yum', 'rocky', 'alma', 'arch', 'ubuntu', 'debian', 'centos']
+ erratum_type_defaults = ['yum', 'rocky', 'alma', 'arch', 'ubuntu', 'debian']
+ if erratum_type:
+ if erratum_type not in erratum_types:
+ error_message(text=f'Erratum type `{erratum_type}` not in {erratum_types}')
+ else:
+ errata_os_updates = erratum_type
+ else:
+ errata_os_updates = get_setting_of_type(
+ setting_name='ERRATA_OS_UPDATES',
+ setting_type=list,
+ default=erratum_type_defaults,
+ )
+ if 'yum' in errata_os_updates:
+ update_yum_repo_errata(repo_id=repo, force=force)
+ if 'arch' in errata_os_updates:
+ update_arch_errata()
+ if 'alma' in errata_os_updates:
+ update_alma_errata()
+ if 'rocky' in errata_os_updates:
+ update_rocky_errata()
+ if 'debian' in errata_os_updates:
+ update_debian_errata()
+ if 'ubuntu' in errata_os_updates:
+ update_ubuntu_errata()
+ if 'centos' in errata_os_updates:
+ update_centos_errata()
+ finally:
+ cache.delete(lock_key)
else:
- errata_os_updates = get_setting_of_type(
- setting_name='ERRATA_OS_UPDATES',
- setting_type=list,
- default=erratum_type_defaults,
- )
- if 'yum' in errata_os_updates:
- update_yum_repo_errata(repo_id=repo, force=force)
- if 'arch' in errata_os_updates:
- update_arch_errata()
- if 'alma' in errata_os_updates:
- update_alma_errata()
- if 'rocky' in errata_os_updates:
- update_rocky_errata()
- if 'debian' in errata_os_updates:
- update_debian_errata()
- if 'ubuntu' in errata_os_updates:
- update_ubuntu_errata()
- if 'centos' in errata_os_updates:
- update_centos_errata()
+ warning_message('Already updating Errata, skipping task.')
@shared_task
diff --git a/errata/utils.py b/errata/utils.py
index d8099db4..e0a5e01b 100644
--- a/errata/utils.py
+++ b/errata/utils.py
@@ -18,10 +18,11 @@
from django.db import connections
-from util import tz_aware_datetime
from errata.models import Erratum
from packages.models import PackageUpdate
-from patchman.signals import pbar_start, pbar_update, warning_message
+from patchman.signals import pbar_start, pbar_update
+from util import tz_aware_datetime
+from util.logging import warning_message
def get_or_create_erratum(name, e_type, issue_date, synopsis):
@@ -36,16 +37,16 @@ def get_or_create_erratum(name, e_type, issue_date, synopsis):
days_delta = abs(e.issue_date.date() - issue_date_tz.date()).days
updated = False
if e.e_type != e_type:
- warning_message.send(sender=None, text=f'Updating {name} type `{e.e_type}` -> `{e_type}`')
+ warning_message(text=f'Updating {name} type `{e.e_type}` -> `{e_type}`')
e.e_type = e_type
updated = True
if days_delta > 1:
text = f'Updating {name} issue date `{e.issue_date.date()}` -> `{issue_date_tz.date()}`'
- warning_message.send(sender=None, text=text)
+ warning_message(text=text)
e.issue_date = issue_date_tz
updated = True
if e.synopsis != synopsis:
- warning_message.send(sender=None, text=f'Updating {name} synopsis `{e.synopsis}` -> `{synopsis}`')
+ warning_message(text=f'Updating {name} synopsis `{e.synopsis}` -> `{synopsis}`')
e.synopsis = synopsis
updated = True
if updated:
diff --git a/errata/views.py b/errata/views.py
index 42d12f71..8e1c0b2f 100644
--- a/errata/views.py
+++ b/errata/views.py
@@ -14,16 +14,15 @@
# You should have received a copy of the GNU General Public License
# along with Patchman. If not, see
-from django.shortcuts import get_object_or_404, render
from django.contrib.auth.decorators import login_required
-from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
+from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator
from django.db.models import Q
-
+from django.shortcuts import get_object_or_404, render
from rest_framework import viewsets
-from operatingsystems.models import OSRelease
from errata.models import Erratum
from errata.serializers import ErratumSerializer
+from operatingsystems.models import OSRelease
from util.filterspecs import Filter, FilterBar
diff --git a/etc/patchman/local_settings.py b/etc/patchman/local_settings.py
index 181c4c4d..40dd2efd 100644
--- a/etc/patchman/local_settings.py
+++ b/etc/patchman/local_settings.py
@@ -41,27 +41,35 @@
# Number of days to wait before raising that a host has not reported
DAYS_WITHOUT_REPORT = 14
+# list of errata sources to update, remove unwanted ones to improve performance
+ERRATA_OS_UPDATES = ['yum', 'rocky', 'alma', 'arch', 'ubuntu', 'debian']
+
+# list of Alma Linux releases to update
+ALMA_RELEASES = [8, 9, 10]
+
+# list of Debian Linux releases to update
+DEBIAN_CODENAMES = ['bookworm', 'trixie']
+
+# list of Ubuntu Linux releases to update
+UBUNTU_CODENAMES = ['jammy', 'noble']
+
# Whether to run patchman under the gunicorn web server
RUN_GUNICORN = False
+# Set the default timeout to e.g. 30 seconds to enable UI caching
+# Note that the UI results may be out of date for this amount of time
CACHES = {
'default': {
- 'BACKEND': 'django.core.cache.backends.dummy.DummyCache',
+ 'BACKEND': 'django.core.cache.backends.redis.RedisCache',
+ 'LOCATION': 'redis://127.0.0.1:6379',
+ 'TIMEOUT': 0,
}
}
-# Uncomment to enable redis caching for e.g. 30 seconds
-# Note that the UI results may be out of date for this amount of time
-# CACHES = {
-# 'default': {
-# 'BACKEND': 'django.core.cache.backends.redis.RedisCache',
-# 'LOCATION': 'redis://127.0.0.1:6379',
-# 'TIMEOUT': 30,
-# }
-# }
-
-from datetime import timedelta # noqa
+from datetime import timedelta # noqa
+
from celery.schedules import crontab # noqa
+
CELERY_BEAT_SCHEDULE = {
'process_all_unprocessed_reports': {
'task': 'reports.tasks.process_reports',
diff --git a/hooks/yum/patchman.py b/hooks/yum/patchman.py
index 343144eb..52f9cc8b 100644
--- a/hooks/yum/patchman.py
+++ b/hooks/yum/patchman.py
@@ -15,6 +15,7 @@
# along with Patchman. If not, see
import os
+
from yum.plugins import TYPE_CORE
requires_api_version = '2.1'
diff --git a/hooks/zypper/patchman.py b/hooks/zypper/patchman.py
index 14781565..d9d478f3 100755
--- a/hooks/zypper/patchman.py
+++ b/hooks/zypper/patchman.py
@@ -18,8 +18,9 @@
#
# zypp system plugin for patchman
-import os
import logging
+import os
+
from zypp_plugin import Plugin
diff --git a/hosts/admin.py b/hosts/admin.py
index 8a42e8cc..43bf31da 100644
--- a/hosts/admin.py
+++ b/hosts/admin.py
@@ -16,6 +16,7 @@
# along with Patchman. If not, see
from django.contrib import admin
+
from hosts.models import Host, HostRepo
diff --git a/hosts/migrations/0001_initial.py b/hosts/migrations/0001_initial.py
index 43366684..0037e094 100644
--- a/hosts/migrations/0001_initial.py
+++ b/hosts/migrations/0001_initial.py
@@ -1,8 +1,9 @@
# Generated by Django 3.2.19 on 2023-12-11 22:15
-from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
+from django.db import migrations, models
+
try:
import tagging.fields
has_tagging = True
diff --git a/hosts/migrations/0002_initial.py b/hosts/migrations/0002_initial.py
index cc59a70e..6c453c49 100644
--- a/hosts/migrations/0002_initial.py
+++ b/hosts/migrations/0002_initial.py
@@ -1,7 +1,7 @@
# Generated by Django 3.2.19 on 2023-12-11 22:15
-from django.db import migrations, models
import django.db.models.deletion
+from django.db import migrations, models
class Migration(migrations.Migration):
diff --git a/hosts/migrations/0004_remove_host_tags_host_tags.py b/hosts/migrations/0004_remove_host_tags_host_tags.py
index 84e7affe..bf03a84e 100644
--- a/hosts/migrations/0004_remove_host_tags_host_tags.py
+++ b/hosts/migrations/0004_remove_host_tags_host_tags.py
@@ -1,8 +1,9 @@
# Generated by Django 4.2.18 on 2025-02-04 23:37
+import taggit.managers
from django.apps import apps
from django.db import migrations
-import taggit.managers
+
try:
import tagging # noqa
except ImportError:
diff --git a/hosts/migrations/0006_migrate_to_tz_aware.py b/hosts/migrations/0006_migrate_to_tz_aware.py
index e36bbf1f..c14ea50b 100644
--- a/hosts/migrations/0006_migrate_to_tz_aware.py
+++ b/hosts/migrations/0006_migrate_to_tz_aware.py
@@ -1,6 +1,7 @@
from django.db import migrations
from django.utils import timezone
+
def make_datetimes_tz_aware(apps, schema_editor):
Host = apps.get_model('hosts', 'Host')
for host in Host.objects.all():
diff --git a/hosts/migrations/0007_alter_host_tags.py b/hosts/migrations/0007_alter_host_tags.py
index 3858b847..3910a06f 100644
--- a/hosts/migrations/0007_alter_host_tags.py
+++ b/hosts/migrations/0007_alter_host_tags.py
@@ -1,7 +1,7 @@
# Generated by Django 4.2.19 on 2025-02-28 19:53
-from django.db import migrations
import taggit.managers
+from django.db import migrations
class Migration(migrations.Migration):
diff --git a/hosts/models.py b/hosts/models.py
index a6c451b5..8ea5e3d5 100644
--- a/hosts/models.py
+++ b/hosts/models.py
@@ -24,6 +24,7 @@
from version_utils.rpm import labelCompare
except ImportError:
from rpm import labelCompare
+
from taggit.managers import TaggableManager
from arch.models import MachineArchitecture
@@ -34,9 +35,9 @@
from operatingsystems.models import OSVariant
from packages.models import Package, PackageUpdate
from packages.utils import get_or_create_package_update
-from patchman.signals import info_message
from repos.models import Repository
from repos.utils import find_best_repo
+from util.logging import info_message
class Host(models.Model):
@@ -85,12 +86,12 @@ def show(self):
text += f'Packages : {self.get_num_packages()}\n'
text += f'Repos : {self.get_num_repos()}\n'
text += f'Updates : {self.get_num_updates()}\n'
- text += f'Tags : {self.tags}\n'
+ text += f'Tags : {" ".join(self.tags.slugs())}\n'
text += f'Needs reboot : {self.reboot_required}\n'
text += f'Updated at : {self.updated_at}\n'
text += f'Host repos : {self.host_repos_only}\n'
- info_message.send(sender=None, text=text)
+ info_message(text=text)
def get_absolute_url(self):
return reverse('hosts:host_detail', args=[self.hostname])
@@ -114,13 +115,13 @@ def check_rdns(self):
if self.check_dns:
update_rdns(self)
if self.hostname.lower() == self.reversedns.lower():
- info_message.send(sender=None, text='Reverse DNS matches')
+ info_message(text='Reverse DNS matches')
else:
text = 'Reverse DNS mismatch found: '
text += f'{self.hostname} != {self.reversedns}'
- info_message.send(sender=None, text=text)
+ info_message(text=text)
else:
- info_message.send(sender=None, text='Reverse DNS check disabled')
+ info_message(text='Reverse DNS check disabled')
def clean_reports(self):
""" Remove all but the last 3 reports for a host
@@ -131,7 +132,7 @@ def clean_reports(self):
for report in Report.objects.filter(host=self).order_by('-created')[3:]:
report.delete()
if rlen > 0:
- info_message.send(sender=None, text=f'{self.hostname}: removed {rlen} old reports')
+ info_message(text=f'{self.hostname}: removed {rlen} old reports')
def get_host_repo_packages(self):
if self.host_repos_only:
@@ -163,7 +164,7 @@ def process_update(self, package, highest_package):
security = True
update = get_or_create_package_update(oldpackage=package, newpackage=highest_package, security=security)
self.updates.add(update)
- info_message.send(sender=None, text=f'{update}')
+ info_message(text=f'{update}')
return update.id
def find_updates(self):
diff --git a/hosts/tasks.py b/hosts/tasks.py
index 2fdce96f..226652f6 100755
--- a/hosts/tasks.py
+++ b/hosts/tasks.py
@@ -15,12 +15,11 @@
# along with Patchman. If not, see
from celery import shared_task
-
from django.db.models import Count
from hosts.models import Host
from util import get_datetime_now
-from patchman.signals import info_message
+from util.logging import info_message
@shared_task
@@ -78,4 +77,4 @@ def find_all_host_updates_homogenous():
phost.updated_at = ts
phost.save()
updated_hosts.append(phost)
- info_message.send(sender=None, text=f'Added the same updates to {phost}')
+ info_message(text=f'Added the same updates to {phost}')
diff --git a/hosts/templatetags/report_alert.py b/hosts/templatetags/report_alert.py
index 3a3e3a9a..48d8f966 100644
--- a/hosts/templatetags/report_alert.py
+++ b/hosts/templatetags/report_alert.py
@@ -1,11 +1,10 @@
-# Copyright 2016-2021 Marcus Furlong
+# Copyright 2016-2025 Marcus Furlong
#
# This file is part of Patchman.
#
# Patchman is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
+# the Free Software Foundation, version 3 only.
#
# Patchman is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
@@ -13,14 +12,14 @@
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
-# along with Patchman If not, see .
+# along with Patchman. If not, see
from datetime import timedelta
from django.template import Library
-from django.utils.html import format_html
from django.templatetags.static import static
from django.utils import timezone
+from django.utils.html import format_html
from util import get_setting_of_type
diff --git a/hosts/utils.py b/hosts/utils.py
index b328129f..44441f9b 100644
--- a/hosts/utils.py
+++ b/hosts/utils.py
@@ -15,11 +15,12 @@
# You should have received a copy of the GNU General Public License
# along with Patchman. If not, see
-from socket import gethostbyaddr, gaierror, herror
+from socket import gaierror, gethostbyaddr, herror
-from django.db import transaction, IntegrityError
+from django.db import IntegrityError, transaction
+from taggit.models import Tag
-from patchman.signals import error_message
+from util.logging import error_message, info_message
def update_rdns(host):
@@ -62,14 +63,28 @@ def get_or_create_host(report, arch, osvariant, domain):
host.osvariant = osvariant
host.domain = domain
host.lastreport = report.created
- host.tags = report.tags
+ host.tags.set(report.tags.split(','), clear=True)
if report.reboot == 'True':
host.reboot_required = True
else:
host.reboot_required = False
host.save()
except IntegrityError as e:
- error_message.send(sender=None, text=e)
+ error_message(text=e)
if host:
host.check_rdns()
return host
+
+
+def clean_tags():
+ """ Delete Tags that have no Host
+ """
+ tags = Tag.objects.filter(
+ host__isnull=True,
+ )
+ tlen = tags.count()
+ if tlen == 0:
+ info_message(text='No orphaned Tags found.')
+ else:
+ info_message(text=f'{tlen} orphaned Tags found.')
+ tags.delete()
diff --git a/hosts/views.py b/hosts/views.py
index 0fc83ffa..8f20ab19 100644
--- a/hosts/views.py
+++ b/hosts/views.py
@@ -15,24 +15,23 @@
# You should have received a copy of the GNU General Public License
# along with Patchman. If not, see
-from django.shortcuts import get_object_or_404, render, redirect
+from django.contrib import messages
from django.contrib.auth.decorators import login_required
-from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
-from django.urls import reverse
+from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator
from django.db.models import Q
-from django.contrib import messages
-
-from taggit.models import Tag
+from django.shortcuts import get_object_or_404, redirect, render
+from django.urls import reverse
from rest_framework import viewsets
+from taggit.models import Tag
-from util.filterspecs import Filter, FilterBar
-from hosts.models import Host, HostRepo
-from domains.models import Domain
from arch.models import MachineArchitecture
-from operatingsystems.models import OSVariant, OSRelease
-from reports.models import Report
+from domains.models import Domain
from hosts.forms import EditHostForm
-from hosts.serializers import HostSerializer, HostRepoSerializer
+from hosts.models import Host, HostRepo
+from hosts.serializers import HostRepoSerializer, HostSerializer
+from operatingsystems.models import OSRelease, OSVariant
+from reports.models import Report
+from util.filterspecs import Filter, FilterBar
@login_required
diff --git a/modules/admin.py b/modules/admin.py
index 33b94d20..9cf21e6c 100644
--- a/modules/admin.py
+++ b/modules/admin.py
@@ -15,6 +15,7 @@
# along with Patchman. If not, see
from django.contrib import admin
+
from modules.models import Module
admin.site.register(Module)
diff --git a/modules/migrations/0001_initial.py b/modules/migrations/0001_initial.py
index 12a8e278..9c27d425 100644
--- a/modules/migrations/0001_initial.py
+++ b/modules/migrations/0001_initial.py
@@ -1,7 +1,7 @@
# Generated by Django 3.2.19 on 2023-12-11 22:17
-from django.db import migrations, models
import django.db.models.deletion
+from django.db import migrations, models
class Migration(migrations.Migration):
diff --git a/modules/models.py b/modules/models.py
index 931a41c3..989b2ec9 100644
--- a/modules/models.py
+++ b/modules/models.py
@@ -24,10 +24,18 @@
class Module(models.Model):
- name = models.CharField(max_length=255)
- stream = models.CharField(max_length=255)
- version = models.CharField(max_length=255)
- context = models.CharField(max_length=255)
+# name = models.CharField(max_length=255)
+# stream = models.CharField(max_length=255)
+# version = models.CharField(max_length=255)
+# context = models.CharField(max_length=255)
+# arch = models.ForeignKey(PackageArchitecture, on_delete=models.CASCADE)
+# repo = models.ForeignKey(Repository, on_delete=models.CASCADE)
+# packages = models.ManyToManyField(Package, blank=True)
+
+ name = models.CharField(max_length=150)
+ stream = models.CharField(max_length=150)
+ version = models.CharField(max_length=150)
+ context = models.CharField(max_length=150)
arch = models.ForeignKey(PackageArchitecture, on_delete=models.CASCADE)
repo = models.ForeignKey(Repository, on_delete=models.CASCADE)
packages = models.ManyToManyField(Package, blank=True)
@@ -35,8 +43,9 @@ class Module(models.Model):
class Meta:
verbose_name = 'Module'
verbose_name_plural = 'Modules'
- unique_together = ['name', 'stream', 'version', 'context', 'arch']
- ordering = ['name', 'stream']
+ #unique_together = ('name', 'stream', 'version', 'context', 'arch',)
+ unique_together = (("name", "stream", "version", "context"),)
+ ordering = ('name', 'stream',)
def __str__(self):
return f'{self.name}-{self.stream}-{self.version}-{self.version}-{self.context}'
diff --git a/modules/utils.py b/modules/utils.py
index 817a610c..da44ac45 100644
--- a/modules/utils.py
+++ b/modules/utils.py
@@ -14,38 +14,41 @@
# You should have received a copy of the GNU General Public License
# along with Patchman. If not, see
-from django.db import IntegrityError
-from patchman.signals import error_message, info_message
+from django.db import IntegrityError, DatabaseError, transaction
+from patchman.signals import error_message
from modules.models import Module
from arch.models import PackageArchitecture
-
def get_or_create_module(name, stream, version, context, arch, repo):
- """ Get or create a module object
- Returns the module and a boolean for created
- """
created = False
- m_arch, c = PackageArchitecture.objects.get_or_create(name=arch)
+ with transaction.atomic():
+ m_arch, _ = PackageArchitecture.objects.get_or_create(name=arch)
try:
- module, created = Module.objects.get_or_create(
- name=name,
- stream=stream,
- version=version,
- context=context,
- arch=m_arch,
- repo=repo,
- )
+ with transaction.atomic():
+ module, created = Module.objects.get_or_create(
+ name=name,
+ stream=stream,
+ version=version,
+ context=context,
+ arch=m_arch,
+ # keep repo in create, but not in uniqueness filter
+ defaults={'repo': repo},
+ )
except IntegrityError as e:
error_message.send(sender=None, text=e)
+ # Lookup only on the unique fields
module = Module.objects.get(
name=name,
stream=stream,
version=version,
context=context,
arch=m_arch,
- repo=repo,
)
+ created = False
+ except DatabaseError as e:
+ error_message.send(sender=None, text=e)
+ module = None
return module, created
@@ -53,7 +56,8 @@ def get_matching_modules(name, stream, version, context, arch):
""" Return modules that match name, stream, version, context, and arch,
regardless of repo
"""
- m_arch, c = PackageArchitecture.objects.get_or_create(name=arch)
+ with transaction.atomic():
+ m_arch, c = PackageArchitecture.objects.get_or_create(name=arch)
modules = Module.objects.filter(
name=name,
stream=stream,
@@ -62,18 +66,3 @@ def get_matching_modules(name, stream, version, context, arch):
arch=m_arch,
)
return modules
-
-
-def clean_modules():
- """ Delete modules that have no host or no repo
- """
- modules = Module.objects.filter(
- host__isnull=True,
- repo__isnull=True,
- )
- mlen = modules.count()
- if mlen == 0:
- info_message.send(sender=None, text='No orphaned Modules found.')
- else:
- info_message.send(sender=None, text=f'{mlen} orphaned Modules found.')
- modules.delete()
diff --git a/modules/views.py b/modules/views.py
index b897a709..2d017220 100644
--- a/modules/views.py
+++ b/modules/views.py
@@ -14,12 +14,11 @@
# You should have received a copy of the GNU General Public License
# along with Patchman. If not, see
-from django.shortcuts import get_object_or_404, render
from django.contrib.auth.decorators import login_required
-from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
+from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator
from django.db.models import Q
-
-from rest_framework import viewsets, permissions
+from django.shortcuts import get_object_or_404, render
+from rest_framework import permissions, viewsets
from modules.models import Module
from modules.serializers import ModuleSerializer
diff --git a/operatingsystems/admin.py b/operatingsystems/admin.py
index 15f5e200..4884b5eb 100644
--- a/operatingsystems/admin.py
+++ b/operatingsystems/admin.py
@@ -16,7 +16,8 @@
# along with Patchman. If not, see
from django.contrib import admin
-from operatingsystems.models import OSVariant, OSRelease
+
+from operatingsystems.models import OSRelease, OSVariant
class OSReleaseAdmin(admin.ModelAdmin):
diff --git a/operatingsystems/forms.py b/operatingsystems/forms.py
index 548a7d88..fa319182 100644
--- a/operatingsystems/forms.py
+++ b/operatingsystems/forms.py
@@ -15,10 +15,10 @@
# You should have received a copy of the GNU General Public License
# along with Patchman. If not, see
-from django.forms import ModelForm, ModelMultipleChoiceField
from django.contrib.admin.widgets import FilteredSelectMultiple
+from django.forms import ModelForm, ModelMultipleChoiceField
-from operatingsystems.models import OSVariant, OSRelease
+from operatingsystems.models import OSRelease, OSVariant
from repos.models import Repository
diff --git a/operatingsystems/migrations/0002_initial.py b/operatingsystems/migrations/0002_initial.py
index 517a3f9a..04cbb411 100644
--- a/operatingsystems/migrations/0002_initial.py
+++ b/operatingsystems/migrations/0002_initial.py
@@ -1,7 +1,7 @@
# Generated by Django 3.2.19 on 2023-12-11 22:15
-from django.db import migrations, models
import django.db.models.deletion
+from django.db import migrations, models
class Migration(migrations.Migration):
diff --git a/operatingsystems/migrations/0003_os_arch.py b/operatingsystems/migrations/0003_os_arch.py
index 2778ca3f..4d0e0f93 100644
--- a/operatingsystems/migrations/0003_os_arch.py
+++ b/operatingsystems/migrations/0003_os_arch.py
@@ -1,7 +1,7 @@
# Generated by Django 3.2.25 on 2025-02-07 13:02
-from django.db import migrations, models
import django.db.models.deletion
+from django.db import migrations, models
class Migration(migrations.Migration):
diff --git a/operatingsystems/serializers.py b/operatingsystems/serializers.py
index 8418c720..be178909 100644
--- a/operatingsystems/serializers.py
+++ b/operatingsystems/serializers.py
@@ -16,7 +16,7 @@
from rest_framework import serializers
-from operatingsystems.models import OSVariant, OSRelease
+from operatingsystems.models import OSRelease, OSVariant
class OSVariantSerializer(serializers.HyperlinkedModelSerializer):
diff --git a/operatingsystems/views.py b/operatingsystems/views.py
index 2b696f92..6009f119 100644
--- a/operatingsystems/views.py
+++ b/operatingsystems/views.py
@@ -15,19 +15,22 @@
# You should have received a copy of the GNU General Public License
# along with Patchman. If not, see
-from django.shortcuts import get_object_or_404, render, redirect
+from django.contrib import messages
from django.contrib.auth.decorators import login_required
-from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
+from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator
from django.db.models import Q
-from django.contrib import messages
+from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
-
from rest_framework import viewsets
from hosts.models import Host
-from operatingsystems.models import OSVariant, OSRelease
-from operatingsystems.forms import AddOSVariantToOSReleaseForm, AddReposToOSReleaseForm, CreateOSReleaseForm
-from operatingsystems.serializers import OSVariantSerializer, OSReleaseSerializer
+from operatingsystems.forms import (
+ AddOSVariantToOSReleaseForm, AddReposToOSReleaseForm, CreateOSReleaseForm,
+)
+from operatingsystems.models import OSRelease, OSVariant
+from operatingsystems.serializers import (
+ OSReleaseSerializer, OSVariantSerializer,
+)
@login_required
diff --git a/packages/admin.py b/packages/admin.py
index 979ba779..bc4b1aaa 100644
--- a/packages/admin.py
+++ b/packages/admin.py
@@ -16,6 +16,7 @@
# along with Patchman. If not, see
from django.contrib import admin
+
from packages.models import Package, PackageName, PackageUpdate
diff --git a/packages/migrations/0001_initial.py b/packages/migrations/0001_initial.py
index 07e7bcb0..bfea3e1a 100644
--- a/packages/migrations/0001_initial.py
+++ b/packages/migrations/0001_initial.py
@@ -1,7 +1,7 @@
# Generated by Django 3.2.19 on 2023-12-11 22:15
-from django.db import migrations, models
import django.db.models.deletion
+from django.db import migrations, models
class Migration(migrations.Migration):
diff --git a/packages/migrations/0002_auto_20250207_1319.py b/packages/migrations/0002_auto_20250207_1319.py
index 1563d139..4c744203 100644
--- a/packages/migrations/0002_auto_20250207_1319.py
+++ b/packages/migrations/0002_auto_20250207_1319.py
@@ -1,7 +1,7 @@
# Generated by Django 3.2.25 on 2025-02-07 13:19
-from django.db import migrations, models
import django.db.models.deletion
+from django.db import migrations, models
class Migration(migrations.Migration):
diff --git a/packages/models.py b/packages/models.py
index f4c9c59e..74a83c0c 100644
--- a/packages/models.py
+++ b/packages/models.py
@@ -195,11 +195,11 @@ def __str__(self):
rel = f'-{self.release}'
else:
rel = ''
- if self.packagetype == self.GENTOO:
+ if self.packagetype == Package.GENTOO:
return f'{self.category}/{self.name}-{epo}{self.version}{rel}-{self.arch}.{self.get_packagetype_display()}'
- elif self.packagetype in [self.DEB, self.ARCH]:
+ elif self.packagetype in [Package.DEB, Package.ARCH]:
return f'{self.name}_{epo}{self.version}{rel}_{self.arch}.{self.get_packagetype_display()}'
- elif self.packagetype == self.RPM:
+ elif self.packagetype == Package.RPM:
return f'{self.name}-{epo}{self.version}{rel}-{self.arch}.{self.get_packagetype_display()}'
else:
return f'{self.name}-{epo}{self.version}{rel}-{self.arch}.{self.get_packagetype_display()}'
diff --git a/packages/serializers.py b/packages/serializers.py
index 902cb3e0..b6e3fb83 100644
--- a/packages/serializers.py
+++ b/packages/serializers.py
@@ -16,7 +16,7 @@
from rest_framework import serializers
-from packages.models import PackageName, Package, PackageUpdate
+from packages.models import Package, PackageName, PackageUpdate
class PackageNameSerializer(serializers.HyperlinkedModelSerializer):
diff --git a/packages/utils.py b/packages/utils.py
index 9b098225..87395ff6 100644
--- a/packages/utils.py
+++ b/packages/utils.py
@@ -21,8 +21,10 @@
from django.db import IntegrityError, transaction
from arch.models import PackageArchitecture
-from packages.models import PackageName, Package, PackageUpdate, PackageCategory, PackageString
-from patchman.signals import error_message, info_message, warning_message
+from packages.models import (
+ Package, PackageCategory, PackageName, PackageString, PackageUpdate,
+)
+from util.logging import error_message, info_message, warning_message
def convert_package_to_packagestring(package):
@@ -141,7 +143,7 @@ def parse_redhat_package_string(pkg_str):
name, epoch, ver, rel, dist, arch = m.groups()
else:
e = f'Error parsing package string: "{pkg_str}"'
- error_message.send(sender=None, text=e)
+ error_message(text=e)
return
if dist:
rel = f'{rel}.{dist}'
@@ -195,7 +197,7 @@ def get_or_create_package(name, epoch, version, release, arch, p_type):
package = packages.first()
# TODO this should handle gentoo package categories too, otherwise we may be deleting packages
# that should be kept
- warning_message.send(sender=None, text=f'Deleting duplicate packages: {packages.exclude(id=package.id)}')
+ warning_message(text=f'Deleting duplicate packages: {packages.exclude(id=package.id)}')
packages.exclude(id=package.id).delete()
return package
@@ -218,10 +220,10 @@ def get_or_create_package_update(oldpackage, newpackage, security):
except MultipleObjectsReturned:
e = 'Error: MultipleObjectsReturned when attempting to add package \n'
e += f'update with oldpackage={oldpackage} | newpackage={newpackage}:'
- error_message.send(sender=None, text=e)
+ error_message(text=e)
updates = PackageUpdate.objects.filter(oldpackage=oldpackage, newpackage=newpackage)
for update in updates:
- error_message.send(sender=None, text=str(update))
+ error_message(text=str(update))
return
try:
if update:
@@ -281,13 +283,13 @@ def clean_packageupdates():
for update in package_updates:
if update.host_set.count() == 0:
text = f'Removing unused PackageUpdate {update}'
- info_message.send(sender=None, text=text)
+ info_message(text=text)
update.delete()
for duplicate in package_updates:
if update.oldpackage == duplicate.oldpackage and update.newpackage == duplicate.newpackage and \
update.security == duplicate.security and update.id != duplicate.id:
text = f'Removing duplicate PackageUpdate: {update}'
- info_message.send(sender=None, text=text)
+ info_message(text=text)
for host in duplicate.host_set.all():
host.updates.remove(duplicate)
host.updates.add(update)
@@ -307,12 +309,12 @@ def clean_packages(remove_duplicates=False):
)
plen = packages.count()
if plen == 0:
- info_message.send(sender=None, text='No orphaned Packages found.')
+ info_message(text='No orphaned Packages found.')
else:
- info_message.send(sender=None, text=f'Removing {plen} orphaned Packages')
+ info_message(text=f'Removing {plen} orphaned Packages')
packages.delete()
if remove_duplicates:
- info_message.send(sender=None, text='Checking for duplicate Packages...')
+ info_message(text='Checking for duplicate Packages...')
for package in Package.objects.all():
potential_duplicates = Package.objects.filter(
name=package.name,
@@ -326,7 +328,7 @@ def clean_packages(remove_duplicates=False):
if potential_duplicates.count() > 1:
for dupe in potential_duplicates:
if dupe.id != package.id:
- info_message.send(sender=None, text=f'Removing duplicate Package {dupe}')
+ info_message(text=f'Removing duplicate Package {dupe}')
dupe.delete()
@@ -336,7 +338,7 @@ def clean_packagenames():
names = PackageName.objects.filter(package__isnull=True)
nlen = names.count()
if nlen == 0:
- info_message.send(sender=None, text='No orphaned PackageNames found.')
+ info_message(text='No orphaned PackageNames found.')
else:
- info_message.send(sender=None, text=f'Removing {nlen} orphaned PackageNames')
+ info_message(text=f'Removing {nlen} orphaned PackageNames')
names.delete()
diff --git a/packages/views.py b/packages/views.py
index cd53fa6e..413faee0 100644
--- a/packages/views.py
+++ b/packages/views.py
@@ -15,17 +15,18 @@
# You should have received a copy of the GNU General Public License
# along with Patchman. If not, see
-from django.shortcuts import get_object_or_404, render
from django.contrib.auth.decorators import login_required
-from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
+from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator
from django.db.models import Q
-
+from django.shortcuts import get_object_or_404, render
from rest_framework import viewsets
-from util.filterspecs import Filter, FilterBar
-from packages.models import PackageName, Package, PackageUpdate
from arch.models import PackageArchitecture
-from packages.serializers import PackageNameSerializer, PackageSerializer, PackageUpdateSerializer
+from packages.models import Package, PackageName, PackageUpdate
+from packages.serializers import (
+ PackageNameSerializer, PackageSerializer, PackageUpdateSerializer,
+)
+from util.filterspecs import Filter, FilterBar
@login_required
@@ -62,9 +63,16 @@ def package_list(request):
if 'affected_by_errata' in request.GET:
affected_by_errata = request.GET['affected_by_errata'] == 'true'
if affected_by_errata:
- packages = packages.filter(erratum__isnull=False)
+ packages = packages.filter(affected_by_erratum__isnull=False)
+ else:
+ packages = packages.filter(affected_by_erratum__isnull=True)
+
+ if 'provides_fix_in_erratum' in request.GET:
+ provides_fix_in_erratum = request.GET['provides_fix_in_erratum'] == 'true'
+ if provides_fix_in_erratum:
+ packages = packages.filter(provides_fix_in_erratum__isnull=False)
else:
- packages = packages.filter(erratum__isnull=True)
+ packages = packages.filter(provides_fix_in_erratum__isnull=True)
if 'installed_on_hosts' in request.GET:
installed_on_hosts = request.GET['installed_on_hosts'] == 'true'
@@ -102,6 +110,8 @@ def package_list(request):
filter_list = []
filter_list.append(Filter(request, 'Affected by Errata', 'affected_by_errata', {'true': 'Yes', 'false': 'No'}))
+ filter_list.append(Filter(request, 'Provides Fix in Errata', 'provides_fix_in_erratum',
+ {'true': 'Yes', 'false': 'No'}))
filter_list.append(Filter(request, 'Installed on Hosts', 'installed_on_hosts', {'true': 'Yes', 'false': 'No'}))
filter_list.append(Filter(request, 'Available in Repos', 'available_in_repos', {'true': 'Yes', 'false': 'No'}))
filter_list.append(Filter(request, 'Package Type', 'packagetype', Package.PACKAGE_TYPES))
diff --git a/patchman/__init__.py b/patchman/__init__.py
index af122cc6..321dd7e5 100644
--- a/patchman/__init__.py
+++ b/patchman/__init__.py
@@ -14,10 +14,9 @@
# You should have received a copy of the GNU General Public License
# along with Patchman. If not, see
-from .receivers import * # noqa
-
# This will make sure the app is always imported when
# Django starts so that shared_task will use this app.
from .celery import app as celery_app
+from .receivers import * # noqa
__all__ = ('celery_app',)
diff --git a/patchman/celery.py b/patchman/celery.py
index 3c58edc5..c47f994d 100644
--- a/patchman/celery.py
+++ b/patchman/celery.py
@@ -15,10 +15,11 @@
# along with Patchman. If not, see
import os
+
from celery import Celery
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'patchman.settings') # noqa
-from django.conf import settings # noqa
+from django.conf import settings # noqa
app = Celery('patchman')
app.config_from_object('django.conf:settings', namespace='CELERY')
diff --git a/patchman/receivers.py b/patchman/receivers.py
index 5ec32cdd..19312ed5 100644
--- a/patchman/receivers.py
+++ b/patchman/receivers.py
@@ -15,15 +15,16 @@
# You should have received a copy of the GNU General Public License
# along with Patchman. If not, see
-from colorama import init, Fore, Style
-from tqdm import tqdm
-
+from colorama import Fore, Style, init
+from django.conf import settings
from django.dispatch import receiver
+from tqdm import tqdm
-from util import create_pbar, update_pbar, get_verbosity
-from patchman.signals import pbar_start, pbar_update, info_message, warning_message, error_message, debug_message
-
-from django.conf import settings
+from patchman.signals import (
+ debug_message_s, error_message_s, info_message_s, pbar_start, pbar_update,
+ warning_message_s,
+)
+from util import create_pbar, get_verbosity, update_pbar
init(autoreset=True)
@@ -47,36 +48,36 @@ def pbar_update_receiver(**kwargs):
update_pbar(index)
-@receiver(info_message)
-def print_info_message(sender=None, **kwargs):
- """ Receiver to print an info message, no color
+@receiver(info_message_s)
+def print_info_message(**kwargs):
+ """ Receiver to handle an info message, no color
"""
text = str(kwargs.get('text'))
if get_verbosity():
tqdm.write(Style.RESET_ALL + Fore.RESET + text)
-@receiver(warning_message)
+@receiver(warning_message_s)
def print_warning_message(**kwargs):
- """ Receiver to print a warning message in yellow text
+ """ Receiver to handle a warning message, yellow text
"""
text = str(kwargs.get('text'))
if get_verbosity():
tqdm.write(Style.BRIGHT + Fore.YELLOW + text)
-@receiver(error_message)
+@receiver(error_message_s)
def print_error_message(**kwargs):
- """ Receiver to print an error message in red text
+ """ Receiver to handle an error message, red text
"""
text = str(kwargs.get('text'))
if text:
tqdm.write(Style.BRIGHT + Fore.RED + text)
-@receiver(debug_message)
+@receiver(debug_message_s)
def print_debug_message(**kwargs):
- """ Receiver to print a debug message in blue, if verbose and DEBUG are set
+ """ Receiver to handle a debug message, blue text if verbose and DEBUG are set
"""
text = str(kwargs.get('text'))
if get_verbosity() and settings.DEBUG and text:
diff --git a/patchman/signals.py b/patchman/signals.py
index 917a48e4..799b9c98 100644
--- a/patchman/signals.py
+++ b/patchman/signals.py
@@ -19,7 +19,7 @@
pbar_start = Signal()
pbar_update = Signal()
-info_message = Signal()
-warning_message = Signal()
-error_message = Signal()
-debug_message = Signal()
+info_message_s = Signal()
+warning_message_s = Signal()
+error_message_s = Signal()
+debug_message_s = Signal()
diff --git a/patchman/urls.py b/patchman/urls.py
index 337d6b63..2ae64f56 100644
--- a/patchman/urls.py
+++ b/patchman/urls.py
@@ -15,12 +15,11 @@
# You should have received a copy of the GNU General Public License
# along with If not, see
-from django.conf.urls import include, handler404, handler500 # noqa
from django.conf import settings
+from django.conf.urls import handler404, handler500, include # noqa
from django.contrib import admin
from django.urls import path
from django.views import static
-
from rest_framework import routers
from arch import views as arch_views
@@ -44,7 +43,7 @@
router.register(r'package', package_views.PackageViewSet)
router.register(r'package-update', package_views.PackageUpdateViewSet)
router.register(r'cve', security_views.CVEViewSet)
-router.register(r'reference', security_views.ReferenceViewSet),
+router.register(r'reference', security_views.ReferenceViewSet)
router.register(r'erratum', errata_views.ErratumViewSet)
router.register(r'repo', repo_views.RepositoryViewSet)
router.register(r'mirror', repo_views.MirrorViewSet)
diff --git a/patchman/wsgi.py b/patchman/wsgi.py
index 9a9b4b7f..16f02d5a 100644
--- a/patchman/wsgi.py
+++ b/patchman/wsgi.py
@@ -19,7 +19,6 @@
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'patchman.settings') # noqa
-from django.conf import settings # noqa
-
+from django.conf import settings # noqa
application = get_wsgi_application()
diff --git a/reports/admin.py b/reports/admin.py
index a37ec4d0..66e0bf5b 100644
--- a/reports/admin.py
+++ b/reports/admin.py
@@ -16,6 +16,7 @@
# along with Patchman. If not, see
from django.contrib import admin
+
from reports.models import Report
diff --git a/reports/migrations/0004_migrate_to_tz_aware.py b/reports/migrations/0004_migrate_to_tz_aware.py
index 98176510..20510dcd 100644
--- a/reports/migrations/0004_migrate_to_tz_aware.py
+++ b/reports/migrations/0004_migrate_to_tz_aware.py
@@ -1,6 +1,7 @@
from django.db import migrations
from django.utils import timezone
+
def make_datetimes_tz_aware(apps, schema_editor):
Report = apps.get_model('reports', 'Report')
for report in Report.objects.all():
diff --git a/reports/models.py b/reports/models.py
index 6818ea23..f1e0f6f3 100644
--- a/reports/models.py
+++ b/reports/models.py
@@ -19,7 +19,7 @@
from django.urls import reverse
from hosts.utils import get_or_create_host
-from patchman.signals import error_message, info_message
+from util.logging import error_message, info_message
class Report(models.Model):
@@ -97,23 +97,25 @@ def process(self, find_updates=True, verbose=False):
""" Process a report and extract os, arch, domain, packages, repos etc
"""
if not self.os or not self.kernel or not self.arch:
- error_message.send(sender=None, text=f'Error: OS, kernel or arch not sent with report {self.id}')
+ error_message(text=f'Error: OS, kernel or arch not sent with report {self.id}')
return
if self.processed:
- info_message.send(sender=None, text=f'Report {self.id} has already been processed')
+ info_message(text=f'Report {self.id} has already been processed')
return
- from reports.utils import get_arch, get_os, get_domain
+ from reports.utils import get_arch, get_domain, get_os
arch = get_arch(self.arch)
osvariant = get_os(self.os, arch)
domain = get_domain(self.domain)
host = get_or_create_host(self, arch, osvariant, domain)
if verbose:
- info_message.send(sender=None, text=f'Processing report {self.id} - {self.host}')
+ info_message(text=f'Processing report {self.id} - {self.host}')
- from reports.utils import process_packages, process_repos, process_updates, process_modules
+ from reports.utils import (
+ process_modules, process_packages, process_repos, process_updates,
+ )
process_repos(report=self, host=host)
process_modules(report=self, host=host)
process_packages(report=self, host=host)
@@ -124,5 +126,5 @@ def process(self, find_updates=True, verbose=False):
if find_updates:
if verbose:
- info_message.send(sender=None, text=f'Finding updates for report {self.id} - {self.host}')
+ info_message(text=f'Finding updates for report {self.id} - {self.host}')
host.find_updates()
diff --git a/reports/tasks.py b/reports/tasks.py
index db9e4103..07fb3004 100755
--- a/reports/tasks.py
+++ b/reports/tasks.py
@@ -16,12 +16,12 @@
# along with Patchman. If not, see
from celery import shared_task
-
+from django.core.cache import cache
from django.db.utils import OperationalError
from hosts.models import Host
from reports.models import Report
-from util import info_message
+from util.logging import info_message, warning_message
@shared_task(bind=True, autoretry_for=(OperationalError,), retry_backoff=True, retry_kwargs={'max_retries': 5})
@@ -29,7 +29,17 @@ def process_report(self, report_id):
""" Task to process a single report
"""
report = Report.objects.get(id=report_id)
- report.process()
+ lock_key = f'process_report_lock_{report_id}'
+ # lock will expire after 1 hour
+ lock_expire = 60 * 60
+
+ if cache.add(lock_key, 'true', lock_expire):
+ try:
+ report.process()
+ finally:
+ cache.delete(lock_key)
+ else:
+ warning_message(f'Already processing report {report_id}, skipping task.')
@shared_task
@@ -48,5 +58,5 @@ def clean_reports_with_no_hosts():
for report in Report.objects.filter(processed=True):
if not Host.objects.filter(hostname=report.host).exists():
text = f'Deleting report {report.id} for Host `{report.host}` as the host no longer exists'
- info_message.send(sender=None, text=text)
+ info_message(text=text)
report.delete()
diff --git a/reports/utils.py b/reports/utils.py
index 641f90df..8b12f046 100644
--- a/reports/utils.py
+++ b/reports/utils.py
@@ -23,12 +23,18 @@
from domains.models import Domain
from hosts.models import HostRepo
from modules.utils import get_or_create_module
-from operatingsystems.utils import get_or_create_osrelease, get_or_create_osvariant
+from operatingsystems.utils import (
+ get_or_create_osrelease, get_or_create_osvariant,
+)
from packages.models import Package, PackageCategory
-from packages.utils import find_evr, get_or_create_package, get_or_create_package_update, parse_package_string
-from patchman.signals import pbar_start, pbar_update, info_message
-from repos.models import Repository, Mirror, MirrorPackage
+from packages.utils import (
+ find_evr, get_or_create_package, get_or_create_package_update,
+ parse_package_string,
+)
+from patchman.signals import pbar_start, pbar_update
+from repos.models import Mirror, MirrorPackage, Repository
from repos.utils import get_or_create_repo
+from util.logging import info_message
def process_repos(report, host):
@@ -93,7 +99,7 @@ def process_packages(report, host):
host.packages.add(package)
else:
if pkg_str[0].lower() != 'gpg-pubkey':
- info_message.send(sender=None, text=f'No package returned for {pkg_str}')
+ info_message(text=f'No package returned for {pkg_str}')
pbar_update.send(sender=None, index=i + 1)
for package in host.packages.all():
diff --git a/reports/views.py b/reports/views.py
index ccef1bb2..f247deb2 100644
--- a/reports/views.py
+++ b/reports/views.py
@@ -15,20 +15,21 @@
# You should have received a copy of the GNU General Public License
# along with Patchman. If not, see
-from tenacity import retry, retry_if_exception_type, stop_after_attempt, wait_exponential
-
-from django.http import HttpResponse, Http404
-from django.views.decorators.csrf import csrf_exempt
-from django.shortcuts import get_object_or_404, render, redirect
+from django.contrib import messages
from django.contrib.auth.decorators import login_required
-from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
-from django.urls import reverse
+from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator
from django.db.models import Q
-from django.contrib import messages
from django.db.utils import OperationalError
+from django.http import Http404, HttpResponse
+from django.shortcuts import get_object_or_404, redirect, render
+from django.urls import reverse
+from django.views.decorators.csrf import csrf_exempt
+from tenacity import (
+ retry, retry_if_exception_type, stop_after_attempt, wait_exponential,
+)
-from util.filterspecs import Filter, FilterBar
from reports.models import Report
+from util.filterspecs import Filter, FilterBar
@retry(
diff --git a/repos/admin.py b/repos/admin.py
index bea87567..a516ff8a 100644
--- a/repos/admin.py
+++ b/repos/admin.py
@@ -16,7 +16,8 @@
# along with Patchman. If not, see
from django.contrib import admin
-from repos.models import Repository, Mirror, MirrorPackage
+
+from repos.models import Mirror, MirrorPackage, Repository
class MirrorAdmin(admin.ModelAdmin):
diff --git a/repos/forms.py b/repos/forms.py
index 0800a5c3..9cb66897 100644
--- a/repos/forms.py
+++ b/repos/forms.py
@@ -15,10 +15,13 @@
# You should have received a copy of the GNU General Public License
# along with Patchman. If not, see
-from django.forms import ModelForm, ModelMultipleChoiceField, TextInput, Form, ModelChoiceField, ValidationError
from django.contrib.admin.widgets import FilteredSelectMultiple
+from django.forms import (
+ Form, ModelChoiceField, ModelForm, ModelMultipleChoiceField, TextInput,
+ ValidationError,
+)
-from repos.models import Repository, Mirror
+from repos.models import Mirror, Repository
class EditRepoForm(ModelForm):
diff --git a/repos/migrations/0001_initial.py b/repos/migrations/0001_initial.py
index a99f6878..1ae96a98 100644
--- a/repos/migrations/0001_initial.py
+++ b/repos/migrations/0001_initial.py
@@ -1,7 +1,7 @@
# Generated by Django 3.2.19 on 2023-12-11 22:15
-from django.db import migrations, models
import django.db.models.deletion
+from django.db import migrations, models
class Migration(migrations.Migration):
diff --git a/repos/migrations/0003_migrate_to_tz_aware.py b/repos/migrations/0003_migrate_to_tz_aware.py
index dddd78ba..38e30488 100644
--- a/repos/migrations/0003_migrate_to_tz_aware.py
+++ b/repos/migrations/0003_migrate_to_tz_aware.py
@@ -1,6 +1,7 @@
from django.db import migrations
from django.utils import timezone
+
def make_datetimes_tz_aware(apps, schema_editor):
Mirror = apps.get_model('repos', 'Mirror')
for mirror in Mirror.objects.all():
diff --git a/repos/models.py b/repos/models.py
index 181a103d..9b9082af 100644
--- a/repos/models.py
+++ b/repos/models.py
@@ -20,13 +20,12 @@
from arch.models import MachineArchitecture
from packages.models import Package
-from util import get_setting_of_type
-
-from repos.repo_types.deb import refresh_deb_repo
-from repos.repo_types.rpm import refresh_rpm_repo, refresh_repo_errata
from repos.repo_types.arch import refresh_arch_repo
+from repos.repo_types.deb import refresh_deb_repo
from repos.repo_types.gentoo import refresh_gentoo_repo
-from patchman.signals import info_message, warning_message, error_message
+from repos.repo_types.rpm import refresh_repo_errata, refresh_rpm_repo
+from util import get_setting_of_type
+from util.logging import error_message, info_message, warning_message
class Repository(models.Model):
@@ -72,7 +71,7 @@ def show(self):
text += f'arch: {self.arch}\n'
text += 'Mirrors:'
- info_message.send(sender=None, text=text)
+ info_message(text=text)
for mirror in self.mirror_set.all():
mirror.show()
@@ -99,10 +98,10 @@ def refresh(self, force=False):
refresh_gentoo_repo(self)
else:
text = f'Error: unknown repo type for repo {self.id}: {self.repotype}'
- error_message.send(sender=None, text=text)
+ error_message(text=text)
else:
text = 'Repo requires authentication, not updating'
- warning_message.send(sender=None, text=text)
+ warning_message(text=text)
def refresh_errata(self, force=False):
""" Refresh errata metadata for all of a repos mirrors
@@ -168,7 +167,7 @@ def show(self):
text = f' {self.id} : {self.url}\n'
text += ' last updated: '
text += f'{self.timestamp} checksum: {self.packages_checksum}\n'
- info_message.send(sender=None, text=text)
+ info_message(text=text)
def fail(self):
""" Records that the mirror has failed
@@ -178,10 +177,10 @@ def fail(self):
"""
if self.repo.auth_required:
text = f'Mirror requires authentication, not updating - {self.url}'
- warning_message.send(sender=None, text=text)
+ warning_message(text=text)
return
text = f'No usable mirror found at {self.url}'
- error_message.send(sender=None, text=text)
+ error_message(text=text)
default_max_mirror_failures = 28
max_mirror_failures = get_setting_of_type(
setting_name='MAX_MIRROR_FAILURES',
@@ -191,11 +190,11 @@ def fail(self):
self.fail_count = self.fail_count + 1
if max_mirror_failures == -1:
text = f'Mirror has failed {self.fail_count} times, but MAX_MIRROR_FAILURES=-1, not disabling refresh'
- error_message.send(sender=None, text=text)
+ error_message(text=text)
elif self.fail_count > max_mirror_failures:
self.refresh = False
text = f'Mirror has failed {self.fail_count} times (max={max_mirror_failures}), disabling refresh'
- error_message.send(sender=None, text=text)
+ error_message(text=text)
self.last_access_ok = False
self.save()
diff --git a/repos/repo_types/arch.py b/repos/repo_types/arch.py
index 6e85b153..390b321d 100644
--- a/repos/repo_types/arch.py
+++ b/repos/repo_types/arch.py
@@ -18,9 +18,13 @@
from io import BytesIO
from packages.models import PackageString
-from patchman.signals import info_message, warning_message, pbar_start, pbar_update
-from repos.utils import get_max_mirrors, fetch_mirror_data, find_mirror_url, update_mirror_packages
-from util import get_datetime_now, get_checksum, Checksum
+from patchman.signals import pbar_start, pbar_update
+from repos.utils import (
+ fetch_mirror_data, find_mirror_url, get_max_mirrors,
+ update_mirror_packages,
+)
+from util import Checksum, get_checksum, get_datetime_now
+from util.logging import info_message, warning_message
def refresh_arch_repo(repo):
@@ -34,7 +38,7 @@ def refresh_arch_repo(repo):
for i, mirror in enumerate(enabled_mirrors):
if i >= max_mirrors:
text = f'{max_mirrors} Mirrors already refreshed (max={max_mirrors}), skipping further refreshes'
- warning_message.send(sender=None, text=text)
+ warning_message(text=text)
break
res = find_mirror_url(mirror.url, [fname])
@@ -42,7 +46,7 @@ def refresh_arch_repo(repo):
continue
mirror_url = res.url
text = f'Found Arch Repo - {mirror_url}'
- info_message.send(sender=None, text=text)
+ info_message(text=text)
package_data = fetch_mirror_data(
mirror=mirror,
@@ -54,7 +58,7 @@ def refresh_arch_repo(repo):
computed_checksum = get_checksum(package_data, Checksum.sha1)
if mirror.packages_checksum == computed_checksum:
text = 'Mirror checksum has not changed, not refreshing Package metadata'
- warning_message.send(sender=None, text=text)
+ warning_message(text=text)
continue
else:
mirror.packages_checksum = computed_checksum
@@ -111,5 +115,5 @@ def extract_arch_packages(data):
packagetype='A')
packages.add(package)
else:
- info_message.send(sender=None, text='No Packages found in Repo')
+ info_message(text='No Packages found in Repo')
return packages
diff --git a/repos/repo_types/deb.py b/repos/repo_types/deb.py
index 25d8eba7..33d1f2c4 100644
--- a/repos/repo_types/deb.py
+++ b/repos/repo_types/deb.py
@@ -15,13 +15,17 @@
# along with Patchman. If not, see
import re
+
from debian.deb822 import Packages
from debian.debian_support import Version
from packages.models import PackageString
-from patchman.signals import error_message, pbar_start, pbar_update, info_message, warning_message
-from repos.utils import fetch_mirror_data, update_mirror_packages, find_mirror_url
-from util import get_datetime_now, get_checksum, Checksum, extract
+from patchman.signals import pbar_start, pbar_update
+from repos.utils import (
+ fetch_mirror_data, find_mirror_url, update_mirror_packages,
+)
+from util import Checksum, extract, get_checksum, get_datetime_now
+from util.logging import error_message, info_message, warning_message
def extract_deb_packages(data, url):
@@ -30,7 +34,7 @@ def extract_deb_packages(data, url):
try:
extracted = extract(data, url).decode('utf-8')
except UnicodeDecodeError as e:
- error_message.send(sender=None, text=f'Skipping {url} : {e}')
+ error_message(text=f'Skipping {url} : {e}')
return
package_re = re.compile('^Package: ', re.M)
plen = len(package_re.findall(extracted))
@@ -61,7 +65,7 @@ def extract_deb_packages(data, url):
packagetype='D')
packages.add(package)
else:
- info_message.send(sender=None, text='No packages found in repo')
+ info_message(text='No packages found in repo')
return packages
@@ -71,7 +75,12 @@ def refresh_deb_repo(repo):
are and then fetches and extracts packages from those files.
"""
- formats = ['Packages.xz', 'Packages.bz2', 'Packages.gz', 'Packages']
+ formats = [
+ 'Packages.xz',
+ 'Packages.bz2',
+ 'Packages.gz',
+ 'Packages',
+ ]
ts = get_datetime_now()
enabled_mirrors = repo.mirror_set.filter(refresh=True, enabled=True)
@@ -81,7 +90,7 @@ def refresh_deb_repo(repo):
continue
mirror_url = res.url
text = f'Found deb Repo - {mirror_url}'
- info_message.send(sender=None, text=text)
+ info_message(text=text)
package_data = fetch_mirror_data(
mirror=mirror,
@@ -93,7 +102,7 @@ def refresh_deb_repo(repo):
computed_checksum = get_checksum(package_data, Checksum.sha1)
if mirror.packages_checksum == computed_checksum:
text = 'Mirror checksum has not changed, not refreshing Package metadata'
- warning_message.send(sender=None, text=text)
+ warning_message(text=text)
continue
else:
mirror.packages_checksum = computed_checksum
diff --git a/repos/repo_types/gentoo.py b/repos/repo_types/gentoo.py
index 94df139a..a0584618 100644
--- a/repos/repo_types/gentoo.py
+++ b/repos/repo_types/gentoo.py
@@ -14,21 +14,28 @@
# You should have received a copy of the GNU General Public License
# along with Patchman. If not, see
-import git
import os
import shutil
import tarfile
import tempfile
-from defusedxml import ElementTree
from fnmatch import fnmatch
from io import BytesIO
from pathlib import Path
+import git
+from defusedxml import ElementTree
+
from packages.models import PackageString
from packages.utils import find_evr
-from patchman.signals import info_message, warning_message, error_message, pbar_start, pbar_update
-from repos.utils import add_mirrors_from_urls, mirror_checksum_is_valid, update_mirror_packages
-from util import extract, get_url, get_datetime_now, get_checksum, Checksum, fetch_content, response_is_valid
+from patchman.signals import pbar_start, pbar_update
+from repos.utils import (
+ add_mirrors_from_urls, mirror_checksum_is_valid, update_mirror_packages,
+)
+from util import (
+ Checksum, extract, fetch_content, get_checksum, get_datetime_now, get_url,
+ response_is_valid,
+)
+from util.logging import error_message, info_message, warning_message
def refresh_gentoo_main_repo(repo):
@@ -56,7 +63,7 @@ def refresh_gentoo_main_repo(repo):
if mirror.packages_checksum == checksum:
text = 'Mirror checksum has not changed, not refreshing Package metadata'
- warning_message.send(sender=None, text=text)
+ warning_message(text=text)
continue
res = get_url(mirror.url)
@@ -70,7 +77,7 @@ def refresh_gentoo_main_repo(repo):
mirror.fail()
continue
extracted = extract(data, mirror.url)
- info_message.send(sender=None, text=f'Found Gentoo Repo - {mirror.url}')
+ info_message(text=f'Found Gentoo Repo - {mirror.url}')
computed_checksum = get_checksum(data, Checksum.md5)
if not mirror_checksum_is_valid(computed_checksum, checksum, mirror, 'package'):
@@ -165,7 +172,7 @@ def get_gentoo_overlay_mirrors(repo_name):
if element.text.startswith('http'):
mirrors.append(element.text)
except ElementTree.ParseError as e:
- error_message.send(sender=None, text=f'Error parsing {gentoo_overlays_url}: {e}')
+ error_message(text=f'Error parsing {gentoo_overlays_url}: {e}')
return mirrors
@@ -199,7 +206,7 @@ def get_gentoo_mirror_urls():
if element.get('protocol') == 'http':
mirrors[name]['urls'].append(element.text)
except ElementTree.ParseError as e:
- error_message.send(sender=None, text=f'Error parsing {gentoo_distfiles_url}: {e}')
+ error_message(text=f'Error parsing {gentoo_distfiles_url}: {e}')
mirror_urls = []
# for now, ignore region data and choose MAX_MIRRORS mirrors at random
for _, v in mirrors.items():
@@ -226,7 +233,7 @@ def extract_gentoo_overlay_ebuilds(t):
""" Extract ebuilds from a Gentoo overlay tarball
"""
extracted_ebuilds = {}
- for root, dirs, files in os.walk(t):
+ for root, _, files in os.walk(t):
for name in files:
if fnmatch(name, '*.ebuild'):
package_name = root.replace(t + '/', '')
@@ -274,7 +281,7 @@ def extract_gentoo_packages_from_ebuilds(extracted_ebuilds):
)
packages.add(package)
plen = len(packages)
- info_message.send(sender=None, text=f'Extracted {plen} Packages', plen=plen)
+ info_message(text=f'Extracted {plen} Packages', plen=plen)
return packages
@@ -282,7 +289,7 @@ def extract_gentoo_overlay_packages(mirror):
""" Extract packages from gentoo overlay repo
"""
t = tempfile.mkdtemp()
- info_message.send(sender=None, text=f'Extracting Gentoo packages from {mirror.url}')
+ info_message(text=f'Extracting Gentoo packages from {mirror.url}')
git.Repo.clone_from(mirror.url, t, depth=1)
packages = set()
extracted_ebuilds = extract_gentoo_overlay_ebuilds(t)
diff --git a/repos/repo_types/rpm.py b/repos/repo_types/rpm.py
index aa3354c7..5ffbb708 100644
--- a/repos/repo_types/rpm.py
+++ b/repos/repo_types/rpm.py
@@ -16,11 +16,14 @@
from django.db.models import Q
-from patchman.signals import info_message, warning_message
from repos.repo_types.yast import refresh_yast_repo
from repos.repo_types.yum import refresh_yum_repo
-from repos.utils import check_for_metalinks, check_for_mirrorlists, find_mirror_url, get_max_mirrors, fetch_mirror_data
+from repos.utils import (
+ check_for_metalinks, check_for_mirrorlists, fetch_mirror_data,
+ find_mirror_url, get_max_mirrors,
+)
from util import get_datetime_now
+from util.logging import info_message, warning_message
def refresh_repo_errata(repo):
@@ -47,7 +50,7 @@ def max_mirrors_refreshed(repo, checksum, ts):
have_checksum_and_ts = repo.mirror_set.filter(mirrors_q).count()
if have_checksum_and_ts >= max_mirrors:
text = f'{max_mirrors} Mirrors already have this checksum and timestamp, skipping further refreshes'
- warning_message.send(sender=None, text=text)
+ warning_message(text=text)
return True
return False
@@ -57,10 +60,12 @@ def refresh_rpm_repo_mirrors(repo, errata_only=False):
which type of repo it is, then refreshes the mirrors
"""
formats = [
+ 'repodata/repomd.xml.zst',
'repodata/repomd.xml.xz',
'repodata/repomd.xml.bz2',
'repodata/repomd.xml.gz',
'repodata/repomd.xml',
+ 'suse/repodata/repomd.xml.zst',
'suse/repodata/repomd.xml.xz',
'suse/repodata/repomd.xml.bz2',
'suse/repodata/repomd.xml.gz',
@@ -69,7 +74,7 @@ def refresh_rpm_repo_mirrors(repo, errata_only=False):
]
ts = get_datetime_now()
enabled_mirrors = repo.mirror_set.filter(mirrorlist=False, refresh=True, enabled=True)
- for i, mirror in enumerate(enabled_mirrors):
+ for mirror in enabled_mirrors:
res = find_mirror_url(mirror.url, formats)
if not res:
mirror.fail()
@@ -85,11 +90,11 @@ def refresh_rpm_repo_mirrors(repo, errata_only=False):
if mirror_url.endswith('content'):
text = f'Found yast rpm Repo - {mirror_url}'
- info_message.send(sender=None, text=text)
+ info_message(text=text)
refresh_yast_repo(mirror, repo_data)
else:
text = f'Found yum rpm Repo - {mirror_url}'
- info_message.send(sender=None, text=text)
+ info_message(text=text)
refresh_yum_repo(mirror, repo_data, mirror_url, errata_only)
if mirror.last_access_ok:
mirror.timestamp = ts
diff --git a/repos/repo_types/yast.py b/repos/repo_types/yast.py
index 0ef54358..e37b9934 100644
--- a/repos/repo_types/yast.py
+++ b/repos/repo_types/yast.py
@@ -17,9 +17,10 @@
import re
from packages.models import PackageString
-from patchman.signals import pbar_start, pbar_update, info_message
+from patchman.signals import pbar_start, pbar_update
from repos.utils import fetch_mirror_data, update_mirror_packages
from util import extract
+from util.logging import info_message
def refresh_yast_repo(mirror, data):
@@ -65,5 +66,5 @@ def extract_yast_packages(data):
packagetype='R')
packages.add(package)
else:
- info_message.send(sender=None, text='No packages found in repo')
+ info_message(text='No packages found in repo')
return packages
diff --git a/repos/repo_types/yum.py b/repos/repo_types/yum.py
index d08c7393..1e96db39 100644
--- a/repos/repo_types/yum.py
+++ b/repos/repo_types/yum.py
@@ -15,16 +15,18 @@
# along with Patchman. If not, see
from celery import shared_task
+from django.core.cache import cache
from repos.models import Repository
+from util.logging import warning_message
@shared_task
@@ -32,5 +34,15 @@ def refresh_repos(force=False):
""" Refresh metadata for all enabled repos
"""
repos = Repository.objects.filter(enabled=True)
- for repo in repos:
- refresh_repo.delay(repo.id, force)
+ lock_key = 'refresh_repos_lock'
+ # lock will expire after 1 day
+ lock_expire = 60 * 60 * 24
+
+ if cache.add(lock_key, 'true', lock_expire):
+ try:
+ for repo in repos:
+ refresh_repo.delay(repo.id, force)
+ finally:
+ cache.delete(lock_key)
+ else:
+ warning_message('Already refreshing repos, skipping task.')
diff --git a/repos/utils.py b/repos/utils.py
index 49b5d07f..29e9cdb3 100644
--- a/repos/utils.py
+++ b/repos/utils.py
@@ -17,16 +17,24 @@
import re
from io import BytesIO
-from defusedxml import ElementTree
-from tenacity import RetryError
+from defusedxml import ElementTree
from django.db import IntegrityError
from django.db.models import Q
+from tenacity import RetryError
from packages.models import Package
-from packages.utils import convert_package_to_packagestring, convert_packagestring_to_package
-from util import get_url, fetch_content, response_is_valid, extract, get_checksum, Checksum, get_setting_of_type
-from patchman.signals import info_message, warning_message, error_message, debug_message, pbar_start, pbar_update
+from packages.utils import (
+ convert_package_to_packagestring, convert_packagestring_to_package,
+)
+from patchman.signals import pbar_start, pbar_update
+from util import (
+ Checksum, extract, fetch_content, get_checksum, get_setting_of_type,
+ get_url, response_is_valid,
+)
+from util.logging import (
+ debug_message, error_message, info_message, warning_message,
+)
def get_or_create_repo(r_name, r_arch, r_type, r_id=None):
@@ -77,7 +85,7 @@ def update_mirror_packages(mirror, packages):
package = convert_packagestring_to_package(strpackage)
mirror_package, c = MirrorPackage.objects.get_or_create(mirror=mirror, package=package)
except Package.MultipleObjectsReturned:
- error_message.send(sender=None, text=f'Duplicate Package found in {mirror}: {strpackage}')
+ error_message(text=f'Duplicate Package found in {mirror}: {strpackage}')
def find_mirror_url(stored_mirror_url, formats):
@@ -89,7 +97,7 @@ def find_mirror_url(stored_mirror_url, formats):
if mirror_url.endswith(f):
mirror_url = mirror_url[:-len(f)]
mirror_url = f"{mirror_url.rstrip('/')}/{fmt}"
- debug_message.send(sender=None, text=f'Checking for Mirror at {mirror_url}')
+ debug_message(text=f'Checking for Mirror at {mirror_url}')
try:
res = get_url(mirror_url)
except RetryError:
@@ -133,7 +141,7 @@ def get_metalink_urls(url):
if greatgreatgrandchild.attrib.get('protocol') in ['https', 'http']:
metalink_urls.append(greatgreatgrandchild.text)
except ElementTree.ParseError as e:
- error_message.send(sender=None, text=f'Error parsing metalink {url}: {e}')
+ error_message(text=f'Error parsing metalink {url}: {e}')
return metalink_urls
@@ -152,12 +160,12 @@ def get_mirrorlist_urls(url):
return
mirror_urls = re.findall(r'^http[s]*://.*$|^ftp://.*$', data.decode('utf-8'), re.MULTILINE)
if mirror_urls:
- debug_message.send(sender=None, text=f'Found mirrorlist: {url}')
+ debug_message(text=f'Found mirrorlist: {url}')
return mirror_urls
else:
- debug_message.send(sender=None, text=f'Not a mirrorlist: {url}')
+ debug_message(text=f'Not a mirrorlist: {url}')
except Exception as e:
- error_message.send(sender=None, text=f'Error attempting to parse a mirrorlist: {e} {url}')
+ error_message(text=f'Error attempting to parse a mirrorlist: {e} {url}')
def add_mirrors_from_urls(repo, mirror_urls):
@@ -172,15 +180,16 @@ def add_mirrors_from_urls(repo, mirror_urls):
existing = repo.mirror_set.filter(q).count()
if existing >= max_mirrors:
text = f'{existing} Mirrors already exist (max={max_mirrors}), not adding more'
- warning_message.send(sender=None, text=text)
+ warning_message(text=text)
break
from repos.models import Mirror
+
# FIXME: maybe we should store the mirrorlist url with full path to repomd.xml?
# that is what metalink urls return now
m, c = Mirror.objects.get_or_create(repo=repo, url=mirror_url.rstrip('/').replace('repodata/repomd.xml', ''))
if c:
text = f'Added Mirror - {mirror_url}'
- info_message.send(sender=None, text=text)
+ info_message(text=text)
def check_for_mirrorlists(repo):
@@ -193,7 +202,7 @@ def check_for_mirrorlists(repo):
mirror.mirrorlist = True
mirror.last_access_ok = True
mirror.save()
- info_message.send(sender=None, text=f'Found mirrorlist - {mirror.url}')
+ info_message(text=f'Found mirrorlist - {mirror.url}')
add_mirrors_from_urls(repo, mirror_urls)
@@ -210,7 +219,7 @@ def check_for_metalinks(repo):
mirror.mirrorlist = True
mirror.last_access_ok = True
mirror.save()
- info_message.send(sender=None, text=f'Found metalink - {mirror.url}')
+ info_message(text=f'Found metalink - {mirror.url}')
add_mirrors_from_urls(repo, mirror_urls)
@@ -249,9 +258,9 @@ def mirror_checksum_is_valid(computed, provided, mirror, metadata_type):
"""
if not computed or computed != provided:
text = f'Checksum failed for mirror {mirror.id}, not refreshing {metadata_type} metadata'
- error_message.send(sender=None, text=text)
+ error_message(text=text)
text = f'Found checksum: {computed}\nExpected checksum: {provided}'
- error_message.send(sender=None, text=text)
+ error_message(text=text)
mirror.last_access_ok = False
mirror.fail()
return False
@@ -296,9 +305,9 @@ def clean_repos():
repos = Repository.objects.filter(mirror__isnull=True)
rlen = repos.count()
if rlen == 0:
- info_message.send(sender=None, text='No Repositories with zero Mirrors found.')
+ info_message(text='No Repositories with zero Mirrors found.')
else:
- info_message.send(sender=None, text=f'Removing {rlen} empty Repositories.')
+ info_message(text=f'Removing {rlen} empty Repositories.')
repos.delete()
@@ -309,13 +318,13 @@ def remove_mirror_trailing_slashes():
mirrors = Mirror.objects.filter(url__endswith='/')
mlen = mirrors.count()
if mlen == 0:
- info_message.send(sender=None, text='No Mirrors with trailing slashes found.')
+ info_message(text='No Mirrors with trailing slashes found.')
else:
- info_message.send(sender=None, text=f'Removing trailing slashes from {mlen} Mirrors.')
+ info_message(text=f'Removing trailing slashes from {mlen} Mirrors.')
for mirror in mirrors:
mirror.url = mirror.url.rstrip('/')
try:
mirror.save()
except IntegrityError:
- warning_message.send(sender=None, text=f'Deleting duplicate Mirror {mirror.id}: {mirror.url}')
+ warning_message(text=f'Deleting duplicate Mirror {mirror.id}: {mirror.url}')
mirror.delete()
diff --git a/repos/views.py b/repos/views.py
index 199c834e..1f0c2bfa 100644
--- a/repos/views.py
+++ b/repos/views.py
@@ -15,24 +15,27 @@
# You should have received a copy of the GNU General Public License
# along with Patchman. If not, see
-from django.shortcuts import get_object_or_404, render, redirect
-from django.http import HttpResponse
-from django.contrib.auth.decorators import login_required
-from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
-from django.urls import reverse
-from django.db.models import Q
from django.contrib import messages
+from django.contrib.auth.decorators import login_required
+from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator
from django.db import IntegrityError
-
+from django.db.models import Q
+from django.http import HttpResponse
+from django.shortcuts import get_object_or_404, redirect, render
+from django.urls import reverse
from rest_framework import viewsets
-from util.filterspecs import Filter, FilterBar
+from arch.models import MachineArchitecture
from hosts.models import HostRepo
-from repos.models import Repository, Mirror, MirrorPackage
from operatingsystems.models import OSRelease
-from arch.models import MachineArchitecture
-from repos.forms import EditRepoForm, LinkRepoForm, CreateRepoForm, EditMirrorForm
-from repos.serializers import RepositorySerializer, MirrorSerializer, MirrorPackageSerializer
+from repos.forms import (
+ CreateRepoForm, EditMirrorForm, EditRepoForm, LinkRepoForm,
+)
+from repos.models import Mirror, MirrorPackage, Repository
+from repos.serializers import (
+ MirrorPackageSerializer, MirrorSerializer, RepositorySerializer,
+)
+from util.filterspecs import Filter, FilterBar
@login_required
diff --git a/requirements.txt b/requirements.txt
index dca4fe03..08ce4573 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,12 +1,11 @@
-Django==4.2.20
+Django==4.2.27
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
-chardet==5.2.0
-requests==2.32.3
+requests==2.32.4
colorama==0.4.6
djangorestframework==3.15.2
django-filter==25.1
@@ -16,7 +15,8 @@ python-magic==0.4.27
gitpython==3.1.44
tenacity==8.2.3
celery==5.4.0
-redis==5.2.1
+redis==6.4.0
django-celery-beat==2.7.0
tqdm==4.67.1
cvss==3.4
+zstandard==0.25.0
diff --git a/sbin/patchman b/sbin/patchman
index 9cc6048e..c415abec 100755
--- a/sbin/patchman
+++ b/sbin/patchman
@@ -17,32 +17,38 @@
# along with Patchman. If not, see
+import argparse
import os
import sys
-import argparse
+from django import setup as django_setup
from django.core.exceptions import MultipleObjectsReturned
from django.db.models import Count
-from django import setup as django_setup
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'patchman.settings')
from django.conf import settings # noqa
+
django_setup()
from arch.utils import clean_architectures
-from errata.utils import mark_errata_security_updates, enrich_errata, \
- scan_package_updates_for_affected_packages
from errata.tasks import update_errata
+from errata.utils import (
+ enrich_errata, mark_errata_security_updates,
+ scan_package_updates_for_affected_packages,
+)
from hosts.models import Host
+from hosts.utils import clean_tags
from modules.utils import clean_modules
-from packages.utils import clean_packages, clean_packageupdates, clean_packagenames
-from repos.models import Repository
-from repos.utils import clean_repos
+from packages.utils import (
+ clean_packagenames, clean_packages, clean_packageupdates,
+)
from reports.models import Report
from reports.tasks import clean_reports_with_no_hosts
+from repos.models import Repository
+from repos.utils import clean_repos
from security.utils import update_cves, update_cwes
-from util import set_verbosity, get_datetime_now
-from patchman.signals import info_message
+from util import get_datetime_now, set_verbosity
+from util.logging import info_message
def get_host(host=None, action='Performing action'):
@@ -63,7 +69,7 @@ def get_host(host=None, action='Performing action'):
matches = Host.objects.filter(hostname__startswith=host).count()
text = f'{matches} Hosts match hostname "{host}"'
- info_message.send(sender=None, text=text)
+ info_message(text=text)
return host_obj
@@ -83,7 +89,7 @@ def get_hosts(hosts=None, action='Performing action'):
host_objs.append(host_obj)
else:
text = f'{action} for all Hosts\n'
- info_message.send(sender=None, text=text)
+ info_message(text=text)
host_objs = Host.objects.all()
return host_objs
@@ -106,7 +112,7 @@ def get_repos(repo=None, action='Performing action', only_enabled=False):
else:
repos = Repository.objects.all()
- info_message.send(sender=None, text=text)
+ info_message(text=text)
return repos
@@ -117,9 +123,9 @@ def refresh_repos(repo=None, force=False):
repos = get_repos(repo, 'Refreshing metadata', True)
for repo in repos:
text = f'Repository {repo.id} : {repo}'
- info_message.send(sender=None, text=text)
+ info_message(text=text)
repo.refresh(force)
- info_message.send(sender=None, text='')
+ info_message(text='')
def list_repos(repos=None):
@@ -160,10 +166,10 @@ def host_updates_alt(host=None):
hosts = get_hosts(host, 'Finding updates')
ts = get_datetime_now()
for host in hosts:
- info_message.send(sender=None, text=str(host))
+ info_message(text=str(host))
if host not in updated_hosts:
host.find_updates()
- info_message.send(sender=None, text='')
+ info_message(text='')
host.updated_at = ts
host.save()
@@ -199,10 +205,10 @@ def host_updates_alt(host=None):
phost.save()
updated_hosts.append(phost)
text = f'Added the same updates to {phost}'
- info_message.send(sender=None, text=text)
+ info_message(text=text)
else:
text = 'Updates already added in this run'
- info_message.send(sender=None, text=text)
+ info_message(text=text)
def host_updates(host=None):
@@ -210,9 +216,9 @@ def host_updates(host=None):
"""
hosts = get_hosts(host, 'Finding updates')
for host in hosts:
- info_message.send(sender=None, text=str(host))
+ info_message(text=str(host))
host.find_updates()
- info_message.send(sender=None, text='')
+ info_message(text='')
def diff_hosts(hosts):
@@ -235,47 +241,47 @@ def diff_hosts(hosts):
repo_diff_AB = reposA.difference(reposB)
repo_diff_BA = reposB.difference(reposA)
- info_message.send(sender=None, text=f'+ {hostA.hostname}')
- info_message.send(sender=None, text=f'- {hostB.hostname}')
+ info_message(text=f'+ {hostA.hostname}')
+ info_message(text=f'- {hostB.hostname}')
if hostA.os != hostB.os:
- info_message.send(sender=None, text='\nOperating Systems')
- info_message.send(sender=None, text=f'+ {hostA.os}')
- info_message.send(sender=None, text=f'- {hostB.os}')
+ info_message(text='\nOperating Systems')
+ info_message(text=f'+ {hostA.os}')
+ info_message(text=f'- {hostB.os}')
else:
- info_message.send(sender=None, text='\nNo OS differences')
+ info_message(text='\nNo OS differences')
if hostA.arch != hostB.arch:
- info_message.send(sender=None, text='\nArchitecture')
- info_message.send(sender=None, text=f'+ {hostA.arch}')
- info_message.send(sender=None, text=f'- {hostB.arch}')
+ info_message(text='\nArchitecture')
+ info_message(text=f'+ {hostA.arch}')
+ info_message(text=f'- {hostB.arch}')
else:
- info_message.send(sender=None, text='\nNo Architecture differences')
+ info_message(text='\nNo Architecture differences')
if hostA.kernel != hostB.kernel:
- info_message.send(sender=None, text='\nKernels')
- info_message.send(sender=None, text=f'+ {hostA.kernel}')
- info_message.send(sender=None, text=f'- {hostB.kernel}')
+ info_message(text='\nKernels')
+ info_message(text=f'+ {hostA.kernel}')
+ info_message(text=f'- {hostB.kernel}')
else:
- info_message.send(sender=None, text='\nNo Kernel differences')
+ info_message(text='\nNo Kernel differences')
if len(package_diff_AB) != 0 or len(package_diff_BA) != 0:
- info_message.send(sender=None, text='\nPackages')
+ info_message(text='\nPackages')
for package in package_diff_AB:
- info_message.send(sender=None, text=f'+ {package}')
+ info_message(text=f'+ {package}')
for package in package_diff_BA:
- info_message.send(sender=None, text=f'- {package}')
+ info_message(text=f'- {package}')
else:
- info_message.send(sender=None, text='\nNo Package differences')
+ info_message(text='\nNo Package differences')
if len(repo_diff_AB) != 0 or len(repo_diff_BA) != 0:
- info_message.send(sender=None, text='\nRepositories')
+ info_message(text='\nRepositories')
for repo in repo_diff_AB:
- info_message.send(sender=None, text=f'+ {repo}')
+ info_message(text=f'+ {repo}')
for repo in repo_diff_BA:
- info_message.send(sender=None, text=f'- {repo}')
+ info_message(text=f'- {repo}')
else:
- info_message.send(sender=None, text='\nNo Repo differences')
+ info_message(text='\nNo Repo differences')
def delete_hosts(hosts=None):
@@ -285,7 +291,7 @@ def delete_hosts(hosts=None):
matching_hosts = get_hosts(hosts)
for host in matching_hosts:
text = f'Deleting host: {host.hostname}:'
- info_message.send(sender=None, text=text)
+ info_message(text=text)
host.delete()
@@ -299,7 +305,7 @@ def toggle_host_hro(hosts=None, host_repos_only=True):
if hosts:
matching_hosts = get_hosts(hosts, f'{toggle} host_repos_only')
for host in matching_hosts:
- info_message.send(sender=None, text=str(host))
+ info_message(text=str(host))
host.host_repos_only = host_repos_only
host.save()
@@ -314,7 +320,7 @@ def toggle_host_check_dns(hosts=None, check_dns=True):
if hosts:
matching_hosts = get_hosts(hosts, f'{toggle} check_dns')
for host in matching_hosts:
- info_message.send(sender=None, text=str(host))
+ info_message(text=str(host))
host.check_dns = check_dns
host.save()
@@ -346,7 +352,7 @@ def process_reports(host=None, force=False):
text = 'Processing Reports for all Hosts'
reports = Report.objects.filter(processed=force).order_by('created')
- info_message.send(sender=None, text=text)
+ info_message(text=text)
for report in reports:
report.process(find_updates=False)
@@ -362,6 +368,7 @@ def dbcheck(remove_duplicates=False):
clean_repos()
clean_modules()
clean_packageupdates()
+ clean_tags()
def collect_args():
diff --git a/security/admin.py b/security/admin.py
index 196a9468..aedeaea9 100644
--- a/security/admin.py
+++ b/security/admin.py
@@ -15,8 +15,8 @@
# along with Patchman. If not, see
from django.contrib import admin
-from security.models import CWE, CVSS, CVE, Reference
+from security.models import CVE, CVSS, CWE, Reference
admin.site.register(CWE)
admin.site.register(CVSS)
diff --git a/security/migrations/0001_initial.py b/security/migrations/0001_initial.py
index 5655f8b0..5f922c9a 100644
--- a/security/migrations/0001_initial.py
+++ b/security/migrations/0001_initial.py
@@ -27,7 +27,7 @@ class Migration(migrations.Migration):
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('cwe_id', models.CharField(max_length=255, unique=True)),
('name', models.CharField(blank=True, max_length=255, null=True)),
- ('description', models.CharField(blank=True, max_length=65535, null=True)),
+ ('description', models.CharField(blank=True, max_length=255, null=True)),
],
),
migrations.CreateModel(
@@ -36,7 +36,7 @@ class Migration(migrations.Migration):
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('cve_id', models.CharField(max_length=255, unique=True)),
('title', models.CharField(blank=True, max_length=255, null=True)),
- ('description', models.CharField(max_length=65535)),
+ ('description', models.CharField(max_length=255)),
('reserved_date', models.DateTimeField(blank=True, null=True)),
('published_date', models.DateTimeField(blank=True, null=True)),
('rejected_date', models.DateTimeField(blank=True, null=True)),
diff --git a/security/migrations/0005_reference_cve_references.py b/security/migrations/0005_reference_cve_references.py
index 97251add..f94cf7d5 100644
--- a/security/migrations/0005_reference_cve_references.py
+++ b/security/migrations/0005_reference_cve_references.py
@@ -15,7 +15,7 @@ class Migration(migrations.Migration):
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('ref_type', models.CharField(max_length=255)),
- ('url', models.URLField(max_length=2000)),
+ ('url', models.URLField(max_length=765)),
],
options={
'unique_together': {('ref_type', 'url')},
diff --git a/security/models.py b/security/models.py
index a847ab02..0f848260 100644
--- a/security/models.py
+++ b/security/models.py
@@ -16,20 +16,20 @@
import json
import re
-from cvss import CVSS2, CVSS3, CVSS4
from time import sleep
+from cvss import CVSS2, CVSS3, CVSS4
from django.db import models
from django.urls import reverse
from security.managers import CVEManager
-from util import get_url, fetch_content, tz_aware_datetime, error_message
+from util import error_message, fetch_content, get_url, tz_aware_datetime
class Reference(models.Model):
ref_type = models.CharField(max_length=255)
- url = models.URLField(max_length=2000)
+ url = models.URLField(max_length=765)
class Meta:
unique_together = ['ref_type', 'url']
@@ -125,19 +125,20 @@ def add_cvss_score(self, vector_string, score=None, severity=None, version=None)
score = cvss_score.base_score
if not severity:
severity = cvss_score.severities()[0]
- existing = self.cvss_scores.filter(version=version, vector_string=vector_string)
- if existing:
- cvss = existing.first()
- else:
+ try:
cvss, created = CVSS.objects.get_or_create(
version=version,
vector_string=vector_string,
score=score,
severity=severity,
)
- cvss.score = score
- cvss.severity = severity
- cvss.save()
+ except CVSS.MultipleObjectsReturned:
+ matching_cvsses = CVSS.objects.filter(
+ version=version,
+ vector_string=vector_string,
+ )
+ cvss = matching_cvsses.first()
+ matching_cvsses.exclude(id=cvss.id).delete()
self.cvss_scores.add(cvss)
def fetch_cve_data(self, fetch_nist_data=False, sleep_secs=6):
@@ -151,7 +152,7 @@ def fetch_mitre_cve_data(self):
mitre_cve_url = f'https://cveawg.mitre.org/api/cve/{self.cve_id}'
res = get_url(mitre_cve_url)
if res.status_code == 404:
- error_message.send(sender=None, text=f'404 - Skipping {self.cve_id} - {mitre_cve_url}')
+ error_message(text=f'404 - Skipping {self.cve_id} - {mitre_cve_url}')
return
data = fetch_content(res, f'Fetching {self.cve_id} MITRE data')
cve_json = json.loads(data)
@@ -161,7 +162,7 @@ def fetch_osv_dev_cve_data(self):
osv_dev_cve_url = f'https://api.osv.dev/v1/vulns/{self.cve_id}'
res = get_url(osv_dev_cve_url)
if res.status_code == 404:
- error_message.send(sender=None, text=f'404 - Skipping {self.cve_id} - {osv_dev_cve_url}')
+ error_message(text=f'404 - Skipping {self.cve_id} - {osv_dev_cve_url}')
return
data = fetch_content(res, f'Fetching {self.cve_id} OSV data')
cve_json = json.loads(data)
@@ -185,7 +186,7 @@ def fetch_nist_cve_data(self):
res = get_url(nist_cve_url)
data = fetch_content(res, f'Fetching {self.cve_id} NIST data')
if res.status_code == 404:
- error_message.send(sender=None, text=f'404 - Skipping {self.cve_id} - {nist_cve_url}')
+ error_message(text=f'404 - Skipping {self.cve_id} - {nist_cve_url}')
cve_json = json.loads(data)
self.parse_nist_cve_data(cve_json)
@@ -196,7 +197,7 @@ def parse_nist_cve_data(self, cve_json):
cve = vulnerability.get('cve')
cve_id = cve.get('id')
if cve_id != self.cve_id:
- error_message.send(sender=None, text=f'CVE ID mismatch - {self.cve_id} != {cve_id}')
+ error_message(text=f'CVE ID mismatch - {self.cve_id} != {cve_id}')
return
metrics = cve.get('metrics')
for metric, score_data in metrics.items():
diff --git a/security/tasks.py b/security/tasks.py
index a04bb1c8..7bff4149 100644
--- a/security/tasks.py
+++ b/security/tasks.py
@@ -15,8 +15,10 @@
# along with Patchman. If not, see
from celery import shared_task
+from django.core.cache import cache
from security.models import CVE, CWE
+from util.logging import warning_message
@shared_task
@@ -31,8 +33,18 @@ def update_cve(cve_id):
def update_cves():
""" Task to update all CVEs
"""
- for cve in CVE.objects.all():
- update_cve.delay(cve.id)
+ lock_key = 'update_cves_lock'
+ # lock will expire after 1 week
+ lock_expire = 60 * 60 * 168
+
+ if cache.add(lock_key, 'true', lock_expire):
+ try:
+ for cve in CVE.objects.all():
+ update_cve.delay(cve.id)
+ finally:
+ cache.delete(lock_key)
+ else:
+ warning_message('Already updating CVEs, skipping task.')
@shared_task
@@ -45,7 +57,17 @@ def update_cwe(cwe_id):
@shared_task
def update_cwes():
- """ Task to update all CWEa
+ """ Task to update all CWEs
"""
- for cwe in CWE.objects.all():
- update_cwe.delay(cwe.id)
+ lock_key = 'update_cwes_lock'
+ # lock will expire after 1 week
+ lock_expire = 60 * 60 * 168
+
+ if cache.add(lock_key, 'true', lock_expire):
+ try:
+ for cwe in CWE.objects.all():
+ update_cwe.delay(cwe.id)
+ finally:
+ cache.delete(lock_key)
+ else:
+ warning_message('Already updating CWEs, skipping task.')
diff --git a/security/views.py b/security/views.py
index 58a686b5..c9e606a6 100644
--- a/security/views.py
+++ b/security/views.py
@@ -14,17 +14,18 @@
# You should have received a copy of the GNU General Public License
# along with Patchman. If not, see
-from django.shortcuts import get_object_or_404, render
from django.contrib.auth.decorators import login_required
-from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
+from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator
from django.db.models import Q
-
+from django.shortcuts import get_object_or_404, render
from rest_framework import viewsets
-from packages.models import Package
from operatingsystems.models import OSRelease
+from packages.models import Package
from security.models import CVE, CWE, Reference
-from security.serializers import CVESerializer, CWESerializer, ReferenceSerializer
+from security.serializers import (
+ CVESerializer, CWESerializer, ReferenceSerializer,
+)
from util.filterspecs import Filter, FilterBar
diff --git a/setup.cfg b/setup.cfg
index 7af9ccb0..b1d5ee4e 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -25,6 +25,7 @@ requires = /usr/bin/python3
python3-importlib-metadata
python3-cvss
python3-redis
+ python3-zstandard
redis
celery
python3-django-celery-beat
diff --git a/setup.py b/setup.py
index d8249a67..8e18eaf9 100755
--- a/setup.py
+++ b/setup.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright 2013-2021 Marcus Furlong
+# Copyright 2013-2025 Marcus Furlong
#
# This file is part of Patchman.
#
@@ -17,7 +17,8 @@
# along with Patchman. If not, see
import os
-from setuptools import setup, find_packages
+
+from setuptools import find_packages, setup
with open('VERSION.txt', 'r', encoding='utf_8') as v:
version = v.readline().strip()
diff --git a/util/__init__.py b/util/__init__.py
index a56ed3b6..c6c9aa0d 100644
--- a/util/__init__.py
+++ b/util/__init__.py
@@ -15,30 +15,47 @@
# You should have received a copy of the GNU General Public License
# along with Patchman. If not, see
-import requests
import bz2
-import magic
-import zlib
import lzma
+import os
+import zlib
+
+import magic
+import requests
+
+try:
+ # python 3.14+ - can also remove the dependency at that stage
+ from compression import zstd
+except ImportError:
+ import zstandard as zstd
+
from datetime import datetime, timezone
from enum import Enum
from hashlib import md5, sha1, sha256, sha512
-from requests.exceptions import HTTPError, Timeout, ConnectionError
-from tenacity import retry, retry_if_exception_type, stop_after_attempt, wait_exponential
from time import time
-from tqdm import tqdm
-
-from patchman.signals import error_message, info_message, debug_message
-from django.utils.timezone import make_aware
-from django.utils.dateparse import parse_datetime
from django.conf import settings
+from django.utils.dateparse import parse_datetime
+from django.utils.timezone import make_aware
+from requests.exceptions import ConnectionError, HTTPError, Timeout
+from tenacity import (
+ retry, retry_if_exception_type, stop_after_attempt, wait_exponential,
+)
+from tqdm import tqdm
+from util.logging import debug_message, error_message, info_message
pbar = None
verbose = None
Checksum = Enum('Checksum', 'md5 sha sha1 sha256 sha512')
+http_proxy = os.getenv('http_proxy')
+https_proxy = os.getenv('https_proxy')
+proxies = {
+ 'http': http_proxy,
+ 'https': https_proxy,
+}
+
def get_verbosity():
""" Get the global verbosity level
@@ -97,7 +114,7 @@ def fetch_content(response, text='', ljust=35):
data += chunk
return data
else:
- info_message.send(sender=None, text=text)
+ info_message(text=text)
return response.content
@@ -107,21 +124,25 @@ def fetch_content(response, text='', ljust=35):
wait=wait_exponential(multiplier=1, min=1, max=10),
reraise=False,
)
-def get_url(url, headers={}, params={}):
+def get_url(url, headers=None, params=None):
""" Perform a http GET on a URL. Return None on error.
"""
response = None
+ if not headers:
+ headers = {}
+ if not params:
+ params = {}
try:
- debug_message.send(sender=None, text=f'Trying {url} headers:{headers} params:{params}')
- response = requests.get(url, headers=headers, params=params, stream=True, timeout=30)
- debug_message.send(sender=None, text=f'{response.status_code}: {response.headers}')
+ debug_message(text=f'Trying {url} headers:{headers} params:{params}')
+ response = requests.get(url, headers=headers, params=params, stream=True, proxies=proxies, timeout=30)
+ debug_message(text=f'{response.status_code}: {response.headers}')
if response.status_code in [403, 404]:
return response
response.raise_for_status()
except requests.exceptions.TooManyRedirects:
- error_message.send(sender=None, text=f'Too many redirects - {url}')
+ error_message(text=f'Too many redirects - {url}')
except ConnectionError:
- error_message.send(sender=None, text=f'Connection error - {url}')
+ error_message(text=f'Connection error - {url}')
return response
@@ -164,7 +185,7 @@ def gunzip(contents):
wbits = zlib.MAX_WBITS | 32
return zlib.decompress(contents, wbits)
except zlib.error as e:
- error_message.send(sender=None, text='gunzip: ' + str(e))
+ error_message(text='gunzip: ' + str(e))
def bunzip2(contents):
@@ -175,10 +196,10 @@ def bunzip2(contents):
return bzip2data
except IOError as e:
if e == 'invalid data stream':
- error_message.send(sender=None, text='bunzip2: ' + e)
+ error_message(text='bunzip2: ' + e)
except ValueError as e:
if e == "couldn't find end of stream":
- error_message.send(sender=None, text='bunzip2: ' + e)
+ error_message(text='bunzip2: ' + e)
def unxz(contents):
@@ -188,7 +209,17 @@ def unxz(contents):
xzdata = lzma.decompress(contents)
return xzdata
except lzma.LZMAError as e:
- error_message.send(sender=None, text='lzma: ' + e)
+ error_message(text='lzma: ' + e)
+
+
+def unzstd(contents):
+ """ unzstd contents in memory and return the data
+ """
+ try:
+ zstddata = zstd.ZstdDecompressor().stream_reader(contents).read()
+ return zstddata
+ except zstd.ZstdError as e:
+ error_message(text=f'zstd: {e}')
def extract(data, fmt):
@@ -203,6 +234,8 @@ def extract(data, fmt):
m = magic.open(magic.MAGIC_MIME)
m.load()
mime = m.buffer(data).split(';')[0]
+ if mime == 'application/zstd' or fmt.endswith('zst'):
+ return unzstd(data)
if mime == 'application/x-xz' or fmt.endswith('xz'):
return unxz(data)
elif mime == 'application/x-bzip2' or fmt.endswith('bz2'):
@@ -225,7 +258,7 @@ def get_checksum(data, checksum_type):
checksum = get_md5(data)
else:
text = f'Unknown checksum type: {checksum_type}'
- error_message.send(sender=None, text=text)
+ error_message(text=text)
return checksum
diff --git a/util/filterspecs.py b/util/filterspecs.py
index 1c845ff3..eac0f747 100644
--- a/util/filterspecs.py
+++ b/util/filterspecs.py
@@ -1,12 +1,11 @@
# Copyright 2010 VPAC
-# Copyright 2014-2021 Marcus Furlong
+# Copyright 2014-2025 Marcus Furlong
#
# This file is part of Patchman.
#
# Patchman is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
+# the Free Software Foundation, version 3 only.
#
# Patchman is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
@@ -14,12 +13,13 @@
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
-# along with Patchman If not, see .
+# along with Patchman. If not, see
-from django.utils.safestring import mark_safe
-from django.db.models.query import QuerySet
from operator import itemgetter
+from django.db.models.query import QuerySet
+from django.utils.safestring import mark_safe
+
def get_query_string(qs):
new_qs = [f'{k}={v}' for k, v in list(qs.items())]
diff --git a/util/logging.py b/util/logging.py
new file mode 100644
index 00000000..cb00ccce
--- /dev/null
+++ b/util/logging.py
@@ -0,0 +1,42 @@
+# Copyright 2025 Marcus Furlong
+#
+# This file is part of Patchman.
+#
+# Patchman is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, version 3 only.
+#
+# Patchman is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Patchman. If not, see
+
+
+from datetime import datetime
+
+from patchman.signals import (
+ debug_message_s, error_message_s, info_message_s, warning_message_s,
+)
+
+
+def info_message(text):
+ ts = datetime.now()
+ info_message_s.send(sender=None, text=text, ts=ts)
+
+
+def warning_message(text):
+ ts = datetime.now()
+ warning_message_s.send(sender=None, text=text, ts=ts)
+
+
+def debug_message(text):
+ ts = datetime.now()
+ debug_message_s.send(sender=None, text=text, ts=ts)
+
+
+def error_message(text):
+ ts = datetime.now()
+ error_message_s.send(sender=None, text=text, ts=ts)
diff --git a/util/tasks.py b/util/tasks.py
index f650e3e2..bd76bac6 100644
--- a/util/tasks.py
+++ b/util/tasks.py
@@ -18,7 +18,9 @@
from arch.utils import clean_architectures
from modules.utils import clean_modules
-from packages.utils import clean_packages, clean_packageupdates, clean_packagenames
+from packages.utils import (
+ clean_packagenames, clean_packages, clean_packageupdates,
+)
from repos.utils import clean_repos, remove_mirror_trailing_slashes
diff --git a/util/templatetags/common.py b/util/templatetags/common.py
index 6737c438..674e1721 100644
--- a/util/templatetags/common.py
+++ b/util/templatetags/common.py
@@ -1,12 +1,10 @@
-# Copyright 2010 VPAC
-# Copyright 2013-2021 Marcus Furlong
+# Copyright 2013-2025 Marcus Furlong
#
# This file is part of Patchman.
#
# Patchman is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
+# the Free Software Foundation, version 3 only.
#
# Patchman is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
@@ -14,19 +12,18 @@
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
-# along with Patchman If not, see .
+# along with Patchman. If not, see
import re
-
-from humanize import naturaltime
from datetime import datetime, timedelta
from urllib.parse import urlencode
+from django.core.paginator import Paginator
from django.template import Library
from django.template.loader import get_template
-from django.utils.html import format_html
from django.templatetags.static import static
-from django.core.paginator import Paginator
+from django.utils.html import format_html
+from humanize import naturaltime
from util import get_setting_of_type
diff --git a/util/views.py b/util/views.py
index b66db6b0..fd003bca 100644
--- a/util/views.py
+++ b/util/views.py
@@ -17,16 +17,16 @@
from datetime import datetime, timedelta
-from django.shortcuts import render
from django.contrib.auth.decorators import login_required
from django.contrib.sites.models import Site
from django.db.models import F
+from django.shortcuts import render
from hosts.models import Host
-from operatingsystems.models import OSVariant, OSRelease
-from repos.models import Repository, Mirror
+from operatingsystems.models import OSRelease, OSVariant
from packages.models import Package
from reports.models import Report
+from repos.models import Mirror, Repository
from util import get_setting_of_type