Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F16450941
D10248.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
37 KB
Referenced Files
None
Subscribers
None
D10248.diff
View Options
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
@@ -566,6 +566,7 @@
'DoorkeeperTagView' => 'applications/doorkeeper/view/DoorkeeperTagView.php',
'DoorkeeperTagsController' => 'applications/doorkeeper/controller/DoorkeeperTagsController.php',
'DrydockAllocatorWorker' => 'applications/drydock/worker/DrydockAllocatorWorker.php',
+ 'DrydockAmazonEC2HostBlueprintImplementation' => 'applications/drydock/blueprint/DrydockAmazonEC2HostBlueprintImplementation.php',
'DrydockApacheWebrootInterface' => 'applications/drydock/interface/webroot/DrydockApacheWebrootInterface.php',
'DrydockBlueprint' => 'applications/drydock/storage/DrydockBlueprint.php',
'DrydockBlueprintController' => 'applications/drydock/controller/DrydockBlueprintController.php',
@@ -3305,6 +3306,7 @@
'DoorkeeperTagView' => 'AphrontView',
'DoorkeeperTagsController' => 'PhabricatorController',
'DrydockAllocatorWorker' => 'PhabricatorWorker',
+ 'DrydockAmazonEC2HostBlueprintImplementation' => 'DrydockBlueprintImplementation',
'DrydockApacheWebrootInterface' => 'DrydockWebrootInterface',
'DrydockBlueprint' => array(
'DrydockDAO',
diff --git a/src/applications/drydock/blueprint/DrydockAmazonEC2HostBlueprintImplementation.php b/src/applications/drydock/blueprint/DrydockAmazonEC2HostBlueprintImplementation.php
new file mode 100644
--- /dev/null
+++ b/src/applications/drydock/blueprint/DrydockAmazonEC2HostBlueprintImplementation.php
@@ -0,0 +1,494 @@
+<?php
+
+final class DrydockAmazonEC2HostBlueprintImplementation
+ extends DrydockBlueprintImplementation {
+
+ public function isEnabled() {
+ // This blueprint is only available if the Amazon EC2 keys are configured.
+ return
+ PhabricatorEnv::getEnvConfig('amazon-ec2.access-key') &&
+ PhabricatorEnv::getEnvConfig('amazon-ec2.secret-key');
+ }
+
+ public function getBlueprintName() {
+ return pht('Amazon EC2 Remote Hosts');
+ }
+
+ public function getDescription() {
+ return pht(
+ 'Allows Drydock to allocate and execute commands on '.
+ 'Amazon EC2 remote hosts.');
+ }
+
+ public function canAllocateMoreResources(array $pool) {
+ $max_count = $this->getDetail('max-count');
+ return count($pool) < $max_count;
+ }
+
+ private function getAWSEC2Future() {
+ return id(new PhutilAWSEC2Future())
+ ->setAWSKeys(
+ PhabricatorEnv::getEnvConfig('amazon-ec2.access-key'),
+ PhabricatorEnv::getEnvConfig('amazon-ec2.secret-key'))
+ ->setAWSRegion($this->getDetail('region'));
+ }
+
+ private function getAWSKeyPairName() {
+ return 'phabricator-'.$this->getDetail('keypair');
+ }
+
+ protected function executeAllocateResource(DrydockLease $lease) {
+
+ // We need to retrieve this as we need to use it for both importing the
+ // key and looking up the ID for the resource attributes.
+ $credential = id(new PassphraseCredentialQuery())
+ ->setViewer(PhabricatorUser::getOmnipotentUser())
+ ->withPHIDs(array($this->getDetail('keypair')))
+ ->executeOne();
+
+ try {
+ $existing_keys = $this->getAWSEC2Future()
+ ->setRawAWSQuery(
+ 'DescribeKeyPairs',
+ array(
+ 'KeyName.0' => $this->getAWSKeyPairName()))
+ ->resolve();
+ } catch (PhutilAWSException $ex) {
+ // The key pair does not exist, so we need to import it.
+
+ $type = PassphraseCredentialType::getTypeByConstant(
+ $credential->getCredentialType());
+ if (!$type) {
+ throw new Exception(pht('Credential has invalid type "%s"!', $type));
+ }
+
+ if (!$type->hasPublicKey()) {
+ throw new Exception(pht('Credential has no public key!'));
+ }
+
+ $public_key = $type->getPublicKey(
+ PhabricatorUser::getOmnipotentUser(),
+ $credential);
+
+ $this->getAWSEC2Future()
+ ->setRawAWSQuery(
+ 'ImportKeyPair',
+ array(
+ 'KeyName' => $this->getAWSKeyPairName(),
+ 'PublicKeyMaterial' => base64_encode($public_key)))
+ ->resolve();
+ }
+
+ $settings = array(
+ 'ImageId' => $this->getDetail('ami'),
+ 'MinCount' => 1,
+ 'MaxCount' => 1,
+ 'KeyName' => $this->getAWSKeyPairName(),
+ 'InstanceType' => $this->getDetail('size'),
+ 'SubnetId' => $this->getDetail('subnet-id')
+ );
+
+ $i = 0;
+ $security_groups = explode(',', $this->getDetail('security-group-ids'));
+ foreach ($security_groups as $security_group) {
+ $settings['SecurityGroupId.'.$i] = $security_group;
+ $i++;
+ }
+
+ $result = $this->getAWSEC2Future()
+ ->setRawAWSQuery(
+ 'RunInstances',
+ $settings)
+ ->resolve();
+
+ $instance = $result->instancesSet->item[0];
+ $instance_id = (string)$instance->instanceId;
+
+ // Allocate the resource and place it into Pending status while
+ // we wait for the instance to start.
+ $blueprint = $this->getInstance();
+ $resource = id(new DrydockResource())
+ ->setBlueprintPHID($blueprint->getPHID())
+ ->setType($blueprint->getImplementation()->getType())
+ ->setName($instance_id)
+ ->setStatus(DrydockResourceStatus::STATUS_PENDING)
+ ->setAttributes(array(
+ 'instance-id' => $instance_id,
+ 'platform' => $this->getDetail('platform'),
+ 'path' => $this->getDetail('storage-path'),
+ 'credential' => $credential->getID()))
+ ->save();
+
+ // Wait until the instance has started.
+ while (true) {
+ try {
+ $result = $this->getAWSEC2Future()
+ ->setRawAWSQuery(
+ 'DescribeInstances',
+ array(
+ 'InstanceId.0' => $instance_id))
+ ->resolve();
+
+ $reservation = $result->reservationSet->item[0];
+ $instance = $reservation->instancesSet->item[0];
+ $instance_state = (string)$instance->instanceState->name;
+
+ if ($instance_state === 'pending') {
+ sleep(5);
+ continue;
+ } else if ($instance_state === 'running') {
+ break;
+ } else {
+ // Instance is shutting down or is otherwise terminated.
+ throw new Exception(
+ 'Allocated instance, but ended up in unexpected state \''.
+ $instance_state.'\'!');
+ }
+ } catch (PhutilAWSException $ex) {
+ // TODO: This can happen because the instance doesn't exist yet, but
+ // we should check specifically for that error.
+ sleep(5);
+ continue;
+ }
+ }
+
+ // Calculate the IP address of the instance.
+ $address = '';
+ if ($this->getDetail('allocate-elastic-ip')) {
+ $resource->setAttribute('eip-allocated', true);
+
+ // Allocate, assign and use a public IP address.
+ $result = $this->getAWSEC2Future()
+ ->setRawAWSQuery(
+ 'AllocateAddress',
+ array(
+ 'Domain' => 'vpc'))
+ ->resolve();
+
+ $public_ip = (string)$result->publicIp;
+ $allocation_id = (string)$result->allocationId;
+
+ $resource->setAttribute('eip-allocation-id', $allocation_id);
+
+ $result = $this->getAWSEC2Future()
+ ->setRawAWSQuery(
+ 'AssociateAddress',
+ array(
+ 'InstanceId' => $instance_id,
+ 'AllocationId' => $allocation_id))
+ ->resolve();
+
+ $association_id = (string)$result->associationId;
+ $resource->setAttribute('eip-association-id', $association_id);
+
+ $address = $public_ip;
+ } else {
+ $resource->setAttribute('eip-allocated', false);
+
+ // Use the private IP address.
+ $result = $this->getAWSEC2Future()
+ ->setRawAWSQuery(
+ 'DescribeInstances',
+ array(
+ 'InstanceId.0' => $instance_id))
+ ->resolve();
+
+ $reservation = $result->reservationSet->item[0];
+ $instance = $reservation->instancesSet->item[0];
+
+ $address = (string)$instance->privateIpAddress;
+ }
+
+ // Update address and port attributes.
+ $resource->setAttribute('host', $address);
+ $resource->setAttribute('port', 22);
+ $resource->save();
+
+ // Wait until we get a successful SSH connection.
+ $ssh = id(new DrydockSSHCommandInterface())
+ ->setConfiguration(array(
+ 'host' => $resource->getAttribute('host'),
+ 'port' => $resource->getAttribute('port'),
+ 'credential' => $resource->getAttribute('credential'),
+ 'platform' => $resource->getAttribute('platform')));
+ $ssh->setConnectTimeout(5);
+
+ while (true) {
+ try {
+ $ssh->getExecFuture('echo "test"')->resolvex();
+ break;
+ } catch (Exception $ex) {
+ continue;
+ }
+ }
+
+ // Update the resource into open status.
+ $resource->setStatus(DrydockResourceStatus::STATUS_OPEN);
+ $resource->save();
+ return $resource;
+ }
+
+ protected function executeCloseResource(DrydockResource $resource) {
+
+ // Deallocate and release the public IP address if we allocated one.
+ if ($resource->getAttribute('eip-allocated')) {
+ $this->getAWSEC2Future()
+ ->setRawAWSQuery(
+ 'DisassociateAddress',
+ array(
+ 'AssociationId' => $resource->getAttribute('eip-association-id')))
+ ->resolve();
+
+ $this->getAWSEC2Future()
+ ->setRawAWSQuery(
+ 'ReleaseAddress',
+ array(
+ 'AllocationId' => $resource->getAttribute('eip-allocation-id')))
+ ->resolve();
+ }
+
+ // Terminate the EC2 instance.
+ $this->getAWSEC2Future()
+ ->setRawAWSQuery(
+ 'TerminateInstances',
+ array(
+ 'InstanceId.0' => $resource->getAttribute('instance-id')))
+ ->resolve();
+
+ }
+
+ protected function shouldCloseUnleasedResource(
+ array $open_resources,
+ DrydockResource $resource) {
+
+ return count($open_resources) > $this->getDetail('min-count');
+ }
+
+ protected function canAllocateLease(
+ DrydockResource $resource,
+ DrydockLease $lease) {
+ return
+ $lease->getAttribute('platform') === $resource->getAttribute('platform');
+ }
+
+ protected function shouldAllocateLease(
+ array $all_resources,
+ array $all_leases_grouped,
+ DrydockResource $resource,
+ DrydockLease $lease,
+ array $other_leases) {
+
+ // If the current resource can allocate a lease, allow it.
+ if (count($other_leases) < $this->getDetail('leases-per-instance')) {
+ return true;
+ }
+
+ // We don't have enough room under the `leases-per-instance` limit, but
+ // this limit can be bypassed if we've allocated all of the resources
+ // we allow.
+ if (count($all_resources) < $this->getDetail('max-count')) {
+ return false;
+ }
+
+ // Find the resource that has the least leases.
+ $minimum_lease_count = count($all_leases_grouped[$resource->getID()]);
+ $minimum_lease_resource_id = $resource->getID();
+ foreach ($all_leases_grouped as $resource_id => $leases) {
+ if ($minimum_lease_count > count($leases)) {
+ $minimum_lease_count = count($leases);
+ $minimum_lease_resource_id = $resource_id;
+ }
+ }
+
+ // If we are that resource, then allow it, otherwise let the other
+ // less-leased resource run through this logic and allocate the lease.
+ return $minimum_lease_resource_id === $resource->getID();
+ }
+
+ protected function executeAcquireLease(
+ DrydockResource $resource,
+ DrydockLease $lease) {
+
+ while ($resource->getStatus() == DrydockResourceStatus::STATUS_PENDING) {
+ // This resource is still being set up by another allocator, wait until
+ // it is set to open.
+ sleep(5);
+ $resource->reload();
+ }
+
+ $platform = $resource->getAttribute('platform');
+ $path = $resource->getAttribute('path');
+
+ $lease_id = $lease->getID();
+
+ // Can't use DIRECTORY_SEPERATOR here because that is relevant to
+ // the platform we're currently running on, not the platform we are
+ // remoting to.
+ $separator = '/';
+ if ($platform === 'windows') {
+ $separator = '\\';
+ }
+
+ // Clean up the directory path a little.
+ $base_path = rtrim($path, '/');
+ $base_path = rtrim($base_path, '\\');
+ $full_path = $base_path.$separator.$lease_id;
+
+ $cmd = $lease->getInterface('command');
+
+ if ($platform !== 'windows') {
+ $cmd->execx('mkdir %s', $full_path);
+ } else {
+ // Windows is terrible. The mkdir command doesn't even support putting
+ // the path in quotes. IN QUOTES. ARGUHRGHUGHHGG!! Do some terribly
+ // inaccurate sanity checking since we can't safely escape the path.
+ if (preg_match('/^[A-Z]\\:\\\\[a-zA-Z0-9\\\\\\ ]/', $full_path) === 0) {
+ throw new Exception(
+ 'Unsafe path detected for Windows platform: "'.$full_path.'".');
+ }
+ $cmd->execx('mkdir %C', $full_path);
+ }
+
+ $lease->setAttribute('path', $full_path);
+ }
+
+ protected function executeReleaseLease(
+ DrydockResource $resource,
+ DrydockLease $lease) {
+
+ // TODO: Remove leased directory
+ }
+
+ public function getType() {
+ return 'host';
+ }
+
+ public function getInterface(
+ DrydockResource $resource,
+ DrydockLease $lease,
+ $type) {
+
+ switch ($type) {
+ case 'command':
+ return id(new DrydockSSHCommandInterface())
+ ->setConfiguration(array(
+ 'host' => $resource->getAttribute('host'),
+ 'port' => $resource->getAttribute('port'),
+ 'credential' => $resource->getAttribute('credential'),
+ 'platform' => $resource->getAttribute('platform')));
+ case 'filesystem':
+ return id(new DrydockSFTPFilesystemInterface())
+ ->setConfiguration(array(
+ 'host' => $resource->getAttribute('host'),
+ 'port' => $resource->getAttribute('port'),
+ 'credential' => $resource->getAttribute('credential')));
+ }
+
+ throw new Exception("No interface of type '{$type}'.");
+ }
+
+ public function getFieldSpecifications() {
+ return array(
+ 'region' => array(
+ 'name' => pht('Region'),
+ 'type' => 'text',
+ 'required' => true,
+ 'caption' => pht('e.g. %s', 'us-west-1')
+ ),
+ 'ami' => array(
+ 'name' => pht('AMI (Amazon Image)'),
+ 'type' => 'text',
+ 'required' => true,
+ 'caption' => pht('e.g. %s', 'ami-7fd3ae4f')
+ ),
+ 'keypair' => array(
+ 'name' => pht('Key Pair'),
+ 'type' => 'credential',
+ 'required' => true,
+ 'credential.provides'
+ => PassphraseCredentialTypeSSHPrivateKey::PROVIDES_TYPE,
+ 'caption' => pht(
+ 'Only the public key component is transmitted to Amazon.')
+ ),
+ 'size' => array(
+ 'name' => pht('Instance Size'),
+ 'type' => 'text',
+ 'required' => true,
+ 'caption' => pht('e.g. %s', 't2.micro')
+ ),
+ 'platform' => array(
+ 'name' => pht('Platform Name'),
+ 'type' => 'text',
+ 'required' => true,
+ 'caption' => pht('e.g. %s or %s', 'windows', 'linux')
+ ),
+ 'subnet-id' => array(
+ 'name' => pht('VPC Subnet'),
+ 'type' => 'text',
+ 'required' => true,
+ 'caption' => pht('e.g. %s', 'subnet-2a67439b')
+ ),
+ 'security-group-ids' => array(
+ 'name' => pht('VPC Security Groups'),
+ 'type' => 'text',
+ 'required' => true,
+ 'caption' => pht('e.g. %s', 'sg-3fa3491f,sg-bg18dea2')
+ ),
+ '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.')
+ ),
+ 'allocate-elastic-ip' => array(
+ 'name' => pht('Allocate Public IP'),
+ 'type' => 'bool',
+ 'caption' => pht(
+ 'If Phabricator is running in the same subnet as the allocated '.
+ 'machines, then you do not need to turn this option on. If '.
+ 'phabricator is hosted externally to Amazon EC2, then enable this '.
+ 'option to automatically allocate and assign elastic IP addresses '.
+ 'to instances so that Phabricator can SSH to them from the '.
+ 'internet (instances are still only accessible by SSH key pairs)')
+ ),
+ 'min-count' => array(
+ 'name' => pht('Minimum Instances'),
+ 'type' => 'int',
+ 'required' => true,
+ 'caption' => pht(
+ 'The minimum number of instances to keep running in '.
+ 'this pool at all times.')
+ ),
+ 'max-count' => array(
+ 'name' => pht('Maximum Instances'),
+ 'type' => 'int',
+ 'caption' => pht(
+ 'The maximum number of instances to allow running at any time. '.
+ 'If the number of instances currently running are equal to '.
+ '`max-count` and another lease is requested, Drydock will place '.
+ 'leases on existing resources and thus exceeding '.
+ '`leases-per-instance`. If this parameter is left blank, then '.
+ 'this blueprint has no limit on the number of EC2 instances it '.
+ 'can allocate.')
+ ),
+ 'leases-per-instance' => array(
+ 'name' => pht('Maximum Leases Per Instance'),
+ 'type' => 'int',
+ 'required' => true,
+ 'caption' => pht(
+ 'The soft limit on the number of leases to allocate to an '.
+ 'individual EC2 instance in the pool. Drydock will choose the '.
+ 'instance with the lowest number of leases when selecting a '.
+ 'resource to lease on. If all current EC2 instances have '.
+ '`leases-per-instance` leases on them, then Drydock will allocate '.
+ 'another EC2 instance providing `max-count` would not be exceeded.'.
+ ' If `max-count` would be exceeded, Drydock will instead '.
+ 'overallocate the lease to an existing EC2 instance and '.
+ 'exceed the limit specified here.')
+ ),
+ );
+ }
+
+}
diff --git a/src/applications/drydock/blueprint/DrydockBlueprintImplementation.php b/src/applications/drydock/blueprint/DrydockBlueprintImplementation.php
--- a/src/applications/drydock/blueprint/DrydockBlueprintImplementation.php
+++ b/src/applications/drydock/blueprint/DrydockBlueprintImplementation.php
@@ -127,6 +127,28 @@
$allocated = false;
$allocation_exception = null;
+ $resource_statuses = array(
+ DrydockResourceStatus::STATUS_PENDING,
+ DrydockResourceStatus::STATUS_OPEN,
+ );
+ $lease_statuses = array(
+ DrydockLeaseStatus::STATUS_ACQUIRING,
+ DrydockLeaseStatus::STATUS_ACTIVE,
+ );
+
+ $all_resources = id(new DrydockResourceQuery())
+ ->setViewer(PhabricatorUser::getOmnipotentUser())
+ ->withBlueprintPHIDs(array($this->getInstance()->getPHID()))
+ ->withStatuses($resource_statuses)
+ ->execute();
+ $all_resource_ids = mpull($all_resources, 'getID');
+ $all_leases = id(new DrydockLeaseQuery())
+ ->setViewer(PhabricatorUser::getOmnipotentUser())
+ ->withResourceIDs($all_resource_ids)
+ ->withStatuses($lease_statuses)
+ ->execute();
+ $all_leases = mgroup($all_leases, 'getResourceID');
+
$resource->openTransaction();
$resource->beginReadLocking();
$resource->reload();
@@ -142,6 +164,8 @@
try {
$allocated = $this->shouldAllocateLease(
+ $all_resources,
+ $all_leases,
$resource,
$ephemeral_lease,
$other_leases);
@@ -197,6 +221,10 @@
* better implemented in @{method:canAllocateLease}, which serves as a
* cheap filter before lock acquisition.
*
+ * @param list<DrydockResource> All allocated resources for the current
+ * blueprint.
+ * @param map<string:list<DrydockLease>> A map of all leases, grouped by
+ * their resource ID.
* @param DrydockResource Candidate resource to allocate the lease on.
* @param DrydockLease Pending lease that wants to allocate here.
* @param list<DrydockLease> Other allocated and acquired leases on the
@@ -207,6 +235,8 @@
* @task lease
*/
abstract protected function shouldAllocateLease(
+ array $all_resources,
+ array $all_leases_grouped,
DrydockResource $resource,
DrydockLease $lease,
array $other_leases);
@@ -265,10 +295,25 @@
DrydockLease $lease);
+ /**
+ * Release an allocated lease, performing any desired cleanup.
+ *
+ * After this method executes, the lease status is moved to `RELEASED`.
+ *
+ * If release fails, throw an exception.
+ *
+ * @param DrydockResource Resource to release the lease from.
+ * @param DrydockLease Lease to release.
+ * @return void
+ */
+ abstract protected function executeReleaseLease(
+ DrydockResource $resource,
+ DrydockLease $lease);
final public function releaseLease(
DrydockResource $resource,
- DrydockLease $lease) {
+ DrydockLease $lease,
+ $caused_by_closing_resource = false) {
$scope = $this->pushActiveScope(null, $lease);
$released = false;
@@ -278,6 +323,9 @@
$lease->reload();
if ($lease->getStatus() == DrydockLeaseStatus::STATUS_ACTIVE) {
+
+ $this->executeReleaseLease($resource, $lease);
+
$lease->setStatus(DrydockLeaseStatus::STATUS_RELEASED);
$lease->save();
$released = true;
@@ -290,6 +338,40 @@
throw new Exception('Unable to release lease: lease not active!');
}
+ if (!$caused_by_closing_resource) {
+ // Check to see if the resource has no more leases, and if so, ask the
+ // blueprint as to whether this resource should be closed.
+ $resource_statuses = array(
+ DrydockResourceStatus::STATUS_PENDING,
+ DrydockResourceStatus::STATUS_OPEN,
+ );
+ $lease_statuses = array(
+ DrydockLeaseStatus::STATUS_ACQUIRING,
+ DrydockLeaseStatus::STATUS_ACTIVE,
+ );
+
+ $open_resources = id(new DrydockResourceQuery())
+ ->setViewer(PhabricatorUser::getOmnipotentUser())
+ ->withBlueprintPHIDs(array($this->getInstance()->getPHID()))
+ ->withStatuses($resource_statuses)
+ ->execute();
+
+ $leases = id(new DrydockLeaseQuery())
+ ->setViewer(PhabricatorUser::getOmnipotentUser())
+ ->withResourceIDs(array($resource->getID()))
+ ->withStatuses($lease_statuses)
+ ->execute();
+
+ if (count($leases) === 0) {
+ if ($this->shouldCloseUnleasedResource($open_resources, $resource)) {
+ DrydockBlueprintImplementation::writeLog(
+ $resource,
+ null,
+ pht('Closing resource because it has no more active leases'));
+ $this->closeResource($resource);
+ }
+ }
+ }
}
@@ -303,6 +385,72 @@
abstract protected function executeAllocateResource(DrydockLease $lease);
+ /**
+ * Closes a previously allocated resource, performing any desired
+ * cleanup.
+ *
+ * After this method executes, the release status is moved to `CLOSED`.
+ *
+ * If release fails, throw an exception.
+ *
+ * @param DrydockResource Resource to close.
+ * @return void
+ */
+ abstract protected function executeCloseResource(
+ DrydockResource $resource);
+
+ /**
+ * Return whether or not a resource that now has no leases on it
+ * should be automatically closed.
+ *
+ * @param list<DrydockResource> All resources currently allocated
+ * (including the unleased one) by the
+ * current blueprint.
+ * @param DrydockResource The resource that has no more leases on it.
+ * @return bool
+ */
+ abstract protected function shouldCloseUnleasedResource(
+ array $open_resources,
+ DrydockResource $resource);
+
+ final public function closeResource(DrydockResource $resource) {
+ $resource->openTransaction();
+ $resource->setStatus(DrydockResourceStatus::STATUS_CLOSING);
+ $resource->save();
+
+ $statuses = array(
+ DrydockLeaseStatus::STATUS_PENDING,
+ DrydockLeaseStatus::STATUS_ACTIVE,
+ );
+
+ $leases = id(new DrydockLeaseQuery())
+ ->setViewer(PhabricatorUser::getOmnipotentUser())
+ ->withResourceIDs(array($resource->getID()))
+ ->withStatuses($statuses)
+ ->execute();
+
+ foreach ($leases as $lease) {
+ switch ($lease->getStatus()) {
+ case DrydockLeaseStatus::STATUS_PENDING:
+ $message = pht('Breaking pending lease (resource closing).');;
+ $lease->setStatus(DrydockLeaseStatus::STATUS_BROKEN);
+ break;
+ case DrydockLeaseStatus::STATUS_ACTIVE:
+ $message = pht('Releasing active lease (resource closing).');
+ $this->releaseLease($resource, $lease, true);
+ $lease->setStatus(DrydockLeaseStatus::STATUS_RELEASED);
+ break;
+ }
+ DrydockBlueprintImplementation::writeLog($resource, $lease, $message);
+ $lease->save();
+ }
+
+ $this->executeCloseResource($resource);
+
+ $resource->setStatus(DrydockResourceStatus::STATUS_CLOSED);
+ $resource->save();
+ $resource->saveTransaction();
+ }
final public function allocateResource(DrydockLease $lease) {
$scope = $this->pushActiveScope(null, $lease);
diff --git a/src/applications/drydock/blueprint/DrydockLocalHostBlueprintImplementation.php b/src/applications/drydock/blueprint/DrydockLocalHostBlueprintImplementation.php
--- a/src/applications/drydock/blueprint/DrydockLocalHostBlueprintImplementation.php
+++ b/src/applications/drydock/blueprint/DrydockLocalHostBlueprintImplementation.php
@@ -57,6 +57,8 @@
}
protected function shouldAllocateLease(
+ array $all_resources,
+ array $all_leases_grouped,
DrydockResource $resource,
DrydockLease $lease,
array $other_leases) {
@@ -94,4 +96,22 @@
throw new Exception("No interface of type '{$type}'.");
}
+ protected function executeReleaseLease(
+ DrydockResource $resource,
+ DrydockLease $lease) {
+
+ // TODO: Remove leased directory
+ }
+
+ protected function shouldCloseUnleasedResource(
+ array $all_resources,
+ DrydockResource $resource) {
+
+ return false;
+ }
+
+ protected function executeCloseResource(DrydockResource $resource) {
+
+ }
+
}
diff --git a/src/applications/drydock/blueprint/DrydockPreallocatedHostBlueprintImplementation.php b/src/applications/drydock/blueprint/DrydockPreallocatedHostBlueprintImplementation.php
--- a/src/applications/drydock/blueprint/DrydockPreallocatedHostBlueprintImplementation.php
+++ b/src/applications/drydock/blueprint/DrydockPreallocatedHostBlueprintImplementation.php
@@ -31,6 +31,8 @@
}
protected function shouldAllocateLease(
+ array $all_resources,
+ array $all_leases_grouped,
DrydockResource $resource,
DrydockLease $lease,
array $other_leases) {
@@ -120,4 +122,22 @@
throw new Exception("No interface of type '{$type}'.");
}
+ protected function executeReleaseLease(
+ DrydockResource $resource,
+ DrydockLease $lease) {
+
+ // TODO: Remove leased directory
+ }
+
+ protected function shouldCloseUnleasedResource(
+ array $all_resources,
+ DrydockResource $resource) {
+
+ return false;
+ }
+
+ protected function executeCloseResource(DrydockResource $resource) {
+
+ }
+
}
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
@@ -26,6 +26,8 @@
}
protected function shouldAllocateLease(
+ array $all_resources,
+ array $all_leases_grouped,
DrydockResource $resource,
DrydockLease $lease,
array $other_leases) {
@@ -110,4 +112,20 @@
throw new Exception("No interface of type '{$type}'.");
}
+ protected function executeReleaseLease(
+ DrydockResource $resource,
+ DrydockLease $lease) {
+ }
+
+ protected function shouldCloseUnleasedResource(
+ array $all_resources,
+ DrydockResource $resource) {
+
+ return false;
+ }
+
+ protected function executeCloseResource(DrydockResource $resource) {
+ // TODO: Remove leased directory
+ }
+
}
diff --git a/src/applications/drydock/constants/DrydockResourceStatus.php b/src/applications/drydock/constants/DrydockResourceStatus.php
--- a/src/applications/drydock/constants/DrydockResourceStatus.php
+++ b/src/applications/drydock/constants/DrydockResourceStatus.php
@@ -7,12 +7,14 @@
const STATUS_CLOSED = 2;
const STATUS_BROKEN = 3;
const STATUS_DESTROYED = 4;
+ const STATUS_CLOSING = 5;
public static function getNameForStatus($status) {
$map = array(
self::STATUS_PENDING => pht('Pending'),
self::STATUS_OPEN => pht('Open'),
self::STATUS_CLOSED => pht('Closed'),
+ self::STATUS_CLOSING => pht('Closing'),
self::STATUS_BROKEN => pht('Broken'),
self::STATUS_DESTROYED => pht('Destroyed'),
);
@@ -25,6 +27,7 @@
self::STATUS_PENDING,
self::STATUS_OPEN,
self::STATUS_CLOSED,
+ self::STATUS_CLOSING,
self::STATUS_BROKEN,
self::STATUS_DESTROYED,
);
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
@@ -36,18 +36,44 @@
$argv = func_get_args();
- // This assumes there's a UNIX shell living at the other
- // end of the connection, which isn't the case for Windows machines.
- if ($this->getConfig('platform') !== 'windows') {
- $argv = $this->applyWorkingDirectoryToArgv($argv);
- }
+ if ($this->getConfig('platform') === 'windows') {
+ // Handle Windows by executing the command under PowerShell.
+ $command = id(new PhutilCommandString($argv))
+ ->setEscapingMode(PhutilCommandString::MODE_POWERSHELL);
- $full_command = call_user_func_array('csprintf', $argv);
+ $change_directory = '';
+ if ($this->getWorkingDirectory() !== null) {
+ $change_directory .= 'cd '.$this->getWorkingDirectory();
+ }
- if ($this->getConfig('platform') === 'windows') {
- // On Windows platforms we need to execute cmd.exe explicitly since
- // most commands are not really executables.
- $full_command = 'C:\\Windows\\system32\\cmd.exe /C '.$full_command;
+ $script = <<<EOF
+$change_directory
+$command
+if (\$LastExitCode -ne 0) {
+ exit \$LastExitCode
+}
+EOF;
+
+ // When Microsoft says "Unicode" they don't mean UTF-8.
+ $script = mb_convert_encoding($script, 'UTF-16LE');
+
+ $script = base64_encode($script);
+
+ $powershell =
+ 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe';
+ $powershell .=
+ ' -ExecutionPolicy Bypass'.
+ ' -NonInteractive'.
+ ' -InputFormat Text'.
+ ' -OutputFormat Text'.
+ ' -EncodedCommand '.$script;
+
+ $full_command = $powershell;
+ } else {
+ // Handle UNIX by executing under the native shell.
+ $argv = $this->applyWorkingDirectoryToArgv($argv);
+
+ $full_command = call_user_func_array('csprintf', $argv);
}
$command_timeout = '';
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
@@ -62,36 +62,8 @@
}
public function closeResource() {
- $this->openTransaction();
- $statuses = array(
- DrydockLeaseStatus::STATUS_PENDING,
- DrydockLeaseStatus::STATUS_ACTIVE,
- );
-
- $leases = id(new DrydockLeaseQuery())
- ->setViewer(PhabricatorUser::getOmnipotentUser())
- ->withResourceIDs(array($this->getID()))
- ->withStatuses($statuses)
- ->execute();
-
- foreach ($leases as $lease) {
- switch ($lease->getStatus()) {
- case DrydockLeaseStatus::STATUS_PENDING:
- $message = pht('Breaking pending lease (resource closing).');
- $lease->setStatus(DrydockLeaseStatus::STATUS_BROKEN);
- break;
- case DrydockLeaseStatus::STATUS_ACTIVE:
- $message = pht('Releasing active lease (resource closing).');
- $lease->setStatus(DrydockLeaseStatus::STATUS_RELEASED);
- break;
- }
- DrydockBlueprintImplementation::writeLog($this, $lease, $message);
- $lease->save();
- }
-
- $this->setStatus(DrydockResourceStatus::STATUS_CLOSED);
- $this->save();
- $this->saveTransaction();
+ $blueprint = $this->getBlueprint();
+ $blueprint->closeResource($this);
}
diff --git a/src/applications/drydock/worker/DrydockAllocatorWorker.php b/src/applications/drydock/worker/DrydockAllocatorWorker.php
--- a/src/applications/drydock/worker/DrydockAllocatorWorker.php
+++ b/src/applications/drydock/worker/DrydockAllocatorWorker.php
@@ -72,6 +72,16 @@
$blueprints = $this->loadAllBlueprints();
+ $resources_per_blueprint = id(new DrydockResourceQuery())
+ ->setViewer(PhabricatorUser::getOmnipotentUser())
+ ->withStatuses(array(
+ DrydockResourceStatus::STATUS_PENDING,
+ DrydockResourceStatus::STATUS_OPEN))
+ ->execute();
+ $resources_per_blueprint = mgroup(
+ $resources_per_blueprint,
+ 'getBlueprintPHID');
+
// TODO: Policy stuff.
$pool = id(new DrydockResource())->loadAllWhere(
'type = %s AND status = %s',
@@ -112,8 +122,53 @@
}
if (!$resource) {
- $blueprints = DrydockBlueprintImplementation
- ::getAllBlueprintImplementationsForResource($type);
+ // Attempt to use pending resources if we can.
+ $pool = id(new DrydockResource())->loadAllWhere(
+ 'type = %s AND status = %s',
+ $lease->getResourceType(),
+ DrydockResourceStatus::STATUS_PENDING);
+
+ $this->logToDrydock(
+ pht('Found %d Pending Resource(s)', count($pool)));
+
+ $candidates = array();
+ foreach ($pool as $key => $candidate) {
+ if (!isset($blueprints[$candidate->getBlueprintPHID()])) {
+ unset($pool[$key]);
+ continue;
+ }
+
+ $blueprint = $blueprints[$candidate->getBlueprintPHID()];
+ $implementation = $blueprint->getImplementation();
+
+ if ($implementation->filterResource($candidate, $lease)) {
+ $candidates[] = $candidate;
+ }
+ }
+
+ $this->logToDrydock(
+ pht('%d Pending Resource(s) Remain',
+ count($candidates)));
+
+ $resource = null;
+ if ($candidates) {
+ shuffle($candidates);
+ foreach ($candidates as $candidate_resource) {
+ $blueprint = $blueprints[$candidate_resource->getBlueprintPHID()]
+ ->getImplementation();
+ if ($blueprint->allocateLease($candidate_resource, $lease)) {
+ $resource = $candidate_resource;
+ break;
+ }
+ }
+ }
+ }
+
+ if (!$resource) {
+ $blueprints = id(new DrydockBlueprintQuery())
+ ->setViewer(PhabricatorUser::getOmnipotentUser())
+ ->execute();
+ $blueprints = mpull($blueprints, 'getImplementation', 'getPHID');
$this->logToDrydock(
pht('Found %d Blueprints', count($blueprints)));
@@ -129,7 +184,8 @@
pht('%d Blueprints Enabled', count($blueprints)));
foreach ($blueprints as $key => $candidate_blueprint) {
- if (!$candidate_blueprint->canAllocateMoreResources($pool)) {
+ $rpool = idx($resources_per_blueprint, $key, array());
+ if (!$candidate_blueprint->canAllocateMoreResources($rpool)) {
unset($blueprints[$key]);
continue;
}
diff --git a/src/applications/harbormaster/step/HarbormasterCommandBuildStepImplementation.php b/src/applications/harbormaster/step/HarbormasterCommandBuildStepImplementation.php
--- a/src/applications/harbormaster/step/HarbormasterCommandBuildStepImplementation.php
+++ b/src/applications/harbormaster/step/HarbormasterCommandBuildStepImplementation.php
@@ -3,6 +3,8 @@
final class HarbormasterCommandBuildStepImplementation
extends HarbormasterBuildStepImplementation {
+ private $platform;
+
public function getName() {
return pht('Run Command');
}
@@ -18,6 +20,18 @@
$this->formatSettingForDescription('hostartifact'));
}
+ public function escapeCommand($pattern, array $args) {
+ array_unshift($args, $pattern);
+
+ $mode = PhutilCommandString::MODE_DEFAULT;
+ if ($this->platform == 'windows') {
+ $mode = PhutilCommandString::MODE_POWERSHELL;
+ }
+
+ return id(new PhutilCommandString($args))
+ ->setEscapingMode($mode);
+ }
+
public function execute(
HarbormasterBuild $build,
HarbormasterBuildTarget $build_target) {
@@ -25,14 +39,18 @@
$settings = $this->getSettings();
$variables = $build_target->getVariables();
+ $artifact = $build->loadArtifact($settings['hostartifact']);
+
+ $lease = $artifact->loadDrydockLease();
+
+ $this->platform = $lease->getAttribute('platform');
+
$command = $this->mergeVariables(
- 'vcsprintf',
+ array($this, 'escapeCommand'),
$settings['command'],
$variables);
- $artifact = $build->loadArtifact($settings['hostartifact']);
-
- $lease = $artifact->loadDrydockLease();
+ $this->platform = null;
$interface = $lease->getInterface('command');
@@ -88,6 +106,9 @@
'name' => pht('Command'),
'type' => 'text',
'required' => true,
+ 'caption' => pht(
+ 'Under Windows, this is executed under PowerShell.'.
+ 'Under UNIX, this is executed using the user\'s shell.'),
),
'hostartifact' => array(
'name' => pht('Host'),
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Tue, Jun 17, 2:41 PM (16 h, 5 m)
Storage Engine
blob
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
8136600
Default Alt Text
D10248.diff (37 KB)
Attached To
Mode
D10248: Execute commands under Powershell on Windows for Harbormaster
Attached
Detach File
Event Timeline
Log In to Comment