mirror of https://github.com/vmware/tdnf.git
610 lines
14 KiB
Bash
Executable File
610 lines
14 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
|
|
#
|
|
# Copyright (C) 2020 VMware, Inc. All Rights Reserved.
|
|
#
|
|
# Licensed under the GNU General Public License v2 (the "License");
|
|
# you may not use this file except in compliance with the License. The terms
|
|
# of the License are located in the COPYING file of this distribution.
|
|
#
|
|
|
|
# File: tdnf-automatic
|
|
# Author: Shreenidhi Shedi <sshedi@vmware.com>
|
|
# Brief: Automates system updates
|
|
|
|
#set -x
|
|
|
|
EchoDbg()
|
|
{
|
|
[ "$DEBUG_TDNF_AUTOMATIC" = "1" ] && EchoErr "$@" || :
|
|
}
|
|
|
|
EchoErr()
|
|
{
|
|
echo -e "$@" 1>&2
|
|
}
|
|
|
|
ShowVersion()
|
|
{
|
|
echo "tdnf-automatic - version: @VERSION@"
|
|
}
|
|
|
|
ShowHelp()
|
|
{
|
|
local ret="$1"
|
|
local msg="\ntdnf-automatic help:"
|
|
msg+="\ntdnf-automatic [{-c|--conf config-file}(optional)] "
|
|
msg+="[{-i|--install}] [{-n|--notify}] [{-h|--help}] [{-v|--version}]\n\n"
|
|
|
|
msg+="-c, --conf\ttdnf-automatic configuration file (Optional argument)\n"
|
|
msg+="-i, --install\tOverride automatic.conf apply_updates and install updates\n"
|
|
msg+="-n, --notify\tShow available updates\n"
|
|
msg+="-h, --help\tShow this help message\n"
|
|
msg+="-v, --version\tShow tdnf-automatic version information\n"
|
|
|
|
[ "${ret}" = "0" ] && echo -e "${msg}" || EchoErr "${msg}"
|
|
|
|
exit "${ret}"
|
|
}
|
|
|
|
# function returns '1' or '0' based on boolean input
|
|
StrToBool()
|
|
{
|
|
if [ -z "$1" ]; then
|
|
echo "0"
|
|
return 0
|
|
fi
|
|
|
|
local v="$(echo "${1,,}")"
|
|
|
|
if [ "${v}" = "yes" -o "${v}" = "1" -o "${v}" = "true" -o \
|
|
"${v}" = "y" -o "${v}" = "on" -o "${v}" = "t" ]; then
|
|
echo "1"
|
|
elif [ "${v}" = "no" -o "${v}" = "0" -o "${v}" = "false" -o \
|
|
"${v}" = "n" -o "${v}" = "off" -o "${v}" = "f" ]; then
|
|
echo "0"
|
|
else
|
|
EchoErr "Invalid input(${v}) to ${FUNCNAME}..."
|
|
exit 22
|
|
fi
|
|
|
|
return 0
|
|
}
|
|
|
|
# remove spaces from beginning & end of a given string
|
|
Trim()
|
|
{
|
|
local var="$*"
|
|
# remove leading whitespace characters
|
|
var="${var#"${var%%[![:space:]]*}"}"
|
|
# remove trailing whitespace characters
|
|
var="${var%"${var##*[![:space:]]}"}"
|
|
|
|
echo -n "${var}"
|
|
}
|
|
|
|
ParseINI()
|
|
{
|
|
local line=
|
|
local CfgFile="$1"
|
|
|
|
if [ ! -s "${CfgFile}" ]; then
|
|
EchoErr "File '${CfgFile}' doesn't exist..."
|
|
exit 77
|
|
fi
|
|
|
|
EchoDbg "\n*** ${FUNCNAME} ***\n"
|
|
|
|
while IFS= read -r line; do
|
|
line="$(Trim "${line}")"
|
|
EchoDbg "[${FUNCNAME}:${LINENO}] Parsing line: '${line}'"
|
|
|
|
# skip empty lines
|
|
if [[ -z "${line}" ]]; then
|
|
continue
|
|
fi
|
|
|
|
# skip comments
|
|
if [[ "${line}" == '#'* || "${line}" == ';'* ]] ; then
|
|
continue
|
|
fi
|
|
|
|
# store data in the form of CfgData[section|key]=val
|
|
if [[ "${line}" =~ \[(.+)\] ]]; then
|
|
section="$(echo "${line}" | cut -d'[' -f2 | cut -d']' -f1)"
|
|
section="$(Trim "${section}")"
|
|
else
|
|
local key="$(echo "${section}|${line}" | cut -d'=' -f1)"
|
|
key="$(Trim "${key}")"
|
|
local val="$(echo "${line}" | cut -d'=' -f2)"
|
|
val="$(Trim "${val}")"
|
|
|
|
EchoDbg "[${FUNCNAME}:${LINENO}] Storing '${key}=${val}' into dictionary..."
|
|
CfgData["${key}"]="${val}"
|
|
fi
|
|
done < "${CfgFile}"
|
|
}
|
|
|
|
# function to add/modify key, values
|
|
CfgDataPut()
|
|
{
|
|
local k=
|
|
local key="$1"
|
|
local val="$2"
|
|
|
|
EchoDbg "\n*** ${FUNCNAME} ***\n"
|
|
EchoDbg "[${FUNCNAME}:${LINENO}] Trying store '${key}'='${val}' into dictionary..."
|
|
|
|
if [ -z "${key}" ]; then
|
|
EchoErr "Empty key to ${FUNCNAME}..."
|
|
return 22
|
|
fi
|
|
|
|
for k in "${!CfgData[@]}"; do
|
|
if [ "${key}" = "${k}" ]; then
|
|
CfgData["${key}"]="${val}"
|
|
return 0
|
|
fi
|
|
done
|
|
|
|
# New section
|
|
CfgData["${key}"]="${val}"
|
|
|
|
# validate after adding new section
|
|
ValidateCfgData
|
|
}
|
|
|
|
# function to get value from key
|
|
CfgDataGet()
|
|
{
|
|
local e=
|
|
local key="$1"
|
|
|
|
if [ -z "${key}" ]; then
|
|
EchoErr "Empty key to ${FUNCNAME}..."
|
|
return 1
|
|
fi
|
|
|
|
for e in "${!CfgData[@]}"; do
|
|
if [ "${key}" = "${e}" ]; then
|
|
echo "${CfgData[${e}]}"
|
|
return 0
|
|
fi
|
|
done
|
|
|
|
EchoDbg "[${FUNCNAME}:${LINENO}] '${key}' not found in dictionary..."
|
|
|
|
return 22
|
|
}
|
|
|
|
# display CfgData
|
|
EchoCfgData()
|
|
{
|
|
local e=
|
|
|
|
EchoDbg "\n*** ${FUNCNAME} ***\n"
|
|
|
|
for e in "${!CfgData[@]}"; do
|
|
EchoDbg -e "Key: ${e}\tValue: $(CfgDataGet "${e}")"
|
|
done
|
|
}
|
|
|
|
# validate CfgData dictionary
|
|
ValidateCfgData()
|
|
{
|
|
local e=
|
|
local key=
|
|
local val=
|
|
local err=
|
|
local errmsg=
|
|
local section=
|
|
|
|
EchoDbg "\n*** ${FUNCNAME} ***\n"
|
|
|
|
for e in "${!CfgData[@]}"; do
|
|
EchoDbg "[${FUNCNAME}:${LINENO}] Validating '${e}'..."
|
|
val="$(CfgDataGet "${e}")"
|
|
key="$(echo "${e}" | cut -d'|' -f2)"
|
|
section="$(echo "${e}" | cut -d'|' -f1)"
|
|
err=0
|
|
|
|
if [ "${section}" = "commands" ]; then
|
|
|
|
if [ "${key}" = "upgrade_type" ]; then
|
|
if [ "${val}" != "all" -a "${val}" != "security" ]; then
|
|
err=1
|
|
fi
|
|
elif [ "${key}" = "random_sleep" -o "${key}" = "network_online_timeout" ]; then
|
|
if [ "${val}" -lt 0 ]; then
|
|
err=1
|
|
fi
|
|
elif [ "${key}" = "show_updates" -o "${key}" = "apply_updates" ]; then
|
|
val="$(StrToBool "${val}")"
|
|
if [ "${val}" != "1" -a "${val}" != "0" ]; then
|
|
err=1
|
|
fi
|
|
else
|
|
err=1
|
|
fi
|
|
|
|
elif [ "${section}" = "base" ]; then
|
|
|
|
if [ "${key}" != "tdnf_conf" -o -z "${val}" ]; then
|
|
err=1
|
|
fi
|
|
|
|
elif [ "${section}" = "emitter" ]; then
|
|
|
|
if [ "${key}" = "emit_to_stdio" ]; then
|
|
val="$(StrToBool "${val}")"
|
|
if [ "${val}" != "1" -a "${val}" != "0" ]; then
|
|
err=1
|
|
fi
|
|
elif [ "${key}" = "emit_to_file" -o "${key}" = "system_name" ];then
|
|
if [ -z "${val}" ]; then
|
|
err=1
|
|
fi
|
|
else
|
|
err=1
|
|
fi
|
|
|
|
else
|
|
err=1
|
|
fi
|
|
|
|
if [ "${err}" = "1" ]; then
|
|
errmsg+="\nInvalid entry '${section}'|'${key}'='${val}'"
|
|
fi
|
|
|
|
done
|
|
|
|
if [ -n "${errmsg}" ]; then
|
|
EchoErr "${errmsg}"
|
|
exit 22
|
|
fi
|
|
}
|
|
|
|
RandomSleep()
|
|
{
|
|
local timer="$1"
|
|
|
|
if [ "${timer}" -ne 1 ]; then
|
|
return 0
|
|
fi
|
|
|
|
local rand_sleep="$(CfgDataGet "commands|random_sleep")"
|
|
|
|
if [ "${rand_sleep}" -gt 0 ]; then
|
|
local sec="$((RANDOM % rand_sleep))"
|
|
echo "Sleep for ${sec} second(s)..."
|
|
sleep "${sec}"
|
|
fi
|
|
}
|
|
|
|
RefreshCache()
|
|
{
|
|
local ret
|
|
local retstr
|
|
|
|
while true; do
|
|
retstr="$(tdnf -q -c "${TdnfConf}" --refresh makecache 2>&1)"
|
|
ret="$?"
|
|
if echo "${retstr}" | grep -qiw "ERROR: failed to acquire lock on"; then
|
|
EchoErr "Another instance of tdnf running, retrying..."
|
|
sleep 30
|
|
elif echo "${retstr}" | grep -qiw "Couldn't resolve host name"; then
|
|
EchoErr "Couldn't resolve repo server..."
|
|
return 121
|
|
fi
|
|
|
|
if [ "${ret}" != "0" ]; then
|
|
EchoErr "Failed to refresh repo cache.."
|
|
return 121
|
|
else
|
|
echo "${FUNCNAME} success..."
|
|
return 0
|
|
fi
|
|
done
|
|
}
|
|
|
|
# function to check network connectivity with update servers
|
|
WaitForNetwork()
|
|
{
|
|
local TdnfConf="$1"
|
|
|
|
EchoDbg "\n*** ${FUNCNAME} ***\n"
|
|
|
|
local Timeout="$(CfgDataGet "commands|network_online_timeout")"
|
|
if [ "${Timeout}" -le 0 ]; then
|
|
return 0
|
|
fi
|
|
|
|
if [ ! -s "${TdnfConf}" ]; then
|
|
EchoErr "Invalid tdnf config '${TdnfConf}'..."
|
|
exit 77
|
|
fi
|
|
|
|
if ! RefreshCache; then
|
|
EchoErr "Failed to refresh cache..."
|
|
return 121
|
|
fi
|
|
|
|
# get current time in epoch & add timeout seconds to it
|
|
local CurTime="$(date +%s)"
|
|
local EndTime="$((CurTime + Timeout))"
|
|
|
|
# try to connect till timeout
|
|
while [ "${CurTime}" -le "${EndTime}" ]; do
|
|
# get list of all enabled repos
|
|
local enabled_repos=($(tdnf -c "${TdnfConf}" --refresh repolist enabled 2> /dev/null | \
|
|
grep -vE "repo id|Refreshing metadata for" | cut -d' ' -f1; exit ${PIPESTATUS[0]}))
|
|
if [ "$?" != "0" ]; then
|
|
EchoErr "Failed to get enabled repos, retrying..."
|
|
elif [ "${#enabled_repos[@]}" = "0" ]; then
|
|
EchoErr "No tdnf repo is enabled, retrying..."
|
|
else
|
|
EchoDbg "[${FUNCNAME}:${LINENO}] Enabled repos: '${enabled_repos[@]}'"
|
|
return 0
|
|
fi
|
|
sleep 3
|
|
CurTime="$((CurTime + 3))"
|
|
done
|
|
|
|
EchoErr "Max timeout reached, not able to connect to any server..."
|
|
return 62
|
|
}
|
|
|
|
HandleEmitter()
|
|
{
|
|
local msg="$1"
|
|
local emit_file="$2"
|
|
local emit_to_stdio="$3"
|
|
|
|
if [ -z "${msg}" ]; then
|
|
return 0
|
|
fi
|
|
|
|
if [ "${emit_to_stdio}" = "1" ]; then
|
|
echo -e "${msg}"
|
|
fi
|
|
|
|
if [ -n "${emit_file}" ] ;then
|
|
# if target file exists already, take a backup
|
|
if [ -s "${emit_file}" ]; then
|
|
mv "${emit_file}" "${emit_file}.tdnf-automatic-$(date '+%F-%H.%M.%S').bak"
|
|
fi
|
|
echo -e "${msg}" > "${emit_file}"
|
|
fi
|
|
}
|
|
|
|
ShowOrApplyUpdates()
|
|
{
|
|
local notify="$1"
|
|
local install="$2"
|
|
local TdnfConf="$3"
|
|
local available_updates=
|
|
|
|
EchoDbg "\n*** ${FUNCNAME} ***\n"
|
|
|
|
if [ ! -s "${TdnfConf}" ]; then
|
|
EchoErr "Invalid tdnf config ${TdnfConf}..."
|
|
exit 77
|
|
fi
|
|
|
|
if ! RefreshCache; then
|
|
EchoErr "Failed to refresh cache..."
|
|
return 121
|
|
fi
|
|
|
|
# notify takes precedence over install
|
|
if [ "${notify}" = 1 -a "${install}" = 1 ]; then
|
|
install=0
|
|
elif [ "${notify}" = 0 -a "${install}" = 0 ]; then
|
|
notify=1
|
|
if [ "$(StrToBool "$(CfgDataGet "commands|show_updates")")" = "0" -a \
|
|
"$(StrToBool "$(CfgDataGet "commands|apply_updates")")" = "1" ]; then
|
|
notify=0
|
|
install=1
|
|
fi
|
|
fi
|
|
|
|
EchoDbg "[${FUNCNAME}:${LINENO}] notify: '${notify}' install: '${install}'"
|
|
|
|
local update_type="$(CfgDataGet "commands|upgrade_type")"
|
|
if [ "${update_type}" != "all" -a "${update_type}" != "security" ]; then
|
|
EchoErr "Unknown update type: ${update_type}"
|
|
return 22
|
|
fi
|
|
|
|
local emitter=0
|
|
local emit_file="$(CfgDataGet "emitter|emit_to_file")"
|
|
local emit_to_stdio="$(StrToBool "$(CfgDataGet "emitter|emit_to_stdio")")"
|
|
|
|
EchoDbg "[${FUNCNAME}:${LINENO}] Update type: '${update_type}' emit_to_stdio: '${emit_to_stdio}' emit_file: '${emit_file}'"
|
|
|
|
if [ "${emit_to_stdio}" = "1" -o -n "${emit_file}" ]; then
|
|
emitter=1
|
|
local sys_name="$(CfgDataGet "emitter|system_name")"
|
|
if [ -z "${sys_name}" ]; then
|
|
sys_name="$(hostname)"
|
|
fi
|
|
|
|
local msg="$(tdnf -c "${TdnfConf}" check-update)"
|
|
if [ "$?" != "0" ]; then
|
|
EchoErr "tdnf check-update failed..."
|
|
return 121
|
|
fi
|
|
|
|
if [ -z "${msg}" ]; then
|
|
msg="\nNo updates available on ${sys_name}. System upto date...\n"
|
|
HandleEmitter "${msg}" "${emit_file}" "${emit_to_stdio}"
|
|
return 0
|
|
fi
|
|
|
|
if [ "${update_type}" = "all" ]; then
|
|
local f="$(mktemp)"
|
|
$(tdnf -c "${TdnfConf}" upgrade --assumeno 2>&1 | grep -Ev "Total|aborted|Upgrading" > "${f}"; \
|
|
exit ${PIPESTATUS[0]})
|
|
if [ "$?" != "8" ]; then
|
|
rm -f "${f}"
|
|
EchoErr "Unexpected error while checking for updates..."
|
|
return 121
|
|
fi
|
|
|
|
local pkg=
|
|
local ver=
|
|
local tmp=
|
|
while IFS=' ' read pkg tmp ver tmp tmp tmp; do
|
|
if [ -n "${pkg}" -a -n "${ver}" ]; then
|
|
available_updates+="${pkg}-${ver}\n"
|
|
fi
|
|
done < "${f}"
|
|
rm -f "${f}"
|
|
else
|
|
available_updates="$(tdnf -c "${TdnfConf}" updateinfo info)"
|
|
if [ "$?" != "0" ]; then
|
|
EchoErr "tdnf updateinfo info failed..."
|
|
return 121
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
if [ "${install}" = 1 ]; then
|
|
[ "${update_type}" = "all" ] && tdnf -c "${TdnfConf}" upgrade -y || tdnf -c "${TdnfConf}" --security update -y
|
|
if [ "$?" != "0" ]; then
|
|
EchoErr "tdnf upgrade | tdnf --security update failed..."
|
|
return 121
|
|
fi
|
|
if [ "${emitter}" = "1" ]; then
|
|
msg="\nThe following updates are applied on - ${sys_name}:\n${available_updates}\n"
|
|
msg+="\n--- Updates completed at $(date) ---\n"
|
|
fi
|
|
elif [ "${notify}" = 1 -a "${emitter}" = "1" ]; then
|
|
msg="\nThe following updates are available on - ${sys_name}:\n${available_updates}\n"
|
|
fi
|
|
|
|
if [ "${emitter}" = "1" ]; then
|
|
HandleEmitter "${msg}" "${emit_file}" "${emit_to_stdio}"
|
|
fi
|
|
|
|
return 0
|
|
}
|
|
|
|
ExitHandler()
|
|
{
|
|
local exit_status="$?"
|
|
local msg=
|
|
|
|
msg="\ntdnf-automatic - completed with exit status ${exit_status} at $(date)...\n"
|
|
[ "${exit_status}" = "0" ] && echo -e "${msg}" || EchoErr "${msg}"
|
|
|
|
exit "${exit_status}"
|
|
}
|
|
|
|
Main()
|
|
{
|
|
local timer=0
|
|
local notify=0
|
|
local install=0
|
|
local auto_conf=
|
|
|
|
EchoDbg "\n*** ${FUNCNAME} ***\n"
|
|
|
|
EchoDbg "[${FUNCNAME}:${LINENO}] Parsing arg(s): '$@'"
|
|
# Parse arguements
|
|
while [[ $# -gt 0 ]]; do
|
|
key="$1"
|
|
|
|
case ${key} in
|
|
-c|--conf)
|
|
auto_conf="$2"
|
|
if [ -z "${auto_conf}" ]; then
|
|
ShowHelp 1
|
|
fi
|
|
|
|
shift
|
|
shift;;
|
|
-n|--notify)
|
|
notify=1
|
|
shift;;
|
|
-i|--install)
|
|
install=1
|
|
shift;;
|
|
-t|--timer)
|
|
timer=1
|
|
shift;;
|
|
-v|--version)
|
|
ShowVersion && exit 0;;
|
|
-h|--help)
|
|
ShowHelp 0;;
|
|
*)
|
|
ShowHelp 22;;
|
|
esac
|
|
done
|
|
|
|
# If no config file given, use default
|
|
if [ -z "${auto_conf}" ]; then
|
|
auto_conf="/etc/tdnf/automatic.conf"
|
|
fi
|
|
|
|
if [ ! -s "${auto_conf}" ]; then
|
|
EchoErr "Configuration file: '${auto_conf}' does not exist"
|
|
exit 2
|
|
fi
|
|
|
|
# Parse the config file
|
|
ParseINI "${auto_conf}"
|
|
ValidateCfgData
|
|
EchoCfgData
|
|
|
|
local TdnfConf="$(CfgDataGet "base|tdnf_conf")"
|
|
if [ -z "${TdnfConf}" ]; then
|
|
TdnfConf="/etc/tdnf/tdnf.conf"
|
|
fi
|
|
|
|
EchoDbg "[${FUNCNAME}:${LINENO}] Automatic conf: '${auto_conf}' tdnf conf: '${TdnfConf}'\n"
|
|
|
|
if [ ! -s "${auto_conf}" -o ! -s "${TdnfConf}" ]; then
|
|
EchoErr "Automatic conf:'${auto_conf}' || tdnf conf '${TdnfConf}' does not exist..."
|
|
exit 2
|
|
fi
|
|
|
|
RandomSleep "${timer}"
|
|
|
|
if ! WaitForNetwork "${TdnfConf}"; then
|
|
EchoErr "System is off-line..."
|
|
exit 64
|
|
fi
|
|
|
|
if ! ShowOrApplyUpdates "${notify}" "${install}" "${TdnfConf}"; then
|
|
EchoErr "Failed to Show/Apply updates..."
|
|
exit 121
|
|
fi
|
|
|
|
exit 0
|
|
}
|
|
|
|
if [ ${EUID} -ne 0 ]; then
|
|
EchoErr "tdnf-automatic - this script must be run as root..."
|
|
exit 13
|
|
fi
|
|
|
|
if ! which tdnf &> /dev/null; then
|
|
EchoErr "tdnf command not found..."
|
|
exit 65
|
|
fi
|
|
|
|
if [ "${BASH_VERSINFO:-0}" -lt 4 ]; then
|
|
EchoErr "Need bash verion >=4 to use this script..."
|
|
exit 125
|
|
fi
|
|
|
|
echo -e "\ntdnf-automatic - started at $(date)...\n"
|
|
|
|
trap ExitHandler EXIT
|
|
|
|
# array to hold config data
|
|
declare -A CfgData
|
|
|
|
Main "$@"
|