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( "** [%s] ** %s\n", $command->getLabel(), pht( 'Command failure (%d).', $exit_code)); } else { $okay_count++; } } if ($okay_count === count($commands)) { echo tsprintf( "** %s ** %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; } }