mirror of
https://github.com/seriousm4x/UpSnap.git
synced 2026-07-31 09:28:58 -04:00
add cron for wake
This commit is contained in:
@@ -6,14 +6,4 @@ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "backend.settings")
|
||||
|
||||
app = Celery("dango_wol")
|
||||
app.config_from_object("django.conf:settings", namespace="CELERY")
|
||||
app.conf.beat_schedule = {
|
||||
"ping_devices": {
|
||||
"task": "wol.tasks.status",
|
||||
"schedule": int(os.getenv("PING_INTERVAL"))
|
||||
},
|
||||
"scheduled_wakes": {
|
||||
"task": "wol.tasks.scheduled_wakes",
|
||||
"schedule": 1
|
||||
}
|
||||
}
|
||||
app.autodiscover_tasks()
|
||||
|
||||
@@ -38,8 +38,9 @@ INSTALLED_APPS = [
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'wol',
|
||||
'channels'
|
||||
'channels',
|
||||
'django_celery_beat',
|
||||
'wol'
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
@@ -74,6 +75,7 @@ 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
|
||||
@@ -119,7 +121,9 @@ CHANNEL_LAYERS = {
|
||||
"default": {
|
||||
"BACKEND": "channels_redis.core.RedisChannelLayer",
|
||||
"CONFIG": {
|
||||
"hosts": [(os.getenv("REDIS_HOST", "127.0.0.1"), os.getenv("REDIS_PORT", 6379))]
|
||||
"hosts": [(os.getenv("REDIS_HOST", "127.0.0.1"), os.getenv("REDIS_PORT", 6379))],
|
||||
"capacity": 1000,
|
||||
"expiry": 10
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
28
backend/run.sh
Normal file
28
backend/run.sh
Normal file
@@ -0,0 +1,28 @@
|
||||
#!/bin/sh
|
||||
|
||||
# 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
|
||||
sleep 20
|
||||
fi
|
||||
/usr/bin/env bash ./wait-for-it.sh "${REDIS_HOST}":"${REDIS_PORT}" -t 300 -s
|
||||
|
||||
# init django
|
||||
python manage.py makemigrations
|
||||
python manage.py migrate
|
||||
python manage.py collectstatic --noinput
|
||||
python manage shell < setup.py
|
||||
|
||||
# set ping interval
|
||||
if [ -z "$PING_INTERVAL" ]; then
|
||||
PING_INTERVAL=5
|
||||
elif [ "$PING_INTERVAL" -lt 5 ]; then
|
||||
echo ""
|
||||
echo "Ping interval lower than 5 seconds is not recommended. Please use an interval of 5 seconds or higher. Automatically set to 5 seconds."
|
||||
echo ""
|
||||
PING_INTERVAL=5
|
||||
fi
|
||||
|
||||
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
|
||||
37
backend/setup.py
Normal file
37
backend/setup.py
Normal file
@@ -0,0 +1,37 @@
|
||||
import os
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
from wol.models import Settings, Websocket
|
||||
from django_celery_beat.models import PeriodicTask, IntervalSchedule
|
||||
|
||||
|
||||
# create superuser
|
||||
if not User.objects.filter(username=os.getenv("DJANGO_SUPERUSER_USER")).exists():
|
||||
User.objects.create_superuser(os.getenv("DJANGO_SUPERUSER_USER"), password=os.getenv("DJANGO_SUPERUSER_PASSWORD"))
|
||||
else:
|
||||
print('Django user exists')
|
||||
|
||||
# reset visitors
|
||||
[i.delete() for i in Websocket.objects.all()]
|
||||
Websocket.objects.create(visitors=0)
|
||||
|
||||
# notifications
|
||||
Settings.objects.update_or_create(
|
||||
id=1,
|
||||
defaults={
|
||||
"enable_notifications": os.getenv("ENABLE_NOTIFICATIONS")
|
||||
}
|
||||
)
|
||||
|
||||
# register device ping task
|
||||
schedule, created = IntervalSchedule.objects.get_or_create(
|
||||
every=int(os.getenv("PING_INTERVAL")),
|
||||
period=IntervalSchedule.SECONDS,
|
||||
)
|
||||
if created:
|
||||
PeriodicTask.objects.create(
|
||||
interval=schedule,
|
||||
name="Ping all devices",
|
||||
task="wol.tasks.ping_all_devices"
|
||||
)
|
||||
@@ -8,6 +8,7 @@ from django.utils.timezone import make_aware
|
||||
|
||||
from wol.models import Device, Port, Websocket
|
||||
from wol.wake import wake
|
||||
from django_celery_beat.models import PeriodicTask, ClockedSchedule, CrontabSchedule
|
||||
|
||||
|
||||
class WSConsumer(AsyncWebsocketConsumer):
|
||||
@@ -46,9 +47,8 @@ class WSConsumer(AsyncWebsocketConsumer):
|
||||
async def receive(self, text_data=None):
|
||||
received = json.loads(text_data)
|
||||
if received["type"] == "wake":
|
||||
dev = await self.get_json_from_device_id(received["id"])
|
||||
wake(dev["fields"]["mac"], dev["fields"]
|
||||
["ip"], dev["fields"]["netmask"])
|
||||
dev = await self.get_device(received["id"])
|
||||
wake(dev.mac, dev.ip, dev.netmask)
|
||||
|
||||
await self.channel_layer.group_send(
|
||||
"wol", {
|
||||
@@ -59,38 +59,6 @@ class WSConsumer(AsyncWebsocketConsumer):
|
||||
}
|
||||
}
|
||||
)
|
||||
elif received["type"] == "add_schedule":
|
||||
if not received["datetime"]:
|
||||
return
|
||||
d = make_aware(parse_datetime(received["datetime"]))
|
||||
await self.add_schedule(received["id"], d)
|
||||
dev = await self.get_json_from_device_id(received["id"])
|
||||
await self.channel_layer.group_send(
|
||||
"wol", {
|
||||
"type": "send_group",
|
||||
"message": {
|
||||
"add_schedule": {
|
||||
"id": received["id"],
|
||||
"name": dev["fields"]["name"],
|
||||
"datetime": str(d.isoformat())
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
elif received["type"] == "delete_schedule":
|
||||
await self.delete_schedule(received["id"])
|
||||
dev = await self.get_json_from_device_id(received["id"])
|
||||
await self.channel_layer.group_send(
|
||||
"wol", {
|
||||
"type": "send_group",
|
||||
"message": {
|
||||
"delete_schedule": {
|
||||
"id": received["id"],
|
||||
"name": dev["fields"]["name"]
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
elif received["type"] == "delete_device":
|
||||
await self.delete_device(received["id"])
|
||||
await self.channel_layer.group_send(
|
||||
@@ -106,6 +74,8 @@ class WSConsumer(AsyncWebsocketConsumer):
|
||||
await self.update_device(received["data"])
|
||||
elif received["type"] == "update_port":
|
||||
await self.update_port(received["data"])
|
||||
elif received["type"] == "celery":
|
||||
await self.celery_create_scheduled_wake(received["data"])
|
||||
|
||||
|
||||
async def send_group(self, event):
|
||||
@@ -127,6 +97,12 @@ class WSConsumer(AsyncWebsocketConsumer):
|
||||
def get_visitors(self):
|
||||
return Websocket.objects.first().visitors
|
||||
|
||||
@database_sync_to_async
|
||||
def get_device(self, id):
|
||||
dev = Device.objects.filter(id=id).first()
|
||||
return dev
|
||||
|
||||
|
||||
@database_sync_to_async
|
||||
def get_all_devices(self):
|
||||
devices = Device.objects.all()
|
||||
@@ -138,7 +114,11 @@ class WSConsumer(AsyncWebsocketConsumer):
|
||||
"ip": dev.ip,
|
||||
"mac": dev.mac,
|
||||
"netmask": dev.netmask,
|
||||
"ports": []
|
||||
"ports": [],
|
||||
"cron": {
|
||||
"enabled": False,
|
||||
"value": ""
|
||||
}
|
||||
}
|
||||
for p in Port.objects.all().order_by("number"):
|
||||
data["ports"].append({
|
||||
@@ -147,6 +127,14 @@ class WSConsumer(AsyncWebsocketConsumer):
|
||||
"checked": False,
|
||||
"open": False
|
||||
})
|
||||
try:
|
||||
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])
|
||||
except PeriodicTask.DoesNotExist:
|
||||
pass
|
||||
d.append(data)
|
||||
return d
|
||||
|
||||
@@ -174,6 +162,33 @@ class WSConsumer(AsyncWebsocketConsumer):
|
||||
p = Port.objects.get(number=port["number"])
|
||||
obj.port.remove(p)
|
||||
|
||||
if data["cron"]["enabled"]:
|
||||
cron_value = data["cron"]["value"].strip().split(" ")
|
||||
if not len(cron_value) == 5:
|
||||
return
|
||||
minute, hour, dom, month, dow = cron_value
|
||||
schedule, _ = CrontabSchedule.objects.get_or_create(
|
||||
minute=minute,
|
||||
hour=hour,
|
||||
day_of_week=dow,
|
||||
day_of_month=dom,
|
||||
month_of_year=month
|
||||
)
|
||||
PeriodicTask.objects.update_or_create(
|
||||
name=data["name"],
|
||||
defaults={
|
||||
"crontab": schedule,
|
||||
"task": "wol.tasks.scheduled_wake",
|
||||
"args": json.dumps([data["id"]]),
|
||||
"enabled": True
|
||||
}
|
||||
)
|
||||
else:
|
||||
for task in PeriodicTask.objects.filter(name=data["name"], task="wol.tasks.scheduled_wake"):
|
||||
task.enabled = False
|
||||
task.save()
|
||||
|
||||
|
||||
@database_sync_to_async
|
||||
def update_port(self, data):
|
||||
if data.get("name"):
|
||||
@@ -187,18 +202,14 @@ class WSConsumer(AsyncWebsocketConsumer):
|
||||
Port.objects.filter(number=data["number"]).delete()
|
||||
|
||||
@database_sync_to_async
|
||||
def get_json_from_device_id(self, id):
|
||||
dev = Device.objects.filter(id=id)
|
||||
return serializers.serialize("python", dev)[0]
|
||||
|
||||
@database_sync_to_async
|
||||
def add_schedule(self, id, datetime):
|
||||
dev = Device.objects.filter(id=id).get()
|
||||
dev.scheduled_wake = datetime
|
||||
dev.save()
|
||||
|
||||
@database_sync_to_async
|
||||
def delete_schedule(self, id):
|
||||
dev = Device.objects.filter(id=id).get()
|
||||
dev.scheduled_wake = None
|
||||
dev.save()
|
||||
def celery_create_scheduled_wake(self, data):
|
||||
aware_time = make_aware(parse_datetime(data["time"]))
|
||||
schedule, _ = ClockedSchedule.objects.get_or_create(
|
||||
clocked_time=aware_time
|
||||
)
|
||||
PeriodicTask.objects.get_or_create(
|
||||
clocked=schedule,
|
||||
name=data["name"],
|
||||
task="wol.tasks.scheduled_wake",
|
||||
args=json.dumps([data["id"]])
|
||||
)
|
||||
|
||||
@@ -5,8 +5,7 @@ import threading
|
||||
from asgiref.sync import async_to_sync
|
||||
from celery import shared_task
|
||||
from channels.layers import get_channel_layer
|
||||
from django.core import serializers
|
||||
from django.utils import timezone
|
||||
from django_celery_beat.models import PeriodicTask, CrontabSchedule
|
||||
|
||||
from wol.models import Device, Port, Websocket
|
||||
from wol.wake import wake
|
||||
@@ -37,8 +36,14 @@ class WolDevice:
|
||||
"mac": dev.mac,
|
||||
"netmask": dev.netmask,
|
||||
"up": False,
|
||||
"ports": []
|
||||
"ports": [],
|
||||
"cron": {
|
||||
"enabled": False,
|
||||
"value": ""
|
||||
}
|
||||
}
|
||||
|
||||
# add ports
|
||||
for p in Port.objects.all().order_by("number"):
|
||||
data["ports"].append({
|
||||
"number": p.number,
|
||||
@@ -47,6 +52,7 @@ class WolDevice:
|
||||
"open": False
|
||||
})
|
||||
|
||||
# set status for device and ports
|
||||
if self.ping_device(dev.ip):
|
||||
data["up"] = True
|
||||
for port in dev.port.all():
|
||||
@@ -58,6 +64,16 @@ class WolDevice:
|
||||
data["ports"][index]["checked"] = True
|
||||
data["ports"][index]["open"] = False
|
||||
|
||||
# set cron for scheduled wake
|
||||
try:
|
||||
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])
|
||||
except PeriodicTask.DoesNotExist:
|
||||
pass
|
||||
|
||||
async_to_sync(channel_layer.group_send)(
|
||||
"wol", {"type": "send_group", "message": {
|
||||
"type": "status",
|
||||
@@ -66,7 +82,7 @@ class WolDevice:
|
||||
|
||||
|
||||
@shared_task
|
||||
def status():
|
||||
def ping_all_devices():
|
||||
if Websocket.objects.first().visitors == 0:
|
||||
return
|
||||
|
||||
@@ -79,26 +95,12 @@ def status():
|
||||
|
||||
|
||||
@shared_task
|
||||
def scheduled_wakes():
|
||||
if Websocket.objects.first().visitors == 0:
|
||||
def scheduled_wake(id):
|
||||
try:
|
||||
device = Device.objects.get(id=id)
|
||||
except Device.DoesNotExist:
|
||||
for task in PeriodicTask.objects.filter(args=id):
|
||||
task.delete()
|
||||
return
|
||||
|
||||
devices = Device.objects.all()
|
||||
|
||||
for dev in devices:
|
||||
if dev.scheduled_wake and dev.scheduled_wake <= timezone.now():
|
||||
wake(dev.mac, dev.ip, dev.netmask)
|
||||
dev.scheduled_wake = None
|
||||
dev.save()
|
||||
async_to_sync(channel_layer.group_send)(
|
||||
"wol", {
|
||||
"type": "send_group",
|
||||
"message": {
|
||||
"type": "wake_schedule",
|
||||
"message": {
|
||||
"id": dev.id,
|
||||
"name": dev.name
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
wake(device.mac, device.ip, device.netmask)
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
|
||||
from celery.schedules import crontab_parser
|
||||
|
||||
print(dir(crontab_parser().parse("0 9 * * 1,5")))
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script>
|
||||
import store from '../store.js';
|
||||
export let device;
|
||||
console.log(device);
|
||||
|
||||
let modalDevice = JSON.parse(JSON.stringify(device));
|
||||
let customPort = {}
|
||||
@@ -82,6 +83,9 @@
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto" class:d-none={!device.cron.enabled} data-bs-toggle="tooltip" title="Crontab: {device.cron.value}">
|
||||
<i class="fa-solid fa-repeat fa-2x text-muted"></i>
|
||||
</div>
|
||||
<div class="col-auto hover" data-bs-toggle="modal" data-bs-target="#device-modal-{device.id}" role="button" on:click="{() => openModal()}">
|
||||
<i class="fa-solid fa-ellipsis-vertical fa-2x"></i>
|
||||
</div>
|
||||
@@ -140,6 +144,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<h5 class="fw-bold">Ports</h5>
|
||||
<p>Select ports to check if they are open.</p>
|
||||
{#each modalDevice.ports as port}
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" id="{device.id}-port-{port.name}" bind:checked="{port['checked']}">
|
||||
@@ -152,8 +157,34 @@
|
||||
<input type="number" min="1" max="65535" class="form-control" placeholder="Port" aria-label="Port" aria-describedby="button-addon2" bind:value={customPort.number} on:input={validatePort}>
|
||||
<button class="btn btn-outline-secondary" type="button" id="button-addon2" on:click="{updatePort}">Update Port</button>
|
||||
</div>
|
||||
<p>Port must be between 1 and 65535. Enter the same port with a differen name to change it. Leave name empty to delete port.</p>
|
||||
<div class="callout callout-info">
|
||||
<p class="mb-0">Ports must be between 1 and 65535. Enter the same port with a differen name to change it. Leave name empty to delete port.</p>
|
||||
</div>
|
||||
<h5 class="fw-bold">Scheduled wake</h5>
|
||||
<p>Wake your devices at a given time.</p>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="flexRadioSchedule" id="flexRadioDisabled{modalDevice.id}"
|
||||
bind:group={modalDevice["cron"]["enabled"]} value={false} checked={!modalDevice["cron"]["enabled"]}>
|
||||
<label class="form-check-label" for="flexRadioDisabled{modalDevice.id}">
|
||||
Disabled
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="flexRadioSchedule" id="flexRadioCron{modalDevice.id}"
|
||||
bind:group={modalDevice["cron"]["enabled"]} value={true} checked={modalDevice["cron"]["enabled"]}>
|
||||
<label class="form-check-label" for="flexRadioCron{modalDevice.id}">
|
||||
Cron
|
||||
</label>
|
||||
</div>
|
||||
<input id="inputCron{modalDevice.id}" type="text" class="form-control" placeholder="* /4 * * *" aria-label="Crontab" aria-describedby="addon-wrapping" bind:value={modalDevice["cron"]["value"]} disabled={!modalDevice["cron"]["enabled"]}>
|
||||
<div class="callout callout-info">
|
||||
<p class="my-2">The cron field uses common cron syntax. Valid examples:</p>
|
||||
<pre>Minute Hour DayOfMonth Month DayOfWeek
|
||||
* /4 * * * (Wake every 4 hours)
|
||||
0 9 * * 1-5 (Wake from Mo-Fr at 9 a.m.)
|
||||
</pre>
|
||||
<p class="mb-0">Read more about <a href="https://linux.die.net/man/5/crontab" target="_blank">valid syntax</a> here or <a href="https://crontab.guru/" target="_blank">generate</a> it. Expressions starting with "@..." are not supported.</p>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer justify-content-between">
|
||||
@@ -168,6 +199,38 @@
|
||||
<style lang="scss">
|
||||
@import "../variables.scss";
|
||||
|
||||
button {
|
||||
&.btn-outline-success {
|
||||
border-color: $success;
|
||||
&:hover {
|
||||
background-color: $success;
|
||||
}
|
||||
}
|
||||
|
||||
&.btn-outline-danger {
|
||||
border-color: $danger;
|
||||
&:hover {
|
||||
background-color: $danger;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
i {
|
||||
&.success {
|
||||
color: $success;
|
||||
}
|
||||
|
||||
&.danger {
|
||||
color: $danger;
|
||||
}
|
||||
}
|
||||
|
||||
pre {
|
||||
white-space: pre-line;
|
||||
background-color: $light;
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
.card {
|
||||
border-radius: 2em;
|
||||
}
|
||||
@@ -197,34 +260,20 @@
|
||||
color: $success;
|
||||
}
|
||||
|
||||
i {
|
||||
&.success {
|
||||
color: $success;
|
||||
}
|
||||
|
||||
&.danger {
|
||||
color: $danger;
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
&.btn-outline-success {
|
||||
border-color: $success;
|
||||
&:hover {
|
||||
background-color: $success;
|
||||
}
|
||||
}
|
||||
|
||||
&.btn-outline-danger {
|
||||
border-color: $danger;
|
||||
&:hover {
|
||||
background-color: $danger;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.color-warning {
|
||||
color: $warning;
|
||||
}
|
||||
|
||||
.callout {
|
||||
padding: 1rem;
|
||||
margin-top: 1.25rem;
|
||||
margin-bottom: 1.25rem;
|
||||
border: 1px solid #e9ecef;
|
||||
border-left-width: 0.25rem;
|
||||
border-radius: 0.25rem;
|
||||
|
||||
&.callout-info {
|
||||
border-left-color: $info;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -3,6 +3,7 @@ $light: #ecedf3;
|
||||
$success: #06d6a0;
|
||||
$danger: #ef476f;
|
||||
$warning: #ffd166;
|
||||
$info: #118ab2;
|
||||
|
||||
// bootstrap
|
||||
$body-bg: $light;
|
||||
|
||||
@@ -2,6 +2,7 @@ celery[redis]
|
||||
channels
|
||||
channels-redis
|
||||
django
|
||||
django-celery-beat
|
||||
gunicorn
|
||||
mysqlclient
|
||||
psycopg2-binary
|
||||
|
||||
Reference in New Issue
Block a user