Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F21779327
D10204.id24561.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
39 KB
Referenced Files
None
Subscribers
None
D10204.id24561.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
@@ -565,10 +565,13 @@
'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',
+ 'DrydockBlueprintCoreCustomField' => 'applications/drydock/customfield/DrydockBlueprintCoreCustomField.php',
'DrydockBlueprintCreateController' => 'applications/drydock/controller/DrydockBlueprintCreateController.php',
+ 'DrydockBlueprintCustomField' => 'applications/drydock/customfield/DrydockBlueprintCustomField.php',
'DrydockBlueprintEditController' => 'applications/drydock/controller/DrydockBlueprintEditController.php',
'DrydockBlueprintEditor' => 'applications/drydock/editor/DrydockBlueprintEditor.php',
'DrydockBlueprintImplementation' => 'applications/drydock/blueprint/DrydockBlueprintImplementation.php',
@@ -3300,13 +3303,20 @@
'DoorkeeperTagView' => 'AphrontView',
'DoorkeeperTagsController' => 'PhabricatorController',
'DrydockAllocatorWorker' => 'PhabricatorWorker',
+ 'DrydockAmazonEC2HostBlueprintImplementation' => 'DrydockBlueprintImplementation',
'DrydockApacheWebrootInterface' => 'DrydockWebrootInterface',
'DrydockBlueprint' => array(
'DrydockDAO',
'PhabricatorPolicyInterface',
+ 'PhabricatorCustomFieldInterface',
),
'DrydockBlueprintController' => 'DrydockController',
+ 'DrydockBlueprintCoreCustomField' => array(
+ 'DrydockBlueprintCustomField',
+ 'PhabricatorStandardCustomFieldInterface',
+ ),
'DrydockBlueprintCreateController' => 'DrydockBlueprintController',
+ 'DrydockBlueprintCustomField' => 'PhabricatorCustomField',
'DrydockBlueprintEditController' => 'DrydockBlueprintController',
'DrydockBlueprintEditor' => 'PhabricatorApplicationTransactionEditor',
'DrydockBlueprintListController' => 'DrydockBlueprintController',
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,487 @@
+<?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 $all_resources,
+ DrydockResource $resource) {
+
+ return count($all_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) {
+
+ $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
@@ -56,6 +56,14 @@
return $this;
}
+ public function getFieldSpecifications() {
+ return array();
+ }
+
+ public function getDetail($key, $default = null) {
+ return $this->getInstance()->getDetail($key, $default);
+ }
+
/* -( Lease Acquisition )-------------------------------------------------- */
@@ -119,6 +127,28 @@
$allocated = false;
$allocation_exception = null;
+ $resource_statuses = array(
+ DrydockResourceStatus::STATUS_PENDING,
+ DrydockResourceStatus::STATUS_OPEN,
+ );
+ $lease_statuses = array(
+ DrydockLeaseStatus::STATUS_PENDING,
+ 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();
@@ -134,6 +164,8 @@
try {
$allocated = $this->shouldAllocateLease(
+ $all_resources,
+ $all_leases,
$resource,
$ephemeral_lease,
$other_leases);
@@ -189,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
@@ -199,6 +235,8 @@
* @task lease
*/
abstract protected function shouldAllocateLease(
+ array $all_resources,
+ array $all_leases_grouped,
DrydockResource $resource,
DrydockLease $lease,
array $other_leases);
@@ -257,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;
@@ -270,6 +323,9 @@
$lease->reload();
if ($lease->getStatus() == DrydockLeaseStatus::STATUS_ACTIVE) {
+
+ $this->executeReleaseLease($resource, $lease);
+
$lease->setStatus(DrydockLeaseStatus::STATUS_RELEASED);
$lease->save();
$released = true;
@@ -282,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_PENDING,
+ DrydockLeaseStatus::STATUS_ACTIVE,
+ );
+
+ $all_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($all_resources, $resource)) {
+ DrydockBlueprintImplementation::writeLog(
+ $resource,
+ null,
+ pht('Closing resource because it has no more active leases'));
+ $this->closeResource($resource);
+ }
+ }
+ }
}
@@ -295,6 +385,69 @@
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 $all_resources,
+ DrydockResource $resource);
+
+ final public function closeResource(DrydockResource $resource) {
+ $resource->openTransaction();
+ $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/controller/DrydockBlueprintEditController.php b/src/applications/drydock/controller/DrydockBlueprintEditController.php
--- a/src/applications/drydock/controller/DrydockBlueprintEditController.php
+++ b/src/applications/drydock/controller/DrydockBlueprintEditController.php
@@ -44,9 +44,17 @@
$cancel_uri = $this->getApplicationURI('blueprint/');
}
+ $field_list = PhabricatorCustomField::getObjectFields(
+ $blueprint,
+ PhabricatorCustomField::ROLE_EDIT);
+ $field_list
+ ->setViewer($viewer)
+ ->readFieldsFromStorage($blueprint);
+
$v_name = $blueprint->getBlueprintName();
$e_name = true;
$errors = array();
+ $validation_exception = null;
if ($request->isFormPost()) {
$v_view_policy = $request->getStr('viewPolicy');
@@ -60,6 +68,10 @@
if (!$errors) {
$xactions = array();
+ $xactions = $field_list->buildFieldTransactionsFromRequest(
+ new DrydockBlueprintTransaction(),
+ $request);
+
$xactions[] = id(new DrydockBlueprintTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_VIEW_POLICY)
->setNewValue($v_view_policy);
@@ -77,12 +89,16 @@
->setContentSourceFromRequest($request)
->setContinueOnNoEffect(true);
- $editor->applyTransactions($blueprint, $xactions);
+ try {
+ $editor->applyTransactions($blueprint, $xactions);
- $id = $blueprint->getID();
- $save_uri = $this->getApplicationURI("blueprint/{$id}/");
+ $id = $blueprint->getID();
+ $save_uri = $this->getApplicationURI("blueprint/{$id}/");
- return id(new AphrontRedirectResponse())->setURI($save_uri);
+ return id(new AphrontRedirectResponse())->setURI($save_uri);
+ } catch (PhabricatorApplicationTransactionValidationException $ex) {
+ $validation_exception = $ex;
+ }
}
}
@@ -117,6 +133,8 @@
->setCapability(PhabricatorPolicyCapability::CAN_EDIT)
->setPolicies($policies));
+ $field_list->appendFieldsToForm($form);
+
$crumbs = $this->buildApplicationCrumbs();
if ($blueprint->getID()) {
@@ -139,6 +157,7 @@
$box = id(new PHUIObjectBoxView())
->setHeaderText($header)
+ ->setValidationException($validation_exception)
->setFormErrors($errors)
->setForm($form);
diff --git a/src/applications/drydock/controller/DrydockBlueprintViewController.php b/src/applications/drydock/controller/DrydockBlueprintViewController.php
--- a/src/applications/drydock/controller/DrydockBlueprintViewController.php
+++ b/src/applications/drydock/controller/DrydockBlueprintViewController.php
@@ -61,6 +61,18 @@
->withObjectPHIDs(array($blueprint->getPHID()))
->execute();
+ $field_list = PhabricatorCustomField::getObjectFields(
+ $blueprint,
+ PhabricatorCustomField::ROLE_VIEW);
+ $field_list
+ ->setViewer($viewer)
+ ->readFieldsFromStorage($blueprint);
+
+ $field_list->appendFieldsToPropertyList(
+ $blueprint,
+ $viewer,
+ $properties);
+
$engine = id(new PhabricatorMarkupEngine())
->setViewer($viewer);
diff --git a/src/applications/drydock/customfield/DrydockBlueprintCoreCustomField.php b/src/applications/drydock/customfield/DrydockBlueprintCoreCustomField.php
new file mode 100644
--- /dev/null
+++ b/src/applications/drydock/customfield/DrydockBlueprintCoreCustomField.php
@@ -0,0 +1,43 @@
+<?php
+
+final class DrydockBlueprintCoreCustomField
+ extends DrydockBlueprintCustomField
+ implements PhabricatorStandardCustomFieldInterface {
+
+ public function getStandardCustomFieldNamespace() {
+ return 'drydock:core';
+ }
+
+ public function createFields($object) {
+ $impl = $object->getImplementation();
+ $specs = $impl->getFieldSpecifications();
+
+ return PhabricatorStandardCustomField::buildStandardFields($this, $specs);
+ }
+
+ public function shouldUseStorage() {
+ return false;
+ }
+
+ public function readValueFromObject(PhabricatorCustomFieldInterface $object) {
+ $key = $this->getProxy()->getRawStandardFieldKey();
+ $this->setValueFromStorage($object->getDetail($key));
+ }
+
+ public function applyApplicationTransactionInternalEffects(
+ PhabricatorApplicationTransaction $xaction) {
+ $object = $this->getObject();
+ $key = $this->getProxy()->getRawStandardFieldKey();
+
+ $this->setValueFromApplicationTransactions($xaction->getNewValue());
+ $value = $this->getValueForStorage();
+
+ $object->setDetail($key, $value);
+ }
+
+ public function applyApplicationTransactionExternalEffects(
+ PhabricatorApplicationTransaction $xaction) {
+ return;
+ }
+
+}
diff --git a/src/applications/drydock/customfield/DrydockBlueprintCustomField.php b/src/applications/drydock/customfield/DrydockBlueprintCustomField.php
new file mode 100644
--- /dev/null
+++ b/src/applications/drydock/customfield/DrydockBlueprintCustomField.php
@@ -0,0 +1,6 @@
+<?php
+
+abstract class DrydockBlueprintCustomField
+ extends PhabricatorCustomField {
+
+}
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
@@ -3,6 +3,7 @@
final class DrydockSSHCommandInterface extends DrydockCommandInterface {
private $passphraseSSHKey;
+ private $connectTimeout;
private function openCredentialsIfNotOpen() {
if ($this->passphraseSSHKey !== null) {
@@ -25,6 +26,11 @@
PhabricatorUser::getOmnipotentUser());
}
+ public function setConnectTimeout($timeout) {
+ $this->connectTimeout = $timeout;
+ return $this;
+ }
+
public function getExecFuture($command) {
$this->openCredentialsIfNotOpen();
@@ -44,8 +50,19 @@
$full_command = 'C:\\Windows\\system32\\cmd.exe /C '.$full_command;
}
+ $command_timeout = '';
+ if ($this->connectTimeout !== null) {
+ $command_timeout = csprintf(
+ '-o %s',
+ 'ConnectTimeout='.$this->connectTimeout);
+ }
+
return new ExecFuture(
- 'ssh -o StrictHostKeyChecking=no -p %s -i %P %P@%s -- %s',
+ 'ssh '.
+ '-o StrictHostKeyChecking=no '.
+ '-o BatchMode=yes '.
+ '%C -p %s -i %P %P@%s -- %s',
+ $command_timeout,
$this->getConfig('port'),
$this->passphraseSSHKey->getKeyfileEnvelope(),
$this->passphraseSSHKey->getUsernameEnvelope(),
diff --git a/src/applications/drydock/storage/DrydockBlueprint.php b/src/applications/drydock/storage/DrydockBlueprint.php
--- a/src/applications/drydock/storage/DrydockBlueprint.php
+++ b/src/applications/drydock/storage/DrydockBlueprint.php
@@ -1,7 +1,9 @@
<?php
final class DrydockBlueprint extends DrydockDAO
- implements PhabricatorPolicyInterface {
+ implements
+ PhabricatorPolicyInterface,
+ PhabricatorCustomFieldInterface {
protected $className;
protected $blueprintName;
@@ -10,6 +12,7 @@
protected $details = array();
private $implementation = self::ATTACHABLE;
+ private $customFields = self::ATTACHABLE;
public static function initializeNewBlueprint(PhabricatorUser $actor) {
$app = id(new PhabricatorApplicationQuery())
@@ -58,6 +61,15 @@
return $this;
}
+ public function getDetail($key, $default = null) {
+ return idx($this->details, $key, $default);
+ }
+
+ public function setDetail($key, $value) {
+ $this->details[$key] = $value;
+ return $this;
+ }
+
/* -( PhabricatorPolicyInterface )----------------------------------------- */
@@ -86,4 +98,27 @@
return null;
}
+
+/* -( PhabricatorCustomFieldInterface )------------------------------------ */
+
+
+ public function getCustomFieldSpecificationForRole($role) {
+ return array();
+ }
+
+ public function getCustomFieldBaseClass() {
+ return 'DrydockBlueprintCustomField';
+ }
+
+ public function getCustomFields() {
+ return $this->assertAttached($this->customFields);
+ }
+
+ public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) {
+ $this->customFields = $fields;
+ return $this;
+ }
+
+
+
}
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
@@ -112,8 +112,10 @@
}
if (!$resource) {
- $blueprints = DrydockBlueprintImplementation
- ::getAllBlueprintImplementationsForResource($type);
+ $blueprints = id(new DrydockBlueprintQuery())
+ ->setViewer(PhabricatorUser::getOmnipotentUser())
+ ->execute();
+ $blueprints = mpull($blueprints, 'getImplementation', 'getPHID');
$this->logToDrydock(
pht('Found %d Blueprints', count($blueprints)));
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Tue, Aug 11, 9:11 PM (22 h, 22 m)
Storage Engine
blob
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
16524303
Default Alt Text
D10204.id24561.diff (39 KB)
Attached To
Mode
D10204: Implement Amazon EC2 blueprint for Drydock
Attached
Detach File
Event Timeline
Log In to Comment