summaryrefslogtreecommitdiffstats
path: root/application/clicommands/JobsCommand.php
diff options
context:
space:
mode:
Diffstat (limited to 'application/clicommands/JobsCommand.php')
-rw-r--r--application/clicommands/JobsCommand.php279
1 files changed, 279 insertions, 0 deletions
diff --git a/application/clicommands/JobsCommand.php b/application/clicommands/JobsCommand.php
new file mode 100644
index 0000000..27f7202
--- /dev/null
+++ b/application/clicommands/JobsCommand.php
@@ -0,0 +1,279 @@
+<?php
+
+// Icinga Web 2 X.509 Module | (c) 2018 Icinga GmbH | GPLv2
+
+namespace Icinga\Module\X509\Clicommands;
+
+use DateTime;
+use Exception;
+use Icinga\Application\Config;
+use Icinga\Application\Logger;
+use Icinga\Data\ResourceFactory;
+use Icinga\Module\X509\CertificateUtils;
+use Icinga\Module\X509\Command;
+use Icinga\Module\X509\Common\Database;
+use Icinga\Module\X509\Common\JobUtils;
+use Icinga\Module\X509\Hook\SniHook;
+use Icinga\Module\X509\Job;
+use Icinga\Module\X509\Model\X509Job;
+use Icinga\Module\X509\Model\X509Schedule;
+use Icinga\Module\X509\Schedule;
+use InvalidArgumentException;
+use ipl\Orm\Query;
+use ipl\Scheduler\Contract\Frequency;
+use ipl\Scheduler\Scheduler;
+use ipl\Stdlib\Filter;
+use React\EventLoop\Loop;
+use React\Promise\ExtendedPromiseInterface;
+use stdClass;
+use Throwable;
+
+class JobsCommand extends Command
+{
+ use JobUtils;
+
+ /**
+ * Run all configured jobs based on their schedule
+ *
+ * USAGE:
+ *
+ * icingacli x509 jobs run [OPTIONS]
+ *
+ * OPTIONS
+ *
+ * --job=<name>
+ * Run all configured schedules only of the specified job.
+ *
+ * --schedule=<name>
+ * Run only the given schedule of the specified job. Providing a schedule name
+ * without a job will fail immediately.
+ *
+ * --parallel=<number>
+ * Allow parallel scanning of targets up to the specified number. Defaults to 256.
+ * May cause **too many open files** error if set to a number higher than the configured one (ulimit).
+ */
+ public function runAction(): void
+ {
+ $parallel = (int) $this->params->get('parallel', Job::DEFAULT_PARALLEL);
+ if ($parallel <= 0) {
+ $this->fail("The 'parallel' option must be set to at least 1");
+ }
+
+ $jobName = (string) $this->params->get('job');
+ $scheduleName = (string) $this->params->get('schedule');
+ if (! $jobName && $scheduleName) {
+ throw new InvalidArgumentException('You cannot provide a schedule without a job');
+ }
+
+ $scheduler = new Scheduler();
+ $this->attachJobsLogging($scheduler);
+
+ $signalHandler = function () use ($scheduler) {
+ $scheduler->removeTasks();
+
+ Loop::futureTick(function () {
+ Loop::stop();
+ });
+ };
+ Loop::addSignal(SIGINT, $signalHandler);
+ Loop::addSignal(SIGTERM, $signalHandler);
+
+ /** @var Job[] $scheduled Caches scheduled jobs */
+ $scheduled = [];
+ // Periodically check configuration changes to ensure that new jobs are scheduled, jobs are updated,
+ // and deleted jobs are canceled.
+ $watchdog = function () use (&$watchdog, &$scheduled, $scheduler, $parallel, $jobName, $scheduleName) {
+ $jobs = [];
+ 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('x509', 'config', true);
+
+ $jobs = $this->fetchSchedules($jobName, $scheduleName);
+ } catch (Throwable $err) {
+ Logger::error('Failed to fetch job schedules from the database: %s', $err);
+ Logger::debug($err->getTraceAsString());
+ }
+
+ $outdatedJobs = array_diff_key($scheduled, $jobs);
+ foreach ($outdatedJobs as $job) {
+ Logger::info(
+ 'Removing schedule %s of job %s, as it either no longer exists in the configuration or its'
+ . ' config has been changed',
+ $job->getSchedule()->getName(),
+ $job->getName()
+ );
+
+ $scheduler->remove($job);
+
+ unset($scheduled[$job->getUuid()->toString()]);
+ }
+
+ $newJobs = array_diff_key($jobs, $scheduled);
+ foreach ($newJobs as $key => $job) {
+ $job->setParallel($parallel);
+
+ /** @var stdClass $config */
+ $config = $job->getSchedule()->getConfig();
+ try {
+ /** @var Frequency $type */
+ $type = $config->type;
+ $frequency = $type::fromJson($config->frequency);
+ } catch (Throwable $err) {
+ Logger::error(
+ 'Cannot create schedule %s of job %s: %s',
+ $job->getSchedule()->getName(),
+ $job->getName(),
+ $err->getMessage()
+ );
+
+ continue;
+ }
+
+ $scheduler->schedule($job, $frequency);
+
+ $scheduled[$key] = $job;
+ }
+
+ Loop::addTimer(5 * 60, $watchdog);
+ };
+ // Check configuration and add jobs directly after starting the scheduler.
+ Loop::futureTick($watchdog);
+ }
+
+ /**
+ * Fetch job schedules from database
+ *
+ * @param ?string $jobName
+ * @param ?string $scheduleName
+ *
+ * @return Job[]
+ */
+ protected function fetchSchedules(?string $jobName, ?string $scheduleName): array
+ {
+ $conn = Database::get();
+ // Even if the Job class regularly pings the same connection whenever its frequency becomes due and is run by
+ // the scheduler, we need to explicitly ping that same connection here, as the interval of the schedule jobs
+ // could be larger than the daemon configuration reload interval (5m).
+ $conn->ping();
+
+ $jobs = X509Job::on($conn);
+ if ($jobName) {
+ $jobs->filter(Filter::equal('name', $jobName));
+ }
+
+ $jobSchedules = [];
+ $snimap = SniHook::getAll();
+ /** @var X509Job $jobConfig */
+ foreach ($jobs as $jobConfig) {
+ $cidrs = $this->parseCIDRs($jobConfig->cidrs);
+ $ports = $this->parsePorts($jobConfig->ports);
+
+ /** @var Query $schedules */
+ $schedules = $jobConfig->schedule;
+ if ($scheduleName) {
+ $schedules->filter(Filter::equal('name', $scheduleName));
+ }
+
+ $schedules = $schedules->execute();
+ $hasSchedules = $schedules->hasResult();
+
+ /** @var X509Schedule $scheduleModel */
+ foreach ($schedules as $scheduleModel) {
+ $job = (new Job($jobConfig->name, $cidrs, $ports, $snimap, Schedule::fromModel($scheduleModel)))
+ ->setId($jobConfig->id)
+ ->setExcludes($this->parseExcludes($jobConfig->exclude_targets));
+
+ $jobSchedules[$job->getUuid()->toString()] = $job;
+ }
+
+ if (! $hasSchedules) {
+ Logger::info('Skipping job %s because no schedules are configured', $jobConfig->name);
+ }
+ }
+
+ return $jobSchedules;
+ }
+
+ /**
+ * Set up logging of jobs states based on scheduler events
+ *
+ * @param Scheduler $scheduler
+ */
+ protected function attachJobsLogging(Scheduler $scheduler): void
+ {
+ $scheduler->on(Scheduler::ON_TASK_CANCEL, function (Job $task, array $_) {
+ Logger::info('Schedule %s of job %s canceled', $task->getSchedule()->getName(), $task->getName());
+ });
+
+ $scheduler->on(Scheduler::ON_TASK_DONE, function (Job $task, $targets = 0) {
+ if ($targets === 0) {
+ $sinceLastScan = $task->getSinceLastScan();
+ if ($sinceLastScan) {
+ Logger::info(
+ 'Schedule %s of job %s does not have any targets to be rescanned matching since last scan: %s',
+ $task->getSchedule()->getName(),
+ $task->getName(),
+ $sinceLastScan->format('Y-m-d H:i:s')
+ );
+ } else {
+ Logger::warning(
+ 'Schedule %s of job %s does not have any targets',
+ $task->getSchedule()->getName(),
+ $task->getName()
+ );
+ }
+ } else {
+ Logger::info(
+ 'Scanned %d target(s) by schedule %s of job %s',
+ $targets,
+ $task->getSchedule()->getName(),
+ $task->getName()
+ );
+
+ try {
+ $verified = CertificateUtils::verifyCertificates(Database::get());
+
+ Logger::info('Checked %d certificate chain(s)', $verified);
+ } catch (Exception $err) {
+ Logger::error($err->getMessage());
+ Logger::debug($err->getTraceAsString());
+ }
+ }
+ });
+
+ $scheduler->on(Scheduler::ON_TASK_FAILED, function (Job $task, Throwable $e) {
+ Logger::error(
+ 'Failed to run schedule %s of job %s: %s',
+ $task->getSchedule()->getName(),
+ $task->getName(),
+ $e->getMessage()
+ );
+ Logger::debug($e->getTraceAsString());
+ });
+
+ $scheduler->on(Scheduler::ON_TASK_RUN, function (Job $task, ExtendedPromiseInterface $_) {
+ Logger::info('Running schedule %s of job %s', $task->getSchedule()->getName(), $task->getName());
+ });
+
+ $scheduler->on(Scheduler::ON_TASK_SCHEDULED, function (Job $task, DateTime $dateTime) {
+ Logger::info(
+ 'Scheduling %s of job %s to run at %s',
+ $task->getSchedule()->getName(),
+ $task->getName(),
+ $dateTime->format('Y-m-d H:i:s')
+ );
+ });
+
+ $scheduler->on(Scheduler::ON_TASK_EXPIRED, function (Job $task, DateTime $dateTime) {
+ Logger::info(
+ 'Detaching expired schedule %s of job %s at %s',
+ $task->getSchedule()->getName(),
+ $task->getName(),
+ $dateTime->format('Y-m-d H:i:s')
+ );
+ });
+ }
+}