mirror of
https://github.com/seriousm4x/UpSnap.git
synced 2026-07-23 05:53:55 -04:00
Merge branch 'master' into v3
This commit is contained in:
58
.github/workflows/master.yml
vendored
Normal file
58
.github/workflows/master.yml
vendored
Normal 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 }}
|
||||
177
app/backend/backend/settings.py
Normal file
177
app/backend/backend/settings.py
Normal 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"
|
||||
335
app/backend/wol/consumers.py
Normal file
335
app/backend/wol/consumers.py
Normal 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
|
||||
Reference in New Issue
Block a user