-
Notifications
You must be signed in to change notification settings - Fork 519
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Qemu wrapper #2194
Qemu wrapper #2194
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,270 @@ | ||
#!/usr/bin/env bash | ||
|
||
shopt -s nullglob | ||
|
||
arch=${BUILDSYS_ARCH} | ||
variant=${BUILDSYS_VARIANT} | ||
host_port_forwards=tcp::2222-:22 | ||
vm_mem=4G | ||
vm_cpus=4 | ||
force_extract= | ||
declare -A extra_files=() | ||
|
||
current_images=() | ||
boot_image= | ||
data_image= | ||
|
||
readonly repo_root=$(git rev-parse --show-toplevel) | ||
|
||
bail() { | ||
>&2 echo "$@" | ||
exit 1 | ||
} | ||
|
||
show_usage() { | ||
echo "\ | ||
usage: ${0##*/} [--arch BUILDSYS_ARCH] [--variant BUILDSYS_VARIANT] | ||
[--host-port-forwards HOST_PORT_FWDS] | ||
[--vm-memory VM_MEMORY] [--vm-cpus VM_CPUS] | ||
[--inject-file LOCAL_PATH[:IMAGE_PATH]]... | ||
|
||
Launch a local virtual machine from a Bottlerocket image. | ||
|
||
Options: | ||
|
||
--arch architecture of the Bottlerocket image (must match the | ||
host architecture ($(uname -m)); may be omitted if the | ||
BUILDSYS_ARCH environment variable is set) | ||
--variant Bottlerocket variant to run (may be omitted if the | ||
BUILDSYS_VARIANT environment variable is set) | ||
--host-port-forwards | ||
list of host ports to forward to the VM; HOST_PORT_FWDS | ||
must be a valid QEMU port forwarding specifier (default | ||
is ${host_port_forwards}) | ||
--vm-memory amount of memory to assign to the VM; VM_MEMORY must be | ||
a valid QEMU memory specifier (default is ${vm_mem}) | ||
--vm-cpus number of CPUs to spawn for VM (default is ${vm_cpus}) | ||
--force-extract force recreation of the extracted Bottlerocket image, | ||
e.g. to force first boot behavior | ||
--inject-file adds a local file to the private partition of the | ||
Bottlerocket image before launching the virtual machine | ||
(may be given multiple times); existing data on the | ||
private partition will be lost | ||
--help shows this usage text | ||
|
||
By default, the virtual machine's port 22 (SSH) will be exposed via the local | ||
port 2222, i.e. if the Bottlerocket admin container has been enabled via | ||
user-data, it can be reached by running | ||
|
||
ssh -p 2222 ec2-user@localhost | ||
|
||
from the host. | ||
|
||
Usage example: | ||
|
||
${0##*/} --arch $(uname -m) --variant metal-dev --inject-file net.toml | ||
" | ||
} | ||
|
||
usage_error() { | ||
local error=$1 | ||
|
||
{ | ||
if [[ -n ${error} ]]; then | ||
printf "%s\n\n" "${error}" | ||
fi | ||
show_usage | ||
} >&2 | ||
|
||
exit 1 | ||
} | ||
|
||
parse_args() { | ||
while [[ $# -gt 0 ]]; do | ||
case $1 in | ||
-h|--help) | ||
show_usage; exit 0 ;; | ||
--arch) | ||
shift; arch=$1 ;; | ||
--variant) | ||
shift; variant=$1 ;; | ||
--host-port-forwards) | ||
shift; host_port_forwards=$1 ;; | ||
--vm-memory) | ||
shift; vm_mem=$1 ;; | ||
--vm-cpus) | ||
shift; vm_cpus=$1 ;; | ||
--force-extract) | ||
force_extract=yes ;; | ||
--inject-file) | ||
shift; local file_spec=$1 | ||
if [[ ${file_spec} = *:* ]]; then | ||
local local_file=${file_spec%%:*} | ||
local image_file=${file_spec#*:} | ||
else | ||
local local_file=${file_spec} | ||
local image_file=${file_spec##*/} | ||
fi | ||
extra_files[${local_file}]=${image_file} | ||
;; | ||
*) | ||
usage_error "unknown option '$1'" ;; | ||
esac | ||
shift | ||
done | ||
|
||
[[ -n ${arch} ]] || usage_error 'Architecture needs to be set via either --arch or BUILDSYS_ARCH.' | ||
[[ -n ${variant} ]] || usage_error 'Variant needs to be set via either --variant or BUILDSYS_VARIANT.' | ||
|
||
local host_arch=$(uname -m) | ||
[[ ${arch} = ${host_arch} ]] || bail "Architecture needs to match host architecture (${host_arch}) for hardware virtualization." | ||
|
||
for path in "${!extra_files[@]}"; do | ||
[[ -e ${path} ]] || bail "Cannot find local file '${path}' to inject." | ||
done | ||
} | ||
|
||
find_current_images() { | ||
# BuildSys removes all but the latest build's compressed images | ||
readonly current_images=( "${repo_root}/build/images/${arch}-${variant}"/*.img.lz4 ) | ||
|
||
if [[ ${#current_images[@]} -eq 0 ]]; then | ||
bail 'No images found. Did the last build fail?' | ||
fi | ||
} | ||
|
||
remove_old_images() { | ||
# Any of the latest images can serve as our touchstone. Older images can go. | ||
declare -r any_current_image=${current_images[0]} | ||
|
||
for extracted_image in "${repo_root}/build/images/${arch}-${variant}"/*.img; do | ||
if [[ ${extracted_image} -ot ${any_current_image} ]]; then | ||
rm "${extracted_image}" | ||
fi | ||
done | ||
bcressey marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
extract_images() { | ||
for compressed_image in "${current_images[@]}"; do | ||
uncompressed_image=${compressed_image%*.lz4} | ||
if [[ ${force_extract} = yes ]] || [[ ${compressed_image} -nt ${uncompressed_image} ]]; then | ||
lz4 --decompress --force --keep "${compressed_image}" | ||
fi | ||
done | ||
} | ||
|
||
select_boot_image() { | ||
for image in "${repo_root}/build/images/${arch}-${variant}/bottlerocket-${variant}-${arch}"-*.img; do | ||
case ${image} in | ||
*data*) readonly data_image=${image} ;; | ||
*) readonly boot_image=${image} ;; | ||
esac | ||
done | ||
} | ||
|
||
create_extra_files() { | ||
# Explicitly instruct the kernel to send its output to the serial port on | ||
# x86 via a bootconfig initrd. Passing in settings via user-data would be | ||
# too late to get console output of the first boot. | ||
if [[ ${arch} = x86_64 ]]; then | ||
extra_files["${repo_root}/tools/bootconfig/qemu-x86-console-bootconfig.data"]=bootconfig.data | ||
fi | ||
|
||
# If the private partition needs to be recreated, ensure that any bootconfig | ||
# data file is present, otherwise GRUB will notice the missing file and wait | ||
# for a key press. | ||
if [[ ${#extra_files[@]} -gt 0 ]]; then | ||
local has_bootconfig=no | ||
for image_file in "${extra_files[@]}"; do | ||
if [[ ${image_file} = bootconfig.data ]]; then | ||
has_bootconfig=yes | ||
break | ||
fi | ||
done | ||
if [[ ${has_bootconfig} = no ]]; then | ||
extra_files["${repo_root}/tools/bootconfig/empty-bootconfig.data"]=bootconfig.data | ||
fi | ||
fi | ||
} | ||
|
||
inject_files() { | ||
if [[ ${#extra_files[@]} -eq 0 ]]; then | ||
return 0 | ||
fi | ||
|
||
# We inject files into the boot image by replacing the private partition | ||
# entirely. The new partition has to perfectly fit over the original one. | ||
# Find the first and last sector, then calculate the partition's size. In | ||
# absence of actual hardware, assume a traditional sector size of 512 bytes. | ||
local private_first_sector private_last_sector | ||
read -r private_first_sector private_last_sector < <( | ||
fdisk --list-details "${boot_image}" \ | ||
| awk '/BOTTLEROCKET-PRIVATE/ { print $2, $3 }') | ||
if [[ -z ${private_first_sector} ]] || [[ -z ${private_last_sector} ]]; then | ||
bail "Failed to find the private partition in '${boot_image}'." | ||
fi | ||
local private_size_mib=$(( (private_last_sector - private_first_sector + 1) * 512 / 1024 / 1024 )) | ||
|
||
local private_mount private_image | ||
private_mount=$(mktemp -d) | ||
private_image=$(mktemp) | ||
|
||
for local_file in "${!extra_files[@]}"; do | ||
local image_file=${extra_files[${local_file}]} | ||
cp "${local_file}" "${private_mount}/${image_file}" | ||
done | ||
|
||
if ! mkfs.ext4 -d "${private_mount}" "${private_image}" "${private_size_mib}M" \ | ||
|| ! dd if="${private_image}" of="${boot_image}" conv=notrunc bs=512 seek="${private_first_sector}" | ||
then | ||
rm -f "${private_image}" | ||
rm -rf "${private_mount}" | ||
bail "Failed to inject files into '${boot_image}'." | ||
fi | ||
} | ||
|
||
launch_vm() { | ||
local -a qemu_args=( | ||
-nographic | ||
-enable-kvm | ||
-cpu host | ||
-smp "${vm_cpus}" | ||
-m "${vm_mem}" | ||
-drive index=0,if=virtio,format=raw,file="${boot_image}" | ||
) | ||
|
||
# Plug the virtual primary NIC in as BDF 00:10.0 so udev will give it a | ||
# consistent name we can know ahead of time--enp0s16 or ens16. | ||
qemu_args+=( | ||
-netdev user,id=net0,hostfwd="${host_port_forwards}" | ||
-device virtio-net-pci,netdev=net0,addr=10.0 | ||
) | ||
|
||
# Resolve the last bit of uncertainty by disabling ACPI-based PCI hot plug, | ||
# causing udev to use the bus location when naming the NIC (enp0s16). Since | ||
# QEMU does not support PCI hot plug via ACPI on Arm, turn it off for the | ||
# emulated x86_64 chipset only to achieve parity. | ||
if [[ ${arch} = x86_64 ]]; then | ||
qemu_args+=( -global PIIX4_PM.acpi-root-pci-hotplug=off ) | ||
fi | ||
|
||
if [[ ${arch} = aarch64 ]]; then | ||
qemu_args+=( -machine virt ) | ||
qemu_args+=( -bios /usr/share/edk2/aarch64/QEMU_EFI.silent.fd ) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this provided by all distros? Or is distro specific, and the file is installed in different locations depending on the distro? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Totally decided by a distro's packagers. On current Fedora, this is where the |
||
fi | ||
|
||
if [[ -n ${data_image} ]]; then | ||
qemu_args+=( -drive index=1,if=virtio,format=raw,file="${data_image}" ) | ||
fi | ||
|
||
qemu-system-"${arch}" "${qemu_args[@]}" | ||
} | ||
|
||
parse_args "$@" | ||
find_current_images | ||
remove_old_images | ||
extract_images | ||
select_boot_image | ||
create_extra_files | ||
inject_files | ||
launch_vm |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I really liked this :D! One potential addition to this might be to let the users attach their own NICs. I know that I would use that since I run a bunch of VMs in metal variants to test OS specifics, with an ENI attached to them which allows me to use SSM to connect to the VMs instead of SSH.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Passing through a secondary ENI on an EC2 instance? I haven't tried that myself yet, but it's definitely one way to extend this and improve the iteration time!