From 052161ac8000e9515438e9944904a195ef4ea6f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maxi=20Quo=C3=9F?= Date: Mon, 21 Feb 2022 21:42:58 +0100 Subject: [PATCH] move projects and change dockerfile --- .dockerignore | 5 + .gitignore | 2 +- Dockerfile | 39 ++-- README.md | 10 +- app/backend/backend/urls.py | 4 +- app/backend/wait-for-it.sh | 182 ++++++++++++++++++ app/backend/wol/consumers.py | 29 ++- app/backend/wol/migrations/0001_initial.py | 51 ----- ..._remove_settings_enable_console_logging.py | 17 -- .../wol/migrations/0003_settings_interval.py | 18 -- app/backend/wol/tests.py | 4 - app/frontend/src/App.svelte | 1 - app/frontend/src/components/DeviceCard.svelte | 7 +- app/frontend/src/components/Navbar.svelte | 4 +- app/{backend => }/run.sh | 11 +- docker-compose-sqlite.yml | 5 +- 16 files changed, 259 insertions(+), 130 deletions(-) create mode 100644 .dockerignore create mode 100755 app/backend/wait-for-it.sh delete mode 100644 app/backend/wol/migrations/0001_initial.py delete mode 100644 app/backend/wol/migrations/0002_remove_settings_enable_console_logging.py delete mode 100644 app/backend/wol/migrations/0003_settings_interval.py rename app/{backend => }/run.sh (84%) mode change 100644 => 100755 diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..6b942081 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,5 @@ +.git +node_modules +/app/frontend/public/build +/app/frontend/scripts +db.sqlite3 diff --git a/.gitignore b/.gitignore index d0ff4caa..e812f545 100644 --- a/.gitignore +++ b/.gitignore @@ -284,4 +284,4 @@ $RECYCLE.BIN/ # End of https://www.toptal.com/developers/gitignore/api/windows,linux,macos,python,django .vscode -app/staticfiles +staticfiles diff --git a/Dockerfile b/Dockerfile index fc3bb185..d10905b5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,28 +1,35 @@ -FROM python:3.10-alpine as base +FROM nikolaik/python-nodejs:python3.10-nodejs17-alpine as base -FROM base as builder - -ENV PYTHONUNBUFFERED 1 - -RUN apk update &&\ - apk add python3-dev musl-dev build-base gcc libffi-dev libressl-dev postgresql-dev mariadb-dev cargo &&\ - rm -rf /var/cache/apk/* &&\ - mkdir /install +# build python dependencies +FROM base as python-build WORKDIR /install -COPY requirements.txt . +ENV PYTHONUNBUFFERED 1 +RUN apk update &&\ + apk add musl-dev build-base gcc libffi-dev libressl-dev postgresql-dev mariadb-dev cargo &&\ + rm -rf /var/cache/apk/* +COPY app/backend/requirements.txt . RUN python -m pip install --no-cache-dir --upgrade pip &&\ pip install --prefix=/install --no-cache-dir -r requirements.txt -FROM base +# build svelte app +FROM base as npm-build +WORKDIR /install +COPY app/frontend/package*.json ./ +RUN npm install +COPY app/frontend/src ./src +COPY app/frontend/public ./public +COPY app/frontend/rollup.config.js ./ +RUN npm run build -COPY --from=builder /install /usr/local -COPY app /app +# build final image +FROM base WORKDIR /app +COPY --from=python-build /install /usr/local +COPY app/backend ./backend +COPY --from=npm-build /install ./frontend +COPY app/run.sh ./ RUN apk update &&\ apk add iputils nmap curl bash mariadb-dev &&\ rm -rf /var/cache/apk/* -HEALTHCHECK --interval=10s \ - CMD curl -fs "http://localhost:$DJANGO_PORT/health/" || exit 1 - CMD ["./run.sh"] diff --git a/README.md b/README.md index 214ff4c2..f1180f59 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@
- +
@@ -11,13 +11,13 @@ ## ✨ Features * Dashboard to wake up devices with 1 click -* Set date and time to schedule a wake event -* Open port scan for VNC, RDP and SSH -* Visualization of incoming websocket messages +* Set timed wake events via cron +* Scan devices for open ports you assigned +* Discover devices by scanning network +* Visualization of pings * Notifications on status changes * Devices only get pinged when there are 1 or more visitors * Dark/light mode via preferes-color-scheme -* Settings page to add/delete device and show system infos * [Docker images](https://hub.docker.com/r/seriousm4x/upsnap) for amd64, arm64, arm/v7 ## 📸 Screenshots diff --git a/app/backend/backend/urls.py b/app/backend/backend/urls.py index 41d49625..3c8a8025 100644 --- a/app/backend/backend/urls.py +++ b/app/backend/backend/urls.py @@ -14,8 +14,10 @@ Including another URLconf 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ from django.contrib import admin +from django.conf import settings +from django.conf.urls.static import static from django.urls import path urlpatterns = [ path('admin/', admin.site.urls) -] +] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) diff --git a/app/backend/wait-for-it.sh b/app/backend/wait-for-it.sh new file mode 100755 index 00000000..d990e0d3 --- /dev/null +++ b/app/backend/wait-for-it.sh @@ -0,0 +1,182 @@ +#!/usr/bin/env bash +# Use this script to test if a given TCP host/port are available + +WAITFORIT_cmdname=${0##*/} + +echoerr() { if [[ $WAITFORIT_QUIET -ne 1 ]]; then echo "$@" 1>&2; fi } + +usage() +{ + cat << USAGE >&2 +Usage: + $WAITFORIT_cmdname host:port [-s] [-t timeout] [-- command args] + -h HOST | --host=HOST Host or IP under test + -p PORT | --port=PORT TCP port under test + Alternatively, you specify the host and port as host:port + -s | --strict Only execute subcommand if the test succeeds + -q | --quiet Don't output any status messages + -t TIMEOUT | --timeout=TIMEOUT + Timeout in seconds, zero for no timeout + -- COMMAND ARGS Execute command with args after the test finishes +USAGE + exit 1 +} + +wait_for() +{ + if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then + echoerr "$WAITFORIT_cmdname: waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT" + else + echoerr "$WAITFORIT_cmdname: waiting for $WAITFORIT_HOST:$WAITFORIT_PORT without a timeout" + fi + WAITFORIT_start_ts=$(date +%s) + while : + do + if [[ $WAITFORIT_ISBUSY -eq 1 ]]; then + nc -z $WAITFORIT_HOST $WAITFORIT_PORT + WAITFORIT_result=$? + else + (echo -n > /dev/tcp/$WAITFORIT_HOST/$WAITFORIT_PORT) >/dev/null 2>&1 + WAITFORIT_result=$? + fi + if [[ $WAITFORIT_result -eq 0 ]]; then + WAITFORIT_end_ts=$(date +%s) + echoerr "$WAITFORIT_cmdname: $WAITFORIT_HOST:$WAITFORIT_PORT is available after $((WAITFORIT_end_ts - WAITFORIT_start_ts)) seconds" + break + fi + sleep 1 + done + return $WAITFORIT_result +} + +wait_for_wrapper() +{ + # In order to support SIGINT during timeout: http://unix.stackexchange.com/a/57692 + if [[ $WAITFORIT_QUIET -eq 1 ]]; then + timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --quiet --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT & + else + timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT & + fi + WAITFORIT_PID=$! + trap "kill -INT -$WAITFORIT_PID" INT + wait $WAITFORIT_PID + WAITFORIT_RESULT=$? + if [[ $WAITFORIT_RESULT -ne 0 ]]; then + echoerr "$WAITFORIT_cmdname: timeout occurred after waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT" + fi + return $WAITFORIT_RESULT +} + +# process arguments +while [[ $# -gt 0 ]] +do + case "$1" in + *:* ) + WAITFORIT_hostport=(${1//:/ }) + WAITFORIT_HOST=${WAITFORIT_hostport[0]} + WAITFORIT_PORT=${WAITFORIT_hostport[1]} + shift 1 + ;; + --child) + WAITFORIT_CHILD=1 + shift 1 + ;; + -q | --quiet) + WAITFORIT_QUIET=1 + shift 1 + ;; + -s | --strict) + WAITFORIT_STRICT=1 + shift 1 + ;; + -h) + WAITFORIT_HOST="$2" + if [[ $WAITFORIT_HOST == "" ]]; then break; fi + shift 2 + ;; + --host=*) + WAITFORIT_HOST="${1#*=}" + shift 1 + ;; + -p) + WAITFORIT_PORT="$2" + if [[ $WAITFORIT_PORT == "" ]]; then break; fi + shift 2 + ;; + --port=*) + WAITFORIT_PORT="${1#*=}" + shift 1 + ;; + -t) + WAITFORIT_TIMEOUT="$2" + if [[ $WAITFORIT_TIMEOUT == "" ]]; then break; fi + shift 2 + ;; + --timeout=*) + WAITFORIT_TIMEOUT="${1#*=}" + shift 1 + ;; + --) + shift + WAITFORIT_CLI=("$@") + break + ;; + --help) + usage + ;; + *) + echoerr "Unknown argument: $1" + usage + ;; + esac +done + +if [[ "$WAITFORIT_HOST" == "" || "$WAITFORIT_PORT" == "" ]]; then + echoerr "Error: you need to provide a host and port to test." + usage +fi + +WAITFORIT_TIMEOUT=${WAITFORIT_TIMEOUT:-15} +WAITFORIT_STRICT=${WAITFORIT_STRICT:-0} +WAITFORIT_CHILD=${WAITFORIT_CHILD:-0} +WAITFORIT_QUIET=${WAITFORIT_QUIET:-0} + +# Check to see if timeout is from busybox? +WAITFORIT_TIMEOUT_PATH=$(type -p timeout) +WAITFORIT_TIMEOUT_PATH=$(realpath $WAITFORIT_TIMEOUT_PATH 2>/dev/null || readlink -f $WAITFORIT_TIMEOUT_PATH) + +WAITFORIT_BUSYTIMEFLAG="" +if [[ $WAITFORIT_TIMEOUT_PATH =~ "busybox" ]]; then + WAITFORIT_ISBUSY=1 + # Check if busybox timeout uses -t flag + # (recent Alpine versions don't support -t anymore) + if timeout &>/dev/stdout | grep -q -e '-t '; then + WAITFORIT_BUSYTIMEFLAG="-t" + fi +else + WAITFORIT_ISBUSY=0 +fi + +if [[ $WAITFORIT_CHILD -gt 0 ]]; then + wait_for + WAITFORIT_RESULT=$? + exit $WAITFORIT_RESULT +else + if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then + wait_for_wrapper + WAITFORIT_RESULT=$? + else + wait_for + WAITFORIT_RESULT=$? + fi +fi + +if [[ $WAITFORIT_CLI != "" ]]; then + if [[ $WAITFORIT_RESULT -ne 0 && $WAITFORIT_STRICT -eq 1 ]]; then + echoerr "$WAITFORIT_cmdname: strict mode, refusing to execute subprocess" + exit $WAITFORIT_RESULT + fi + exec "${WAITFORIT_CLI[@]}" +else + exit $WAITFORIT_RESULT +fi diff --git a/app/backend/wol/consumers.py b/app/backend/wol/consumers.py index fa0556dd..5b396bc7 100644 --- a/app/backend/wol/consumers.py +++ b/app/backend/wol/consumers.py @@ -1,4 +1,3 @@ -from array import array import ipaddress import json import subprocess @@ -8,7 +7,7 @@ from channels.generic.websocket import AsyncWebsocketConsumer from django.utils.dateparse import parse_datetime from django.utils.timezone import make_aware from django_celery_beat.models import (ClockedSchedule, CrontabSchedule, - PeriodicTask) + IntervalSchedule, PeriodicTask) from wol.models import Device, Port, Settings, Websocket from wol.wake import wake @@ -141,11 +140,13 @@ class WSConsumer(AsyncWebsocketConsumer): "open": False }) try: - task = PeriodicTask.objects.filter(name=data["name"], task="wol.tasks.scheduled_wake", crontab_id__isnull=False).get() + task = PeriodicTask.objects.filter( + name=data["name"], task="wol.tasks.scheduled_wake", crontab_id__isnull=False).get() if task: cron = CrontabSchedule.objects.get(id=task.crontab_id) data["cron"]["enabled"] = task.enabled - data["cron"]["value"] = " ".join([cron.minute, cron.hour, cron.day_of_week, cron.day_of_month, cron.month_of_year]) + data["cron"]["value"] = " ".join( + [cron.minute, cron.hour, cron.day_of_week, cron.day_of_month, cron.month_of_year]) except PeriodicTask.DoesNotExist: pass d.append(data) @@ -169,11 +170,13 @@ class WSConsumer(AsyncWebsocketConsumer): if data.get("ports"): for port in data.get("ports"): if port["checked"]: - p, _ = Port.objects.get_or_create(number=port["number"], name=port["name"]) + p, _ = Port.objects.get_or_create( + number=port["number"], name=port["name"]) obj.port.add(p) else: - p = Port.objects.get(number=port["number"]) - obj.port.remove(p) + p = Port.objects.filter(number=port["number"]) + if p.exists(): + obj.port.remove(p) if data.get("cron"): if data["cron"]["enabled"]: @@ -227,6 +230,8 @@ class WSConsumer(AsyncWebsocketConsumer): @database_sync_to_async def update_settings(self, data): + if data["interval"] > 5: + data["interval"] = 5 Settings.objects.update_or_create( id=1, defaults={ @@ -235,6 +240,16 @@ class WSConsumer(AsyncWebsocketConsumer): "interval": data["interval"] } ) + schedule, _ = IntervalSchedule.objects.get_or_create( + every=data["interval"], + period=IntervalSchedule.SECONDS, + ) + PeriodicTask.objects.update_or_create( + task="wol.tasks.ping_all_devices", + defaults={ + "interval": schedule + } + ) @database_sync_to_async def celery_create_scheduled_wake(self, data): diff --git a/app/backend/wol/migrations/0001_initial.py b/app/backend/wol/migrations/0001_initial.py deleted file mode 100644 index afc50341..00000000 --- a/app/backend/wol/migrations/0001_initial.py +++ /dev/null @@ -1,51 +0,0 @@ -# Generated by Django 3.2.12 on 2022-02-20 18:24 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ] - - operations = [ - migrations.CreateModel( - name='Port', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('number', models.PositiveIntegerField()), - ('name', models.SlugField()), - ], - ), - migrations.CreateModel( - name='Settings', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('enable_notifications', models.BooleanField(default=True)), - ('enable_console_logging', models.BooleanField(default=False)), - ('sort_by', models.SlugField(default='name')), - ('scan_address', models.GenericIPAddressField(blank=True, null=True)), - ], - ), - migrations.CreateModel( - name='Websocket', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('visitors', models.PositiveSmallIntegerField(default=0)), - ], - ), - migrations.CreateModel( - name='Device', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.SlugField(default='Unknown', max_length=100)), - ('ip', models.GenericIPAddressField()), - ('mac', models.CharField(max_length=17)), - ('netmask', models.CharField(default='255.255.255.0', max_length=15)), - ('scheduled_wake', models.DateTimeField(blank=True, null=True)), - ('port', models.ManyToManyField(blank=True, to='wol.Port')), - ], - ), - ] diff --git a/app/backend/wol/migrations/0002_remove_settings_enable_console_logging.py b/app/backend/wol/migrations/0002_remove_settings_enable_console_logging.py deleted file mode 100644 index 31696609..00000000 --- a/app/backend/wol/migrations/0002_remove_settings_enable_console_logging.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 3.2.12 on 2022-02-20 19:41 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('wol', '0001_initial'), - ] - - operations = [ - migrations.RemoveField( - model_name='settings', - name='enable_console_logging', - ), - ] diff --git a/app/backend/wol/migrations/0003_settings_interval.py b/app/backend/wol/migrations/0003_settings_interval.py deleted file mode 100644 index 7db62a5c..00000000 --- a/app/backend/wol/migrations/0003_settings_interval.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 3.2.12 on 2022-02-20 19:53 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('wol', '0002_remove_settings_enable_console_logging'), - ] - - operations = [ - migrations.AddField( - model_name='settings', - name='interval', - field=models.PositiveSmallIntegerField(blank=True, null=True), - ), - ] diff --git a/app/backend/wol/tests.py b/app/backend/wol/tests.py index 32bf5a30..7ce503c2 100644 --- a/app/backend/wol/tests.py +++ b/app/backend/wol/tests.py @@ -1,7 +1,3 @@ from django.test import TestCase # Create your tests here. - -from celery.schedules import crontab_parser - -print(dir(crontab_parser().parse("0 9 * * 1,5"))) diff --git a/app/frontend/src/App.svelte b/app/frontend/src/App.svelte index a5bb0eb4..5fa20347 100644 --- a/app/frontend/src/App.svelte +++ b/app/frontend/src/App.svelte @@ -38,7 +38,6 @@ } } else if (currentMessage.type == "wake") { // set device waking - console.log("WAKE:", currentMessage.message); setWake(currentMessage.message) } else if (currentMessage.type == "visitor") { // update visitor count diff --git a/app/frontend/src/components/DeviceCard.svelte b/app/frontend/src/components/DeviceCard.svelte index 0ff71487..14aa7d81 100644 --- a/app/frontend/src/components/DeviceCard.svelte +++ b/app/frontend/src/components/DeviceCard.svelte @@ -144,10 +144,13 @@
Ports

Select ports to check if they are open.

+ {#if modalDevice.ports.length === 0} +

No ports available. Add ports below.

+ {/if} {#each modalDevice.ports as port}
- - + +
{/each} diff --git a/app/frontend/src/components/Navbar.svelte b/app/frontend/src/components/Navbar.svelte index f3b78741..0b30e986 100644 --- a/app/frontend/src/components/Navbar.svelte +++ b/app/frontend/src/components/Navbar.svelte @@ -217,8 +217,8 @@
- - + +
diff --git a/app/backend/run.sh b/app/run.sh old mode 100644 new mode 100755 similarity index 84% rename from app/backend/run.sh rename to app/run.sh index 026e0ff9..1f067467 --- a/app/backend/run.sh +++ b/app/run.sh @@ -1,5 +1,7 @@ #!/bin/sh +cd /app/backend/ || exit + # wait for db and redis if [ "${DB_TYPE}" != "sqlite" ]; then /usr/bin/env bash ./wait-for-it.sh "${DB_HOST}":"${DB_PORT}" -t 300 -s @@ -21,8 +23,11 @@ fi python manage.py makemigrations python manage.py migrate python manage.py collectstatic --noinput -python manage shell < setup.py - +python manage.py shell < setup.py celery -A backend worker & celery -A backend beat & -gunicorn --bind 0.0.0.0:"$DJANGO_PORT" --workers 4 backend.asgi:application -k uvicorn.workers.UvicornWorker +gunicorn --bind 0.0.0.0:"$DJANGO_PORT" --workers 4 backend.asgi:application -k uvicorn.workers.UvicornWorker & + + +cd /app/frontend/ || exit +npm start diff --git a/docker-compose-sqlite.yml b/docker-compose-sqlite.yml index bc6c8c9f..466e4968 100644 --- a/docker-compose-sqlite.yml +++ b/docker-compose-sqlite.yml @@ -2,14 +2,15 @@ version: "3" services: upsnap_django: container_name: upsnap_django - image: seriousm4x/upsnap:latest + #image: seriousm4x/upsnap:latest + build: . network_mode: host restart: unless-stopped environment: - DJANGO_SUPERUSER_USER=admin - DJANGO_SUPERUSER_PASSWORD=admin - DJANGO_SECRET_KEY=secret - - DJANGO_DEBUG=False + - DJANGO_DEBUG=True - DJANGO_LANGUAGE_CODE=de - DJANGO_TIME_ZONE=Europe/Berlin - DJANGO_PORT=8000