1018 lines
27 KiB
Bash
Executable File
1018 lines
27 KiB
Bash
Executable File
#!/bin/bash
|
|
|
|
################################################################################
|
|
### INSTRUCTIONS AT https://github.com/gh2o/digitalocean-debian-to-arch/ ###
|
|
################################################################################
|
|
|
|
run_from_file() {
|
|
local f t
|
|
for f in /dev/fd/*; do
|
|
[ -h $f ] || continue
|
|
[ $f -ef "$0" ] && return
|
|
done
|
|
t=$(mktemp)
|
|
cat > $t
|
|
if [ "$(head -n 1 $t)" = '#!/bin/bash' ]; then
|
|
chmod +x $t
|
|
exec /bin/bash $t "$@" </dev/fd/2
|
|
else
|
|
rm -f $t
|
|
echo "Direct execution not supported with this shell ($_)." >&2
|
|
echo "Please try bash instead." >&2
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
# do not modify the two lines below
|
|
[ -h /dev/fd/0 ] && run_from_file
|
|
#!/bin/bash
|
|
|
|
########################################
|
|
### DEFAULT CONFIGURATION ###
|
|
########################################
|
|
|
|
# mirror from which to download archlinux packages
|
|
archlinux_mirror="http://mirrors.digitalocean.com/archlinux"
|
|
|
|
# package to use as kernel (linux or linux-lts)
|
|
kernel_package=linux
|
|
|
|
# migrated machine architecture (x86_64/i686)
|
|
target_architecture="$(uname -m)"
|
|
|
|
# new disklabel type (gpt/dos)
|
|
target_disklabel="gpt"
|
|
|
|
# new filesystem type (ext4/btrfs)
|
|
target_filesystem="ext4"
|
|
|
|
# NOT EXPOSED NORMALLY: don't prompt
|
|
continue_without_prompting=0
|
|
|
|
########################################
|
|
### END OF CONFIGURATION ###
|
|
########################################
|
|
|
|
if [ -n "${POSIXLY_CORRECT}" ] || [ -z "${DEBIAN_TO_ARCH_ENV_CLEARED}" ]; then
|
|
exec /usr/bin/env -i \
|
|
TERM="$TERM" \
|
|
PATH=/usr/sbin:/sbin:/usr/bin:/bin \
|
|
DEBIAN_TO_ARCH_ENV_CLEARED=1 \
|
|
/bin/bash "$0" "$@"
|
|
fi
|
|
|
|
set -eu
|
|
set -o pipefail
|
|
shopt -s nullglob
|
|
shopt -s dotglob
|
|
umask 022
|
|
|
|
sector_size=512
|
|
|
|
flag_variables=(
|
|
archlinux_mirror
|
|
kernel_package
|
|
target_architecture
|
|
target_disklabel
|
|
target_filesystem
|
|
)
|
|
|
|
host_packages=(
|
|
haveged
|
|
parted
|
|
)
|
|
|
|
arch_packages=(
|
|
grub
|
|
openssh
|
|
)
|
|
|
|
gpt1_size_MiB=1
|
|
doroot_size_MiB=6
|
|
biosboot_size_MiB=1
|
|
archroot_size_MiB=
|
|
gpt2_size_MiB=1
|
|
|
|
doroot_offset_MiB=$((gpt1_size_MiB))
|
|
biosboot_offset_MiB=$((doroot_offset_MiB + doroot_size_MiB))
|
|
archroot_offset_MiB=$((biosboot_offset_MiB + biosboot_size_MiB))
|
|
|
|
log() {
|
|
echo "[$(date)]" "$@" >&2
|
|
}
|
|
|
|
fatal() {
|
|
log "$@"
|
|
log "Exiting."
|
|
exit 1
|
|
}
|
|
|
|
extract_embedded_file() {
|
|
awk -v n="$1" '$0=="!!!!"{p=0};p;$0=="!!!!"n{p=1}' "$0"
|
|
}
|
|
|
|
parse_flags() {
|
|
local c conf_key conf_val
|
|
while [ $# -gt 0 ]; do
|
|
conf_key=
|
|
conf_val=
|
|
for c in ${flag_variables[@]}; do
|
|
case "$1" in
|
|
--$c)
|
|
shift
|
|
[ $# -gt 0 ] || fatal "Option $c requires a value."
|
|
conf_key="$c"
|
|
conf_val="$1"
|
|
shift
|
|
break
|
|
;;
|
|
--$c=*)
|
|
conf_key="$c"
|
|
conf_val="${1#*=}"
|
|
shift
|
|
break
|
|
;;
|
|
--i_understand_that_this_droplet_will_be_completely_wiped)
|
|
continue_without_prompting=1
|
|
conf_key=option_acknowledged
|
|
shift
|
|
break
|
|
;;
|
|
--help)
|
|
print_help_and_exit
|
|
;;
|
|
esac
|
|
done
|
|
[ "${conf_key}" = option_acknowledged ] && continue
|
|
[ -n "${conf_key}" ] || fatal "Unknown option: $1"
|
|
[ -n "${conf_val}" ] || fatal "Empty value for option ${conf_key}."
|
|
local -n conf_ref=${conf_key}
|
|
conf_ref="${conf_val}"
|
|
done
|
|
log "Configuration:"
|
|
for conf_key in ${flag_variables[@]}; do
|
|
local -n conf_ref=${conf_key}
|
|
log "- ${conf_key} = ${conf_ref}"
|
|
done
|
|
}
|
|
|
|
print_help_and_exit() {
|
|
local conf_key
|
|
echo "Available options: (see script for details)" >&2
|
|
for conf_key in ${flag_variables[@]}; do
|
|
local -n conf_ref=${conf_key}
|
|
echo " --${conf_key}=[${conf_ref}]" >&2
|
|
done
|
|
exit 1
|
|
}
|
|
|
|
validate_flags_and_augment_globals() {
|
|
arch_packages+=(${kernel_package})
|
|
case "${target_disklabel}" in
|
|
gpt)
|
|
;;
|
|
dos)
|
|
;;
|
|
*)
|
|
fatal "Unknown disklabel type: ${target_disklabel}"
|
|
;;
|
|
esac
|
|
case "${target_filesystem}" in
|
|
ext4)
|
|
;;
|
|
btrfs)
|
|
host_packages+=(btrfs-tools)
|
|
arch_packages+=(btrfs-progs)
|
|
;;
|
|
*)
|
|
fatal "Unknown filesystem type: ${target_filesystem}"
|
|
;;
|
|
esac
|
|
local disk_MiB=$(($(cat /sys/block/vda/size) >> 11))
|
|
archroot_size_MiB=$((disk_MiB - gpt2_size_MiB - archroot_offset_MiB))
|
|
}
|
|
|
|
read_flags() {
|
|
local filename=$1
|
|
source ${filename}
|
|
}
|
|
|
|
write_flags() {
|
|
local filename=$1
|
|
{
|
|
local conf_key
|
|
for conf_key in ${flag_variables[@]}; do
|
|
local -n conf_ref=${conf_key}
|
|
printf "%s=%q\n" "${conf_key}" "${conf_ref}"
|
|
done
|
|
} > ${filename}
|
|
}
|
|
|
|
sanity_checks() {
|
|
[ ${EUID} -eq 0 ] || fatal "Script must be run as root."
|
|
[ ${UID} -eq 0 ] || fatal "Script must be run as root."
|
|
[ -e /dev/vda ] || fatal "Script must be run on a KVM machine."
|
|
[[ "$(cat /etc/debian_version)" == 8.? ]] || \
|
|
fatal "This script only supports Debian 8.x."
|
|
}
|
|
|
|
prompt_for_destruction() {
|
|
(( continue_without_prompting )) && return 0
|
|
log "*** ALL DATA ON THIS DROPLET WILL BE WIPED. ***"
|
|
log "Please backup all important data on this droplet before continuing."
|
|
log 'Type "wipe this droplet" to continue or anything else to cancel.'
|
|
local response
|
|
read -p ' > ' response
|
|
if [ "${response}" = "wipe this droplet" ]; then
|
|
return 0
|
|
else
|
|
log "Cancelled."
|
|
exit 0
|
|
fi
|
|
}
|
|
|
|
download_and_verify() {
|
|
local file_url="$1"
|
|
local local_path="$2"
|
|
local expected_sha1="$3"
|
|
for try in {0..3}; do
|
|
if [ ${try} -eq 0 ]; then
|
|
[ -e "${local_path}" ] || continue
|
|
else
|
|
wget -O "${local_path}" "${file_url}"
|
|
fi
|
|
set -- $(sha1sum "${local_path}")
|
|
if [ $1 = "${expected_sha1}" ]; then
|
|
return 0
|
|
else
|
|
rm -f "${local_path}"
|
|
fi
|
|
done
|
|
return 1
|
|
}
|
|
|
|
build_parted_cmdline() {
|
|
local cmdline=
|
|
local biosboot_name=BIOSBoot
|
|
local doroot_name=DORoot
|
|
local archroot_name=ArchRoot
|
|
if [ ${target_disklabel} = dos ]; then
|
|
cmdline="mklabel msdos"
|
|
biosboot_name=primary
|
|
doroot_name=primary
|
|
archroot_name=primary
|
|
else
|
|
cmdline="mklabel ${target_disklabel}"
|
|
fi
|
|
local archroot_end_MiB=$((archroot_offset_MiB + archroot_size_MiB))
|
|
cmdline+=" mkpart ${doroot_name} ${doroot_offset_MiB}MiB ${biosboot_offset_MiB}MiB"
|
|
cmdline+=" mkpart ${biosboot_name} ${biosboot_offset_MiB}MiB ${archroot_offset_MiB}MiB"
|
|
cmdline+=" mkpart ${archroot_name} ${archroot_offset_MiB}MiB ${archroot_end_MiB}MiB"
|
|
if [ ${target_disklabel} = gpt ]; then
|
|
cmdline+=" set 2 bios_grub on"
|
|
fi
|
|
echo "${cmdline}"
|
|
}
|
|
|
|
setup_loop_device() {
|
|
local offset_MiB=$1
|
|
local size_MiB=$2
|
|
losetup --find --show --offset ${offset_MiB}MiB --size ${size_MiB}MiB /d2a/work/image
|
|
}
|
|
|
|
package_digitalocean_synchronize() {
|
|
local destination=$1
|
|
local pkgroot=/d2a/work/dosync
|
|
local sysdir=${pkgroot}/usr/lib/systemd/system
|
|
local netdir=${pkgroot}/usr/lib/systemd/network
|
|
|
|
mkdir -p ${pkgroot}
|
|
extract_embedded_file digitalocean-synchronize.PKGINFO > ${pkgroot}/.PKGINFO
|
|
mkdir -p ${pkgroot}/usr/bin
|
|
extract_embedded_file digitalocean-synchronize > ${pkgroot}/usr/bin/digitalocean-synchronize
|
|
mkdir -p ${sysdir}
|
|
extract_embedded_file digitalocean-synchronize.service > ${sysdir}/digitalocean-synchronize.service
|
|
mkdir -p ${sysdir}/multi-user.target.wants
|
|
ln -s ../digitalocean-synchronize.service ${sysdir}/multi-user.target.wants
|
|
mkdir -p ${netdir}
|
|
extract_embedded_file 90-virtio-no-rename.link > ${netdir}/90-virtio-no-rename.link
|
|
|
|
chmod 0755 ${pkgroot}/usr/bin/digitalocean-synchronize
|
|
|
|
( cd ${pkgroot} && tar -cf ${destination} * )
|
|
rm -rf ${pkgroot}
|
|
}
|
|
|
|
kill_processes_in_mountpoint() {
|
|
if mountpoint -q $1; then
|
|
fuser -kms $1 || true
|
|
find /proc -maxdepth 2 -name root -lname $1 | \
|
|
grep -o '[0-9]*' | xargs -r kill || true
|
|
fi
|
|
}
|
|
|
|
quietly_umount() {
|
|
if mountpoint -q $1; then
|
|
umount -d $1
|
|
fi
|
|
}
|
|
|
|
cleanup_work_directory() {
|
|
kill_processes_in_mountpoint /d2a/work/doroot
|
|
kill_processes_in_mountpoint /d2a/work/archroot
|
|
quietly_umount /d2a/work/doroot
|
|
quietly_umount /d2a/work/archroot/var/cache/pacman/pkg
|
|
quietly_umount /d2a/work/archroot/dev/pts
|
|
quietly_umount /d2a/work/archroot/dev
|
|
quietly_umount /d2a/work/archroot/sys
|
|
quietly_umount /d2a/work/archroot/proc
|
|
quietly_umount /d2a/work/archroot
|
|
rm -rf --one-file-system /d2a/work
|
|
}
|
|
|
|
stage1_install_exit() {
|
|
set +e
|
|
cleanup_work_directory
|
|
}
|
|
|
|
stage1_install() {
|
|
trap stage1_install_exit EXIT
|
|
cleanup_work_directory
|
|
mkdir -p /d2a/work
|
|
|
|
log "Installing required packages ..."
|
|
DEBIAN_FRONTEND=noninteractive apt-get install -y ${host_packages[@]}
|
|
|
|
log "Partitioning image ..."
|
|
local disk_sectors=$(cat /sys/block/vda/size)
|
|
rm -f /d2a/work/image
|
|
truncate -s $((disk_sectors * sector_size)) /d2a/work/image
|
|
parted /d2a/work/image $(build_parted_cmdline)
|
|
|
|
log "Formatting image ..."
|
|
local doroot_loop=$(setup_loop_device ${doroot_offset_MiB} ${doroot_size_MiB})
|
|
local archroot_loop=$(setup_loop_device ${archroot_offset_MiB} ${archroot_size_MiB})
|
|
mkfs.ext4 -L DOROOT ${doroot_loop}
|
|
mkfs.${target_filesystem} -L ArchRoot ${archroot_loop}
|
|
|
|
log "Mounting image ..."
|
|
mkdir -p /d2a/work/{doroot,archroot}
|
|
mount ${doroot_loop} /d2a/work/doroot
|
|
mount ${archroot_loop} /d2a/work/archroot
|
|
|
|
log "Setting up DOROOT ..."
|
|
mkdir -p /d2a/work/doroot/etc/network
|
|
touch /d2a/work/doroot/etc/network/interfaces
|
|
cat > /d2a/work/doroot/README <<-EOF
|
|
DO NOT TOUCH FILES ON THIS PARTITION.
|
|
|
|
The DOROOT partition is where DigitalOcean writes passwords and other data
|
|
when a droplet is rebuilt from an image or restored from a snapshot.
|
|
If certain files are missing, restores/rebuilds will not work and you will
|
|
end up with an unusable image.
|
|
|
|
The digitalocean-synchronize script also watches this partition.
|
|
If this partition (particularly etc/shadow) is written to, the script will
|
|
reset the root password to the one provided by DigitalOcean and wipe all
|
|
SSH host keys for security.
|
|
EOF
|
|
chmod 0444 /d2a/work/doroot/README
|
|
|
|
log "Downloading bootstrap tarball ..."
|
|
set -- $(wget -qO- ${archlinux_mirror}/iso/latest/sha1sums.txt |
|
|
grep "archlinux-bootstrap-[^-]*-${target_architecture}.tar.gz")
|
|
local expected_sha1=$1
|
|
local bootstrap_filename=$2
|
|
download_and_verify \
|
|
${archlinux_mirror}/iso/latest/${bootstrap_filename} \
|
|
/d2a/bootstrap.tar.gz \
|
|
${expected_sha1}
|
|
|
|
log "Extracting bootstrap tarball ..."
|
|
tar -xzf /d2a/bootstrap.tar.gz \
|
|
--directory=/d2a/work/archroot \
|
|
--strip-components=1
|
|
|
|
log "Mounting virtual filesystems ..."
|
|
mount -t proc proc /d2a/work/archroot/proc
|
|
mount -t sysfs sys /d2a/work/archroot/sys
|
|
mount -t devtmpfs dev /d2a/work/archroot/dev
|
|
mkdir -p /d2a/work/archroot/dev/pts
|
|
mount -t devpts pts /d2a/work/archroot/dev/pts
|
|
|
|
log "Binding packages directory ..."
|
|
mkdir -p /d2a/packages
|
|
mount --bind /d2a/packages /d2a/work/archroot/var/cache/pacman/pkg
|
|
|
|
log "Preparing bootstrap filesystem ..."
|
|
echo "Server = ${archlinux_mirror}/\$repo/os/\$arch" > /d2a/work/archroot/etc/pacman.d/mirrorlist
|
|
echo 'nameserver 8.8.8.8' > /d2a/work/archroot/etc/resolv.conf
|
|
|
|
log "Installing base system ..."
|
|
chroot /d2a/work/archroot pacman-key --init
|
|
chroot /d2a/work/archroot pacman-key --populate archlinux
|
|
local chroot_pacman="chroot /d2a/work/archroot pacman --arch ${target_architecture}"
|
|
${chroot_pacman} -Sy
|
|
${chroot_pacman} -Su --noconfirm --needed \
|
|
$(${chroot_pacman} -Sgq base | grep -v '^linux$') \
|
|
${arch_packages[@]}
|
|
|
|
log "Configuring base system ..."
|
|
hostname > /d2a/work/archroot/etc/hostname
|
|
cp /etc/ssh/ssh_host_* /d2a/work/archroot/etc/ssh/
|
|
local encrypted_password=$(awk -F: '$1 == "root" { print $2 }' /etc/shadow)
|
|
chroot /d2a/work/archroot usermod -p "${encrypted_password}" root
|
|
chroot /d2a/work/archroot systemctl enable systemd-networkd.service
|
|
chroot /d2a/work/archroot systemctl enable sshd.service
|
|
package_digitalocean_synchronize /d2a/work/archroot/dosync.pkg.tar
|
|
${chroot_pacman} -U --noconfirm /dosync.pkg.tar
|
|
rm /d2a/work/archroot/dosync.pkg.tar
|
|
|
|
log "Finishing up image generation ..."
|
|
ln -f /d2a/work/image /d2a/image
|
|
cleanup_work_directory
|
|
trap - EXIT
|
|
}
|
|
|
|
bisect_left_on_allocation() {
|
|
local alloc_start_sector=$1
|
|
local alloc_end_sector=$2
|
|
local -n bisection_output=$3
|
|
local -n allocation_map=$4
|
|
local lo=0 hi=${#allocation_map[@]}
|
|
while (( lo < hi )); do
|
|
local mid=$(((lo+hi)/2))
|
|
set -- ${allocation_map[$mid]}
|
|
if (( $# == 0 )) || (( $1 < alloc_start_sector )); then
|
|
lo=$((mid+1))
|
|
else
|
|
hi=$((mid))
|
|
fi
|
|
done
|
|
bisection_output=$lo
|
|
}
|
|
|
|
check_for_allocation_overlap() {
|
|
local check_start_sector=$1
|
|
local check_end_sector=$2
|
|
local -n overlap_start_sector=$3
|
|
local -n overlap_end_sector=$4
|
|
shift 4
|
|
local allocation_maps="$*"
|
|
|
|
# overlap_end_sector = 0 if no overlap
|
|
overlap_start_sector=0
|
|
overlap_end_sector=0
|
|
|
|
local map_name
|
|
for map_name in ${allocation_maps}; do
|
|
local -n allocation_map=${map_name}
|
|
local map_length=${#allocation_map[@]}
|
|
(( ${map_length} )) || continue
|
|
local bisection_index
|
|
bisect_left_on_allocation ${check_start_sector} ${check_end_sector} \
|
|
bisection_index ${map_name}
|
|
local check_index
|
|
for check_index in $((bisection_index - 1)) $((bisection_index)); do
|
|
(( check_index < 0 || check_index >= map_length )) && continue
|
|
set -- ${allocation_map[${check_index}]}
|
|
(( $# == 0 )) && continue
|
|
local alloc_start_sector=$1
|
|
local alloc_end_sector=$2
|
|
(( check_start_sector >= alloc_end_sector || alloc_start_sector >= check_end_sector )) && continue
|
|
# overlap detected
|
|
overlap_start_sector=$((alloc_start_sector > check_start_sector ?
|
|
alloc_start_sector : check_start_sector))
|
|
overlap_end_sector=$((alloc_end_sector < check_end_sector ?
|
|
alloc_end_sector : check_end_sector))
|
|
return
|
|
done
|
|
done
|
|
}
|
|
|
|
insert_into_allocation_map() {
|
|
local -n allocation_map=$1
|
|
shift
|
|
local alloc_start_sector=$1
|
|
local alloc_end_sector=$2
|
|
if (( ${#allocation_map[@]} == 0 )); then
|
|
allocation_map=("$*")
|
|
else
|
|
local bisection_index
|
|
bisect_left_on_allocation ${alloc_start_sector} ${alloc_end_sector} \
|
|
bisection_index ${!allocation_map}
|
|
allocation_map=(
|
|
"${allocation_map[@]:0:${bisection_index}}"
|
|
"$*"
|
|
"${allocation_map[@]:${bisection_index}}")
|
|
fi
|
|
}
|
|
|
|
stage2_arrange() {
|
|
local disk_sectors=$(cat /sys/block/vda/size)
|
|
local root_device=$(awk '$2 == "/" { root = $1 } END { print root }' /proc/mounts)
|
|
local root_offset_sectors=$(cat /sys/block/vda/${root_device#/dev/}/start)
|
|
local srcdst_map=() # original source to target map
|
|
local unalloc_map=() # extents not used by either source or target (for tmpdst_map)
|
|
local tmpdst_map=() # extents on temporary redirection (allocated from unalloc_map)
|
|
local source_start_sector source_end_sector target_start_sector target_end_sector
|
|
|
|
log "Creating block rearrangement plan ..."
|
|
|
|
# get and sort extents
|
|
filefrag -e -s -v -b${sector_size} /d2a/image | \
|
|
sed '/^ *[0-9]*:/!d;s/[:.]/ /g' | \
|
|
sort -nk4 > /d2a/imagemap
|
|
while read line; do
|
|
set -- ${line}
|
|
source_start_sector=$(($4 + root_offset_sectors))
|
|
source_end_sector=$((source_start_sector + $6))
|
|
target_start_sector=$2
|
|
target_end_sector=$((target_start_sector + $6))
|
|
echo ${source_start_sector} ${source_end_sector}
|
|
echo ${target_start_sector} ${target_end_sector}
|
|
srcdst_map+=("${source_start_sector} ${source_end_sector} ${target_start_sector}")
|
|
done < /d2a/imagemap > /d2a/unsortedallocs
|
|
sort -n < /d2a/unsortedallocs > /d2a/sortedallocs
|
|
|
|
# build map of unallocated sectors
|
|
local unalloc_start_sector=0 unalloc_end_sector=${disk_sectors}
|
|
while read source_start_sector source_end_sector; do
|
|
if (( source_end_sector <= unalloc_start_sector )); then
|
|
# does not overlap unallocated part
|
|
continue
|
|
elif (( source_start_sector > unalloc_start_sector )); then
|
|
# full overlap with unallocated part
|
|
unalloc_map+=("${unalloc_start_sector} ${source_start_sector}")
|
|
unalloc_start_sector=${source_end_sector}
|
|
else
|
|
# partial overlap
|
|
unalloc_start_sector=${source_end_sector}
|
|
fi
|
|
done < /d2a/sortedallocs
|
|
if (( unalloc_start_sector != unalloc_end_sector )); then
|
|
unalloc_map+=("${unalloc_start_sector} ${unalloc_end_sector}")
|
|
fi
|
|
|
|
# open blockplan
|
|
exec {blockplan_fd}>/d2a/blockplan
|
|
|
|
# arrange sectors
|
|
while (( ${#srcdst_map[@]} )); do
|
|
set -- ${srcdst_map[-1]}
|
|
source_start_sector=$1
|
|
source_end_sector=$2
|
|
target_start_sector=$3
|
|
target_end_sector=$((target_start_sector + (source_end_sector - source_start_sector)))
|
|
if (( source_start_sector == target_start_sector )); then
|
|
unset 'srcdst_map[-1]'
|
|
continue
|
|
elif (( target_start_sector >= source_end_sector ||
|
|
source_start_sector >= target_end_sector )); then
|
|
unset 'srcdst_map[-1]'
|
|
else
|
|
local new_extent_sectors=$((target_start_sector - source_start_sector))
|
|
new_extent_sectors=${new_extent_sectors#-} # absolute value
|
|
set -- \
|
|
$((source_start_sector + new_extent_sectors)) \
|
|
$((source_end_sector)) \
|
|
$((target_start_sector + new_extent_sectors))
|
|
srcdst_map[-1]="$*"
|
|
source_end_sector=$((source_start_sector + new_extent_sectors))
|
|
fi
|
|
local overlap_start_sector overlap_end_sector
|
|
check_for_allocation_overlap \
|
|
${target_start_sector} ${target_end_sector} \
|
|
overlap_start_sector overlap_end_sector \
|
|
srcdst_map
|
|
if (( overlap_end_sector )); then
|
|
# insert non-overlapping parts back into srcdst_map
|
|
if (( target_start_sector < overlap_start_sector )); then
|
|
local nonoverlap_length_sectors=$((overlap_start_sector - target_start_sector))
|
|
insert_into_allocation_map srcdst_map \
|
|
${source_start_sector} \
|
|
$((source_start_sector + nonoverlap_length_sectors)) \
|
|
${target_start_sector}
|
|
fi
|
|
if (( target_end_sector > overlap_end_sector )); then
|
|
local nonoverlap_length_sectors=$((target_end_sector - overlap_end_sector))
|
|
insert_into_allocation_map srcdst_map \
|
|
$((source_end_sector - nonoverlap_length_sectors)) \
|
|
${source_end_sector} \
|
|
${overlap_end_sector}
|
|
fi
|
|
# copy overlapping portion into tmpdst_map
|
|
while (( overlap_start_sector < overlap_end_sector )); do
|
|
set -- ${unalloc_map[-1]}
|
|
unset 'unalloc_map[-1]' # or nullglob will eat it up
|
|
local unalloc_start_sector=$1
|
|
local unalloc_end_sector=$2
|
|
local unalloc_length_sectors=$((unalloc_end_sector - unalloc_start_sector))
|
|
local overlap_length_sectors=$((overlap_end_sector - overlap_start_sector))
|
|
if (( overlap_length_sectors < unalloc_length_sectors )); then
|
|
# return unused portion to unalloc_map
|
|
unalloc_map+=("${unalloc_start_sector} $((unalloc_end_sector - overlap_length_sectors))")
|
|
unalloc_start_sector=$((unalloc_end_sector - overlap_length_sectors))
|
|
unalloc_length_sectors=${overlap_length_sectors}
|
|
fi
|
|
echo >&${blockplan_fd} \
|
|
$((source_start_sector + (overlap_start_sector - target_start_sector))) \
|
|
${unalloc_start_sector} \
|
|
${unalloc_length_sectors}
|
|
insert_into_allocation_map tmpdst_map \
|
|
${unalloc_start_sector} \
|
|
${unalloc_end_sector} \
|
|
${overlap_start_sector}
|
|
(( overlap_start_sector += unalloc_length_sectors ))
|
|
done
|
|
else
|
|
echo >&${blockplan_fd} \
|
|
${source_start_sector} \
|
|
${target_start_sector} \
|
|
$((source_end_sector - source_start_sector))
|
|
fi
|
|
done
|
|
|
|
# restore overlapped sectors
|
|
while (( ${#tmpdst_map[@]} )); do
|
|
set -- ${tmpdst_map[-1]}
|
|
unset 'tmpdst_map[-1]'
|
|
source_start_sector=$1
|
|
source_end_sector=$2
|
|
target_start_sector=$3
|
|
echo >&${blockplan_fd} \
|
|
${source_start_sector} \
|
|
${target_start_sector} \
|
|
$((source_end_sector - source_start_sector))
|
|
done
|
|
|
|
# close blockplan
|
|
exec {blockplan_fd}>&-
|
|
}
|
|
|
|
cleanup_mid_directory() {
|
|
quietly_umount /d2a/mid
|
|
rm -rf --one-file-system /d2a/mid
|
|
}
|
|
|
|
add_binary_to_mid() {
|
|
mkdir -p $(dirname /d2a/mid/$1)
|
|
cp $1 /d2a/mid/$1
|
|
ldd $1 | grep -o '/[^ ]* (0x[0-9a-f]*)' | \
|
|
while read libpath ignored; do
|
|
[ -e /d2a/mid/${libpath} ] && continue
|
|
mkdir -p $(dirname /d2a/mid/${libpath})
|
|
cp ${libpath} /d2a/mid/${libpath}
|
|
done
|
|
}
|
|
|
|
stage3_prepare_exit() {
|
|
set +e
|
|
cleanup_mid_directory
|
|
}
|
|
|
|
stage3_prepare() {
|
|
trap stage3_prepare_exit EXIT
|
|
cleanup_mid_directory
|
|
mkdir -p /d2a/mid
|
|
|
|
# mount tmpfs
|
|
mount -t tmpfs mid /d2a/mid
|
|
|
|
# add binaries
|
|
add_binary_to_mid /bin/busybox
|
|
add_binary_to_mid /bin/bash
|
|
|
|
# create symlinks
|
|
local dir
|
|
for dir in bin sbin usr/bin usr/sbin; do mkdir -p /d2a/mid/${dir}; done
|
|
ln -s bash /d2a/mid/bin/sh
|
|
chroot /d2a/mid /bin/busybox --install
|
|
|
|
# create directories (will be filled by systemd)
|
|
mkdir /d2a/mid/{proc,sys,dev}
|
|
|
|
# copy in the blockplan
|
|
cp /d2a/blockplan /d2a/mid/blockplan
|
|
|
|
# write out flags
|
|
write_flags /d2a/mid/flags
|
|
|
|
# copy myself
|
|
cat "$0" > /d2a/mid/init
|
|
chmod 0755 /d2a/mid/init
|
|
|
|
# detach all loop devices
|
|
losetup -D || true
|
|
|
|
# reboot!
|
|
log "The machine will now reboot."
|
|
log "Check the console for errors if the machine is still unaccessible after a few minutes."
|
|
sleep 1
|
|
trap - EXIT
|
|
systemctl switch-root /d2a/mid /init
|
|
}
|
|
|
|
stage4_convert_exit() {
|
|
log "Error occurred. You're on your own!"
|
|
exec /bin/bash </dev/console >/dev/console 2>&1
|
|
}
|
|
|
|
stage4_convert() {
|
|
trap stage4_convert_exit EXIT
|
|
|
|
# unmount old root
|
|
local retry
|
|
for retry in 1 2 3 4 5; do
|
|
if umount /mnt; then
|
|
retry=0
|
|
break
|
|
else
|
|
sleep 1
|
|
fi
|
|
done
|
|
if (( retry )); then
|
|
umount -rl /mnt
|
|
fi
|
|
|
|
# get total number of sectors
|
|
local processed_length=0
|
|
local total_length=$(awk '{x+=$3}END{print+x}' /blockplan)
|
|
local prev_percentage=-1
|
|
local next_percentage=-1
|
|
|
|
# execute the block plan
|
|
local source_sector target_sector extent_length
|
|
while read source_sector target_sector extent_length; do
|
|
# increment processed length before extent length gets optimized
|
|
(( processed_length += extent_length )) || true
|
|
# optimize extent length
|
|
local transfer_size=${sector_size}
|
|
until (( (source_sector & 1) || (target_sector & 1) ||
|
|
(extent_length & 1) || (transfer_size >= 0x100000) )); do
|
|
(( source_sector >>= 1 , target_sector >>= 1 , extent_length >>= 1,
|
|
transfer_size <<= 1 )) || true
|
|
done
|
|
# do the actual transfer
|
|
dd if=/dev/vda of=/dev/vda bs=${transfer_size} \
|
|
skip=${source_sector} seek=${target_sector} \
|
|
count=${extent_length} 2>/dev/null
|
|
# print out the percentage
|
|
next_percentage=$((100 * processed_length / total_length))
|
|
if (( next_percentage != prev_percentage )); then
|
|
printf "\rTransferring blocks ... %s%%" ${next_percentage}
|
|
prev_percentage=${next_percentage}
|
|
fi
|
|
done < /blockplan
|
|
echo
|
|
|
|
# reread partition table
|
|
blockdev --rereadpt /dev/vda
|
|
|
|
# install bootloader
|
|
mkdir /archroot
|
|
mount /dev/vda3 /archroot
|
|
mount -t proc proc /archroot/proc
|
|
mount -t sysfs sys /archroot/sys
|
|
mount -t devtmpfs dev /archroot/dev
|
|
chroot /archroot grub-mkconfig -o /boot/grub/grub.cfg
|
|
chroot /archroot grub-install /dev/vda
|
|
umount /archroot/dev
|
|
umount /archroot/sys
|
|
umount /archroot/proc
|
|
umount /archroot
|
|
|
|
# we're done!
|
|
sync
|
|
reboot -f
|
|
}
|
|
|
|
reinstall_digitalocean_synchronize() {
|
|
local package_file=$(mktemp --suffix=.pkg.tar)
|
|
package_digitalocean_synchronize ${package_file}
|
|
pacman -U --noconfirm ${package_file}
|
|
rm ${package_file}
|
|
}
|
|
|
|
if [ -e /var/lib/pacman ]; then
|
|
if [ $# -eq 0 ]; then
|
|
reinstall_digitalocean_synchronize
|
|
else
|
|
log "Run this script to install/update the digitalocean-synchronize package."
|
|
fi
|
|
exit 0
|
|
fi
|
|
|
|
if [ $$ -ne 1 ]; then
|
|
parse_flags "$@"
|
|
sanity_checks
|
|
validate_flags_and_augment_globals
|
|
prompt_for_destruction
|
|
stage1_install
|
|
stage2_arrange
|
|
stage3_prepare
|
|
else
|
|
read_flags /flags
|
|
validate_flags_and_augment_globals
|
|
stage4_convert
|
|
fi
|
|
exit 0
|
|
|
|
cat <<'EMBEDDED'
|
|
|
|
!!!!digitalocean-synchronize
|
|
#!/bin/bash
|
|
|
|
meta_base=http://169.254.169.254/metadata/v1/
|
|
|
|
set -eu
|
|
set -o pipefail
|
|
shopt -s nullglob
|
|
shopt -s dotglob
|
|
umask 022
|
|
|
|
log() {
|
|
logger -t digitalocean-synchronize "$@" || \
|
|
echo "[$(date)]" "$@" >&2
|
|
}
|
|
|
|
netmask_to_prefix() {
|
|
local pfx=0 cmp msk
|
|
for cmp in ${1//./ } 0; do
|
|
for msk in 128 64 32 16 8 4 2 1; do
|
|
if (( cmp & msk )); then
|
|
(( pfx += 1 ))
|
|
else
|
|
echo ${pfx}
|
|
return
|
|
fi
|
|
done
|
|
done
|
|
}
|
|
|
|
update_shadow_if_changed() {
|
|
local etcdir=$1/etc
|
|
mkdir -p ${etcdir} || return 0
|
|
if [ -e ${etcdir}/shadow ]; then
|
|
# change password if file was touched
|
|
local encrypted_password=$(awk -F: '$1 == "root" { print $2 }' ${etcdir}/shadow)
|
|
if [ "${encrypted_password}" != "z" ]; then
|
|
log "Snapshot restore detected."
|
|
usermod -p "${encrypted_password}" root
|
|
if [ ${#encrypted_password} -gt 1 ]; then
|
|
chage -d 0 root
|
|
fi
|
|
log "Password has been reset."
|
|
rm -f /etc/ssh/ssh_host_key /etc/ssh/ssh_host_*_key
|
|
log "SSH host keys will be regenerated."
|
|
fi
|
|
fi
|
|
cat > ${etcdir}/shadow <<-EOF
|
|
root:z:1::::::
|
|
nobody:z:1::::::
|
|
EOF
|
|
chmod 0600 ${etcdir}/shadow
|
|
}
|
|
|
|
process_interface() {
|
|
local url=$1
|
|
local attrs=$2
|
|
local mac=$(curl -Ssf ${url}mac)
|
|
local type=$(curl -Ssf ${url}type)
|
|
local interface=
|
|
local cand path
|
|
for cand in $(ls /sys/class/net); do
|
|
path=/sys/class/net/${cand}/address
|
|
if [ -e ${path} ] && [ "$(<${path})" = "${mac}" ]; then
|
|
interface=${cand}
|
|
break
|
|
fi
|
|
done
|
|
[ -n "${interface}" ] || return 0
|
|
mkdir -p /run/systemd/network
|
|
{
|
|
cat <<-EOF
|
|
# Generated by digitalocean-synchronize
|
|
[Match]
|
|
Name=${interface}
|
|
[Network]
|
|
EOF
|
|
if [[ " ${attrs} " =~ " ipv4/ " ]]; then
|
|
local address=$(curl -sf ${url}ipv4/address)
|
|
local prefix=$(netmask_to_prefix $(curl -sf ${url}ipv4/netmask))
|
|
echo "Address=${address}/${prefix}"
|
|
if [ "${type}" != "private" ]; then
|
|
echo "Gateway=$(curl -sf ${url}ipv4/gateway)"
|
|
fi
|
|
log "Added IPv4 address ${address}/${prefix} on ${interface}."
|
|
fi
|
|
if [[ " ${attrs} " =~ " ipv6/ " ]]; then
|
|
local address=$(curl -sf ${url}ipv6/address)
|
|
local prefix=$(curl -sf ${url}ipv6/cidr)
|
|
echo "Address=${address}/${prefix}"
|
|
if [ "${type}" != "private" ]; then
|
|
echo "Gateway=$(curl -sf ${url}ipv6/gateway)"
|
|
fi
|
|
log "Added IPv6 address ${address}/${prefix} on ${interface}."
|
|
fi
|
|
local network_tail=/etc/systemd/network/template/dosync-${interface}.network.tail
|
|
if [[ -r "${network_tail}" ]]; then
|
|
cat ${network_tail}
|
|
log "Appended user specified config for ${interface}."
|
|
fi
|
|
} > /run/systemd/network/dosync-${interface}.network
|
|
}
|
|
|
|
traverse_interfaces() {
|
|
local url=$1
|
|
set -- $(curl -Ssf ${url})
|
|
if [[ " $* " =~ " mac " ]]; then
|
|
process_interface ${url} "$*"
|
|
else
|
|
local dir
|
|
for dir in $*; do
|
|
# only want dirs with slash suffix
|
|
[ "${dir}" = "${dir%/}" ] && continue
|
|
traverse_interfaces ${url}${dir}
|
|
done
|
|
fi
|
|
}
|
|
|
|
setup_from_metadata_service() {
|
|
local sshkeys
|
|
if sshkeys=$(curl -Ssf ${meta_base}public-keys) && test -n "${sshkeys}"; then
|
|
[ -d /root/.ssh ] || mkdir -m 0700 /root/.ssh
|
|
[ -e /root/.ssh/authorized_keys ] || touch /root/.ssh/authorized_keys
|
|
if ! grep -q "${sshkeys}" /root/.ssh/authorized_keys; then
|
|
printf '\n%s\n' "${sshkeys}" >> /root/.ssh/authorized_keys
|
|
log "Added SSH public keys from metadata service."
|
|
fi
|
|
fi
|
|
local hostname
|
|
if ! test -e /etc/hostname && hostname=$(curl -Ssf ${meta_base}hostname); then
|
|
echo "${hostname}" > /etc/hostname
|
|
hostname "${hostname}"
|
|
log "Hostname set to ${hostname} from metadata service."
|
|
fi
|
|
traverse_interfaces ${meta_base}interfaces/
|
|
}
|
|
|
|
digitalocean_synchronize() {
|
|
if test -e /dev/disk/by-label/DOROOT && mkdir -p /mnt/doroot; then
|
|
mount /dev/disk/by-label/DOROOT /mnt/doroot
|
|
update_shadow_if_changed /mnt/doroot
|
|
umount /mnt/doroot
|
|
else
|
|
log "Unable to check DOROOT for snapshot check!"
|
|
fi
|
|
|
|
ip link set dev eth0 up
|
|
ip addr add dev eth0 169.254.169.252/30 2>/dev/null || true
|
|
local retry
|
|
for retry in {1..20}; do
|
|
log "Attempting to connect to metadata service ..."
|
|
if curl -Ssf -m 1 ${meta_base} >/dev/null; then
|
|
setup_from_metadata_service
|
|
break
|
|
else
|
|
log "Unable to connect to metadata service!"
|
|
sleep 1
|
|
fi
|
|
done
|
|
ip addr del dev eth0 169.254.169.252/30 2>/dev/null || true
|
|
}
|
|
|
|
digitalocean_synchronize
|
|
!!!!
|
|
|
|
!!!!digitalocean-synchronize.service
|
|
[Unit]
|
|
Description=DigitalOcean Synchronization
|
|
DefaultDependencies=no
|
|
Before=systemd-networkd.service
|
|
After=systemd-udevd.service
|
|
|
|
[Service]
|
|
Type=oneshot
|
|
ExecStart=/usr/sbin/digitalocean-synchronize
|
|
!!!!
|
|
|
|
!!!!digitalocean-synchronize.PKGINFO
|
|
pkgname = digitalocean-synchronize
|
|
pkgver = 2.4-2
|
|
pkgdesc = DigitalOcean Synchronization (passwords, keys, networks)
|
|
url = https://github.com/gh2o/digitalocean-debian-to-arch
|
|
arch = any
|
|
license = GPL
|
|
!!!!
|
|
|
|
!!!!90-virtio-no-rename.link
|
|
# Prevent virtio network devices from being renamed.
|
|
[Match]
|
|
Driver=virtio_net
|
|
[Link]
|
|
NamePolicy=kernel
|
|
!!!!
|
|
|
|
EMBEDDED
|