Skip to content

Commit b0482d2

Browse files
authored
Merge pull request #661 from furlongm/develop (patchman 4)
patchman 4
2 parents c08e72a + c3fe0e9 commit b0482d2

File tree

194 files changed

+8006
-2569
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

194 files changed

+8006
-2569
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,5 @@ dist
1414
run
1515
pyvenv.cfg
1616
.vscode
17+
.venv
18+
*.xml

TODO

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,21 @@
1-
* allow sending updates from Red Hat / SuSE machines
2-
* web interface support for updating repos, finding updates
31
* add checkrestart-style options to see which services need restarting
4-
* CVE/OVAL apps
2+
* OVAL/OSCAP apps
53
* CA support (tinyca?)
6-
* native python client, using apt/yum/debian libraries
4+
* native python/go client, using apt/yum/debian libraries
75
* record the history of installed packages on a host
86
* also store package descriptions/tags/urls
97
* check for unused repos
108
* suggest names for repos with the same checksum
119
* helper script to change paths (e.g. /usr/lib/python3/dist-packages/patchman)
1210
* Dockerfile/Dockerimage
1311
* compressed reports
14-
* add cronjobs to built packages
15-
* install celery/rabbit/memcache with packages
12+
* add cronjobs to build packages
13+
* dnf5 support
14+
* proxy support
15+
* GLSA support
16+
* only use date for errata issue date?
17+
* parallelize package extraction
18+
* use django-tables2
19+
* autonaming for deb repos
20+
* associate repos with gentoo hosts
21+
* populate authenticated repos with package lists from hosts?

arch/utils.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# Copyright 2025 Marcus Furlong <furlongm@gmail.com>
2+
#
3+
# This file is part of Patchman.
4+
#
5+
# Patchman is free software: you can redistribute it and/or modify
6+
# it under the terms of the GNU General Public License as published by
7+
# the Free Software Foundation, version 3 only.
8+
#
9+
# Patchman is distributed in the hope that it will be useful,
10+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
# GNU General Public License for more details.
13+
#
14+
# You should have received a copy of the GNU General Public License
15+
# along with Patchman. If not, see <http://www.gnu.org/licenses/>
16+
17+
from arch.models import PackageArchitecture, MachineArchitecture
18+
from patchman.signals import info_message
19+
20+
21+
def clean_package_architectures():
22+
""" Remove package architectures that are no longer in use
23+
"""
24+
parches = PackageArchitecture.objects.filter(package__isnull=True)
25+
plen = parches.count()
26+
if plen == 0:
27+
info_message.send(sender=None, text='No orphaned PackageArchitectures found.')
28+
else:
29+
info_message.send(sender=None, text=f'Removing {plen} orphaned PackageArchitectures')
30+
parches.delete()
31+
32+
33+
def clean_machine_architectures():
34+
""" Remove machine architectures that are no longer in use
35+
"""
36+
marches = MachineArchitecture.objects.filter(
37+
host__isnull=True,
38+
repository__isnull=True,
39+
)
40+
mlen = marches.count()
41+
if mlen == 0:
42+
info_message.send(sender=None, text='No orphaned MachineArchitectures found.')
43+
else:
44+
info_message.send(sender=None, text=f'Removing {mlen} orphaned MachineArchitectures')
45+
marches.delete()
46+
47+
48+
def clean_architectures():
49+
""" Remove architectures that are no longer in use
50+
"""
51+
clean_package_architectures()
52+
clean_machine_architectures()

client/patchman-client

Lines changed: 76 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -297,10 +297,20 @@ get_installed_archlinux_packages() {
297297
fi
298298
}
299299

300+
get_installed_gentoo_packages() {
301+
if check_command_exists qkeyword ; then
302+
gentoo_package_arch=$(qkeyword -A)
303+
fi
304+
if check_command_exists qlist ; then
305+
qlist -Ic -F "'%{PN}' '%{SLOT}' '%{PV}' REL'%{PR}' '${gentoo_package_arch}' 'gentoo' '%{CAT}' '%{REPO}'" | sed -e "s/REL'r/'/g" >> "${tmpfile_pkg}"
306+
fi
307+
}
308+
300309
get_packages() {
301310
get_installed_rpm_packages
302311
get_installed_deb_packages
303312
get_installed_archlinux_packages
313+
get_installed_gentoo_packages
304314
}
305315

306316
get_modules() {
@@ -346,7 +356,9 @@ get_host_data() {
346356
os="${PRETTY_NAME}"
347357
elif [ "${ID}" == "arch" ] ; then
348358
os="${NAME}"
349-
elif [[ "${ID}" =~ "suse" ]] ; then
359+
elif [ "${ID}" == "gentoo" ] ; then
360+
os="${PRETTY_NAME} ${VERSION_ID}"
361+
elif [[ "${ID_LIKE}" =~ "suse" ]] ; then
350362
os="${PRETTY_NAME}"
351363
elif [ "${ID}" == "astra" ] ; then
352364
os="${NAME} $(cat /etc/astra_version)"
@@ -386,6 +398,9 @@ get_host_data() {
386398
fi
387399
done
388400
fi
401+
if [ ! -z "${CPE_NAME}" ] ; then
402+
os="${os} [${CPE_NAME}]"
403+
fi
389404
if ${verbose} ; then
390405
echo "Kernel: ${host_kernel}"
391406
echo "Arch: ${host_arch}"
@@ -452,7 +467,7 @@ get_repos() {
452467
fi
453468
# replace this with a dedicated awk or simple python script?
454469
yum_repolist=$(yum repolist enabled --verbose 2>/dev/null | sed -e "s/:\? *([0-9]\+ more)$//g" -e "s/ ([0-9]\+$//g" -e "s/:\? more)$//g" -e "s/'//g" -e "s/%/%%/g")
455-
for i in $(echo "${yum_repolist}" | awk '{ if ($1=="Repo-id") {printf "'"'"'"; for (i=3; i<NF; i++) printf $i " "; printf $NF"'"'"' "} if ($1=="Repo-name") {printf "'"'"'"; for (i=3; i<NF; i++) printf $i " "; printf $NF"'" ${host_arch}'"' "} if ($1=="Repo-mirrors" || $1=="Repo-metalink:") {printf "'"'"'"; for (i=3; i<NF; i++) printf $i " "; printf $NF"'"'"' "} if ($1=="Repo-baseurl" || $1=="Repo-baseurl:") { url=1; comma=match($NF,","); if (comma) out=substr($NF,1,comma-1); else out=$NF; printf "'"'"'"out"'"'"' "; } else { if (url==1) { if ($1==":") { comma=match($NF,","); if (comma) out=substr($NF,1,comma-1); else out=$NF; printf "'"'"'"out"'"'"' "; } else {url=0; print "";} } } }' | sed -e "s/\/'/'/g" | sed -e "s/ ' /' /") ; do
470+
for i in $(echo "${yum_repolist}" | awk '{ if ($1=="Repo-id") {printf "'"'"'"; for (i=3; i<NF; i++) printf $i " "; printf $NF"'"'"' "} if ($1=="Repo-name") {printf "'"'"'"; for (i=3; i<NF; i++) printf $i " "; printf $NF"'" ${host_arch}'"' "} if ($1=="Repo-mirrors" || $1=="Repo-metalink") {printf "'"'"'"; for (i=3; i<NF; i++) printf $i " "; printf $NF"'"'"' "} if ($1=="Repo-baseurl" || $1=="Repo-baseurl:") { url=1; comma=match($NF,","); if (comma) out=substr($NF,1,comma-1); else out=$NF; printf "'"'"'"out"'"'"' "; } else { if (url==1) { if ($1==":") { comma=match($NF,","); if (comma) out=substr($NF,1,comma-1); else out=$NF; printf "'"'"'"out"'"'"' "; } else {url=0; print "";} } } }' | sed -e "s/\/'/'/g" | sed -e "s/ ' /' /") ; do
456471
full_id=$(echo ${i} | cut -d \' -f 2)
457472
id=$(echo ${i} | cut -d \' -f 2 | cut -d \/ -f 1)
458473
name=$(echo ${i} | cut -d \' -f 4)
@@ -463,7 +478,7 @@ get_repos() {
463478
if [ "${priority}" == "" ] ; then
464479
priority=99
465480
fi
466-
redhat_repo=$(echo ${i} | grep -e "https://.*/XMLRPC.*\|https://cdn.redhat.com/.*")
481+
redhat_repo=$(echo ${i} | grep -e "https://.*/XMLRPC.*\|https://cdn[-[a-z]*]*.redhat.com/.*")
467482
if [ ${?} == 0 ] || ${local_updates} ; then
468483
if ${verbose} ; then
469484
echo "Finding updates locally for ${id}"
@@ -473,7 +488,7 @@ get_repos() {
473488
if [ ! -z ${CPE_NAME} ] ; then
474489
id="${CPE_NAME}-${id}"
475490
fi
476-
j=$(echo ${i} | sed -e "s#'${full_id}' '${name}'#'${name}' '${id}' '${priority}'#")
491+
j=$(echo ${i} | sed -e "s#'${full_id}' '${name}'#'${name}' '${id}' '${priority}'#" | sed -e "s/'\[/'/g" -e "s/\]'/'/g")
477492
echo "'rpm' ${j}" >> "${tmpfile_rep}"
478493
unset priority
479494
done
@@ -484,7 +499,8 @@ get_repos() {
484499
if ${verbose} ; then
485500
echo 'Finding apt repos...'
486501
fi
487-
IFS=${FULL_IFS} read -r osname shortversion <<<$(echo "${os}" | awk '{print $1,$2}' | cut -d . -f 1,2)
502+
osname=$(echo ${os} | cut -d " " -f 1)
503+
shortversion=${VERSION_ID}
488504
repo_string="'deb\' \'${osname} ${shortversion} ${host_arch} repo at"
489505
repos=$(apt-cache policy | grep -v Translation | grep -E "^ *[0-9]{1,5}" | grep -E " mirror\+file|http(s)?:" | sed -e "s/^ *//g" -e "s/ *$//g" | cut -d " " -f 1,2,3,4)
490506
non_mirror_repos=$(echo "${repos}" | grep -Ev "mirror\+file")
@@ -510,11 +526,11 @@ get_repos() {
510526
echo 'Finding zypper repos...'
511527
fi
512528
if [ $(zypper -q --no-refresh lr --details | head -n 1 | grep Keep) ] ; then
513-
zypper_lr_cols="2,3,8,10"
529+
zypper_lr_cols='{print "${os}" $3 "|" $2 "|" $8 "|" $10}'
514530
else
515-
zypper_lr_cols="2,3,7,9"
531+
zypper_lr_cols='{print "${os}" $3 "|" $2 "|" $7 "|" $9}'
516532
fi
517-
for i in $(zypper -q --no-refresh lr -E -u --details | grep -v ^$ | tail -n +3 | cut -d "|" -f ${zypper_lr_cols} | sed -e "s/ *|/ ${host_arch} |/" -e "s/\?[a-zA-Z0-9_-]* *$//" -e "s/^ /'/g" -e "s/ *| */' '/g" -e "s/ *$/'/g") ; do
533+
for i in $(zypper -q --no-refresh lr -E -u --details | grep -v ^$ | tail -n +3 | awk -F"|" "${zypper_lr_cols}" | sed -e "s/\${os}/${PRETTY_NAME}/" -e "s/ *|/ ${host_arch} |/" -e "s/\?[a-zA-Z0-9_-]* *$//" -e "s/^/'/g" -e "s/ *| */' '/g" -e "s/ *$/'/g") ; do
518534
echo \'rpm\' ${i} >> "${tmpfile_rep}"
519535
id=$(echo ${i} | cut -d \' -f 4)
520536
suse_repo=$(echo ${i} | grep -e "https://updates.suse.com/.*")
@@ -557,6 +573,56 @@ get_repos() {
557573
done
558574
fi
559575

576+
# Gentoo
577+
if [[ "${os}" =~ "Gentoo" ]] ; then
578+
if [ ${verbose} == 1 ] ; then
579+
echo 'Finding portage repos...'
580+
fi
581+
declare -A repo_info
582+
repos_output=$(portageq repos_config /)
583+
repo_name=""
584+
priority=""
585+
sync_uri=""
586+
587+
while IFS= read -r line; do
588+
# if the line starts with a section header (e.g., [gentoo], [guru]), it's the repo name
589+
if [[ "${line}" =~ ^\[(.*)\] ]]; then
590+
# if we already have a repo_name, save the previous entry
591+
if [[ -n "${repo_name}" && -n "${sync_uri}" ]]; then
592+
repo_info["${repo_name}"]="${priority},${sync_uri}"
593+
fi
594+
# else start new repo parsing, resetting vars
595+
repo_name="${BASH_REMATCH[1]}"
596+
priority=""
597+
sync_uri=""
598+
fi
599+
600+
# if the line contains "priority", extract the value, 0 if it doesnt exist
601+
if [[ "${line}" =~ "priority" ]]; then
602+
priority=$(echo "${line}" | cut -d'=' -f2 | xargs)
603+
fi
604+
605+
# if the line contains "sync-uri", extract the value
606+
if [[ "${line}" =~ "sync-uri" ]]; then
607+
sync_uri=$(echo "${line}" | cut -d'=' -f2 | xargs)
608+
fi
609+
done <<< "${repos_output}"
610+
611+
# save the last repository entry if it's available
612+
if [[ -n "${repo_name}" && -n "${sync_uri}" ]]; then
613+
repo_info["${repo_name}"]="${priority},${sync_uri}"
614+
fi
615+
616+
for repo in "${!repo_info[@]}"; do
617+
priority=$(echo ${repo_info[$repo]} | cut -d',' -f1)
618+
sync_uri=$(echo ${repo_info[$repo]} | cut -d',' -f2)
619+
if [ "${priority}" == "" ] ; then
620+
priority=0
621+
fi
622+
echo "'gentoo' 'Gentoo Linux ${repo} Repo ${host_arch}' '${repo}' '${priority}' '${sync_uri}'" >> "${tmpfile_rep}"
623+
done
624+
fi
625+
560626
IFS=${FULL_IFS}
561627

562628
sed -i -e '/^$/d' "${tmpfile_rep}"
@@ -629,10 +695,11 @@ post_data() {
629695
}
630696

631697
if ! check_command_exists which || \
698+
! check_command_exists awk || \
632699
! check_command_exists mktemp || \
633700
! check_command_exists curl || \
634701
! check_command_exists flock ; then
635-
echo "which, mktemp, flock or curl was not found, exiting."
702+
echo "which, awk, mktemp, flock or curl was not found, exiting."
636703
exit 1
637704
fi
638705

debian/control

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,11 @@ Homepage: https://github.com/furlongm/patchman
1616
Depends: ${misc:Depends}, python3 (>= 3.10), python3-django (>= 3.2),
1717
python3-django-tagging, python3-django-extensions, python3-django-bootstrap3,
1818
python3-djangorestframework, python3-django-filters, python3-debian,
19-
python3-rpm, python3-progressbar, python3-lxml, python3-defusedxml,
19+
python3-rpm, python3-tqdm, python3-lxml, python3-defusedxml,
2020
python3-requests, python3-colorama, python3-magic, python3-humanize,
21-
python3-pip, python3-pymemcache, python3-yaml, memcached, libapache2-mod-wsgi-py3, apache2
21+
python3-pip, python3-pymemcache, python3-yaml, memcached, libapache2-mod-wsgi-py3,
22+
apache2, python3-django-taggit
23+
>>>>>>> 922796e (switch from obsolete django-tagging to django-taggit)
2224
Suggests: python3-django-celery, python3-mysqldb, python3-psycopg2
2325
Description: Django-based patch status monitoring tool for linux systems.
2426
.
@@ -41,7 +43,7 @@ Description: Django-based patch status monitoring tool for linux systems.
4143
Package: patchman-client
4244
Architecture: all
4345
Homepage: https://github.com/furlongm/patchman
44-
Depends: ${misc:Depends}, curl, debianutils, util-linux, coreutils
46+
Depends: ${misc:Depends}, curl, debianutils, util-linux, coreutils, mawk
4547
Description: Client for the patchman monitoring system.
4648
.
4749
The client will send a list of packages and repositories to the upstream

debian/python3-patchman.install

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
#!/usr/bin/dh-exec
22
etc/patchman/apache.conf.example => etc/apache2/conf-available/patchman.conf
33
etc/patchman/local_settings.py etc/patchman
4+
etc/systemd/system/patchman-celery.service => lib/systemd/system/patchman-celery.service

debian/python3-patchman.postinst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ if [ "$1" = "configure" ] ; then
2020

2121
patchman-manage makemigrations
2222
patchman-manage migrate --run-syncdb --fake-initial
23+
sqlite3 /var/lib/patchman/db/patchman.db 'PRAGMA journal_mode=WAL;'
2324

2425
chown -R www-data:www-data /var/lib/patchman
2526

errata/admin.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Copyright 2025 Marcus Furlong <furlongm@gmail.com>
2+
#
3+
# This file is part of Patchman.
4+
#
5+
# Patchman is free software: you can redistribute it and/or modify
6+
# it under the terms of the GNU General Public License as published by
7+
# the Free Software Foundation, version 3 only.
8+
#
9+
# Patchman is distributed in the hope that it will be useful,
10+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
# GNU General Public License for more details.
13+
#
14+
# You should have received a copy of the GNU General Public License
15+
# along with Patchman. If not, see <http://www.gnu.org/licenses/>
16+
17+
from django.contrib import admin
18+
from errata.models import Erratum
19+
20+
21+
class ErratumAdmin(admin.ModelAdmin):
22+
readonly_fields = ('affected_packages', 'fixed_packages', 'references')
23+
24+
25+
admin.site.register(Erratum, ErratumAdmin)

errata/apps.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Copyright 2025 Marcus Furlong <furlongm@gmail.com>
2+
#
3+
# This file is part of Patchman.
4+
#
5+
# Patchman is free software: you can redistribute it and/or modify
6+
# it under the terms of the GNU General Public License as published by
7+
# the Free Software Foundation, version 3 only.
8+
#
9+
# Patchman is distributed in the hope that it will be useful,
10+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
# GNU General Public License for more details.
13+
#
14+
# You should have received a copy of the GNU General Public License
15+
# along with Patchman. If not, see <http://www.gnu.org/licenses/>
16+
17+
from django.apps import AppConfig
18+
19+
20+
class ErrataConfig(AppConfig):
21+
name = 'errata'

errata/managers.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Copyright 2025 Marcus Furlong <furlongm@gmail.com>
2+
#
3+
# This file is part of Patchman.
4+
#
5+
# Patchman is free software: you can redistribute it and/or modify
6+
# it under the terms of the GNU General Public License as published by
7+
# the Free Software Foundation, version 3 only.
8+
#
9+
# Patchman is distributed in the hope that it will be useful,
10+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
# GNU General Public License for more details.
13+
#
14+
# You should have received a copy of the GNU General Public License
15+
# along with Patchman. If not, see <http://www.gnu.org/licenses/>
16+
17+
from django.db import models
18+
19+
20+
class ErratumManager(models.Manager):
21+
def get_queryset(self):
22+
return super().get_queryset().select_related()

0 commit comments

Comments
 (0)