build-inside-vm.sh 10.5 KB
Newer Older
1
#!/bin/bash
2
# build-inside-vm.sh builds the images (cloud image, vagrant boxes)
Kristian Klausen's avatar
Kristian Klausen committed
3
4
5

# nounset: "Treat unset variables and parameters [...] as an error when performing parameter expansion."
# errexit: "Exit immediately if [...] command exits with a non-zero status."
6
set -o nounset -o errexit
7
readonly DISK_SIZE="20G"
8
readonly IMAGE="image.img"
9
# shellcheck disable=SC2016
10
readonly MIRROR='https://mirror.pkgbuild.com/$repo/os/$arch'
11

12
13
14
15
16
17
18
19
20
function init() {
  readonly ORIG_PWD="${PWD}"
  readonly OUTPUT="${PWD}/output"
  readonly TMPDIR="$(mktemp --dry-run --directory --tmpdir="${PWD}/tmp")"
  mkdir -p "${OUTPUT}" "${TMPDIR}"
  if [ -n "${SUDO_UID:-}" ]; then
    chown "${SUDO_UID}:${SUDO_GID}" "${OUTPUT}" "${TMPDIR}"
  fi
  cd "${TMPDIR}"
21

22
23
24
  readonly MOUNT="${PWD}/mount"
  mkdir "${MOUNT}"
}
25

Kristian Klausen's avatar
Kristian Klausen committed
26
# Do some cleanup when the script exits
27
function cleanup() {
Kristian Klausen's avatar
Kristian Klausen committed
28
  # We want all the commands to run, even if one of them fails.
29
30
31
32
33
34
35
36
37
38
39
40
41
42
  set +o errexit
  if [ -n "${LOOPDEV:-}" ]; then
    losetup -d "${LOOPDEV}"
  fi
  if [ -n "${MOUNT:-}" ] && mountpoint -q "${MOUNT}"; then
    # We do not want risking deleting ex: the package cache
    umount --recursive "${MOUNT}" || exit 1
  fi
  if [ -n "${TMPDIR:-}" ]; then
    rm -rf "${TMPDIR}"
  fi
}
trap cleanup EXIT

Kristian Klausen's avatar
Kristian Klausen committed
43
# Create the disk, partitions it, format the partition and mount the filesystem
44
45
46
47
48
49
50
51
function setup_disk() {
  truncate -s "${DISK_SIZE}" "${IMAGE}"
  sgdisk --clear \
    --new 1::+1M --typecode=1:ef02 \
    --new 2::-0 --typecode=2:8300 \
    "${IMAGE}"

  LOOPDEV=$(losetup --find --partscan --show "${IMAGE}")
52
  # Partscan is racy
53
  wait_until_settled "${LOOPDEV}"
54
55
56
57
  mkfs.btrfs "${LOOPDEV}p2"
  mount -o compress-force=zstd "${LOOPDEV}p2" "${MOUNT}"
}

Kristian Klausen's avatar
Kristian Klausen committed
58
# Install Arch Linux to the filesystem (bootstrap)
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
function bootstrap() {
  cat <<EOF >pacman.conf
[options]
Architecture = auto

[core]
Include = mirrorlist

[extra]
Include = mirrorlist

[community]
Include = mirrorlist
EOF
  echo "Server = ${MIRROR}" >mirrorlist

  # We use the hosts package cache
76
  pacstrap -c -C pacman.conf -M "${MOUNT}" base linux grub openssh sudo haveged btrfs-progs reflector
77
78
79
  cp mirrorlist "${MOUNT}/etc/pacman.d/"
}

Kristian Klausen's avatar
Kristian Klausen committed
80
# Misc "tweaks" done after bootstrapping
81
function postinstall() {
82
83
84
85
86
  # Remove machine-id see:
  # https://gitlab.archlinux.org/archlinux/arch-boxes/-/issues/25
  # https://gitlab.archlinux.org/archlinux/arch-boxes/-/issues/117
  rm "${MOUNT}/etc/machine-id"

Kristian Klausen's avatar
Kristian Klausen committed
87
88
89
90
91
92
93
  arch-chroot "${MOUNT}" /usr/bin/btrfs subvolume create /swap
  chattr +C "${MOUNT}/swap"
  chmod 0700 "${MOUNT}/swap"
  fallocate -l 512M "${MOUNT}/swap/swapfile"
  mkswap "${MOUNT}/swap/swapfile"
  echo -e "/swap/swapfile none swap defaults 0 0" >>"${MOUNT}/etc/fstab"

94
  echo "COMPRESSION=\"zstd\"" >>"${MOUNT}/etc/mkinitcpio.conf"
95
96
  arch-chroot "${MOUNT}" /usr/bin/mkinitcpio -p linux

97
98
99
  sed -i -e 's/^#\(en_US.UTF-8\)/\1/' "${MOUNT}/etc/locale.gen"
  arch-chroot "${MOUNT}" /usr/bin/locale-gen
  arch-chroot "${MOUNT}" /usr/bin/systemd-firstboot --locale=en_US.UTF-8 --timezone=UTC --hostname=archlinux --keymap=us
100
  ln -sf /run/systemd/resolve/stub-resolv.conf "${MOUNT}/etc/resolv.conf"
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157

  # Setup pacman-init.service for clean pacman keyring initialization
  cat <<EOF >"${MOUNT}/etc/systemd/system/pacman-init.service"
[Unit]
Description=Initializes Pacman keyring
Wants=haveged.service
After=haveged.service
Before=sshd.service cloud-final.service
ConditionFirstBoot=yes

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/bin/pacman-key --init
ExecStart=/usr/bin/pacman-key --populate archlinux

[Install]
WantedBy=multi-user.target
EOF

  # Add service for running reflector on first boot
  cat <<EOF >"${MOUNT}/etc/systemd/system/reflector-init.service"
[Unit]
Description=Initializes mirrors for the VM
After=network-online.target
Wants=network-online.target
Before=sshd.service cloud-final.service
ConditionFirstBoot=yes

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=reflector --latest 20 --protocol https --sort rate --save /etc/pacman.d/mirrorlist

[Install]
WantedBy=multi-user.target
EOF

  # enabling important services
  arch-chroot "${MOUNT}" /bin/bash -e <<EOF
. /etc/profile
systemctl enable sshd
systemctl enable haveged
systemctl enable systemd-networkd
systemctl enable systemd-resolved
systemctl enable systemd-timesyncd
systemctl enable pacman-init.service
systemctl enable reflector-init.service
EOF

  # GRUB
  arch-chroot "${MOUNT}" /usr/bin/grub-install --target=i386-pc "${LOOPDEV}"
  sed -i 's/^GRUB_TIMEOUT=.*$/GRUB_TIMEOUT=1/' "${MOUNT}/etc/default/grub"
  # setup unpredictable kernel names
  sed -i 's/^GRUB_CMDLINE_LINUX=.*$/GRUB_CMDLINE_LINUX="net.ifnames=0"/' "${MOUNT}/etc/default/grub"
  sed -i 's/^GRUB_CMDLINE_LINUX_DEFAULT=.*/GRUB_CMDLINE_LINUX_DEFAULT=\"rootflags=compress-force=zstd\"/' "${MOUNT}/etc/default/grub"
  arch-chroot "${MOUNT}" /usr/bin/grub-mkconfig -o /boot/grub/grub.cfg
158
159
}

Kristian Klausen's avatar
Kristian Klausen committed
160
# Cleanup the image and trim it
161
162
163
164
165
166
167
168
function image_cleanup() {
  # Remove pacman key ring for re-initialization
  rm -rf "${MOUNT}/etc/pacman.d/gnupg/"

  sync -f "${MOUNT}/etc/os-release"
  fstrim --verbose "${MOUNT}"
}

169
170
171
172
173
174
175
176
177
178
179
# Helper function: wait until a given loop device has settled
# ${1} - loop device
function wait_until_settled() {
  udevadm settle
  blockdev --flushbufs --rereadpt ${1}
  until test -e "${1}p2"; do
      echo "${1}p2 doesn't exist yet..."
      sleep 1
  done
}

Kristian Klausen's avatar
Kristian Klausen committed
180
# Mount image helper (loop device + mount)
181
182
function mount_image() {
  LOOPDEV=$(losetup --find --partscan --show "${1:-${IMAGE}}")
183
  # Partscan is racy
184
  wait_until_settled ${LOOPDEV}
185
186
187
188
189
  mount -o compress-force=zstd "${LOOPDEV}p2" "${MOUNT}"
  # Setup bind mount to package cache
  mount --bind "/var/cache/pacman/pkg" "${MOUNT}/var/cache/pacman/pkg"
}

Kristian Klausen's avatar
Kristian Klausen committed
190
# Unmount image helper (umount + detach loop device)
191
192
193
194
195
196
function unmount_image() {
  umount --recursive "${MOUNT}"
  losetup -d "${LOOPDEV}"
  LOOPDEV=""
}

Kristian Klausen's avatar
Kristian Klausen committed
197
# Copy image and mount the copied image
198
199
200
201
202
function copy_and_mount_image() {
  cp -a "${IMAGE}" "${1}"
  mount_image "${1}"
}

Kristian Klausen's avatar
Kristian Klausen committed
203
# Compute SHA256, adjust owner to $SUDO_UID:$SUDO_UID and move to output/
204
205
206
207
208
209
210
211
function mv_to_output() {
  sha256sum "${1}" >"${1}.SHA256"
  if [ -n "${SUDO_UID:-}" ]; then
    chown "${SUDO_UID}:${SUDO_GID}" "${1}"{,.SHA256}
  fi
  mv "${1}"{,.SHA256} "${OUTPUT}/"
}

Kristian Klausen's avatar
Kristian Klausen committed
212
# Helper function: create a new image from the "base" image
Kristian Klausen's avatar
Kristian Klausen committed
213
214
215
# ${1} - final file
# ${2} - pre
# ${3} - post
216
function create_image() {
Kristian Klausen's avatar
Kristian Klausen committed
217
218
219
  local tmp_image="$(basename "$(mktemp -u)")"
  copy_and_mount_image "${tmp_image}"
  "${2}"
220
221
  image_cleanup
  unmount_image
Kristian Klausen's avatar
Kristian Klausen committed
222
223
  "${3}" "${tmp_image}" "${1}"
  mv_to_output "${1}"
224
225
226
}

function cloud_image() {
227
228
229
  # The growpart module[1] requires the growpart program, provided by the
  # cloud-guest-utils package
  # [1] https://cloudinit.readthedocs.io/en/latest/topics/modules.html#growpart
Kristian Klausen's avatar
Kristian Klausen committed
230
  arch-chroot "${MOUNT}" /usr/bin/pacman -S --noconfirm cloud-init cloud-guest-utils
231
232
233
234
  arch-chroot "${MOUNT}" /usr/bin/systemctl enable cloud-init-local.service cloud-init.service cloud-config.service cloud-final.service
}

function cloud_image_post() {
Kristian Klausen's avatar
Kristian Klausen committed
235
  qemu-img convert -c -f raw -O qcow2 "${1}" "${2}"
236
237
238
  rm "${1}"
}

239
240
function vagrant_common() {
  arch-chroot "${MOUNT}" /usr/bin/pacman -S --noconfirm netctl polkit
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270

  NEWUSER="vagrant"
  # setting the user credentials
  arch-chroot "${MOUNT}" /usr/bin/useradd -m -U "${NEWUSER}"
  echo -e "${NEWUSER}\n${NEWUSER}" | arch-chroot "${MOUNT}" /usr/bin/passwd "${NEWUSER}"

  # setting sudo for the user
  cat <<EOF >"${MOUNT}/etc/sudoers.d/${NEWUSER}"
Defaults:${NEWUSER} !requiretty
${NEWUSER} ALL=(ALL) NOPASSWD: ALL
EOF
  chmod 440 "${MOUNT}/etc/sudoers.d/${NEWUSER}"

  # setup network
  cat <<EOF >"${MOUNT}/etc/systemd/network/eth0.network"
[Match]
Name=eth0

[Network]
DHCP=ipv4
EOF

  # install vagrant ssh key
  arch-chroot "${MOUNT}" /bin/bash -e <<EOF
install --directory --owner=vagrant --group=vagrant --mode=0700 /home/vagrant/.ssh
curl --output /home/vagrant/.ssh/authorized_keys --location https://raw.github.com/mitchellh/vagrant/master/keys/vagrant.pub
chown vagrant:vagrant /home/vagrant/.ssh/authorized_keys
chmod 0600 /home/vagrant/.ssh/authorized_keys
EOF

271
272
273
274
275
276
277
278
  # setting automatic authentication for any action requiring admin rights via Polkit
  cat <<EOF >"${MOUNT}/etc/polkit-1/rules.d/49-nopasswd_global.rules"
polkit.addRule(function(action, subject) {
    if (subject.isInGroup("vagrant")) {
        return polkit.Result.YES;
    }
});
EOF
279
280
281
282
283
}

function vagrant_qemu_post() {
  # Create vagrant box
  cat <<EOF >Vagrantfile
284
285
286
287
288
289
Vagrant.configure("2") do |config|
  config.vm.provider :libvirt do |libvirt|
    libvirt.driver = "kvm"
  end
end
EOF
290
291
292
  local virtual_size
  virtual_size="$(grep -o "^[0-9]*" <<<"${DISK_SIZE}")"
  echo '{"format":"qcow2","provider":"libvirt","virtual_size":'"${virtual_size}"'}' >metadata.json
293
294
  qemu-img convert -f raw -O qcow2 "${1}" box.img
  rm "${1}"
295

296
297
  tar -czf "${2}" Vagrantfile metadata.json box.img
  rm Vagrantfile metadata.json box.img
298
299
300
}

function vagrant_virtualbox() {
301
302
  vagrant_common
  arch-chroot "${MOUNT}" /usr/bin/pacman -S --noconfirm virtualbox-guest-utils-nox
303
304
305
306
  arch-chroot "${MOUNT}" /usr/bin/systemctl enable vboxservice
}

function vagrant_virtualbox_post() {
307
308
  # Create vagrant box
  # VirtualBox-6.1.12 src/VBox/NetworkServices/Dhcpd/Config.cpp line 276
309
310
  local mac_address
  mac_address="080027$(openssl rand -hex 3 | tr '[:lower:]' '[:upper:]')"
311
312
  cat <<EOF >Vagrantfile
Vagrant.configure("2") do |config|
313
  config.vm.base_mac = "${mac_address}"
314
315
316
317
318
319
320
321
322
end
EOF
  echo '{"provider":"virtualbox"}' >metadata.json
  qemu-img convert -f raw -O vmdk "${1}" "packer-virtualbox.vmdk"
  rm "${1}"

  cp "${ORIG_PWD}/box.ovf" .
  sed -e "s/MACHINE_UUID/$(uuidgen)/" \
    -e "s/DISK_UUID/$(uuidgen)/" \
323
    -e "s/DISK_CAPACITY/$(qemu-img info --output=json "packer-virtualbox.vmdk" | jq '."virtual-size"')/" \
324
    -e "s/UNIX/$(date +%s)/" \
325
    -e "s/MAC_ADDRESS/${mac_address}/" \
326
327
328
329
    -i box.ovf

  tar -czf "${2}" Vagrantfile metadata.json packer-virtualbox.vmdk box.ovf
  rm Vagrantfile metadata.json packer-virtualbox.vmdk box.ovf
330
331
}

332
# ${1} - Optional build version. If not set, will generate a default based on date.
333
334
335
336
337
function main() {
  if [ "$(id -u)" -ne 0 ]; then
    echo "root is required"
    exit 1
  fi
Kristian Klausen's avatar
Kristian Klausen committed
338
  init
339

340
341
342
343
344
  setup_disk
  bootstrap
  postinstall
  unmount_image

345
346
  local build_version
  if [ -z "${1:-}" ]; then
347
    build_version="$(date +%Y%m%d).0"
348
349
350
351
    echo "WARNING: BUILD_VERSION wasn't set!"
    echo "Falling back to $build_version"
  else
    build_version="${1}"
352
  fi
Kristian Klausen's avatar
Kristian Klausen committed
353
  create_image "Arch-Linux-x86_64-cloudimg-${build_version}.qcow2" cloud_image cloud_image_post
354
  create_image "Arch-Linux-x86_64-libvirt-${build_version}.box" vagrant_common vagrant_qemu_post
355
  create_image "Arch-Linux-x86_64-virtualbox-${build_version}.box" vagrant_virtualbox vagrant_virtualbox_post
356
}
357
main "$@"