Compare commits

..

No commits in common. "master" and "last-docker" have entirely different histories.

779 changed files with 11582 additions and 94169 deletions

View File

@ -0,0 +1,13 @@
### Steps to reproduce
1.
2.
3.
### Expected behaviour
### Observed behaviour
### Additional data (ticket URL, log, timestamp, stack trace etc.)

6
.gitmodules vendored
View File

@ -1,6 +0,0 @@
[submodule "app-vmmgr"]
path = apk/vmmgr
url = ssh://git@git.spotter.cz:2222/Spotter-Cluster/vmmgr.git
[submodule "spoc"]
path = apk/spoc
url = ssh://git@git.spotter.cz:2222/Spotter-Cluster/spoc.git

40
00-install.sh Executable file
View File

@ -0,0 +1,40 @@
#!/bin/sh
set -e
# For production builds change to DEBUG=0 or comment the line entirely
export DEBUG=1
SOURCE_DIR=$(realpath $(dirname "${0}"))
# Install shared packages and perform OS customization
${SOURCE_DIR}/basic.sh
${SOURCE_DIR}/basic-runtimes.sh
# Install applications
${SOURCE_DIR}/ckan.sh
# ${SOURCE_DIR}/crisiscleanup.sh
# ${SOURCE_DIR}/cts.sh
${SOURCE_DIR}/frontlinesms.sh
${SOURCE_DIR}/gnuhealth.sh
${SOURCE_DIR}/kanboard.sh
${SOURCE_DIR}/mifosx.sh
${SOURCE_DIR}/motech.sh
${SOURCE_DIR}/opendatakit-build.sh
${SOURCE_DIR}/opendatakit.sh
${SOURCE_DIR}/openmapkit.sh
${SOURCE_DIR}/pandora.sh
${SOURCE_DIR}/sahana.sh
${SOURCE_DIR}/sahana-demo.sh
${SOURCE_DIR}/sambro.sh
${SOURCE_DIR}/seeddms.sh
${SOURCE_DIR}/sigmah.sh
${SOURCE_DIR}/ushahidi.sh
# Perform cleanup only if DEBUG mode is not set
[ ${DEBUG:-0} -eq 1 ] && exit 0
# Clean package cache
rm -rf /var/cache/apk/*
# Remove root settings
find /root -mindepth 1 -maxdepth 1 | xargs rm -rf
# Change root password
passwd

90
README.md Normal file
View File

@ -0,0 +1,90 @@
This is the main repository containing all installation scripts, configuration and customization of operating system all application present in Spotter Cluster virtual machine.
## Virtual machine specifications
- **Memory:** 4 GB
- **CPU:** 1 processor, 2 cores
- **Hard Disk:** SCSI, 60 GB
In case you're setting up a VMWare virtual machine, select OS type *Other Linux 3.x kernel 64-bit* and after you create the VM, manually edit the `*.vmx` file using a text editor and set there `mem.hotadd = "FALSE"`. Failing to do so will result in system unable to boot. Other hypervizors don't need this adjustment.
## Operating system installation
Download **Alpine Virtual 3.8.0 x86_64** from <https://alpinelinux.org/downloads/> and boot from it. At the login prompt, use the root user without password to log in.
```
# Set up interfaces (leave the default choices)
setup-interfaces
ifup eth0
# Download and launch the setup script
wget dl.dasm.cz/alpine.sh
sh alpine.sh
```
The script will perform the bare Alpine linux installation into VM using LUKS-on-LVM. The default disk encryption password is `password`. No root password is set.
## Application installation
### First time setup
```
# Install git
apk --no-cache add git
# Clone the repository
git clone https://gitlab.dasm.cz/Spotter-Cluster/Spotter-Cluster.git
# Enter the repository directory
cd Spotter-Cluster
# Optional: Edit the install sequence script
vi 00-install.sh
# Launch the script
./00-install.sh
```
### Resuming from a snapshot
Assumes that the repository has already been cloned.
```
# Enter the repository directory
cd Spotter-Cluster
# Update repository
git pull
# Optional: Edit the install sequence script
vi 00-install.sh
# Launch the script
./00-install.sh
```
## Host assignment
| Application | Container | UID/GID | Internal Port | Host |
|-----------------|------------------:|--------:|-----------------:|----------|
| ActiveMQ | activemq | 61616 | 61616 (ActiveMQ) | - |
| CKAN | ckan | 8003 | 8080 (HTTP) | ckan |
| CKAN Datapusher | ckan-datapusher | 8004 | 8080 (HTTP) | - |
| Crisis Cleanup | crisiscleanup | 8005 | 8080 (HTTP) | cc |
| CTS | cts | 8006 | 8080 (HTTP) | cts |
| FrontlineSMS | frontlinesms | 8018 | 8080 (HTTP) | sms |
| GNU Health | gnuhealth | 8008 | 8080 (HTTP) | gh |
| KanBoard | kanboard | 8009 | 8080 (HTTP) | kb |
| MariaDB | mariadb | 3306 | 3306 (MySQL) | - |
| Mifos X | mifosx | 8012 | 8080 (HTTP) | mifosx |
| Motech | motech | 8013 | 8080 (HTTP) | motech |
| ODK Aggregate | opendatakit | 8015 | 8080 (HTTP) | odk |
| ODK Build | opendatakit-build | 8017 | 8080 (HTTP) | odkbuild |
| OpenMapKit | openmapkit | 8007 | 8080 (HTTP) | omk |
| Pan.do/ra | pandora | 8002 | 8080 (HTTP) | pandora |
| Postfix | postfix | 587 | 25 (SMTP) | - |
| Postgres | postgres | 5432 | 5432 (Postgres) | - |
| RabbitMQ | rabbitmq | 5672 | 5672 (AMQP) | - |
| Redis | redis | 6379 | 6379 (Redis) | - |
| Sahana | sahana | 8001 | 8080 (HTTP) | sahana |
| SAMBRO | sambro | 8001 | 8080 (HTTP) | sambro |
| SeedDMS | seeddms | 8010 | 8080 (HTTP) | dms |
| Sigmah | sigmah | 8011 | 8080 (HTTP) | sigmah |
| Solr | solr | 8983 | 8983 (HTTP) | - |
| Ushahidi | ushahidi | 8014 | 8080 (HTTP) | ush |

13
activemq.sh Executable file
View File

@ -0,0 +1,13 @@
#!/bin/sh
set -e
SOURCE_DIR=$(realpath $(dirname "${0}"))/activemq
# Build Docker container
docker build -t activemq ${SOURCE_DIR}
cp ${SOURCE_DIR}/etc/init.d/activemq /etc/init.d/activemq
rc-update -u
# Configure ActiveMQ
mkdir -p /srv/activemq/data
chown -R 61616:61616 /srv/activemq/data

23
activemq/Dockerfile Normal file
View File

@ -0,0 +1,23 @@
FROM java
LABEL maintainer="Disassembler <disassembler@dasm.cz>"
RUN \
# Download and install ActiveMQ
wget http://archive.apache.org/dist/activemq/5.15.5/apache-activemq-5.15.5-bin.tar.gz -O /tmp/activemq.tgz \
&& tar xf /tmp/activemq.tgz -C /srv \
&& mv /srv/apache-activemq-5.15.5 /srv/activemq \
&& rm -f /tmp/activemq.tgz \
# Create OS user
&& addgroup -S -g 61616 activemq \
&& adduser -S -u 61616 -h /srv/activemq -s /bin/false -g activemq -G activemq activemq \
&& mkdir /srv/activemq/tmp \
&& chown activemq:activemq /srv/activemq/tmp \
# Configure Java heap size
&& sed -i "s/-Xms64M -Xmx1G/-Xms32M -Xmx256M/" /srv/activemq/bin/env
COPY docker/ /
VOLUME ["/srv/activemq/data"]
EXPOSE 61616
CMD ["s6-svscan", "/etc/services.d"]

View File

@ -0,0 +1,5 @@
#!/bin/execlineb -P
fdmove -c 2 1
s6-setuidgid 61616:61616
/srv/activemq/bin/activemq console

19
activemq/etc/init.d/activemq Executable file
View File

@ -0,0 +1,19 @@
#!/sbin/openrc-run
description="ActiveMQ docker container"
depend() {
need docker
}
start() {
/usr/bin/docker run -d --rm \
--name activemq \
-h activemq \
-v /srv/activemq/data:/srv/activemq/data \
activemq
}
stop() {
/usr/bin/docker stop activemq
}

100
alpine.sh Executable file
View File

@ -0,0 +1,100 @@
#!/bin/sh
# Based on
# https://wiki.alpinelinux.org/wiki/LVM_on_LUKS
# Prerequisites for this script
# setup-interfaces
# ifup eth0
# Set up repositories
cat <<EOF >/etc/apk/repositories
http://dl-cdn.alpinelinux.org/alpine/v3.8/main
http://dl-cdn.alpinelinux.org/alpine/v3.8/community
#http://dl-cdn.alpinelinux.org/alpine/edge/main
#http://dl-cdn.alpinelinux.org/alpine/edge/community
#http://dl-cdn.alpinelinux.org/alpine/edge/testing
EOF
# Install disk management tools
apk --no-cache add lvm2 cryptsetup e2fsprogs syslinux
# Create disk partitions
cat <<EOF | fdisk /dev/sda
n
p
1
+100m
a
1
n
p
2
t
2
8e
w
EOF
# Set up partition encryption
echo -n 'password' | cryptsetup -q luksFormat /dev/sda2
echo -n 'password' | cryptsetup open --type luks /dev/sda2 system
# Set up LVM
pvcreate /dev/mapper/system
vgcreate vg0 /dev/mapper/system
lvcreate -l 100%FREE vg0 -n root
# Format
mkfs.ext4 -m0 /dev/sda1
mkfs.ext4 -m1 /dev/vg0/root
# Mount
mount -t ext4 /dev/vg0/root /mnt
mkdir /mnt/boot
mount -t ext4 /dev/sda1 /mnt/boot
# Install Alpine linux
setup-disk -m sys /mnt
# Update boot-time volume information
BOOT_UUID=$(blkid /dev/sda1 | cut -d' ' -f2 | tr -d '"')
cat <<EOF >/mnt/etc/fstab
/dev/vg0/root / ext4 rw,noatime,data=ordered 0 1
${BOOT_UUID} /boot ext4 rw,noatime,data=ordered 0 2
/dev/vg0/swap swap swap defaults 0 0
EOF
echo "system /dev/sda2 none luks" >/mnt/etc/crypttab
# Rebuild initfs
sed -i 's/lvm/lvm cryptsetup/' /mnt/etc/mkinitfs/mkinitfs.conf
mkinitfs -c /mnt/etc/mkinitfs/mkinitfs.conf -b /mnt $(ls /mnt/lib/modules)
# Update extlinux (ignore the errors)
sed -i 's/rootfstype=ext4/rootfstype=ext4 cryptroot=\/dev\/sda2 cryptdm=system/' /mnt/etc/update-extlinux.conf
chroot /mnt update-extlinux
# Set time zone
chroot /mnt setup-timezone -z Europe/Prague
# Set hostname
echo 'spotter.vm' >/mnt/etc/hostname
echo '127.0.0.1 spotter.vm localhost localhost.localdomain' >/mnt/etc/hosts
sed -i '/hostname/d' /mnt/etc/network/interfaces
# Enable services on boot
ln -s /etc/init.d/networking /mnt/etc/runlevels/boot
ln -s /etc/init.d/urandom /mnt/etc/runlevels/boot
# Install bootloader to MBR
dd bs=440 count=1 conv=notrunc if=/mnt/usr/share/syslinux/mbr.bin of=/dev/sda
# Unmount and shut down
umount /mnt/boot
umount /mnt
vgchange -a n
cryptsetup luksClose system
poweroff

View File

@ -1,28 +0,0 @@
# Maintainer: Disassembler <disassembler@dasm.cz>
pkgname=py3-secure-cookie
_pkgname=secure-cookie
pkgver=0.1.0
pkgrel=0
pkgdesc="Secure cookie and session interface for WSGI applications"
url="https://secure-cookie.readthedocs.io/"
arch="noarch"
license="MIT"
depends="python3"
makedepends="py3-setuptools"
checkdepends="py3-pytest py3-werkzeug"
source="https://files.pythonhosted.org/packages/source/${_pkgname:0:1}/$_pkgname/$_pkgname-$pkgver.tar.gz"
builddir="$srcdir/$_pkgname-$pkgver"
build() {
python3 setup.py build
}
package() {
python3 setup.py install --prefix=/usr --root="$pkgdir"
}
check() {
PYTHONPATH=$PWD/build/lib pytest
}
sha512sums="2e57dba6f73ceb03eda33c804dbe2277c9fe700dd1be219bb3d8d43a5c9105c2323fb6b28d74d3a1dfc8fbbd938b91ab54d3e1bac1dc74490335b1d27e43b55a secure-cookie-0.1.0.tar.gz"

@ -1 +0,0 @@
Subproject commit 8c22df2e71de329a286e75af9bff69e82876db35

@ -1 +0,0 @@
Subproject commit 1c810db9472f50bd9dbe1e0f38df72590b120124

12
basic-runtimes.sh Executable file
View File

@ -0,0 +1,12 @@
#!/bin/sh
set -e
SOURCE_DIR=$(realpath $(dirname "${0}"))/basic-runtimes
# Build Docker images
docker build -t java ${SOURCE_DIR}/java
docker build -t php ${SOURCE_DIR}/php
docker build -t python2 ${SOURCE_DIR}/python2
docker build -t python3 ${SOURCE_DIR}/python3
docker build -t ruby ${SOURCE_DIR}/ruby
docker build -t tomcat ${SOURCE_DIR}/tomcat

View File

@ -0,0 +1,6 @@
FROM alpine
LABEL maintainer="Disassembler <disassembler@dasm.cz>"
RUN \
# Install Java 1.8 JRE
apk --no-cache add openjdk8-jre-base

View File

@ -0,0 +1,6 @@
FROM alpine
LABEL maintainer="Disassembler <disassembler@dasm.cz>"
RUN \
# Install PHP runtime
apk --no-cache add nginx php7 php7-ctype php7-fpm php7-gd php7-json php7-mbstring php7-mcrypt php7-opcache php7-session

View File

@ -0,0 +1,10 @@
FROM alpine
LABEL maintainer="Disassembler <disassembler@dasm.cz>"
RUN \
# Install XML libs
apk --no-cache add libxml2 libxslt
RUN \
# Install Python2 runtime
apk --no-cache add python2

View File

@ -0,0 +1,11 @@
FROM alpine
LABEL maintainer="Disassembler <disassembler@dasm.cz>"
RUN \
# Install XML libs
apk --no-cache add libxml2 libxslt
RUN \
# Install Python3 runtime
apk --no-cache add python3 \
&& ln -s /usr/bin/python3 /usr/bin/python

View File

@ -0,0 +1,33 @@
FROM alpine
LABEL maintainer="Disassembler <disassembler@dasm.cz>"
RUN \
# Install Ruby runtime dependencies
apk --no-cache add gdbm libressl readline zlib
RUN \
# Install Ruby build dependencies
apk --no-cache add --virtual .deps build-base autoconf gdbm-dev libressl-dev linux-headers readline-dev zlib-dev \
# Download and unpack Ruby
&& wget http://cache.ruby-lang.org/pub/ruby/2.3/ruby-2.3.6.tar.xz -O ruby.tar.xz \
&& mkdir -p /usr/src/ruby \
&& tar -xJf ruby.tar.xz -C /usr/src/ruby --strip-components=1 \
&& rm ruby.tar.xz \
&& cd /usr/src/ruby \
# Hackfix to suppress "Insecure world writable dir" warning
&& sed -ni 'p;13a #define ENABLE_PATH_CHECK 0' file.c \
# Configure compilation + hackfix to detect isnan/isinf macros
&& autoconf \
&& ac_cv_func_isnan=yes ac_cv_func_isinf=yes ./configure --build=x86_64-linux-musl --disable-install-doc --enable-shared \
# Compile and install Ruby
&& make -j $(nproc) \
&& make install \
# Install RubyGems and Bundler
&& mkdir -p /usr/local/etc \
&& echo -e 'install: --no-document\nupdate: --no-document' >/usr/local/etc/gemrc \
&& gem update --system \
# Cleanup
&& cd /tmp \
&& rm -r /usr/src/ruby \
&& apk --no-cache del .deps \
&& rm -rf /root/.gem

View File

@ -0,0 +1,15 @@
FROM java
LABEL maintainer="Disassembler <disassembler@dasm.cz>"
RUN \
# Install Tomcat 8
wget http://mirror.hosting90.cz/apache/tomcat/tomcat-8/v8.0.53/bin/apache-tomcat-8.0.53.tar.gz -O /tmp/apache-tomcat-8.tgz \
&& tar xf /tmp/apache-tomcat-8.tgz -C /srv \
&& mv /srv/apache-tomcat-8.0.53 /srv/tomcat \
# Make catalina.sh available globally
&& ln -s /srv/tomcat/bin/catalina.sh /usr/bin/catalina.sh \
# Cleanup
&& rm -rf /srv/tomcat/webapps/ROOT /srv/tomcat/webapps/docs /srv/tomcat/webapps/examples /srv/tomcat/webapps/host-manager /srv/tomcat/webapps/manager \
&& rm -f /tmp/apache-tomcat-8.tgz
COPY docker/ /

70
basic.sh Executable file
View File

@ -0,0 +1,70 @@
#!/bin/sh
set -e
SOURCE_DIR=$(realpath $(dirname "${0}"))/basic
# Install packages
apk --no-cache add curl docker e2fsprogs-extra gettext kbd-misc libressl python3 py3-bcrypt py3-cffi py3-dnspython py3-jinja2 py3-requests py3-six py3-werkzeug nginx util-linux
if [ ${DEBUG:-0} -eq 1 ]; then
# Install some utilities for DEBUG mode
apk --no-cache add git file htop less openssh-server openssh-sftp-server
fi
# Copy root profile files and settings for DEBUG mode
if [ ${DEBUG:-0} -eq 1 ]; then
mkdir -p /root/.config/htop /root/.ssh
cp ${SOURCE_DIR}/root/.profile /root/.profile
cp ${SOURCE_DIR}/root/.ssh/authorized_keys /root/.ssh/authorized_keys
cp ${SOURCE_DIR}/root/.config/htop/htoprc /root/.config/htop/htoprc
fi
# Copy boot configuration
cp ${SOURCE_DIR}/boot/extlinux.conf /boot/extlinux.conf
cp ${SOURCE_DIR}/boot/vm.txt /boot/vm.txt
cp ${SOURCE_DIR}/etc/inittab /etc/inittab
cp ${SOURCE_DIR}/sbin/extend-disk /sbin/extend-disk
cp ${SOURCE_DIR}/sbin/vmtty /sbin/vmtty
>/etc/motd
# Enable support for Czech characters
cp ${SOURCE_DIR}/etc/rc.conf /etc/rc.conf
cp ${SOURCE_DIR}/etc/conf.d/consolefont /etc/conf.d/consolefont
# Configure NTP client
cp ${SOURCE_DIR}/etc/conf.d/ntpd /etc/conf.d/ntpd
# Download and configure acme.sh
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
chmod +x /usr/bin/acme.sh
# Copy AppMgr resources
cp ${SOURCE_DIR}/etc/init.d/vm-appmgr /etc/init.d/vm-appmgr
rc-update -u
cp -r ${SOURCE_DIR}/srv/vm /srv/vm
ln -s /srv/vm/cli.py /usr/bin/vm-appmgr
# Create a self-signed certificate
vm-appmgr create-selfsigned
# Configure nginx
cp ${SOURCE_DIR}/etc/nginx/nginx.conf /etc/nginx/nginx.conf
# Configure services
for SERVICE in consolefont crond nginx ntpd sshd vm-appmgr swap; do
rc-update add ${SERVICE} boot
service ${SERVICE} start
done
# Configure Docker service
cp ${SOURCE_DIR}/etc/init.d/docker /etc/init.d/docker
rc-update -u
rc-update add docker
service docker start
# Create basic images
docker build -t alpine ${SOURCE_DIR}
# Set dummy host and generate related files
vm-appmgr update-host spotter.vm 443

6
basic/Dockerfile Normal file
View File

@ -0,0 +1,6 @@
FROM alpine:3.8
LABEL maintainer="Disassembler <disassembler@dasm.cz>"
RUN \
# Install S6 supervisor
apk --no-cache add s6

9
basic/boot/extlinux.conf Normal file
View File

@ -0,0 +1,9 @@
DEFAULT vm
PROMPT 0
ALLOWOPTIONS 0
NOESCAPE 1
DISPLAY vm.txt
LABEL vm
LINUX vmlinuz-virt
INITRD initramfs-virt
APPEND root=/dev/vg0/root modules=sd-mod,usb-storage,ext4 nomodeset quiet rootfstype=ext4 cryptroot=/dev/sda2 cryptdm=system

1
basic/etc/conf.d/ntpd Normal file
View File

@ -0,0 +1 @@
NTPD_OPTS="-N -p tik.cesnet.cz -p tak.cesnet.cz"

40
basic/etc/init.d/docker Executable file
View File

@ -0,0 +1,40 @@
#!/sbin/openrc-run
# Copyright 1999-2013 Gentoo Foundation
# Distributed under the terms of the GNU General Public License v2
command="${DOCKERD_BINARY:-/usr/bin/dockerd}"
pidfile="${DOCKER_PIDFILE:-/run/${RC_SVCNAME}.pid}"
command_args="-p \"${pidfile}\" ${DOCKER_OPTS}"
DOCKER_LOGFILE="${DOCKER_LOGFILE:-/var/log/${RC_SVCNAME}.log}"
DOCKER_ERRFILE="${DOCKER_ERRFILE:-${DOCKER_LOGFILE}}"
DOCKER_OUTFILE="${DOCKER_OUTFILE:-${DOCKER_LOGFILE}}"
start_stop_daemon_args="--background \
--stderr \"${DOCKER_ERRFILE}\" --stdout \"${DOCKER_OUTFILE}\""
grsecdir=/proc/sys/kernel/grsecurity
depend() {
need sysfs cgroups
}
start_pre() {
checkpath -f -m 0644 -o root:docker "$DOCKER_LOGFILE"
for i in $disable_grsec; do
if [ -e "$grsecdir/$i" ]; then
einfo " Disabling $i"
echo 0 > "$grsecdir/$i"
fi
done
ulimit -n 1048576
# Having non-zero limits causes performance problems due to accounting overhead
# in the kernel. We recommend using cgroups to do container-local accounting.
ulimit -p unlimited
return 0
}
start_post() {
ewaitfile 10 /var/run/docker.sock
}

6
basic/etc/init.d/vm-appmgr Executable file
View File

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

View File

@ -2,10 +2,12 @@
::sysinit:/sbin/openrc sysinit >/dev/null 2>&1 ::sysinit:/sbin/openrc sysinit >/dev/null 2>&1
::sysinit:/sbin/openrc boot >/dev/null 2>&1 ::sysinit:/sbin/openrc boot >/dev/null 2>&1
::wait:/sbin/extend-disk >/dev/null 2>&1
::wait:/sbin/openrc default >/dev/null 2>&1 ::wait:/sbin/openrc default >/dev/null 2>&1
# Set up getty # Set up getty
tty1::respawn:/sbin/getty -n -l /sbin/vmtty 38400 tty1 tty1::respawn:/sbin/getty -n -l /sbin/vmtty 38400 tty1
ttyS0::respawn:/sbin/getty -L 115200 ttyS0 xterm
# Stuff to do for the 3-finger salute # Stuff to do for the 3-finger salute
::ctrlaltdel:/sbin/reboot ::ctrlaltdel:/sbin/reboot

View File

@ -15,17 +15,15 @@ http {
server_tokens off; server_tokens off;
client_max_body_size 100m; client_max_body_size 100m;
sendfile on; sendfile on;
tcp_nodelay on;
gzip_vary on; gzip_vary on;
charset utf-8; charset utf-8;
ssl_protocols TLSv1.2 TLSv1.3; ssl_protocols TLSv1.2;
ssl_prefer_server_ciphers off; ssl_ciphers 'ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256';
ssl_prefer_server_ciphers on;
ssl_certificate /etc/ssl/services.pem; ssl_certificate /etc/ssl/services.pem;
ssl_certificate_key /etc/ssl/services.key; ssl_certificate_key /etc/ssl/services.key;
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:1m; ssl_session_cache shared:SSL:1m;
ssl_session_tickets off;
log_format main '$remote_addr - $remote_user [$time_local] "$request" $status $body_bytes_sent "$http_referer" "$http_user_agent" "$http_x_forwarded_for"'; log_format main '$remote_addr - $remote_user [$time_local] "$request" $status $body_bytes_sent "$http_referer" "$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main; access_log /var/log/nginx/access.log main;

View File

@ -1,3 +1,2 @@
rc_cgroup_mode=unified
rc_tty_number=1
unicode="YES" unicode="YES"
rc_tty_number=1

2
basic/root/.profile Normal file
View File

@ -0,0 +1,2 @@
alias ll="ls -la"
alias view="vi"

View File

@ -0,0 +1 @@
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILc3Mu7OlKrV7VqDQZ31vT3I3JJxtNNBiemUTRQVOZ3I Disassembler

39
basic/sbin/extend-disk Executable file
View File

@ -0,0 +1,39 @@
#!/bin/sh
set -e
# No resizing with less than 10k unused blocks
BLOCKS_FREE=$(/usr/bin/awk '/sda$/ {blocks = $3} /sda\d/ {blocks -= $3} END {print blocks}' /proc/partitions)
[ ${BLOCKS_FREE} -lt 10240 ] && exit 0
# Resize physical partition
# Force busybox fdisk as util-linux fdisk breaks subsequent partx command
cat <<EOF | /bin/busybox fdisk /dev/sda || /bin/true
d
2
n
p
2
t
2
8e
w
EOF
# Re-read partition table
/usr/sbin/partx -u /dev/sda2
# Resize dmcrypt and LVM PV
/sbin/cryptsetup resize system
/sbin/pvresize /dev/mapper/system
# Create swap if it doesn't exist
if [ ! -e /dev/vg0/swap ]; then
/sbin/lvcreate -L 4G vg0 -n swap
/sbin/mkswap /dev/vg0/swap
fi
# Extend LV and underlying filesystem
/sbin/lvextend -l +100%FREE vg0/root
/usr/sbin/resize2fs /dev/vg0/root

8
basic/sbin/vmtty Executable file
View File

@ -0,0 +1,8 @@
#!/bin/sh
# Rebuild /etc/issue
/usr/bin/vm-appmgr rebuild-issue
# Remove double-escaping needed for the real /etc/issue
/bin/sed 's|\\\\|\\|g' /etc/issue
# Wait for key press
read a

View File

@ -0,0 +1,344 @@
# -*- 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/vm/config.json'
ISSUE_FILE = '/etc/issue'
NGINX_DIR = '/etc/nginx/conf.d'
ACME_CRON = '/etc/periodic/daily/acme-sh'
CERT_PUB_FILE = '/etc/ssl/services.pem'
CERT_KEY_FILE = '/etc/ssl/services.key'
CERT_SAN_FILE = '/etc/ssl/san.cnf'
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 /502.html;
location = /502.html {{
root /srv/vm/templates;
}}
location = /vm-ping {{
add_header Content-Type text/plain;
return 200 "vm-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 = /vm-ping {{
add_header Content-Type text/plain;
return 200 "vm-pong";
}}
}}
server {{
listen [::]:{port} ssl http2 default_server ipv6only=off;
location / {{
proxy_pass http://127.0.0.1:8080;
}}
location /static {{
root /srv/vm;
}}
error_page 502 /502.html;
location = /502.html {{
root /srv/vm/templates;
}}
location = /vm-ping {{
add_header Content-Type text/plain;
return 200 "vm-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[1m{url}\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
'''
CERT_SAN = '''[ req ]
distinguished_name = dn
x509_extensions = ext
[ dn ]
[ ext ]
subjectAltName=DNS:{domain},DNS:*.{domain}"
'''
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.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_nginx()
def rebuild_issue(self):
# Compile the HTTPS host displayed in terminal banner
domain = self.domain
# If the dummy host is used, take an IP address of a primary interface instead
if domain == 'spotter.vm':
domain = tools.get_local_ipv4()
if not domain:
domain = tools.get_local_ipv6()
if not domain:
domain = '127.0.0.1'
# Rebuild the terminal banner
with open(ISSUE_FILE, 'w') as f:
f.write(ISSUE_TEMPLATE.format(url=tools.compile_url(domain, self.port)))
def update_apps_urls(self):
# Update configuration for respective applications
confupdater.update_url(tools.compile_url(self.domain, self.port, None))
# 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 update_password(self, oldpassword, newpassword):
# Update LUKS password and adminpwd for WSGI application
tools.update_luks_password(oldpassword, newpassword)
self.conf['host']['adminpwd'] = tools.adminpwd_hash(newpassword)
# Save config to file
self.save_conf()
def create_selfsigned(self):
# Create selfsigned certificate with wildcard alternative subject name
with open(os.path.join(CERT_SAN_FILE), 'w') as f:
f.write(CERT_SAN.format(domain=self.domain))
subprocess.run(['openssl', 'req', '-config', CERT_SAN_FILE, '-x509', '-new', '-out', CERT_PUB_FILE, '-keyout', CERT_KEY_FILE, '-nodes', '-days', '7305', '-subj', '/CN={}'.format(self.domain)], check=True)
os.chmod(CERT_KEY_FILE, 0o640)
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,138 @@
# -*- 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):
maria_started = tools.is_service_started('mariadb')
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 app_exists(app):
return os.path.exists(os.path.join('/srv/', app))
def update_gmaps_api_key(api_key):
# CKAN
if app_exists('ckan'):
replace_file_line('/srv/ckan/conf/ckan.ini', 'ckanext.geoview.gapi_key = ', api_key)
# Crisis Cleanup
if app_exists('crisiscleanup'):
replace_file_line('/srv/crisiscleanup/conf/boot.rb', 'ENV[\'GOOGLE_MAPS_API_KEY\'] = ', api_key)
# Pan.do/ra
if app_exists('pandora'):
replace_file_line('/srv/pandora/conf/local_settings.py', 'GOOGLE_API_KEY = ', '\'{}\''.format(api_key))
# Sahana
if app_exists('sahana'):
replace_file_line('/srv/sahana/conf/000_config.py', 'settings.gis.api_google = ', '"{}"'.format(api_key))
# Sahana Demo
if app_exists('sahana-demo'):
replace_file_line('/srv/sahana-demo/conf/000_config.py', 'settings.gis.api_google = ', '"{}"'.format(api_key))
# SAMBRO
if app_exists('sambro'):
replace_file_line('/srv/sambro/conf/000_config.py', 'settings.gis.api_google = ', '"{}"'.format(api_key))
# Sigmah
if app_exists('sigmah'):
replace_file_line('/srv/sigmah/conf/sigmah.properties', 'maps.key=', api_key)
# Ushahidi
if app_exists('ushahidi'):
replace_file_line('/srv/ushahidi/conf/config.json', ' "google_analytics_id": ', '"{}"'.format(api_key))
def update_email(email):
# CKAN
if app_exists('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
if app_exists('crisiscleanup'):
replace_file_line('/srv/crisiscleanup/conf/initializers/devise.rb', ' config.mailer_sender = ', '\'{}\''.format(email))
# CTS
if app_exists('cts'):
replace_file_line('/srv/cts/conf/spotter.py', 'SERVER_EMAIL = ', '\'{}\''.format(email))
# GNU Health
if app_exists('gnuhealth'):
replace_file_line('/srv/gnuhealth/conf/trytond.conf', 'from = ', email)
# KanBoard
if app_exists('kanboard'):
replace_file_line('/srv/kanboard/conf/config.php', 'define(\'MAIL_FROM\', ', '\'{}\');'.format(email))
# Mifos X
if app_exists('mifosx'):
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
if app_exists('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
if app_exists('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
if app_exists('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
if app_exists('seeddms'):
replace_file_line('/srv/seeddms/conf/settings.xml', ' <smtp smtpServer="postfix" smtpPort="25" smtpSendFrom=', '"{}" smtpUser="" smtpPassword=""/>'.format(email))
# Sigmah
if app_exists('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
if app_exists('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\\"}}'.format(email, email, email)
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')
query = 'UPDATE `config` SET `config_value` = "\\"{}\\"" WHERE `group_name` LIKE "site" AND `config_key` LIKE "email";'.format(email)
run_mysql_query(query, 'ushahidi')
def update_url(host):
# CKAN
if app_exists('ckan'):
replace_file_line('/srv/ckan/conf/ckan.ini', 'ckan.site_url = ', 'https://ckan.{}'.format(host))
# Motech
if app_exists('motech'):
replace_file_line('/srv/motech/conf/config/motech-settings.properties', 'server.url=', 'https://motech.{}'.format(host))
# Pan.do/ra
if app_exists('pandora'):
replace_file_line('/srv/pandora/conf/config.jsonc', ' "url": ', '"pandora.{}"'.format(host))
# Sahana
if app_exists('sahana'):
replace_file_line('/srv/sahana/conf/000_config.py', 'settings.base.public_url = ', '"https://sahana.{}"'.format(host))
# Sahana Demo
if app_exists('sahana-demo'):
replace_file_line('/srv/sahana-demo/conf/000_config.py', 'settings.base.public_url = ', '"https://sahana-demo.{}"'.format(host))
# SAMBRO
if app_exists('sambro'):
replace_file_line('/srv/sambro/conf/000_config.py', 'settings.base.public_url = ', '"https://sambro.{}"'.format(host))
# Ushahidi
if app_exists('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\\"'.format(host)
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,124 @@
# -*- coding: utf-8 -*-
import bcrypt
import dns.exception
import dns.resolver
import os
import requests
import socket
import ssl
import subprocess
NULL_IP = '[100::1]'
def compile_url(domain, port, proto='https'):
port = ':{}'.format(port) if (proto == 'https' and port != '443') or (proto == 'http' and port != '80') else ''
host = '{}{}'.format(domain, port)
return '{}://{}'.format(proto, host) if proto is not None else host
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('https://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('https://tools.dasm.cz/vm-ping.php', data = {'url': url}, timeout=5).text == 'vm-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 restart_nginx():
restart_service('nginx')
def get_cert_info(cert):
data = ssl._ssl._test_decode_cert(cert)
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
def adminpwd_hash(password):
return bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode()
def adminpwd_verify(password, hash):
return bcrypt.checkpw(password.encode(), hash.encode())
def update_luks_password(oldpassword, newpassword):
input = '{}\n{}'.format(oldpassword, newpassword).encode()
subprocess.run(['cryptsetup', 'luksChangeKey', '/dev/sda2'], input=input, check=True)
def shutdown_vm():
subprocess.run(['/sbin/poweroff'])
def reboot_vm():
subprocess.run(['/sbin/reboot'])

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,282 @@
# -*- coding: utf-8 -*-
import json
import os
from werkzeug.exceptions import BadRequest, HTTPException, NotFound
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, CERT_PUB_FILE
from . import tools
from .validator import InvalidValueException
from .wsgilang import WSGILang
from .wsgisession import WSGISession
SESSION_KEY = os.urandom(26)
class WSGIApp(object):
def __init__(self):
self.jinja_env = Environment(loader=FileSystemLoader('/srv/vm/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)
# Enhance request
request.mgr = AppMgr()
request.session = WSGISession(request.cookies, SESSION_KEY)
request.session.lang = WSGILang()
# Dispatch request
response = self.dispatch_request(request)
# Save session if changed
request.session.save(response)
return response(environ, start_response)
def dispatch_request(self, request):
adapter = self.get_url_map(request.session).bind_to_environ(request.environ)
try:
endpoint, values = adapter.match()
return getattr(self, endpoint)(request, **values)
except NotFound as e:
# Return custom 404 page
response = self.render_template('404.html', request)
response.status_code = 404
return response
except HTTPException as e:
return e
def get_url_map(self, session):
rules = [
Rule('/', endpoint='portal_view'),
Rule('/login', methods=['GET'], endpoint='login_view'),
Rule('/login', methods=['POST'], endpoint='login_action'),
Rule('/logout', endpoint='logout_action')
]
if session['admin']:
rules += [
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'),
Rule('/update-password', endpoint='update_password_action'),
Rule('/shutdown-vm', endpoint='shutdown_vm_action'),
Rule('/reboot-vm', endpoint='reboot_vm_action'),
]
return Map(rules)
def render_template(self, template_name, request, **context):
# Enhance context
context['conf'] = request.mgr.conf
context['session'] = request.session
# Render template
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 login_view(self, request):
return self.render_template('login.html', request)
def login_action(self, request):
password = request.form['password']
if tools.adminpwd_verify(password, request.mgr.conf['host']['adminpwd']):
request.session['admin'] = True
return redirect('/')
else:
return self.render_template('login.html', request, message=request.session.lang.bad_password())
def logout_action(self, request):
request.session.reset()
return redirect('/')
def portal_view(self, request):
# Default view. If domain is set to the default dummy domain, redirects to first-run setup instead.
if request.session['admin']:
return self.render_template('portal-admin.html', request)
return self.render_template('portal-user.html', request)
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(CERT_PUB_FILE)
return self.render_template('setup-host.html', request, 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', request)
def update_host_action(self, request):
# Update domain and port, then restart nginx
try:
domain = request.form['domain']
port = request.form['port']
request.mgr.update_host(domain, port, False)
server_name = request.environ['HTTP_X_FORWARDED_SERVER_NAME']
url = '{}/setup-host'.format(tools.compile_url(server_name, port))
response = self.render_json({'ok': request.session.lang.host_updated(url, url)})
response.call_on_close(tools.restart_nginx)
return response
except BadRequest:
return self.render_json({'error': request.session.lang.malformed_request()})
except InvalidValueException as e:
if e.args[0] == 'domain':
return self.render_json({'error': request.session.lang.invalid_domain(domain)})
if e.args[0] == 'port':
return self.render_json({'error': request.session.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
mgr = request.mgr
domains = [mgr.domain]+['{}.{}'.format(mgr.conf['apps'][app]['host'], mgr.domain) for app in 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': request.session.lang.dns_record_does_not_exist(domain)})
if a and a != ipv4:
return self.render_json({'error': request.session.lang.dns_record_mismatch(domain, a, ipv4)})
if aaaa and aaaa != ipv6:
return self.render_json({'error': request.session.lang.dns_record_mismatch(domain, aaaa, ipv6)})
except:
return self.render_json({'error': request.session.lang.dns_timeout()})
return self.render_json({'ok': request.session.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']
mgr = request.mgr
port = mgr.port if proto == 'https' else '80'
domains = [mgr.domain]+['{}.{}'.format(mgr.conf['apps'][app]['host'], mgr.domain) for app in mgr.conf['apps']]
for domain in domains:
url = tools.compile_url(domain, port, proto)
try:
if not tools.ping_url(url):
return self.render_json({'error': request.session.lang.http_host_not_reachable(url)})
except:
return self.render_json({'error': request.session.lang.http_timeout()})
return self.render_json({'ok': request.session.lang.http_hosts_ok(port)})
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': request.session.lang.cert_file_missing()})
if not request.files['private']:
return self.render_json({'error': request.session.lang.key_file_missing()})
request.files['public'].save('/tmp/public.pem')
request.files['private'].save('/tmp/private.pem')
request.mgr.install_cert('/tmp/public.pem', '/tmp/private.pem')
os.unlink('/tmp/public.pem')
os.unlink('/tmp/private.pem')
else:
request.mgr.request_cert()
except BadRequest:
return self.render_json({'error': request.session.lang.malformed_request()})
except:
return self.render_json({'error': request.session.lang.cert_request_error()})
url = tools.compile_url(request.mgr.domain, request.mgr.port)
return self.render_json({'ok': request.session.lang.cert_installed(url, url)})
def update_common_action(self, request):
# Update common settings shared between apps - admin e-mail address, Google Maps API key
try:
request.mgr.update_common(request.form['email'], request.form['gmaps-api-key'])
except BadRequest:
return self.render_json({'error': request.session.lang.malformed_request()})
return self.render_json({'ok': request.session.lang.common_updated()})
def update_app_visibility_action(self, request):
# Update application visibility on portal page
try:
if request.form['value'] == 'true':
request.mgr.show_tiles(request.form['app'])
else:
request.mgr.hide_tiles(request.form['app'])
except (BadRequest, InvalidValueException):
return self.render_json({'error': request.session.lang.malformed_request()})
return self.render_json({'ok': 'ok'})
def update_app_autostart_action(self, request):
# Update value determining if the app should be automatically started after VM boot
try:
if request.form['value'] == 'true':
request.mgr.enable_autostart(request.form['app'])
else:
request.mgr.disable_autostart(request.form['app'])
except (BadRequest, InvalidValueException):
return self.render_json({'error': request.session.lang.malformed_request()})
return self.render_json({'ok': 'ok'})
def start_app_action(self, request):
# Starts application along with its dependencies
try:
request.mgr.start_app(request.form['app'])
except (BadRequest, InvalidValueException):
return self.render_json({'error': request.session.lang.malformed_request()})
except:
return self.render_json({'error': request.session.lang.stop_start_error()})
return self.render_json({'ok': request.session.lang.app_started()})
def stop_app_action(self, request):
# Stops application along with its dependencies
try:
request.mgr.stop_app(request.form['app'])
except (BadRequest, InvalidValueException):
return self.render_json({'error': request.session.lang.malformed_request()})
except:
return self.render_json({'error': request.session.lang.stop_start_error()})
return self.render_json({'ok': request.session.lang.app_stopped()})
def update_password_action(self, request):
# Updates password for both HDD encryption (LUKS-on-LVM) and admin account to vm-appmgr
try:
if request.form['newpassword'] != request.form['newpassword2']:
return self.render_json({'error': request.session.lang.password_mismatch()})
if request.form['newpassword'] == '':
return self.render_json({'error': request.session.lang.password_empty()})
# No need to explicitly validate old password, update_luks_password will raise exception if it's wrong
request.mgr.update_password(request.form['oldpassword'], request.form['newpassword'])
except:
return self.render_json({'error': request.session.lang.bad_password()})
return self.render_json({'ok': request.session.lang.password_changed()})
def reboot_vm_action(self, request):
# Reboots VM
response = self.render_json({'ok': request.session.lang.reboot_initiated()})
response.call_on_close(tools.reboot_vm)
return response
def shutdown_vm_action(self, request):
# Shuts down VM
response = self.render_json({'ok': request.session.lang.shutdown_initiated()})
response.call_on_close(tools.shutdown_vm)
return response
class InvalidRecordException(Exception):
pass

View File

@ -0,0 +1,35 @@
# -*- coding: utf-8 -*-
class WSGILang:
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. Přejděte na URL <a href="{}">{}</a> nebo 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.',
'bad_password': 'Nesprávné heslo',
'password_mismatch': 'Zadaná hesla se neshodují',
'password_empty': 'Nové heslo nesmí být prázdné',
'password_changed': 'Heslo úspěšně změněno',
'reboot_initiated': 'Příkaz odeslán. Vyčkejte na restartování virtuálního stroje.',
'shutdown_initiated': 'Příkaz odeslán. Vyčkejte na vypnutí virtuálního stroje.',
}
def __getattr__(self, key):
def function(*args):
return self.lang[key].format(*args)
return function

View File

@ -0,0 +1,32 @@
# -*- coding: utf-8 -*-
from werkzeug.contrib.securecookie import SecureCookie
class WSGISession:
def __init__(self, cookies, secret_key):
self.secret_key = secret_key
data = cookies.get('session')
if data:
self.sc = SecureCookie.unserialize(data, secret_key)
else:
self.reset()
if 'admin' not in self.sc:
self.reset()
def __getitem__(self, key):
return self.sc.__getitem__(key)
def __setitem__(self, key, value):
return self.sc.__setitem__(key, value)
def __delitem__(self, key):
return self.sc.__delitem__(key)
def __contains__(self, key):
return self.sc.__contains__(key)
def reset(self):
self.sc = SecureCookie(secret_key=self.secret_key)
self.sc['admin'] = False
def save(self, response):
if self.sc.should_save:
data = self.sc.serialize()
response.set_cookie('session', data, httponly=True)

114
basic/srv/vm/cli.py Executable file
View File

@ -0,0 +1,114 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
import argparse
import getpass
import sys
sys.path.append('/srv/vm')
from appmgr import AppMgr
parser = argparse.ArgumentParser(description='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_rebuild_issue = subparsers.add_parser('rebuild-issue', help='Rebuilds /etc/issue using current settings - used on VM startup')
parser_rebuild_issue.set_defaults(action='rebuild-issue')
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_update_password = subparsers.add_parser('update-password', help='Updates password for HDD encryption and WSGI administration interface')
parser_update_password.set_defaults(action='update-password')
parser_create_selfsigned = subparsers.add_parser('create-selfsigned', help='Creates and installs selfsigned certificate for currently set domain')
parser_create_selfsigned.set_defaults(action='create-selfsigned')
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 == 'rebuild-issue':
mgr.rebuild_issue()
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 == 'update-password':
oldpassword = getpass.getpass('Old password: ')
newpassword = getpass.getpass('New password: ')
mgr.update_password(oldpassword, newpassword)
elif args.action == 'create-selfsigned':
mgr.create_selfsigned()
elif args.action == 'request-cert':
mgr.request_cert()
elif args.action == 'install-cert':
mgr.install_cert(args.certificate, args.key)

139
basic/srv/vm/config.json Normal file
View File

@ -0,0 +1,139 @@
{
"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": {
"adminpwd": "$2b$12$nLrIefUoWN.pK6j90gsfkO0/tg4EGXDmdjN8HOGB0U.9BcHTFxzWS",
"domain": "spotter.vm",
"port": "443"
}
}

View File

@ -0,0 +1,196 @@
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;
}
nav #menu-button {
float: right;
cursor: pointer;
}
nav #menu-button div {
width: 24px;
height: 4px;
background-color: #fff;
border: 1px solid #000;
margin: 2px 0px;
}
nav ul {
display: none;
list-style: none;
border: 1px solid #000;
margin: 26px 0px 0px 0px;
position: absolute;
background-color: #fff;
padding: 10px;
right: 30px;
z-index: 1;
}
nav a {
display: block;
}
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;
border: solid 1px #000;
padding: 10px;
}
.portal-box {
position: relative;
margin-right: 13px;
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="password"],
.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); }
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

@ -0,0 +1,202 @@
$(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);
$('#update-password').on('submit', update_password);
$('#reboot-vm').on('click', reboot_vm);
$('#shutdown-vm').on('click', shutdown_vm);
});
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();
$('input').prop('disabled', true);
$('.setup-box').slice(1).css('opacity', '0.5');
}
});
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').html(data.error).show();
$('#cert-submit').show();
} else {
$('#cert-message').attr('class','info').html(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;
}
function update_password() {
$('#password-submit').hide();
$('#password-message').hide();
$('#password-wait').show();
$.post('/update-password', {'oldpassword': $('#oldpassword').val(), 'newpassword': $('#newpassword').val(), 'newpassword2': $('#newpassword2').val()}, function(data) {
$('#password-wait').hide();
if (data.error) {
$('#password-message').attr('class','error').html(data.error).show();
$('#password-submit').show();
} else {
$('#password-message').attr('class','info').html(data.ok).show();
}
});
return false;
}
function reboot_vm() {
if (confirm('Do you really want to reboot VM?')) {
$.get('/reboot-vm', function(data) {
$('#vm-message').attr('class','info').html(data.ok).show();
});
}
return false;
}
function shutdown_vm() {
if (confirm('Do you really want to shutdown VM?')) {
$.get('/shutdown-vm', function(data) {
$('#vm-message').attr('class','info').html(data.ok).show();
});
}
return false;
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,7 @@
$(function() {
$('#menu-button').on('click', toggle_menu);
});
function toggle_menu() {
$('#menu').toggle();
}

View File

@ -0,0 +1,15 @@
<!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>Chyba 404</title>
</head>
<body>
<h1>Stránka nebyla nalezena</h1>
<p>Stránka, kterou se pokoušíte zobrazit, nebyla na serveru nalezena. Zkontrolujte prosím URL v adresním řádku nebo se vraťte <a href="/">zpět na úvodní stránku</a>.</p>
</body>
</html>

View File

@ -0,0 +1,15 @@
<!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>Chyba 502</title>
</head>
<body>
<h1>Chyba spojení s aplikací</h1>
<p>Aplikace ke které se pokoušíte připojit není dostupná. Nejspíše byla vypnuta správcem serveru.</p>
</body>
</html>

View File

@ -0,0 +1,42 @@
<!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>
{% if session.admin %}
<script src="static/js/admin.js"></script>
{% endif %}
</head>
<body>
<nav>
<div id="menu-button">
<div></div>
<div></div>
<div></div>
</div>
<ul id="menu">
<li><a href="/">Portál</a></li>
{% if session.admin %}
<li><a href="/setup-host">Nastavení hostitele</a></li>
<li><a href="/setup-apps">Nastavení aplikací</a></li>
<li><a href="/logout">Odhlášení</a></li>
{% else %}
<li><a href="/login">Přihlášení</a></li>
{% endif %}
</ul>
</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,28 @@
{% extends 'layout.html' %}
{% block title %}Přihlášení{% endblock %}
{% block body %}
<div class="setup-box">
<h2>Přihlášení</h2>
<form action="/login" method="post">
<table>
<tr>
<td>Jméno:</td>
<td>admin</td>
</tr>
<tr>
<td>Heslo</td>
<td><input type="password" name="password"></td>
</tr>
<tr>
<td>&nbsp;</td>
<td>
<input type="submit" value="Přihlásit">
</td>
</tr>
</table>
{% if message is defined %}
<p class="error">{{ message }}</p>
{% endif %}
</form>
</div>
{% endblock %}

View File

@ -0,0 +1,352 @@
{% 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'] and is_service_started('sahana') %}
<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'] and is_service_started('sahana-demo') %}
<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'] and is_service_started('sambro') %}
<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'] and is_service_started('crisiscleanup') %}
<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'] and is_service_started('ckan') %}
<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'] and is_service_started('opendatakit-build') %}
<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="https://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'] and is_service_started('opendatakit') %}
<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'] and is_service_started('openmapkit') %}
<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'] and is_service_started('frontlinesms') %}
<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/FrontlineSync.png" alt="FrontlineSync" title="FrontlineSync">FrontlineSync</a></h2>
<p>Mobilní aplikace pro<br>
<a href="https://play.google.com/store/apps/details?id=com.simlab.frontlinesync"><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'] and is_service_started('seeddms') %}
<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'] and is_service_started('pandora') %}
<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'] and is_service_started('ushahidi') %}
<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>
<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']['kanboard'] %}
{% if app['visible'] and is_service_started('kanboard') %}
<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'] and is_service_started('cts') %}
<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'] and is_service_started('gnuhealth') %}
<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'] and is_service_started('sigmah') %}
<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'] and is_service_started('motech') %}
<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'] and is_service_started('mifosx') %}
<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="https://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,123 @@
{% 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'] %}
{% if conf['apps']['sahana-demo']['visible'] and is_service_started('sahana-demo') %}
<div class="portal-box">
<h2><a href="https://sahana-demo.{{ host }}/eden/">Řízení humanítární činnosti</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>
</div>
{% endif %}
{% if conf['apps']['sambro']['visible'] and is_service_started('sambro') %}
<div class="portal-box">
<h2><a href="https://sambro.{{ host }}/eden/">Centrum hlášení a výstrah</a></h2>
<p>Samostatná instance s šablonou pro centrum hlášení a výstrah.</p>
</div>
{% endif %}
{% if conf['apps']['crisiscleanup']['visible'] and is_service_started('crisiscleanup') %}
<div class="portal-box">
<h2><a href="https://cc.{{ host }}">Mapování následků katastrof</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>
</div>
{% endif %}
{% if conf['apps']['ckan']['visible'] and is_service_started('ckan') %}
<div class="portal-box">
<h2><a href="https://ckan.{{ host }}">Datový sklad</a></h2>
<p><strong>Repository</strong> management a datová analýza pro vytváření otevřených dat.</p>
</div>
{% endif %}
{% if conf['apps']['opendatakit-build']['visible'] and is_service_started('opendatakit-build') %}
<div class="portal-box">
<h2><a href="https://odkbuild.{{ host }}">Sběr formulářových dat</a></h2>
<p><strong>Sběr dat s pomocí smartphone</strong>.<br>Aplikace pro návrh formulářů</p>
</div>
{% endif %}
{% if conf['apps']['opendatakit']['visible'] and is_service_started('opendatakit') %}
<div class="portal-box">
<h2><a href="https://odk.{{ host }}/">Sběr formulářových dat</a></h2>
<p><strong>Sběr dat s pomocí smartphone</strong>.</p>
</div>
{% endif %}
{% if conf['apps']['openmapkit']['visible'] and is_service_started('openmapkit') %}
<div class="portal-box">
<h2><a href="https://omk.{{ host }}">Sběr mapových dat</a></h2>
<p><strong>Sběr dat s pomocí smartphone</strong>.<br>
</div>
{% endif %}
{% if conf['apps']['frontlinesms']['visible'] and is_service_started('frontlinesms') %}
<div class="portal-box">
<h2><a href="https://sms.{{ host }}">Hromadné odesílání zpráv</a></h2>
<p><strong>SMS messaging</strong> přes veřejné datové brány</p>
</div>
{% endif %}
{% if conf['apps']['seeddms']['visible'] and is_service_started('seeddms') %}
<div class="portal-box">
<h2><a href="https://dms.{{ host }}">Archiv dokumentace</a></h2>
<p><strong>Dokument management</strong> na dokumentaci a projektovou dokumentaci</p>
</div>
{% endif %}
{% if conf['apps']['pandora']['visible'] and is_service_started('pandora') %}
<div class="portal-box">
<h2><a href="https://pandora.{{ host }}">Archiv medií</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>
</div>
{% endif %}
{% if conf['apps']['ushahidi']['visible'] and is_service_started('ushahidi') %}
<div class="portal-box">
<h2><a href="https://ush.{{ host }}">Skupinová reakce na události</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>
</div>
{% endif %}
{% if conf['apps']['kanboard']['visible'] and is_service_started('kanboard') %}
<div class="portal-box">
<h2><a href="https://kb.{{ host }}">Kanban řízení projektů</a></h2>
<p>Usnadňuje tvorbu a řízení projektů s pomocí Kanban metodiky.</p>
</div>
{% endif %}
{% if conf['apps']['gnuhealth']['visible'] and is_service_started('gnuhealth') %}
<div class="portal-box">
<h2><a href="https://gh.{{ host }}/index.html">Lékařské záznamy pacientů</a></h2>
<p>Zdravotní a nemocniční informační systém.</p>
</div>
{% endif %}
{% if conf['apps']['sigmah']['visible'] and is_service_started('sigmah') %}
<div class="portal-box">
<h2><a href="https://sigmah.{{ host }}/sigmah/">Finanční řízení sbírek</a></h2>
<p>Rozpočtování získávání finančních prostředků.</p>
</div>
{% endif %}
{% if conf['apps']['motech']['visible'] and is_service_started('motech') %}
<div class="portal-box">
<h2><a href="https://motech.{{ host }}/">Automatizace komunikace</a></h2>
<p>Integrace zdravotnických a komunikačních služeb.</p>
</div>
{% endif %}
{% if conf['apps']['mifosx']['visible'] and is_service_started('mifosx') %}
<div class="portal-box">
<h2><a href="https://mifosx.{{ host }}/">Mikrofinancování rozvojových projektů</a></h2>
<p>Nástroj na rozvojovou, humanitární pomoc a mikrofinancování.</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,94 @@
{% 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>
<div class="setup-box">
<h2>Správce virtuálního stroje</h2>
<p>Změna hesla k šifrovanému diskovému oddílu a administračnímu rozhraní.</p>
<form id="update-password" action="/update-password" method="post">
<table>
<tr>
<td>Stávající heslo:</td>
<td><input type="password" name="oldpassword" id="oldpassword"></td>
</tr>
<tr>
<td>Nové heslo:</td>
<td><input type="password" name="newpassword" id="newpassword"></td>
</tr>
<tr>
<td>Kontrola nového hesla:</td>
<td><input type="password" name="newpassword2" id="newpassword2"></td>
</tr>
<tr>
<td>&nbsp;</td>
<td colspan="2">
<input type="submit" id="password-submit" value="Změnit heslo">
<div id="password-message"></div>
<div id="password-wait" class="loader-wrap">
<div class="loader"></div>
<span>Provádí se změna hesla, prosím čekejte...</span>
</div>
</td>
</tr>
</table>
</form>
<p>Restartování nebo vypnutí virtuálního stroje.</p>
<input type="button" id="reboot-vm" value="Restartovat VM">
<input type="button" id="shutdown-vm" value="Vypnout VM">
<div id="vm-message"></div>
</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 %}

16
basic/srv/vm/wsgi.py Executable file
View File

@ -0,0 +1,16 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
import sys
sys.path.append('/srv/vm')
from appmgr.wsgiapp import WSGIApp
application = WSGIApp()
if __name__ == '__main__':
import os
from werkzeug.contrib.fixers import ProxyFix
from werkzeug.serving import run_simple
run_simple('127.0.0.1', 8080, ProxyFix(application))

View File

@ -1,117 +0,0 @@
#!/bin/sh
set -ev
ROOT=$(dirname $(dirname $(realpath "${0}")))
# Build documentation
cd ${ROOT}/doc
make html
# Build basic tar
cd ${ROOT}/vm
tar czpf /srv/build/vm.tar.gz *
# Build native apps
cd ${ROOT}/apk/py3-secure-cookie
apk add -U py3-setuptools py3-pytest py3-werkzeug
abuild -F
cd ${ROOT}/apk/spoc
abuild -F
cd ${ROOT}/apk/vmmgr
abuild -F
# Build runtimes
cd ${ROOT}/lxc-shared
spoc-image build -p alpine3.8/image
spoc-image build -p alpine3.8-java8/image
spoc-image build -p alpine3.8-ruby2.4/image
spoc-image build -p alpine3.10/image
spoc-image build -p alpine3.10-nodejs10/image
spoc-image build -p alpine3.11/image
spoc-image build -p alpine3.11-python2.7/image
spoc-image build -p alpine3.12/image
spoc-image build -p alpine3.12-java8/image
spoc-image build -p alpine3.12-php7.3/image
spoc-image build -p alpine3.12-python3.8/image
spoc-image build -p alpine3.12-ruby2.4/image
spoc-image build -p alpine3.12-ruby2.7/image
spoc-image build -p alpine3.12-tomcat7/image
spoc-image build -p alpine3.12-tomcat8.5/image
spoc-image build -p debian10/image
# Build services
cd ${ROOT}/lxc-services
spoc-image build -p activemq/image
spoc-image build -p mariadb/image
spoc-image build -p mongodb/image
spoc-image build -p postgres/image
spoc-image build -p postgis/image
spoc-image build -p rabbitmq/image
spoc-image build -p redis/image
spoc-image build -p solr6/image
# Build applications
cd ${ROOT}/lxc-apps
spoc-image build -p ckan/ckan.image
spoc-image build -p ckan/ckan-datapusher.image
spoc-app publish ckan/app
spoc-image build -p crisiscleanup/image
spoc-app publish crisiscleanup/app
spoc-image build -p cts/image
spoc-app publish cts/app
spoc-image build -p decidim/decidim-nginx.image
spoc-image build -p decidim/decidim.image
spoc-app publish decidim/app
spoc-image build -p dhis2/image
spoc-app publish dhis2/app
spoc-image build -p frontlinesms/image
spoc-app publish frontlinesms/app
spoc-image build -p gnuhealth/image
spoc-app publish gnuhealth/app
spoc-image build -p kanboard/image
spoc-app publish kanboard/app
spoc-image build -p mifosx/image
spoc-app publish mifosx/app
spoc-image build -p motech/image
spoc-app publish motech/app
spoc-image build -p odoo/image
spoc-app publish odoo/app
spoc-image build -p opendatakit/opendatakit.image
spoc-image build -p opendatakit/opendatakit-build.image
spoc-app publish opendatakit/app
spoc-image build -p openmapkit/image
spoc-app publish openmapkit/app
spoc-image build -p pandora/image
spoc-app publish pandora/app
spoc-image build -p sahana/image
spoc-app publish sahana/app
spoc-app publish sahana-demo/app
spoc-app publish sambro/app
spoc-app publish safire/app
spoc-app publish share/app
spoc-image build -p seeddms/image
spoc-app publish seeddms/app
spoc-image build -p taarifa/image
spoc-app publish taarifa/app
spoc-image build -p ushahidi/image
spoc-app publish ushahidi/app

View File

@ -1,29 +0,0 @@
#!/bin/sh
set -ev
# Clean documentation
rm -rf /srv/build/doc/*
# Clean basic tar
rm -f /srv/build/vm.tar.gz
# Clean native apps
rm -rf /srv/build/alpine/*
# Clean built LXC packages
rm -rf /srv/build/spoc
# Remove nginx configs
for CONF in $(find /etc/nginx/conf.d -name '*.conf' -a ! -name repo.conf -a ! -name default.conf); do
rm -f ${CONF}
done
service nginx reload
# Stop running containers
for APP in $(spoc-container list); do
spoc-container stop ${APP}
done
# Remove data
rm -rf /var/lib/spoc
rm -rf /var/log/spoc

View File

@ -1,32 +0,0 @@
export CFLAGS="-Os -fomit-frame-pointer"
export CXXFLAGS="$CFLAGS"
export CPPFLAGS="$CFLAGS"
export LDFLAGS="-Wl,--as-needed"
export JOBS=2
export MAKEFLAGS=-j$JOBS
# remove line below to disable colors
USE_COLORS=1
# uncomment line below to enable ccache support.
#USE_CCACHE=1
SRCDEST=/var/cache/distfiles
# uncomment line below to store built packages in other location
# The package will be stored as $REPODEST/$repo/$pkgname-$pkgver-r$pkgrel.apk
# where $repo is the name of the parent directory of $startdir.
REPODEST=/srv/build/alpine/v3.12
# PACKAGER and MAINTAINER are used by newapkbuild when creating new aports for
# the APKBUILD's "Contributor:" and "Maintainer:" comments, respectively.
#PACKAGER="Your Name <your@email.address>"
#MAINTAINER="$PACKAGER"
PACKAGER_PRIVKEY="/root/repo.spotter.cz.rsa"
# what to clean up after a successful build
CLEANUP="srcdir bldroot pkgdir deps"
# what to cleanup after a failed build
ERROR_CLEANUP="bldroot deps"

View File

@ -1,8 +0,0 @@
server {
listen [::]:80;
server_name repo.build.vm;
location / {
root /srv/build;
}
}

View File

@ -1,36 +0,0 @@
#!/bin/sh
set -ev
cd $(realpath $(dirname "${0}"))
# Install basic build tools
apk update
apk add git file htop less openssh-client tree
# Install Alpine SDK (for APK builds)
apk add alpine-sdk
# Install Sphinx support (for documentation builds)
apk add py3-sphinx py3-sphinx_rtd_theme
# Copy root profile files and settings
mkdir -p /root/.config/htop
cp root/.profile /root/.profile
cp root/.config/htop/htoprc /root/.config/htop/htoprc
# Prepare abuild toolchain
adduser root abuild
cp etc/abuild.conf /etc/abuild.conf
# Prepare local APK repository
cp etc/nginx/conf.d/repo.conf /etc/nginx/conf.d/repo.conf
echo "172.17.0.1 repo.build.vm" >>/etc/hosts
service nginx reload
# Change SPOC repository
sed -i 's/https:\/\/repo\.spotter\.cz/http:\/\/repo.build.vm/' /etc/spoc/spoc.conf
# Supply abuild key
# echo '/root/repo.spotter.cz.rsa' | abuild-keygen
# Supply SPOC key
# openssl ecparam -genkey -name secp384r1 -out /etc/spoc/publish.key
# openssl ec -in /etc/spoc/publish.key -pubout -out /tmp/repository.pub

View File

@ -1,2 +0,0 @@
alias ll="ls -la"
alias view="vi -R"

Some files were not shown because too many files have changed in this diff Show More