Merge spotter-appmgr WSGI application

This commit is contained in:
Disassembler 2018-08-02 10:41:40 +02:00
parent edcbb2633e
commit b42fe67f89
Signed by: Disassembler
GPG Key ID: 524BD33A0EE29499
87 changed files with 1972 additions and 965 deletions

View File

@ -4,8 +4,8 @@ set -e
SOURCE_DIR=$(realpath $(dirname "${0}"))/basic
# Install packages
apk --no-cache add --virtual .useful curl git file htop libressl openssh-server openssh-sftp-server
apk --no-cache add docker gettext kbd-misc python3 nginx
apk --no-cache add --virtual .useful git file htop openssh-server openssh-sftp-server
apk --no-cache add curl docker gettext kbd-misc libressl python3 py3-dnspython py3-jinja2 py3-requests py3-werkzeug nginx
# Copy profile files and settings
mkdir -p /root/.config/htop /root/.ssh
@ -38,17 +38,13 @@ cp ${SOURCE_DIR}/etc/nginx/nginx.conf /etc/nginx/nginx.conf
mkdir /etc/acme.sh.d
wget https://raw.githubusercontent.com/Neilpang/acme.sh/master/acme.sh -O /usr/bin/acme.sh
sed -i 's|$HOME/.$PROJECT_NAME|/etc/acme.sh.d|' /usr/bin/acme.sh
cp ${SOURCE_DIR}/etc/periodic/daily/acme-sh /etc/periodic/daily/acme-sh
chmod +x /usr/bin/acme.sh
# Copy Spotter resources
mkdir /etc/spotter
cp ${SOURCE_DIR}/srv/config.json /srv/config.json
cp ${SOURCE_DIR}/usr/bin/spotter-appmgr /usr/bin/spotter-appmgr
cp -r ${SOURCE_DIR}/srv/portal /srv/portal
cp -r ${SOURCE_DIR}/srv/spotter /srv/spotter
ln -s /srv/spotter/cli.py /usr/bin/spotter-appmgr
# Configure services
for SERVICE in consolefont crond nginx ntpd sshd; do
for SERVICE in consolefont crond nginx ntpd sshd spotter-appmgr; do
rc-update add ${SERVICE} boot
service ${SERVICE} start
done
@ -61,5 +57,5 @@ service docker start
# Create basic images
docker build -t alpine ${SOURCE_DIR}
# Set dummy domain and generate related files
spotter-appmgr update-domain spotter.vm 443
# Set dummy host and generate related files
spotter-appmgr update-host spotter.vm 443

View File

@ -0,0 +1,6 @@
#!/sbin/openrc-run
command=/srv/spotter/wsgi.py
description="Spotter application manager"
pidfile=/var/run/spotter-appmgr.pid
start_stop_daemon_args="--background --make-pidfile --stderr /dev/null --stdout /dev/null"

View File

@ -1,3 +0,0 @@
#!/bin/sh
[ -x /usr/bin/acme.sh ] && /usr/bin/acme.sh --cron >/dev/null

View File

@ -1,134 +0,0 @@
{
"apps":{
"ckan":{
"host":"ckan",
"login":"N/A",
"password":"N/A",
"tiles":[],
"tiles-shown":false
},
"crisiscleanup":{
"host":"cc",
"login":"N/A",
"password":"N/A",
"tiles":[],
"tiles-shown":false
},
"cts":{
"host":"cts",
"login":"N/A",
"password":"N/A",
"tiles":[],
"tiles-shown":false
},
"frontlinesms":{
"host":"sms",
"login":"N/A",
"password":"N/A",
"tiles":[],
"tiles-shown":false
},
"gnuhealth":{
"host":"gh",
"login":"N/A",
"password":"N/A",
"tiles":["gnuhealth-clients"],
"tiles-shown":false
},
"kanboard":{
"host":"kb",
"login":"N/A",
"password":"N/A",
"tiles":["kanboard-mobile"],
"tiles-shown":false
},
"mifosx":{
"host":"mifosx",
"login":"N/A",
"password":"N/A",
"tiles":["mifosx-mobile"],
"tiles-shown":false
},
"motech":{
"host":"motech",
"login":"N/A",
"password":"N/A",
"tiles":[],
"tiles-shown":false
},
"opendatakit":{
"host":"odk",
"login":"N/A",
"password":"N/A",
"tiles":["opendatakit-clients"],
"tiles-shown":false
},
"opendatakit-build":{
"host":"odkbuild",
"login":"N/A",
"password":"N/A",
"tiles":[],
"tiles-shown":false
},
"openmapkit":{
"host":"omk",
"login":"N/A",
"password":"N/A",
"tiles":["geoodk-clients", "openmapkit-clients"],
"tiles-shown":false
},
"pandora":{
"host":"pandora",
"login":"N/A",
"password":"N/A",
"tiles":[],
"tiles-shown":false
},
"sahana":{
"host":"sahana",
"login":"N/A",
"password":"N/A",
"tiles":[],
"tiles-shown":false
},
"sahana-demo":{
"host":"sahana-demo",
"login":"N/A",
"password":"N/A",
"tiles":[],
"tiles-shown":false
},
"sambro":{
"host":"sambro",
"login":"N/A",
"password":"N/A",
"tiles":["sambro-mobile"],
"tiles-shown":false
},
"seeddms":{
"host":"dms",
"login":"N/A",
"password":"N/A",
"tiles":[],
"tiles-shown":false
},
"sigmah":{
"host":"sigmah",
"login":"N/A",
"password":"N/A",
"tiles":[],
"tiles-shown":false
},
"ushahidi":{
"host":"ush",
"login":"N/A",
"password":"N/A",
"tiles":["ushahidi-mobile"],
"tiles-shown":false
}
},
"host":{
"domain":"spotter.vm",
"port": "443"
}
}

View File

@ -1,86 +0,0 @@
* {
margin: 0;
padding: 0;
border: 0;
}
body {
font-family: 'Calibri', 'Verdana', 'Tahoma', sans-serif;
background-color: silver;
color: black;
line-height: 150%;
margin: 25px 30px;
}
a {
text-decoration: none;
}
h1, h2 {
font-size: 150%;
font-weight: normal;
}
h2 a {
color: inherit;
}
h2 img {
float: right;
margin-left: 10px;
margin-bottom:10px;
width: 100px;
height: 100px;
}
ul {
margin-left: 30px;
}
header {
color: white;
}
header h1 {
font-weight: bold;
}
header p {
padding: 0px;
margin: 0px;
}
.c {
background-color: white;
position: relative;
min-width: 365px;
max-width: 365px;
width: 90%;
float: left;
min-height: 175px;
margin-top: 13px;
margin-right: 13px;
border: solid 1px black;
padding: 10px;
display: none;
}
.c2 {
max-width: 765px;
width: 95%;
}
.visible {
display: initial;
}
.ico {
margin-right: 5px;
width: 20px;
height: 20px;
vertical-align: top;
}
.cleaner {
clear: both;
}

View File

@ -1,308 +0,0 @@
<!DOCTYPE html>
<html lang="cs">
<head>
<meta charset="utf-8">
<meta name="author" content="TS">
<meta name="copyright" content="page is under CC BY-NC-ND 3.0 CZ">
<meta name="generator" content="Spotter.ngo">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Cluster NGO</title>
<link rel="icon" href="img/cluster_spotter.png" type="image/png">
<link rel="stylesheet" href="css/style.css" type="text/css" media="screen">
<script src="js/jquery-3.3.1.min.js" type="text/javascript"></script>
<script src="js/script.js" type="text/javascript"></script>
</head>
<body>
<header>
<h1>CLUSTER NGO</h1>
<p>Sada softwarových nástrojů určená pro krizový management.</p>
</header>
<div class="c c2" id="sahana">
<h2><a href="https://sahana.{host}/eden/"><img src="img/EDEN.png" alt="Sahana EDEN" title="Sahana EDEN">Sahana EDEN</a></h2>
<p><strong>Registr kontaktů</strong> asociací, organizací, jednotek zaměstnanců, dobrovolníků, <strong>Registr prostředků</strong>, materiálních zdrojů určených pro činnost v krizových situacích, <strong>logistika</strong> krizového zboží ve skladištích, úkrytech, <strong>organizace lidských zdrojů</strong>, diobrovolníků, <strong>mapová vizualizace</strong> pro lokalizaci a popis krizové události a <strong>mnoho dalších funkcí</strong>.</p>
<ul>
<li><strong>Login:</strong> <span class="login"></span></li>
<li><strong>Heslo:</strong> <span class="password"></span></li>
</ul>
</div>
<div class="c" id="sahana-demo">
<h2><a href="https://sahana-demo.{host}/eden/"><img src="img/EDEN.png" alt="Sahana EDEN DEMO" title="Sahana EDEN DEMO">Sahana EDEN DEMO</a></h2>
<p>Přístup určený k bezpečnému vyzkoušení aplikace. Zde můžete přidávat i mazat testovací data.</p>
<ul>
<li><strong>Login:</strong> <span class="login"></span></li>
<li><strong>Heslo:</strong> <span class="password"></span></li>
</ul>
</div>
<div class="c" id="sambro">
<h2><a href="https://sambro.{host}/eden/"><img src="img/EDEN.png" alt="Sahana EDEN SAMBRO" title="Sahana EDEN SAMBRO">Sahana EDEN SAMBRO</a></h2>
<p>Samostatná instance Sahana EDEN s šablonou SAMBRO.</p>
<ul>
<li><strong>Login:</strong> <span class="login"></span></li>
<li><strong>Heslo:</strong> <span class="password"></span></li>
</ul>
</div>
<div class="c" id="sambro-mobile">
<h2><a href="#"><img src="img/EDEN.png" alt="SAMBRO Mobile" title="SAMBRO Mobile">SAMBRO Mobile</a></h2>
<p>Mobilní klient k aplikaci Sahana EDEN.<br>
<a href="https://itunes.apple.com/us/app/sambro-mobile/id1127251669"><img src="img/ios.png" class="ico" alt="IOS">IOS 6.0 a vyšší</a><br>
<a href="https://apkpure.com/sambro-mobile/io.sahana.sambro.mobile"><img src="img/android.png" class="ico" alt="Android">Android 4.0 a vyšší</a>
</p>
<ul>
<li><strong>URL:</strong> <span class="clienturl">https://sambro.{host}/eden/</span></li>
</ul>
</div>
<div class="c" id="crisiscleanup">
<h2><a href="https://cc.{host}"><img src="img/Crisis_Cleanup.png" alt="Crisis Cleanup" title="Crisis Cleanup">Crisis Cleanup</a></h2>
<p><strong>Mapování krizové pomoci</strong> při odstraňování následků katastrof a koordinaci práce. Jde o majetek, ne o lidi.</p>
<ul>
<li><strong>Login:</strong> <span class="login"></span></li>
<li><strong>Heslo:</strong> <span class="password"></span></li>
</ul>
</div>
<div class="c" id="ckan">
<h2><a href="https://ckan.{host}"><img src="img/CKAN.png" alt="CKAN" title="CKAN">CKAN</a></h2>
<p><strong>Repository</strong> management a datová analýza pro vytváření otevřených dat.</p>
<ul>
<li><strong>Login:</strong> <span class="login"></span></li>
<li><strong>Heslo:</strong> <span class="password"></span></li>
</ul>
</div>
<div class="c" id="opendatakit-build">
<h2><a href="https://odkbuild.{host}"><img src="img/ODK.png" alt="Open Data Kit" title="Open Data Kit">ODK Build</a></h2>
<p><strong>Sběr dat s pomocí smartphone</strong>.<br>Aplikace pro návrh formulářů<br>
<p><a href="http://opendatakit.org/xiframe/">XLSForm</a> - online konverter XLS.<br>
<a href="https://opendatakit.org/downloads/download-info/odk-formuploader/"><img src="img/java.png" class="ico" alt="ODK Form Uploader">ODK Form Uploader</a><br>
<a href="https://opendatakit.org/downloads/download-info/odk-validate-2/"><img src="img/java.png" class="ico" alt="ODK Validate">ODK Validate</a></p>
</div>
<div class="c" id="opendatakit-clients">
<h2><a href="#"><img src="img/ODK_Collect.png" alt="Open Data Kit" title="Open Data Kit">ODK Collect</a></h2>
<p>Mobilní aplikace<br>
<a href="https://play.google.com/store/apps/details?id=org.odk.collect.android"><img src="img/android.png" class="ico" alt="ODK Collect">ODK Collect pro Android</a><br>
<a href="https://opendatakit.org/downloads/download-info/odk-briefcase/"><img src="img/java.png" class="ico" alt="ODK Briefcase">ODK Briefcase</a><br>
</p>
<ul>
<li><strong>URL:</strong> <span class="clienturl">https://odk.{host}</span></li>
</ul>
</div>
<div class="c" id="opendatakit">
<h2><a href="https://odk.{host}/"><img src="img/ODK.png" alt="Open Data Kit" title="Open Data Kit">ODK Aggregate</a></h2>
<p><strong>Sběr dat s pomocí smartphone</strong>.<br>
<a href="http://geoodk.com">GeoODK Collect</a> - náhrada papírových dotazníků smartphonem.
</p>
<ul>
<li><strong>Login:</strong> <span class="login"></span></li>
<li><strong>Heslo:</strong> <span class="password"></span></li>
</ul>
</div>
<div class="c" id="openmapkit">
<h2><a href="https://omk.{host}"><img src="img/OMK.png" alt="Open Map Kit" title="Open Map Kit">OpenMapKit Server</a></h2>
<p><strong>Sběr dat s pomocí smartphone</strong>.<br>
<ul>
<li><strong>Login:</strong> <span class="login"></span></li>
<li><strong>Heslo:</strong> <span class="password"></span></li>
</ul>
</div>
<div class="c" id="geoodk-clients">
<h2><a href="#"><img src="img/GeoODK_Collect.png" alt="GeoODK Collect" title="GeoODK Collect">GeoODK Collect</a></h2>
<p>Mobilní aplikace<br>
<a href="https://play.google.com/store/apps/details?id=com.geoodk.collect.android"><img src="img/android.png" class="ico" alt="GeoODK Collect">GeoODK Collect pro Android</a>
</p>
<ul>
<li><strong>URL:</strong> <span class="clienturl">https://omk.{host}</span></li>
</ul>
</div>
<div class="c" id="openmapkit-clients">
<h2><a href="#"><img src="img/OMK.png" alt="Open Map Kit" title="Open Map Kit">OpenMapKit</a></h2>
<p>Mobilní aplikace<br>
<a href="https://play.google.com/store/apps/details?id=org.odk.collect.android"><img src="img/android.png" class="ico" alt="ODK Collect">ODK Collect pro Android</a><br>
<a href="https://play.google.com/store/apps/details?id=org.redcross.openmapkit"><img src="img/android.png" class="ico" alt="Android">OpenMapKit pro Android 4.1 a vyšší</a>
</p>
<ul>
<li><strong>URL:</strong> <span class="clienturl">https://omk.{host}</span></li>
</ul>
</div>
<div class="c" id="frontlinesms">
<h2><a href="https://sms.{host}"><img src="img/FrontlineSMS.png" alt="FrontlineSMS" title="FrontlineSMS">FrontlineSMS</a></h2>
<p><strong>SMS messaging</strong> přes veřejné datové brány</p>
<ul>
<li><strong>Login:</strong> <span class="login"></span></li>
<li><strong>Heslo:</strong> <span class="password"></span></li>
</ul>
</div>
<div class="c" id="seeddms">
<h2><a href="https://dms.{host}"><img src="img/SeedDMS.png" alt="SeedDMS" title="SeedDMS">SeedDMS</a></h2>
<p><strong>Dokument management</strong> na dokumentaci a projektovou dokumentaci</p>
<ul>
<li><strong>Login:</strong> <span class="login"></span></li>
<li><strong>Heslo:</strong> <span class="password"></span></li>
</ul>
</div>
<div class="c" id="pandora">
<h2><a href="https://pandora.{host}"><img src="img/Pandora.png" alt="Pan.do/ra" title="Pan.do/ra">Pan.do/ra</a></h2>
<p><strong>Media management</strong> na foto a video z krizové události. Tvorba metadat, komentářů, lokalizace v čase a na mapě.</p>
<ul>
<li><strong>Login:</strong> <span class="login"></span></li>
<li><strong>Heslo:</strong> <span class="password"></span></li>
</ul>
</div>
<div class="c" id="ushahidi">
<h2><a href="https://ush.{host}"><img src="img/Ushahidi.png" alt="Ushahidi" title="Ushahidi">Ushahidi</a></h2>
<p>Reakce na krizovou událost. Shromažďujte zprávy od obětí a pracovníků v terénu prostřednictvím SMS, e-mailu, webu, Twitteru.</p>
<ul>
<li><strong>Login:</strong> <span class="login"></span></li>
<li><strong>Heslo:</strong> <span class="password"></span></li>
</ul>
</div>
<div class="c" id="ushahidi-mobile">
<h2><a href="#"><img src="img/Ushahidi_mobile.png" alt="Ushahidi" title="Ushahidi">Ushahidi Mobile</a></h2>
<p>Mobilní aplikace Ushahidi pro<br>
<a href="https://itunes.apple.com/us/app/ushahidi-mobile/id1205994516?mt=8"><img src="img/ios.png" class="ico" alt="IOS">IOS 9.0 a vyšší</a><br>
<a href="https://play.google.com/store/apps/details?id=com.ushahidi.mobile"><img src="img/android.png" class="ico" alt="Android">Android 4.4 a vyšší</a>
</p>
<ul>
<li><strong>URL:</strong> <span class="clienturl">ush.{host}</span></li>
</ul>
</div>
<div class="c" id="sms-sync-gateway">
<h2><a href="#"><img src="img/SMS_Sync.png" alt="SMS Sync Gateway" title="SMS Sync Gateway">SMS Sync Gateway</a></h2>
<p>Mobilní aplikace pro<br>
<a href="https://play.google.com/store/apps/details?id=org.addhen.smssync"><img src="img/android.png" class="ico" alt="Android">Android 2.3 a vyšší</a>
</p>
</div>
<div class="c" id="kanboard">
<h2><a href="https://kb.{host}"><img src="img/Kanboard.png" alt="Kanboard" title="Kanboard">Kanboard</a></h2>
<p>Usnadňuje tvorbu a řízení projektů s pomocí Kanban metodiky.</p>
<ul>
<li><strong>Login:</strong> <span class="login"></span></li>
<li><strong>Heslo:</strong> <span class="password"></span></li>
</ul>
</div>
<div class="c" id="kanboard-mobile">
<h2><a href="#"><img src="img/Kanboard.png" alt="Kanboard" title="Kanboard">Kanboard Mobile</a></h2>
<p>Mobilní aplikace<br>
<a href="https://play.google.com/store/apps/details?id=eu.it_quality.kanboard"><img src="img/android.png" class="ico" alt="KanBoard">KanBoard client pro Android 4.1 a vyšší</a><br>
<a href="https://f-droid.org/packages/in.andres.kandroid/"><img src="img/android.png" class="ico" alt="Android">Kandroid pro Android 4.2 a vyšší</a>
</p>
<ul>
<li><strong>URL:</strong> <span class="clienturl">https://kb.{host}</span></li>
</ul>
</div>
<div class="c" id="cts">
<h2><a href="https://cts.{host}"><img src="img/CTS.png" alt="CTS" title="CTS">CTS</a></h2>
<p>Logistika hmotné pomoci pro humanitární potřeby.</p>
<ul>
<li><strong>Login:</strong> <span class="login"></span></li>
<li><strong>Heslo:</strong> <span class="password"></span></li>
</ul>
</div>
<div class="c" id="gnuhealth">
<h2><a href="https://gh.{host}/index.html"><img src="img/GNU_Health.png" alt="GNU Health" title="GNU Health">GNU Health</a></h2>
<p>Zdravotní a nemocniční informační systém.</p>
<ul>
<li><strong>Login:</strong> <span class="login"></span></li>
<li><strong>Heslo:</strong> <span class="password"></span></li>
<li><strong>Heslo k demu:</strong> <span class="demopassword">gnusolidario</span></li>
</ul>
</div>
<div class="c" id="gnuhealth-clients">
<h2><a href="#"><img src="img/GNU_Health.png" alt="GNU Health" title="GNU Health">GNU Health klienti</a></h2>
<p>Klientské aplikace platformy Tryton GNU Health pro<br>
<a href="https://downloads.tryton.org/4.2/tryton-last.exe"><img src="img/Windows.png" class="ico" alt="Windows">Windows</a><br>
<a href="https://downloads.tryton.org/4.2/tryton-last.dmg"><img src="img/MacOS.png" class="ico" alt="MacOS">MacOS</a><br>
<a href="https://downloads.tryton.org/4.2/tryton-last.tar.gz"><img src="img/Linux.png" class="ico" alt="Linux">Linux</a>
</p>
<ul>
<li><strong>URL:</strong> <span class="clienturl">gh.{host}</span></li>
</ul>
</div>
<div class="c" id="sigmah">
<h2><a href="https://sigmah.{host}/sigmah/"><img src="img/Sigmah.png" alt="Sigmah" title="Sigmah">Sigmah</a></h2>
<p>Rozpočtování získávání finančních prostředků.</p>
<ul>
<li><strong>Login:</strong> <span class="login"></span></li>
<li><strong>Heslo:</strong> <span class="password"></span></li>
</ul>
</div>
<div class="c" id="motech">
<h2><a href="https://motech.{host}/"><img src="img/Motech.png" alt="Motech" title="Motech">Motech</a></h2>
<p>Integrace zdravotnických a komunikačních služeb.</p>
<ul>
<li><strong>Login:</strong> <span class="login"></span></li>
<li><strong>Heslo:</strong> <span class="password"></span></li>
</ul>
</div>
<div class="c" id="mifosx">
<h2><a href="https://mifosx.{host}/"><img src="img/MifosX.png" alt="Mifos X" title="Mifos X">Mifos X</a></h2>
<p>Nástroj na rozvojovou, humanitární pomoc a mikrofinancování.</p>
<ul>
<li><strong>Login:</strong> <span class="login"></span></li>
<li><strong>Heslo:</strong> <span class="password"></span></li>
</ul>
</div>
<div class="c" id="mifosx-mobile">
<h2><a href="#"><img src="img/MifosX_Mobile.png" alt="Mifos X" title="Mifos X">Mifos X</a></h2>
<p>Mobilní aplikace<br>
<a href="https://github.com/openMF/android-client/releases"><img src="img/android.png" class="ico" alt="Mifos X">Mifos X client pro Android 3.0 a vyšší</a><br>
</p>
<ul>
<li><strong>URL:</strong> <span class="clienturl">mifosx.{host}</span></li>
<li><strong>Tenant ID:</strong> <span>default</span></li>
</ul>
</div>
<div class="c" id="diaspora">
<h2><a href="#"><img src="img/Diaspora.png" alt="diaspora*" title="diaspora*">diaspora*</a></h2>
<p>Autonomní sociání síť s možností propojení do cizích sociálních sítí.</p>
<ul>
<li><strong>Login:</strong> <span class="login"></span></li>
<li><strong>Heslo:</strong> <span class="password"></span></li>
</ul>
</div>
<div class="c" id="openid">
<h2><a href="http://openid.net"><img src="img/OpenID.png" alt="OpenID" title="OpenID">OpenID</a></h2>
<p>Pro ověření identity budete potřebovat účet OpenID. Zaregistrujte se. Registraci využijete v software Sahana EDEN.</p>
</div>
<div class="c" id="posm">
<h2><a href="#"><img src="img/POSM.png" alt="POSM" title="POSM">POSM</a></h2>
<p><strong>Portable Open Street Map</strong> - softwarový balík na offline používání OpenStreet Map v samostatné virtuální image.</p>
</div>
<div class="c visible" id="cluster-spotter">
<h2><a href="http://spotter.ngo"><img src="img/cluster_spotter.png" alt="Cluster Spotter" title="Cluster Spotter">Cluster Spotter</a></h2>
<p>Info o Misi a Vizi projektu, včetně kontaktu. Zachovejte data bezpečná a neposkytujte je nepovolaným osobám.<br>
<small>CC 4.0 CZ by <a href="http://trendspotter.cz">TS</a>. Content is based on PD, CC, GNU/GPL. Brand names, trademarks belong to their respective holders.</small>
</p>
</div>
<div class="cleaner"></div>
</body>
</html>

View File

@ -1,21 +0,0 @@
$(function() {
$.getJSON('config.json', function(data) {
$.each(data.apps, function(app, appdata) {
if (!appdata['tiles-shown'])
return true;
var div = $('#'+app).show();
div.find('.login').text(appdata.login);
div.find('.password').text(appdata.password);
$.each(appdata.tiles, function(idx, tile) {
$('#'+tile).show();
});
});
var host = data.host.domain + (data.host.port != '443' ? ':'+data.host.port : '')
$('a').each(function(){
$(this).attr('href', $(this).attr('href').replace('{host}', host));
});
$('span').each(function(){
$(this).text($(this).text().replace('{host}', host));
});
});
});

View File

@ -0,0 +1,336 @@
# -*- coding: utf-8 -*-
import json
import os
import shutil
import subprocess
from . import confupdater
from . import tools
from . import validator
VERSION = '0.0.1'
CONF_FILE = '/srv/spotter/config.json'
ISSUE_FILE = '/etc/issue'
NGINX_DIR = '/etc/nginx/conf.d'
ACME_CRON = '/etc/periodic/daily/acme-sh'
CERT_PUB_FILE = '/etc/ssl/certs/services.pem'
CERT_KEY_FILE = '/etc/ssl/private/services.key'
NGINX_TEMPLATE = '''server {{
listen [::]:{port} ssl http2;
server_name {host}.{domain};
access_log /var/log/nginx/{app}.access.log;
error_log /var/log/nginx/{app}.error.log;
location / {{
proxy_pass http://{ip}:8080;
}}
error_page 502 /error.html;
location = /error.html {{
root /srv/spotter;
}}
location = /spotter-ping {{
add_header Content-Type text/plain;
return 200 "spotter-pong";
}}
}}
'''
NGINX_DEFAULT_TEMPLATE = '''server {{
listen [::]:80 default_server ipv6only=off;
location / {{
return 301 https://$host:{port}$request_uri;
}}
location /.well-known/acme-challenge/ {{
root /etc/acme.sh.d;
}}
location = /spotter-ping {{
add_header Content-Type text/plain;
return 200 "spotter-pong";
}}
}}
server {{
listen [::]:{port} ssl http2 default_server ipv6only=off;
location / {{
proxy_pass http://127.0.0.1:8080;
}}
location /static {{
root /srv/spotter;
}}
error_page 502 /error.html;
location = /error.html {{
root /srv/spotter;
}}
location = /spotter-ping {{
add_header Content-Type text/plain;
return 200 "spotter-pong";
}}
}}
'''
ISSUE_TEMPLATE = '''
\x1b[1;32m _____ _ _ __ ____ __
/ ____| | | | | \\\\ \\\\ / / \\\\/ |
| (___ _ __ ___ | |_| |_ ___ _ _\\\\ \\\\ / /| \\\\ / |
\\\\___ \\\\| '_ \\\\ / _ \\\\| __| __/ _ \\\\ '__\\\\ \\\\/ / | |\\\\/| |
____) | |_) | (_) | |_| || __/ | \\\\ / | | | |
|_____/| .__/ \\\\___/ \\\\__|\\\\__\\\\___|_| \\\\/ |_| |_|
| |
|_|\x1b[0m
\x1b[1;33mUPOZORNĚNÍ:\x1b[0m Neoprávněný přístup k tomuto zařízení je zakázán.
Musíte mít výslovné oprávnění k přístupu nebo konfiguraci tohoto zařízení.
Neoprávněné pokusy a kroky k přístupu nebo používání tohoto systému mohou mít
za následek občanské nebo trestní sankce.
\x1b[1;33mCAUTION:\x1b[0m Unauthozired access to this device is prohibited.
You must have explicit, authorized permission to access or configure this
device. Unauthorized attempts and actions to access or use this system may
result in civil or criminal penalties.
Pro přístup k aplikacím otevřete URL \x1b[1mhttps://{host}\x1b[0m ve Vašem
internetovém prohlížeči.
\x1b[0;30m
'''
ACME_CRON_TEMPLATE = '''#!/bin/sh
[ -x /usr/bin/acme.sh ] && /usr/bin/acme.sh --cron >/dev/null
'''
class AppMgr:
def __init__(self):
# Load JSON configuration
with open(CONF_FILE, 'r') as f:
self.conf = json.load(f)
self.domain = self.conf['host']['domain']
self.port = self.conf['host']['port']
def save_conf(self):
# Save a sorted JSON configuration object with indentation
with open(CONF_FILE, 'w') as f:
json.dump(self.conf, f, sort_keys=True, indent=4)
def update_login(self, app, login, password):
# Update login and password for an app in the configuration
if not validator.is_valid_app(app, self.conf):
raise validator.InvalidValueException('app', app)
if login is not None:
self.conf['apps'][app]['login'] = login
if password is not None:
self.conf['apps'][app]['password'] = password
self.save_conf()
def show_tiles(self, app):
# Update visibility for the app in the configuration
if not validator.is_valid_app(app, self.conf):
raise validator.InvalidValueException('app', app)
self.conf['apps'][app]['visible'] = True
self.save_conf()
def hide_tiles(self, app):
# Update visibility for the app in the configuration
if not validator.is_valid_app(app, self.conf):
raise validator.InvalidValueException('app', app)
self.conf['apps'][app]['visible'] = False
self.save_conf()
def start_app(self, app):
# Start the actual app service
if not validator.is_valid_app(app, self.conf):
raise validator.InvalidValueException('app', app)
tools.start_service(app)
def stop_app(self, app):
# Stop the actual app service
if not validator.is_valid_app(app, self.conf):
raise validator.InvalidValueException('app', app)
tools.stop_service(app)
# Stop the app service's dependencies if they are not used by another running app
deps = self.build_deps_tree()
for dep in self.get_app_deps(app):
if not any([tools.is_service_started(d) for d in deps[dep]]):
tools.stop_service(dep)
def build_deps_tree(self):
# Fisrt, build a dictionary of {app: [needs]}
needs = {}
for app in self.conf['apps']:
needs[app] = self.get_app_deps(app)
# Then reverse it to {need: [apps]}
deps = {}
for app, need in needs.items():
for n in need:
deps.setdefault(n, []).append(app)
return deps
def get_app_deps(self, app):
# Get "needs" line from init script and split it to list, skipping first two elements (docker, net)
try:
with open(os.path.join('/etc/init.d', app), 'r') as f:
return [l.split()[2:] for l in f.readlines() if l.startswith('\tneed')][0]
except:
return []
def enable_autostart(self, app):
# Add the app to OpenRC default runlevel
if not validator.is_valid_app(app, self.conf):
raise validator.InvalidValueException('app', app)
subprocess.run(['/sbin/rc-update', 'add', app])
def disable_autostart(self, app):
# Remove the app from OpenRC default runlevel
if not validator.is_valid_app(app, self.conf):
raise validator.InvalidValueException('app', app)
subprocess.run(['/sbin/rc-update', 'del', app])
def register_proxy(self, app):
# Rebuild nginx configuration using IP of referenced app container and reload nginx
if not validator.is_valid_app(app, self.conf):
raise validator.InvalidValueException('app', app)
self.update_proxy_conf(app, tools.get_container_ip(app))
tools.reload_nginx()
def update_proxy_conf(self, app, ip):
with open(os.path.join(NGINX_DIR, '{}.conf'.format(app)), 'w') as f:
f.write(NGINX_TEMPLATE.format(app=app, host=self.conf['apps'][app]['host'], ip=ip, domain=self.domain, port=self.port))
def unregister_proxy(self, app):
# Remove nginx configuration to prevent proxy mismatch when the container IP is reassigned to another container
if not validator.is_valid_app(app, self.conf):
raise validator.InvalidValueException('app', app)
self.update_proxy_conf(app, tools.NULL_IP)
tools.reload_nginx()
def update_host(self, domain, port, restart_nginx=True):
# Update domain and port and rebuild all configurtion. Defer nginx restart when updating from web interface
if not validator.is_valid_domain(domain):
raise validator.InvalidValueException('domain', domain)
if not validator.is_valid_port(port):
raise validator.InvalidValueException('port', port)
self.domain = self.conf['host']['domain'] = domain
self.port = self.conf['host']['port'] = port
self.save_conf()
self.rebuild_nginx(restart_nginx)
self.rebuild_issue()
self.update_apps_urls()
def rebuild_nginx(self, restart_nginx):
# Rebuild nginx config for the portal app
with open(os.path.join(NGINX_DIR, 'default.conf'), 'w') as f:
f.write(NGINX_DEFAULT_TEMPLATE.format(port=self.port))
# Unregister nginx proxy for apps (will be repopulated on app restart)
for app in self.conf['apps']:
self.update_proxy_conf(app, tools.NULL_IP)
# Restart nginx to properly bind the new listen port
if restart_nginx:
tools.restart_service('nginx')
def rebuild_issue(self):
# Compile the HTTPS host displayed in terminal banner
host = self.domain
# If the dummy host is used, take an IP address of a primary interface instead
if self.domain == 'spotter.vm':
host = tools.get_local_ipv4()
if not host:
host = tools.get_local_ipv6()
if not host:
host = '127.0.0.1'
# Show port number only when using the non-default HTTPS port
if self.port != '443':
host += ':{}'.format(self.port)
# Rebuild the terminal banner
with open(ISSUE_FILE, 'w') as f:
f.write(ISSUE_TEMPLATE.format(host=host))
def update_apps_urls(self):
# Update configuration for respective applications
confupdater.update_url()
# Restart currently running apps in order to update config and re-register nginx proxy
for app in self.conf['apps']:
if tools.is_service_started(app):
tools.restart_service(app)
def update_common(self, email, gmaps_api_key):
# Update common configuration values
if email != None:
# Update email
if not validator.is_valid_email(email):
raise validator.InvalidValueException('email', email)
self.conf['common']['email'] = email
confupdater.update_email(email)
if gmaps_api_key != None:
# Update Google Maps API key
self.conf['common']['gmaps-api-key'] = gmaps_api_key
confupdater.update_gmaps_api_key(gmaps_api_key)
# Save config to file
self.save_conf()
# Restart currently running apps in order to update config
for app in self.conf['apps']:
if tools.is_service_started(app):
tools.restart_service(app)
def request_cert(self):
# Remove all possible conflicting certificates requested in the past
certs = [i for i in os.listdir('/etc/acme.sh.d') if i not in ('account.conf', 'ca', 'http.header')]
for cert in certs:
if cert != self.domain:
subprocess.run(['/usr/bin/acme.sh', '--remove', '-d', cert])
# Compile an acme.sh command for certificate requisition only if the certificate hasn't been requested before
if not os.path.exists(os.path.join('/etc/acme.sh.d', self.domain)):
cmd = ['/usr/bin/acme.sh', '--issue', '-d', self.domain]
for app in self.conf['apps']:
cmd += ['-d', '{}.{}'.format(self.conf['apps'][app]['host'], self.domain)]
cmd += ['-w', '/etc/acme.sh.d']
# Request the certificate
subprocess.run(cmd, check=True)
# Otherwise just try to renew
else:
# Acme.sh returns code 2 on skipped renew
try:
subprocess.run(['/usr/bin/acme.sh', '--renew', '-d', self.domain], check=True)
except subprocess.CalledProcessError as e:
if e.returncode != 2:
raise
# Install the issued certificate
subprocess.run(['/usr/bin/acme.sh', '--install-cert', '-d', self.domain, '--key-file', CERT_KEY_FILE, '--fullchain-file', CERT_PUB_FILE, '--reloadcmd', '/sbin/service nginx reload'], check=True)
# Install acme.sh cronjob
with open(ACME_CRON, 'w') as f:
f.write(ACME_CRON_TEMPLATE)
def install_cert(self, public_file, private_file):
# Remove acme.sh cronjob
if os.path.exists(ACME_CRON):
os.unlink(ACME_CRON)
# Copy certificate files
shutil.copyfile(public_file, CERT_PUB_FILE)
shutil.copyfile(private_file, CERT_KEY_FILE)
os.chmod(CERT_KEY_FILE, 0o640)
# Reload nginx
tools.reload_nginx()

View File

@ -0,0 +1,105 @@
# -*- coding: utf-8 -*-
import os
import shutil
import subprocess
from . import tools
TMP_FILE = '/tmp/confupdater.tmp'
def replace_file_line(filename, oldline, newline):
with open(filename, 'r') as conf, open(TMP_FILE, 'w') as tmp:
for line in conf:
# Find line starting with oldline
if line.startswith(oldline):
# Replace te line with oldline, newline, \n (to not repeat the oldline in newline)
tmp.write(oldline)
tmp.write(newline)
tmp.write('\n')
# Dump the rest of the file and break the loop
tmp.write(conf.read())
break
else:
tmp.write(line)
# Copy the file contents to the original file (preserves permissions of the original file)
shutil.copyfile(TMP_FILE, filename)
os.unlink(TMP_FILE)
def run_mysql_query(query, database):
if not maria_started:
tools.start_service('mariadb')
subprocess.run(['docker', 'exec', '-i', 'mariadb', 'mysql', '-e', query, database])
if not maria_started:
tools.stop_service('mariadb')
def update_gmaps_api_key(api_key):
# CKAN
replace_file_line('/srv/ckan/conf/ckan.ini', 'ckanext.geoview.gapi_key = ', api_key)
# Crisis Cleanup
replace_file_line('/srv/crisiscleanup/conf/boot.rb', 'ENV[\'GOOGLE_MAPS_API_KEY\'] = ', api_key)
# Pan.do/ra
replace_file_line('/srv/pandora/conf/local_settings.py', 'GOOGLE_API_KEY = ', '\'{}\''.format(api_key))
# Sahana
replace_file_line('/srv/sahana/conf/000_config.py', 'settings.gis.api_google = ', '"{}"'.format(api_key))
# Sahana Demo
replace_file_line('/srv/sahana-demo/conf/000_config.py', 'settings.gis.api_google = ', '"{}"'.format(api_key))
# SAMBRO
replace_file_line('/srv/sambro/conf/000_config.py', 'settings.gis.api_google = ', '"{}"'.format(api_key))
# Sigmah
replace_file_line('/srv/sigmah/conf/sigmah.properties', 'maps.key=', api_key)
# Ushahidi
replace_file_line('/srv/ushahidi/conf/config.json', ' "google_analytics_id": ', '"{}"'.format(api_key))
def update_email(email):
# CKAN
replace_file_line('/srv/ckan/conf/ckan.ini', 'smtp.mail_from = ', email)
replace_file_line('/srv/ckan-datapusher/conf/datapusher_settings.py', 'FROM_EMAIL = ', '\'{}\''.format(email)
# Crisis Cleanup
replace_file_line('/srv/crisiscleanup/conf/initializers/devise.rb', ' config.mailer_sender = ', '\'{}\''.format(email)
# CTS
replace_file_line('/srv/cts/conf/spotter.py', 'SERVER_EMAIL = ', '\'{}\''.format(email)
# GNU Health
replace_file_line('/srv/gnuhealth/conf/trytond.conf', 'from = ', email)
# KanBoard
replace_file_line('/srv/kanboard/conf/config.php', 'define(\'MAIL_FROM\', ', '\'{}\');'.format(email))
# Mifos X
query = 'UPDATE `c_external_service_properties` SET `value` = "{}" WHERE `external_service_id` = 2 and `name` LIKE "username";'.format(email)
run_mysql_query(query, 'mifostenant-default')
# Sahana
replace_file_line('/srv/sahana/conf/000_config.py', 'settings.mail.sender = ', '"{}"'.format(email))
replace_file_line('/srv/sahana/conf/000_config.py', 'settings.mail.approver = ', '"{}"'.format(email))
# Sahana Demo
replace_file_line('/srv/sahana-demo/conf/000_config.py', 'settings.mail.sender = ', '"{}"'.format(email))
replace_file_line('/srv/sahana-demo/conf/000_config.py', 'settings.mail.approver = ', '"{}"'.format(email))
# SAMBRO
replace_file_line('/srv/sambro/conf/000_config.py', 'settings.mail.sender = ', '"{}"'.format(email))
replace_file_line('/srv/sambro/conf/000_config.py', 'settings.mail.approver = ', '"{}"'.format(email))
# SeedDMS
replace_file_line('/srv/seeddms/conf/settings.xml', ' <smtp smtpServer="postfix" smtpPort="25" smtpSendFrom=', '"{}" smtpUser="" smtpPassword=""/>'.format(email))
# Sigmah
replace_file_line('/srv/sigmah/conf/sigmah.properties', 'mail.from.address=', email)
replace_file_line('/srv/sigmah/conf/sigmah.properties', 'mail.support.to=', email)
# Ushahidi
email_json = '{\"incoming_type\":\"IMAP\",\"incoming_server\":\"localhost\",\"incoming_port\":143,\"incoming_security\":\"None\",\"incoming_username\":\"{}\",\"incoming_password\":\"password\",\"outgoing_type\":\"SMTP\",\"outgoing_server\":\"postfix\",\"outgoing_port\":25,\"outgoing_security\":\"None\",\"outgoing_username\":\"{}\",\"outgoing_password\":\"password\",\"from\":\"{}\",\"from_name\":\"Ushahidi\"}'
query = 'UPDATE `config` SET `config_value` = "{}" WHERE `group_name` LIKE "data-provider" AND `config_key` LIKE "email";'.format(email_json)
run_mysql_query(query, 'ushahidi')
def update_url(host):
# CKAN
replace_file_line('/srv/ckan/conf/ckan.ini', 'ckan.site_url = ', 'https://ckan.{}'.format(host))
# Motech
replace_file_line('/srv/motech/conf/config/motech-settings.properties', 'server.url=', 'https://motech.{}'.format(host))
# Pan.do/ra
replace_file_line('/srv/pandora/conf/config.jsonc', ' "url": ', '"pandora.{}"'.format(host))
# Sahana
replace_file_line('/srv/sahana/conf/000_config.py', 'settings.base.public_url = ', '"https://sahana.{}"'.format(host))
# Sahana Demo
replace_file_line('/srv/sahana-demo/conf/000_config.py', 'settings.base.public_url = ', '"https://sahana-demo.{}"'.format(host))
# SAMBRO
replace_file_line('/srv/sambro/conf/000_config.py', 'settings.base.public_url = ', '"https://sambro.{}"'.format(host))
# Ushahidi
replace_file_line('/srv/ushahidi/conf/config.json', ' "backend_url": ', '"https://ush.{}/platform",'.format(host))
api_url = '\"https:\\/\\/ush.{}\\/platform\\/api\\/v3\\/config\\/data-provider\"'
query = 'UPDATE `config` SET `config_value` = "{}" WHERE `group_name` LIKE "data-provider" AND `config_key` LIKE "url";'.format(api_url)
run_mysql_query(query, 'ushahidi')

View File

@ -0,0 +1,99 @@
# -*- coding: utf-8 -*-
import dns.exception
import dns.resolver
import os
import requests
import socket
import ssl
import subprocess
NULL_IP = '[100::1]'
def get_container_ip(app):
# Return an IP address of a container. If the container is not running, return address from IPv6 discard prefix instead
try:
return subprocess.run(['/usr/bin/docker', 'inspect', '-f', '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}', app], check=True, stdout=subprocess.PIPE).stdout.decode().strip()
except:
return NULL_IP
def get_local_ipv4():
# Return first routable IPv4 address
try:
return subprocess.run(['/sbin/ip', 'route', 'get', '1'], check=True, stdout=subprocess.PIPE).stdout.decode().split()[-1]
except:
return None
def get_local_ipv6():
# Return first routable IPv6 address
try:
return subprocess.run(['/sbin/ip', 'route', 'get', '2003::'], check=True, stdout=subprocess.PIPE).stdout.decode().split()[-3]
except:
return None
def get_external_ip(family):
# Return external IP address of given family via 3rd party service
allowed_gai_family = requests.packages.urllib3.util.connection.allowed_gai_family
try:
requests.packages.urllib3.util.connection.allowed_gai_family = lambda: family
return requests.get('http://tools.dasm.cz/myip.php', timeout=5).text
except:
return None
finally:
requests.packages.urllib3.util.connection.allowed_gai_family = allowed_gai_family
def get_external_ipv4():
# Return external IPv4 address
return get_external_ip(socket.AF_INET)
def get_external_ipv6():
# Return external IPv6 address
return get_external_ip(socket.AF_INET6)
resolver = dns.resolver.Resolver()
resolver.timeout = 3
resolver.lifetime = 3
resolver.nameservers = ['8.8.8.8', '8.8.4.4', '2001:4860:4860::8888', '2001:4860:4860::8844']
def resolve_ip(domain, type):
# Resolve domain name using Google Public DNS
try:
return resolver.query(domain, type)[0].address
except dns.exception.Timeout:
raise
except:
return None
def ping_url(url):
try:
return requests.post('http://tools.dasm.cz/spotter-ping.php', data = {'url': url}, timeout=5).text == 'spotter-pong'
except requests.exceptions.Timeout:
raise
except:
return False
def is_service_started(app):
# Check OpenRC service status without calling any binary
return os.path.exists(os.path.join('/run/openrc/started', app))
def is_service_autostarted(app):
# Check OpenRC service enablement
return os.path.exists(os.path.join('/etc/runlevels/default', app))
def start_service(service):
subprocess.run(['/sbin/service', service, 'start'], check=True)
def stop_service(service):
subprocess.run(['/sbin/service', service, 'stop'], check=True)
def restart_service(service):
subprocess.run(['/sbin/service', service, 'restart'])
def reload_nginx():
subprocess.run(['/sbin/service', 'nginx', 'reload'])
def get_cert_info():
data = ssl._ssl._test_decode_cert('/etc/ssl/certs/services.pem')
data['subject'] = dict(data['subject'][i][0] for i in range(len(data['subject'])))
data['issuer'] = dict(data['issuer'][i][0] for i in range(len(data['issuer'])))
return data

View File

@ -0,0 +1,28 @@
# -*- coding: utf-8 -*-
import re
domain_re = re.compile(r'^(?!-)[a-z0-9-]{1,63}(?<!-)(?:\.(?!-)[a-z0-9-]{1,63}(?<!-)){0,125}\.(?!-)(?![0-9]+$)[a-z0-9-]{1,63}(?<!-)$')
box_re = re.compile(r'^[a-z0-9!#$%&\'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&\'*+/=?^_`{|}~-]+)*$')
class InvalidValueException(Exception):
pass
def is_valid_domain(domain):
return bool(domain_re.match(domain))
def is_valid_port(port):
try:
port = int(port)
return port > 0 and port < 65536
except:
return False
def is_valid_app(app, conf):
return app in conf['apps']
def is_valid_email(email):
parts = email.split('@')
if len(parts) != 2:
return False
return bool(box_re.match(parts[0])) and bool(domain_re.match(parts[1]))

View File

@ -0,0 +1,234 @@
# -*- coding: utf-8 -*-
import json
import os
from werkzeug.exceptions import BadRequest, HTTPException
from werkzeug.routing import Map, Rule
from werkzeug.utils import redirect
from werkzeug.wrappers import Request, Response
from werkzeug.wsgi import ClosingIterator
from jinja2 import Environment, FileSystemLoader
from . import AppMgr
from . import tools
from .validator import InvalidValueException
class Lang:
lang = {
'malformed_request': 'Byl zaslán chybný požadavek. Obnovte stránku a zkuste akci zopakovat.',
'invalid_domain': 'Zadaný doménový název "{}" není platný.',
'invalid_port': 'Zadaný port "{}" není platný.',
'host_updated': 'Nastavení hostitele bylo úspěšně změněno. Přejděte na URL <a href="{}">{}</a> a pokračujte následujícími kroky.',
'dns_record_does_not_exist': 'DNS záznam pro název "{}" neexistuje.',
'dns_record_mismatch': 'DNS záznam pro název "{}" směřuje na IP {} místo očekávané {}.',
'dns_timeout': 'Nepodařilo se kontaktovat DNS server. Zkontrolujte, zda má virtuální stroj přístup k internetu.',
'dns_records_ok': 'DNS záznamy jsou nastaveny správně.',
'http_host_not_reachable': 'Adresa {} není dostupná z internetu. Zkontrolujte nastavení síťových komponent.',
'http_timeout': 'Nepodařilo se kontaktovat ping server. Zkontrolujte, zda má virtuální stroj přístup k internetu.',
'http_hosts_ok': 'Síť je nastavena správně. Všechny aplikace na portu {} jsou z internetu dostupné.',
'cert_file_missing': 'Nebyl vybrán soubor s certifikátem.',
'key_file_missing': 'Nebyl vybrán soubor se soukromým klíčem.',
'cert_request_error': 'Došlo k chybě při žádosti o certifikát. Zkontrolujte, zda je virtuální stroj dostupný z internetu na portu 80.',
'cert_installed': 'Certifikát byl úspěšně nainstalován. Restartujte webový prohlížeč pro jeho načtení.',
'common_updated': 'Nastavení aplikací bylo úspěšně změněno.',
'app_started': '<span class="info">Spuštěna</span> (<a href="#" class="app-stop">zastavit</a>)',
'app_stopped': '<span class="error">Zastavena</span> (<a href="#" class="app-start">spustit</a>)',
'stop_start_error': 'Došlo k chybě při spouštění/zastavování. Zkuste akci opakovat nebo restartuje virtuální stroj.',
}
def __getattr__(self, key):
def function(*args):
return self.lang[key].format(*args)
return function
class WSGIApp(object):
def __init__(self):
self.mgr = AppMgr()
self.lang = Lang()
self.jinja_env = Environment(loader=FileSystemLoader('/srv/spotter/templates'), autoescape=True, lstrip_blocks=True, trim_blocks=True)
self.jinja_env.globals.update(is_service_autostarted=tools.is_service_autostarted)
self.jinja_env.globals.update(is_service_started=tools.is_service_started)
def __call__(self, environ, start_response):
return self.wsgi_app(environ, start_response)
def wsgi_app(self, environ, start_response):
request = Request(environ)
response = self.dispatch_request(request)
response = response(environ, start_response)
# Defer nginx restart for /update-host request
if request.path == '/update-host':
return ClosingIterator(response, tools.restart_nginx)
return response
def dispatch_request(self, request):
map = Map([
Rule('/', endpoint='portal_view'),
Rule('/setup-host', endpoint='setup_host_view'),
Rule('/setup-apps', endpoint='setup_apps_view'),
Rule('/update-host', endpoint='update_host_action'),
Rule('/verify-dns', endpoint='verify_dns_action'),
Rule('/verify-https', endpoint='verify_http_action', defaults={'proto': 'https'}),
Rule('/verify-http', endpoint='verify_http_action', defaults={'proto': 'http'}),
Rule('/update-cert', endpoint='update_cert_action'),
Rule('/update-common', endpoint='update_common_action'),
Rule('/update-app-visibility', endpoint='update_app_visibility_action'),
Rule('/update-app-autostart', endpoint='update_app_autostart_action'),
Rule('/start-app', endpoint='start_app_action'),
Rule('/stop-app', endpoint='stop_app_action'),
])
adapter = map.bind_to_environ(request.environ)
try:
endpoint, values = adapter.match()
return getattr(self, endpoint)(request, **values)
except HTTPException as e:
return e
def render_template(self, template_name, **context):
t = self.jinja_env.get_template(template_name)
return Response(t.render(context), mimetype='text/html')
def render_json(self, data):
return Response(json.dumps(data), mimetype='application/json')
def portal_view(self, request):
# Default view. If domain is set to the default dummy domain, redirects to first-run setup instead.
if self.mgr.domain == 'spotter.vm':
return redirect('/setup-host')
return self.render_template('portal.html', conf=self.mgr.conf)
def setup_host_view(self, request):
# First-run setup view.
ex_ipv4 = tools.get_external_ipv4()
ex_ipv6 = tools.get_external_ipv6()
in_ipv4 = tools.get_local_ipv4()
in_ipv6 = tools.get_local_ipv6()
is_letsencrypt = os.path.exists('/etc/periodic/daily/acme-sh')
cert_info = tools.get_cert_info()
return self.render_template('setup-host.html', conf=self.mgr.conf, ex_ipv4=ex_ipv4, ex_ipv6=ex_ipv6, in_ipv4=in_ipv4, in_ipv6=in_ipv6, is_letsencrypt=is_letsencrypt, cert_info=cert_info)
def setup_apps_view(self, request):
# Application manager view.
return self.render_template('setup-apps.html', conf=self.mgr.conf)
def update_host_action(self, request):
# Update domain and port, then restart nginx (done via ClosingIterator in self.wsgi_app())
try:
domain = request.form['domain']
port = request.form['port']
self.mgr.update_host(domain, port, False)
server_name = request.environ['HTTP_X_FORWARDED_SERVER_NAME']
url = 'https://{}/setup-host'.format('{}:{}'.format(server_name, port) if port != '443' else server_name)
return self.render_json({'ok': self.lang.host_updated(url, url)})
except BadRequest:
return self.render_json({'error': self.lang.malformed_request()})
except InvalidValueException as e:
if e.args[0] == 'domain':
return self.render_json({'error': self.lang.invalid_domain(domain)})
if e.args[0] == 'port':
return self.render_json({'error': self.lang.invalid_port(port)})
def verify_dns_action(self, request):
# Check if all FQDNs for all applications are resolvable and point to current external IP
domains = [self.mgr.domain]+['{}.{}'.format(self.mgr.conf['apps'][app]['host'], self.mgr.domain) for app in self.mgr.conf['apps']]
ipv4 = tools.get_external_ipv4()
ipv6 = tools.get_external_ipv6()
for domain in domains:
try:
a = tools.resolve_ip(domain, 'A')
aaaa = tools.resolve_ip(domain, 'AAAA')
if not a and not aaaa:
return self.render_json({'error': self.lang.dns_record_does_not_exist(domain)})
if a and a != ipv4:
return self.render_json({'error': self.lang.dns_record_mismatch(domain, a, ipv4)})
if aaaa and aaaa != ipv6:
return self.render_json({'error': self.lang.dns_record_mismatch(domain, aaaa, ipv6)})
except:
return self.render_json({'error': self.lang.dns_timeout()})
return self.render_json({'ok': self.lang.dns_records_ok()})
def verify_http_action(self, request, **kwargs):
# Check if all applications are accessible from the internet using 3rd party ping service
proto = kwargs['proto']
domains = [self.mgr.domain]+['{}.{}'.format(self.mgr.conf['apps'][app]['host'], self.mgr.domain) for app in self.mgr.conf['apps']]
for domain in domains:
host = '{}:{}'.format(domain, self.mgr.port) if proto == 'https' and self.mgr.port != '443' else domain
url = '{}://{}/'.format(proto, host)
try:
if not tools.ping_url(url):
return self.render_json({'error': self.lang.http_host_not_reachable(url)})
except:
return self.render_json({'error': self.lang.http_timeout()})
return self.render_json({'ok': self.lang.http_hosts_ok(self.mgr.port if proto == 'https' else '80')})
def update_cert_action(self, request):
# Update certificate - either request via Let's Encrypt or manually upload files
try:
if request.form['method'] not in ['auto', 'manual']:
raise BadRequest()
if request.form['method'] == 'manual':
if not request.files['public']:
return self.render_json({'error': self.lang.cert_file_missing()})
if not request.files['private']:
return self.render_json({'error': self.lang.key_file_missing()})
request.files['public'].save('/tmp/public.pem')
request.files['private'].save('/tmp/private.pem')
self.mgr.install_cert('/tmp/public.pem', '/tmp/private.pem')
os.unlink('/tmp/public.pem')
os.unlink('/tmp/private.pem')
else:
self.mgr.request_cert()
except BadRequest:
return self.render_json({'error': self.lang.malformed_request()})
except:
return self.render_json({'error': self.lang.cert_request_error()})
return self.render_json({'ok': self.lang.cert_installed()})
def update_common_action(self, request):
try:
self.mgr.update_common(request.form['email'], request.form['gmaps-api-key'])
except BadRequest:
return self.render_json({'error': self.lang.malformed_request()})
return self.render_json({'ok': self.lang.common_updated()})
def update_app_visibility_action(self, request):
try:
if request.form['value'] == 'true':
self.mgr.show_tiles(request.form['app'])
else:
self.mgr.hide_tiles(request.form['app'])
except (BadRequest, InvalidValueException):
return self.render_json({'error': self.lang.malformed_request()})
return self.render_json({'ok': 'ok'})
def update_app_autostart_action(self, request):
try:
if request.form['value'] == 'true':
self.mgr.enable_autostart(request.form['app'])
else:
self.mgr.disable_autostart(request.form['app'])
except (BadRequest, InvalidValueException):
return self.render_json({'error': self.lang.malformed_request()})
return self.render_json({'ok': 'ok'})
def start_app_action(self, request):
try:
self.mgr.start_app(request.form['app'])
except (BadRequest, InvalidValueException):
return self.render_json({'error': self.lang.malformed_request()})
except:
return self.render_json({'error': self.lang.stop_start_error()})
return self.render_json({'ok': self.lang.app_started()})
def stop_app_action(self, request):
try:
self.mgr.stop_app(request.form['app'])
except (BadRequest, InvalidValueException):
return self.render_json({'error': self.lang.malformed_request()})
except:
return self.render_json({'error': self.lang.stop_start_error()})
return self.render_json({'ok': self.lang.app_stopped()})
class InvalidRecordException(Exception):
pass

96
basic/srv/spotter/cli.py Executable file
View File

@ -0,0 +1,96 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
import argparse
import sys
sys.path.append('/srv/spotter')
from appmgr import AppMgr
parser = argparse.ArgumentParser(description='Spotter VM application manager')
subparsers = parser.add_subparsers()
parser_update_login = subparsers.add_parser('update-login', help='Updates application login')
parser_update_login.set_defaults(action='update-login')
parser_update_login.add_argument('app', help='Application name')
parser_update_login.add_argument('login', help='Administrative login')
parser_update_login.add_argument('password', help='Administrative password')
parser_show_tiles = subparsers.add_parser('show-tiles', help='Shows application tiles in Portal')
parser_show_tiles.set_defaults(action='show-tiles')
parser_show_tiles.add_argument('app', help='Application name')
parser_hide_tiles = subparsers.add_parser('hide-tiles', help='Hides application tiles in Portal')
parser_hide_tiles.set_defaults(action='hide-tiles')
parser_hide_tiles.add_argument('app', help='Application name')
parser_start_app = subparsers.add_parser('start-app', help='Start application including it\'s dependencies')
parser_start_app.set_defaults(action='start-app')
parser_start_app.add_argument('app', help='Application name')
parser_stop_app = subparsers.add_parser('stop-app', help='Stops application including it\'s dependencies if they are not used by another running application')
parser_stop_app.set_defaults(action='stop-app')
parser_stop_app.add_argument('app', help='Application name')
parser_enable_autostart = subparsers.add_parser('enable-autostart', help='Enables application autostart')
parser_enable_autostart.set_defaults(action='enable-autostart')
parser_enable_autostart.add_argument('app', help='Application name')
parser_disable_autostart = subparsers.add_parser('disable-autostart', help='Disables application autostart')
parser_disable_autostart.set_defaults(action='disable-autostart')
parser_disable_autostart.add_argument('app', help='Application name')
parser_register_proxy = subparsers.add_parser('register-proxy', help='Rebuilds nginx proxy target for an application container')
parser_register_proxy.set_defaults(action='register-proxy')
parser_register_proxy.add_argument('app', help='Application name')
parser_unregister_proxy = subparsers.add_parser('unregister-proxy', help='Removes nginx proxy target for an application container')
parser_unregister_proxy.set_defaults(action='unregister-proxy')
parser_unregister_proxy.add_argument('app', help='Application name')
parser_update_host = subparsers.add_parser('update-host', help='Rebuilds domain structure of VM with new host name and new HTTPS port')
parser_update_host.set_defaults(action='update-host')
parser_update_host.add_argument('domain', help='Domain name')
parser_update_host.add_argument('port', help='HTTPS port')
parser_update_common = subparsers.add_parser('update-common', help='Updates common configuration properties used by multiple applications')
parser_update_common.set_defaults(action='update-common')
parser_update_common.add_argument('--email', help='Administrative e-mail address')
parser_update_common.add_argument('--gmaps-api-key', help='Google Maps API key')
parser_request_cert = subparsers.add_parser('request-cert', help='Requests and installs Let\'s Encrypt certificate for currently set domain')
parser_request_cert.set_defaults(action='request-cert')
parser_install_cert = subparsers.add_parser('install-cert', help='Installs user supplied certificate')
parser_install_cert.set_defaults(action='install-cert')
parser_install_cert.add_argument('certificate', help='Certificate file')
parser_install_cert.add_argument('key', help='Key file')
args = parser.parse_args()
mgr = AppMgr()
if args.action == 'update-login':
mgr.update_login(args.app, args.login, args.password)
elif args.action == 'show-tiles':
mgr.show_tiles(args.app)
elif args.action == 'hide-tiles':
mgr.hide_tiles(args.app)
elif args.action == 'start-app':
mgr.start_app(args.app)
elif args.action == 'stop-app':
mgr.stop_app(args.app)
elif args.action == 'enable-autostart':
mgr.enable_autostart(args.app)
elif args.action == 'disable-autostart':
mgr.disable_autostart(args.app)
elif args.action == 'register-proxy':
mgr.register_proxy(args.app)
elif args.action == 'unregister-proxy':
mgr.unregister_proxy(args.app)
elif args.action == 'update-host':
mgr.update_host(args.domain, args.port)
elif args.action == 'update-common':
mgr.update_common(args.email, args.gmaps_api_key)
elif args.action == 'request-cert':
mgr.request_cert()
elif args.action == 'install-cert':
mgr.install_cert(args.certificate, args.key)

View File

@ -0,0 +1,138 @@
{
"apps": {
"ckan": {
"host": "ckan",
"login": "N/A",
"password": "N/A",
"title": "CKAN",
"visible": false
},
"crisiscleanup": {
"host": "cc",
"login": "N/A",
"password": "N/A",
"title": "Crisis Cleanup",
"visible": false
},
"cts": {
"host": "cts",
"login": "N/A",
"password": "N/A",
"title": "CTS",
"visible": false
},
"frontlinesms": {
"host": "sms",
"login": "N/A",
"password": "N/A",
"title": "Frontline SMS",
"visible": false
},
"gnuhealth": {
"host": "gh",
"login": "N/A",
"password": "N/A",
"title": "GNU Health",
"visible": false
},
"kanboard": {
"host": "kb",
"login": "N/A",
"password": "N/A",
"title": "KanBoard",
"visible": false
},
"mifosx": {
"host": "mifosx",
"login": "N/A",
"password": "N/A",
"title": "Mifos X",
"visible": false
},
"motech": {
"host": "motech",
"login": "N/A",
"password": "N/A",
"title": "Motech",
"visible": false
},
"opendatakit": {
"host": "odk",
"login": "N/A",
"password": "N/A",
"title": "OpenDataKit Aggregate",
"visible": false
},
"opendatakit-build": {
"host": "odkbuild",
"login": "N/A",
"password": "N/A",
"title": "OpenDataKit Build",
"visible": false
},
"openmapkit": {
"host": "omk",
"login": "N/A",
"password": "N/A",
"title": "OpenMapKit",
"visible": false
},
"pandora": {
"host": "pandora",
"login": "N/A",
"password": "N/A",
"title": "Pan.do/ra",
"visible": false
},
"sahana": {
"host": "sahana",
"login": "N/A",
"password": "N/A",
"title": "Sahana EDEN",
"visible": false
},
"sahana-demo": {
"host": "sahana-demo",
"login": "N/A",
"password": "N/A",
"title": "Sahana EDEN Demo",
"visible": false
},
"sambro": {
"host": "sambro",
"login": "N/A",
"password": "N/A",
"title": "Sahana EDEN SAMBRO",
"visible": false
},
"seeddms": {
"host": "dms",
"login": "N/A",
"password": "N/A",
"title": "SeedDMS",
"visible": false
},
"sigmah": {
"host": "sigmah",
"login": "N/A",
"password": "N/A",
"title": "Sigmah",
"visible": false
},
"ushahidi": {
"host": "ush",
"login": "N/A",
"password": "N/A",
"title": "Ushahidi",
"visible": false
}
},
"common": {
"email": "admin@example.com",
"gmaps-api-key": ""
},
"host": {
"domain": "spotter.vm",
"port": "443"
}
}

View File

@ -0,0 +1,172 @@
body {
font-family: 'Calibri', 'Verdana', 'Tahoma', sans-serif;
background-color: #bbb;
color: #000;
line-height: 150%;
margin: 25px 30px;
}
a {
color: #06f;
text-decoration: none;
}
img {
border: 0px;
}
nav {
float: right;
margin-right: 30px;
}
nav a {
display: block;
color: #00c;
}
h1, h2 {
font-size: 150%;
}
header {
color: #fff;
}
header h1,
header p,
.portal-box p {
padding: 0px;
margin: 0px;
}
.portal-box,
.setup-box {
background-color: #fff;
margin-top: 13px;
margin-right: 13px;
border: solid 1px #000;
padding: 10px;
}
.portal-box {
position: relative;
width: 365px;
float: left;
height: 175px;
}
.portal-box h2 {
margin: 0px;
font-weight: normal;
}
.portal-box h2 a {
color: inherit;
}
.portal-box h2 img {
float: right;
margin-left: 10px;
margin-bottom:10px;
width: 100px;
height: 100px;
}
.portal-box ul {
margin: 0px;
padding-left: 30px;
}
.portal-box:last-child:after {
clear: both;
}
.portal-box-double-width {
width: 765px;
}
.ico {
margin-right: 5px;
width: 20px;
height: 20px;
vertical-align: top;
}
.setup-box h2 {
margin-top: 0px;
}
.setup-box input[type="text"],
.setup-box input[type="submit"],
.setup-box input[type="button"],
.setup-box input[type="file"],
.setup-box select {
box-sizing: border-box;
width: 180px;
}
.setup-box table {
border-collapse: collapse;
}
.setup-box thead {
font-weight: bold;
}
.setup-box td {
padding: 1px 10px;
vertical-align: top;
}
.setup-box td:first-child {
white-space: nowrap;
}
.setup-box td.remark {
color: #999;
font-size: 80%;
font-style: italic;
line-height: 125%;
padding-top: 5px;
}
.center {
text-align: center;
}
.error {
color: #c00;
font-weight: bold;
}
.info {
color: #090;
font-weight: bold;
}
.loader-wrap {
display: none;
}
.loader-wrap span:after {
clear: both;
content: '';
display: table;
}
.loader {
float: left;
width: 14px;
height: 14px;
border: 5px solid #eee;
border-top: 5px solid #fa3;
border-radius: 50%;
margin-right: 5px;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}

View File

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

View File

Before

Width:  |  Height:  |  Size: 69 KiB

After

Width:  |  Height:  |  Size: 69 KiB

View File

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View File

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 33 KiB

View File

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 70 KiB

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

View File

Before

Width:  |  Height:  |  Size: 119 KiB

After

Width:  |  Height:  |  Size: 119 KiB

View File

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 42 KiB

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

View File

Before

Width:  |  Height:  |  Size: 83 KiB

After

Width:  |  Height:  |  Size: 83 KiB

View File

Before

Width:  |  Height:  |  Size: 101 KiB

After

Width:  |  Height:  |  Size: 101 KiB

View File

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

Before

Width:  |  Height:  |  Size: 7.9 KiB

After

Width:  |  Height:  |  Size: 7.9 KiB

View File

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 47 KiB

View File

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View File

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

View File

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

View File

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 53 KiB

View File

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View File

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

View File

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View File

Before

Width:  |  Height:  |  Size: 9.2 KiB

After

Width:  |  Height:  |  Size: 9.2 KiB

View File

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

@ -0,0 +1,163 @@
$(function() {
$('#update-host').on('submit', update_host);
$('#verify-dns').on('click', verify_dns);
$('#verify-https').on('click', verify_https);
$('#verify-http').on('click', verify_http);
$('#cert-method').on('change', toggle_cert_method);
$('#update-cert').on('submit', update_cert);
$('#update-common').on('submit', update_common);
$('.app-visible').on('click', update_app_visibility);
$('.app-autostart').on('click', update_app_autostart);
$('tr[data-app]').on('click', '.app-start', start_app).on('click', '.app-stop', stop_app);
});
function update_host() {
$('#host-submit').hide();
$('#host-message').hide();
$('#host-wait').show();
$.post('/update-host', {'domain': $('#domain').val(), 'port': $('#port').val()}, function(data) {
$('#host-wait').hide();
if (data.error) {
$('#host-message').attr('class','error').html(data.error).show();
$('#host-submit').show();
} else {
$('#host-message').attr('class','info').html(data.ok).show();
}
});
return false;
}
function verify_dns() {
$('#verify-dns').hide();
$('#dns-message').hide();
$('#dns-wait').show();
$.get('/verify-dns', function(data) {
$('#dns-wait').hide();
if (data.error) {
$('#dns-message').attr('class','error').html(data.error).show();
$('#verify-dns').show();
} else {
$('#dns-message').attr('class','info').html(data.ok).show();
}
});
return false;
}
function _verify_http(proto) {
$('#verify-'+proto).hide();
$('#'+proto+'-message').hide();
$('#'+proto+'-wait').show();
$.get('/verify-' + proto, function(data) {
$('#'+proto+'-wait').hide();
if (data.error) {
$('#'+proto+'-message').attr('class','error').html(data.error).show();
$('#verify-'+proto).show();
} else {
$('#'+proto+'-message').attr('class','info').html(data.ok).show();
}
});
return false;
}
function verify_http() {
return _verify_http('http');
}
function verify_https() {
return _verify_http('https');
}
function toggle_cert_method() {
if ($('#cert-method').val() == 'manual') {
$('.cert-upload').show();
} else {
$('.cert-upload').hide();
}
}
function update_cert() {
$('#cert-submit').hide();
$('#cert-message').hide();
$('#cert-wait').show();
$.ajax({url: '/update-cert', type: 'POST', data: new FormData($('#update-cert')[0]), cache: false, contentType: false, processData: false, success: function(data) {
$('#cert-wait').hide();
if (data.error) {
$('#cert-message').attr('class','error').text(data.error).show();
$('#cert-submit').show();
} else {
$('#cert-message').attr('class','info').text(data.ok).show();
}
}});
return false;
}
function update_common() {
$('#common-submit').hide();
$('#common-message').hide();
$('#common-wait').show();
$.post('/update-common', {'email': $('#email').val(), 'gmaps-api-key': $('#gmaps-api-key').val()}, function(data) {
$('#common-wait').hide();
if (data.error) {
$('#common-message').attr('class','error').html(data.error).show();
$('#common-submit').show();
} else {
$('#common-message').attr('class','info').html(data.ok).show();
$('#common-submit').show();
}
});
return false;
}
function update_app_visibility(ev) {
var el = $(ev.target);
var app = el.closest('tr').data('app');
var value = el.is(':checked') ? 'true' : '';
$.post('/update-app-visibility', {'app': app, 'value': value}, function(data) {
if (data.error) {
el.prop('checked', !value);
alert(data.error);
}
});
}
function update_app_autostart(ev) {
var el = $(ev.target);
var app = el.closest('tr').data('app');
var value = el.is(':checked') ? 'true' : '';
$.post('/update-app-autostart', {'app': app, 'value': value}, function(data) {
if (data.error) {
el.prop('checked', !value);
alert(data.error);
}
});
}
function start_app(ev) {
var el = $(ev.target);
var app = el.closest('tr').data('app');
var td = el.closest('td');
td.html('<div class="loader"></div>');
$.post('/start-app', {'app': app}, function(data) {
if (data.error) {
td.attr('class','error').html(data.error);
} else {
td.removeAttr('class').html(data.ok);
}
});
return false;
}
function stop_app(ev) {
var el = $(ev.target);
var app = el.closest('tr').data('app');
var td = el.closest('td');
td.html('<div class="loader"></div>');
$.post('/stop-app', {'app': app}, function(data) {
if (data.error) {
td.attr('class','error').html(data.error);
} else {
td.removeAttr('class').html(data.ok);
}
});
return false;
}

View File

@ -0,0 +1,28 @@
<!DOCTYPE html>
<html lang="cs">
<head>
<meta charset="utf-8">
<meta name="author" content="TS">
<meta name="copyright" content="page is under CC BY-NC-ND 3.0 CZ">
<meta name="generator" content="Spotter.ngo">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}{% endblock %}</title>
<link rel="icon" href="static/img/Cluster_Spotter.png" type="image/png">
<link rel="stylesheet" href="static/css/style.css" type="text/css" media="screen">
<script src="static/js/jquery-3.3.1.min.js"></script>
<script src="static/js/script.js"></script>
</head>
<body>
<nav>
{% set template = self._TemplateReference__context.name %}
{% if template != 'portal.html' %}<a href="/">Portál</a>{% endif %}
{% if template != 'setup-host.html' %}<a href="/setup-host">Nastavení hostitele</a>{% endif %}
{% if template != 'setup-apps.html' %}<a href="/setup-apps">Nastavení aplikací</a>{% endif %}
</nav>
<header>
<h1>CLUSTER NGO</h1>
<p>Sada softwarových nástrojů určená pro krizový management.</p>
</header>
{% block body %}{% endblock %}
</body>
</html>

View File

@ -0,0 +1,345 @@
{% extends 'layout.html' %}
{% block title %}Cluster NGO{% endblock %}
{% block body %}
{% set host = '{}:{}'.format(conf['host']['domain'], conf['host']['port']) if conf['host']['port'] != '443' else conf['host']['domain'] %}
{% set app = conf['apps']['sahana'] %}
{% if app['visible'] %}
<div class="portal-box portal-box-double-width">
<h2><a href="https://sahana.{{ host }}/eden/"><img src="static/img/EDEN.png" alt="Sahana EDEN" title="Sahana EDEN">Sahana EDEN</a></h2>
<p><strong>Registr kontaktů</strong> asociací, organizací, jednotek zaměstnanců, dobrovolníků, <strong>Registr prostředků</strong>, materiálních zdrojů určených pro činnost v krizových situacích, <strong>logistika</strong> krizového zboží ve skladištích, úkrytech, <strong>organizace lidských zdrojů</strong>, diobrovolníků, <strong>mapová vizualizace</strong> pro lokalizaci a popis krizové události a <strong>mnoho dalších funkcí</strong>.</p>
<ul>
<li><strong>Login:</strong> <span class="login">{{ app['login'] }}</span></li>
<li><strong>Heslo:</strong> <span class="password">{{ app['password'] }}</span></li>
</ul>
</div>
{% endif %}
{% set app = conf['apps']['sahana-demo'] %}
{% if app['visible'] %}
<div class="portal-box">
<h2><a href="https://sahana-demo.{{ host }}/eden/"><img src="static/img/EDEN.png" alt="Sahana EDEN DEMO" title="Sahana EDEN DEMO">Sahana EDEN DEMO</a></h2>
<p>Přístup určený k bezpečnému vyzkoušení aplikace. Zde můžete přidávat i mazat testovací data.</p>
<ul>
<li><strong>Login:</strong> <span class="login">{{ app['login'] }}</span></li>
<li><strong>Heslo:</strong> <span class="password">{{ app['password'] }}</span></li>
</ul>
</div>
{% endif %}
{% set app = conf['apps']['sambro'] %}
{% if app['visible'] %}
<div class="portal-box">
<h2><a href="https://sambro.{{ host }}/eden/"><img src="static/img/EDEN.png" alt="Sahana EDEN SAMBRO" title="Sahana EDEN SAMBRO">Sahana EDEN SAMBRO</a></h2>
<p>Samostatná instance Sahana EDEN s šablonou SAMBRO.</p>
<ul>
<li><strong>Login:</strong> <span class="login">{{ app['login'] }}</span></li>
<li><strong>Heslo:</strong> <span class="password">{{ app['password'] }}</span></li>
</ul>
</div>
<div class="portal-box">
<h2><a href="#"><img src="static/img/EDEN.png" alt="SAMBRO Mobile" title="SAMBRO Mobile">SAMBRO Mobile</a></h2>
<p>Mobilní klient k aplikaci Sahana EDEN.<br>
<a href="https://itunes.apple.com/us/app/sambro-mobile/id1127251669"><img src="static/img/icons/iOS.png" class="ico" alt="IOS">IOS 6.0 a vyšší</a><br>
<a href="https://apkpure.com/sambro-mobile/io.sahana.sambro.mobile"><img src="static/img/icons/Android.png" class="ico" alt="Android">Android 4.0 a vyšší</a>
</p>
<ul>
<li><strong>URL:</strong> <span class="clienturl">https://sambro.{{ host }}/eden/</span></li>
</ul>
</div>
{% endif %}
{% set app = conf['apps']['crisiscleanup'] %}
{% if app['visible'] %}
<div class="portal-box">
<h2><a href="https://cc.{{ host }}"><img src="static/img/Crisis_Cleanup.png" alt="Crisis Cleanup" title="Crisis Cleanup">Crisis Cleanup</a></h2>
<p><strong>Mapování krizové pomoci</strong> při odstraňování následků katastrof a koordinaci práce. Jde o majetek, ne o lidi.</p>
<ul>
<li><strong>Login:</strong> <span class="login">{{ app['login'] }}</span></li>
<li><strong>Heslo:</strong> <span class="password">{{ app['password'] }}</span></li>
</ul>
</div>
{% endif %}
{% set app = conf['apps']['ckan'] %}
{% if app['visible'] %}
<div class="portal-box">
<h2><a href="https://ckan.{{ host }}"><img src="static/img/CKAN.png" alt="CKAN" title="CKAN">CKAN</a></h2>
<p><strong>Repository</strong> management a datová analýza pro vytváření otevřených dat.</p>
<ul>
<li><strong>Login:</strong> <span class="login">{{ app['login'] }}</span></li>
<li><strong>Heslo:</strong> <span class="password">{{ app['password'] }}</span></li>
</ul>
</div>
{% endif %}
{% set app = conf['apps']['opendatakit-build'] %}
{% if app['visible'] %}
<div class="portal-box">
<h2><a href="https://odkbuild.{{ host }}"><img src="static/img/ODK.png" alt="Open Data Kit" title="Open Data Kit">ODK Build</a></h2>
<p><strong>Sběr dat s pomocí smartphone</strong>.<br>Aplikace pro návrh formulářů<br>
<p><a href="http://opendatakit.org/xiframe/">XLSForm</a> - online konverter XLS.<br>
<a href="https://opendatakit.org/downloads/download-info/odk-formuploader/"><img src="static/img/icons/Java.png" class="ico" alt="ODK Form Uploader">ODK Form Uploader</a><br>
<a href="https://opendatakit.org/downloads/download-info/odk-validate-2/"><img src="static/img/icons/Java.png" class="ico" alt="ODK Validate">ODK Validate</a></p>
</div>
{% endif %}
{% set app = conf['apps']['opendatakit'] %}
{% if app['visible'] %}
<div class="portal-box">
<h2><a href="#"><img src="static/img/ODK_Collect.png" alt="Open Data Kit" title="Open Data Kit">ODK Collect</a></h2>
<p>Mobilní aplikace<br>
<a href="https://play.google.com/store/apps/details?id=org.odk.collect.android"><img src="static/img/icons/Android.png" class="ico" alt="ODK Collect">ODK Collect pro Android</a><br>
<a href="https://opendatakit.org/downloads/download-info/odk-briefcase/"><img src="static/img/icons/Java.png" class="ico" alt="ODK Briefcase">ODK Briefcase</a><br>
</p>
<ul>
<li><strong>URL:</strong> <span class="clienturl">https://odk.{{ host }}</span></li>
</ul>
</div>
<div class="portal-box">
<h2><a href="https://odk.{{ host }}/"><img src="static/img/ODK.png" alt="Open Data Kit" title="Open Data Kit">ODK Aggregate</a></h2>
<p><strong>Sběr dat s pomocí smartphone</strong>.<br>
<a href="http://geoodk.com">GeoODK Collect</a> - náhrada papírových dotazníků smartphonem.
</p>
<ul>
<li><strong>Login:</strong> <span class="login">{{ app['login'] }}</span></li>
<li><strong>Heslo:</strong> <span class="password">{{ app['password'] }}</span></li>
</ul>
</div>
{% endif %}
{% set app = conf['apps']['openmapkit'] %}
{% if app['visible'] %}
<div class="portal-box">
<h2><a href="https://omk.{{ host }}"><img src="static/img/OMK.png" alt="Open Map Kit" title="Open Map Kit">OpenMapKit Server</a></h2>
<p><strong>Sběr dat s pomocí smartphone</strong>.<br>
<ul>
<li><strong>Login:</strong> <span class="login">{{ app['login'] }}</span></li>
<li><strong>Heslo:</strong> <span class="password">{{ app['password'] }}</span></li>
</ul>
</div>
<div class="portal-box">
<h2><a href="#"><img src="static/img/GeoODK_Collect.png" alt="GeoODK Collect" title="GeoODK Collect">GeoODK Collect</a></h2>
<p>Mobilní aplikace<br>
<a href="https://play.google.com/store/apps/details?id=com.geoodk.collect.android"><img src="static/img/icons/Android.png" class="ico" alt="GeoODK Collect">GeoODK Collect pro Android</a>
</p>
<ul>
<li><strong>URL:</strong> <span class="clienturl">https://omk.{{ host }}</span></li>
</ul>
</div>
<div class="portal-box">
<h2><a href="#"><img src="static/img/OMK.png" alt="Open Map Kit" title="Open Map Kit">OpenMapKit</a></h2>
<p>Mobilní aplikace<br>
<a href="https://play.google.com/store/apps/details?id=org.odk.collect.android"><img src="static/img/icons/Android.png" class="ico" alt="ODK Collect">ODK Collect pro Android</a><br>
<a href="https://play.google.com/store/apps/details?id=org.redcross.openmapkit"><img src="static/img/icons/Android.png" class="ico" alt="Android">OpenMapKit pro Android 4.1 a vyšší</a>
</p>
<ul>
<li><strong>URL:</strong> <span class="clienturl">https://omk.{{ host }}</span></li>
</ul>
</div>
{% endif %}
{% set app = conf['apps']['frontlinesms'] %}
{% if app['visible'] %}
<div class="portal-box">
<h2><a href="https://sms.{{ host }}"><img src="static/img/FrontlineSMS.png" alt="FrontlineSMS" title="FrontlineSMS">FrontlineSMS</a></h2>
<p><strong>SMS messaging</strong> přes veřejné datové brány</p>
<ul>
<li><strong>Login:</strong> <span class="login">{{ app['login'] }}</span></li>
<li><strong>Heslo:</strong> <span class="password">{{ app['password'] }}</span></li>
</ul>
</div>
<div class="portal-box">
<h2><a href="#"><img src="static/img/SMS_Sync.png" alt="SMS Sync Gateway" title="SMS Sync Gateway">SMS Sync Gateway</a></h2>
<p>Mobilní aplikace pro<br>
<a href="https://play.google.com/store/apps/details?id=org.addhen.smssync"><img src="static/img/icons/Android.png" class="ico" alt="Android">Android 2.3 a vyšší</a>
</p>
</div>
{% endif %}
{% set app = conf['apps']['seeddms'] %}
{% if app['visible'] %}
<div class="portal-box">
<h2><a href="https://dms.{{ host }}"><img src="static/img/SeedDMS.png" alt="SeedDMS" title="SeedDMS">SeedDMS</a></h2>
<p><strong>Dokument management</strong> na dokumentaci a projektovou dokumentaci</p>
<ul>
<li><strong>Login:</strong> <span class="login">{{ app['login'] }}</span></li>
<li><strong>Heslo:</strong> <span class="password">{{ app['password'] }}</span></li>
</ul>
</div>
{% endif %}
{% set app = conf['apps']['pandora'] %}
{% if app['visible'] %}
<div class="portal-box">
<h2><a href="https://pandora.{{ host }}"><img src="static/img/Pandora.png" alt="Pan.do/ra" title="Pan.do/ra">Pan.do/ra</a></h2>
<p><strong>Media management</strong> na foto a video z krizové události. Tvorba metadat, komentářů, lokalizace v čase a na mapě.</p>
<ul>
<li><strong>Login:</strong> <span class="login">{{ app['login'] }}</span></li>
<li><strong>Heslo:</strong> <span class="password">{{ app['password'] }}</span></li>
</ul>
</div>
{% endif %}
{% set app = conf['apps']['ushahidi'] %}
{% if app['visible'] %}
<div class="portal-box">
<h2><a href="https://ush.{{ host }}"><img src="static/img/Ushahidi.png" alt="Ushahidi" title="Ushahidi">Ushahidi</a></h2>
<p>Reakce na krizovou událost. Shromažďujte zprávy od obětí a pracovníků v terénu prostřednictvím SMS, e-mailu, webu, Twitteru.</p>
<ul>
<li><strong>Login:</strong> <span class="login">{{ app['login'] }}</span></li>
<li><strong>Heslo:</strong> <span class="password">{{ app['password'] }}</span></li>
</ul>
</div>
<div class="portal-box">
<h2><a href="#"><img src="static/img/Ushahidi_mobile.png" alt="Ushahidi" title="Ushahidi">Ushahidi Mobile</a></h2>
<p>Mobilní aplikace Ushahidi pro<br>
<a href="https://itunes.apple.com/us/app/ushahidi-mobile/id1205994516?mt=8"><img src="static/img/icons/iOS.png" class="ico" alt="IOS">IOS 9.0 a vyšší</a><br>
<a href="https://play.google.com/store/apps/details?id=com.ushahidi.mobile"><img src="static/img/icons/Android.png" class="ico" alt="Android">Android 4.4 a vyšší</a>
</p>
<ul>
<li><strong>URL:</strong> <span class="clienturl">ush.{{ host }}</span></li>
</ul>
</div>
{% endif %}
{% set app = conf['apps']['kanboard'] %}
{% if app['visible'] %}
<div class="portal-box">
<h2><a href="https://kb.{{ host }}"><img src="static/img/Kanboard.png" alt="Kanboard" title="Kanboard">Kanboard</a></h2>
<p>Usnadňuje tvorbu a řízení projektů s pomocí Kanban metodiky.</p>
<ul>
<li><strong>Login:</strong> <span class="login">{{ app['login'] }}</span></li>
<li><strong>Heslo:</strong> <span class="password">{{ app['password'] }}</span></li>
</ul>
</div>
<div class="portal-box">
<h2><a href="#"><img src="static/img/Kanboard.png" alt="Kanboard" title="Kanboard">Kanboard Mobile</a></h2>
<p>Mobilní aplikace<br>
<a href="https://play.google.com/store/apps/details?id=eu.it_quality.kanboard"><img src="static/img/icons/Android.png" class="ico" alt="KanBoard">KanBoard client pro Android 4.1 a vyšší</a><br>
<a href="https://f-droid.org/packages/in.andres.kandroid/"><img src="static/img/icons/Android.png" class="ico" alt="Android">Kandroid pro Android 4.2 a vyšší</a>
</p>
<ul>
<li><strong>URL:</strong> <span class="clienturl">https://kb.{{ host }}</span></li>
</ul>
</div>
{% endif %}
{% set app = conf['apps']['cts'] %}
{% if app['visible'] %}
<div class="portal-box">
<h2><a href="https://cts.{{ host }}"><img src="static/img/CTS.png" alt="CTS" title="CTS">CTS</a></h2>
<p>Logistika hmotné pomoci pro humanitární potřeby.</p>
<ul>
<li><strong>Login:</strong> <span class="login">{{ app['login'] }}</span></li>
<li><strong>Heslo:</strong> <span class="password">{{ app['password'] }}</span></li>
</ul>
</div>
{% endif %}
{% set app = conf['apps']['gnuhealth'] %}
{% if app['visible'] %}
<div class="portal-box">
<h2><a href="https://gh.{{ host }}/index.html"><img src="static/img/GNU_Health.png" alt="GNU Health" title="GNU Health">GNU Health</a></h2>
<p>Zdravotní a nemocniční informační systém.</p>
<ul>
<li><strong>Login:</strong> <span class="login">{{ app['login'] }}</span></li>
<li><strong>Heslo:</strong> <span class="password">{{ app['password'] }}</span></li>
<li><strong>Heslo k demu:</strong> <span class="demopassword">gnusolidario</span></li>
</ul>
</div>
<div class="portal-box">
<h2><a href="#"><img src="static/img/GNU_Health.png" alt="GNU Health" title="GNU Health">GNU Health klienti</a></h2>
<p>Klientské aplikace platformy Tryton GNU Health pro<br>
<a href="https://downloads.tryton.org/4.2/tryton-last.exe"><img src="static/img/icons/Windows.png" class="ico" alt="Windows">Windows</a><br>
<a href="https://downloads.tryton.org/4.2/tryton-last.dmg"><img src="static/img/icons/MacOS.png" class="ico" alt="MacOS">MacOS</a><br>
<a href="https://downloads.tryton.org/4.2/tryton-last.tar.gz"><img src="static/img/icons/Linux.png" class="ico" alt="Linux">Linux</a>
</p>
<ul>
<li><strong>URL:</strong> <span class="clienturl">gh.{{ host }}</span></li>
</ul>
</div>
{% endif %}
{% set app = conf['apps']['sigmah'] %}
{% if app['visible'] %}
<div class="portal-box">
<h2><a href="https://sigmah.{{ host }}/sigmah/"><img src="static/img/Sigmah.png" alt="Sigmah" title="Sigmah">Sigmah</a></h2>
<p>Rozpočtování získávání finančních prostředků.</p>
<ul>
<li><strong>Login:</strong> <span class="login">{{ app['login'] }}</span></li>
<li><strong>Heslo:</strong> <span class="password">{{ app['password'] }}</span></li>
</ul>
</div>
{% endif %}
{% set app = conf['apps']['motech'] %}
{% if app['visible'] %}
<div class="portal-box">
<h2><a href="https://motech.{{ host }}/"><img src="static/img/Motech.png" alt="Motech" title="Motech">Motech</a></h2>
<p>Integrace zdravotnických a komunikačních služeb.</p>
<ul>
<li><strong>Login:</strong> <span class="login">{{ app['login'] }}</span></li>
<li><strong>Heslo:</strong> <span class="password">{{ app['password'] }}</span></li>
</ul>
</div>
{% endif %}
{% set app = conf['apps']['mifosx'] %}
{% if app['visible'] %}
<div class="portal-box">
<h2><a href="https://mifosx.{{ host }}/"><img src="static/img/MifosX.png" alt="Mifos X" title="Mifos X">Mifos X</a></h2>
<p>Nástroj na rozvojovou, humanitární pomoc a mikrofinancování.</p>
<ul>
<li><strong>Login:</strong> <span class="login">{{ app['login'] }}</span></li>
<li><strong>Heslo:</strong> <span class="password">{{ app['password'] }}</span></li>
</ul>
</div>
<div class="portal-box">
<h2><a href="#"><img src="static/img/MifosX_Mobile.png" alt="Mifos X" title="Mifos X">Mifos X</a></h2>
<p>Mobilní aplikace<br>
<a href="https://github.com/openMF/android-client/releases"><img src="static/img/icons/Android.png" class="ico" alt="Mifos X">Mifos X client pro Android 3.0 a vyšší</a><br>
</p>
<ul>
<li><strong>URL:</strong> <span class="clienturl">mifosx.{{ host }}</span></li>
<li><strong>Tenant ID:</strong> <span>default</span></li>
</ul>
</div>
{% endif %}
{% if false %}
<div class="portal-box">
<h2><a href="#"><img src="static/img/Diaspora.png" alt="diaspora*" title="diaspora*">diaspora*</a></h2>
<p>Autonomní sociání síť s možností propojení do cizích sociálních sítí.</p>
<ul>
<li><strong>Login:</strong> <span class="login">{{ app['login'] }}</span></li>
<li><strong>Heslo:</strong> <span class="password">{{ app['password'] }}</span></li>
</ul>
</div>
<div class="portal-box">
<h2><a href="http://openid.net"><img src="static/img/OpenID.png" alt="OpenID" title="OpenID">OpenID</a></h2>
<p>Pro ověření identity budete potřebovat účet OpenID. Zaregistrujte se. Registraci využijete v software Sahana EDEN.</p>
</div>
<div class="portal-box">
<h2><a href="#"><img src="static/img/POSM.png" alt="POSM" title="POSM">POSM</a></h2>
<p><strong>Portable Open Street Map</strong> - softwarový balík na offline používání OpenStreet Map v samostatné virtuální image.</p>
</div>
{% endif %}
<div class="portal-box">
<h2><a href="http://spotter.ngo"><img src="static/img/Cluster_Spotter.png" alt="Cluster Spotter" title="Cluster Spotter">Cluster Spotter</a></h2>
<p>Info o Misi a Vizi projektu, včetně kontaktu. Zachovejte data bezpečná a neposkytujte je nepovolaným osobám.<br>
<small>CC 4.0 CZ by <a href="http://trendspotter.cz">TS</a>. Content is based on PD, CC, GNU/GPL. Brand names, trademarks belong to their respective holders.</small>
</p>
</div>
{% endblock %}

View File

@ -0,0 +1,58 @@
{% extends 'layout.html' %}
{% block title %}Nastavení aplikací{% endblock %}
{% block body %}
<div class="setup-box">
<h2>Nastavení aplikací</h2>
<p>Společné nastavení sdílené některými aplikacemi.</p>
<form id="update-common" action="/update-common" method="post">
<table>
<tr>
<td>E-mail</td>
<td><input type="text" name="email" id="email" value="{{ conf['common']['email'] }}"></td>
<td class="remark">Administrativní e-mail na který budou doručovány zprávy a upozornění z aplikací. Stejná e-mailová adresa bude také využita některými aplikacemi pro odesílání zpráv uživatelům.</td>
</tr>
<tr>
<td>Google Maps API klíč</td>
<td><input type="text" name="gmaps-api-key" id="gmaps-api-key" value="{{ conf['common']['gmaps-api-key'] }}"></td>
<td class="remark">API klíč pro službu Google Maps, která je využita některými aplikacemi.</td>
</tr>
<tr>
<td>&nbsp;</td>
<td colspan="2">
<input type="submit" id="common-submit" value="Nastavit hodnoty">
<div id="common-message"></div>
<div id="common-wait" class="loader-wrap">
<div class="loader"></div>
<span>Provádí se změna nastavení, prosím čekejte...</span>
</div>
</td>
</tr>
</table>
</form>
</div>
<div class="setup-box">
<h2>Správce aplikací</h2>
<p>Vyberte které aplikace se mají zobrazovat na hlavní straně portálu a které mají být automaticky spuštěny při startu virtuálního stroje.</p>
<table>
<thead>
<tr>
<td>Aplikace</td>
<td>Zobrazena</td>
<td>Autostart</td>
<td>Stav</td>
</tr>
</thead>
<tbody>
{% for app in conf['apps']|sort %}
<tr data-app="{{ app }}">
<td>{{ conf['apps'][app]['title'] }}</td>
<td class="center"><input type="checkbox" class="app-visible"{% if conf['apps'][app]['visible'] %} checked{% endif %}></td>
<td class="center"><input type="checkbox" class="app-autostart"{% if is_service_autostarted(app) %} checked{% endif %}></td>
<td>{% if is_service_started(app) %}<span class="info">Spuštěna</span> (<a href="#" class="app-stop">zastavit</a>){% else %}<span class="error">Zastavena</span> (<a href="#" class="app-start">spustit</a>){% endif %}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}

View File

@ -0,0 +1,115 @@
{% extends 'layout.html' %}
{% block title %}Nastavení hostitele{% endblock %}
{% block body %}
<div class="setup-box">
<h2>HTTPS Hostitel</h2>
<p>Základní doménové jméno a HTTPS port na kterých budou přístupny všechny aplikace.</p>
<form id="update-host" action="/update-host" method="post">
<table>
<tr>
<td>Doména</td>
<td><input type="text" name="domain" id="domain" value="{{ conf['host']['domain'] }}"></td>
<td class="remark">Plně kvalifikovaný doménový název, na kterém bude dostupný aplikační portál. Jednotlivé aplikace budou dostupné na subdoménách této domény.</td>
</tr>
<tr>
<td>Port</td>
<td><input type="text" name="port" id="port" value="{{ conf['host']['port'] }}"></td>
<td class="remark">HTTPS port na kterém budou dostupné aplikace. Výchozí HTTPS port je 443.</td>
</tr>
<tr>
<td>&nbsp;</td>
<td colspan="2">
<input type="submit" id="host-submit" value="Nastavit hostitele">
<div id="host-message"></div>
<div id="host-wait" class="loader-wrap">
<div class="loader"></div>
<span>Provádí se změna nastavení, prosím čekejte...</span>
</div>
</td>
</tr>
</table>
</form>
</div>
<div class="setup-box">
<h2>DNS záznamy</h2>
<p>Na jmenném serveru domény nastavené v sekci <em>HTTPS Hostitel</em> nastavte DNS záznamy typu A, případně i AAAA pro následující doménové názvy a nasměrujte je na vnější (tj. dostupnou z internetu) IP adresu tohoto virtuální stroje. Toto nastavení lze obvykle provést skrze webové rozhraní registrátora domény.</p>
<p>Vnější IPv4 {% if ex_ipv4 %}je <strong>{{ ex_ipv4 }}</strong>{% else %}nebyla zjištěna{% endif %} a IPv6 {% if ex_ipv6 %}je <strong>{{ ex_ipv6 }}</strong>{% else %}nebyla zjištěna{% endif %}.</p>
<ul>
<li>{{ conf['host']['domain'] }}</li>
<li>*.{{ conf['host']['domain'] }}</li>
</ul>
<p>Pokud jmenný server nepodporuje wildcard záznamy nebo pokud nemůžete či nechcete dedikovat virtuálnímu stroji všechny subdomény, nastavte místo toho záznamy pro následující doménové názvy</p>
<ul style="column-count:3">
<li>{{ conf['host']['domain'] }}</li>
{% for app in conf['apps']|sort %}
<li>{{ conf['apps'][app]['host'] }}.{{ conf['host']['domain'] }}</li>
{% endfor %}
</ul>
<input type="button" id="verify-dns" value="Ověřit nastavení DNS">
<div id="dns-message"></div>
<div id="dns-wait" class="loader-wrap">
<div class="loader"></div>
<span>Ověřuje se nastavení DNS, prosím čekejte...</span>
</div>
</div>
<div class="setup-box">
<h2>Firewall a NAT</h2>
<p>Pokud je stávající připojení k internetu zprostředkováno routerem s NAT, na hypervizoru je nastaven firewall nebo existují jiné restrikce síťového provozu, je nutno upravit nastavení příslušných komponent, aby byl provoz na portu {{ conf['host']['port'] }} (nastaveném v sekci <em>HTTPS Hostitel</em>) z internetu korektně nasměrován na místní adresu virtuálního stroje.</p>
<p>Pokud bude využit systém automatického vyžádání a obnovy certifikátu (sekce <em>HTTPS certifikát</em>), je nutno aby byl na místní adresu virtuálního stroje nasměrován i port 80, případně byla nastavena HTTP proxy přesměrovávající doménová jména zmíněná v sekci <em>DNS záznamy</em>.</p>
<p>Místní IPv4 {% if in_ipv4 %}je <strong>{{ in_ipv4 }}</strong>{% else %}nebyla zjištěna{% endif %} a IPv6 {% if in_ipv6 %}je <strong>{{ in_ipv6 }}</strong>{% else %}nebyla zjištěna{% endif %}.</p>
<input type="button" id="verify-https" value="Ověřit nastavení portu {{ conf['host']['port'] }}">
<div id="https-message"></div>
<div id="https-wait" class="loader-wrap">
<div class="loader"></div>
<span>Ověřuje se nastavení firewallu a NAT pro port {{ conf['host']['port'] }}, prosím čekejte...</span>
</div>
<input type="button" id="verify-http" value="Ověřit nastavení portu 80">
<div id="http-message"></div>
<div id="http-wait" class="loader-wrap">
<div class="loader"></div>
<span>Ověřuje se nastavení firewallu a NAT pro port 80, prosím čekejte...</span>
</div>
</div>
<div class="setup-box">
<h2>HTTPS certifikát</h2>
<p>Stávající certifikát je vystaven na jméno <strong>{{ cert_info['subject']['commonName'] }}</strong> vystavitelem <strong>{{ cert_info['issuer']['commonName'] }}</strong> a jeho platnost vyprší <strong>{{ cert_info['notAfter'] }}</strong>.</p>
<form id="update-cert" action="/update-cert" method="post">
<table>
<tr>
<td>Způsob správy</td>
<td>
<select name="method" id="cert-method">
<option value="auto"{% if is_letsencrypt %} selected{% endif %}>Automaticky</option>
<option value="manual"{% if not is_letsencrypt %} selected{% endif %}>Ručně</option>
</select>
</td>
<td class="remark">Volba "Automaticky" způsobí, že systém automaticky zažádá o certifikát certifikační autority Let's Encrypt pro všechny plně kvalifikované doménové názvy (tj. nikoliv wildcard) zmíněné v sekci <em>DNS záznamy</em> a nainstaluje úlohu pro jeho automatickou obnovu. Tato akce může trvat několik minut.<br>Volba "Ručně" znamená, že soubory certifikátu a jeho soukromého klíče je nutno nahrát a následně obnovovat ručně skrze formulář na této stránce.</td>
</tr>
<tr class="cert-upload"{% if is_letsencrypt %} style="display:none"{% endif %}>
<td>Soubor certifikátu</td>
<td><input type="file" name="public" accept=".cer, .crt, .der, .pem"></td>
<td class="remark">Soubor s certifikátem ve formátu PEM.<br>Pokud je podepsán certifikační autoritou třetí strany, pak by tento soubor měl mimo koncového certifikátu obsahovat i podpisový certifikát.</td>
</tr>
<tr class="cert-upload"{% if is_letsencrypt %} style="display:none"{% endif %}>
<td>Soubor klíče</td>
<td><input type="file" name="private" accept=".key, .pem"></td>
<td class="remark">Soubor se soukromým klíčem ve formátu PEM pro výše vybraný certifikát.</td>
</tr>
<tr>
<td>&nbsp;</td>
<td colspan="2">
<input type="submit" id="cert-submit" value="Nastavit certifikát">
<div id="cert-message"></div>
<div id="cert-wait" class="loader-wrap">
<div class="loader"></div>
<span>Provádí se změna nastavení, prosím čekejte...</span>
</div>
</td>
</tr>
</table>
</form>
</div>
{% endblock %}

19
basic/srv/spotter/wsgi.py Executable file
View File

@ -0,0 +1,19 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
import sys
sys.path.append('/srv/spotter')
from appmgr.wsgiapp import WSGIApp
application = WSGIApp()
if __name__ == '__main__':
import os
from werkzeug.serving import run_simple
from werkzeug.wsgi import SharedDataMiddleware
application = SharedDataMiddleware(application, {
'/static': os.path.join(os.path.dirname(__file__), 'static')
})
run_simple('127.0.0.1', 8080, application, use_reloader=True)

View File

@ -1,318 +0,0 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
import argparse
import json
import os
import subprocess
CONF_FILE = '/srv/config.json'
DISCARD_IP = '[100::1]'
ISSUE_FILE = '/etc/issue'
NGINX_DIR = '/etc/nginx/conf.d'
NGINX_TEMPLATE = '''server {{
listen [::]:{port} ssl http2;
server_name {host}.{domain};
access_log /var/log/nginx/{app}.access.log;
error_log /var/log/nginx/{app}.error.log;
location / {{
proxy_pass http://{ip}:8080;
}}
error_page 502 /error.html;
location /error.html {{
root /srv/portal;
}}
}}
'''
NGINX_DEFAULT_TEMPLATE = '''server {{
listen [::]:80 default_server ipv6only=off;
location / {{
return 301 https://$host:{port}$request_uri;
}}
location /.well-known/acme-challenge/ {{
root /etc/acme.sh.d;
}}
}}
server {{
listen [::]:{port} ssl http2 default_server ipv6only=off;
root /srv/portal;
index index.html;
location / {{
try_files $uri $uri/ =404;
}}
location /config.json {{
alias /srv/config.json;
}}
}}
'''
ISSUE_TEMPLATE = '''
\x1b[1;32m _____ _ _ __ ____ __
/ ____| | | | | \\\\ \\\\ / / \\\\/ |
| (___ _ __ ___ | |_| |_ ___ _ _\\\\ \\\\ / /| \\\\ / |
\\\\___ \\\\| '_ \\\\ / _ \\\\| __| __/ _ \\\\ '__\\\\ \\\\/ / | |\\\\/| |
____) | |_) | (_) | |_| || __/ | \\\\ / | | | |
|_____/| .__/ \\\\___/ \\\\__|\\\\__\\\\___|_| \\\\/ |_| |_|
| |
|_|\x1b[0m
\x1b[1;33mUPOZORNĚNÍ:\x1b[0m Neoprávněný přístup k tomuto zařízení je zakázán.
Musíte mít výslovné oprávnění k přístupu nebo konfiguraci tohoto zařízení.
Neoprávněné pokusy a kroky k přístupu nebo používání tohoto systému mohou mít
za následek občanské nebo trestní sankce.
\x1b[1;33mCAUTION:\x1b[0m Unauthozired access to this device is prohibited.
You must have explicit, authorized permission to access or configure this
device. Unauthorized attempts and actions to access or use this system may
result in civil or criminal penalties.
Pro přístup k aplikacím otevřete URL \x1b[1mhttps://{host}\x1b[0m ve Vašem
internetovém prohlížeči.
\x1b[0;30m
'''
class SpotterManager:
def __init__(self):
# Load JSON configuration
with open(CONF_FILE, 'r') as f:
self.conf = json.load(f)
self.domain = self.conf['host']['domain']
self.port = self.conf['host']['port']
def save_conf(self):
# Save a sorted JSON configuration object with indentation
with open(CONF_FILE, 'w') as f:
json.dump(self.conf, f, sort_keys=True, indent=4)
def update_login(self, app, login, password):
# Update login and password for an app in the configuration
if login is not None:
self.conf['apps'][app]['login'] = login
if password is not None:
self.conf['apps'][app]['password'] = password
self.save_conf()
def show_tiles(self, app):
# Update tiles-shown for the app in the configuration
self.conf['apps'][app]['tiles-shown'] = True
self.save_conf()
def hide_tiles(self, app):
# Update tiles-shown for the app in the configuration
self.conf['apps'][app]['tiles-shown'] = False
self.save_conf()
def start_app(self, app):
# Start the actual app service
subprocess.call(['/sbin/service', app, 'start'])
def stop_app(self, app):
# Stop the actual app service
subprocess.call(['/sbin/service', app, 'stop'])
# Stop the app service's dependencies if they are not used by another running app
deps = self.build_deps_tree()
for dep in self.get_app_deps(app):
if not any([self.is_app_started(d) for d in deps[dep]]):
subprocess.call(['/sbin/service', dep, 'stop'])
def build_deps_tree(self):
# Fisrt, build a dictionary of {app: [needs]}
needs = {}
for app in self.conf['apps']:
needs[app] = self.get_app_deps(app)
# Then reverse it to {need: [apps]}
deps = {}
for app, need in needs.iteritems():
for n in need:
deps.setdefault(n, []).append(app)
return deps
def get_app_deps(self, app):
# Get "needs" line from init script and split it to list, skipping first two elements (docker, net)
try:
with open(os.path.join('/etc/init.d', app), 'r') as f:
return [l.split()[2:] for l in f.readlines() if l.startswith('\tneed')][0]
except:
return []
def is_app_started(self, app):
# Check OpenRC service status without calling any binary
return os.path.exists(os.path.join('/run/openrc/started', app))
def enable_autostart(self, app):
# Add the app to OpenRC default runlevel
subprocess.call(['/sbin/rc-update', 'add', app])
def disable_autostart(self, app):
# Remove the app from OpenRC default runlevel
subprocess.call(['/sbin/rc-update', 'del', app])
def register_proxy(self, app):
# Rebuild nginx configuration using IP of referenced app container and reload nginx
self.update_proxy_conf(app, self.get_container_ip(app))
subprocess.call(['/sbin/service', 'nginx', 'reload'])
def update_proxy_conf(self, app, ip):
with open(os.path.join(NGINX_DIR, '{}.conf'.format(app)), 'w') as f:
f.write(NGINX_TEMPLATE.format(app=app, host=self.conf['apps'][app]['host'], ip=ip, domain=self.domain, port=self.port))
def unregister_proxy(self, app):
# Remove nginx configuration to prevent proxy mismatch when the container IP is reassigned to another container
self.update_proxy_conf(app, DISCARD_IP)
subprocess.call(['/sbin/service', 'nginx', 'reload'])
def get_container_ip(self, app):
# Return an IP address of a container. If the container is not running, return address from IPv6 discard prefix instead
try:
return subprocess.check_output(['/usr/bin/docker', 'inspect', '-f', '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}', app]).strip()
except:
return DISCARD_IP
def update_domain(self, domain, port):
self.domain = self.conf['host']['domain'] = domain
self.port = self.conf['host']['port'] = port
self.save_conf()
self.rebuild_nginx()
self.rebuild_issue()
self.restart_apps()
def rebuild_nginx(self):
# Rebuild nginx config for the portal app
with open(os.path.join(NGINX_DIR, 'default.conf'), 'w') as f:
f.write(NGINX_DEFAULT_TEMPLATE.format(port=self.port))
# Unregister nginx proxy for apps (will be repopulated on app restart)
for app in self.conf['apps']:
self.update_proxy_conf(app, DISCARD_IP)
# Restart nginx to properly bind the new listen port
subprocess.call(['/sbin/service', 'nginx', 'restart'])
def rebuild_issue(self):
# Compile the HTTPS host displayed in terminal banner
host = self.domain
# If the dummy host is used, take an IP address of a primary interface instead
if self.domain == 'spotter.vm':
host = subprocess.check_output(['/sbin/ip', 'route', 'get', '1']).split()[-1]
# Show port number only when using the non-default HTTPS port
if self.port != '443':
host += ':{}'.format(self.port)
# Rebuild the terminal banner
with open(ISSUE_FILE, 'w') as f:
f.write(ISSUE_TEMPLATE.format(host=host))
def restart_apps(self):
for app in self.conf['apps']:
# Check if a script for internal update of URL in the app exists and is executable and run it
script_path = os.path.join('/srv', app, 'update-url.sh')
if os.path.exists(script_path) and os.access(script_path, os.X_OK):
subprocess.call([script_path, '{}.{}'.format(self.conf['apps'][app]['host'], self.domain), self.port])
# If the app is currently running, restart the app service
if self.is_app_started(app):
subprocess.call(['/sbin/service', app, 'restart'])
def request_cert(self, email):
# Compile an acme.sh command for certificate requisition
cmd = ['/usr/bin/acme.sh', '--issue', '-d', self.domain]
for app in self.conf['apps']:
cmd += ['-d', '{}.{}'.format(self.conf['apps'][app]['host'], self.domain)]
cmd += ['-w', '/etc/acme.sh.d', '--accountemail', email]
# Request the certificate. If the requisition command fails, CalledProcessError will be raised
subprocess.check_output(cmd, stderr=subprocess.STDOUT)
# Install the issued certificate
subprocess.call(['/usr/bin/acme.sh', '--installcert', '-d', self.domain, '--keypath', '/etc/ssl/private/services.key', '--fullchainpath', '/etc/ssl/certs/services.pem', '--reloadcmd', 'service nginx reload'])
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='Spotter VM application manager')
subparsers = parser.add_subparsers()
parser_update_login = subparsers.add_parser('update-login', help='Updates application login')
parser_update_login.set_defaults(action='update-login')
parser_update_login.add_argument('app', help='Application name')
parser_update_login.add_argument('login', help='Administrative login')
parser_update_login.add_argument('password', help='Administrative password')
parser_show_tiles = subparsers.add_parser('show-tiles', help='Shows application tiles in Portal')
parser_show_tiles.set_defaults(action='show-tiles')
parser_show_tiles.add_argument('app', help='Application name')
parser_hide_tiles = subparsers.add_parser('hide-tiles', help='Hides application tiles in Portal')
parser_hide_tiles.set_defaults(action='hide-tiles')
parser_hide_tiles.add_argument('app', help='Application name')
parser_start_app = subparsers.add_parser('start-app', help='Start application including it\'s dependencies')
parser_start_app.set_defaults(action='start-app')
parser_start_app.add_argument('app', help='Application name')
parser_stop_app = subparsers.add_parser('stop-app', help='Stops application including it\'s dependencies if they are not used by another running application')
parser_stop_app.set_defaults(action='stop-app')
parser_stop_app.add_argument('app', help='Application name')
parser_enable_autostart = subparsers.add_parser('enable-autostart', help='Enables application autostart')
parser_enable_autostart.set_defaults(action='enable-autostart')
parser_enable_autostart.add_argument('app', help='Application name')
parser_disable_autostart = subparsers.add_parser('disable-autostart', help='Disables application autostart')
parser_disable_autostart.set_defaults(action='disable-autostart')
parser_disable_autostart.add_argument('app', help='Application name')
parser_register_proxy = subparsers.add_parser('register-proxy', help='Rebuilds nginx proxy target for an application container')
parser_register_proxy.set_defaults(action='register-proxy')
parser_register_proxy.add_argument('app', help='Application name')
parser_unregister_proxy = subparsers.add_parser('unregister-proxy', help='Removes nginx proxy target for an application container')
parser_unregister_proxy.set_defaults(action='unregister-proxy')
parser_unregister_proxy.add_argument('app', help='Application name')
parser_update_domain = subparsers.add_parser('update-domain', help='Rebuilds domain structure of VM with new domain name and new HTTPS port')
parser_update_domain.set_defaults(action='update-domain')
parser_update_domain.add_argument('domain', help='Domain name')
parser_update_domain.add_argument('port', help='HTTPS port')
parser_request_cert = subparsers.add_parser('request-cert', help='Requests and installs Let\'s Encrypt certificate for currently set domain')
parser_request_cert.set_defaults(action='request-cert')
parser_request_cert.add_argument('email', help='Email address to receive certificate notifications')
args = parser.parse_args()
sm = SpotterManager()
if args.action == 'update-login':
sm.update_login(args.app, args.login, args.password)
elif args.action == 'show-tiles':
sm.show_tiles(args.app)
elif args.action == 'hide-tiles':
sm.hide_tiles(args.app)
elif args.action == 'start-app':
sm.start_app(args.app)
elif args.action == 'stop-app':
sm.stop_app(args.app)
elif args.action == 'enable-autostart':
sm.enable_autostart(args.app)
elif args.action == 'disable-autostart':
sm.disable_autostart(args.app)
elif args.action == 'register-proxy':
sm.register_proxy(args.app)
elif args.action == 'unregister-proxy':
sm.unregister_proxy(args.app)
elif args.action == 'update-domain':
sm.update_domain(args.domain, args.port)
elif args.action == 'request-cert':
sm.request_cert(args.email)

View File

@ -15,6 +15,6 @@ SQLALCHEMY_DATABASE_URI = 'sqlite:////srv/ckan-datapusher/data/jobs.db'
HOST = '0.0.0.0'
PORT = 8080
FROM_EMAIL = 'ckan@spotter.ngo'
FROM_EMAIL = 'admin@example.com'
STDERR = True

View File

@ -37,7 +37,6 @@ export CKAN_SECRET=$(head -c 18 /dev/urandom | base64)
export CKAN_UUID=$(cat /proc/sys/kernel/random/uuid)
envsubst <${SOURCE_DIR}/srv/ckan/conf/ckan.ini >/srv/ckan/conf/ckan.ini
cp ${SOURCE_DIR}/srv/ckan/conf/who.ini /srv/ckan/conf/who.ini
cp ${SOURCE_DIR}/srv/ckan/update-url.sh /srv/ckan/update-url.sh
# Set "production values" (increases performance) only if the DEBUG environment variable is not set
if [ ${DEBUG:-0} -eq 0 ]; then

View File

@ -116,7 +116,7 @@ ckan.views.default_views = image_view text_view recline_view geo_view geojson_vi
# GeoView plugin settings
ckanext.geoview.ol_viewer.formats = wms wfs geojson gml kml arcgis_rest gft
ckanext.geoview.gapi_key = AIzaSyBvIF3D550tlpL6o1xRrDurGo-81VhHlOw
ckanext.geoview.gapi_key =
# Pages plugin settings
ckanext.pages.organization = true
@ -203,7 +203,7 @@ smtp.server = postfix
smtp.starttls = False
#smtp.user = username@example.com
#smtp.password = your_password
smtp.mail_from = ckan@spotter.ngo
smtp.mail_from = admin@example.com
## Logging configuration

View File

@ -1,6 +0,0 @@
#!/bin/sh
HOST="${1}"
[ "${2}" != "443" ] && HOST="${1}:${2}"
sed -i "s|^ckan\.site_url.*|ckan.site_url = https://${HOST}|" /srv/ckan/conf/ckan.ini

View File

@ -1,5 +1,5 @@
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
ENV['GOOGLE_MAPS_API_KEY'] = 'AIzaSyBvIF3D550tlpL6o1xRrDurGo-81VhHlOw'
ENV['GOOGLE_MAPS_API_KEY'] = ''
require 'bundler/setup' # Set up gems listed in the Gemfile.

View File

@ -10,7 +10,7 @@ Devise.setup do |config|
# Configure the e-mail address which will be shown in Devise::Mailer,
# note that it will be overwritten if you use your own mailer class
# with default "from" parameter.
config.mailer_sender = 'crisiscleanup@spotter.ngo'
config.mailer_sender = 'admin@example.com'
# Configure the class responsible to send e-mails.
# config.mailer = 'Devise::Mailer'

View File

@ -35,7 +35,7 @@ SENDFILE_ROOT = os.path.join(PUBLIC_ROOT, 'static/protected')
COMPRESS_ENABLED = False
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
SERVER_EMAIL = 'cts@spotter.ngo'
SERVER_EMAIL = 'admin@example.com'
CELERY_ALWAYS_EAGER = True
CELERY_EAGER_PROPAGATES_EXCEPTIONS = True

View File

@ -11,5 +11,5 @@ listen = *:8080
ssl_webdav = False
[email]
from = gnuhealth@spotter.ngo
from = admin@example.com
uri = smtp://postfix:25

View File

@ -38,7 +38,7 @@ define('FILES_DIR', DATA_DIR.DIRECTORY_SEPARATOR.'files');
define('MAIL_CONFIGURATION', true);
// E-mail address used for the "From" header (notifications)
define('MAIL_FROM', 'kanboard@spotter.ngo');
define('MAIL_FROM', 'admin@example.com');
// Mail transport available: "smtp", "sendmail", "mail" (PHP mail function), "postmark", "mailgun", "sendgrid"
define('MAIL_TRANSPORT', 'smtp');

View File

@ -25,7 +25,6 @@ envsubst <${SOURCE_DIR}/schemapwd.sql | docker exec -i mariadb mysql mifosplatfo
mkdir -p /srv/mifosx/conf
envsubst <${SOURCE_DIR}/srv/mifosx/conf/context.xml >/srv/mifosx/conf/context.xml
cp ${SOURCE_DIR}/srv/mifosx/conf/server.xml /srv/mifosx/conf/server.xml
cp ${SOURCE_DIR}/srv/mifosx/update-url.sh /srv/mifosx/update-url.sh
# Populate database
service mifosx start

View File

@ -1,6 +1,6 @@
UPDATE `m_appuser` SET `username` = "${MIFOSX_ADMIN_USER}", `password` = "${MIFOSX_ADMIN_HASH}", `email` = "${MIFOSX_ADMIN_EMAIL}" WHERE `id` = 1;
UPDATE `c_external_service_properties` SET `value` = "mifosx@spotter.ngo" WHERE `external_service_id` = 2 and `name` LIKE "username";
UPDATE `c_external_service_properties` SET `value` = "admin@example.com" WHERE `external_service_id` = 2 and `name` LIKE "username";
UPDATE `c_external_service_properties` SET `value` = "" WHERE `external_service_id` = 2 and `name` LIKE "password";
UPDATE `c_external_service_properties` SET `value` = "postfix" WHERE `external_service_id` = 2 and `name` LIKE "host";
UPDATE `c_external_service_properties` SET `value` = "false" WHERE `external_service_id` = 2 and `name` LIKE "useTLS";

View File

@ -25,7 +25,6 @@ cp ${SOURCE_DIR}/srv/motech/conf/config-locations.properties /srv/motech/conf/co
cp ${SOURCE_DIR}/srv/motech/conf/config/motech-settings.properties /srv/motech/conf/config/motech-settings.properties
cp ${SOURCE_DIR}/srv/motech/conf/config/org.motechproject.motech-platform-email/motech-email.properties /srv/motech/conf/config/org.motechproject.motech-platform-email/motech-email.properties
chown -R 8013:8013 /srv/motech/conf
cp ${SOURCE_DIR}/srv/motech/update-url.sh /srv/motech/update-url.sh
# Populate database and create admin account
service motech start

View File

@ -1,6 +0,0 @@
#!/bin/sh
HOST="${1}"
[ "${2}" != "443" ] && HOST="${1}:${2}"
sed -i "s|^server\.url.*|server.url=https://${HOST}|" /srv/motech/conf/config/motech-settings.properties

View File

@ -24,7 +24,6 @@ mkdir -p /srv/opendatakit/conf
envsubst <${SOURCE_DIR}/srv/opendatakit/conf/jdbc.properties >/srv/opendatakit/conf/jdbc.properties
envsubst <${SOURCE_DIR}/srv/opendatakit/conf/security.properties >/srv/opendatakit/conf/security.properties
cp ${SOURCE_DIR}/srv/opendatakit/conf/server.xml /srv/opendatakit/conf/server.xml
cp ${SOURCE_DIR}/srv/opendatakit/update-url.sh /srv/opendatakit/update-url.sh
chown -R 8015:8015 /srv/opendatakit/conf
# Populate database

View File

@ -37,7 +37,6 @@ else
fi
cp ${SOURCE_DIR}/srv/pandora/conf/gunicorn_config.py /srv/pandora/conf/gunicorn_config.py
envsubst <${SOURCE_DIR}/srv/pandora/conf/local_settings.py >/srv/pandora/conf/local_settings.py
cp ${SOURCE_DIR}/srv/pandora/update-url.sh /srv/pandora/update-url.sh
# Set "production values" (increases performance) only if the DEBUG environment variable is not set
if [ ${DEBUG:-0} -eq 0 ]; then

View File

@ -15,7 +15,7 @@ EMAIL_HOST = 'postfix'
XACCELREDIRECT = True
GOOGLE_API_KEY = 'AIzaSyBvIF3D550tlpL6o1xRrDurGo-81VhHlOw'
GOOGLE_API_KEY = ''
DEBUG = True
JSON_DEBUG = DEBUG

View File

@ -1,6 +0,0 @@
#!/bin/sh
HOST="${1}"
[ "${2}" != "443" ] && HOST="${1}:${2}"
sed -i "s|^ \"url\":.*| \"url\": \"pandora.${HOST}\"|" /srv/pandora/conf/config.jsonc

View File

@ -30,7 +30,6 @@ export SAHANADEMO_ADMIN_USER=admin@example.com
export SAHANADEMO_ADMIN_PWD=$(head -c 12 /dev/urandom | base64)
envsubst <${SOURCE_DIR}/srv/sahana-demo/conf/000_config.py >/srv/sahana-demo/conf/000_config.py
envsubst <${SOURCE_DIR}/masterUsers.csv >/tmp/masterUsers.csv
cp ${SOURCE_DIR}/srv/sahana-demo/update-url.sh /srv/sahana-demo/update-url.sh
spotter-appmgr update-login sahana-demo "${SAHANADEMO_ADMIN_USER}" "${SAHANADEMO_ADMIN_PWD}"
# Populate database

View File

@ -100,7 +100,7 @@ settings.mail.server = "postfix:25"
#settings.mail.tls = True
#settings.mail.login = "username:password"
# From Address - until this is set, no mails can be sent
settings.mail.sender = "'Sahana' <sahanademo@spotter.ngo>"
settings.mail.sender = "admin@example.com"
# Default email address to which requests to approve new user accounts gets sent
# This can be overridden for specific domains/organisations via the auth_domain table
#settings.mail.approver = "useradmin@example.org"
@ -134,7 +134,7 @@ settings.frontpage.rss = [
# http://www.microsoft.com/maps/create-a-bing-maps-key.aspx
#settings.gis.api_bing = ""
# Google API Key (for Google Maps Layers)
settings.gis.api_google = "AIzaSyBvIF3D550tlpL6o1xRrDurGo-81VhHlOw"
settings.gis.api_google = ""
# Yahoo API Key (for Geocoder)
#settings.gis.api_yahoo = ""

View File

@ -1,6 +0,0 @@
#!/bin/sh
HOST="${1}"
[ "${2}" != "443" ] && HOST="${1}:${2}"
sed -i "s|^settings\.base\.public_url.*|settings.base.public_url = \"https://${HOST}\"|" /srv/sahana-demo/conf/000_config.py

View File

@ -33,7 +33,6 @@ export SAHANA_ADMIN_PWD=$(head -c 12 /dev/urandom | base64)
envsubst <${SOURCE_DIR}/srv/sahana/conf/000_config.py >/srv/sahana/conf/000_config.py
envsubst <${SOURCE_DIR}/srv/sahana/data/Spotter/masterUsers.csv >/srv/sahana/data/Spotter/masterUsers.csv
cp ${SOURCE_DIR}/srv/sahana/conf/00_settings.py /srv/sahana/conf/00_settings.py
cp ${SOURCE_DIR}/srv/sahana/update-url.sh /srv/sahana/update-url.sh
spotter-appmgr update-login sahana "${SAHANA_ADMIN_USER}" "${SAHANA_ADMIN_PWD}"
# Populate database

View File

@ -100,10 +100,10 @@ settings.mail.server = "postfix:25"
#settings.mail.tls = True
#settings.mail.login = "username:password"
# From Address - until this is set, no mails can be sent
settings.mail.sender = "'Sahana' <sahana@spotter.ngo>"
settings.mail.sender = "admin@example.com"
# Default email address to which requests to approve new user accounts gets sent
# This can be overridden for specific domains/organisations via the auth_domain table
settings.mail.approver = "info@spotter.ngo"
settings.mail.approver = "admin@example.com"
# Daily Limit on Sending of emails
#settings.mail.limit = 1000
@ -134,7 +134,7 @@ settings.frontpage.rss = [
# http://www.microsoft.com/maps/create-a-bing-maps-key.aspx
#settings.gis.api_bing = ""
# Google API Key (for Google Maps Layers)
settings.gis.api_google = "AIzaSyBvIF3D550tlpL6o1xRrDurGo-81VhHlOw"
settings.gis.api_google = ""
# Yahoo API Key (for Geocoder)
#settings.gis.api_yahoo = ""

View File

@ -1,6 +0,0 @@
#!/bin/sh
HOST="${1}"
[ "${2}" != "443" ] && HOST="${1}:${2}"
sed -i "s|^settings\.base\.public_url.*|settings.base.public_url = \"https://${HOST}\"|" /srv/sahana/conf/000_config.py

View File

@ -31,7 +31,6 @@ envsubst <${SOURCE_DIR}/srv/sambro/conf/000_config.py >/srv/sambro/conf/000_conf
envsubst <${SOURCE_DIR}/masterUsers.csv >/tmp/masterUsers.csv
cp ${SOURCE_DIR}/srv/sambro/conf/00_settings.py /srv/sambro/conf/00_settings.py
cp ${SOURCE_DIR}/srv/sambro/data/SAMBRO/config.py /srv/sambro/data/SAMBRO/config.py
cp ${SOURCE_DIR}/srv/sambro/update-url.sh /srv/sambro/update-url.sh
spotter-appmgr update-login sambro "${SAMBRO_ADMIN_USER}" "${SAMBRO_ADMIN_PWD}"
# Populate database

View File

@ -100,10 +100,10 @@ settings.mail.server = "postfix:25"
#settings.mail.tls = True
#settings.mail.login = "username:password"
# From Address - until this is set, no mails can be sent
settings.mail.sender = "'SAMBRO' <sambro@spotter.ngo>"
settings.mail.sender = "admin@example.com"
# Default email address to which requests to approve new user accounts gets sent
# This can be overridden for specific domains/organisations via the auth_domain table
settings.mail.approver = "info@spotter.ngo"
settings.mail.approver = "admin@example.com"
# Daily Limit on Sending of emails
#settings.mail.limit = 1000
@ -134,7 +134,7 @@ settings.frontpage.rss = [
# http://www.microsoft.com/maps/create-a-bing-maps-key.aspx
#settings.gis.api_bing = ""
# Google API Key (for Google Maps Layers)
settings.gis.api_google = "AIzaSyBvIF3D550tlpL6o1xRrDurGo-81VhHlOw"
settings.gis.api_google = ""
# Yahoo API Key (for Geocoder)
#settings.gis.api_yahoo = ""

View File

@ -1,6 +0,0 @@
#!/bin/sh
HOST="${1}"
[ "${2}" != "443" ] && HOST="${1}:${2}"
sed -i "s|^settings\.base\.public_url.*|settings.base.public_url = \"https://${HOST}\"|" /srv/sambro/conf/000_config.py

View File

@ -14,7 +14,7 @@
</connectors>
</authentication>
<database dbDriver="pgsql" dbHostname="postgres" dbDatabase="seeddms" dbUser="seeddms" dbPass="${SEEDDMS_PWD}" doNotCheckVersion="false"/>
<smtp smtpServer="postfix" smtpPort="25" smtpSendFrom="seeddms@spotter.ngo" smtpUser="" smtpPassword=""/>
<smtp smtpServer="postfix" smtpPort="25" smtpSendFrom="admin@example.com" smtpUser="" smtpPassword=""/>
</system>
<advanced>
<display siteDefaultPage="" rootFolderID="1" titleDisplayHack="true" showMissingTranslations="false"/>

View File

@ -24,7 +24,7 @@ files.upload.maxSize=20971520
mail.hostname=postfix
mail.port=25
mail.from.address=sigmah@spotter.ngo
mail.from.address=admin@example.com
mail.from.name=Sigmah
# Authentication (leave empty if no authentication is required).
mail.auth.username=
@ -32,9 +32,9 @@ mail.auth.password=
mail.encoding=UTF-8
mail.contentType=text/html
mail.support.to=sigmah@spotter.ngo
mail.support.to=admin@example.com
# --
# MAPS API
# --
maps.key=AIzaSyBvIF3D550tlpL6o1xRrDurGo-81VhHlOw
maps.key=

View File

@ -22,7 +22,6 @@ mkdir -p /srv/ushahidi/conf /srv/ushahidi/data
chown 8014:8014 /srv/ushahidi/data
envsubst <${SOURCE_DIR}/srv/ushahidi/conf/env >/srv/ushahidi/conf/env
cp ${SOURCE_DIR}/srv/ushahidi/conf/config.json /srv/ushahidi/conf/config.json
cp ${SOURCE_DIR}/srv/ushahidi/update-url.sh /srv/ushahidi/update-url.sh
# Populate database
docker run --rm -h ushahidi --link mariadb -v /srv/ushahidi/conf/env:/srv/ushahidi/platform/.env ushahidi /srv/ushahidi/platform/bin/phinx migrate -c /srv/ushahidi/platform/application/phinx.php

View File

@ -2,5 +2,5 @@
"backend_url": "https://ush.spotter.vm/platform",
"client_id": "ushahidiui",
"client_secret": "35e7f0bca957836d05ca0492211b0ac707671261",
"google_analytics_id": "AIzaSyBvIF3D550tlpL6o1xRrDurGo-81VhHlOw"
"google_analytics_id": ""
}

View File

@ -1,16 +0,0 @@
#!/bin/sh
HOST="${1}"
[ "${2}" != "443" ] && HOST="${1}:${2}"
sed -i "s|^ \"backend_url\".*| \"backend_url\": \"https://${HOST}/platform\",|" /srv/ushahidi/conf/config.json
if [ ! -e /run/openrc/started/mariadb ]; then
service mariadb start
STOP_MARIADB=1
fi
API_URL='\\\"https:\\\\/\\\\/'${HOST}'\\\\/platform\\\\/api\\\\/v3\\\\/config\\\\/data-provider\\\"'
echo 'UPDATE `config` SET `config_value` = "'${API_URL}'" WHERE `group_name` LIKE "data-provider" AND `config_key` LIKE "url";' | docker exec -i mariadb mysql ushahidi
if [ ${STOP_MARIADB} ]; then
service mariadb stop
fi