diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -850,6 +850,30 @@
     'DrydockDefaultViewCapability' => 'applications/drydock/capability/DrydockDefaultViewCapability.php',
     'DrydockFilesystemInterface' => 'applications/drydock/interface/filesystem/DrydockFilesystemInterface.php',
     'DrydockInterface' => 'applications/drydock/interface/DrydockInterface.php',
+    'DrydockKVMAllocatedImageLogType' => 'applications/drydock/logtype/DrydockKVMAllocatedImageLogType.php',
+    'DrydockKVMAllocatingImageLogType' => 'applications/drydock/logtype/DrydockKVMAllocatingImageLogType.php',
+    'DrydockKVMAssignedIPAddressLogType' => 'applications/drydock/logtype/DrydockKVMAssignedIPAddressLogType.php',
+    'DrydockKVMAttemptConnectionLogType' => 'applications/drydock/logtype/DrydockKVMAttemptConnectionLogType.php',
+    'DrydockKVMCreatedVMLogType' => 'applications/drydock/logtype/DrydockKVMCreatedVMLogType.php',
+    'DrydockKVMCreatingVMLogType' => 'applications/drydock/logtype/DrydockKVMCreatingVMLogType.php',
+    'DrydockKVMCredentialUsageLogType' => 'applications/drydock/logtype/DrydockKVMCredentialUsageLogType.php',
+    'DrydockKVMFailedConnectionLogType' => 'applications/drydock/logtype/DrydockKVMFailedConnectionLogType.php',
+    'DrydockKVMHostBlueprintImplementation' => 'applications/drydock/blueprint/DrydockKVMHostBlueprintImplementation.php',
+    'DrydockKVMRemovedImageLogType' => 'applications/drydock/logtype/DrydockKVMRemovedImageLogType.php',
+    'DrydockKVMRemovingImageLogType' => 'applications/drydock/logtype/DrydockKVMRemovingImageLogType.php',
+    'DrydockKVMRetrievedImagePathLogType' => 'applications/drydock/logtype/DrydockKVMRetrievedImagePathLogType.php',
+    'DrydockKVMRetrievedMACAddressLogType' => 'applications/drydock/logtype/DrydockKVMRetrievedMACAddressLogType.php',
+    'DrydockKVMRetrievingImagePathLogType' => 'applications/drydock/logtype/DrydockKVMRetrievingImagePathLogType.php',
+    'DrydockKVMRetrievingMACAddressLogType' => 'applications/drydock/logtype/DrydockKVMRetrievingMACAddressLogType.php',
+    'DrydockKVMShutdownVMLogType' => 'applications/drydock/logtype/DrydockKVMShutdownVMLogType.php',
+    'DrydockKVMShuttingDownVMLogType' => 'applications/drydock/logtype/DrydockKVMShuttingDownVMLogType.php',
+    'DrydockKVMSuccessfulConnectionLogType' => 'applications/drydock/logtype/DrydockKVMSuccessfulConnectionLogType.php',
+    'DrydockKVMUncleanShutdownLogType' => 'applications/drydock/logtype/DrydockKVMUncleanShutdownLogType.php',
+    'DrydockKVMUnexpectedVMShutdownLogType' => 'applications/drydock/logtype/DrydockKVMUnexpectedVMShutdownLogType.php',
+    'DrydockKVMVirtualMachineShutdownException' => 'applications/drydock/exception/DrydockKVMVirtualMachineShutdownException.php',
+    'DrydockKVMWaitingForConnectivityLogType' => 'applications/drydock/logtype/DrydockKVMWaitingForConnectivityLogType.php',
+    'DrydockKVMWaitingForIPAddressLogType' => 'applications/drydock/logtype/DrydockKVMWaitingForIPAddressLogType.php',
+    'DrydockKVMWinRMCredentialUsageLogType' => 'applications/drydock/logtype/DrydockKVMWinRMCredentialUsageLogType.php',
     'DrydockLandRepositoryOperation' => 'applications/drydock/operation/DrydockLandRepositoryOperation.php',
     'DrydockLease' => 'applications/drydock/storage/DrydockLease.php',
     'DrydockLeaseAcquiredLogType' => 'applications/drydock/logtype/DrydockLeaseAcquiredLogType.php',
@@ -3715,6 +3739,7 @@
     'ReleephWorkRecordConduitAPIMethod' => 'applications/releeph/conduit/work/ReleephWorkRecordConduitAPIMethod.php',
     'ReleephWorkRecordPickStatusConduitAPIMethod' => 'applications/releeph/conduit/work/ReleephWorkRecordPickStatusConduitAPIMethod.php',
     'RemarkupProcessConduitAPIMethod' => 'applications/remarkup/conduit/RemarkupProcessConduitAPIMethod.php',
+    'RemoteTempFile' => 'applications/drydock/interface/command/RemoteTempFile.php',
     'RepositoryConduitAPIMethod' => 'applications/repository/conduit/RepositoryConduitAPIMethod.php',
     'RepositoryCreateConduitAPIMethod' => 'applications/repository/conduit/RepositoryCreateConduitAPIMethod.php',
     'RepositoryQueryConduitAPIMethod' => 'applications/repository/conduit/RepositoryQueryConduitAPIMethod.php',
@@ -4707,6 +4732,30 @@
     'DrydockDefaultViewCapability' => 'PhabricatorPolicyCapability',
     'DrydockFilesystemInterface' => 'DrydockInterface',
     'DrydockInterface' => 'Phobject',
+    'DrydockKVMAllocatedImageLogType' => 'DrydockLogType',
+    'DrydockKVMAllocatingImageLogType' => 'DrydockLogType',
+    'DrydockKVMAssignedIPAddressLogType' => 'DrydockLogType',
+    'DrydockKVMAttemptConnectionLogType' => 'DrydockLogType',
+    'DrydockKVMCreatedVMLogType' => 'DrydockLogType',
+    'DrydockKVMCreatingVMLogType' => 'DrydockLogType',
+    'DrydockKVMCredentialUsageLogType' => 'DrydockLogType',
+    'DrydockKVMFailedConnectionLogType' => 'DrydockLogType',
+    'DrydockKVMHostBlueprintImplementation' => 'DrydockBlueprintImplementation',
+    'DrydockKVMRemovedImageLogType' => 'DrydockLogType',
+    'DrydockKVMRemovingImageLogType' => 'DrydockLogType',
+    'DrydockKVMRetrievedImagePathLogType' => 'DrydockLogType',
+    'DrydockKVMRetrievedMACAddressLogType' => 'DrydockLogType',
+    'DrydockKVMRetrievingImagePathLogType' => 'DrydockLogType',
+    'DrydockKVMRetrievingMACAddressLogType' => 'DrydockLogType',
+    'DrydockKVMShutdownVMLogType' => 'DrydockLogType',
+    'DrydockKVMShuttingDownVMLogType' => 'DrydockLogType',
+    'DrydockKVMSuccessfulConnectionLogType' => 'DrydockLogType',
+    'DrydockKVMUncleanShutdownLogType' => 'DrydockLogType',
+    'DrydockKVMUnexpectedVMShutdownLogType' => 'DrydockLogType',
+    'DrydockKVMVirtualMachineShutdownException' => 'PhabricatorWorkerYieldException',
+    'DrydockKVMWaitingForConnectivityLogType' => 'DrydockLogType',
+    'DrydockKVMWaitingForIPAddressLogType' => 'DrydockLogType',
+    'DrydockKVMWinRMCredentialUsageLogType' => 'DrydockLogType',
     'DrydockLandRepositoryOperation' => 'DrydockRepositoryOperationType',
     'DrydockLease' => array(
       'DrydockDAO',
@@ -8138,6 +8187,7 @@
     'ReleephWorkRecordConduitAPIMethod' => 'ReleephConduitAPIMethod',
     'ReleephWorkRecordPickStatusConduitAPIMethod' => 'ReleephConduitAPIMethod',
     'RemarkupProcessConduitAPIMethod' => 'ConduitAPIMethod',
+    'RemoteTempFile' => 'Phobject',
     'RepositoryConduitAPIMethod' => 'ConduitAPIMethod',
     'RepositoryCreateConduitAPIMethod' => 'RepositoryConduitAPIMethod',
     'RepositoryQueryConduitAPIMethod' => 'RepositoryConduitAPIMethod',
diff --git a/src/applications/drydock/blueprint/DrydockKVMHostBlueprintImplementation.php b/src/applications/drydock/blueprint/DrydockKVMHostBlueprintImplementation.php
new file mode 100644
--- /dev/null
+++ b/src/applications/drydock/blueprint/DrydockKVMHostBlueprintImplementation.php
@@ -0,0 +1,909 @@
+<?php
+
+final class DrydockKVMHostBlueprintImplementation
+  extends DrydockBlueprintImplementation {
+
+  const RESOURCE_STATE_HOST_NOT_ALLOCATED = 'host-not-allocated';
+  const RESOURCE_STATE_WAIT_FOR_IP_ADDRESS = 'wait-for-ip-address';
+  const RESOURCE_STATE_TEST_CONNECTIVITY = 'test-connectivity';
+  const RESOURCE_STATE_READY = 'ready';
+
+  const LEASE_STATE_CHECK_VM_ONLINE = 'check-vm-online';
+  const LEASE_STATE_WAIT_FOR_IP_ADDRESS = 'wait-for-ip-address';
+  const LEASE_STATE_READY = 'ready';
+
+  public function isEnabled() {
+    return true;
+  }
+
+  public function getBlueprintName() {
+    return pht('KVM Hosts');
+  }
+
+  public function getDescription() {
+    return pht(
+      'Allows Drydock to build and lease virtual machine hosts '.
+      'on a machine with KVM installed.');
+  }
+
+  public function canAnyBlueprintEverAllocateResourceForLease(
+    DrydockLease $lease) {
+    return true;
+  }
+
+  public function canEverAllocateResourceForLease(
+    DrydockBlueprint $blueprint,
+    DrydockLease $lease) {
+
+    return true;
+  }
+
+  public function canAllocateResourceForLease(
+    DrydockBlueprint $blueprint,
+    DrydockLease $lease) {
+
+    return true;
+  }
+
+
+  public function allocateResource(
+    DrydockBlueprint $blueprint,
+    DrydockLease $lease) {
+
+    $resource = $this->newResourceTemplate($blueprint)
+      ->setAttribute('state', self::RESOURCE_STATE_HOST_NOT_ALLOCATED)
+      ->needSlotLock('kvm.'.$blueprint->getPHID());
+
+    return $resource->allocateResource();
+  }
+
+  public function activateResource(
+    DrydockBlueprint $blueprint,
+    DrydockResource $resource) {
+
+    switch ($resource->getAttribute('state')) {
+      case self::RESOURCE_STATE_HOST_NOT_ALLOCATED:
+        $this->performHostAllocation($blueprint, $resource);
+        break;
+      case self::RESOURCE_STATE_WAIT_FOR_IP_ADDRESS:
+        $this->checkIfIPAddressIsAssigned($blueprint, $resource);
+        break;
+      case self::RESOURCE_STATE_TEST_CONNECTIVITY:
+        $this->testConnectivity($blueprint, $resource);
+        break;
+      case self::RESOURCE_STATE_READY:
+        return $resource->activateResource();
+      default:
+        throw new Exception(pht(
+          'Resource is in an unexpected KVM state: "%s".',
+          $resource->getAttribute('state')));
+    }
+
+  }
+
+  public function destroyResource(
+    DrydockBlueprint $blueprint,
+    DrydockResource $resource) {
+
+    if (!$resource->getAttribute('vm-name') ||
+        !$resource->getAttribute('image-name') ||
+        !$resource->getAttribute('storage-pool')) {
+      return;
+    }
+
+    try {
+      $loaded_credential = PassphraseSSHKey::loadFromPHID(
+        $blueprint->getFieldValue('keypair'),
+        PhabricatorUser::getOmnipotentUser());
+
+      $resource->logEvent(
+        DrydockKVMShutdownVMLogType::LOGCONST,
+        array(
+          'vm-name' => $resource->getAttribute('vm-name'),
+        ));
+
+      $future = $this->getSSHFuture(
+        $blueprint,
+        $loaded_credential,
+        '/usr/bin/virsh destroy %s',
+        $resource->getAttribute('vm-name'));
+      $future->resolvex();
+
+      $resource->logEvent(
+        DrydockKVMShutdownVMLogType::LOGCONST,
+        array(
+          'vm-name' => $resource->getAttribute('vm-name'),
+        ));
+
+      $resource->logEvent(
+        DrydockKVMRemovingImageLogType::LOGCONST,
+        array(
+          'image-name' => $resource->getAttribute('image-name'),
+        ));
+
+      $future = $this->getSSHFuture(
+        $blueprint,
+        $loaded_credential,
+        '/usr/bin/virsh vol-delete %s --pool %s',
+        $resource->getAttribute('image-name'),
+        $resource->getAttribute('storage-pool'));
+      $future->resolvex();
+
+      $resource->logEvent(
+        DrydockKVMRemovedImageLogType::LOGCONST,
+        array(
+          'image-name' => $resource->getAttribute('image-name'),
+        ));
+    } catch (CommandException $ex) {
+      $resource->logEvent(
+        DrydockKVMUncleanShutdownLogType::LOGCONST,
+        array(
+          'vm-name' => $resource->getAttribute('vm-name'),
+          'image-name' => $resource->getAttribute('image-name'),
+          'stdout' => $ex->getStdout(),
+          'stderr' => $ex->getStderr(),
+        ));
+    }
+
+    return;
+  }
+
+  public function getResourceName(
+    DrydockBlueprint $blueprint,
+    DrydockResource $resource) {
+    $vm_name = $resource->getAttribute(
+      'vm-name',
+      pht('<Unknown>'));
+    return pht('Host (%s)', $vm_name);
+  }
+
+  public function canAcquireLeaseOnResource(
+    DrydockBlueprint $blueprint,
+    DrydockResource $resource,
+    DrydockLease $lease) {
+
+    return true;
+  }
+
+  public function acquireLease(
+    DrydockBlueprint $blueprint,
+    DrydockResource $resource,
+    DrydockLease $lease) {
+
+    $lease
+      ->setAttribute('state', self::LEASE_STATE_CHECK_VM_ONLINE)
+      ->acquireOnResource($resource);
+  }
+
+  public function activateLease(
+    DrydockBlueprint $blueprint,
+    DrydockResource $resource,
+    DrydockLease $lease) {
+
+    if ($resource->isActive()) {
+      try {
+        switch ($lease->getAttribute('state')) {
+          case self::LEASE_STATE_CHECK_VM_ONLINE:
+            $this->checkVMOnline($blueprint, $resource, $lease);
+            break;
+          case self::LEASE_STATE_WAIT_FOR_IP_ADDRESS:
+            $this->checkIfIPAddressIsAssigned($blueprint, $resource, $lease);
+            break;
+          case self::LEASE_STATE_READY:
+            $lease->activateOnResource($resource);
+            break;
+          default:
+            throw new Exception(pht(
+              'Lease is in an unexpected KVM state: "%s".',
+              $lease->getAttribute('state')));
+        }
+      } catch (DrydockKVMVirtualMachineShutdownException $ex) {
+        $lease->logEvent(
+          DrydockKVMUnexpectedVMShutdownLogType::LOGCONST,
+          array(
+            'vm-name' => $resource->getAttribute('vm-name'),
+          ));
+
+        $resource->setStatus(DrydockResourceStatus::STATUS_BROKEN);
+        $resource->save();
+        $resource->scheduleUpdate();
+
+        throw new Exception('Virtual machine no longer exists or is shut down');
+      }
+    }
+
+    throw new PhabricatorWorkerYieldException(1);
+  }
+
+  public function checkVMOnline(
+    DrydockBlueprint $blueprint,
+    DrydockResource $resource,
+    DrydockLease $lease) {
+
+    $loaded_credential = PassphraseSSHKey::loadFromPHID(
+    $blueprint->getFieldValue('keypair'),
+    PhabricatorUser::getOmnipotentUser());
+
+    $future = $this->getSSHFuture(
+      $blueprint,
+      $loaded_credential,
+      '/usr/bin/virsh domstate %s',
+      $resource->getAttribute('vm-name'));
+    list($err, $stdout, $stderr) = $future->resolve();
+    $status = trim($stdout).trim($stderr);
+    if (substr_count($status, 'shut off') > 0 ||
+        substr_count($status, 'Domain not found') > 0) {
+      throw new DrydockKVMVirtualMachineShutdownException();
+    }
+
+    $lease->setAttribute('state', self::LEASE_STATE_WAIT_FOR_IP_ADDRESS);
+    $lease->save();
+  }
+
+  public function didReleaseLease(
+    DrydockBlueprint $blueprint,
+    DrydockResource $resource,
+    DrydockLease $lease) {
+    // Almanac hosts stick around indefinitely so we don't need to recycle them
+    // if they don't have any leases.
+    return;
+  }
+
+  public function destroyLease(
+    DrydockBlueprint $blueprint,
+    DrydockResource $resource,
+    DrydockLease $lease) {
+    // We don't create anything when activating a lease, so we don't need to
+    // throw anything away.
+    return;
+  }
+
+  public function getType() {
+    return 'host';
+  }
+
+  public function getInterface(
+    DrydockBlueprint $blueprint,
+    DrydockResource $resource,
+    DrydockLease $lease,
+    $type) {
+
+    $viewer = PhabricatorUser::getOmnipotentUser();
+
+    switch ($type) {
+      case DrydockCommandInterface::INTERFACE_TYPE:
+        return $this->getCommandInterfaceWithExplicitHost(
+            $blueprint,
+            $resource,
+            $lease->getAttribute('ip-address'))
+          ->pushWorkingDirectory($lease->getAttribute('path'));
+        break;
+    }
+  }
+
+  private function getCommandInterfaceWithExplicitHost(
+    DrydockBlueprint $blueprint,
+    DrydockResource $resource,
+    $host) {
+
+    $loaded_credential = PassphraseSSHKey::loadFromPHID(
+    $blueprint->getFieldValue('keypair'),
+    PhabricatorUser::getOmnipotentUser());
+
+    return id(new DrydockSSHCommandInterface())
+      ->setConfig('host', $host)
+      ->setConfig('port', $resource->getAttribute('port'))
+      ->setConfig('credentialPHID', $resource->getAttribute('keypair'))
+      ->setConfig('platform', $resource->getAttribute('platform'))
+      ->setSSHProxy(
+        $blueprint->getFieldValue('host'),
+        $blueprint->getFieldValue('port'),
+        $loaded_credential);
+  }
+
+  public function getFieldSpecifications() {
+    return array(
+      'kvm' => array(
+        'name' => pht('KVM Host Configuration'),
+        'type' => 'header',
+      ),
+      'host' => array(
+        'name' => pht('SSH Host'),
+        'type' => 'text',
+        'required' => true,
+        'caption' => pht('e.g. 10.0.0.1'),
+      ),
+      'port' => array(
+        'name' => pht('SSH Port'),
+        'type' => 'text',
+        'required' => false,
+        'caption' => pht('Defaults to port 22'),
+      ),
+      'keypair' => array(
+        'name' => pht('SSH Key Pair'),
+        'type' => 'credential',
+        'required' => true,
+        'credential.provides'
+          => PassphraseSSHPrivateKeyCredentialType::PROVIDES_TYPE,
+        'caption' => pht(
+          'Used to connect over SSH to the KVM host.'),
+      ),
+      'winrm-auth' => array(
+        'name' => pht('WinRM Credentials'),
+        'type' => 'credential',
+        'credential.provides'
+          => PassphrasePasswordCredentialType::PROVIDES_TYPE,
+        'caption' => pht(
+          'This is only required if the platform is "windows".'),
+      ),
+      'platform' => array(
+        'name' => pht('Platform Name'),
+        'type' => 'text',
+        'required' => true,
+        'caption' => pht('e.g. %s or %s', 'windows', 'linux'),
+      ),
+      'storage-path' => array(
+        'name' => pht('Storage Path'),
+        'type' => 'text',
+        'required' => true,
+        'caption' => pht(
+          'A writable location on the instance where new directories / files '.
+          'can be created and data can be stored in.'),
+      ),
+      'machine' => array(
+        'name' => pht('Instance Configuration'),
+        'type' => 'header',
+      ),
+      'cpu' => array(
+        'name' => pht('CPUs'),
+        'type' => 'int',
+        'required' => true,
+        'caption' => pht('The number of CPUs to allocate to instances.'),
+      ),
+      'ram' => array(
+        'name' => pht('RAM'),
+        'type' => 'int',
+        'required' => true,
+        'caption' => pht(
+          'The amount of RAM (in megabytes) to '.
+          'allocate to instances.'),
+      ),
+      'storage-pool' => array(
+        'name' => pht('Storage Pool'),
+        'type' => 'text',
+        'required' => true,
+        'caption' => pht(
+          'The storage pool to clone instance images into.'),
+      ),
+      'base-image' => array(
+        'name' => pht('Base Image Name'),
+        'type' => 'text',
+        'required' => true,
+        'caption' => pht(
+          'The name of the qcow2 image in the storage pool, which '.
+          'is cloned for new instances.'),
+      ),
+      'network' => array(
+        'name' => pht('Networking Configuration'),
+        'type' => 'header',
+      ),
+      'network-id' => array(
+        'name' => pht('Network Name'),
+        'type' => 'text',
+        'caption' => pht(
+          'The name of the libvirt network to assign to the instance.'),
+      ),
+      'dnsmasq-path' => array(
+        'name' => pht('DNSMasq Leases Path'),
+        'type' => 'text',
+        'caption' => pht(
+          'The path to the DNSMasq leases file, usually located at a path '.
+          'such as /var/lib/libvirt/dnsmasq/<network>.leases.'),
+      ),
+      'dnsmasq-is-json' => array(
+        'name' => pht('DNSMasq Leases are JSON'),
+        'type' => 'bool',
+        'caption' => pht(
+          'Whether or not the lease file is stored in a JSON format.'),
+      ),
+      'attr-header' => array(
+        'name' => pht('Host Attributes'),
+        'type' => 'header',
+      ),
+      'attributes' => array(
+        'name' => pht('Host Attributes'),
+        'type' => 'textarea',
+        'caption' => pht(
+          'A newline separated list of host attributes.  Each attribute '.
+          'should be specified in a key=value format.'),
+        'monospace' => true,
+      ),
+    ) + parent::getFieldSpecifications();
+  }
+
+
+  // ------------------------------------------------------------------
+
+
+  private function performHostAllocation(
+    DrydockBlueprint $blueprint,
+    DrydockResource $resource) {
+
+    $credential = id(new PassphraseCredentialQuery())
+      ->setViewer(PhabricatorUser::getOmnipotentUser())
+      ->withPHIDs(array($blueprint->getFieldValue('keypair')))
+      ->executeOne();
+
+    if ($credential === null) {
+      throw new Exception('Specified credential does not exist!');
+    }
+
+    $resource->logEvent(
+      DrydockKVMCredentialUsageLogType::LOGCONST,
+      array(
+        'phid' => $credential->getPHID(),
+      ));
+
+    $loaded_credential = PassphraseSSHKey::loadFromPHID(
+      $blueprint->getFieldValue('keypair'),
+      PhabricatorUser::getOmnipotentUser());
+
+    $winrm_auth_id = null;
+    if ($blueprint->getFieldValue('platform') === 'windows') {
+      $winrm_auth = id(new PassphraseCredentialQuery())
+        ->setViewer(PhabricatorUser::getOmnipotentUser())
+        ->withPHIDs(array($blueprint->getFieldValue('winrm-auth')))
+        ->executeOne();
+
+      if ($winrm_auth === null) {
+        throw new Exception(
+          'Specified credential for WinRM auth does not exist!');
+      }
+
+      $winrm_auth_id = $winrm_auth->getID();
+
+      $resource->logEvent(
+        DrydockKVMWinRMCredentialUsageLogType::LOGCONST,
+        array(
+          'phid' => $winrm_auth->getPHID(),
+        ));
+    }
+
+    $pool_name = $blueprint->getFieldValue('storage-pool');
+    $image_name = 'image-'.$resource->getID();
+    $vm_name = 'vm-'.$resource->getID();
+
+    $resource->logEvent(
+      DrydockKVMAllocatingImageLogType::LOGCONST,
+      array(
+        'base-image' => $blueprint->getFieldValue('base-image'),
+        'new-image' => $image_name,
+      ));
+
+    $future = $this->getSSHFuture(
+      $blueprint,
+      $loaded_credential,
+      '/usr/bin/virsh vol-create-as %s %s 256GB '.
+      '--format qcow2 --backing-vol %s '.
+      '--backing-vol-format qcow2',
+      $blueprint->getFieldValue('storage-pool'),
+      $image_name,
+      $blueprint->getFieldValue('base-image'));
+    $future->resolvex();
+
+    $resource->logEvent(
+      DrydockKVMAllocatedImageLogType::LOGCONST,
+      array(
+        'base-image' => $blueprint->getFieldValue('base-image'),
+        'new-image' => $image_name,
+      ));
+
+    $resource->logEvent(
+      DrydockKVMRetrievingImagePathLogType::LOGCONST,
+      array(
+        'new-image' => $image_name,
+      ));
+
+    $future = $this->getSSHFuture(
+      $blueprint,
+      $loaded_credential,
+      '/usr/bin/virsh vol-path %s --pool %s',
+      $image_name,
+      $blueprint->getFieldValue('storage-pool'));
+    list($stdout, $stderr) = $future->resolvex();
+    $image_path = trim($stdout);
+
+    $resource->logEvent(
+      DrydockKVMRetrievedImagePathLogType::LOGCONST,
+      array(
+        'new-image' => $image_name,
+        'path' => $image_path,
+      ));
+
+    $ram = $blueprint->getFieldValue('ram');
+    $vcpu = $blueprint->getFieldValue('cpu');
+    $network = $blueprint->getFieldValue('network-id');
+
+    $xml = <<<EOF
+<domain type='kvm'>
+  <name>$vm_name</name>
+  <memory unit='MiB'>$ram</memory>
+  <currentMemory unit='MiB'>$ram</currentMemory>
+  <vcpu placement='static'>$vcpu</vcpu>
+  <os>
+    <type arch='x86_64' machine='pc-1.1'>hvm</type>
+    <boot dev='hd'/>
+  </os>
+  <features>
+    <acpi/>
+    <apic/>
+    <pae/>
+  </features>
+  <cpu mode='custom' match='exact'>
+    <model fallback='allow'>Nehalem</model>
+  </cpu>
+  <clock offset='localtime'>
+    <timer name='rtc' tickpolicy='catchup'/>
+    <timer name='pit' tickpolicy='delay'/>
+    <timer name='hpet' present='no'/>
+  </clock>
+  <on_poweroff>destroy</on_poweroff>
+  <on_reboot>restart</on_reboot>
+  <on_crash>restart</on_crash>
+  <devices>
+    <emulator>/usr/bin/qemu-system-x86_64</emulator>
+    <disk type='file' device='disk'>
+      <driver name='qemu' type='qcow2' cache='writeback'/>
+      <source file='$image_path' />
+      <target dev='vda' bus='virtio'/>
+      <address type='pci' domain='0x0000'
+        bus='0x00' slot='0x0b' function='0x0'/>
+    </disk>
+    <controller type='usb' index='0' model='ich9-ehci1'>
+      <address type='pci' domain='0x0000'
+        bus='0x00' slot='0x05' function='0x0'/>
+    </controller>
+    <controller type='usb' index='0' model='ich9-uhci1'>
+      <master startport='0'/>
+      <address type='pci' domain='0x0000'
+        bus='0x00' slot='0x06' function='0x0'/>
+    </controller>
+    <controller type='usb' index='0' model='ich9-uhci2'>
+      <master startport='2'/>
+      <address type='pci' domain='0x0000'
+        bus='0x00' slot='0x07' function='0x0'/>
+    </controller>
+    <controller type='usb' index='0' model='ich9-uhci3'>
+      <master startport='4'/>
+      <address type='pci' domain='0x0000'
+        bus='0x00' slot='0x08' function='0x0'/>
+    </controller>
+    <controller type='ide' index='0'>
+      <address type='pci' domain='0x0000'
+        bus='0x00' slot='0x01' function='0x1'/>
+    </controller>
+    <controller type='virtio-serial' index='0'>
+      <address type='pci' domain='0x0000'
+        bus='0x00' slot='0x09' function='0x0'/>
+    </controller>
+    <interface type='network'>
+      <source network='$network'/>
+      <model type='virtio'/>
+      <address type='pci' domain='0x0000'
+        bus='0x00' slot='0x03' function='0x0'/>
+    </interface>
+    <serial type='pty'>
+      <target port='0'/>
+    </serial>
+    <console type='pty'>
+      <target type='serial' port='0'/>
+    </console>
+    <channel type='spicevmc'>
+      <target type='virtio' name='com.redhat.spice.0'/>
+      <address type='virtio-serial' controller='0' bus='0' port='1'/>
+    </channel>
+    <input type='tablet' bus='usb'/>
+    <input type='mouse' bus='ps2'/>
+    <graphics type='spice' autoport='yes' listen='0.0.0.0'>
+      <listen type='address' address='0.0.0.0'/>
+    </graphics>
+    <sound model='ich6'>
+      <address type='pci' domain='0x0000'
+        bus='0x00' slot='0x04' function='0x0'/>
+    </sound>
+    <video>
+      <model type='cirrus' vram='9216' heads='1'/>
+      <address type='pci' domain='0x0000'
+        bus='0x00' slot='0x02' function='0x0'/>
+    </video>
+    <memballoon model='virtio'>
+      <address type='pci' domain='0x0000'
+        bus='0x00' slot='0x0a' function='0x0'/>
+    </memballoon>
+  </devices>
+</domain>
+EOF;
+
+    $resource->logEvent(
+      DrydockKVMCreatingVMLogType::LOGCONST,
+      array(
+        'vm-name' => $vm_name,
+      ));
+
+    $future = $this->getSSHFuture(
+      $blueprint,
+      $loaded_credential,
+      '/usr/bin/virsh create /dev/stdin');
+    $future->write($xml);
+    $future->resolvex();
+
+    $resource->logEvent(
+      DrydockKVMCreatedVMLogType::LOGCONST,
+      array(
+        'vm-name' => $vm_name,
+      ));
+
+    $resource
+      ->setStatus(DrydockResourceStatus::STATUS_PENDING)
+      ->setAttributes(array(
+        'platform' => $blueprint->getFieldValue('platform'),
+        'protocol' => 'ssh',
+        'path' => $blueprint->getFieldValue('storage-path'),
+        'keypair' => $blueprint->getFieldValue('keypair'),
+        'winrm-auth' => $winrm_auth_id,
+        'vm-name' => $vm_name,
+        'image-name' => $image_name,
+        'image-path' => $image_path,
+        'storage-pool' => $blueprint->getFieldValue('storage-pool'),
+      ))
+      ->save();
+
+    $resource->logEvent(
+      DrydockKVMRetrievingMACAddressLogType::LOGCONST,
+      array(
+        'vm-name' => $vm_name,
+      ));
+
+    $future = $this->getSSHFuture(
+      $blueprint,
+      $loaded_credential,
+      '/usr/bin/virsh dumpxml %s',
+      $vm_name);
+    list($stdout, $stderr) = $future->resolvex();
+    $status_xml = simplexml_load_string(trim($stdout));
+    if ($status_xml === false) {
+      throw new Exception('Unable to read VM XML!');
+    }
+
+    $mac_address = $status_xml->devices->interface->mac->attributes()->address;
+
+    $resource->logEvent(
+      DrydockKVMRetrievedMACAddressLogType::LOGCONST,
+      array(
+        'vm-name' => $vm_name,
+        'mac-address' => $mac_address,
+      ));
+
+    $resource->setAttribute('mac-address', $mac_address);
+
+    if ($resource->getAttribute('platform') === 'windows') {
+      $resource->setAttribute('port', 5985);
+    } else {
+      $resource->setAttribute('port', 22);
+    }
+
+    $resource->setAttribute('state', self::RESOURCE_STATE_WAIT_FOR_IP_ADDRESS);
+    $resource->save();
+
+    $resource->logEvent(
+      DrydockKVMWaitingForIPAddressLogType::LOGCONST,
+      array());
+
+    throw new PhabricatorWorkerYieldException(0);
+  }
+
+  private function checkIfIPAddressIsAssigned(
+    DrydockBlueprint $blueprint,
+    DrydockResource $resource = null,
+    DrydockLease $lease = null) {
+
+    $loaded_credential = PassphraseSSHKey::loadFromPHID(
+      $blueprint->getFieldValue('keypair'),
+      PhabricatorUser::getOmnipotentUser());
+
+    $mac_address = $resource->getAttribute('mac-address');
+    if (is_array($mac_address)) {
+      $mac_address = head($mac_address);
+    }
+
+    $future = $this->getSSHFuture(
+      $blueprint,
+      $loaded_credential,
+      'cat %s',
+      $blueprint->getFieldValue('dnsmasq-path'));
+    list($stdout, $stderr) = $future->resolvex();
+
+    $ip_address = null;
+    if ($blueprint->getFieldValue('dnsmasq-is-json')) {
+      $json = phutil_json_decode($stdout);
+      foreach ($json as $entry) {
+        if (idx($entry, 'mac-address') === trim($mac_address)) {
+          $ip_address = idx($entry, 'ip-address');
+          break;
+        }
+      }
+    } else {
+      foreach (phutil_split_lines($stdout) as $line) {
+        $components = explode(' ', $line);
+        if (trim($components[1]) === trim($mac_address)) {
+          $ip_address = $components[2];
+          break;
+        }
+      }
+    }
+
+    if ($ip_address != null) {
+      // Now we save the IP address depending on why we need
+      // the IP address.  If we have a resource, this IP address
+      // will be used to establish initial connectivity over
+      // WinRM.  If we have a lease, this IP address will be used
+      // to execute commands on the lease.
+      if ($lease !== null) {
+        $lease->setAttribute(
+          'ip-address',
+          $ip_address);
+        $lease->setAttribute(
+          'state',
+          self::LEASE_STATE_READY);
+        $lease->save();
+      } else if ($resource !== null) {
+        $resource->logEvent(
+          DrydockKVMAssignedIPAddressLogType::LOGCONST,
+          array(
+            'vm-name' => $resource->getAttribute('vm-name'),
+            'ip-address' => $ip_address,
+          ));
+        $resource->setAttribute(
+          'ip-address-for-connectivity',
+          $ip_address);
+        $resource->setAttribute(
+          'state',
+          self::RESOURCE_STATE_TEST_CONNECTIVITY);
+        $resource->logEvent(
+          DrydockKVMWaitingForConnectivityLogType::LOGCONST,
+          array(
+            'protocol' => $resource->getAttribute('protocol'),
+          ));
+        $resource->save();
+      }
+
+      // We have moved to the next stage.
+      throw new PhabricatorWorkerYieldException(0);
+    }
+
+    $future = $this->getSSHFuture(
+      $blueprint,
+      $loaded_credential,
+      '/usr/bin/virsh domstate %s',
+      $resource->getAttribute('vm-name'));
+    list($stdout, $stderr) = $future->resolvex();
+    $status = trim($stdout).trim($stderr);
+    if (substr_count($status, 'shut off') > 0 ||
+        substr_count($status, 'Domain not found') > 0) {
+      throw new DrydockKVMVirtualMachineShutdownException();
+    }
+
+    // The machine isn't yet assigned an IP address, so yield for
+    // 10 seconds to let the VM spin up.
+    throw new PhabricatorWorkerYieldException(10);
+  }
+
+  private function testConnectivity(
+    DrydockBlueprint $blueprint,
+    DrydockResource $resource) {
+
+    $loaded_credential = PassphraseSSHKey::loadFromPHID(
+      $blueprint->getFieldValue('keypair'),
+      PhabricatorUser::getOmnipotentUser());
+
+    $ip_address = $resource->getAttribute('ip-address-for-connectivity');
+
+    $resource->logEvent(
+      DrydockKVMAttemptConnectionLogType::LOGCONST,
+      array(
+        'protocol' => $resource->getAttribute('protocol'),
+        'vm-name' => $resource->getAttribute('vm-name'),
+        'ip-address' => $ip_address,
+      ));
+
+    $interface = $this->getCommandInterfaceWithExplicitHost(
+      $blueprint,
+      $resource,
+      $resource->getAttribute('ip-address-for-connectivity'));
+    $interface->enableConnectionDebugging();
+    $interface->setExecTimeout(60);
+    if ($resource->getAttribute('protocol') !== 'winrm') {
+      $interface->setConnectTimeout(60);
+    }
+
+    try {
+      $future = $interface->getExecFuture('echo "test"');
+      $future->resolvex();
+      if ($future->getWasKilledByTimeout()) {
+        throw new CommandException(
+          'Command timed out.',
+          'echo "test"',
+          -255,
+          'timeout',
+          '');
+      }
+
+      $resource->unsetAttribute('ip-address-for-connectivity');
+      $resource->setAttribute(
+        'state',
+        self::RESOURCE_STATE_READY);
+      $resource->logEvent(
+        DrydockKVMSuccessfulConnectionLogType::LOGCONST,
+        array(
+          'protocol' => $resource->getAttribute('protocol'),
+          'vm-name' => $resource->getAttribute('vm-name'),
+          'ip-address' => $ip_address,
+        ));
+      $resource->save();
+
+      // We have moved to the next stage.
+      throw new PhabricatorWorkerYieldException(0);
+    } catch (CommandException $ex) {
+      // Fallthrough to perform the status check.
+      $resource->logEvent(
+        DrydockKVMFailedConnectionLogType::LOGCONST,
+        array(
+          'protocol' => $resource->getAttribute('protocol'),
+          'vm-name' => $resource->getAttribute('vm-name'),
+          'message' => $ex->getMessage(),
+          'stacktrace' => $ex->getTraceAsString(),
+          'stdout' => $ex->getStdout(),
+          'stderr' => $ex->getStderr(),
+        ));
+    }
+
+    $future = $this->getSSHFuture(
+      $blueprint,
+      $loaded_credential,
+      '/usr/bin/virsh domstate %s',
+      $resource->getAttribute('vm-name'));
+    list($stdout, $stderr) = $future->resolvex();
+    $status = trim($stdout).trim($stderr);
+    if (substr_count($status, 'shut off') > 0 ||
+        substr_count($status, 'Domain not found') > 0) {
+      throw new Exception('Virtual machine no longer exists or is shut down');
+    }
+
+    // The machine isn't yet serving remote connections, so yield for
+    // 10 seconds to let the VM start services.
+    throw new PhabricatorWorkerYieldException(10);
+  }
+
+  private function getSSHFuture(
+    DrydockBlueprint $blueprint,
+    $credential,
+    $command) {
+
+    $argv = func_get_args();
+    array_shift($argv);
+    array_shift($argv);
+    $future = new ExecFuture(
+      'ssh '.
+      '-o LogLevel=quiet '.
+      '-o StrictHostKeyChecking=no '.
+      '-o UserKnownHostsFile=/dev/null '.
+      '-o BatchMode=yes '.
+      '-p %s -i %P %P@%s -- %s',
+      $blueprint->getFieldValue('port'),
+      $credential->getKeyfileEnvelope(),
+      $credential->getUsernameEnvelope(),
+      $blueprint->getFieldValue('host'),
+      call_user_func_array('csprintf', $argv));
+    return $future;
+  }
+}
diff --git a/src/applications/drydock/blueprint/DrydockWorkingCopyBlueprintImplementation.php b/src/applications/drydock/blueprint/DrydockWorkingCopyBlueprintImplementation.php
--- a/src/applications/drydock/blueprint/DrydockWorkingCopyBlueprintImplementation.php
+++ b/src/applications/drydock/blueprint/DrydockWorkingCopyBlueprintImplementation.php
@@ -102,6 +102,18 @@
     DrydockResource $resource,
     DrydockLease $lease) {
 
+    $host_lease = id(new DrydockLeaseQuery())
+      ->setViewer(PhabricatorUser::getOmnipotentUser())
+      ->withPHIDs(array($resource->getAttribute('host.leasePHID')))
+      ->executeOne();
+    if ($host_lease === null || !$host_lease->isActive()) {
+      $resource->setStatus(DrydockResourceStatus::STATUS_BROKEN);
+      $resource->save();
+      $resource->scheduleUpdate();
+
+      throw new Exception('Host lease is no longer active');
+    }
+
     $lease
       ->needSlotLock($this->getLeaseSlotLock($resource))
       ->acquireOnResource($resource);
@@ -176,6 +188,7 @@
       $path = "{$root}/repo/{$directory}/";
 
       // TODO: Run these in parallel?
+      $interface->enableConnectionDebugging();
       $interface->execx(
         'git clone -- %s %s',
         (string)$repository->getCloneURIObject(),
diff --git a/src/applications/drydock/exception/DrydockKVMVirtualMachineShutdownException.php b/src/applications/drydock/exception/DrydockKVMVirtualMachineShutdownException.php
new file mode 100644
--- /dev/null
+++ b/src/applications/drydock/exception/DrydockKVMVirtualMachineShutdownException.php
@@ -0,0 +1,10 @@
+<?php
+
+final class DrydockKVMVirtualMachineShutdownException
+  extends PhabricatorWorkerYieldException {
+
+  public function __construct() {
+    parent::__construct(15);
+  }
+
+}
diff --git a/src/applications/drydock/interface/command/DrydockCommandInterface.php b/src/applications/drydock/interface/command/DrydockCommandInterface.php
--- a/src/applications/drydock/interface/command/DrydockCommandInterface.php
+++ b/src/applications/drydock/interface/command/DrydockCommandInterface.php
@@ -5,6 +5,10 @@
   const INTERFACE_TYPE = 'command';
 
   private $workingDirectoryStack = array();
+  private $sshProxyHost;
+  private $sshProxyPort;
+  private $sshProxyCredential;
+  private $debugConnection;
 
   public function pushWorkingDirectory($working_directory) {
     $this->workingDirectoryStack[] = $working_directory;
@@ -27,10 +31,49 @@
     return null;
   }
 
+  public function enableConnectionDebugging() {
+    $this->debugConnection = true;
+    return $this;
+  }
+
+  protected function getConnectionDebugging() {
+    return $this->debugConnection;
+  }
+
   final public function getInterfaceType() {
     return self::INTERFACE_TYPE;
   }
 
+  public function setSSHProxy($host, $port, $credential) {
+    $this->sshProxyHost = $host;
+    $this->sshProxyPort = $port;
+    $this->sshProxyCredential = $credential;
+    return $this;
+  }
+
+  public function getSSHProxyCommand() {
+    if ($this->sshProxyHost === null) {
+      return '';
+    }
+
+    return csprintf(
+      'ssh '.
+      '-o LogLevel=%s '.
+      '-o StrictHostKeyChecking=no '.
+      '-o UserKnownHostsFile=/dev/null '.
+      '-o BatchMode=yes '.
+      '-p %s -i %P %P@%s --',
+      $this->getConnectionDebugging() ? 'debug' : 'quiet',
+      $this->sshProxyPort,
+      $this->sshProxyCredential->getKeyfileEnvelope(),
+      $this->sshProxyCredential->getUsernameEnvelope(),
+      $this->sshProxyHost);
+  }
+
+  public function isSSHProxied() {
+    return $this->sshProxyHost !== null;
+  }
+
   final public function exec($command) {
     $argv = func_get_args();
     $exec = call_user_func_array(
diff --git a/src/applications/drydock/interface/command/DrydockSSHCommandInterface.php b/src/applications/drydock/interface/command/DrydockSSHCommandInterface.php
--- a/src/applications/drydock/interface/command/DrydockSSHCommandInterface.php
+++ b/src/applications/drydock/interface/command/DrydockSSHCommandInterface.php
@@ -4,6 +4,8 @@
 
   private $credential;
   private $connectTimeout;
+  private $execTimeout;
+  private $remoteKeyFile;
 
   private function loadCredential() {
     if ($this->credential === null) {
@@ -22,6 +24,11 @@
     return $this;
   }
 
+  public function setExecTimeout($timeout) {
+    $this->execTimeout = $timeout;
+    return $this;
+  }
+
   public function getExecFuture($command) {
     $credential = $this->loadCredential();
 
@@ -31,7 +38,8 @@
 
     $flags = array();
     $flags[] = '-o';
-    $flags[] = 'LogLevel=quiet';
+    $flags[] = 'LogLevel='.(
+      $this->getConnectionDebugging() ? 'debug' : 'quiet');
 
     $flags[] = '-o';
     $flags[] = 'StrictHostKeyChecking=no';
@@ -47,13 +55,52 @@
       $flags[] = 'ConnectTimeout='.$this->connectTimeout;
     }
 
-    return new ExecFuture(
+    $key = $credential->getKeyfileEnvelope();
+    if ($this->isSSHProxied()) {
+      if ($this->remoteKeyFile === null) {
+        $temp_name = '/tmp/'.Filesystem::readRandomCharacters(20).'.proxy';
+        $key_future = new ExecFuture(
+          'cat %P | %C %s',
+          $key,
+          $this->getSSHProxyCommand(),
+          csprintf(
+            'touch %s && chmod 0600 %s && cat - >%s',
+            $temp_name,
+            $temp_name,
+            $temp_name));
+        $key_future->resolvex();
+        $this->remoteKeyFile = new RemoteTempFile(
+          $temp_name,
+          new ExecFuture(
+            '%C %s',
+            $this->getSSHProxyCommand(),
+            csprintf('rm %s', $temp_name)));
+      }
+      $key = new PhutilOpaqueEnvelope((string)$this->remoteKeyFile);
+    }
+
+    $escaped_command = csprintf(
       'ssh %Ls -l %P -p %s -i %P %s -- %s',
       $flags,
       $credential->getUsernameEnvelope(),
       $this->getConfig('port'),
-      $credential->getKeyfileEnvelope(),
+      $key,
       $this->getConfig('host'),
       $full_command);
+
+    $proxy_cmd = $this->getSSHProxyCommand();
+    if ($proxy_cmd !== '') {
+      $future = new ExecFuture(
+        '%C %s',
+        $proxy_cmd,
+        $escaped_command);
+    } else {
+      $future = new ExecFuture(
+        '%C',
+        $escaped_command);
+    }
+
+    $future->setTimeout($this->execTimeout);
+    return $future;
   }
 }
diff --git a/src/applications/drydock/interface/command/RemoteTempFile.php b/src/applications/drydock/interface/command/RemoteTempFile.php
new file mode 100644
--- /dev/null
+++ b/src/applications/drydock/interface/command/RemoteTempFile.php
@@ -0,0 +1,31 @@
+<?php
+
+final class RemoteTempFile extends Phobject {
+
+  private $file;
+  private $destroyed = false;
+  private $destructionFuture;
+
+  public function __construct($filename, $destruction_future) {
+    $this->file = $filename;
+    $this->destructionFuture = $destruction_future;
+
+    register_shutdown_function(array($this, '__destruct'));
+  }
+
+  public function __toString() {
+    return $this->file;
+  }
+
+  public function __destruct() {
+    if ($this->destroyed) {
+      return;
+    }
+
+    $this->destructionFuture->resolve();
+
+    $this->file = null;
+    $this->destroyed = true;
+  }
+
+}
diff --git a/src/applications/drydock/logtype/DrydockKVMAllocatedImageLogType.php b/src/applications/drydock/logtype/DrydockKVMAllocatedImageLogType.php
new file mode 100644
--- /dev/null
+++ b/src/applications/drydock/logtype/DrydockKVMAllocatedImageLogType.php
@@ -0,0 +1,24 @@
+<?php
+
+final class DrydockKVMAllocatedImageLogType extends DrydockLogType {
+
+  const LOGCONST = 'kvm.image.allocated';
+
+  public function getLogTypeName() {
+    return pht('Allocated Image');
+  }
+
+  public function getLogTypeIcon(array $data) {
+    return '';
+  }
+
+  public function renderLog(array $data) {
+    $base_image = idx($data, 'base-image', null);
+    $new_image = idx($data, 'new-image', null);
+    return pht(
+      'Allocated new image from "%s" as "%s".',
+      $base_image,
+      $new_image);
+  }
+
+}
diff --git a/src/applications/drydock/logtype/DrydockKVMAllocatingImageLogType.php b/src/applications/drydock/logtype/DrydockKVMAllocatingImageLogType.php
new file mode 100644
--- /dev/null
+++ b/src/applications/drydock/logtype/DrydockKVMAllocatingImageLogType.php
@@ -0,0 +1,24 @@
+<?php
+
+final class DrydockKVMAllocatingImageLogType extends DrydockLogType {
+
+  const LOGCONST = 'kvm.image.allocating';
+
+  public function getLogTypeName() {
+    return pht('Allocating Image');
+  }
+
+  public function getLogTypeIcon(array $data) {
+    return '';
+  }
+
+  public function renderLog(array $data) {
+    $base_image = idx($data, 'base-image', null);
+    $new_image = idx($data, 'new-image', null);
+    return pht(
+      'Allocating new image from "%s" as "%s".',
+      $base_image,
+      $new_image);
+  }
+
+}
diff --git a/src/applications/drydock/logtype/DrydockKVMAssignedIPAddressLogType.php b/src/applications/drydock/logtype/DrydockKVMAssignedIPAddressLogType.php
new file mode 100644
--- /dev/null
+++ b/src/applications/drydock/logtype/DrydockKVMAssignedIPAddressLogType.php
@@ -0,0 +1,24 @@
+<?php
+
+final class DrydockKVMAssignedIPAddressLogType extends DrydockLogType {
+
+  const LOGCONST = 'kvm.network.assigned-ip-address';
+
+  public function getLogTypeName() {
+    return pht('Assigned IP Address');
+  }
+
+  public function getLogTypeIcon(array $data) {
+    return '';
+  }
+
+  public function renderLog(array $data) {
+    $vm_name = idx($data, 'vm-name', null);
+    $ip_address = idx($data, 'ip-address', null);
+    return pht(
+      'Virtual machine "%s" currently has an IP address of "%s".',
+      $vm_name,
+      $ip_address);
+  }
+
+}
diff --git a/src/applications/drydock/logtype/DrydockKVMAttemptConnectionLogType.php b/src/applications/drydock/logtype/DrydockKVMAttemptConnectionLogType.php
new file mode 100644
--- /dev/null
+++ b/src/applications/drydock/logtype/DrydockKVMAttemptConnectionLogType.php
@@ -0,0 +1,29 @@
+<?php
+
+final class DrydockKVMAttemptConnectionLogType extends DrydockLogType {
+
+  const LOGCONST = 'kvm.network.attempt-connection';
+
+  public function getLogTypeName() {
+    return pht('Attempting Connection');
+  }
+
+  public function getLogTypeIcon(array $data) {
+    return '';
+  }
+
+  public function renderLog(array $data) {
+    $vm_name = idx($data, 'vm-name', null);
+    if (idx($data, 'protocol', null) === 'winrm') {
+      $protocol_name = 'WinRM';
+    } else {
+      $protocol_name = 'SSH';
+    }
+
+    return pht(
+      'Attempting to connect to "%s" via %s',
+      $vm_name,
+      $protocol_name);
+  }
+
+}
diff --git a/src/applications/drydock/logtype/DrydockKVMCreatedVMLogType.php b/src/applications/drydock/logtype/DrydockKVMCreatedVMLogType.php
new file mode 100644
--- /dev/null
+++ b/src/applications/drydock/logtype/DrydockKVMCreatedVMLogType.php
@@ -0,0 +1,22 @@
+<?php
+
+final class DrydockKVMCreatedVMLogType extends DrydockLogType {
+
+  const LOGCONST = 'kvm.vm.created';
+
+  public function getLogTypeName() {
+    return pht('Created VM');
+  }
+
+  public function getLogTypeIcon(array $data) {
+    return '';
+  }
+
+  public function renderLog(array $data) {
+    $vm_name = idx($data, 'vm-name', null);
+    return pht(
+      'Created new virtual machine "%s".',
+      $vm_name);
+  }
+
+}
diff --git a/src/applications/drydock/logtype/DrydockKVMCreatingVMLogType.php b/src/applications/drydock/logtype/DrydockKVMCreatingVMLogType.php
new file mode 100644
--- /dev/null
+++ b/src/applications/drydock/logtype/DrydockKVMCreatingVMLogType.php
@@ -0,0 +1,22 @@
+<?php
+
+final class DrydockKVMCreatingVMLogType extends DrydockLogType {
+
+  const LOGCONST = 'kvm.vm.creating';
+
+  public function getLogTypeName() {
+    return pht('Creating VM');
+  }
+
+  public function getLogTypeIcon(array $data) {
+    return '';
+  }
+
+  public function renderLog(array $data) {
+    $vm_name = idx($data, 'vm-name', null);
+    return pht(
+      'Creating new virtual machine "%s".',
+      $vm_name);
+  }
+
+}
diff --git a/src/applications/drydock/logtype/DrydockKVMCredentialUsageLogType.php b/src/applications/drydock/logtype/DrydockKVMCredentialUsageLogType.php
new file mode 100644
--- /dev/null
+++ b/src/applications/drydock/logtype/DrydockKVMCredentialUsageLogType.php
@@ -0,0 +1,26 @@
+<?php
+
+final class DrydockKVMCredentialUsageLogType extends DrydockLogType {
+
+  const LOGCONST = 'kvm.credential.usage';
+
+  public function getLogTypeName() {
+    return pht('Credential Usage');
+  }
+
+  public function getLogTypeIcon(array $data) {
+    return '';
+  }
+
+  public function renderLog(array $data) {
+    $phid = idx($data, 'phid', null);
+    if ($phid) {
+      return pht(
+        'Using credential %s to allocate.',
+        $phid);
+    } else {
+      return pht('No associated credential data.');
+    }
+  }
+
+}
diff --git a/src/applications/drydock/logtype/DrydockKVMFailedConnectionLogType.php b/src/applications/drydock/logtype/DrydockKVMFailedConnectionLogType.php
new file mode 100644
--- /dev/null
+++ b/src/applications/drydock/logtype/DrydockKVMFailedConnectionLogType.php
@@ -0,0 +1,48 @@
+<?php
+
+final class DrydockKVMFailedConnectionLogType extends DrydockLogType {
+
+  const LOGCONST = 'kvm.network.failed-connection';
+
+  public function getLogTypeName() {
+    return pht('Failed Connection');
+  }
+
+  public function getLogTypeIcon(array $data) {
+    return '';
+  }
+
+  public function renderLog(array $data) {
+    $vm_name = idx($data, 'vm-name', null);
+    $stdout = idx($data, 'stdout', null);
+    $stderr = idx($data, 'stderr', null);
+
+    $stdout = phutil_split_lines($stdout);
+    $stderr = phutil_split_lines($stderr);
+
+    $formatted_stdout = array();
+    $formatted_stderr = array();
+    foreach ($stdout as $line) {
+      $formatted_stdout[] = $line;
+      $formatted_stdout[] = phutil_tag('br', array(), null);
+    }
+    foreach ($stderr as $line) {
+      $formatted_stderr[] = $line;
+      $formatted_stderr[] = phutil_tag('br', array(), null);
+    }
+
+    array_pop($formatted_stderr);
+
+    return array(
+      pht('Unable to connect to "%s":', $vm_name),
+      phutil_tag('br', array(), null),
+      pht('STDOUT'),
+      phutil_tag('br', array(), null),
+      $formatted_stdout,
+      pht('STDERR'),
+      phutil_tag('br', array(), null),
+      $formatted_stderr,
+    );
+  }
+
+}
diff --git a/src/applications/drydock/logtype/DrydockKVMRemovedImageLogType.php b/src/applications/drydock/logtype/DrydockKVMRemovedImageLogType.php
new file mode 100644
--- /dev/null
+++ b/src/applications/drydock/logtype/DrydockKVMRemovedImageLogType.php
@@ -0,0 +1,22 @@
+<?php
+
+final class DrydockKVMRemovedImageLogType extends DrydockLogType {
+
+  const LOGCONST = 'kvm.image.removed';
+
+  public function getLogTypeName() {
+    return pht('Removed Image');
+  }
+
+  public function getLogTypeIcon(array $data) {
+    return '';
+  }
+
+  public function renderLog(array $data) {
+    $image_name = idx($data, 'image-name', null);
+    return pht(
+      'Removed image "%s".',
+      $image_name);
+  }
+
+}
diff --git a/src/applications/drydock/logtype/DrydockKVMRemovingImageLogType.php b/src/applications/drydock/logtype/DrydockKVMRemovingImageLogType.php
new file mode 100644
--- /dev/null
+++ b/src/applications/drydock/logtype/DrydockKVMRemovingImageLogType.php
@@ -0,0 +1,22 @@
+<?php
+
+final class DrydockKVMRemovingImageLogType extends DrydockLogType {
+
+  const LOGCONST = 'kvm.image.removing';
+
+  public function getLogTypeName() {
+    return pht('Removing Image');
+  }
+
+  public function getLogTypeIcon(array $data) {
+    return '';
+  }
+
+  public function renderLog(array $data) {
+    $image_name = idx($data, 'image-name', null);
+    return pht(
+      'Removing image "%s"...',
+      $image_name);
+  }
+
+}
diff --git a/src/applications/drydock/logtype/DrydockKVMRetrievedImagePathLogType.php b/src/applications/drydock/logtype/DrydockKVMRetrievedImagePathLogType.php
new file mode 100644
--- /dev/null
+++ b/src/applications/drydock/logtype/DrydockKVMRetrievedImagePathLogType.php
@@ -0,0 +1,24 @@
+<?php
+
+final class DrydockKVMRetrievedImagePathLogType extends DrydockLogType {
+
+  const LOGCONST = 'kvm.image.retrieved-path';
+
+  public function getLogTypeName() {
+    return pht('Retrieved Image Path');
+  }
+
+  public function getLogTypeIcon(array $data) {
+    return '';
+  }
+
+  public function renderLog(array $data) {
+    $new_image = idx($data, 'new-image', null);
+    $path = idx($data, 'path', null);
+    return pht(
+      'Retrieved image path of "%s" as "%s".',
+      $new_image,
+      $path);
+  }
+
+}
diff --git a/src/applications/drydock/logtype/DrydockKVMRetrievedMACAddressLogType.php b/src/applications/drydock/logtype/DrydockKVMRetrievedMACAddressLogType.php
new file mode 100644
--- /dev/null
+++ b/src/applications/drydock/logtype/DrydockKVMRetrievedMACAddressLogType.php
@@ -0,0 +1,24 @@
+<?php
+
+final class DrydockKVMRetrievedMACAddressLogType extends DrydockLogType {
+
+  const LOGCONST = 'kvm.network.retrieved-mac-address';
+
+  public function getLogTypeName() {
+    return pht('Retrieved MAC Address');
+  }
+
+  public function getLogTypeIcon(array $data) {
+    return '';
+  }
+
+  public function renderLog(array $data) {
+    $vm_name = idx($data, 'vm-name', null);
+    $mac_address = idx($data, 'mac-address', null);
+    return pht(
+      'MAC address of virtual machine "%s" is "%s".',
+      $vm_name,
+      $mac_address);
+  }
+
+}
diff --git a/src/applications/drydock/logtype/DrydockKVMRetrievingImagePathLogType.php b/src/applications/drydock/logtype/DrydockKVMRetrievingImagePathLogType.php
new file mode 100644
--- /dev/null
+++ b/src/applications/drydock/logtype/DrydockKVMRetrievingImagePathLogType.php
@@ -0,0 +1,22 @@
+<?php
+
+final class DrydockKVMRetrievingImagePathLogType extends DrydockLogType {
+
+  const LOGCONST = 'kvm.image.retrieving-path';
+
+  public function getLogTypeName() {
+    return pht('Retrieving Image Path');
+  }
+
+  public function getLogTypeIcon(array $data) {
+    return '';
+  }
+
+  public function renderLog(array $data) {
+    $new_image = idx($data, 'new-image', null);
+    return pht(
+      'Retrieving path to new image "%s".',
+      $new_image);
+  }
+
+}
diff --git a/src/applications/drydock/logtype/DrydockKVMRetrievingMACAddressLogType.php b/src/applications/drydock/logtype/DrydockKVMRetrievingMACAddressLogType.php
new file mode 100644
--- /dev/null
+++ b/src/applications/drydock/logtype/DrydockKVMRetrievingMACAddressLogType.php
@@ -0,0 +1,22 @@
+<?php
+
+final class DrydockKVMRetrievingMACAddressLogType extends DrydockLogType {
+
+  const LOGCONST = 'kvm.network.retrieving-mac-address';
+
+  public function getLogTypeName() {
+    return pht('Retrieving MAC Address');
+  }
+
+  public function getLogTypeIcon(array $data) {
+    return '';
+  }
+
+  public function renderLog(array $data) {
+    $vm_name = idx($data, 'vm-name', null);
+    return pht(
+      'Retrieving MAC address of virtual machine "%s".',
+      $vm_name);
+  }
+
+}
diff --git a/src/applications/drydock/logtype/DrydockKVMShutdownVMLogType.php b/src/applications/drydock/logtype/DrydockKVMShutdownVMLogType.php
new file mode 100644
--- /dev/null
+++ b/src/applications/drydock/logtype/DrydockKVMShutdownVMLogType.php
@@ -0,0 +1,22 @@
+<?php
+
+final class DrydockKVMShutdownVMLogType extends DrydockLogType {
+
+  const LOGCONST = 'kvm.vm.shutdown';
+
+  public function getLogTypeName() {
+    return pht('Shutdown VM');
+  }
+
+  public function getLogTypeIcon(array $data) {
+    return '';
+  }
+
+  public function renderLog(array $data) {
+    $vm_name = idx($data, 'vm-name', null);
+    return pht(
+      'Shut down of virtual machine "%s" complete.',
+      $vm_name);
+  }
+
+}
diff --git a/src/applications/drydock/logtype/DrydockKVMShuttingDownVMLogType.php b/src/applications/drydock/logtype/DrydockKVMShuttingDownVMLogType.php
new file mode 100644
--- /dev/null
+++ b/src/applications/drydock/logtype/DrydockKVMShuttingDownVMLogType.php
@@ -0,0 +1,22 @@
+<?php
+
+final class DrydockKVMShuttingDownVMLogType extends DrydockLogType {
+
+  const LOGCONST = 'kvm.vm.shutting-down';
+
+  public function getLogTypeName() {
+    return pht('Shutting Down VM');
+  }
+
+  public function getLogTypeIcon(array $data) {
+    return '';
+  }
+
+  public function renderLog(array $data) {
+    $vm_name = idx($data, 'vm-name', null);
+    return pht(
+      'Shutting down virtual machine "%s"...',
+      $vm_name);
+  }
+
+}
diff --git a/src/applications/drydock/logtype/DrydockKVMSuccessfulConnectionLogType.php b/src/applications/drydock/logtype/DrydockKVMSuccessfulConnectionLogType.php
new file mode 100644
--- /dev/null
+++ b/src/applications/drydock/logtype/DrydockKVMSuccessfulConnectionLogType.php
@@ -0,0 +1,23 @@
+<?php
+
+final class DrydockKVMSuccessfulConnectionLogType extends DrydockLogType {
+
+  const LOGCONST = 'kvm.network.successful-connection';
+
+  public function getLogTypeName() {
+    return pht('Successful Connection');
+  }
+
+  public function getLogTypeIcon(array $data) {
+    return '';
+  }
+
+  public function renderLog(array $data) {
+    $vm_name = idx($data, 'vm-name', null);
+
+    return pht(
+      'Connected successfully to "%s"',
+      $vm_name);
+  }
+
+}
diff --git a/src/applications/drydock/logtype/DrydockKVMUncleanShutdownLogType.php b/src/applications/drydock/logtype/DrydockKVMUncleanShutdownLogType.php
new file mode 100644
--- /dev/null
+++ b/src/applications/drydock/logtype/DrydockKVMUncleanShutdownLogType.php
@@ -0,0 +1,51 @@
+<?php
+
+final class DrydockKVMUncleanShutdownLogType extends DrydockLogType {
+
+  const LOGCONST = 'kvm.vm.unclean-shutdown';
+
+  public function getLogTypeName() {
+    return pht('Unclean Shutdown');
+  }
+
+  public function getLogTypeIcon(array $data) {
+    return '';
+  }
+
+  public function renderLog(array $data) {
+    $vm_name = idx($data, 'vm-name', null);
+    $image_name = idx($data, 'image-name', null);
+    $stdout = idx($data, 'stdout', null);
+    $stderr = idx($data, 'stderr', null);
+
+    $stdout = phutil_split_lines($stdout);
+    $stderr = phutil_split_lines($stderr);
+
+    $formatted_stdout = array();
+    $formatted_stderr = array();
+    foreach ($stdout as $line) {
+      $formatted_stdout[] = $line;
+      $formatted_stdout[] = phutil_tag('br', array(), null);
+    }
+    foreach ($stderr as $line) {
+      $formatted_stderr[] = $line;
+      $formatted_stderr[] = phutil_tag('br', array(), null);
+    }
+
+    array_pop($formatted_stderr);
+
+    return array(
+      pht(
+        'Unable to cleanly shutdown VM "%s" (using image "%s").  Was the VM '.
+        'already shut down?', $vm_name, $image_name),
+      phutil_tag('br', array(), null),
+      pht('STDOUT'),
+      phutil_tag('br', array(), null),
+      $formatted_stdout,
+      pht('STDERR'),
+      phutil_tag('br', array(), null),
+      $formatted_stderr,
+    );
+  }
+
+}
diff --git a/src/applications/drydock/logtype/DrydockKVMUnexpectedVMShutdownLogType.php b/src/applications/drydock/logtype/DrydockKVMUnexpectedVMShutdownLogType.php
new file mode 100644
--- /dev/null
+++ b/src/applications/drydock/logtype/DrydockKVMUnexpectedVMShutdownLogType.php
@@ -0,0 +1,23 @@
+<?php
+
+final class DrydockKVMUnexpectedVMShutdownLogType extends DrydockLogType {
+
+  const LOGCONST = 'kvm.network.vm-shutdown';
+
+  public function getLogTypeName() {
+    return pht('Unexpected VM Shutdown');
+  }
+
+  public function getLogTypeIcon(array $data) {
+    return 'fa-times red';
+  }
+
+  public function renderLog(array $data) {
+    $vm_name = idx($data, 'vm-name', null);
+
+    return pht(
+      'Virtual machine "%s" no longer exists or is shut down',
+      $vm_name);
+  }
+
+}
diff --git a/src/applications/drydock/logtype/DrydockKVMWaitingForConnectivityLogType.php b/src/applications/drydock/logtype/DrydockKVMWaitingForConnectivityLogType.php
new file mode 100644
--- /dev/null
+++ b/src/applications/drydock/logtype/DrydockKVMWaitingForConnectivityLogType.php
@@ -0,0 +1,28 @@
+<?php
+
+final class DrydockKVMWaitingForConnectivityLogType extends DrydockLogType {
+
+  const LOGCONST = 'kvm.network.waiting-for-connectivity';
+
+  public function getLogTypeName() {
+    return pht('Waiting for Connectivity');
+  }
+
+  public function getLogTypeIcon(array $data) {
+    return '';
+  }
+
+  public function renderLog(array $data) {
+    $protocol_name = '';
+    if (idx($data, 'protocol', null) === 'winrm') {
+      $protocol_name = 'WinRM';
+    } else {
+      $protocol_name = 'SSH';
+    }
+
+    return pht(
+      'Waiting for a successful %s connection',
+      $protocol_name);
+  }
+
+}
diff --git a/src/applications/drydock/logtype/DrydockKVMWaitingForIPAddressLogType.php b/src/applications/drydock/logtype/DrydockKVMWaitingForIPAddressLogType.php
new file mode 100644
--- /dev/null
+++ b/src/applications/drydock/logtype/DrydockKVMWaitingForIPAddressLogType.php
@@ -0,0 +1,20 @@
+<?php
+
+final class DrydockKVMWaitingForIPAddressLogType extends DrydockLogType {
+
+  const LOGCONST = 'kvm.network.waiting-for-ip-address';
+
+  public function getLogTypeName() {
+    return pht('Waiting for IP Address');
+  }
+
+  public function getLogTypeIcon(array $data) {
+    return '';
+  }
+
+  public function renderLog(array $data) {
+    return pht(
+      'Waiting until the virtual machine is allocated an IP address...');
+  }
+
+}
diff --git a/src/applications/drydock/logtype/DrydockKVMWinRMCredentialUsageLogType.php b/src/applications/drydock/logtype/DrydockKVMWinRMCredentialUsageLogType.php
new file mode 100644
--- /dev/null
+++ b/src/applications/drydock/logtype/DrydockKVMWinRMCredentialUsageLogType.php
@@ -0,0 +1,26 @@
+<?php
+
+final class DrydockKVMWinRMCredentialUsageLogType extends DrydockLogType {
+
+  const LOGCONST = 'kvm.credential.winrm.usage';
+
+  public function getLogTypeName() {
+    return pht('WinRM Credential Usage');
+  }
+
+  public function getLogTypeIcon(array $data) {
+    return '';
+  }
+
+  public function renderLog(array $data) {
+    $phid = idx($data, 'phid', null);
+    if ($phid) {
+      return pht(
+        'Using credential %s to authenticate over WinRM.',
+        $phid);
+    } else {
+      return pht('No associated credential data.');
+    }
+  }
+
+}
diff --git a/src/applications/drydock/logtype/DrydockLeaseActivationFailureLogType.php b/src/applications/drydock/logtype/DrydockLeaseActivationFailureLogType.php
--- a/src/applications/drydock/logtype/DrydockLeaseActivationFailureLogType.php
+++ b/src/applications/drydock/logtype/DrydockLeaseActivationFailureLogType.php
@@ -15,8 +15,40 @@
   public function renderLog(array $data) {
     $class = idx($data, 'class');
     $message = idx($data, 'message');
+    $stdout = idx($data, 'stdout', null);
+    $stderr = idx($data, 'stderr', null);
 
-    return pht('Lease activation failed: [%s] %s', $class, $message);
+    $primary = pht('Lease activation failed: [%s] %s', $class, $message);
+    if ($stdout !== null || $stderr !== null) {
+      $stdout = phutil_split_lines($stdout);
+      $stderr = phutil_split_lines($stderr);
+
+      $formatted_stdout = array();
+      $formatted_stderr = array();
+      foreach ($stdout as $line) {
+        $formatted_stdout[] = $line;
+        $formatted_stdout[] = phutil_tag('br', array(), null);
+      }
+      foreach ($stderr as $line) {
+        $formatted_stderr[] = $line;
+        $formatted_stderr[] = phutil_tag('br', array(), null);
+      }
+
+      array_pop($formatted_stderr);
+
+      $primary = array(
+        $primary,
+        phutil_tag('br', array(), null),
+        pht('STDOUT'),
+        phutil_tag('br', array(), null),
+        $formatted_stdout,
+        pht('STDERR'),
+        phutil_tag('br', array(), null),
+        $formatted_stderr,
+      );
+    }
+
+    return $primary;
   }
 
 }
diff --git a/src/applications/drydock/logtype/DrydockResourceActivationFailureLogType.php b/src/applications/drydock/logtype/DrydockResourceActivationFailureLogType.php
--- a/src/applications/drydock/logtype/DrydockResourceActivationFailureLogType.php
+++ b/src/applications/drydock/logtype/DrydockResourceActivationFailureLogType.php
@@ -15,8 +15,40 @@
   public function renderLog(array $data) {
     $class = idx($data, 'class');
     $message = idx($data, 'message');
+    $stdout = idx($data, 'stdout', null);
+    $stderr = idx($data, 'stderr', null);
 
-    return pht('Resource activation failed: [%s] %s', $class, $message);
+    $primary = pht('Resource activation failed: [%s] %s', $class, $message);
+    if ($stdout !== null || $stderr !== null) {
+      $stdout = phutil_split_lines($stdout);
+      $stderr = phutil_split_lines($stderr);
+
+      $formatted_stdout = array();
+      $formatted_stderr = array();
+      foreach ($stdout as $line) {
+        $formatted_stdout[] = $line;
+        $formatted_stdout[] = phutil_tag('br', array(), null);
+      }
+      foreach ($stderr as $line) {
+        $formatted_stderr[] = $line;
+        $formatted_stderr[] = phutil_tag('br', array(), null);
+      }
+
+      array_pop($formatted_stderr);
+
+      $primary = array(
+        $primary,
+        phutil_tag('br', array(), null),
+        pht('STDOUT'),
+        phutil_tag('br', array(), null),
+        $formatted_stdout,
+        pht('STDERR'),
+        phutil_tag('br', array(), null),
+        $formatted_stderr,
+      );
+    }
+
+    return $primary;
   }
 
 }
diff --git a/src/applications/drydock/storage/DrydockResource.php b/src/applications/drydock/storage/DrydockResource.php
--- a/src/applications/drydock/storage/DrydockResource.php
+++ b/src/applications/drydock/storage/DrydockResource.php
@@ -66,6 +66,11 @@
     return $this;
   }
 
+  public function unsetAttribute($key) {
+    unset($this->attributes[$key]);
+    return $this;
+  }
+
   public function getCapability($key, $default = null) {
     return idx($this->capbilities, $key, $default);
   }
diff --git a/src/applications/drydock/worker/DrydockLeaseUpdateWorker.php b/src/applications/drydock/worker/DrydockLeaseUpdateWorker.php
--- a/src/applications/drydock/worker/DrydockLeaseUpdateWorker.php
+++ b/src/applications/drydock/worker/DrydockLeaseUpdateWorker.php
@@ -757,6 +757,8 @@
       array(
         'class' => get_class($ex),
         'message' => $ex->getMessage(),
+        'stdout' => ($ex instanceof CommandException) ? $ex->getStdout() : null,
+        'stderr' => ($ex instanceof CommandException) ? $ex->getStderr() : null,
       ));
 
     $lease->awakenTasks();
diff --git a/src/applications/drydock/worker/DrydockResourceUpdateWorker.php b/src/applications/drydock/worker/DrydockResourceUpdateWorker.php
--- a/src/applications/drydock/worker/DrydockResourceUpdateWorker.php
+++ b/src/applications/drydock/worker/DrydockResourceUpdateWorker.php
@@ -251,7 +251,9 @@
       DrydockResourceActivationFailureLogType::LOGCONST,
       array(
         'class' => get_class($ex),
-        'message' => $ex->getMessage(),
+        'message' => $ex->getMessage().$ex->getTraceAsString(),
+        'stdout' => ($ex instanceof CommandException) ? $ex->getStdout() : null,
+        'stderr' => ($ex instanceof CommandException) ? $ex->getStderr() : null,
       ));
 
     throw new PhabricatorWorkerPermanentFailureException(
diff --git a/src/applications/harbormaster/step/HarbormasterLeaseHostBuildStepImplementation.php b/src/applications/harbormaster/step/HarbormasterLeaseHostBuildStepImplementation.php
--- a/src/applications/harbormaster/step/HarbormasterLeaseHostBuildStepImplementation.php
+++ b/src/applications/harbormaster/step/HarbormasterLeaseHostBuildStepImplementation.php
@@ -12,38 +12,84 @@
   }
 
   public function getBuildStepGroupKey() {
-    return HarbormasterPrototypeBuildStepGroup::GROUPKEY;
+    return HarbormasterDrydockBuildStepGroup::GROUPKEY;
   }
 
   public function execute(
     HarbormasterBuild $build,
     HarbormasterBuildTarget $build_target) {
+    $viewer = PhabricatorUser::getOmnipotentUser();
 
     $settings = $this->getSettings();
 
-    // Create the lease.
-    $lease = id(new DrydockLease())
-      ->setResourceType('host')
-      ->setOwnerPHID($build_target->getPHID())
-      ->setAttributes(
+    // TODO: We should probably have a separate temporary storage area for
+    // execution stuff that doesn't step on configuration state?
+    $lease_phid = $build_target->getDetail('exec.leasePHID');
+
+    if ($lease_phid) {
+      $lease = id(new DrydockLeaseQuery())
+        ->setViewer($viewer)
+        ->withPHIDs(array($lease_phid))
+        ->executeOne();
+      if (!$lease) {
+        throw new PhabricatorWorkerPermanentFailureException(
+          pht(
+            'Lease "%s" could not be loaded.',
+            $lease_phid));
+      }
+    } else {
+      $working_copy_type =
+        id(new DrydockAlmanacServiceHostBlueprintImplementation())
+          ->getType();
+
+      $allowed_phids = $build_target->getFieldValue('blueprintPHIDs');
+      if (!is_array($allowed_phids)) {
+        $allowed_phids = array();
+      }
+      $authorizing_phid = $build_target->getBuildStep()->getPHID();
+
+      $lease = DrydockLease::initializeNewLease()
+        ->setResourceType($working_copy_type)
+        ->setOwnerPHID($build_target->getPHID())
+        ->setAuthorizingPHID($authorizing_phid)
+        ->setAllowedBlueprintPHIDs($allowed_phids);
+
+      $task_id = $this->getCurrentWorkerTaskID();
+      if ($task_id) {
+        $lease->setAwakenTaskIDs(array($task_id));
+      }
+
+      // TODO: Maybe add a method to mark artifacts like this as pending?
+
+      // Create the artifact now so that the lease is always disposed of, even
+      // if this target is aborted.
+      $build_target->createArtifact(
+        $viewer,
+        $settings['name'],
+        HarbormasterWorkingCopyArtifact::ARTIFACTCONST,
         array(
-          'platform' => $settings['platform'],
-        ))
-      ->queueForActivation();
-
-    // Wait until the lease is fulfilled.
-    // TODO: This will throw an exception if the lease can't be fulfilled;
-    // we should treat that as build failure not build error.
-    $lease->waitUntilActive();
-
-    // Create the associated artifact.
-    $artifact = $build_target->createArtifact(
-      PhabricatorUser::getOmnipotentUser(),
-      $settings['name'],
-      HarbormasterHostArtifact::ARTIFACTCONST,
-      array(
-        'drydockLeasePHID' => $lease->getPHID(),
-      ));
+          'drydockLeasePHID' => $lease->getPHID(),
+        ));
+
+      $lease->queueForActivation();
+
+      $build_target
+        ->setDetail('exec.leasePHID', $lease->getPHID())
+        ->save();
+    }
+
+    if ($lease->isActivating()) {
+      // TODO: Smart backoff?
+      throw new PhabricatorWorkerYieldException(15);
+    }
+
+    if (!$lease->isActive()) {
+      // TODO: We could just forget about this lease and retry?
+      throw new PhabricatorWorkerPermanentFailureException(
+        pht(
+          'Lease "%s" never activated.',
+          $lease->getPHID()));
+    }
   }
 
   public function getArtifactOutputs() {
@@ -63,9 +109,9 @@
         'type' => 'text',
         'required' => true,
       ),
-      'platform' => array(
-        'name' => pht('Platform'),
-        'type' => 'text',
+      'blueprintPHIDs' => array(
+        'name' => pht('Use Blueprints'),
+        'type' => 'blueprints',
         'required' => true,
       ),
     );
diff --git a/src/infrastructure/daemon/workers/exception/PhabricatorWorkerYieldException.php b/src/infrastructure/daemon/workers/exception/PhabricatorWorkerYieldException.php
--- a/src/infrastructure/daemon/workers/exception/PhabricatorWorkerYieldException.php
+++ b/src/infrastructure/daemon/workers/exception/PhabricatorWorkerYieldException.php
@@ -6,7 +6,7 @@
  * If a worker throws this exception while processing a task, the task will be
  * pushed toward the back of the queue and tried again later.
  */
-final class PhabricatorWorkerYieldException extends Exception {
+class PhabricatorWorkerYieldException extends Exception {
 
   private $duration;