diff --git a/src/app/Backends/IMAP.php b/src/app/Backends/IMAP.php --- a/src/app/Backends/IMAP.php +++ b/src/app/Backends/IMAP.php @@ -31,6 +31,19 @@ } /** + * Check if we can connect to the imap server + * + * @return bool True on success + */ + public static function healthcheck(): bool + { + $config = self::getConfig(); + $imap = self::initIMAP($config); + $imap->closeConnection(); + return true; + } + + /** * Check if a shared folder is set up. * * @param string $folder Folder name, e.g. shared/Resources/Name@domain.tld diff --git a/src/app/Backends/LDAP.php b/src/app/Backends/LDAP.php --- a/src/app/Backends/LDAP.php +++ b/src/app/Backends/LDAP.php @@ -65,6 +65,30 @@ } /** + * Validates that ldap is available as configured. + * + * @throws \Exception + */ + public static function healthcheck(): void + { + $config = self::getConfig('admin'); + $ldap = self::initLDAP($config); + + $mgmtRootDN = \config('ldap.admin.root_dn'); + $hostedRootDN = \config('ldap.hosted.root_dn'); + + $result = $ldap->search($mgmtRootDN, '', 'base'); + if (!$result || $result->count() != 1) { + self::throwException($ldap, "Failed to find the configured management domain $mgmtRootDN"); + } + + $result = $ldap->search($hostedRootDN, '', 'base'); + if (!$result || $result->count() != 1) { + self::throwException($ldap, "Failed to find the configured hosted domain $hostedRootDN"); + } + } + + /** * Create a domain in LDAP. * * @param \App\Domain $domain The domain to create. diff --git a/src/app/Backends/OpenExchangeRates.php b/src/app/Backends/OpenExchangeRates.php --- a/src/app/Backends/OpenExchangeRates.php +++ b/src/app/Backends/OpenExchangeRates.php @@ -49,4 +49,25 @@ throw new \Exception("Failed to retrieve exchange rates"); } + + /** + * Validates that openexchange is available as configured. + * + * @throws \Exception + */ + public static function healthcheck(): void + { + $apiKey = \config('services.openexchangerates.api_key'); + if (!empty($apiKey)) { + $query = http_build_query(['app_id' => $apiKey]); + $url = 'https://openexchangerates.org/api/usage.json' . $query; + $html = file_get_contents($url, false); + + if ($html && ($result = json_decode($html, true)) && !empty($result['status'])) { + print($result); + } + + throw new \Exception("Failed to retrieve exchange rates status"); + } + } } diff --git a/src/app/Console/Commands/Status/Health.php b/src/app/Console/Commands/Status/Health.php new file mode 100644 --- /dev/null +++ b/src/app/Console/Commands/Status/Health.php @@ -0,0 +1,207 @@ +line($exception); + return false; + } + } + + private function checkOpenExchangeRates() + { + try { + OpenExchangeRates::healthcheck(); + return true; + } catch (\Exception $exception) { + $this->line($exception); + return false; + } + } + + private function checkMollie() + { + try { + return Mollie::healthcheck(); + } catch (\Exception $exception) { + $this->line($exception); + return false; + } + } + + private function checkLDAP() + { + try { + LDAP::healthcheck(); + return true; + } catch (\Exception $exception) { + $this->line($exception); + return false; + } + } + + private function checkIMAP() + { + try { + IMAP::healthcheck(); + return true; + } catch (\Exception $exception) { + $this->line($exception); + return false; + } + } + + private function checkRoundcube() + { + try { + //TODO maybe run a select? + Roundcube::dbh(); + return true; + } catch (\Exception $exception) { + $this->line($exception); + return false; + } + } + + private function checkRedis() + { + try { + Redis::connection(); + return true; + } catch (\Exception $exception) { + $this->line($exception); + return false; + } + } + + private function checkMeet() + { + try { + $urls = \config('meet.api_urls'); + foreach ($urls as $url) { + $this->line("Checking $url"); + + $client = new \GuzzleHttp\Client( + [ + 'http_errors' => false, // No exceptions from Guzzle + 'base_uri' => $url, + 'verify' => \config('meet.api_verify_tls'), + 'headers' => [ + 'X-Auth-Token' => \config('meet.api_token'), + ], + 'connect_timeout' => 10, + 'timeout' => 10, + 'on_stats' => function (\GuzzleHttp\TransferStats $stats) { + $threshold = \config('logging.slow_log'); + if ($threshold && ($sec = $stats->getTransferTime()) > $threshold) { + $url = $stats->getEffectiveUri(); + $method = $stats->getRequest()->getMethod(); + \Log::warning(sprintf("[STATS] %s %s: %.4f sec.", $method, $url, $sec)); + } + }, + ] + ); + + $response = $client->request('GET', "ping"); + if ($response->getStatusCode() != 200) { + $this->line("Backend not available: " . var_export($response, true)); + return false; + } + } + return true; + } catch (\Exception $exception) { + $this->line($exception); + return false; + } + } + + /** + * Execute the console command. + * + * @return mixed + */ + public function handle() + { + $this->line("Checking DB..."); + if ($this->checkDB()) { + $this->info("OK"); + } else { + $this->error("Not found"); + } + $this->line("Checking Redis..."); + if ($this->checkRedis()) { + $this->info("OK"); + } else { + $this->error("Not found"); + } + $this->line("Checking LDAP..."); + if ($this->checkLDAP()) { + $this->info("OK"); + } else { + $this->error("Not found"); + } + $this->line("Checking IMAP..."); + if ($this->checkIMAP()) { + $this->info("OK"); + } else { + $this->error("Not found"); + } + $this->line("Checking Roundcube..."); + if ($this->checkRoundcube()) { + $this->info("OK"); + } else { + $this->error("Not found"); + } + $this->line("Checking Meet..."); + if ($this->checkMeet()) { + $this->info("OK"); + } else { + $this->error("Not found"); + } + $this->line("Checking OpenExchangeRates..."); + if ($this->checkOpenExchangeRates()) { + $this->info("OK"); + } else { + $this->error("Not found"); + } + $this->line("Checking Mollie..."); + if ($this->checkMollie()) { + $this->info("OK"); + } else { + $this->error("Not found"); + } + } +} diff --git a/src/app/Providers/Payment/Mollie.php b/src/app/Providers/Payment/Mollie.php --- a/src/app/Providers/Payment/Mollie.php +++ b/src/app/Providers/Payment/Mollie.php @@ -34,6 +34,18 @@ } /** + * Validates that mollie available. + * + * @throws \Mollie\Api\Exceptions\ApiException on failure + * @return bool true on success + */ + public static function healthcheck() + { + mollie()->methods()->allActive(); + return true; + } + + /** * Create a new auto-payment mandate for a wallet. * * @param \App\Wallet $wallet The wallet