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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
+  }
+
+  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