diff options
Diffstat (limited to 'application/clicommands')
-rw-r--r-- | application/clicommands/DownloadCommand.php | 98 | ||||
-rw-r--r-- | application/clicommands/ListCommand.php | 133 | ||||
-rw-r--r-- | application/clicommands/ScheduleCommand.php | 120 |
3 files changed, 348 insertions, 3 deletions
diff --git a/application/clicommands/DownloadCommand.php b/application/clicommands/DownloadCommand.php new file mode 100644 index 0000000..9b7c99d --- /dev/null +++ b/application/clicommands/DownloadCommand.php @@ -0,0 +1,98 @@ +<?php + +/* Icinga Reporting | (c) 2023 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Reporting\Clicommands; + +use Icinga\Exception\NotFoundError; +use Icinga\Module\Pdfexport\ProvidedHook\Pdfexport; +use Icinga\Module\Reporting\Cli\Command; +use Icinga\Module\Reporting\Database; +use Icinga\Module\Reporting\Model; +use Icinga\Module\Reporting\Report; +use InvalidArgumentException; +use ipl\Stdlib\Filter; + +class DownloadCommand extends Command +{ + /** + * Download report with specified ID as PDF, CSV or JSON + * + * USAGE + * + * icingacli reporting download <id> [--format=<pdf|csv|json>] + * + * OPTIONS + * + * --format=<pdf|csv|json> + * Download report as PDF, CSV or JSON. Defaults to pdf. + * + * --output=<file> + * Save report to the specified <file>. + * + * EXAMPLES + * + * Download report with ID 1: + * icingacli reporting download 1 + * + * Download report with ID 1 as CSV: + * icingacli reporting download 1 --format=csv + * + * Download report with ID 1 as JSON to the specified file: + * icingacli reporting download 1 --format=json --output=sla.json + */ + public function defaultAction() + { + $id = $this->params->getStandalone(); + if ($id === null) { + $this->fail($this->translate('Argument id is mandatory')); + } + + /** @var Model\Report $report */ + $report = Model\Report::on(Database::get()) + ->with('timeframe') + ->filter(Filter::equal('id', $id)) + ->first(); + + if ($report === null) { + throw new NotFoundError('Report not found'); + } + + $report = Report::fromModel($report); + + /** @var string $format */ + $format = $this->params->get('format', 'pdf'); + $format = strtolower($format); + switch ($format) { + case 'pdf': + $content = Pdfexport::first()->htmlToPdf($report->toPdf()); + break; + case 'csv': + $content = $report->toCsv(); + break; + case 'json': + $content = $report->toJson(); + break; + default: + throw new InvalidArgumentException(sprintf('Format %s is not supported', $format)); + } + + /** @var string $output */ + $output = $this->params->get('output'); + if ($output === null) { + $name = sprintf( + '%s (%s) %s', + $report->getName(), + $report->getTimeframe()->getName(), + date('Y-m-d H:i') + ); + + $output = "$name.$format"; + } elseif (is_dir($output)) { + $this->fail($this->translate(sprintf('%s is a directory', $output))); + } + + file_put_contents($output, $content); + echo "$output\n"; + } +} diff --git a/application/clicommands/ListCommand.php b/application/clicommands/ListCommand.php new file mode 100644 index 0000000..2486ae0 --- /dev/null +++ b/application/clicommands/ListCommand.php @@ -0,0 +1,133 @@ +<?php + +/* Icinga Reporting | (c) 2023 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Reporting\Clicommands; + +use Icinga\Module\Reporting\Cli\Command; +use Icinga\Module\Reporting\Database; +use Icinga\Module\Reporting\Model; +use InvalidArgumentException; +use ipl\Stdlib\Filter; + +class ListCommand extends Command +{ + /** + * List reports + * + * USAGE + * + * icingacli reporting list [OPTIONS] + * + * OPTIONS + * + * --sort=<id|name|author> + * Sort the reports by the given column. Defaults to id. + * + * --direction=<asc|desc> + * Sort the reports by the specified sort column in ascending or descending order. Defaults to asc. + * + * --filter=<name> + * Filter the reports by the specified report name. Performs a wildcard search by default. + * + * EXAMPLES + * + * Sort the reports by name: + * icingacli reporting list --sort=name + * + * Sort the reports by author in descending order: + * icingacli reporting list --sort=author --direction=DESC + * + * Filter the reports that contain "Host" in the report name: + * icingacli reporting list --filter=Host + * + * Filter the reports that begin with "Service": + * icingacli reporting list --filter=Service* + * + * Filter the reports that end with "SLA": + * icingacli reporting list --filter=*SLA + */ + public function indexAction() + { + /** @var string $sort */ + $sort = $this->params->get('sort', 'id'); + $sort = strtolower($sort); + + if ($sort !== 'id' && $sort !== 'name' && $sort !== 'author') { + throw new InvalidArgumentException(sprintf('Sorting by %s is not supported', $sort)); + } + + $direction = $this->params->get('direction', 'ASC'); + + $reports = Model\Report::on(Database::get()); + $reports + ->with(['reportlets']) + ->orderBy($sort, $direction); + + $filter = $this->params->get('filter'); + if ($filter !== null) { + if (strpos($filter, '*') === false) { + $filter = '*' . $filter . '*'; + } + $reports->filter(Filter::like('name', $filter)); + } + + if ($reports->count() === 0) { + print $this->translate("No reports found\n"); + exit; + } + + $dataCallbacks = [ + 'ID' => function ($report) { + return $report->id; + }, + 'Name' => function ($report) { + return $report->name; + }, + 'Author' => function ($report) { + return $report->author; + }, + 'Type' => function ($report) { + return (new $report->reportlets->class())->getName(); + } + ]; + + $this->outputTable($reports, $dataCallbacks); + } + + protected function outputTable($reports, array $dataCallbacks) + { + $columnsAndLengths = []; + foreach ($dataCallbacks as $key => $_) { + $columnsAndLengths[$key] = strlen($key); + } + + $rows = []; + foreach ($reports as $report) { + $row = []; + foreach ($dataCallbacks as $key => $callback) { + $row[] = $callback($report); + $columnsAndLengths[$key] = max($columnsAndLengths[$key], mb_strlen($callback($report))); + } + + $rows[] = $row; + } + + $format = '|'; + $beautifier = '|'; + foreach ($columnsAndLengths as $length) { + $headerFormat = " %-" . sprintf('%ss |', $length); + $format .= $headerFormat; + $beautifier .= sprintf($headerFormat, str_repeat('-', $length)); + } + $format .= "\n"; + $beautifier .= "\n"; + + printf($format, ...array_keys($columnsAndLengths)); + print $beautifier; + + foreach ($rows as $row) { + printf($format, ...$row); + } + } +} diff --git a/application/clicommands/ScheduleCommand.php b/application/clicommands/ScheduleCommand.php index e554138..f50d046 100644 --- a/application/clicommands/ScheduleCommand.php +++ b/application/clicommands/ScheduleCommand.php @@ -1,10 +1,25 @@ <?php + // Icinga Reporting | (c) 2018 Icinga GmbH | GPLv2 namespace Icinga\Module\Reporting\Clicommands; +use DateTime; +use Exception; +use Icinga\Application\Config; +use Icinga\Application\Logger; +use Icinga\Data\ResourceFactory; use Icinga\Module\Reporting\Cli\Command; -use Icinga\Module\Reporting\Scheduler; +use Icinga\Module\Reporting\Database; +use Icinga\Module\Reporting\Model; +use Icinga\Module\Reporting\Report; +use Icinga\Module\Reporting\Schedule; +use ipl\Scheduler\Contract\Frequency; +use ipl\Scheduler\Contract\Task; +use ipl\Scheduler\Scheduler; +use React\EventLoop\Loop; +use React\Promise\ExtendedPromiseInterface; +use Throwable; class ScheduleCommand extends Command { @@ -17,8 +32,107 @@ class ScheduleCommand extends Command */ public function runAction() { - $scheduler = new Scheduler($this->getDb()); + $scheduler = new Scheduler(); + $this->attachJobsLogging($scheduler); + + /** @var Schedule[] $runningSchedules */ + $runningSchedules = []; + // Check for configuration changes every 5 minutes to make sure new jobs are scheduled, updated and deleted + // jobs are cancelled. + $watchdog = function () use (&$watchdog, $scheduler, &$runningSchedules) { + $schedules = []; + try { + // Since this is a long-running daemon, the resources or module config may change meanwhile. + // Therefore, reload the resources and module config from disk each time (at 5m intervals) + // before reconnecting to the database. + ResourceFactory::setConfig(Config::app('resources', true)); + Config::module('reporting', 'config', true); + + $schedules = $this->fetchSchedules(); + } catch (Throwable $err) { + Logger::error('Failed to fetch report schedules from the database: %s', $err); + Logger::debug($err->getTraceAsString()); + } + + $outdated = array_diff_key($runningSchedules, $schedules); + foreach ($outdated as $schedule) { + Logger::info( + 'Removing %s as it either no longer exists in the database or its config has been changed', + $schedule->getName() + ); + + $scheduler->remove($schedule); + + unset($runningSchedules[$schedule->getUuid()->toString()]); + } + + $newSchedules = array_diff_key($schedules, $runningSchedules); + foreach ($newSchedules as $key => $schedule) { + $config = $schedule->getConfig(); + $frequency = $config['frequency']; + + try { + /** @var Frequency $type */ + $type = $config['frequencyType']; + $frequency = $type::fromJson($frequency); + } catch (Exception $err) { + Logger::error( + '%s has invalid schedule expression %s: %s', + $schedule->getName(), + $frequency, + $err->getMessage() + ); + + continue; + } + + $scheduler->schedule($schedule, $frequency); + + $runningSchedules[$key] = $schedule; + } + + Loop::addTimer(5 * 60, $watchdog); + }; + Loop::futureTick($watchdog); + } + + /** + * Fetch schedules from the database + * + * @return Schedule[] + */ + protected function fetchSchedules(): array + { + $schedules = []; + $query = Model\Schedule::on(Database::get())->with(['report.timeframe', 'report']); + + foreach ($query as $schedule) { + $schedule = Schedule::fromModel($schedule, Report::fromModel($schedule->report)); + $schedules[$schedule->getUuid()->toString()] = $schedule; + } + + return $schedules; + } + + protected function attachJobsLogging(Scheduler $scheduler) + { + $scheduler->on(Scheduler::ON_TASK_FAILED, function (Task $job, Throwable $e) { + Logger::error('Failed to run job %s: %s', $job->getName(), $e->getMessage()); + Logger::debug($e->getTraceAsString()); + }); + + $scheduler->on(Scheduler::ON_TASK_RUN, function (Task $job, ExtendedPromiseInterface $_) { + Logger::info('Running job %s', $job->getName()); + }); + + $scheduler->on(Scheduler::ON_TASK_SCHEDULED, function (Task $job, DateTime $dateTime) { + Logger::info('Scheduling job %s to run at %s', $job->getName(), $dateTime->format('Y-m-d H:i:s')); + }); - $scheduler->run(); + $scheduler->on(Scheduler::ON_TASK_EXPIRED, function (Task $task, DateTime $dateTime) { + Logger::info( + sprintf('Detaching expired job %s at %s', $task->getName(), $dateTime->format('Y-m-d H:i:s')) + ); + }); } } |