11#! /bin/bash
2+ # shellcheck disable=SC1001,SC1090,SC1091,SC2001,SC2002,SC2012,SC2013,SC2016,SC2034,SC2045,SC2046,SC2086,SC2143,SC2153,SC2154,SC2181,SC2206,SC2219,SC2236
23
34export LC_ALL=C
45export FULL_IFS=$' \t\n '
@@ -11,11 +12,12 @@ debug=false
1112report=false
1213local_updates=false
1314repo_check=true
15+ dry_run=false
1416tags=' '
1517api_key=' '
1618
1719usage () {
18- echo " ${0} [-v] [-d] [-n] [-u] [-r] [-s SERVER] [-c FILE] [-t TAGS] [-h HOSTNAME] [-p PROTOCOL] [-k API_KEY]"
20+ echo " ${0} [-v] [-d] [-n] [-u] [-y] [- r] [-s SERVER] [-c FILE] [-t TAGS] [-H HOSTNAME] [-p PROTOCOL] [-k API_KEY]"
1921 echo " -v: verbose output (default is silent)"
2022 echo " -d: debug output"
2123 echo " -n: no repo check (required when used as an apt or yum plugin)"
@@ -24,16 +26,17 @@ usage() {
2426 echo " -s SERVER: web server address, e.g. https://patchman.example.com"
2527 echo " -c FILE: config file location (default is /etc/patchman/patchman-client.conf)"
2628 echo " -t TAGS: comma-separated list of tags, e.g. -t www,dev"
27- echo " -h HOSTNAME: specify the hostname of the local host"
29+ echo " -H HOSTNAME: specify the hostname of the local host"
2830 echo " -p PROTOCOL: protocol version (1 or 2, default is 1)"
2931 echo " -k API_KEY: API key for protocol 2 authentication"
32+ echo " -y: dry run (collect data but do not submit)"
3033 echo
3134 echo " Command line options override config file options."
3235 exit 0
3336}
3437
3538parseopts () {
36- while getopts " vdnurs :c:t:h:p:k:" opt; do
39+ while getopts " vdnuyrs :c:t:h:H :p:k:" opt; do
3740 case ${opt} in
3841 v)
3942 verbose=true
@@ -48,6 +51,9 @@ parseopts() {
4851 u)
4952 local_updates=true
5053 ;;
54+ y)
55+ dry_run=true
56+ ;;
5157 r)
5258 cli_report=true
5359 ;;
@@ -60,7 +66,7 @@ parseopts() {
6066 t)
6167 cli_tags=" ${OPTARG} "
6268 ;;
63- h)
69+ h|H )
6470 cli_hostname=${OPTARG}
6571 ;;
6672 p)
@@ -121,12 +127,11 @@ check_conf() {
121127 fi
122128
123129 if [ -z " ${conf} " ] || [ ! -f " ${conf} " ] ; then
124- if ${verbose} ; then
125- echo " Warning: config file '${conf} ' not found."
126- fi
127- else
128- source " ${conf} "
130+ echo " patchman-client: config file not found: ${conf} " >&2
131+ echo " Create the config file and set server= to your patchman server." >&2
132+ exit 1
129133 fi
134+ source " ${conf} "
130135
131136 conf_dir=$( dirname " ${conf} " ) /conf.d
132137 if [ -d " ${conf_dir} " ] ; then
@@ -136,9 +141,21 @@ check_conf() {
136141 fi
137142 fi
138143
139- if [ -z " ${server} " ] && [ -z " ${cli_server} " ] ; then
140- echo ' Patchman server not set, exiting.'
141- exit 1
144+ # check server is configured and not the example placeholder
145+ if ! ${dry_run} ; then
146+ if [ -z " ${server} " ] && [ -z " ${cli_server} " ] ; then
147+ echo " patchman-client: server not configured." >&2
148+ echo " Edit ${conf} and set server= to your patchman server URL." >&2
149+ exit 1
150+ fi
151+ if [ ! -z " ${cli_server} " ] ; then
152+ server=${cli_server}
153+ fi
154+ if echo " ${server} " | grep -qE ' patchman\.example\.com' ; then
155+ echo " patchman-client: server not configured." >&2
156+ echo " Edit ${conf} and set server= to your patchman server URL." >&2
157+ exit 1
158+ fi
142159 else
143160 if [ ! -z " ${cli_server} " ] ; then
144161 server=${cli_server}
@@ -191,15 +208,15 @@ check_conf() {
191208 if [ ! -z " ${api_key} " ] ; then
192209 echo " API Key: ${api_key: 0: 12} ..."
193210 fi
194- for var in report local_updates repo_check verbose debug ; do
211+ for var in report local_updates repo_check dry_run verbose debug ; do
195212 eval val=\$ ${var}
196213 echo " ${var} : ${val} "
197214 done
198215 fi
199216}
200217
201218check_booleans () {
202- for var in report local_updates repo_check verbose debug ; do
219+ for var in report local_updates repo_check dry_run verbose debug ; do
203220 eval val=\$ ${var}
204221 if [ -z ${val} ] || [ " ${val} " == " 0" ] || [ " ${val,,} " == " false" ] ; then
205222 eval ${var} =false
@@ -490,6 +507,30 @@ get_zypper_updates() {
490507 zypper -q -n -s11 lu -r ${1} | grep ^v | awk ' {print $2"."$5,$4}' | sed -e " s/$/ ${1} /" >> " ${tmpfile_bug} "
491508}
492509
510+ get_apt_updates () {
511+ if ! check_command_exists apt ; then
512+ return
513+ fi
514+ if ${verbose} ; then
515+ echo ' Finding apt updates...'
516+ fi
517+ apt list --upgradable 2> /dev/null | grep -v ' ^Listing' | while IFS= read -r line ; do
518+ if [ -z " ${line} " ] ; then
519+ continue
520+ fi
521+ # Format: package/suite version arch [upgradable from: old-version]
522+ pkg=$( echo " ${line} " | cut -d ' /' -f 1)
523+ suite=$( echo " ${line} " | cut -d ' /' -f 2 | cut -d ' ' -f 1)
524+ version=$( echo " ${line} " | awk ' {print $2}' )
525+ arch=$( echo " ${line} " | awk ' {print $3}' )
526+ if echo " ${suite} " | grep -qi ' security' ; then
527+ echo " ${pkg} .${arch} ${version} " >> " ${tmpfile_sec} "
528+ else
529+ echo " ${pkg} .${arch} ${version} " >> " ${tmpfile_bug} "
530+ fi
531+ done
532+ }
533+
493534get_repos () {
494535 IFS=${NL_IFS}
495536
@@ -505,12 +546,12 @@ get_repos() {
505546 fi
506547 # replace this with a dedicated awk or simple python script?
507548 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" )
508- 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
549+ for i in $( echo " ${yum_repolist} " | awk ' BEGIN{n=0} { if ($1=="Repo-id") {if(n>0){print ""} n++; url=0; 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} } } } END{if(n>0) print "" }' | sed -e " s/\/'/'/g" | sed -e " s/ ' /' /" ) ; do
509550 full_id=$( echo ${i} | cut -d \' -f 2)
510551 id=$( echo ${i} | cut -d \' -f 2 | cut -d \/ -f 1)
511- name =$( echo ${i} | cut -d \' -f 4)
552+ orig_name =$( echo ${i} | cut -d \' -f 4)
512553 # Strip " - arch arch" suffix pattern to avoid duplicates like "EPEL - x86_64 x86_64"
513- name=$( echo " ${name } " | sed -e " s/ - ${host_arch} ${host_arch} $/ ${host_arch} /" )
554+ name=$( echo " ${orig_name } " | sed -e " s/ - ${host_arch} ${host_arch} $/ ${host_arch} /" )
514555 if [ " ${priorities} " != " " ] ; then
515556 priority=$( echo " ${priorities} " | grep " '${name} '" | sed -e " s/priority=\(.*\) '${name} '/\1/" )
516557 fi
@@ -528,7 +569,7 @@ get_repos() {
528569 if [ ! -z ${CPE_NAME} ] ; then
529570 id=" ${CPE_NAME} -${id} "
530571 fi
531- j=$( echo ${i} | sed -e " s#'${full_id} ' '${name } '#'${name} ' '${id} ' '${priority} '#" | sed -e " s/'\[/'/g" -e " s/\]'/'/g" )
572+ j=$( echo ${i} | sed -e " s#'${full_id} ' '${orig_name } '#'${name} ' '${id} ' '${priority} '#" | sed -e " s/'\[/'/g" -e " s/\]'/'/g" )
532573 echo " 'rpm' ${j} " >> " ${tmpfile_rep} "
533574 unset priority
534575 done
@@ -673,13 +714,23 @@ get_repos() {
673714}
674715
675716reboot_required () {
676- # On debian-based clients, the update-notifier-common
677- # package needs to be installed for this to work.
717+ # Debian/Ubuntu: update-notifier-common sets this file
678718 if [ -e /var/run/reboot-required ] ; then
679719 reboot=True
680- else
681- reboot=ServerCheck
720+ return
721+ fi
722+
723+ # Compare running vs installed kernel via /boot/vmlinuz symlink
724+ if [ -e /proc/sys/kernel/osrelease ] && [ -L /boot/vmlinuz ] ; then
725+ running_kernel=$( cat /proc/sys/kernel/osrelease)
726+ installed_kernel=$( readlink /boot/vmlinuz | sed -e ' s/^vmlinuz-//' )
727+ if [ " ${running_kernel} " != " ${installed_kernel} " ] ; then
728+ reboot=True
729+ return
730+ fi
682731 fi
732+
733+ reboot=ServerCheck
683734}
684735
685736build_packages_json () {
@@ -1083,8 +1134,78 @@ get_modules
10831134if ${repo_check} ; then
10841135 get_repos
10851136fi
1137+ if ${local_updates} ; then
1138+ get_apt_updates
1139+ fi
10861140reboot_required
10871141
1142+ if ${dry_run} ; then
1143+ echo
1144+ echo " === Dry Run Summary ==="
1145+ echo " Hostname: ${hostname} "
1146+ echo " OS: ${os} "
1147+ echo " Arch: ${host_arch} "
1148+ echo " Kernel: ${host_kernel} "
1149+ echo " Packages: $( wc -l < ${tmpfile_pkg} ) "
1150+ echo " Repos: $( wc -l < ${tmpfile_rep} ) "
1151+ echo " Modules: $( wc -l < ${tmpfile_mod} ) "
1152+ echo " Security updates: $( wc -l < ${tmpfile_sec} ) "
1153+ echo " Bugfix updates: $( wc -l < ${tmpfile_bug} ) "
1154+ echo " Reboot required: ${reboot} "
1155+ if [ ! -z " ${tags} " ] ; then
1156+ echo " Tags: ${tags} "
1157+ fi
1158+ if ${verbose} ; then
1159+ echo
1160+ echo " === Packages ==="
1161+ cut -d \' -f 2 " ${tmpfile_pkg} " | sort
1162+ echo
1163+ echo " === Repos ==="
1164+ awk -F\' ' {printf " [%s] %s\n", $2, $4}' " ${tmpfile_rep} "
1165+ if [ -s " ${tmpfile_sec} " ] ; then
1166+ echo
1167+ echo " === Security Updates ==="
1168+ awk -F\' ' {printf " %s %s\n", $2, $4}' " ${tmpfile_sec} "
1169+ fi
1170+ if [ -s " ${tmpfile_bug} " ] ; then
1171+ echo
1172+ echo " === Bugfix Updates ==="
1173+ awk -F\' ' {printf " %s %s\n", $2, $4}' " ${tmpfile_bug} "
1174+ fi
1175+ if [ -s " ${tmpfile_mod} " ] ; then
1176+ echo
1177+ echo " === Modules ==="
1178+ awk -F\' ' {printf " %s\n", $2}' " ${tmpfile_mod} "
1179+ fi
1180+ fi
1181+ if ${debug} ; then
1182+ if [ " ${protocol} " == " 2" ] && check_command_exists jq ; then
1183+ tmpfile_packages_json=$( mktemp)
1184+ tmpfile_repos_json=$( mktemp)
1185+ tmpfile_modules_json=$( mktemp)
1186+ tmpfile_sec_json=$( mktemp)
1187+ tmpfile_bug_json=$( mktemp)
1188+ echo
1189+ echo " === Full JSON Report ==="
1190+ build_json_report
1191+ else
1192+ echo
1193+ echo " === Raw Data Files ==="
1194+ echo " --- ${tmpfile_pkg} ---"
1195+ cat " ${tmpfile_pkg} "
1196+ echo " --- ${tmpfile_rep} ---"
1197+ cat " ${tmpfile_rep} "
1198+ echo " --- ${tmpfile_sec} ---"
1199+ cat " ${tmpfile_sec} "
1200+ echo " --- ${tmpfile_bug} ---"
1201+ cat " ${tmpfile_bug} "
1202+ echo " --- ${tmpfile_mod} ---"
1203+ cat " ${tmpfile_mod} "
1204+ fi
1205+ fi
1206+ exit 0
1207+ fi
1208+
10881209# Use protocol 2 (JSON) or protocol 1 (form data) based on config
10891210if [ " ${protocol} " == " 2" ] ; then
10901211 post_json_data
0 commit comments