Commit 8cf6cb4c authored by Alain Takoudjou's avatar Alain Takoudjou Committed by Rafael Monnerat

SlapOS Monitoring stack

Monitoring stack, included in webrunner and KVM SR

/reviewed-on !66
parents 5d9b1dd7 141af519
......@@ -43,9 +43,9 @@ environment =
[debian-amd64-netinst.iso]
# Download the installer of Debian 8 (Jessie)
recipe = hexagonit.recipe.download
url = http://cdimage.debian.org/debian-cd/8.2.0/amd64/iso-cd/debian-8.2.0-amd64-netinst.iso
url = http://cdimage.debian.org/debian-cd/8.3.0/amd64/iso-cd/debian-8.3.0-amd64-netinst.iso
filename = ${:_buildout_section_name_}
md5sum = 762eb3dfc22f85faf659001ebf270b4f
md5sum = a9b490b4215d1e72e876b031dafa7184
download-only = true
mode = 0644
location = ${buildout:parts-directory}/${:_buildout_section_name_}
......@@ -68,6 +68,10 @@ class Recipe(object):
${storage-configuration:storage-home}
Output:
root-instance-title
Hosting subscription or root instance title
instance-title
Title of instance running into this partition
slap-software-type
Current partition's software type.
ipv4
......@@ -146,6 +150,12 @@ class Recipe(object):
pass
else:
options[his_key.replace('_', '-')] = value
# Get Instance and root instance title or return UNKNOW if not set
options['instance-title'] = parameter_dict.pop('instance_title',
'UNKNOW Instance')
options['root-instance-title'] = parameter_dict.pop('root_instance_title',
'UNKNOW')
ipv4_set = set()
v4_add = ipv4_set.add
ipv6_set = set()
......
......@@ -90,7 +90,7 @@ command =
[template]
recipe = slapos.recipe.template
url = ${:_profile_base_location_}/instance.cfg.in
md5sum = ac94fdcf8e3db4bdb2dff4478426595d
md5sum = c597309c00b657db92f8c43e733b1763
output = ${buildout:directory}/template.cfg
mode = 0644
......@@ -98,7 +98,7 @@ mode = 0644
recipe = hexagonit.recipe.download
url = ${:_profile_base_location_}/instance-kvm.cfg.jinja2
mode = 644
md5sum = e72f42d880877a841e87908566c28610
md5sum = de733ac612bf498199e68d1d6b7d8ac9
download-only = true
on-update = true
......@@ -106,7 +106,7 @@ on-update = true
recipe = hexagonit.recipe.download
url = ${:_profile_base_location_}/instance-kvm-cluster.cfg.jinja2.in
mode = 644
md5sum = 6e81c08669e164b852bd8d062c620de2
md5sum = 24a717e6ccadf5708b8d5d82a75a7b25
download-only = true
on-update = true
......@@ -114,7 +114,7 @@ on-update = true
recipe = hexagonit.recipe.download
url = ${:_profile_base_location_}/instance-kvm-resilient.cfg.jinja2
mode = 644
md5sum = 7564bfbb74e6557e1041e9d6d1bc5d14
md5sum = c8481ad7ef56b245e89df76cd19242db
download-only = true
on-update = true
......@@ -127,11 +127,12 @@ download-only = true
on-update = true
[template-kvm-import]
recipe = slapos.recipe.template
recipe = hexagonit.recipe.download
url = ${:_profile_base_location_}/instance-kvm-import.cfg.in
md5sum = 6835c9309ff4bf4a0efd1850e6c66b24
output = ${buildout:directory}/template-kvm-import.cfg
md5sum = 3177381b65b4b95ba29190a6ac03b771
mode = 0644
download-only = true
on-update = true
[template-kvm-import-script]
recipe = hexagonit.recipe.download
......@@ -145,7 +146,7 @@ mode = 0755
recipe = hexagonit.recipe.download
url = ${:_profile_base_location_}/instance-kvm-export.cfg.jinja2
mode = 644
md5sum = c9f13c1f481ed08c75089aef1d3c6981
md5sum = ff281bf8a8905632b32254622db105b7
download-only = true
on-update = true
......
......@@ -151,6 +151,18 @@
"description": "Text content which will be written in a file data of cluster http server. All VM will be able to download that file via the static URL of cluster HTTP server: https://10.0.2.101/FOLDER_HASH/data.",
"type": "string"
},
"monitor-interface-url": {
"title": "Monitor Web Interface URL",
"description": "Give Url of HTML web interface that will be used to render this monitor instance.",
"type": "string",
"format": "uri"
},
"monitor-cors-domains": {
"title": "Monitor CORS domains",
"description": "List of cors domains separated with space. Needed for ajax query on this monitor instance from a different domain.",
"type": "string",
"default": ""
},
"kvm-partition-dict": {
"title": "kvm instances definition",
"description": "kvm instances definition",
......
......@@ -8,6 +8,7 @@
{% set slave_frontend_iguid = slave_frontend_dict.get('instance-guid', '') -%}
{% set kvm_instance_dict = {} -%}
{% set kvm_hostname_list = [] -%}
{% set monitor_url_list = [] -%}
[request-common]
recipe = slapos.cookbook:request
......@@ -71,6 +72,9 @@ config-httpd-port = {{ dumps(kvm_parameter_dict.get('httpd-port', 8081)) }}
config-data-to-vm = {{ dumps(kvm_parameter_dict.get('data-to-vm', '')) }}
{% endif -%}
config-enable-monitor = {{ dumps(kvm_parameter_dict.get('enable-monitor', True)) }}
config-monitor-cors-domains = {{ slapparameter_dict.get('monitor-cors-domains', 'monitor.node.vifib.com') }}
config-monitor-username = ${monitor-htpasswd:username}
config-monitor-password = ${monitor-htpasswd:passwd}
# Enable simple http server on ipv6 so all VMs will access it
config-document-host = ${apache-conf:ip}
......@@ -86,8 +90,9 @@ sla-fw_rejected_sources = {{ rejected_source_list | join(' ') }}
sla-fw_restricted_access = {{ dumps(slapparameter_dict.get('fw-restricted-access', 'off')) }}
return =
backend-url
url
backend-url
monitor-base-url
{% if str(use_nat).lower() == 'true' -%}
{% for port in nat_rules_list -%}
{{ ' ' }}nat-rule-url-{{ port }}
......@@ -99,6 +104,9 @@ return =
{% do publish_dict.__setitem__('lan-' ~ instance_name, '${' ~ section ~ ':connection-tap-ipv4}') -%}
{% do kvm_hostname_list.append(instance_name ~ ' ' ~ '${' ~ section ~ ':connection-tap-ipv4}') -%}
{% endif -%}
{% if str(kvm_parameter_dict.get('enable-monitor', 'True')).lower() == 'true' -%}
{% do monitor_url_list.append('${' ~ section ~ ':connection-monitor-base-url}') -%}
{% endif -%}
{% do publish_dict.__setitem__(instance_name ~ '-backend-url', '${' ~ section ~ ':connection-backend-url}') -%}
{% do publish_dict.__setitem__(instance_name ~ '-url', '${' ~ section ~ ':connection-url}') -%}
{% do kvm_instance_dict.__setitem__(instance_name, (use_nat, nat_rules_list)) -%}
......@@ -206,14 +214,41 @@ mode = {{ mode }}
{{ writefile('cluster-data-content', '${directory:webroot}/${hash-code:passwd}/data', slapparameter_dict.get('cluster-data', ''), '700') }}
{% endif -%}
[monitor-htpasswd]
recipe = slapos.cookbook:generate.password
storage-path = ${directory:etc}/.monitor_user
bytes = 8
username = admin
[monitor-instance-parameter]
monitor-httpd-port = 8060
monitor-title = KVM Cluster Main Instance
cors-domains = {{ slapparameter_dict.get('monitor-cors-domains', '') }}
username = ${monitor-htpasswd:username}
password = ${monitor-htpasswd:passwd}
[monitor-conf-parameters]
monitor-url-list +=
{% for url in monitor_url_list -%}
{{ ' ' ~ url }}
{% endfor %}
private-path-list +=
${directory:webroot}/
[publish]
recipe = slapos.cookbook:publish
{% for name, value in publish_dict.items() -%}
{{ name }} = {{ value }}
{% endfor %}
{% set monitor_interface_url = slapparameter_dict.get('monitor-interface-url', 'https://monitor.node.vifib.com') -%}
{% if monitor_interface_url -%}
monitor-setup-url = {{ monitor_interface_url }}/#page=settings_configurator&url=${publish:monitor-url}
{% endif -%}
[buildout]
extends =
{{ template_httpd_cfg }}
{{ template_monitor }}
parts =
httpd
......@@ -221,6 +256,18 @@ parts =
httpd-promise
publish
directory-doc
monitor-base
cron-entry-logrotate
certificate-authority
monitor-conf
start-monitor
ca-httpd
monitor-httpd-promise
monitor-httpd-promise-conf
monitor-status2rss-cron-entry
# End monitor
# Complete parts with sections
{{ part_list | join('\n ') }}
......
{% set monitor = True -%}
{% if slapparameter_dict.get('enable-monitor', 'True').lower() == 'false' -%}
{% set monitor = False -%}
{% endif -%}
[buildout]
extends =
{{ kvm_template }}
......@@ -14,6 +20,18 @@ parts +=
novnc-promise
cron
frontend-promise
{% if monitor -%}
# monitor parts
monitor-base
cron-entry-logrotate
certificate-authority
monitor-conf
start-monitor
ca-httpd
monitor-httpd-promise
monitor-httpd-promise-conf
monitor-status2rss-cron-entry
{% endif %}
# Create the exporter executable, which is a simple shell script
[exporter]
......
[buildout]
eggs-directory = {{ eggs_directory }}
develop-eggs-directory = {{ develop_eggs_directory }}
offline = true
# Here, we don't need KVM to run to import data, so we don't
# even extend the kvm instance profile.
extends = ${pbsready-import:output}
extends =
{{ pbsready_import_template }}
{% if slapparameter_dict.get('enable-monitor', 'True').lower() == 'true' -%}
{{ ' ' ~ template_monitor }}
eggs-directory = ${buildout:eggs-directory}
develop-eggs-directory = ${buildout:develop-eggs-directory}
offline = true
[resilient-publish-connection-parameter]
monitor-base-url = ${publish:monitor-base-url}
monitor-url = ${publish:monitor-url}
monitor-user = ${publish:monitor-user}
monitor-password = ${publish:monitor-password}
[monitor-instance-parameter]
monitor-httpd-port = 8276
monitor-title = {{ slapparameter_dict.get('name', 'Kvm Resilient clone') }}
cors-domains = {{ slapparameter_dict.get('monitor-cors-domains', '') }}
{% if slapparameter_dict.get('monitor-username', '') -%}
username = {{ slapparameter_dict['monitor-username'] }}
{% endif -%}
{% if slapparameter_dict.get('monitor-password', '') -%}
password = {{ slapparameter_dict['monitor-password'] }}
{% endif -%}
instance-configuration =
raw takeover-url ${resilient-publish-connection-parameter:takeover-url}
raw takeover-password ${resilient-publish-connection-parameter:takeover-password}
{% endif -%}
[directory]
recipe = slapos.cookbook:mkdirectory
etc = $${buildout:directory}/etc
bin = $${buildout:directory}/bin
srv = $${buildout:directory}/srv
var = $${buildout:directory}/var
log = $${:var}/log
scripts = $${:etc}/run
services = $${:etc}/service
promises = $${:etc}/promise
novnc-conf = $${:etc}/novnc
run = $${:var}/run
ca-dir = $${:srv}/ssl
cron-entries = $${:etc}/cron.d
crontabs = $${:etc}/crontabs
cronstamps = $${:etc}/cronstamps
etc = ${buildout:directory}/etc
bin = ${buildout:directory}/bin
srv = ${buildout:directory}/srv
var = ${buildout:directory}/var
log = ${:var}/log
scripts = ${:etc}/run
services = ${:etc}/service
promises = ${:etc}/promise
novnc-conf = ${:etc}/novnc
run = ${:var}/run
ca-dir = ${:srv}/ssl
cron-entries = ${:etc}/cron.d
crontabs = ${:etc}/crontabs
cronstamps = ${:etc}/cronstamps
[importer]
recipe = slapos.recipe.template:jinja2
template = ${template-kvm-import-script:location}/${template-kvm-import-script:filename}
rendered = $${directory:bin}/$${slap-parameter:namebase}-importer
template = {{ template_kvm_import }}
rendered = ${directory:bin}/${slap-parameter:namebase}-importer
mode = 0700
# Resilient stack wants a "wrapper" parameter
wrapper = $${:rendered}
wrapper = ${:rendered}
context =
section directory directory
raw zcat_binary ${gzip:location}/bin/zcat
raw gzip_binary ${gzip:location}/bin/gzip
raw zcat_binary {{ zcat_binary }}
raw gzip_binary {{ gzip_binary }}
backup-disk-path = $${directory:backup}/virtual.qcow2
backup-disk-path = ${directory:backup}/virtual.qcow2
......@@ -163,6 +163,19 @@
"type": "boolean",
"default": true
},
"monitor-interface-url": {
"title": "Monitor Web Interface URL",
"description": "Give Url of HTML web interface that will be used to render this monitor instance.",
"type": "string",
"format": "uri",
"default": "https://monitor.node.vifib.com"
},
"monitor-cors-domains": {
"title": "Monitor CORS domains",
"description": "List of cors domains separated with space. Needed for ajax query on this monitor instance from a different domain.",
"type": "string",
"default": "monitor.node.vifib.com"
},
"enable-http-server": {
"title": "Enable local http server",
"description": "Set if local http server which serve files to the vm should be deployed. If set to true, get file into the vm with URL: http://10.0.2.100/FILE.",
......
......@@ -4,6 +4,14 @@
{% import 'replicated' as replicated with context %}
{% set backup_amount = slapparameter_dict.pop('resilient-clone-number', "1")|int + 1 -%}
{% set monitor_dict = {} -%}
{% if slapparameter_dict.get('enable-monitor', 'True').lower() == 'true' -%}
{% set monitor_return = ['monitor-base-url', 'monitor-url', 'monitor-user', 'monitor-password'] -%}
{% set monitor_parameter = {'monitor-cors-domains': slapparameter_dict.pop('monitor-cors-domains', "monitor.node.vifib.com")} -%}
{% set monitor_dict = {'parameter': monitor_parameter, 'return': monitor_return} -%}
{% endif -%}
{% set monitor_interface_url = slapparameter_dict.pop('monitor-interface-url', 'https://monitor.node.vifib.com') -%}
[buildout]
eggs-directory = {{ eggs_directory }}
......@@ -17,7 +25,21 @@ parts +=
kvm-frontend-url-promise
kvm-backend-url-promise
{{ replicated.replicate("kvm", backup_amount, "kvm-export", "kvm-import", slapparameter_dict=slapparameter_dict) }}
{% if slapparameter_dict.get('enable-monitor', 'True').lower() == 'true' -%}
extends = {{ template_monitor }}
[monitor-htpasswd]
recipe = slapos.cookbook:generate.password
storage-path = ${directory:etc}/.monitor_user
bytes = 8
username = admin
{% do monitor_parameter.__setitem__('monitor-username', slapparameter_dict.get('monitor-username', 'admin'))%}
{% do monitor_parameter.__setitem__('monitor-password', slapparameter_dict.get('monitor-password', '${monitor-htpasswd:passwd}'))%}
{% endif -%}
{{ replicated.replicate("kvm", backup_amount, "kvm-export", "kvm-import", slapparameter_dict=slapparameter_dict, monitor_parameter_dict=monitor_dict) }}
[directory]
recipe = slapos.cookbook:mkdirectory
......@@ -29,16 +51,28 @@ promises = ${:etc}/promise
# Note: += doesn't work.
return =
# Resilient related parameters
url ssh-public-key ssh-url notification-id ip
url ssh-public-key ssh-url notification-id ip {{ monitor_return | join(' ') }}
# KVM related parameters
# XXX: return ALL parameters (like nat rules), through jinja
backend-url url ip
# XXX Monitoring Main Instane
[monitor-instance-parameter]
monitor-httpd-port = 8160
cors-domains = {{ monitor_parameter.get('monitor-cors-domains', '') }}
[publish-connection-information]
recipe = slapos.cookbook:publish
backend-url = ${request-kvm:connection-backend-url}
url = ${request-kvm:connection-url}
ipv6 = ${request-kvm:connection-ip}
monitor-base-url = ${publish:monitor-base-url}
monitor-url = ${publish:monitor-url}
monitor-user = ${publish:monitor-user}
monitor-password = ${publish:monitor-password}
{% if monitor_interface_url -%}
monitor-setup-url = {{ monitor_interface_url }}/#page=settings_configurator&url=${publish:monitor-url}
{% endif -%}
[kvm-frontend-url-promise]
# Check that url parameter is complete
......
......@@ -316,40 +316,18 @@ port = ${httpd:port}
{% endif %}
{% if monitor -%}
[monitor-access-log]
< = monitor-directory-access
source = ${directory:log}
[monitor-access-public]
< = monitor-directory-access
source = ${directory:public}
[monitor-parameters]
port = 8026
{% if instance_type == 'cluster' -%}
# XXX - Set frontend software type to 'custom-personal' by default for cluster instance
{% set frontend_software_type = 'custom-personal' -%}
[monitor-instance-parameter]
monitor-httpd-port = 8026
monitor-title = {{ slapparameter_dict.get('name', 'KVM Standalone') }}
cors-domains = {{ slapparameter_dict.get('monitor-cors-domains', 'monitor.node.vifib.com') }}
{% if slapparameter_dict.get('monitor-username', '') -%}
username = {{ slapparameter_dict['monitor-username'] }}
{% endif -%}
{% if slapparameter_dict.get('monitor-password', '') -%}
password = {{ slapparameter_dict['monitor-password'] }}
{% endif -%}
[request-monitor-frontend]
<= slap-connection
recipe = slapos.cookbook:requestoptional
name = Monitor {{ slapparameter_dict.get('name', '') }} Frontend
# XXX We have hardcoded SR URL here.
software-url = http://git.erp5.org/gitweb/slapos.git/blob_plain/HEAD:/software/apache-frontend/software.cfg
slave = true
config-url = ${monitor-parameters:url}
software-type = {{ slapparameter_dict.get('monitor-frontend-software-type', frontend_software_type) }}
return = site_url domain
[monitor-frontend-promise]
recipe = slapos.cookbook:check_url_available
path = ${directory:promises}/monitor_frontend
url = ${publish-connection-information:monitor_url}
dash_path = {{ dash_executable_location }}
curl_path = {{ curl_executable_location }}
check-secure = 1
{% endif -%}
[publish-connection-information]
......@@ -387,8 +365,14 @@ tap-ipv4 = ${slap-network-information:tap-ipv4}
{% endif %}
{% endif %}
{% if monitor -%}
monitor_url = ${request-monitor-frontend:connection-site_url}
monitor_v6_url = ${monitor-parameters:url}
monitor-base-url = ${publish:monitor-base-url}
monitor-url = ${publish:monitor-url}
monitor-user = ${publish:monitor-user}
monitor-password = ${publish:monitor-password}
{% set monitor_interface_url = slapparameter_dict.get('monitor-interface-url', 'https://monitor.node.vifib.com') -%}
{% if monitor_interface_url -%}
monitor-setup-url = {{ monitor_interface_url }}/#page=settings_configurator&url=${publish:monitor-url}
{% endif -%}
{% endif -%}
{% if use_tap == 'true' and tap_network_dict.has_key('ipv4') -%}
......@@ -617,26 +601,18 @@ parts =
# kvm-monitor
cron
cron-entry-logrotate
# cron-entry-monitor
frontend-promise
{% if monitor -%}
# monitor parts
cron-entry-monitor
cron-entry-rss
deploy-index
deploy-status-history-cgi
deploy-status-cgi
# deploy-logfile-cgi
# deploy-resource-consumption-monitoring-cgi
setup-static-files
public-symlink
cgi-httpd-wrapper
cgi-httpd-graceful-wrapper
monitor-promise
monitor-instance-log-access
monitor-access-log
monitor-access-public
# monitor-frontend-promise
monitor-base
cron-entry-logrotate
certificate-authority
monitor-conf
start-monitor
ca-httpd
monitor-httpd-promise
monitor-httpd-promise-conf
monitor-status2rss-cron-entry
{% endif -%}
# Complete parts with sections
{{ part_list | join('\n ') }}
......
......@@ -14,7 +14,7 @@ nbd = ${template-nbd:output}
frontend = ${template-frontend:output}
kvm-resilient = $${dynamic-template-kvm-resilient:rendered}
kvm-import = ${template-kvm-import:output}
kvm-import = $${dynamic-template-kvm-import:rendered}
kvm-export = $${dynamic-template-kvm-export:rendered}
# Used for the test of resiliency. The system wants a "test" software_type.
......@@ -69,7 +69,7 @@ extra-context =
raw logrotate_cfg ${template-logrotate-base:rendered}
raw template_content ${template-content:location}/${template-content:filename}
raw template_httpd_cfg ${template-httpd:rendered}
raw template_monitor ${monitor-template:output}
raw template_monitor ${monitor2-template:rendered}
[dynamic-template-kvm]
recipe = slapos.recipe.template:jinja2
......@@ -100,7 +100,7 @@ context =
raw template_content ${template-content:location}/${template-content:filename}
raw template_kvm_controller_run ${template-kvm-controller:location}/${template-kvm-controller:filename}
raw template_kvm_run ${template-kvm-run:location}/${template-kvm-run:filename}
raw template_monitor ${monitor-template:output}
raw template_monitor ${monitor2-template:rendered}
raw websockify_executable_location ${buildout:directory}/bin/websockify
template-parts-destination = ${template-parts:destination}
template-replicated-destination = ${template-replicated:destination}
......@@ -118,6 +118,7 @@ context =
key eggs_directory buildout:eggs-directory
key slapparameter_dict slap-configuration:configuration
raw curl_executable_location ${curl:location}/bin/curl
raw template_monitor ${monitor2-template:rendered}
template-parts-destination = ${template-parts:destination}
template-replicated-destination = ${template-replicated:destination}
import-list = file parts :template-parts-destination
......@@ -136,6 +137,23 @@ context =
raw template_kvm_export ${template-kvm-export-script:location}/${template-kvm-export-script:filename}
raw pbsready_export_template ${pbsready-export:output}
raw gzip_binary ${gzip:location}/bin/gzip
key slapparameter_dict slap-configuration:configuration
mode = 0644
[dynamic-template-kvm-import]
recipe = slapos.recipe.template:jinja2
template = ${template-kvm-import:location}/instance-kvm-import.cfg.in
rendered = $${buildout:directory}/template-kvm-import.cfg
extensions = jinja2.ext.do
context =
key develop_eggs_directory buildout:develop-eggs-directory
key eggs_directory buildout:eggs-directory
raw template_kvm_import ${template-kvm-import-script:location}/${template-kvm-import-script:filename}
raw pbsready_import_template ${pbsready-import:output}
raw template_monitor ${monitor2-template:rendered}
key slapparameter_dict slap-configuration:configuration
raw zcat_binary ${gzip:location}/bin/zcat
raw gzip_binary ${gzip:location}/bin/gzip
mode = 0644
[dynamic-template-kvm-resilient-test]
......
......@@ -54,7 +54,7 @@ mode = 0644
recipe = slapos.recipe.template
url = ${:_profile_base_location_}/instance-runner.cfg
output = ${buildout:directory}/template-runner.cfg.in
md5sum = 61297b0882cc9d674f4099b8abdd413f
md5sum = 315f8d0e391fbe81e815e143470f1b92
mode = 0644
[template-runner-import-script]
......@@ -69,7 +69,7 @@ mode = 0644
recipe = slapos.recipe.template
url = ${:_profile_base_location_}/instance-runner-import.cfg.in
output = ${buildout:directory}/instance-runner-import.cfg
md5sum = 6c0a0b0bf28cbcb63831a818edbd6a5d
md5sum = 8ae80f9a9d5523219e1c9065f1cab6d8
mode = 0644
[template-runner-export-script]
......@@ -84,13 +84,13 @@ mode = 0644
recipe = slapos.recipe.template
url = ${:_profile_base_location_}/instance-runner-export.cfg.in
output = ${buildout:directory}/instance-runner-export.cfg
md5sum = 994e355d713f90bcc17e4b54da65f354
md5sum = 8f4912ca04a650298c3c260689109c2e
mode = 0644
[template-resilient]
recipe = slapos.recipe.build:download
url = ${:_profile_base_location_}/instance-resilient.cfg.jinja2
md5sum = aa9a99235571729ab93360c4712efa12
md5sum = 1721ed960ae5b9ae55864bcdc5b1d487
filename = instance-resilient.cfg.jinja2
mode = 0644
......@@ -114,7 +114,7 @@ mode = 0644
recipe = hexagonit.recipe.download
url = ${:_profile_base_location_}/httpd_conf.in
download-only = true
md5sum = b5d095f54f714d17dff12c0c5fe4afb7
md5sum = 21009dac6e9868bed61a669632103830
filename = httpd_conf.in
mode = 0644
......@@ -171,15 +171,6 @@ filename = listener_slapgrid.py.in
download-only = true
mode = 0644
[cors-domain-cgi]
recipe = hexagonit.recipe.download
url = ${:_profile_base_location_}/template/${:filename}
download-only = true
md5sum = d4c564267dd98cd178a890158c52c384
destination = ${buildout:parts-directory}/monitor-template-cors-domain-cgi
filename = cors-domain.jinja
mode = 0644
[monitor-check-webrunner-internal-instance]
recipe = hexagonit.recipe.download
url = ${:_profile_base_location_}/template/${:filename}
......@@ -193,6 +184,7 @@ mode = 0644
recipe = zc.recipe.egg
eggs =
collective.recipe.environment
collective.recipe.template
cns.recipe.symlink
erp5.util
lock-file
......
......@@ -2,9 +2,9 @@ PidFile "{{ parameters.path_pid }}"
ServerName example.com
ServerAdmin someone@email
<IfDefine !MonitorPort>
Listen [{{ parameters.global_ip }}]:{{ parameters.monitor_port }}
Define MonitorPort
<IfDefine !HTTPDPort>
Listen [{{ parameters.global_ip }}]:{{ parameters.global_port }}
Define HTTPDPort
</IfDefine>
LoadModule unixd_module modules/mod_unixd.so
......@@ -16,7 +16,7 @@ LoadModule authz_host_module modules/mod_authz_host.so
LoadModule authn_core_module modules/mod_authn_core.so
LoadModule authn_file_module modules/mod_authn_file.so
LoadModule mime_module modules/mod_mime.so
LoadModule cgid_module modules/mod_cgid.so
#LoadModule cgid_module modules/mod_cgid.so
LoadModule ssl_module modules/mod_ssl.so
LoadModule alias_module modules/mod_alias.so
LoadModule env_module modules/mod_env.so
......@@ -28,6 +28,9 @@ LoadModule dav_fs_module modules/mod_dav_fs.so
LoadModule cache_module modules/mod_cache.so
LoadModule file_cache_module modules/mod_file_cache.so
LoadModule setenvif_module modules/mod_setenvif.so
LoadModule dir_module modules/mod_dir.so
LoadModule cgid_module modules/mod_cgid.so
LoadModule autoindex_module modules/mod_autoindex.so
ErrorLog "{{ parameters.path_error_log }}"
LogFormat "%h %l %u %t \"%r\" %>s %b" common
......@@ -51,6 +54,15 @@ Header set Access-Control-Allow-Credentials "true"
Header set Access-Control-Allow-Methods "PROPFIND, PROPPATCH, COPY, MOVE, DELETE, MKCOL, LOCK, UNLOCK, PUT, GETLIB, VERSION-CONTROL, CHECKIN, CHECKOUT, UNCHECKOUT, REPORT, UPDATE, CANCELUPLOAD, HEAD, OPTIONS, GET, POST"
Header set Access-Control-Allow-Headers "Overwrite, Destination, Content-Type, Depth, User-Agent, X-File-Size, X-Requested-With, If-Modified-Since, X-File-Name, Cache-Control, Authorization"
DocumentRoot {{ parameters.runner_home }}/public
# Directory protection
<Directory />
Options FollowSymLinks
AllowOverride None
Require all denied
</Directory>
Alias /public {{ parameters.runner_home }}/public
<Directory {{ parameters.runner_home }}/public>
Order Allow,Deny
......@@ -65,23 +77,20 @@ Alias /public {{ parameters.runner_home }}/public
</Files>
</Directory>
DavLockDB {{ parameters.var_dir }}/DavLock
DavLockDB {{ parameters.dav_lock }}
Alias /share {{ parameters.runner_home }}
<Directory {{ parameters.runner_home }}>
DirectoryIndex disabled
DAV On
Options Indexes FollowSymLinks
AuthType Basic
AuthName "webdav"
AuthUserFile "{{ parameters.etc_dir }}/.htpasswd"
AuthName "Webrunner Dav"
AuthUserFile "{{ parameters.htpasswd_file }}"
<LimitExcept OPTIONS>
Require valid-user
</LimitExcept>
</Directory>
ScriptSock {{ parameters.path_pid }}
SetEnv GIT_HTTP_EXPORT_ALL
ScriptAlias /git/ {{ parameters.git_http_backend }}/
ScriptAlias /git-public/ {{ parameters.git_http_backend }}/
......@@ -96,7 +105,7 @@ RewriteCond %{REQUEST_URI} /git-receive-pack$
AuthType Basic
AuthName "Git Access"
AuthUserFile "{{ parameters.etc_dir }}/.htpasswd"
AuthUserFile "{{ parameters.htpasswd_file }}"
Require valid-user
</LocationMatch>
......@@ -107,9 +116,7 @@ RewriteCond %{REQUEST_URI} /git-receive-pack$
AuthType Basic
AuthName "Git Access"
AuthUserFile "{{ parameters.etc_dir }}/.htpasswd"
AuthUserFile "{{ parameters.htpasswd_file }}"
Require valid-user
Satisfy any
</LocationMatch>
include {{ parameters.cgi_httpd_conf }}
......@@ -10,6 +10,10 @@
{% if number_of_instances > 2 %}
{% set number_of_instances = 2 %}
{% endif %}
{% set monitor_return = ['monitor-base-url', 'monitor-url', 'monitor-user', 'monitor-password'] -%}
{% set monitor_parameter = {'monitor-cors-domains': slapparameter_dict.pop('monitor-cors-domains', "monitor.node.vifib.com")} -%}
{% set monitor_dict = {'parameter': monitor_parameter, 'return': monitor_return, 'set-monitor-url': True} -%}
{% set monitor_interface_url = slapparameter_dict.pop('monitor-interface-url', 'https://monitor.node.vifib.com') -%}
{% import 'parts' as parts %}
{% import 'replicated' as replicated %}
......@@ -24,11 +28,30 @@ parts +=
{{ parts.replicate("runner", number_of_instances + 1) }}
publish-connection-information
{{ replicated.replicate("runner", number_of_instances + 1, "runner-export", "runner-import", slapparameter_dict=slapparameter_dict) }}
[monitor-htpasswd]
recipe = slapos.cookbook:generate.password
storage-path = ${directory:etc}/.monitor_user
bytes = 8
username = admin
{% do monitor_parameter.__setitem__('monitor-username', slapparameter_dict.get('monitor-username', 'admin'))%}
{% do monitor_parameter.__setitem__('monitor-password', slapparameter_dict.get('monitor-password', '${monitor-htpasswd:passwd}'))%}
{{ replicated.replicate("runner", number_of_instances + 1, "runner-export", "runner-import", slapparameter_dict=slapparameter_dict, monitor_parameter_dict=monitor_dict) }}
[directory]
recipe = slapos.cookbook:mkdirectory
etc = ${buildout:directory}/etc
# XXX Monitoring Main Instane
[monitor-instance-parameter]
monitor-httpd-port = 8160
cors-domains = {{ monitor_parameter.get('monitor-cors-domains', '') }}
# Bubble up the parameters
[request-runner]
return = url ssh-public-key ssh-url notification-id ip backend_url url ssh_command access_url 1_info 2_info monitor_url monitor_backend_url webdav_url public_url git_public_url git_private_url
return = url ssh-public-key ssh-url notification-id ip backend_url url ssh_command access_url 1_info 2_info webdav_url public_url git_public_url git_private_url {{ monitor_return | join(' ') }}
[publish-connection-information]
recipe = slapos.cookbook:publish
......@@ -38,12 +61,16 @@ backend_url = ${request-runner:connection-backend_url}
access_url = ${request-runner:connection-access_url}
url = ${request-runner:connection-url}
ssh_command = ${request-runner:connection-ssh_command}
monitor_url = ${request-runner:connection-monitor_url}
monitor_backend_url = ${request-runner:connection-monitor_backend_url}
webdav_url = ${request-runner:connection-webdav_url}
public_url = ${request-runner:connection-public_url}
git_public_url = ${request-runner:connection-git_public_url}
git_private_url = ${request-runner:connection-git_private_url}
{% for key in monitor_return -%}
{{ key }} = ${request-runner:connection-{{ key }}}
{% endfor -%}
{% if monitor_interface_url -%}
monitor_setup_url = {{ monitor_interface_url }}/#page=settings_configurator&url=${request-runner:connection-monitor-url}
{% endif -%}
[slap-parameter]
# Default parameters for distributed deployment
......
......@@ -15,6 +15,8 @@ parts +=
publish-connection-information
slaprunner-promise
slaprunner-frontend-promise
apache-httpd-promise
httpd-frontend-promise
slaprunner-supervisord-wrapper
dropbear-promise
runtestsuite
......@@ -22,33 +24,17 @@ parts +=
shellinabox
slapos-cfg
slapos-repo
cron-entry-backup
cron-entry-prepare-software
deploy-instance-parameters
instance-software
instance-software-type
minishell-cwd
bash-profile
supervisord-wrapper
supervisord-promise
httpd-graceful-wrapper
## Monitoring part
###Parts to add for monitoring
cron
certificate-authority
cron-entry-monitor
cron-entry-rss
deploy-index
deploy-settings-cgi
deploy-status-cgi
deploy-status-history-cgi
setup-static-files
certificate-authority
zero-parameters
public-symlink
cgi-httpd-wrapper
cgi-httpd-graceful-wrapper
monitor-promise
monitor-instance-log-access
bash-profile
## Monitor for runner
monitor-current-log-access
monitor-deploy-cors-domain-cgi
monitor-check-resilient-feed-file
monitor-check-webrunner-internal-instance
......@@ -65,8 +51,13 @@ context =
raw shell_binary ${dash:location}/bin/dash
raw rsync_binary ${rsync:location}/bin/rsync
[monitor-promise]
url = $${monitor-frontend:config-url}/$${deploy-index-template:filename}
[monitor-instance-parameter]
monitor-httpd-port = 8437
# Pass some parameter to dispay in monitoring interface
instance-configuration =
file recovery-code $${recovery-code:storage-path}
httpdcors cors-domain $${slaprunner-httpd-cors:location} $${httpd-graceful-wrapper:output}
raw webrunner-url https://$${request-frontend:connection-domain}
# Extends publish section with resilient parameters
[publish-connection-information]
......@@ -75,10 +66,10 @@ url = $${monitor-frontend:config-url}/$${deploy-index-template:filename}
[monitor-check-resilient-feed-file]
recipe = slapos.recipe.template:jinja2
template = ${template-monitor-check-resilient-feed:location}/${template-monitor-check-resilient-feed:filename}
rendered = $${monitor-directory:monitor-custom-scripts}/check-create-resilient-feed-files.py
rendered = $${monitor-directory:promises}/check-create-resilient-feed-files
mode = 700
context =
key input_feed_directory directory:notifier-feeds
key monitor_feed_directory monitor-directory:public-cgi
key monitor_feed_directory monitor-directory:public
raw base_url http://[$${notifier:host}]:$${notifier:port}/get/
raw python_executable ${buildout:executable}
......@@ -28,29 +28,6 @@ parts +=
importer-consistency-promise
# have to repeat the next one, as it's not inherited from pbsready-import
import-on-notification
## Monitoring part
###Parts to add for monitoring
cron
certificate-authority
cron-entry-monitor
cron-entry-rss
deploy-index
deploy-settings-cgi
deploy-status-cgi
deploy-status-history-cgi
setup-static-files
certificate-authority
zero-parameters
public-symlink
cgi-httpd-wrapper
cgi-httpd-graceful-wrapper
monitor-promise
monitor-instance-log-access
## Monitor for runner
monitor-current-log-access
monitor-backup-log-access
## Monitor for import runner
monitor-latest-restored-backup
# For the needs of importer, we run the full slaprunner
# In case both exporter and importer (aka main instance and clone instance)
......@@ -95,19 +72,29 @@ mode = 755
[slap-parameter]
auto-deploy-instance = false
auto-deploy = true
name = Webrunner import
monitor-cors-domains =
monitor-username = $${monitor-htpasswd:username}
monitor-password = $${monitor-htpasswd:passwd}
[resilient-publish-connection-parameter]
monitor-url = $${monitor-parameters:url}
monitor-base-url = $${publish:monitor-base-url}
monitor-url = $${publish:monitor-url}
monitor-user = $${publish:monitor-user}
monitor-password = $${publish:monitor-password}
[monitor-backup-log-access]
< = monitor-directory-access
source = $${directory:logrotate-backup}
[monitor-latest-restored-backup]
recipe = slapos.recipe.template:jinja2
command = if [ -f $${directory:etc}/.resilient-timestamp ]; then echo "$(date -d @$(cat $${directory:etc}/.resilient-timestamp) +%c)"; else echo "No backup timestamp found"; fi
rendered = $${monitor-directory:monitoring-cgi}/latest-restored-backup
template = ${template-wrapper:output}
mode = 744
context =
key content :command
[monitor-instance-parameter]
monitor-httpd-port = 8360
#monitor-title = $${slap-parameter:name}
#cors-domains = $${slap-parameter:monitor-cors-domains}
#username = $${slap-parameter:monitor-username}
#password = $${slap-parameter:monitor-password}
# Pass some parameter to dispay in monitoring interface
instance-configuration =
raw takeover-url http://[$${resilient-web-takeover-httpd-configuration-file:listening-ip}]:$${resilient-web-takeover-httpd-configuration-file:listening-port}/
raw takeover-password $${resilient-web-takeover-password:passwd}
[monitor-conf-parameters]
private-path-list +=
$${directory:logrotate-backup}
......@@ -98,6 +98,19 @@
"minimum": 9683,
"exclusiveMinimum": true
},
"monitor-interface-url": {
"title": "Monitor Web Interface URL",
"description": "Give Url of HTML web interface that will be used to render this monitor instance.",
"type": "string",
"format": "uri",
"default": "https://monitor.node.vifib.com"
},
"monitor-cors-domains": {
"title": "Monitor CORS domains",
"description": "List of cors domains separated with space. Needed for ajax query on this monitor instance from a different domain.",
"type": "string",
"default": "monitor.node.vifib.com"
},
"cpu-usage-ratio": {
"title": "CPU Usage Ratio",
"description": "Ratio of the CPU use for compilation, if value is set to n, compilation will use number-of-cpu/n of cpus (need instance restart)",
......
......@@ -12,6 +12,8 @@ parts =
publish-connection-information
slaprunner-promise
slaprunner-frontend-promise
apache-httpd-promise
httpd-frontend-promise
slaprunner-supervisord-wrapper
dropbear-promise
runtestsuite
......@@ -27,33 +29,15 @@ parts =
bash-profile
supervisord-wrapper
supervisord-promise
httpd-graceful-wrapper
{% if slapparameter_dict.get('custom-frontend-backend-url') and slapparameter_dict.get('check-custom-frontend-promise', 'false') == 'true' %}
custom-frontend-promise
{% endif %}
## Monitoring part
###Parts to add for monitoring
cron
certificate-authority
cron-entry-monitor
cron-entry-rss
deploy-index
deploy-settings-cgi
deploy-status-cgi
deploy-status-history-cgi
setup-static-files
certificate-authority
zero-parameters
public-symlink
cgi-httpd-wrapper
cgi-httpd-graceful-wrapper
monitor-promise
monitor-instance-log-access
## Monitor for runner
monitor-current-log-access
monitor-deploy-cors-domain-cgi
monitor-check-webrunner-internal-instance
extends = ${monitor-template:output}
extends = ${monitor2-template:rendered}
eggs-directory = ${buildout:eggs-directory}
develop-eggs-directory = ${buildout:develop-eggs-directory}
......@@ -179,7 +163,7 @@ debug = False
auto_deploy = $${slap-parameter:auto-deploy}
auto_deploy_instance = $${slap-parameter:auto-deploy-instance}
autorun = $${slap-parameter:autorun}
knowledge0_file = $${buildout:directory}/$${public:filename}
knowledge0_file = $${monitor-instance-parameter:configuration-file-path}
minishell_cwd_file = $${directory:etc}/.minishell-cwd
minishell_history_file = $${directory:etc}/.minishell_history
software_info_json = $${runnerdirectory:home}/software_info.json
......@@ -320,14 +304,16 @@ context =
path_pid = $${directory:run}/httpd.pid
path_error_log = $${directory:log}/httpd-error.log
path_access_log = $${directory:log}/httpd-access.log
key_file = $${ca-httpd:key-file}
cert_file = $${ca-httpd:cert-file}
# XXX Use ca-nginx, no need to regenerate certificate
cert_file = $${ca-nginx:cert-file}
key_file = $${ca-nginx:key-file}
global_ip = $${slap-network-information:global-ipv6}
global_port = $${slaprunner:runner_port}
monitor_port = $${monitor-parameters:port}
monitor_index = $${deploy-index:rendered}
global_port = 8386
#httpd_port = $${monitor-parameters:port}
#monitor_index = $${deploy-index:rendered}
working_directory = $${slaprunner:working-directory}
dav_lock = $${directory:var}/DavLock
dav_lock = $${directory:var}/WebDavLock
htpasswd_file = $${monitor-httpd-conf-parameter:htpasswd-file}
etc_dir = $${directory:etc}
var_dir = $${directory:var}
project_folder = $${directory:project}
......@@ -335,8 +321,8 @@ project_private_folder = $${runnerdirectory:private-project}
project_public_folder = $${runnerdirectory:public-project}
runner_home = $${runnerdirectory:home}
git_http_backend = ${git:location}/libexec/git-core/git-http-backend
cgi_httpd_conf = $${monitor-httpd-configuration-file:rendered}
httpd_cors_file = $${monitor-httpd-cors:location}
#cgi_httpd_conf = $${monitor-httpd-configuration-file:rendered}
httpd_cors_file = $${slaprunner-httpd-cors:location}
[httpd-conf]
recipe = slapos.recipe.template:jinja2
......@@ -345,11 +331,39 @@ rendered = $${directory:etc}/httpd.conf
context =
section parameters httpd-parameters
[cgi-httpd-wrapper]
[apache-httpd]
recipe = slapos.cookbook:wrapper
apache-executable = ${apache:location}/bin/httpd
wrapper-path = $${ca-httpd:executable}
wrapper-path = $${directory:services}/slaprunner-httpd
command-line = $${:apache-executable} -f $${httpd-conf:rendered} -DFOREGROUND
access-url = https://[$${httpd-parameters:global_ip}]:$${httpd-parameters:global_port}
wait-for-files =
$${ca-nginx:cert-file}
$${ca-nginx:key-file}
[httpd-graceful-wrapper]
recipe = collective.recipe.template
input = inline:
#!/bin/sh
exec kill -USR1 $(cat $${httpd-parameters:path_pid})
output = $${directory:scripts}/slaprunner-httpd-graceful
mode = 700
[apache-httpd-promise]
recipe = slapos.cookbook:check_url_available
path = $${directory:promises}/$${:filename}
filename = apache-httpd-listening-on-tcp
url = $${apache-httpd:access-url}
check-secure = 1
dash_path = {{ dash_executable_location }}
curl_path = {{ curl_executable_location }}
[slaprunner-httpd-cors]
recipe = plone.recipe.command
command = if [ ! -f $${:location} ]; then touch $${:location}; fi
location = $${directory:etc}/$${:filename}
filename = slaprunner-httpd-cors.cfg
stop-on-error = true
#--------------------
#--
......@@ -424,16 +438,24 @@ config-url = $${slaprunner:access-url}
config-domain = $${slap-parameter:frontend-domain}
return = site_url domain
[monitor-frontend]
[request-httpd-frontend]
<= slap-connection
recipe = slapos.cookbook:requestoptional
name = Monitor Frontend
name = SlapRunner httpd Frontend
# XXX We have hardcoded SR URL here.
software-url = http://git.erp5.org/gitweb/slapos.git/blob_plain/HEAD:/software/apache-frontend/software.cfg
slave = true
config-url = https://[$${monitor-httpd-configuration:listening-ip}]:$${monitor-parameters:port}
config-domain = $${slap-parameter:frontend-domain}
return = site_url domain
config-url = $${apache-httpd:access-url}
config-domain =
return = secure_access domain
[httpd-frontend-promise]
recipe = slapos.cookbook:check_url_available
path = $${directory:promises}/slaprunner-apache-http-frontend
url = $${request-httpd-frontend:connection-secure_access}
dash_path = {{ dash_executable_location }}
curl_path = {{ curl_executable_location }}
check-secure = 1
#--------------------------------------
#--
......@@ -447,13 +469,17 @@ backend_url = $${slaprunner:access-url}
access_url = $${:url}/login
url = https://$${request-frontend:connection-domain}
ssh_command = ssh $${dropbear-runner-server:host} -p $${dropbear-runner-server:port}
monitor_url = https://$${monitor-frontend:connection-domain}
monitor_backend_url = https://[$${monitor-httpd-configuration:listening-ip}]:$${monitor-parameters:port}
webdav_url = $${:monitor_url}/share/
public_url = $${:monitor_url}/public/
git_public_url = https://[$${httpd-parameters:global_ip}]:$${httpd-parameters:monitor_port}/git-public/
git_private_url = https://[$${httpd-parameters:global_ip}]:$${httpd-parameters:monitor_port}/git/
webdav_url = $${request-httpd-frontend:connection-secure_access}/shared/
public_url = $${request-httpd-frontend:connection-secure_access}/public/
git_public_url = https://[$${httpd-parameters:global_ip}]:$${httpd-parameters:global_port}/git-public/
git_private_url = https://[$${httpd-parameters:global_ip}]:$${httpd-parameters:global_port}/git/
monitor-base-url = $${publish:monitor-base-url}
monitor-url = $${publish:monitor-url}
monitor-user = $${publish:monitor-user}
monitor-password = $${publish:monitor-password}
{% if slapparameter_dict.get('monitor-interface-url', 'https://monitor.node.vifib.com') -%}
monitor_setup_url = {{ slapparameter_dict['monitor-interface-url'] }}/#page=settings_configurator&url=$${publish:monitor-url}
{% endif -%}
#---------------------------
#--
#-- Deploy promises scripts
......@@ -497,8 +523,10 @@ slapos-reference = master
auto-deploy = false
auto-deploy-instance = true
autorun = false
monitor-port = 9684
monitor-port = 9687
instance-name =
monitor-cors-domains =
monitor-interface-url =
[monitor-parameters]
port = $${slap-parameter:monitor-port}
......@@ -599,11 +627,6 @@ url = $${slap-connection:server-url}
key = $${slap-connection:key-file}
cert = $${slap-connection:cert-file}
[public]
recovery-code = $${recovery-code:passwd}
[zero-parameters]
[minishell-cwd]
recipe = plone.recipe.command
......@@ -692,36 +715,36 @@ path = $${directory:promises}/supervisord
hostname = $${slaprunner:ipv4}
port = $${supervisord:port}
[monitor-current-log-access]
< = monitor-directory-access
source = $${directory:log}
[monitor-deploy-cors-domain-cgi]
recipe = slapos.recipe.template:jinja2
template = ${cors-domain-cgi:location}/${cors-domain-cgi:filename}
rendered = $${monitor-directory:knowledge0-cgi}/$${:filename}
filename = cors-domain.cgi
mode = 0744
context =
raw config_cfg $${buildout:directory}/knowledge0.cfg
raw timestamp $${buildout:directory}/.timestamp
raw python_executable ${buildout:executable}
key apache_file httpd-parameters:httpd_cors_file
key pwd monitor-directory:knowledge0-cgi
key this_file :filename
key httpd_graceful cgi-httpd-graceful-wrapper:rendered
# XXX Monitor
[monitor-instance-parameter]
monitor-httpd-port = $${slap-parameter:monitor-port}
{% if slapparameter_dict.get('name', '') -%}
monitor-title = {{ slapparameter_dict['name'] }}
{% endif -%}
cors-domains = {{ slapparameter_dict.get('monitor-cors-domains', 'monitor.node.vifib.com') }}
{% if slapparameter_dict.get('monitor-username', '') -%}
username = {{ slapparameter_dict['monitor-username'] }}
{% endif -%}
{% if slapparameter_dict.get('monitor-password', '') -%}
password = {{ slapparameter_dict['monitor-password'] }}
{% endif -%}
{% if slapparameter_dict.get('monitor-url-list', '') -%}
opml-url-list = {{ slapparameter_dict['monitor-url-list'] }}
{% endif -%}
{% if not slapparameter_dict.get('authorized-key', '') -%}
# Parameter for resilient
# Pass some parameter to dispay in monitoring interface
instance-configuration =
file recovery-code $${recovery-code:storage-path}
httpdcors cors-domain $${slaprunner-httpd-cors:location} $${httpd-graceful-wrapper:output}
raw webrunner-url https://$${request-frontend:connection-domain}
{% endif -%}
configuration-file-path = $${buildout:directory}/knowledge0.cfg
[monitor-check-webrunner-internal-instance]
recipe = slapos.recipe.template:jinja2
template = ${monitor-check-webrunner-internal-instance:location}/${monitor-check-webrunner-internal-instance:filename}
rendered = $${monitor-directory:monitor-custom-scripts}/$${:filename}
filename = monitor-check-webrunner-internal-instance.py
rendered = $${monitor-directory:promises}/$${:filename}
filename = monitor-check-webrunner-internal-instance
mode = 0744
[monitor-httpd-cors]
recipe = plone.recipe.command
command = if [ ! -f $${:location} ]; then touch $${:location}; fi
location = $${directory:etc}/$${:filename}
filename = httpd-cors.cfg
stop-on-error = true
* This stack has for purpose to know if all promises, services, custom monitoring scripts went/are ok.
* The second purpose of this stack is to implement a zero-knowledge feature : it means you can use its control interface to provide the user with sensible data. It can also let the user change some parameters
* It also provides a web interface, to see which promises, services and custom scripts 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. You can also add your own monitoring scripts, or cgi files (or just files) that you would want to check easily using a web interface.
Implementation :
----------------
1/ In the software.cfg of your Software Release, extends the stack
2/ In the template that will be copied for the buildout in the instance folder (instance.cfg ?), you have to add these parts:
###Parts to add for monitoring
slap-parameters
certificate-authority
cron
cron-entry-monitor
cron-entry-rss
deploy-index
deploy-index-template
deploy-monitor-script
deploy-rss-script
deploy-settings-cgi
deploy-status-cgi
make-rss
monitor-promise
setup-static-files
certificate-authority
public
zero-parameters
cgi-httpd-wrappers
public-symlink
* If you want to add a custom monitoring script, you can write it (in whatever language you wish) and save it in YOUR_INSTANCE_FOLDER/etc/monitor.
The only thing to know, is that if your script successfully passed, do not return or print nothing. If there is a problem, you can print the explanation on stdout or stderr
* Here are 2 promises that you can add to your instance buildout, to see if it is working (one is ok, not the other) :
[google-promise]
recipe = slapos.cookbook:check_url_available
path = $${directory:promise}/google
url = http://www.google.com
dash_path = ${dash:location}/bin/dash
curl_path = ${curl:location}/bin/curl
[failing-promise]
recipe = slapos.cookbook:check_url_available
path = $${directory:promise}/fail
url = http://127.0.0.2
dash_path = ${dash:location}/bin/dash
curl_path = ${curl:location}/bin/curl
CGI Scripts:
------------
This stack also provides a web interface, in wich you can execute custom cgi scripts, or just print files. The web link is provided in the published parameters, as for the password that you have to change as soon as possible
In that interface you will have access to the previous scripts and the RSS feed. You can also add your files/scripts.
For that, there exists a folder /var/cgi-bin. You should see that directory as a tree having of deep 2. In /var/cgi-bin, you must create only folders, which are called categories. In each category, you can then add your own files.
The backend system will automatically render the webpage according to the inside structure of the cgi-bin directory. Moreover, it will also let you access to your scripts only if you are logged in : you do not need do do your own authentication system !
Notice :
--------
* /!\A default password is set up at the installation : "passwordtochange". It has to be rewritten in the control interface by the user itself
* /!\ If you use the recipe zeroknown, never name a parameter "recipe" or "password".
* The control interface will let you change the values of the options declared in the [public] section of the config file (see zeroknown recipe). Other section's values will just be printed. These values won't be overwritten by buildout.
* If you want to allow a user to change a parameter, use the recipe zeroknown, with the buildout section name : "[public]"
* If you manually change a parameter, it could take some time for the modifications to be applied (at least 1 or 2 slapgrid-cp)
* If you need to change the port of the web interface of the monitoring stack, just create in your software release file a part called [monitor-parameters] and give the new port value to the parameter "port".
[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
......@@ -7,151 +11,176 @@ extends =
../../component/dcron/buildout.cfg
../../component/openssl/buildout.cfg
parts =
parts +=
slapos-cookbook
dcron
monitor-eggs
eggs
extra-eggs
monitor-bin
monitor-template
rss-bin
monitor2-template
[monitor-eggs]
[monitor-download-base]
recipe = hexagonit.recipe.download
download-only = true
url = ${:_profile_base_location_}/${:filename}
mode = 0644
[monitor-web-base]
<= monitor-download-base
url = ${:_profile_base_location_}/web/${:filename}
destination = ${buildout:parts-directory}/monitor-web
on-update = true
[monitor-template-base]
<= monitor-download-base
url = ${:_profile_base_location_}/templates/${:filename}
[monitor-template-script]
<= monitor-download-base
url = ${:_profile_base_location_}/scripts/${:filename}
destination = ${buildout:parts-directory}/monitor-scripts
on-update = true
[eggs]
recipe = zc.recipe.egg
eggs =
eggs +=
collective.recipe.template
cns.recipe.symlink
[extra-eggs]
recipe = zc.recipe.egg
<= eggs
interpreter = pythonwitheggs
eggs =
eggs +=
psutil
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-template]
recipe = slapos.recipe.template
url = ${:_profile_base_location_}/monitor.cfg.in
output = ${buildout:directory}/monitor.cfg
filename = monitor.cfg
md5sum = 9b31959560d3cde094199e267bbb013b
mode = 0644
# Monitor templates files
[monitor-httpd-conf]
<= monitor-template-base
md5sum = 08137be9b80e0e13d9a906c264a2f51f
filename = monitor-httpd.conf.in
[monitor-bin]
recipe = hexagonit.recipe.download
url = ${:_profile_base_location_}/${:filename}
download-only = true
md5sum = 5b12e864f1762d7984f7d4863d0b795d
destination = ${buildout:parts-directory}/monitor-template-monitor-bin
filename = monitor.py.in
mode = 0644
[monitor-service-conf-template]
<= monitor-template-base
filename = monitor-service.cfg.in
md5sum = 5913d2a0096b50537f394a49b762b3e5
[monitor-httpd-template]
recipe = hexagonit.recipe.download
url = ${:_profile_base_location_}/${:filename}
download-only = true
md5sum = 93e1dda50cb71bfe29966b2946c02dd1
filename = cgi-httpd.conf.in
mode = 0644
[template-wrapper]
<= monitor-template-base
filename = wrapper.in
md5sum = 8cde04bfd0c0e9bd56744b988275cfd8
[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
[monitor-conf]
<= monitor-template-base
filename = monitor.conf.in
md5sum = c8f024d741c6494d7c9ba01601d0b917
[monitor-instance-info]
<= monitor-template-base
filename = instance-info.conf.in
md5sum = 1bdb4e05c6be04f4e5766c64467fbcec
[monitor-httpd-cors]
<= monitor-template-base
filename = httpd-cors.cfg.in
md5sum = 5afad2bb6e088e080e907f1d837effbb
# End templates files
[monitor2-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 = e439e22e754a50e1a3500cd4a995f6d8
context =
key apache_location apache:location
key gzip_location gzip:location
raw monitor_bin ${monitor2-bin:location}/${monitor2-bin:filename}
raw monitor_collect ${monitor-collect:location}/${monitor-collect:filename}
raw monitor_conf_template ${monitor-conf:location}/${monitor-conf:filename}
raw monitor_document_edit ${monitor-document-edit:location}/${monitor-document-edit:filename}
raw monitor_https_cors ${monitor-httpd-cors:location}/${monitor-httpd-cors:filename}
raw monitor_instance_info ${monitor-instance-info:location}/${monitor-instance-info:filename}
raw monitor_globalstate ${monitor-globalstate:location}/${monitor-globalstate:filename}
raw monitor_password_promise_template ${monitor-password-promise:location}/${monitor-password-promise: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_template ${monitor-httpd-conf:location}/${monitor-httpd-conf:filename}
raw monitor_service_conf_template ${monitor-service-conf-template:location}/${monitor-service-conf-template:filename}
raw openssl_executable_location ${openssl:location}/bin/openssl
raw python_executable ${buildout:executable}
raw python_with_eggs ${buildout:directory}/bin/${extra-eggs:interpreter}
raw promise_executor_py ${run-promise-py:rendered}
raw template_wrapper ${template-wrapper:output}
raw status2rss_executable_path ${status2rss-executable:location}/${status2rss-executable:filename}
[monitor2-bin]
<= monitor-template-script
filename = monitor.py
md5sum = 222365a469f8ab08a0367d81c0b03982
[run-promise-py]
recipe = slapos.recipe.template:jinja2
template = ${:_profile_base_location_}/scripts/run-promise.py
rendered = ${buildout:parts-directory}/monitor-scripts/run-promise.py
md5sum = 8ba8b661c55f2c5a379e9e42573be486
mode = 0755
context =
raw python ${buildout:directory}/bin/${extra-eggs:interpreter}
[monitor-password-promise]
<= monitor-template-script
filename = monitor-password-promise.py
md5sum = f7e937d6619eb674f39f34718928d91d
[status2rss-executable]
<= monitor-template-script
filename = status2rss.py
md5sum = f297779d0881f4bd48081506efb492a4
[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
[monitor-globalstate]
<= monitor-template-script
filename = globalstate.py
md5sum = 384a1148cb3da9cf353a108fe70709c5
[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
[monitor-collect]
<= monitor-template-script
filename = collect.py
md5sum = cc65aebd4c35b3172a7ca83abde761bc
[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
[monitor-document-edit]
<= monitor-template-script
filename = monitor-document.py
md5sum = f3e557e5d81291a22d6d2837a9e37bd0
[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
[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
[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
[download-monitor-static]
recipe = hexagonit.recipe.download
url = http://git.erp5.org/gitweb/slapos.git/snapshot/930be99041ea26b7b1186830e5eb56ef0acc1bdf.tar.gz
download-only = false
filename = monitor-static.tar.gz
destination = ${buildout:parts-directory}/monitor-static-files
ignore-existing = true
strip-top-level-dir = true
mode = 0644
[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
[download-monitor-jquery]
recipe = hexagonit.recipe.download
url = http://code.jquery.com/jquery-1.10.2.min.js
download-only = true
destination = ${download-monitor-static:destination}
filename = jquery-1.10.2.min.js
mode = 0644
[monitor-web-monitor-promise-runner-cgi]
<= monitor-download-base
filename = monitor-run-promise.py.cgi
md5sum = 15625e5bf6c1b57b9199250951ffc16e
[template-wrapper]
recipe = slapos.recipe.template
url = ${:_profile_base_location_}/wrapper.in
output = ${buildout:directory}/template-wrapper.cfg
mode = 0644
md5sum = 8cde04bfd0c0e9bd56744b988275cfd8
[monitor-password-py-cgi]
<= monitor-download-base
md5sum = 04fc7e6d892d29a601cfd43d1700eeda
filename = monitor-password.py.cgi
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 %}
# Put here default Name for instance and Hosting Subscription
[slap-configuration]
root-instance-title = UNKNOW H-S
instance-title = UNKNOW Instance
[slap-configuration]
recipe = slapos.cookbook:slapconfiguration.serialised
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}
[cron]
recipe = slapos.cookbook:cron
cron-entries = ${logrotate-directory:cron-entries}
......@@ -56,8 +69,10 @@ bin = ${directory:bin}
etc = ${directory:etc}
run = ${directory:monitor}/run
#run = ${directory:scripts}
promises = ${directory:monitor-promise}
pids = ${directory:run}/monitor
cgi-bin = ${directory:monitor}/cgi-bin
webdav = ${directory:monitor}/webdav
public = ${directory:monitor}/public
private = ${directory:monitor}/private
services = ${directory:services}
......@@ -65,7 +80,6 @@ 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
monitor-var = ${directory:var}/monitor
monitor-password-var = ${monitor-directory:monitor-var}/password
monitor-password-interface = ${monitor-directory:monitor-password-var}/password/interface
......@@ -110,24 +124,45 @@ ca-crl = ${ca-directory:crl}
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}
executable = ${monitor-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 }}
root-title = ${monitor-instance-parameter:root-instance-title}
public-folder = ${monitor-directory:public}
private-folder = ${monitor-directory:private}
webdav-folder = ${monitor-directory:webdav}
web-folder = ${monitor-directory:web-dir}
monitor-hal-json = ${monitor-directory:web-dir}/monitor.haljson
base-url = ${monitor-frontend-promise:url}
#base-url = ${monitor-httpd-conf-parameter:url}
monitor-hal-json = ${monitor-directory:public}/monitor.hal.json
service-pid-folder = ${monitor-directory:pids}
crond-folder = ${logrotate-directory:cron-entries}
logrotate-folder = ${logrotate:logrotate-entries}
promise-runner = {{ promise_executor_py }}
promise-folder-list =
${directory:promises}
${directory:monitor-promise}
public-path-list =
${directory:log}
private-path-list =
${directory:log}
#
monitor-url-list =
${monitor-instance-parameter:opml-url-list}
parameter-file-path = ${monitor-instance-parameter:configuration-file-path}
parameter-list =
raw monitor-user ${monitor-instance-parameter:username}
htpasswd monitor-password ${monitor-htpassword-file:password-file} ${monitor-instance-parameter:username} ${httpd-monitor-htpasswd:htpasswd-path}
${monitor-instance-parameter:instance-configuration}
# htpasswd entry: htpasswd key password-file username htpasswd-file
collector-db = ${monitor-instance-parameter:collector-db}
collect-script = {{ monitor_collect }}
python = {{ python_with_eggs }}
[monitor-conf]
recipe = slapos.recipe.template:jinja2
......@@ -136,25 +171,72 @@ rendered = ${directory:etc}/${:filename}
filename = monitor.conf
context = section parameter_dict monitor-conf-parameters
[instance-info-parameters]
name = ${monitor-instance-parameter:monitor-title}
root-name = ${monitor-instance-parameter:root-instance-title}
computer-id = ${slap-connection:computer-id}
ipv4 = ${slap-configuration:ipv4-random}
ipv6 = ${slap-configuration:ipv6-random}
software-release = ${slap-connection:software-release-url}
software-type = ${slap-configuration:slap-software-type}
partition-id = ${slap-connection:partition-id}
[monitor-instance-info]
recipe = slapos.recipe.template:jinja2
template = {{ monitor_instance_info }}
rendered = ${directory:etc}/${:filename}
filename = instance-info.conf
context =
section instance_dict instance-info-parameters
[python-symlink]
recipe = plone.recipe.command
target = ${directory:bin}
command = ln -sf {{ python_with_eggs }} ${:target}/python
update-command = ${:command}
[start-monitor]
recipe = slapos.cookbook:wrapper
command-line = {{ python_executable }} {{ monitor_bin }} --config_file ${monitor-conf:rendered}
wrapper-path = ${directory:scripts}/bootstrap-monitor
environment =
PATH=${python-symlink:target}:/usr/local/bin:/usr/bin:/bin
[monitor-htpasswd]
recipe = slapos.cookbook:generate.password
storage-path = ${directory:etc}/.monitor_user
bytes = 8
username = admin
[monitor-htpassword-file]
recipe = plone.recipe.command
stop-on-error = true
password-file = ${directory:etc}/.monitor_pwd
command =
if [ ! -f "${:password-file}" ]; then echo "${monitor-instance-parameter:password}" > ${:password-file}; fi
update-command = ${:command}
[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
user = ${monitor-instance-parameter:username}
password = ${monitor-instance-parameter:password}
[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
pid-file = ${directory:run}/monitor-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
access-log = ${monitor-directory:log}/monitor-httpd-access.log
error-log = ${monitor-directory:log}/monitor-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}/
url = https://[${monitor-instance-parameter:monitor-httpd-ipv6}]:${:port}
httpd-cors-config-file = ${monitor-httpd-cors:rendered}
httpd-include-file =
[monitor-httpd-conf]
recipe = slapos.recipe.template:jinja2
......@@ -165,19 +247,27 @@ context =
section directory monitor-directory
section parameter_dict monitor-httpd-conf-parameter
[httpd-wrapper]
[monitor-httpd-cors]
recipe = slapos.recipe.template:jinja2
template = {{ monitor_https_cors }}
rendered = ${directory:etc}/httpd-cors.cfg
mode = 0600
context =
key domain monitor-instance-parameter:cors-domains
[monitor-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}
${monitor-httpd-graceful-wrapper:rendered}
[cgi-httpd-graceful-wrapper]
[monitor-httpd-graceful-wrapper]
recipe = slapos.recipe.template:jinja2
template = {{ template_wrapper }}
rendered = ${directory:run}/monitor-httpd-graceful
rendered = ${directory:scripts}/monitor-httpd-graceful
mode = 0700
context =
key content :command
......@@ -185,7 +275,9 @@ command = kill -USR1 $(cat ${monitor-httpd-conf-parameter:pid-file})
[monitor-status2rss-wrapper]
recipe = slapos.cookbook:wrapper
command-line = {{ python_executable }} {{ status2rss_executable_path }} '${monitor-instance-parameter:monitor-title}' '${monitor-httpd-conf-parameter:url}' ${monitor-directory:public} ${monitor-directory:monitor-status2rss-var}/previous_status ${monitor-directory:web-dir}/feed
# XXX - hard-coded Urls
command-line = {{ python_with_eggs }} {{ status2rss_executable_path }} --output '${monitor-directory:public}/feed' --items_folder '${monitor-directory:public}' --public_url '${monitor-conf-parameters:base-url}/share/jio_public/' --private_url '${monitor-conf-parameters:base-url}/share/jio_private/' --instance_name '${monitor-conf-parameters:title}' --hosting_name '${monitor-conf-parameters:root-title}'
wrapper-path = ${directory:bin}/monitor-status2rss.py
[monitor-status2rss-cron-entry]
......@@ -195,74 +287,30 @@ name = monitor-status2rss
frequency = * * * * *
command = ${monitor-status2rss-wrapper:wrapper-path}
[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-globalstate-wrapper]
recipe = slapos.cookbook:wrapper
command-line = {{ python_with_eggs }} {{ monitor_globalstate }} '${monitor-conf:rendered}' '${monitor-instance-info:rendered}'
wrapper-path = ${directory:bin}/monitor-globalstate
[monitor-web-monitor-logout-page]
recipe = slapos.recipe.template:jinja2
template = {{ monitor_web_monitor_logout_page }}
rendered = ${monitor-directory:web-dir}/logout
context =
[monitor-configurator-wrapper]
recipe = slapos.cookbook:wrapper
# XXX - hard coded path
command-line = {{ python_with_eggs }} {{ monitor_document_edit }} --config_folder '${monitor-conf-parameters:private-folder}/config/.jio_documents' --output_cfg_file '${monitor-instance-parameter:configuration-file-path}' --htpasswd_bin '{{ apache_location }}/bin/htpasswd'
wrapper-path = ${directory:bin}/monitor-configurator
[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
[monitor-globalstate-cron-entry]
recipe = slapos.cookbook:cron.d
cron-entries = ${cron:cron-entries}
name = monitor-globalstate
frequency = * * * * *
command = ${monitor-globalstate-wrapper:wrapper-path}
[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-configurator-cron-entry]
recipe = slapos.cookbook:cron.d
cron-entries = ${cron:cron-entries}
name = monitor-configurator
frequency = * * * * *
command = ${monitor-configurator-wrapper:wrapper-path}
[monitor-httpd-promise]
recipe = slapos.cookbook:check_url_available
......@@ -282,8 +330,8 @@ 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 = * * * * *
# frequency 5 minute hour day mounth weekday
frequency = */5 * * * *
public-path-list = ${monitor-httpd-conf-parameter:access-log} ${monitor-httpd-conf-parameter:error-log}
#private-path-list =
......@@ -312,48 +360,68 @@ rendered = ${directory:monitor-promise}/${monitor-password-promise:filename}.cfg
mode = 0644
context = section parameter_dict monitor-password-promise-conf-parameter
[monitor-password-cgi]
recipe = slapos.recipe.template:jinja2
template = {{ monitor_password_cgi_template }}
rendered = ${monitor-directory:cgi-bin}/monitor-password.cgi
context =
raw python_executable {{ python_executable }}
key password_changed_once_path monitor-password-parameter:password-changed-once-path
raw htpasswd_executable {{ apache_location }}/bin/htpasswd
key htpasswd_path httpd-monitor-htpasswd:htpasswd-path
[monitor-password-promise-interface]
recipe = slapos.recipe.template:jinja2
template = {{ monitor_password_promise_interface_template }}
rendered = ${monitor-directory:monitor-password-interface}/index.html
context =
[publish]
recipe = slapos.cookbook:publish
monitor-url = ${monitor-httpd-conf-parameter:url}
<= monitor-base
monitor-base-url = ${monitor-conf-parameters:base-url}
monitor-url = ${:monitor-base-url}/public/feeds
monitor-user = ${monitor-instance-parameter:username}
monitor-password = ${monitor-instance-parameter:password}
[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
monitor-password-promise
monitor-password-promise-conf
monitor-password-cgi
monitor-password-promise-interface
monitor-status2rss-cron-entry
publish
monitor-title = ${slap-configuration:instance-title}
monitor-httpd-ipv6 = ${slap-configuration:ipv6-random}
monitor-httpd-port = 8196
root-instance-title = ${slap-configuration:root-instance-title}
opml-url-list =
cors-domains =
# XXX Hard coded parameter
collector-db = /srv/slapgrid/var/data-log/collector.db
# Credentials
password = ${monitor-htpasswd:passwd}
username = ${monitor-htpasswd:username}
# XXX: type key value
# ex raw monitor-password resqdsdsd34
instance-configuration =
configuration-file-path = ${monitor-directory:etc}/monitor_knowledge0.cfg
[monitor-frontend]
<= slap-connection
recipe = slapos.cookbook:requestoptional
name = Monitor Frontend ${monitor-instance-parameter:monitor-title}
# XXX We have hardcoded SR URL here.
software-url = http://git.erp5.org/gitweb/slapos.git/blob_plain/HEAD:/software/apache-frontend/software.cfg
slave = true
config-url = ${monitor-httpd-conf-parameter:url}
config-https-only = true
#software-type = custom-personal
return = domain secure_access
[monitor-frontend-promise]
recipe = slapos.cookbook:check_url_available
path = ${directory:promises}/monitor-http-frontend
url = ${monitor-frontend:connection-secure_access}
dash_path = {{ dash_executable_location }}
curl_path = {{ curl_executable_location }}
check-secure = 1
[monitor-base]
# create dependencies between required monitor parts
recipe = plone.recipe.command
command = true
update-command =
depends =
${monitor-globalstate-cron-entry:name}
${monitor-configurator-cron-entry:name}
${cron-entry-logrotate:name}
${certificate-authority:wrapper}
${monitor-conf:rendered}
${start-monitor:wrapper-path}
${ca-httpd:wrapper}
${monitor-httpd-promise:filename}
${monitor-status2rss-cron-entry:name}
#[buildout]
#parts =
# monitor-base
# publish
......@@ -34,4 +34,8 @@ def main():
pidfile.write(str(process.pid))
if __name__ == "__main__":
if len(sys.argv) == 1:
print "Use: %s Monitor_Config_File"
sys.exit(1)
sys.exit(main())
\ No newline at end of file
[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}
[publish-connection-information]
<= publish-connection-informations
#!{{ python_executable }}
import json
import os
import subprocess
import sys
import sqlite3
import time
import threading
from optparse import OptionParser, make_option
FAILURE = "FAILURE"
SUCCESS = "SUCCESS"
db_path = "{{ monitor_parameter['db-path'] }}"
instance_path = "{{ directory['home'] }}"
monitor_dir = "{{ directory['monitor-custom-scripts'] }}"
pid_dir = "{{ directory['run'] }}"
promise_dir = "{{ directory['promise'] }}"
monitoring_file_json = "{{ monitoring_file_json }}"
option_list = [
make_option("-a", "--all", action="store_true", dest="all",
help="test everything : promises, services, customs"),
make_option("-n", "--no-write", action="store_true", dest="only_stdout",
help="just show the json output on stdout"),
make_option("-m", "--monitors", action="store_true", dest="monitor",
help="add the custom monitoring file to the files to monitor"),
make_option("-p", "--promises", action="store_true", dest="promise",
help="add the promises\'file to the files to monitor"),
make_option("-s", "--services", action="store_true", dest="service",
help="add the file containing services\'pid to the files to monitor")
]
class Popen(subprocess.Popen):
def set_timeout(self, timeout):
self.set_timeout = None # assert we're not called twice
event = threading.Event()
event.__killed = False # we just need a mutable
def t():
# do not call wait() or poll() because they're not thread-safe
if not event.wait(timeout) and self.returncode is None:
# race condition if waitpid completes just before the signal sent ?
self.terminate()
event.__killed = True
if event.wait(5):
return
if self.returncode is None:
self.kill() # same race as for terminate ?
t = threading.Thread(target=t)
t.daemon = True
t.start()
def killed():
event.set()
t.join()
return event.__killed
return killed
def init_db(db):
db.executescript("""
CREATE TABLE IF NOT EXISTS status (
timestamp INTEGER UNIQUE,
status VARCHAR(255));
CREATE TABLE IF NOT EXISTS individual_status (
timestamp INTEGER,
status VARCHAR(255),
element VARCHAR(255),
output TEXT);
""")
def getListOfScripts(directory):
"""
Get the list of script inside of a directory (not recursive)
"""
scripts = []
if os.path.exists(directory) and os.path.isdir(directory):
for file_name in os.listdir(directory):
file = os.path.join(directory, file_name)
if os.access(file, os.X_OK) and not os.path.isdir(file):
scripts.append(file)
else:
exit("There is a problem in your directories" \
"of monitoring. Please check them")
return scripts
def runServices(directory):
services = getListOfScripts(directory)
result = {}
for service in services:
service_path = os.path.join(pid_dir, service)
service_name = os.path.basename(service_path)
try:
pid = int(open(service_path).read())
### because apache (or others) can write sockets
### We also ignore not readable pid files
except (IOError, ValueError):
continue
try:
os.kill(pid, 0)
result[service_name] = ''
except OSError:
result[service_name] = "This service is not running anymore"
return result
def runScripts(directory):
# XXX script_timeout could be passed as parameters
script_timeout = 60 # in seconds
result = {}
with open(os.devnull, 'r+') as f:
for script in getListOfScripts(directory):
command = os.path.join(promise_dir, script),
script = os.path.basename(script)
result[script] = ''
p = Popen(command, cwd=instance_path,
env=None if sys.platform == 'cygwin' else {},
stdin=f, stdout=f, stderr=subprocess.PIPE)
killed = p.set_timeout(script_timeout)
stderr = p.communicate()[1]
if killed():
result[script] = "Time Out"
elif p.returncode:
result[script] = stderr.strip()
return result
def writeFiles(monitors):
timestamp = int(time.time())
db = sqlite3.connect(db_path)
init_db(db)
status = SUCCESS
for key, value in monitors.iteritems():
if value:
element_status = status = FAILURE
else:
element_status = SUCCESS
db.execute("insert into individual_status(timestamp, element, output, status) values (?, ?, ?, ?)", (timestamp, key, value, element_status))
db.execute("insert into status(timestamp, status) values (?, ?)", (timestamp, status))
db.commit()
db.close()
monitors['datetime'] = time.ctime(timestamp)
json.dump(monitors, open(monitoring_file_json, "w+"))
def main():
parser = OptionParser(option_list=option_list)
monitors = {}
(options, args) = parser.parse_args()
if not (options.monitor or options.promise
or options.service or options.all):
exit("Please provide at list one arg in : -a, -m, -p, -s")
if options.monitor or options.all:
monitors.update(runScripts(monitor_dir))
if options.promise or options.all:
monitors.update(runScripts(promise_dir))
if options.service or options.all:
monitors.update(runServices(pid_dir))
if options.only_stdout:
print json.dumps(monitors)
else:
writeFiles(monitors)
if __name__ == "__main__":
main()
# -*- coding: utf-8 -*-
##############################################################################
#
# Copyright (c) 2010-2014 Vifib SARL and Contributors.
# All Rights Reserved.
#
# WARNING: This program as such is intended to be used by professional
# programmers who take the whole responsibility of assessing all potential
# consequences resulting from its eventual inadequacies and bugs
# End users who are looking for a ready-to-use solution with commercial
# guarantees and support are strongly adviced to contract a Free Software
# Service Company
#
# This program is Free Software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public License
# as published by the Free Software Foundation; either version 2.1
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
#
##############################################################################
import sqlite3
import os
import pwd
import time
import json
import argparse
import psutil
from time import strftime
from datetime import datetime, timedelta
def parseArguments():
"""
Parse arguments for monitor collector instance.
"""
parser = argparse.ArgumentParser()
parser.add_argument('--output_folder',
help='Path of the folder where output files should be written.')
parser.add_argument('--partition_id',
help='ID of the computer partition to collect data from.')
parser.add_argument('--collector_db',
help='The path of slapos collect database.')
return parser.parse_args()
class RessourceCollect:
def __init__(self, db_path = None):
assert os.path.exists(db_path) and os.path.isfile(db_path)
self.uri = db_path
self.connection = None
self.cursor = None
def connect(self):
self.connection = sqlite3.connect(self.uri)
self.cursor = self.connection.cursor()
def close(self):
assert self.connection is not None
self.cursor.close()
self.connection.close()
def _execute(self, sql):
assert self.connection is not None
return self.cursor.execute(sql)
def select(self, table, date=None, columns="*", where=None):
""" Query database for a full table information """
if date is not None:
where_clause = " WHERE date = '%s' " % date
else:
where_clause = ""
if where is not None:
if where_clause == "":
where_clause += " WHERE 1 = 1 "
where_clause += " AND %s " % where
select_sql = "SELECT %s FROM %s %s " % (columns, table, where_clause)
return self._execute(select_sql)
def has_table(self, name):
self.connect()
check_result_cursor = self.select(
table="sqlite_master",
columns='name',
where="type='table' AND name='%s'" % name)
table_exists_result = zip(*check_result_cursor)
if not len(table_exists_result) or table_exists_result[0][0] is None:
return False
return True
def getPartitionCPULoadAverage(self, partition_id, date_scope):
self.connect()
query_result_cursor = self.select("user", date_scope,
columns="SUM(cpu_percent)",
where="partition = '%s'" % partition_id)
cpu_percent_sum = zip(*query_result_cursor)
if len(cpu_percent_sum) and cpu_percent_sum[0][0] is None:
return
query_result_cursor = self.select("user", date_scope,
columns="COUNT(DISTINCT time)",
where="partition = '%s'" % partition_id)
sample_amount = zip(*query_result_cursor)
self.close()
if len(sample_amount) and len(cpu_percent_sum):
return round(cpu_percent_sum[0][0]/sample_amount[0][0], 2)
def getPartitionUsedMemoryAverage(self, partition_id, date_scope):
self.connect()
query_result_cursor = self.select("user", date_scope,
columns="SUM(memory_rss)",
where="partition = '%s'" % partition_id)
memory_sum = zip(*query_result_cursor)
if len(memory_sum) and memory_sum[0][0] is None:
return
query_result_cursor = self.select("user", date_scope,
columns="COUNT(DISTINCT time)",
where="partition = '%s'" % partition_id)
sample_amount = zip(*query_result_cursor)
self.close()
if len(sample_amount) and len(memory_sum):
return round(memory_sum[0][0]/(sample_amount[0][0]*1024*1024.0), 2)
def getPartitionDiskUsedAverage(self, partition_id, date_scope):
if not self.has_table('folder'):
return
self.db.connect()
query_result_cursor = self.select("folder", date_scope,
columns="SUM(disk_used)",
where="partition = '%s'" % partition_id)
disk_used_sum = zip(*query_result_cursor)
if len(disk_used_sum) and disk_used_sum[0][0] is None:
return
query_result_cursor = self.select("folder", date_scope,
columns="COUNT(DISTINCT time)",
where="partition = '%s'" % partition_id)
collect_amount = zip(*query_result_cursor)
self.db.close()
if len(collect_amount) and len(disk_used_sum):
return round(disk_used_sum[0][0]/(collect_amount[0][0]*1024.0), 2)
def getPartitionConsumption(self, partition_id, where=""):
"""
Query collector db to get consumed ressource for last minute
"""
self.connect()
comsumption_list = []
if where != "":
where = "and %s" % where
date_scope = datetime.now().strftime('%Y-%m-%d')
min_time = (datetime.now() - timedelta(minutes=1)).strftime('%H:%M:00')
max_time = (datetime.now() - timedelta(minutes=1)).strftime('%H:%M:59')
sql_query = """select count(pid), SUM(cpu_percent) as cpu_result, SUM(cpu_time),
MAX(cpu_num_threads), SUM(memory_percent), SUM(memory_rss), pid, SUM(io_rw_counter),
SUM(io_cycles_counter) from user
where date='%s' and partition='%s' and (time between '%s' and '%s') %s
group by pid order by cpu_result desc""" % (
date_scope, partition_id, min_time, max_time, where)
query_result = self._execute(sql_query)
for result in query_result:
count = int(result[0])
if not count > 0:
continue
resource_dict = {
'pid': result[6],
'cpu_percent': round(result[1]/count, 2),
'cpu_time': round((result[2] or 0)/(60.0), 2),
'cpu_num_threads': round(result[3]/count, 2),
'memory_percent': round(result[4]/count, 2),
'memory_rss': round((result[5] or 0)/(1024*1024.0), 2),
'io_rw_counter': round(result[7]/count, 2),
'io_cycles_counter': round(result[8]/count, 2)
}
try:
pprocess = psutil.Process(int(result[6]))
except psutil.NoSuchProcess:
pass
else:
resource_dict['name'] = pprocess.name()
resource_dict['command'] = pprocess.cmdline()
resource_dict['user'] = pprocess.username()
resource_dict['date'] = datetime.fromtimestamp(pprocess.create_time()).strftime("%Y-%m-%d %H:%M:%S")
comsumption_list.append(resource_dict)
self.close()
return comsumption_list
def getPartitionComsumptionStatus(self, partition_id, where=""):
self.connect()
if where != "":
where = " and %s" % where
date_scope = datetime.now().strftime('%Y-%m-%d')
min_time = (datetime.now() - timedelta(minutes=1)).strftime('%H:%M:00')
max_time = (datetime.now() - timedelta(minutes=1)).strftime('%H:%M:59')
sql_query = """select count(pid), SUM(cpu_percent), SUM(cpu_time),
SUM(cpu_num_threads), SUM(memory_percent), SUM(memory_rss), SUM(io_rw_counter),
SUM(io_cycles_counter) from user where
date='%s' and partition='%s' and (time between '%s' and '%s') %s""" % (
date_scope, partition_id, min_time, max_time, where)
query_result = self._execute(sql_query)
result_list = zip(*query_result)
process_dict = memory_dict = io_dict = {}
if len(result_list):
result = result_list
process_dict = {'total_process': result[0][0],
'cpu_percent': round((result[1][0] or 0), 2),
'cpu_time': round((result[2][0] or 0)/(60.0), 2),
'cpu_num_threads': round((result[3][0] or 0), 2),
'date': '%s %s' % (date_scope, min_time)
}
memory_dict = {'memory_percent': round((result[4][0] or 0), 2),
'memory_rss': round((result[5][0] or 0)/(1024*1024.0), 2),
'date': '%s %s' % (date_scope, min_time)
}
io_dict = {'io_rw_counter': round((result[6][0] or 0), 2),
'io_cycles_counter': round((result[7][0] or 0), 2),
'disk_used': 0,
'date': '%s %s' % (date_scope, min_time)
}
if self.has_table('folder'):
disk_result_cursor = self.select(
"folder", date_scope,
columns="SUM(disk_used)",
where="partition='%s' and (time between '%s' and '%s') %s" % (
partition_id, min_time, max_time, where
)
)
disk_used_sum = zip(*disk_result_cursor)
if len(disk_used_sum) and disk_used_sum[0][0] is not None:
io_dict['disk_used'] = round(disk_used_sum[0][0]/1024.0, 2)
self.close()
return (process_dict, memory_dict, io_dict)
def appendToJsonFile(file_path, content, stepback=2):
with open (file_path, mode="r+") as jfile:
jfile.seek(0, 2)
position = jfile.tell() - stepback
jfile.seek(position)
jfile.write('%s}' % ',"{}"]'.format(content))
if __name__ == "__main__":
parser = parseArguments()
if not os.path.exists(parser.output_folder) and os.path.isdir(parser.output_folder):
raise Exception("Invalid ouput folder: %s" % parser.output_folder)
collector = RessourceCollect(parser.collector_db)
date_scope = datetime.now().strftime('%Y-%m-%d')
stat_info = os.stat(parser.output_folder)
partition_user = pwd.getpwuid(stat_info.st_uid)[0]
# Consumption global status
process_file = os.path.join(parser.output_folder, 'monitor_resource_process.data.json')
mem_file = os.path.join(parser.output_folder, 'monitor_resource_memory.data.json')
io_file = os.path.join(parser.output_folder, 'monitor_resource_io.data.json')
process_result, memory_result, io_result = collector.getPartitionComsumptionStatus(partition_user)
resource_file = os.path.join(parser.output_folder, 'monitor_process_resource.status.json')
label_list = ['date', 'total_process', 'cpu_percent', 'cpu_time', 'cpu_num_threads',
'memory_percent', 'memory_rss', 'io_rw_counter', 'io_cycles_counter',
'disk_used']
resource_status_dict = {}
if not os.path.exists(process_file):
with open(process_file, 'w') as fprocess:
data_dict = {
"date": time.time(),
"data": ["date, total process, CPU percent, CPU time, CPU threads"]
}
fprocess.write(json.dumps(data_dict))
if not os.path.exists(mem_file):
with open(mem_file, 'w') as fmem:
data_dict = {
"date": time.time(),
"data": ["date, memory used percent, memory used"]
}
fmem.write(json.dumps(data_dict))
if not os.path.exists(io_file):
with open(io_file, 'w') as fio:
data_dict = {
"date": time.time(),
"data": ["date, io rw counter, io cycles counter, disk used"]
}
fio.write(json.dumps(data_dict))
if process_result and process_result['total_process'] != 0.0:
appendToJsonFile(process_file, ", ".join(
[str(process_result[key]) for key in label_list if process_result.has_key(key)])
)
resource_status_dict.update(process_result)
if memory_result and memory_result['memory_rss'] != 0.0:
appendToJsonFile(mem_file, ", ".join(
[str(memory_result[key]) for key in label_list if memory_result.has_key(key)])
)
resource_status_dict.update(memory_result)
if io_result and io_result['io_rw_counter'] != 0.0:
appendToJsonFile(io_file, ", ".join(
[str(io_result[key]) for key in label_list if io_result.has_key(key)])
)
resource_status_dict.update(io_result)
with open(os.path.join(parser.output_folder, 'monitor_resource.status.json'), 'w') as fp:
fp.write(json.dumps(resource_status_dict))
# Consumption Ressource
resource_process_status_list = collector.getPartitionConsumption(partition_user)
if resource_process_status_list:
with open(resource_file, 'w') as rf:
rf.write(json.dumps(resource_process_status_list))
#!/usr/bin/env python
import sys
import os
import glob
import json
import ConfigParser
import time
from datetime import datetime
def softConfigGet(config, *args, **kwargs):
try:
return config.get(*args, **kwargs)
except (ConfigParser.NoOptionError, ConfigParser.NoSectionError):
return ""
def generateStatisticsData(stat_file_path, content):
# csv document for statictics
if not os.path.exists(stat_file_path):
with open(stat_file_path, 'w') as fstat:
data_dict = {
"date": time.time(),
"data": ["Date, Success, Error, Warning"]
}
fstat.write(json.dumps(data_dict))
current_state = ''
if content.has_key('state'):
current_state = '%s, %s, %s, %s' % (
content['date'],
content['state']['success'],
content['state']['error'],
content['state']['warning'])
# append to file
if current_state:
with open (stat_file_path, mode="r+") as fstat:
fstat.seek(0,2)
position = fstat.tell() -2
fstat.seek(position)
fstat.write('%s}' % ',"{}"]'.format(current_state))
def main(args_list):
monitor_file, instance_file = args_list
monitor_config = ConfigParser.ConfigParser()
monitor_config.read(monitor_file)
base_folder = monitor_config.get('monitor', 'private-folder')
status_folder = monitor_config.get('monitor', 'public-folder')
base_url = monitor_config.get('monitor', 'base-url')
related_monitor_list = monitor_config.get("monitor", "monitor-url-list").split()
statistic_folder = os.path.join(base_folder, 'data', '.jio_documents')
parameter_file = os.path.join(base_folder, 'config', '.jio_documents', 'config.json')
if not os.path.exists(statistic_folder):
try:
os.makedirs(statistic_folder)
except OSError, e:
if e.errno == os.errno.EEXIST and os.path.isdir(statistic_folder):
pass
else: raise
# search for all status files
file_list = filter(os.path.isfile,
glob.glob("%s/*.status.json" % status_folder)
)
error = warning = success = 0
latest_date = ''
status = 'OK'
promise_list = []
global_state_file = os.path.join(base_folder, 'monitor.global.json')
public_state_file = os.path.join(status_folder, 'monitor.global.json')
for file in file_list:
try:
with open(file, 'r') as temp_file:
tmp_json = json.loads(temp_file.read())
except ValueError:
# bad json file ?
continue
if tmp_json['status'] == 'ERROR':
error += 1
elif tmp_json['status'] == 'OK':
success += 1
elif tmp_json['status'] == 'WARNING':
warning += 1
if tmp_json['start-date'] > latest_date:
latest_date = tmp_json['start-date']
tmp_json['time'] = tmp_json['start-date'].split(' ')[1]
del tmp_json['start-date']
promise_list.append(tmp_json)
if error:
status = 'ERROR'
elif warning:
status = 'WARNING'
if not latest_date:
latest_date = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
global_state_dict = dict(
status=status,
state={
'error': error,
'success': success,
'warning': warning,
},
date=latest_date,
_links={"rss_url": {"href": "%s/public/feed" % base_url},
"public_url": {"href": "%s/share/jio_public/" % base_url},
"private_url": {"href": "%s/share/jio_private/" % base_url}
},
data={'state': 'monitor_state.data',
'process_state': 'monitor_process_resource.status',
'process_resource': 'monitor_resource_process.data',
'memory_resource': 'monitor_resource_memory.data',
'io_resource': 'monitor_resource_io.data',
'monitor_process_state': 'monitor_resource.status'}
)
global_state_dict['_embedded'] = {'promises': promise_list}
if os.path.exists(instance_file):
config = ConfigParser.ConfigParser()
config.read(instance_file)
if 'instance' in config.sections():
instance_dict = {}
global_state_dict['title'] = config.get('instance', 'name')
global_state_dict['hosting-title'] = config.get('instance', 'root-name')
if not global_state_dict['title']:
global_state_dict['title'] = 'Instance Monitoring'
instance_dict['computer'] = config.get('instance', 'computer')
instance_dict['ipv4'] = config.get('instance', 'ipv4')
instance_dict['ipv6'] = config.get('instance', 'ipv6')
instance_dict['software-release'] = config.get('instance', 'software-release')
instance_dict['software-type'] = config.get('instance', 'software-type')
instance_dict['partition'] = config.get('instance', 'partition')
global_state_dict['_embedded'].update({'instance' : instance_dict})
if related_monitor_list:
global_state_dict['_links']['related_monitor'] = [{'href': "%s/share/jio_public" % url}
for url in related_monitor_list]
if os.path.exists(parameter_file):
with open(parameter_file) as cfile:
global_state_dict['parameters'] = json.loads(cfile.read())
# Public information with the link to private folder
public_state_dict = dict(
status=status,
date=latest_date,
_links={'monitor': {'href': '%s/share/jio_private/' % base_url}},
title=global_state_dict.get('title', '')
)
public_state_dict['hosting-title'] = global_state_dict.get('hosting-title', '')
public_state_dict['_links']['related_monitor'] = global_state_dict['_links'].get('related_monitor', [])
with open(global_state_file, 'w') as fglobal:
fglobal.write(json.dumps(global_state_dict))
with open(public_state_file, 'w') as fpglobal:
fpglobal.write(json.dumps(public_state_dict))
generateStatisticsData(
os.path.join(statistic_folder, 'monitor_state.data.json'),
global_state_dict)
return 0
if __name__ == "__main__":
if len(sys.argv) < 3:
print("Usage: %s <monitor_conf_path> <instance_conf_path>" % sys.argv[0])
sys.exit(2)
sys.exit(main(sys.argv[1:]))
#!/usr/bin/env python
import sys
import os
import re
import json
import argparse
import subprocess
from datetime import datetime
import time
def parseArguments():
"""
Parse arguments for monitor instance.
"""
parser = argparse.ArgumentParser()
parser.add_argument('--config_folder',
help='Path where json configuration/document will be read and write')
parser.add_argument('--htpasswd_bin',
help='Path apache htpasswd binary. Needed to write htpasswd file.')
parser.add_argument('--output_cfg_file',
help='Ouput parameters in cfg file.')
return parser.parse_args()
def fileWrite(file_path, content):
if os.path.exists(file_path):
try:
with open(file_path, 'w') as wf:
wf.write(content)
return True
except OSError, e:
print "ERROR while writing changes to %s.\n %s" % (file_path, str(e))
return False
def htpasswdWrite(htpasswd_bin, parameter_dict, value):
if not os.path.exists(parameter_dict['file']):
return False
command = [htpasswd_bin, '-cb', parameter_dict['htpasswd'], parameter_dict['user'], value]
process = subprocess.Popen(
command,
stdin=None,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
)
result = process.communicate()[0]
if process.returncode != 0:
print result
return False
with open(parameter_dict['file'], 'w') as pfile:
pfile.write(value)
return True
def httpdCorsDomainWrite(httpd_cors_file, httpd_gracefull_bin, cors_domain):
cors_string = ""
cors_domain_list = cors_domain.split()
old_httpd_cors_file = os.path.join(
os.path.dirname(httpd_cors_file),
'prev_%s' % os.path.basename(httpd_cors_file)
)
if os.path.exists(old_httpd_cors_file) and os.path.isfile(old_httpd_cors_file):
try:
with open(old_httpd_cors_file, 'r') as cors_file:
if cors_file.read() == cors_domain:
return True
except OSError, e:
print "Failed to open file at %s. \n%s" % (old_httpd_cors_file, str(e))
for domain in cors_domain_list:
if cors_string:
cors_string += '|'
cors_string += re.escape(domain)
try:
with open(httpd_cors_file, 'w') as file:
file.write('SetEnvIf Origin "^http(s)?://(.+\.)?(%s)$" origin_is=$0\n' % cors_string)
file.write('Header always set Access-Control-Allow-Origin %{origin_is}e env=origin_is')
except OSError, e:
print "ERROR while writing CORS changes to %s.\n %s" % (httpd_cors_file, str(e))
return False
# Save current cors domain list
try:
with open(old_httpd_cors_file, 'w') as cors_file:
cors_file.write(cors_domain)
except OSError, e:
print "Failed to open file at %s. \n%s" % (old_httpd_cors_file, str(e))
return False
# Restart httpd process
try:
subprocess.call(httpd_gracefull_bin)
except OSError, e:
print "Failed to execute command %s.\n %s" % (httpd_gracefull_bin, str(e))
return False
def applyEditChage(parser):
parameter_tmp_file = os.path.join(parser.config_folder, 'config.tmp.json')
config_file = os.path.join(parser.config_folder, 'config.json')
parameter_config_file = os.path.join(parser.config_folder, 'config.parameters.json')
if not os.path.exists(parameter_tmp_file) or not os.path.isfile(parameter_tmp_file):
return {}
if not os.path.exists(config_file):
print "ERROR: Config file doesn't exist... Exiting"
return {}
new_parameter_list = []
parameter_list = []
description_dict = {}
result_dict = {}
try:
with open(parameter_tmp_file) as tmpfile:
new_parameter_list = json.loads(tmpfile.read())
except ValueError:
print "Error: Couldn't parse json file %s" % parameter_tmp_file
with open(parameter_config_file) as tmpfile:
description_dict = json.loads(tmpfile.read())
for i in range(0, len(new_parameter_list)):
key = new_parameter_list[i]['key']
if key != '':
description_entry = description_dict[key]
if description_entry['type'] == 'file':
result_dict[key] = fileWrite(description_entry['file'], new_parameter_list[i]['value'])
elif description_entry['type'] == 'htpasswd':
result_dict[key] = htpasswdWrite(parser.htpasswd_bin, description_entry, new_parameter_list[i]['value'])
elif description_entry['type'] == 'httpdcors':
result_dict[key] = httpdCorsDomainWrite(description_entry['cors_file'], description_entry['gracefull_bin'], new_parameter_list[i]['value'])
if (parser.output_cfg_file):
try:
with open(parser.output_cfg_file, 'w') as pfile:
pfile.write('[public]\n')
for parameter in new_parameter_list:
if parameter['key']:
pfile.write('%s = %s\n' % (parameter['key'], parameter['value']))
except OSError, e:
print "Error failed to create file %s" % parser.output_cfg_file
pass
return result_dict
if __name__ == "__main__":
parser = parseArguments()
parameter_tmp_file = os.path.join(parser.config_folder, 'config.tmp.json')
config_file = os.path.join(parser.config_folder, 'config.json')
# Run 4 times with sleep
run_counter = 1
max_runn = 4
sleep_time = 15
while True:
result_dict = applyEditChage(parser)
if result_dict != {}:
status = True
for key in result_dict:
if not result_dict[key]:
status = False
if status and os.path.exists(parameter_tmp_file):
try:
os.unlink(config_file)
except OSError, e:
print "ERROR cannot remove file: %s" % parameter_tmp_file
else:
os.rename(parameter_tmp_file, config_file)
if run_counter == max_runn:
break
else:
run_counter += 1
time.sleep(sleep_time)
#!{{ python_executable }}
#!/usr/bin/env python
password_changed_once_path = "{{ password_changed_once_path }}"
import os
......
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import sys
import os
import stat
import json
import ConfigParser
import traceback
import argparse
import time
import glob
import urllib2
import ssl
from datetime import datetime
OPML_START = """<?xml version="1.0" encoding="UTF-8"?>
<!-- OPML generated by SlapOS -->
<opml version="1.1">
<head>
<title>%(root_title)s</title>
<dateCreated>%(creation_date)s</dateCreated>
<dateModified>%(modification_date)s</dateModified>
</head>
<body>
<outline text="%(outline_title)s">"""
OPML_END = """ </outline>
</body>
</opml>"""
OPML_OUTLINE_FEED = '<outline text="%(title)s" title="%(title)s" type="rss" version="RSS" htmlUrl="%(html_url)s" xmlUrl="%(xml_url)s" url="%(global_url)s" />'
def parseArguments():
"""
Parse arguments for monitor instance.
"""
parser = argparse.ArgumentParser()
parser.add_argument('--config_file',
default='monitor.cfg',
help='Monitor Configuration file')
parser.add_argument('--promise-folder',
action='append', dest='promise_folder_list',
default=[],
help='The path to get promise executable files')
parser.add_argument('--public-folder',
action='append', dest='public_folder',
help='The path of public folder. All files in this folders will have public acess')
parser.add_argument('--private-folder',
action='append', dest='private_folder',
help='The path of private folder. All files in this folders will be accessible with password')
parser.add_argument('--promise-runner',
help='The path of promise runner, use to run promise files')
parser.add_argument('--wrapper-path',
help='Path of monitor generated promise scripts files.')
return parser.parse_args()
def mkdirAll(path):
try:
os.makedirs(path)
except OSError, e:
if e.errno == os.errno.EEXIST and os.path.isdir(path):
pass
else: raise
def softConfigGet(config, *args, **kwargs):
try:
return config.get(*args, **kwargs)
except (ConfigParser.NoOptionError, ConfigParser.NoSectionError):
return None
class Monitoring(object):
def __init__(self, configuration_file):
config = self.loadConfig([configuration_file])
# Set Monitor variables
self.monitor_hal_json = config.get("monitor", "monitor-hal-json")
self.title = config.get("monitor", "title")
self.root_title = config.get("monitor", "root-title")
self.service_pid_folder = config.get("monitor", "service-pid-folder")
self.crond_folder = config.get("monitor", "crond-folder")
self.logrotate_d = config.get("monitor", "logrotate-folder")
self.promise_runner = config.get("monitor", "promise-runner")
self.promise_folder_list = config.get("monitor", "promise-folder-list").split()
self.public_folder = config.get("monitor", "public-folder")
self.private_folder = config.get("monitor", "private-folder")
self.collector_db = config.get("monitor", "collector-db")
self.collect_script = config.get("monitor", "collect-script")
self.webdav_folder = config.get("monitor", "webdav-folder")
self.webdav_url = '%s/share' % config.get("monitor", "base-url")
self.public_url = '%s/public' % config.get("monitor", "base-url")
self.status_history_folder = os.path.join(self.public_folder, 'history')
self.python = config.get("monitor", "python") or "python"
self.public_path_list = config.get("monitor", "public-path-list").split()
self.private_path_list = config.get("monitor", "private-path-list").split()
self.monitor_url_list = config.get("monitor", "monitor-url-list").split()
self.parameter_list = [param.strip() for param in config.get("monitor", "parameter-list").split('\n') if param]
# Use this file to write knowledge0_cfg required by webrunner
self.parameter_cfg_file = config.get("monitor", "parameter-file-path").strip()
self.config_folder = os.path.join(self.private_folder, 'config')
self.promise_dict = {}
for promise_folder in self.promise_folder_list:
self.setupPromiseDictFromFolder(promise_folder)
def loadConfig(self, pathes, config=None):
if config is None:
config = ConfigParser.ConfigParser()
try:
config.read(pathes)
except ConfigParser.MissingSectionHeaderError:
traceback.print_exc()
return config
def readInstanceConfiguration(self):
type_list = ['raw', 'file', 'htpasswd', 'httpdcors']
configuration_list = []
if not self.parameter_list:
return []
for config in self.parameter_list:
config_list = config.strip().split(' ')
# type: config_list[0]
if len(config_list) >= 3 and config_list[0] in type_list:
if config_list[0] == 'raw':
configuration_list.append(dict(
key='',
title=config_list[1],
value=' '.join(config_list[2:])
))
elif (config_list[0] == 'file' or config_list[0] == 'htpasswd') and \
os.path.exists(config_list[2]) and os.path.isfile(config_list[2]):
try:
with open(config_list[2]) as cfile:
parameter = dict(
key=config_list[1],
title=config_list[1],
value=cfile.read(),
description={
"type": config_list[0],
"file": config_list[2]
}
)
if config_list[0] == 'htpasswd':
if len(config_list) != 5 or not os.path.exists(config_list[4]):
print 'htpasswd file is not specified: %s' % str(config_list)
continue
parameter['description']['user'] = config_list[3]
parameter['description']['htpasswd'] = config_list[4]
configuration_list.append(parameter)
except OSError, e:
print 'Cannot read file %s, Error is: %s' % (config_list[2], str(e))
pass
elif config_list[0] == 'httpdcors' and os.path.exists(config_list[2]) and \
os.path.exists(config_list[3]):
old_cors_file = os.path.join(
os.path.dirname(config_list[2]),
'prev_%s' % os.path.basename(config_list[2])
)
try:
cors_content = ""
if os.path.exists(old_cors_file):
with open(old_cors_file) as cfile:
cors_content = cfile.read()
else:
# Create empty file
with open(old_cors_file, 'w') as cfile:
cfile.write("")
parameter = dict(
key=config_list[1],
title=config_list[1],
value=cors_content,
description={
"type": config_list[0],
"cors_file": config_list[2],
"gracefull_bin": config_list[3]
}
)
configuration_list.append(parameter)
except OSError, e:
print 'Cannot read file at %s, Error is: %s' % (old_cors_file, str(e))
pass
return configuration_list
def setupPromiseDictFromFolder(self, 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):
self.promise_dict[filename] = {"path": path,
"configuration": ConfigParser.ConfigParser()}
# 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"])
def createSymlinksFromConfig(self, destination_folder, source_path_list, name=""):
if destination_folder:
if source_path_list:
for path in source_path_list:
path = path.rstrip('/')
dirname = os.path.join(destination_folder, 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 getMonitorTitleFromUrl(self, monitor_url):
# This file should be generated
if not monitor_url.startswith('https://') and not monitor_url.startswith('http://'):
return 'Unknow Instance'
if not monitor_url.endswith('/'):
monitor_url = monitor_url + '/'
context = ssl._create_unverified_context()
url = monitor_url + '/.jio_documents/monitor.global.json' # XXX Hard Coded path
try:
response = urllib2.urlopen(url, context=context)
except urllib2.HTTPError:
return 'Unknow Instance'
else:
try:
monitor_dict = json.loads(response.read())
return monitor_dict.get('title', 'Unknow Instance')
except ValueError, e:
print "Bad Json file at %s" % url
return 'Unknow Instance'
def configureFolders(self):
# configure public and private folder
self.createSymlinksFromConfig(self.webdav_folder, [self.public_folder])
self.createSymlinksFromConfig(self.webdav_folder, [self.private_folder])
#configure jio_documents folder
jio_public = os.path.join(self.webdav_folder, 'jio_public')
jio_private = os.path.join(self.webdav_folder, 'jio_private')
mkdirAll(jio_public)
mkdirAll(jio_private)
mkdirAll(self.status_history_folder)
try:
os.symlink(self.public_folder, os.path.join(jio_public, '.jio_documents'))
except OSError, e:
if e.errno != os.errno.EEXIST:
raise
try:
os.symlink(self.private_folder, os.path.join(jio_private, '.jio_documents'))
except OSError, e:
if e.errno != os.errno.EEXIST:
raise
self.data_folder = os.path.join(self.private_folder, 'data', '.jio_documents')
config_folder = os.path.join(self.config_folder, '.jio_documents')
mkdirAll(self.data_folder)
mkdirAll(config_folder)
try:
os.symlink(os.path.join(self.private_folder, 'data'),
os.path.join(jio_private, 'data'))
except OSError, e:
if e.errno != os.errno.EEXIST:
raise
try:
os.symlink(self.config_folder, os.path.join(jio_private, 'config'))
except OSError, e:
if e.errno != os.errno.EEXIST:
raise
def makeConfigurationFiles(self):
config_folder = os.path.join(self.config_folder, '.jio_documents')
parameter_config_file = os.path.join(config_folder, 'config.parameters.json')
parameter_file = os.path.join(config_folder, 'config.json')
#mkdirAll(config_folder)
parameter_list = self.readInstanceConfiguration()
description_dict = {}
if parameter_list:
for i in range(0, len(parameter_list)):
key = parameter_list[i]['key']
if key:
description_dict[key] = parameter_list[i].pop('description')
with open(parameter_config_file, 'w') as config_file:
config_file.write(json.dumps(description_dict))
with open(parameter_file, 'w') as config_file:
config_file.write(json.dumps(parameter_list))
try:
with open(self.parameter_cfg_file, 'w') as pfile:
pfile.write('[public]\n')
for parameter in parameter_list:
if parameter['key']:
pfile.write('%s = %s\n' % (parameter['key'], parameter['value']))
except OSError, e:
print "Error failed to create file %s" % self.parameter_cfg_file
pass
def generateOpmlFile(self, feed_url_list, output_file):
if os.path.exists(output_file):
creation_date = datetime.fromtimestamp(os.path.getctime(output_file)).utcnow().strftime("%a, %d %b %Y %H:%M:%S +0000")
modification_date = datetime.utcnow().strftime("%a, %d %b %Y %H:%M:%S +0000")
else:
creation_date = modification_date = datetime.utcnow().strftime("%a, %d %b %Y %H:%M:%S +0000")
opml_content = OPML_START % {'creation_date': creation_date,
'modification_date': modification_date,
'outline_title': 'Monitoring RSS Feed list',
'root_title': self.root_title}
opml_content += OPML_OUTLINE_FEED % {'title': self.title,
'html_url': self.public_url + '/feed',
'xml_url': self.public_url + '/feed',
'global_url': "%s/jio_public/" % self.webdav_url}
for feed_url in feed_url_list:
opml_content += OPML_OUTLINE_FEED % {'title': self.getMonitorTitleFromUrl(feed_url + "/share/jio_public/"),
'html_url': feed_url + '/public/feed',
'xml_url': feed_url + '/public/feed',
'global_url': "%s/share/jio_public/" % feed_url}
opml_content += OPML_END
with open(output_file, 'w') as wfile:
wfile.write(opml_content)
def generateLogrotateEntry(self, name, file_list, option_list):
"""
Will add a new entry in logrotate.d folder. This can help to rotate data file daily
"""
content = "%(logfiles)s {\n%(options)s\n}\n" % {
'logfiles': ' '.join(file_list),
'options': '\n'.join(option_list)
}
file_path = os.path.join(self.logrotate_d, name)
with open(file_path, 'w') as flog:
flog.write(content)
def generateMonitorHalJson(self):
monitor_link_dict = {"webdav": {"href": self.webdav_url},
"public": {"href": "%s/public" % self.webdav_url},
"private": {"href": "%s/private" % self.webdav_url},
"rss": {"href": "%s/feed" % self.public_url},
"jio_public": {"href": "%s/jio_public/" % self.webdav_url},
"jio_private": {"href": "%s/jio_private/" % self.webdav_url}
}
if self.title:
self.monitor_dict["title"] = self.title
if self.monitor_url_list:
monitor_link_dict["related_monitor"] = [{"href": url}
for url in self.monitor_url_list]
self.monitor_dict["_links"] = monitor_link_dict
if self.promise_items:
service_list = []
for service_name, promise in self.promise_items:
service_config = promise["configuration"]
tmp = softConfigGet(service_config, "service", "title")
service_dict = {}
service_dict["id"] = service_name
service_dict["_links"] = {"status": {"href": "%s/public/%s.status.json" % (self.webdav_url, service_name)}} # hardcoded
if tmp:
service_dict["title"] = tmp
service_list.append(service_dict)
self.monitor_dict["_embedded"] = {"service": service_list}
with open(self.monitor_hal_json, "w") as fp:
json.dump(self.monitor_dict, fp)
def generateServiceCronEntries(self):
# XXX only if at least one configuration file is modified, then write in the cron
#cron_line_list = ['PATH=%s\n' % os.environ['PATH']]
cron_line_list = []
service_name_list = [name.replace('.status.json', '')
for name in os.listdir(self.public_folder) if name.endswith('.status.json')]
for service_name, promise in self.promise_items:
service_config = promise["configuration"]
service_status_path = "%s/%s.status.json" % (self.public_folder, service_name) # hardcoded
mkdirAll(os.path.dirname(service_status_path))
promise_cmd_line = [
softConfigGet(service_config, "service", "frequency") or "* * * * *",
self.promise_runner,
'--pid_path %s' % os.path.join(self.service_pid_folder,
"%s.pid" % service_name),
'--output %s' % service_status_path,
'--promise_script %s' % promise["path"],
'--promise_name "%s"' % service_name,
'--monitor_url "%s/jio_private/"' % self.webdav_url, # XXX hardcoded,
'--history_folder %s' % self.status_history_folder,
'--instance_name "%s"' % self.title,
'--hosting_name "%s"' % self.root_title]
cron_line_list.append(' '.join(promise_cmd_line))
if service_name in service_name_list:
service_name_list.pop(service_name_list.index(service_name))
"""wrapper_path = os.path.join(self.wraper_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 )"""
if service_name_list != []:
# XXX Some service was removed, delete his status file so monitor will not consider his status anymore
for service_name in service_name_list:
status_path = os.path.join(self.public_folder, '%s.status.json' % service_name)
if os.path.exists(status_path):
try:
os.unlink(status_path)
except OSError, e:
print "Error: Failed to delete %s" % status_path, str(e)
pass
with open(self.crond_folder + "/monitor-promises", "w") as fp:
fp.write("\n".join(cron_line_list))
def addCronEntry(self, name, frequency, command):
entry_line = '%s %s' % (frequency, command)
cron_entry_file = os.path.join(self.crond_folder, name)
with open(cron_entry_file, "w") as cronf:
cronf.write(entry_line)
def bootstrapMonitor(self):
# create symlinks from service configurations
self.promise_items = self.promise_dict.items()
for service_name, promise in self.promise_items:
service_config = promise["configuration"]
public_path_list = softConfigGet(service_config, "service", "public-path-list")
private_path_list = softConfigGet(service_config, "service", "private-path-list")
if public_path_list:
self.createSymlinksFromConfig(self.public_folder,
public_path_list.split(),
service_name)
if private_path_list:
self.createSymlinksFromConfig(self.private_folder,
private_path_list.split(),
service_name)
# create symlinks from monitor.conf
self.createSymlinksFromConfig(self.public_folder, self.public_path_list)
self.createSymlinksFromConfig(self.private_folder, self.private_path_list)
self.configureFolders()
# generate monitor.json
self.monitor_dict = {}
self.generateMonitorHalJson()
# Generate OPML file
self.generateOpmlFile(self.monitor_url_list,
os.path.join(self.public_folder, 'feeds'))
# put promises to a cron file
self.generateServiceCronEntries()
# Generate parameters files and scripts
self.makeConfigurationFiles()
# Rotate monitor data files
option_list = [
'daily', 'nocreate', 'noolddir', 'rotate 30',
'nocompress', 'extension .json', 'dateext',
'dateformat -%Y-%m-%d', 'notifempty'
]
file_list = ["%s/*.data.json" % self.data_folder]
self.generateLogrotateEntry('monitor.data', file_list, option_list)
# Add cron entry for SlapOS Collect
command = "%s %s --output_folder %s --collector_db %s" % (self.python,
self.collect_script, self.data_folder, self.collector_db)
self.addCronEntry('monitor_collect', '* * * * *', command)
return 0
if __name__ == "__main__":
parser = parseArguments()
monitor = Monitoring(parser.config_file)
sys.exit(monitor.bootstrapMonitor())
#!/usr/bin/env python
import json
import os
import time
from datetime import datetime
OPML_START = """<?xml version="1.0" encoding="UTF-8"?>
<!-- OPML generated by SlapOS -->
<opml version="1.1">
<head>
<title>SlapOS Monitoring Status Lists</title>
<dateCreated>%(creation_date)s</dateCreated>
<dateModified>%(mondification_date)s</dateModified>
</head>
<body>
<outline text="%(outline_title)s">"""
OPML_END = """ </outline>
</body>
</opml>"""
OPML_OUTLINE_FEED = '<outline text="%(title)s" title="%(title)s" type="rss" version="RSS" htmlUrl="%(html_url)s" xmlUrl="%(xml_url)s" />'
def main(config_file, output_file):
feed_url_list = []
if os.path.exists(output_file):
creation_date = datetime.fromtimestamp(os.path.getctime(output_file)).utcnow().strftime("%a, %d %b %Y %H:%M:%S +0000")
modification_date = datetime.utcnow().strftime("%a, %d %b %Y %H:%M:%S +0000")
else:
creation_date = modification_date = datetime.utcnow().strftime("%a, %d %b %Y %H:%M:%S +0000")
with open(config_file, 'r') as fconfig:
feed_url_list = json.loads(fconfig.read())
opml_content = OPML_START
for feed_line in feed_url_list:
opml_content += OPML_OUTLINE_FEED % {'title': feed_line['title'], 'html_url': feed_line['url'], 'xml_url': feed_line['url']}
opml_content += OPML_END
with open(output_file, 'w') as wfile:
wfile.write(opml_content)
if __name__ == "__main__":
if len(sys.argv) < 3:
print("Usage: %s <rss_conf_file> <output_path>" % sys.argv[0])
sys.exit(2)
config_file = sys.argv[1]
output_file = sys.argv[2]
main(config_file, output_file)
\ No newline at end of file
#!{{ python }}
# -*- coding: utf-8 -*-
import sys
import os
import subprocess
import json
import psutil
import time
from shutil import copyfile
import glob
import argparse
def parseArguments():
"""
Parse arguments for monitor collector instance.
"""
parser = argparse.ArgumentParser()
parser.add_argument('--pid_path',
help='Path where the pid of this process will be writen.')
parser.add_argument('--output',
help='The Path of file where Json result of this promise will be saved.')
parser.add_argument('--promise_script',
help='Promise script to execute.')
parser.add_argument('--promise_name',
help='Title to give to this promise.')
parser.add_argument('--monitor_url',
help='Monitor Instance website URL.')
parser.add_argument('--history_folder',
help='Path where old result file will be placed before generate a new json result file.')
parser.add_argument('--instance_name',
default='UNKNOW Software Instance',
help='Software Instance name.')
parser.add_argument('--hosting_name',
default='UNKNOW Hosting Subscription',
help='Hosting Subscription name.')
return parser.parse_args()
def main():
parser = parseArguments()
if os.path.exists(parser.pid_path):
with open(parser.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
start_date = ""
with open(parser.pid_path, "w") as pidfile:
process = executeCommand(parser.promise_script)
ps_process = psutil.Process(process.pid)
start_date = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(ps_process.create_time()))
pidfile.write(str(process.pid))
status_json = generateStatusJsonFromProcess(process, start_date=start_date)
status_json['_links'] = {"monitor": {"href": parser.monitor_url}}
status_json['title'] = parser.promise_name
status_json['instance'] = parser.instance_name
status_json['hosting_subscription'] = parser.hosting_name
# Save the lastest status change date (needed for rss)
status_json['change-time'] = ps_process.create_time()
if os.path.exists(parser.output):
with open(parser.output) as f:
last_result = json.loads(f.read())
if status_json['status'] == last_result['status'] and last_result.has_key('change-time'):
status_json['change-time'] = last_result['change-time']
updateStatusHistoryFolder(
parser.promise_name,
parser.output,
parser.history_folder
)
with open(parser.output, "w") as outputfile:
json.dump(status_json, outputfile)
os.remove(parser.pid_path)
def updateStatusHistoryFolder(name, status_file, history_folder):
old_history_list = []
history_path = os.path.join(history_folder, name, '.jio_documents')
if not os.path.exists(status_file):
return
if not os.path.exists(history_folder):
return
if not os.path.exists(history_path):
try:
os.makedirs(history_path)
except OSError, e:
if e.errno == os.errno.EEXIST and os.path.isdir(history_path):
pass
else: raise
with open(status_file, 'r') as sf:
status_dict = json.loads(sf.read())
filename = '%s.status.json' % (
status_dict['start-date'].replace(' ', '_').replace(':', ''))
copyfile(status_file, os.path.join(history_path, filename))
# Don't let history foler grow too much, keep 40 files
file_list = filter(os.path.isfile,
glob.glob("%s/*.status.json" % history_path)
)
file_count = len(file_list)
if file_count > 40:
file_list.sort(key=lambda x: os.path.getmtime(x))
while file_count > 40:
to_delete = file_list.pop(0)
try:
os.unlink(to_delete)
file_count -= 1
except OSError:
raise
def generateStatusJsonFromProcess(process, start_date=None, title=None):
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["message"] = stderr
if start_date:
status_json["start-date"] = start_date
if title:
status_json["title"] = title
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())
import sys
import os
import json
import datetime
import base64
import hashlib
import PyRSS2Gen
import argparse
def parseArguments():
"""
Parse arguments for monitor Rss Generator.
"""
parser = argparse.ArgumentParser()
parser.add_argument('--items_folder',
help='Path where to get *.status.json files which contain result of promises.')
parser.add_argument('--output',
help='The Path of file where feed file will be saved.')
parser.add_argument('--public_url',
help='Monitor Instance public URL.')
parser.add_argument('--private_url',
help='Monitor Instance private URL.')
parser.add_argument('--instance_name',
default='UNKNOW Software Instance',
help='Software Instance name.')
parser.add_argument('--hosting_name',
default='',
help='Hosting Subscription name.')
return parser.parse_args()
def getKey(item):
return item.pubDate
def main():
parser = parseArguments()
rss_item_list = []
for filename in os.listdir(parser.items_folder):
if filename.endswith(".status.json"):
filepath = os.path.join(parser.items_folder, filename)
result_dict = None
try:
result_dict = json.load(open(filepath, "r"))
except ValueError:
print "Failed to load json file: %s" % filepath
continue
description = result_dict.get('message', '')
event_time = datetime.datetime.fromtimestamp(result_dict['change-time'])
rss_item = PyRSS2Gen.RSSItem(
categories = [result_dict['status']],
source = PyRSS2Gen.Source(result_dict['title'], parser.public_url),
title = '[%s] %s' % (result_dict['status'], result_dict['title']),
comments = description,
description = "%s: %s\n%s" % (event_time, result_dict['status'], description),
link = parser.private_url,
pubDate = event_time,
guid = PyRSS2Gen.Guid(base64.b64encode("%s, %s" % (event_time, result_dict['status'])))
)
rss_item_list.append(rss_item)
### Build the rss feed
sorted(rss_item_list, key=getKey)
rss_feed = PyRSS2Gen.RSS2 (
title = parser.instance_name,
link = parser.public_url,
description = parser.hosting_name,
lastBuildDate = datetime.datetime.utcnow(),
items = rss_item_list
)
with open(parser.output, 'w') as frss:
frss.write(rss_feed.to_xml())
if __name__ == "__main__":
exit(main())
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()
{% if domain -%}
{% set allow_domain = '|'.join(domain.replace('.', '\.').split()) -%}
SetEnvIf Origin "^http(s)?://(.+\.)?({{ allow_domain }})$" ORIGIN_DOMAIN=$0
Header always set Access-Control-Allow-Origin "%{ORIGIN_DOMAIN}e" env=ORIGIN_DOMAIN
{% endif -%}
\ No newline at end of file
[instance]
name = {{ instance_dict['name'] }}
root-name = {{ instance_dict['root-name'] }}
computer = {{ instance_dict['computer-id'] }}
ipv4 = {{ instance_dict['ipv4'] }}
ipv6 = {{ instance_dict['ipv6'] }}
software-release = {{ instance_dict['software-release'] }}
software-type = {{ instance_dict['software-type'] }}
partition = {{ instance_dict['partition-id'] }}
\ No newline at end of file
......@@ -11,7 +11,7 @@ ServerAdmin someone@email
Listen [{{ parameter_dict.get('listening-ip') }}]:{{ parameter_dict.get('port') }}
Define MonitorPort
</IfDefine>
DocumentRoot "{{ directory.get('www') }}"
DocumentRoot "{{ directory.get('webdav') }}"
ErrorLog "{{ parameter_dict.get('error-log') }}"
LoadModule unixd_module modules/mod_unixd.so
LoadModule access_compat_module modules/mod_access_compat.so
......@@ -30,6 +30,11 @@ 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
LoadModule headers_module modules/mod_headers.so
LoadModule dav_module modules/mod_dav.so
LoadModule dav_fs_module modules/mod_dav_fs.so
LoadModule env_module modules/mod_env.so
LoadModule setenvif_module modules/mod_setenvif.so
# SSL Configuration
<IfDefine !SSLConfigured>
......@@ -47,6 +52,50 @@ SSLCipherSuite RC4-SHA:HIGH:!ADH
AddType application/hal+json .haljson
SSLEngine On
Include {{ parameter_dict.get('httpd-cors-config-file') }}
Header set Access-Control-Allow-Credentials "true"
Header set Access-Control-Allow-Methods "PROPFIND, PROPPATCH, COPY, MOVE, DELETE, MKCOL, LOCK, UNLOCK, PUT, GETLIB, VERSION-CONTROL, CHECKIN, CHECKOUT, UNCHECKOUT, REPORT, UPDATE, CANCELUPLOAD, HEAD, OPTIONS, GET, POST"
Header set Access-Control-Allow-Headers "Overwrite, Destination, Content-Type, Depth, User-Agent, X-File-Size, X-Requested-With, If-Modified-Since, X-File-Name, Cache-Control, Authorization"
{% if parameter_dict.has_key('monitor-url-list') -%}
RewriteEngine on
SSLProxyEngine on
ProxyPreserveHost On
SSLProxyVerify none
SSLProxyCheckPeerCN off
SSLProxyCheckPeerName off
{% set index=1 -%}
{% set monitor_url_list = parameter_dict.get('monitor-url-list').split('\n') -%}
{% for url in monitor_url_list -%}
{% if url.strip() -%}
RewriteRule /monitor{{ index }}/(.*) {{ url }}/$1 [L,P]
{% set index = index + 1 -%}
{% endif -%}
{% endfor -%}
{% endif -%}
DavLockDB {{ directory.get('monitor-var') }}/DavLock
Alias /share {{ directory.get('webdav') }}
<Directory {{ directory.get('webdav') }}>
DirectoryIndex disabled
DAV On
Options Indexes FollowSymLinks
AuthType Basic
AuthName "webdav"
AuthUserFile "{{ parameter_dict.get('htpasswd-file') }}"
<LimitExcept OPTIONS>
Require valid-user
</LimitExcept>
</Directory>
<LocationMatch "/share/(jio_)?public">
<Limit GET HEAD OPTIONS REPORT PROPFIND>
Allow from all
Satisfy any
</Limit>
</LocationMatch>
ScriptSock {{ parameter_dict.get('cgid-pid-file') }}
<Directory {{ directory.get('www') }}>
SSLVerifyDepth 1
......@@ -55,6 +104,7 @@ ScriptSock {{ parameter_dict.get('cgid-pid-file') }}
# XXX: security????
DirectoryIndex index.html
Options FollowSymLinks
AllowOverride All
Order Deny,Allow
AuthType Basic
AuthName "Private access"
......@@ -103,3 +153,6 @@ Alias /cgi-bin {{ directory.get('cgi-bin') }}
Options Indexes FollowSymLinks
Satisfy all
</Directory>
{% if parameter_dict.get('httpd-include-file', '') -%}
Include {{ parameter_dict.get('httpd-include-file') }}
{% endif -%}
#!{{ 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)
<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>
#!{{ 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>
"""
#!{{ 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>"
$(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
/*!
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
$(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();
}
});
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;
}
<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>
#!{{ 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>"
#!{{ 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>"
[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 = eaedae330cd155f8b693b418286d0d98
[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 = 3451788c49d3664cd9b72551fab34a9b
[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 = 6d5f1ceff198262319566ee25093c350
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_password_promise_template ${monitor-password-promise:location}/${monitor-password-promise:filename}
raw monitor_password_cgi_template ${monitor-password-cgi:location}/${monitor-password-cgi:filename}
raw monitor_password_promise_interface_template ${monitor-password-promise-interface:location}/${monitor-password-promise-interface: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_template ${monitor-httpd-conf:location}/${monitor-httpd-conf:filename}
raw monitor_service_conf_template ${monitor-service-conf-template:location}/${monitor-service-conf-template: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}
raw status2rss_executable_path ${status2rss-executable:location}/${status2rss-executable:filename}
[monitor-httpd-conf]
<= monitor-download-base
md5sum = 625d3d948c0af7b4848d7fad92bfb844
filename = monitor-httpd.conf.in
[monitor-service-conf-template]
<= monitor-download-base
filename = monitor-service.cfg.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-promise]
<= monitor-download-base
filename = monitor-password-promise.py.in
md5sum = 0a9a42551ed6bdb973fd1f0dd1d4ec86
[monitor-password-cgi]
<= monitor-download-base
md5sum = 04fc7e6d892d29a601cfd43d1700eeda
filename = monitor-password.py.cgi
[monitor-password-promise-interface]
<= monitor-download-base
filename = monitor-password-interface.html
md5sum = 04b664dfb47bfd3d01502768311aa239
[status2rss-executable]
<= monitor-download-base
filename = status2rss.py
md5sum = 65315ded80cd72f54f6e12d06ce813c4
[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
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 %}
<!DOCTYPE html>
<html>
<head>
<title>Promise status</title>
<style>
input, button {
min-height: 10mm;
min-width: 10mm;
}
</style>
<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>
<!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>
#!${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
<!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>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Monitor password</title>
<style>
input, button {
min-height: 10mm;
min-width: 10mm;
}
</style>
<script>
var service_name = "monitor-password", // XXX hardcoded
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,
password_cgi_url_part = "/cgi-bin/monitor-password.cgi?password=";
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(); };
if (param.username) {
xhr.open((param.method || "GET").toUpperCase(), param.url, true, param.username, param.password);
} else {
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 {
r = new Error("HTTP: " + (t.status ? t.status + " " : "") + (t.statusText || "Unknown"));
r.target = t;
d.reject(r);
}
}, 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 MonitorPasswordInterface(config) {
var it = this,
statusP = document.createElement("p"),
descriptionP = document.createElement("p"),
form = document.createElement("form"),
formPassword1Input = document.createElement("input"),
formPassword2Input = document.createElement("input"),
formChangePasswordButton = document.createElement("button"),
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.formPassword1Input = formPassword1Input;
this.formPassword2Input = formPassword2Input;
this.formChangePasswordButton = formChangePasswordButton;
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";
this.element.appendChild(h1);
h1.textContent = "Monitor password";
this.element.appendChild(statusP);
this.element.appendChild(descriptionP);
this.element.appendChild(form);
form.appendChild(formPassword1Input);
formPassword1Input.setAttribute("type", "password");
form.onsubmit = this.onFormSubmit.bind(this);
form.appendChild(document.createElement("br"));
form.appendChild(formPassword2Input);
formPassword2Input.setAttribute("type", "password");
form.appendChild(document.createElement("br"));
form.appendChild(formChangePasswordButton);
formChangePasswordButton.setAttribute("type", "submit");
formChangePasswordButton.textContent = "Change password";
this.element.appendChild(errorH2);
errorH2.textContent = "Operational error";
errorH2.style.display = "none";
this.element.appendChild(errorPre);
errorPre.style.display = "none";
this.loadStatusUi();
this.loadDescriptionUi();
this.loadErrorUi();
}
MonitorPasswordInterface.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;
};
MonitorPasswordInterface.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.innerHTML = "&nbsp;";
} else {
this.statusP.textContent = "/!\\ The password needs to be changed at least once! /!\\";
}
}.bind(this), function (reason) {
if (reason && reason.target && reason.target.status === 404) {
this.statusP.textContent = "/!\\ The password needs to be changed at least once! /!\\";
return;
}
var message = reason && (reason.target && (reason.target.statusText || "Unknown") || reason.message);
this.statusP.textContent = "Status Json Error: " + (message || "Unknown error");
}.bind(this)).catch(unexpectedError);
};
MonitorPasswordInterface.prototype.loadDescriptionUi = function () {
this.descriptionP.textContent = [
"The monitor password is the password used to connect to this interface.",
"Here you can change the monitor password by filling the formular just below."
].join("\n");
};
MonitorPasswordInterface.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);
};
MonitorPasswordInterface.prototype.onFormSubmit = function (event) {
event.preventDefault();
event.stopPropagation();
this.execForm();
};
MonitorPasswordInterface.prototype.execForm = function () {
if (this.formPassword1Input.value !== this.formPassword2Input.value) {
this.statusP.textContent = "The two typed passwords should match!";
return;
}
this.statusP.textContent = "Changing password...";
var password = this.formPassword1Input.value;
return Promise.resolve().then(function () {
return xhr({url: password_cgi_url_part + password, method: "POST", withCredentials: true});
}).then(function () {
this.statusP.textContent = "Password changed succesfully!";
this.formPassword1Input.value = this.formPassword2Input.value = "";
// rerun promise with new login (also does the relogin)
xhr({url: rerun_cgi_url, method: "POST", withCredentials: true, username: "admin", password: password});
}.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));
};
MonitorPasswordInterface.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 MonitorPasswordInterface({rootElement: document.body});
});
</script>
</head>
<body>
<h1>Monitor password</h1>
<noscript>Javascript should be enabled</noscript>
</body>
</html>
[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}
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;
}
/*jslint indent: 2 */
(function () {
"use strict";
var monitor_title = '{{ dumps(monitor_title)[5:-1] }}',
RSS_ICON_DATA_URI = [
"data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIj8+CjwhRE9DVFlQR",
"SBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cu",
"dzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+CjxzdmcgeG1sbnM",
"9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB2ZXJzaW9uPSIxLjEiIHdpZHRoPS",
"IxMjhweCIgaGVpZ2h0PSIxMjhweCIgdmlld0JveD0iMCAwIDI1NiAyNTYiPgo8cmVjd",
"CB3aWR0aD0iMjU2IiBoZWlnaHQ9IjI1NiIgeD0iMCIgIHk9IjAiICBmaWxsPSIjRjQ5",
"QzUyIi8+CjxjaXJjbGUgY3g9IjY4IiBjeT0iMTg5IiByPSIyNCIgZmlsbD0iI0ZGRiI",
"vPgo8cGF0aCBkPSJNMTYwIDIxM2gtMzRhODIgODIgMCAwIDAgLTgyIC04MnYtMzRhMT",
"E2IDExNiAwIDAgMSAxMTYgMTE2eiIgZmlsbD0iI0ZGRiIvPgo8cGF0aCBkPSJNMTg0I",
"DIxM0ExNDAgMTQwIDAgMCAwIDQ0IDczIFYgMzhhMTc1IDE3NSAwIDAgMSAxNzUgMTc1",
"eiIgZmlsbD0iI0ZGRiIvPgo8L3N2Zz4K"
].join("");
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>",
" <a href=\"/feed\"><img src=\"" + RSS_ICON_DATA_URI + "\" style=\"width: 10mm; height: 10mm; vertical-align: middle;\" alt=\"[RSS Feed]\" /></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;
[].reduce.call(element_list, function (array, element) {
if (element.parentNode.parentNode) { return array; }
array.push(element);
return array;
}, []).forEach(function (element) {
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);
});
}());
#!{{ 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())
#!{{ 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())
import sys
import os
import json
import datetime
import base64
import hashlib
def main():
_, title, link, public_folder, previous_status_path, output_path = sys.argv
final_status = "OK";
# getting status
for filename in os.listdir(public_folder):
if filename.endswith(".status.json"):
filepath = os.path.join(public_folder, filename)
status = None
try:
status = json.load(open(filepath, "r"))
except ValueError:
continue
try:
if status["status"] != "OK":
final_status = "BAD"
break
except KeyError:
final_status = "BAD"
break
# checking previous status
try:
status = open(previous_status_path, "r").readline(4)
if status == final_status:
return 0
except IOError:
pass
# update status
open(previous_status_path, "w").write(final_status)
# generating RSS
utcnow = datetime.datetime.utcnow()
open(output_path, "w").write(
newRssString(
title,
title,
link,
utcnow,
utcnow,
"60",
[
newRssItemString(
"Status is %s" % final_status,
"Status is %s" % final_status,
link,
newGuid("%s, %s" % (utcnow, final_status)),
utcnow,
)
],
)
)
def escapeHtml(string):
return string.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace("\"", "&quot;")
def newGuid(string):
sha256 = hashlib.sha256()
sha256.update(string)
return sha256.hexdigest()
def newRssItemString(title, description, link, guid, pub_date, guid_is_perma_link=True):
return """<item>
<title>%(title)s</title>
<description>%(description)s</description>
<link>%(link)s</link>
<guid isPermaLink="%(guid_is_perma_link)s">%(guid)s</guid>
<pubDate>%(pub_date)s</pubDate>
</item>""" % {
"title": escapeHtml(title),
"description": escapeHtml(description),
"link": escapeHtml(link),
"guid": escapeHtml(guid),
"pub_date": escapeHtml(pub_date.strftime("%a, %d %b %Y %H:%M:%S +0000")),
"guid_is_perma_link": escapeHtml(repr(guid_is_perma_link).lower()),
}
def newRssString(title, description, link, last_build_date, pub_date, ttl, rss_item_string_list):
return """<?xml version="1.0" encoding="UTF-8" ?>
<rss version="2.0">
<channel>
<title>%(title)s</title>
<description>%(description)s</description>
<link>%(link)s</link>
<lastBuildDate>%(last_build_date)s</lastBuildDate>
<pubDate>%(pub_date)s</pubDate>
<ttl>%(ttl)s</ttl>
%(items)s
</channel>
</rss>
""" % {
"title": escapeHtml(title),
"description": escapeHtml(description),
"link": escapeHtml(link),
"last_build_date": escapeHtml(last_build_date.strftime("%a, %d %b %Y %H:%M:%S +0000")),
"pub_date": escapeHtml(pub_date.strftime("%a, %d %b %Y %H:%M:%S +0000")),
"ttl": escapeHtml(str(ttl)),
"items": "\n\n".join([" " + item.replace("\n", "\n ") for item in rss_item_string_list]),
}
if __name__ == "__main__":
exit(main())
#!{{ 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)
<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>
#!{{ 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>
"""
#!{{ 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>"
$(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
/*!
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
$(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();
}
});
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;
}
<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>
#!{{ 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>"
#!{{ 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>"
#!${dash-output:dash}
{{ content }}
\ No newline at end of file
......@@ -38,7 +38,7 @@ eggs = collective.recipe.template
recipe = slapos.recipe.template
url = ${:_profile_base_location_}/pbsready.cfg.in
output = ${buildout:directory}/pbsready.cfg
md5sum = 4962204261e38c782c80ab9ca71410e9
md5sum = d2b06a13354127e9cbbf1c5d21791cb4
mode = 0644
[pbsready-import]
......@@ -63,13 +63,13 @@ mode = 0644
recipe = slapos.recipe.template
url = ${:_profile_base_location_}/instance-pull-backup.cfg.in
output = ${buildout:directory}/instance-pull-backup.cfg
md5sum = 932acec2718e3daef839886b71e667cd
md5sum = 028dfc01dfb0d738e1b4793f67b24e8c
mode = 0644
[template-replicated]
recipe = slapos.recipe.build:download
url = ${:_profile_base_location_}/template-replicated.cfg.in
md5sum = 738f21e9fbfaee0d150d49da0afd7435
md5sum = 7a6234465ae845cb262d4f94c158764e
mode = 0644
destination = ${buildout:directory}/template-replicated.cfg.in
......
......@@ -8,29 +8,12 @@ parts =
cron-entry-logrotate
sshkeys-authority
sshkeys-dropbear
## Monitoring part
###Parts to add for monitoring
certificate-authority
cron-entry-monitor
cron-entry-rss
deploy-index
deploy-settings-cgi
deploy-status-cgi
deploy-status-history-cgi
setup-static-files
certificate-authority
zero-parameters
public-symlink
cgi-httpd-wrapper
cgi-httpd-graceful-wrapper
monitor-promise
monitor-instance-log-access
## Monitor for pbs
monitor-current-log-access
monitor-backup-log-access
monitor-check-resilient-feed-file
extends = ${monitor-template:output}
extends = ${monitor2-template:rendered}
eggs-directory = ${buildout:eggs-directory}
develop-eggs-directory = ${buildout:develop-eggs-directory}
offline = true
......@@ -234,11 +217,20 @@ identity-file = $${basedirectory:ssh-home}/id_rsa
#----------------
#--
#-- Slave instance list (empty default).
[htpasswd]
recipe = slapos.cookbook:generate.password
storage-path = $${directory:etc}/.monitor_user
bytes = 8
username = admin
[slap-parameter]
slave_instance_list = []
ignore-known-hosts-file = false
monitor-cors-domains =
monitor-httpd-port = 8070
monitor-title = PBS Instance
monitor-password = $${htpasswd:passwd}
monitor-username = $${htpasswd:username}
#----------------
......@@ -250,27 +242,34 @@ recipe = slapos.cookbook:publish
ssh-key = $${sshkeys-dropbear:public-key-value}
notification-url = http://[$${notifier:host}]:$${notifier:port}/notify
feeds-url = http://[$${notifier:host}]:$${notifier:port}/get/
monitor-base-url = $${publish:monitor-base-url}
monitor-url = $${publish:monitor-url}
monitor-user = $${publish:monitor-user}
monitor-password = $${publish:monitor-password}
#----------------
#--
#-- Monitor
[monitor-backup-log-access]
< = monitor-directory-access
source = $${directory:logrotate-backup}
[monitor-instance-parameter]
monitor-httpd-port = $${slap-parameter:monitor-httpd-port}
monitor-title = $${slap-parameter:monitor-title}
cors-domains = $${slap-parameter:monitor-cors-domains}
username = $${slap-parameter:monitor-username}
password = $${slap-parameter:monitor-password}
[monitor-current-log-access]
< = monitor-directory-access
source = $${basedirectory:log}
[monitor-conf-parameters]
private-path-list +=
$${directory:logrotate-backup}
$${basedirectory:log}
[monitor-check-resilient-feed-file]
recipe = slapos.recipe.template:jinja2
template = ${template-monitor-check-resilient-feed:location}/${template-monitor-check-resilient-feed:filename}
rendered = $${monitor-directory:monitor-custom-scripts}/check-create-resilient-feed-files.py
rendered = $${monitor-directory:promises}/check-create-resilient-feed-files
mode = 700
context =
key input_feed_directory directory:notifier-feeds
key monitor_feed_directory monitor-directory:public-cgi
key monitor_feed_directory monitor-directory:public
key base_url publish-connection-information:feeds-url
raw python_executable ${buildout:executable}
......@@ -192,7 +192,7 @@ key = $${slap-parameter:authorized-key}
recipe = collective.recipe.template
log = $${basedirectory:log}/sshd.log
input = inline:#!/bin/sh
exec $${dropbear-server:wrapper} >> $${:log}/sshd.log 2>&1
exec $${dropbear-server:wrapper} >> $${:log} 2>&1
output = $${rootdirectory:bin}/raw_sshd_log
mode = 700
......
{% macro replicate(namebase, nbbackup, typeexport, typeimport, heriteLeader='', heriteBackup='', slapparameter_dict={}) %}
{% macro replicate(namebase, nbbackup, typeexport, typeimport, heriteLeader='', heriteBackup='', slapparameter_dict={}, monitor_parameter_dict={}) %}
{% set sla_parameter_dict = {} -%}
{% set monitor_dict = monitor_parameter_dict.get('parameter', {}) -%}
{% set monitor_return = ' '.join(monitor_parameter_dict.get('return', [])) -%}
{% set monitor_url_list = [] -%}
# prepare sla-parameters
{% if slapparameter_dict is defined -%}
{% for key in slapparameter_dict.keys() -%}
......@@ -30,15 +33,20 @@ software-url = ${slap-connection:software-release-url}
{{heriteLeader}}
software-type = {{typeexport}}
name = {{namebase}}0
return = ssh-public-key ssh-url notification-id ip
return = ssh-public-key ssh-url notification-id ip {{ monitor_return }}
config-number = 0
config-authorized-key = {% for id in range(1,nbbackup|int) %} ${request-pbs-{{namebase}}-{{id}}:connection-ssh-key}{% endfor %}
config-notify = {% for id in range(1,nbbackup|int) %} ${request-pbs-{{namebase}}-{{id}}:connection-notification-url}{% endfor %}
config-name = {{namebase}}0
# Bubble up all the instance parameters to the requested export instance.
{% if slapparameter_dict is defined %}
{% for parameter_name, parameter_value in slapparameter_dict.items() %}config-{{parameter_name}} = {{parameter_value}}
{% endfor %}
{% endif %}
{% for key, value in monitor_dict.iteritems() -%}
config-{{ key }} = {{ value }}
{% endfor -%}
{% if sla_parameter_dict == {} -%}
sla-mode = unique_by_network
{% else %}
......@@ -59,6 +67,10 @@ sla-{{ key }} = {{ value }}
{% endfor -%}
{% endif -%}
{% if 'monitor-base-url' in monitor_return and not monitor_parameter_dict.get('set-monitor-url', False) -%}
{% do monitor_url_list.append('${request-' ~ namebase ~ ':connection-monitor-base-url}') -%}
{% endif -%}
{% for id in range(1,nbbackup|int) %}
[request-{{namebase}}-pseudo-replicating-{{id}}]
......@@ -70,13 +82,17 @@ name = {{namebase}}{{id}}
software-url = ${slap-connection:software-release-url}
software-type = {{typeimport}}
return = ssh-public-key ssh-url notification-url ip takeover-url takeover-password
return = ssh-public-key ssh-url notification-url ip takeover-url takeover-password {{ monitor_return }}
pbs-notification-id = ${slap-connection:computer-id}-${slap-connection:partition-id}-{{namebase}}-{{id}}-push
config-number = {{id}}
config-name = {{namebase}}{{id}}
config-authorized-key = ${request-pbs-{{namebase}}-{{id}}:connection-ssh-key}
config-on-notification = ${request-pbs-{{namebase}}-{{id}}:connection-feeds-url}${:pbs-notification-id}
{% for key, value in monitor_dict.iteritems() -%}
config-{{ key }} = {{ value }}
{% endfor -%}
{% if sla_parameter_dict == {} -%}
sla-mode = unique_by_network
{% else %}
......@@ -102,6 +118,10 @@ feed-url-{{namebase}}-{{id}}-push = ${request-pbs-{{namebase}}-{{id}}:connection
takeover-{{namebase}}-{{id}}-url = ${request-{{namebase}}-pseudo-replicating-{{id}}:connection-takeover-url}
takeover-{{namebase}}-{{id}}-password = ${request-{{namebase}}-pseudo-replicating-{{id}}:connection-takeover-password}
{% if 'monitor-base-url' in monitor_return -%}
{% do monitor_url_list.append('${request-' ~ namebase ~ '-pseudo-replicating-' ~ id ~ ':connection-monitor-base-url}') -%}
{% endif -%}
{% endfor -%}
......@@ -157,7 +177,11 @@ software-type = pull-backup
<= request-pbs-common
name = PBS ({{namebase}} / {{id}})
config-ignore-known-hosts-file = ${slap-parameter:ignore-known-hosts-file}
return = ssh-key notification-url feeds-url
config-monitor-title = PBS ${slap-connection:computer-id}-{{namebase}}-{{id}}
{% for key, value in monitor_dict.iteritems() -%}
config-{{ key }} = {{ value }}
{% endfor -%}
return = ssh-key notification-url feeds-url {{ monitor_return }}
slave = false
{% if sla_parameter_dict == {} -%}
sla-mode = unique_by_network
......@@ -211,6 +235,17 @@ sla-instance_guid = ${request-pbs-{{namebase}}-{{id}}:instance_guid}
[publish-connection-information]
feed-url-{{namebase}}-{{id}}-pull = ${request-pbs-{{namebase}}-{{id}}:connection-feeds-url}${request-pull-backup-server-{{namebase}}-{{id}}:config-notification-id}
{% if 'monitor-base-url' in monitor_return -%}
{% do monitor_url_list.append('${request-pbs-' ~ namebase ~ '-' ~ id ~ ':connection-monitor-base-url}') -%}
{% endif -%}
{% if monitor_parameter_dict.get('set-monitor-url', False) -%}
[request-{{namebase}}]
config-monitor-url-list = {{ monitor_url_list | join(' ') }}
{% else -%}
[monitor-conf-parameters]
monitor-url-list = {{ monitor_url_list | join(' ') }}
{% endif -%}
[request-pull-backup-server-{{namebase}}-backup-{{id}}]
<= request-pbs-common
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment