1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-25 16:22:43 +01:00
phorge-phorge/src/applications/drydock/blueprint/DrydockAlmanacServiceHostBlueprintImplementation.php
epriestley f1119ffcf5 Support working copies and separate allocate + activate steps for resources/leases in Drydock
Summary:
Ref T9253. For resources and leases that need to do something which takes a lot of time or requires waiting, allow them to allocate/acquire first and then activate later.

When we allocate a resource or acquire a lease, the blueprint can either activate it immediately (if all the work can happen quickly/inline) or activate it later. If the blueprint activates it later, we queue a worker to handle activating it.

Rebuild the "working copy" blueprint to work with this model: it allocates/acquires and activates in a separate step, once it is able to acquire a host.

Test Plan: With some power of imagination, brought up a bunch of working copies with `bin/drydock lease --type working-copy ...`

Reviewers: hach-que, chad

Reviewed By: hach-que, chad

Maniphest Tasks: T9253

Differential Revision: https://secure.phabricator.com/D14127
2015-09-21 04:46:24 -07:00

263 lines
7.4 KiB
PHP

<?php
final class DrydockAlmanacServiceHostBlueprintImplementation
extends DrydockBlueprintImplementation {
private $services;
private $freeBindings;
public function isEnabled() {
$almanac_app = 'PhabricatorAlmanacApplication';
return PhabricatorApplication::isClassInstalled($almanac_app);
}
public function getBlueprintName() {
return pht('Almanac Hosts');
}
public function getDescription() {
return pht(
'Allows Drydock to lease existing hosts defined in an Almanac service '.
'pool.');
}
public function canAnyBlueprintEverAllocateResourceForLease(
DrydockLease $lease) {
return true;
}
public function canEverAllocateResourceForLease(
DrydockBlueprint $blueprint,
DrydockLease $lease) {
$services = $this->loadServices($blueprint);
$bindings = $this->loadAllBindings($services);
if (!$bindings) {
// If there are no devices bound to the services for this blueprint,
// we can not allocate resources.
return false;
}
return true;
}
public function canAllocateResourceForLease(
DrydockBlueprint $blueprint,
DrydockLease $lease) {
// We will only allocate one resource per unique device bound to the
// services for this blueprint. Make sure we have a free device somewhere.
$free_bindings = $this->loadFreeBindings($blueprint);
if (!$free_bindings) {
return false;
}
return true;
}
public function allocateResource(
DrydockBlueprint $blueprint,
DrydockLease $lease) {
$free_bindings = $this->loadFreeBindings($blueprint);
shuffle($free_bindings);
$exceptions = array();
foreach ($free_bindings as $binding) {
$device = $binding->getDevice();
$device_name = $device->getName();
$binding_phid = $binding->getPHID();
$resource = $this->newResourceTemplate($blueprint, $device_name)
->setActivateWhenAllocated(true)
->setAttribute('almanacServicePHID', $binding->getServicePHID())
->setAttribute('almanacBindingPHID', $binding_phid)
->needSlotLock("almanac.host.binding({$binding_phid})");
try {
return $resource->allocateResource();
} catch (Exception $ex) {
$exceptions[] = $ex;
}
}
throw new PhutilAggregateException(
pht('Unable to allocate any binding as a resource.'),
$exceptions);
}
public function canAcquireLeaseOnResource(
DrydockBlueprint $blueprint,
DrydockResource $resource,
DrydockLease $lease) {
if (!DrydockSlotLock::isLockFree($this->getLeaseSlotLock($resource))) {
return false;
}
return true;
}
public function acquireLease(
DrydockBlueprint $blueprint,
DrydockResource $resource,
DrydockLease $lease) {
$lease
->setActivateWhenAcquired(true)
->needSlotLock($this->getLeaseSlotLock($resource))
->acquireOnResource($resource);
}
private function getLeaseSlotLock(DrydockResource $resource) {
$resource_phid = $resource->getPHID();
return "almanac.host.lease({$resource_phid})";
}
public function getType() {
return 'host';
}
public function getInterface(
DrydockBlueprint $blueprint,
DrydockResource $resource,
DrydockLease $lease,
$type) {
$viewer = PhabricatorUser::getOmnipotentUser();
switch ($type) {
case DrydockCommandInterface::INTERFACE_TYPE:
$credential_phid = $blueprint->getFieldValue('credentialPHID');
$binding_phid = $resource->getAttribute('almanacBindingPHID');
$binding = id(new AlmanacBindingQuery())
->setViewer($viewer)
->withPHIDs(array($binding_phid))
->executeOne();
if (!$binding) {
// TODO: This is probably a permanent failure, destroy this resource?
throw new Exception(
pht(
'Unable to load binding "%s" to create command interface.',
$binding_phid));
}
$interface = $binding->getInterface();
return id(new DrydockSSHCommandInterface())
->setConfig('credentialPHID', $credential_phid)
->setConfig('host', $interface->getAddress())
->setConfig('port', $interface->getPort());
}
}
public function getFieldSpecifications() {
return array(
'almanacServicePHIDs' => array(
'name' => pht('Almanac Services'),
'type' => 'datasource',
'datasource.class' => 'AlmanacServiceDatasource',
'datasource.parameters' => array(
'serviceClasses' => $this->getAlmanacServiceClasses(),
),
'required' => true,
),
'credentialPHID' => array(
'name' => pht('Credentials'),
'type' => 'credential',
'credential.provides' =>
PassphraseSSHPrivateKeyCredentialType::PROVIDES_TYPE,
'credential.type' =>
PassphraseSSHPrivateKeyTextCredentialType::CREDENTIAL_TYPE,
),
) + parent::getFieldSpecifications();
}
private function loadServices(DrydockBlueprint $blueprint) {
if (!$this->services) {
$service_phids = $blueprint->getFieldValue('almanacServicePHIDs');
if (!$service_phids) {
throw new Exception(
pht(
'This blueprint ("%s") does not define any Almanac Service PHIDs.',
$blueprint->getBlueprintName()));
}
$viewer = PhabricatorUser::getOmnipotentUser();
$services = id(new AlmanacServiceQuery())
->setViewer($viewer)
->withPHIDs($service_phids)
->withServiceClasses($this->getAlmanacServiceClasses())
->needBindings(true)
->execute();
$services = mpull($services, null, 'getPHID');
if (count($services) != count($service_phids)) {
$missing_phids = array_diff($service_phids, array_keys($services));
throw new Exception(
pht(
'Some of the Almanac Services defined by this blueprint '.
'could not be loaded. They may be invalid, no longer exist, '.
'or be of the wrong type: %s.',
implode(', ', $missing_phids)));
}
$this->services = $services;
}
return $this->services;
}
private function loadAllBindings(array $services) {
assert_instances_of($services, 'AlmanacService');
$bindings = array_mergev(mpull($services, 'getBindings'));
return mpull($bindings, null, 'getPHID');
}
private function loadFreeBindings(DrydockBlueprint $blueprint) {
if ($this->freeBindings === null) {
$viewer = PhabricatorUser::getOmnipotentUser();
$pool = id(new DrydockResourceQuery())
->setViewer($viewer)
->withBlueprintPHIDs(array($blueprint->getPHID()))
->withStatuses(
array(
DrydockResourceStatus::STATUS_PENDING,
DrydockResourceStatus::STATUS_OPEN,
DrydockResourceStatus::STATUS_CLOSED,
))
->execute();
$allocated_phids = array();
foreach ($pool as $resource) {
$allocated_phids[] = $resource->getAttribute('almanacDevicePHID');
}
$allocated_phids = array_fuse($allocated_phids);
$services = $this->loadServices($blueprint);
$bindings = $this->loadAllBindings($services);
$free = array();
foreach ($bindings as $binding) {
if (empty($allocated_phids[$binding->getPHID()])) {
$free[] = $binding;
}
}
$this->freeBindings = $free;
}
return $this->freeBindings;
}
private function getAlmanacServiceClasses() {
return array(
'AlmanacDrydockPoolServiceType',
);
}
}