# Simple Makefile to build base VM images using packer and ansible
# TODO
# * support ssh as user instead of root
# * testing target
PACKER ?= packer
ANSIBLE_DIR_CORE ?= ansible-dracut
ANSIBLE_DIR_PROV ?= ansible-roles-prov
HASHER ?= sha256sum
SHELL := /usr/bin/env bash
ARCH := $(shell uname -m | sed 's/i686/i386/')
BUILDER_QEMU_EXE := qemu-system-$(ARCH)
BUILDER_QEMU_NAME := qemu
BUILDER_QEMU_PROV := $(BUILDER_QEMU_NAME)
BUILDER_VMWARE_EXE := vmware
BUILDER_VMWARE_NAME := vmware-iso
BUILDER_VMWARE_PROV := vmware-vmx
BUILDER_VIRTUALBOX_EXE := virtualbox
BUILDER_VIRTUALBOX_NAME := virtualbox-iso
BUILDER_VIRTUALBOX_PROV := virtualbox-ovf
BUILDERS := QEMU VMWARE VIRTUALBOX
$(foreach cur, $(BUILDERS),\
$(if $(shell command -v $(BUILDER_$(cur)_EXE)),\
$(eval AVAILABLE_BUILDERS += $(cur))))
# If now BUILDER or PROVISIONER was specified on the command line, take the
# first available one; otherwise use that specified one.
BUILDER_SELECTED := $(strip $(if $(filter $(BUILDER),$(AVAILABLE_BUILDERS)),\
$(BUILDER),\
$(firstword $(AVAILABLE_BUILDERS))))
PROVISIONER_SELECTED := $(strip $(if $(filter $(PROVISIONER),$(AVAILABLE_BUILDERS)),\
$(PROVISIONER),\
$(BUILDER_SELECTED)))
override BUILDER = $(BUILDER_$(BUILDER_SELECTED)_NAME)
override PROVISIONER = $(BUILDER_$(PROVISIONER_SELECTED)_PROV)
# The packer templates, detected as *.json (excluding base.json)
TEMPLATES := $(basename $(filter-out base.json ansible-provisioner.json,$(wildcard *.json)))
# The provisioning flavors, detected as ansible-roles/setup-<flavor>.yml
FLAVORS := $(patsubst $(ANSIBLE_DIR_PROV)/setup-%.yml,%, $(wildcard $(ANSIBLE_DIR_PROV)/setup-*.yml))
BASETARGETS := $(foreach template, $(TEMPLATES), $(template)/base)
PROVTARGETS := $(foreach template, $(TEMPLATES), $(foreach flavor, $(FLAVORS), $(template)/$(flavor)))
REPROVTARGETS := $(foreach template, $(TEMPLATES), $(foreach flavor, $(FLAVORS), $(filter-out $(template)/$(flavor).latest,$(wildcard $(template)/$(flavor).*))))
COMPRESSPOSSIBLE := $(foreach template, $(TEMPLATES), $(foreach flavor, $(FLAVORS), $(wildcard $(template)/$(flavor).*)))
COMPRESSTARGETS := $(foreach possible, $(COMPRESSPOSSIBLE), $(possible)/compress)
BOOTTARGETS := $(foreach template, $(TEMPLATES), $(template)/base/boot)
BOOTTARGETS += $(foreach prov, $(PROVTARGETS), $(prov)/boot)
CURBOOTTARGETS := $(foreach prov, $(REPROVTARGETS), $(prov)/boot)
BASERMTARGETS := $(foreach target, $(BASETARGETS), rm/$(target))
PROVRMTARGETS := $(foreach target, $(REPROVTARGETS), rm/$(target))
ifndef ANSIBLE_PROV_EXTRA_ARGS
ANSIBLE_PROV_EXTRA_ARGS :=
endif
#override ANSIBLE_PROV_EXTRA_ARGS += --scp-extra-args,"-O"
override PACKER_OPTS += -var-file=base.json
ifdef DEBUG
VERBOSE := 1
ifeq ($(DEBUG),STEP)
override PACKER_OPTS += -debug
else
override PACKER_OPTS += -on-error=ask
endif
endif
ifdef WINDOW
override PACKER_OPTS += -var='headless=false'
endif
# We support parallel Provisioning packer builds.
# To ensure data consistency we save the used ansible roles before executing in
# its own environment, tagged by the current date & time.
TIMESTAMP := $(shell date "+%Y-%m-%d_%H-%M-%S")
# The ROOTPW is only needed for the base, boot and provisioning targets.
# In every other case it must not be checked as it should be possible to call
# help or cleanup without the need of defining the password.
PW_NEEDED := $(filter $(strip $(BASETARGETS) $(PROVTARGETS) $(BOOTTARGETS)),$(MAKECMDGOALS))
ifneq ($(PW_NEEDED),)
ifeq ($(strip $(ROOTPW)),)
$(error No root password is set, set it as ROOTPW in your environment.)
else
$(foreach cur,$(PW_NEEDED),\
$(eval cur_dir := $(firstword $(subst /, ,$(cur)))/base)\
$(if $(shell test -d "$(cur_dir)" && echo yes),\
$(shell echo "$(ROOTPW)" | $(HASHER) --check --status "$(cur_dir)/rootpw.$(HASHER)")\
$(if $(filter 1,$(.SHELLSTATUS)),\
$(error The wrong ROOTPW is set. Please correct it))))
endif
endif
ifdef VERBOSE
$(info root password: $(ROOTPW))
$(info hasher: $(HASHER))
$(info )
$(info timestamp: $(TIMESTAMP))
$(info )
$(info packer: executable: $(PACKER))
$(info packer: options: $(PACKER_OPTS))
$(info )
$(info ansible: core: $(ANSIBLE_DIR_CORE))
$(info ansible: flavors: $(ANSIBLE_DIR_PROV))
$(info )
$(info builder: available: $(AVAILABLE_BUILDERS))
$(info builder: chosen: $(BUILDER))
$(info )
$(info targets: base: $(strip $(BASETARGETS)))
$(info targets: boot: $(strip $(BOOTTARGETS)))
$(info targets: provision: $(strip $(PROVTARGETS)))
$(info )
endif
.PHONY: help clean_except_last clean_bases clean_all clean_failed backinglock $(REPROVTARGETS)
help:
# Creating base images. When DRACUT_INIT is specified, the dracut module
# repository will be initially cloned and its required components (dnbd3,
# xmount, libxmount-qemu) will be pre-build to accelerate subsequent builds.
$(BASETARGETS):
$(info ** Building template '$(@D)' using '$(BUILDER)' **)
$(eval INIT_TAG := $(if $(DRACUT_INIT),\
install,\
untagged))
$(PACKER) build -only=$(BUILDER) \
$(PACKER_OPTS) \
-var='vm_name=rootfs-image' \
-var='output_directory=$(@D)/base' \
-var='playbook=$(ANSIBLE_DIR_CORE)/slx-builder.yml' \
-var='extra_ansible_args=-t,$(INIT_TAG)' \
$(@D).json
@echo "$(ROOTPW)" | $(HASHER) > $(@D)/base/rootpw.$(HASHER)
# Provisioning images
$(PROVTARGETS): $(foreach flav, $(FLAVORS), %/$(flav)): %/base
$(PROVTARGETS) $(REPROVTARGETS):
$(eval FLAVOR := $(basename $(@F)))
$(eval VERSION := $(if $(suffix $(@)), $(subst .,,$(suffix $(@))), 0))
$(eval BASE_IMAGE := $(if $(wildcard $(@)/build/rootfs-image),\
$(@)/build/rootfs-image,\
$(@D)/base/rootfs-image))
$(eval BUILD_DIR := $(@D)/$(FLAVOR).$(TIMESTAMP))
$(info ** Provisioning '$(@D)' with '$(FLAVOR)' **)
@mkdir -p $(BUILD_DIR)
@cp -r `readlink -f $(ANSIBLE_DIR_PROV)` $(BUILD_DIR)/$(ANSIBLE_DIR_PROV)
@ln -sfn $(FLAVOR).$(TIMESTAMP) $(@D)/$(FLAVOR).latest
@ln -sfr $(BUILD_DIR) $(dir $(BASE_IMAGE))/$(TIMESTAMP).backinglock
@-cp -r $@/$(ANSIBLE_DIR_PROV) $(BUILD_DIR)/$(ANSIBLE_DIR_PROV).$(VERSION)
@-cp -r $@/$(ANSIBLE_DIR_PROV).* $(BUILD_DIR)
$(PACKER) build -only=$(PROVISIONER) \
$(PACKER_OPTS) \
-var='vm_name=rootfs-image' \
-var='output_directory=$(BUILD_DIR)/build' \
-var='base_image=$(BASE_IMAGE)' \
-var='playbook=$(BUILD_DIR)/$(ANSIBLE_DIR_PROV)/setup-$(FLAVOR).yml' \
-var='extra_ansible_args=$(ANSIBLE_PROV_EXTRA_ARGS)' \
ansible-provisioner.json
# Generating boot files
$(BOOTTARGETS): %/boot: %
$(BOOTTARGETS) $(CURBOOTTARGETS):
$(eval BASE_DIR := $(if $(filter base,$(notdir $(@D))),\
,\
$(if $(wildcard $(@D)/build/.),\
$(@D),\
$(@D).$(TIMESTAMP))))
$(eval BUILD_DIR := $(if $(BASE_DIR),\
$(BASE_DIR)/build,\
$(@D)))
$(eval BUILD_DIR := $(if $(filter $(@), $(CURBOOTTARGETS)),\
$(BASE_DIR)/build,\
$(BUILD_DIR)))
$(eval ANSIBLE_DIR_CUR := $(if $(BASE_DIR),\
$(BASE_DIR)/$(ANSIBLE_DIR_PROV),\
$(ANSIBLE_DIR_PROV)))
$(info ** Generating boot files for '$(BUILD_DIR)')
$(PACKER) build -only=$(PROVISIONER) \
$(PACKER_OPTS) \
-var='vm_name=rootfs-image.tmp' \
-var='output_directory=$(BUILD_DIR)/tmp' \
-var='base_image=$(BUILD_DIR)/rootfs-image' \
-var='playbook=$(ANSIBLE_DIR_CORE)/slx-builder.yml' \
-var='extra_ansible_args=-t,install,-t,build' \
-var='extra_ansible_args=$(ANSIBLE_PROV_EXTRA_ARGS)' \
ansible-provisioner.json
@mkdir -p $(BUILD_DIR)/boot
@mv -f $(ANSIBLE_DIR_CORE)/boot_files/* $(BUILD_DIR)/boot
$(if $(DEBUG),,@rm -rf $(BUILD_DIR)/tmp)
$(COMPRESSTARGETS):
$(info ** Commiting and Compressing all changes to the image **)
$(eval IMAGE_NAME := $(@D)/build/rootfs-image)
@virt-sparsify --compress --verbose $(IMAGE_NAME) $(IMAGE_NAME).tmp
@rm -f $(IMAGE_NAME)
@mv $(IMAGE_NAME).tmp $(IMAGE_NAME)
# Safe removal of images
$(BASERMTARGETS):
rm/%: %/*.backinglock
$(eval BUILD_DIR := $(subst rm/,,$(@)))
@rm -rf $(BUILD_DIR)
$(PROVRMTARGETS):
rm/%: %/build/*.backinglock
$(eval BUILD_DIR := $(subst rm/,,$(@))/build)
$(eval FATHER_BUILD := $(dir $(shell qemu-img info $(BUILD_DIR)/rootfs-image | grep "backing file" | cut -d\ -f3-)))
$(eval BUILD_TIME := $(subst .,,$(suffix $(@))))
@rm -rf $(FATHER_BUILD)/$(BUILD_TIME).backinglock
@rm -rf $(BUILD_DIR)
%.backinglock: backinglock
@qemu-img convert -f qcow2 $(@)/build/rootfs-image -O qcow2 $(@)/build/rootfs-image.tmp
@rm $(@)/build/rootfs-image
@mv $(@)/build/rootfs-image.tmp $(@)/build/rootfs-image
@rm $(@)
backinglock:
clean_except_last:
@-$(foreach template,$(TEMPLATES),\
$(eval exclusions := $(shell test -d $(template) && \
find $(template) \
-maxdepth 1 \
-type l \
-print0 \
| xargs -r -0 -n1 readlink))\
test -d $(template) && \
find $(template)/* \
-maxdepth 0 \
-type d \
$(foreach file,$(exclusions),-not -name $(file) ) \
-not -name base \
-print0 \
| xargs -r -0 -n1 rm -rf; )
clean_failed:
@-$(foreach template,$(TEMPLATES),\
test -d $(template) && \
find $(template)/* \
-maxdepth 0 \
-type d \
-not -name base \
-print0 \
| xargs -r -0 -n1 -i \
$(SHELL) -c 'test -d "{}/build" || rm -rf "{}"'; )
clean_bases:
@-$(foreach template,$(TEMPLATES),\
test -d $(template) && rm -rf $(template)/base;)
clean_all:
@-$(foreach template,$(TEMPLATES),\
test -d $(template) && rm -rf $(template);)
help:
@printf "Usage:\n\tmake <template>/<flavor>[/boot]\n"
@echo
@echo "Base images targets:"
@for T in $(BASETARGETS); do printf "\t%s\n" "$$T"; done
@echo
@echo "Provisioning images targets: "
@for P in $(PROVTARGETS); do printf "\t%s\n" "$$P"; done
@for P in $(REPROVTARGETS); do printf "\t%s\n" "$$P"; done
@echo
@echo "Generate boot files targets:"
@printf "\t<{base,provisioning}_target>/boot\n"
@echo
@echo "Commiting backing files and Compressing targets:"
@for C in $(COMPRESSTARGETS); do printf "\t%s\n" "$$C"; done
@echo
@echo "For safely removing targets:"
@printf "\trm/<target>\n"
@echo
@echo "Available options are:"
@printf "\tANSIBLE_DIR_CORE: Set directory with ansible roles for building initramfs (def: ansible-dracut)\n"
@printf "\tANSIBLE_DIR_PROV: Set directory with ansible roles for provisioning (def: ansible-roles-prov)\n"
@printf "\tBUILDER: Set an ISO builder, do not autodetect\n"
@printf "\tPROVISIONER: Set a provisioning builder, do not autodetect\n"
@printf "\tDEBUG: Enable debug mode in packer (includes VERBOSE)\n"
@printf "\t DEBUG= Enable enhanced on-error handling\n"
@printf "\t DEBUG=STEP Enable step by step debugging in packer\n"
@printf "\tHASHER: Set wanted hasher (def: sha256sum)\n"
@printf "\tPACKER: Set packer executable (def: packer)\n"
@printf "\tPACKER_OPTS: Set packer options\n"
@printf "\tROOTPW: Set root password for output image\n"
@printf "\tVERBOSE: Enable verbose output\n"
@printf "\tWINDOW: Disable headless mode\n"
@echo
@echo "Clean targets are:"
@printf "\tclean_except_last\n"
@printf "\tclean_failed\n"
@printf "\tclean_bases\n"
@printf "\tclean_all\n"