summarylogtreecommitdiffstats
diff options
context:
space:
mode:
authorKyle Manna2015-07-10 08:12:13 -0700
committerKyle Manna2015-07-10 08:26:37 -0700
commit32dd1127b5c281c7826c42244dd0a7949121fdf6 (patch)
tree19addd823e4305d196d0bcb288cae8b7c5d44f6a
downloadaur-32dd1127b5c281c7826c42244dd0a7949121fdf6.tar.gz
aur: Prepare for AUR package
* This copied from the upstream repository and unfortunately history is lost as every commit needs to include a .SRCINFO file. * Add .SRCINFO to Makefile * Update .gitignore to ignore src tarballs
-rw-r--r--.SRCINFO15
-rw-r--r--.gitignore5
-rw-r--r--Makefile30
-rw-r--r--PKGBUILD22
-rw-r--r--README.md79
-rw-r--r--digitalocean-synchronize162
-rw-r--r--digitalocean-synchronize.install4
-rw-r--r--digitalocean-synchronize.service12
-rwxr-xr-xinstall.sh802
9 files changed, 1131 insertions, 0 deletions
diff --git a/.SRCINFO b/.SRCINFO
new file mode 100644
index 000000000000..46ced9520c9a
--- /dev/null
+++ b/.SRCINFO
@@ -0,0 +1,15 @@
+pkgbase = digitalocean-synchronize
+ pkgdesc = DigitalOcean Synchronization (passwords, keys, networks)
+ pkgver = 2.4
+ pkgrel = 2
+ url = https://github.com/gh2o/digitalocean-debian-to-arch
+ install = digitalocean-synchronize.install
+ arch = any
+ license = GPL
+ source = digitalocean-synchronize
+ source = digitalocean-synchronize.service
+ sha256sums = 2115bcf34d80186103e4399f5a20d410145ee50d316a67bdfe6f43c4b11d2064
+ sha256sums = 5888d367a08604b17528d58aa26050209d8ececf7ed35f90b5e96b31165b6a1c
+
+pkgname = digitalocean-synchronize
+
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 000000000000..c57f3cc85950
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+.swp
+.*.swp
+*pkg.tar*
+/install.pkg.sh
+*.src.tar.gz
diff --git a/Makefile b/Makefile
new file mode 100644
index 000000000000..30089ce1572f
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,30 @@
+PKGNAME = $(shell grep ^pkgname PKGBUILD | sed -e 's:.*=::')
+PKGVER = $(shell grep ^pkgver PKGBUILD | sed -e 's:.*=::')
+PKGREL = $(shell grep ^pkgrel PKGBUILD | sed -e 's:.*=::')
+PKGARCH = $(shell grep ^arch PKGBUILD | sed -e 's:.*=::')
+
+PKG = $(PKGNAME)-$(PKGVER)-$(PKGREL)-$(PKGARCH).pkg.tar.xz
+PKG_SRC = $(PKGNAME)-$(PKGVER)-$(PKGREL).src.tar.gz
+
+DEPS = digitalocean-synchronize \
+ digitalocean-synchronize.service \
+ digitalocean-synchronize.install \
+ PKGBUILD \
+ Makefile
+
+install.pkg.sh: $(PKG)
+ @cat $(subst .pkg,,$@) > $@
+ @echo -e '\ncat <<EMBEDDED\n\n!!!!digitalocean-synchronize.pkg.tar.xz' >> $@
+ @base64 $< >> $@
+ @echo -e '!!!!\n\nEMBEDDED' >> $@
+
+ $(info Build complete!)
+ $(info Output file: $@)
+
+$(PKG): $(DEPS)
+ updpkgsums
+ mksrcinfo
+ makepkg -fc
+
+clean:
+ rm -f $(PKG) install.pkg.sh
diff --git a/PKGBUILD b/PKGBUILD
new file mode 100644
index 000000000000..f650732122b3
--- /dev/null
+++ b/PKGBUILD
@@ -0,0 +1,22 @@
+# Maintainer: Gavin Li
+# Contributor: Kyle Manna <kyle at kylemanna dot com>
+
+pkgname=digitalocean-synchronize
+pkgver=2.4
+pkgrel=2
+pkgdesc='DigitalOcean Synchronization (passwords, keys, networks)'
+url='https://github.com/gh2o/digitalocean-debian-to-arch'
+arch=any
+license=GPL
+install=digitalocean-synchronize.install
+
+source=('digitalocean-synchronize'
+ 'digitalocean-synchronize.service')
+
+sha256sums=('2115bcf34d80186103e4399f5a20d410145ee50d316a67bdfe6f43c4b11d2064'
+ '5888d367a08604b17528d58aa26050209d8ececf7ed35f90b5e96b31165b6a1c')
+
+package() {
+ install -Dm755 digitalocean-synchronize ${pkgdir}/usr/bin/digitalocean-synchronize
+ install -Dm644 digitalocean-synchronize.service ${pkgdir}/usr/lib/systemd/system/digitalocean-synchronize.service
+}
diff --git a/README.md b/README.md
new file mode 100644
index 000000000000..3b3172054a47
--- /dev/null
+++ b/README.md
@@ -0,0 +1,79 @@
+DigitalOcean Debian to Arch
+===========================
+DigitalOcean deprecated Arch Linux a while back because it was relatively
+difficult to support due to the rolling updates. I wrote this script to
+bring it back! This script downloads a bootstrap Arch Linux image, updates it
+to the latest version, then overwrites the host operating system with it.
+Unlike Debian 7.x, Debian 8.x on DigitalOcean boots traditionally (through the
+MBR and Grub), so no dirty *kexec* magic is needed.
+
+Warning / Disclaimer
+--------------------
+<h3>ALL DATA ON THE DROPLET WILL BE UNCONDITIONALLY DESTROYED.</h3>
+This script may cause your VPS to become unbootable.
+I only recommend running this script on newly created droplets with no
+important data.
+
+Installation
+------------
+1. Create a new Debian 8.x droplet (either 32-bit or 64-bit is fine).
+2. In the droplet, run the following as root:
+ `wget https://raw.githubusercontent.com/gh2o/digitalocean-debian-to-arch/debian8/install.sh && bash install.sh`
+3. Follow the instructions when prompted.
+4. Sit back and relax! The system will automatically reboot once complete,
+ and you should have a fully updated Arch Linux system in within minutes.
+
+Advanced Configuration
+----------------------
+This script supports several flags, all of which are optional.
+
+* `--archlinux_mirror`
+ The Arch Linux mirror from which the bootstrap image and packages should be
+ downloaded. Defaults to the DigitalOcean mirror at
+ http://mirrors.digitalocean.com/archlinux.
+* `--kernel_package`
+ The kernel package to install. Defaults to the vanilla `linux` package.
+ Other options include `linux-lts` for long term support and `linux-grsec` for
+ a kernel with grsecurity/PaX patches.
+* `--target_architecture`
+ The architecture of the new Arch Linux installation. Defaults to the
+ architecture of the original Debian image as provided by `uname -m`.
+ A 64-bit Debian image may convert to either `x86_64` or `i686`.
+ A 32-bit Debian image may only convert to `i686`.
+* `--target_disklabel`
+ The type of partition table to use. Defaults to `gpt` (GUID partition table
+ as used by EFI). The alternative is `dos` (traditional MBR).
+* `--target_filesystem`
+ The filesystem on which the Arch Linux installation should be installed.
+ Defaults to `ext4`. The alternative is `btrfs`.
+
+How it Works
+------------
+1. A sparse disk image is created with the same size of the droplet's disk.
+2. Three partitions are made and formatted.
+ * **DORoot**: A "dummy" partition to keep DigitalOcean happy. When snapshots
+ are restored, new passwords are written here.
+ * **BIOSBoot**: The virtual machine BIOS cannot boot from GPT partitions
+ directly, so a small partition is placed here for bootloader code.
+ * **ArchRoot**: The main root filesystem for Arch Linux.
+3. The Arch Linux bootstrap image is downloaded and unpacked onto ArchRoot.
+4. `pacman -Syu` is called inside the image to pull in all the base packages
+ along with OpenSSH.
+5. The root password and SSH host keys are copied into the image.
+6. A special script called `digitalocean-synchronize` is installed into
+ the image. This script is run at every startup to autodetect the network
+ settings from the metadata service. It also detects if the droplet
+ was just restored, and if so, it resets the root password and regenerates
+ the host SSH keys.
+7. The image is now ready. The script then generates a "blockplan". It is
+ essentially a list of instructions to image the virtual disk with the
+ sparse disk image without requiring any extra space.
+8. A minimal root filesystem is generated on RAM so that the disk can
+ be unmounted.
+9. The script calls `systemctl switch-root` to enter the minimal
+ root filesystem.
+10. The disk is unmounted.
+11. The blockplan is executed.
+12. The bootloader (Grub) is installed.
+13. Reboot!
+14. Done!
diff --git a/digitalocean-synchronize b/digitalocean-synchronize
new file mode 100644
index 000000000000..35593fa1900f
--- /dev/null
+++ b/digitalocean-synchronize
@@ -0,0 +1,162 @@
+#!/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
diff --git a/digitalocean-synchronize.install b/digitalocean-synchronize.install
new file mode 100644
index 000000000000..ca8c7b497d9f
--- /dev/null
+++ b/digitalocean-synchronize.install
@@ -0,0 +1,4 @@
+#!/bin/sh
+post_install() {
+ systemctl enable digitalocean-synchronize.service
+}
diff --git a/digitalocean-synchronize.service b/digitalocean-synchronize.service
new file mode 100644
index 000000000000..74a0c1cd3596
--- /dev/null
+++ b/digitalocean-synchronize.service
@@ -0,0 +1,12 @@
+[Unit]
+Description=DigitalOcean Synchronization
+DefaultDependencies=no
+Before=systemd-networkd.service
+After=systemd-udevd.service
+
+[Service]
+Type=oneshot
+ExecStart=/usr/sbin/digitalocean-synchronize
+
+[Install]
+WantedBy=multi-user.target
diff --git a/install.sh b/install.sh
new file mode 100755
index 000000000000..e4b4c3fb1843
--- /dev/null
+++ b/install.sh
@@ -0,0 +1,802 @@
+#!/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" | base64 --decode
+}
+
+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
+
+ extract_embedded_file digitalocean-synchronize.pkg.tar.xz > ${destination}
+}
+
+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