pkvlm-create
author Tero Marttila <terom@paivola.fi>
Mon, 13 Aug 2012 10:36:55 +0300
changeset 37 64c068ab02ac
parent 34 34f0edd3aab7
child 38 7b331b54d376
permissions -rwxr-xr-x
debian 6.0.5 + hotplug acpihp
#!/bin/bash

### Initialize 
set -u
set -e

TESTING=

DO_SHOWSPEC=y
DO_VIRTINSTALL=

lib=$(dirname $0)/lib
. $lib/lib.sh

### Command-line input
## Command-line options
function _help () {
    cat <<END
Usage: $1 [options] <name> [param=[value] [...]]

Options:
    -h      Show this help text

    -D      Debug
    -v      Verbose
    -q      Quiet

    -N      Mock command execution
    -P      Skip command prompts

    -I      Do a virt-install

    -T      Just testing..

Parameters are given as:

    name=value-{FOO}

END
}

while getopts "hDvqNPTI" opt; do
    case "$opt" in
        h)
            _help $0

            exit 0
            ;;

        D)
            LOG_DEBUG=y
            LOG_CMD=y

            ;;

        v)
            LOG_CMD=y

            log_debug "log: Commands"
            ;;

        q)
            LOG_CMD=
            LOG_INFO=
            DO_SHOWSPEC=
            
            log_debug "log: Quiet"
            ;;

        N)
            log_info "Mock-executing commands (this will break functionality..)"

            CMD_MOCK=y
            ;;

        P)
            log_info "Skipping command confirmations"

            CMD_PROMPT=
            ;;

        T)
            log_info "Just testing.."

            TESTING=y
            ;;

        I)
            log_info "Do virt-install"

            DO_VIRTINSTALL=y
            ;;
        ?)
            _help $0

            exit 1
            ;;
    esac
done


# forget them
[ $OPTIND -gt 1 ] && shift $(( $OPTIND - 1 ))

## Command-line arguments
# Name
[ -z $1 ] && die "Machine name must be given as first argument"

opt_name="$1"; shift

# Defaults
function define_opt () {
    local name=$1
    local value=${2:-}

    log_debugf "%-20s = %s" $name "$value"
    eval "opt_${name}=${value}"
}

function resolve_name () {
    local name=$1
    local out=$(dig +short $name)

    [ -z "$out" ] && die "Hostname lookup failed: $name"

    echo $out
}

define_opt  ram         1G
define_opt  cpus        2
define_opt  os          debiansqueeze
define_opt  disk_size   10G
define_opt  disk_vg     pvl
define_opt  disk_lv     $opt_name
define_opt  disk_bus    virtio
define_opt  guest_disk  /dev/vda
define_opt  hostname    $opt_name
define_opt  bridge      br-lan
define_opt  domain      paivola.fi
define_opt  ip          
define_opt  user        $USER
define_opt  puppet      
define_opt  puppet_master   puppet
define_opt  serial_console  yes

log_info "Processing ${#@} parameters:"
for param in "$@"; do
    name=${param%=*}
    value=${param##*=}

    # evaluate
    value=$(expand_line "$value")

    define_opt ${name} "${value}"
done

# resolve defaults
if [ -z $opt_ip ]; then
    define_opt  ip          $(resolve_name ${opt_name}.${opt_domain})
fi

### Virtual machine config
## General
NAME=$opt_name
DOMAIN=$opt_domain
FQDN=${NAME}.${DOMAIN}

## libvirt guest info
# Name
GUEST_NAME=$NAME

# Basic params
GUEST_RAM=$opt_ram
GUEST_VCPUS=$opt_cpus

# OS variant (for virt-install)
GUEST_OS_VARIANT=$opt_os

# I/O
GUEST_DISK_BUS=$opt_disk_bus

## Disk
# Size of LV to create
DISK_SIZE=$opt_disk_size

# LVM vg to use
DISK_VG=$opt_disk_vg

# LVM lv to use
DISK_NAME=$opt_disk_lv

# Path to disk block device
DISK_PATH=/dev/mapper/${DISK_VG}-${DISK_NAME}

## Serial
# Serial console?
# XXX: hardcoded as ttyS0
case x"$opt_serial_console" in
#    xtty*)  SERIAL_CONSOLE="$opt_serial_console" ;;
    x)       SERIAL_CONSOLE=     ;;
    x*)      SERIAL_CONSOLE="ttyS0" ;;
esac

### Preseed content
## Network
# Network configuration, for /etc/network/interfaces
NET_DOMAIN=${DOMAIN}
NET_HOSTNAME=${NAME}
NET_BRIDGE=$opt_bridge
NET_IPADDR=$opt_ip
NET_NETMASK=255.255.255.0
NET_GATEWAY=194.197.235.1
NET_NAMESERVERS=( 194.197.235.210 194.197.235.252 )

## Clock/time
TIME_ZONE='Europe/Helsinki'

# only used during install, not stored in target
TIME_NTP_SERVER=ntp.paivola.fi                      # XXX: harcoded

## User account
function getent_user_attr () {
    local user=$1
    local db=$2
    local attr=$3

    line=$(getent $db $user) || die "Unable to read $db database for $user"
    
    echo "$line" | cut -d ':' -f $attr
}
function user_fullname () {
    local user=$1

    getent_user_attr $user passwd 5
}
function user_shadow () {
    local user=$1

    if [ $UID -eq 0 ]; then
        log_debug "Get user password from shadow: $user"
        getent_user_attr $user shadow 2
    else
        echo -n "Install target login ($USER_NAME) " >&2
        mkpasswd -m sha-512
    fi
}

# per opt
case x"$opt_user" in
    x|xroot) 
        log_info "user: Skipping user account creation"

        USER_CREATE='false'
        USER_NAME=
        USER_FULLNAME=
        USER_SHADOW=
        
        ;;

    x*)
        log_info "user: With username=${opt_user}"

        USER_CREATE='true'
        USER_NAME=$opt_user
        USER_FULLNAME=$(user_fullname $opt_user)
        USER_SHADOW=$(user_shadow $opt_user)

        ;;
esac
        
USER_GROUPS=( cdrom sudo adm )

### Paths
## Installation image
# Original Debian Installer image (iso)
BASE_INSTALLER_VERSION="6.0.5"
BASE_INSTALLER_NAME="debian-${BASE_INSTALLER_VERSION}-amd64"
BASE_INSTALLER_ISO="iso/${BASE_INSTALLER_NAME}-netinst.iso"
BASE_INSTALLER_TREE="images/$BASE_INSTALLER_NAME"
BASE_INSTALLER_FLAG="${BASE_INSTALLER_TREE}.unpacked"


## preseed config templates
# Source files for preseeding
PRESEED_DIR="preseed"
PRESEED_NAME="preseed.cfg"
PRESEED_FILE="${PRESEED_DIR}/${PRESEED_NAME}"

# Source for isolinux bootloader config
PRESEED_ISOLINUX_FILE="${PRESEED_DIR}/isolinux.cfg"


## Installer tree
# Customized preseed image name
INSTALL_NAME="debian-${BASE_INSTALLER_VERSION}-amd64_${NAME}"

# Customized image content
INSTALL_TREE="images/${INSTALL_NAME}"
INSTALL_ISO="iso/${INSTALL_NAME}.iso"

# Preseed data in installer
INSTALL_PRESEED_DIR_NAME="preseed"
INSTALL_PRESEED_DIR="${INSTALL_TREE}/${INSTALL_PRESEED_DIR_NAME}"
INSTALL_PRESEED_FILE_NAME="${NAME}.cfg"
INSTALL_PRESEED_FILE="${INSTALL_PRESEED_DIR}/${INSTALL_PRESEED_FILE_NAME}"

# Checksum of target preseed.cfg
INSTALL_PRESEED_CHECKSUM= # set later

# isolinux.cfg for bootloader
INSTALL_ISOLINUX_FILE="${INSTALL_TREE}/isolinux/isolinux.cfg"

## Paths within installer runtime
# Paths to preseed files from within installer runtime
INSTALLER_MOUNT="/cdrom"
INSTALLER_PRESEED_DIR="${INSTALLER_MOUNT}/${INSTALL_PRESEED_DIR_NAME}"
INSTALLER_PRESEED_FILE="${INSTALLER_PRESEED_DIR}/${INSTALL_PRESEED_FILE_NAME}"


## Stage dir, for generating files for INSTALL_PRESEED
STAGE_ROOT="stage"
STAGE_DIR="${STAGE_ROOT}/${NAME}"

log_info "Stage: prepare stage dir"

# root
[ ! -d ${STAGE_ROOT} ] && cmd mkdir ${STAGE_ROOT}

# host-specific; clean
[ -d ${STAGE_DIR} ] && cmd rm -r ${STAGE_DIR}
cmd mkdir ${STAGE_DIR}

function stage_file () {
    local src=$1
    local name=$2

    local tgt=${STAGE_DIR}/$name

    log_debug "stage: file: $name"
    cmd cp $src $tgt
}

### preseed.cfg contents

## Disk
# Disk to install to
PARTMAN_DISK=$opt_guest_disk

# regular/lvm/crypto
PARTMAN_METHOD=regular

# Recipe to use
PARTMAN_RECIPE='Trvial'

# recipe file
PARTMAN_RECIPE_FILE_NAME="partman.recipe"
PRESEED_PARTMAN_RECIPE_FILE="${PRESEED_DIR}/${PARTMAN_RECIPE_FILE_NAME}"
INSTALL_PARTMAN_RECIPE_FILE_NAME="${PARTMAN_RECIPE_FILE_NAME}"
INSTALLER_PARTMAN_RECIPE_FILE="${INSTALLER_PRESEED_DIR}/${PARTMAN_RECIPE_FILE_NAME}"

# deploy
stage_file ${PRESEED_PARTMAN_RECIPE_FILE} ${INSTALL_PARTMAN_RECIPE_FILE_NAME}

# path for d-i to find it
PARTMAN_RECIPE_FILE="${INSTALLER_PARTMAN_RECIPE_FILE}"

## Misc
# List of additional packages to install
PRESEED_PACKAGES=( sudo screen vim )

# Script commands to execute
PRESEED_LATE_COMMANDS=( )

# Chainload preseed files
PRESEED_INCLUDES=( 'passwords.cfg' 'host.cfg' )

# Add packages to preseed install
function preseed_packages () {
    PRESEED_PACKAGES=( ${PRESEED_PACKAGES[@]} "$@" )
}

# Add command to execute
function preseed_late_commands () {
    PRESEED_LATE_COMMANDS=( "${PRESEED_LATE_COMMANDS[@]:-}" "$@" )
}

## preseed-files
# staged files
# name must match with INSTALL_PRESEED_FILES_DIR
STAGE_PRESEED_FILES_DIR="${STAGE_DIR}/files"

log_info "Stage: prepare preseed-files"
cmd mkdir "${STAGE_PRESEED_FILES_DIR}"

# template source trees
PRESEED_FILES_SOURCES=( "${PRESEED_DIR}/files" )

# template output into install tree
INSTALL_PRESEED_FILES_DIR_NAME="${INSTALL_PRESEED_DIR_NAME}/files"
INSTALL_PRESEED_FILES_DIR="${INSTALL_TREE}/${INSTALL_PRESEED_FILES_DIR_NAME}"

# paths within installer runtime
INSTALLER_PRESEED_FILES_DIR="${INSTALLER_MOUNT}/${INSTALL_PRESEED_FILES_DIR_NAME}"
INSTALLER_PRESEED_FILES_TARGET='/target'

# Add a tree of configuration files to template into the installer
function preseed_files_sources () {
    PRESEED_FILES_SOURCES=( "${PRESEED_FILES_SOURCES[@]:-}" "$@" )
}

# Add a file to install in preseed, without templating
# preseed_file <src> <dst>/
#   if dst is a dir, it must end in /
function preseed_file () {
    local src=$1
    local dst=$2
    local dir=$(dirname $dst)
    local tgt=${STAGE_PRESEED_FILES_DIR}

    local tgt_dir="$tgt/$dir"

    if [ ! -d "$tgt_dir" ]; then
        cmd mkdir -p "$tgt_dir"
    fi

    cmd cp "$src" "$tgt/$dst"
}

## preseed-scripts
STAGE_PRESEED_SCRIPTS_DIR="${STAGE_DIR}/scripts"

cmd mkdir "${STAGE_PRESEED_SCRIPTS_DIR}"

# output into install tree
INSTALL_PRESEED_SCRIPTS_DIR_NAME="${INSTALL_PRESEED_DIR_NAME}/scripts"
INSTALL_PRESEED_SCRIPTS_DIR="${INSTALL_TREE}/${INSTALL_PRESEED_SCRIPTS_DIR_NAME}"

# paths within installer runtime
INSTALLER_PRESEED_SCRIPTS_DIR="${INSTALLER_MOUNT}/${INSTALL_PRESEED_SCRIPTS_DIR_NAME}"


# add a preseed script to execute, from stdin
function preseed_script () {
    local name=$1
    local path="${STAGE_PRESEED_SCRIPTS_DIR}/$name"
    
    log_debug "preseed script: $name"

    # header
    echo '#!/bin/sh' > $path

    # from stdin
    cat >> $path

    chmod +x $path
}

### Extra
## Puppet
if [ $opt_puppet ]; then
    log_info "Puppetizing preseed"
    
    PUPPET_PRESEED_DIR="${PRESEED_DIR}/puppet"

    ## Packages
    preseed_packages puppet

    ## Vars for preseed-files
    # hostname for puppetmaster (server)
    PUPPET_MASTER="${opt_puppet_master}"

    # path ssl data (ssldir)
    PUPPET_SSLDIR=/etc/puppet/ssl
   
    ## Preseed files 
    # add to list of conf files to copy
    preseed_files_sources "${PUPPET_PRESEED_DIR}/files"

    ## Preseed ssl certs?
    PUPPET_SOURCE_SSLDIR="${PUPPET_PRESEED_DIR}/ssl"

    # copy file to preseed if exists
    function puppet_preseed_ssl_file () {
        local name=$1

        local src=${PUPPET_SOURCE_SSLDIR}/$name
        local dst=${PUPPET_SSLDIR}/$name

        if [ -f $src ]; then
            log_info "puppet: preseed ssl data: $name"

            cmd preseed_file $src $dst
        else
            log_debug "puppet: skip ssl preseed: $name"
        fi
    }

    # ca.pem
    puppet_preseed_ssl_file certs/ca.pem

    # guest cert/pkey
    puppet_preseed_ssl_file certs/${FQDN}.pem
    puppet_preseed_ssl_file private_keys/${FQDN}.pem
fi

## Configure GRUB, via preseed/files: /etc/default/grub 
# Kernel commandline/grub terminal
# The last console=... for kernel is used as /dev/console, i.e. init output
log_info "Configure grub / serial console"

if [ $SERIAL_CONSOLE ]; then
    BOOT_KERNEL_CONSOLE="console=${SERIAL_CONSOLE} console=tty0"
    BOOT_GRUB_TERMINAL="console serial"

    # Configure /etc/inittab for serial console
    preseed_script '11-serial-console' <<END
        in-target sed -i 's/#T0/T0/' /etc/inittab
END

else
    BOOT_KERNEL_CONSOLE=""
    BOOT_GRUB_TERMINAL="console"
fi

# re-generate grub.cfg
preseed_script '10-boot-grub' <<END
    in-target update-grub
END

# Kernel boot args (overrides those generated by installer) - default boot option uses args + args_default
BOOT_KERNEL_ARGS_DEFAULT="quiet"
BOOT_KERNEL_ARGS="${BOOT_KERNEL_CONSOLE}"

## Hotplug modules
log_info "Configure acpi/pci hotplug"

modules=/etc/modules
module='acpihp'

preseed_script '20-hotplug-modules' <<END
    in-target grep -q $module $modules || echo "
# KVM virtio hotplug
$module" >> $modules
END

### Postprocess preseed data
## Preseed / config files

# copy at end of install
preseed_late_commands "cp -rd -- ${INSTALLER_PRESEED_FILES_DIR}/* ${INSTALLER_PRESEED_FILES_TARGET}"

## Preseed / scripts
preseed_late_commands "for script in ${INSTALLER_PRESEED_SCRIPTS_DIR}/*; do \$script; done"

## preseed.cfg

# Additional files to copy
PRESEED_INCLUDE_FILES=( $(for preseed in ${PRESEED_INCLUDES[@]}; do echo "${PRESEED_DIR}/${preseed}"; done) )

# preseed.cfg 'includes' line
PRESEED_INCLUDE=${PRESEED_INCLUDES[@]}

# preseed command execution
PRESEED_LATE_COMMAND=$(for cmd in "${PRESEED_LATE_COMMANDS[@]}"; do if [ "$cmd" ]; then echo -n "$cmd;" $'\\\n    '; fi; done; echo true)


## Isolinux
# Automagics for serial boot :)
#      virt-install assumes we have an X $DISPLAY if given --vnc
#      but we also want to enable the serial console...
#      d-i automagically configures the serial boot/login console, but only if the installer is run under serial...
#      we want to have both :(
if [ ${DISPLAY:-} ]; then
    INSTALLER_BOOT_CONSOLE=""
else
    INSTALLER_BOOT_CONSOLE="console=ttyS0"
fi

# isolinux installer boot args
INSTALLER_BOOT_ARGS="auto=true priority=critical preseed/file=${INSTALLER_PRESEED_FILE} preseed/file/checksum=${INSTALL_PRESEED_CHECKSUM} -- ${INSTALLER_BOOT_CONSOLE} quiet"

### External progs
## Bootable .iso for Debian isolinux-based installer CDs
GENISOIMAGE=/usr/bin/genisoimage
GENISOIMAGE_OPTS="-r -J -no-emul-boot -boot-load-size 4 -boot-info-table -b isolinux/isolinux.bin -c isolinux/boot.cat"

## LVM
LVM=/sbin/lvm

## Libvirt 
# --connect URL
LIBVIRT=qemu:///system
VIRSH=/usr/bin/virsh

function virsh () {
    $VIRSH --connect $LIBVIRT "$@"
}

# type of guest to create
LIBVIRT_TYPE=kvm

## virt-install
VIRT_INSTALL="/usr/bin/virt-install"

## SELinux?
#SEMANAGE=/usr/sbin/semanage
#RESTORECON=/sbin/restorecon



### Prepare
if [ $DO_SHOWSPEC ]; then
    cat <<END
Guest:
    Name:       $GUEST_NAME
    CPUs:       $GUEST_VCPUS
    RAM:        $GUEST_RAM
    OS:         $GUEST_OS_VARIANT

Boot:
    Serial console: $SERIAL_CONSOLE
    Grub terminal:  $BOOT_GRUB_TERMINAL
    Kernel args:    $BOOT_KERNEL_ARGS ($BOOT_KERNEL_ARGS_DEFAULT)

Disk:
    Method:     LVM
    Size:       $DISK_SIZE
    LVM:
        VG:     $DISK_VG
        LV:     $DISK_NAME

    Path:       $DISK_PATH
    Bus:        $GUEST_DISK_BUS
    
    Name:       $PARTMAN_DISK

Net:
    Hostname:   $NET_HOSTNAME
    Method:     Bridge
    IP:         $NET_IPADDR

    Bridge:
        Name:   $NET_BRIDGE

User:
    username:   $USER_NAME
    fullname:   $USER_FULLNAME
    shadow:     $USER_SHADOW

Installer:
    Name:       $BASE_INSTALLER_NAME
    ISO:        $BASE_INSTALLER_ISO
    Tree:       $BASE_INSTALLER_TREE

Install:
    Name:       $INSTALL_NAME
    Tree:       $INSTALL_TREE
    ISO:        $INSTALL_ISO

Preseed:
    Template:   $PRESEED_FILE
    Includes:   $PRESEED_INCLUDES

END
fi

[ $TESTING ] && exit 0

### Check
# Parameters given?
[ -z $NET_IPADDR ] && die "net: No IP-address given: ip"

# Installer exists?
if [ ! -f ${BASE_INSTALLER_ISO} ]; then
    die "Base installer image not found: ${BASE_INSTALLER_ISO}"
fi




### Prepare install


## Extract .iso
if [ -f ${BASE_INSTALLER_FLAG} -a -d ${BASE_INSTALLER_TREE} ]; then
    log_info "Installer already unpacked: ${BASE_INSTALLER_TREE}"

else
    log_info "Unpacking installer: ${BASE_INSTALLER_ISO}"
    cmd extract_iso ${BASE_INSTALLER_ISO} ${BASE_INSTALLER_TREE}
    cmd touch ${BASE_INSTALLER_FLAG}
fi



## Copy to customized tree
log_info "Prepare customized install tree..."

[ -d ${INSTALL_TREE} ] && cmd rm -r ${INSTALL_TREE}
cmd cp -rd ${BASE_INSTALLER_TREE} ${INSTALL_TREE}
cmd chmod -R u=rwX,og=rX ${INSTALL_TREE}



### Customize preseed
log_info "Generate preseed..."

## Prepare
cmd mkdir ${INSTALL_PRESEED_DIR}

## Expand preseed.cfg
cmd expand_file ${PRESEED_FILE} ${INSTALL_PRESEED_FILE}

# md5sum
INSTALL_PRESEED_CHECKSUM=$(my_md5sum $INSTALL_PRESEED_FILE)


## Expand includes
for file in ${PRESEED_INCLUDE_FILES[@]}; do
    name=$(basename $file)

    cmd expand_file $file ${INSTALL_PRESEED_DIR}/${name}
done

## Expand isolinux
cmd expand_file ${PRESEED_ISOLINUX_FILE} ${INSTALL_ISOLINUX_FILE}

log_info "Preseed generated: $INSTALL_PRESEED_DIR"


## Expand preseed-files
log_info "Copy preseed-files..."

for tree in "${PRESEED_FILES_SOURCES[@]}"; do
    log_info "preseed-files: $tree"

    cmd expand_tree $tree ${INSTALL_PRESEED_FILES_DIR}
done

# debug?
[ $LOG_DEBUG ] && tree ${INSTALL_PRESEED_FILES_DIR}


## Copy preseed scripts
log_info "Copy preseed-scripts..."


## Staged files
log_info "Copy staged preseed files..."

# STAGE_PRESEED_*_DIR must be the same name as INSTALL_PRESEED_*_DIR
cmd cp -rv ${STAGE_DIR}/* ${INSTALL_PRESEED_DIR}/



### Create .iso
log_info "Generate ISO..."

[ -f ${INSTALL_ISO} ] && cmd rm -f ${INSTALL_ISO}

# generates a lot of output
cmd ${GENISOIMAGE} -o ${INSTALL_ISO} -quiet ${GENISOIMAGE_OPTS} ${INSTALL_TREE} 

log_info "Install ISO generated: $INSTALL_ISO"





### Create virtual machine
if [ $DO_VIRTINSTALL ]; then
    log_info "Create VM..."

    ## Check
    # VM exists?
    if cmd virsh domid ${GUEST_NAME} > /dev/null 2> /dev/null; then
        die "Virtual machine already exists: ${GUEST_NAME}"
    fi

    ## Disk
    # Create LV (unless it already exists)
    [ -e $DISK_PATH ] || cmd_sudo $LVM lvcreate -L $DISK_SIZE -n $DISK_NAME $DISK_VG

    ## SELinux?
    #$SEMANAGE fcontext -a -t virt_image_t $DISK
    #$RESTORECON -v $DISK

    ## virt-install
    cmd_confirm $VIRT_INSTALL \
            --connect $LIBVIRT \
            --name $GUEST_NAME \
            --ram $(expand_MB $GUEST_RAM) --vcpus $GUEST_VCPUS \
            --cdrom "$INSTALL_ISO" \
            --os-variant $GUEST_OS_VARIANT \
            --disk path=$DISK_PATH,bus=$GUEST_DISK_BUS \
            --network bridge:$NET_BRIDGE \
            --virt-type $LIBVIRT_TYPE \
            --vnc \
            --serial pty
fi