Page MenuHomePhabricator

D14583.diff
No OneTemporary

D14583.diff

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;

File Metadata

Mime Type
text/plain
Expires
Sun, May 12, 7:45 AM (3 w, 1 d ago)
Storage Engine
amazon-s3
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
phabricator/secure/p3/xy/zl4ky4pzowqwccft
Default Alt Text
D14583.diff (70 KB)

Event Timeline