Merge branch 'master' into v3

This commit is contained in:
Maxi Quoß
2023-01-27 01:31:36 +01:00
3 changed files with 570 additions and 0 deletions

58
.github/workflows/master.yml vendored Normal file
View File

@@ -0,0 +1,58 @@
name: CI to Docker Hub
on:
push:
tags:
- "v*.*.*"
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Check Out Repo
uses: actions/checkout@v2
- name: Prepare tags
id: prep
run: |
DOCKER_IMAGE=${{ secrets.DOCKER_HUB_USERNAME }}/upsnap
VERSION=${GITHUB_REF#refs/tags/}
TAGS="${DOCKER_IMAGE}:${VERSION},${DOCKER_IMAGE}:${VERSION:0:2},${DOCKER_IMAGE}:latest"
echo ::set-output name=tags::${TAGS}
- name: Cache Docker layers
uses: actions/cache@v2
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v1
- name: Login to Docker Hub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
- name: Build and push
id: docker_build
uses: docker/build-push-action@v2
with:
context: ./
file: ./Dockerfile
platforms: linux/amd64,linux/arm64,linux/arm/v7
builder: ${{ steps.buildx.outputs.name }}
push: true
tags: ${{ steps.prep.outputs.tags }}
cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache
- name: Image digest
run: echo ${{ steps.docker_build.outputs.digest }}

View File

@@ -0,0 +1,177 @@
"""
Django settings for backend project.
Generated by 'django-admin startproject' using Django 4.0.2.
For more information on this file, see
https://docs.djangoproject.com/en/4.0/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/4.0/ref/settings/
"""
import os
from django.core.management.utils import get_random_secret_key
from pathlib import Path
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/4.0/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = os.getenv('DJANGO_SECRET_KEY', get_random_secret_key())
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = os.getenv('DJANGO_DEBUG', 'False') == 'True'
ALLOWED_HOSTS = ["*"]
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'channels',
'django_celery_beat',
'wol'
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'whitenoise.middleware.WhiteNoiseMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'backend.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'backend.wsgi.application'
WSGI_APPLICATION = 'backend.wsgi.application'
ASGI_APPLICATION = 'backend.asgi.application'
CELERY_BROKER_URL = f"redis://{os.getenv('REDIS_HOST', '127.0.0.1')}:{os.getenv('REDIS_PORT', 6379)}"
CELERY_BEAT_SCHEDULER = 'django_celery_beat.schedulers:DatabaseScheduler'
# Database
# https://docs.djangoproject.com/en/4.0/ref/settings/#databases
if os.getenv("DB_TYPE") == "postgres":
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql_psycopg2",
"NAME": os.getenv("DB_NAME", "upsnap"),
"USER": os.getenv("DB_USER", "upsnap"),
"PASSWORD": os.getenv("DB_PASSWORD", "upsnap"),
"HOST": os.getenv("DB_HOST", "127.0.0.1"),
"PORT": os.getenv("DB_PORT", 5432),
"OPTIONS": {
"connect_timeout": 30
}
}
}
elif os.getenv("DB_TYPE") == "mysql":
DATABASES = {
"default": {
"ENGINE": "django.db.backends.mysql",
"NAME": os.getenv("DB_NAME", "upsnap"),
"USER": os.getenv("DB_USER", "upsnap"),
"PASSWORD": os.getenv("DB_PASSWORD", "upsnap"),
"HOST": os.getenv("DB_HOST", "127.0.0.1"),
"PORT": os.getenv("DB_PORT", 3306),
"OPTIONS": {
"connect_timeout": 30
}
}
}
elif os.getenv("DB_TYPE") == "sqlite":
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": "/app/backend/db/db.sqlite3",
}
}
CHANNEL_LAYERS = {
"default": {
"BACKEND": "channels_redis.core.RedisChannelLayer",
"CONFIG": {
"hosts": [(os.getenv("REDIS_HOST", "127.0.0.1"), os.getenv("REDIS_PORT", 6379))],
"capacity": 1000,
"expiry": 10
}
}
}
# Password validation
# https://docs.djangoproject.com/en/4.0/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/4.0/topics/i18n/
LANGUAGE_CODE = os.getenv("DJANGO_LANGUAGE_CODE", "en")
TIME_ZONE = os.getenv("DJANGO_TIME_ZONE", "UTC")
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.0/howto/static-files/
STATIC_URL = 'static/'
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
# Default primary key field type
# https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
VERSION = "v2.3.2"

View File

@@ -0,0 +1,335 @@
import ipaddress
import json
import os
import subprocess
import pytz
from channels.db import database_sync_to_async
from channels.generic.websocket import AsyncWebsocketConsumer
from django_celery_beat.models import (CrontabSchedule, IntervalSchedule,
PeriodicTask)
from wol.commands import shutdown, wake
from wol.models import Device, Port, Settings, Websocket
class WSConsumer(AsyncWebsocketConsumer):
async def connect(self):
await self.channel_layer.group_add("wol", self.channel_name)
await self.accept()
await self.send(text_data=json.dumps({
"type": "init",
"message": {
"devices": await self.get_all_devices(),
"settings": await self.get_settings()
}
}))
await self.add_visitor()
await self.channel_layer.group_send(
"wol", {
"type": "send_group",
"message": {
"type": "visitor",
"message": await self.get_visitors()
}
}
)
async def disconnect(self, _):
await self.channel_layer.group_discard("wol", self.channel_name)
await self.remove_visitor()
await self.channel_layer.group_send(
"wol", {
"type": "send_group",
"message": {
"type": "visitor",
"message": await self.get_visitors()
}
}
)
async def receive(self, text_data=None):
received = json.loads(text_data)
if received["type"] == "wake":
await self.wake_device(received["id"])
await self.channel_layer.group_send(
"wol", {
"type": "send_group",
"message": {
"type": "pending",
"message": received["id"]
}
}
)
elif received["type"] == "shutdown":
await self.shutdown_device(received["id"])
await self.channel_layer.group_send(
"wol", {
"type": "send_group",
"message": {
"type": "pending",
"message": received["id"]
}
}
)
elif received["type"] == "delete_device":
await self.delete_device(received["id"])
await self.channel_layer.group_send(
"wol", {
"type": "send_group",
"message": {
"type": "delete",
"message": received["id"]
}
}
)
elif received["type"] == "update_device":
try:
await self.update_device(received["data"])
await self.send(text_data=json.dumps({
"type": "operationStatus",
"message": "Success"
}))
except Exception:
await self.send(text_data=json.dumps({
"type": "operationStatus",
"message": "Error"
}))
elif received["type"] == "update_port":
await self.update_port(received["data"])
elif received["type"] == "update_settings":
await self.update_settings(received["data"])
elif received["type"] == "scan_network":
await self.send(text_data=json.dumps({
"type": "scan_network",
"message": await self.scan_network()
}))
elif received["type"] == "backup":
await self.send(text_data=json.dumps({
"type": "backup",
"message": await self.get_all_devices()
}))
async def send_group(self, event):
await self.send(json.dumps(event["message"]))
@database_sync_to_async
def add_visitor(self):
visitor_count = Websocket.objects.first()
visitor_count.visitors += 1
visitor_count.save()
@database_sync_to_async
def remove_visitor(self):
visitor_count = Websocket.objects.first()
visitor_count.visitors -= 1
visitor_count.save()
@database_sync_to_async
def get_visitors(self):
return Websocket.objects.first().visitors
@database_sync_to_async
def wake_device(self, id):
dev = Device.objects.filter(id=id).first()
wake(dev.mac, dev.ip, dev.netmask)
@database_sync_to_async
def shutdown_device(self, id):
dev = Device.objects.filter(id=id).first()
shutdown(dev.shutdown_cmd)
@database_sync_to_async
def get_all_devices(self):
devices = Device.objects.all()
d = []
for dev in devices:
data = {
"id": dev.id,
"name": dev.name,
"ip": dev.ip,
"mac": dev.mac,
"netmask": dev.netmask,
"link": dev.link,
"ports": [],
"wake": {
"enabled": False,
"cron": ""
},
"shutdown": {
"enabled": False,
"cron": "",
"command": dev.shutdown_cmd
}
}
for p in dev.port.order_by("number"):
data["ports"].append({
"number": p.number,
"name": p.name
})
for action in ["wake", "shutdown"]:
try:
task = PeriodicTask.objects.filter(
name=f"{data['name']}-{action}",
task=f"wol.tasks.scheduled_{action}", crontab_id__isnull=False).get()
if task:
wake = CrontabSchedule.objects.get(id=task.crontab_id)
data[action]["enabled"] = task.enabled
data[action]["cron"] = " ".join(
[wake.minute, wake.hour, wake.day_of_month, wake.month_of_year, wake.day_of_week])
except PeriodicTask.DoesNotExist:
pass
d.append(data)
return d
@database_sync_to_async
def delete_device(self, id):
dev = Device.objects.get(id=id)
dev.delete()
@database_sync_to_async
def update_device(self, data):
obj, _ = Device.objects.update_or_create(
mac=data["mac"],
defaults={
"name": data["name"],
"ip": data["ip"],
"netmask": data["netmask"],
"link": data["link"] if data.get("link") else "",
"shutdown_cmd": data["shutdown"]["command"] if data.get("shutdown") else ""
}
)
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"])
obj.port.add(p)
else:
p = Port.objects.filter(number=port["number"]).first()
if p and p in obj.port.all():
obj.port.remove(p)
for action in ["wake", "shutdown"]:
if data.get(action):
if data[action]["enabled"]:
cron = data[action]["cron"].strip().split(" ")
if not len(cron) == 5:
return
minute, hour, dom, month, dow = cron
schedule, _ = CrontabSchedule.objects.get_or_create(
minute=minute,
hour=hour,
day_of_week=dow,
day_of_month=dom,
month_of_year=month,
timezone=pytz.timezone(
os.getenv("DJANGO_TIME_ZONE", "UTC"))
)
PeriodicTask.objects.update_or_create(
name=f"{data['name']}-{action}",
defaults={
"crontab": schedule,
"task": f"wol.tasks.scheduled_{action}",
"args": json.dumps([data["id"]]),
"enabled": True
}
)
else:
for task in PeriodicTask.objects.filter(name=f"{data['name']}-{action}", task=f"wol.tasks.scheduled_{action}"):
task.enabled = False
task.save()
@database_sync_to_async
def update_port(self, data):
if data.get("name"):
Port.objects.update_or_create(
number=data["number"],
defaults={
"name": data["name"]
}
)
else:
Port.objects.filter(number=data["number"]).delete()
@database_sync_to_async
def get_settings(self):
conf = Settings.objects.get(id=1)
data = {
"discovery": conf.scan_address,
"interval": conf.interval,
"scan_network": [],
"notifications": conf.notifications
}
return data
@database_sync_to_async
def update_settings(self, data):
if data["interval"] > 5:
data["interval"] = 5
Settings.objects.update_or_create(
id=1,
defaults={
"scan_address": data["discovery"],
"interval": data["interval"],
"notifications": data["notifications"]
}
)
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 scan_network(self):
data = []
conf = Settings.objects.get(id=1)
netmask = str(ipaddress.ip_network(conf.scan_address).netmask)
if not conf.scan_address:
return
p = subprocess.Popen(
["nmap", os.getenv("NMAP_ARGS", "-sP"), conf.scan_address], stdout=subprocess.PIPE)
out = p.communicate()[0].decode("utf-8")
ip_line = "Nmap scan report for"
mac_line = "MAC Address:"
for line in out.splitlines():
if line.startswith(ip_line):
line_splitted = line.split()
if len(line_splitted) == 6:
name = line_splitted[4]
ip = line_splitted[5]
ip = ip.replace("(", "")
ip = ip.replace(")", "")
else:
name = "Unknown"
ip = line_splitted[4]
elif line.startswith(mac_line):
line_splitted = line.split()
mac = line_splitted[2]
data.append({
"name": name,
"ip": ip,
"netmask": netmask,
"mac": mac,
"link": "",
"ports": [],
"wake": {
"enabled": False,
"cron": ""
},
"shutdown": {
"enabled": False,
"cron": "",
"command": ""
}
})
return data