From e0f806a2f44bc08ff8931f8e5dab09579850154d Mon Sep 17 00:00:00 2001 From: Peter Bex Date: Sun, 14 Feb 2016 19:16:04 +0100 Subject: First version of vps image builder. --- assets/boot/extlinux.conf | 5 + assets/firewall/ferm.conf | 38 +++++ assets/fstab | 3 + assets/modprobe/blacklist.conf | 2 + assets/network/cloudstack-guest-setup | 30 ++++ assets/network/interfaces | 18 +++ assets/package-manager/apt/apt-settings | 4 + vps-builder.scm | 268 ++++++++++++++++++++++++++++++++ 8 files changed, 368 insertions(+) create mode 100644 assets/boot/extlinux.conf create mode 100644 assets/firewall/ferm.conf create mode 100644 assets/fstab create mode 100644 assets/modprobe/blacklist.conf create mode 100644 assets/network/cloudstack-guest-setup create mode 100644 assets/network/interfaces create mode 100644 assets/package-manager/apt/apt-settings create mode 100644 vps-builder.scm diff --git a/assets/boot/extlinux.conf b/assets/boot/extlinux.conf new file mode 100644 index 0000000..c340924 --- /dev/null +++ b/assets/boot/extlinux.conf @@ -0,0 +1,5 @@ +default linux +timeout 1 +label linux +kernel {KERNEL} +append initrd={RAMDISK} root=/dev/vda1 console=tty0 console=ttyS0,115200 ro quiet \ No newline at end of file diff --git a/assets/firewall/ferm.conf b/assets/firewall/ferm.conf new file mode 100644 index 0000000..e9a1e88 --- /dev/null +++ b/assets/firewall/ferm.conf @@ -0,0 +1,38 @@ +# -*- shell-script -*- +# +# Configuration file for ferm(1). +# + +@def $PUBLIC_SERVICES=(ssh); +@def $BADGUYS=(); + +domain (ip ip6) table filter { + chain (INPUT OUTPUT FORWARD) { + # connection tracking + mod state state INVALID DROP; + mod state state (ESTABLISHED RELATED) ACCEPT; + } + + chain INPUT { + policy DROP; + + # drop blacklisted connections + saddr @ipfilter($BADGUYS) DROP; + + # allow local packet + interface lo ACCEPT; + + # respond to ping + proto icmp ACCEPT; + + proto tcp dport $PUBLIC_SERVICES ACCEPT; + } + + chain OUTPUT { + policy ACCEPT; + } + + chain FORWARD { + policy DROP; + } +} diff --git a/assets/fstab b/assets/fstab new file mode 100644 index 0000000..703881b --- /dev/null +++ b/assets/fstab @@ -0,0 +1,3 @@ +# /etc/fstab: static file system information. +proc /proc proc nodev,noexec,nosuid 0 0 +/dev/vda1 / ext3 errors=remount-ro 0 1 diff --git a/assets/modprobe/blacklist.conf b/assets/modprobe/blacklist.conf new file mode 100644 index 0000000..4a36d77 --- /dev/null +++ b/assets/modprobe/blacklist.conf @@ -0,0 +1,2 @@ +# disable pc speaker +blacklist pcspkr \ No newline at end of file diff --git a/assets/network/cloudstack-guest-setup b/assets/network/cloudstack-guest-setup new file mode 100644 index 0000000..3583afe --- /dev/null +++ b/assets/network/cloudstack-guest-setup @@ -0,0 +1,30 @@ +#!/bin/sh +# +# From https://github.com/shankerbalan/cloudstack-scripts +# Stripped out the non-Debian and Plesk stuff, the cron +# randomisation and the 127.0.1.1 removal in /etc/hosts. +# +# TODO: Rewrite in CHICKEN? +# +# /etc/dhcp/dhclient-exit-hooks.d/cloudstack-guest-setup (debian/ubuntu) +# runs on firstboot after acquiring DHCP lease + +if [ "$reason" != BOUND ] && [ "$reason" != RENEW ] && [ "$reason" != REBIND ] && [ "$reason" != REBOOT ]; then + return +fi + +# set hostname +logger -t "cloudstack" "Setting hostname to \"${new_host_name}\"" +hostname "$new_host_name" > /dev/null 2>&1 +echo "$new_host_name" > /etc/hostname + +# add hostname to /etc/hosts and remove previous localhost-style +sed -i "/127.0.0.1/a $new_ip_address $new_host_name.$new_domain_name $new_host_name" /etc/hosts > /dev/null 2>&1 + +# generate ssh host keys +logger -t "cloudstack" "Generating ssh host keys" +ssh-keygen -A && rm /etc/ssh/ssh_host_key /etc/ssh/ssh_host_key.pub +# remove self +rm /etc/dhcp/dhclient-exit-hooks.d/cloudstack-guest-setup > /dev/null 2>&1 + +exit 0 diff --git a/assets/network/interfaces b/assets/network/interfaces new file mode 100644 index 0000000..b40848c --- /dev/null +++ b/assets/network/interfaces @@ -0,0 +1,18 @@ +# This file describes the network interfaces available on your system +# and how to activate them. For more information, see interfaces(5). + +# The loopback network interface +auto lo +iface lo inet loopback + +# The normal eth0 +auto eth0 +iface eth0 inet dhcp + +# Maybe the VM has 2 NICs? +allow-hotplug eth1 +iface eth1 inet dhcp + +# Maybe the VM has 3 NICs? +allow-hotplug eth2 +iface eth2 inet dhcp diff --git a/assets/package-manager/apt/apt-settings b/assets/package-manager/apt/apt-settings new file mode 100644 index 0000000..32ee0c8 --- /dev/null +++ b/assets/package-manager/apt/apt-settings @@ -0,0 +1,4 @@ +// Avoid installing unnecessary packages, to keep a lean system. +// This is installed into /etc/apt/apt.conf.d/90custom-config +APT::Install-Recommends "0"; +APT::Install-Suggests "0"; diff --git a/vps-builder.scm b/vps-builder.scm new file mode 100644 index 0000000..7c4b60f --- /dev/null +++ b/vps-builder.scm @@ -0,0 +1,268 @@ +;;; +;;; Lightweight VPS image builder, by Peter Bex. +;;; +;;; This program is hereby placed in the public domain. +;;; +;; +;; This provides a way of building custom images to run on VPSes which +;; can automatically configure themselves, without requiring +;; heavyweight, difficult to install solutions like Hashicorp's +;; "Packer" (which has no Debian package) and/or CloudInit (which in +;; Debian requires not one, but two versions of Python to be +;; installed!) +;; +;; Heavily inspired by Debian's "build-openstack-debian-image" shell +;; script, provided by the "openstack-debian-images" package. +;; +;; Currently only Debian Jessie and CloudStack are supported. +;; Auto-resizing is NOT supported at the moment, you'll need to know +;; in advance how big the VM's root disk will be. In fact, you'll +;; need to hack this script to change what it does. +;; +;; Maybe later, if I use this script more, it can be turned into +;; a proper customisable program/library. +;; +;; To run, this requires the following packges: +;; qemu-utils, kpartx, parted, mbr +;; +;; It depends on the "scsh-process" egg +;; +;; Before running it, you can create a "users" directory containing +;; the public keys of all the users you want to prepopulate the image +;; with. The names of these files need to be USER:GROUP1,... so for +;; example, to create a user "peter" who is a member of groups "sudo" +;; and "adm", copy his id_rsa.pub file to "users/peter:sudo,adm". + +(module vps-builder () + + (import chicken scheme) + (use data-structures extras files posix scsh-process) + + (define debug? #t) + + (reset-handler (lambda () #f)) + + ;; Might it be useful to put this in scsh-process? + (define-syntax run* + (syntax-rules () + ((_ ?pf ?redir ...) + (begin + (when debug? + (fprintf (current-error-port) "$ ~S\n" `?pf)) + (receive (status normal? pid) + (run ?pf ?redir ...) + (unless (and normal? (zero? status)) + (error (sprintf "Pipeline ~S exited abnormally, with code: ~A" + `(run ?pf ?redir ...) status)))))))) + + (define minimal-packages + `("sudo" "locales" "extlinux" "openssh-server" "file" "kbd" + ;; TODO: What about other architectures? + "linux-image-amd64")) + + ;; It's necessary to be root due to chroot and mount calls. + ;; TODO: Maybe support using sudo instead? For now, just call + ;; the whole script with sudo. Otherwise we'll need to mess + ;; with PATH and so on, too. + (define (check-privileges) + (unless (zero? (current-effective-user-id)) + (error "Sorry, you must be root to build an image!"))) + + (define (check-users) + (when (or (not (directory? "users")) (null? (glob "users/*:*"))) + (error (conc "You need to put public ssh keys in the " + "\"users\" directory, named like " + "USERNAME:GROUP1,...")))) + + (define (create-image-file raw-file size-in-gb) + (let ((size (sprintf "~AG" size-in-gb))) + (run* (qemu-img create ,raw-file ,size)))) + + (define (convert-to-qcow2 raw-file qcow-file) + ;; TODO: Figure out if compat option is really needed + (run* (qemu-img convert -c -f raw -O qcow2 -o compat=0.10 + ,raw-file ,qcow-file))) + + (define (partition-image image-file) + (run* (parted -s ,image-file mktable msdos)) + (run* (parted -s ,image-file -a optimal + mkpart primary ext3 1Mi 100%)) + (run* (parted -s ,image-file set 1 boot on)) + (run* (install-mbr ,image-file))) + + (define (prepare-image raw-image size-in-gb) + (create-image-file raw-image size-in-gb) + (partition-image raw-image)) + + (define (call-with-devmapped-image image-file proc) + (let ((kpartx-output #f)) + (dynamic-wind + (lambda () + ;; TODO: run/string doesn't (and can't) check exit status! + (set! kpartx-output (run/string (kpartx -asv ,image-file)))) + (lambda () + ;; Yeah this is stupid... + (let* ((parts (string-split kpartx-output " ")) + (loopback-dev (list-ref parts 2))) + (proc (make-pathname "/dev/mapper" loopback-dev)))) + (lambda () + (set! kpartx-output #f) + ;; For some reason kpartx will sometimes(?) fail + (handle-exceptions exn #f + (run* (kpartx -d ,image-file))))))) + + (define (prepare-filesystem device) + ;; Apparently, operating on ext2 is much faster than ext3, so we + ;; make it ext2, and convert to ext3 later + (run* (mkfs.ext2 ,device))) + + (define (finalize-filesystem device) + ;; Add journal, turning it into ext3 + (run* (tune2fs -j ,device))) + + (define (call-with-mounted-device device proc) + (let ((tempdir #f)) + (dynamic-wind + (lambda () + (set! tempdir (create-temporary-directory)) + (run* (mount -o loop ,device ,tempdir))) + (lambda () (proc tempdir)) + (lambda () + (run* (umount ,tempdir)) + (delete-directory tempdir #t) + (set! tempdir #f))))) + + ;; Create a very basic setup which allows running programs inside + ;; a chroot (mount essential filesystems). + (define (with-running-system root-dir thunk) + (dynamic-wind + (lambda () (run* (chroot ,root-dir mount /proc))) + (lambda () (thunk)) + (lambda () (run* (chroot ,root-dir umount /proc))))) + + (define (install-basic-system target-dir package-list) + (let ((include (sprintf "--include=~A" + (string-intersperse package-list ",")))) + (run* (debootstrap + --verbose ,include "jessie" ,target-dir + "http://ftp.surfnet.nl/os/Linux/distr/debian")))) + + (define (install-file root-dir source target owner group mode) + (let* ((full-path (make-pathname root-dir target)) + (full-path (if (directory? full-path) + (make-pathname full-path + (pathname-strip-directory source)) + full-path))) + (file-copy source full-path #t) ; allow clobber + ;; Must run in chroot to use correct passwd/group db + (run* (chroot ,root-dir chown ,(conc owner ":" group) ,target)) + (change-file-mode full-path mode))) + + (define (install-directory root-dir target owner group mode) + (let ((full-path (make-pathname root-dir target))) + (create-directory full-path) + ;; Must run in chroot to use correct passwd/group db + (run* (chroot ,root-dir chown ,(conc owner ":" group) ,target)) + (change-file-mode full-path mode))) + + (define (install-packages root-dir . packages) + (run* (chroot ,root-dir apt-get install -y ,@packages))) + + (define (configure-basic-system root-dir) + ;;;; Configure apt, FS and disable console bleeping (just in case) + (install-file root-dir "assets/package-manager/apt/apt-settings" + "/etc/apt/apt.conf.d/90custom-config" + "root" "root" #o644) + (install-file root-dir "assets/fstab" + "/etc/fstab" "root" "root" #o644) + (install-file root-dir "assets/modprobe/blacklist.conf" + "/etc/modprobe.d/blacklist.conf" "root" "root" #o644) + + ;;;; Setup network and related settings + (delete-file* (make-pathname + root-dir "etc/udev/rules.d/70-persistent-net.rules")) + (install-file root-dir "assets/network/interfaces" + "/etc/network/interfaces" "root" "root" #o644) + (install-file root-dir "assets/network/cloudstack-guest-setup" + "/etc/dhcp/dhclient-exit-hooks.d" "root" "root" #o644)) + + (define (update-packages root-dir) + (run* (chroot ,root-dir apt-get update)) + (run* (chroot ,root-dir apt-get upgrade -y))) + + (define (make-bootable root-dir) + + (define (rooted-glob dir pattern) + (let ((f (car (glob (make-pathname `(,root-dir ,dir) pattern))))) + (make-pathname `("/" ,dir) (pathname-strip-directory f)))) + + (let* ((kernel (rooted-glob "boot" "vmlinuz-*")) + (ramdisk (rooted-glob "boot" "initrd.img-*")) + (template (with-input-from-file + "assets/boot/extlinux.conf" read-string)) + (conf (string-translate* template + `(("{KERNEL}" . ,kernel) + ("{RAMDISK}" . ,ramdisk)))) + (tgt (make-pathname `(,root-dir "boot" "extlinux") + "extlinux.conf"))) + (install-directory root-dir "/boot/extlinux" "root" "root" #o755) + (with-output-to-file tgt (lambda () (write-string conf))) + (run* (extlinux --install ,(make-pathname root-dir "boot"))))) + + (define (setup-firewall root-dir) + (install-packages root-dir "ferm" "sshguard") + (install-file root-dir "assets/firewall/ferm.conf" + "/etc/ferm/ferm.conf" "root" "adm" #o644)) + + ;; Create user and copy matching users/*:* file to .ssh/authorized_keys + (define (create-users root-dir) + (for-each (lambda (pubkey) + (let* ((fn (pathname-strip-directory pubkey ":")) + (user+cs-groups (string-split fn)) + (user (car user+cs-groups)) + (cs-groups (cadr user+cs-groups)) + (.ssh (make-pathname `("/" "home" ,user) ".ssh")) + (keys (make-pathname .ssh "authorized_keys"))) + (run* (chroot ,root-dir useradd -m -G ,cs-groups ,user)) + (install-directory root-dir .ssh user user #o700) + (install-file root-dir pubkey keys user user #o600))) + (glob "users/*:*"))) + + (define (build-image image-base-name size-in-gb) + (let ((raw-image (make-pathname '() image-base-name ".raw")) + (qcow-image (make-pathname '() image-base-name ".qcow2"))) + (check-privileges) + (check-users) + (prepare-image raw-image size-in-gb) + + (call-with-devmapped-image + raw-image + (lambda (dev) + (prepare-filesystem dev) + + (call-with-mounted-device + dev + (lambda (mountpoint) + (install-basic-system mountpoint minimal-packages) + (configure-basic-system mountpoint) + + (with-running-system + mountpoint + (lambda () + (update-packages mountpoint) + (make-bootable mountpoint) + + (setup-firewall mountpoint) + + (create-users mountpoint))))) + + (finalize-filesystem dev))) + + (convert-to-qcow2 raw-image qcow-image) + (printf "Success! Images finished, raw: ~A, qcow2: ~A\n" + raw-image qcow-image))) + +;; Just do it! +(handle-exceptions exn (signal exn) ; re-throw, to unwind dynamic extents + (build-image "debian-jessie" 20)) +) -- cgit v1.2.3