diff --git a/docs/source/library.rst b/docs/source/library.rst
--- a/docs/source/library.rst
+++ b/docs/source/library.rst
@@ -77,6 +77,12 @@
 .. automodule:: libtaskotron.runner
    :members:
 
+virtualmachine
+==============
+
+.. automodule:: libtaskotron.virtualmachine
+   :members:
+
 yumrepoinfo
 ===========
 
diff --git a/libtaskotron/runner.py b/libtaskotron/runner.py
--- a/libtaskotron/runner.py
+++ b/libtaskotron/runner.py
@@ -24,6 +24,7 @@
 from libtaskotron import config
 from libtaskotron import remote_exec
 from libtaskotron import rpm_utils
+from libtaskotron import virtualmachine
 from libtaskotron.logger import log
 import libtaskotron.exceptions as exc
 from libtaskotron.directives import exitcode_directive
@@ -113,14 +114,23 @@
 
         filelog_path = os.path.join(self.uuid_dir, 'taskotron-initiator.log')
         logger.add_filehandler(filelog_path=filelog_path, remove_mem_handler=True)
+
         stdio_filename = os.path.join(self.uuid_dir, 'taskotron-stdio.log')
 
         if self.direct_ssh:
             log.info("Running task over SSH on machine '%s'...", self.machine)
         else:
             log.info("Running task on a disposable client using libvirt...")
-            # FIXME: create disposable client here
-            raise exc.TaskotronError('libvirt execution not yet implemented')
+
+            log.info("Creating VM for task")
+            task_vm = virtualmachine.TestCloudMachine(self.arg_data['uuid'])
+            task_vm.prepare()
+            self.machine = task_vm.ipaddr
+            self.port = 22
+            self.user = task_vm.username
+            self.ssh_privkey = 'conf/id_taskotron'
+
+        log.info("Running Task over SSH on machine %s" % task_vm.instancename)
 
         with remote_exec.RemoteExec(self.machine, self.port, self.user,
                                     self.ssh_privkey, stdio_filename) as ssh:
@@ -130,8 +140,25 @@
                 task_runner.run()
             except exc.TaskotronRemoteError, e:
                 log.exception(e)
+
             task_runner.get_output()
 
+        if self.direct_ssh:
+            # don't try to run teardown in the direct ssh case
+            return
+
+        # only teardown the VM when not in debug mode
+        if self.arg_data['debug']:
+            log.info("debug mode was set from command line, not tearing VM "
+                     "down for task {}".format(self.arg_data['uuid']))
+        else:
+            try:
+                task_vm.teardown()
+            except exc.TaskotronRemoteError, e:
+                log.error("Hit problem during teardown of VM for "
+                          "task {}".format(self.arg_data['uuid']))
+                log.error(e)
+
 
 class LocalRunner(object):
     def __init__(self, taskdata, argdata, workdir=None):
@@ -173,7 +200,7 @@
 
         if not os.path.exists(directive_file):
             raise exc.TaskotronYamlError("Directive %s not found in directory %s" %
-                            (directive_name, directive_dir))
+                                         (directive_name, directive_dir))
 
         loaded_directive = imp.load_source(real_name, directive_file)
         self.directives[directive_name] = loaded_directive
@@ -308,23 +335,24 @@
         self.ssh.cmd('wget -O /etc/yum.repos.d/tflink-taskotron-fedora-f21.repo '
                      'http://copr-fe.cloud.fedoraproject.org/coprs/tflink/taskotron/'
                      'repo/fedora-21/tflink-taskotron-fedora-21.repo')
+
         self.ssh.install_pkgs('libtaskotron')
 
         # configure libtaskotron
         if config.get_config().config_filename:
             self.ssh.put_file(config.get_config().config_filename, '/etc/taskotron/taskotron.yaml')
 
+        # create needed dirs
+        self.ssh.mkdir(self.taskdir)
+
         # patch remote libtaskotron if needed (for dev, shouldn't be used in production)
         if self.arg_data['patch'] is not None:
             self.ssh.put_file(os.path.abspath(self.arg_data['patch']), '%s/%s' % (
                 self.taskdir, self.arg_data['patch']))
             self.ssh.install_pkgs('patch')
             self.ssh.cmd('patch -d /usr/lib/python2.7/site-packages/ -p1 -i %s/%s' % (
                 self.taskdir, self.arg_data['patch']))
 
-        # create needed dirs
-        self.ssh.mkdir(self.taskdir)
-
         # put files needed for execution (task, input files, etc)
         self.ssh.put_dir(os.path.dirname(self.arg_data['taskfile']), self.taskdir)
 
diff --git a/libtaskotron/virtualmachine.py b/libtaskotron/virtualmachine.py
--- a/libtaskotron/virtualmachine.py
+++ b/libtaskotron/virtualmachine.py
@@ -7,13 +7,146 @@
 """Interface to locally spawned virtual machines that are used as disposable
 clients for executing Taskotron tasks."""
 
+from time import sleep
+
+from libtaskotron.logger import log
+import libtaskotron.exceptions as exc
+
+from testcloud import instance, image, cli
+from testcloud.exceptions import TestcloudInstanceError, TestcloudImageError
+
+
 class TestCloudMachine(object):
-    """Launch virtual machines with TestCloud and prepare them for executing
-    tasks (install packages etc.)
-    """
-    # need to add paramiko methods for ssh/scp
-    pass
+    '''Launch virtual machines with TestCloud and prepare them for executing
+    tasks (install packages etc.)'''
+
+    def __init__(self, uuid):
+        ''':param uuid: unicode string uuid for the task being executed'''
+
+        # this should be temporary and go away once we have better image
+        # finding and discovery.
+        self.imageurl = 'http://dl.fedoraproject.org/pub/fedora/linux/releases/22/Cloud/x86_64/\
+Images/Fedora-Cloud-Base-22-20150521.x86_64.qcow2'
+
+        #: name of the testcloud instance spawned for this task
+        self.instancename = 'taskotron-{}'.format(uuid)
+
+        #: username to use when connecting to the virtual machine
+        self.username = 'root'
+
+        #: password for user on the remote machine
+        self.password = 'passw0rd'
+
+        #: ip address of the remote machine (only set after prepared and spawned successfully)
+        self.ipaddr = None
+
+        #: uuid of the task which spawned this instance
+        self.uuid = uuid
+
+    def _prepare_image(self):
+        '''Use testcloud to prepare an image for local booting
+        :raises TaskotronRemoteError: for errors in preparing the image with testcloud'''
+
+        log.debug("preparing image {} for task {}".format(self.imageurl, self.uuid))
+        tc_image = None
+
+        try:
+            tc_image = image.Image(self.imageurl)
+            tc_image.prepare()
+        except TestcloudImageError, e:
+            log.error(e)
+            raise exc.TaskotronRemoteError("There was an error while preparing the "
+                                           "testcloud image", e)
+
+        return tc_image
+
+    def _prepare_instance(self, tc_image):
+        '''Prepare an instance for booting and boot it with testcloud'''
+        log.debug("preparing testcloud instance {}".format(self.instancename))
+        tc_instance = instance.Instance(self.instancename, tc_image)
+        tc_instance.prepare()
+
+        log.debug("spawning testcloud instance {}".format(self.instancename))
+        tc_instance.spawn_vm()
+
+    def _check_existing_instance(self, should_exist=False):
+        '''Check whether an instance with the same name already exists, raise errors if the result
+        was not as expected.
+
+        :param should_exist: sets expectation on whether the instance should
+                             exist already or not
+        :raises TaskotronRemoteError: if the preset expectation is not met
+        '''
+        existing_instance = instance.find_instance(self.instancename)
+
+        if existing_instance is None:
+            if should_exist:
+                raise exc.TaskotronRemoteError("Was expecting to find instance {} but it does not"
+                                               " already exist".format(self.instancename))
+        else:
+            if not should_exist:
+                raise exc.TaskotronRemoteError("Expected to NOT find instance {} but it is "
+                                               "already defined".format(self.instancename))
+        return existing_instance
+
+    def prepare(self):
+        '''Prepare a virtual machine for running tasks.
+
+        :raises TaskotronRemoteError: if there are any errors while preparing
+                                      the virtual machine.
+       '''
+
+        tc_image = self._prepare_image()
+
+        self._check_existing_instance(should_exist=False)
+
+        self._prepare_instance(tc_image)
+
+        self.ipaddr = cli.find_vm_ip(self.instancename)
+
+    def _stop_instance(self, tc_instance):
+        '''Stop (shut down) a virtual machine instance
+
+        :raises TaskotronRemoteError: if stopping the instance fails'''
+
+        log.debug("Stopping instance {}".format(self.instancename))
+        try:
+            tc_instance.stop()
+        except TestcloudInstanceError, e:
+            log.error("Hit error while stopping instance {}".format(self.instancename), e)
+            raise exc.TaskotronRemoteError(e)
+
+    def _destroy_instance(self, tc_instance):
+        '''Destroy (delete) the virtual machine instance.
+
+        :raises TaskotronRemoteError: if destroying the instance fails'''
+
+        log.debug("destroying instance {}".format(self.instancename))
+        try:
+            tc_instance.destroy()
+        except TestcloudInstanceError, e:
+            log.error("Hit error while destroying instance {}".format(self.instancename), e)
+            raise exc.TaskotronRemoteError(e)
+
+    def teardown(self):
+        '''Tear down the virtual machine by shutting it down and removing any
+        trace of it from the host machine.
+
+        :raises TaskotronRemoteError: if there is a failure while stopping or
+                                      removing the virtual machine instance
+        '''
+
+        tc_instance = self._check_existing_instance(should_exist=True)
+
+        self._stop_instance(tc_instance)
+
+        # wait for testcloud to actually stop the instance, currently
+        # instance.stop() returns before the instance is actually stopped
+        sleep(2)
+
+        self._destroy_instance(tc_instance)
+
 
 class Image(object):
-    """Retrieve images from either local or remote sources"""
+    '''Retrieve images from either local or remote sources'''
     pass
diff --git a/testing/test_virtualmachine.py b/testing/test_virtualmachine.py
new file mode 100644
--- /dev/null
+++ b/testing/test_virtualmachine.py
@@ -0,0 +1,157 @@
+# -*- coding: utf-8 -*-
+# Copyright 2009-2015, Red Hat, Inc.
+# License: GPL-2.0+ <http://spdx.org/licenses/GPL-2.0+>
+# See the LICENSE file for more details on Licensing
+
+import pytest
+from mock import Mock, MagicMock
+
+from libtaskotron import virtualmachine
+from libtaskotron import exceptions as exc
+
+from testcloud import image, instance
+from testcloud.exceptions import TestcloudImageError, TestcloudInstanceError
+
+
+class TestVirtualMachineImagePrepare(object):
+    '''Test libtaskotron.virtualmachine._prepare_image'''
+
+    def setup_method(self, method):
+        self.ref_uuid = '1234-5678-9012'
+
+    def should_raise_on_failure(self, monkeypatch):
+        stub_image = Mock(side_effect=TestcloudImageError())
+
+        monkeypatch.setattr(image, 'Image', stub_image)
+
+        test_vm = virtualmachine.TestCloudMachine(self.ref_uuid)
+
+        with pytest.raises(exc.TaskotronRemoteError):
+            test_vm._prepare_image()
+
+    def should_behave_on_success(self, monkeypatch):
+        stub_image = MagicMock()
+
+        monkeypatch.setattr(image, 'Image', stub_image)
+
+        test_vm = virtualmachine.TestCloudMachine(self.ref_uuid)
+
+        test_vm._prepare_image()
+
+
+class TestVirtualMachineInstancePrepare(object):
+    '''Test libtaskotron.virtualmachine._prepare_instance'''
+
+    def setup_method(self, method):
+        self.ref_uuid = '1234-5678-9012'
+
+    def should_make_proper_calls(self, monkeypatch):
+        stub_image = Mock()
+        stub_instance = MagicMock()
+        stub_instanceclass = Mock(return_value=stub_instance)
+
+        monkeypatch.setattr(instance, 'Instance', stub_instanceclass)
+
+        test_vm = virtualmachine.TestCloudMachine(self.ref_uuid)
+
+        test_vm._prepare_instance(stub_image)
+
+        assert stub_instance.method_calls == [('prepare',), ('spawn_vm',)]
+
+
+class TestVirtualMachineStop(object):
+    '''Test libtaskotron.virtualmachine._stop_instance'''
+
+    def setup_method(self, method):
+        self.ref_uuid = '1234-5678-9012'
+
+    def should_bequiet_on_success(self):
+        stub_instance = Mock()
+
+        test_vm = virtualmachine.TestCloudMachine(self.ref_uuid)
+
+        test_vm._stop_instance(stub_instance)
+
+    def should_raise_on_failure(self):
+        stub_instance = Mock()
+        stub_instance.stop.side_effect = TestcloudInstanceError()
+
+        test_vm = virtualmachine.TestCloudMachine(self.ref_uuid)
+
+        with pytest.raises(exc.TaskotronRemoteError):
+            test_vm._stop_instance(stub_instance)
+
+
+class TestVirtualMachineDestroy(object):
+    '''Tests for the libtaskotron.virtualmachine.destroy() method'''
+
+    def setup_method(self, method):
+        self.ref_uuid = '1234-5678-9012'
+
+    def should_bequiet_on_success(self):
+        stub_instance = Mock()
+
+        test_vm = virtualmachine.TestCloudMachine(self.ref_uuid)
+
+        test_vm._destroy_instance(stub_instance)
+
+    def should_raise_on_failure(self):
+        stub_instance = Mock()
+        stub_instance.destroy.side_effect = TestcloudInstanceError()
+
+        test_vm = virtualmachine.TestCloudMachine(self.ref_uuid)
+
+        with pytest.raises(exc.TaskotronRemoteError):
+            test_vm._destroy_instance(stub_instance)
+
+
+class TestVirtualMachineExistingInstances(object):
+    '''Test the possible outcomes of checking for existing instances. Exception
+    should be thrown if the should_exist arg doesn't match "reality".'''
+
+    def setup_method(self, method):
+        self.ref_uuid = '1234-5678-9012'
+
+    def should_raise_existing_expecting_novm(self, monkeypatch):
+        stub_instance = Mock(return_value=None)
+
+        monkeypatch.setattr(instance, 'find_instance', stub_instance)
+
+        test_vm = virtualmachine.TestCloudMachine(self.ref_uuid)
+
+        with pytest.raises(exc.TaskotronRemoteError):
+            test_vm._check_existing_instance(True)
+
+    def should_return_none_expecting_novm(self, monkeypatch):
+        stub_instance = Mock(return_value=None)
+
+        monkeypatch.setattr(instance, 'find_instance', stub_instance)
+
+        test_vm = virtualmachine.TestCloudMachine(self.ref_uuid)
+
+        test_instance = test_vm._check_existing_instance(False)
+
+        assert test_instance is None
+
+    def should_return_instance_found_expecting_vm(self, monkeypatch):
+        stub_found_instance = Mock()
+        stub_instance = Mock(return_value=stub_found_instance)
+
+        monkeypatch.setattr(instance, 'find_instance', stub_instance)
+
+        test_vm = virtualmachine.TestCloudMachine(self.ref_uuid)
+
+        test_instance = test_vm._check_existing_instance(True)
+
+        assert test_instance == stub_found_instance
+
+    def should_raise_noexisting_expecting_vm(self, monkeypatch):
+        stub_found_instance = Mock()
+        stub_instance = Mock(return_value=stub_found_instance)
+
+        monkeypatch.setattr(instance, 'find_instance', stub_instance)
+
+        test_vm = virtualmachine.TestCloudMachine(self.ref_uuid)
+
+        with pytest.raises(exc.TaskotronRemoteError):
+            test_vm._check_existing_instance(False)

