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;