#!/bin/bash ################################################################################ # # # netoik-cicd-deployer # # # # This binary is made to be run by root, it expects a request from an # # unprivileged user server, deploys (installs or updates) the related rpm # # package and sends a response to the user. # # # # This script is coded with respect of google shell styleguide: # # https://google.github.io/styleguide/shellguide.html # # # ################################################################################ # Exit immediately if any command fails. set -e # Exit with the last non-zero fail code. set -o pipefail # Program constants. readonly PACKAGE_NAME="netoik-cicd" readonly PROGRAM_NAME="${PACKAGE_NAME}-deployer" # Declare default variables. readonly DEFAULT_CONFIG_FILE="/etc/${PACKAGE_NAME}/${PACKAGE_NAME}.conf" readonly DEFAULT_ERRORS_FILE="/etc/${PACKAGE_NAME}/errors.conf" # Declare levels variables. readonly VERBOSITY_LEVELS=( [10]="CRITICAL" [20]="ERROR" [30]="WARN" [40]="INFO" [50]="DEBUG" ) readonly DEFAULT_VERBOSITY_LEVEL=40 # RPM constants. readonly RPM_RELEASE="1" RPM_ARCH=$(rpm --eval "%{_arch}") readonly RPM_ARCH RPM_DIST=$(rpm --eval "%{dist}") readonly RPM_DIST # Functions. log() ( level="$1" msg="$2" if [ "${level}" -le "${verbosity_level}" ]; then echo "${VERBOSITY_LEVELS[${level}]} - ${msg}" fi ) log_critical() (log 10 "$1" >&2) log_error() (log 20 "$1" >&2) log_warn() (log 30 "$1" >&2) log_info() (log 40 "$1") log_debug() (log 50 "$1") fail() ( msg="$1" if [ $# -eq 2 ]; then code="$2" else code=1 fi log_critical "${msg}" if [ "${code}" -gt 0 ]; then exit "${code}" else log_error "Undefined error code for '${msg}'." exit 1 fi ) usage() ( echo "Usage: ${PROGRAM_NAME} [OPTION]... Start deployer server, wait for unixsock requests to deploy rpm packages. Mandatory argumentes for long options are mandatory for short options too. -c, --conf= name of configuration file, default to ${DEFAULT_CONFIG_FILE} -d, --daemon run processing loop in background -e, --errs= name of errors file, default to ${DEFAULT_ERRORS_FILE} -q, --quiet set level verbosity to WARN, default to INFO -s, --stop stop listening after first request process -t, --test just test config file and do not run start loop -v, --verbose set level verbosity to DEBUG, default to INFO -h, --help display this help message and exit " ) reply() ( code="$1" msg="$2" if [ "${code}" -gt 0 ]; then log_error "${msg}" else log_info "${msg}" fi if ! output="$(jq --null-input \ --compact-output \ --arg c "${code}" \ --arg m "${msg}" \ '{"code":$c|tonumber,"msg":$m}' | ncat --unixsock "${response_sock}" 2>&1)"; then log_error "Cannot write to sock '${response_sock}': ${output}." fi ) process_request() ( # Get response_sock from json. log_debug "Get response sock file." if ! response_sock="$(echo "${request}" | jq --raw-output ".response_sock" 2>&1)"; then log_error "Cannot parse json request '${request}': ${response_sock}." return fi if [ "${response_sock}" = "null" ]; then log_error "Missing key 'response_sock' in request '${request}'." return fi if [ -z "${response_sock}" ]; then log_error "Empty value for key 'response_sock' in request '${request}'." return fi if [ ! -S "${response_sock}" ]; then log_error \ "Sock file '${response_sock}' does not exist in request '${request}'." return fi log_debug "Sock file '${response_sock}' is reachable." # Get pkg_name from json. log_debug "Get pkg_name from json." if ! pkg_name="$(echo "${request}" | jq --raw-output ".pkg_name" 2>&1)"; then reply "${err_json_bad_format}" \ "Cannot parse json '${request}': ${pkg_name}." return fi if [ "${pkg_name}" = "null" ]; then reply "${err_pkg_name_missing}" "Missing key pkg_name in '${request}'." return fi if [ -z "${pkg_name}" ]; then reply "${err_pkg_name_empty}" \ "Empty value for key pkg_name in '${request}'." return fi log_debug "Got pkg name: '${pkg_name}'." # Get repository folder. repo_dir="${repos_dir}/${pkg_name}.git" if [ ! -d "${repo_dir}" ]; then reply "${err_repo_dir_not_exist}" "Repository '${repo_dir}' does not exist." return fi log_debug "Repository is present at '${repo_dir}'." # Get pkg_version from json. log_debug "Get pkg_version from json." if ! pkg_version="$(echo "${request}" | jq --raw-output ".pkg_version" 2>&1)"; then reply "${err_json_bad_format}" \ "Cannot parse json '${request}': ${pkg_version}." return fi if [ "${pkg_version}" = "null" ]; then reply "${err_pkg_version_missing}" \ "Missing key pkg_version in '${request}'." return fi if [ -z "${pkg_version}" ]; then reply "${err_pkg_version_empty}" \ "Empty value for key pkg_version in '${request}'." return fi log_debug "Got pkg version '${pkg_version}'." # Check if package does exist. rpm_path="${rpms_dir}/${RPM_ARCH}/${pkg_name}-${pkg_version}-${RPM_RELEASE}${RPM_DIST}.${RPM_ARCH}.rpm" if [ ! -f "${rpm_path}" ]; then reply "${err_rpm_path_not_exist}" \ "RPM package '${rpm_path}' does not exist!" return fi log_debug "RPM path found at '${rpm_path}'." # Upgrade package if already installed. log_debug "Check if pkg '${pkg_name}' is already installed." if rpm --query "${pkg_name}" 1>/dev/null 2>/dev/null; then log_debug "Package '${pkg_name}' already installed, so upgrade to v ${pkg_version}" if ! output="$(sudo rpm --upgrade \ --verbose --hash "${rpm_path}" 2>&1)"; then reply "${err_rpm_upgrade}" \ "Cannot upgrade package '${pkg_name}' to v${pkg_version}: ${output}." return fi log_debug "RPM package '${pkg_name}' upgraded to v${pkg_version}." fi # Install package if not already installed. log_debug "Package '${pkg_name}' is not already installed, so install v${pkg_version}." if ! output="$(sudo rpm --install --verbose --hash "${rpm_path}")"; then reply "${err_rpm_install}" \ "Cannot install package '${pkg_name}' v${pkg_version}: ${output}." return fi # Package deployed. reply 0 "RPM package '${pkg_name}' v${pkg_version} has been deployed." ) process_loop() ( # Loop on every request. if "${keep_open}"; then log_info "Start listening unixsock with keep-open at '${deployer_sock}'." while read -r request; do log_info "Process new request '${request}'." process_request done < <(ncat --listen --keep-open --unixsock "${deployer_sock}") else log_info "Start listening unixsock without keep-open at '${deployer_sock}'." while read -r request; do log_info "Process new request '${request}'." process_request done < <(ncat --listen --unixsock "${deployer_sock}") fi # Kill all remaining subprocesses only if daemon. log_info "End of loop." if "${daemon}"; then log_debug "Kill child jobs." jobs -p | xargs kill 2>/dev/null || true fi # Remove sock file log_debug "Remove deployer unixsock file." rm --force "${deployer_sock}" log_info "End of process." ) main() ( # Parse arguments. daemon="false" testing="false" keep_open="true" config_file="${DEFAULT_CONFIG_FILE}" errors_file="${DEFAULT_ERRORS_FILE}" verbosity_option="" verbosity_level="${DEFAULT_VERBOSITY_LEVEL}" if ! args="$(getopt --name "${PROGRAM_NAME}" \ --options c:de:hqstv \ --longoptions conf:,daemon,errs:,help,quiet,stop,test,verbose \ -- "$@")"; then usage fail "Bad arguments." fi eval set -- "${args}" while true; do case "$1" in -c | --conf) config_file="$2" shift 2 ;; -d | --daemon) daemon="true" shift ;; -e | --errs) errors_file="$2" shift 2 ;; -h | --help) usage exit 0 ;; -q | --quiet) if [ "${verbosity_option}" ]; then usage fail "Options $1 and ${verbosity_option} are mutually exclusive." fi verbosity_option="$1" ((verbosity_level -= 10)) shift ;; -s | --stop) keep_open="false" shift ;; -t | --test) testing="true" shift ;; -v | --verbose) if [ "${verbosity_option}" ]; then usage fail "Options $1 and ${verbosity_option} are mutually exclusive." fi verbosity_option="$1" ((verbosity_level += 10)) shift ;; --) shift break ;; *) usage fail "Unexpected option '$1'." ;; esac done if [ "$@" ]; then usage fail "Unexpected extra arguments '$*'." fi # Load config file. if [ ! -r "${config_file}" ]; then fail "Config file '${config_file}' is not readable." fi log_info "Load config file '${config_file}'." # shellcheck source=./conf/netoik-cicd.conf.sample source "${config_file}" # Load errors file. if [ ! -r "${errors_file}" ]; then fail "Errors file '${errors_file}' is not reachable." fi log_info "Load errors file '${errors_file}'." # shellcheck source=./conf/errors.conf.sample source "${errors_file}" # Check variables in config file. if [ -z "${deployer_sock}" ]; then fail "Variable deployer_sock is empty." "${err_deployer_sock_empty}" fi if [ ! -d "$(dirname "${deployer_sock}")" ]; then fail "Dirname of deployer_sock='${deployer_sock}' is not a directory." \ "${err_deployer_sock_dir_not_directory}" fi if [ ! -w "$(dirname "${deployer_sock}")" ]; then fail "Directory of deployer_sock='${deployer_sock}' is not writable." \ "${err_deployer_sock_dir_not_writable}" fi if [ -e "${deployer_sock}" ]; then fail "Sock deployer_sock='${deployer_sock}' is already in use." \ "${err_deployer_sock_already_in_use}" fi if [ -z "${repos_dir}" ]; then fail "Variable repos_dir is empty." "${err_repos_dir_empty}" fi if [ ! -x "${repos_dir}" ]; then fail "Directory repos_dir='${repos_dir}' is not accessible." \ "${err_repos_dir_not_accessible}" fi if [ -z "${rpms_dir}" ]; then fail "Variable rpms_dir is empty." "${err_rpms_dir_empty}" fi if [ -z "${runner_username}" ]; then fail "Variable runner_username is empty." "${err_runner_username_empty}" fi if ! id --user --name "${runner_username}" 1>/dev/null; then fail "Runner user '${runner_username}' does not exist." \ "${err_runner_user_not_exist}" fi if [ -z "${runner_deployer_groupname}" ]; then fail "Variable runner_deployer_username is empty." "${err_runner_deployer_groupname_empty}" fi if ! getent group "${runner_deployer_groupname}"; then fail "Runner-deployer group '${runner_deployer_groupname}' does not exist." \ "${err_runner_deployer_group_not_exist}" fi # Stop now if we are only testing config. if "${testing}"; then exit 0 fi # Set right access in background after nc listen. ( inotifywait --event create "$(dirname "${deployer_sock}")" --quiet --quiet chmod 775 "${deployer_sock}" chgrp "${runner_deployer_groupname}" "${deployer_sock}" ) & # Run process loop in background or foreground. if "${daemon}"; then log_info "Run process loop in background." process_loop & else log_info "Run process loop in foreground." process_loop jobs -p | xargs kill 2>/dev/null || true fi ) main "$@"