I wanted a clean VM for compiling some software. Since the host computer is headless, and I rarely run a desktop on it, I wanted the build and VM to be purely console-based, accessed over SSH.

Obtain installer ISO

Download the "Current weekly snapshot" CD ISO image linked from here: https://www.debian.org/releases/testing/

I usually rename these images to have the date I downloaded it, in the filename, so I called it debian-testing-amd64-netinst.2016-05-04.iso and stuck it in my ~/ISO directory.

Setup

Make working directory, and create an empty 8GB HDD image to install into.

user@vmhost:~$ mkdir VM/debian_testing
user@vmhost:~$ cd VM/debian_testing
user@vmhost:~/VM/debian_testing$
user@vmhost:~/VM/debian_testing$ qemu-img create -f qcow2 sda_8G.qcow2 8G
Formatting 'sda_8G.qcow2', fmt=qcow2 size=8589934592 encryption=off cluster_size=65536 lazy_refcounts=off refcount_bits=16

Get the kernel and initrd

Mount the CD, find out where the kernel and initrd are, and copy them out.

user@vmhost:~/VM/debian_testing$ sudo mount ~/ISO/debian-testing-amd64-netinst.2016-05-04.iso mnt
mount: /dev/loop2 is write-protected, mounting read-only

user@vmhost:~/VM/debian_testing$ grep Expert -A4 mnt/boot/grub/grub.cfg
    menuentry '... Expert install' {
        set background_color=black
        linux    /install.amd/vmlinuz priority=low vga=788 ---
        initrd   /install.amd/initrd.gz
    }

user@vmhost:~/VM/debian_testing$ cp mnt/install.amd/vmlinuz vmlinuz_install
'mnt/install.amd/vmlinuz' -> 'vmlinuz_install'

user@vmhost:~/VM/debian_testing$ cp mnt/install.amd/initrd.gz initrd_install.gz
'mnt/install.amd/initrd.gz' -> 'initrd_install.gz'

user@vmhost:~/VM/debian_testing$ sudo umount mnt

Setup script

These are the files I have, and the content of my initial boot script:

user@vmhost:~/VM/debian_testing$ ls
go_cd_boot.sh  initrd_install.gz  sda_8G.qcow2  vmlinuz_install

user@vmhost:~/VM/debian_testing$ cat go_cd_boot.sh
#! /bin/sh

qemu-system-x86_64   \
   -kernel vmlinuz_install   \
   -initrd initrd_install.gz \
   -append "priority=low console=ttyS0" \
   -hda sda_8G.qcow2 \
   -cdrom /home/user/ISO/debian-testing-amd64-netinst.2016-05-04.iso  \
   -net nic    \
   -net user   \
   -m   1024   \
   -enable-kvm \
   -nographic

Start it up!

user@vmhost:~/VM/debian_testing$ ./go_cd_boot.sh

Install Debian

I chose things which suited me - mostly just defaults. When it asked for tasks, I deselected everything but the ssh server task.

Note, you can find out what the tasks consist of like this (run on a Debian system - this is somewhat chicken/egg if you haven't yet got a Debian system built):

user@vmhost:~/ISO$ sudo tasksel --list-tasks
i desktop       Debian desktop environment
u gnome-desktop GNOME
u xfce-desktop  Xfce
u kde-desktop   KDE
u cinnamon-desktop      Cinnamon
u mate-desktop  MATE
i lxde-desktop  LXDE
u web-server    web server
u print-server  print server
i ssh-server    SSH server
u laptop        laptop
user@vmhost:~/ISO$ sudo tasksel --task-packages ssh-server
task-ssh-server
user@vmhost:~/ISO$ sudo aptitude show task-ssh-server
Package: task-ssh-server
State: installed
Automatically installed: no
Version: 3.34
Priority: optional
Section: tasks
Maintainer: Debian Install System Team <debian-boot@lists.debian.org>
Architecture: all
Uncompressed Size: 6,144
Depends: tasksel (= 3.34), openssh-server
Recommends: openssh-client
Description: SSH server
 This task sets up your system to be remotely accessed through SSH connections.

Also see http://www.csmojo.com/2015/07/what-debians-standard-system-utilities.html for info on the "standard system utilities" task.

Finish install

Finish the installation process, then shut down the VM. Actually, I let it reboot into the installer, then killed qemu - not exactly elegant. You should be able to use the qemu monitor to unmount the CD image, then reboot into the installed system - if you're doing this then you'll need to edit the boot options when the grub menu pops up.

First real boot

This is my boot script:

user@vmhost:~/VM/debian_testing$ cat go.sh
#! /bin/sh

qemu-system-x86_64   \
   -hda sda_8G.qcow2 \
   -net nic    \
   -net user   \
   -m   1024   \
   -enable-kvm \
   -nographic

Start it up and be ready to press an arrow key...

user@vmhost:~/VM/debian_testing$ ./go.sh

Wait for this screen to appear, then press cursor-down (fairly soon after the screen appears) to let it know that you want to do something. If you wait too long, it'll start booting, and won't be set for serial console, so you won't see what you need to see - in which case you probably need to kill qemu (or maybe use the qemu monitor to shut it down).

                        GNU GRUB  version 2.02~beta2-36

 +----------------------------------------------------------------------------+
 |*Debian GNU/Linux                                                           |
 | Advanced options for Debian GNU/Linux                                      |
 |                                                                            |
 |                                                                            |
 |                                                                            |
 |                                                                            |
 |                                                                            |
 |                                                                            |
 |                                                                            |
 |                                                                            |
 |                                                                            |
 |                                                                            |
 +----------------------------------------------------------------------------+

      Use the ^ and v keys to select which entry is highlighted.
      Press enter to boot the selected OS, `e' to edit the commands
      before booting or `c' for a command-line.

Move the cursor (the asterisk "*") to the first line and press e to edit it. Scroll down to the linux line

                        GNU GRUB  version 2.02~beta2-36

 +----------------------------------------------------------------------------+
 |          search --no-floppy --fs-uuid --set=root --hint-bios=hd0,msdos1 --\|^
 |hint-efi=hd0,msdos1 --hint-baremetal=ahci0,msdos1  3bd74887-1631-43fd-ada5-\|
 |8e00892b5dd9                                                                |
 |        else                                                                |
 |          search --no-floppy --fs-uuid --set=root 3bd74887-1631-43fd-ada5-8\|
 |e00892b5dd9                                                                 |
 |        fi                                                                  |
 |        echo        'Loading Linux 4.5.0-1-amd64 ...'                       |
 |        linux        /boot/vmlinuz-4.5.0-1-amd64 root=/dev/sda1 ro  quiet   |
 |        echo        'Loading initial ramdisk ...'                           |
 |        initrd        /boot/initrd.img-4.5.0-1-amd64                        |
 |                                                                            |
 +----------------------------------------------------------------------------+

      Minimum Emacs-like screen editing is supported. TAB lists
      completions. Press Ctrl-x or F10 to boot, Ctrl-c or F2 for
      a command-line or ESC to discard edits and return to the GRUB menu.

Delete that annoying quiet option (quiet is for people who are afraid to know what their computer is doing) and replace it with console=ttyS0,115200

                        GNU GRUB  version 2.02~beta2-36

 +----------------------------------------------------------------------------+
 |hint-efi=hd0,msdos1 --hint-baremetal=ahci0,msdos1  3bd74887-1631-43fd-ada5-\|^
 |8e00892b5dd9                                                                |
 |        else                                                                |
 |          search --no-floppy --fs-uuid --set=root 3bd74887-1631-43fd-ada5-8\|
 |e00892b5dd9                                                                 |
 |        fi                                                                  |
 |        echo        'Loading Linux 4.5.0-1-amd64 ...'                       |
 |        linux        /boot/vmlinuz-4.5.0-1-amd64 root=/dev/sda1 ro console=\|
 |ttyS0,115200                                                                |
 |        echo        'Loading initial ramdisk ...'                           |
 |        initrd        /boot/initrd.img-4.5.0-1-amd64                        |
 |                                                                            |
 +----------------------------------------------------------------------------+

      Minimum Emacs-like screen editing is supported. TAB lists
      completions. Press Ctrl-x or F10 to boot, Ctrl-c or F2 for
      a command-line or ESC to discard edits and return to the GRUB menu.

Press Ctrl-X and wait for it to boot

  Booting a command list

Loading Linux 4.5.0-1-amd64 ...
Loading initial ramdisk ...
[    0.000000] Linux version 4.5.0-1-amd64 (debian-kernel@lists.debian.org) (gcc version 5.3.1 20160409 (Debian 5.3.1-14) ) #1 SMP Debian 4.5.1-1 (2016-04-14)
[    0.000000] Command line: BOOT_IMAGE=/boot/vmlinuz-4.5.0-1-amd64 root=/dev/sda1 ro console=ttyS0,115200
[    0.000000] x86/fpu: Legacy x87 FPU detected.
...SNIP...
[  OK  ] Reached target Graphical Interface.
         Starting Update UTMP about System Runlevel Changes...
[  OK  ] Started Update UTMP about System Runlevel Changes.

Debian GNU/Linux stretch/sid testing ttyS0

testing login:

Make console boot permanent

Login using whatever credentials you setup for the root user. Use vi to edit grub options. The listing here shows which things I changed (I usually also set the timeout to be 1s):

root@testing:~# vi /etc/default/grub
root@testing:~# grep -A2 KMW /etc/default/grub
# KMW 2016-05-04 - change to serial console and get rid of stupid quiet
#GRUB_CMDLINE_LINUX_DEFAULT="quiet"
GRUB_CMDLINE_LINUX_DEFAULT="console=ttyS0,115200"
--
# KMW 2016-05-04 - change to serial console
#GRUB_TERMINAL=console
GRUB_TERMINAL=console
--
# KMW 2016-05-04 - increase speed, in case it makes a difference
#GRUB_SERIAL_COMMAND="serial --unit=0 --speed=9600 --stop=1"
GRUB_SERIAL_COMMAND="serial --unit=0 --speed=115200 --stop=1"

Run update-grub to update the actual files read at boot time. I saw a few error messages, but they appear not to be relevant.

root@testing:~# update-grub
[  278.405923] device-mapper: uevent: version 1.0.3
[  278.410901] device-mapper: ioctl: 4.34.0-ioctl (2015-10-28) initialised: dm-devel@redhat.com
Generating grub configuration file ...
Found linux image: /boot/vmlinuz-4.5.0-1-amd64
Found initrd image: /boot/initrd.img-4.5.0-1-amd64
[  279.213528] SGI XFS with ACLs, security attributes, realtime, no debug enabled
[  279.232881] JFS: nTxBlock = 7971, nTxLock = 63774
[  279.271794] ntfs: driver 2.1.32 [Flags: R/O MODULE].
[  279.314018] QNX4 filesystem 0.2.3 registered.
[  279.404671] raid6: sse2x1   gen()  1133 MB/s
[  279.472672] raid6: sse2x1   xor()  2203 MB/s
[  279.540708] raid6: sse2x2   gen()  1027 MB/s
[  279.608673] raid6: sse2x2   xor()  2551 MB/s
[  279.676667] raid6: sse2x4   gen()  1633 MB/s
[  279.744675] raid6: sse2x4   xor()  1805 MB/s
[  279.747849] raid6: using algorithm sse2x4 gen() 1633 MB/s
[  279.751961] raid6: .... xor() 1805 MB/s, rmw enabled
[  279.756338] raid6: using intx1 recovery algorithm
[  279.764205] xor: measuring software checksum speed
[  279.804668]    prefetch64-sse:  6775.000 MB/sec
[  279.844670]    generic_sse:  6120.000 MB/sec
[  279.847586] xor: using function: prefetch64-sse (6775.000 MB/sec)
[  279.884725] Btrfs loaded
[  279.902527] fuse init (API version 7.24)
[  280.004351] EXT4-fs (sda2): unable to read superblock
[  280.010945] EXT4-fs (sda2): unable to read superblock
[  280.017672] EXT4-fs (sda2): unable to read superblock
[  280.025434] XFS (sda2): Invalid superblock magic number
[  280.039873] FAT-fs (sda2): utf8 is not a recommended IO charset for FAT filesystems, filesystem will be case sensitive!
[  280.049289] FAT-fs (sda2): bogus number of reserved sectors
[  280.053966] FAT-fs (sda2): Can't find a valid FAT filesystem
[  280.062174] FAT-fs (sda2): utf8 is not a recommended IO charset for FAT filesystems, filesystem will be case sensitive!
[  280.074227] FAT-fs (sda2): bogus number of reserved sectors
[  280.079580] FAT-fs (sda2): Can't find a valid FAT filesystem
[  280.092136] ntfs: (device sda2): read_ntfs_boot_sector(): Primary boot sector is invalid.
[  280.100723] ntfs: (device sda2): read_ntfs_boot_sector(): Mount option errors=recover not used. Aborting without trying to recover.
[  280.108948] ntfs: (device sda2): ntfs_fill_super(): Not an NTFS volume.
[  280.116124] MINIX-fs: unable to read superblock
[  280.123983] attempt to access beyond end of device
[  280.128096] sda2: rw=16, want=3, limit=2
[  280.132866] hfsplus: unable to find HFS+ superblock
[  280.141723] qnx4: no qnx4 filesystem (no root dir).
[  280.150349] ufs: You didn't specify the type of your ufs filesystem
[  280.150349]
[  280.150349] mount -t ufs -o ufstype=sun|sunx86|44bsd|ufs2|5xbsd|old|hp|nextstep|nextstep-cd|openstep ...
[  280.150349]
[  280.150349] >>>WARNING<<< Wrong ufstype may corrupt your filesystem, default is ufstype=old
[  280.173616] hfs: can't find a HFS filesystem on dev sda2
done

Optionally check that we now have console options configured:

root@testing:~# grep console /boot/grub/grub.cfg
        linux   /boot/vmlinuz-4.5.0-1-amd64 root=UUID=3bd74887-1631-43fd-ada5-8e00892b5dd9 ro  console=ttyS0,115200
                linux   /boot/vmlinuz-4.5.0-1-amd64 root=UUID=3bd74887-1631-43fd-ada5-8e00892b5dd9 ro  console=ttyS0,115200

Test

Poweroff the VM and start it again - it should boot without any need for fiddling.