@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.)
hq.recaptime.dev/wiki/Phorge
phorge
phabricator
1<?php
2
3final class DrydockAlmanacServiceHostBlueprintImplementation
4 extends DrydockBlueprintImplementation {
5
6 private $services;
7 private $freeBindings;
8
9 public function isEnabled() {
10 return PhabricatorApplication::isClassInstalled(
11 PhabricatorAlmanacApplication::class);
12 }
13
14 public function getBlueprintName() {
15 return pht('Almanac Hosts');
16 }
17
18 public function getBlueprintIcon() {
19 return 'fa-server';
20 }
21
22 public function getDescription() {
23 return pht(
24 'Allows Drydock to lease existing hosts defined in an Almanac service '.
25 'pool.');
26 }
27
28 public function canAnyBlueprintEverAllocateResourceForLease(
29 DrydockLease $lease) {
30 return true;
31 }
32
33 public function canEverAllocateResourceForLease(
34 DrydockBlueprint $blueprint,
35 DrydockLease $lease) {
36 $services = $this->loadServices($blueprint);
37 $bindings = $this->getActiveBindings($services);
38
39 if (!$bindings) {
40 // If there are no devices bound to the services for this blueprint,
41 // we can not allocate resources.
42 return false;
43 }
44
45 return true;
46 }
47
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.
55 return true;
56 }
57
58 public function canAllocateResourceForLease(
59 DrydockBlueprint $blueprint,
60 DrydockLease $lease) {
61
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) {
66 return false;
67 }
68
69 return true;
70 }
71
72 public function allocateResource(
73 DrydockBlueprint $blueprint,
74 DrydockLease $lease) {
75
76 $free_bindings = $this->loadFreeBindings($blueprint);
77 shuffle($free_bindings);
78
79 $exceptions = array();
80 foreach ($free_bindings as $binding) {
81 $device = $binding->getDevice();
82 $device_name = $device->getName();
83
84 $binding_phid = $binding->getPHID();
85
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})");
92
93 try {
94 return $resource->allocateResource();
95 } catch (Exception $ex) {
96 $exceptions[] = $ex;
97 }
98 }
99
100 throw new PhutilAggregateException(
101 pht('Unable to allocate any binding as a resource.'),
102 $exceptions);
103 }
104
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
109 // any cleanup here.
110 return;
111 }
112
113 public function getResourceName(
114 DrydockBlueprint $blueprint,
115 DrydockResource $resource) {
116 $device_name = $resource->getAttribute(
117 'almanacDeviceName',
118 pht('<Unknown>'));
119 return pht('Host (%s)', $device_name);
120 }
121
122 public function canAcquireLeaseOnResource(
123 DrydockBlueprint $blueprint,
124 DrydockResource $resource,
125 DrydockLease $lease) {
126
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()) {
131 return false;
132 }
133
134 return true;
135 }
136
137 public function acquireLease(
138 DrydockBlueprint $blueprint,
139 DrydockResource $resource,
140 DrydockLease $lease) {
141
142 $lease
143 ->setActivateWhenAcquired(true)
144 ->acquireOnResource($resource);
145 }
146
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.
153 return;
154 }
155
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.
162 return;
163 }
164
165 public function getType() {
166 return 'host';
167 }
168
169 public function getInterface(
170 DrydockBlueprint $blueprint,
171 DrydockResource $resource,
172 DrydockLease $lease,
173 $type) {
174
175 switch ($type) {
176 case DrydockCommandInterface::INTERFACE_TYPE:
177 $credential_phid = $blueprint->getFieldValue('credentialPHID');
178 $binding = $this->loadBindingForResource($resource);
179 $interface = $binding->getInterface();
180
181 return id(new DrydockSSHCommandInterface())
182 ->setConfig('credentialPHID', $credential_phid)
183 ->setConfig('host', $interface->getAddress())
184 ->setConfig('port', $interface->getPort());
185 }
186 }
187
188 protected function getCustomFieldSpecifications() {
189 return array(
190 'almanacServicePHIDs' => array(
191 'name' => pht('Almanac Services'),
192 'type' => 'datasource',
193 'datasource.class' => 'AlmanacServiceDatasource',
194 'datasource.parameters' => array(
195 'serviceTypes' => $this->getAlmanacServiceTypes(),
196 ),
197 'required' => true,
198 ),
199 'credentialPHID' => array(
200 'name' => pht('Credentials'),
201 'type' => 'credential',
202 'credential.provides' =>
203 PassphraseSSHPrivateKeyCredentialType::PROVIDES_TYPE,
204 'credential.type' =>
205 PassphraseSSHPrivateKeyTextCredentialType::CREDENTIAL_TYPE,
206 ),
207 );
208 }
209
210 private function loadServices(DrydockBlueprint $blueprint) {
211 if (!$this->services) {
212 $service_phids = $blueprint->getFieldValue('almanacServicePHIDs');
213 if (!$service_phids) {
214 throw new Exception(
215 pht(
216 'This blueprint ("%s") does not define any Almanac Service PHIDs.',
217 $blueprint->getBlueprintName()));
218 }
219
220 $viewer = $this->getViewer();
221 $services = id(new AlmanacServiceQuery())
222 ->setViewer($viewer)
223 ->withPHIDs($service_phids)
224 ->withServiceTypes($this->getAlmanacServiceTypes())
225 ->needActiveBindings(true)
226 ->execute();
227 $services = mpull($services, null, 'getPHID');
228
229 if (count($services) != count($service_phids)) {
230 $missing_phids = array_diff($service_phids, array_keys($services));
231 throw new Exception(
232 pht(
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)));
237 }
238
239 $this->services = $services;
240 }
241
242 return $this->services;
243 }
244
245 /**
246 * @param array<AlmanacService> $services
247 */
248 private function getActiveBindings(array $services) {
249 assert_instances_of($services, AlmanacService::class);
250 $bindings = array_mergev(mpull($services, 'getActiveBindings'));
251 return mpull($bindings, null, 'getPHID');
252 }
253
254 private function loadFreeBindings(DrydockBlueprint $blueprint) {
255 if ($this->freeBindings === null) {
256 $viewer = $this->getViewer();
257
258 $pool = id(new DrydockResourceQuery())
259 ->setViewer($viewer)
260 ->withBlueprintPHIDs(array($blueprint->getPHID()))
261 ->withStatuses(
262 array(
263 DrydockResourceStatus::STATUS_PENDING,
264 DrydockResourceStatus::STATUS_ACTIVE,
265 DrydockResourceStatus::STATUS_BROKEN,
266 DrydockResourceStatus::STATUS_RELEASED,
267 ))
268 ->execute();
269
270 $allocated_phids = array();
271 foreach ($pool as $resource) {
272 $allocated_phids[] = $resource->getAttribute('almanacBindingPHID');
273 }
274 $allocated_phids = array_fuse($allocated_phids);
275
276 $services = $this->loadServices($blueprint);
277 $bindings = $this->getActiveBindings($services);
278
279 $free = array();
280 foreach ($bindings as $binding) {
281 if (empty($allocated_phids[$binding->getPHID()])) {
282 $free[] = $binding;
283 }
284 }
285
286 $this->freeBindings = $free;
287 }
288
289 return $this->freeBindings;
290 }
291
292 private function getAlmanacServiceTypes() {
293 return array(
294 AlmanacDrydockPoolServiceType::SERVICETYPE,
295 );
296 }
297
298 private function loadBindingForResource(DrydockResource $resource) {
299 $binding_phid = $resource->getAttribute('almanacBindingPHID');
300 if (!$binding_phid) {
301 throw new Exception(
302 pht(
303 'Drydock resource ("%s") has no Almanac binding PHID, so its '.
304 'binding can not be loaded.',
305 $resource->getPHID()));
306 }
307
308 $viewer = $this->getViewer();
309
310 $binding = id(new AlmanacBindingQuery())
311 ->setViewer($viewer)
312 ->withPHIDs(array($binding_phid))
313 ->executeOne();
314 if (!$binding) {
315 throw new Exception(
316 pht(
317 'Unable to load Almanac binding ("%s") for resource ("%s").',
318 $binding_phid,
319 $resource->getPHID()));
320 }
321
322 return $binding;
323 }
324
325}