summaryrefslogtreecommitdiffstats
path: root/application/clicommands
diff options
context:
space:
mode:
Diffstat (limited to 'application/clicommands')
-rw-r--r--application/clicommands/DownloadCommand.php98
-rw-r--r--application/clicommands/ListCommand.php133
-rw-r--r--application/clicommands/ScheduleCommand.php120
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'))
+ );
+ });
}
}