Allow "bin/drydock lease ..." to select particular blueprints with "--blueprint"
[phabricator.git] / src / applications / drydock / management / DrydockManagementLeaseWorkflow.php
blobaf85f6bbec931ccab2a645202f2682fb8a2cbc4f
1 <?php
3 final class DrydockManagementLeaseWorkflow
4 extends DrydockManagementWorkflow {
6 protected function didConstruct() {
7 $this
8 ->setName('lease')
9 ->setSynopsis(pht('Lease a resource.'))
10 ->setArguments(
11 array(
12 array(
13 'name' => 'type',
14 'param' => 'resource_type',
15 'help' => pht('Resource type.'),
17 array(
18 'name' => 'until',
19 'param' => 'time',
20 'help' => pht('Set lease expiration time.'),
22 array(
23 'name' => 'attributes',
24 'param' => 'file',
25 'help' => pht(
26 'JSON file with lease attributes. Use "-" to read attributes '.
27 'from stdin.'),
29 array(
30 'name' => 'count',
31 'param' => 'N',
32 'default' => 1,
33 'help' => pht('Lease a given number of identical resources.'),
35 array(
36 'name' => 'blueprint',
37 'param' => 'identifier',
38 'repeat' => true,
39 'help' => pht('Lease resources from a specific blueprint.'),
41 ));
44 public function execute(PhutilArgumentParser $args) {
45 $viewer = $this->getViewer();
47 $resource_type = $args->getArg('type');
48 if (!phutil_nonempty_string($resource_type)) {
49 throw new PhutilArgumentUsageException(
50 pht(
51 'Specify a resource type with "--type".'));
54 $until = $args->getArg('until');
55 if (phutil_nonempty_string($until)) {
56 $until = strtotime($until);
57 if ($until <= 0) {
58 throw new PhutilArgumentUsageException(
59 pht(
60 'Unable to parse argument to "--until".'));
64 $count = $args->getArgAsInteger('count');
65 if ($count < 1) {
66 throw new PhutilArgumentUsageException(
67 pht(
68 'Value provided to "--count" must be a nonzero, positive '.
69 'number.'));
72 $attributes_file = $args->getArg('attributes');
73 if (phutil_nonempty_string($attributes_file)) {
74 if ($attributes_file == '-') {
75 echo tsprintf(
76 "%s\n",
77 pht('Reading JSON attributes from stdin...'));
78 $data = file_get_contents('php://stdin');
79 } else {
80 $data = Filesystem::readFile($attributes_file);
83 $attributes = phutil_json_decode($data);
84 } else {
85 $attributes = array();
88 $filter_identifiers = $args->getArg('blueprint');
89 if ($filter_identifiers) {
90 $filter_blueprints = $this->getBlueprintFilterMap($filter_identifiers);
91 } else {
92 $filter_blueprints = array();
95 $blueprint_phids = null;
97 $leases = array();
98 for ($idx = 0; $idx < $count; $idx++) {
99 $lease = id(new DrydockLease())
100 ->setResourceType($resource_type);
102 $drydock_phid = id(new PhabricatorDrydockApplication())->getPHID();
103 $lease->setAuthorizingPHID($drydock_phid);
105 if ($attributes) {
106 $lease->setAttributes($attributes);
109 if ($blueprint_phids === null) {
110 $blueprint_phids = $this->newAllowedBlueprintPHIDs(
111 $lease,
112 $filter_blueprints);
115 $lease->setAllowedBlueprintPHIDs($blueprint_phids);
117 if ($until) {
118 $lease->setUntil($until);
121 // If something fatals or the user interrupts the process (for example,
122 // with "^C"), release the lease. We'll cancel this below, if the lease
123 // actually activates.
124 $lease->setReleaseOnDestruction(true);
126 $leases[] = $lease;
129 // TODO: This would probably be better handled with PhutilSignalRouter,
130 // but it currently doesn't route SIGINT. We're initializing it to setup
131 // SIGTERM handling and make eventual migration easier.
132 $router = PhutilSignalRouter::getRouter();
133 pcntl_signal(SIGINT, array($this, 'didReceiveInterrupt'));
135 $t_start = microtime(true);
138 echo tsprintf(
139 "%s\n\n",
140 pht('Leases queued for activation:'));
142 foreach ($leases as $lease) {
143 $lease->queueForActivation();
145 echo tsprintf(
146 " __%s__\n",
147 PhabricatorEnv::getProductionURI($lease->getURI()));
150 echo tsprintf(
151 "\n%s\n\n",
152 pht('Waiting for daemons to activate leases...'));
154 foreach ($leases as $lease) {
155 $this->waitUntilActive($lease);
158 // Now that we've survived activation and the lease is good, make it
159 // durable.
160 foreach ($leases as $lease) {
161 $lease->setReleaseOnDestruction(false);
164 $t_end = microtime(true);
166 echo tsprintf(
167 "\n%s\n\n",
168 pht(
169 'Activation complete. Leases are permanent until manually '.
170 'released with:'));
172 foreach ($leases as $lease) {
173 echo tsprintf(
174 " %s\n",
175 pht('$ ./bin/drydock release-lease --id %d', $lease->getID()));
178 echo tsprintf(
179 "\n%s\n",
180 pht(
181 'Leases activated in %sms.',
182 new PhutilNumber((int)(($t_end - $t_start) * 1000))));
184 return 0;
187 public function didReceiveInterrupt($signo) {
188 // Doing this makes us run destructors, particularly the "release on
189 // destruction" trigger on the lease.
190 exit(128 + $signo);
193 private function waitUntilActive(DrydockLease $lease) {
194 $viewer = $this->getViewer();
196 $log_cursor = 0;
197 $log_types = DrydockLogType::getAllLogTypes();
199 $is_active = false;
200 while (!$is_active) {
201 $lease->reload();
203 $pager = id(new AphrontCursorPagerView())
204 ->setBeforeID($log_cursor);
206 // While we're waiting, show the user any logs which the daemons have
207 // generated to give them some clue about what's going on.
208 $logs = id(new DrydockLogQuery())
209 ->setViewer($viewer)
210 ->withLeasePHIDs(array($lease->getPHID()))
211 ->executeWithCursorPager($pager);
212 if ($logs) {
213 $logs = mpull($logs, null, 'getID');
214 ksort($logs);
215 $log_cursor = last_key($logs);
218 foreach ($logs as $log) {
219 $type_key = $log->getType();
220 if (isset($log_types[$type_key])) {
221 $type_object = id(clone $log_types[$type_key])
222 ->setLog($log)
223 ->setViewer($viewer);
225 $log_data = $log->getData();
227 $type = $type_object->getLogTypeName();
228 $data = $type_object->renderLogForText($log_data);
229 } else {
230 $type = pht('Unknown ("%s")', $type_key);
231 $data = null;
234 echo tsprintf(
235 "(Lease #%d) <%s> %B\n",
236 $lease->getID(),
237 $type,
238 $data);
241 $status = $lease->getStatus();
243 switch ($status) {
244 case DrydockLeaseStatus::STATUS_ACTIVE:
245 $is_active = true;
246 break;
247 case DrydockLeaseStatus::STATUS_RELEASED:
248 throw new Exception(pht('Lease has already been released!'));
249 case DrydockLeaseStatus::STATUS_DESTROYED:
250 throw new Exception(pht('Lease has already been destroyed!'));
251 case DrydockLeaseStatus::STATUS_BROKEN:
252 throw new Exception(pht('Lease has been broken!'));
253 case DrydockLeaseStatus::STATUS_PENDING:
254 case DrydockLeaseStatus::STATUS_ACQUIRED:
255 break;
256 default:
257 throw new Exception(
258 pht(
259 'Lease has unknown status "%s".',
260 $status));
263 if ($is_active) {
264 break;
265 } else {
266 sleep(1);
271 private function getBlueprintFilterMap(array $identifiers) {
272 $viewer = $this->getViewer();
274 $query = id(new DrydockBlueprintQuery())
275 ->setViewer($viewer)
276 ->withIdentifiers($identifiers);
278 $blueprints = $query->execute();
279 $blueprints = mpull($blueprints, null, 'getPHID');
281 $map = $query->getIdentifierMap();
283 $seen = array();
284 foreach ($identifiers as $identifier) {
285 if (!isset($map[$identifier])) {
286 throw new PhutilArgumentUsageException(
287 pht(
288 'Blueprint "%s" could not be loaded. Try a blueprint ID or '.
289 'PHID.',
290 $identifier));
293 $blueprint = $map[$identifier];
295 $blueprint_phid = $blueprint->getPHID();
296 if (isset($seen[$blueprint_phid])) {
297 throw new PhutilArgumentUsageException(
298 pht(
299 'Blueprint "%s" is specified more than once (as "%s" and "%s").',
300 $blueprint->getBlueprintName(),
301 $seen[$blueprint_phid],
302 $identifier));
305 $seen[$blueprint_phid] = true;
308 return mpull($map, null, 'getPHID');
311 private function newAllowedBlueprintPHIDs(
312 DrydockLease $lease,
313 array $filter_blueprints) {
314 assert_instances_of($filter_blueprints, 'DrydockBlueprint');
316 $viewer = $this->getViewer();
318 $impls = DrydockBlueprintImplementation::getAllForAllocatingLease($lease);
320 if (!$impls) {
321 throw new PhutilArgumentUsageException(
322 pht(
323 'No known blueprint class can ever allocate the specified '.
324 'lease. Check that the resource type is spelled correctly.'));
327 $classes = array_keys($impls);
329 $blueprints = id(new DrydockBlueprintQuery())
330 ->setViewer($viewer)
331 ->withBlueprintClasses($classes)
332 ->withDisabled(false)
333 ->execute();
335 if (!$blueprints) {
336 throw new PhutilArgumentUsageException(
337 pht(
338 'No enabled blueprints exist with a blueprint class that can '.
339 'plausibly allocate resources to satisfy the requested lease.'));
342 $phids = mpull($blueprints, 'getPHID');
344 if ($filter_blueprints) {
345 $allowed_map = array_fuse($phids);
346 $filter_map = mpull($filter_blueprints, null, 'getPHID');
348 foreach ($filter_map as $filter_phid => $blueprint) {
349 if (!isset($allowed_map[$filter_phid])) {
350 throw new PhutilArgumentUsageException(
351 pht(
352 'Specified blueprint "%s" is not capable of satisfying the '.
353 'configured lease.',
354 $blueprint->getBlueprintName()));
358 $phids = mpull($filter_blueprints, 'getPHID');
361 return $phids;