diff --git a/alpine/Dockerfile.ami b/alpine/Dockerfile.ami new file mode 100644 index 000000000..ab509a278 --- /dev/null +++ b/alpine/Dockerfile.ami @@ -0,0 +1,14 @@ +FROM alpine + +RUN apk add --update \ + python \ + py-pip \ + bash \ + curl \ + e2fsprogs \ + jq \ + syslinux +RUN pip install -U awscli +COPY aws/bake-ami.sh . + +ENTRYPOINT ["./bake-ami.sh"] diff --git a/alpine/Makefile b/alpine/Makefile index 4bcb6871a..4ece35e12 100644 --- a/alpine/Makefile +++ b/alpine/Makefile @@ -48,6 +48,13 @@ initrd-arm.img: Dockerfile.armhf docker-compose build arm docker-compose run --rm -T arm /bin/mkinitrd.sh > $@ +ami: initrd.img + $(MAKE) -C kernel + $(MAKE) -C packages + docker-compose build ami + docker-compose run --rm -T ami clean + docker-compose run --rm -T ami bake + clean: rm -f initrd.img initrd.img.gz initrd-arm.img Dockerfile.armhf etc/inittab rm -f mobylinux-bios.iso mobylinux-efi.iso mobylinux.efi diff --git a/alpine/aws/.gitignore b/alpine/aws/.gitignore new file mode 100644 index 000000000..f47cb2045 --- /dev/null +++ b/alpine/aws/.gitignore @@ -0,0 +1 @@ +*.out diff --git a/alpine/aws/bake-ami.sh b/alpine/aws/bake-ami.sh new file mode 100755 index 000000000..ab9246e60 --- /dev/null +++ b/alpine/aws/bake-ami.sh @@ -0,0 +1,220 @@ +#!/bin/bash + +# Script to automate creation and snapshotting of a Moby AMI. Currently, it's +# intended to be invoked from an instance running in the same region as the +# target AMI will be in, since it directly mounts the created EBS volume as a +# device on this running instance. + +set -e + +TAG_KEY=moby-bake +INSTANCE_METADATA_API_ENDPOINT=http://169.254.169.254/latest/meta-data/ + +# TODO(nathanleclaire): This could be calculated dynamically to avoid conflicts. +EBS_DEVICE=/dev/xvdb + +function arrowecho () { + echo " --->" "$@" +} + +function current_instance_az () { + curl -s ${INSTANCE_METADATA_API_ENDPOINT}/placement/availability-zone +} + +function current_instance_id () { + curl -s ${INSTANCE_METADATA_API_ENDPOINT}/instance-id +} + +# We tag resources created as part of the build to ensure that they can be +# cleaned up later. +function tag () { + arrowecho "Tagging $1" + aws ec2 create-tags --resources "$1" --tags Key=${TAG_KEY},Value= >/dev/null +} + +function format_device () { + arrowecho "Waiting for EBS device to appear in build container" + + while [[ ! -e ${EBS_DEVICE} ]]; do + sleep 1 + done + + # This heredoc might be confusing at first glance, so here is a detailed + # summary of what each line does: + # + # n - create new partition + # p - make it a primary partition + # 1 - it should be partition #1 + # \n - use default first cylinder + # \n - use default last cylinder + # a - toggle a partition as bootable + # 1 - do the 1st partition specifically + # w - write changes and exit + arrowecho "Formatting boot partition" + fdisk ${EBS_DEVICE} >/dev/null << EOF +n +p +1 + + +a +1 +w +EOF + + # To ensure everything went smoothly, print the resulting partition table. + echo + arrowecho "Printing device partition contents" + fdisk -l ${EBS_DEVICE} + + ROOT_PARTITION="${EBS_DEVICE}1" + ROOT_PARTITION_MOUNT="/mnt/moby" + + # Mount created root partition, format it as ext4, and copy over the needed + # files for boot (syslinux configuration, kernel binary, and initrd.img) + arrowecho "Making filesystem on partition 1" + mke2fs -t ext4 ${ROOT_PARTITION} + + arrowecho "Mounting partition filesystem" + mkdir ${ROOT_PARTITION_MOUNT} + mount -t ext4 ${ROOT_PARTITION} ${ROOT_PARTITION_MOUNT} + + arrowecho "Copying image and kernel binary to partition" + + # Get files needed to boot in place. + cp /mnt/syslinux.cfg ${ROOT_PARTITION_MOUNT} + cp /mnt/kernel/vmlinuz64 ${ROOT_PARTITION_MOUNT} + cp /mnt/initrd.img ${ROOT_PARTITION_MOUNT} + + # From http://www.syslinux.org/wiki/index.php?title=EXTLINUX: + # + # "Note that EXTLINUX installs in the filesystem partition like a + # well-behaved bootloader :). Thus, it needs a master boot record in the + # partition table; the mbr.bin shipped with SYSLINUX should work well." + + # Thus, this step installs syslinux on the mounted filesystem (partition + # 1). + arrowecho "Installing syslinux to partition" + extlinux --install ${ROOT_PARTITION_MOUNT} + + # Format master boot record in partition table on target device. + arrowecho "Copying MBR to partition table in target device" + dd if=/usr/share/syslinux/mbr.bin of=${EBS_DEVICE} bs=440 count=1 + + umount ${ROOT_PARTITION_MOUNT} + + arrowecho "Checking device/partition sanity" + fdisk -l ${EBS_DEVICE} +} + +function bake_image () { + # Create a new EBS volume. We will format this volume to boot into Moby + # initrd via syslinux in MBR. That formatted drive can then be snapshotted + # and turned into an AMI. + VOLUME_ID=$(aws ec2 create-volume \ + --size 1 \ + --availability-zone $(current_instance_az) | jq -r .VolumeId) + + tag ${VOLUME_ID} + + aws ec2 wait volume-available --volume-ids ${VOLUME_ID} + + arrowecho "Attaching volume" + aws ec2 attach-volume \ + --volume-id ${VOLUME_ID} \ + --device ${EBS_DEVICE} \ + --instance-id $(current_instance_id) >/dev/null + + aws ec2 wait volume-in-use --volume-ids ${VOLUME_ID} + + format_device + + arrowecho "Taking snapshot!" + + # Take a snapshot of the volume we wrote to. + SNAPSHOT_ID=$(aws ec2 create-snapshot \ + --volume-id ${VOLUME_ID} \ + --description "Snapshot of Moby device for AMI baking" | jq -r .SnapshotId) + + tag ${SNAPSHOT_ID} + + arrowecho "Waiting for snapshot completion" + + aws ec2 wait snapshot-completed --snapshot-ids ${SNAPSHOT_ID} + + # Convert that snapshot into an AMI as the root device. + IMAGE_ID=$(aws ec2 register-image \ + --name "Moby Linux" \ + --description "The best OS for running Docker" \ + --architecture x86_64 \ + --root-device-name "${EBS_DEVICE}" \ + --virtualization-type "hvm" \ + --block-device-mappings "[ + { + \"DeviceName\": \"${EBS_DEVICE}\", + \"Ebs\": { + \"SnapshotId\": \"${SNAPSHOT_ID}\" + } + } + ]" | jq -r .ImageId) + + tag ${IMAGE_ID} + + # Boom, now you (should) have a Moby AMI. + arrowecho "Created AMI: ${IMAGE_ID}" + + echo ${IMAGE_ID} >/mnt/aws/ami_id.out +} + +function clean_tagged_resources () { + if [[ -d /mnt/moby ]]; then + rm -rf /mnt/moby + fi + + VOLUME_ID=$(aws ec2 describe-volumes --filters "Name=tag-key,Values=$TAG_KEY" | jq -r .Volumes[0].VolumeId) + if [[ ${VOLUME_ID} == "null" ]]; then + arrowecho "No volume found, skipping" + else + arrowecho "Detaching volume" + aws ec2 detach-volume --volume-id ${VOLUME_ID} >/dev/null + aws ec2 wait volume-available --volume-ids ${VOLUME_ID} + arrowecho "Deleting volume" + aws ec2 delete-volume --volume-id ${VOLUME_ID} >/dev/null + fi + + IMAGE_ID=$(aws ec2 describe-images --filters "Name=tag-key,Values=$TAG_KEY" | jq -r .Images[0].ImageId) + if [[ ${IMAGE_ID} == "null" ]]; then + arrowecho "No image found, skipping" + else + arrowecho "Deregistering previously baked AMI" + + # Sometimes describe-images does not return null even if the found + # image cannot be deregistered + # + # TODO(nathanleclaire): More elegant solution? + aws ec2 deregister-image --image-id ${IMAGE_ID} >/dev/null || true + fi + + SNAPSHOT_ID=$(aws ec2 describe-snapshots --filters "Name=tag-key,Values=$TAG_KEY" | jq -r .Snapshots[0].SnapshotId) + if [[ ${SNAPSHOT_ID} == "null" ]]; then + arrowecho "No snapshot found, skipping" + else + arrowecho "Deleting volume snapshot" + aws ec2 delete-snapshot --snapshot-id ${SNAPSHOT_ID} + fi +} + +case "$1" in + bake) + bake_image + ;; + clean) + clean_tagged_resources + ;; + regions) + # TODO(nathanleclaire): Propogate created image (if existing) to every + # possible region / AZ + ;; + *) + arrowecho "Command $1 not found. Usage: ./bake-ami.sh [bake|clean|regions]" +esac diff --git a/alpine/aws/run-instance.sh b/alpine/aws/run-instance.sh new file mode 100755 index 000000000..023c12301 --- /dev/null +++ b/alpine/aws/run-instance.sh @@ -0,0 +1,35 @@ +#!/bin/bash + +# Quick script to boot an instance from generated AMI. Intended to be invoked +# from "alpine" directory. + +set -e + +INSTANCE_ID=$(cat ./aws/instance_id.out) +aws ec2 terminate-instances --instance-id ${INSTANCE_ID} || true + +if [[ ! -f ./aws/ami_id.out ]]; then + echo "AMI ID to launch instance from not found" + exit 1 +fi + +AMI_ID=$(cat ./aws/ami_id.out) + +echo "Running instance from ${AMI_ID}" + +INSTANCE_ID=$(aws ec2 run-instances --image-id ${AMI_ID} --instance-type t2.nano | jq -r .Instances[0].InstanceId) + +aws ec2 create-tags --resources ${INSTANCE_ID} --tags Key=Name,Value=moby-boot-from-ami + +echo "Running instance ${INSTANCE_ID}" +echo ${INSTANCE_ID} >./aws/instance_id.out + +echo "Waiting for instance boot log to become available" + +INSTANCE_BOOT_LOG="null" +while [[ ${INSTANCE_BOOT_LOG} == "null" ]]; do + INSTANCE_BOOT_LOG=$(aws ec2 get-console-output --instance-id ${INSTANCE_ID} | jq -r .Output) + sleep 5 +done + +aws ec2 get-console-output --instance-id ${INSTANCE_ID} | jq -r .Output diff --git a/alpine/docker-compose.yml b/alpine/docker-compose.yml index 4707fd4e2..2bd9d6d6e 100644 --- a/alpine/docker-compose.yml +++ b/alpine/docker-compose.yml @@ -26,3 +26,13 @@ services: context: . dockerfile: Dockerfile.armhf network_mode: bridge + ami: + privileged: true + build: + context: . + dockerfile: Dockerfile.ami + network_mode: bridge + volumes: + - .:/mnt + - $HOME/.aws:/root/.aws:ro + - /dev:/dev diff --git a/alpine/mkinitrd.sh b/alpine/mkinitrd.sh index 5b746c8ff..74ecbbd27 100755 --- a/alpine/mkinitrd.sh +++ b/alpine/mkinitrd.sh @@ -26,7 +26,10 @@ mknod -m 666 zero c 1 5 mknod -m 666 tty c 5 0 mknod -m 600 console c 5 1 +mknod -m 600 tty0 c 4 11 +mknod -m 600 tty1 c 4 12 mknod -m 600 ttyS0 c 4 64 +mknod -m 600 hvc0 c 229 2 mknod -m 600 fuse c 10 229 # we are using sata emulation at present @@ -44,6 +47,8 @@ mknod -m 600 sdb3 b 8 19 mknod -m 600 sdb4 b 8 20 mknod -m 600 sdb5 b 8 21 mknod -m 600 sdb6 b 8 22 +mknod -m 600 xvdf b 8 23 +mknod -m 600 xvdf1 b 8 24 # mount points in /dev mkdir pts mqueue shm diff --git a/alpine/syslinux.cfg b/alpine/syslinux.cfg new file mode 100644 index 000000000..22f50360c --- /dev/null +++ b/alpine/syslinux.cfg @@ -0,0 +1,7 @@ +DEFAULT linux +TIMEOUT 0 +PROMPT 0 +LABEL linux + KERNEL /vmlinuz64 + INITRD /initrd.img + APPEND root=/dev/xvdb1 console=hvc0 console=tty0 console=tty1 console=ttyS0