| Federico Ressi | 08c74e9 | 2018-06-12 14:19:21 +0200 | [diff] [blame] | 1 | #!/bin/bash | 
|  | 2 |  | 
|  | 3 | # IMPLEMENTATION NOTE: It was not possible to implement this script using | 
|  | 4 | # virt-customize because of below ubuntu bugs: | 
|  | 5 | #  - https://bugs.launchpad.net/ubuntu/+source/libguestfs/+bug/1632405 | 
|  | 6 | #  - https://bugs.launchpad.net/ubuntu/+source/isc-dhcp/+bug/1650740 | 
|  | 7 | # | 
|  | 8 | # It has therefore been adopted a more low level strategy performing below | 
|  | 9 | # steps: | 
|  | 10 | #  - mount guest image to a temporary folder | 
|  | 11 | #  - set up an environment suitable for executing chroot | 
|  | 12 | #  - execute customize_image function inside chroot environment | 
|  | 13 | #  - cleanup chroot environment | 
|  | 14 |  | 
|  | 15 | # Array of packages to be installed of guest image | 
|  | 16 | INSTALL_GUEST_PACKAGES=( | 
|  | 17 | socat  # used to replace nc for testing advanced network features like | 
|  | 18 | # multicast | 
| Slawek Kaplonski | 7e5923a | 2021-10-08 16:05:21 +0200 | [diff] [blame] | 19 | iperf3 | 
|  | 20 | iputils-ping | 
|  | 21 | ncat | 
| Slawek Kaplonski | 32b0f8b | 2023-03-10 17:03:20 +0100 | [diff] [blame] | 22 | nmap | 
| Slawek Kaplonski | 7e5923a | 2021-10-08 16:05:21 +0200 | [diff] [blame] | 23 | psmisc  # provides killall command | 
|  | 24 | python3 | 
|  | 25 | tcpdump | 
|  | 26 | vlan | 
| Federico Ressi | 08c74e9 | 2018-06-12 14:19:21 +0200 | [diff] [blame] | 27 | ) | 
|  | 28 |  | 
|  | 29 | # Function to be executed once after chroot on guest image | 
|  | 30 | # Add more customization steps here | 
|  | 31 | function customize_image { | 
|  | 32 | # dhclient-script requires to read /etc/fstab for setting up network | 
|  | 33 | touch /etc/fstab | 
|  | 34 | chmod ugo+r /etc/fstab | 
|  | 35 |  | 
|  | 36 | # Ubuntu guest image _apt user could require access to below folders | 
|  | 37 | local apt_user_folders=( /var/lib/apt/lists/partial ) | 
|  | 38 | mkdir -p "${apt_user_folders[@]}" | 
|  | 39 | chown _apt.root -fR "${apt_user_folders[@]}" | 
|  | 40 |  | 
|  | 41 | # Install desired packages to Ubuntu guest image | 
| Federico Ressi | 71bda86 | 2018-05-28 11:38:56 +0200 | [diff] [blame] | 42 | ( | 
|  | 43 | DEBIAN_FRONTEND=noninteractive | 
| Slawek Kaplonski | 7e5923a | 2021-10-08 16:05:21 +0200 | [diff] [blame] | 44 | sudo apt-get update -y | 
|  | 45 | sudo apt-get install -y "${INSTALL_GUEST_PACKAGES[@]}" | 
| Federico Ressi | 71bda86 | 2018-05-28 11:38:56 +0200 | [diff] [blame] | 46 | ) | 
| Federico Ressi | 08c74e9 | 2018-06-12 14:19:21 +0200 | [diff] [blame] | 47 | } | 
|  | 48 |  | 
|  | 49 | function main { | 
|  | 50 | set -eux | 
|  | 51 | trap cleanup EXIT | 
|  | 52 | "${ENTRY_POINT:-chroot_image}" "$@" | 
|  | 53 | } | 
|  | 54 |  | 
|  | 55 | # Chroot to guest image then executes customize_image function inside it | 
|  | 56 | function chroot_image { | 
|  | 57 | local image_file=$1 | 
|  | 58 | local temp_dir=${TEMP_DIR:-$(make_temp -d)} | 
|  | 59 |  | 
|  | 60 | # Mount guest image into a temporary directory | 
|  | 61 | local mount_dir=${temp_dir}/mount | 
|  | 62 | mkdir -p "${mount_dir}" | 
|  | 63 | mount_image "${mount_dir}" "${temp_dir}/pid" | 
|  | 64 |  | 
|  | 65 | # Mount system directories | 
|  | 66 | bind_dir "/dev" "${mount_dir}/dev" | 
|  | 67 | bind_dir "/dev/pts" "${mount_dir}/dev/pts" | 
|  | 68 | bind_dir "/proc" "${mount_dir}/proc" | 
|  | 69 | bind_dir "/sys" "${mount_dir}/sys" | 
| yatinkarel | 20d0da0 | 2025-05-12 15:54:50 +0530 | [diff] [blame] | 70 | if [ -f /etc/apt/sources.list ]; then | 
|  | 71 | mirror=$(grep -oP 'https?://\K[^/ ]+' /etc/apt/sources.list|head -1) | 
| yatinkarel | f345ae0 | 2025-07-10 16:31:15 +0530 | [diff] [blame] | 72 | if [ -n "${mirror}" ]; then | 
|  | 73 | if sudo test -f ${mount_dir}/etc/apt/sources.list.d/ubuntu.sources; then | 
|  | 74 | sudo sed -Ei "s|(http[s]?://)([^/]+)|\1${mirror}|g" ${mount_dir}/etc/apt/sources.list.d/ubuntu.sources | 
|  | 75 | sudo sed -i "/URIs:/a Trusted: yes" ${mount_dir}/etc/apt/sources.list.d/ubuntu.sources | 
|  | 76 | elif sudo test -f ${mount_dir}/etc/apt/sources.list; then | 
|  | 77 | source <(sudo cat ${mount_dir}/etc/os-release) | 
|  | 78 | sudo tee ${mount_dir}/etc/apt/sources.list <<EOF | 
|  | 79 | deb [ trusted=yes ] https://${mirror}/ubuntu ${UBUNTU_CODENAME} main universe | 
|  | 80 | deb [ trusted=yes ] https://${mirror}/ubuntu ${UBUNTU_CODENAME}-updates main universe | 
|  | 81 | deb [ trusted=yes ] https://${mirror}/ubuntu ${UBUNTU_CODENAME}-backports main universe | 
|  | 82 | deb [ trusted=yes ] https://${mirror}/ubuntu ${UBUNTU_CODENAME}-security main universe | 
| yatinkarel | 20d0da0 | 2025-05-12 15:54:50 +0530 | [diff] [blame] | 83 | EOF | 
| yatinkarel | f345ae0 | 2025-07-10 16:31:15 +0530 | [diff] [blame] | 84 | fi | 
| yatinkarel | 20d0da0 | 2025-05-12 15:54:50 +0530 | [diff] [blame] | 85 | fi | 
|  | 86 | fi | 
| Federico Ressi | 08c74e9 | 2018-06-12 14:19:21 +0200 | [diff] [blame] | 87 |  | 
|  | 88 | # Mount to keep temporary files out of guest image | 
|  | 89 | mkdir -p "${temp_dir}/apt" "${temp_dir}/cache" "${temp_dir}/tmp" | 
|  | 90 | bind_dir "${temp_dir}/cache" "${mount_dir}/var/cache" | 
|  | 91 | bind_dir "${temp_dir}/tmp" "${mount_dir}/tmp" | 
|  | 92 | bind_dir "${temp_dir}/tmp" "${mount_dir}/var/tmp" | 
|  | 93 | bind_dir "${temp_dir}/apt" "${mount_dir}/var/lib/apt" | 
|  | 94 |  | 
| Federico Ressi | 71bda86 | 2018-05-28 11:38:56 +0200 | [diff] [blame] | 95 | # Temporarly replace /etc/resolv.conf symlink to use the same DNS as this | 
|  | 96 | # host | 
|  | 97 | local resolv_file=${mount_dir}/etc/resolv.conf | 
|  | 98 | sudo mv -f "${resolv_file}" "${resolv_file}.orig" | 
|  | 99 | sudo cp /etc/resolv.conf "${resolv_file}" | 
|  | 100 | add_cleanup sudo mv -f "${resolv_file}.orig" "${resolv_file}" | 
| Federico Ressi | 08c74e9 | 2018-06-12 14:19:21 +0200 | [diff] [blame] | 101 |  | 
|  | 102 | # Makesure /etc/fstab exists and it is readable because it is required by | 
|  | 103 | # /sbin/dhclient-script | 
|  | 104 | sudo touch /etc/fstab | 
|  | 105 | sudo chmod 644 /etc/fstab | 
|  | 106 |  | 
|  | 107 | # Copy this script to mount dir | 
|  | 108 | local script_name=$(basename "$0") | 
|  | 109 | local script_file=${mount_dir}/${script_name} | 
|  | 110 | sudo cp "$0" "${script_file}" | 
|  | 111 | sudo chmod 500 "${script_file}" | 
|  | 112 | add_cleanup sudo rm -f "'${script_file}'" | 
|  | 113 |  | 
|  | 114 | # Execute customize_image inside chroot environment | 
|  | 115 | local command_line=( ${CHROOT_COMMAND:-customize_image} ) | 
|  | 116 | local entry_point=${command_line[0]} | 
|  | 117 | unset command_line[0] | 
|  | 118 | sudo -E "ENTRY_POINT=${entry_point}" \ | 
|  | 119 | chroot "${mount_dir}" "/${script_name}" "${command_line[@]:-}" | 
|  | 120 | } | 
|  | 121 |  | 
|  | 122 | # Mounts guest image to $1 directory writing pid to $1 pid file | 
|  | 123 | # Then registers umount of such directory for final cleanup | 
|  | 124 | function mount_image { | 
|  | 125 | local mount_dir=$1 | 
|  | 126 | local pid_file=$2 | 
|  | 127 |  | 
|  | 128 | # export libguest settings | 
|  | 129 | export LIBGUESTFS_BACKEND=${LIBGUESTFS_BACKEND:-direct} | 
|  | 130 | export LIBGUESTFS_BACKEND_SETTINGS=${LIBGUESTFS_BACKEND_SETTINGS:-force_tcg} | 
|  | 131 |  | 
|  | 132 | # Mount guest image | 
|  | 133 | sudo -E guestmount -i \ | 
|  | 134 | --add "${image_file}" \ | 
|  | 135 | --pid-file "${pid_file}" \ | 
|  | 136 | "${mount_dir}" | 
|  | 137 |  | 
|  | 138 | add_cleanup \ | 
|  | 139 | 'ENTRY_POINT=umount_image' \ | 
|  | 140 | "'$0'" "'${mount_dir}'" "'${pid_file}'" | 
|  | 141 | } | 
|  | 142 |  | 
|  | 143 | # Unmounts guest image directory | 
|  | 144 | function umount_image { | 
|  | 145 | local mount_dir=$1 | 
|  | 146 | local pid_file=$2 | 
|  | 147 | local timeout=10 | 
|  | 148 |  | 
|  | 149 | # Take PID just before unmounting | 
|  | 150 | local pid=$(cat ${pid_file} || true) | 
|  | 151 | sudo -E guestunmount "${mount_dir}" | 
|  | 152 |  | 
|  | 153 | if [ "${pid:-}" != "" ]; then | 
|  | 154 | # Make sure guestmount process is not running before using image | 
|  | 155 | # file again | 
|  | 156 | local count=${timeout} | 
|  | 157 | while sudo kill -0 "${pid}" 2> /dev/null && (( count-- > 0 )); do | 
|  | 158 | sleep 1 | 
|  | 159 | done | 
|  | 160 | if [ ${count} == 0 ]; then | 
|  | 161 | # It is not safe to use image file at this point | 
|  | 162 | echo "Wait for guestmount to exit failed after ${timeout} seconds" | 
|  | 163 | fi | 
|  | 164 | fi | 
|  | 165 | } | 
|  | 166 |  | 
|  | 167 | # Creates a temporary file or directory and register removal for final cleanup | 
|  | 168 | function make_temp { | 
|  | 169 | local temporary=$(mktemp "$@") | 
|  | 170 | add_cleanup sudo rm -fR "'${temporary}'" | 
|  | 171 | echo "${temporary}" | 
|  | 172 | } | 
|  | 173 |  | 
|  | 174 | # Bind directory $1 to directory $2 and register umount for final cleanup | 
|  | 175 | function bind_dir { | 
|  | 176 | local source_dir=$1 | 
|  | 177 | local target_dir=$2 | 
|  | 178 | sudo mount --bind "${source_dir}" "${target_dir}" | 
|  | 179 | add_cleanup sudo umount "'${target_dir}'" | 
|  | 180 | } | 
|  | 181 |  | 
|  | 182 | # Registers a command line to be executed for final cleanup | 
|  | 183 | function add_cleanup { | 
|  | 184 | CLEANUP_FILE=${CLEANUP_FILE:-$(mktemp)} | 
|  | 185 |  | 
|  | 186 | echo -e "$*" >> ${CLEANUP_FILE} | 
|  | 187 | } | 
|  | 188 |  | 
|  | 189 | # Execute command lines for final cleanup in reversed order | 
|  | 190 | function cleanup { | 
|  | 191 | error=$? | 
|  | 192 |  | 
|  | 193 | local cleanup_file=${CLEANUP_FILE:-} | 
|  | 194 | if [ -r "${cleanup_file}" ]; then | 
|  | 195 | tac "${cleanup_file}" | bash +e -x | 
|  | 196 | CLEANUP_FILE= | 
|  | 197 | rm -fR "${cleanup_file}" | 
|  | 198 | fi | 
|  | 199 |  | 
|  | 200 | exit ${error} | 
|  | 201 | } | 
|  | 202 |  | 
|  | 203 | main "$@" |