mirror-sync: - Add a repo-sync sync method for rpm/deb/arch/apk repositories, with installer, prune setting, an optional type filter, and trace fields passed through to it. - Count what each sync changed and publish SYNC_FILES_* / SYNC_CHANGED to the post successful sync hook, so a hook can stay idle on a no-op run. - Make trace saving configurable with save_trace (global and per module) and publish traces from every method, not just rsync. Exclude the local trace from deletes, and stop hierarchy entries stacking up per run. - Add parse_bool so boolean settings reject values that would otherwise be read as their opposite; keep an explicit bare=false from being auto-detected away, and require hide_remote on the clone path too. - MIRROR_SYNC_CONF names an alternate configuration file. - Fix docroot joining when repo has a trailing slash, unparsable last-modified headers skipping every sync, s3cmd --host taking a full URL, and the average rate line for methods with no byte count. - Exit non-zero when help is printed because of a mistake. mirror-file-generator: - Match du summary paths exactly instead of by substring, so sibling repos sharing a prefix no longer return several sizes. - Fall back to the default icon when no image is found, and stop the fallback recursing into itself. - Validate sections against SECTIONS, count repos written to decide whether to publish the index, and create the log directory if missing. - MIRROR_SYNC_CONF support, and exit non-zero on an unknown mirror. Docs: rewrite README around quick start and sync methods, and move the file generator reference to docs/mirror-file-generator.md.
1965 lines
68 KiB
Bash
1965 lines
68 KiB
Bash
#!/bin/bash
|
|
# This script is designed to handle mirror syncing tasks from external mirrors.
|
|
# Each mirror is handled within a module which can be configured via the configuration file /etc/mirror-sync.conf,
|
|
# or another file named by the MIRROR_SYNC_CONF environment variable.
|
|
PATH="/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin:$HOME/.local/bin:$HOME/bin"
|
|
|
|
# Variables for trace generation.
|
|
PROGRAM="mirror-sync"
|
|
VERSION="20260727"
|
|
TRACEHOST=$(hostname -f)
|
|
mirror_hostname=$(hostname -f)
|
|
DATE_STARTED=$(LC_ALL=POSIX LANG=POSIX date -u -R)
|
|
INFO_TRIGGER=cron
|
|
if [[ $SUDO_USER ]]; then
|
|
INFO_TRIGGER=ssh
|
|
fi
|
|
|
|
# Pid file temporary path.
|
|
PIDPATH="/tmp"
|
|
PIDSUFFIX="-mirror-sync.pid"
|
|
PIDFILE="" # To be filled by acquire_lock().
|
|
|
|
# Log file.
|
|
LOGPATH="/var/log/mirror-sync"
|
|
LOGFILE="" # To be filled by acquire_lock().
|
|
ERRORFILE="" # To be filled by acquire_lock().
|
|
error_count=0
|
|
max_errors=3
|
|
tmpDirBase="$HOME/tmp"
|
|
sync_timeout="timeout 1d"
|
|
# Do not check upstream unless it was updated in the last 5 hours.
|
|
upstream_max_age=18000
|
|
# Update anyway if last check was more than 24 hours ago.
|
|
upstream_timestamp_min=86400
|
|
# Publish a project trace file describing this mirror after each successful
|
|
# sync. Every sync method honors this, and an individual module can override it
|
|
# with ${MODULE}_save_trace.
|
|
save_trace="true"
|
|
|
|
# quick-fedora-mirror tool config.
|
|
QFM_GIT="https://pagure.io/quick-fedora-mirror.git"
|
|
QFM_PATH="$HOME/quick-fedora-mirror"
|
|
QFM_BIN="$QFM_PATH/quick-fedora-mirror"
|
|
|
|
# For installing Jigdo
|
|
JIGDO_SOURCE_URL="http://deb.debian.org/debian/pool/main/j/jigdo/jigdo_0.8.0.orig.tar.xz"
|
|
JIGDO_FILE_BIN="$HOME/bin/jigdo-file"
|
|
JIGDO_MIRROR_BIN="$HOME/bin/jigdo-mirror"
|
|
jigdoConf="$HOME/etc/jigdo/jigdo-mirror.conf"
|
|
|
|
# Path to s5cmd bin.
|
|
S5CMD_BIN="$HOME/bin/s5cmd"
|
|
|
|
# Path to repo-sync bin.
|
|
REPO_SYNC_BIN="$HOME/bin/repo-sync"
|
|
|
|
# Prevent run as root.
|
|
if (( EUID == 0 )); then
|
|
echo "Do not mirror as root."
|
|
exit 1
|
|
fi
|
|
|
|
# Load the required configuration file or quit. The default is where a system
|
|
# installation keeps it; MIRROR_SYNC_CONF names another one, so the script can
|
|
# be pointed at a configuration of its own for testing or for a mirror run out
|
|
# of a home directory.
|
|
CONFIG_FILE="${MIRROR_SYNC_CONF:-/etc/mirror-sync.conf}"
|
|
if [[ -f $CONFIG_FILE ]]; then
|
|
# shellcheck source=/dev/null
|
|
source "$CONFIG_FILE"
|
|
else
|
|
echo "No configuration file at '${CONFIG_FILE}', please setup a proper configuration file."
|
|
exit 1
|
|
fi
|
|
|
|
# Files to remove if a sync is interrupted (Archive-Update-in-Progress markers,
|
|
# temporary stage logs, ...). The sync functions append to this list as they
|
|
# create such files so that a Ctrl-C does not leave stale state behind.
|
|
cleanup_files=()
|
|
|
|
# Handle Ctrl-C (SIGINT) and termination (SIGTERM). When run from a terminal
|
|
# the signal is delivered to the whole process group, so the rsync/aws/git/...
|
|
# process that is currently syncing receives it and stops on its own. This
|
|
# handler then cleans up after it: it removes the in-progress markers, temporary
|
|
# logs and the lock file, and exits without counting the interruption as a sync
|
|
# failure (so it does not inflate the error count or trigger error emails).
|
|
handle_interrupt() {
|
|
# Ignore any further interrupts while we clean up.
|
|
trap '' INT TERM
|
|
|
|
echo
|
|
echo "Interrupt received, stopping sync and cleaning up..."
|
|
|
|
# Remove any files registered for cleanup by the running module.
|
|
local _f
|
|
for _f in "${cleanup_files[@]}"; do
|
|
[[ $_f ]] && rm -f "$_f"
|
|
done
|
|
|
|
# Remove the pid lock file for this module so a later run is not blocked.
|
|
[[ $PIDFILE ]] && rm -f "$PIDFILE"
|
|
|
|
echo "Sync interrupted."
|
|
exit 130
|
|
}
|
|
trap handle_interrupt INT TERM
|
|
|
|
# Print the help for this command. An exit status may be given so the paths
|
|
# that print help because of a mistake (an unknown module or option) exit
|
|
# non-zero, letting cron or a calling script tell a misconfiguration apart from
|
|
# a run that simply asked for help.
|
|
print_help() {
|
|
echo "Mirror Sync (${VERSION})"
|
|
echo
|
|
echo "Usage:"
|
|
echo "$0 [--help|--update-support-utilities|--version] {module} [--force]"
|
|
echo
|
|
echo "Available modules:"
|
|
for MODULE in ${MODULES:?}; do
|
|
echo "$MODULE"
|
|
done
|
|
exit "${1:-0}"
|
|
}
|
|
|
|
# Send email to admins about error.
|
|
mail_error() {
|
|
if [[ -z $MAILTO ]]; then
|
|
echo "MAILTO is undefined."
|
|
return
|
|
fi
|
|
{
|
|
cat <<EOF
|
|
Subject: ${PROGRAM} Error
|
|
To: ${MAILTO}
|
|
Auto-Submitted: auto-generated
|
|
MIME-Version: 1.0
|
|
Content-Type: text/plain
|
|
|
|
Host: $(hostname -f)
|
|
Module: ${MODULE}
|
|
Logfile: ${LOGFILE}
|
|
|
|
$*
|
|
EOF
|
|
} | sendmail -i -t
|
|
}
|
|
|
|
#region Tool installation
|
|
|
|
# Installs quick-fedora-mirror and updates.
|
|
quick_fedora_mirror_install() {
|
|
if ! [[ -f $QFM_BIN ]]; then
|
|
echo "quick-fedora-mirror is not on this system, attempting to get it"
|
|
[[ -e $QFM_PATH ]] && rm -Rf "$QFM_PATH"
|
|
git clone "$QFM_GIT" "$QFM_PATH"
|
|
if ! [[ -f $QFM_BIN ]]; then
|
|
echo "Failed to get quick-fedora-mirror!"
|
|
exit 1
|
|
fi
|
|
fi
|
|
(
|
|
if [[ $1 == "-u" ]]; then
|
|
if ! cd "$QFM_PATH"; then
|
|
echo "Unable to enter QFM path."
|
|
exit 1
|
|
fi
|
|
if ! git pull; then
|
|
echo "Unable to update QFM."
|
|
exit 1
|
|
fi
|
|
fi
|
|
) || return 1
|
|
}
|
|
|
|
# Installs jigdo image tool.
|
|
jigdo_install() {
|
|
# Only install if not already installed or update requested.
|
|
if [[ $1 == "-u" ]] || ! [[ -e $JIGDO_FILE_BIN ]]; then
|
|
# Make sure we're in the home dir and bin exists.
|
|
if ! cd "$HOME"; then
|
|
echo "Unable to access home dir."
|
|
exit 1
|
|
fi
|
|
if [[ ! -d bin ]]; then
|
|
mkdir -p bin
|
|
fi
|
|
|
|
# Download jigdo source code.
|
|
if ! wget "$JIGDO_SOURCE_URL" -O jigdo.tar.xz; then
|
|
echo "Unable to download jigdo utility."
|
|
exit 1
|
|
fi
|
|
|
|
# Extract jigdo.
|
|
if ! tar -xvf jigdo.tar.xz; then
|
|
echo "Unable to unarchive jigdo."
|
|
exit 1
|
|
fi
|
|
rm -f jigdo.tar.xz
|
|
(
|
|
if ! cd jigdo-*/; then
|
|
echo "Unable to enter extracted archive."
|
|
exit 1
|
|
fi
|
|
cat > jigdo.patch <<'EOF'
|
|
--- src/util/sha256sum.hh 2019-11-19 10:43:22.000000000 -0500
|
|
+++ src-fix/util/sha256sum.hh 2023-04-19 16:33:40.840831304 -0400
|
|
@@ -27,6 +27,7 @@
|
|
#include <cstring>
|
|
#include <iosfwd>
|
|
#include <string>
|
|
+#include <stdint.h>
|
|
|
|
#include <bstream.hh>
|
|
#include <debug.hh>
|
|
EOF
|
|
patch -u src/util/sha256sum.hh -i jigdo.patch
|
|
if ! ./configure --prefix="$HOME"; then
|
|
echo "Unable to configure jigdo."
|
|
exit 1
|
|
fi
|
|
|
|
# Build fails first few times due to docs, but clears after a few builds.
|
|
if ! make; then
|
|
if ! make; then
|
|
make
|
|
fi
|
|
fi
|
|
make install
|
|
) || return 1
|
|
fi
|
|
}
|
|
|
|
# Installs s5cmd for aws syncing.
|
|
s5cmd_install() {
|
|
# Only install if not installed or update requested.
|
|
if ! [[ -f $S5CMD_BIN ]] || [[ $1 == "-u" ]]; then
|
|
# Make sure we're in the home dir and the bin dir exists.
|
|
if ! cd "$HOME"; then
|
|
echo "Unable to access home dir."
|
|
exit 1
|
|
fi
|
|
if [[ ! -d bin ]]; then
|
|
mkdir -p bin
|
|
fi
|
|
|
|
# Get the latest download URL from github.
|
|
download_url=$(curl -s https://api.github.com/repos/peak/s5cmd/releases/latest | jq '.assets[] | select(.browser_download_url | test("Linux-64bit.tar.gz")?) | .browser_download_url' -r)
|
|
if [[ -z $download_url ]]; then
|
|
echo "Unable to get download url for s5cmd."
|
|
exit 1
|
|
fi
|
|
|
|
# Download s5cmd
|
|
if ! wget "$download_url" -O s5cmd.tar.gz; then
|
|
echo "Unable to download s5cmd."
|
|
exit 1
|
|
fi
|
|
|
|
# Extract and check that s5cmd extracted correctly.
|
|
if ! tar -xf s5cmd.tar.gz; then
|
|
echo "Unable to extract s5cmd."
|
|
exit 1
|
|
fi
|
|
if ! [[ -f s5cmd ]]; then
|
|
echo "Unable to extract s5cmd."
|
|
exit 1
|
|
fi
|
|
|
|
# Remove the tar.gz file.
|
|
rm -f s5cmd.tar.gz
|
|
|
|
# Move s5cmd to the bin path.
|
|
mv s5cmd "$S5CMD_BIN"
|
|
fi
|
|
}
|
|
|
|
# Installs repo-sync for package repository syncing.
|
|
repo_sync_install() {
|
|
# Only install if not installed or update requested.
|
|
if ! [[ -f $REPO_SYNC_BIN ]] || [[ $1 == "-u" ]]; then
|
|
# Make sure we're in the home dir and the bin dir exists.
|
|
if ! cd "$HOME"; then
|
|
echo "Unable to access home dir."
|
|
exit 1
|
|
fi
|
|
if [[ ! -d bin ]]; then
|
|
mkdir -p bin
|
|
fi
|
|
|
|
# Releases are published per architecture, so map this machine's
|
|
# architecture onto the name used in the release asset.
|
|
machine=$(uname -m)
|
|
case "$machine" in
|
|
x86_64|amd64) release_arch="amd64" ;;
|
|
i386|i486|i586|i686) release_arch="386" ;;
|
|
aarch64|arm64) release_arch="arm64" ;;
|
|
armv6*|armv7*|armhf|arm) release_arch="armv6" ;;
|
|
ppc64le) release_arch="ppc64le" ;;
|
|
*)
|
|
echo "Unsupported architecture '${machine}' for repo-sync."
|
|
exit 1
|
|
;;
|
|
esac
|
|
|
|
# Get the latest download URL from github.
|
|
download_url=$(curl -s https://api.github.com/repos/grmrgecko/repo-sync/releases/latest | jq --arg suffix "_linux_${release_arch}.tar.gz" '.assets[] | select(.name | endswith($suffix)) | .browser_download_url' -r)
|
|
if [[ -z $download_url ]]; then
|
|
echo "Unable to get download url for repo-sync."
|
|
exit 1
|
|
fi
|
|
|
|
# Download repo-sync
|
|
if ! wget "$download_url" -O repo-sync.tar.gz; then
|
|
echo "Unable to download repo-sync."
|
|
exit 1
|
|
fi
|
|
|
|
# Extract just the binary. The archive also ships a LICENSE and
|
|
# README.md, which would clobber files in the home directory.
|
|
if ! tar -xf repo-sync.tar.gz repo-sync; then
|
|
echo "Unable to extract repo-sync."
|
|
exit 1
|
|
fi
|
|
if ! [[ -f repo-sync ]]; then
|
|
echo "Unable to extract repo-sync."
|
|
exit 1
|
|
fi
|
|
|
|
# Remove the tar.gz file.
|
|
rm -f repo-sync.tar.gz
|
|
|
|
# Move repo-sync to the bin path.
|
|
chmod +x repo-sync
|
|
mv repo-sync "$REPO_SYNC_BIN"
|
|
fi
|
|
}
|
|
|
|
# Updates the mirror support utilties on server with upstream.
|
|
update_support_utilities() {
|
|
quick_fedora_mirror_install -u
|
|
jigdo_install -u
|
|
s5cmd_install -u
|
|
repo_sync_install -u
|
|
}
|
|
|
|
# Builds iso images from jigdo files.
|
|
jigdo_hook() {
|
|
# Ensure jigdo is installed.
|
|
jigdo_install
|
|
|
|
# Determine the current version of Debian.
|
|
currentVersion=$(ls -l "${repo}/current")
|
|
currentVersion="${currentVersion##* -> }"
|
|
versionDir="$(realpath "$repo")/${currentVersion}"
|
|
|
|
# For each architecture, run jigdo to build iso files.
|
|
for a in "$versionDir"/*/; do
|
|
arch=$(basename "$a")
|
|
|
|
# Determine what releases are needed for this architecture.
|
|
sets=$(cat "${repo}/project/build/${currentVersion}/${arch}")
|
|
|
|
# For each set, build iso files.
|
|
for s in $sets; do
|
|
# Determine the jigdo and iso dir for this set.
|
|
jigdoDir="${repo}/${currentVersion}/${arch}/jigdo-${s}"
|
|
imageDir="${repo}/${currentVersion}/${arch}/iso-${s}"
|
|
|
|
# Create iso dir if not already made.
|
|
if [[ ! -d $imageDir ]]; then
|
|
mkdir -p "$imageDir"
|
|
fi
|
|
|
|
# Copy SUM files from the jigdo dir over to the new ISO dir.
|
|
# Sums are now SHA256SUMS and SHA512SUMS.
|
|
cp -a "${jigdoDir}"/*SUMS* "${imageDir}/"
|
|
|
|
# Build jigdo configuration.
|
|
cat >"${jigdoConf:?}.${arch}.${s}" <<EOF
|
|
LOGROTATE=14
|
|
jigdoFile="$JIGDO_FILE_BIN --cache=\$tmpDir/jigdo-cache.db --cache-expiry=1w --report=noprogress --no-check-files"
|
|
debianMirror="file:${jigdo_pkg_repo:-}"
|
|
nonusMirror="file:/tmp"
|
|
include='.' # include all files,
|
|
exclude='^$' # then exclude none
|
|
jigdoDir=${jigdoDir}
|
|
imageDir=${imageDir}
|
|
tmpDir=${tmpDirBase:?}/${arch}.${s}
|
|
#logfile=${LOGPATH}/${MODULE}-${arch}.${s}.log
|
|
EOF
|
|
|
|
# Run jigdo.
|
|
echo "Running jigdo for ${arch}.${s}"
|
|
$JIGDO_MIRROR_BIN "${jigdoConf:?}.${arch}.${s}"
|
|
done
|
|
done
|
|
}
|
|
|
|
# Sum a "Field: count" statistic across every run recorded in a log. Both
|
|
# rsync's --stats and repo-sync's run summary report in this form, and one log
|
|
# can hold several runs, as quick-fedora-mirror calls rsync once per module. A
|
|
# log that recorded no run at all counts as none.
|
|
sum_log_stat() {
|
|
[[ -f $2 ]] || { echo 0; return; }
|
|
awk -F': ' -v field="$1" '
|
|
$1==field {split($2, count, " "); total+=count[1]}
|
|
END {print total+0}
|
|
' "$2" 2>/dev/null
|
|
}
|
|
|
|
# Count what an rsync run changed, from the itemized change lines in one or
|
|
# more of its logs. rsync prints one line per item it acts on: "*deleting" for
|
|
# something it removed, and otherwise an eleven character summary of the change
|
|
# such as ">f+++++++++", followed by the item's path. quick-fedora-mirror
|
|
# formats those lines with a leading marker, which is skipped over.
|
|
#
|
|
# Only content counts. Directories are left out, as an unchanged directory is
|
|
# still restamped whenever something inside it changes, and so are the trace
|
|
# files this script maintains, which it rewrites after every sync no matter
|
|
# what upstream did. The two counts are printed as "transferred deleted".
|
|
count_rsync_changes() {
|
|
local _logs=() _log
|
|
for _log in "$@"; do
|
|
[[ -f $_log ]] && _logs+=("$_log")
|
|
done
|
|
if (( ${#_logs[@]} == 0 )); then
|
|
echo "0 0"
|
|
return
|
|
fi
|
|
awk '
|
|
{
|
|
field = ($1 == "@") ? 2 : 1
|
|
summary = $field
|
|
name = $(field+1)
|
|
if (name ~ /(^|\/)project\/trace\//) next
|
|
if (summary == "*deleting") {
|
|
if (name !~ /\/$/) deleted++
|
|
next
|
|
}
|
|
if (summary ~ /^[<>ch.][fLDS]/) transferred++
|
|
}
|
|
END {print transferred+0, deleted+0}
|
|
' "${_logs[@]}"
|
|
}
|
|
|
|
# Count how many times a message appears in a log. Sync tools that show
|
|
# progress rewrite the current line with a carriage return rather than ending
|
|
# it, so several messages can end up sharing one line; occurrences are counted
|
|
# rather than lines, and a message is only recognized where a line or a
|
|
# carriage return puts it at the start of the output.
|
|
count_sync_events() {
|
|
local _log=$1 _message=$2 _cr=$'\r'
|
|
[[ -f $_log ]] || { echo 0; return; }
|
|
grep -oE "(^|${_cr})${_message}" "$_log" 2>/dev/null | wc -l
|
|
}
|
|
|
|
# Pull a field from a trace file or rsync stats.
|
|
extract_trace_field() {
|
|
value=$(awk -F': ' "\$1==\"$1\" {print \$2; exit}" "$2" 2>/dev/null)
|
|
[[ $value ]] || return 1
|
|
echo "$value"
|
|
}
|
|
|
|
# Build content for a trace file that contains info on a repository.
|
|
build_trace_content() {
|
|
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 $TRACE_MASTER_FILE ]]; then
|
|
echo "Archive serial: $(extract_trace_field 'Archive serial' "$TRACE_MASTER_FILE" || echo unknown )"
|
|
fi
|
|
|
|
echo "Used ${PROGRAM} version: ${VERSION}"
|
|
echo "Creator: ${PROGRAM} ${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
|
|
|
|
# Depending on repo type, find architectures supported.
|
|
ARCH_REGEX='(source|SRPMS|amd64|mips64el|mipsel|i386|x86_64|aarch64|ppc64le|ppc64el|s390x|armhf)'
|
|
if [[ $repo_type == "deb" ]]; then
|
|
ARCH=$(find "${repo}/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
|
|
elif [[ $repo_type == "rpm" ]]; then
|
|
ARCH=$(find "$repo" -name 'repomd.xml' 2>/dev/null |
|
|
grep -Po "$ARCH_REGEX" |
|
|
sort -u | tr '\n' ' ')
|
|
if [[ $ARCH ]]; then
|
|
echo "Architectures: ${ARCH}"
|
|
fi
|
|
elif [[ $repo_type == "iso" ]]; then
|
|
ARCH=$(find "$repo" -name '*.iso' 2>/dev/null |
|
|
grep -Po "$ARCH_REGEX" |
|
|
sort -u | tr '\n' ' ')
|
|
if [[ $ARCH ]]; then
|
|
echo "Architectures: ${ARCH}"
|
|
fi
|
|
elif [[ $repo_type == "source" ]]; then
|
|
echo "Architectures: source"
|
|
fi
|
|
echo "Architectures-Configuration: ${arch_configurations:-ALL}"
|
|
|
|
echo "Upstream-mirror: ${RSYNC_HOST:-unknown}"
|
|
|
|
# Total bytes synced per rsync stage.
|
|
total=0
|
|
if [[ -f $LOGFILE_SYNC ]]; then
|
|
all_bytes=$(sed -Ene 's/(^|.* )sent ([0-9]+) bytes received ([0-9]+) bytes.*/\3/p' "$LOGFILE_SYNC")
|
|
for bytes in $all_bytes; do
|
|
total=$(( total + bytes ))
|
|
done
|
|
elif [[ -f $LOGFILE_STAGE1 ]]; then
|
|
all_bytes=$(sed -Ene 's/(^|.* )sent ([0-9]+) bytes received ([0-9]+) bytes.*/\3/p' "$LOGFILE_STAGE1")
|
|
for bytes in $all_bytes; do
|
|
total=$(( total + bytes ))
|
|
done
|
|
fi
|
|
if [[ -f $LOGFILE_STAGE2 ]]; then
|
|
all_bytes=$(sed -Ene 's/(^|.* )sent ([0-9]+) bytes received ([0-9]+) bytes.*/\3/p' "$LOGFILE_STAGE2")
|
|
for bytes in $all_bytes; do
|
|
total=$(( total + bytes ))
|
|
done
|
|
fi
|
|
if (( total > 0 )); then
|
|
echo "Total bytes received in rsync: ${total}"
|
|
fi
|
|
|
|
# Calculate time per rsync stage and print both stages if both were started.
|
|
total_time=0
|
|
if [[ $sync_started ]]; then
|
|
STATS_TOTAL_RSYNC_TIME1=$(( sync_ended - sync_started ))
|
|
total_time=$STATS_TOTAL_RSYNC_TIME1
|
|
elif [[ $stage1_started ]]; then
|
|
STATS_TOTAL_RSYNC_TIME1=$(( stage1_ended - stage1_started ))
|
|
total_time=$STATS_TOTAL_RSYNC_TIME1
|
|
fi
|
|
if [[ $stage2_started ]]; then
|
|
STATS_TOTAL_RSYNC_TIME2=$(( stage2_ended - stage2_started ))
|
|
total_time=$(( total_time + 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}"
|
|
fi
|
|
echo "Total time spent in rsync: ${total_time}"
|
|
# Only the rsync based methods report a byte count, so without one there is
|
|
# no rate to state; printing 0 B/s would claim the sync moved nothing.
|
|
if (( total > 0 && total_time != 0 )); then
|
|
rate=$(( total / total_time ))
|
|
echo "Average rate: ${rate} B/s"
|
|
fi
|
|
}
|
|
|
|
# Reduce an upstream location to the host it names, for the trace's
|
|
# Upstream-mirror field. Sources come in several shapes across the sync
|
|
# methods: rsync URLs and rsync daemon syntax (host::module), http(s) and ftp
|
|
# URLs, s3 bucket URLs, and space separated lists of URLs. Only the first entry
|
|
# of a list is reported, as the trace names a single upstream.
|
|
upstream_host() {
|
|
local _host="${1%% *}" # First entry of a space separated list.
|
|
_host="${_host#*://}" # Drop any scheme.
|
|
_host="${_host#*@}" # Drop any credentials.
|
|
_host="${_host%%/*}" # Drop the path.
|
|
_host="${_host%%:*}" # Drop the port, or the rsync daemon module.
|
|
echo "$_host"
|
|
}
|
|
|
|
# For modules that are repositories (with RPM, DEB, ISOs, or source code),
|
|
# build a project trace file with information about the repo.
|
|
save_trace_file() {
|
|
# Trace file/dir paths.
|
|
TRACE_DIR="${repo}/project/trace"
|
|
mkdir -p "$TRACE_DIR"
|
|
TRACE_FILE="${TRACE_DIR}/${mirror_hostname:?}"
|
|
TRACE_MASTER_FILE="${TRACE_DIR}/master"
|
|
TRACE_HIERARCHY="${TRACE_DIR}/_hierarchy"
|
|
|
|
# Parse the upstream host. Modules define their upstream differently, so a
|
|
# method may set trace_upstream to whatever names its source; the rsync
|
|
# based methods leave it unset and their source is used.
|
|
RSYNC_HOST=$(upstream_host "${trace_upstream:-${source:-}}")
|
|
|
|
# Build trace and save to file.
|
|
build_trace_content > "${TRACE_FILE}.new"
|
|
mv "${TRACE_FILE}.new" "$TRACE_FILE"
|
|
|
|
# Build the hierarchy file, which lists every mirror the content passed
|
|
# through. The upstream entries come from the copy saved on the previous
|
|
# run; this mirror's own entry is dropped from that base before a fresh one
|
|
# is appended, so repeated syncs replace the entry rather than stacking a
|
|
# new copy of it on every run.
|
|
local _self _name _rest
|
|
_self=$(basename "$TRACE_FILE")
|
|
{
|
|
if [[ -e "${TRACE_HIERARCHY}.mirror" ]]; then
|
|
while read -r _name _rest; do
|
|
[[ $_name ]] || continue
|
|
[[ $_name == "$_self" ]] && continue
|
|
echo "$_name $_rest"
|
|
done < "${TRACE_HIERARCHY}.mirror"
|
|
fi
|
|
echo "${_self} $mirror_hostname $TRACEHOST ${RSYNC_HOST:-unknown}"
|
|
} > "${TRACE_HIERARCHY}.new"
|
|
mv "${TRACE_HIERARCHY}.new" "$TRACE_HIERARCHY"
|
|
cp "$TRACE_HIERARCHY" "${TRACE_HIERARCHY}.mirror"
|
|
|
|
# Output all traces to _traces file. Disabling shell check because the glob in this case is used right.
|
|
# shellcheck disable=SC2035
|
|
(cd "$TRACE_DIR" && find * -type f \! -name "_*") > "$TRACE_DIR/_traces"
|
|
}
|
|
|
|
# Acquire a sync lock for this command.
|
|
acquire_lock() {
|
|
MODULE=$1
|
|
# Pid file for this module sync.
|
|
PIDFILE="${PIDPATH}/${MODULE}${PIDSUFFIX}"
|
|
LOGFILE="${LOGPATH}/${MODULE}.log"
|
|
ERRORFILE="${LOGPATH}/${MODULE}.error_count"
|
|
if [[ -e $ERRORFILE ]]; then
|
|
error_count=$(cat "$ERRORFILE")
|
|
fi
|
|
|
|
# Redirect stdout to both stdout and log file.
|
|
mkdir -p "$LOGPATH"
|
|
exec 1> >(tee -a "$LOGFILE")
|
|
# Redirect errors to stdout so they also are logged.
|
|
exec 2>&1
|
|
|
|
# Check existing pid file.
|
|
if [[ -f $PIDFILE ]]; then
|
|
PID=$(cat "$PIDFILE")
|
|
# Prevent double locks.
|
|
if [[ $PID == "$BASHPID" ]]; then
|
|
echo "Double lock detected."
|
|
exit 1
|
|
fi
|
|
|
|
# Check if PID is active.
|
|
if ps -p "$PID" >/dev/null; then
|
|
echo "A sync is already in progress for ${MODULE} with pid ${PID}."
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
# Create a new pid file for this process.
|
|
echo "$BASHPID" >"$PIDFILE"
|
|
|
|
# On exit, remove pid file.
|
|
trap 'rm -f "$PIDFILE"' EXIT
|
|
}
|
|
|
|
log_start_header() {
|
|
echo
|
|
echo "=========================================="
|
|
echo "Starting execution: $(date +"%Y-%m-%d %T")"
|
|
echo "=========================================="
|
|
echo
|
|
}
|
|
|
|
log_end_header() {
|
|
echo
|
|
echo "=========================================="
|
|
echo "Execution complete: $(date +"%Y-%m-%d %T")"
|
|
echo "=========================================="
|
|
}
|
|
|
|
# Build dsum totals files if defined.
|
|
rebuild_dusum_totals() {
|
|
# Rebuild killo byte total.
|
|
if [[ $dusum_kbytes_total_file ]]; then
|
|
{
|
|
date
|
|
totalKBytes=0
|
|
for _DUSUM_MODULE in ${MODULES:?}; do
|
|
eval _dusum="\${${_DUSUM_MODULE}_dusum:-}"
|
|
if [[ -n $_dusum ]] && [[ -f $_dusum ]]; then
|
|
while read -r size path; do
|
|
if [[ -n $size ]]; then
|
|
totalKBytes=$((totalKBytes+size))
|
|
printf "%-12s %s\n" "$size" "$path"
|
|
fi
|
|
done < "$_dusum"
|
|
fi
|
|
done
|
|
printf "%-12s %s\n" "$totalKBytes" "total"
|
|
} > "$dusum_kbytes_total_file"
|
|
fi
|
|
|
|
# Rebuild human readable total.
|
|
if [[ $dusum_human_readable_total_file ]]; then
|
|
{
|
|
date
|
|
totalKBytes=0
|
|
for _DUSUM_MODULE in ${MODULES:?}; do
|
|
eval _dusum="\${${_DUSUM_MODULE}_dusum:-}"
|
|
if [[ -n $_dusum ]] && [[ -f $_dusum ]]; then
|
|
while read -r size path; do
|
|
if [[ -n $size ]]; then
|
|
totalKBytes=$((totalKBytes+size))
|
|
printf "%-5s %s\n" "$(echo "$size*1024" | bc | numfmt --to=iec)" "$path"
|
|
fi
|
|
done < "$_dusum"
|
|
fi
|
|
done
|
|
printf "%-5s %s\n" "$(echo "$totalKBytes*1024" | bc | numfmt --to=iec)" "total"
|
|
} > "$dusum_human_readable_total_file"
|
|
fi
|
|
}
|
|
|
|
# Counts describing what a sync changed. Every sync method publishes these
|
|
# before it runs any hook, so a hook can stay idle on a run that found nothing
|
|
# new:
|
|
#
|
|
# example_post_successful_sync_hook='[[ $SYNC_CHANGED ]] && do_something'
|
|
#
|
|
# They are exported so a hook that calls out to a script sees them as well.
|
|
# What counts as a file differs between the sync methods, and a few of them can
|
|
# only report what their tool chose to print, so the counts are each method's
|
|
# best statement of what it moved rather than a number to compare across
|
|
# methods. SYNC_CHANGED is the dependable part: it is set whenever a method saw
|
|
# any change at all.
|
|
reset_sync_counts() {
|
|
SYNC_FILES_TRANSFERRED=0
|
|
SYNC_FILES_DELETED=0
|
|
SYNC_FILES_CHANGED=0
|
|
SYNC_REFS_UPDATED=0
|
|
SYNC_CHANGED=""
|
|
export SYNC_FILES_TRANSFERRED SYNC_FILES_DELETED SYNC_FILES_CHANGED
|
|
export SYNC_REFS_UPDATED SYNC_CHANGED
|
|
}
|
|
|
|
# Publish what a sync changed, and report it to the log. A third argument
|
|
# states whether the mirror changed for a method whose changes are not measured
|
|
# in files (git counts refs); without one, any counted file is a change.
|
|
set_sync_counts() {
|
|
SYNC_FILES_TRANSFERRED=${1:-0}
|
|
SYNC_FILES_DELETED=${2:-0}
|
|
SYNC_FILES_CHANGED=$((SYNC_FILES_TRANSFERRED+SYNC_FILES_DELETED))
|
|
if (( $# >= 3 )); then
|
|
SYNC_CHANGED=$3
|
|
elif (( SYNC_FILES_CHANGED > 0 )); then
|
|
SYNC_CHANGED="true"
|
|
else
|
|
SYNC_CHANGED=""
|
|
fi
|
|
echo "Files changed: ${SYNC_FILES_CHANGED} (${SYNC_FILES_TRANSFERRED} transferred, ${SYNC_FILES_DELETED} deleted)"
|
|
}
|
|
|
|
# Items to do post an successful sync.
|
|
post_successful_sync() {
|
|
# Save timestamp of last sync.
|
|
if [[ $timestamp ]]; then
|
|
date +%s > "$timestamp"
|
|
fi
|
|
|
|
# Remove error count file if existing.
|
|
if [[ -e $ERRORFILE ]]; then
|
|
rm -f "$ERRORFILE"
|
|
fi
|
|
|
|
# Update repo directory sum.
|
|
if [[ $dusum ]]; then
|
|
# Get a sum, store to variable first to avoid having an empty file when another cron finishes.
|
|
SUM=$({
|
|
# If modules are defined, sum each module directory.
|
|
if [[ $modules ]]; then
|
|
for module in $modules; do
|
|
du -s "${docroot%/}/$(module_dir "$module")/"
|
|
done
|
|
else
|
|
# Standard repo sum.
|
|
du -s "$repo"
|
|
fi
|
|
} 2>/dev/null)
|
|
|
|
# Save sum to file.
|
|
echo "$SUM" > "$dusum"
|
|
|
|
rebuild_dusum_totals
|
|
fi
|
|
}
|
|
|
|
# Run the configured post-successful-sync hook, if any. This is called at the
|
|
# very end of each sync method, after all cleanup has finished and the sync has
|
|
# been confirmed successful (a failed sync exits earlier via post_failed_sync).
|
|
run_post_successful_sync_hook() {
|
|
eval post_successful_sync_hook="\$${MODULE}_post_successful_sync_hook"
|
|
if [[ $post_successful_sync_hook ]]; then
|
|
echo "Executing post-successful-sync hook:"
|
|
eval "$post_successful_sync_hook"
|
|
fi
|
|
}
|
|
|
|
# On failed sync, count failure and exit.
|
|
post_failed_sync() {
|
|
echo "Sync failed."
|
|
|
|
# Update failure count.
|
|
new_error_count=$((error_count+1))
|
|
|
|
# If failure count is over our maximum defined errors, email about the failure.
|
|
if ((new_error_count>max_errors)); then
|
|
mail_error "Unable to sync with $MODULE, check logs."
|
|
|
|
# Remove the error count file so that the count resets.
|
|
rm -f "$ERRORFILE"
|
|
|
|
# Exit to not save the updated count.
|
|
exit 1
|
|
fi
|
|
|
|
# Update the error count file and exit.
|
|
echo "$new_error_count" > "$ERRORFILE"
|
|
exit 1
|
|
}
|
|
|
|
# Read common configurations, start logging, and acquire lock.
|
|
module_config() {
|
|
MODULE=$1
|
|
acquire_lock "$MODULE"
|
|
reset_sync_counts
|
|
|
|
# Read the configuration for this module.
|
|
eval repo="\$${MODULE}_repo"
|
|
eval timestamp="\$${MODULE}_timestamp"
|
|
eval dusum="\$${MODULE}_dusum"
|
|
eval options="\$${MODULE}_options"
|
|
|
|
# If configuration is not set, exit.
|
|
if [[ ! $repo ]]; then
|
|
echo "No configuration exists for ${MODULE}"
|
|
exit 1
|
|
fi
|
|
resolve_save_trace
|
|
log_start_header
|
|
}
|
|
|
|
# Normalize a boolean configuration in place. Only "true", "false", "1" and "0"
|
|
# are accepted, so a value that was meant to enable something but does not spell
|
|
# it one of those ways is reported instead of being quietly read as its
|
|
# opposite. An unset value takes the given default. The named variable is left
|
|
# holding "true" or the empty string, so callers can test it with [[ ]].
|
|
#
|
|
# The variable is passed by name rather than by value because a value returned
|
|
# through a command substitution would be produced in a subshell, where a
|
|
# rejected value could not stop the script.
|
|
parse_bool() {
|
|
local _var=$1 _default=$2 _config=$3 _value _resolved
|
|
eval _value="\$${_var}"
|
|
_resolved="${_value:-$_default}"
|
|
case "${_resolved,,}" in
|
|
true|1) eval "${_var}=true" ;;
|
|
false|0) eval "${_var}=" ;;
|
|
*)
|
|
echo "Invalid value '${_value}' for ${_config}, expected true, false, 1 or 0."
|
|
exit 1
|
|
;;
|
|
esac
|
|
}
|
|
|
|
# Resolve whether this module publishes a trace file. The module setting wins
|
|
# when present, otherwise the global save_trace applies, which defaults to on.
|
|
# Every sync method reads the result from save_trace_enabled.
|
|
resolve_save_trace() {
|
|
eval save_trace_enabled="\$${MODULE}_save_trace"
|
|
if [[ $save_trace_enabled ]]; then
|
|
parse_bool save_trace_enabled "true" "${MODULE}_save_trace"
|
|
else
|
|
save_trace_enabled="${save_trace:-}"
|
|
parse_bool save_trace_enabled "true" "save_trace"
|
|
fi
|
|
}
|
|
|
|
# List every ref in the repository, so what a sync changed can be measured by
|
|
# comparing the listing against one taken beforehand. Tags and remote tracking
|
|
# refs are listed alongside branches, so an update that moved any of them is
|
|
# seen.
|
|
git_ref_state() {
|
|
git -C "${repo:?}" show-ref 2>/dev/null
|
|
}
|
|
|
|
# Count the refs that differ between two listings. Refs are compared by name,
|
|
# so one that moved, one that appeared and one that was deleted each count
|
|
# once. Blank lines are ignored, which is what an empty listing (a repository
|
|
# that did not exist yet) reads as.
|
|
git_changed_refs() {
|
|
awk '
|
|
NF<2 {next}
|
|
NR==FNR {before[$2]=$1; next}
|
|
{after[$2]=$1}
|
|
END {
|
|
for (ref in after) if (!(ref in before) || before[ref]!=after[ref]) changed++
|
|
for (ref in before) if (!(ref in after)) changed++
|
|
print changed+0
|
|
}
|
|
' <(echo "$1") <(echo "$2")
|
|
}
|
|
|
|
# Publish what a git sync changed, given the ref listing and the checked out
|
|
# commit from before it ran. A bare mirror has no working tree whose files
|
|
# could be counted, so refs are all it reports. A working tree also counts the
|
|
# files the update brought in, by comparing the commit checked out before
|
|
# against the one checked out now; a fresh clone has no commit from before, so
|
|
# its whole tree is new.
|
|
git_sync_counts() {
|
|
local _refs_before=$1 _head_before=$2 _head_after _transferred=0 _deleted=0 _changed=""
|
|
|
|
SYNC_REFS_UPDATED=$(git_changed_refs "$_refs_before" "$(git_ref_state)")
|
|
|
|
if [[ ! $bare ]]; then
|
|
_head_after=$(git -C "${repo:?}" rev-parse HEAD 2>/dev/null)
|
|
if [[ $_head_before ]] && [[ $_head_after ]]; then
|
|
_transferred=$(git -C "${repo:?}" diff --name-only --diff-filter=d "$_head_before" "$_head_after" 2>/dev/null | wc -l)
|
|
_deleted=$(git -C "${repo:?}" diff --name-only --diff-filter=D "$_head_before" "$_head_after" 2>/dev/null | wc -l)
|
|
elif [[ $_head_after ]]; then
|
|
_transferred=$(git -C "${repo:?}" ls-files 2>/dev/null | wc -l)
|
|
fi
|
|
fi
|
|
|
|
if (( SYNC_REFS_UPDATED > 0 || _transferred > 0 || _deleted > 0 )); then
|
|
_changed="true"
|
|
fi
|
|
set_sync_counts "$_transferred" "$_deleted" "$_changed"
|
|
echo "Refs updated: ${SYNC_REFS_UPDATED}"
|
|
}
|
|
|
|
# Maintenance to run after a successful sync (or clone) of a bare repository.
|
|
# Bare repositories are typically served to clients over "dumb" HTTP (a plain
|
|
# file server), which requires the info files kept up to date and, optionally,
|
|
# the upstream URL scrubbed so it cannot be read from the served files.
|
|
git_bare_post_sync() {
|
|
# When hiding the remote, strip anything that records the upstream URL so it
|
|
# cannot be probed over dumb HTTP. `config` holds it under [remote ...] and
|
|
# `git fetch` writes it into FETCH_HEAD on every run.
|
|
if [[ $hide_remote ]]; then
|
|
local _remote
|
|
for _remote in $(git -C "${repo:?}" remote 2>/dev/null); do
|
|
git -C "${repo:?}" remote remove "$_remote"
|
|
done
|
|
rm -f "${repo:?}/FETCH_HEAD"
|
|
fi
|
|
|
|
# Regenerate the dumb-HTTP info files (info/refs and objects/info/packs).
|
|
# A mirror fetch never fires the post-update hook (that only runs on push),
|
|
# so this has to run explicitly after each sync. It is cheap, so rather than
|
|
# try to detect whether it is needed we simply run it every time.
|
|
git -C "${repo:?}" update-server-info
|
|
}
|
|
|
|
# Sync git based mirrors.
|
|
#
|
|
# This is the one method that publishes no trace file, so save_trace does not
|
|
# apply to it. A trace is an ordinary file inside the repository: in a checked
|
|
# out working tree it shows up as untracked and leaves the tree permanently
|
|
# dirty, and in a bare repository it means dropping a directory that is not part
|
|
# of the object store into the repository root. Neither is worth a trace, and
|
|
# naming the upstream would work against hide_remote besides.
|
|
git_sync() {
|
|
# Start the module.
|
|
module_config "$1"
|
|
|
|
# Read git specific configuration.
|
|
eval source="\$${MODULE}_source"
|
|
eval bare="\$${MODULE}_bare"
|
|
eval hide_remote="\$${MODULE}_hide_remote"
|
|
|
|
# An unset bare flag stays empty so the repository can be auto-detected
|
|
# below; only an explicit value is normalized here. Normalization maps
|
|
# false to the empty string, which an unset flag also has, so whether the
|
|
# flag was explicitly configured is remembered separately to keep an
|
|
# explicit false from being auto-detected away.
|
|
bare_configured=""
|
|
if [[ $bare ]]; then
|
|
parse_bool bare "false" "${MODULE}_bare"
|
|
bare_configured=1
|
|
fi
|
|
|
|
# Hiding the remote requires a configured source, since without a stored
|
|
# remote the URL to fetch from must come from the configuration on every
|
|
# sync.
|
|
parse_bool hide_remote "false" "${MODULE}_hide_remote"
|
|
if [[ $hide_remote ]] && [[ ! $source ]]; then
|
|
echo "hide_remote requires a source to be configured."
|
|
exit 1
|
|
fi
|
|
|
|
# If the destination does not yet contain a git repository, clone it from
|
|
# the configured source. Bare repositories are cloned with --mirror so the
|
|
# configured fetch refspec mirrors all refs from upstream.
|
|
if [[ ! -e "${repo:?}/.git" ]] && [[ ! -e "${repo:?}/HEAD" ]]; then
|
|
if [[ ! $source ]]; then
|
|
echo "No git repository at '${repo:?}' and no source defined to clone from."
|
|
exit 1
|
|
fi
|
|
# There is no repository to auto-detect yet, so the bare flag is
|
|
# whatever was configured. Refuse the same combination the update path
|
|
# refuses rather than cloning a working tree and leaving the upstream
|
|
# URL in the served config, which is what hide_remote exists to prevent.
|
|
if [[ $hide_remote ]] && [[ ! $bare ]]; then
|
|
echo "hide_remote requires a bare repository."
|
|
exit 1
|
|
fi
|
|
echo "Cloning '${source}' into '${repo:?}'."
|
|
if [[ $bare ]]; then
|
|
eval git clone --mirror ${options:+$options} "'${source}'" "'${repo:?}'"
|
|
else
|
|
eval git clone ${options:+$options} "'${source}'" "'${repo:?}'"
|
|
fi
|
|
RT=$?
|
|
if (( RT == 0 )); then
|
|
# Nothing existed here before the clone, so it is measured against
|
|
# an empty repository.
|
|
git_sync_counts "" ""
|
|
[[ $bare ]] && git_bare_post_sync
|
|
post_successful_sync
|
|
else
|
|
post_failed_sync
|
|
fi
|
|
run_post_successful_sync_hook
|
|
log_end_header
|
|
return
|
|
fi
|
|
|
|
# Move into the repo folder to sync.
|
|
if ! cd "${repo:?}"; then
|
|
echo "Failed to access '${repo:?}' git repository."
|
|
exit 1
|
|
fi
|
|
|
|
# Determine whether the repository is bare. Honor an explicit configuration
|
|
# if set, otherwise auto-detect. Bare repositories have no working tree, so a
|
|
# `git pull` is not possible; instead update by fetching the configured
|
|
# refspec.
|
|
if [[ ! $bare_configured ]] && [[ $(git rev-parse --is-bare-repository 2>/dev/null) == "true" ]]; then
|
|
bare="true"
|
|
fi
|
|
# Hiding the remote only happens in the bare update path, so requiring it
|
|
# here keeps a working tree from silently serving the upstream URL.
|
|
if [[ $hide_remote ]] && [[ ! $bare ]]; then
|
|
echo "hide_remote requires a bare repository."
|
|
exit 1
|
|
fi
|
|
# Record what the repository holds now, so the sync can be measured
|
|
# against it once it has run.
|
|
refs_before=$(git_ref_state)
|
|
head_before=$(git -C "${repo:?}" rev-parse HEAD 2>/dev/null)
|
|
|
|
if [[ $bare ]]; then
|
|
# When hiding the remote there is no stored remote to update, so fetch
|
|
# directly from the source with a mirror refspec. Otherwise honor the
|
|
# remote's configured fetch refspec (e.g. from a --mirror clone).
|
|
if [[ $hide_remote ]]; then
|
|
eval git fetch --prune ${options:+$options} "'${source}'" "'+refs/*:refs/*'"
|
|
else
|
|
eval git remote update --prune ${options:+$options}
|
|
fi
|
|
else
|
|
eval git pull ${options:+$options}
|
|
fi
|
|
RT=$?
|
|
if (( RT == 0 )); then
|
|
# Count before any post-sync maintenance runs, as hiding the remote
|
|
# drops the refs it stored and that is not a change from upstream.
|
|
git_sync_counts "$refs_before" "$head_before"
|
|
[[ $bare ]] && git_bare_post_sync
|
|
post_successful_sync
|
|
else
|
|
post_failed_sync
|
|
fi
|
|
|
|
run_post_successful_sync_hook
|
|
log_end_header
|
|
}
|
|
|
|
# Read config common for AWS.
|
|
read_aws_config() {
|
|
eval bucket="\$${MODULE}_aws_bucket"
|
|
eval AWS_ACCESS_KEY_ID="\$${MODULE}_aws_access_key"
|
|
export AWS_ACCESS_KEY_ID
|
|
eval AWS_SECRET_ACCESS_KEY="\$${MODULE}_aws_secret_key"
|
|
export AWS_SECRET_ACCESS_KEY
|
|
eval AWS_ENDPOINT_URL="\$${MODULE}_aws_endpoint_url"
|
|
|
|
# The trace names the service the objects came from, which is the endpoint
|
|
# when one is configured and otherwise the bucket itself.
|
|
trace_upstream="${AWS_ENDPOINT_URL:-$bucket}"
|
|
}
|
|
|
|
# The path of this mirror's own trace file, relative to the repository root.
|
|
# The sync methods that delete removed files have to be told to leave it alone,
|
|
# as it exists only locally and would otherwise be treated as removed upstream.
|
|
trace_relative_path() {
|
|
echo "project/trace/${mirror_hostname:?}"
|
|
}
|
|
|
|
# Sync AWS S3 bucket based mirrors.
|
|
aws_sync() {
|
|
# Start the module.
|
|
module_config "$1"
|
|
read_aws_config
|
|
|
|
if [[ -n $AWS_ENDPOINT_URL ]]; then
|
|
options="$options --endpoint-url='$AWS_ENDPOINT_URL'"
|
|
fi
|
|
|
|
# Keep --delete from removing the trace file this module publishes.
|
|
if [[ $save_trace_enabled ]]; then
|
|
options="$options --exclude='$(trace_relative_path)'"
|
|
fi
|
|
|
|
# Capture the sync's output so what it changed can be counted from it.
|
|
LOGFILE_SYNC="${LOGFILE}.sync"
|
|
echo -n > "$LOGFILE_SYNC"
|
|
cleanup_files+=("$LOGFILE_SYNC")
|
|
|
|
# Run AWS client to sync the S3 bucket.
|
|
sync_started=$(date +%s)
|
|
eval "$sync_timeout" aws s3 sync \
|
|
--no-follow-symlinks \
|
|
--delete \
|
|
"$options" \
|
|
"'${bucket:?}'" "'${repo:?}'" | tee -a "$LOGFILE_SYNC"
|
|
RT=${PIPESTATUS[0]}
|
|
sync_ended=$(date +%s)
|
|
|
|
# The client reports each object it downloads or removes on its own line.
|
|
set_sync_counts \
|
|
"$(count_sync_events "$LOGFILE_SYNC" 'download: ')" \
|
|
"$(count_sync_events "$LOGFILE_SYNC" 'delete: ')"
|
|
rm -f "$LOGFILE_SYNC"
|
|
|
|
if (( RT == 0 )); then
|
|
post_successful_sync
|
|
else
|
|
post_failed_sync
|
|
fi
|
|
|
|
if [[ $save_trace_enabled ]]; then
|
|
save_trace_file
|
|
fi
|
|
|
|
run_post_successful_sync_hook
|
|
log_end_header
|
|
}
|
|
|
|
# Sync AWS S3 bucket based mirrors using s3cmd.
|
|
s3cmd_sync() {
|
|
# Start the module.
|
|
module_config "$1"
|
|
read_aws_config
|
|
|
|
# s3cmd's --host takes a bare "hostname[:port]", while the aws and s5cmd
|
|
# methods read the same aws_endpoint_url setting as a full URL. Trim the
|
|
# scheme and path so one endpoint value works for all three; a value that
|
|
# was already written as a bare host passes through unchanged.
|
|
if [[ -n $AWS_ENDPOINT_URL ]]; then
|
|
s3cmd_host="${AWS_ENDPOINT_URL#*://}"
|
|
s3cmd_host="${s3cmd_host%%/*}"
|
|
options="$options --host='$s3cmd_host'"
|
|
fi
|
|
|
|
# Keep --delete-removed from removing the trace file this module publishes.
|
|
if [[ $save_trace_enabled ]]; then
|
|
options="$options --exclude='$(trace_relative_path)'"
|
|
fi
|
|
|
|
# Capture the sync's output so what it changed can be counted from it.
|
|
LOGFILE_SYNC="${LOGFILE}.sync"
|
|
echo -n > "$LOGFILE_SYNC"
|
|
cleanup_files+=("$LOGFILE_SYNC")
|
|
|
|
# Run AWS client to sync the S3 bucket.
|
|
sync_started=$(date +%s)
|
|
eval "$sync_timeout" s3cmd sync \
|
|
-v --progress \
|
|
--skip-existing \
|
|
--delete-removed \
|
|
--delete-after \
|
|
"$options" \
|
|
"'${bucket:?}'" "'${repo:?}'" | tee -a "$LOGFILE_SYNC"
|
|
RT=${PIPESTATUS[0]}
|
|
sync_ended=$(date +%s)
|
|
|
|
# s3cmd reports each object it downloads or removes with the path quoted,
|
|
# which is also what tells those messages apart from the warnings it
|
|
# writes about deletions it refused to make.
|
|
set_sync_counts \
|
|
"$(count_sync_events "$LOGFILE_SYNC" "download: '")" \
|
|
"$(count_sync_events "$LOGFILE_SYNC" "delete: '")"
|
|
rm -f "$LOGFILE_SYNC"
|
|
|
|
if (( RT == 0 )); then
|
|
post_successful_sync
|
|
else
|
|
post_failed_sync
|
|
fi
|
|
|
|
if [[ $save_trace_enabled ]]; then
|
|
save_trace_file
|
|
fi
|
|
|
|
run_post_successful_sync_hook
|
|
log_end_header
|
|
}
|
|
|
|
# Sync AWS S3 bucket based mirrors using s5cmd.
|
|
s5cmd_sync() {
|
|
# Install s5cmd if not already installed.
|
|
s5cmd_install
|
|
|
|
# Start the module.
|
|
module_config "$1"
|
|
read_aws_config
|
|
eval sync_options="\$${MODULE}_sync_options"
|
|
|
|
if [[ -n $AWS_ENDPOINT_URL ]]; then
|
|
options="$options --endpoint-url='$AWS_ENDPOINT_URL'"
|
|
fi
|
|
|
|
# Keep --delete from removing the trace file this module publishes.
|
|
if [[ $save_trace_enabled ]]; then
|
|
sync_options="${sync_options:+$sync_options }--exclude='$(trace_relative_path)'"
|
|
fi
|
|
|
|
# Capture the sync's output so what it changed can be counted from it.
|
|
LOGFILE_SYNC="${LOGFILE}.sync"
|
|
echo -n > "$LOGFILE_SYNC"
|
|
cleanup_files+=("$LOGFILE_SYNC")
|
|
|
|
# Run AWS client to sync the S3 bucket.
|
|
sync_started=$(date +%s)
|
|
eval "$sync_timeout" "$S5CMD_BIN" "$options" \
|
|
sync ${sync_options:+$sync_options} \
|
|
--no-follow-symlinks \
|
|
--delete \
|
|
"'${bucket:?}'" "'${repo:?}'" | tee -a "$LOGFILE_SYNC"
|
|
RT=${PIPESTATUS[0]}
|
|
sync_ended=$(date +%s)
|
|
|
|
# s5cmd echoes the operation it performed for each object, so a sync
|
|
# reports one "cp" per object copied and one "rm" per object removed.
|
|
set_sync_counts \
|
|
"$(count_sync_events "$LOGFILE_SYNC" 'cp ')" \
|
|
"$(count_sync_events "$LOGFILE_SYNC" 'rm ')"
|
|
rm -f "$LOGFILE_SYNC"
|
|
|
|
if (( RT == 0 )); then
|
|
post_successful_sync
|
|
else
|
|
post_failed_sync
|
|
fi
|
|
|
|
if [[ $save_trace_enabled ]]; then
|
|
save_trace_file
|
|
fi
|
|
|
|
run_post_successful_sync_hook
|
|
log_end_header
|
|
}
|
|
|
|
# Sync Linux package repositories (rpm/deb/arch/apk) using repo-sync.
|
|
repo_sync_sync() {
|
|
# Install repo-sync if not already installed.
|
|
repo_sync_install
|
|
|
|
# Start the module.
|
|
module_config "$1"
|
|
|
|
# Read repo-sync specific configuration.
|
|
eval source="\$${MODULE}_source"
|
|
eval repo_type="\$${MODULE}_type"
|
|
eval prune="\$${MODULE}_prune"
|
|
eval sync_options="\$${MODULE}_sync_options"
|
|
|
|
# repo-sync identifies each repository from its own metadata, so a type is
|
|
# only needed to limit which formats a run accepts, or to state the format
|
|
# outright for a mirrorlist or metalink URL, which cannot be identified.
|
|
# Several may be listed, separated by spaces or commas.
|
|
type_options=""
|
|
if [[ $repo_type ]]; then
|
|
read -r -a repo_sync_types <<< "${repo_type//,/ }"
|
|
for repo_sync_type in "${repo_sync_types[@]}"; do
|
|
case "${repo_sync_type,,}" in
|
|
rpm|deb|arch|apk) type_options+=" --type ${repo_sync_type,,}" ;;
|
|
*)
|
|
echo "Unknown type '${repo_sync_type}' for ${MODULE}, expected one of: rpm, deb, arch, apk."
|
|
exit 1
|
|
;;
|
|
esac
|
|
done
|
|
fi
|
|
|
|
# Pruning removes files that upstream no longer publishes, matching the
|
|
# delete behavior of the other sync methods. Default it on, but allow it to
|
|
# be turned off for repositories that should keep old packages around.
|
|
parse_bool prune "true" "${MODULE}_prune"
|
|
|
|
# A module may mirror several repositories in one run, so the source is a
|
|
# list of URLs. Quote each so the eval below does not glob or re-split them.
|
|
read -r -a repo_sync_urls <<< "${source:?}"
|
|
if (( ${#repo_sync_urls[@]} == 0 )); then
|
|
echo "No source URLs defined for ${MODULE}."
|
|
exit 1
|
|
fi
|
|
quoted_urls=""
|
|
for repo_sync_url in "${repo_sync_urls[@]}"; do
|
|
quoted_urls+=" '${repo_sync_url}'"
|
|
done
|
|
|
|
# repo-sync writes its own trace files, so rather than calling
|
|
# save_trace_file this module tells repo-sync what to record. The details
|
|
# are the same ones the other methods put in their trace files, passed
|
|
# through from the INFO_* configuration rather than configured a second time
|
|
# in repo-sync's own config file. Tracing is stated either way, so a
|
|
# repo-sync config file cannot contradict this module's setting.
|
|
# These values are shell quoted with printf rather than wrapped in single
|
|
# quotes, as a maintainer name may itself contain an apostrophe.
|
|
trace_options="--no-trace"
|
|
if [[ $save_trace_enabled ]]; then
|
|
trace_options="--trace --trace-host $(printf '%q' "${mirror_hostname:?}")"
|
|
for trace_field in \
|
|
"maintainer:${INFO_MAINTAINER:-}" \
|
|
"sponsor:${INFO_SPONSOR:-}" \
|
|
"country:${INFO_COUNTRY:-}" \
|
|
"location:${INFO_LOCATION:-}" \
|
|
"throughput:${INFO_THROUGHPUT:-}"
|
|
do
|
|
trace_value="${trace_field#*:}"
|
|
if [[ $trace_value ]]; then
|
|
trace_options+=" --trace-${trace_field%%:*} $(printf '%q' "$trace_value")"
|
|
fi
|
|
done
|
|
fi
|
|
|
|
# Capture the sync's output so its run summary can be read from it.
|
|
LOGFILE_SYNC="${LOGFILE}.sync"
|
|
echo -n > "$LOGFILE_SYNC"
|
|
cleanup_files+=("$LOGFILE_SYNC")
|
|
|
|
# Run repo-sync to synchronize the repositories. The destination is always
|
|
# the last argument.
|
|
eval "$sync_timeout" "$REPO_SYNC_BIN" "$options" \
|
|
${trace_options:+$trace_options} \
|
|
sync ${type_options:+$type_options} ${sync_options:+$sync_options} \
|
|
${prune:+--prune} \
|
|
"$quoted_urls" "'${repo:?}'" | tee -a "$LOGFILE_SYNC"
|
|
RT=${PIPESTATUS[0]}
|
|
|
|
# repo-sync ends a run by printing a summary of what it moved. It states
|
|
# whether the repository changed itself, which is taken as given rather
|
|
# than inferred from the counts, as a dry run reports work it only
|
|
# planned.
|
|
repo_sync_changed=""
|
|
if grep -q '^Repository changed: true' "$LOGFILE_SYNC" 2>/dev/null; then
|
|
repo_sync_changed="true"
|
|
fi
|
|
set_sync_counts \
|
|
"$(sum_log_stat "Number of files transferred" "$LOGFILE_SYNC")" \
|
|
"$(sum_log_stat "Number of files pruned" "$LOGFILE_SYNC")" \
|
|
"$repo_sync_changed"
|
|
rm -f "$LOGFILE_SYNC"
|
|
|
|
if (( RT == 0 )); then
|
|
post_successful_sync
|
|
else
|
|
post_failed_sync
|
|
fi
|
|
|
|
run_post_successful_sync_hook
|
|
log_end_header
|
|
}
|
|
|
|
# Sync using FTP.
|
|
ftp_sync() {
|
|
# Start the module.
|
|
module_config "$1"
|
|
eval source="\$${MODULE}_source"
|
|
|
|
# Keep --delete from removing the trace file this module publishes. lftp
|
|
# takes a glob here, matched against the path below the mirrored directory.
|
|
trace_exclude=""
|
|
if [[ $save_trace_enabled ]]; then
|
|
trace_exclude="--exclude-glob $(trace_relative_path) "
|
|
fi
|
|
|
|
# lftp's mirror writes the commands it runs to its log, one per file it
|
|
# transfers or removes, which is what the changes are counted from. The
|
|
# verbose output it prints is meant for a person to read and is translated
|
|
# to the running locale, so it is not counted from.
|
|
LOGFILE_SYNC="${LOGFILE}.sync"
|
|
echo -n > "$LOGFILE_SYNC"
|
|
cleanup_files+=("$LOGFILE_SYNC")
|
|
|
|
# Mirror the source tree with lftp.
|
|
sync_started=$(date +%s)
|
|
$sync_timeout lftp <<< "mirror -v --log='${LOGFILE_SYNC}' --delete --no-perms ${trace_exclude}$options '${source:?}' '${repo:?}'"
|
|
RT=${PIPESTATUS[0]}
|
|
sync_ended=$(date +%s)
|
|
|
|
# Transfers are logged as a get command, in whichever form lftp chose for
|
|
# the transfer, and removals as an rm. Removed directories are logged as
|
|
# rmdir and are not counted, as they hold no content of their own.
|
|
set_sync_counts \
|
|
"$(count_sync_events "$LOGFILE_SYNC" '(m|p)?get1? ')" \
|
|
"$(count_sync_events "$LOGFILE_SYNC" 'rm ')"
|
|
rm -f "$LOGFILE_SYNC"
|
|
|
|
if (( RT == 0 )); then
|
|
post_successful_sync
|
|
else
|
|
post_failed_sync
|
|
fi
|
|
|
|
if [[ $save_trace_enabled ]]; then
|
|
save_trace_file
|
|
fi
|
|
|
|
run_post_successful_sync_hook
|
|
log_end_header
|
|
}
|
|
|
|
# Sync using wget.
|
|
wget_sync() {
|
|
# Start the module.
|
|
module_config "$1"
|
|
eval source="\$${MODULE}_source"
|
|
|
|
if [[ -z $options ]]; then
|
|
options="--mirror --no-host-directories --no-parent"
|
|
fi
|
|
|
|
# Capture the sync's output so what it changed can be counted from it. The
|
|
# counting happens out here rather than in the subshell below, which
|
|
# cannot hand a count back to the hooks.
|
|
LOGFILE_SYNC="${LOGFILE}.sync"
|
|
echo -n > "$LOGFILE_SYNC"
|
|
cleanup_files+=("$LOGFILE_SYNC")
|
|
|
|
# The sync runs in a subshell, so it is timed from out here where the
|
|
# result is still readable once it has finished. wget never deletes, so
|
|
# unlike the other methods the trace needs no protection from this run.
|
|
sync_started=$(date +%s)
|
|
(
|
|
trap - EXIT
|
|
# Make sure the repo directory exists and we are in it.
|
|
if ! [[ -e $repo ]]; then
|
|
mkdir -p "$repo"
|
|
fi
|
|
|
|
if ! cd "$repo"; then
|
|
echo "Unable to enter repo directory."
|
|
exit 1
|
|
fi
|
|
|
|
# Run wget with configured options. wget reports on its progress
|
|
# through stderr, which is what has to be captured to see what it
|
|
# retrieved.
|
|
eval "$sync_timeout" wget "$options" "'${source:?}'" 2>&1 | tee -a "$LOGFILE_SYNC"
|
|
RT=${PIPESTATUS[0]}
|
|
if (( RT == 0 )); then
|
|
post_successful_sync
|
|
else
|
|
post_failed_sync
|
|
fi
|
|
)
|
|
RT=$?
|
|
sync_ended=$(date +%s)
|
|
|
|
# wget closes a run by reporting what it downloaded. Mirroring only
|
|
# retrieves what changed upstream, so that count is the count of changed
|
|
# files; a run that found nothing new prints no such report at all, and
|
|
# wget never deletes.
|
|
set_sync_counts \
|
|
"$(awk '$1=="Downloaded:" {total+=$2} END {print total+0}' "$LOGFILE_SYNC" 2>/dev/null)" \
|
|
0
|
|
rm -f "$LOGFILE_SYNC"
|
|
|
|
if (( RT != 0 )); then
|
|
exit "$RT"
|
|
fi
|
|
|
|
if [[ $save_trace_enabled ]]; then
|
|
save_trace_file
|
|
fi
|
|
|
|
run_post_successful_sync_hook
|
|
log_end_header
|
|
}
|
|
|
|
# Common config for rsync based modules.
|
|
read_rsync_config() {
|
|
eval pre_hook="\$${MODULE}_pre_hook"
|
|
eval source="\$${MODULE}_source"
|
|
eval report_mirror="\$${MODULE}_report_mirror"
|
|
eval RSYNC_PASSWORD="\$${MODULE}_rsync_password"
|
|
if [[ $RSYNC_PASSWORD ]]; then
|
|
export RSYNC_PASSWORD
|
|
fi
|
|
eval post_hook="\$${MODULE}_post_hook"
|
|
eval arch_configurations="\$${MODULE}_arch_configurations"
|
|
eval repo_type="\$${MODULE}_type"
|
|
}
|
|
|
|
# Modules based on rsync.
|
|
rsync_sync() {
|
|
# Start the module.
|
|
module_config "$1"
|
|
shift
|
|
|
|
# Check for any arguments.
|
|
force=0
|
|
while (( $# > 0 )); do
|
|
case $1 in
|
|
# Force rsync, ignore upstream check.
|
|
-f|--force)
|
|
force=1
|
|
shift
|
|
;;
|
|
*)
|
|
echo "Unknown option $1"
|
|
echo
|
|
print_help 1
|
|
;;
|
|
esac
|
|
done
|
|
|
|
# Read the configuration for this module.
|
|
read_rsync_config
|
|
eval jigdo_pkg_repo="\$${MODULE}_jigdo_pkg_repo"
|
|
eval options_stage2="\$${MODULE}_options_stage2"
|
|
eval pre_stage2_hook="\$${MODULE}_pre_stage2_hook"
|
|
eval upstream_check="\$${MODULE}_upstream_check"
|
|
eval time_file_check="\$${MODULE}_time_file_check"
|
|
|
|
# Check if upstream was updated recently if configured.
|
|
# This is designed to slow down rsync so we only rsync
|
|
# when we detect its needed or when last rsync was a long time ago.
|
|
if [[ $upstream_check ]] && (( force == 0 )); then
|
|
now=$(date +%s)
|
|
if [[ ! -f ${timestamp:?} ]]; then
|
|
echo "Timestamp file not found, skipping upstream check."
|
|
else
|
|
last_timestamp=$(cat "$timestamp")
|
|
|
|
# If last update was not that long ago, we should check if upstream was updated recently.
|
|
if (( now-last_timestamp < ${upstream_timestamp_min:?} )); then
|
|
echo "Checking upstream's last modified."
|
|
|
|
# Get the last modified date.
|
|
IFS=': ' read -r _ last_modified < <(curl -sI "${upstream_check:?}" | grep -i Last-Modified)
|
|
last_modified="${last_modified//$'\r'/}"
|
|
|
|
# If last modified couldn't be determined, proceed with sync.
|
|
if [[ -z $last_modified ]]; then
|
|
echo "Could not determine upstream last-modified, proceeding with sync."
|
|
else
|
|
# If the date cannot be parsed, proceed with the sync just
|
|
# as when the header is missing; an empty value would
|
|
# otherwise be read as the epoch and skip every sync until
|
|
# the timestamp minimum passes.
|
|
if ! last_modified_unix=$(date -u +%s -d "$last_modified" 2>/dev/null); then
|
|
echo "Could not parse upstream last-modified '${last_modified}', proceeding with sync."
|
|
# If last modified is greater than our max age, it wasn't modified recently and we should not rsync.
|
|
elif (( now-last_modified_unix > ${upstream_max_age:-0} )); then
|
|
echo "Skipping sync as upstream wasn't updated recently."
|
|
exit 88
|
|
fi
|
|
fi
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# If a time file check was defined, and check if needed.
|
|
if [[ ${time_file_check:-} ]] && (( force == 0 )); then
|
|
now=$(date +%s)
|
|
if [[ ! -f ${timestamp:?} ]]; then
|
|
echo "Timestamp file not found, skipping time file check."
|
|
else
|
|
last_timestamp=$(cat "$timestamp")
|
|
|
|
# Only check time file if the timestamp was recently updated.
|
|
if (( now-last_timestamp < ${upstream_timestamp_min:?} )); then
|
|
echo "Checking if time file has changed since last sync."
|
|
checkresult=$($sync_timeout rsync \
|
|
--no-motd \
|
|
--dry-run \
|
|
--out-format="%n" \
|
|
"${source:?}/${time_file_check:?}" "${repo:?}/${time_file_check:?}")
|
|
rsync_rt=$?
|
|
if (( rsync_rt != 0 )); then
|
|
echo "time_file_check rsync failed (exit ${rsync_rt}), proceeding with sync."
|
|
elif [[ -z $checkresult ]]; then
|
|
echo "The time file has not changed since last sync, we are not updating at this time."
|
|
exit 88
|
|
fi
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# Run any hooks.
|
|
if [[ $pre_hook ]]; then
|
|
echo "Executing pre-hook:"
|
|
eval "$pre_hook"
|
|
fi
|
|
|
|
# Add arguments from configurations.
|
|
extra_args="${options:-}"
|
|
# If 2 stage, we do not want to delete in stage 1.
|
|
if [[ ! $options_stage2 ]]; then
|
|
extra_args+=" --delete --delete-after"
|
|
echo "Running rsync:"
|
|
else
|
|
echo "Running rsync stage 1:"
|
|
fi
|
|
|
|
# Create archive update file.
|
|
mirror_update_file="${repo:?}/Archive-Update-in-Progress-${mirror_hostname:?}"
|
|
touch "$mirror_update_file"
|
|
LOGFILE_STAGE1="${LOGFILE}.stage1"
|
|
echo -n > "$LOGFILE_STAGE1"
|
|
# Register for cleanup so an interrupt does not leave these behind.
|
|
cleanup_files+=("$mirror_update_file" "$LOGFILE_STAGE1")
|
|
|
|
# Run the rsync. Using eval here so extra_args expands and is used as arguments.
|
|
stage1_started=$(date +%s)
|
|
eval "$sync_timeout" rsync -avH \
|
|
--progress \
|
|
--safe-links \
|
|
--delay-updates \
|
|
--stats \
|
|
--no-human-readable \
|
|
--itemize-changes \
|
|
--timeout=10800 \
|
|
"$extra_args" \
|
|
--exclude "Archive-Update-in-Progress-${mirror_hostname:?}" \
|
|
--exclude "project/trace/${mirror_hostname:?}" \
|
|
"'${source:?}'" "'${repo:?}'" | tee -a "$LOGFILE_STAGE1"
|
|
stage1_ended=$(date +%s)
|
|
|
|
# Check if run was successful.
|
|
if [[ $(grep -c '^total size is' "$LOGFILE_STAGE1") -ne 1 ]]; then
|
|
rm -f "$mirror_update_file"
|
|
post_failed_sync
|
|
fi
|
|
|
|
# If 2 stage, perform second stage.
|
|
if [[ $options_stage2 ]]; then
|
|
# Check if upstream is currently updating.
|
|
for aupfile in "${repo:?}/Archive-Update-in-Progress-"*; do
|
|
case "$aupfile" in
|
|
"$mirror_update_file")
|
|
:
|
|
;;
|
|
*)
|
|
if [[ -f $aupfile ]]; then
|
|
# Remove the file, it will be synced again if
|
|
# upstream is still not done
|
|
rm -f "$aupfile"
|
|
else
|
|
echo "AUIP file '${aupfile}' is not really a file, weird"
|
|
fi
|
|
echo "Upstream is currently updating their repo, skipping second stage for now."
|
|
rm -f "$mirror_update_file"
|
|
exit 0
|
|
;;
|
|
esac
|
|
done
|
|
|
|
# Run any hooks.
|
|
if [[ $pre_stage2_hook ]]; then
|
|
echo "Executing pre-stage2 hook:"
|
|
eval "$pre_stage2_hook"
|
|
fi
|
|
|
|
# Add stage 2 options from configurations.
|
|
extra_args="${options_stage2:-}"
|
|
LOGFILE_STAGE2="${LOGFILE}.stage2"
|
|
echo -n > "$LOGFILE_STAGE2"
|
|
cleanup_files+=("$LOGFILE_STAGE2")
|
|
|
|
echo
|
|
echo "Running rsync stage 2:"
|
|
|
|
# Run the rsync. Using eval here so extra_args expands and is used as arguments.
|
|
stage2_started=$(date +%s)
|
|
eval "$sync_timeout" rsync -avH \
|
|
--progress \
|
|
--safe-links \
|
|
--delete \
|
|
--delete-after \
|
|
--delay-updates \
|
|
--stats \
|
|
--no-human-readable \
|
|
--itemize-changes \
|
|
--timeout=10800 \
|
|
"$extra_args" \
|
|
--exclude "Archive-Update-in-Progress-${mirror_hostname:?}" \
|
|
--exclude "project/trace/${mirror_hostname:?}" \
|
|
"'${source:?}'" "'${repo:?}'" | tee -a "$LOGFILE_STAGE2"
|
|
stage2_ended=$(date +%s)
|
|
|
|
# Check if run was successful.
|
|
if [[ $(grep -c '^total size is' "$LOGFILE_STAGE2") -ne 1 ]]; then
|
|
rm -f "$mirror_update_file"
|
|
post_failed_sync
|
|
fi
|
|
fi
|
|
|
|
# Count what the sync changed, before the logs it is counted from are
|
|
# cleaned up below. Both stages write into the same repository, so their
|
|
# logs are read as one.
|
|
read -r rsync_transferred rsync_deleted < <(count_rsync_changes "$LOGFILE_STAGE1" "${LOGFILE_STAGE2:-}")
|
|
set_sync_counts "$rsync_transferred" "$rsync_deleted"
|
|
|
|
# At this point we are successful.
|
|
post_successful_sync
|
|
|
|
# Run any hooks.
|
|
if [[ $post_hook ]]; then
|
|
echo "Executing post hook:"
|
|
eval "$post_hook"
|
|
fi
|
|
|
|
# Save trace information.
|
|
if [[ $save_trace_enabled ]]; then
|
|
save_trace_file
|
|
fi
|
|
rm -f "$LOGFILE_STAGE1"
|
|
[[ -n ${LOGFILE_STAGE2:-} ]] && rm -f "$LOGFILE_STAGE2"
|
|
|
|
# Remove archive update file.
|
|
rm -f "$mirror_update_file"
|
|
|
|
# If report mirror configuration file provided, run report mirror.
|
|
if [[ $report_mirror ]]; then
|
|
echo
|
|
echo "Reporting mirror update:"
|
|
/bin/report_mirror -c "${report_mirror:?}"
|
|
fi
|
|
|
|
run_post_successful_sync_hook
|
|
log_end_header
|
|
}
|
|
|
|
# Modules based on quick-fedora-mirror.
|
|
quick_fedora_mirror_sync() {
|
|
# Start the module.
|
|
module_config "$1"
|
|
|
|
# We need a mapping so we can know the final directory name.
|
|
MODULEMAPPING=(
|
|
fedora-alt alt
|
|
fedora-archive archive
|
|
fedora-enchilada fedora
|
|
fedora-epel epel
|
|
fedora-secondary fedora-secondary
|
|
)
|
|
|
|
# Helper function to map to dir name.
|
|
module_dir() {
|
|
for ((M=0; M<${#MODULEMAPPING[@]}; M++)); do
|
|
N=$((M+1))
|
|
if [[ "${MODULEMAPPING[$M]}" == "$1" ]]; then
|
|
echo "${MODULEMAPPING[$N]}"
|
|
break
|
|
fi
|
|
M=$N
|
|
done
|
|
}
|
|
|
|
# Read the configuration for this module.
|
|
read_rsync_config
|
|
eval master_module="\$${MODULE}_master_module"
|
|
eval module_mapping="\$${MODULE}_module_mapping"
|
|
eval mirror_manager_mapping="\$${MODULE}_mirror_manager_mapping"
|
|
eval modules="\$${MODULE}_modules"
|
|
eval filterexp="\$${MODULE}_filterexp"
|
|
eval rsync_options="\$${MODULE}_rsync_options"
|
|
|
|
# Install QFM if not already installed.
|
|
quick_fedora_mirror_install
|
|
|
|
# Build configuration file for QFM.
|
|
conf_path="${QFM_PATH}/${MODULE}_qfm.conf"
|
|
cat <<EOF > "$conf_path"
|
|
DESTD="$repo"
|
|
TIMEFILE="${LOGPATH}/${MODULE}_timefile.txt"
|
|
REMOTE="$source"
|
|
MODULES=(${modules:?})
|
|
FILTEREXP='${filterexp:-}'
|
|
VERBOSE=7
|
|
LOGITEMS=aeEl
|
|
RSYNCOPTS=(-aSH -f 'R .~tmp~' --stats --no-human-readable --preallocate --delay-updates ${rsync_options:-} --out-format='@ %i %n%L')
|
|
EOF
|
|
if [[ $master_module ]]; then
|
|
echo "MASTERMODULE='$master_module'" >> "$conf_path"
|
|
fi
|
|
if [[ $module_mapping ]]; then
|
|
echo "MODULEMAPPING=($module_mapping)" >> "$conf_path"
|
|
IFS=" " read -ra MODULEMAPPING < <(echo "$module_mapping")
|
|
fi
|
|
if [[ $mirror_manager_mapping ]]; then
|
|
echo "MIRRORMANAGERMAPPING=($mirror_manager_mapping)" >> "$conf_path"
|
|
fi
|
|
|
|
# Run any hooks.
|
|
if [[ $pre_hook ]]; then
|
|
echo "Executing pre-hook:"
|
|
eval "$pre_hook"
|
|
fi
|
|
|
|
# Create archive update file. The repo path is normalized without its
|
|
# trailing slash so module paths can be built with an explicit separator
|
|
# regardless of how the configuration spelled it.
|
|
docroot=${repo%/}
|
|
for module in $modules; do
|
|
_auip="${docroot}/$(module_dir "$module")/Archive-Update-in-Progress-${mirror_hostname:?}"
|
|
touch "$_auip"
|
|
# Register for cleanup so an interrupt does not leave these behind.
|
|
cleanup_files+=("$_auip")
|
|
done
|
|
LOGFILE_SYNC="${LOGFILE}.sync"
|
|
echo -n > "$LOGFILE_SYNC"
|
|
cleanup_files+=("$LOGFILE_SYNC")
|
|
|
|
# Add arguments from configurations.
|
|
extra_args="${options:-}"
|
|
|
|
# Run the rsync. Using eval here so extra_args expands and is used as arguments.
|
|
sync_started=$(date +%s)
|
|
eval "$sync_timeout" "$QFM_BIN" \
|
|
-c "'$conf_path'" \
|
|
"$extra_args" | tee -a "$LOGFILE_SYNC"
|
|
sync_ended=$(date +%s)
|
|
|
|
# Check if run was successful.
|
|
if ! grep -q '^total size is' "$LOGFILE_SYNC"; then
|
|
for module in $modules; do
|
|
rm -f "${docroot}/$(module_dir "$module")/Archive-Update-in-Progress-${mirror_hostname:?}"
|
|
done
|
|
post_failed_sync
|
|
fi
|
|
|
|
# Count what the sync changed, before the log it is counted from is
|
|
# cleaned up below. Every module quick-fedora-mirror synchronized reports
|
|
# into the one log, so the counts cover them all.
|
|
read -r qfm_transferred qfm_deleted < <(count_rsync_changes "$LOGFILE_SYNC")
|
|
set_sync_counts "$qfm_transferred" "$qfm_deleted"
|
|
|
|
# At this point we are successful.
|
|
post_successful_sync
|
|
|
|
# Run any hooks.
|
|
if [[ $post_hook ]]; then
|
|
echo "Executing post hook:"
|
|
eval "$post_hook"
|
|
fi
|
|
|
|
# Save trace information. Each sub-module gets its own trace, so repo is
|
|
# pointed at each in turn and then restored, as the configured repo is what
|
|
# the post-successful-sync hook expects to be given.
|
|
if [[ $save_trace_enabled ]]; then
|
|
module_repo=$repo
|
|
for module in $modules; do
|
|
repo="${docroot}/$(module_dir "$module")"
|
|
save_trace_file
|
|
done
|
|
repo=$module_repo
|
|
fi
|
|
rm -f "$LOGFILE_SYNC"
|
|
|
|
# Remove archive update file.
|
|
for module in $modules; do
|
|
rm -f "${docroot}/$(module_dir "$module")/Archive-Update-in-Progress-${mirror_hostname:?}"
|
|
done
|
|
|
|
# If report mirror configuration file provided, run report mirror.
|
|
if [[ $report_mirror ]]; then
|
|
echo
|
|
echo "Reporting mirror update:"
|
|
/bin/report_mirror -c "${report_mirror:?}"
|
|
fi
|
|
|
|
run_post_successful_sync_hook
|
|
log_end_header
|
|
}
|
|
|
|
# If no arguments are provided, we can print help.
|
|
if (( $# < 1 )); then
|
|
print_help
|
|
fi
|
|
|
|
# Parse arguments.
|
|
while (( $# > 0 )); do
|
|
case "$1" in
|
|
# Installs utilities used by this script which are not available in the standard repositories.
|
|
-u|--update-support-utilities)
|
|
update_support_utilities
|
|
exit 0
|
|
;;
|
|
# If help is requested, print it.
|
|
-h|h|help|--help)
|
|
print_help
|
|
;;
|
|
# Print version.
|
|
-v|--version)
|
|
echo "Mirror Sync (${VERSION})"
|
|
exit 0
|
|
;;
|
|
# Default to rsync if module has no special options, otherwise if no module is found give help.
|
|
*)
|
|
for MODULE in ${MODULES:?}; do
|
|
if [[ "$1" == "$MODULE" ]]; then
|
|
eval sync_method="\${${MODULE}_sync_method:-rsync}"
|
|
if [[ "${sync_method:?}" == "git" ]]; then
|
|
git_sync "$@"
|
|
elif [[ "${sync_method:?}" == "aws" ]]; then
|
|
aws_sync "$@"
|
|
elif [[ "${sync_method:?}" == "s3cmd" ]]; then
|
|
s3cmd_sync "$@"
|
|
elif [[ "${sync_method:?}" == "s5cmd" ]]; then
|
|
s5cmd_sync "$@"
|
|
elif [[ "${sync_method:?}" == "repo-sync" ]] || [[ "${sync_method:?}" == "repo_sync" ]]; then
|
|
repo_sync_sync "$@"
|
|
elif [[ "${sync_method:?}" == "ftp" ]]; then
|
|
ftp_sync "$@"
|
|
elif [[ "${sync_method:?}" == "wget" ]]; then
|
|
wget_sync "$@"
|
|
elif [[ "${sync_method:?}" == "qfm" ]]; then
|
|
quick_fedora_mirror_sync "$@"
|
|
else
|
|
rsync_sync "$@"
|
|
fi
|
|
exit 0
|
|
fi
|
|
done
|
|
# No module was found, so give help.
|
|
echo "Unknown module '$1'"
|
|
echo
|
|
print_help 1
|
|
;;
|
|
esac
|
|
done
|