# 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-.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 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 $(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