Add rendered example

This commit is contained in:
Daniel Berteaud 2024-01-05 15:08:00 +01:00
parent fa90b04da7
commit a88e2188cf
10 changed files with 623 additions and 0 deletions

View File

@ -0,0 +1,3 @@
Kind = "service-defaults"
Name = "unifi"
Protocol = "http"

View File

@ -0,0 +1,16 @@
Kind = "service-intentions"
Name = "unifi"
Sources = [
{
Name = "traefik"
Permissions = [
{
Action = "allow"
HTTP {
PathPrefix = "/"
Methods = ["GET","HEAD","POST","PUT","DELETE"]
}
}
]
}
]

View File

@ -0,0 +1,16 @@
FROM danielberteaud/alpine:24.1-1
MAINTAINER Daniel Berteaud <dbd@ehtrace.com>
ENV UBNT_UPDATE_API="https://fw-update.ubnt.com/api/firmware-latest?filter=eq~~product~~unifi-firmware&filter=eq~~channel~~release" \
UBNT_FIRMWARE_DIR="/opt/unifi/app/data/firmware/" \
UBNT_PLATFORMS=
RUN set -eux &&\
apk --no-cache upgrade &&\
apk --no-cache add perl-libwww \
perl-lwp-protocol-https \
perl-json \
supercronic
COPY root/ /
CMD ["ubnt-firmware-downloader.sh"]

View File

@ -0,0 +1,141 @@
#!/usr/bin/perl
use strict;
use warnings;
use JSON;
use LWP::UserAgent;
use File::Basename;
use File::Path qw(make_path);
use Digest::SHA;
use Data::Dumper;
my $ubnt_api = $ENV{UBNT_UPDATE_API} || 'https://fw-update.ubnt.com/api/firmware-latest?filter=eq~~product~~unifi-firmware&filter=eq~~channel~~release';
my $fw_dir = $ENV{UBNT_FIRMWARE_DIR} || '/opt/unifi/app/data/firmware/';
my @dl_platform = split(/,/, $ENV{UBNT_PLATFORMS} || "");
my $fw_cached = {};
my $fw_entries = {};
my $fw_devices = {};
my $fw_latest = {};
# Ensure the list is empty if UBNT_PLATFORMS was empty
if (scalar @dl_platform > 0 and $dl_platform[0] eq "") {
@dl_platform = ();
}
if (scalar @dl_platform > 0){
print STDERR "Will only try to download firmware for platforms " . join(",", @dl_platform) . "\n";
}
# If the firmware_meta.json file exists, open it and read
# already present firmwares
if (-e "$fw_dir/firmware_meta.json"){
open CACHED_FIRMWARES, "<$fw_dir/firmware_meta.json";
my $data = <CACHED_FIRMWARES>;
my $json = eval {
from_json($data);
};
if (defined $json and defined $json->{cached_firmwares}){
foreach my $fw (@{$json->{cached_firmwares}}){
print STDERR "file $fw->{path} found\n";
if (-e "$fw_dir/$fw->{path}"){
$fw_entries->{$fw->{md5}} = $fw;
}
}
}
close CACHED_FIRMWARES;
}
my $ua = LWP::UserAgent->new(timeout => 10);
$ua->env_proxy;
# Ask ubnt API for the list of firmwares
my $resp = $ua->get($ubnt_api);
if (not $resp->is_success){
die "Couldn't fetch $ubnt_api : " . $resp->status_line . "\n";
}
my $firmwares = from_json($resp->decoded_content);
FIRMWARE: foreach my $fw (@{$firmwares->{_embedded}->{firmware}}){
my $version = $fw->{version};
$version =~ s/^v//;
$version =~ s/\+/./g;
my $file = basename($fw->{_links}->{data}->{href});
my $fw_path = "$fw_dir/$fw->{platform}/$version/$file";
my $sha256;
# If the firmware is already cached, we need to check its checksum
if (-e "$fw_path"){
print STDERR "Found firmware for $fw->{platform} version $version, checking checksum of $fw_path\n";
$sha256 = Digest::SHA->new("sha256")->addfile($fw_path)->hexdigest;
if ($sha256 eq $fw->{sha256_checksum}){
print STDERR "Checksum matched, firmware already cached\n";
$fw_entries->{$fw->{md5}} = {
md5 => $fw->{md5},
version => $version,
size => $fw->{file_size},
path => "$fw->{platform}/$file"
};
push @{$fw_devices->{$fw->{md5}}}, $fw->{platform};
$fw_latest->{$fw->{md5}} = 1;
# File is OK, just continue with the next firmware
next FIRMWARE;
} else {
print STDERR "Checksum mismatch: got $sha256 while expecting $fw->{sha256_checksum}, downloading again\n";
}
}
# If we restrict the list of platform to download firmwares for
# check the current firmware matches
if (scalar @dl_platform > 0 and not grep { $_ eq $fw->{platform} } @dl_platform){
print STDERR "Platform $fw->{platform} is not in the list of downloads, skipping download\n";
} else {
print STDERR "Downloading firmware from $fw->{_links}->{data}->{href}\n";
make_path "$fw_dir/$fw->{platform}/$version/";
#$resp = getstore($fw->{_links}->{data}->{href}, $fw_path);
$resp = $ua->get($fw->{_links}->{data}->{href}, ":content_file" => $fw_path);
if (not $resp->is_success){
print STDERR "Error downloading $fw->{_links}->{data}->{href} : " . $resp->status_line ."\n";
next FIRMWARE;
}
$sha256 = Digest::SHA->new("sha256")->addfile($fw_path)->hexdigest;
if ($sha256 ne $fw->{sha256_checksum}){
print STDERR "Checksum mismatch : got $sha256 while expecting $fw->{sha256_checksum}\n";
next FIRMWARE;
} else {
print STDERR "Checksum correctly verified\n";
push @{$fw_devices->{$fw->{md5}}}, $fw->{platform};
$fw_latest->{$fw->{md5}} = 1;
$fw_entries->{$fw->{md5}} = {
md5 => $fw->{md5},
version => $version,
size => $fw->{file_size},
path => "$fw->{platform}/$file"
};
}
}
# In anycase, push this platform to the fw_devices hash
# so the list of devices will be complete
push @{$fw_devices->{$fw->{md5}}}, $fw->{platform};
}
print STDERR "Finished downloading firmwares, now building the firmware_meta.json file\n";
foreach my $fw (sort { $fw_entries->{$a}->{version} cmp $fw_entries->{$b}->{version} } keys %{$fw_entries}){
my $firmware = $fw_entries->{$fw};
# Only override device list for latest firmwares (those returned by the ubnt API)
# for the previous one, we do not have fresh info, so just trust what was in the firmware_meta.json file
if (defined $fw_latest->{$fw_entries->{$fw}->{md5}}){
$firmware->{devices} = $fw_devices->{$fw};
}
push @{$fw_cached->{cached_firmwares}}, $firmware;
}
open CACHED_FIRMWARES, ">$fw_dir/firmware_meta.json";
print CACHED_FIRMWARES to_json($fw_cached);

View File

@ -0,0 +1,14 @@
#!/bin/sh
set -euo pipefail
/usr/local/bin/ubnt-firmware-downloader
# If a cron expression is defined, run a cron daemon
if [ -n "${UBNT_CRON}" ]; then
echo "Running using cron with expression ${UBNT_CRON}"
cat <<_EOF > /tmp/crontab
${UBNT_CRON} /usr/local/bin/ubnt-firmware-downloader
_EOF
supercronic /tmp/crontab
fi

View File

@ -0,0 +1,41 @@
FROM danielberteaud/java:17.24.1-1 AS builder
ARG UNIFI_VERSION=8.0.26
RUN set -euxo pipefail &&\
apk --no-cache add curl ca-certificates unzip &&\
cd /tmp &&\
curl -sSLO https://www.ubnt.com/downloads/unifi/${UNIFI_VERSION}/UniFi.unix.zip &&\
unzip UniFi.unix.zip &&\
rm -f UniFi.unix.zip &&\
ls -l &&\
rm -f UniFi/bin/mongod &&\
chown -R root:root UniFi
FROM danielberteaud/java:17.24.1-1
MAINTAINER Daniel Berteaud <dbd@ehtrace.com>
ENV JAVA_OPTS="-Djava.awt.headless=true -Dlogback.configurationFile=/opt/unifi/logback.xml --add-opens=java.base/java.io=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.rmi/sun.rmi.transport=ALL-UNNAMED --add-opens=java.base/java.time=ALL-UNNAMED" \
TZ=Europe/Paris \
LANG=fr_FR.utf8
COPY --from=builder /tmp/UniFi /opt/unifi
COPY root/ /
RUN set -euxo pipefail &&\
apk --no-cache upgrade &&\
addgroup -g 8443 unifi &&\
adduser --system --ingroup unifi --disabled-password --uid 8443 --home /opt/unifi --shell /sbin/nologin unifi &&\
mkdir -p /data/unifi &&\
mkdir -p /data/logs &&\
chown unifi:unifi /data &&\
chmod 700 /data &&\
ln -s /data/unifi /opt/unifi/data &&\
ln -s /data/logs /opt/unifi/logs
EXPOSE 8443 8080 8843 3778
USER unifi
VOLUME /data
WORKDIR /opt/unifi
CMD ["sh", "-c", "java ${JAVA_OPTS} -jar /opt/unifi/lib/ace.jar start"]

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %highlight([%-5level]) %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="STDOUT"/>
</root>
</configuration>

19
example/prep.d/mv_conf.sh Executable file
View File

@ -0,0 +1,19 @@
#!/bin/sh
set -eu
if [ "unifi" != "unifi" ]; then
for DIR in vault consul nomad; do
if [ -d output/${DIR} ]; then
for FILE in $(find output/${DIR} -name "*unifi*.hcl" -type f); do
NEW_FILE=$(echo "${FILE}" | sed -E "s/unifi/unifi/g")
mv "${FILE}" "${NEW_FILE}"
done
fi
done
fi

359
example/unifi.nomad.hcl Normal file
View File

@ -0,0 +1,359 @@
job "unifi" {
datacenters = ["dc1"]
group "unifi" {
network {
mode = "bridge"
port "stun" {
to = 3478
}
}
service {
name = "unifi"
port = 8888
connect {
sidecar_service {
}
sidecar_task {
resources {
cpu = 50
memory = 64
}
}
}
tags = [
"traefik.enable=true",
# Note : no Host as inform requests are sent without. But it's binded to the dedicated entrypoint anyway
"traefik.http.routers.unifi-inform.rule=(Path(`/inform`) && Method(`POST`)) || (PathPrefix(`/dl/firmware-cached`) && (Method(`GET`) || Method(`HEAD`)))",
"traefik.http.routers.unifi-inform.entrypoints=unifi-inform",
"traefik.http.routers.unifi-inform.middlewares=rate-limit-std@file,inflight-std@file",
"traefik.http.routers.unifi-controller.rule=Host(`unifi.example.org`)",
"traefik.http.routers.unifi-controller.entrypoints=https",
"traefik.http.routers.unifi-controller.tls=true",
"traefik.http.routers.unifi-controller.middlewares=rate-limit-std@file,security-headers@file,compression@file,csp-relaxed@file",
"traefik.http.routers.unifi-portal.rule=Host(`unifi-portal.example.org`) && PathPrefix(`/guest`)",
"traefik.http.routers.unifi-portal.entrypoints=unifi-portal",
"traefik.http.routers.unifi-portal.tls=true",
"traefik.http.routers.unifi-portal.middlewares=rate-limit-std@file,inflight-std@file,security-headers@file,hsts@file,compression@file,csp-relaxed@file"
]
}
service {
name = "unifi-stun"
port = "stun"
tags = [
"traefik.enable=true",
"traefik.udp.routers.unifi-stun.entrypoints=unifi-stun",
"traefik.consulcatalog.connect=false"
]
}
service {
name = "unifi-mongo"
port = 27017
check {
type = "script"
command = "sh"
args = ["-c", "mongo --quiet --eval 'db.runCommand(\"ping\").ok'"]
interval = "30s"
timeout = "5s"
task = "mongo"
check_restart {
limit = 4
grace = "3m"
}
}
}
volume "mongo" {
source = "unifi-mongo"
type = "csi"
access_mode = "single-node-writer"
attachment_mode = "file-system"
}
volume "data" {
source = "unifi-data"
type = "csi"
access_mode = "single-node-writer"
attachment_mode = "file-system"
}
# wait for required services tp be ready before starting the main task
task "wait-for" {
driver = "docker"
user = 1053
config {
image = "danielberteaud/wait-for:24.1-1"
readonly_rootfs = true
pids_limit = 20
}
lifecycle {
hook = "prestart"
}
env {
SERVICE_0 = "unifi-mongo.service.consul"
}
resources {
cpu = 10
memory = 10
memory_max = 30
}
}
task "nginx" {
driver = "docker"
user = 8306
lifecycle {
hook = "poststart"
sidecar = "true"
}
config {
image = "nginxinc/nginx-unprivileged:alpine"
volumes = ["local/nginx.conf:/etc/nginx/conf.d/default.conf"]
}
template {
data = <<_EOF
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 127.0.0.1:8888;
server_name _;
server_tokens off;
root /usr/share/html;
proxy_set_header Origin "";
proxy_set_header Authorization "";
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_socket_keepalive on;
client_max_body_size 100m;
set_real_ip_from 127.0.0.1;
real_ip_header X-Forwarded-For;
real_ip_recursive on;
# Inform endpoint
location ~ ^/(inform|dl/firmware-cached).* {
if ($request_method !~ ^(GET|HEAD|POST)$ ) {
return 405;
}
proxy_pass http://localhost:8080;
}
# Guest portal
location /guest/ {
if ($request_method !~ ^(GET|HEAD|POST)$ ) {
return 405;
}
proxy_pass https://localhost:8843;
}
# Main console
location / {
if ($request_method !~ ^(GET|HEAD|POST|PUT|DELETE)$ ) {
return 405;
}
proxy_pass https://localhost:8443;
}
}
_EOF
destination = "local/nginx.conf"
}
resources {
cpu = 10
memory = 15
}
}
task "controller" {
leader = true
driver = "docker"
config {
image = "danielberteaud/unifi:8.0.26-1"
volumes = [
"local/init-system.properties.sh:/entrypoint.d/10-init-system.properties.sh"
]
mount {
type = "tmpfs"
target = "/opt/unifi/run"
}
}
vault {
policies = ["unifi"]
env = false
disable_file = true
}
env {
LANG = "fr_FR.utf8"
TZ = "Europe/Paris"
}
template {
data = <<_EOF
unifi.http.port=8080
unifi.https.port=8443
portal.http.port=8880
portal.https.port=8843
unifi.stun.port={{ env "NOMAD_PORT_stun" }}
unifi.db.nojournal=true
db.mongo.local=false
db.mongo.uri=mongodb://127.0.0.1:27017/unifi?
statdb.mongo.uri=mongodb://127.0.0.1:27017/unifi_stats?
debug.device=info
debug.mgmt=info
debug.system=info
debug.sdn=warn
autobackup.dir=/data/backup
_EOF
destination = "secrets/system.properties"
}
template {
data = <<_EOF
#!/bin/sh
# vim: syntax=sh
set -euo pipefail
mkdir -p /data/unifi
mkdir -p /data/logs
if [ \! -f "/opt/unifi/data/system.properties" ]; then
echo "System initialization, copy the default system.properties"
cp /secrets/system.properties /opt/unifi/data/system.properties
else
for PROP in $(grep -vE '^(\s*$|#)' /secrets/system.properties); do
KEY=$(echo ${PROP} | cut -d= -f1)
VALUE=$(echo ${PROP} | cut -d= -f2)
if grep -q -E "^${KEY}=" /opt/unifi/data/system.properties; then
if grep -q -E "^${PROP}" /opt/unifi/data/system.properties; then
echo "${PROP} already set in system.properties"
else
echo "Updating ${PROP} in system.properties"
sed -i -E "s|^${KEY}=.*|${PROP}|" /opt/unifi/data/system.properties
fi
else
echo "Adding ${PROP} in system.properties"
echo ${PROP} >> /opt/unifi/data/system.properties
fi
done
fi
_EOF
destination = "local/init-system.properties.sh"
perms = "755"
}
volume_mount {
volume = "data"
destination = "/data"
}
resources {
cpu = 200
memory = 1024
}
}
task "mongo" {
driver = "docker"
lifecycle {
hook = "prestart"
sidecar = true
}
config {
image = "danielberteaud/mongo:5.0.24.1-1"
command = "mongod"
args = [
"--config",
"/local/mongod.conf"
]
}
template {
data = <<_EOF
net:
bindIp: 127.0.0.1
storage:
dbPath: /data/db
directoryPerDB: true
wiredTiger:
engineConfig:
directoryForIndexes: true
journalCompressor: snappy
collectionConfig:
blockCompressor: snappy
_EOF
destination = "local/mongod.conf"
}
volume_mount {
volume = "mongo"
destination = "/data/db"
}
resources {
cpu = 100
memory = 256
}
}
}
}

View File

@ -0,0 +1,3 @@
path "kv/data/service/unifi" {
capabilities = ["read"]
}