diff --git a/.env.dist b/.env.dist index dc52483..e3731ad 100644 --- a/.env.dist +++ b/.env.dist @@ -40,3 +40,7 @@ MESSENGER_TRANSPORT_RESULTS_DSN=doctrine://default?queue_name=results ###< symfony/mailer ### CHECK_SCRIPTS_PATH="scripts/checks" + +###> php-amqplib/rabbitmq-bundle ### +RABBITMQ_URL=amqp://guest:guest@localhost:5672 +###< php-amqplib/rabbitmq-bundle ### diff --git a/.gitignore b/.gitignore index 542aacb..7f1dd03 100644 --- a/.gitignore +++ b/.gitignore @@ -26,4 +26,6 @@ clover.xml /public/coverage/ npm-debug.log yarn-error.log -###< symfony/webpack-encore-bundle ### \ No newline at end of file +###< symfony/webpack-encore-bundle ### + +.DS_Store diff --git a/assets/styles/_variables.scss b/assets/styles/_variables.scss index b94d30e..5674838 100644 --- a/assets/styles/_variables.scss +++ b/assets/styles/_variables.scss @@ -456,7 +456,7 @@ $font-family-code: var(--#{$variable-prefix}font-monospace) !default; // $font-size-root effects the value of `rem`, which is used for as well font sizes, paddings and margins // $font-size-base effects the font size of the body text $font-size-root: null !default; -$font-size-base: 1rem !default; // Assumes the browser default, typically `16px` +$font-size-base: 0.85rem !default; // Assumes the browser default, typically `16px` $font-size-sm: $font-size-base * .875 !default; $font-size-lg: $font-size-base * 1.25 !default; @@ -699,7 +699,6 @@ $input-padding-x-lg: $input-btn-padding-x-lg !default; $input-font-size-lg: $input-btn-font-size-lg !default; $input-bg: $white !default; -$input-disabled-bg: $gray-200 !default; $input-disabled-border-color: null !default; $input-color: $body-color !default; @@ -1065,8 +1064,8 @@ $card-border-width: $border-width !default; $card-border-radius: .7rem !default; $card-border-color: rgba($black, .125) !default; $card-inner-border-radius: subtract($card-border-radius, $card-border-width) !default; -$card-cap-padding-y: 1.5rem !default; -$card-cap-padding-x: 1.5rem !default; +$card-cap-padding-y: 0.5rem !default; +$card-cap-padding-x: 0.5rem !default; $card-cap-bg: white !default; $card-cap-color: null !default; $card-height: null !default; diff --git a/assets/styles/components/_accordion.scss b/assets/styles/components/_accordion.scss new file mode 100644 index 0000000..ae0c146 --- /dev/null +++ b/assets/styles/components/_accordion.scss @@ -0,0 +1,16 @@ +.accordion-button { + border-bottom: 1px solid var(--o-form-control-border-color); + + &.collapsed { + background-color: var(--o-form-control-bg-color); + } + + &:not(.collapsed) { + background-color: var(--o-form-control-bg-focus-color); + } +} + +.accordion-item { + color: var(--o-text-color); +} + diff --git a/assets/styles/components/_card.scss b/assets/styles/components/_card.scss index 9a92871..be6fc71 100644 --- a/assets/styles/components/_card.scss +++ b/assets/styles/components/_card.scss @@ -1,7 +1,13 @@ .card { margin-bottom: 2.2rem; border: none; - + background-color: var(--o-card-bg-color); + + &.nested { + background-color: var(--o-card-nested-bg-color); + margin-bottom: 0rem; + } + &.card-statistic { box-shadow: 1px 2px 5px rgba(#2FAAF4, .5); background: linear-gradient(to bottom, #25a6f1, #54b9ff); @@ -51,7 +57,6 @@ } .card-body { padding: $card-cap-padding-y $card-cap-padding-x; - background-color: var(--o-card-body-bg-color); } .card-heading { color: #555; @@ -129,4 +134,8 @@ font-size: 1.8rem; } } + + + } + diff --git a/assets/styles/components/_forms.scss b/assets/styles/components/_forms.scss index 0b8565b..e462084 100644 --- a/assets/styles/components/_forms.scss +++ b/assets/styles/components/_forms.scss @@ -80,6 +80,11 @@ } } + +.form-control::placeholder { + color: var(--o-form-control-placeholder-color); +} + .form-control { background-color: var(--o-form-control-bg-color); border: 1px solid var(--o-form-control-border-color); @@ -96,6 +101,7 @@ background-color: var(--o-form-control-bg-focus-color); color: var(--o-form-control-text-color); } + } .form-check { .form-check-input { diff --git a/assets/styles/components/_pagination.scss b/assets/styles/components/_pagination.scss index cd22f86..5ae7f1a 100644 --- a/assets/styles/components/_pagination.scss +++ b/assets/styles/components/_pagination.scss @@ -1,4 +1,5 @@ .pagination { + margin: 0; @each $key, $value in $theme-colors { &.pagination-#{$key} { .page-item.active { @@ -20,14 +21,13 @@ } } i, svg { - font-size: 13px; - width: 13px; - height: 13px; + font-size: .6rem; } .page-link { - font-size: .875rem; + font-size: .6rem; background-color: var(--o-pagination-bg); border-color: var(--o-pagination-border-color); + padding: $pagination-padding-y-sm $pagination-padding-x-sm; &:focus { box-shadow: none; } diff --git a/assets/styles/components/_table.scss b/assets/styles/components/_table.scss index 6ea37a0..ba56099 100644 --- a/assets/styles/components/_table.scss +++ b/assets/styles/components/_table.scss @@ -10,6 +10,7 @@ .table { > :not(caption) > * > * { color: var(--o-table-text-color); + padding: 0.2rem; } tr, td, th { @@ -19,19 +20,22 @@ &.table-sm { tr td, tr th { - padding: 1rem; + padding: 0.2rem; } } &.table-md { tr td, tr th { - padding: 1rem; + padding: 0.2rem; } } &.table-lg { tr td, tr th { - padding: 1.3rem; + padding: 0.2rem; } } + &.mw-500 { + max-width: 500px; + } } .dataTable-table { diff --git a/assets/styles/dark.scss b/assets/styles/dark.scss index b52dabe..5c9b01a 100644 --- a/assets/styles/dark.scss +++ b/assets/styles/dark.scss @@ -1,29 +1,36 @@ [data-bs-theme=dark] { - --bs-heading-color: #e6d5ac; - --bs-secondary-color: #ebb22d; + --bs-heading-color: #e6d5ac; + --bs-secondary-color: #ebb22d; - --o-sidebar-bg-color: #1c1d1f; - --o-sidebar-link-color: #858585; - --o-sidebar-icon-color: #fdaf00; - - --o-card-header-bg-color: #3c3837; - --o-card-body-bg-color: #3c3837; + --o-sidebar-bg-color: #1c1d1f; + --o-sidebar-link-color: #858585; + --o-sidebar-icon-color: #fdaf00; - --o-pagination-bg: #3a3a3a; - --o-pagination-border-color: #4f4f4f; + --o-text-color: #adadad; - --o-table-text-color: #b5b5b5; - --o-table-heading-color: #e6d5ac; - --o-table-border-color: #544e40; + --o-card-header-bg-color: #3c3837; + --o-card-bg-color: #3c3837; - --o-topbar-bg-color: #3c3837; + --o-card-nested-header-bg-color: #272625; + --o-card-nested-bg-color: #272625; - --o-form-control-bg-color: #2c2a29; - --o-form-control-border-color: #474747; - --o-form-control-text-color: #adadad; - --o-form-control-bg-focus-color: #252423; + --o-pagination-bg: #3a3a3a; + --o-pagination-border-color: #4f4f4f; + + --o-table-text-color: #b5b5b5; + --o-table-heading-color: #e6d5ac; + --o-table-border-color: #544e40; + + --o-topbar-bg-color: #3c3837; + + --o-form-control-bg-color: #2c2a29; + --o-form-control-border-color: #474747; + --o-form-control-text-color: #e2e2e2; + --o-form-control-bg-focus-color: #252423; + --o-form-control-placeholder-color: #7e7e7e; + + --o-alert-bg-color: #494949; + --o-alert-text-color: #fff; + --o-alert-button-color: #fff; - --o-alert-bg-color: #494949; - --o-alert-text-color: #fff; - --o-alert-button-color: #fff; } diff --git a/assets/styles/global.scss b/assets/styles/global.scss index a56f9f7..ab7d674 100644 --- a/assets/styles/global.scss +++ b/assets/styles/global.scss @@ -5,6 +5,7 @@ @import "./components/alert"; @import "./components/avatar"; +@import "./components/accordion"; @import "./components/badge"; @import "./components/buttons"; @import "./components/breadcrumb"; diff --git a/assets/styles/layouts/main.scss b/assets/styles/layouts/main.scss index bfba694..cfd6984 100644 --- a/assets/styles/layouts/main.scss +++ b/assets/styles/layouts/main.scss @@ -33,3 +33,10 @@ a { text-decoration: none; } + +.hint { + font-size: 0.8rem; + color: #6c757d; + margin-top: 0.25rem; + font-style: italic; +} diff --git a/assets/styles/light.scss b/assets/styles/light.scss index b7636a9..d79b2a5 100644 --- a/assets/styles/light.scss +++ b/assets/styles/light.scss @@ -1,8 +1,15 @@ [data-bs-theme=light] { - --o-sidebar-bg-color: #e7e7e7; + --o-sidebar-bg-color: #f6f6f6; - --o-topbar-bg-color: #e7e7e7; + --o-topbar-bg-color: #e7e7e7; + + --o-card-header-bg-color: #fff; + --o-card-body-bg-color: #fff; + + --o-form-control-bg-color: #fff; + --o-form-control-border-color: #f2f2f2; + --o-form-control-text-color: #555252; + + --o-form-control-placeholder-color: #b0b0b0; - --o-card-header-bg-color: #fff; - --o-card-body-bg-color: #fff; } diff --git a/composer.json b/composer.json index 46f6bac..236a74c 100644 --- a/composer.json +++ b/composer.json @@ -11,8 +11,10 @@ "doctrine/doctrine-bundle": "^2.10", "doctrine/doctrine-migrations-bundle": "^3.2", "doctrine/orm": "^2.15", + "php-amqplib/rabbitmq-bundle": "^2.13", "phpdocumentor/reflection-docblock": "^5.3", "phpstan/phpdoc-parser": "^1.22", + "symfony/amqp-messenger": "6.3.*", "symfony/apache-pack": "^1.0", "symfony/asset": "6.3.*", "symfony/console": "6.3.*", diff --git a/composer.lock b/composer.lock index 9538f7d..a9fd93b 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": "e0d0272d134103b1c3eb7b8dd6218014", + "content-hash": "3ad4492de8b01dd8cd0bb9f0424ead82", "packages": [ { "name": "components/font-awesome", @@ -1536,6 +1536,283 @@ ], "time": "2023-10-27T15:32:31+00:00" }, + { + "name": "paragonie/constant_time_encoding", + "version": "v2.6.3", + "source": { + "type": "git", + "url": "https://github.com/paragonie/constant_time_encoding.git", + "reference": "58c3f47f650c94ec05a151692652a868995d2938" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/58c3f47f650c94ec05a151692652a868995d2938", + "reference": "58c3f47f650c94ec05a151692652a868995d2938", + "shasum": "" + }, + "require": { + "php": "^7|^8" + }, + "require-dev": { + "phpunit/phpunit": "^6|^7|^8|^9", + "vimeo/psalm": "^1|^2|^3|^4" + }, + "type": "library", + "autoload": { + "psr-4": { + "ParagonIE\\ConstantTime\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com", + "homepage": "https://paragonie.com", + "role": "Maintainer" + }, + { + "name": "Steve 'Sc00bz' Thomas", + "email": "steve@tobtu.com", + "homepage": "https://www.tobtu.com", + "role": "Original Developer" + } + ], + "description": "Constant-time Implementations of RFC 4648 Encoding (Base-64, Base-32, Base-16)", + "keywords": [ + "base16", + "base32", + "base32_decode", + "base32_encode", + "base64", + "base64_decode", + "base64_encode", + "bin2hex", + "encoding", + "hex", + "hex2bin", + "rfc4648" + ], + "support": { + "email": "info@paragonie.com", + "issues": "https://github.com/paragonie/constant_time_encoding/issues", + "source": "https://github.com/paragonie/constant_time_encoding" + }, + "time": "2022-06-14T06:56:20+00:00" + }, + { + "name": "paragonie/random_compat", + "version": "v9.99.100", + "source": { + "type": "git", + "url": "https://github.com/paragonie/random_compat.git", + "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/random_compat/zipball/996434e5492cb4c3edcb9168db6fbb1359ef965a", + "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a", + "shasum": "" + }, + "require": { + "php": ">= 7" + }, + "require-dev": { + "phpunit/phpunit": "4.*|5.*", + "vimeo/psalm": "^1" + }, + "suggest": { + "ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes." + }, + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com", + "homepage": "https://paragonie.com" + } + ], + "description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7", + "keywords": [ + "csprng", + "polyfill", + "pseudorandom", + "random" + ], + "support": { + "email": "info@paragonie.com", + "issues": "https://github.com/paragonie/random_compat/issues", + "source": "https://github.com/paragonie/random_compat" + }, + "time": "2020-10-15T08:29:30+00:00" + }, + { + "name": "php-amqplib/php-amqplib", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/php-amqplib/php-amqplib.git", + "reference": "fb84e99589de0904a25861451b0552f806284ee5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-amqplib/php-amqplib/zipball/fb84e99589de0904a25861451b0552f806284ee5", + "reference": "fb84e99589de0904a25861451b0552f806284ee5", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "ext-sockets": "*", + "php": "^7.2||^8.0", + "phpseclib/phpseclib": "^2.0|^3.0" + }, + "conflict": { + "php": "7.4.0 - 7.4.1" + }, + "replace": { + "videlalvaro/php-amqplib": "self.version" + }, + "require-dev": { + "ext-curl": "*", + "nategood/httpful": "^0.2.20", + "phpunit/phpunit": "^7.5|^9.5", + "squizlabs/php_codesniffer": "^3.6" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "psr-4": { + "PhpAmqpLib\\": "PhpAmqpLib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-2.1-or-later" + ], + "authors": [ + { + "name": "Alvaro Videla", + "role": "Original Maintainer" + }, + { + "name": "Raúl Araya", + "email": "nubeiro@gmail.com", + "role": "Maintainer" + }, + { + "name": "Luke Bakken", + "email": "luke@bakken.io", + "role": "Maintainer" + }, + { + "name": "Ramūnas Dronga", + "email": "github@ramuno.lt", + "role": "Maintainer" + } + ], + "description": "Formerly videlalvaro/php-amqplib. This library is a pure PHP implementation of the AMQP protocol. It's been tested against RabbitMQ.", + "homepage": "https://github.com/php-amqplib/php-amqplib/", + "keywords": [ + "message", + "queue", + "rabbitmq" + ], + "support": { + "issues": "https://github.com/php-amqplib/php-amqplib/issues", + "source": "https://github.com/php-amqplib/php-amqplib/tree/v3.6.0" + }, + "time": "2023-10-22T15:02:02+00:00" + }, + { + "name": "php-amqplib/rabbitmq-bundle", + "version": "2.13.1", + "source": { + "type": "git", + "url": "https://github.com/php-amqplib/RabbitMqBundle.git", + "reference": "eec4e4e35d0b48f2d955d73a984c4dddcc1a135b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-amqplib/RabbitMqBundle/zipball/eec4e4e35d0b48f2d955d73a984c4dddcc1a135b", + "reference": "eec4e4e35d0b48f2d955d73a984c4dddcc1a135b", + "shasum": "" + }, + "require": { + "php": "^7.4|^8.0", + "php-amqplib/php-amqplib": "^2.12.2|^3.0", + "psr/log": "^1.0 || ^2.0 || ^3.0", + "symfony/config": "^4.4|^5.3|^6.0", + "symfony/console": "^4.4|^5.3|^6.0", + "symfony/dependency-injection": "^4.4|^5.3|^6.0", + "symfony/event-dispatcher": "^4.4|^5.3|^6.0", + "symfony/framework-bundle": "^4.4|^5.3|^6.0", + "symfony/http-kernel": "^4.4|^5.3|^6.0", + "symfony/yaml": "^4.4|^5.3|^6.0" + }, + "replace": { + "emag-tech-labs/rabbitmq-bundle": "self.version", + "oldsound/rabbitmq-bundle": "self.version" + }, + "require-dev": { + "phpstan/phpstan": "^1.2", + "phpstan/phpstan-phpunit": "^1.0", + "phpunit/phpunit": "^9.5", + "symfony/serializer": "^4.4|^5.3|^6.0" + }, + "suggest": { + "ext-pcntl": "*", + "symfony/framework-bundle": "To use this lib as a full Symfony Bundle and to use the profiler data collector" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-master": "1.10.x-dev" + } + }, + "autoload": { + "psr-4": { + "OldSound\\RabbitMqBundle\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Alvaro Videla" + } + ], + "description": "Integrates php-amqplib with Symfony & RabbitMq. Formerly emag-tech-labs/rabbitmq-bundle, oldsound/rabbitmq-bundle.", + "keywords": [ + "AMQP", + "message", + "queue", + "rabbitmq", + "symfony", + "symfony4", + "symfony5" + ], + "support": { + "issues": "https://github.com/php-amqplib/RabbitMqBundle/issues", + "source": "https://github.com/php-amqplib/RabbitMqBundle/tree/2.13.1" + }, + "time": "2023-11-14T14:43:53+00:00" + }, { "name": "phpdocumentor/reflection-common", "version": "2.2.0", @@ -1704,6 +1981,116 @@ }, "time": "2023-08-12T11:01:26+00:00" }, + { + "name": "phpseclib/phpseclib", + "version": "3.0.33", + "source": { + "type": "git", + "url": "https://github.com/phpseclib/phpseclib.git", + "reference": "33fa69b2514a61138dd48e7a49f99445711e0ad0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/33fa69b2514a61138dd48e7a49f99445711e0ad0", + "reference": "33fa69b2514a61138dd48e7a49f99445711e0ad0", + "shasum": "" + }, + "require": { + "paragonie/constant_time_encoding": "^1|^2", + "paragonie/random_compat": "^1.4|^2.0|^9.99.99", + "php": ">=5.6.1" + }, + "require-dev": { + "phpunit/phpunit": "*" + }, + "suggest": { + "ext-dom": "Install the DOM extension to load XML formatted public keys.", + "ext-gmp": "Install the GMP (GNU Multiple Precision) extension in order to speed up arbitrary precision integer arithmetic operations.", + "ext-libsodium": "SSH2/SFTP can make use of some algorithms provided by the libsodium-php extension.", + "ext-mcrypt": "Install the Mcrypt extension in order to speed up a few other cryptographic operations.", + "ext-openssl": "Install the OpenSSL extension in order to speed up a wide variety of cryptographic operations." + }, + "type": "library", + "autoload": { + "files": [ + "phpseclib/bootstrap.php" + ], + "psr-4": { + "phpseclib3\\": "phpseclib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jim Wigginton", + "email": "terrafrost@php.net", + "role": "Lead Developer" + }, + { + "name": "Patrick Monnerat", + "email": "pm@datasphere.ch", + "role": "Developer" + }, + { + "name": "Andreas Fischer", + "email": "bantu@phpbb.com", + "role": "Developer" + }, + { + "name": "Hans-Jürgen Petrich", + "email": "petrich@tronic-media.com", + "role": "Developer" + }, + { + "name": "Graham Campbell", + "email": "graham@alt-three.com", + "role": "Developer" + } + ], + "description": "PHP Secure Communications Library - Pure-PHP implementations of RSA, AES, SSH2, SFTP, X.509 etc.", + "homepage": "http://phpseclib.sourceforge.net", + "keywords": [ + "BigInteger", + "aes", + "asn.1", + "asn1", + "blowfish", + "crypto", + "cryptography", + "encryption", + "rsa", + "security", + "sftp", + "signature", + "signing", + "ssh", + "twofish", + "x.509", + "x509" + ], + "support": { + "issues": "https://github.com/phpseclib/phpseclib/issues", + "source": "https://github.com/phpseclib/phpseclib/tree/3.0.33" + }, + "funding": [ + { + "url": "https://github.com/terrafrost", + "type": "github" + }, + { + "url": "https://www.patreon.com/phpseclib", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpseclib/phpseclib", + "type": "tidelift" + } + ], + "time": "2023-10-21T14:00:39+00:00" + }, { "name": "phpstan/phpdoc-parser", "version": "1.24.2", @@ -2057,6 +2444,75 @@ }, "time": "2021-07-14T16:46:02+00:00" }, + { + "name": "symfony/amqp-messenger", + "version": "v6.3.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/amqp-messenger.git", + "reference": "0391200eb277d16d1a4ccad1aea05f0a23ee90ac" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/amqp-messenger/zipball/0391200eb277d16d1a4ccad1aea05f0a23ee90ac", + "reference": "0391200eb277d16d1a4ccad1aea05f0a23ee90ac", + "shasum": "" + }, + "require": { + "ext-amqp": "*", + "php": ">=8.1", + "symfony/messenger": "^6.1" + }, + "require-dev": { + "symfony/event-dispatcher": "^5.4|^6.0", + "symfony/process": "^5.4|^6.0", + "symfony/property-access": "^5.4|^6.0", + "symfony/serializer": "^5.4|^6.0" + }, + "type": "symfony-messenger-bridge", + "autoload": { + "psr-4": { + "Symfony\\Component\\Messenger\\Bridge\\Amqp\\": "" + }, + "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": "Symfony AMQP extension Messenger Bridge", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/amqp-messenger/tree/v6.3.4" + }, + "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": "2023-08-14T14:06:04+00:00" + }, { "name": "symfony/apache-pack", "version": "v1.0.1", diff --git a/config/bundles.php b/config/bundles.php index 075b830..f8b7630 100644 --- a/config/bundles.php +++ b/config/bundles.php @@ -16,4 +16,5 @@ Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true], Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true], Symfony\WebpackEncoreBundle\WebpackEncoreBundle::class => ['all' => true], + OldSound\RabbitMqBundle\OldSoundRabbitMqBundle::class => ['all' => true], ]; diff --git a/config/packages/messenger.yaml b/config/packages/messenger.yaml index 6aa4c9e..e86cb7c 100644 --- a/config/packages/messenger.yaml +++ b/config/packages/messenger.yaml @@ -6,9 +6,6 @@ framework: # https://symfony.com/doc/current/messenger.html#transport-configuration async: dsn: '%env(MESSENGER_TRANSPORT_DSN)%' - options: - use_notify: true - check_delayed_interval: 60000 retry_strategy: max_retries: 3 multiplier: 2 @@ -17,9 +14,6 @@ framework: async_checks: dsn: '%env(MESSENGER_TRANSPORT_CHECKS_DSN)%' - options: - use_notify: true - check_delayed_interval: 60000 retry_strategy: max_retries: 3 multiplier: 2 @@ -27,9 +21,6 @@ framework: async_results: dsn: '%env(MESSENGER_TRANSPORT_RESULTS_DSN)%' - options: - use_notify: true - check_delayed_interval: 60000 retry_strategy: max_retries: 3 multiplier: 2 diff --git a/config/packages/security.yaml b/config/packages/security.yaml index 3a8d28c..69dfcd2 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -42,6 +42,7 @@ security: # - { path: ^/admin, roles: ROLE_ADMIN } # - { path: ^/profile, roles: ROLE_USER } - { path: ^/login, roles: PUBLIC_ACCESS } + - { path: ^/api/status$, roles: PUBLIC_ACCESS } - { path: ^/api, roles: ROLE_USER } - { path: ^/, roles: ROLE_USER } diff --git a/makefile b/makefile index 1689e81..277b829 100644 --- a/makefile +++ b/makefile @@ -10,17 +10,20 @@ help: clean: ## cleans all dependencies rm -rf vendor + rm -rf node_modules prod: ## installs all vendors in prod mode COMPOSER_MEMORY_LIMIT=-1 composer install --no-dev --optimize-autoloader - yarn install --production=true + yarn install yarn encore prod + bin/console doctrine:migrations:migrate --no-interaction dev: ## installs all vendors in dev mode COMPOSER_MEMORY_LIMIT=-1 composer install -n patch -t vendor/symfony/error-handler/ErrorHandler.php custom/patches/dev/SymfonyErrorHandler.patch - yarn install --production=false + yarn install yarn encore dev + bin/console doctrine:migrations:migrate --no-interaction watch: ## webpack watcher yarn encore dev --watch diff --git a/migrations/Version1700217530.php b/migrations/Version1700217530.php new file mode 100644 index 0000000..9cc5fd4 --- /dev/null +++ b/migrations/Version1700217530.php @@ -0,0 +1,29 @@ + User::DEFAULT_CODE_EDITOR_KEYBINDING, + ]; + + $this->addSql('UPDATE user SET code_editor_config = :codeEditorConfig', [ + 'codeEditorConfig' => json_encode($codeEditorConfig), + ]); + + } +} diff --git a/scripts/checks/README.md b/scripts/checks/README.md index 259a75f..7f2cf51 100755 --- a/scripts/checks/README.md +++ b/scripts/checks/README.md @@ -1,14 +1,18 @@ % check scripts -scripts will be called by orcano with the following parameters: +scripts will be called by orcano with a JSON string containing the following: -```bash -$1 = hostname -$2 = Ipv4 address -$3 = Ipv6 address +```json +{ + "host": "hostname", + "ipv4": "127.0.0.1", + "ipv6": "::fff", + + ... aditional data based on check ... +} ``` Example: - `/scripts/checks/./ping4.sh localhost 127.0.0.1 ::fff` + `/scripts/checks/./ping4.sh '{"host":"google.com","ipv4":"127.0.0.1"}'` After adding/updating a script, refresh the scripts by typing: `bin/console orcano:script:refresh` diff --git a/scripts/checks/api_response.sh b/scripts/checks/api_response.sh new file mode 100755 index 0000000..d208aaa --- /dev/null +++ b/scripts/checks/api_response.sh @@ -0,0 +1,11 @@ +#!/bin/bash +# name: API Response +# desc: calls an API Endpoint and returns the content +# parameters: url + +# decode json and get the url +url=$(echo $1 | jq -r '.url') + +response=$(curl -s $url) + +printf '%s' $response diff --git a/scripts/checks/http_status.sh b/scripts/checks/http_status.sh index d0f8389..d4945ca 100755 --- a/scripts/checks/http_status.sh +++ b/scripts/checks/http_status.sh @@ -1,10 +1,11 @@ #!/bin/bash -# script will be called by orcano with parameters: $1 = host, $2 = ipv4, $3 = ipv6 -# # name: HTTP Status Check -# desc: Checks the HTTP/S status and returns the result +# desc: Checks HTTP and returns all repsonse headers +# parameters: url -httpCode=$(curl -s -o /dev/null -w "%{http_code}" http://$1) -httpsCode=$(curl -s -o /dev/null -w "%{http_code}" https://$1) +# decode json and get the url +url=$(echo $1 | jq -r '.url') -printf 'ODATA: {"http":%d, "https":%d}' $httpCode $httpsCode +response=$(curl -s --write-out "%{json}" -o /dev/null $url) + +printf '%s' $response diff --git a/scripts/checks/mqtt.sh b/scripts/checks/mqtt.sh new file mode 100755 index 0000000..146cb73 --- /dev/null +++ b/scripts/checks/mqtt.sh @@ -0,0 +1,17 @@ +#!/bin/bash +# name: MQTT Check +# desc: Checks a MQTT topic +# parameters: port,topic,user,password + +# requires mosiquitto-client to be installed + +# decode json and get the url +host=$(echo $1 | jq -r '.host') +port=$(echo $1 | jq -r '.port') +topic=$(echo $1 | jq -r '.topic') +username=$(echo $1 | jq -r '.username') +password=$(echo $1 | jq -r '.password') + +# run command and return json output +mosquitto_sub -t $topic -h $host -p $port -u $username -P $password -v -C 1 -F "%j" + diff --git a/scripts/checks/ping4.sh b/scripts/checks/ping4.sh index 9ff8b02..1198f5f 100755 --- a/scripts/checks/ping4.sh +++ b/scripts/checks/ping4.sh @@ -1,8 +1,7 @@ #!/bin/bash -# script will be called by orcano with parameters: $1 = host, $2 = ipv4, $3 = ipv6 -# -# name: IPV4 Ping testing +# name: IPV4 Ping # desc: Pings the host and returns result +# parameters: ipv4 # # note: needs GNU grep because of the PCRE option (-oP) and should return ms # @@ -11,22 +10,25 @@ # 1 = NO REPLY # 2 = ERROR +# decode json and get the ip +ipv4=$(echo $1 | jq -r '.ipv4') + +if [ -z "${ipv4}" ]; then + echo '{"result":"ERROR", "message":"no ipv4 address"}' + exit 2 +fi + GREP=/bin/grep -#GREP=/usr/local/bin/ggrep +##GREP=/usr/local/bin/ggrep -PING="ping -w 3 -c 1" -#PING="ping -t 3 -c 1" +PING4="ping -w 3 -c 1" +##PING4="ping -t 3 -c 1" -# use ipv4 and lastly the hostname -if [ -n "$2" ]; then - address=$2 -else - address=$1 -fi +PRINTF="/usr/bin/printf" -pingResult=$($PING $address 2>/dev/null) +pingResult=$($PING4 $ipv4 2>/dev/null) pingResultCode=$? pingTime=$(echo "$pingResult" | $GREP -oP 'time=\K\S+') -# LC_NUMERIC=C is needed to force printf to use a dot instead of a comma for the decimal separator -LC_NUMERIC=C printf 'ODATA: {"result":%d,"time":"%f"}' $pingResultCode $pingTime +# LC_ALL=C is needed to force printf to use a dot instead of a comma for the decimal separator +LC_ALL=C $PRINTF '{"ipv4":"%s","result":%d,"time":"%f","unit":"s"}' $ipv4 $pingResultCode $pingTime diff --git a/scripts/checks/ping6.sh b/scripts/checks/ping6.sh index 51d3992..8cb4fb9 100755 --- a/scripts/checks/ping6.sh +++ b/scripts/checks/ping6.sh @@ -1,8 +1,7 @@ #!/bin/bash -# script will be called by orcano with parameters: $1 = host, $2 = ipv4, $3 = ipv6 -# # name: IPV6 Ping # desc: Pings the host and returns result +# parameters: ipv6 # # note: needs GNU grep because of the PCRE option (-oP) and should return ms # @@ -11,16 +10,25 @@ # 1 = NO REPLY # 2 = ERROR -# use ipv6 and lastly the hostname -if [ -n "$3" ]; then - address=$3 -else - address=$1 +# decode json and get the ip +ipv6=$(echo $1 | jq -r '.ipv6') + +if [ -z "${ipv6}" ]; then + printf '{"result":"ERROR", "message": "no ipv6 address"}' + exit 2 fi -pingResult=$(ping6 -w 3 -c 1 $1 2>/dev/null) +GREP=/bin/grep +##GREP=/usr/local/bin/ggrep + +PING6="ping6 -w 3 -c 1" +##PING6="ping6 -t 3 -c 1" + +pingResult=$($PING6 $ipv6 2>/dev/null) pingResultCode=$? -pingTime=$(echo "$pingResult" | grep -oP 'time=\K\S+') +pingTime=$(echo "$pingResult" | $GREP -oP 'time=\K\S+') + +PRINTF="/usr/bin/printf" -# LC_NUMERIC=C is needed to force printf to use a dot instead of a comma for the decimal separator -LC_NUMERIC=C printf 'ODATA: {"result":%d,"time":"%f"}' $pingResultCode $pingTime +# LC_ALL=C is needed to force printf to use a dot instead of a comma for the decimal separator +LC_ALL=C $PRINTF '{"ipv4":"%s","result":%d,"time":"%f","unit":"s"}' $ipv4 $pingResultCode $pingTime diff --git a/src/Condition/AbstractCondition.php b/src/Condition/AbstractCondition.php index c2f9d2d..9f191d6 100644 --- a/src/Condition/AbstractCondition.php +++ b/src/Condition/AbstractCondition.php @@ -6,4 +6,57 @@ namespace App\Condition; -abstract class AbstractCondition implements ConditionInterface {} +abstract class AbstractCondition implements ConditionInterface, \Stringable +{ + public function __toString(): string + { + $reflectionClass = new \ReflectionClass(static::class); + + return $reflectionClass->getShortName(); + } + + public function getConditionClassName(): string + { + return static::class; + } + + /** @return array */ + public function getParameters(): array + { + $reflection = new \ReflectionClass(static::class); + $parameters = $reflection->getConstructor()->getParameters(); + + $result = []; + foreach ($parameters as $parameter) { + $result[] = $parameter->getName(); + } + + return $result; + } + + public function get(string $parameterName): mixed + { + $getter = 'get' . ucfirst($parameterName); + if (!method_exists($this, $getter)) { + throw new \Exception(sprintf('Could not find getter %s in %s', $getter, static::class)); + } + + return $this->{$getter}(); + } + + protected function setData(string $key, mixed $value): void + { + if (is_float($value)) { + $value = (float) $value; + } else if (is_numeric($value)) { + $value = (int) $value; + } else if (is_bool($value)) { + $value = (bool) $value; + } else if (is_string($value)) { + $value = (string) $value; + } + + $this->{$key} = $value; + } + +} diff --git a/src/Condition/ConditionCollection.php b/src/Condition/ConditionCollection.php index 0e3511b..07d3ea3 100644 --- a/src/Condition/ConditionCollection.php +++ b/src/Condition/ConditionCollection.php @@ -6,33 +6,47 @@ namespace App\Condition; -class ConditionCollection +use App\DataObject\Collection\DataObjectCollection; + +class ConditionCollection extends DataObjectCollection { - /** @var array */ - private array $conditions = []; + /** @var array */ + protected array $objects = []; - /** @return array> */ + /** @return array> */ public function __serialize(): array { return [ - 'conditions' => $this->conditions, + 'conditions' => $this->objects, ]; } - /** @param array> $data */ + /** @param array> $data */ public function __unserialize(array $data): void { - $this->conditions = $data['conditions']; + $this->objects = $data['objects'] ?? []; } public function addCondition(string $resultKey, AbstractCondition $condition): void { - $this->conditions[$resultKey] = $condition; + $id = md5(serialize($condition) . $resultKey); + $this->objects[$id] = new ConditionCollectionItem($resultKey, $condition); + } + + public function removeCondition(string $id): void + { + unset($this->objects[$id]); } - /** @return array */ + /** @return array */ public function getConditions(): array { - return $this->conditions; + return $this->objects; + } + + /** @param array $objects */ + public function setConditions(array $objects): void + { + $this->objects = $objects; } } diff --git a/src/Condition/ConditionCollectionHydrator.php b/src/Condition/ConditionCollectionHydrator.php new file mode 100644 index 0000000..b79dd96 --- /dev/null +++ b/src/Condition/ConditionCollectionHydrator.php @@ -0,0 +1,46 @@ +> $conditions */ + public function hydrateFromFormPost(array $conditions): ConditionCollection + { + $result = new ConditionCollection(); + + foreach ($conditions as $conditionData) { + $conditionClassName = $conditionData['name'] ?? null; + + if (empty($conditionClassName)) { + $this->logger->warning(sprintf('Could not find condition class name in %s', json_encode($conditionData, JSON_THROW_ON_ERROR))); + continue; + } + + $reflectionClass = new \ReflectionClass($conditionClassName); + + $parameters = $reflectionClass->getConstructor()->getParameters(); + + $parameterValues = []; + foreach ($parameters as $parameter) { + $parameterValues[] = $conditionData[$parameter->getName()] ?? null; + } + + $condition = $reflectionClass->newInstance(...$parameterValues); + + $result->addCondition($conditionData['key'], $condition); + } + + return $result; + } +} diff --git a/src/Condition/ConditionCollectionItem.php b/src/Condition/ConditionCollectionItem.php new file mode 100644 index 0000000..ca4c8d3 --- /dev/null +++ b/src/Condition/ConditionCollectionItem.php @@ -0,0 +1,25 @@ +name; + } + + public function getCondition(): AbstractCondition { + return $this->condition; + } +} diff --git a/src/Condition/ConditionInterface.php b/src/Condition/ConditionInterface.php index e898acc..691af51 100644 --- a/src/Condition/ConditionInterface.php +++ b/src/Condition/ConditionInterface.php @@ -11,4 +11,11 @@ interface ConditionInterface public function checkIfOk(mixed $value): bool; public function checkIfWarn(mixed $value): bool; + + public function getConditionClassName(): string; + + /** @return array */ + public function getParameters(): array; + + public function get(string $parameterName): mixed; } diff --git a/src/Condition/EqualsCondition.php b/src/Condition/EqualsCondition.php index b303f41..0b9a730 100644 --- a/src/Condition/EqualsCondition.php +++ b/src/Condition/EqualsCondition.php @@ -8,7 +8,16 @@ class EqualsCondition extends AbstractCondition { - public function __construct(private mixed $okValue, private mixed $warnValue = null) {} + protected mixed $okValue; + protected mixed $warnValue; + + public function __construct( + mixed $okValue = null, + mixed $warnValue = null + ) { + $this->setData('okValue', $okValue); + $this->setData('warnValue', $warnValue); + } /** * @return array @@ -39,4 +48,14 @@ public function checkIfWarn(mixed $value): bool { return $this->warnValue === $value; } + + public function getOkValue(): mixed + { + return $this->okValue; + } + + public function getWarnValue(): mixed + { + return $this->warnValue; + } } diff --git a/src/Condition/MinMaxCondition.php b/src/Condition/MinMaxCondition.php index c2396b1..ee483c9 100644 --- a/src/Condition/MinMaxCondition.php +++ b/src/Condition/MinMaxCondition.php @@ -17,7 +17,22 @@ class MinMaxCondition extends AbstractCondition private string $operator = self::DEFAULT_OPERATOR; - public function __construct(private mixed $okMin, private mixed $okMax, private mixed $warnMin = null, private mixed $warnMax = null) {} + protected mixed $okMin = null; + protected mixed $okMax = null; + protected mixed $warnMin = null; + protected mixed $warnMax = null; + + public function __construct( + mixed $okMin = null, + mixed $okMax = null, + mixed $warnMin = null, + mixed $warnMax = null + ) { + $this->setData('okMin', $okMin); + $this->setData('okMax', $okMax); + $this->setData('warnMin', $warnMin); + $this->setData('warnMax', $warnMax); + } /** * @return array @@ -69,6 +84,26 @@ public function getOperator(): string return $this->operator; } + public function getOkMin(): mixed + { + return $this->okMin; + } + + public function getOkMax(): mixed + { + return $this->okMax; + } + + public function getWarnMin(): mixed + { + return $this->warnMin; + } + + public function getWarnMax(): mixed + { + return $this->warnMax; + } + private function check(mixed $min, mixed $max, mixed $value): bool { if (!is_numeric($value)) { diff --git a/src/Controller/Api/Status/StatusController.php b/src/Controller/Api/Status/StatusController.php new file mode 100644 index 0000000..c5ae8ef --- /dev/null +++ b/src/Controller/Api/Status/StatusController.php @@ -0,0 +1,21 @@ +getJson(); + } +} + diff --git a/src/Controller/ConditionBuilder/ConditionBuilderController.php b/src/Controller/ConditionBuilder/ConditionBuilderController.php new file mode 100644 index 0000000..49f1902 --- /dev/null +++ b/src/Controller/ConditionBuilder/ConditionBuilderController.php @@ -0,0 +1,50 @@ +conditionService->getAllAvailableConditions(); + + $all = $request->query->all(); + if (!isset($all['condition'])) { + throw new \Exception('No condition given'); + } + + $conditions = $all['condition']; + + if (!is_array($conditions)) { + throw new \Exception('Condition is not an array'); + } + + $serviceCheckId = key($conditions); + $conditionIndex = key($conditions[$serviceCheckId]); + + return $this->renderPage('condition-builder/parameters.html.twig', [ + 'serviceCheckId' => $serviceCheckId, + 'conditionIndex' => $conditionIndex, + 'condition' => $availableConditions[$conditions[$serviceCheckId][$conditionIndex]['name']], + ]); + } +} diff --git a/src/Controller/Detail/CheckResultPageController.php b/src/Controller/Detail/CheckResultPageController.php new file mode 100644 index 0000000..5d8528a --- /dev/null +++ b/src/Controller/Detail/CheckResultPageController.php @@ -0,0 +1,34 @@ +checkResultPageLoader->load($request, $context, $id); + + return $this->renderPage('detail/check-result.html.twig', [ + 'page' => $page, + ]); + } +} diff --git a/src/Controller/Edit/AssetGroupPageController.php b/src/Controller/Edit/AssetGroupPageController.php index abece74..5ebafe9 100644 --- a/src/Controller/Edit/AssetGroupPageController.php +++ b/src/Controller/Edit/AssetGroupPageController.php @@ -6,10 +6,13 @@ namespace App\Controller\Edit; +use App\Condition\ConditionCollectionHydrator; use App\Context\Context; use App\Controller\Page\AbstractPageController; use App\DataObject\Page\PageMessageDataObject; +use App\Entity\AssetGroup; use App\Repository\AssetGroupRepository; +use App\Repository\AssetGroupServiceCheckConditionRepository; use App\Service\Page\AssetGroupPageLoader; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -20,7 +23,9 @@ class AssetGroupPageController extends AbstractPageController public function __construct( Context $context, private readonly AssetGroupPageLoader $assetGroupPageLoader, - private readonly AssetGroupRepository $assetGroupRepository + private readonly AssetGroupRepository $assetGroupRepository, + private readonly ConditionCollectionHydrator $conditionCollectionHydrator, + private readonly AssetGroupServiceCheckConditionRepository $assetGroupServiceCheckConditionRepository ) { parent::__construct($context); } @@ -35,6 +40,16 @@ public function indexAction(Request $request, Context $context, int $id = null): return $this->renderPage('edit/asset-group.html.twig', ['page' => $page]); } + #[Route('/delete/asset-group-service-check-condition/{assetGroupId}/{serviceCheckId}/{conditionId}', name: 'delete_asset_group_service_check_condition')] + public function deleteAssetGroupServiceCheckConditionAction(int $assetGroupId, int $serviceCheckId, string $conditionId): Response + { + $this->assetGroupServiceCheckConditionRepository->deleteByConditionId($assetGroupId, $serviceCheckId, $conditionId); + + $this->addMessage('label.entity-deleted', PageMessageDataObject::TYPE_SUCCESS); + + return $this->redirectToRoute('edit_asset_group', ['id' => $assetGroupId]); + } + private function processForm(Request $request, int $id = null): void { $errors = []; @@ -47,14 +62,27 @@ private function processForm(Request $request, int $id = null): void $data['id'] = $id ?? 0; + $assetGroup = null; + if ($errors === []) { try { - $this->assetGroupRepository->upsert($data); + $assetGroup = $this->assetGroupRepository->upsert($data); } catch (\Exception $ex) { $this->addMessage($ex->getMessage(), PageMessageDataObject::TYPE_DANGER); } } + if (!$assetGroup instanceof AssetGroup) { + $this->addMessage('label.entity-not-saved', PageMessageDataObject::TYPE_DANGER); + } + + if (isset($data['condition']) && $assetGroup instanceof AssetGroup) { + foreach ($data['condition'] as $serviceCheckId => $conditionData) { + $conditionCollection = $this->conditionCollectionHydrator->hydrateFromFormPost($conditionData); + $this->assetGroupServiceCheckConditionRepository->upsertByIds($assetGroup->getId(), $serviceCheckId, $conditionCollection); + } + } + $this->setErrors($errors); if ($errors === []) { diff --git a/src/Controller/Edit/AssetPageController.php b/src/Controller/Edit/AssetPageController.php index 1feec6a..52742ca 100644 --- a/src/Controller/Edit/AssetPageController.php +++ b/src/Controller/Edit/AssetPageController.php @@ -6,10 +6,13 @@ namespace App\Controller\Edit; +use App\Condition\ConditionCollectionHydrator; use App\Context\Context; use App\Controller\Page\AbstractPageController; use App\DataObject\Page\PageMessageDataObject; +use App\Entity\Asset; use App\Repository\AssetRepository; +use App\Repository\AssetServiceCheckRepository; use App\Service\Page\AssetPageLoader; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -20,7 +23,9 @@ class AssetPageController extends AbstractPageController public function __construct( Context $context, private readonly AssetPageLoader $assetPageLoader, - private readonly AssetRepository $assetRepository + private readonly AssetRepository $assetRepository, + private readonly ConditionCollectionHydrator $conditionCollectionHydrator, + private readonly AssetServiceCheckRepository $assetServiceCheckRepository ) { parent::__construct($context); } @@ -36,6 +41,16 @@ public function indexAction(Request $request, Context $context, int $id = null): 'page' => $page, ]); } + + #[Route('/delete/asset-service-check-condition/{assetId}/{serviceCheckId}/{conditionId}', name: 'delete_asset_service_check_condition')] + public function deleteAssetServiceCheckConditionAction(int $assetId, int $serviceCheckId, string $conditionId): Response + { + $this->assetServiceCheckRepository->deleteByConditionId($assetId, $serviceCheckId, $conditionId); + + $this->addMessage('label.entity-deleted', PageMessageDataObject::TYPE_SUCCESS); + + return $this->redirectToRoute('edit_asset', ['id' => $assetId]); + } private function processForm(Request $request, int $id = null): void { @@ -57,14 +72,27 @@ private function processForm(Request $request, int $id = null): void $data['id'] = $id ?? 0; + $asset = null; + if ($errors === []) { try { - $this->assetRepository->upsert($data); + $asset = $this->assetRepository->upsert($data); } catch (\Exception $ex) { $this->addMessage($ex->getMessage(), PageMessageDataObject::TYPE_DANGER); } } + if (!$asset instanceof Asset) { + $this->addMessage('label.entity-not-saved', PageMessageDataObject::TYPE_DANGER); + } + + if (isset($data['condition']) && $asset instanceof Asset) { + foreach ($data['condition'] as $serviceCheckId => $conditionData) { + $conditionCollection = $this->conditionCollectionHydrator->hydrateFromFormPost($conditionData); + $this->assetServiceCheckRepository->upsertByIds($asset->getId(), $serviceCheckId, $conditionCollection); + } + } + $this->setErrors($errors); if ($errors === []) { diff --git a/src/Controller/Edit/AssetServiceCheckPageController.php b/src/Controller/Edit/AssetServiceCheckPageController.php new file mode 100644 index 0000000..2dde939 --- /dev/null +++ b/src/Controller/Edit/AssetServiceCheckPageController.php @@ -0,0 +1,70 @@ +processForm($request, $id); + + $page = $this->assetServiceCheckPageLoader->load($request, $context, $assetId, $id); + + return $this->renderPage('edit/asset-service-check.html.twig', ['page' => $page]); + } + + private function processForm(Request $request, int $id = null): void + { + $errors = []; + if ($request->isMethod('POST')) { + $data = $request->request->all(); + + if (empty($data['name'])) { + $errors['name'] = new PageMessageDataObject('alert.name-empty', PageMessageDataObject::TYPE_DANGER); + } + + if (($data['service-check'] || $data['service-check'] === 0) === false) { + $errors['asset_service_check'] = new PageMessageDataObject('alert.service-check-empty', PageMessageDataObject::TYPE_DANGER); + } + + $data['id'] = $id ?? 0; + + if ($errors === []) { + try { + $this->assetServiceCheckRepository->upsert($data); + } catch (\Exception $ex) { + $this->addMessage($ex->getMessage(), PageMessageDataObject::TYPE_DANGER); + } + } + + $this->setErrors($errors); + + if ($errors === []) { + $this->addMessage('label.entity-saved', PageMessageDataObject::TYPE_SUCCESS); + } + } + } + +} diff --git a/src/Controller/Edit/AssetServiceCheckParameterPageController.php b/src/Controller/Edit/AssetServiceCheckParameterPageController.php new file mode 100644 index 0000000..cd8fbad --- /dev/null +++ b/src/Controller/Edit/AssetServiceCheckParameterPageController.php @@ -0,0 +1,32 @@ +assetServiceCheckPageLoader->load($request, $context, $assetId, $id); + + return $this->renderPage('edit/asset-service-check-parameter.html.twig', ['page' => $page]); + } +} diff --git a/src/Controller/Edit/CheckScriptParameterPageController.php b/src/Controller/Edit/CheckScriptParameterPageController.php new file mode 100644 index 0000000..de94497 --- /dev/null +++ b/src/Controller/Edit/CheckScriptParameterPageController.php @@ -0,0 +1,32 @@ +checkScriptParameterPageLoader->load($request, $context, $id); + + return $this->renderPage('edit/check-script-parameter.html.twig', ['page' => $page]); + } +} diff --git a/src/Controller/Edit/CheckScriptTestPageController.php b/src/Controller/Edit/CheckScriptTestPageController.php new file mode 100644 index 0000000..ba717a6 --- /dev/null +++ b/src/Controller/Edit/CheckScriptTestPageController.php @@ -0,0 +1,103 @@ +getCheckScriptParameter($request); + + $scriptFile = $this->getScriptFile($id); + + $message = new CheckNotification( + 0, + 0, + 0, + $this->getHostname($config), + $this->getIpv4Address($config), + $this->getIpv6Address($config), + $scriptFile, + $config + ); + + $result = $this->scriptRunnerService->runScript($message); + + $response = [ + 'executedCommand' => $result->getExecutedCommand(), + 'checkResult' => $result->getCheckResult(), + 'message' => $result->getMessage(), + 'note' => $result->getNote(), + 'rawScriptOutput' => $result->getRawScriptOutput(), + 'scriptOutput' => $result->getScriptOutput(), + ]; + + return $this->renderPage( + 'partials/check-script-test-output.html.twig', + $response + ); + } + + private function getScriptFile(int $id): string + { + $checkScript = $this->checkScriptRepository->find($id); + if ($checkScript === null) { + throw new \Exception('Check script not found'); + } + return $checkScript->getFilename(); + } + + /** @return array */ + private function getCheckScriptParameter(Request $request): array + { + $allParameters = $request->request->all(); + + if (!isset($allParameters['check-script-parameter'])) { + return []; + } + + return [ + 'checkScriptParameter' => $allParameters['check-script-parameter'] + ]; + } + + /** @param array $config */ + private function getHostname(array $config): string + { + return $config['hostname'] ?? 'localhost'; + } + + /** @param array $config */ + private function getIpv4Address(array $config): string + { + return $config['ipv4'] ?? '127.0.0.1'; + } + + /** @param array $config */ + private function getIpv6Address(array $config): string + { + return $config['ipv6'] ?? '::1'; + } +} diff --git a/src/DataObject/Collection/DataObjectCollection.php b/src/DataObject/Collection/DataObjectCollection.php index 797abd0..07013b1 100644 --- a/src/DataObject/Collection/DataObjectCollection.php +++ b/src/DataObject/Collection/DataObjectCollection.php @@ -12,7 +12,7 @@ class DataObjectCollection implements DataObjectCollectionInterface { /** @var array */ - private array $objects; + protected array $objects; /** * @param array $dataObjects diff --git a/src/DataObject/Page/AbstractPageDataObject.php b/src/DataObject/Page/AbstractPageDataObject.php index b0ada29..c9df536 100644 --- a/src/DataObject/Page/AbstractPageDataObject.php +++ b/src/DataObject/Page/AbstractPageDataObject.php @@ -17,6 +17,10 @@ abstract class AbstractPageDataObject implements PageDataObjectInterface, DataOb public function getParameters(): ParameterBag { + if (!$this->parameters instanceof ParameterBag) { + $this->parameters = new ParameterBag(); + } + return $this->parameters; } @@ -33,6 +37,10 @@ public function addParameter(string $key, mixed $value): self public function getParameter(string $key): mixed { + if (!$this->parameters instanceof ParameterBag) { + $this->parameters = new ParameterBag(); + } + return $this->parameters->get($key); } diff --git a/src/DataObject/Page/PageDataObjectInterface.php b/src/DataObject/Page/PageDataObjectInterface.php index 97e337e..2bd33ff 100644 --- a/src/DataObject/Page/PageDataObjectInterface.php +++ b/src/DataObject/Page/PageDataObjectInterface.php @@ -6,9 +6,17 @@ namespace App\DataObject\Page; +use Symfony\Component\HttpFoundation\ParameterBag; + interface PageDataObjectInterface { public function getTitle(): ?string; public function setTitle(?string $title): self; + + public function getParameters(): ParameterBag; + + public function addParameter(string $key, mixed $value): self; + + public function getParameter(string $key): mixed; } diff --git a/src/DataObject/ScriptResultDataObject.php b/src/DataObject/ScriptResultDataObject.php index 4e48442..c097da2 100644 --- a/src/DataObject/ScriptResultDataObject.php +++ b/src/DataObject/ScriptResultDataObject.php @@ -13,7 +13,7 @@ class ScriptResultDataObject implements DataObjectInterface public const RESULT_ERROR = 'ERROR'; public const RESULT_UNKNOWN = 'UNKNOWN'; - private string $checkResult; + private string $checkResult = self::RESULT_UNKNOWN; /** @var array */ private array $message = []; @@ -21,8 +21,14 @@ class ScriptResultDataObject implements DataObjectInterface /** @var array */ private array $scriptOutput = []; + private string $rawScriptOutput = ''; + + private float $durationMs = 0; + private ?string $note = null; + private string $executedCommand = ''; + public function getCheckResult(): string { return $this->checkResult; @@ -74,4 +80,40 @@ public function setScriptOutput(array $scriptOutput): self return $this; } + + public function getRawScriptOutput(): string + { + return $this->rawScriptOutput; + } + + public function setRawScriptOutput(string $rawScriptOutput): self + { + $this->rawScriptOutput = $rawScriptOutput; + + return $this; + } + + public function getExecutedCommand(): string + { + return $this->executedCommand; + } + + public function setExecutedCommand(string $executedCommand): self + { + $this->executedCommand = $executedCommand; + + return $this; + } + + public function getDurationMs(): float + { + return $this->durationMs; + } + + public function setDurationMs(float $durationMs): self + { + $this->durationMs = $durationMs; + + return $this; + } } diff --git a/src/DataObject/Scripts/MetaDataObject.php b/src/DataObject/Scripts/MetaDataObject.php index 2cf0b59..effff64 100644 --- a/src/DataObject/Scripts/MetaDataObject.php +++ b/src/DataObject/Scripts/MetaDataObject.php @@ -7,6 +7,7 @@ namespace App\DataObject\Scripts; use App\DataObject\DataObjectInterface; +use App\Entity\CheckScriptParameter; class MetaDataObject implements DataObjectInterface { @@ -16,6 +17,9 @@ class MetaDataObject implements DataObjectInterface private ?string $description = null; + /** @var array */ + private array $parameters = []; + public function getFilename(): string { return $this->filename; @@ -51,4 +55,24 @@ public function setDescription(?string $description): self return $this; } + + /** @return array */ + public function getParameters(): array + { + return $this->parameters; + } + + public function addParameter(CheckScriptParameter $parameter): self + { + $this->parameters[] = $parameter; + + return $this; + } + + public function setParameters(array $parameters): self + { + $this->parameters = $parameters; + + return $this; + } } diff --git a/src/Entity/Asset.php b/src/Entity/Asset.php index 6698cc0..46c587d 100644 --- a/src/Entity/Asset.php +++ b/src/Entity/Asset.php @@ -34,11 +34,15 @@ class Asset implements DataObjectInterface, ApiEntityInterface #[ORM\ManyToMany(targetEntity: AssetGroup::class, inversedBy: 'assets')] private Collection $assetGroups; + #[ORM\OneToMany(mappedBy: 'asset', targetEntity: AssetServiceCheck::class, orphanRemoval: true)] + private Collection $serviceChecks; + public function __construct() { $this->createdAt = new \DateTime(); $this->updatedAt = new \DateTime(); $this->assetGroups = new ArrayCollection(); + $this->serviceChecks = new ArrayCollection(); } #[Ignore] @@ -141,4 +145,29 @@ public function getAssetGroupsAsString(): string return rtrim($assetGroupsAsString, ', '); } + + /** + * @return Collection + */ + public function getServiceChecks(): Collection + { + return $this->serviceChecks; + } + + public function addServiceCheck(ServiceCheck $serviceCheck): self + { + if (!$this->serviceChecks->contains($serviceCheck)) { + $this->serviceChecks->add($serviceCheck); + } + + return $this; + } + + public function removeServiceCheck(ServiceCheck $serviceCheck): self + { + $this->serviceChecks->removeElement($serviceCheck); + + return $this; + } + } diff --git a/src/Entity/AssetGroupServiceCheckCondition.php b/src/Entity/AssetGroupServiceCheckCondition.php index fe001cf..c7cda8c 100644 --- a/src/Entity/AssetGroupServiceCheckCondition.php +++ b/src/Entity/AssetGroupServiceCheckCondition.php @@ -6,12 +6,14 @@ namespace App\Entity; +use App\Condition\ConditionCollection; +use App\DataObject\DataObjectInterface; use App\Repository\AssetGroupServiceCheckConditionRepository; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; #[ORM\Entity(repositoryClass: AssetGroupServiceCheckConditionRepository::class)] -class AssetGroupServiceCheckCondition +class AssetGroupServiceCheckCondition implements DataObjectInterface { use Trait\IdTrait; @@ -26,12 +28,18 @@ class AssetGroupServiceCheckCondition #[ORM\Column(type: Types::TEXT, nullable: true)] private ?string $conditions = null; + public function __construct() + { + $this->createdAt = new \DateTime(); + $this->updatedAt = new \DateTime(); + } + public function getAssetGroup(): ?AssetGroup { return $this->assetGroup; } - public function setAssetGroup(?AssetGroup $assetGroup): static + public function setAssetGroup(?AssetGroup $assetGroup): self { $this->assetGroup = $assetGroup; @@ -43,7 +51,12 @@ public function getServiceCheck(): ?ServiceCheck return $this->serviceCheck; } - public function setServiceCheck(?ServiceCheck $serviceCheck): static + public function getServiceCheckId(): ?int + { + return $this->serviceCheck->getId(); + } + + public function setServiceCheck(?ServiceCheck $serviceCheck): self { $this->serviceCheck = $serviceCheck; @@ -55,10 +68,22 @@ public function getConditions(): ?string return $this->conditions; } - public function setConditions(?string $conditions): static + public function setConditions(?string $conditions): self { $this->conditions = $conditions; return $this; } + + public function setConditionCollection(ConditionCollection $conditionCollection): self + { + $this->conditions = serialize($conditionCollection); + + return $this; + } + + public function getConditionCollection(): ConditionCollection + { + return unserialize($this->conditions, [ConditionCollection::class]); + } } diff --git a/src/Entity/AssetServiceCheck.php b/src/Entity/AssetServiceCheck.php new file mode 100644 index 0000000..56810c7 --- /dev/null +++ b/src/Entity/AssetServiceCheck.php @@ -0,0 +1,131 @@ + */ + #[ORM\Column(nullable: true)] + private ?array $config = null; + + public function __construct() + { + $this->createdAt = new \DateTime(); + $this->updatedAt = new \DateTime(); + } + + public function getAsset(): ?Asset + { + return $this->asset; + } + + public function setAsset(?Asset $asset): self + { + $this->asset = $asset; + + return $this; + } + + public function getServiceCheckId(): ?int + { + return $this->serviceCheck?->getId(); + } + + public function getServiceCheck(): ?ServiceCheck + { + return $this->serviceCheck; + } + + public function setServiceCheck(?ServiceCheck $serviceCheck): self + { + $this->serviceCheck = $serviceCheck; + + return $this; + } + + public function getName(): ?string + { + return $this->name; + } + + public function setName(?string $name): self + { + $this->name = $name; + + return $this; + } + + public function getConditions(): ?string + { + return $this->conditions; + } + + public function setConditions(?string $conditions): self + { + $this->conditions = $conditions; + + return $this; + } + + public function setConditionCollection(ConditionCollection $conditionCollection): self + { + $this->conditions = serialize($conditionCollection); + + return $this; + } + + public function getConditionCollection(): ConditionCollection + { + if ($this->conditions === null) { + return new ConditionCollection(); + } + + return unserialize($this->conditions, [ConditionCollection::class]); + } + + /** @return array */ + public function getConfig(): array + { + if ($this->config === null) { + return []; + } + + return $this->config; + } + + /** @param ?array $config */ + public function setConfig(?array $config): self + { + $this->config = $config; + + return $this; + } +} diff --git a/src/Entity/CheckResult.php b/src/Entity/CheckResult.php new file mode 100644 index 0000000..fcdeed1 --- /dev/null +++ b/src/Entity/CheckResult.php @@ -0,0 +1,147 @@ +createdAt = new \DateTime(); + $this->updatedAt = new \DateTime(); + $this->durationMs = 0.0; + } + + #[Ignore] + public function setData(array $data): self + { + $this->setDataIfNotEmptyString($data, 'result', 'result'); + $this->setDataIfNotEmptyString($data, 'message', 'message'); + $this->setDataIfNotEmptyString($data, 'scriptOutput', 'scriptOutput'); + $this->setDataIfNotEmptyFloat($data, 'durationMs', 'durationMs'); + + return $this; + } + + public function setResult(string $result): self + { + $this->result = $result; + + return $this; + } + + public function getResult(): string + { + return $this->result; + } + + public function setMessage(string $message): self + { + $this->message = $message; + + return $this; + } + + public function getMessage(): string + { + return $this->message; + } + + public function setScriptOutput(string $scriptOutput): self + { + $this->scriptOutput = $scriptOutput; + + return $this; + } + + public function getScriptOutput(): string + { + return $this->scriptOutput; + } + + public function getAsset(): ?Asset + { + return $this->asset; + } + + public function setAsset(?Asset $asset): self + { + $this->asset = $asset; + + return $this; + } + + public function getServiceCheck(): ?ServiceCheck + { + return $this->serviceCheck; + } + + public function setServiceCheck(?ServiceCheck $serviceCheck): self + { + $this->serviceCheck = $serviceCheck; + + return $this; + } + + public function getAssetServiceCheck(): ?AssetServiceCheck + { + return $this->assetServiceCheck; + } + + public function setAssetServiceCheck(?AssetServiceCheck $assetServiceCheck): self + { + $this->assetServiceCheck = $assetServiceCheck; + + return $this; + } + + public function getDurationMs(): float + { + return $this->durationMs; + } + + public function setDurationMs(float $durationMs): self + { + $this->durationMs = $durationMs; + + return $this; + } + +} diff --git a/src/Entity/CheckScript.php b/src/Entity/CheckScript.php index e0577af..4798e12 100644 --- a/src/Entity/CheckScript.php +++ b/src/Entity/CheckScript.php @@ -8,6 +8,8 @@ use App\DataObject\DataObjectInterface; use App\Repository\CheckScriptRepository; +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Serializer\Annotation\Ignore; @@ -32,10 +34,14 @@ class CheckScript implements DataObjectInterface, ApiEntityInterface private bool $isChangedInFilesystem = false; + #[ORM\OneToMany(mappedBy: 'checkScript', targetEntity: CheckScriptParameter::class, cascade: ['persist'])] + private Collection $checkScriptParameters; + public function __construct() { $this->createdAt = new \DateTime(); $this->updatedAt = new \DateTime(); + $this->checkScriptParameters = new ArrayCollection(); } #[Ignore] @@ -112,4 +118,41 @@ public function setIsChangedInFilesystem(bool $isChangedInFilesystem): self return $this; } + + /** + * @return Collection + */ + public function getCheckScriptParameters(): Collection + { + return $this->checkScriptParameters; + } + + public function setCheckScriptParameters(Collection $checkScriptParameters): self + { + $this->checkScriptParameters = $checkScriptParameters; + + return $this; + } + + public function addCheckScriptParameter(CheckScriptParameter $checkScriptParameter): self + { + if (!$this->checkScriptParameters->containsKey($checkScriptParameter->getName())) { + $this->checkScriptParameters->set($checkScriptParameter->getName(), $checkScriptParameter); + $checkScriptParameter->setCheckScript($this); + } + + return $this; + } + + public function removeCheckScriptParameter(CheckScriptParameter $checkScriptParameter): self + { + if ($this->checkScriptParameters->removeElement($checkScriptParameter)) { + // set the owning side to null (unless already changed) + if ($checkScriptParameter->getCheckScript() === $this) { + $checkScriptParameter->setCheckScript(null); + } + } + + return $this; + } } diff --git a/src/Entity/CheckScriptParameter.php b/src/Entity/CheckScriptParameter.php new file mode 100644 index 0000000..379fdcd --- /dev/null +++ b/src/Entity/CheckScriptParameter.php @@ -0,0 +1,69 @@ +createdAt = new \DateTime(); + $this->updatedAt = new \DateTime(); + } + + public function getId(): ?int + { + return $this->id; + } + + public function getCheckScript(): ?CheckScript + { + return $this->checkScript; + } + + public function setCheckScript(?CheckScript $checkScript): static + { + $this->checkScript = $checkScript; + + return $this; + } + + public function getName(): string + { + return $this->name; + } + + public function setName(string $name): static + { + $this->name = $name; + + return $this; + } + + public function getDataType(): string + { + return $this->dataType; + } + + public function setDataType(string $dataType): static + { + $this->dataType = $dataType; + + return $this; + } +} diff --git a/src/Entity/ServiceCheck.php b/src/Entity/ServiceCheck.php index a0b77d8..17e4865 100644 --- a/src/Entity/ServiceCheck.php +++ b/src/Entity/ServiceCheck.php @@ -23,7 +23,7 @@ class ServiceCheck implements DataObjectInterface, ApiEntityInterface public const DEFAULT_MAX_RETRIES = 3; - #[ORM\Column(length: 255)] + #[ORM\Column(length: 255, unique: true)] private ?string $name = null; #[ORM\ManyToOne] @@ -202,4 +202,5 @@ private function getAssetGroups(): Collection { return $this->assetGroups; } + } diff --git a/src/Entity/User.php b/src/Entity/User.php index 8b56d73..35751fb 100644 --- a/src/Entity/User.php +++ b/src/Entity/User.php @@ -53,6 +53,9 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface, DataObj 'keybinding' => self::DEFAULT_CODE_EDITOR_KEYBINDING, ]; + #[ORM\Column] + private ?string $scriptTestData = null; + public function __construct() { $this->createdAt = new \DateTime(); @@ -217,4 +220,16 @@ public function setCodeEditorConfig(?array $codeEditorConfig): static return $this; } + + public function getScriptTestData(): ?string + { + return $this->scriptTestData; + } + + public function setScriptTestData(?string $scriptTestData): static + { + $this->scriptTestData = $scriptTestData; + + return $this; + } } diff --git a/src/Message/CheckNotification.php b/src/Message/CheckNotification.php index 342731f..6d12937 100644 --- a/src/Message/CheckNotification.php +++ b/src/Message/CheckNotification.php @@ -8,13 +8,16 @@ class CheckNotification { + /** @param array $config */ public function __construct( private readonly int $assetId, + private readonly int $assetServiceCheckId, private readonly int $serviceCheckId, private readonly string $hostname, private readonly ?string $ipv4Address, private readonly ?string $ipv6Address, - private readonly string $checkScriptFilename + private readonly string $checkScriptFilename, + private readonly array $config = [] ) {} public function getAssetId(): int @@ -22,6 +25,11 @@ public function getAssetId(): int return $this->assetId; } + public function getAssetServiceCheckId(): int + { + return $this->assetServiceCheckId; + } + public function getServiceCheckId(): int { return $this->serviceCheckId; @@ -46,4 +54,10 @@ public function getCheckScriptFilename(): string { return $this->checkScriptFilename; } + + /** @return array */ + public function getConfig(): array + { + return $this->config; + } } diff --git a/src/MessageHandler/CheckNotificationHandler.php b/src/MessageHandler/CheckNotificationHandler.php index b80aa7e..505c4f4 100644 --- a/src/MessageHandler/CheckNotificationHandler.php +++ b/src/MessageHandler/CheckNotificationHandler.php @@ -9,23 +9,17 @@ use App\DataObject\ScriptResultDataObject; use App\Message\CheckNotification; use App\Message\CheckResultNotification; -use App\Service\Scripts\ResultParserService; -use Psr\Log\LoggerInterface; -use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface; +use App\Service\Scripts\ScriptRunnerService; use Symfony\Component\Messenger\Attribute\AsMessageHandler; use Symfony\Component\Messenger\MessageBusInterface; -use Symfony\Component\Process\Process; #[AsMessageHandler] class CheckNotificationHandler { - private const PROCESS_MAX_RUNTIME_SECONDS = 15; public function __construct( - private readonly ParameterBagInterface $parameterBag, - private readonly ResultParserService $resultParserService, - private readonly MessageBusInterface $bus, - private readonly LoggerInterface $logger + private readonly ScriptRunnerService $scriptRunnerService, + private readonly MessageBusInterface $bus ) {} public function __invoke(CheckNotification $message): void @@ -35,58 +29,16 @@ public function __invoke(CheckNotification $message): void private function runScript(CheckNotification $message): bool { - $scriptPath = $this->parameterBag->get('kernel.project_dir') . '/' . $message->getCheckScriptFilename(); - if (!file_exists($scriptPath)) { - $this->sendError(sprintf('Script %s does not exist.', $scriptPath), $message); - - return false; - } - - $command = sprintf('%s "%s" "%s" "%s"', - $scriptPath, - $message->getHostname(), - $message->getIpv4Address(), - $message->getIpv6Address() - ); - - $this->logger->notice(sprintf('CMD: %s', $command)); - - $process = Process::fromShellCommandline($command); - $process->setTimeout(self::PROCESS_MAX_RUNTIME_SECONDS); - $process->run(); - - if (!$process->isSuccessful()) { - $this->sendError(sprintf('Script %s failed with error: %s.', $scriptPath, $process->getErrorOutput()), $message); - - return false; - } - - $output = $process->getOutput(); - - $jsonResponse = $this->resultParserService->extractJson($output); - - $result = new ScriptResultDataObject(); - $result->setScriptOutput($jsonResponse); - + $result = $this->scriptRunnerService->runScript($message); $this->sendResultMessage($result, $message); return true; } - private function sendError(string $message, CheckNotification $originalMessage): void - { - $result = new ScriptResultDataObject(); - $result->setCheckResult(ScriptResultDataObject::RESULT_UNKNOWN); - $result->setNote($message); - - $this->logger->error($message); - - $this->sendResultMessage($result, $originalMessage); - } - private function sendResultMessage(ScriptResultDataObject $result, CheckNotification $originalMessage): void { $message = new CheckResultNotification($result, $originalMessage); $this->bus->dispatch($message); } + } diff --git a/src/MessageHandler/CheckResultNotificationHandler.php b/src/MessageHandler/CheckResultNotificationHandler.php index f669acb..97dd05e 100644 --- a/src/MessageHandler/CheckResultNotificationHandler.php +++ b/src/MessageHandler/CheckResultNotificationHandler.php @@ -7,12 +7,16 @@ namespace App\MessageHandler; use App\Condition\ConditionCollection; -use App\Condition\EqualsCondition; -use App\Condition\MinMaxCondition; use App\DataObject\ScriptResultDataObject; +use App\Entity\CheckResult; +use App\Entity\CheckScript; +use App\Message\CheckNotification; use App\Message\CheckResultNotification; +use App\Repository\AssetServiceCheckRepository; +use App\Repository\CheckResultRepository; use App\Service\Condition\ConditionService; use App\Service\Scripts\ResultParserService; +use Doctrine\ORM\EntityManagerInterface; use Psr\Log\LoggerInterface; use Symfony\Component\Messenger\Attribute\AsMessageHandler; @@ -21,6 +25,9 @@ class CheckResultNotificationHandler { public function __construct( private readonly ResultParserService $resultParserService, + private readonly AssetServiceCheckRepository $assetServiceCheckRepository, + private readonly EntityManagerInterface $entityManager, + private readonly CheckResultRepository $checkResultRepository, private readonly ConditionService $conditionService, private readonly LoggerInterface $logger ) {} @@ -30,46 +37,72 @@ public function __invoke(CheckResultNotification $message): void $result = $message->getResult(); $originalNotification = $message->getOriginalNotification(); - // todo: - // - // have specific conditions per checkscript - // - // have an inheritance would make very much sense: - // AssetGroup -> Asset - // $conditionTemplateString = // serialized conditions string - // $conditions = unserialize($conditionTemplateString); - /* $conditions = new ConditionCollection(); - $conditions->addCondition('result', new EqualsCondition(0)); - $conditions->addCondition('time', new MinMaxCondition(0, 1000, 20)); // ok if between 0 and 1000, warn if between 20 and 1000 - - echo addslashes(serialize($conditions));*/ - $conditions = $this->conditionService->getCheckConditions( $originalNotification->getAssetId(), - $originalNotification->getServiceCheckId() + $originalNotification->getAssetServiceCheckId() ); - $checkResult = $this->checkResult($result, $conditions); + $this->checkResult($result, $conditions); $this->logger->notice(sprintf('check %s on %s, result: %s (%s)', $originalNotification->getCheckScriptFilename(), $originalNotification->getHostname(), - $checkResult->getCheckResult(), - $checkResult->getNote() + $result->getCheckResult(), + $result->getNote() )); + + $checkResultEntity = $this->transformCheckResult($result, $originalNotification, $result); + + $serviceCheckName = $checkResultEntity->getServiceCheck()->getName(); + $checkScript = $checkResultEntity->getServiceCheck()->getCheckScript(); + + if (!$checkScript instanceof CheckScript) { + $this->logger->error(sprintf('Check script not found for service check %s', $serviceCheckName)); + return; + } + + $this->entityManager->persist($checkResultEntity); + $this->entityManager->flush(); + + if ($checkScript->getName() === null) { + $this->logger->error(sprintf('Check script name not found for service check %s.', $serviceCheckName)); + return; + } + + $this->checkResultRepository->updateCheckResultTableStructure($result, $checkScript->getName()); + $this->checkResultRepository->insertCheckResult($result, $checkScript->getName(), $checkResultEntity->getId()); } - private function checkResult(ScriptResultDataObject $result, ConditionCollection $conditions): ScriptResultDataObject + private function transformCheckResult(ScriptResultDataObject $scriptResult, CheckNotification $checkNotification): CheckResult { - $output = $result->getScriptOutput(); + $checkResultEntity = new CheckResult(); + $checkResultEntity->setData([ + 'result' => $scriptResult->getCheckResult(), + 'message' => json_encode($scriptResult->getMessage()), + 'durationMs' => $scriptResult->getDurationMs(), + 'scriptOutput' => json_encode($scriptResult->getScriptOutput()), + ]); + + $assetServiceCheck = $this->assetServiceCheckRepository->find($checkNotification->getAssetServiceCheckId()); + if ($assetServiceCheck === null) { + throw new \Exception('Asset service check not found'); + } + $checkResultEntity->setAsset($assetServiceCheck->getAsset()); + $checkResultEntity->setServiceCheck($assetServiceCheck->getServiceCheck()); + $checkResultEntity->setAssetServiceCheck($assetServiceCheck); + + return $checkResultEntity; + } + + + private function checkResult(ScriptResultDataObject $result, ConditionCollection $conditions): void + { try { - $result = $this->resultParserService->parseResultJson($output, $conditions); + $this->resultParserService->parseResultJson($result, $conditions); } catch (\Exception $e) { $this->logger->error(sprintf('ResultParserService failed: %s', $e->getMessage()), $output); $result->setCheckResult(ScriptResultDataObject::RESULT_UNKNOWN); } - - return $result; } } diff --git a/src/Repository/AssetGroupRepository.php b/src/Repository/AssetGroupRepository.php index 9056509..34842e9 100644 --- a/src/Repository/AssetGroupRepository.php +++ b/src/Repository/AssetGroupRepository.php @@ -9,6 +9,7 @@ use App\DataObject\Collection\DataObjectCollection; use App\DataObject\Collection\DataObjectCollectionInterface; use App\Entity\AssetGroup; +use App\Entity\ServiceCheck; use Doctrine\Persistence\ManagerRegistry; /** @@ -31,6 +32,18 @@ public function upsert(array $data): AssetGroup $assetGroup->setName($data['name'] ?? ''); + $this->clearServiceChecks($assetGroup); + + if (isset($data['service-checks'])) { + foreach ($data['service-checks'] as $serviceCheckId) { + $serviceCheck = $em->getRepository(ServiceCheck::class)->find($serviceCheckId); + if ($serviceCheck === null) { + throw new \Exception('Service check not found'); + } + $assetGroup->addServiceCheck($serviceCheck); + } + } + $em->persist($assetGroup); $em->flush(); @@ -52,28 +65,12 @@ public function findByNamesAsCollection(array $names): DataObjectCollectionInter return new DataObjectCollection($result); } - // /** - // * @return AssetGroup[] Returns an array of AssetGroup objects - // */ - // public function findByExampleField($value): array - // { - // return $this->createQueryBuilder('a') - // ->andWhere('a.exampleField = :val') - // ->setParameter('val', $value) - // ->orderBy('a.id', 'ASC') - // ->setMaxResults(10) - // ->getQuery() - // ->getResult() - // ; - // } + private function clearServiceChecks(AssetGroup $assetGroup): void + { + $assetGroup->getServiceChecks()->clear(); - // public function findOneBySomeField($value): ?AssetGroup - // { - // return $this->createQueryBuilder('a') - // ->andWhere('a.exampleField = :val') - // ->setParameter('val', $value) - // ->getQuery() - // ->getOneOrNullResult() - // ; - // } + $conn = $this->getEntityManager()->getConnection(); + $conn->delete('asset_group_service_check_condition', ['asset_group_id' => $assetGroup->getId()]); + $conn->delete('service_check_asset_group', ['asset_group_id' => $assetGroup->getId()]); + } } diff --git a/src/Repository/AssetGroupServiceCheckConditionRepository.php b/src/Repository/AssetGroupServiceCheckConditionRepository.php index 11f8312..4419948 100644 --- a/src/Repository/AssetGroupServiceCheckConditionRepository.php +++ b/src/Repository/AssetGroupServiceCheckConditionRepository.php @@ -6,6 +6,7 @@ namespace App\Repository; +use App\Condition\ConditionCollection; use App\Entity\AssetGroupServiceCheckCondition; use Doctrine\Persistence\ManagerRegistry; @@ -22,28 +23,37 @@ public function __construct(ManagerRegistry $registry) parent::__construct($registry, AssetGroupServiceCheckCondition::class); } - // /** - // * @return AssetGroupServiceCheckCondition[] Returns an array of AssetGroupServiceCheckCondition objects - // */ - // public function findByExampleField($value): array - // { - // return $this->createQueryBuilder('a') - // ->andWhere('a.exampleField = :val') - // ->setParameter('val', $value) - // ->orderBy('a.id', 'ASC') - // ->setMaxResults(10) - // ->getQuery() - // ->getResult() - // ; - // } - - // public function findOneBySomeField($value): ?AssetGroupServiceCheckCondition - // { - // return $this->createQueryBuilder('a') - // ->andWhere('a.exampleField = :val') - // ->setParameter('val', $value) - // ->getQuery() - // ->getOneOrNullResult() - // ; - // } + public function upsertByIds(int $assetGroupId, int $serviceCheckId, ConditionCollection $conditions): AssetGroupServiceCheckCondition + { + $em = $this->getEntityManager(); + $assetGroupServiceCheckCondition = $this->findOneBy(['assetGroup' => $assetGroupId, 'serviceCheck' => $serviceCheckId]) ?? new AssetGroupServiceCheckCondition(); + + $assetGroupServiceCheckCondition->setAssetGroup($em->getReference(\App\Entity\AssetGroup::class, $assetGroupId)); + $assetGroupServiceCheckCondition->setServiceCheck($em->getReference(\App\Entity\ServiceCheck::class, $serviceCheckId)); + $assetGroupServiceCheckCondition->setConditionCollection($conditions); + + $em->persist($assetGroupServiceCheckCondition); + $em->flush(); + + return $assetGroupServiceCheckCondition; + } + + public function deleteByConditionId(int $assetGroupId, int $serviceCheckId, string $conditionId): void + { + $em = $this->getEntityManager(); + $assetGroupServiceCheckCondition = $this->findOneBy(['assetGroup' => $assetGroupId, 'serviceCheck' => $serviceCheckId]); + if ($assetGroupServiceCheckCondition === null) { + throw new \Exception('Asset group service check condition not found'); + } + + $conditionCollection = $assetGroupServiceCheckCondition->getConditionCollection(); + $conditions = $conditionCollection->getConditions(); + if (isset($conditions[$conditionId])) { + unset($conditions[$conditionId]); + $conditionCollection->setConditions($conditions); + $assetGroupServiceCheckCondition->setConditionCollection($conditionCollection); + $em->persist($assetGroupServiceCheckCondition); + $em->flush(); + } + } } diff --git a/src/Repository/AssetRepository.php b/src/Repository/AssetRepository.php index 8bf221a..9728af1 100644 --- a/src/Repository/AssetRepository.php +++ b/src/Repository/AssetRepository.php @@ -8,6 +8,7 @@ use App\Entity\Asset; use App\Entity\AssetGroup; +use App\Entity\ServiceCheck; use Doctrine\Persistence\ManagerRegistry; /** @@ -18,8 +19,6 @@ */ class AssetRepository extends AbstractServiceEntityRepository { - use BaseRepositoryTrait; - public function __construct(ManagerRegistry $registry) { parent::__construct($registry, Asset::class); @@ -54,6 +53,18 @@ public function upsert(array $data): Asset } } + $asset->getServiceChecks()->clear(); + + if (isset($data['service-checks'])) { + foreach ($data['service-checks'] as $serviceCheckId) { + $serviceCheck = $em->getRepository(ServiceCheck::class)->find($serviceCheckId); + if ($serviceCheck === null) { + throw new \Exception('ServiceCheck not found'); + } + $asset->addServiceCheck($serviceCheck); + } + } + $em->persist($asset); $em->flush(); diff --git a/src/Repository/AssetServiceCheckRepository.php b/src/Repository/AssetServiceCheckRepository.php new file mode 100644 index 0000000..f4f7a0b --- /dev/null +++ b/src/Repository/AssetServiceCheckRepository.php @@ -0,0 +1,116 @@ +getEntityManager(); + $assetServiceCheck = $data['id'] !== 0 ? $this->find($data['id']) : new AssetServiceCheck(); + + $assetServiceCheck->setName($data['name'] ?? ''); + + $serviceCheck = $this->serviceCheckRepository->find($data['service-check']); + if ($serviceCheck === null) { + throw new \Exception('Service check not found'); + } + + $assetServiceCheck->setServiceCheck($serviceCheck); + + $asset = $this->assetRepository->find($data['assetId']); + if ($asset === null) { + throw new \Exception('Asset not found'); + } + + $assetServiceCheck->setAsset($asset); + + $assetServiceCheck->setConfig([ + 'checkScriptParameter' => $data['check-script-parameter'] ?? [], + ]); + + $em->persist($assetServiceCheck); + $em->flush(); + + return $assetServiceCheck; + } + + public function upsertByIds(int $assetId, int $serviceCheckId, ConditionCollection $conditions): AssetServiceCheck + { + $em = $this->getEntityManager(); + $assetServiceCheckCondition = $this->findOneBy(['asset' => $assetId, 'serviceCheck' => $serviceCheckId]) ?? new AssetServiceCheck(); + + $assetServiceCheckCondition->setAsset($em->getReference(Asset::class, $assetId)); + $assetServiceCheckCondition->setServiceCheck($em->getReference(ServiceCheck::class, $serviceCheckId)); + $assetServiceCheckCondition->setConditionCollection($conditions); + + $em->persist($assetServiceCheckCondition); + $em->flush(); + + return $assetServiceCheckCondition; + } + + public function deleteByConditionId(int $assetId, int $serviceCheckId, string $conditionId): void + { + $em = $this->getEntityManager(); + $assetServiceCheckCondition = $this->findOneBy(['asset' => $assetId, 'serviceCheck' => $serviceCheckId]); + if ($assetServiceCheckCondition === null) { + throw new \Exception('Asset service check condition not found'); + } + + $conditionCollection = $assetServiceCheckCondition->getConditionCollection(); + $conditions = $conditionCollection->getConditions(); + if (isset($conditions[$conditionId])) { + unset($conditions[$conditionId]); + $conditionCollection->setConditions($conditions); + $assetServiceCheckCondition->setConditionCollection($conditionCollection); + $em->persist($assetServiceCheckCondition); + $em->flush(); + } + } + +// /** +// * @return AssetServiceCheck[] Returns an array of AssetServiceCheck objects +// */ +// public function findByExampleField($value): array +// { +// return $this->createQueryBuilder('a') +// ->andWhere('a.exampleField = :val') +// ->setParameter('val', $value) +// ->orderBy('a.id', 'ASC') +// ->setMaxResults(10) +// ->getQuery() +// ->getResult() +// ; +// } + +// public function findOneBySomeField($value): ?AssetServiceCheck +// { +// return $this->createQueryBuilder('a') +// ->andWhere('a.exampleField = :val') +// ->setParameter('val', $value) +// ->getQuery() +// ->getOneOrNullResult() +// ; +// } +} diff --git a/src/Repository/CheckResultRepository.php b/src/Repository/CheckResultRepository.php new file mode 100644 index 0000000..1b11136 --- /dev/null +++ b/src/Repository/CheckResultRepository.php @@ -0,0 +1,213 @@ +registry, CheckResult::class); + } + + /** @return array */ + public function fetchDetailsByCheckResult(CheckResult $checkResult): array + { + if ($checkResult->getId() === null) { + throw new \Exception('Check result id is null'); + } + + if (!$checkResult->getServiceCheck() instanceof ServiceCheck) { + throw new \Exception('Service check not found'); + } + + if (!$checkResult->getServiceCheck()->getCheckScript() instanceof CheckScript) { + throw new \Exception('Check script not found'); + } + + $checkScript = $checkResult->getServiceCheck()->getCheckScript(); + $tableName = $this->transformTableName($checkScript->getName()); + + $q = "SELECT * FROM {$tableName} WHERE check_result_id = :check_result_id"; + + $em = $this->getEntityManager(); + $conn = $em->getConnection(); + $stmt = $conn->prepare($q); + + $stmt->bindValue('check_result_id', $checkResult->getId()); + + $results = $stmt->executeQuery()->fetchAssociative(); + + if ($results === false) { + return []; + } + + return $results; + } + + public function insertCheckResult(ScriptResultDataObject $scriptResult, string $checkScriptName, int $checkResultId): void + { + $scriptMessage = $scriptResult->getMessage(); + + if (!is_array($scriptMessage)) { + return; + } + + $tableName = $this->transformTableName($checkScriptName); + + $keysSql = implode(',', array_keys($scriptMessage)); + $keysPlaceholerSql = implode(', :', array_keys($scriptMessage)); + + if (!empty($keysPlaceholerSql)) { + $keysSql = ', ' . $keysSql; + $placeholderString = ', :' . $keysPlaceholerSql; + } else { + $keysSql = ''; + $placeholderString = ''; + } + + $q =<<getEntityManager(); + $conn = $em->getConnection(); + $stmt = $conn->prepare($q); + + $stmt->bindValue('check_result_id', $checkResultId); + + foreach ($scriptMessage as $key => $value) { + $parameterType = MySqlTypeService::getParameterType($value); + $stmt->bindValue($key, MySqlTypeService::transformValue($value), $parameterType); + } + + $stmt->executeStatement(); + + } + + public function updateCheckResultTableStructure(ScriptResultDataObject $scriptResult, string $checkScriptName): void + { + $scriptMessage = $scriptResult->getMessage(); + + if (!is_array($scriptMessage)) { + return; + } + + $tableName = $this->transformTableName($checkScriptName); + + $existingColumns = $this->getTableColumns($tableName); + + $qFields = $this->createFieldsSql($existingColumns, $scriptMessage); + + if (empty($existingColumns)) { + + $this->logger->info('Creating result table ' . $tableName); + + $q = 'CREATE TABLE ' . $tableName . ' ( + `id` INT AUTO_INCREMENT PRIMARY KEY, + `check_result_id` INT NOT NULL, + `created_at` DATETIME NOT NULL '; + + if (!empty($qFields)) { + $q .= ', ' . $qFields; + } + + $q .= ') ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + '; + + } else { + + $this->logger->info('Updating result table ' . $tableName); + + if ($qFields !== '') { + $q = 'ALTER TABLE ' . $tableName . ' ADD ' . $qFields; + } + + } + + if (!empty($q)) { + $em = $this->getEntityManager(); + $conn = $em->getConnection(); + $stmt = $conn->prepare($q); + $stmt->executeStatement(); + } + } + + /** + * @param array> $existingColumns + * @param array $scriptMessage + **/ + private function createFieldsSql(array $existingColumns, array $scriptMessage): string + { + $fields = []; + foreach ($scriptMessage as $key => $value) { + + // check for existing column + if (array_key_exists($key, $existingColumns)) { + continue; + } + + $mysqlType = MySqlTypeService::getType($value); + + $fields[$key] = sprintf('`%s` %s', $key, $mysqlType); + } + + $fields = array_unique($fields); + + return implode(',', $fields); + } + + /** @return array> */ + private function getTableColumns(string $tableName): array + { + $q = 'DESC ' . $tableName; + $stmt = $this->getEntityManager()->getConnection()->prepare($q); + + try { + $results = $stmt->executeQuery(); + } catch (Exception $e) { + return []; + } + + if ($results->rowCount() === 0) { + return []; + } + + return $results->fetchAllAssociativeIndexed(); + } + + private function transformTableName(string $checkScriptName): string + { + $name = StringDataTransformer::transformStringToLatin($checkScriptName); + + $tableName = strtolower(sprintf('csr_%s', $name)); + + if (strlen($tableName) > 64) { + throw new Exception('Result table name is too long. Cannot create a result table without a valid serviceCheckName. Make sure it is not longer than 56 characters.'); + } + + return $tableName; + } + + +} diff --git a/src/Repository/CheckScriptParameterRepository.php b/src/Repository/CheckScriptParameterRepository.php new file mode 100644 index 0000000..151bd6f --- /dev/null +++ b/src/Repository/CheckScriptParameterRepository.php @@ -0,0 +1,60 @@ + + * + * @method CheckScriptParameter|null find($id, $lockMode = null, $lockVersion = null) + * @method CheckScriptParameter|null findOneBy(array $criteria, array $orderBy = null) + * @method CheckScriptParameter[] findAll() + * @method CheckScriptParameter[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) + */ +class CheckScriptParameterRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, CheckScriptParameter::class); + } + + public function deleteParametersForCheckScript(CheckScript $checkScript): void + { + $conn = $this->getEntityManager()->getConnection(); + + $sql = 'DELETE FROM check_script_parameter WHERE check_script_id = :check_script_id'; + $stmt = $conn->prepare($sql); + + $stmt->bindValue('check_script_id', $checkScript->getId()); + $stmt->executeStatement(); + } + +// /** +// * @return CheckScriptParameter[] Returns an array of CheckScriptParameter objects +// */ +// public function findByExampleField($value): array +// { +// return $this->createQueryBuilder('c') +// ->andWhere('c.exampleField = :val') +// ->setParameter('val', $value) +// ->orderBy('c.id', 'ASC') +// ->setMaxResults(10) +// ->getQuery() +// ->getResult() +// ; +// } + +// public function findOneBySomeField($value): ?CheckScriptParameter +// { +// return $this->createQueryBuilder('c') +// ->andWhere('c.exampleField = :val') +// ->setParameter('val', $value) +// ->getQuery() +// ->getOneOrNullResult() +// ; +// } +} diff --git a/src/Service/Condition/ConditionService.php b/src/Service/Condition/ConditionService.php index 431281f..dab5f6e 100644 --- a/src/Service/Condition/ConditionService.php +++ b/src/Service/Condition/ConditionService.php @@ -6,47 +6,76 @@ namespace App\Service\Condition; +use App\Condition\AbstractCondition; use App\Condition\ConditionCollection; +use App\Condition\EqualsCondition; +use App\Condition\MinMaxCondition; use App\Repository\AssetGroupServiceCheckConditionRepository; use App\Repository\AssetRepository; +use App\Repository\AssetServiceCheckRepository; class ConditionService { public function __construct( private readonly AssetGroupServiceCheckConditionRepository $assetGroupServiceCheckConditionRepository, + private readonly AssetServiceCheckRepository $assetServiceCheckRepository, private readonly AssetRepository $assetRepository, ) {} - // TODO: inheritance from assets - public function getCheckConditions(int $assetId, int $serviceCheckId): ConditionCollection + public function getCheckConditions(int $assetId, int $assetServiceCheckId): ConditionCollection { - $asset = $this->assetRepository->find($assetId); - $assetGroups = $asset->getAssetGroups(); - - $ids = []; - foreach ($assetGroups as $assetGroup) { - $ids[] = $assetGroup->getId(); + $assetServiceCheck = $this->assetServiceCheckRepository->find($assetServiceCheckId); + if ($assetServiceCheck === null) { + throw new \Exception('Asset service check not found'); } - $conditions = $this->assetGroupServiceCheckConditionRepository->findBy([ - 'assetGroup' => $ids, - 'serviceCheck' => $serviceCheckId, - ]); + $conditions = $assetServiceCheck->getConditionCollection(); + + // if asset has no conditions for this service check, check asset groups + if ($conditions->getCount() === 0) { + $asset = $this->assetRepository->find($assetId); + $assetGroups = $asset->getAssetGroups(); + + $assetGroupIds = []; + foreach ($assetGroups as $assetGroup) { + $assetGroupIds[] = $assetGroup->getId(); + } + + $conditions = $this->assetGroupServiceCheckConditionRepository->findBy([ + 'assetGroup' => $assetGroupIds, + 'serviceCheck' => $assetServiceCheck->getServiceCheck()->getId(), + ]); + } if ($conditions === []) { - throw new \Exception('Could not find any conditions for assetId ' . $assetId . ' and serviceCheckId ' . $serviceCheckId); + return new ConditionCollection(); } if (count($conditions) > 1) { - throw new \Exception('Found more than one condition collection for assetId ' . $assetId . ' and serviceCheckId ' . $serviceCheckId . ' - this is not yet supported (which one has priority?)'); + throw new \Exception('Found more than one condition collection for assetId ' . $assetId . ' and assetServiceCheckId ' . $assetServiceCheckId . ' - this is not yet supported (which one has priority?)'); } $result = unserialize($conditions[0]->getConditions()); if ($result === false) { - throw new \Exception('Could not unserialize conditions for assetId ' . $assetId . ' and serviceCheckId ' . $serviceCheckId); + throw new \Exception('Could not unserialize conditions for assetId ' . $assetId . ' and assetServiceCheckId ' . $assetServiceCheckId); } return $result; } + + /** @return array */ + public function getAllAvailableConditions(): array + { + $conditions = []; + + // TODO: get them from filesystem + $availableConditions = [EqualsCondition::class, MinMaxCondition::class]; + + foreach ($availableConditions as $className) { + $conditions[$className] = new $className(); + } + + return $conditions; + } } diff --git a/src/Service/Converter/TimeConverter.php b/src/Service/Converter/TimeConverter.php new file mode 100644 index 0000000..e9871a4 --- /dev/null +++ b/src/Service/Converter/TimeConverter.php @@ -0,0 +1,32 @@ + $hours, + 'min' => $minutes, + 'sec' => $seconds, + 'ms' => $milliseconds, + ]; + + $timeParts = []; + + foreach ($sections as $name => $value){ + if ($value > 0){ + $timeParts[] = $value . ' ' . $name; + } + } + + return implode(', ', $timeParts); + } +} diff --git a/src/Service/DataTransformer/StringDataTransformer.php b/src/Service/DataTransformer/StringDataTransformer.php new file mode 100644 index 0000000..4febeb6 --- /dev/null +++ b/src/Service/DataTransformer/StringDataTransformer.php @@ -0,0 +1,27 @@ +getAssetGroup($id); + $assetGroupServiceCheckConditions = $this->assetGroupServiceCheckConditionRepository->findByAsCollection(['assetGroup' => $assetGroup], null, 'serviceCheckId'); + + $availableConditions = $this->conditionService->getAllAvailableConditions(); + return (new PageDataObject()) ->setTitle($title) ->addParameter('assetGroup', $assetGroup) + ->addParameter('availableConditions', $availableConditions) + ->addParameter('availableServiceChecks', $this->getAllServiceChecks()) + ->addParameter('assetGroupServiceCheckConditions', $assetGroupServiceCheckConditions) ; } @@ -37,4 +51,9 @@ private function getAssetGroup(int $id): ?AssetGroup { return $this->assetGroupRepository->find($id); } + + private function getAllServiceChecks(): DataObjectCollectionInterface + { + return $this->serviceCheckRepository->findAllAsCollection(); + } } diff --git a/src/Service/Page/AssetPageLoader.php b/src/Service/Page/AssetPageLoader.php index 9957cc3..80366de 100644 --- a/src/Service/Page/AssetPageLoader.php +++ b/src/Service/Page/AssetPageLoader.php @@ -13,6 +13,10 @@ use App\Entity\Asset; use App\Repository\AssetGroupRepository; use App\Repository\AssetRepository; +use App\Repository\AssetServiceCheckConditionRepository; +use App\Repository\AssetServiceCheckRepository; +use App\Repository\ServiceCheckRepository; +use App\Service\Condition\ConditionService; use Symfony\Component\HttpFoundation\Request; use Symfony\Contracts\Translation\TranslatorInterface; @@ -21,7 +25,10 @@ class AssetPageLoader public function __construct( private readonly TranslatorInterface $translator, private readonly AssetRepository $assetRepository, - private readonly AssetGroupRepository $assetGroupRepository + private readonly AssetGroupRepository $assetGroupRepository, + private readonly ServiceCheckRepository $serviceCheckRepository, + private readonly AssetServiceCheckRepository $assetServiceCheckRepository, + private readonly ConditionService $conditionService ) {} public function load(Request $request, Context $context, int $id = null): PageDataObjectInterface @@ -34,6 +41,9 @@ public function load(Request $request, Context $context, int $id = null): PageDa ->setTitle($title) ->addParameter('asset', $asset) ->addParameter('availableAssetGroups', $this->getAllAssetGroups()) + ->addParameter('availableServiceChecks', $this->getAllServiceChecks()) + ->addParameter('availableConditions', $this->conditionService->getAllAvailableConditions()) + ->addParameter('assetServiceCheckConditions', $this->getAssetServiceCheckConditions($asset)) ; } @@ -46,4 +56,15 @@ private function getAllAssetGroups(): DataObjectCollectionInterface { return $this->assetGroupRepository->findAllAsCollection(); } + + private function getAllServiceChecks(): DataObjectCollectionInterface + { + return $this->serviceCheckRepository->findAllAsCollection(); + } + + private function getAssetServiceCheckConditions(Asset $asset): DataObjectCollectionInterface + { + return $this->assetServiceCheckRepository->findByAsCollection(['asset' => $asset], null, 'serviceCheckId'); + } + } diff --git a/src/Service/Page/AssetServiceCheckPageLoader.php b/src/Service/Page/AssetServiceCheckPageLoader.php new file mode 100644 index 0000000..282de89 --- /dev/null +++ b/src/Service/Page/AssetServiceCheckPageLoader.php @@ -0,0 +1,50 @@ +translator->trans('title.asset-service-check.edit'); + + $assetServiceCheck = is_null($id) ? new AssetServiceCheck() : $this->getAssetServiceCheck($id); + + return (new PageDataObject()) + ->setTitle($title) + ->addParameter('assetServiceCheck', $assetServiceCheck) + ->addParameter('availableServiceChecks', $this->getAvailableServiceChecks()) + ->addParameter('assetId', $assetId) + ; + } + + private function getAssetServiceCheck(int $id): ?AssetServiceCheck + { + return $this->assetServiceCheckRepository->find($id); + } + + private function getAvailableServiceChecks(): DataObjectCollectionInterface + { + return $this->serviceCheckRepository->findAllAsCollection(); + } +} diff --git a/src/Service/Page/AssetServiceCheckParameterPageLoader.php b/src/Service/Page/AssetServiceCheckParameterPageLoader.php new file mode 100644 index 0000000..7178cb8 --- /dev/null +++ b/src/Service/Page/AssetServiceCheckParameterPageLoader.php @@ -0,0 +1,69 @@ +translator->trans('title.asset-service-check-parameters.edit'); + + $assetServiceCheck = $assetServiceCheckId == 0 ? new AssetServiceCheck() : $this->getAssetServiceCheck($assetServiceCheckId); + + $page = new PageDataObject(); + $page->setTitle($title); + + $serviceCheckId = $request->query->getInt('service-check'); + if ($serviceCheckId === 0) { + return $page; + } + + $serviceCheck = $this->getServiceCheck($serviceCheckId); + if ($serviceCheck === null) { + throw new \RuntimeException('Service check not found'); + } + + $checkScript = $serviceCheck->getCheckScript(); + if ($checkScript === null) { + throw new \RuntimeException('Check script not found'); + } + + $checkScriptParameter = $checkScript->getCheckScriptParameters(); + + $page->addParameter('checkScriptParameter', $checkScriptParameter); + $page->addParameter('assetServiceCheckConfig', $assetServiceCheck->getConfig()); + + return $page; + } + + private function getServiceCheck(int $id): ?ServiceCheck + { + return $this->serviceCheckRepository->find($id); + } + + private function getAssetServiceCheck(int $id): ?AssetServiceCheck + { + return $this->assetServiceCheckRepository->find($id); + } + +} diff --git a/src/Service/Page/CheckResultPageLoader.php b/src/Service/Page/CheckResultPageLoader.php new file mode 100644 index 0000000..64c9431 --- /dev/null +++ b/src/Service/Page/CheckResultPageLoader.php @@ -0,0 +1,47 @@ +translator->trans('title.check-result.detail'); + + $checkResult = is_null($id) ? new CheckResult() : $this->getCheckResult($id); + + if (!$checkResult instanceof CheckResult) { + throw new \Exception('Check result not found'); + } + + $checkResultDetails = $this->checkResultRepository->fetchDetailsByCheckResult($checkResult); + + return (new PageDataObject()) + ->setTitle($title) + ->addParameter('checkResult', $checkResult) + ->addParameter('checkResultDetails', $checkResultDetails) + ; + } + + private function getCheckResult(int $id): ?CheckResult + { + return $this->checkResultRepository->find($id); + } +} diff --git a/src/Service/Page/CheckScriptParameterPageLoader.php b/src/Service/Page/CheckScriptParameterPageLoader.php new file mode 100644 index 0000000..6080c7e --- /dev/null +++ b/src/Service/Page/CheckScriptParameterPageLoader.php @@ -0,0 +1,46 @@ +translator->trans('title.service-check-parameters.edit'); + + $checkScript = is_null($checkScriptId) ? new ServiceCheck() : $this->getCheckScript($checkScriptId); + + $page = new PageDataObject(); + $page->setTitle($title); + + $checkScriptParameter = $checkScript->getCheckScriptParameters(); + + $page->addParameter('checkScriptParameter', $checkScriptParameter); + + return $page; + } + + private function getCheckScript(int $id): ?CheckScript + { + return $this->checkScriptRepository->find($id); + } + +} diff --git a/src/Service/Page/ListingPageLoader.php b/src/Service/Page/ListingPageLoader.php index 63f2040..12bc317 100644 --- a/src/Service/Page/ListingPageLoader.php +++ b/src/Service/Page/ListingPageLoader.php @@ -22,19 +22,44 @@ public function load(Request $request, string $entityName, Context $context): Pa $limit = $context->getCurrentUser()->getRowLimit(); - $result = $repo->getListing([], null, null, $limit, $page); + $search = $request->query->getString('search'); + if (!empty($search)) { + $searches = json_decode($search, true, 512, JSON_THROW_ON_ERROR); + } else { + $searches = []; + } + + $order = $request->query->getString('order'); + if (!empty($order)) { + $orderBy = json_decode($order, true, 512, JSON_THROW_ON_ERROR); + } else { + $orderBy = null; + } + + $result = $repo->getListing($searches, $orderBy, null, $limit, $page); $pagination = $this->createPagination($limit, $result->getTotalCount(), $page, $entityName); $title = $this->translator->trans('title.' . $entityName . '.listing'); - return (new ListingPageDataObject()) + $listingPageDataObject = (new ListingPageDataObject()) ->setEntityName($entityName) ->setPage($page) ->setPagination($pagination) ->setResult($result) ->setTitle($title) ; + + $this->addQueryParametersToPage($request, $listingPageDataObject); + + return $listingPageDataObject; + } + + private function addQueryParametersToPage(Request $request, PageDataObjectInterface $page): void + { + foreach ($request->query->all() as $key => $value) { + $page->addParameter($key, $value); + } } private function createPagination(int $limit, int $total, int $currentPage, string $entityName): PaginationDataObject diff --git a/src/Service/SchedulerService.php b/src/Service/SchedulerService.php index 46984e3..f0c956c 100644 --- a/src/Service/SchedulerService.php +++ b/src/Service/SchedulerService.php @@ -8,9 +8,13 @@ use App\Entity\Asset; use App\Entity\AssetGroup; +use App\Entity\AssetServiceCheck; use App\Entity\ServiceCheck; use App\Entity\ServiceCheckWorkerStats; use App\Message\CheckNotification; +use App\Repository\AssetGroupRepository; +use App\Repository\AssetRepository; +use App\Repository\ServiceCheckWorkerStatsRepository; use Doctrine\ORM\EntityManagerInterface; use Psr\Log\LoggerInterface; use Symfony\Component\Messenger\MessageBusInterface; @@ -20,17 +24,40 @@ class SchedulerService public function __construct( private readonly EntityManagerInterface $em, private readonly MessageBusInterface $bus, + private readonly AssetGroupRepository $assetGroupRepository, + private readonly AssetRepository $assetRepository, + private readonly ServiceCheckWorkerStatsRepository $serviceCheckWorkerStatsRepository, private readonly LoggerInterface $logger ) {} public function run(): void { - /* @var AssetGroupRepository $assetGroupRepo */ - $assetGroupRepo = $this->em->getRepository(AssetGroup::class); + $this->runAssetChecks(); + $this->runAssetGroupChecks(); + } - $this->em->getRepository(Asset::class); + private function runAssetChecks(): void + { + $assets = $this->assetRepository->findAll(); - $assetGroups = $assetGroupRepo->findAll(); + /** @var Asset $asset */ + foreach ($assets as $asset) { + $checks = $asset->getServiceChecks(); + + /** @var AssetServiceCheck $check */ + foreach ($checks as $check) { + if ($this->isCheckScheduled($asset, $check)) { + $this->runCheck($asset, $check); + } else { + $this->logger->info(sprintf('Check %s is not scheduled for asset %s', $check->getId(), $asset->getId())); + } + } + } + } + + private function runAssetGroupChecks(): void + { + $assetGroups = $this->assetGroupRepository->findAll(); /** @var AssetGroup $assetGroup */ foreach ($assetGroups as $assetGroup) { @@ -50,41 +77,61 @@ public function run(): void } } - private function runCheck(Asset $asset, ServiceCheck $check): void + /** + * TODO: parameters: this is confusing - ServiceCheck would be from AssetGroups (should we support this?) + * maybe we just support checks directly on the assets as those would need to be configured anyway + * - besides ping4/ping6 - which we could support for internal host online checks + */ + private function runCheck(Asset $asset, ServiceCheck|AssetServiceCheck $check): void { - $checkScript = $check->getCheckScript(); + if ($check instanceof AssetServiceCheck) { + $serviceCheck = $check->getServiceCheck(); + } else { + $serviceCheck = $check; + } + + + if (!$serviceCheck instanceof ServiceCheck) { + $this->logger->warning(sprintf('Service check not found for check %s', $check->getId())); + return; + } + + $this->logger->info(sprintf('Running check %s (%s) on %s (%d)', + $check->getName(), + $serviceCheck->getName(), + $asset->getHostname(), + $asset->getId()) + ); + + $checkScript = $serviceCheck->getCheckScript(); $message = new CheckNotification( $asset->getId(), $check->getId(), + $serviceCheck->getId(), $asset->getHostname(), $asset->getIpv4Address(), $asset->getIpv6Address(), - $checkScript->getFilename() - ); - - $this->logger->info(sprintf('Scheduling check %s on %s (%d) using script %s', - $check->getName(), - $asset->getHostname(), - $message->getAssetId(), - $checkScript->getFilename()) + $checkScript->getFilename(), + $check->getConfig() ); $this->bus->dispatch($message); } - private function isCheckScheduled(Asset $asset, ServiceCheck $check): bool + private function isCheckScheduled(Asset $asset, ServiceCheck|AssetServiceCheck $check): bool { - $serviceCheckWorkerStatsRepo = $this->em->getRepository(ServiceCheckWorkerStats::class); - $serviceCheckWorkerStats = $serviceCheckWorkerStatsRepo->findOneBy([ + $serviceCheckWorkerStats = $this->serviceCheckWorkerStatsRepository->findOneBy([ 'asset' => $asset, 'serviceCheck' => $check, ]); + $this->logger->warning('todo: check if check is actually scheduled'); + if ($serviceCheckWorkerStats === null) { return true; } - dd('TODO: check if check is scheduled'); + return false; } } diff --git a/src/Service/Scripts/MetaDataService.php b/src/Service/Scripts/MetaDataService.php index b3a055e..eb38c9e 100644 --- a/src/Service/Scripts/MetaDataService.php +++ b/src/Service/Scripts/MetaDataService.php @@ -7,14 +7,22 @@ namespace App\Service\Scripts; use App\DataObject\Scripts\MetaDataObject; +use App\Entity\CheckScriptParameter; use App\Exception\MetaDataNotFoundException; +use Psr\Log\LoggerInterface; use Symfony\Component\Filesystem\Exception\FileNotFoundException; class MetaDataService { public const MAX_LINES_TO_READ = 20; + public const VALID_DATATYPES = ['string', 'int', 'float', 'bool']; + private string $commentStartsWith = '#'; + public function __construct( + private readonly LoggerInterface $logger + ) { } + /** * @param array $validKeys */ @@ -49,13 +57,52 @@ public function extractMetaDataFromFile(string $filename, array $validKeys): Met throw new MetaDataNotFoundException(null, 0, null, $filename); } + $parameters = $this->parseParameterString($metaData['parameters']); + return (new MetaDataObject()) ->setFilename($filename) ->setName($metaData['name']) ->setDescription($metaData['desc']) + ->setParameters($parameters) ; } + /** @return array */ + private function parseParameterString(string $parameterString): array + { + $parameters = []; + $parameterString = trim($parameterString); + + $parts = explode(',', $parameterString); + + // parameterString is 'parameter1, parameter2' + foreach ($parts as $part) { + $part = trim($part); + $parts2 = explode('<', $part); + if (count($parts2) !== 2) { + continue; + } + $key = trim($parts2[0]); + $type = trim($parts2[1]); + $type = str_replace('>', '', $type); + + if (!in_array($type, self::VALID_DATATYPES, true)) { + $this->logger->warning(sprintf('Parameter %s has invalid type %s', $key, $type)); + continue; + } + + $checkScriptParameter = new CheckScriptParameter(); + $checkScriptParameter->setName($key); + $checkScriptParameter->setDataType($type); + + $parameters[] = $checkScriptParameter; + + } + + return $parameters; + + } + /** * @param array $validKeys * diff --git a/src/Service/Scripts/ResultParserService.php b/src/Service/Scripts/ResultParserService.php index 5d8c691..2befbcf 100644 --- a/src/Service/Scripts/ResultParserService.php +++ b/src/Service/Scripts/ResultParserService.php @@ -6,69 +6,59 @@ namespace App\Service\Scripts; -use App\Condition\AbstractCondition; use App\Condition\ConditionCollection; use App\DataObject\ScriptResultDataObject; use App\Exception\ArrayIsNullException; use App\Exception\MissingKeyException; -use App\Exception\ODataStringNotFoundException; +use App\Service\DataTransformer\StringDataTransformer; class ResultParserService { - public const ODATA_STRING = 'ODATA:'; - /** @return array */ public function extractJson(string $result): array { - if (!stristr($result, self::ODATA_STRING)) { - throw new ODataStringNotFoundException('Result does not start with ODATA: string'); - } - - $jsonString = substr($result, strpos($result, self::ODATA_STRING) + strlen(self::ODATA_STRING)); - - $array = json_decode($jsonString, true, 512, JSON_THROW_ON_ERROR); + $array = json_decode($result, true, 512, JSON_THROW_ON_ERROR); if ($array === null) { - throw new ArrayIsNullException(sprintf('Could not decode json string: %s', $jsonString)); + throw new ArrayIsNullException(sprintf('Could not decode json string: %s', $result)); } - return $this->convertAllKeysToLowerCase($array); + return $this->transformAllKeys($array); } - /** - * @param array $scriptResult - */ - public function parseResultJson(array $scriptResult, ConditionCollection $conditions): ScriptResultDataObject + public function parseResultJson(ScriptResultDataObject $scriptResult, ConditionCollection $conditions): ScriptResultDataObject { - $result = new ScriptResultDataObject(); - $result->setMessage($scriptResult); + $scriptResult->setMessage($scriptResult->getScriptOutput()); + + foreach ($conditions->getConditions() as $conditionCollectionItem) { - /** @var AbstractCondition $condition */ - foreach ($conditions->getConditions() as $key => $condition) { - $value = $scriptResult[$key] ?? null; + $condition = $conditionCollectionItem->getCondition(); + $oDataKey = $conditionCollectionItem->getName(); + + $value = $scriptResult[$oDataKey] ?? null; if ($value !== null) { if ($condition->checkIfOk($value)) { - $result->setCheckResult(ScriptResultDataObject::RESULT_OK); + $scriptResult->setCheckResult(ScriptResultDataObject::RESULT_OK); } else { - $result->setCheckResult(ScriptResultDataObject::RESULT_ERROR); + $scriptResult->setCheckResult(ScriptResultDataObject::RESULT_ERROR); } if ($condition->checkIfWarn($value)) { - $result->setCheckResult(ScriptResultDataObject::RESULT_WARNING); + $scriptResult->setCheckResult(ScriptResultDataObject::RESULT_WARNING); } - $result->setNote(sprintf('"%s" is %s', $key, $value)); + $scriptResult->setNote(sprintf('"%s" is %s', $oDataKey, $value)); } else { - throw new MissingKeyException(sprintf('Key "%s" not found in the script return result: %s', $key, json_encode($scriptResult, JSON_THROW_ON_ERROR))); + throw new MissingKeyException(sprintf('Key "%s" not found in the script return result: %s', $oDataKey, json_encode($scriptResult, JSON_THROW_ON_ERROR))); } // if a check fails, we can stop here - if ($result->getCheckResult() !== ScriptResultDataObject::RESULT_OK) { + if ($scriptResult->getCheckResult() !== ScriptResultDataObject::RESULT_OK) { break; } } - return $result; + return $scriptResult; } /** @@ -76,10 +66,11 @@ public function parseResultJson(array $scriptResult, ConditionCollection $condit * * @return array */ - private function convertAllKeysToLowerCase(array $array): array + private function transformAllKeys(array $array): array { $result = []; foreach ($array as $key => $value) { + $key = StringDataTransformer::transformStringToLatin($key); $result[strtolower($key)] = $value; } diff --git a/src/Service/Scripts/ScriptRunnerService.php b/src/Service/Scripts/ScriptRunnerService.php new file mode 100644 index 0000000..79a9232 --- /dev/null +++ b/src/Service/Scripts/ScriptRunnerService.php @@ -0,0 +1,110 @@ +setCheckResult(ScriptResultDataObject::RESULT_UNKNOWN); + + $scriptPath = $this->parameterBag->get('kernel.project_dir') . '/' . $message->getCheckScriptFilename(); + if (!file_exists($scriptPath)) { + $errorMessage = sprintf('Script %s does not exist.', $scriptPath); + $result->setNote($errorMessage); + $this->logger->error($errorMessage); + + return $result; + } + + $process = $this->runProcess($scriptPath, $message); + $result->setExecutedCommand($process->getCommandLine()); + + if (!$process->isSuccessful()) { + $errorMessage = sprintf('Script %s failed with error: %s.', $scriptPath, $process->getErrorOutput()); + $result->setNote($errorMessage); + $result->setCheckResult(ScriptResultDataObject::RESULT_ERROR); + $result->setDurationMs($this->getScriptRuntimeInMs($startTime)); + $this->logger->error($errorMessage); + + return $result; + } + + $result->setRawScriptOutput($process->getOutput()); + + try { + $jsonResponse = $this->resultParserService->extractJson($result->getRawScriptOutput()); + $result->setScriptOutput($jsonResponse); + } catch (\Exception $e) { + $errorMessage = sprintf('Script %s failed with error: %s.', $scriptPath, $e->getMessage()); + $result->setNote($errorMessage); + $result->setCheckResult(ScriptResultDataObject::RESULT_ERROR); + $result->setDurationMs($this->getScriptRuntimeInMs($startTime)); + $this->logger->error($errorMessage); + } + + $result->setDurationMs($this->getScriptRuntimeInMs($startTime)); + + return $result; + } + + private function getScriptRuntimeInMs(float $startTimeMs): float + { + return (microtime(true) - $startTimeMs) * 1000; + } + + private function runProcess(string $scriptPath, CheckNotification $message): Process + { + $command = sprintf('%s \'%s\'', + $scriptPath, + $this->createJsonArguments($message), + ); + + $this->logger->notice(sprintf('CMD: %s', $command)); + + $process = Process::fromShellCommandline($command); + $process->setTimeout(self::PROCESS_MAX_RUNTIME_SECONDS); + $process->run(); + + return $process; + } + + private function createJsonArguments(CheckNotification $message): string + { + $arguments = [ + 'hostname' => $message->getHostname(), + 'ipv4' => $message->getIpv4Address(), + 'ipv6' => $message->getIpv6Address() + ]; + + $config = $message->getConfig(); + if (isset($config['checkScriptParameter']) && !empty($config['checkScriptParameter'])) { + $arguments = array_merge($arguments, $config['checkScriptParameter']); + } + + return json_encode($arguments); + } + +} diff --git a/src/Service/Scripts/ScriptsService.php b/src/Service/Scripts/ScriptsService.php index d3239a0..2f51ae9 100644 --- a/src/Service/Scripts/ScriptsService.php +++ b/src/Service/Scripts/ScriptsService.php @@ -9,7 +9,9 @@ use App\DataObject\Collection\DataObjectCollection; use App\DataObject\Collection\DataObjectCollectionInterface; use App\Entity\CheckScript; +use App\Repository\CheckScriptParameterRepository; use App\Repository\CheckScriptRepository; +use Doctrine\Common\Collections\ArrayCollection; use Doctrine\ORM\EntityManagerInterface; use Psr\Log\LoggerInterface; use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface; @@ -24,6 +26,7 @@ class ScriptsService public const VALID_METADATA_KEYS = [ 'name', 'desc', + 'parameters' ]; public function __construct( @@ -32,6 +35,8 @@ public function __construct( private readonly MetaDataService $metaDataService, private readonly HashService $hashService, private readonly EntityManagerInterface $em, + private readonly CheckScriptRepository $checkScriptRepository, + private readonly CheckScriptParameterRepository $checkScriptParameterRepository, private readonly LoggerInterface $logger ) {} @@ -95,8 +100,11 @@ public function getAllScriptsFromFilesystem(): DataObjectCollectionInterface $dir = $this->parameterBag->get('kernel.project_dir') . '/' . $this->checkScriptsPath; foreach (glob($dir . '/*') as $script) { + + $this->logger->debug('Checking script: ' . $script); + if ($this->isValidScript($script) === false) { - $this->logger->warning('Invalid script found: ' . $script); + $this->logger->warning('Skipping invalid script: ' . $script); $this->logger->warning('Valid script extensions are: ' . implode(', ', self::VALID_SCRIPT_EXTENSIONS)); continue; } @@ -118,6 +126,12 @@ public function getAllScriptsFromFilesystem(): DataObjectCollectionInterface ->setFilehash($filehash) ; + foreach ($metaData->getParameters() as $parameter) { + $parameter->setCheckScript($scriptObj); + $scriptObj->addCheckScriptParameter($parameter); + } + + $scripts[] = $scriptObj; } @@ -126,10 +140,7 @@ public function getAllScriptsFromFilesystem(): DataObjectCollectionInterface public function getAllScripts(): DataObjectCollectionInterface { - /** @var CheckScriptRepository $checkScriptRepository */ - $checkScriptRepository = $this->em->getRepository(CheckScript::class); - - $dbCheckScripts = $checkScriptRepository->findAllAsCollection(); + $dbCheckScripts = $this->checkScriptRepository->findAllAsCollection(); $filesystemCheckScripts = $this->getAllScriptsFromFilesystem(); $scripts = []; @@ -153,19 +164,20 @@ public function getAllScripts(): DataObjectCollectionInterface private function upsertCheckScripts(DataObjectCollectionInterface $scripts): void { - $checkScriptRepository = $this->em->getRepository(CheckScript::class); - /** @var CheckScript $script */ foreach ($scripts as $script) { $relativePath = str_replace($this->parameterBag->get('kernel.project_dir') . '/', '', $script->getFilename()); - $checkScript = $checkScriptRepository->findOneBy(['filename' => $relativePath]); + $checkScript = $this->checkScriptRepository->findOneBy(['filename' => $relativePath]); if ($checkScript instanceof CheckScript) { if ($script->getFilehash() === $checkScript->getFilehash()) { $this->logger->debug('Script contents have not changed, skipping: ' . $relativePath); continue; } + + $this->checkScriptParameterRepository->deleteParametersForCheckScript($checkScript); + $this->logger->info('Updating script ' . $relativePath); } else { $checkScript = new CheckScript(); @@ -177,6 +189,10 @@ private function upsertCheckScripts(DataObjectCollectionInterface $scripts): voi $checkScript->setDescription($script->getDescription()); $checkScript->setFilehash($script->getFilehash()); + foreach ($script->getCheckScriptParameters() as $parameter) { + $checkScript->addCheckScriptParameter($parameter); + } + $this->em->persist($checkScript); } @@ -187,6 +203,10 @@ private function isValidScript(string $scriptFilename): bool { $pathInfo = pathinfo($scriptFilename); + if (empty($pathInfo['extension'])) { + return false; + } + return in_array($pathInfo['extension'], self::VALID_SCRIPT_EXTENSIONS); } } diff --git a/src/Twig/TimeConverterExtension.php b/src/Twig/TimeConverterExtension.php new file mode 100644 index 0000000..f1fc94c --- /dev/null +++ b/src/Twig/TimeConverterExtension.php @@ -0,0 +1,30 @@ +millisecondsToTime(...)), + ]; + } + + public function millisecondsToTime(?float $ms = null): string + { + if ($ms === null) { + return ''; + } + + return TimeConverter::millisecondsToTime($ms); + } +} diff --git a/symfony.lock b/symfony.lock index 6692ab7..6ad0c03 100644 --- a/symfony.lock +++ b/symfony.lock @@ -47,6 +47,19 @@ ".php-cs-fixer.dist.php" ] }, + "php-amqplib/rabbitmq-bundle": { + "version": "2.13", + "recipe": { + "repo": "github.com/symfony/recipes-contrib", + "branch": "main", + "version": "1.12", + "ref": "ce3ca2e4577270c4f518539aa6b9e878998fcfef" + }, + "files": [ + "config/packages/old_sound_rabbit_mq.yaml", + "src/Consumer/.gitignore" + ] + }, "phpstan/phpstan": { "version": "1.10", "recipe": { diff --git a/templates/condition-builder/index.html.twig b/templates/condition-builder/index.html.twig new file mode 100644 index 0000000..86be1b6 --- /dev/null +++ b/templates/condition-builder/index.html.twig @@ -0,0 +1,36 @@ +{% block condition_builder %} +{% set name = 'condition[' ~ serviceCheck.id ~ '][' ~ conditionIndex ~ ']' %} +{% set parametersId = 'condition-' ~ serviceCheck.id ~ '-' ~ conditionIndex ~ '-parameters' %} +{% set odataKey = selectedCondition is defined ? selectedCondition.getName : '' %} + +
+
+ +
+
+ for script json result key: +
+
+
+
+
+ {% if selectedCondition is defined %} + {% include 'condition-builder/parameters.html.twig' with { + 'serviceCheckId': serviceCheck.id, + 'conditionIndex': conditionIndex, + 'condition': selectedCondition.condition + } %} + + {% endif %} +
+
+
+{% endblock %} diff --git a/templates/condition-builder/parameters.html.twig b/templates/condition-builder/parameters.html.twig new file mode 100644 index 0000000..b1c220f --- /dev/null +++ b/templates/condition-builder/parameters.html.twig @@ -0,0 +1,11 @@ +{% block condition_builder_parameters %} + +{% for parameter in condition.getParameters %} +{% set name = 'condition[' ~ serviceCheckId ~ '][' ~ conditionIndex ~ '][' ~ parameter ~ ']' %} + +
+ + +
+{% endfor %} +{% endblock %} diff --git a/templates/detail/check-result.html.twig b/templates/detail/check-result.html.twig new file mode 100644 index 0000000..935427d --- /dev/null +++ b/templates/detail/check-result.html.twig @@ -0,0 +1,68 @@ +{% set hx = app.request.get('hx') %} +{% extends hx == null ? 'base.html.twig' : 'htmx.html.twig' %} + +{% set checkResult = page.parameters.get('checkResult') %} +{% set checkResultDetails = page.parameters.get('checkResultDetails') %} + +{% block title %}{{ 'title.check-result.detail'|trans }}{% endblock %} + +{% block page_title %}{{ 'title.check-result.detail'|trans }}{% endblock %} + +{% block page_heading %}{{ 'title.check-result.detail'|trans }}{% endblock %} + +{% block page_content %} +{% block detail_check_result %} +
+
+ +
+
+ {{ dump(checkResult) }} + + + + + + + + + + + + + + + + + + + + + + + +
{{ 'label.key'|trans }}{{ 'label.value'|trans }}
ID{{ checkResult.id }}
{{ 'label.check-result.result'|trans }}{{ checkResult.result }}
{{ 'label.createdAt'|trans }}{{ checkResult.createdAt|date('d.m.Y H:i:s') }}
{{ 'label.duration'|trans }}{{ checkResult.durationMs|millisecondsToTime }}
+ +
+
+ +
+ +
+
+ {{ dump(checkResultDetails) }} +

{{ 'title.check-result.result'|trans }}

+
+
+ +
+ +
+ +
+ + +
+
+{% endblock %} +{% endblock %} diff --git a/templates/edit/asset-group.html.twig b/templates/edit/asset-group.html.twig index 820e52e..3cf6dd2 100644 --- a/templates/edit/asset-group.html.twig +++ b/templates/edit/asset-group.html.twig @@ -2,6 +2,9 @@ {% extends hx == null ? 'base.html.twig' : 'htmx.html.twig' %} {% set assetGroup = page.parameters.get('assetGroup') %} +{% set availableConditions = page.parameters.get('availableConditions') %} +{% set availableServiceChecks = page.parameters.get('availableServiceChecks') %} +{% set assetGroupServiceCheckConditions = page.parameters.get('assetGroupServiceCheckConditions') %} {% block page_title %}{{ 'title.asset-group.edit'|trans }}{% endblock %} @@ -23,13 +26,24 @@
- -
TODO: SERVICE CHECKS PICKER?
- maybe we just enable addding service checks to asset groups in /service-checks/edit,
- because they would need to be configurable -
+ + +
+ {{ include('partials/service-check-conditions.html.twig', { + 'type': 'asset-group', + 'entity': assetGroup, + 'availableConditions': availableConditions, + 'availableServiceChecks': availableServiceChecks, + 'selectedServiceCheckConditions': assetGroupServiceCheckConditions, + }) }} +
diff --git a/templates/edit/asset-service-check-parameter.html.twig b/templates/edit/asset-service-check-parameter.html.twig new file mode 100644 index 0000000..e1a60e5 --- /dev/null +++ b/templates/edit/asset-service-check-parameter.html.twig @@ -0,0 +1,16 @@ +{% set hx = app.request.get('hx') %} + +{% set availableServiceChecks = page.parameters.get('availableServiceChecks') %} +{% set assetServiceCheck = page.parameters.get('assetServiceCheck') %} +{% set assetId = page.parameters.get('assetId') %} +{% set checkScriptParameter = page.parameters.get('checkScriptParameter') %} +{% set assetServiceCheckConfig = page.parameters.get('assetServiceCheckConfig') %} + +{% block page_title %}{% endblock %} +{% block page_heading %}{% endblock %} + +{% block page_content %} +{% block edit_service_check_parameter %} + {% include 'partials/check-script-parameter.html.twig' with { checkScriptParameter: checkScriptParameter } %} +{% endblock %} +{% endblock %} diff --git a/templates/edit/asset-service-check.html.twig b/templates/edit/asset-service-check.html.twig new file mode 100644 index 0000000..784a2cb --- /dev/null +++ b/templates/edit/asset-service-check.html.twig @@ -0,0 +1,87 @@ +{% set hx = app.request.get('hx') %} + +{% set availableServiceChecks = page.parameters.get('availableServiceChecks') %} +{% set assetServiceCheck = page.parameters.get('assetServiceCheck') %} +{% set assetId = page.parameters.get('assetId') %} + +{% block page_title %}{{ 'title.asset-service-check.edit'|trans }}{% endblock %} +{% block page_heading %}{{ 'title.asset-service-check.edit'|trans }}{% endblock %} + +{% block page_content %} +{% block edit_service_check %} +
+ +
+ +
+ + + {% if errors.asset_service_check is defined %} +
{{ errors.asset_service_check.text|trans }}
+ {% endif %} +
+ +
+ + + {% if errors.name is defined %} +
{{ errors.name.text|trans }}
+ {% endif %} +
+ +
+
+
+ +
+ +
+ CONDITIONS for this check +
+
+ + {# +
+ + +
+ #} + +
+ +
+ + + {% set search = [{ + 'field': 'asset', + 'operator': 'equals', + 'value': assetId + }] %} + +
+ +
+ +
+{% endblock %} +{% endblock %} diff --git a/templates/edit/asset.html.twig b/templates/edit/asset.html.twig index 46348f7..3167260 100644 --- a/templates/edit/asset.html.twig +++ b/templates/edit/asset.html.twig @@ -3,6 +3,9 @@ {% set asset = page.parameters.get('asset') %} {% set availableAssetGroups = page.parameters.get('availableAssetGroups') %} +{% set availableServiceChecks = page.parameters.get('availableServiceChecks') %} +{% set availableConditions = page.parameters.get('availableConditions') %} +{% set assetServiceCheckConditions = page.parameters.get('assetServiceCheckConditions') %} {% block title %}{{ 'title.asset.edit'|trans }}{% endblock %} @@ -12,8 +15,6 @@ {% block page_content %} {% block edit_asset %} - -
@@ -59,7 +60,27 @@ {% endfor %} +
+
+
+ + + {% block asset_service_checks %} + {% set search = [{ + 'field': 'asset', + 'operator': 'equals', + 'value': asset.id + }] %} +
+
+ {% endblock %} + +

diff --git a/templates/edit/check-script-parameter.html.twig b/templates/edit/check-script-parameter.html.twig new file mode 100644 index 0000000..fcd8de3 --- /dev/null +++ b/templates/edit/check-script-parameter.html.twig @@ -0,0 +1,12 @@ +{% set hx = app.request.get('hx') %} + +{% set checkScriptParameter = page.parameters.get('checkScriptParameter') %} + +{% block page_title %}{% endblock %} +{% block page_heading %}{% endblock %} + +{% block page_content %} +{% block edit_service_check_parameter %} + {% include 'partials/check-script-parameter.html.twig' with { checkScriptParameter: checkScriptParameter } %} +{% endblock %} +{% endblock %} diff --git a/templates/edit/check-script.html.twig b/templates/edit/check-script.html.twig index a7cfc1f..dd7eef0 100644 --- a/templates/edit/check-script.html.twig +++ b/templates/edit/check-script.html.twig @@ -18,7 +18,10 @@
- + +

{{ 'help.check-script.name'|trans }}

@@ -54,6 +57,40 @@
+ +
+ +
+
+ +

{{ 'title.check-script.test'|trans }}

+ +
+
+ +
+ + + +
+ +
+ +
+
+
+ +
{% endblock %} {% endblock %} diff --git a/templates/listing/asset-group-items.html.twig b/templates/listing/asset-group-items.html.twig index 8487eb3..882d5d7 100644 --- a/templates/listing/asset-group-items.html.twig +++ b/templates/listing/asset-group-items.html.twig @@ -18,7 +18,7 @@ - {% include 'listing/pagination.html.twig' with { 'pagination': page.pagination } %} + {% include 'listing/pagination.html.twig' with { 'target': '#asset-group-listing-body', 'pagination': page.pagination } %} diff --git a/templates/listing/asset-group.html.twig b/templates/listing/asset-group.html.twig index cfeaafc..1b0e92e 100644 --- a/templates/listing/asset-group.html.twig +++ b/templates/listing/asset-group.html.twig @@ -20,10 +20,10 @@
- +
- + diff --git a/templates/listing/asset-items.html.twig b/templates/listing/asset-items.html.twig index 30fbd03..4f28363 100644 --- a/templates/listing/asset-items.html.twig +++ b/templates/listing/asset-items.html.twig @@ -9,6 +9,7 @@ + diff --git a/templates/listing/asset-service-check-items.html.twig b/templates/listing/asset-service-check-items.html.twig new file mode 100644 index 0000000..3fe6d34 --- /dev/null +++ b/templates/listing/asset-service-check-items.html.twig @@ -0,0 +1,30 @@ +{% block listing_table_asset_service_check_items %} + +{% set result = page.result %} +{% for row in result %} +{% set url = path('edit_asset_service_check', {'assetId': assetId, 'id': row.id}) %} +{% set hxUrl = url ~ '?hx=1' %} + + + + + + + + +{% endfor %} + + + + + + +{% endblock %} diff --git a/templates/listing/asset-service-check.html.twig b/templates/listing/asset-service-check.html.twig new file mode 100644 index 0000000..c0d04ab --- /dev/null +++ b/templates/listing/asset-service-check.html.twig @@ -0,0 +1,32 @@ +{% block page_heading_wrapper %}{% endblock %} + +{% set assetId = page.parameters.get('assetId') %} + +{% block asset_service_checks %} +
{{ 'label.name'|trans }}{{ 'label.serviceChecks'|trans }}{{ 'label.service-checks'|trans }} {{ 'label.createdAt'|trans }} {{ 'label.actions'|trans }}
{{ row.ipv4Address }} {{ row.ipv6Address }} {% for assetGroup in row.assetGroups %}{{ assetGroup.name }}{% endfor %}{% for serviceCheck in row.serviceChecks %}{{ serviceCheck.name }}{% endfor %} {{ row.createdAt|date('d.m.Y H:i:s') }} - {% include 'listing/pagination.html.twig' with { 'pagination': page.pagination } %} + {% include 'listing/pagination.html.twig' with { 'target': '#asset-listing-body', 'pagination': page.pagination } %}
{{ row.name }}{{ row.serviceCheck.name }}conditions todoconfig todo{{ row.createdAt|date('d.m.Y H:i:s') }}{{ 'label.edit'|trans }}
+ todo: pagination here does not work correctly + {% include 'listing/pagination.html.twig' with { 'target': '#asset-service-check-listing-body', 'pagination': page.pagination } %} +
+ + + + + + + + + + + {% include 'listing/asset-service-check-items.html.twig' %} + + +
{{ 'label.name'|trans }}{{ 'label.service-check'|trans }}{{ 'label.number-of-conditions'|trans }}{{ 'label.config'|trans }}{{ 'label.createdAt'|trans }}{{ 'label.actions'|trans }}
+
+ + +{% endblock %} diff --git a/templates/listing/asset.html.twig b/templates/listing/asset.html.twig index 85d2404..6cd6d6b 100644 --- a/templates/listing/asset.html.twig +++ b/templates/listing/asset.html.twig @@ -20,13 +20,14 @@
- +
+ diff --git a/templates/listing/check-result-items.html.twig b/templates/listing/check-result-items.html.twig new file mode 100644 index 0000000..198864b --- /dev/null +++ b/templates/listing/check-result-items.html.twig @@ -0,0 +1,27 @@ +{% block listing_table_asset %} +{% set result = page.result %} +{% for row in result %} +{% set url = path('detail_check_result', {'id': row.id}) %} +{% set hxUrl = url ~ '?hx=1' %} + + + + + + + + +{% endfor %} + + + + + +{% endblock %} diff --git a/templates/listing/check-result.html.twig b/templates/listing/check-result.html.twig new file mode 100644 index 0000000..76c3802 --- /dev/null +++ b/templates/listing/check-result.html.twig @@ -0,0 +1,39 @@ +{% set hx = app.request.get('hx') %} +{% extends hx is null ? 'base.html.twig' : 'htmx.html.twig' %} + +{% block page_heading_wrapper %}{% endblock %} + +{% block page_content %} +
+
+
+
{% if page.title is defined %}{{ page.title }}{% endif %}
+
+
+
{{ 'label.hostname'|trans }} {{ 'label.name'|trans }} {{ 'label.ipv4Address'|trans }} {{ 'label.ipv6Address'|trans }} {{ 'label.assetGroups'|trans }}{{ 'label.service-checks'|trans }} {{ 'label.createdAt'|trans }} {{ 'label.actions'|trans }}
{{ row.asset.hostname }}{{ row.serviceCheck.name }}{{ row.result }}{{ row.durationMs|millisecondsToTime }}{{ row.createdAt|date('d.m.Y H:i:s') }}{{ 'label.details'|trans }}
+ {% include 'listing/pagination.html.twig' with { 'target': '#check-result-listing-body', 'pagination': page.pagination } %} +
+ + + + + + + + + + {% include 'listing/check-result-items.html.twig' %} + +
{{ 'label.hostname'|trans }}{{ 'label.service-name'|trans }}{{ 'label.result'|trans }}{{ 'label.check-duration'|trans }}{{ 'label.createdAt'|trans }}{{ 'label.actions'|trans }}
+
+ + + + + +{% endblock %} diff --git a/templates/listing/check-script-items.html.twig b/templates/listing/check-script-items.html.twig index 670114b..28f610d 100644 --- a/templates/listing/check-script-items.html.twig +++ b/templates/listing/check-script-items.html.twig @@ -18,7 +18,7 @@ - {% include 'listing/pagination.html.twig' with { 'pagination': page.pagination } %} + {% include 'listing/pagination.html.twig' with { 'target': '#check-script-listing-body', 'pagination': page.pagination } %} diff --git a/templates/listing/check-script.html.twig b/templates/listing/check-script.html.twig index 7240760..db645c8 100644 --- a/templates/listing/check-script.html.twig +++ b/templates/listing/check-script.html.twig @@ -13,14 +13,14 @@