Page MenuHomePhabricator
Paste P2107

PhageRemoteWorkflow.php
ActivePublic

Authored by epriestley on Aug 2 2018, 10:20 PM.
Tags
None
Referenced Files
F5777615: PhageRemoteWorkflow.php
Aug 2 2018, 10:20 PM
Subscribers
None
<?php
final class PhageRemoteWorkflow
extends PhageWorkflow {
protected function didConstruct() {
$this
->setName('remote')
->setExamples('**remote** --hosts __hosts__ [__options__] -- __command__')
->setSynopsis(pht('Run a "bin/remote" command on a group of hosts.'))
->setArguments(
array(
array(
'name' => 'hosts',
'param' => 'hosts',
'help' => pht('Run on hosts.'),
),
array(
'name' => 'pools',
'param' => 'pools',
'help' => pht('Run on pools.'),
),
array(
'name' => 'limit',
'param' => 'count',
'help' => pht('Limit parallelism.'),
),
array(
'name' => 'throttle',
'param' => 'seconds',
'help' => pht('Wait this many seconds between commands.'),
),
array(
'name' => 'args',
'wildcard' => true,
'help' => pht('Arguments to pass to "bin/remote".'),
),
array(
'name' => 'timeout',
'param' => 'seconds',
'help' => pht('Command timeout in seconds.'),
),
));
}
public function execute(PhutilArgumentParser $args) {
$hosts = $args->getArg('hosts');
$pools = $args->getArg('pools');
if (!strlen($hosts) && !strlen($pools)) {
throw new PhutilArgumentUsageException(
pht(
'Provide a list of hosts to execute on with "--hosts", or a '.
'list of host pools with "--pools".'));
}
$remote_args = $args->getArg('args');
if (!$remote_args) {
throw new PhutilArgumentUsageException(
pht('Provide a remote command to execute.'));
}
$limit = $args->getArg('limit');
$throttle = $args->getArg('throttle');
$timeout = $args->getArg('timeout');
$hosts = $this->expandHosts($hosts, $pools);
$plan = new PhagePlanAction();
$local = new PhageLocalAction();
if ($limit) {
$local->setLimit($limit);
}
if ($throttle) {
$local->setThrottle($throttle);
}
$plan->addAction($local);
$bin_remote = PhacilityCore::getCorePath('bin/remote');
$commands = array();
foreach ($hosts as $host) {
$host_args = $remote_args;
array_splice($host_args, 1, 0, array($host));
$command = csprintf(
'%R %Ls',
$bin_remote,
$host_args);
$execute = id(new PhageExecuteAction())
->setLabel($host)
->setCommand($command);
if ($timeout) {
$execute->setTimeout($timeout);
}
$commands[] = $execute;
$local->addAction($execute);
}
$t_start = microtime(true);
$plan->executePlan();
$t_end = microtime(true);
$done = pht('DONE');
echo tsprintf(
"\n-< %s >%s\n\n",
$done,
str_repeat('-', 80 - (5 + strlen($done))));
$okay_count = 0;
foreach ($commands as $command) {
$exit_code = $command->getExitCode();
if ($exit_code !== 0) {
echo tsprintf(
"**<bg:red> [%s] </bg>** %s\n",
$command->getLabel(),
pht(
'Command failure (%d).',
$exit_code));
} else {
$okay_count++;
}
}
if ($okay_count === count($commands)) {
echo tsprintf(
"**<bg:green> %s </bg>** %s\n",
pht('COMPLETE'),
pht(
'Everything went according to plan (in %sms).',
new PhutilNumber(1000 * ($t_end - $t_start))));
}
}
private function expandHosts($spec, $pools) {
$parts = preg_split('/[, ]+/', $spec);
$parts = array_filter($parts);
$hosts = array();
foreach ($parts as $part) {
$matches = null;
$ok = preg_match_all('/(\d+-\d+)/', $part, $matches, PREG_OFFSET_CAPTURE);
// If there's nothing like "001-12" in the specification, just use the
// raw host as provided.
if (!$ok) {
$hosts[] = $part;
continue;
}
if (count($matches[1]) > 1) {
throw new Exception(
pht(
'Host specification "%s" is ambiguous.',
$part));
}
$match = $matches[1][0][0];
$offset = $matches[1][0][1];
$range = explode('-', $match, 2);
$width = strlen($range[0]);
$min = (int)$range[0];
$max = (int)$range[1];
if ($min > $max) {
throw new Exception(
pht(
'Host range "%s" is invalid: minimum is larger than maximum.',
$match));
}
if (strlen($max) > $width) {
throw new Exception(
pht(
'Host range "%s" is invalid: range start does not have enough '.
'leading zeroes to contain the entire range.',
$match));
}
$values = range($min, $max);
foreach ($values as $value) {
$value = sprintf("%0{$width}d", $value);
$host = substr_replace($part, $value, $offset, strlen($match));
$hosts[] = $host;
}
}
if (strlen($pools)) {
$bin_remote = PhacilityCore::getCorePath('bin/remote');
list($stdout) = execx(
'%R list-hosts %R',
$bin_remote,
// NOTE: This is a dummy argument, but "bin/remote" currently requires
// a host target even if we're just going directly to the bastion. This
// could be cleaned up at some point.
'bastion-external.phacility.net');
$pool_hosts = phutil_json_decode($stdout);
$prefixes = preg_split('/[, ]+/', $pools);
$prefixes = array_fill_keys($prefixes, array());
foreach ($pool_hosts as $pool_host) {
$host = $pool_host['host'];
foreach ($prefixes as $prefix => $host_list) {
if (preg_match('(^'.preg_quote($prefix).'\d)', $host)) {
$prefixes[$prefix][] = $host;
}
}
}
foreach ($prefixes as $prefix => $host_list) {
if (!$host_list) {
throw new Exception(
pht(
'Pool "%s" matched no hosts. Use real pools which contain '.
'actual hosts.',
$prefix));
}
foreach ($host_list as $host) {
$hosts[] = $host;
}
}
}
return $hosts;
}
}