#!/usr/bin/env bash # No, we can not deal with sh alone. set -e set -u # ERR traps should be inherited from functions too. (And command # substitutions and subshells and whatnot, but for us the function is # the important part here) set -E # A pipeline's return status is the value of the last (rightmost) # command to exit with a non-zero status, or zero if all commands exit # success fully. set -o pipefail # ftpsync script for Debian # Based losely on a number of existing scripts, written by an # unknown number of different people over the years. # # Copyright (C) 2008-2016 Joerg Jaspert # Copyright (C) 2016 Peter Palfrader # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License as # published by the Free Software Foundation; version 2. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. VERSION="20180513" # -*- mode:sh -*- # vim:syn=sh # Little common functions # push a mirror attached to us. # Arguments (using an array named SIGNAL_OPTS): # # $MIRROR - Name for the mirror, also basename for the logfile # $HOSTNAME - Hostname to push to # $USERNAME - Username there # $SSHPROTO - Protocol version, either 1 or 2. # $SSHKEY - the ssh private key file to use for this push # $SSHOPTS - any other option ssh accepts, passed blindly, be careful # $PUSHLOCKOWN - own lockfile name to touch after stage1 in pushtype=staged # $PUSHTYPE - what kind of push should be done? # all - normal, just push once with ssh backgrounded and finish # staged - staged. first push stage1, then wait for $PUSHLOCKs to appear, # then push stage2 # $PUSHARCHIVE - what archive to sync? (Multiple mirrors behind one ssh key!) # $PUSHCB - do we want a callback? # $PUSHKIND - whats going on? are we doing mhop push or already stage2? # $FROMFTPSYNC - set to true if we run from within ftpsync. # # This function assumes that the variable LOG is set to a directory where # logfiles can be written to. # Additionally $PUSHLOCKS has to be defined as a set of space delimited strings # (list of "lock"files) to wait for if you want pushtype=staged # # Pushes might be done in background (for type all). signal () { ARGS="SIGNAL_OPTS[*]" local ${!ARGS} MIRROR=${MIRROR:-""} HOSTNAME=${HOSTNAME:-""} USERNAME=${USERNAME:-""} SSHPROTO=${SSHPROTO:-""} SSHKEY=${SSHKEY:-""} SSHOPTS=${SSHOPTS:-""} PUSHLOCKOWN=${PUSHLOCKOWN:-""} PUSHTYPE=${PUSHTYPE:-"all"} PUSHARCHIVE=${PUSHARCHIVE:-""} PUSHCB=${PUSHCB:-""} PUSHKIND=${PUSHKIND:-"all"} FROMFTPSYNC=${FROMFTPSYNC:-"false"} # And now get # back to space... SSHOPTS=${SSHOPTS/\#/ } # Defaults we always want, no matter what SSH_OPTIONS="-o user=${USERNAME} -o BatchMode=yes -o ServerAliveInterval=45 -o ConnectTimeout=45 -o PasswordAuthentication=no" # If there are userdefined ssh options, add them. if [[ -n ${SSH_OPTS} ]]; then SSH_OPTIONS="${SSH_OPTIONS} ${SSH_OPTS}" fi # Does this machine need a special key? if [[ -n ${SSHKEY} ]]; then SSH_OPTIONS="${SSH_OPTIONS} -i ${SSHKEY}" fi # Does this machine have an extra own set of ssh options? if [[ -n ${SSHOPTS} ]]; then SSH_OPTIONS="${SSH_OPTIONS} ${SSHOPTS}" fi # Set the protocol version if [[ ${SSHPROTO} -ne 1 ]] && [[ ${SSHPROTO} -ne 2 ]] && [[ ${SSHPROTO} -ne 99 ]]; then # Idiots, we only want 1 or 2. Cant decide? Lets force 2. SSHPROTO=2 fi if [[ -n ${SSHPROTO} ]] && [[ ${SSHPROTO} -ne 99 ]]; then SSH_OPTIONS="${SSH_OPTIONS} -${SSHPROTO}" fi date -u >> "${LOGDIR}/${MIRROR}.log" PUSHARGS="" # PUSHARCHIVE empty or not, we always add the sync:archive: command to transfer. # Otherwise, if nothing else is added, ssh -f would not work ("no command to execute") # But ftpsync does treat "sync:archive:" as the main archive, so this works nicely. PUSHARGS="${PUSHARGS} sync:archive:${PUSHARCHIVE}" # We have a callback wish, tell downstreams if [[ -n ${PUSHCB} ]]; then PUSHARGS="${PUSHARGS} sync:callback" fi # If we are running an mhop push AND our downstream is one to receive it, tell it. if [[ mhop = ${PUSHKIND} ]] && [[ mhop = ${PUSHTYPE} ]]; then PUSHARGS="${PUSHARGS} sync:mhop" fi if [[ all = ${PUSHTYPE} ]]; then # Default normal "fire and forget" push. We background that, we do not care about the mirrors doings PUSHARGS1="sync:all" signal_ssh "normal" "${MIRROR}" "${HOSTNAME}" $SSH_OPTIONS "${PUSHARGS} ${PUSHARGS1}" elif [[ staged = ${PUSHTYPE} ]] || [[ mhop = ${PUSHTYPE} ]]; then # Want a staged push. Fine, lets do that. Not backgrounded. We care about the mirrors doings. # Only send stage1 if we havent already send it. When called with stage2, we already did. if [[ stage2 != ${PUSHKIND} ]]; then # Step1: Do a push to only sync stage1, do not background PUSHARGS1="sync:stage1" signal_ssh "first stage" "${MIRROR}" "${HOSTNAME}" $SSH_OPTIONS "${PUSHARGS} ${PUSHARGS1}" touch "${PUSHLOCKOWN}" # Step2: Wait for all the other "lock"files to appear. # In case we did not have all PUSHLOCKS and still continued, note it # This is a little racy, especially if the other parts decide to do this # at the same time, but it wont hurt more than a mail too much, so I don't care much if ! wait_for_pushlocks ${PUSHDELAY}; then msg "Failed to wait for all other mirrors. Failed ones are:" >> "${LOGDIR}/${MIRROR}.log" for file in ${PUSHLOCKS}; do if [[ ! -f ${file} ]]; then msg "${file}" >> "${LOGDIR}/${MIRROR}.log" log "Missing Pushlockfile ${file} after waiting for more than ${PUSHDELAY} seconds, continuing" fi done fi rm -f "${PUSHLOCKOWN}" fi # Step3: It either timed out or we have all the "lock"files, do the rest # If we are doing mhop AND are called from ftpsync - we now exit. # That way we notify our uplink that we and all our clients are done with their # stage1. It can then finish its own, and if all our upstreams downlinks are done, # it will send us stage2. # If we are not doing mhop or are not called from ftpsync, we start stage2 if [[ true = ${FROMFTPSYNC} ]] && [[ mhop = ${PUSHKIND} ]]; then return else PUSHARGS2="sync:stage2" signal_ssh "second stage" "${MIRROR}" "${HOSTNAME}" $SSH_OPTIONS "${PUSHARGS} ${PUSHARGS2}" fi else # Can't decide? Then you get nothing. return fi } signal_ssh() { local t=$1 local mirror_log="${LOGDIR}/${2}.log" local hostname=$3 shift 3 msg "Sending ${t} trigger" >> $mirror_log output=$(ssh -n $hostname "$@" 2>&1 | tee -a $mirror_log) if [[ $? -eq 255 ]]; then error_mailf "${t} trigger failed: $hostname" -b "$output" else log "${t} trigger succeeded: $hostname" fi } wait_for_pushlocks() { local tries=0 local found local total local timeout=${1}; shift # We do not wait forever while [[ ${tries} -lt ${timeout} ]]; do total=0 found=0 for file in ${PUSHLOCKS}; do total=$(( total + 1 )) if [[ -f ${file} ]]; then found=$(( found + 1 )) fi done if [[ ${total} -eq ${found} ]] || [[ -f ${LOCKDIR}/all_stage1 ]]; then break fi tries=$(( tries + 5 )) sleep 5 done # Regardless of the state of our siblings, hitting one timeout cancels all waits touch "${LOCKDIR}/all_stage1" if [[ ${tries} -ge ${timeout} ]]; then return 1 else return 0 fi } # callback, used by ftpsync callback () { # Defaults we always want, no matter what SSH_OPTIONS="-o BatchMode=yes -o ServerAliveInterval=45 -o ConnectTimeout=45 -o PasswordAuthentication=no" ssh -n $SSH_OPTIONS -i "$3" -o"user $1" "$2" callback:${HOSTNAME} } # open log file open_log() { local log=$1 shift exec 4>&1 1>>$log } # assemble log message (basically echo it together with a timestamp) # # Set $PROGRAM to a string to have it added to the output. msg() { if [[ -z "${PROGRAM}" ]]; then echo "$(date +"%b %d %H:%M:%S") $(hostname -s) [$$] $@" else echo "$(date +"%b %d %H:%M:%S") $(hostname -s) ${PROGRAM}[$$]: $@" fi } # log something log() { msg "$@" } # log the message using log() but then also send a mail # to the address configured in MAILTO (if non-empty) error () { log "$@" LOG_ERROR=1 mailf -s "[$PROGRAM@$(hostname -s)] ERROR: $*" -b "$*" ${MAILTO} } # log the message using log() but then also send a mail # to the address configured in MAILTO (if non-empty) error_mailf () { local m="$1" shift log "$m" LOG_ERROR=1 mailf -s "[$PROGRAM@$(hostname -s)] ERROR: $m" "$@" ${MAILTO} } # run a hook # needs array variable HOOK setup with HOOKNR being a number an HOOKSCR # the script to run. hook () { ARGS='HOOK[@]' local "${!ARGS}" if [[ -n ${HOOKSCR} ]]; then log "Running hook $HOOKNR: ${HOOKSCR}" set +e ${HOOKSCR} result=$? set -e if [[ ${result} -ne 0 ]] ; then error "Back from hook $HOOKNR, got returncode ${result}" else log "Back from hook $HOOKNR, got returncode ${result}" fi return $result else return 0 fi } # Return the list of 2-stage mirrors. get2stage() { egrep -s '^(staged|mhop)' "${MIRRORS}" | { while read MTYPE MLNAME MHOSTNAME MUSER MPROTO MKEYFILE; do PUSHLOCKS="${LOCKDIR}/${MLNAME}.stage1 ${PUSHLOCKS}" done echo "$PUSHLOCKS" } } # Rotate logfiles savelog() { torotate="$1" count=${2:-${LOGROTATE}} while [[ ${count} -gt 0 ]]; do prev=$(( count - 1 )) if [[ -e ${torotate}.${prev} ]]; then mv "${torotate}.${prev}" "${torotate}.${count}" fi count=$prev done if [[ -e ${torotate} ]]; then mv "${torotate}" "${torotate}.0" fi } # Return rsync version rsync_protocol() { RSYNC_VERSION="$(${RSYNC} --version)" RSYNC_REGEX="(protocol[ ]+version[ ]+([0-9]+))" if [[ ${RSYNC_VERSION} =~ ${RSYNC_REGEX} ]]; then echo ${BASH_REMATCH[2]} fi unset RSYNC_VERSION RSYNC_REGEX } extract_trace_field() { local field="$1" local file="$2" local value=$(awk -F': ' "\$1==\"$field\" {print \$2; exit}" "$file" 2>/dev/null) [[ $value ]] || return 1 echo $value } extract_trace_field_string() { local field="$1" local string="$2" local value=$(awk -F': ' "\$1==\"$field\" {print \$2; exit}" <<< "$string" 2>/dev/null) [[ $value ]] || return 1 echo $value } extract_trace_serial() { extract_trace_field 'Archive serial' "$1" return $? } extract_trace_serial_string() { extract_trace_field_string 'Archive serial' "$1" return $? } # Search config files in various locations search_config() { local file for i in ${CONFDIRS[@]}; do file="$i/$1" if [ -f "$file" ]; then echo "$file" return fi done } # Read config file read_config() { local name=$(echo "$1" | sed -e 's/[^A-Za-z0-9._-]/_/g') local config=$(search_config "$name") if [ "$config" ]; then . "$config" CURRENT_CONFIG="$config" return 0 else echo "Can't read config file ${name}!" >&2 exit 78 # EX_CONFIG fi } # Create lock dir create_lockdir() { mkdir -p "$LOCKDIR" } # Create log dir create_logdir() { mkdir -p "$LOGDIR" } join_by() { local IFS="$1" shift echo $* } # Sends mail # mailf [-a attachment] [-b body] [-s subject] to-addr ... mailf() { local boundary="==--$RANDOM--$RANDOM--$RANDOM--==" local attachment=() local body=() local subject= OPTIND=1 while getopts ":a:b:s:" arg; do case $arg in a) attachment+=("$OPTARG") ;; b) body+=("$OPTARG") ;; s) subject="$OPTARG" ;; esac done shift $((OPTIND-1)) ( cat < "${LOGDIR}/ftpsync.newversion" fi fi else # Remove a possible stampfile rm -f "${LOGDIR}/ftpsync.newversion" fi fi } ######################################################################## ######################################################################## ## functions ## ######################################################################## ######################################################################## check_commandline() { while [[ $# -gt 0 ]]; do case "$1" in sync:stage1) SYNCSTAGE1="true" SYNCALL="false" ;; sync:stage2) SYNCSTAGE2="true" SYNCALL="false" ;; sync:callback) SYNCCALLBACK="true" ;; sync:archive:*) ARCHIVE=${1##sync:archive:} ;; sync:all) SYNCALL="true" ;; sync:mhop) SYNCMHOP="true" ;; *) echo "Unknown option ${1} ignored" ;; esac shift # Check next set of parameters. done } # All the stuff we want to do when we exit, no matter where cleanup() { rc=$? trap - ERR TERM HUP INT QUIT EXIT # all done. Mail the log, exit. if [[ $rc -gt 0 ]]; then log "Mirrorsync done with errors" else log "Mirrorsync done" fi if [[ -n ${MAILTO} ]]; then local args=() local send= local subject="SUCCESS" # In case rsync had something on stderr if [[ -s $LOG_RSYNC_ERROR ]]; then args+=(-a $LOG_RSYNC_ERROR -a $LOG) subject="ERROR: rsync errors" send=1 # In case of direct errors elif [[ $rc -gt 0 ]]; then args+=(-a $LOG) subject="ERROR" send=1 # In case admin want all logs elif [[ ${ERRORSONLY} = false ]]; then args+=(-a $LOG) if [[ ${LOG_ERROR:-} ]]; then subject="ERROR" fi send=1 fi if [[ $send ]]; then # Someone wants full logs including rsync if [[ ${FULLLOGS} = true ]]; then args+=(-a $LOG_RSYNC) fi mailf "${args[@]}" -s "[${PROGRAM}@$(hostname -s)] ${subject}" ${MAILTO} fi fi savelog "${LOG_RSYNC}" savelog "${LOG_RSYNC_ERROR}" savelog "$LOG" > /dev/null rm -f "${LOCK}" exit $rc } run_rsync() { local t=$1 shift log "Running $t:" "${_RSYNC[@]}" "$@" "${_RSYNC[@]}" "$@" \ >>"${LOG_RSYNC_ERROR}" 2>&1 || return $? } # Check rsyncs return value check_rsync() { ret=$1 msg=$2 # Lets get a statistical value if [[ -f ${LOG_RSYNC} ]]; then SPEED=$(tail -n 2 ${LOG_RSYNC} | sed -Ene 's#.* ([0-9.,]+) bytes/sec#\1#p') if [[ ${SPEED} ]]; then SPEED=${SPEED%%.*} SPEED=${SPEED//,} SPEED=$(( SPEED / 1024 )) log "Latest recorded rsync transfer speed: ${SPEED} KB/s" fi fi # 24 - vanished source files. Ignored, that should be the target of $UPDATEREQUIRED # and us re-running. If it's not, uplink is broken anyways. case "${ret}" in 0) return 0;; 24) return 0;; 23) return 2;; 30) return 2;; *) error "ERROR: ${msg}" return 1 ;; esac } function tracefile_content() { set +e LC_ALL=POSIX LANG=POSIX date -u rfc822date=$(LC_ALL=POSIX LANG=POSIX date -u -R) echo "Date: ${rfc822date}" echo "Date-Started: ${DATE_STARTED}" if [[ -e $TRACEFILE_MASTER ]]; then echo "Archive serial: $(extract_trace_serial $TRACEFILE_MASTER || echo unknown )" fi echo "Used ftpsync version: ${VERSION}" echo "Creator: ftpsync ${VERSION}" echo "Running on host: ${TRACEHOST}" if [[ ${INFO_MAINTAINER:-} ]]; then echo "Maintainer: ${INFO_MAINTAINER}" fi if [[ ${INFO_SPONSOR:-} ]]; then echo "Sponsor: ${INFO_SPONSOR}" fi if [[ ${INFO_COUNTRY:-} ]]; then echo "Country: ${INFO_COUNTRY}" fi if [[ ${INFO_LOCATION:-} ]]; then echo "Location: ${INFO_LOCATION}" fi if [[ ${INFO_THROUGHPUT:-} ]]; then echo "Throughput: ${INFO_THROUGHPUT}" fi if [[ ${INFO_TRIGGER:-} ]]; then echo "Trigger: ${INFO_TRIGGER}" fi if [[ -d ${TO}/dists ]]; then ARCH=$(find ${TO}/dists \( -name 'Packages.*' -o -name 'Sources.*' \) 2>/dev/null | sed -Ene 's#.*/binary-([^/]+)/Packages.*#\1#p; s#.*/(source)/Sources.*#\1#p' | sort -u | tr '\n' ' ') if [[ $ARCH ]]; then echo "Architectures: ${ARCH}" fi fi if [[ ${ARCH_INCLUDE} ]]; then echo "Architectures-Configuration: INCLUDE $(tr ' ' '\n' <<< ${ARCH_INCLUDE} | sort -u | tr '\n' ' ')" elif [[ ${ARCH_EXCLUDE} ]]; then echo "Architectures-Configuration: EXCLUDE $(tr ' ' '\n' <<< ${ARCH_EXCLUDE} | sort -u | tr '\n' ' ')" else echo "Architectures-Configuration: ALL" fi echo "Upstream-mirror: ${RSYNC_HOST:-unknown}" echo "Rsync-Transport: ${RSYNC_TRANSPORT}" total=0 if [[ -e ${LOG_RSYNC} ]]; then for bytes in $(sed -Ene 's/(^|.* )sent ([0-9]+) bytes received ([0-9]+) bytes.*/\3/p' "${LOG_RSYNC}"); do total=$(( total + bytes )) done if [[ $total -gt 0 ]]; then echo "Total bytes received in rsync: ${total}" fi fi total_time=$(( STATS_TOTAL_RSYNC_TIME1 + STATS_TOTAL_RSYNC_TIME2 )) echo "Total time spent in stage1 rsync: ${STATS_TOTAL_RSYNC_TIME1}" echo "Total time spent in stage2 rsync: ${STATS_TOTAL_RSYNC_TIME2}" echo "Total time spent in rsync: ${total_time}" if [[ 0 != ${total_time} ]]; then rate=$(( total / total_time )) echo "Average rate: ${rate} B/s" fi set -e } # Write a tracefile tracefile() { local TRACEFILE=${1:-"${TO}/${TRACE}"} local TRACEFILE_MASTER="${TO}/${TRACEDIR}/master" tracefile_content > "${TRACEFILE}.new" mv "${TRACEFILE}.new" "${TRACEFILE}" { if [[ -e ${TO}/${TRACEHIERARCHY}.mirror ]]; then cat ${TO}/${TRACEHIERARCHY}.mirror fi echo "$(basename "${TRACEFILE}") ${MIRRORNAME} ${TRACEHOST} ${RSYNC_HOST:-unknown}" } > "${TO}/${TRACEHIERARCHY}".new mv "${TO}/${TRACEHIERARCHY}".new "${TO}/${TRACEHIERARCHY}" cp "${TO}/${TRACEHIERARCHY}" "${TO}/${TRACEHIERARCHY}.mirror" (cd "${TO}/${TRACEDIR}" && ls -1rt $(find * -type f \! -name "_*")) > "${TO}/${TRACELIST}" } arch_imexclude() { local param="$1" arch="$2" if [[ $arch = source ]]; then _RSYNC+=( "--filter=${param}_/dists/**/source/" "--filter=${param}_/pool/**/*.tar.*" "--filter=${param}_/pool/**/*.diff.*" "--filter=${param}_/pool/**/*.dsc" ) else _RSYNC+=( "--filter=${param}_/dists/**/binary-${arch}/" "--filter=${param}_/dists/**/installer-${arch}/" "--filter=${param}_/dists/**/Contents-${arch}.gz" "--filter=${param}_/dists/**/Contents-udeb-${arch}.gz" "--filter=${param}_/dists/**/Contents-${arch}.diff/" "--filter=${param}_/indices/**/arch-${arch}.files" "--filter=${param}_/indices/**/arch-${arch}.list.gz" "--filter=${param}_/pool/**/*_${arch}.deb" "--filter=${param}_/pool/**/*_${arch}.udeb" "--filter=${param}_/pool/**/*_${arch}.changes" ) fi } arch_exclude() { arch_imexclude exclude "$1" } arch_include() { arch_imexclude include "$1" } # Learn which archs to include/exclude based on ARCH_EXCLUDE and ARCH_INCLUDE # settings. # Sets EXCLUDE (which might also have --include statements # followed by a --exclude *_*.. set_exclude_include_archs() { if [[ -n "${ARCH_EXCLUDE}" ]] && [[ -n "${ARCH_INCLUDE}" ]]; then echo >&2 "ARCH_EXCLUDE and ARCH_INCLUDE are mutually exclusive. Set only one." exit 1 fi if [[ -n "${ARCH_EXCLUDE}" ]]; then for ARCH in ${ARCH_EXCLUDE}; do arch_exclude ${ARCH} done arch_include '*' arch_include source elif [[ -n "${ARCH_INCLUDE}" ]]; then local include_arch_all=false for ARCH in ${ARCH_INCLUDE}; do arch_include ${ARCH} if [[ ${ARCH} != source ]]; then include_arch_all=true fi done if [[ true = ${include_arch_all} ]]; then arch_include all fi arch_exclude '*' arch_exclude source fi } ######################################################################## ######################################################################## # As what are we called? NAME="$(basename $0)" # What should we do? ARCHIVE= # Do we sync stage1? SYNCSTAGE1=false # Do we sync stage2? SYNCSTAGE2=false # Do we sync all? SYNCALL=true # Do we have a mhop sync? SYNCMHOP=false # Do we callback? (May get changed later) SYNCCALLBACK=false while getopts T: option; do case $option in T) INFO_TRIGGER=$OPTARG;; ?) exit 64;; esac done shift $(($OPTIND - 1)) # Now, check if we got told about stuff via ssh if [[ -n ${SSH_ORIGINAL_COMMAND:-} ]]; then INFO_TRIGGER=${INFO_TRIGGER:-ssh} check_commandline ${SSH_ORIGINAL_COMMAND} fi # Now, we can locally override all the above variables by just putting # them into the .ssh/authorized_keys file forced command. if [[ $# -gt 0 ]]; then check_commandline "$@" fi # If we have been told to do stuff for a different archive than default, # set the name accordingly. if [[ -n ${ARCHIVE} ]]; then NAME="${NAME}-${ARCHIVE}" fi # Now source the config for the archive we run on. # (Yes, people can also overwrite the options above in the config file # if they want to) read_config "${NAME}.conf" create_logdir ######################################################################## # Config defaults # ######################################################################## MIRRORNAME=${MIRRORNAME:-$(hostname -f)} TO=${TO:-"/srv/mirrors/debian/"} MAILTO=${MAILTO:-${LOGNAME:?Environment variable LOGNAME unset, please check your system or specify MAILTO}} HUB=${HUB:-"false"} # Connection options if [[ -z ${RSYNC_SOURCE:-} ]]; then RSYNC_HOST=${RSYNC_HOST:?Missing a host to mirror from, please set RSYNC_HOST variable in ${CURRENT_CONFIG}} RSYNC_PATH=${RSYNC_PATH:-"debian"} RSYNC_USER=${RSYNC_USER:-""} fi RSYNC_PASSWORD=${RSYNC_PASSWORD:-""} if [[ ${RSYNC_SSL:-} = true ]]; then RSYNC_TRANSPORT=${RSYNC_TRANSPORT:-"ssl"} else RSYNC_TRANSPORT=${RSYNC_TRANSPORT:-"undefined"} fi RSYNC_SSL_PORT=${RSYNC_SSL_PORT:-"1873"} RSYNC_SSL_CAPATH=${RSYNC_SSL_CAPATH:-"/etc/ssl/certs"} RSYNC_SSL_METHOD=${RSYNC_SSL_METHOD:-"stunnel"} RSYNC_PROXY=${RSYNC_PROXY:-""} # Include and exclude options ARCH_INCLUDE=${ARCH_INCLUDE:-""} ARCH_EXCLUDE=${ARCH_EXCLUDE:-""} EXCLUDE=${EXCLUDE:-""} # Log options LOG=${LOG:-"${LOGDIR}/${NAME}.log"} ERRORSONLY=${ERRORSONLY:-"true"} FULLLOGS=${FULLLOGS:-"false"} LOGROTATE=${LOGROTATE:-14} LOG_RSYNC="${LOGDIR}/rsync-${NAME}.log" LOG_RSYNC_ERROR="${LOGDIR}/rsync-${NAME}.error" # Other options LOCKTIMEOUT=${LOCKTIMEOUT:-3600} UIPSLEEP=${UIPSLEEP:-1200} UIPRETRIES=${UIPRETRIES:-3} TRACEHOST=${TRACEHOST:-$(hostname -f)} RSYNC=${RSYNC:-rsync} RSYNC_PROTOCOL=$(rsync_protocol) RSYNC_EXTRA=${RSYNC_EXTRA:-""} RSYNC_BW=${RSYNC_BW:-0} if [[ $RSYNC_PROTOCOL -ge 31 ]]; then RSYNC_OPTIONS=${RSYNC_OPTIONS:-"-prltvHSB8192 --safe-links --chmod=D755,F644 --timeout 120 --stats --no-human-readable --no-inc-recursive"} else RSYNC_OPTIONS=${RSYNC_OPTIONS:-"-prltvHSB8192 --safe-links --timeout 120 --stats --no-human-readable --no-inc-recursive"} fi RSYNC_OPTIONS1=${RSYNC_OPTIONS1:-"--include=*.diff/ --exclude=*.diff/Index --exclude=Packages* --exclude=Sources* --exclude=Release* --exclude=InRelease --include=i18n/by-hash --exclude=i18n/* --exclude=ls-lR*"} RSYNC_OPTIONS2=${RSYNC_OPTIONS2:-"--max-delete=40000 --delay-updates --delete --delete-delay --delete-excluded"} CALLBACKUSER=${CALLBACKUSER:-"archvsync"} CALLBACKHOST=${CALLBACKHOST:-"none"} CALLBACKKEY=${CALLBACKKEY:-"none"} # Hooks HOOK1=${HOOK1:-""} HOOK2=${HOOK2:-""} HOOK3=${HOOK3:-""} HOOK4=${HOOK4:-""} HOOK5=${HOOK5:-""} ######################################################################## ######################################################################## # used by log() and error() PROGRAM=${PROGRAM:-"${NAME}"} # Our trace and lock files LOCK_NAME="Archive-Update-in-Progress-${MIRRORNAME}" LOCK="${TO}/${LOCK_NAME}" UPDATEREQUIRED_NAME="Archive-Update-Required-${MIRRORNAME}" UPDATEREQUIRED="${TO}/${UPDATEREQUIRED_NAME}" TRACEDIR=project/trace TRACE="${TRACEDIR}/${MIRRORNAME}" TRACE_STAGE1="${TRACEDIR}/${MIRRORNAME}-stage1" TRACEHIERARCHY="${TRACEDIR}/_hierarchy" TRACELIST="${TRACEDIR}/_traces" _TRACE_FILES=( "${LOCK_NAME}" "${UPDATEREQUIRED_NAME}" "${TRACE}" "${TRACE_STAGE1}" "${TRACEHIERARCHY}" "${TRACELIST}" ) _RSYNC=( $RSYNC --quiet --log-file "${LOG_RSYNC}" ) # Rsync filter rules. Used to protect various files we always want to keep, even if we otherwise delete # excluded files for i in ${_TRACE_FILES[@]}; do _RSYNC+=("--filter=exclude_/${i}" "--filter=protect_/${i}") done _RSYNC+=( "--filter=include_/project/" "--filter=protect_/project/" "--filter=include_/project/trace/" "--filter=protect_/project/trace/" "--filter=include_/project/trace/*" ) # Default rsync options for *every* rsync call # Now add the bwlimit option. As default is 0 we always add it, rsync interprets # 0 as unlimited, so this is safe. _RSYNC+=(${RSYNC_EXTRA} --bwlimit=${RSYNC_BW} ${RSYNC_OPTIONS} ${EXCLUDE}) # collect some stats STATS_TOTAL_RSYNC_TIME1=0 STATS_TOTAL_RSYNC_TIME2=0 # The temp directory used by rsync --delay-updates is not # world-readable remotely. Always exclude it to avoid errors. _RSYNC+=("--exclude=.~tmp~/") if [[ ${RSYNC_TRANSPORT} = undefined ]]; then : elif [[ ${RSYNC_TRANSPORT} = ssh ]]; then _RSYNC+=(-e "ssh") elif [[ ${RSYNC_TRANSPORT} = ssl ]]; then export RSYNC_SSL_PORT export RSYNC_SSL_CAPATH export RSYNC_SSL_METHOD _RSYNC+=(-e "${BINDIR:+${BINDIR}/}rsync-ssl-tunnel") else echo "Unknown rsync transport configured (${RSYNC_TRANSPORT})" >&2 exit 1 fi # Exclude architectures defined in $ARCH_EXCLUDE set_exclude_include_archs ######################################################################## # Really nothing to see below here. Only code follows. # ######################################################################## ######################################################################## DATE_STARTED=$(LC_ALL=POSIX LANG=POSIX date -u -R) # Some sane defaults cd "${BASEDIR:-}" umask 022 # If we are here for the first time, create the # destination and the trace directory mkdir -p "${TO}/${TRACEDIR}" # Used to make sure we will have the archive fully and completly synced before # we stop, even if we get multiple pushes while this script is running. # Otherwise we can end up with a half-synced archive: # - get a push # - sync, while locked # - get another push. Of course no extra sync run then happens, we are locked. # - done. Archive not correctly synced, we don't have all the changes from the second push. touch "${UPDATEREQUIRED}" # Check to see if another sync is in progress if ! ( set -o noclobber; echo "$$" > "${LOCK}") 2> /dev/null; then if [[ ${BASH_VERSINFO[0]} -gt 3 ]] || [[ -L /proc/self ]]; then # We have a recent enough bash version, lets do it the easy way, # the lock will contain the right pid, thanks to $BASHPID if ! $(kill -0 $(< ${LOCK}) 2>/dev/null); then # Process does either not exist or is not owned by us. echo "$$" > "${LOCK}" else echo "Unable to start rsync, lock file still exists, PID $(< ${LOCK})" exit 1 fi else # Old bash, means we dont have the right pid in our lockfile # So take a different way - guess if it is still there by comparing its age. # Not optimal, but hey. stamptime=$(date --reference="${LOCK}" +%s) unixtime=$(date +%s) difference=$(( $unixtime - $stamptime )) if [[ ${difference} -ge ${LOCKTIMEOUT} ]]; then # Took longer than LOCKTIMEOUT minutes? Assume it broke and take the lock echo "$$" > "${LOCK}" else echo "Unable to start rsync, lock file younger than one hour" exit 1 fi fi fi # We want to cleanup always trap cleanup EXIT TERM HUP INT QUIT # Open log and close stdin open_log $LOG exec 2>&1 <&- log "Mirrorsync start" # Look who pushed us and note that in the log. SSH_CONNECTION=${SSH_CONNECTION:-""} PUSHFROM="${SSH_CONNECTION%%\ *}" if [[ -n ${PUSHFROM} ]]; then log "We got pushed from ${PUSHFROM}" fi if [[ true = ${SYNCCALLBACK} ]]; then if [[ none = ${CALLBACKHOST} ]] || [[ none = ${CALLBACKKEY} ]]; then SYNCCALLBACK="false" error "We are asked to call back, but we do not know where to and do not have a key, ignoring callback" fi fi HOOK=( HOOKNR=1 HOOKSCR=${HOOK1} ) hook $HOOK # Now, we might want to sync from anonymous too. # This is that deep in this script so hook1 could, if wanted, change things! if [[ -z ${RSYNC_SOURCE:-} ]]; then if [[ -z ${RSYNC_USER:-} ]]; then RSYNC_SOURCE="${RSYNC_HOST}::${RSYNC_PATH}" else RSYNC_SOURCE="${RSYNC_USER}@${RSYNC_HOST}::${RSYNC_PATH}" fi fi _RSYNC+=("${RSYNC_SOURCE}" "$TO") # Now do the actual mirroring, and run as long as we have an updaterequired file. export RSYNC_PASSWORD export RSYNC_PROXY UPDATE_RETRIES=0 while [[ -e ${UPDATEREQUIRED} ]]; do log "Running mirrorsync, update is required, ${UPDATEREQUIRED} exists" # if we want stage1 *or* all if [[ true = ${SYNCSTAGE1} ]] || [[ true = ${SYNCALL} ]]; then while [[ -e ${UPDATEREQUIRED} ]]; do rm -f "${UPDATEREQUIRED}" # Step one, sync everything except Packages/Releases rsync_started=$(date +%s) result=0 run_rsync "stage1" ${RSYNC_OPTIONS1} || result=$? rsync_ended=$(date +%s) STATS_TOTAL_RSYNC_TIME1=$(( STATS_TOTAL_RSYNC_TIME1 + rsync_ended - rsync_started )) log "Back from rsync with returncode ${result}" done else time1=$(extract_trace_field 'Total time spent in stage1 rsync' "${TO}/${TRACE_STAGE1}" || :) if [[ $time1 ]]; then STATS_TOTAL_RSYNC_TIME1="$time1" fi # Fake a good resultcode result=0 fi # Sync stage 1? rm -f "${UPDATEREQUIRED}" set +e check_rsync $result "Sync step 1 went wrong, got errorcode ${result}. Logfile: ${LOG}" GO=$? set -e if [[ ${GO} -eq 2 ]] && [[ -e ${UPDATEREQUIRED} ]]; then log "We got error ${result} from rsync, but a second push went in hence ignoring this error for now" elif [[ ${GO} -ne 0 ]]; then exit 3 fi HOOK=( HOOKNR=2 HOOKSCR=${HOOK2} ) hook $HOOK # if we want stage2 *or* all if [[ true = ${SYNCSTAGE2} ]] || [[ true = ${SYNCALL} ]]; then upstream_uip=false for aupfile in "${TO}/Archive-Update-in-Progress-"*; do case "$aupfile" in "${TO}/Archive-Update-in-Progress-*") error "Lock file is missing, this should not happen" ;; "${LOCK}") : ;; *) if [[ -f $aupfile ]]; then # Remove the file, it will be synced again if # upstream is still not done rm -f "$aupfile" else log "AUIP file '$aupfile' is not really a file, weird" fi upstream_uip=true ;; esac done if [[ true = ${upstream_uip} ]]; then log "Upstream archive update in progress, skipping stage2" if [[ ${UPDATE_RETRIES} -lt ${UIPRETRIES} ]]; then log "Retrying update in ${UIPSLEEP}" touch "${UPDATEREQUIRED}" UPDATE_RETRIES=$(($UPDATE_RETRIES+1)) sleep "${UIPSLEEP}" result=0 else error "Update has been retried ${UPDATE_RETRIES} times, aborting" log "Perhaps upstream is still updating or there's a stale AUIP file" result=1 fi else # We are lucky, it worked. Now do step 2 and sync again, this time including # the packages/releases files rsync_started=$(date +%s) result=0 run_rsync "stage2" ${RSYNC_OPTIONS2} || result=$? rsync_ended=$(date +%s) STATS_TOTAL_RSYNC_TIME2=$(( STATS_TOTAL_RSYNC_TIME2 + rsync_ended - rsync_started )) log "Back from rsync with returncode ${result}" fi else # Fake a good resultcode result=0 fi # Sync stage 2? set +e check_rsync $result "Sync step 2 went wrong, got errorcode ${result}. Logfile: ${LOG}" GO=$? set -e if [[ ${GO} -eq 2 ]] && [[ -e ${UPDATEREQUIRED} ]]; then log "We got error ${result} from rsync, but a second push went in hence ignoring this error for now" elif [[ ${GO} -ne 0 ]]; then exit 4 fi HOOK=( HOOKNR=3 HOOKSCR=${HOOK3} ) hook $HOOK done # We only update our tracefile when we had a stage2 or an all sync. # Otherwise we would update it after stage1 already, which is wrong. if [[ true = ${SYNCSTAGE2} ]] || [[ true = ${SYNCALL} ]]; then tracefile if [[ true = ${SYNCALL} ]]; then rm -f "${TO}/${TRACE_STAGE1}" fi elif [[ true = ${SYNCSTAGE1} ]]; then tracefile "${TO}/${TRACE_STAGE1}" fi HOOK=( HOOKNR=4 HOOKSCR=${HOOK4} ) hook $HOOK if [[ true = ${SYNCCALLBACK} ]]; then set +e callback ${CALLBACKUSER} ${CALLBACKHOST} "${CALLBACKKEY}" set -e fi # Remove the Archive-Update-in-Progress file before we push our downstreams. rm -f "${LOCK}" declare -f -F send_mail_new_version > /dev/null && send_mail_new_version || : if [[ ${HUB} = true ]]; then # Trigger slave mirrors if we had a push for stage2 or all, or if its mhop if [[ true = ${SYNCSTAGE2} ]] || [[ true = ${SYNCALL} ]] || [[ true = ${SYNCMHOP} ]]; then RUNMIRRORARGS="" if [[ -n ${ARCHIVE} ]]; then # We tell runmirrors about the archive we are running on. RUNMIRRORARGS="-a ${ARCHIVE}" fi # We also tell runmirrors that we are running it from within ftpsync, so it can change # the way it works with mhop based on that. RUNMIRRORARGS="${RUNMIRRORARGS} -f" if [[ true = ${SYNCSTAGE1} ]]; then # This is true when we have a mhop sync. A normal multi-stage push sending stage1 will # not get to this point. # So if that happens, tell runmirrors we are doing mhop RUNMIRRORARGS="${RUNMIRRORARGS} -k mhop" elif [[ true = ${SYNCSTAGE2} ]]; then RUNMIRRORARGS="${RUNMIRRORARGS} -k stage2" elif [[ true = ${SYNCALL} ]]; then RUNMIRRORARGS="${RUNMIRRORARGS} -k all" fi log "Trigger slave mirrors using ${RUNMIRRORARGS}" ${BINDIR:+${BINDIR}/}runmirrors ${RUNMIRRORARGS} log "Trigger slave done" HOOK=( HOOKNR=5 HOOKSCR=${HOOK5} ) hook $HOOK fi fi