Skip to content

Commit

Permalink
Merge pull request #126 from m0dular/BOLT-1104-linux-package-task-ref…
Browse files Browse the repository at this point in the history
…actor

Bolt 1104 linux package task refactor
  • Loading branch information
donoghuc committed May 17, 2019
2 parents 7f6f5fd + d16ff3e commit 7c455e6
Show file tree
Hide file tree
Showing 5 changed files with 153 additions and 112 deletions.
43 changes: 43 additions & 0 deletions files/common.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
#!/bin/bash

# TODO: helper task?

# Exit with an error message and error code, defaulting to 1
fail() {
# Print a message: entry if there were anything printed to stderr
if [[ -s $_tmp ]]; then
# Hack to try and output valid json by replacing newlines with spaces.
error_data="{ \"msg\": \"$(tr '\n' ' ' <"$_tmp")\", \"kind\": \"bash-error\", \"details\": {} }"
else
error_data="{ \"msg\": \"Task error\", \"kind\": \"bash-error\", \"details\": {} }"
fi
echo "{ \"status\": \"failure\", \"_error\": $error_data }"
exit ${2:-1}
}

validation_error() {
error_data="{ \"msg\": \""$1"\", \"kind\": \"bash-error\", \"details\": {} }"
echo "{ \"status\": \"failure\", \"_error\": $error_data }"
exit 255
}

success() {
echo "$1"
exit 0
}

# Test for colors. If unavailable, unset variables are ok
if tput colors &>/dev/null; then
green="$(tput setaf 2)"
red="$(tput setaf 1)"
reset="$(tput sgr0)"
fi

_tmp="$(mktemp)"
exec 2>>"$_tmp"

# Use indirection to munge PT_ environment variables
# e.g. "$PT_version" becomes "$version"
for v in ${!PT_*}; do
declare "${v#*PT_}"="${!v}"
done
2 changes: 1 addition & 1 deletion spec/acceptance/linux_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ def bolt_config
apply_manifest_on(default, "package { 'rsyslog': ensure => present, }")
result = run_task('package::linux', 'default', 'action' => 'uninstall', 'name' => 'rsyslog')
expect(result[0]).to include('status' => 'success')
expect(result[0]['result']).to include('status' => %r{not install|deinstall})
expect(result[0]['result']).to include('status' => %r{not install|deinstall|uninstall})
end
end

Expand Down
2 changes: 1 addition & 1 deletion tasks/init.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"implementations": [
{"name": "init.rb", "requirements": ["puppet-agent"]},
{"name": "windows.ps1", "requirements": ["powershell"], "input_method": "powershell"},
{"name": "linux.sh", "requirements": ["shell"], "input_method": "environment"}
{"name": "linux.sh", "requirements": ["shell"], "input_method": "environment", "files": ["package/files/common.sh"]}
],
"extensions": {
"discovery": {
Expand Down
6 changes: 5 additions & 1 deletion tasks/linux.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,9 @@
"description": "The provider to use to manage or inspect the package, defaults to the system package manager. Only used when the 'puppet-agent' feature is available on the target so we can leverage Puppet.",
"type": "Optional[String[1]]"
}
}
},
"implementations": [
{"name": "linux.sh", "requirements": ["shell"], "files": ["package/files/common.sh"], "input_method": "environment"}
]

}
212 changes: 103 additions & 109 deletions tasks/linux.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,37 +2,60 @@

# example: bolt task run package::linux action=install name=rsyslog

# Exit with an error message and error code, defaulting to 1
fail() {
# Print a message: entry if there were anything printed to stderr
if [[ -s $_tmp ]]; then
# Hack to try and output valid json by replacing newlines with spaces.
error_data="{ \"msg\": \"$(tr '\n' ' ' <$_tmp)\", \"kind\": \"bash-error\", \"details\": {} }"
# $1 = string literal to feed to dpkg-query. include json formatting
# No error check here because querying the status of uninstalled packages will return non-zero
# Use --showformat to make a json object with status and version
# ${name%%=*} removes the version in case we installed a specific one
# This will print nothing to stdout if the package is not installed
apt_status() {
dpkg-query --show --showformat="$1" "${name%%=*}"
}

# Determine if newer package is available to mirror the ruby/puppet implementation
apt_check_latest() {
installed="$(apt_status '${Version}')"
[[ $installed ]] || success '{ "status": "uninstalled", "version": ""}'

candidate="$(apt-cache policy "$name" | grep 'Candidate:')"
candidate="${candidate#*: }"

if [[ $installed != $candidate ]]; then
cmd_status="$(apt_status "{ \"status\":\"\${Status}\", \"version\":\"${installed}\", \"latest\":\"${candidate}\" }")"
else
error_data="{ \"msg\": \"Task error\", \"kind\": \"bash-error\", \"details\": {} }"
cmd_status="$(apt_status '{ "status":"${Status}", "version":"${Version}" }')"
fi
echo "{ \"status\": \"failure\", \"_error\": $error_data }"
exit ${2:-1}

success "$cmd_status"
}

success() {
echo "$1"
exit 0
# $1 = string literal to feed to rpm -q. include json formatting
# Use --queryformat to make a json object with status and version
# yum returns non-zero if the package isn't installed
yum_status() {
rpm -q --queryformat "$1" "$provided_package"
}

validation_error() {
error_data="{ \"msg\": \""$1"\", \"kind\": \"bash-error\", \"details\": {} }"
echo "{ \"status\": \"failure\", \"_error\": $error_data }"
exit 255
# Determine if newer package is available to mirror the ruby/puppet implementation
yum_check_latest() {
installed="$(yum_status "%{VERSION}-%{RELEASE}" "$provided_package")" || success '{ "status": "uninstalled", "version": "" }'
candidate=($(yum check-update --quiet "${name}"))

# format of check-update is <package_name> <release>
# i.e rpm.x86_64 4.11.3-35.el7
if [[ $candidate && $installed != ${candidate[1]} ]]; then
cmd_status="$(yum_status \
"{ \"status\": \"installed\", \"version\": \"$installed\", \"latest\": \"${candidate[1]}\" \}")"
else
cmd_status="$(yum_status '\{ "status": "installed", "version": "%{VERSION}-%{RELEASE}" \}')" || {
cmd_status='{ "status": "uninstalled", "version": "" }'
}
fi
success "$cmd_status"
}

# Keep stderr in a temp file. Easier than `tee` or capturing process substitutions
_tmp="$(mktemp)"
exec 2>"$_tmp"
declare PT__installdir
source "$PT__installdir/package/files/common.sh"

action="$PT_action"
name="$PT_name"
version="$PT_version"
package_managers=("apt-get" "yum")
options=()

Expand All @@ -57,114 +80,85 @@ case "$available_manager" in
# <package>=<version> is the syntax for installing a specific version in apt
[[ $version ]] && name="${name}=${version}"

# Use --showformat to make a json object with status and version
# ${name%%=*} removes the version in case we installed a specific one
# This will print nothing to stdout if the package is not installed
# For Ruby compatability, 'status' and 'install' will check if the package is upgradable
case "$action" in
"status")
apt_check_latest
;;

"install")
apt-get install "$name" "${options[@]}" >/dev/null || fail
apt_check_latest
;;

# uninstall: do not print any version information
"uninstall")
action="remove"
apt-get remove "$name" "${options[@]}" >/dev/null || fail
cmd_status="$(apt_status '{ "status":"${Status}" }')" || cmd_status='{ "status": "uninstalled" }'
success "$cmd_status"
;;

"upgrade")
action="install"
options+=("--only-upgrade")
old_version="$(dpkg-query --show --showformat='"${Version}"' ${name%%=*})"
esac
# Get the currently installed version to compare with the upgraded version
old_version="$(apt_status '"${Version}"')"

[[ $action == "status" ]] || DEBIAN_FRONTEND=noninteractive "apt-get" "$action" "$name" "${options[@]}" >/dev/null || fail

# uninstall: do not print any version information
if [ "$PT_action" == 'uninstall' ]; then
cmd_status="$(dpkg-query --show --showformat='{ "status":"${Status}" }' ${name%%=*})"
[[ $cmd_status ]] || cmd_status='{ "status": "uninstalled"}'
success "$cmd_status"
fi

# upgrade: only show version information
if [ "$PT_action" == 'upgrade' ]; then
new_version="$(dpkg-query --show --showformat='"${Version}"' ${name%%=*})"
cmd_status="{ \"old_version\":"${old_version}", \"version\":"${new_version}" }"
success "$cmd_status"
fi

# Determine if newer package is available to mirror the ruby/puppet implementation
installed="$(apt-cache policy $PT_name | grep 'Installed:')"
installed="${installed#*: }"

candidate="$(apt-cache policy $PT_name | grep 'Candidate:')"
candidate="${candidate#*: }"

if [[ $installed != $candidate ]]; then
pkg_status="$(dpkg-query --show --showformat='"${Status}"' ${name%%=*})"
pkg_version="$(dpkg-query --show --showformat='"${Version}"' ${name%%=*})"
cmd_status="{ \"status\":"${pkg_status}", \"version\":"${pkg_version}", \"latest\":\""${candidate}"\" }"
else
cmd_status="$(dpkg-query --show --showformat='{ "status":"${Status}", "version":"${Version}" }' ${name%%=*})"
fi

[[ $cmd_status ]] || cmd_status='{ "status": "uninstalled", "version": "" }'
success "$cmd_status"
apt-get install "$name" "${options[@]}" >/dev/null || fail
# For Ruby compatability, this command uses a different output format
cmd_status="$(apt_status "{ \"old_version\": ${old_version}, \"version\": \"\${Version}\" }")"
success "$cmd_status"
esac
;;

"yum")
# assume yes
options+=("-y")

# yum install <pkg> and rpm -q <pkg> may produce different results because one package may provide another
# For example, 'vim' can be installed because the 'vim-enhanced' package provides 'vim'
# So, find out the exact package to get the status for
provided_package="$(rpm -q --whatprovides "$name")" || provided_package=

# <package>-<version> is the syntax for installing a specific version in yum
[[ $version ]] && name="${name}-${version}"

# Use --queryformat to make a json object with status and version
# yum is ok with including the version in the package name
# yum returns non-zero if the package isn't installed
# For Ruby compatibility, 'status' and 'install' will check if the package is upgradable
case "$action" in
"status")
yum_check_latest
;;

"install")
yum install "$name" "${options[@]}" >/dev/null || fail
# Check for this again after installing
provided_package="$(rpm -q --whatprovides "$name")" || provided_package=
yum_check_latest
;;

"uninstall")
action="remove"
yum remove "$name" "${options[@]}" >/dev/null || fail
cmd_status="$(yum_status '\{ "status": "installed", "version": "%{VERSION}-%{RELEASE}" \}')" || {
cmd_status='{ "status": "uninstalled" }'
}
success "$cmd_status"
;;

"upgrade")
possible_version="$(rpm -q --whatprovides "$name")" || possible_version="uninstalled"
if [ "$possible_version" == "uninstalled" ]; then
old_version="\"uninstalled\""
if [[ $provided_package ]]; then
old_version="$(yum_status '"%{VERSION}-%{RELEASE}"')"
else
old_version="$(rpm -q --queryformat '"%{VERSION}"' "$possible_version")"
old_version='"uninstalled"'
fi
esac

[[ $action == "status" ]] || "yum" "$action" "$name" "${options[@]}" >/dev/null || fail
# Why does yum upgrade not return non-zero on a nonexistent package...
# Because of this, check the output
yum_out=$(yum upgrade "$name" "${options[@]}") || fail
[[ $yum_out =~ "No package $name available" ]] && fail

# yum install <pkg> and rpm -q <pkg> may produce different results because one package may provide another
# For example, 'vim' can be installed because the 'vim-enhanced' package provides 'vim'
# So, find out the exact package to get the status for
package="$(rpm -q --whatprovides "$name")"

# uninstall: do not print any version information
if [ "$PT_action" == 'uninstall' ]; then
rpm_status="$(rpm -q "$package")"
cmd_status="{ \"status\": \"$rpm_status\"}"
[[ $cmd_status ]] || cmd_status='{ "status": "uninstalled"}'
success "$cmd_status"
fi

# upgrade: only show version information
if [ "$PT_action" == 'upgrade' ]; then
new_version="$(rpm -q --queryformat '"%{VERSION}"' "$package")" || new_version="\"uninstalled\""
cmd_status="{ \"old_version\":"${old_version}", \"version\":"${new_version}" }"
success "$cmd_status"
fi

# Determine if newer package is available to mirror the ruby/puppet implementation
upgradable_list=($(yum check-update --quiet "${PT_name}"))
upgradable="${upgradable_list[@]}"
if [ -n "${upgradable}" ]; then
if rpm -q --whatprovides "$name" &>/dev/null; then
pkg_version="$(rpm -q --queryformat '"%{VERSION}"' "$package")"
cmd_status="{ \"status\":\"installed\", \"version\":"${pkg_version}", \"latest\":\""${upgradable}"\" }"
else
cmd_status='{ "status": "uninstalled", "version": "" }'
fi
else
cmd_status="$(rpm -q --queryformat '\{ "status": "installed", "version": "%{VERSION}" \}' "$package")" || {
cmd_status='{ "status": "uninstalled", "version": "" }'
}
fi

success "$cmd_status"
# For Ruby compatibility, this command uses a different output format
# Check for this again after installing
provided_package="$(rpm -q --whatprovides "$name")" || provided_package=
cmd_status="$(yum_status "\{ \"old_version\": ${old_version}, \"version\": \"%{VERSION}-%{RELEASE}\" \}")"
success "$cmd_status"
esac
esac

0 comments on commit 7c455e6

Please sign in to comment.