From 2f25b1baf40f08ec7fde3e30c509934e51892acc Mon Sep 17 00:00:00 2001 From: Tristan Cavelier <tristan.cavelier@tiolive.com> Date: Wed, 7 Oct 2015 09:07:23 +0000 Subject: [PATCH] monitor2: Add stack/monitor2 More information available on the README file. --- stack/monitor2/README.md | 244 ++++++++++++++ stack/monitor2/buildout.cfg | 234 ++++++++++++++ stack/monitor2/cgi-httpd.conf.in | 85 +++++ stack/monitor2/default-promise-interface.html | 221 +++++++++++++ stack/monitor2/index.html | 10 + stack/monitor2/instance-monitor.cfg.jinja2.in | 297 ++++++++++++++++++ stack/monitor2/make-rss.sh.in | 8 + stack/monitor2/monitor-httpd-promise.conf.in | 4 + stack/monitor2/monitor-httpd.conf.in | 105 +++++++ stack/monitor2/monitor-logout.html | 26 ++ stack/monitor2/monitor-logout.py.cgi | 2 + stack/monitor2/monitor-run-promise.py.cgi | 20 ++ stack/monitor2/monitor-service-run.in | 37 +++ stack/monitor2/monitor.cfg.in | 290 +++++++++++++++++ stack/monitor2/monitor.conf.in | 4 + stack/monitor2/monitor.css | 50 +++ stack/monitor2/monitor.js.in | 173 ++++++++++ stack/monitor2/monitor.py.in | 141 +++++++++ stack/monitor2/run-promise.py | 60 ++++ stack/monitor2/status2rss.py | 59 ++++ .../webfile-directory/ansible-report.cgi.in | 0 stack/monitor2/webfile-directory/index.cgi.in | 190 +++++++++++ .../webfile-directory/index.html.jinja2 | 35 +++ .../webfile-directory/monitor-password.cgi.in | 29 ++ .../webfile-directory/settings.cgi.in | 64 ++++ .../static/monitor-register.js | 17 + .../webfile-directory/static/pure-min.css | 11 + .../webfile-directory/static/script.js | 35 +++ .../webfile-directory/static/style.css | 31 ++ .../webfile-directory/static/welcome.html | 11 + .../webfile-directory/status-history.cgi.in | 44 +++ .../monitor2/webfile-directory/status.cgi.in | 57 ++++ stack/monitor2/wrapper.in | 2 + 33 files changed, 2596 insertions(+) create mode 100644 stack/monitor2/README.md create mode 100644 stack/monitor2/buildout.cfg create mode 100644 stack/monitor2/cgi-httpd.conf.in create mode 100644 stack/monitor2/default-promise-interface.html create mode 100644 stack/monitor2/index.html create mode 100644 stack/monitor2/instance-monitor.cfg.jinja2.in create mode 100644 stack/monitor2/make-rss.sh.in create mode 100644 stack/monitor2/monitor-httpd-promise.conf.in create mode 100644 stack/monitor2/monitor-httpd.conf.in create mode 100644 stack/monitor2/monitor-logout.html create mode 100644 stack/monitor2/monitor-logout.py.cgi create mode 100644 stack/monitor2/monitor-run-promise.py.cgi create mode 100644 stack/monitor2/monitor-service-run.in create mode 100644 stack/monitor2/monitor.cfg.in create mode 100644 stack/monitor2/monitor.conf.in create mode 100644 stack/monitor2/monitor.css create mode 100644 stack/monitor2/monitor.js.in create mode 100644 stack/monitor2/monitor.py.in create mode 100644 stack/monitor2/run-promise.py create mode 100644 stack/monitor2/status2rss.py create mode 100644 stack/monitor2/webfile-directory/ansible-report.cgi.in create mode 100755 stack/monitor2/webfile-directory/index.cgi.in create mode 100644 stack/monitor2/webfile-directory/index.html.jinja2 create mode 100644 stack/monitor2/webfile-directory/monitor-password.cgi.in create mode 100755 stack/monitor2/webfile-directory/settings.cgi.in create mode 100644 stack/monitor2/webfile-directory/static/monitor-register.js create mode 100644 stack/monitor2/webfile-directory/static/pure-min.css create mode 100644 stack/monitor2/webfile-directory/static/script.js create mode 100644 stack/monitor2/webfile-directory/static/style.css create mode 100644 stack/monitor2/webfile-directory/static/welcome.html create mode 100644 stack/monitor2/webfile-directory/status-history.cgi.in create mode 100755 stack/monitor2/webfile-directory/status.cgi.in create mode 100644 stack/monitor2/wrapper.in diff --git a/stack/monitor2/README.md b/stack/monitor2/README.md new file mode 100644 index 000000000..2044a626b --- /dev/null +++ b/stack/monitor2/README.md @@ -0,0 +1,244 @@ +Monitor +======= + +This stack has for purpose to know if all promises went/are ok. + +It provides a web interface, to see which promises failed. It also provide a rss +feed to easily know the actual state of your instance, and to know when it +started to went bad. + +THIS STACK IS A KIND OF FORK OF THE `stack/monitor`. THIS ONE WAS CREATED AS A +REDESIGNED ONE TO REMOVE UNWANTED FEATURES AND TO GO FURTHER TO THE GOOD DESIGN +DIRECTION. PLEASE, DO NOT USE THE OLD MONITORING INTERFACE OR ONLY FOR BACKWARD +COMPATIBILITY REASON. + +Summary: + +- Activate monitoring for you software +- Add a monitor promise +- Information about URL access +- Monitor promise configuration example +- Promise requirements +- monitor.haljson example +- monitor.conf example + + +Activate monitoring for your software +------------------------------------- + +You just have to extend the monitor stack from your software.cfg. + +You can also create a new buildout which extends your software, and the +monitoring stack: + + [buildout] + extends = + monitor_url + my_software_url + +In your instance.cfg, your publish section should be named `[publish]` in order +to extends the one of the monitoring stack. + +Then, in the same file you can configure the monitor by adding this section: + + [monitor-instance-parameter] + monitor-httpd-ipv6 = ... + monitor-httpd-port = ... + monitor-title = ... + + +Add a monitor promise +--------------------- + +For instance, we want to create a promise for KVM log parsing. Add these +sections in its instance.cfg: + + [directory] + monitor-promise = ${:etc}/monitor-promise + + [kvm-log-parser-promise] + recipe = .... + filename = kvm-log-parser + rendered = ${directory:monitor-promise}/${:filename} + mode = 0755 + + [buildout] + parts += kvm-log-parser-promise + +We can optionaly add promise title: + + [kvm-log-parser-promise-parameter] + # fill with -> see "Service config example" below + title = Kvm log parse + + [kvm-log-parser-promise-cfg] + recipe = slapos.recipe.template:jinja2 + rendered = ${directory:monitor-promise}/${kvm-log-parser-promise:filename}.cfg + template = service.cfg + context = section parameter_dict kvm-log-parser-promise-parameter + + [buildout] + parts += kvm-log-parser-promise-cfg + +... and optionaly a specific frequency: + + [kvm-log-parser-promise-parameter] + frequency = */5 * * * * + +Optionaly, we also want a custom interface: + + [directory] + kvm-log-parser-promise-interface-dir = ....../interface + + [kvm-log-parser-promise-parameter] + private-path-list += ${directory:kvm-log-parser-promise-interface-dir} + + [kvm-log-parser-promise-interface] + recipe = .... + rendered = ${directory:kvm-log-parser-interface-dir}/index.html + + [buildout] + parts += kvm-log-parser-promise-interface + +service.cfg: + + [service] + {% for key, value in parameter_dict.items() -%} + {{ key }} = {{ dumps(value) }} + {% endfor -%} + + +Information about URL access +---------------------------- + +Open HTTP GET on static files, open HTTP POST on cgi + + GET <root_monitor>/ // classical monitoring interface + GET <root_monitor>/monitor.haljson // monitor conf + GET <root_monitor>/public/<service>.status.json // service status json + +Example for KVM log parsing promise + + GET <kvm_monitor>/monitor.haljson + GET <kvm_monitor>/public/kvm-log-parser.status.json + GET <kvm_monitor>/public/kvm-log-parser/index.html + POST <kvm_monitor>/cgi-bin/monitor-run-promise.cgi?service=kvm-log-parse // rerun the promise + + +Information about internal file tree +------------------------------------ + +Tree for monitor runtime: + + etc/monitor.conf // generated by slapos + etc/cron.d/monitor // generated by slapos + bin/monitor.py // generated by slapos + srv/monitor/web/index.html // static + srv/monitor/web/monitor.css // static + srv/monitor/web/monitor.js // static + srv/monitor/web/monitor.haljson // generated by monitor.py + srv/monitor/public/.... // generated by monitor.py + srv/monitor/private/.... // generated by monitor.py + +Example for KVM log parsing promise + + etc/monitor-promise/kvm-log-parse.cfg // generated by slapos (kvm-log-parser-promise) + etc/monitor-promise/kvm-log-parse // generated by slapos (kvm-log-parser-promise) + var/kvm-log-parser-promise/interface/index.html // generated by slapos (kvm-log-parser-promise) + var/log/kvm.log // generated by kvm + var/log/kvm-log-parse-last-report.csv // generated by kvm-log-parse + srv/monitor/public/kvm-log-parse.status.json // generated by kvm-log-parse (indirectly by the monitor promise executor) + srv/monitor/public/kvm-log-parse/kvm.log -> var/log/kvm.log // generated by monitor.py + srv/monitor/public/kvm-log-parse/kvm-log-parse-last-report.csv -> var/log/kvm-log-parse-last-report.csv // genareted by monitor.py + srv/monitor/private/kvm-log-parse/interface -> var/kvm-log-parser-promise/interface // generated by monitor.py + + +Monitor promise config example +------------------------------ + +Example for KVM log parsing promise + + # etc/monitor-promise/kvm-log-parse.cfg + [service] + title = Kvm log parse + frequency = <Cron Syntax> + public-path-list = $instance/var/log/kvm.log # automatically symlink to srv/monitor/public/$service/ + private-path-list = $instance/var/log # automatically symlink to srv/monitor/private/$service/ + +On cron, the command will be something like: + + ${service:frequency} ${monitor:promise-executor-path} '${monitor:service-pid-folder}/${service:name}.pid' '${service:status-path}' '${promise_path}' + +and "monitor:promise-executor-path" is a script that would run a promise if not +already on going (see `run-promise.py`). + +TODO cron accepts 999 characters maximum for a command, so we should reduce the +size of the cron command + +TODO put `run-promise.py` in the software + + +Promise requirements +-------------------- + +A promise should check something (like web page is well cached, there's not too +much slow queries, ...): + +- MUST output the status.json in stdout +- SHOULD output on stdout +- MUST return 0 if status is good else != 0 +- the status.json MUST contain "message" (string) which explains why the status is OK or bad + + +monitor.haljson example +----------------------- + + { + "_links": { + "related_monitor": [ + { "href": "<url>/static" }, + { "href": "http://my.other.monitor" } + ] + }, + "_embedded": { + "service": [ + { + "_links": { + "status": { "href": "<url>/kvm-log-parse/status.json" }, + "interface": { "href": "<url>/kvm-log-parse/index.html" } + }, + "title": "KVM log parse", + "id": "kvm-log-parse" + }, + { + "_links": { + "status": { "href": "<url>/<service>/status.json" }, + "interface": { "href": "<url>/<service>/index.html" } + }, + "title": "Service name", + "id": "<service>" + } + ] + }, + "title": "KVM Monitoring interface" + } + + +monitor.conf example +-------------------- + + [monitor] + title = KVM Monitoring interface + monitor-hal-json = $instance/srv/monitor/web/monitor.haljson + public-folder = $instance/srv/monitor/public + private-folder = $instance/srv/monitor/private + web-folder = $instance/srv/monitor/web + cgi-folder = $instance/srv/monitor/cgi-bin + service-pid-folder = $instance/var/monitor/service-pid + public-path-list = + $instance/var/log + private-path-list = + $instance/srv/backup/log_rotate + monitor-url-list = + https://[...]/ + https://[...]/ diff --git a/stack/monitor2/buildout.cfg b/stack/monitor2/buildout.cfg new file mode 100644 index 000000000..a2f40ca45 --- /dev/null +++ b/stack/monitor2/buildout.cfg @@ -0,0 +1,234 @@ +[buildout] +# XXX THIS STACK IS A KIND OF FORK OF `stack/monitor`. THIS ONE WAS +# CREATED AS A REDESIGNED ONE TO REMOVE UNWANTED FEATURES AND +# TO GO FURTHER TO THE GOOD DESIGN DIRECTION. SEE THE README FOR +# MORE INFORMATION. + +extends = + ../../component/apache/buildout.cfg + ../../component/curl/buildout.cfg + ../../component/dash/buildout.cfg + ../../component/dcron/buildout.cfg + ../../component/openssl/buildout.cfg + +parts += + slapos-cookbook + dcron + monitor-eggs + extra-eggs + monitor-conf + monitor-bin + monitor-web-index-html + monitor-web-monitor-css + monitor-web-monitor-js + monitor-web-monitor-logout-cgi + monitor-web-monitor-logout-page + monitor-template + rss-bin + +[monitor-download-base] +recipe = hexagonit.recipe.download +download-only = true +url = ${:_profile_base_location_}/${:filename} +mode = 0644 + +[monitor-eggs] +recipe = zc.recipe.egg +eggs = + collective.recipe.template + cns.recipe.symlink + +[extra-eggs] +recipe = zc.recipe.egg +interpreter = pythonwitheggs +eggs = + PyRSS2Gen + Jinja2 + +[make-rss-script] +recipe = slapos.recipe.template +url = ${:_profile_base_location_}/make-rss.sh.in +md5sum = 98c8f6fd81e405b0ad10db07c3776321 +output = ${buildout:directory}/template-make-rss.sh.in +mode = 0644 + +[monitor-conf] +<= monitor-download-base +filename = monitor.conf.in +md5sum = 2db5c08c7e8658981b4b1e3f27fd5967 + +[monitor-bin] +<= monitor-download-base +filename = monitor.py.in +md5sum = 2484cb185c391890a05db26c2163af8e + +[monitor-web-default-promise-interface] +<= monitor-download-base +filename = default-promise-interface.html +md5sum = 29c899529f4539c9dd1432907b37b7b1 + +[monitor-web-index-html] +<= monitor-download-base +filename = index.html +md5sum = 262db07691c145301252a49b6b51d11d + +[monitor-web-monitor-css] +<= monitor-download-base +filename = monitor.css +md5sum = a18ab932e5e2e656995f47c7d4a7853a + +[monitor-web-monitor-js] +<= monitor-download-base +filename = monitor.js.in +md5sum = 8bc4b8368a752f90da2571866768e81f + +[monitor-web-monitor-logout-cgi] +recipe = slapos.recipe.template:jinja2 +filename = monitor-logout.py.cgi +md5sum = 5b3c0aa559722a3bae5a692ea9a0a441 +mode = 0755 +template = ${:_profile_base_location_}/${:filename} +rendered = ${buildout:directory}/monitor-logout.cgi +context = key python_executable buildout:executable + +[monitor-web-monitor-logout-page] +<= monitor-download-base +filename = monitor-logout.html +md5sum = b210c6842df541305d299081bc1bf81e + +[monitor-web-monitor-promise-runner-cgi] +<= monitor-download-base +filename = monitor-run-promise.py.cgi +md5sum = 15625e5bf6c1b57b9199250951ffc16e + +[monitor-template] +recipe = slapos.recipe.template:jinja2 +filename = template-monitor.cfg +template = ${:_profile_base_location_}/instance-monitor.cfg.jinja2.in +rendered = ${buildout:directory}/template-monitor.cfg +md5sum = fde8b1c9ff04c64a3b7bb0ae11ffe0c5 +context = + key apache_location apache:location + key gzip_location gzip:location + raw monitor_bin ${monitor-bin:location}/${monitor-bin:filename} + raw monitor_conf_template ${monitor-conf:location}/${monitor-conf:filename} + raw monitor_web_default_promise_interface ${monitor-web-default-promise-interface:location}/${monitor-web-default-promise-interface:filename} + raw monitor_web_index_html ${monitor-web-index-html:location}/${monitor-web-index-html:filename} + raw monitor_web_monitor_css ${monitor-web-monitor-css:location}/${monitor-web-monitor-css:filename} + key monitor_web_monitor_logout_cgi monitor-web-monitor-logout-cgi:rendered + raw monitor_web_monitor_logout_page ${monitor-web-monitor-logout-page:location}/${monitor-web-monitor-logout-page:filename} + raw monitor_web_monitor_promise_runner_cgi ${monitor-web-monitor-promise-runner-cgi:location}/${monitor-web-monitor-promise-runner-cgi:filename} + raw monitor_web_monitor_js ${monitor-web-monitor-js:location}/${monitor-web-monitor-js:filename} + raw curl_executable_location ${curl:location}/bin/curl + raw dash_executable_location ${dash:location}/bin/dash + raw dcron_executable_location ${dcron:location}/sbin/crond + raw logrotate_executable_location ${logrotate:location}/usr/sbin/logrotate + raw monitor_httpd_promise_conf ${monitor-httpd-promise-conf:location}/${monitor-httpd-promise-conf:filename} + raw monitor_httpd_template ${monitor-httpd-conf:location}/${monitor-httpd-conf:filename} + raw monitor_service_run ${monitor-service-template-run:location}/${monitor-service-template-run:filename} + raw openssl_executable_location ${openssl:location}/bin/openssl + raw python_executable ${buildout:executable} + raw promise_executor_py ${run-promise-py:location}/${run-promise-py:filename} + raw template_wrapper ${template-wrapper:output} + +[monitor-httpd-conf] +<= monitor-download-base +md5sum = 625d3d948c0af7b4848d7fad92bfb844 +filename = monitor-httpd.conf.in + +[monitor-httpd-promise-conf] +<= monitor-download-base +filename = monitor-httpd-promise.conf.in +md5sum = 5913d2a0096b50537f394a49b762b3e5 + +[monitor-service-template-run] +<= monitor-download-base +md5sum = d5f29fa859a45696e1ff1bb174ab1111 +filename = monitor-service-run.in + +[run-promise-py] +<= monitor-download-base +filename = run-promise.py +md5sum = 6db26ce13becf8a190e34c14cb8b6f9f + +[monitor-httpd-template] +<= monitor-download-base +md5sum = 93e1dda50cb71bfe29966b2946c02dd1 +filename = cgi-httpd.conf.in + +[index] +recipe = hexagonit.recipe.download +url = ${:_profile_base_location_}/webfile-directory/${:filename} +download-only = true +md5sum = e759977b21c70213daa4c2701f2c2078 +destination = ${buildout:parts-directory}/monitor-index +filename = index.cgi.in +mode = 0644 + +[index-template] +recipe = hexagonit.recipe.download +url = ${:_profile_base_location_}/webfile-directory/${:filename} +download-only = true +destination = ${buildout:parts-directory}/monitor-template-index +md5sum = 7400c8cfa16a15a0d41f512b8bbb1581 +filename = index.html.jinja2 +mode = 0644 + +[status-cgi] +recipe = hexagonit.recipe.download +url = ${:_profile_base_location_}/webfile-directory/${:filename} +download-only = true +md5sum = e43d79bec8824265e22df7960744113a +destination = ${buildout:parts-directory}/monitor-template-status-cgi +filename = status.cgi.in +mode = 0644 + +[status-history-cgi] +recipe = hexagonit.recipe.download +url = ${:_profile_base_location_}/webfile-directory/${:filename} +download-only = true +#md5sum = 4fb26753ee669b8ac90ffe33dbd12e8f +destination = ${buildout:parts-directory}/monitor-template-status-history-cgi +filename = status-history.cgi.in +mode = 0644 + +[settings-cgi] +recipe = hexagonit.recipe.download +url = ${:_profile_base_location_}/webfile-directory/${:filename} +download-only = true +md5sum = b4cef123a3273e848e8fe496e22b20a8 +destination = ${buildout:parts-directory}/monitor-template-settings-cgi +filename = settings.cgi.in +mode = 0644 + +[monitor-password-cgi] +recipe = hexagonit.recipe.download +url = ${:_profile_base_location_}/webfile-directory/${:filename} +download-only = true +md5sum = c7ba7ecb09d0d1d24e7cb73a212cc33f +destination = ${buildout:parts-directory}/monitor-template-monitor-password-cgi +filename = monitor-password.cgi.in +mode = 0644 + +[rss-bin] +recipe = hexagonit.recipe.download +url = ${:_profile_base_location_}/${:filename} +download-only = true +md5sum = 6c84a826778cb059754623f39b33651b +destination = ${buildout:parts-directory}/monitor-template-rss-bin +filename = status2rss.py +mode = 0644 + +[dcron-service] +recipe = slapos.recipe.template +url = ${template-dcron-service:output} +output = $${directory:services}/crond +mode = 0700 +logfile = $${directory:log}/crond.log + +[template-wrapper] +recipe = slapos.recipe.template +url = ${:_profile_base_location_}/wrapper.in +output = ${buildout:directory}/template-wrapper.cfg +mode = 0644 +md5sum = 8cde04bfd0c0e9bd56744b988275cfd8 diff --git a/stack/monitor2/cgi-httpd.conf.in b/stack/monitor2/cgi-httpd.conf.in new file mode 100644 index 000000000..d08b8dc4f --- /dev/null +++ b/stack/monitor2/cgi-httpd.conf.in @@ -0,0 +1,85 @@ +PidFile "{{ httpd_configuration.get('pid-file') }}" + +StartServers 1 +ServerLimit 1 +ThreadLimit 4 +ThreadsPerChild 4 + +ServerName example.com +ServerAdmin someone@email +<IfDefine !MonitorPort> +Listen [{{ httpd_configuration.get('listening-ip') }}]:{{ monitor_parameters.get('port') }} +Define MonitorPort +</IfDefine> +DocumentRoot "{{ directory.get('www') }}" +ErrorLog "{{ httpd_configuration.get('error-log') }}" +LoadModule unixd_module modules/mod_unixd.so +LoadModule access_compat_module modules/mod_access_compat.so +LoadModule authz_core_module modules/mod_authz_core.so +LoadModule authn_core_module modules/mod_authn_core.so +LoadModule authz_host_module modules/mod_authz_host.so +LoadModule mime_module modules/mod_mime.so +LoadModule cgid_module modules/mod_cgid.so +LoadModule dir_module modules/mod_dir.so +LoadModule ssl_module modules/mod_ssl.so +LoadModule alias_module modules/mod_alias.so +LoadModule autoindex_module modules/mod_autoindex.so +LoadModule auth_basic_module modules/mod_auth_basic.so +LoadModule authz_user_module modules/mod_authz_user.so +LoadModule authn_file_module modules/mod_authn_file.so +LoadModule proxy_module modules/mod_proxy.so +LoadModule proxy_http_module modules/mod_proxy_http.so +LoadModule rewrite_module modules/mod_rewrite.so + +# SSL Configuration +<IfDefine !SSLConfigured> +Define SSLConfigured +SSLCertificateFile {{ httpd_configuration.get('certificate') }} +SSLCertificateKeyFile {{ httpd_configuration.get('key') }} +SSLRandomSeed startup builtin +SSLRandomSeed connect builtin +SSLRandomSeed startup /dev/urandom 256 +SSLRandomSeed connect builtin +SSLProtocol -ALL +SSLv3 +TLSv1 +SSLHonorCipherOrder On +SSLCipherSuite RC4-SHA:HIGH:!ADH +</IfDefine> +SSLEngine On +ScriptSock {{ httpd_configuration.get('cgid-pid-file') }} +<Directory {{ directory.get('www') }}> + SSLVerifyDepth 1 + SSLRequireSSL + SSLOptions +StrictRequire + # XXX: security???? + Options +ExecCGI + AddHandler cgi-script .cgi + DirectoryIndex {{ monitor_parameters.get('index-filename') }} +</Directory> +Alias /private/ {{ directory.get('private-directory') }}/ +<Directory {{ directory.get('private-directory') }}> +Order Deny,Allow +Deny from env=AUTHREQUIRED +<Files ".??*"> + Order Allow,Deny + Deny from all +</Files> +AuthType Basic +AuthName "Private access" +AuthUserFile "{{ monitor_parameters.get('htaccess-file') }}" +Require valid-user +Options Indexes FollowSymLinks +Satisfy all +</Directory> + +<Location /rewrite> +AuthType Basic +AuthName "Private access" +AuthUserFile "{{ monitor_parameters.get('htaccess-file') }}" +Require valid-user +</Location> + +ProxyVia On +RewriteEngine On +{% for key, value in monitor_rewrite_rule.iteritems() %} +RewriteRule ^/rewrite/{{ key }}($|/.*) {{ value }}/$1 [P,L] +{% endfor %} diff --git a/stack/monitor2/default-promise-interface.html b/stack/monitor2/default-promise-interface.html new file mode 100644 index 000000000..df79f582c --- /dev/null +++ b/stack/monitor2/default-promise-interface.html @@ -0,0 +1,221 @@ +<!DOCTYPE html> +<html> + <head> + <title>Promise status</title> + <script> + + function getServiceName() { + var match = /(?:&|\?)service_name=([^&]*)/.exec(location.search); + if (match) { + return match[1]; + } + throw new Error("no service name found"); + } + + var service_name = getServiceName(), + monitor_json_url = "/monitor.haljson", + status_json_url = "/public/" + service_name + ".status.json", + rerun_cgi_url = "/cgi-bin/monitor-run-promise.cgi?service=" + service_name; + + function newDeferred() { + var d = { + "promise": undefined, + "resolve": undefined, + "reject": undefined + }; + d.promise = new Promise(function (resolve, reject) { + d.resolve = resolve; + d.reject = reject; + }); + return d; + } + + function xhr(param) { + /*global XMLHttpRequest */ + var d = newDeferred(), xhr = new XMLHttpRequest(), k, i, l, a; + d.promise.cancel = function () { xhr.abort(); }; + xhr.open((param.method || "GET").toUpperCase(), param.url, true); + xhr.responseType = param.responseType || ""; + if (param.withCredentials !== undefined) { + xhr.withCredentials = param.withCredentials; + } + if (param.headers) { + a = Object.keys(param.headers); + l = a.length; + for (i = 0; i < l; i += 1) { + k = a[i]; + xhr.setRequestHeader(k, param.headers[k]); + } + } + xhr.addEventListener("load", function (e) { + var r, t = e.target, callback; + if (param.noStatusCheck) { + d.resolve(t); + } else if (t.status < 400) { + d.resolve(t); + } else { + d.reject(new Error("HTTP: " + (t.status ? t.status + " " : "") + (t.statusText || "Unknown"))); + } + }, false); + xhr.addEventListener("error", function (e) { + return d.reject(new Error("HTTP: Error")); + }, false); + xhr.addEventListener("abort", function (e) { + return d.reject(new Error("HTTP: Aborted")); + }, false); + xhr.send(param.data); + return d.promise; + } + + function unexpectedError(reason) { + console.error(reason); + alert(reason); + } + + function PromiseStatusInterface(config) { + var it = this, + statusP = document.createElement("p"), + descriptionH2 = document.createElement("h2"), + descriptionP = document.createElement("p"), + errorH2 = document.createElement("h2"), + errorPre = document.createElement("pre"), + header = document.createElement("header"), + h1 = document.createElement("h1"), + h2 = document.createElement("h2"), + a = document.createElement("a"), + button = document.createElement("button"); + + this.element = config.rootElement || document.createElement("div"); + this.statusP = statusP; + this.descriptionP = descriptionP; + this.errorH2 = errorH2; + this.errorPre = errorPre; + + this.element.appendChild(header); + header.appendChild(a); + a.setAttribute("tabindex", "-1"); + a.setAttribute("href", "/"); + a.appendChild(button); + button.textContent = "Home"; + + a = document.createElement("a"); + button = document.createElement("button"); + header.appendChild(a); + a.setAttribute("tabindex", "-1"); + a.setAttribute("href", ""); + a.appendChild(button); + button.textContent = "Refresh"; + + button = document.createElement("button"); + header.appendChild(button); + button.textContent = "Run promise now"; + button.onclick = function () { + this.runPromiseNow(); + }.bind(this); + this.runPromiseNowButton = button; + + this.element.appendChild(h1); + h1.textContent = "Promise status"; + + this.element.appendChild(statusP); + this.element.appendChild(descriptionH2); + descriptionH2.textContent = "Description"; + this.element.appendChild(descriptionP); + this.element.appendChild(errorH2); + errorH2.textContent = "Error output"; + errorH2.style.display = "none"; + this.element.appendChild(errorPre); + errorPre.style.display = "none"; + + this.loadStatusUi(); + this.loadDescriptionUi(); + this.loadErrorUi(); + } + PromiseStatusInterface.prototype.loadStatusJson = function () { + if (this.status_json_promise) { return; } + this.status_json_promise = Promise.resolve().then(function () { + return xhr({url: status_json_url, withCredentials: true, responseType: "json"}); + }).then(function (xhr) { + return xhr.response; + }); + this.status_json_promise.catch(function () { return; }).then(function () { + setTimeout(function () { + delete this.status_json_promise; + }.bind(this), 1000); + }.bind(this)); + return this.status_json_promise; + }; + PromiseStatusInterface.prototype.loadStatusUi = function () { + this.loadStatusJson(); + this.statusP.textContent = "Loading status..."; + return this.status_json_promise.then(function (status_json) { + if (status_json.status === "OK") { + this.statusP.textContent = "Status: OK."; + } else { + this.statusP.textContent = "Status: BAD (" + status_json.status + ")."; + } + if (status_json.message) { + this.statusP.appendChild(document.createTextNode(" " + status_json.message)); + } + }.bind(this), function (reason) { + var message = reason && (reason.target && (reason.target.statusText || "Unknown") || reason.message); + this.statusP.textContent = "Status Json Error: " + (message || "Unknown error"); + }.bind(this)).catch(unexpectedError); + }; + PromiseStatusInterface.prototype.loadDescriptionUi = function () { + this.loadStatusJson(); + this.descriptionP.textContent = "Loading description..."; + return this.status_json_promise.then(function (status_json) { + if (status_json.description) { + this.descriptionP.textContent = status_json.description; + } else { + this.descriptionP.textContent = "No description"; + } + }.bind(this), function (reason) { + var message = reason && (reason.target && (reason.target.statusText || "Unknown") || reason.message); + this.descriptionP.textContent = "Status Json Error: " + (message || "Unknown error"); + }.bind(this)).catch(unexpectedError); + }; + PromiseStatusInterface.prototype.loadErrorUi = function () { + this.loadStatusJson(); + this.errorPre.textContent = "Loading error output..."; + return this.status_json_promise.then(function (status_json) { + if (status_json.error) { + this.errorH2.style.display = ""; + this.errorPre.style.display = ""; + this.errorPre.textContent = status_json.error; + } else { + this.errorH2.style.display = "none"; + this.errorPre.style.display = "none"; + this.errorPre.textContent = ""; + } + }.bind(this), function (reason) { + var message = reason && (reason.target && (reason.target.statusText || "Unknown") || reason.message); + this.errorPre.textContent = "Status Json Error: " + (message || "Unknown error"); + }.bind(this)).catch(unexpectedError); + }; + PromiseStatusInterface.prototype.runPromiseNow = function () { + this.runPromiseNowButton.disabled = true; + var original_text = this.runPromiseNowButton.textContent; + this.runPromiseNowButton.textContent = "Sending message..."; + return Promise.resolve().then(function () { + return xhr({url: rerun_cgi_url, method: "POST", withCredentials: true}); + }).catch(unexpectedError).then(function () { + this.runPromiseNowButton.textContent = original_text; + }.bind(this)); + }; + + /*global setTimeout */ + setTimeout(function () { + /*global document */ + document.body.innerHTML = ""; + return new PromiseStatusInterface({rootElement: document.body}); + }); + + </script> + </head> + <body> + <h1>Promise status</h1> + <noscript>Javascript should be enabled</noscript> + </body> +</html> diff --git a/stack/monitor2/index.html b/stack/monitor2/index.html new file mode 100644 index 000000000..91a95e31d --- /dev/null +++ b/stack/monitor2/index.html @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<html> + <head> + <link rel="stylesheet" href="monitor.css" /> + <script src="monitor.js"></script> + </head> + <body> + <noscript>Please enable javascript on your browser to make this application to work.</noscript> + </body> +</html> diff --git a/stack/monitor2/instance-monitor.cfg.jinja2.in b/stack/monitor2/instance-monitor.cfg.jinja2.in new file mode 100644 index 000000000..ff0855ffa --- /dev/null +++ b/stack/monitor2/instance-monitor.cfg.jinja2.in @@ -0,0 +1,297 @@ +[cron] +recipe = slapos.cookbook:cron +cron-entries = ${logrotate-directory:cron-entries} +dcrond-binary = {{ dcron_executable_location }} +crontabs = ${logrotate-directory:crontabs} +cronstamps = ${logrotate-directory:cronstamps} +catcher = ${cron-simplelogger:wrapper} +binary = ${logrotate-directory:services}/crond + +[cron-simplelogger] +recipe = slapos.cookbook:simplelogger +wrapper = ${logrotate-directory:bin}/cron_simplelogger +log = ${logrotate-directory:log}/cron.log + +[logrotate] +recipe = slapos.cookbook:logrotate +logrotate-entries = ${logrotate-directory:logrotate-entries} +backup = ${logrotate-directory:logrotate-backup} +logrotate-binary = {{ logrotate_executable_location }} +gzip-binary = {{ gzip_location }}/bin/gzip +gunzip-binary = {{ gzip_location }}/bin/gunzip +wrapper = ${logrotate-directory:bin}/logrotate +conf = ${logrotate-directory:etc}/logrotate.conf +state-file = ${logrotate-directory:srv}/logrotate.status + +[cron-entry-logrotate] +recipe = slapos.cookbook:cron.d +cron-entries = ${cron:cron-entries} +name = logrotate +frequency = 0 0 * * * +command = ${logrotate:wrapper} + +# Add log to cron +[cron-simplelogger] +recipe = slapos.cookbook:simplelogger +wrapper = ${monitor-directory:bin}/cron_simplelogger +log = ${monitor-directory:log}/cron.log + +[directory] +recipe = slapos.cookbook:mkdirectory +etc = ${buildout:directory}/etc +bin = ${buildout:directory}/bin +srv = ${buildout:directory}/srv +var = ${buildout:directory}/var +run = ${:var}/run +log = ${:var}/log +scripts = ${:etc}/run +services = ${:etc}/service +promises = ${:etc}/promise +monitor = ${:srv}/monitor +monitor-promise = ${:etc}/monitor-promise + +[monitor-directory] +recipe = slapos.cookbook:mkdirectory +bin = ${directory:bin} +etc = ${directory:etc} +run = ${directory:monitor}/run +#run = ${directory:scripts} +pids = ${directory:run}/monitor +cgi-bin = ${directory:monitor}/cgi-bin +public = ${directory:monitor}/public +private = ${directory:monitor}/private +services = ${directory:services} +services-conf = ${directory:etc}/monitor.conf.d +www = ${directory:monitor}/web +web-dir = ${directory:monitor}/web +log = ${directory:log}/monitor +promise-wrapper = ${directory:var}/monitor-promise-wrapper + +[logrotate-directory] +recipe = slapos.cookbook:mkdirectory +cron-entries = ${:etc}/cron.d +cronstamps = ${:etc}/cronstamps +crontabs = ${:etc}/crontabs +logrotate-backup = ${:backup}/logrotate +logrotate-entries = ${:etc}/logrotate.d +bin = ${buildout:directory}/bin +srv = ${buildout:directory}/srv +backup = ${:srv}/backup +etc = ${buildout:directory}/etc +services = ${:etc}/service +log = ${buildout:directory}/var/log + +[ca-directory] +recipe = slapos.cookbook:mkdirectory +root = ${directory:srv}/ssl +requests = ${:root}/requests +private = ${:root}/private +certs = ${:root}/certs +newcerts = ${:root}/newcerts +crl = ${:root}/crl + +[certificate-authority] +recipe = slapos.cookbook:certificate_authority +openssl-binary = {{ openssl_executable_location }} +ca-dir = ${ca-directory:root} +requests-directory = ${ca-directory:requests} +wrapper = ${monitor-directory:services}/certificate_authority +ca-private = ${ca-directory:private} +ca-certs = ${ca-directory:certs} +ca-newcerts = ${ca-directory:newcerts} +ca-crl = ${ca-directory:crl} + +[ca-httpd] +<= certificate-authority +recipe = slapos.cookbook:certificate_authority.request +key-file = ${monitor-httpd-conf-parameter:key-file} +cert-file = ${monitor-httpd-conf-parameter:cert-file} +executable = ${httpd-wrapper:wrapper-path} +wrapper = ${directory:services}/monitor-httpd + +[monitor-conf-parameters] +title = ${monitor-instance-parameter:monitor-title} +service-executable-dir = ${monitor-directory:run} +template-service-run = {{ monitor_service_run }} +public-folder = ${monitor-directory:public} +private-folder = ${monitor-directory:private} +web-folder = ${monitor-directory:web-dir} +monitor-hal-json = ${monitor-directory:web-dir}/monitor.haljson +service-pid-folder = ${monitor-directory:pids} +crond-folder = ${logrotate-directory:cron-entries} +public-path-list = + ${directory:log} +private-path-list = + +monitor-url-list = + +[monitor-conf] +recipe = slapos.recipe.template:jinja2 +template = {{ monitor_conf_template }} +rendered = ${directory:etc}/${:filename} +filename = monitor.conf +context = section parameter_dict monitor-conf-parameters + +[httpd-monitor-htpasswd] +recipe = plone.recipe.command +stop-on-error = true +htpasswd-path = ${monitor-directory:etc}/monitor-htpasswd +command = {{ apache_location }}/bin/htpasswd -cb ${:htpasswd-path} ${:user} ${:password} +user = admin +password = admin + +[monitor-httpd-conf-parameter] +listening-ip = ${monitor-instance-parameter:monitor-httpd-ipv6} +port = ${monitor-instance-parameter:monitor-httpd-port} +pid-file = ${directory:run}/httpd.pid +cgid-pid-file = ${directory:run}/cgid.pid +access-log = ${monitor-directory:log}/httpd-access.log +error-log = ${monitor-directory:log}/httpd-error.log +cert-file = ${ca-directory:certs}/httpd.crt +key-file = ${ca-directory:certs}/httpd.key +htpasswd-file = ${httpd-monitor-htpasswd:htpasswd-path} +url = https://[${monitor-instance-parameter:monitor-httpd-ipv6}]:${:port}/ + +[monitor-httpd-conf] +recipe = slapos.recipe.template:jinja2 +template = {{ monitor_httpd_template }} +rendered = ${monitor-directory:etc}/monitor-httpd.conf +mode = 0744 +context = + section directory monitor-directory + section parameter_dict monitor-httpd-conf-parameter + +[httpd-wrapper] +recipe = slapos.cookbook:wrapper +command-line = {{ apache_location }}/bin/httpd -f ${monitor-httpd-conf:rendered} -DFOREGROUND +wrapper-path = ${directory:bin}/monitor-httpd +wait-for-files = + ${ca-directory:certs}/httpd.key + ${ca-directory:certs}/httpd.crt + ${cgi-httpd-graceful-wrapper:rendered} + +[cgi-httpd-graceful-wrapper] +recipe = slapos.recipe.template:jinja2 +template = {{ template_wrapper }} +rendered = ${directory:run}/monitor-httpd-graceful +mode = 0700 +context = + key content :command +command = kill -USR1 $(cat ${monitor-httpd-conf-parameter:pid-file}) + +[monitor-web-default-promise-interface] +recipe = slapos.recipe.template:jinja2 +template = {{ monitor_web_default_promise_interface }} +rendered = ${monitor-directory:web-dir}/default-promise-interface.html +context = + +[monitor-web-index-html] +recipe = slapos.recipe.template:jinja2 +template = {{ monitor_web_index_html }} +rendered = ${monitor-directory:web-dir}/index.html +context = + +[monitor-web-monitor-css] +recipe = slapos.recipe.template:jinja2 +template = {{ monitor_web_monitor_css }} +rendered = ${monitor-directory:web-dir}/monitor.css +context = + +[monitor-web-monitor-js] +recipe = slapos.recipe.template:jinja2 +template = {{ monitor_web_monitor_js }} +rendered = ${monitor-directory:web-dir}/monitor.js +context = + key monitor_title monitor-instance-parameter:monitor-title + +[monitor-web-monitor-logout-cgi] +recipe = slapos.recipe.template:jinja2 +template = {{ monitor_web_monitor_logout_cgi }} +rendered = ${monitor-directory:cgi-bin}/monitor-logout.cgi +mode = 0755 +context = + +[monitor-web-monitor-logout-page] +recipe = slapos.recipe.template:jinja2 +template = {{ monitor_web_monitor_logout_page }} +rendered = ${monitor-directory:web-dir}/logout +context = + +[monitor-web-monitor-promise-runner-cgi] +recipe = slapos.recipe.template:jinja2 +template = {{ monitor_web_monitor_promise_runner_cgi }} +rendered = ${monitor-directory:cgi-bin}/monitor-run-promise.cgi +mode = 0755 +context = + raw python_executable {{ python_executable }} + key promise_wrapper_folder monitor-directory:promise-wrapper + +[start-monitor] +recipe = slapos.recipe.template:jinja2 +template = {{ monitor_bin }} +rendered = ${directory:scripts}/bootstrap-monitor +context = + raw python_executable {{ python_executable }} + key public_folder monitor-directory:public + key private_folder monitor-directory:private + key monitor_configuration_path monitor-conf:rendered + key promise_runner_path monitor-run-promise:rendered + key promise_folder directory:promises + key monitor_promise_folder directory:monitor-promise + key promise_wrapper_folder monitor-directory:promise-wrapper + +[monitor-run-promise] +recipe = slapos.recipe.template:jinja2 +template = {{ promise_executor_py }} +rendered = ${directory:bin}/monitor-run-promise +mode = 700 +context = + raw python_executable {{ python_executable }} + +[monitor-httpd-promise] +recipe = slapos.cookbook:check_url_available +path = ${directory:promises}/${:filename} +filename = monitor-httpd-listening-on-tcp +url = ${monitor-httpd-conf-parameter:url} +check-secure = 1 +dash_path = {{ dash_executable_location }} +curl_path = {{ curl_executable_location }} + +[monitor-httpd-promise-conf] +recipe = slapos.recipe.template:jinja2 +rendered = ${directory:monitor-promise}/${monitor-httpd-promise:filename}.cfg +template = {{ monitor_httpd_promise_conf }} +mode = 0644 +context = section parameter_dict monitor-httpd-promise-conf-parameter + +[monitor-httpd-promise-conf-parameter] +title = Monitor httpd listening +# frequency minute hour day mounth weekday +frequency = * * * * * +public-path-list = ${monitor-httpd-conf-parameter:access-log} ${monitor-httpd-conf-parameter:error-log} +#private-path-list = + +[publish] +recipe = slapos.cookbook:publish +monitor-url = ${monitor-httpd-conf-parameter:url} + +[monitor-instance-parameter] +monitor-title = Monitoring interface + +[buildout] +parts = + monitor-web-default-promise-interface + monitor-web-index-html + monitor-web-monitor-css + monitor-web-monitor-js + monitor-web-monitor-logout-cgi + monitor-web-monitor-logout-page + monitor-web-monitor-promise-runner-cgi + cron-entry-logrotate + certificate-authority + monitor-conf + start-monitor + ca-httpd + monitor-httpd-promise + monitor-httpd-promise-conf + publish diff --git a/stack/monitor2/make-rss.sh.in b/stack/monitor2/make-rss.sh.in new file mode 100644 index 000000000..d84092ce8 --- /dev/null +++ b/stack/monitor2/make-rss.sh.in @@ -0,0 +1,8 @@ +#!${dash-output:dash} + +STATUS_DB={{ monitor_parameters['db-path'] }} +RSS_FILE={{ monitor_parameters['rss-path'] }} +PYTHON=${buildout:directory}/bin/${extra-eggs:interpreter} +STATUS2RSS=${rss-bin:location}/${rss-bin:filename} + +$PYTHON $STATUS2RSS "Monitoring RSS feed" "{{ monitor_parameters['url'] }}/{{ monitor_parameters['index-filename'] }}" $STATUS_DB > $RSS_FILE diff --git a/stack/monitor2/monitor-httpd-promise.conf.in b/stack/monitor2/monitor-httpd-promise.conf.in new file mode 100644 index 000000000..128eec9da --- /dev/null +++ b/stack/monitor2/monitor-httpd-promise.conf.in @@ -0,0 +1,4 @@ +[service] +{% for key, value in parameter_dict.items() -%} +{{ key }} = {{ value.strip().replace("\n", "\n ") }} +{% endfor -%} diff --git a/stack/monitor2/monitor-httpd.conf.in b/stack/monitor2/monitor-httpd.conf.in new file mode 100644 index 000000000..5539fba3e --- /dev/null +++ b/stack/monitor2/monitor-httpd.conf.in @@ -0,0 +1,105 @@ +PidFile "{{ parameter_dict.get('pid-file') }}" + +StartServers 1 +ServerLimit 1 +ThreadLimit 4 +ThreadsPerChild 4 + +ServerName example.com +ServerAdmin someone@email +<IfDefine !MonitorPort> +Listen [{{ parameter_dict.get('listening-ip') }}]:{{ parameter_dict.get('port') }} +Define MonitorPort +</IfDefine> +DocumentRoot "{{ directory.get('www') }}" +ErrorLog "{{ parameter_dict.get('error-log') }}" +LoadModule unixd_module modules/mod_unixd.so +LoadModule access_compat_module modules/mod_access_compat.so +LoadModule authz_core_module modules/mod_authz_core.so +LoadModule authn_core_module modules/mod_authn_core.so +LoadModule authz_host_module modules/mod_authz_host.so +LoadModule mime_module modules/mod_mime.so +LoadModule cgid_module modules/mod_cgid.so +LoadModule dir_module modules/mod_dir.so +LoadModule ssl_module modules/mod_ssl.so +LoadModule alias_module modules/mod_alias.so +LoadModule autoindex_module modules/mod_autoindex.so +LoadModule auth_basic_module modules/mod_auth_basic.so +LoadModule authz_user_module modules/mod_authz_user.so +LoadModule authn_file_module modules/mod_authn_file.so +LoadModule proxy_module modules/mod_proxy.so +LoadModule proxy_http_module modules/mod_proxy_http.so +LoadModule rewrite_module modules/mod_rewrite.so + +# SSL Configuration +<IfDefine !SSLConfigured> +Define SSLConfigured +SSLCertificateFile {{ parameter_dict.get('cert-file') }} +SSLCertificateKeyFile {{ parameter_dict.get('key-file') }} +SSLRandomSeed startup builtin +SSLRandomSeed connect builtin +SSLRandomSeed startup /dev/urandom 256 +SSLRandomSeed connect builtin +SSLProtocol -ALL +SSLv3 +TLSv1 +SSLHonorCipherOrder On +SSLCipherSuite RC4-SHA:HIGH:!ADH +</IfDefine> + +AddType application/hal+json .haljson +SSLEngine On +ScriptSock {{ parameter_dict.get('cgid-pid-file') }} +<Directory {{ directory.get('www') }}> + SSLVerifyDepth 1 + SSLRequireSSL + SSLOptions +StrictRequire + # XXX: security???? + DirectoryIndex index.html + Options FollowSymLinks + Order Deny,Allow + AuthType Basic + AuthName "Private access" + AuthUserFile "{{ parameter_dict.get('htpasswd-file') }}" + Require valid-user +</Directory> + +Alias /private {{ directory.get('private') }}/ +<Directory {{ directory.get('private') }}> + Order Deny,Allow + Deny from env=AUTHREQUIRED + <Files ".??*"> + Order Allow,Deny + Deny from all + </Files> + AuthType Basic + AuthName "Private access" + AuthUserFile "{{ parameter_dict.get('htpasswd-file') }}" + Require valid-user + Options Indexes FollowSymLinks + Satisfy all +</Directory> + +Alias /public {{ directory.get('public') }}/ +<Directory {{ directory.get('public') }}> + Options Indexes FollowSymLinks + Order Allow,Deny + Allow from all +</Directory> + +Alias /cgi-bin {{ directory.get('cgi-bin') }} +<Directory {{ directory.get('cgi-bin') }}> + # XXX security ??? + Order Deny,Allow + Deny from all + <Files "*.cgi"> + Order Deny,Allow + Deny from env=AUTHREQUIRED + AuthType Basic + AuthName "Private access" + AuthUserFile "{{ parameter_dict.get('htpasswd-file') }}" + Require valid-user + </Files> + Options +ExecCGI + AddHandler cgi-script .cgi + Options Indexes FollowSymLinks + Satisfy all +</Directory> diff --git a/stack/monitor2/monitor-logout.html b/stack/monitor2/monitor-logout.html new file mode 100644 index 000000000..f8c7161eb --- /dev/null +++ b/stack/monitor2/monitor-logout.html @@ -0,0 +1,26 @@ +<!DOCTYPE html> +<html> + <head><title>Monitor logout</title></head> + <body> + <noscript>Cannot logout without javascript</noscript> + <script> + var logoutURL = "/cgi-bin/monitor-logout.cgi", + xhr = new XMLHttpRequest(); + xhr.onload = function () { + if (xhr.status === 401) { + document.body.innerHTML = "<p>You are now logged out. You can go back to the monitor interface <a href=\"/\">here</a>.</p>"; + } else { + console.error("Cannot logout (" + xhr.status + ")"); + document.body.innerHTML = "<p>Cannot logout, retrying in 5 seconds.</p>"; + setTimeout(location.reload.bind(location), 5000); + } + }; + xhr.onerror = function () { + document.body.innerHTML = "<p>Cannot logout, please try again later.</p>"; + }; + xhr.open("POST", logoutURL, true, " logout", " password"); + xhr.send(); + document.body.innerHTML = "<p>Logging out...</p>"; + </script> + </body> +</html> diff --git a/stack/monitor2/monitor-logout.py.cgi b/stack/monitor2/monitor-logout.py.cgi new file mode 100644 index 000000000..bcf1b9613 --- /dev/null +++ b/stack/monitor2/monitor-logout.py.cgi @@ -0,0 +1,2 @@ +#!{{ python_executable }} +print("Status: 401 Unauthorized\r\nWWW-Authenticate: Basic realm=\"Private access\"\r\n\r") diff --git a/stack/monitor2/monitor-run-promise.py.cgi b/stack/monitor2/monitor-run-promise.py.cgi new file mode 100644 index 000000000..6a2270060 --- /dev/null +++ b/stack/monitor2/monitor-run-promise.py.cgi @@ -0,0 +1,20 @@ +#!{{ python_executable }} +# Put this file in the software release +promise_wrapper_folder = "{{ promise_wrapper_folder }}" + +import cgi +import cgitb +import os + +cgitb.enable(display=0) + +def main(): + form = cgi.FieldStorage() + promise_name = form["service"].value + if "/" not in promise_name: + promise_path = os.path.join(promise_wrapper_folder, promise_name) + os.spawnl(os.P_NOWAIT, promise_path, promise_path) + print("Status: 204 No Content\r\n\r") + +if __name__ == "__main__": + exit(main()) diff --git a/stack/monitor2/monitor-service-run.in b/stack/monitor2/monitor-service-run.in new file mode 100644 index 000000000..f3dd7be45 --- /dev/null +++ b/stack/monitor2/monitor-service-run.in @@ -0,0 +1,37 @@ +#!/usr/bin/env python + +configuration_location = "%(configuration_location)s" +process_pid_file = "%(process_pid_file)s" + +import sys +import os +import ConfigParser +import json +import subprocess + +def loadConfig(config_file): + config = ConfigParser.ConfigParser() + config.read(config_file) + return config + +def main(): + config = loadConfig(configuration_location) + script_path = config.get("service", "script-path") + executable_folder = os.path.dirname(script_path) + executable = os.path.basename(script_path) + parameter_json = os.path.join(os.path.abspath(os.path.dirname(__file__)), + 'parameters_%%s.json' %% executable) + with open(parameter_json, 'w') as fjson: + fjson.write(json.dumps(dict(config.items("parameter")))) + + process = subprocess.Popen( + [script_path, parameter_json], + stdin=None, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE + ) + with open(process_pid_file, "w") as pidfile: + pidfile.write(str(process.pid)) + +if __name__ == "__main__": + sys.exit(main()) \ No newline at end of file diff --git a/stack/monitor2/monitor.cfg.in b/stack/monitor2/monitor.cfg.in new file mode 100644 index 000000000..cab2b5145 --- /dev/null +++ b/stack/monitor2/monitor.cfg.in @@ -0,0 +1,290 @@ +[slap-parameters] +recipe = slapos.cookbook:slapconfiguration +computer = $${slap-connection:computer-id} +partition = $${slap-connection:partition-id} +url = $${slap-connection:server-url} +key = $${slap-connection:key-file} +cert = $${slap-connection:cert-file} + +[monitor-parameters] +json-filename = monitor.json +json-path = $${monitor-directory:monitor-result}/$${:json-filename} +rss-filename = rssfeed.html +rss-path = $${monitor-directory:public-cgi}/$${:rss-filename} +executable = $${monitor-directory:bin}/monitor.py +port = 9685 +htaccess-file = $${monitor-directory:etc}/.htaccess-monitor +url = https://[$${slap-parameters:ipv6-random}]:$${:port} +index-filename = index.cgi +index-path = $${monitor-directory:www}/$${:index-filename} +db-path = $${monitor-directory:etc}/monitor.db +monitor-password-path = $${monitor-directory:etc}/.monitor.shadow + +[monitor-directory] +recipe = slapos.cookbook:mkdirectory +# Standard directory needed by monitoring stack +home = $${buildout:directory} +etc = $${:home}/etc +bin = $${:home}/bin +srv = $${:home}/srv +var = $${:home}/var +log = $${:var}/log +run = $${:var}/run +service = $${:etc}/service/ +etc-run = $${:etc}/run/ +tmp = $${:home}/tmp +promise = $${:etc}/promise + +cron-entries = $${:etc}/cron.d +crontabs = $${:etc}/crontabs +cronstamps = $${:etc}/cronstamps + +ca-dir = $${:srv}/ssl +www = $${:var}/www + +cgi-bin = $${:var}/cgi-bin +monitoring-cgi = $${:cgi-bin}/monitoring +knowledge0-cgi = $${:cgi-bin}/zero-knowledge +public-cgi = $${:cgi-bin}/monitor-public + +monitor-custom-scripts = $${:etc}/monitor +monitor-result = $${:var}/monitor + +private-directory = $${:srv}/monitor-private + +[public-symlink] +recipe = cns.recipe.symlink +symlink = $${monitor-directory:public-cgi} = $${monitor-directory:www}/monitor-public +autocreate = true + +[cron] +recipe = slapos.cookbook:cron +dcrond-binary = ${dcron:location}/sbin/crond +cron-entries = $${monitor-directory:cron-entries} +crontabs = $${monitor-directory:crontabs} +cronstamps = $${monitor-directory:cronstamps} +catcher = $${cron-simplelogger:wrapper} +binary = $${monitor-directory:service}/crond + +# Add log to cron +[cron-simplelogger] +recipe = slapos.cookbook:simplelogger +wrapper = $${monitor-directory:bin}/cron_simplelogger +log = $${monitor-directory:log}/cron.log + +[cron-entry-monitor] +<= cron +recipe = slapos.cookbook:cron.d +name = launch-monitor +frequency = */5 * * * * +command = $${deploy-monitor-script:rendered} -a + +[cron-entry-rss] +<= cron +recipe = slapos.cookbook:cron.d +name = build-rss +frequency = */5 * * * * +command = $${make-rss:rendered} + +[setup-static-files] +recipe = plone.recipe.command +command = ln -s ${download-monitor-jquery:destination} $${monitor-directory:www}/static +update-command = $${:command} + +[deploy-index] +recipe = slapos.recipe.template:jinja2 +template = ${index:location}/${index:filename} +rendered = $${monitor-parameters:index-path} +update-apache-access = ${apache:location}/bin/htpasswd -cb $${monitor-parameters:htaccess-file} admin +mode = 0744 +context = + key cgi_directory monitor-directory:cgi-bin + raw index_template $${deploy-index-template:location}/$${deploy-index-template:filename} + key monitor_password_path monitor-parameters:monitor-password-path + key monitor_password_script_path deploy-monitor-password-cgi:rendered + key apache_update_command :update-apache-access + raw extra_eggs_interpreter ${buildout:directory}/bin/${extra-eggs:interpreter} + raw default_page /static/welcome.html + section rewrite_element monitor-rewrite-rule + +[deploy-index-template] +recipe = hexagonit.recipe.download +url = ${index-template:location}/$${:filename} +destination = $${monitor-directory:www} +filename = ${index-template:filename} +download-only = true +mode = 0644 + +[deploy-status-cgi] +recipe = slapos.recipe.template:jinja2 +template = ${status-cgi:location}/${status-cgi:filename} +rendered = $${monitor-directory:monitoring-cgi}/$${:filename} +filename = status.cgi +mode = 0744 +context = + key json_file monitor-parameters:json-path + key monitor_bin monitor-parameters:executable + key pwd monitor-directory:monitoring-cgi + key this_file :filename + raw python_executable ${buildout:executable} + +[deploy-status-history-cgi] +recipe = slapos.recipe.template:jinja2 +template = ${status-history-cgi:location}/${status-history-cgi:filename} +rendered = $${monitor-directory:monitoring-cgi}/$${:filename} +filename = status-history.cgi +mode = 0744 +context = + key monitor_db_path monitor-parameters:db-path + key status_history_length zero-parameters:status-history-length + raw python_executable ${buildout:executable} + +[deploy-settings-cgi] +recipe = slapos.recipe.template:jinja2 +template = ${settings-cgi:location}/${settings-cgi:filename} +rendered = $${monitor-directory:knowledge0-cgi}/$${:filename} +filename = settings.cgi +mode = 0744 +context = + raw config_cfg $${buildout:directory}/knowledge0.cfg + raw timestamp $${buildout:directory}/.timestamp + raw python_executable ${buildout:executable} + key pwd monitor-directory:knowledge0-cgi + key this_file :filename + +[deploy-monitor-password-cgi] +recipe = slapos.recipe.template:jinja2 +template = ${monitor-password-cgi:location}/${monitor-password-cgi:filename} +rendered = $${monitor-directory:knowledge0-cgi}/$${:filename} +filename = monitor-password.cgi +mode = 0744 +context = + raw python_executable ${buildout:executable} + key pwd monitor-directory:knowledge0-cgi + key this_file :filename + +[deploy-monitor-script] +recipe = slapos.recipe.template:jinja2 +template = ${monitor-bin:location}/${monitor-bin:filename} +rendered = $${monitor-parameters:executable} +mode = 0744 +context = + section directory monitor-directory + section monitor_parameter monitor-parameters + key monitoring_file_json monitor-parameters:json-path + raw python_executable ${buildout:executable} + +[make-rss] +recipe = slapos.recipe.template:jinja2 +template = ${make-rss-script:output} +rendered = $${monitor-directory:bin}/make-rss.sh +mode = 0744 +context = + section directory monitor-directory + section monitor_parameters monitor-parameters + +[monitor-directory-access] +recipe = plone.recipe.command +command = ln -s $${:source} $${monitor-directory:private-directory} +source = + +[monitor-instance-log-access] +recipe = plone.recipe.command +command = if [ -d $${:source} ]; then ln -s $${:source} $${monitor-directory:private-directory}/instance-logs; fi +update-command = if [ -d $${:source} ]; then ln -s $${:source} $${monitor-directory:private-directory}/instance-logs; fi +source = $${monitor-directory:home}/.slapgrid/log/ +location = $${:source} + +[cadirectory] +recipe = slapos.cookbook:mkdirectory +requests = $${monitor-directory:ca-dir}/requests/ +private = $${monitor-directory:ca-dir}/private/ +certs = $${monitor-directory:ca-dir}/certs/ +newcerts = $${monitor-directory:ca-dir}/newcerts/ +crl = $${monitor-directory:ca-dir}/crl/ + +[certificate-authority] +recipe = slapos.cookbook:certificate_authority +openssl-binary = ${openssl:location}/bin/openssl +ca-dir = $${monitor-directory:ca-dir} +requests-directory = $${cadirectory:requests} +wrapper = $${monitor-directory:service}/certificate_authority +ca-private = $${cadirectory:private} +ca-certs = $${cadirectory:certs} +ca-newcerts = $${cadirectory:newcerts} +ca-crl = $${cadirectory:crl} + +[ca-httpd] +<= certificate-authority +recipe = slapos.cookbook:certificate_authority.request +key-file = $${cadirectory:certs}/httpd.key +cert-file = $${cadirectory:certs}/httpd.crt +executable = $${monitor-directory:bin}/cgi-httpd +wrapper = $${monitor-directory:service}/cgi-httpd +# Put domain name +name = example.com + +########### +# Deploy a webserver running cgi scripts for monitoring +########### +[public] +recipe = slapos.cookbook:zero-knowledge.write +filename = knowledge0.cfg +status-history-length = 5 + +[zero-parameters] +recipe = slapos.cookbook:zero-knowledge.read +filename = $${public:filename} + +[monitor-rewrite-rule] + +# XXX could it be something lighter? +[monitor-httpd-configuration] +pid-file = $${monitor-directory:run}/cgi-httpd.pid +cgid-pid-file = $${monitor-directory:run}/cgi-httpd-cgid.pid +error-log = $${monitor-directory:log}/cgi-httpd-error-log +listening-ip = $${slap-parameters:ipv6-random} +certificate = $${ca-httpd:cert-file} +key = $${ca-httpd:key-file} + +[monitor-httpd-configuration-file] +recipe = slapos.recipe.template:jinja2 +template = ${monitor-httpd-template:destination}/${monitor-httpd-template:filename} +rendered = $${monitor-directory:etc}/cgi-httpd.conf +mode = 0744 +context = + section directory monitor-directory + section monitor_parameters monitor-parameters + section httpd_configuration monitor-httpd-configuration + section monitor_rewrite_rule monitor-rewrite-rule + +[cgi-httpd-wrapper] +recipe = slapos.cookbook:wrapper +apache-executable = ${apache:location}/bin/httpd +command-line = $${:apache-executable} -f $${monitor-httpd-configuration-file:rendered} -DFOREGROUND +wrapper-path = $${ca-httpd:executable} +wait-for-files = + $${cadirectory:certs}/httpd.key + $${cadirectory:certs}/httpd.crt + +[cgi-httpd-graceful-wrapper] +recipe = slapos.recipe.template:jinja2 +template = ${template-wrapper:output} +rendered = $${monitor-directory:etc-run}/cgi-httpd-graceful +mode = 0700 +context = + key content :command +command = kill -USR1 $(cat $${monitor-httpd-configuration:pid-file}) + + +[monitor-promise] +recipe = slapos.cookbook:check_url_available +path = $${monitor-directory:promise}/monitor +url = $${monitor-parameters:url}/$${monitor-parameters:index-filename} +check-secure = 1 +dash_path = ${dash:location}/bin/dash +curl_path = ${curl:location}/bin/curl + +[publish-connection-informations] +recipe = slapos.cookbook:publish +monitor_url = $${monitor-parameters:url} diff --git a/stack/monitor2/monitor.conf.in b/stack/monitor2/monitor.conf.in new file mode 100644 index 000000000..b9f2e1b6e --- /dev/null +++ b/stack/monitor2/monitor.conf.in @@ -0,0 +1,4 @@ +[monitor] +{% for key, value in parameter_dict.items() -%} +{{ key }} = {{ value.strip().replace("\n", "\n ") }} +{% endfor -%} diff --git a/stack/monitor2/monitor.css b/stack/monitor2/monitor.css new file mode 100644 index 000000000..788b9c19a --- /dev/null +++ b/stack/monitor2/monitor.css @@ -0,0 +1,50 @@ +body { width: 80vw; margin: auto; padding-top: 1%; } +/* h1 { align-text: center; margin: auto; } */ +/*td { padding: 0 2%; }/**/ +td { padding: 0 1em; }/**/ +table { border: 1px solid black; } +table > table { margin-top: 1em; } + +input { + box-sizing: border-box; + min-height: 10mm; + min-width: 10mm; +} + +button { + box-sizing: border-box; + min-height: 10mm; + min-width: 10mm; + background-color: lightgray; + background: linear-gradient(180deg, #F6F6F6 0%, #DDDDDD 100%); + border-radius: 2px; + border-style: solid; + border-width: 1px; + border-color: #A4A4A4; +} + +a.as-button { + display: inline-block; + box-sizing: border-box; + min-height: 10mm; + min-width: 10mm; + padding: 0.5em 0.5em; + text-align: center; + text-decoration: initial; +} +a.as-button { + color: black; + background-color: lightgray; + background: linear-gradient(180deg, #F6F6F6 0%, #DDDDDD 100%); + border-radius: 2px; + border-style: solid; + border-width: 1px; + border-color: #A4A4A4; +} +a.as-button:active, button:active { + background-color: white; + background: linear-gradient(0deg, #F6F6F6 0%, #DDDDDD 100%); +} +a.as-button:hover, button:hover { + border-color: #777777; +} diff --git a/stack/monitor2/monitor.js.in b/stack/monitor2/monitor.js.in new file mode 100644 index 000000000..dce96f0ea --- /dev/null +++ b/stack/monitor2/monitor.js.in @@ -0,0 +1,173 @@ +/*jslint indent: 2 */ +(function () { + "use strict"; + + var monitor_title = '{{ dumps(monitor_title)[5:-1] }}'; + + function loadJson(url) { + /*global XMLHttpRequest */ + return new Promise(function (resolve, reject) { + var xhr = new XMLHttpRequest(); + xhr.onload = function (event) { + var response = event.target; + if (response.status < 400) { + try { + resolve(JSON.parse(response.responseText)); + } catch (e) { + reject(e); + } + } else { + reject(new Error("XHR: " + response.status + ": " + response.statusText)); + } + }; + xhr.onerror = function () { + reject(new Error("XHR: Error")); + }; + xhr.open("GET", url, true); + xhr.send(); + }); + } + + /////////////////// + // tools for HAL // + + function getProperty(object, path) { + if (Array.isArray(path)) { + while (path.length) { + object = object[path.shift()]; + } + } else { + return object[path]; + } + return object; + } + + function softGetProperty(object, path) { + try { + return getProperty(object, path); + } catch (ignored) { + return undefined; + } + } + + function forceList(value) { + if (Array.isArray(value)) { + return value; + } + return [value]; + } + + function softGetPropertyAsList(object, path) { + try { + return forceList(getProperty(object, path)); + } catch (ignored) { + return []; + } + } + + /////////////////// + + function htmlToElementList(html) { + /*global document */ + var div = document.createElement("div"); + div.innerHTML = html; + return div.querySelectorAll("*"); + } + + function resolveUrl(firstUrl) { + /*jslint plusplus: true */ + /*global URL, location */ + var l = arguments.length, i = 1, url = new URL(firstUrl, location.href); + while (i < l) { url = new URL(arguments[i++], url); } + return url.href; + } + + function escapeHtml(html) { + return html.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'"); + } + + function loadAndRenderMonitorSection(root, monitor_dict, monitor_url) { + var table, service_list = softGetPropertyAsList(monitor_dict, ["_embedded", "service"]); + if (!service_list) { + root.textContent = ""; + return; + } + table = document.createElement("table"); + root.appendChild(table); + return Promise.all(service_list.map(function (service_dict) { + var interface_url = softGetProperty(service_dict, ["_links", "interface", "href"]), + status_url = softGetProperty(service_dict, ["_links", "status", "href"]), + href_html_part = (interface_url ? " href=\"" + escapeHtml(interface_url) + "\"" : ""), + title_html_part = (service_dict.title ? escapeHtml(service_dict.title) : (service_dict.id ||"Untitled")), + row = htmlToElementList("<table><tbody><tr><td><a" + href_html_part + ">" + title_html_part + "</a></td><td>Loading status...</td><td><a" + href_html_part + "><div style=\"height: 10mm; width: 10mm; background-color: gray;\"></div></a></td></tr></tbody></table>"); + table.appendChild(row[2]); + if (!status_url) { + row[5].textContent = "No status"; + return; + } + return loadJson(resolveUrl(monitor_url, status_url)).then(function (status_dict) { + if (status_dict.description) { + row[2].title = status_dict.description; + } + row[5].textContent = status_dict.message || ""; + row[8].style.backgroundColor = status_dict.status === "OK" ? "green" : "red"; + }).catch(function (reason) { + row[5].textContent = (reason && (reason.name + ": " + reason.message)); + row[8].style.backgroundColor = "red"; + }); + })); + } + + function loadAndRenderMonitorJson(root) { + root.textContent = "Loading monitor section..."; + return loadJson("monitor.haljson").then(function (monitor_dict) { + //monitor_json_list.push(monitor_dict); + root.innerHTML = ""; + var loading = loadAndRenderMonitorSection(root, monitor_dict), related_monitor_list = softGetPropertyAsList(monitor_dict, ["_links", "related_monitor"]); + if (!related_monitor_list.length) { return loading; } + return Promise.all([loading, Promise.all(related_monitor_list.map(function (link) { + var div = htmlToElementList("<div>Loading monitor section...</div>")[0]; + root.appendChild(div); + if (link.href[link.href.length - 1] !== "/") { + link.href += "/"; + } + link.href = resolveUrl(link.href, "monitor.haljson"); + return loadJson(link.href).catch(function (reason) { + div.textContent = (reason && (reason.name + ": " + reason.message)); + }).then(function (monitor_dict) { + //monitor_json_list.push(monitor_dict); + div.remove(); + return loadAndRenderMonitorSection(root, monitor_dict, link.href); + }); + }))]); + }); + } + + function bootstrap(root) { + var element_list = htmlToElementList([ + "<header><a href=\"\" class=\"as-button\">Refresh</a> <a href=\"/logout\" class=\"as-button\">Logout</a></header>", + "<h1>" + monitor_title + "</h1>", + "<h2>System health status</h2>", + "<p>This interface allow to see the status of several features, it may show problems and sometimes provides a way to fix them.</p>", + "<p>Red square means the feature has a problem, green square means it is ok.</p>", + "<p>You can click on a feature below to get more precise information.</p>" + ].join("\n")), div = document.createElement("div"), tmp; + [].forEach.call(element_list, function (element) { + if (element.parentNode.parentNode) { return; } + root.appendChild(element); + }); + document.title = monitor_title; + root.appendChild(div); + /*global alert */ + tmp = loadAndRenderMonitorJson(div); + tmp.catch(alert); + /*global console */ + tmp.catch(console.error.bind(console)); + } + + /*global setTimeout */ + setTimeout(function () { + /*global document */ + bootstrap(document.body); + }); +}()); diff --git a/stack/monitor2/monitor.py.in b/stack/monitor2/monitor.py.in new file mode 100644 index 000000000..4e29527b6 --- /dev/null +++ b/stack/monitor2/monitor.py.in @@ -0,0 +1,141 @@ +#!{{ python_executable }} +# Put this file in the software release +promise_runner_path = "{{ promise_runner_path }}" +public_folder = "{{ public_folder }}" +private_folder = "{{ private_folder }}" +monitor_configuration_path = "{{ monitor_configuration_path }}" +promise_folder = "{{ promise_folder }}" +monitor_promise_folder = "{{ monitor_promise_folder }}" +promise_wrapper_folder = "{{ promise_wrapper_folder }}" + +import sys +import os +import stat +import subprocess +import threading +import json +import ConfigParser +import traceback + + +def main(): + # initialisation + config = loadConfig([monitor_configuration_path]) + # get promises in monitor_promise_folder + promise_dict = {} + fillPromiseDictFromFolder(promise_dict, monitor_promise_folder) + # get promises in promise_folder + fillPromiseDictFromFolder(promise_dict, promise_folder) + # get promises configurations + for filename in os.listdir(monitor_promise_folder): + path = os.path.join(monitor_promise_folder, filename) + if os.path.isfile(path) and filename[-4:] == ".cfg": + promise_name = filename[:-4] + if promise_name in promise_dict: + loadConfig([path], promise_dict[promise_name]["configuration"]) + promise_items = promise_dict.items() + # create symlinks from service configurations + for service_name, promise in promise_items: + service_config = promise["configuration"] + createSymlinksFromConfig((config, "monitor", "public-folder"), (service_config, "service", "public-path-list"), service_name) + createSymlinksFromConfig((config, "monitor", "private-folder"), (service_config, "service", "private-path-list"), service_name) + # create symlinks from monitor.conf + createSymlinksFromConfig((config, "monitor", "public-folder"), (config, "monitor", "public-path-list")) + createSymlinksFromConfig((config, "monitor", "private-folder"), (config, "monitor", "private-path-list")) + # generate monitor.json + monitor_dict = {} + tmp = softConfigGet(config, "monitor", "title") + if tmp: + monitor_dict["title"] = tmp + tmp = softConfigGet(config, "monitor", "monitor-url-list") + if tmp: + monitor_dict["_links"] = {"related_monitor": [{"href": url} for url in tmp.split()]} + if promise_items: + service_list = [] + monitor_dict["_embedded"] = {"service": service_list} + for service_name, promise in promise_items: + service_config = promise["configuration"] + service_dict = {} + service_list.append(service_dict) + service_dict["id"] = service_name + service_dict["_links"] = {"status": {"href": "/public/%s.status.json" % service_name}} # hardcoded + tmp = softConfigGet(service_config, "service", "title") + if tmp: + service_dict["title"] = tmp + interface_path = os.path.join(private_folder, service_name, "interface/index.html") # hardcoded + if os.path.isfile(interface_path): + service_dict["_links"]["interface"] = {"href": "/private/%s/interface/" % service_name} # hardcoded + else: + service_dict["_links"]["interface"] = {"href": "/default-promise-interface.html?service_name=%s" % service_name} # XXX hardcoded + with open(config.get("monitor", "monitor-hal-json"), "w") as fp: + json.dump(monitor_dict, fp) + # put promises to a cron file + # XXX only if at least one configuration file is modified, then write in the cron + service_pid_folder = config.get("monitor", "service-pid-folder") + crond_folder = config.get("monitor", "crond-folder") + cron_line_list = [] + for service_name, promise in promise_items: + service_status_path = "%s/%s.status.json" % (public_folder, service_name) # hardcoded + mkdirAll(os.path.dirname(service_status_path)) + command = ("%s %s %s " % ( + promise_runner_path, + os.path.join(service_pid_folder, "%s.pid" % service_name), + service_status_path, + )) + promise["path"] + cron_line_list.append("%s %s" % ( + softConfigGet(service_config, "service", "frequency") or "* * * * *", + command.replace("%", "\\%"), + )) + wrapper_path = os.path.join(promise_wrapper_folder, service_name) + with open(wrapper_path, "w") as fp: + fp.write("#!/bin/sh\n%s" % command) # XXX hardcoded, use dash, sh or bash binary! + os.chmod(wrapper_path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR | stat.S_IRGRP | stat.S_IROTH ) + with open(crond_folder + "/monitor-promises", "w") as fp: + fp.write("\n".join(cron_line_list)) + return 0 + +def loadConfig(pathes, config=None): + if config is None: + config = ConfigParser.ConfigParser() + try: + config.read(pathes) + except ConfigParser.MissingSectionHeaderError: + traceback.print_exc() + return config + +def fillPromiseDictFromFolder(promise_dict, folder): + for filename in os.listdir(folder): + path = os.path.join(folder, filename) + if os.path.isfile(path) and os.access(path, os.X_OK): + promise_dict[filename] = {"path": path, "configuration": ConfigParser.ConfigParser()} + +def softConfigGet(config, *args, **kwargs): + try: + return config.get(*args, **kwargs) + except (ConfigParser.NoOptionError, ConfigParser.NoSectionError): + return None + +def createSymlinksFromConfig(destination_folder_config_tuple, source_list_config_tuple, service_name=""): + destination_folder = softConfigGet(*destination_folder_config_tuple) + if destination_folder: + source_path_str = softConfigGet(*source_list_config_tuple) + if source_path_str: + for path in source_path_str.split(): + dirname = os.path.join(destination_folder, service_name) + try: + mkdirAll(dirname) # could also raise OSError + os.symlink(path, os.path.join(dirname, os.path.basename(path))) + except OSError, e: + if e.errno != os.errno.EEXIST: + raise + +def mkdirAll(path): + try: + os.makedirs(path) + except OSError, e: + if e.errno == os.errno.EEXIST and os.path.isdir(path): + pass + else: raise + +if __name__ == "__main__": + sys.exit(main()) diff --git a/stack/monitor2/run-promise.py b/stack/monitor2/run-promise.py new file mode 100644 index 000000000..1b2c6121e --- /dev/null +++ b/stack/monitor2/run-promise.py @@ -0,0 +1,60 @@ +#!{{ python_executable }} +# -*- coding: utf-8 -*- + +import sys +import os +import subprocess +import json +from cStringIO import StringIO + +def main(): + if len(sys.argv) < 4: + print("Usage: %s <pid_path> <output_path> <command...>" % sys.argv[0]) + return 2 + pid_path=sys.argv[1] + output_path=sys.argv[2] + if os.path.exists(pid_path): + with open(pid_path, "r") as pidfile: + try: + pid = int(pidfile.read(6)) + except ValueError: + pid = None + if pid and os.path.exists("/proc/" + str(pid)): + print("A process is already running with pid " + str(pid)) + return 1 + with open(pid_path, "w") as pidfile: + process = executeCommand(sys.argv[3:]) + pidfile.write(str(process.pid)) + status_json = generateStatusJsonFromProcess(process) + with open(output_path, "w") as outputfile: + json.dump(status_json, outputfile) + os.remove(pid_path) + + +def generateStatusJsonFromProcess(process): + stdout, stderr = process.communicate() + try: + status_json = json.loads(stdout) + except ValueError: + status_json = {} + if process.returncode != 0: + status_json["status"] = "error" + elif not status_json.get("status"): + status_json["status"] = "OK" + if stderr: + status_json["error"] = stderr + return status_json + + +def executeCommand(args): + return subprocess.Popen( + args, + #cwd=instance_path, + #env=None if sys.platform == 'cygwin' else {}, + stdin=None, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE + ) + +if __name__ == "__main__": + sys.exit(main()) diff --git a/stack/monitor2/status2rss.py b/stack/monitor2/status2rss.py new file mode 100644 index 000000000..26ed8f187 --- /dev/null +++ b/stack/monitor2/status2rss.py @@ -0,0 +1,59 @@ +import datetime +import PyRSS2Gen +import sys +import sqlite3 +import time +import base64 + +# Based on http://thehelpfulhacker.net/2011/03/27/a-rss-feed-for-your-crontabs/ + +# ### Defaults +TITLE = sys.argv[1] +LINK = sys.argv[2] +db_path = sys.argv[3] +DESCRIPTION = TITLE +SUCCESS = "SUCCESS" +FAILURE = "FAILURE" + +items = [] +status = "" + +current_timestamp = int(time.time()) +# We only build the RSS for the last ten days +period = 3600 * 24 * 10 +db = sqlite3.connect(db_path) +rows = db.execute("select timestamp, status from status where timestamp>? order by timestamp", (current_timestamp - period,)) +for row in rows: + line_timestamp, line_status = row + line_status = line_status.encode() + + if line_status == status: + continue + + status = line_status + + event_time = datetime.datetime.fromtimestamp(line_timestamp).strftime('%Y-%m-%d %H:%M:%S') + + individual_rows = db.execute("select status, element, output from individual_status where timestamp=?", (line_timestamp,)) + description = '\n'.join(['%s: %s %s' % row for row in individual_rows]) + + rss_item = PyRSS2Gen.RSSItem( + title = status, + description = "%s: %s\n%s" % (event_time, status, description), + link = LINK, + pubDate = event_time, + guid = PyRSS2Gen.Guid(base64.b64encode("%s, %s" % (event_time, status))) + ) + items.append(rss_item) + +### Build the rss feed +items.reverse() +rss_feed = PyRSS2Gen.RSS2 ( + title = TITLE, + link = LINK, + description = DESCRIPTION, + lastBuildDate = datetime.datetime.utcnow(), + items = items + ) + +print rss_feed.to_xml() diff --git a/stack/monitor2/webfile-directory/ansible-report.cgi.in b/stack/monitor2/webfile-directory/ansible-report.cgi.in new file mode 100644 index 000000000..e69de29bb diff --git a/stack/monitor2/webfile-directory/index.cgi.in b/stack/monitor2/webfile-directory/index.cgi.in new file mode 100755 index 000000000..52ae379f2 --- /dev/null +++ b/stack/monitor2/webfile-directory/index.cgi.in @@ -0,0 +1,190 @@ +#!{{ extra_eggs_interpreter }} + +import cgi +import cgitb +import Cookie +import base64 +import hashlib +import hmac +import jinja2 +import os +import subprocess +import urllib + +cgitb.enable(display=0, logdir="/tmp/cgi.log") + +form = cgi.FieldStorage() +cookie = Cookie.SimpleCookie() + +cgi_path = "{{ cgi_directory }}" + +monitor_password_path = "{{ monitor_password_path }}" +monitor_password_script_path = "{{ monitor_password_script_path }}" + +monitor_apache_password_command = "{{ apache_update_command }}" + +monitor_rewrite = "{{ ' '.join(rewrite_element.keys()) }}" + +######## +# Password functions +####### +def crypt(word, salt="$$"): + salt = salt.split("$") + algo = salt[0] or 'sha1' + if algo in hashlib.algorithms: + H = getattr(hashlib, algo) + elif algo == "plain": + return "%s$%s" % (algo, word) + else: + raise ValueError + rounds = min(max(0, int(salt[1])), 30) if salt[1] else 9 + salt = salt[2] or base64.b64encode(os.urandom(12), "./") + h = hmac.new(salt, word, H).digest() + for x in xrange(1, 1 << rounds): + h = H(h).digest() + return "%s$%s$%s$%s" % (algo, rounds, salt, + base64.b64encode(h, "./").rstrip("=")) + +def is_password_set(): + if not os.path.exists(monitor_password_path): + return False + hashed_password = open(monitor_password_path, 'r').read() + try: + void, algo, salt, hsh = hashed_password.split('$') + except ValueError: + return False + return True + +def set_password(raw_password): + hashed_password = crypt(raw_password) + subprocess.check_call(monitor_apache_password_command + " %s" % raw_password, + shell=True) + open(monitor_password_path, 'w').write(hashed_password) + + +def check_password(raw_password): + """ + Returns a boolean of whether the raw_password was correct. Handles + encryption formats behind the scenes. + """ + if not os.path.exists(monitor_password_path) or not raw_password: + return False + hashed_password = open(monitor_password_path, 'r').read() + return hashed_password == crypt(raw_password, hashed_password) +### End of password functions + +def forward_form(): + command = os.path.join(cgi_path, form['posting-script'].value) + params_dict = {} + for f in form: + params_dict[f] = form[f].value + del params_dict['posting-script'] + os.environ['QUERY_STRING'] = urllib.urlencode(params_dict) + try: + if os.access(command, os.X_OK): + print '\n', subprocess.check_output([command]) + except subprocess.CalledProcessError: + print "There is a problem with sub-process" + pass + + +def return_document(command=None): + if not command: + script = form['script'].value + command = os.path.join(cgi_path, script) + #XXX this functions should be called only for display, + #so a priori it doesn't need form data + os.environ['QUERY_STRING'] = '' + try: + if os.access(command, os.X_OK): + print '\n', subprocess.check_output([command]) + elif os.access(command, os.R_OK): + print open(command).read() + else: + raise OSError + except (subprocess.CalledProcessError, OSError) as e: + print "<p>Error :</p><pre>%s</pre>" % e + + +def make_menu(): + # Transform deep-2 tree in json + folder_list = {} + for folder in os.listdir(cgi_path): + if os.path.isdir(os.path.join(cgi_path, folder)): + folder_list[folder] = [] + for folder in folder_list: + for file in os.listdir(os.path.join(cgi_path, folder)): + if os.path.isfile(os.path.join(cgi_path, folder, file)): + folder_list[folder].append(file) + return folder_list + + +def get_cookie_password(): + cookie_string = os.environ.get('HTTP_COOKIE') + if cookie_string: + cookie.load(cookie_string) + try: + return cookie['password'].value + except KeyError: + pass + return None + +def set_cookie_password(password): + cookie['password'] = password + print cookie, "; Path=/; HttpOnly" + + +# Beginning of response +print "Content-Type: text/html" + +password = None + +# Check if user is logged +if "password_2" in form and "password" in form: + password_2 = form['password_2'].value + password_1 = form['password'].value + password = get_cookie_password() + if not is_password_set() or check_password(password): + if password_2 == password_1: + password = password_1 + set_password(password) + set_cookie_password(password) +elif "password" in form: + password = form['password'].value + if is_password_set() and check_password(password): + set_cookie_password(password) +else: + password = get_cookie_password() +print '\n' + + +if not is_password_set(): + return_document(monitor_password_script_path) +elif not check_password(password): + print "<html><head>" + print """ + <link rel="stylesheet" href="static/pure-min.css"> + <link rel="stylesheet" href="static/style.css">""" + print "</head><body>" + if password is None: + print "<h1>This is the monitoring interface</h1>" + else: + print "<h1>Error</h1><p>Wrong password</p>" + print """ + <p>Please enter the monitor_password in the next field to access the data</p> + <form action="/index.cgi" method="post" class="pure-form-aligned"> + Password : <input type="password" name="password"> + <button type="submit" class="pure-button pure-button-primary">Access</button> + </form> + </body></html>""" +# redirection to the required script/page +else: + print + if "posting-script" in form: + forward_form() + elif "script" in form: + return_document() + else: + html_base = jinja2.Template(open('{{ index_template }}').read()) + print + print html_base.render(tree=make_menu(), default_page="{{ default_page }}", monitor_rewrite=monitor_rewrite) diff --git a/stack/monitor2/webfile-directory/index.html.jinja2 b/stack/monitor2/webfile-directory/index.html.jinja2 new file mode 100644 index 000000000..2dcf05dc2 --- /dev/null +++ b/stack/monitor2/webfile-directory/index.html.jinja2 @@ -0,0 +1,35 @@ +<html> +<head> + <title>Monitoring Interface</title> + <link rel="stylesheet" href="static/pure-min.css"> + <link rel="stylesheet" href="static/style.css"> + <script src="static/jquery-1.10.2.min.js"></script> + <script src="static/script.js"></script> +</head> +<body> + <div id="div-menu"> + <h1>Monitoring</h1> + <div id="script-categories" class="pure-menu pure-menu-open"> + <ul> + {% for category in tree %} + <li class="pure-menu-heading category">{{ category }}</li> + {% for script in tree[category] %} + <li><a href="{{ category }}/{{ script }}" class="script">{{ script }}</a></li> + {% endfor %} + {% endfor %} + <li class="pure-menu-heading category">Files</li> + <li><a href="./private/" class="link"> User: admin</br> Password is yours</a></li> + <li class="pure-menu-heading category">Local Service</li> + {% set rewrite_list = monitor_rewrite.split() %} + {% for path in rewrite_list %} + <li><a href="./rewrite/{{path}}/" class="link">{{path}}</a></li> + {% endfor %} + </ul> + </div> + </div> + <div id="content"> + <iframe src="{{ default_page }}"> + </iframe> + </div> +</body> +</html> diff --git a/stack/monitor2/webfile-directory/monitor-password.cgi.in b/stack/monitor2/webfile-directory/monitor-password.cgi.in new file mode 100644 index 000000000..23817f144 --- /dev/null +++ b/stack/monitor2/webfile-directory/monitor-password.cgi.in @@ -0,0 +1,29 @@ +#!{{ python_executable }} + +import cgitb + +cgitb.enable() + +print "<html><head>" +print """ + <script type="text/javascript" src="static/jquery-1.10.2.min.js"></script> + <link rel="stylesheet" href="static/pure-min.css"> + <link rel="stylesheet" href="static/style.css">""" +print "</head><body>" +print "<h1>This is the monitoring interface</h1>" +print "<h2>Please set your password for later access</h2>" +print """ +<form action="/index.cgi" method="post" class="pure-form-aligned"> +<div class="pure-control-group"> +<label for="password">Password*:</label> +<input placeholder="Set your password" type="password" name="password" id="password"></br> +</div><div class="pure-control-group"> +<label for="password">Verify Password*:</label> +<input placeholder="Verify password" type="password" name="password_2" id="password_2"></br> +</div><p id="validate-status" style="color:red"></p> +<div class="pure-controls"> +<button id="register-button" type="submit" class="pure-button pure-button-primary" disabled>Access</button></div> +</form> +<script type="text/javascript" src="static/monitor-register.js"></script> +</body></html> +""" diff --git a/stack/monitor2/webfile-directory/settings.cgi.in b/stack/monitor2/webfile-directory/settings.cgi.in new file mode 100755 index 000000000..5aa2de061 --- /dev/null +++ b/stack/monitor2/webfile-directory/settings.cgi.in @@ -0,0 +1,64 @@ +#!{{ python_executable }} + +import cgi +import cgitb +import ConfigParser +import os + +cgitb.enable() +form = cgi.FieldStorage() + +print "<html><head>" +print "<link rel=\"stylesheet\" href=\"static/pure-min.css\">" +print "<link rel=\"stylesheet\" href=\"static/style.css\">" +print "</head><body>" + +config_file = "{{ config_cfg }}" + +if not os.path.exists(config_file): + print "Your software does <b>not</b> embed 0-knowledge. \ + This interface is useless in this case</body></html>" + exit(0) + +parser = ConfigParser.ConfigParser() +parser.read(config_file) + +if not parser.has_section('public'): + print "<p>Your software does not use 0-knowledge settings.</p></body></html>" + exit(0) + +for name in form: + if parser.has_option('public', name): + parser.set('public', name, form[name].value) +with open(config_file, 'w') as file: + parser.write(file) + +if len(form) > 0: + try: + os.remove("{{ timestamp }}") + except OSError: + pass + +print "<h1>Values that can be defined :</h1>" +print "<form action=\"/index.cgi\" method=\"post\" class=\"pure-form-aligned\">" +print "<input type=\"hidden\" name=\"posting-script\" value=\"{{ pwd }}/{{ this_file }}\">" +for option in parser.options("public"): + print "<div class=\"pure-control-group\">" + print "<label for=\"%s\">%s</label>" % (cgi.escape(option, quote=True), cgi.escape(option)) + print "<input type=\"text\" name=\"%s\" value=\"%s\">" % (cgi.escape(option, quote=True), cgi.escape(parser.get('public', option), quote=True)) + print "</div>" +print "<div class=\"pure-controls\"><button type=\"submit\" class=\"pure-button \ + pure-button-primary\">Save</button></div></form>" + +print "<br><h1>Other values :</h1>" +print "<form class=\"pure-form-aligned\">" +for section in parser.sections(): + if section != 'public': + for option in parser.options(section): + print "<div class=\"pure-control-group\">" + print "<label for=\"%s\">%s</label>" % (cgi.escape(option, quote=True), cgi.escape(option)) + print "<input type=\"text\" name=\"%s\" value=\"%s\" readonly>" %(cgi.escape(option, quote=True), cgi.escape(parser.get(section, option), quote=True)) + print "</div>" +print "</form>" + +print "</body></html>" diff --git a/stack/monitor2/webfile-directory/static/monitor-register.js b/stack/monitor2/webfile-directory/static/monitor-register.js new file mode 100644 index 000000000..88d40d4b7 --- /dev/null +++ b/stack/monitor2/webfile-directory/static/monitor-register.js @@ -0,0 +1,17 @@ +$(window).load(function(){ + $(document).ready(function() { + $("#password_2").keyup(validate); + }); + function validate() { + var password1 = $("#password").val(); + var password2 = $("#password_2").val(); + if(password1 == password2) { + $("#register-button").removeAttr("disabled"); + $("#validate-status").attr("style", "display:none"); + } + else { + $("#register-button").attr("disabled", "disabled"); + $("#validate-status").attr("style", "").text("Passwords do not match"); + } + } +}); \ No newline at end of file diff --git a/stack/monitor2/webfile-directory/static/pure-min.css b/stack/monitor2/webfile-directory/static/pure-min.css new file mode 100644 index 000000000..39212bf07 --- /dev/null +++ b/stack/monitor2/webfile-directory/static/pure-min.css @@ -0,0 +1,11 @@ +/*! +Pure v0.3.0 +Copyright 2013 Yahoo! Inc. All rights reserved. +Licensed under the BSD License. +https://github.com/yui/pure/blob/master/LICENSE.md +*/ +/*! +normalize.css v1.1.2 | MIT License | git.io/normalize +Copyright (c) Nicolas Gallagher and Jonathan Neal +*/ +/*! normalize.css v1.1.2 | MIT License | git.io/normalize */article,aside,details,figcaption,figure,footer,header,hgroup,main,nav,section,summary{display:block}audio,canvas,video{display:inline-block;*display:inline;*zoom:1}audio:not([controls]){display:none;height:0}[hidden]{display:none}html{font-size:100%;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}html,button,input,select,textarea{font-family:sans-serif}body{margin:0}a:focus{outline:thin dotted}a:active,a:hover{outline:0}h1{font-size:2em;margin:.67em 0}h2{font-size:1.5em;margin:.83em 0}h3{font-size:1.17em;margin:1em 0}h4{font-size:1em;margin:1.33em 0}h5{font-size:.83em;margin:1.67em 0}h6{font-size:.67em;margin:2.33em 0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}blockquote{margin:1em 40px}dfn{font-style:italic}hr{-moz-box-sizing:content-box;box-sizing:content-box;height:0}mark{background:#ff0;color:#000}p,pre{margin:1em 0}code,kbd,pre,samp{font-family:monospace,serif;_font-family:'courier new',monospace;font-size:1em}pre{white-space:pre;white-space:pre-wrap;word-wrap:break-word}q{quotes:none}q:before,q:after{content:'';content:none}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}dl,menu,ol,ul{margin:1em 0}dd{margin:0 0 0 40px}menu,ol,ul{padding:0 0 0 40px}nav ul,nav ol{list-style:none;list-style-image:none}img{border:0;-ms-interpolation-mode:bicubic}svg:not(:root){overflow:hidden}figure{margin:0}form{margin:0}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}legend{border:0;padding:0;white-space:normal;*margin-left:-7px}button,input,select,textarea{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle}button,input{line-height:normal}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer;*overflow:visible}button[disabled],html input[disabled]{cursor:default}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0;*height:13px;*width:13px}input[type=search]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}textarea{overflow:auto;vertical-align:top}table{border-collapse:collapse;border-spacing:0}.pure-button{display:inline-block;*display:inline;zoom:1;line-height:normal;white-space:nowrap;vertical-align:baseline;text-align:center;cursor:pointer;-webkit-user-drag:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.pure-button::-moz-focus-inner{padding:0;border:0}.pure-button{font-size:100%;*font-size:90%;*overflow:visible;padding:.5em 1.5em;color:#444;color:rgba(0,0,0,.8);*color:#444;border:1px solid #999;border:0 rgba(0,0,0,0);background-color:#E6E6E6;text-decoration:none;border-radius:2px;-webkit-transition:.1s linear -webkit-box-shadow;-moz-transition:.1s linear -moz-box-shadow;-ms-transition:.1s linear box-shadow;-o-transition:.1s linear box-shadow;transition:.1s linear box-shadow}.pure-button-hover,.pure-button:hover,.pure-button:focus{filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#1a000000', GradientType=0);background-image:-webkit-gradient(linear,0 0,0 100%,from(transparent),color-stop(40%,rgba(0,0,0,.05)),to(rgba(0,0,0,.1)));background-image:-webkit-linear-gradient(transparent,rgba(0,0,0,.05) 40%,rgba(0,0,0,.1));background-image:-moz-linear-gradient(top,rgba(0,0,0,.05) 0,rgba(0,0,0,.1));background-image:-ms-linear-gradient(transparent,rgba(0,0,0,.05) 40%,rgba(0,0,0,.1));background-image:-o-linear-gradient(transparent,rgba(0,0,0,.05) 40%,rgba(0,0,0,.1));background-image:linear-gradient(transparent,rgba(0,0,0,.05) 40%,rgba(0,0,0,.1))}.pure-button:focus{outline:0}.pure-button-active,.pure-button:active{box-shadow:0 0 0 1px rgba(0,0,0,.15) inset,0 0 6px rgba(0,0,0,.2) inset}.pure-button[disabled],.pure-button-disabled,.pure-button-disabled:hover,.pure-button-disabled:focus,.pure-button-disabled:active{border:0;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);filter:alpha(opacity=40);-khtml-opacity:.4;-moz-opacity:.4;opacity:.4;cursor:not-allowed;box-shadow:none}.pure-button-hidden{display:none}.pure-button::-moz-focus-inner{padding:0;border:0}.pure-button-primary,.pure-button-selected,a.pure-button-primary,a.pure-button-selected{background-color:#0078e7;color:#fff}.pure-form input[type=text],.pure-form input[type=password],.pure-form input[type=email],.pure-form input[type=url],.pure-form input[type=date],.pure-form input[type=month],.pure-form input[type=time],.pure-form input[type=datetime],.pure-form input[type=datetime-local],.pure-form input[type=week],.pure-form input[type=number],.pure-form input[type=search],.pure-form input[type=tel],.pure-form input[type=color],.pure-form select,.pure-form textarea{padding:.5em .6em;display:inline-block;border:1px solid #ccc;font-size:.8em;box-shadow:inset 0 1px 3px #ddd;border-radius:4px;-webkit-transition:.3s linear border;-moz-transition:.3s linear border;-ms-transition:.3s linear border;-o-transition:.3s linear border;transition:.3s linear border;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.pure-form input[type=text]:focus,.pure-form input[type=password]:focus,.pure-form input[type=email]:focus,.pure-form input[type=url]:focus,.pure-form input[type=date]:focus,.pure-form input[type=month]:focus,.pure-form input[type=time]:focus,.pure-form input[type=datetime]:focus,.pure-form input[type=datetime-local]:focus,.pure-form input[type=week]:focus,.pure-form input[type=number]:focus,.pure-form input[type=search]:focus,.pure-form input[type=tel]:focus,.pure-form input[type=color]:focus,.pure-form select:focus,.pure-form textarea:focus{outline:0;outline:thin dotted \9;border-color:#129FEA}.pure-form input[type=file]:focus,.pure-form input[type=radio]:focus,.pure-form input[type=checkbox]:focus{outline:thin dotted #333;outline:1px auto #129FEA}.pure-form .pure-checkbox,.pure-form .pure-radio{margin:.5em 0;display:block}.pure-form input[type=text][disabled],.pure-form input[type=password][disabled],.pure-form input[type=email][disabled],.pure-form input[type=url][disabled],.pure-form input[type=date][disabled],.pure-form input[type=month][disabled],.pure-form input[type=time][disabled],.pure-form input[type=datetime][disabled],.pure-form input[type=datetime-local][disabled],.pure-form input[type=week][disabled],.pure-form input[type=number][disabled],.pure-form input[type=search][disabled],.pure-form input[type=tel][disabled],.pure-form input[type=color][disabled],.pure-form select[disabled],.pure-form textarea[disabled]{cursor:not-allowed;background-color:#eaeded;color:#cad2d3}.pure-form input[readonly],.pure-form select[readonly],.pure-form textarea[readonly]{background:#eee;color:#777;border-color:#ccc}.pure-form input:focus:invalid,.pure-form textarea:focus:invalid,.pure-form select:focus:invalid{color:#b94a48;border:1px solid #ee5f5b}.pure-form input:focus:invalid:focus,.pure-form textarea:focus:invalid:focus,.pure-form select:focus:invalid:focus{border-color:#e9322d}.pure-form input[type=file]:focus:invalid:focus,.pure-form input[type=radio]:focus:invalid:focus,.pure-form input[type=checkbox]:focus:invalid:focus{outline-color:#e9322d}.pure-form select{border:1px solid #ccc;background-color:#fff}.pure-form select[multiple]{height:auto}.pure-form label{margin:.5em 0 .2em;font-size:90%}.pure-form fieldset{margin:0;padding:.35em 0 .75em;border:0}.pure-form legend{display:block;width:100%;padding:.3em 0;margin-bottom:.3em;font-size:125%;color:#333;border-bottom:1px solid #e5e5e5}.pure-form-stacked input[type=text],.pure-form-stacked input[type=password],.pure-form-stacked input[type=email],.pure-form-stacked input[type=url],.pure-form-stacked input[type=date],.pure-form-stacked input[type=month],.pure-form-stacked input[type=time],.pure-form-stacked input[type=datetime],.pure-form-stacked input[type=datetime-local],.pure-form-stacked input[type=week],.pure-form-stacked input[type=number],.pure-form-stacked input[type=search],.pure-form-stacked input[type=tel],.pure-form-stacked input[type=color],.pure-form-stacked select,.pure-form-stacked label,.pure-form-stacked textarea{display:block;margin:.25em 0}.pure-form-aligned input,.pure-form-aligned textarea,.pure-form-aligned select,.pure-form-aligned .pure-help-inline,.pure-form-message-inline{display:inline-block;*display:inline;*zoom:1;vertical-align:middle}.pure-form-aligned .pure-control-group{margin-bottom:.5em}.pure-form-aligned .pure-control-group label{text-align:right;display:inline-block;vertical-align:middle;width:10em;margin:0 1em 0 0}.pure-form-aligned .pure-controls{margin:1.5em 0 0 10em}.pure-form input.pure-input-rounded,.pure-form .pure-input-rounded{border-radius:2em;padding:.5em 1em}.pure-form .pure-group fieldset{margin-bottom:10px}.pure-form .pure-group input{display:block;padding:10px;margin:0;border-radius:0;position:relative;top:-1px}.pure-form .pure-group input:focus{z-index:2}.pure-form .pure-group input:first-child{top:1px;border-radius:4px 4px 0 0}.pure-form .pure-group input:last-child{top:-2px;border-radius:0 0 4px 4px}.pure-form .pure-group button{margin:.35em 0}.pure-form .pure-input-1{width:100%}.pure-form .pure-input-2-3{width:66%}.pure-form .pure-input-1-2{width:50%}.pure-form .pure-input-1-3{width:33%}.pure-form .pure-input-1-4{width:25%}.pure-form .pure-help-inline,.pure-form-message-inline{display:inline-block;padding-left:.3em;color:#666;vertical-align:middle;font-size:90%}.pure-form-message{display:block;color:#666;font-size:90%}@media only screen and (max-width :480px){.pure-form button[type=submit]{margin:.7em 0 0}.pure-form input[type=text],.pure-form input[type=password],.pure-form input[type=email],.pure-form input[type=url],.pure-form input[type=date],.pure-form input[type=month],.pure-form input[type=time],.pure-form input[type=datetime],.pure-form input[type=datetime-local],.pure-form input[type=week],.pure-form input[type=number],.pure-form input[type=search],.pure-form input[type=tel],.pure-form input[type=color],.pure-form label{margin-bottom:.3em;display:block}.pure-group input[type=text],.pure-group input[type=password],.pure-group input[type=email],.pure-group input[type=url],.pure-group input[type=date],.pure-group input[type=month],.pure-group input[type=time],.pure-group input[type=datetime],.pure-group input[type=datetime-local],.pure-group input[type=week],.pure-group input[type=number],.pure-group input[type=search],.pure-group input[type=tel],.pure-group input[type=color]{margin-bottom:0}.pure-form-aligned .pure-control-group label{margin-bottom:.3em;text-align:left;display:block;width:100%}.pure-form-aligned .pure-controls{margin:1.5em 0 0}.pure-form .pure-help-inline,.pure-form-message-inline,.pure-form-message{display:block;font-size:80%;padding:.2em 0 .8em}}.pure-g{letter-spacing:-.31em;*letter-spacing:normal;*word-spacing:-.43em;text-rendering:optimizespeed;font-family:FreeSans,Arimo,"Droid Sans",Helvetica,Arial,sans-serif;display:-webkit-flex;-webkit-flex-flow:row wrap;display:-ms-flexbox;-ms-flex-flow:row wrap}.opera-only :-o-prefocus,.pure-g{word-spacing:-.43em}.pure-u{display:inline-block;*display:inline;zoom:1;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-g [class *="pure-u"]{font-family:sans-serif}.pure-u-1,.pure-u-1-2,.pure-u-1-3,.pure-u-2-3,.pure-u-1-4,.pure-u-3-4,.pure-u-1-5,.pure-u-2-5,.pure-u-3-5,.pure-u-4-5,.pure-u-1-6,.pure-u-5-6,.pure-u-1-8,.pure-u-3-8,.pure-u-5-8,.pure-u-7-8,.pure-u-1-12,.pure-u-5-12,.pure-u-7-12,.pure-u-11-12,.pure-u-1-24,.pure-u-5-24,.pure-u-7-24,.pure-u-11-24,.pure-u-13-24,.pure-u-17-24,.pure-u-19-24,.pure-u-23-24{display:inline-block;*display:inline;zoom:1;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-u-1{width:100%}.pure-u-1-2{width:50%;*width:49.969%}.pure-u-1-3{width:33.3333%;*width:33.3023%}.pure-u-2-3{width:66.6667%;*width:66.6357%}.pure-u-1-4{width:25%;*width:24.969%}.pure-u-3-4{width:75%;*width:74.969%}.pure-u-1-5{width:20%;*width:19.969%}.pure-u-2-5{width:40%;*width:39.969%}.pure-u-3-5{width:60%;*width:59.969%}.pure-u-4-5{width:80%;*width:79.969%}.pure-u-1-6{width:16.6667%;*width:16.6357%}.pure-u-5-6{width:83.3333%;*width:83.3023%}.pure-u-1-8{width:12.5%;*width:12.469%}.pure-u-3-8{width:37.5%;*width:37.469%}.pure-u-5-8{width:62.5%;*width:62.469%}.pure-u-7-8{width:87.5%;*width:87.469%}.pure-u-1-12{width:8.3333%;*width:8.3023%}.pure-u-5-12{width:41.6667%;*width:41.6357%}.pure-u-7-12{width:58.3333%;*width:58.3023%}.pure-u-11-12{width:91.6667%;*width:91.6357%}.pure-u-1-24{width:4.1667%;*width:4.1357%}.pure-u-5-24{width:20.8333%;*width:20.8023%}.pure-u-7-24{width:29.1667%;*width:29.1357%}.pure-u-11-24{width:45.8333%;*width:45.8023%}.pure-u-13-24{width:54.1667%;*width:54.1357%}.pure-u-17-24{width:70.8333%;*width:70.8023%}.pure-u-19-24{width:79.1667%;*width:79.1357%}.pure-u-23-24{width:95.8333%;*width:95.8023%}.pure-g-r{letter-spacing:-.31em;*letter-spacing:normal;*word-spacing:-.43em;font-family:FreeSans,Arimo,"Droid Sans",Helvetica,Arial,sans-serif;display:-webkit-flex;-webkit-flex-flow:row wrap;display:-ms-flexbox;-ms-flex-flow:row wrap}.opera-only :-o-prefocus,.pure-g-r{word-spacing:-.43em}.pure-g-r [class *="pure-u"]{font-family:sans-serif}.pure-g-r img{max-width:100%;height:auto}@media (min-width:980px){.pure-visible-phone{display:none}.pure-visible-tablet{display:none}.pure-hidden-desktop{display:none}}@media (max-width:480px){.pure-g-r>.pure-u,.pure-g-r>[class *="pure-u-"]{width:100%}}@media (max-width:767px){.pure-g-r>.pure-u,.pure-g-r>[class *="pure-u-"]{width:100%}.pure-hidden-phone{display:none}.pure-visible-desktop{display:none}}@media (min-width:768px) and (max-width:979px){.pure-hidden-tablet{display:none}.pure-visible-desktop{display:none}}.pure-menu ul{position:absolute;visibility:hidden}.pure-menu.pure-menu-open{visibility:visible;z-index:2;width:100%}.pure-menu ul{left:-10000px;list-style:none;margin:0;padding:0;top:-10000px;z-index:1}.pure-menu>ul{position:relative}.pure-menu-open>ul{left:0;top:0;visibility:visible}.pure-menu-open>ul:focus{outline:0}.pure-menu li{position:relative}.pure-menu a,.pure-menu .pure-menu-heading{display:block;color:inherit;line-height:1.5em;padding:5px 20px;text-decoration:none;white-space:nowrap}.pure-menu.pure-menu-horizontal>.pure-menu-heading{display:inline-block;*display:inline;zoom:1;margin:0;vertical-align:middle}.pure-menu.pure-menu-horizontal>ul{display:inline-block;*display:inline;zoom:1;vertical-align:middle;height:2.4em}.pure-menu li a{padding:5px 20px}.pure-menu-can-have-children>.pure-menu-label:after{content:'\25B8';float:right;font-family:'Lucida Grande','Lucida Sans Unicode','DejaVu Sans',sans-serif;margin-right:-20px;margin-top:-1px}.pure-menu-can-have-children>.pure-menu-label{padding-right:30px}.pure-menu-separator{background-color:#dfdfdf;display:block;height:1px;font-size:0;margin:7px 2px;overflow:hidden}.pure-menu-hidden{display:none}.pure-menu-fixed{position:fixed;top:0;left:0;width:100%}.pure-menu-horizontal li{display:inline-block;*display:inline;zoom:1;vertical-align:middle}.pure-menu-horizontal li li{display:block}.pure-menu-horizontal>.pure-menu-children>.pure-menu-can-have-children>.pure-menu-label:after{content:"\25BE"}.pure-menu-horizontal>.pure-menu-children>.pure-menu-can-have-children>.pure-menu-label{padding-right:30px}.pure-menu-horizontal li.pure-menu-separator{height:50%;width:1px;margin:0 7px}.pure-menu-horizontal li li.pure-menu-separator{height:1px;width:auto;margin:7px 2px}.pure-menu.pure-menu-open,.pure-menu.pure-menu-horizontal li .pure-menu-children{background:#fff;border:1px solid #b7b7b7}.pure-menu.pure-menu-horizontal,.pure-menu.pure-menu-horizontal .pure-menu-heading{border:0}.pure-menu a{border:1px solid transparent;border-left:0;border-right:0}.pure-menu a,.pure-menu .pure-menu-can-have-children>li:after{color:#777}.pure-menu .pure-menu-can-have-children>li:hover:after{color:#fff}.pure-menu .pure-menu-open{background:#dedede}.pure-menu li a:hover,.pure-menu li a:focus{background:#eee}.pure-menu li.pure-menu-disabled a:hover,.pure-menu li.pure-menu-disabled a:focus{background:#fff;color:#bfbfbf}.pure-menu .pure-menu-disabled>a{background-image:none;border-color:transparent;cursor:default}.pure-menu .pure-menu-disabled>a,.pure-menu .pure-menu-can-have-children.pure-menu-disabled>a:after{color:#bfbfbf}.pure-menu .pure-menu-heading{color:#565d64;text-transform:uppercase;font-size:90%;margin-top:.5em;border-bottom-width:1px;border-bottom-style:solid;border-bottom-color:#dfdfdf}.pure-menu .pure-menu-selected a{color:#000}.pure-menu.pure-menu-open.pure-menu-fixed{border:0;border-bottom:1px solid #b7b7b7}.pure-paginator{letter-spacing:-.31em;*letter-spacing:normal;*word-spacing:-.43em;text-rendering:optimizespeed;list-style:none;margin:0;padding:0}.opera-only :-o-prefocus,.pure-paginator{word-spacing:-.43em}.pure-paginator li{display:inline-block;*display:inline;zoom:1;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-paginator .pure-button{border-radius:0;padding:.8em 1.4em;vertical-align:top;height:1.1em}.pure-paginator .pure-button:focus,.pure-paginator .pure-button:active{outline-style:none}.pure-paginator .prev,.pure-paginator .next{color:#C0C1C3;text-shadow:0 -1px 0 rgba(0,0,0,.45)}.pure-paginator .prev{border-radius:2px 0 0 2px}.pure-paginator .next{border-radius:0 2px 2px 0}@media (max-width:480px){.pure-menu-horizontal{width:100%}.pure-menu-children li{display:block;border-bottom:1px solid #000}}.pure-table{border-collapse:collapse;border-spacing:0;empty-cells:show;border:1px solid #cbcbcb}.pure-table caption{color:#000;font:italic 85%/1 arial,sans-serif;padding:1em 0;text-align:center}.pure-table td,.pure-table th{border-left:1px solid #cbcbcb;border-width:0 0 0 1px;font-size:inherit;margin:0;overflow:visible;padding:6px 12px}.pure-table td:first-child,.pure-table th:first-child{border-left-width:0}.pure-table thead{background:#e0e0e0;color:#000;text-align:left;vertical-align:bottom}.pure-table td{background-color:transparent}.pure-table-odd td{background-color:#f2f2f2}.pure-table-striped tr:nth-child(2n-1) td{background-color:#f2f2f2}.pure-table-bordered td{border-bottom:1px solid #cbcbcb}.pure-table-bordered tbody>tr:last-child td,.pure-table-horizontal tbody>tr:last-child td{border-bottom-width:0}.pure-table-horizontal td,.pure-table-horizontal th{border-width:0 0 1px;border-bottom:1px solid #cbcbcb}.pure-table-horizontal tbody>tr:last-child td{border-bottom-width:0} \ No newline at end of file diff --git a/stack/monitor2/webfile-directory/static/script.js b/stack/monitor2/webfile-directory/static/script.js new file mode 100644 index 000000000..db735ee2c --- /dev/null +++ b/stack/monitor2/webfile-directory/static/script.js @@ -0,0 +1,35 @@ +$(document).ready(function() { + function doDataUrl (data) { + var frame_content = document.getElementsByTagName("iframe")[0].contentWindow; + var b64 = btoa(data); + dataurl = 'data:text/html;base64,' + b64; + $("iframe").attr('src', dataurl); + } + + if ( window.self === window.top ) { + //not in an iframe + $(".script").click(function(e) { + e.preventDefault(); + var message = $(this).attr('href'); + var slash_pos = message.search('/'); + //let's differenciate kind of script called + if ( slash_pos === -1 || slash_pos === 0) { + url = message; + } + else { + url = '/index.cgi'; + } + + $("iframe").attr('src', url + '?script=' + encodeURIComponent(message)); + }); + $(".link").click(function(e) { + e.preventDefault(); + var url = $(this).attr('href'); + $("iframe").attr('src', url); + }); + } + else { + //in an iframe + $("body").empty(); + } +}); diff --git a/stack/monitor2/webfile-directory/static/style.css b/stack/monitor2/webfile-directory/static/style.css new file mode 100644 index 000000000..d0a4bb5ea --- /dev/null +++ b/stack/monitor2/webfile-directory/static/style.css @@ -0,0 +1,31 @@ +body { + padding: 15px; +} + +.pure-menu .pure-menu-heading { + font-size: 120%; +} + +#content { + display: inline-block; + min-width: 72%; + height: 97%; + margin-left: 30px; +} + +#div-menu { + display: inline-block; + vertical-align: top; +} + +#div-menu h1 { + text-align: center; +} + +iframe { + width: 100%; + height: 100%; + margin: 0px; + padding: 0px; + border-style: none; +} diff --git a/stack/monitor2/webfile-directory/static/welcome.html b/stack/monitor2/webfile-directory/static/welcome.html new file mode 100644 index 000000000..a2216263d --- /dev/null +++ b/stack/monitor2/webfile-directory/static/welcome.html @@ -0,0 +1,11 @@ +<html> +<head> + <title>Welcome to the Monitoring Interface</title> + <link rel="stylesheet" href="pure-min.css"> + <link rel="stylesheet" href="style.css"> +</head> +<body> +<h1>Welcome to your monitoring interface</h1> +<p>From this interface you can monitor, configure your instance</p> +</body> +</html> diff --git a/stack/monitor2/webfile-directory/status-history.cgi.in b/stack/monitor2/webfile-directory/status-history.cgi.in new file mode 100644 index 000000000..33edc7d12 --- /dev/null +++ b/stack/monitor2/webfile-directory/status-history.cgi.in @@ -0,0 +1,44 @@ +#!{{ python_executable }} + +import cgi +import datetime +import os +import sqlite3 + +db_path = '{{ monitor_db_path }}' + +status_history_length = '{{ status_history_length }}' + +db = sqlite3.connect(db_path) + +print """<html><head> + <link rel="stylesheet" href="static/pure-min.css"> + <link rel="stylesheet" href="static/style.css"> + </head><body> + <h1>Monitor Status History :</h1>""" + + +def get_date_from_timestamp(timestamp): + return datetime.datetime.fromtimestamp(timestamp).strftime('%Y-%m-%d %H:%M:%S') + +def print_individual_status(timestamp): + print "<div><h3>Failure on %s</h3><ul>" % get_date_from_timestamp(timestamp) + rows = db.execute("select status, element, output from individual_status where timestamp=?", (timestamp,)) + for row in rows: + status, element, output = row + print "<li>%s , %s :</br><pre>%s</pre></li>" % (status, cgi.escape(element), cgi.escape(output)) + print "</ul></div>" + + + +if not os.path.exists(db_path): + print """No status history found</p></body></html>""" + exit(0) + +failure_row_list = db.execute("select timestamp from status where status='FAILURE' order by timestamp desc limit ?", status_history_length ) + +for failure_row in failure_row_list: + timestamp, = failure_row + print_individual_status(timestamp) + +print "</body></html>" diff --git a/stack/monitor2/webfile-directory/status.cgi.in b/stack/monitor2/webfile-directory/status.cgi.in new file mode 100755 index 000000000..b15b92cd5 --- /dev/null +++ b/stack/monitor2/webfile-directory/status.cgi.in @@ -0,0 +1,57 @@ +#!{{ python_executable }} + +import cgi +import cgitb +import json +import os +import subprocess + +def refresh(): + command = ["{{ monitor_bin }}", "-a"] + subprocess.call(command) + +cgitb.enable(display=0, logdir="/tmp/cgi.log") +form = cgi.FieldStorage() + +json_file = "{{ json_file }}" + +if not os.path.exists(json_file) or "refresh" in form: + refresh() + +if not os.path.exists(json_file): + print """<html><head> + <link rel="stylesheet" href="static/pure-min.css"> + <link rel="stylesheet" href="static/style.css"> + </head><body> + <h1>Monitoring :</h1> + No status file found</p></body></html>""" + exit(0) + +result = json.load(open(json_file)) + +print "<html><head>" +print "<link rel=\"stylesheet\" href=\"static/pure-min.css\">" +print "<link rel=\"stylesheet\" href=\"static/style.css\">" +print "</head><body>" +print "<h1>Monitoring :</h1>" +print "<form action=\"/index.cgi\" method=\"post\" class=\"pure-form-aligned\">" +print "<input type=\"hidden\" name=\"posting-script\" value=\"{{ pwd }}/{{ this_file }}\">" +print "<p><em>Last time of monitoring process : %s</em></p>" % (result['datetime']) +del result['datetime'] +print "<div class=\"pure-controls\"><button type=\"submit\" class=\"pure-button \ + pure-button-primary\" name=\"refresh\" value=\"refresh\">Refresh</button></div></form>" +print "<br/>" + +print "<h2>These scripts and promises have failed :</h2>" +for r in result: + if result[r] != '': + print "<h3>%s</h3><pre style=\"padding-left:30px;\">%s</pre>" % (cgi.escape(r), cgi.escape(result[r])) +print "<br/>" + +print "<h2>These scripts and promises were successful :</h2>" +print "<ul>" +for r in result: + if result[r] == '': + print "<li>%s</li>" % (r) +print "</ul>" +print "</body></html>" diff --git a/stack/monitor2/wrapper.in b/stack/monitor2/wrapper.in new file mode 100644 index 000000000..6fa3fa47b --- /dev/null +++ b/stack/monitor2/wrapper.in @@ -0,0 +1,2 @@ +#!${dash-output:dash} +{{ content }} \ No newline at end of file -- 2.30.9