-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcheckunits.sh
More file actions
executable file
·360 lines (313 loc) · 15.2 KB
/
Copy pathcheckunits.sh
File metadata and controls
executable file
·360 lines (313 loc) · 15.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
#!/bin/bash
# Version and Commit ID
# shellcheck disable=SC2016
COMMIT='$Id$'
VERSION="1.2"
# Check the shell version
if [ -z "${BASH_VERSINFO[*]}" ] || [ "${BASH_VERSINFO[0]}" -lt 4 ]; then
echo "At least Bash version 4 required."
exit 2
fi
# Tweak some settings
shopt -sq nullglob
# Output font and color definitions
# If the output is not a terminal, do not use any control sequences.
if [ -t 1 ]; then
fontBold=$(tput -S <<< $'smul')
fontInfo=$(tput -S <<< $'setaf 0\nsetab 6')
fontRemark=$(tput -S <<< $'setaf 3\nbold')
fontWarn=$(tput -S <<< $'setaf 0\nsetab 3')
fontError=$(tput -S <<< $'setaf 0\nsetab 1')
fontCode=$(tput -S <<< $'bold')
fontReset=$(tput -S <<< $'sgr0')
fontDone=$(tput -S <<< $'setaf 2\nbold')
fontDoneRemarks=$(tput -S <<< $'setaf 3\nbold')
else
fontBold=''
fontInfo=''
fontRemark=''
fontWarn=''
fontError=''
fontCode=''
fontReset=''
fontDone=''
fontDoneRemarks=''
fi
# Outputs usage information.
function Usage () {
# Use fmt to format the output. Prevent lines from being joined
# together to allow better formatting.
fmt -t -s <<- END
CheckUnits v${VERSION} (${COMMIT:5:10})
usage:
checkunits.sh [-p] [-c] [-r <Restarts>] [-s] [-v] [-i <Unit>] [-h]
This shell script checks the systemd configuration of a modern Linux system and makes suggestions to optimize the use of systemd.
It knows the following options:
END
# This lines might be joined to create a propper parameter list.
fmt -t <<- END
-p
Report if the enabled/disabled state of the unit does not equal the preset state.
-c
Report units that where stopped because they are in conflict with an other unit.
-r
Allows to specify the number of restarts after which a warning is issued for a unit.
-i
Ignores the given unit. This option can be passed multiple times to ignore multiple units.
-s
Disables the version warning and the summary output if no remarks where shown.
-e
Show errors only. This also enables -s. Use this for sending e-mails, etc.
-v
Show additional information massages that are usefull in some cases.
-h
Display usage info.
END
# Non joind output again
fmt -t -s <<- END
Depending on the result of the operation the script sets one of the following exit codes:
END
# Joined output for list (numbers must be right aligned)
fmt -t <<- END
0
The script completed successfully and without remarks.
127
The script completed successfully, but with remarks.
1
Invalid command line option.
2
Unsupported bash version. Use bash 4.0 or newer.
END
}
# Checks the state of the unit file by using a bunch of global variables.
# Globals: unitInfo, ignoreUnits, sdUnitPath, checkPresets, showConflicted, warnRestarts, verbose, errorsOnly
# These checks are based on the information in https://www.freedesktop.org/wiki/Software/systemd/dbus/
# The return code of this function is the number of output remarks.
function CheckState () {
local remarks=()
[ "${unitInfo['UnitFileState']}" == 'transient' ] && return 0
# Check if the unit file of this unit could be found. If that's the case, all other checks make no sense and we only warn about the missing file.
if [ "${unitInfo['LoadState']}" == "not-found" ]; then
while IFS='' read -rs -d' ' unitPath; do
if [ -d "${unitPath}" ]; then
for unitLink in "${unitPath}"/*/"${unitInfo['Id']}"; do
[ -r "${unitLink}" ] || remarks+=("E: The symlink for this unit in ${unitLink%/*} is missing its destination unit file.:Maybe you have uninstalled the corresponding application and want to remove the symlink via [[rm ${unitLink}]].")
done;
fi;
done <<< "${sdUnitPath#*=} " # Mind the space at the end!
else
for ignoredUnit in "${ignoreUnits[@]}"; do
[ "${ignoredUnit}" == "${unitInfo['Id']}" ] && return 0
done
# Determin the class of the unit from the extension of the name
unitClass="${unitInfo['Id']##*.}"
# Map the current ActiveState of the unit to a more simple ActiveStateClass to simplify
# the rest of the checks.
case "${unitInfo['ActiveState']}" in
'active'|'reloading'|'activating') simpleState='active' ;;
'inactive'|'deactivating') simpleState='inactive' ;;
'failed') simpleState='failed' ;;
esac
# Map the multiple UnitFileState values to a simplified set for testing.
case "${unitInfo['UnitFileState']}" in
'enabled'|'linked') simpleUnitFileState='enabled' ;;
'enabled-runtime'|'linked-runtime'|'masked'|'masked-runtime'|'disabled') simpleUnitFileState='disabled' ;;
# Catch "invalid", "static" and empty ("")
*) simpleUnitFileState="${unitInfo['UnitFileState']}"
esac
# Check for failed units.
[ "${simpleState}" == 'failed' ] && remarks+=("E: Unit is is failed state.:Check why it has failed using [[systemctl status ${unitInfo['Id']}]] or use [[journalctl -le -u ${unitInfo['Id']}]] to view the log. If everything is ok but you don't want to restart the unit, you can use [[systemctl reset-failed ${unitInfo['Id']}]] to reset the failed state.")
# Check for restarted units.
if [ -n "${unitInfo['NRestarts']}" ]; then
local restartSeverity=""
# If we are in verbose mode, we output an information for restartet units.
[ "$verbose" -gt 0 ] && [ "${unitInfo['NRestarts']}" -gt 0 ] && restartSeverity='I'
# If the -r option was given: Check for units with too many restarts and output a warning.
[ "$warnRestarts" -gt 0 ] && [ "${unitInfo['NRestarts']}" -ge "$warnRestarts" ] && restartSeverity='W'
[ -n "$restartSeverity" ] && remarks+=("$restartSeverity: The Unit ${unitInfo['Id']} was automatically restarted ${unitInfo['NRestarts']} times.:You can check the logs via [[journalctl -le -u ${unitInfo['Id']}]] or reset the restart counter using [[systemctl reset-failed ${unitInfo['Id']}]].")
fi
# If the service-unit has a sourcePath that points to /etc/init.d it's a generated legacy unit.
# THe Unit file state "generated" can not be used here because it's currently not documented.
[ -n "${unitInfo['SourcePath']}" ] && [ "${unitInfo['SourcePath']:0:11}" == '/etc/init.d' ] && [ "${unitClass}" == 'service' ] && remarks+=("I: The unit is a legacy unit generated by systemd.:Consider migrating the init script [[${unitInfo['SourcePath']}]] to a real systemd unit.")
# Units triggered by timer units should be static
# Check each trigger if it's a timer
local triggeredByTimer=0
while IFS='' read -rs -d' ' trigger; do
[ "${trigger##*.}" == "timer" ] && triggeredByTimer=1 && break
done <<< "${unitInfo['TriggeredBy']} " # Mind the space at the end!
if [ "${triggeredByTimer}" -gt 0 ] && [ "${unitInfo['UnitFileState']}" != "static" ]; then
remarks+=("W: A unit file triggered by a timer should be static.:The unit is started by a timer and should not need an install section. Mostly you can remove the[[ [Install] ]]section from the unit file in [[${unitInfo['FragmentPath']}]] or create an override with [[systemctl edit ${unitInfo['Id']}]] for package provided units.")
fi
# In verbose mode, check if a unit triggered by a timer is disabled by a condition and provide an information for that case.
if [ "${verbose}" -gt 0 ] && [ "${triggeredByTimer}" -gt 0 ] && [ -n "${unitInfo['ConditionTimestamp']}" ] && [ "${unitInfo['ConditionResult']}" == 'no' ]; then
remarks+=("I: This unit is triggered by a timer but a condition does not allow it to run.:If you think the unit should be running check the condition via [[systemctl cat ${unitInfo['Id']}]].")
fi
# If this unit was enabled, but is not active and the ConflictedBy value is set, we check if any of the
# conflicting units is running. If that's the case the conflicted variable is set.
conflicted=0
if [ "${simpleState}" == 'inactive' ] && [ "${simpleUnitFileState}" == 'enabled' ] && [ -n "${unitInfo['ConflictedBy']}" ]; then
while IFS='' read -r -s -d' ' conflict; do
if systemctl -q is-active "${conflict}"; then
conflicted=1
[ "${showConflicted}" -gt 0 ] && remarks+=("I: Unit is stopped due to a conflict with unit ${conflict}.")
break
fi
done <<< "${unitInfo['ConflictedBy']} " # Mind the space at the end of this string!
fi
case "${simpleUnitFileState}" in
'enabled')
# Only check the preset, if the unit this was enabled and
# if the unit is not masked (because presets do not make sense for masked units.)
if [ "${checkPresets}" -gt 0 ] && [ "${unitInfo['LoadState']}" != 'masked' ]; then
[ "${simpleUnitFileState}" == "${unitInfo['UnitFilePreset']}" ] || remarks+=("W: Unit is enabled but preset wants it to be ${unitInfo['UnitFilePreset']}.:Create a preset file in [[/etc/systemd/system-preset/]] containing [[enable ${unitInfo['Id']}]] to change the preset to enabled or disable the unit via [[systemctl disable ${unitInfo['Id']}]]. For more information about presets use [[man systemd.preset]].")
fi
# If the unit is enabled, it should not be inactive. If it's in failed state, we've already reported this.
# If the unit is conflicted, we do not report this, because someone wanted the unit to be off now.
if [ "${simpleState}" == 'inactive' ] && [ ${conflicted} -eq 0 ]; then
# Check if the unit was disabled by a condition. Because that would be ok.
if [ -z "${unitInfo['ConditionTimestamp']}" ] || [ "${unitInfo['ConditionResult']}" != 'no' ]; then
# If the unit is of type oneshot and ramainAfterExit is no and it exited successfully (because if the simpleState where
# "failed" we wouldn't be here) then everything went as planned and we can ignore the inactive unit.
# The condition is a little awkward because it's negated.
if [ "${unitInfo['Type']}" != 'oneshot' ] || [ "${unitInfo['RemainAfterExit']}" != 'no' ]; then
remarks+=("W: Unit is enabled but not active.:Use [[systemctl start ${unitInfo['Id']}]] to start the unit.")
fi
else
[ "${verbose}" -gt 0 ] && remarks+=("I: Unit ${unitInfo['Id']} is disabled by a failed condition.:Use [[systemctl cat ${unitInfo['Id']}]] to show the unit file and check for unsatisified conditions.")
fi
fi
;;
'disabled')
# See enabled
if [ "${checkPresets}" -gt 0 ] && [ "${unitInfo['LoadState']}" != 'masked' ]; then
[ "${simpleUnitFileState}" == "${unitInfo['UnitFilePreset']}" ] || remarks+=("W: Unit is disabled but preset wants it to be ${unitInfo['UnitFilePreset']}.:Create a preset file in [[/etc/systemd/system-preset/]] containing [[disable ${unitInfo['Id']}]] to change the preset to disabled or enable the unit via [[systemctl enable ${unitInfo['Id']}]]. For more information about presets use [[man systemd.preset]]..")
fi
# If this unit is active, check if any units that want this unit are active.
# If that's the case, this unit may be active, because it was started by another unit.
local activelyWanted=0
if [ "${simpleState}" == 'active' ] && [ -n "${unitInfo['WantedBy']}" ]; then
for service in ${unitInfo['WantedBy']}; do
if systemctl is-active --quiet "${service}"; then
activelyWanted=1
# If we're in verbose mode, list the units wanting this unit.
[ "${verbose}" -gt 0 ] && remarks+=("I: Unit ${unitInfo['Id']} is disabled but active because it is wanted by the active unit ${service}.")
fi
done
fi
# If the unit is disabled, it should be inactive as long as it's not triggered by another unit or by dbus.
# For the dbus units we should check that there is really dbus activation registered. But communicating
# with the dbus service and checking the configuration is beyond the scope this script.
[ "${simpleState}" == 'active' ] && [ -z "${unitInfo['TriggeredBy']}" ] && [ "${activelyWanted}" -eq 0 ] && [ "${unitInfo['Type']}" != 'dbus' ] && remarks+=("W: Unit is disabled but ${unitInfo['ActiveState']}.:The unit will not start automatically on next reboot. If the unit should not be active, use [[systemctl stop ${unitInfo['Id']}]] to stop the unit. If the start of this unit was intentional, use [[systemctl enable ${unitInfo['Id']}]] to enable it permanently.")
;;
'invalid')
;;
'static')
;;
esac
fi
# End of checks. Start of output routine
local messageCounter=0
if [ "${#remarks[@]}" -gt 0 ]; then
for remark in "${remarks[@]}"; do
IFS=":" read -r severity msg suggestion <<< "${remark}"
# Filter messages that should not be shown here:
[ "${errorsOnly}" -gt 0 ] && [ "${severity}" != "E" ] && continue
# Output the header before the first remark. This prevents a header from being output if
# all remarks are filtered.
[ "${messageCounter}" == 0 ] && echo "Remarks for unit ${fontBold}${unitInfo['Id']}${fontReset}:"
((messageCounter++))
case "${severity}" in
I) echo -en "${fontInfo}[ INFO ]" ;;
W) echo -en "${fontWarn}[WARNING]" ;;
E) echo -en "${fontError}[ ERROR ]" ;;
esac
echo -en "${fontReset}"
echo "${msg}"
if [ -n "${suggestion}" ]; then
# Escaping the brackets is not neccessary but it makes the syntax highlighting of Sublime Text happy
suggestion="${suggestion//\[\[/${fontCode}}"
suggestion="${suggestion//\]\]/${fontReset}}"
echo -e "${suggestion}${fontReset}"
fi
done
fi
[ "${messageCounter}" -gt 0 ] && echo
return "${messageCounter}"
}
# Parse command line argument
declare -a ignoreUnits
checkPresets=0
showConflicted=0
warnRestarts=0
silent=0
verbose=0
errorsOnly=0
while getopts "pcr:svehi:" opt; do
case "$opt" in
'p')
checkPresets=1
;;
'c')
showConflicted=1
;;
'r')
if ! [[ "$OPTARG" =~ ^[1-9][0-9]*$ ]]; then
echo "Invalid argument for -r. Only positive numbers are allowed."
exit 1
fi
warnRestarts=$OPTARG
;;
's')
silent=1
;;
'i')
ignoreUnits+=("$OPTARG")
;;
'v')
verbose=1
;;
'e')
errorsOnly=1
silent=1
;;
'h')
Usage
exit 0
;;
'?')
exit 1
;;
esac
done
[ "${silent}" -eq 0 ] && echo "CheckUnits v${VERSION} (${COMMIT:5:10})..."
# Check the systemd version
IFS=" " read -rs _ version _ < <(systemctl --version)
if [ "${version}" -lt 239 ] && [ "${silent}" -eq 0 ]; then
if [ "${verbose}" -eq 1 ]; then
echo -e "${fontRemark}This system uses systemd version $version. This script has been tested with systemd 239 and above. The output may be incorrect or some information may be missing.${fontReset}"
else
echo -e "${fontRemark}Only systemd 239 and above supported."
fi
fi
# Gather some global information about systemd
sdUnitPath=$(systemctl show -p UnitPath)
# Check unit file info
declare -A unitInfo
messageCount=0
while IFS="=" read -r key value; do
if [ -z "${key}" ]; then
CheckState; ((messageCount+=$?))
unset unitInfo; declare -A unitInfo
else
unitInfo["${key}"]="${value}"
fi
done < <(systemctl show -p Id -p Type -p NRestarts -p RemainAfterExit -p UnitFileState -p UnitFilePreset -p ActiveState -p TriggeredBy -p WantedBy -p ConflictedBy -p SourcePath -p LoadState -p ConditionResult -p ConditionTimestamp -p FragmentPath '*')
CheckState; ((messageCount+=$?))
if [ "$messageCount" -gt 0 ]; then
echo -e "${fontDone}Check completed. ${fontDoneRemarks}$messageCount remarks.${fontReset}"
else
[ "${silent}" -eq 0 ] && echo -e "${fontDone}Check completed without remarks.${fontReset} This does not mean everything will work as expected ;)"
fi
# If everything went well, the exit code is 0
# If messages where output, the exit code is 127
[ "${messageCount}" -gt 0 ] && exit 127 || exit 0