3 final class DrydockAlmanacServiceHostBlueprintImplementation
4 extends DrydockBlueprintImplementation
{
9 public function isEnabled() {
10 $almanac_app = 'PhabricatorAlmanacApplication';
11 return PhabricatorApplication
::isClassInstalled($almanac_app);
14 public function getBlueprintName() {
15 return pht('Almanac Hosts');
18 public function getBlueprintIcon() {
22 public function getDescription() {
24 'Allows Drydock to lease existing hosts defined in an Almanac service '.
28 public function canAnyBlueprintEverAllocateResourceForLease(
29 DrydockLease
$lease) {
33 public function canEverAllocateResourceForLease(
34 DrydockBlueprint
$blueprint,
35 DrydockLease
$lease) {
36 $services = $this->loadServices($blueprint);
37 $bindings = $this->getActiveBindings($services);
40 // If there are no devices bound to the services for this blueprint,
41 // we can not allocate resources.
48 public function shouldAllocateSupplementalResource(
49 DrydockBlueprint
$blueprint,
50 DrydockResource
$resource,
51 DrydockLease
$lease) {
52 // We want to use every host in an Almanac service, since the amount of
53 // hardware is fixed and there's normally no value in packing leases onto a
54 // subset of it. Always build a new supplemental resource if we can.
58 public function canAllocateResourceForLease(
59 DrydockBlueprint
$blueprint,
60 DrydockLease
$lease) {
62 // We will only allocate one resource per unique device bound to the
63 // services for this blueprint. Make sure we have a free device somewhere.
64 $free_bindings = $this->loadFreeBindings($blueprint);
65 if (!$free_bindings) {
72 public function allocateResource(
73 DrydockBlueprint
$blueprint,
74 DrydockLease
$lease) {
76 $free_bindings = $this->loadFreeBindings($blueprint);
77 shuffle($free_bindings);
79 $exceptions = array();
80 foreach ($free_bindings as $binding) {
81 $device = $binding->getDevice();
82 $device_name = $device->getName();
84 $binding_phid = $binding->getPHID();
86 $resource = $this->newResourceTemplate($blueprint)
87 ->setActivateWhenAllocated(true)
88 ->setAttribute('almanacDeviceName', $device_name)
89 ->setAttribute('almanacServicePHID', $binding->getServicePHID())
90 ->setAttribute('almanacBindingPHID', $binding_phid)
91 ->needSlotLock("almanac.host.binding({$binding_phid})");
94 return $resource->allocateResource();
95 } catch (Exception
$ex) {
100 throw new PhutilAggregateException(
101 pht('Unable to allocate any binding as a resource.'),
105 public function destroyResource(
106 DrydockBlueprint
$blueprint,
107 DrydockResource
$resource) {
108 // We don't create anything when allocating hosts, so we don't need to do
113 public function getResourceName(
114 DrydockBlueprint
$blueprint,
115 DrydockResource
$resource) {
116 $device_name = $resource->getAttribute(
119 return pht('Host (%s)', $device_name);
122 public function canAcquireLeaseOnResource(
123 DrydockBlueprint
$blueprint,
124 DrydockResource
$resource,
125 DrydockLease
$lease) {
127 // Require the binding to a given host be active before we'll hand out more
128 // leases on the corresponding resource.
129 $binding = $this->loadBindingForResource($resource);
130 if ($binding->getIsDisabled()) {
137 public function acquireLease(
138 DrydockBlueprint
$blueprint,
139 DrydockResource
$resource,
140 DrydockLease
$lease) {
143 ->setActivateWhenAcquired(true)
144 ->acquireOnResource($resource);
147 public function didReleaseLease(
148 DrydockBlueprint
$blueprint,
149 DrydockResource
$resource,
150 DrydockLease
$lease) {
151 // Almanac hosts stick around indefinitely so we don't need to recycle them
152 // if they don't have any leases.
156 public function destroyLease(
157 DrydockBlueprint
$blueprint,
158 DrydockResource
$resource,
159 DrydockLease
$lease) {
160 // We don't create anything when activating a lease, so we don't need to
161 // throw anything away.
165 public function getType() {
169 public function getInterface(
170 DrydockBlueprint
$blueprint,
171 DrydockResource
$resource,
176 case DrydockCommandInterface
::INTERFACE_TYPE
:
177 $credential_phid = $blueprint->getFieldValue('credentialPHID');
178 $binding = $this->loadBindingForResource($resource);
179 $interface = $binding->getInterface();
181 return id(new DrydockSSHCommandInterface())
182 ->setConfig('credentialPHID', $credential_phid)
183 ->setConfig('host', $interface->getAddress())
184 ->setConfig('port', $interface->getPort());
188 protected function getCustomFieldSpecifications() {
190 'almanacServicePHIDs' => array(
191 'name' => pht('Almanac Services'),
192 'type' => 'datasource',
193 'datasource.class' => 'AlmanacServiceDatasource',
194 'datasource.parameters' => array(
195 'serviceTypes' => $this->getAlmanacServiceTypes(),
199 'credentialPHID' => array(
200 'name' => pht('Credentials'),
201 'type' => 'credential',
202 'credential.provides' =>
203 PassphraseSSHPrivateKeyCredentialType
::PROVIDES_TYPE
,
205 PassphraseSSHPrivateKeyTextCredentialType
::CREDENTIAL_TYPE
,
210 private function loadServices(DrydockBlueprint
$blueprint) {
211 if (!$this->services
) {
212 $service_phids = $blueprint->getFieldValue('almanacServicePHIDs');
213 if (!$service_phids) {
216 'This blueprint ("%s") does not define any Almanac Service PHIDs.',
217 $blueprint->getBlueprintName()));
220 $viewer = $this->getViewer();
221 $services = id(new AlmanacServiceQuery())
223 ->withPHIDs($service_phids)
224 ->withServiceTypes($this->getAlmanacServiceTypes())
225 ->needActiveBindings(true)
227 $services = mpull($services, null, 'getPHID');
229 if (count($services) != count($service_phids)) {
230 $missing_phids = array_diff($service_phids, array_keys($services));
233 'Some of the Almanac Services defined by this blueprint '.
234 'could not be loaded. They may be invalid, no longer exist, '.
235 'or be of the wrong type: %s.',
236 implode(', ', $missing_phids)));
239 $this->services
= $services;
242 return $this->services
;
245 private function getActiveBindings(array $services) {
246 assert_instances_of($services, 'AlmanacService');
247 $bindings = array_mergev(mpull($services, 'getActiveBindings'));
248 return mpull($bindings, null, 'getPHID');
251 private function loadFreeBindings(DrydockBlueprint
$blueprint) {
252 if ($this->freeBindings
=== null) {
253 $viewer = $this->getViewer();
255 $pool = id(new DrydockResourceQuery())
257 ->withBlueprintPHIDs(array($blueprint->getPHID()))
260 DrydockResourceStatus
::STATUS_PENDING
,
261 DrydockResourceStatus
::STATUS_ACTIVE
,
262 DrydockResourceStatus
::STATUS_BROKEN
,
263 DrydockResourceStatus
::STATUS_RELEASED
,
267 $allocated_phids = array();
268 foreach ($pool as $resource) {
269 $allocated_phids[] = $resource->getAttribute('almanacBindingPHID');
271 $allocated_phids = array_fuse($allocated_phids);
273 $services = $this->loadServices($blueprint);
274 $bindings = $this->getActiveBindings($services);
277 foreach ($bindings as $binding) {
278 if (empty($allocated_phids[$binding->getPHID()])) {
283 $this->freeBindings
= $free;
286 return $this->freeBindings
;
289 private function getAlmanacServiceTypes() {
291 AlmanacDrydockPoolServiceType
::SERVICETYPE
,
295 private function loadBindingForResource(DrydockResource
$resource) {
296 $binding_phid = $resource->getAttribute('almanacBindingPHID');
297 if (!$binding_phid) {
300 'Drydock resource ("%s") has no Almanac binding PHID, so its '.
301 'binding can not be loaded.',
302 $resource->getPHID()));
305 $viewer = $this->getViewer();
307 $binding = id(new AlmanacBindingQuery())
309 ->withPHIDs(array($binding_phid))
314 'Unable to load Almanac binding ("%s") for resource ("%s").',
316 $resource->getPHID()));