379 lines
11 KiB
PHP
Executable file
379 lines
11 KiB
PHP
Executable file
#!/usr/bin/env php
|
|
<?php
|
|
require_once 'vendor/autoload.php';
|
|
|
|
use Bramus\Monolog\Formatter\ColoredLineFormatter;
|
|
use GuzzleHttp\Client as Guzzle;
|
|
use Monolog\Handler\StreamHandler;
|
|
use Monolog\Logger;
|
|
use Spatie\Emoji\Emoji;
|
|
|
|
class AgentState
|
|
{
|
|
private string $hostname = 'unknown host';
|
|
/** @var string[] */
|
|
private array $labels = [];
|
|
private int $containerCount = 0;
|
|
private int $containerCountRunning = 0;
|
|
|
|
private int $memAvailableKB = 0;
|
|
private int $memFreeKB = 0;
|
|
private int $memTotalKB = 0;
|
|
private int $swapFreeKB = 0;
|
|
private int $swapTotalKB = 0;
|
|
|
|
public function __toString(): string
|
|
{
|
|
return sprintf(
|
|
'Population: %d running of %d. Memory %.2fGB/%.2fGB. Swap %.2fGB/%.2fGB',
|
|
$this->getContainerCountRunning(),
|
|
$this->getContainerCount(),
|
|
$this->getMemAvailableKB() / 1024 / 1024,
|
|
$this->getMemTotalKB() / 1024 / 1024,
|
|
$this->getSwapFreeKB() / 1024 / 1024,
|
|
$this->getSwapTotalKB() / 1024 / 1024,
|
|
);
|
|
}
|
|
|
|
public function emit(Redis $redis, int $redisExpiry = 60): void
|
|
{
|
|
$instanceDataKey = "swarm:{$this->getHostname()}";
|
|
|
|
$redis->hset($instanceDataKey, "population_running", $this->getContainerCountRunning());
|
|
$redis->hset($instanceDataKey, "population_total", $this->getContainerCount());
|
|
$redis->hset($instanceDataKey, "memory_available_kb", $this->getMemAvailableKB());
|
|
$redis->hset($instanceDataKey, "memory_free_kb", $this->getMemFreeKB());
|
|
$redis->hset($instanceDataKey, "memory_total_kb", $this->getMemTotalKB());
|
|
$redis->hset($instanceDataKey, "swap_free_kb", $this->getSwapFreeKB());
|
|
$redis->hset($instanceDataKey, "swap_total_kb", $this->getSwapTotalKB());
|
|
$redis->hset($instanceDataKey, "updated_at", date("Y-m-d H:i:s"));
|
|
$redis->hset($instanceDataKey, "labels", implode(", ", $this->getLabels()));
|
|
|
|
$redis->expire($instanceDataKey, $redisExpiry);
|
|
}
|
|
|
|
public function getHostname(): string
|
|
{
|
|
return strtolower($this->hostname);
|
|
}
|
|
|
|
public function setHostname(string $hostname): AgentState
|
|
{
|
|
$this->hostname = $hostname;
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function getContainerCount(): int
|
|
{
|
|
return $this->containerCount;
|
|
}
|
|
|
|
public function setContainerCount(int $containerCount): AgentState
|
|
{
|
|
$this->containerCount = $containerCount;
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function getContainerCountRunning(): int
|
|
{
|
|
return $this->containerCountRunning;
|
|
}
|
|
|
|
public function setContainerCountRunning(int $containerCountRunning): AgentState
|
|
{
|
|
$this->containerCountRunning = $containerCountRunning;
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function getMemAvailableKB(): int
|
|
{
|
|
return $this->memAvailableKB;
|
|
}
|
|
|
|
public function setMemAvailableKB(int $memAvailableKB): AgentState
|
|
{
|
|
$this->memAvailableKB = $memAvailableKB;
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function getMemFreeKB(): int
|
|
{
|
|
return $this->memFreeKB;
|
|
}
|
|
|
|
public function setMemFreeKB(int $memFreeKB): AgentState
|
|
{
|
|
$this->memFreeKB = $memFreeKB;
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function getMemTotalKB(): int
|
|
{
|
|
return $this->memTotalKB;
|
|
}
|
|
|
|
public function setMemTotalKB(int $memTotalKB): AgentState
|
|
{
|
|
$this->memTotalKB = $memTotalKB;
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function getSwapFreeKB(): int
|
|
{
|
|
return $this->swapFreeKB;
|
|
}
|
|
|
|
public function setSwapFreeKB(int $swapFreeKB): AgentState
|
|
{
|
|
$this->swapFreeKB = $swapFreeKB;
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function getSwapTotalKB(): int
|
|
{
|
|
return $this->swapTotalKB;
|
|
}
|
|
|
|
public function setSwapTotalKB(int $swapTotalKB): AgentState
|
|
{
|
|
$this->swapTotalKB = $swapTotalKB;
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* @return string[]
|
|
*/
|
|
public function getLabels(): array
|
|
{
|
|
return $this->labels;
|
|
}
|
|
|
|
/**
|
|
* @param string[] $labels
|
|
* @return AgentState
|
|
*/
|
|
public function setLabels(array $labels): AgentState
|
|
{
|
|
$this->labels = $labels;
|
|
return $this;
|
|
}
|
|
|
|
}
|
|
class Agent
|
|
{
|
|
private array $environment;
|
|
private Guzzle $client;
|
|
private Logger $logger;
|
|
private ?string $instanceStateHash = null;
|
|
private ?AgentState $currentState = null;
|
|
private ?AgentState $lastState = null;
|
|
private Redis $redis;
|
|
private int $minimumUpdateIntervalSeconds = 60;
|
|
private int $lastRunTimestamp = 0;
|
|
|
|
public function __construct()
|
|
{
|
|
$this->environment = array_merge($_ENV, $_SERVER);
|
|
ksort($this->environment);
|
|
|
|
$this->logger = new Monolog\Logger('agent');
|
|
$this->logger->pushHandler(new StreamHandler('/var/log/swarm-agent.log', Logger::DEBUG));
|
|
$stdout = new StreamHandler('php://stdout', Logger::DEBUG);
|
|
$stdout->setFormatter(new ColoredLineFormatter(null, "%level_name%: %message% \n"));
|
|
$this->logger->pushHandler($stdout);
|
|
|
|
if(isset($this->environment['MINIMUM_UPDATE_INTERVAL'])){
|
|
$this->minimumUpdateIntervalSeconds = $this->environment['MINIMUM_UPDATE_INTERVAL'];
|
|
}
|
|
|
|
$this->redis = new Redis();
|
|
$this->redis->pconnect(
|
|
$this->environment['REDIS_HOST'] ?? "redis",
|
|
$this->environment['REDIS_PORT'] ?? 6379
|
|
);
|
|
|
|
$this->client = new Guzzle(
|
|
[
|
|
'base_uri' => 'http://localhost',
|
|
'curl' => [
|
|
CURLOPT_UNIX_SOCKET_PATH => '/var/run/docker.sock',
|
|
],
|
|
]
|
|
);
|
|
|
|
// Prevent double-starting
|
|
$this->lastRunTimestamp = time();
|
|
}
|
|
|
|
public function findContainersSwarmMode(): array
|
|
{
|
|
$services = json_decode($this->client->request('GET', 'services')->getBody()->getContents(), true);
|
|
}
|
|
|
|
public function run(): void
|
|
{
|
|
$this->logger->info(sprintf('%s Starting Swarm Agent...', Emoji::CHARACTER_TIMER_CLOCK));
|
|
while (true) {
|
|
$this->runLoop();
|
|
}
|
|
}
|
|
|
|
private function calculateStateHash(): string
|
|
{
|
|
$newInstanceStates = [];
|
|
|
|
$containers = json_decode($this->client->request('GET', 'containers/json')->getBody()->getContents(), true);
|
|
foreach ($containers as $container) {
|
|
$inspect = json_decode($this->client->request('GET', "containers/{$container['Id']}/json")->getBody()->getContents(), true);
|
|
$newInstanceStates['container-'.$inspect['Id']] = implode('::', [
|
|
$inspect['Name'],
|
|
$inspect['Created'],
|
|
$inspect['Image'],
|
|
$inspect['State']['Status'],
|
|
sha1(implode('|', $inspect['Config']['Env'])),
|
|
]);
|
|
}
|
|
|
|
return sha1(implode("\n", $newInstanceStates));
|
|
}
|
|
|
|
/**
|
|
* Returns true when something has changed.
|
|
*
|
|
* @throws \GuzzleHttp\Exception\GuzzleException
|
|
*/
|
|
private function stateHasChanged(): bool
|
|
{
|
|
$newStateHash = $this->calculateStateHash();
|
|
// $this->logger->debug(sprintf("Old state = %s. New State = %s.", substr($this->instanceStateHash,0,7), substr($newStateHash, 0,7)));
|
|
if (!$this->instanceStateHash || $this->instanceStateHash != $newStateHash) {
|
|
$this->instanceStateHash = $newStateHash;
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private function timeElapsed() : bool
|
|
{
|
|
if($this->lastRunTimestamp < time() - $this->minimumUpdateIntervalSeconds){
|
|
$this->lastRunTimestamp = time();
|
|
$this->logger->debug(sprintf("Max interval of %d seconds has elapsed", $this->minimumUpdateIntervalSeconds));
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private function containerInventory(): void
|
|
{
|
|
$containers = json_decode($this->client->request('GET', 'containers/json')->getBody()->getContents(), true);
|
|
|
|
$runningContainers = 0;
|
|
foreach ($containers as $container) {
|
|
if ($container['State'] == 'running') {
|
|
++$runningContainers;
|
|
} else {
|
|
\Kint::dump($container['State']);
|
|
|
|
exit;
|
|
}
|
|
}
|
|
|
|
$this->currentState
|
|
->setContainerCount(count($containers))
|
|
->setContainerCountRunning($runningContainers)
|
|
;
|
|
}
|
|
|
|
private function runLoop(): void
|
|
{
|
|
if ($this->currentState instanceof AgentState) {
|
|
$this->lastState = $this->currentState;
|
|
}
|
|
$this->currentState = (new AgentState());
|
|
|
|
$this->probeSysInfo();
|
|
|
|
$this->containerInventory();
|
|
$this->probeMemory();
|
|
$this->probeDisks();
|
|
$this->logger->debug($this->currentState->__toString());
|
|
$this->currentState->emit($this->redis, $this->minimumUpdateIntervalSeconds+30);
|
|
|
|
$this->waitUntilContainerChange();
|
|
}
|
|
|
|
private function probeSysInfo() : void
|
|
{
|
|
$info = json_decode($this->client->request('GET', 'info')->getBody()->getContents(), true);
|
|
$this->currentState->setHostname($info['Name']);
|
|
$this->currentState->setLabels($info['Labels']);
|
|
}
|
|
|
|
private function probeMemory(): void
|
|
{
|
|
$memInfo = file_get_contents('/proc/meminfo');
|
|
foreach (explode("\n", $memInfo) as $line) {
|
|
if (stripos($line, ':') === false) {
|
|
continue;
|
|
}
|
|
[$key, $value] = explode(':', $line);
|
|
$key = trim($key);
|
|
$value = trim($value);
|
|
$value = str_replace(' kB', '', $value);
|
|
|
|
switch ($key) {
|
|
case 'MemTotal':
|
|
$this->currentState->setMemTotalKB($value);
|
|
|
|
break;
|
|
|
|
case 'MemFree':
|
|
$this->currentState->setMemFreeKB($value);
|
|
|
|
break;
|
|
|
|
case 'MemAvailable':
|
|
$this->currentState->setMemAvailableKB($value);
|
|
|
|
break;
|
|
|
|
case 'SwapTotal':
|
|
$this->currentState->setSwapTotalKB($value);
|
|
|
|
break;
|
|
|
|
case 'SwapFree':
|
|
$this->currentState->setSwapFreeKB($value);
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
private function probeDisks(): void
|
|
{
|
|
// @todo
|
|
// $diskinfo = file_get_contents("/proc/diskstats")
|
|
}
|
|
|
|
private function waitUntilContainerChange(): void
|
|
{
|
|
while ($this->stateHasChanged() === false && $this->timeElapsed() === false) {
|
|
sleep(5);
|
|
}
|
|
$this->logger->info(sprintf('%s Host Container state has changed', Emoji::CHARACTER_WARNING));
|
|
}
|
|
}
|
|
|
|
(new Agent())->run();
|