diff --git a/.env.sample b/.env.sample index 4a120f8..800d473 100644 --- a/.env.sample +++ b/.env.sample @@ -46,4 +46,15 @@ TZ=Europe/Brussels ## Set it to the IP address of your proxy. You can set to multiple proxies by comma-separating them TRUSTED_PROXIES=127.0.0.1 +############## +### MAILER ### +############## + +## Webcron management is sending you mails when cronjob are failing. The MAILER_DSN is providing usefull information on +## the how mails are being sent. Need info? https://symfony.com/doc/current/mailer.html#transport-setup +MAILER_DSN=native://default + +## Anonymous is still someone. So even if this someone is unknown you need someone who is sending your mails. +MAILER_FROM=www-data@example.com + ## Now that everything is set up: go to your friends and get wasted! \ No newline at end of file diff --git a/composer.json b/composer.json index bb47dbf..9b175eb 100644 --- a/composer.json +++ b/composer.json @@ -16,6 +16,7 @@ "symfony/console": "^5.3", "symfony/dotenv": "^5.3", "symfony/http-foundation": "^5.3", + "symfony/mailer": "^5.3", "symfony/routing": "^5.3", "symfony/yaml": "^5.3", "twig/intl-extra": "^3.3", diff --git a/composer.lock b/composer.lock index 0677fee..9d4ba8b 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "a9ab04a9aabfad43a84bb6181c37717f", + "content-hash": "fe2fc744691aef0b1c463d6dc857a69c", "packages": [ { "name": "composer/package-versions-deprecated", @@ -424,6 +424,154 @@ ], "time": "2020-05-29T18:28:51+00:00" }, + { + "name": "doctrine/lexer", + "version": "1.2.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/lexer.git", + "reference": "e864bbf5904cb8f5bb334f99209b48018522f042" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/lexer/zipball/e864bbf5904cb8f5bb334f99209b48018522f042", + "reference": "e864bbf5904cb8f5bb334f99209b48018522f042", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^6.0", + "phpstan/phpstan": "^0.11.8", + "phpunit/phpunit": "^8.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\Common\\Lexer\\": "lib/Doctrine/Common/Lexer" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.", + "homepage": "https://www.doctrine-project.org/projects/lexer.html", + "keywords": [ + "annotations", + "docblock", + "lexer", + "parser", + "php" + ], + "support": { + "issues": "https://github.com/doctrine/lexer/issues", + "source": "https://github.com/doctrine/lexer/tree/1.2.1" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Flexer", + "type": "tidelift" + } + ], + "time": "2020-05-25T17:44:05+00:00" + }, + { + "name": "egulias/email-validator", + "version": "3.1.2", + "source": { + "type": "git", + "url": "https://github.com/egulias/EmailValidator.git", + "reference": "ee0db30118f661fb166bcffbf5d82032df484697" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/ee0db30118f661fb166bcffbf5d82032df484697", + "reference": "ee0db30118f661fb166bcffbf5d82032df484697", + "shasum": "" + }, + "require": { + "doctrine/lexer": "^1.2", + "php": ">=7.2", + "symfony/polyfill-intl-idn": "^1.15" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.2", + "phpunit/phpunit": "^8.5.8|^9.3.3", + "vimeo/psalm": "^4" + }, + "suggest": { + "ext-intl": "PHP Internationalization Libraries are required to use the SpoofChecking validation" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Egulias\\EmailValidator\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Eduardo Gulias Davis" + } + ], + "description": "A library for validating emails against several RFCs", + "homepage": "https://github.com/egulias/EmailValidator", + "keywords": [ + "email", + "emailvalidation", + "emailvalidator", + "validation", + "validator" + ], + "support": { + "issues": "https://github.com/egulias/EmailValidator/issues", + "source": "https://github.com/egulias/EmailValidator/tree/3.1.2" + }, + "funding": [ + { + "url": "https://github.com/egulias", + "type": "github" + } + ], + "time": "2021-10-11T09:18:27+00:00" + }, { "name": "guzzlehttp/guzzle", "version": "7.4.0", @@ -2500,6 +2648,164 @@ ], "time": "2021-09-14T15:57:41+00:00" }, + { + "name": "symfony/mailer", + "version": "v5.3.9", + "source": { + "type": "git", + "url": "https://github.com/symfony/mailer.git", + "reference": "c1f83da2296741110be35dd779f2a9e412cec466" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/mailer/zipball/c1f83da2296741110be35dd779f2a9e412cec466", + "reference": "c1f83da2296741110be35dd779f2a9e412cec466", + "shasum": "" + }, + "require": { + "egulias/email-validator": "^2.1.10|^3", + "php": ">=7.2.5", + "psr/log": "^1|^2|^3", + "symfony/deprecation-contracts": "^2.1", + "symfony/event-dispatcher": "^4.4|^5.0", + "symfony/mime": "^5.2.6", + "symfony/polyfill-php80": "^1.16", + "symfony/service-contracts": "^1.1|^2" + }, + "conflict": { + "symfony/http-kernel": "<4.4" + }, + "require-dev": { + "symfony/http-client-contracts": "^1.1|^2", + "symfony/messenger": "^4.4|^5.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Mailer\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Helps sending emails", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/mailer/tree/v5.3.9" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-07-23T15:55:36+00:00" + }, + { + "name": "symfony/mime", + "version": "v5.3.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/mime.git", + "reference": "a756033d0a7e53db389618653ae991eba5a19a11" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/mime/zipball/a756033d0a7e53db389618653ae991eba5a19a11", + "reference": "a756033d0a7e53db389618653ae991eba5a19a11", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1", + "symfony/polyfill-intl-idn": "^1.10", + "symfony/polyfill-mbstring": "^1.0", + "symfony/polyfill-php80": "^1.16" + }, + "conflict": { + "egulias/email-validator": "~3.0.0", + "phpdocumentor/reflection-docblock": "<3.2.2", + "phpdocumentor/type-resolver": "<1.4.0", + "symfony/mailer": "<4.4" + }, + "require-dev": { + "egulias/email-validator": "^2.1.10|^3.1", + "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", + "symfony/dependency-injection": "^4.4|^5.0", + "symfony/property-access": "^4.4|^5.1", + "symfony/property-info": "^4.4|^5.1", + "symfony/serializer": "^5.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Mime\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows manipulating MIME messages", + "homepage": "https://symfony.com", + "keywords": [ + "mime", + "mime-type" + ], + "support": { + "source": "https://github.com/symfony/mime/tree/v5.3.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-09-10T12:30:38+00:00" + }, { "name": "symfony/polyfill-ctype", "version": "v1.23.0", @@ -2660,6 +2966,93 @@ ], "time": "2021-05-27T12:26:48+00:00" }, + { + "name": "symfony/polyfill-intl-idn", + "version": "v1.23.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-idn.git", + "reference": "65bd267525e82759e7d8c4e8ceea44f398838e65" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/65bd267525e82759e7d8c4e8ceea44f398838e65", + "reference": "65bd267525e82759e7d8c4e8ceea44f398838e65", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "symfony/polyfill-intl-normalizer": "^1.10", + "symfony/polyfill-php72": "^1.10" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.23-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Intl\\Idn\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Laurent Bassin", + "email": "laurent@bassin.info" + }, + { + "name": "Trevor Rowbotham", + "email": "trevor.rowbotham@pm.me" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "idn", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.23.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-05-27T09:27:20+00:00" + }, { "name": "symfony/polyfill-intl-normalizer", "version": "v1.23.0", @@ -2824,6 +3217,82 @@ ], "time": "2021-05-27T12:26:48+00:00" }, + { + "name": "symfony/polyfill-php72", + "version": "v1.23.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php72.git", + "reference": "9a142215a36a3888e30d0a9eeea9766764e96976" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/9a142215a36a3888e30d0a9eeea9766764e96976", + "reference": "9a142215a36a3888e30d0a9eeea9766764e96976", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.23-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Php72\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 7.2+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php72/tree/v1.23.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-05-27T09:17:38+00:00" + }, { "name": "symfony/polyfill-php73", "version": "v1.23.0", diff --git a/src/Command/MailFailedRunsCommand.php b/src/Command/MailFailedRunsCommand.php new file mode 100644 index 0000000..09f541e --- /dev/null +++ b/src/Command/MailFailedRunsCommand.php @@ -0,0 +1,68 @@ +kernel = $kernel; + parent::__construct(); + } + + protected function configure() + { + $this + ->setDescription('Sends email about failed runs') + ->setHelp('This command will send emails to the users when jobs are failing'); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + + $userRepo = new User($this->kernel->getDbCon()); + $jobRepo = new Job($this->kernel->getDbCon()); + + $failedJobs = $jobRepo->getFailingJobs(); + + if(!empty($failedJobs)) { + $twig = new Twig($this->kernel); + $html = $twig->render('mail-failed-runs.html.twig', ['jobs' => $failedJobs]); + $transport = Transport::fromDsn($_ENV['MAILER_DSN']); + $mailer = new Mailer($transport); + + $email = (new Email()) + ->from($_ENV['MAILER_FROM']) + ->subject('Some cronjobs are failing') + ->html($html); + + + $recipients = $userRepo->getMailAddresses(); + foreach ($recipients as $recipient) { + $email->addTo($recipient); + } + + $mailer->send($email); + + $output->writeln('Message sent'); + } + + + return Command::SUCCESS; + } +} \ No newline at end of file diff --git a/src/Repository/Job.php b/src/Repository/Job.php index 7afe927..f351af7 100644 --- a/src/Repository/Job.php +++ b/src/Repository/Job.php @@ -13,6 +13,39 @@ use phpseclib3\Net\SSH2; class Job extends Repository { + public function getFailingJobs() + { + $runRepo = new Run($this->dbcon); + + $jobsSql = "SELECT * FROM job"; + $jobsStmt = $this->dbcon->prepare($jobsSql); + $jobsRslt = $jobsStmt->executeQuery(); + $jobs = $jobsRslt->fetchAllAssociative(); + foreach ($jobs as $key=>&$job) { + $job['data'] = json_decode($job['data'], true); + $job['host-displayname'] = $job['data']['host']; + $job['host'] = $job['data']['host']; + $job['service'] = $job['data']['service'] ?? ''; + $failedruns = $runRepo->getRunsForJob($job['id'], true, $job['data']['fail-days']); + $failed = count($failedruns); + $all = count($runRepo->getRunsForJob($job['id'], false, $job['data']['fail-days'])); + $job['lastfail'] = $failedruns[0] ?? NULL; + $job['needschecking'] = $all > 0 && (($failed / $all) * 100) > $job['data']['fail-pct']; + if(!empty($job['data']['containertype']) && $job['data']['containertype'] != 'none') { + $job['host-displayname'] = $job['data']['service'] . ' on ' . $job['data']['host']; + } + if($job['needschecking']) $failingjobs[] = $job; + } + + if(empty($failingjobs)) return []; + array_multisort( + array_column($failingjobs, 'name'), SORT_ASC, + array_column($failingjobs, 'host'), SORT_ASC, + array_column($failingjobs, 'service'), SORT_ASC, + $failingjobs); + return $failingjobs; + } + public function getAllJobs(bool $idiskey = false) { $runRepo = new Run($this->dbcon); diff --git a/src/Repository/User.php b/src/Repository/User.php index dc65b1e..f5ea2a3 100644 --- a/src/Repository/User.php +++ b/src/Repository/User.php @@ -22,7 +22,7 @@ class User extends Repository { $userSql = "SELECT * from user WHERE email = :user"; $userStmt = $this->dbcon->prepare($userSql); - $userRslt = $userStmt->execute([':user' => $user]); + $userRslt = $userStmt->executeQuery([':user' => $user]); if($user = $userRslt->fetchAssociative()) { if($autologin) $password = $this->getPassFromAutologinToken($password); @@ -55,4 +55,16 @@ class User extends Repository ) ? substr($decrypted, 0, -7) : null; } + + public function getMailAddresses() { + $emailSql = "SELECT email FROM user WHERE sendmail = 1"; + $emailStmt = $this->dbcon->prepare($emailSql); + $emailRslt = $emailStmt->executeQuery(); + + $return = []; + foreach($emailRslt->fetchAllAssociative() as $email) { + $return[] = $email['email']; + } + return $return; + } } \ No newline at end of file diff --git a/storage/database.sql b/storage/database.sql index 789ed54..7b73607 100644 --- a/storage/database.sql +++ b/storage/database.sql @@ -14,7 +14,8 @@ CREATE TABLE job ( CREATE TABLE "user" ( id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, email TEXT(50) NOT NULL, - password TEXT(72) NOT NULL + password TEXT(72) NOT NULL, + sendmail INTEGER NOT NULL ); -- run definition diff --git a/templates/mail-failed-runs.html.twig b/templates/mail-failed-runs.html.twig new file mode 100644 index 0000000..197b386 --- /dev/null +++ b/templates/mail-failed-runs.html.twig @@ -0,0 +1,17 @@ + + + + + Some cronjobs are Failing + + +

Some cronjobs are failing

+{% for job in jobs %} +

{{ job.name }} ({{ attribute(job, 'host-displayname') }})

+

Last failed on {{ job.lastfail.timestamp | date("d/m/Y H:i:s") }}

+ {% if job.lastfail.output is not empty %} +
{{ job.lastfail.output }}
+ {% endif %} +{% endfor %} + + \ No newline at end of file diff --git a/webcron b/webcron index 6ee7047..2561d0e 100644 --- a/webcron +++ b/webcron @@ -5,6 +5,7 @@ require_once 'bootstrap.php'; use JeroenED\Framework\Kernel; use JeroenED\Webcron\Command\CleanupCommand; use JeroenED\Webcron\Command\DaemonCommand; +use JeroenED\Webcron\Command\MailFailedRunsCommand; use JeroenED\Webcron\Command\RunCommand; use Symfony\Component\Console\Application; @@ -20,6 +21,7 @@ $kernel->parseDotEnv($kernel->getProjectDir() . '/.env'); $application->add(new RunCommand($kernel)); $application->add(new DaemonCommand($kernel)); $application->add(new CleanupCommand($kernel)); +$application->add(new MailFailedRunsCommand($kernel)); $application->run();