Fast shutdown of development Vagrant/VirtualBox virtual machines

21 February 2013

vagrant, virtualbox


One great advantage of using Vagrant is that it allows to work with virtual machines (VMs) that provide cleanly separated, sandboxed environments. I find this so convenient that I systematically build individual VMs for every single Web project I work on. This setup definitely works great. However, since I frequently switch between several projects multiple times during the day, I often end up having multiple VMs running at the same time. This eventually causes a lot of RAM to be consumed on my laptop and it then becomes necessary to shut down some of the VMs that I don't need to use any more.

Vagrant already offers a pretty simple command to turn off a VM:

$ vagrant halt

With this command, Vagrant attempts a graceful shutdown of the VM (e.g. by issuing a halt in Linux). However, it sometimes happens that, when something gets screwed up within the VM's environment, this command simply freezes and never completes. Also, if you wish to turn off multiple VMs, you'll have to execute this command multiple times: once for each VM from within its corresponding directory on your host machine.

So, to make life a little bit easier, I wrote a simple Python script (full code provided in the gist below) that directly issues a poweroff command to VirtualBox. This command has the same effect on a VM as pulling off the power cable on a real computer. While I would obviously recommend against doing this in production, it is generally perfectly safe to do in a development environment. Besides, it makes the VM shut down really fast. By passing the --all parameter you may also run this script just once to instantly shut down all running VMs.

To make this script executable from anywhere, I recommend placing it inside your PATH (e.g. ~/bin/) and giving it the execution flag (e.g. chmod u+x poweroff.py). Here's an example running the script:

$ poweroff.py --all
2 VM(s) currently running...
Powering off VM: d8ec66a6-6455-416a-969b-be44fc094c91...
0%...10%...20%...30%...40%...50%...60%...70%...80%...90%...100%
Powering off VM: d3ce89aa-5700-4060-87cd-1e04b8c8cef2...
0%...10%...20%...30%...40%...50%...60%...70%...80%...90%...100%

Hopefully this will of some use to the Vagrant users out there. When Vagrant eventually (soon!) supports VMWare, this script could be easily updated to work with it as well.

#!/usr/bin/env python
import argparse
import re
import os
import subprocess

try:
    import json
except ImportError:
    import simplejson as json


def get_running_vms():
    """
    Returns the list of ids for the VMs currently running in VirtualBox.
    """
    output = subprocess.Popen(['VBoxManage', 'list', 'runningvms'], stdout=subprocess.PIPE).communicate()[0]
    vms = []
    if output is not None:
        lines = output.split('\n')
        for line in lines:
            pattern = re.compile(r'.*{(.*)}')
            match = pattern.match(line)
            if match:
                vms.append(match.group(1))
    return vms


def poweroff_vm(vm_id):
    """
    Issues a 'poweroff' command to VirtualBox for the given VM.
    """
    print "Powering off VM: %s..." % vm_id
    subprocess.call(['VBoxManage', 'controlvm', vm_id, 'poweroff'])


def find_machines_dir():
    """
    Walks up from the current path untils it finds a '.vagrant/machines/' directory.
    """
    path = os.getcwd()
    while path != '/':
        machines_dir = os.path.join(path, '.vagrant/machines/')
        if os.path.isdir(machines_dir):
            return machines_dir
        # Try one level up
        path = os.path.abspath(os.path.join(path, os.pardir))

def find_vm_ids(machines_dir):
    """
    Returns all the VM id files found under the given .vagrant/machines/ directory.
    """
    matches = []
    for root, dirnames, filenames in os.walk(machines_dir):
        for filename in filenames:
            if filename == 'id':
                matches.append(os.path.join(root, filename))
    return matches


# Parse the command's arguments
parser = argparse.ArgumentParser()
parser.add_argument('--all', action='store_true')
args = parser.parse_args()

running_vms = get_running_vms()

if args.all:
    if len(running_vms):
        print "%s VM(s) currently running..." % len(running_vms)
        for vm in running_vms:
            poweroff_vm(vm)
    else:
        print "No VMs are currently running."
else:
    machines_dir = find_machines_dir()
    if machines_dir:
        for vm_id in find_vm_ids(machines_dir):
            vm_id = open(vm_id).read()
            if vm_id in running_vms:
                poweroff_vm(vm_id)
            else:
                print "VM %s is already powered off." % vm_id
    else:
        print "Cannot find any '.vagrant/machines/' directory..."

Quick note: if you're using Python < 2.7 you will first need to install the argparse module:

$ pip install argparse

Update July 29th, 2013: Updated the script to work with Vagrant 1.1+

Update September 23th, 2013: vagrant halt --force can in fact be used for the same thing. However, this script also allows to shutdown all currently-running VMS with the --all option, which you may still find useful.