implement scan network

This commit is contained in:
Maxi Quoß
2022-02-21 00:56:36 +01:00
parent 54537bb13a
commit bbfc21ae3d
18 changed files with 324 additions and 181 deletions

View File

@@ -7,12 +7,6 @@ if [ "${DB_TYPE}" != "sqlite" ]; then
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
@@ -23,6 +17,12 @@ elif [ "$PING_INTERVAL" -lt 5 ]; then
PING_INTERVAL=5
fi
# init django
python manage.py makemigrations
python manage.py migrate
python manage.py collectstatic --noinput
python manage 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

View File

@@ -20,7 +20,8 @@ Websocket.objects.create(visitors=0)
Settings.objects.update_or_create(
id=1,
defaults={
"enable_notifications": os.getenv("ENABLE_NOTIFICATIONS")
"enable_notifications": os.getenv("ENABLE_NOTIFICATIONS"),
"interval": os.getenv("PING_INTERVAL")
}
)

View File

@@ -11,17 +11,13 @@ class DeviceAdmin(admin.ModelAdmin):
def ports(self, obj):
return ", ".join([p.name for p in obj.port.all()])
class PortAdmin(admin.ModelAdmin):
list_display = ["number", "name"]
search_fields = ["number", "name"]
list_filter = ["number", "name"]
class SettingsAdmin(admin.ModelAdmin):
list_display = ["enable_notifications",
"enable_console_logging", "sort_by",
"scan_address"]
list_display = ["enable_notifications", "sort_by", "scan_address", "interval"]
admin.site.register(Device, DeviceAdmin)

View File

@@ -1,14 +1,17 @@
from array import array
import ipaddress
import json
import subprocess
from channels.db import database_sync_to_async
from channels.generic.websocket import AsyncWebsocketConsumer
from django.core import serializers
from django.utils.dateparse import parse_datetime
from django.utils.timezone import make_aware
from django_celery_beat.models import (ClockedSchedule, CrontabSchedule,
PeriodicTask)
from wol.models import Device, Port, Websocket
from wol.models import Device, Port, Settings, Websocket
from wol.wake import wake
from django_celery_beat.models import PeriodicTask, ClockedSchedule, CrontabSchedule
class WSConsumer(AsyncWebsocketConsumer):
@@ -17,10 +20,12 @@ class WSConsumer(AsyncWebsocketConsumer):
await self.accept()
await self.send(text_data=json.dumps({
"type": "init",
"message": await self.get_all_devices()
"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",
@@ -47,9 +52,7 @@ class WSConsumer(AsyncWebsocketConsumer):
async def receive(self, text_data=None):
received = json.loads(text_data)
if received["type"] == "wake":
dev = await self.get_device(received["id"])
wake(dev.mac, dev.ip, dev.netmask)
await self.wake_device(received["id"])
await self.channel_layer.group_send(
"wol", {
"type": "send_group",
@@ -74,9 +77,15 @@ class WSConsumer(AsyncWebsocketConsumer):
await self.update_device(received["data"])
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"] == "celery":
await self.celery_create_scheduled_wake(received["data"])
elif received["type"] == "scan_network":
await self.send(text_data=json.dumps({
"type": "scan_network",
"message": await self.scan_network()
}))
async def send_group(self, event):
await self.send(json.dumps(event["message"]))
@@ -98,10 +107,9 @@ class WSConsumer(AsyncWebsocketConsumer):
return Websocket.objects.first().visitors
@database_sync_to_async
def get_device(self, id):
def wake_device(self, id):
dev = Device.objects.filter(id=id).first()
return dev
wake(dev.mac, dev.ip, dev.netmask)
@database_sync_to_async
def get_all_devices(self):
@@ -188,7 +196,6 @@ class WSConsumer(AsyncWebsocketConsumer):
task.enabled = False
task.save()
@database_sync_to_async
def update_port(self, data):
if data.get("name"):
@@ -201,6 +208,28 @@ class WSConsumer(AsyncWebsocketConsumer):
else:
Port.objects.filter(number=data["number"]).delete()
@database_sync_to_async
def get_settings(self):
conf = Settings.objects.get(id=1)
data = {
"notifications": conf.enable_notifications,
"discovery": conf.scan_address,
"interval": conf.interval,
"scan_network": []
}
return data
@database_sync_to_async
def update_settings(self, data):
Settings.objects.update_or_create(
id=1,
defaults={
"enable_notifications": data["notifications"],
"scan_address": data["discovery"],
"interval": data["interval"]
}
)
@database_sync_to_async
def celery_create_scheduled_wake(self, data):
aware_time = make_aware(parse_datetime(data["time"]))
@@ -213,3 +242,40 @@ class WSConsumer(AsyncWebsocketConsumer):
task="wol.tasks.scheduled_wake",
args=json.dumps([data["id"]])
)
@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", "-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
})
return data

View File

@@ -1,4 +1,4 @@
# Generated by Django 4.0.2 on 2022-02-16 20:54
# Generated by Django 3.2.12 on 2022-02-20 18:24
from django.db import migrations, models
@@ -12,14 +12,11 @@ class Migration(migrations.Migration):
operations = [
migrations.CreateModel(
name='Device',
name='Port',
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)),
('number', models.PositiveIntegerField()),
('name', models.SlugField()),
],
),
migrations.CreateModel(
@@ -39,4 +36,16 @@ class Migration(migrations.Migration):
('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')),
],
),
]

View File

@@ -1,21 +0,0 @@
# Generated by Django 4.0.2 on 2022-02-18 18:20
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('wol', '0001_initial'),
]
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()),
],
),
]

View File

@@ -0,0 +1,17 @@
# 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',
),
]

View File

@@ -1,19 +0,0 @@
# Generated by Django 4.0.2 on 2022-02-18 18:28
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('wol', '0002_port'),
]
operations = [
migrations.AddField(
model_name='device',
name='port',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='wol.port'),
),
]

View File

@@ -0,0 +1,18 @@
# 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),
),
]

View File

@@ -1,22 +0,0 @@
# Generated by Django 4.0.2 on 2022-02-18 18:31
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('wol', '0003_device_port'),
]
operations = [
migrations.RemoveField(
model_name='device',
name='port',
),
migrations.AddField(
model_name='device',
name='port',
field=models.ManyToManyField(blank=True, null=True, to='wol.Port'),
),
]

View File

@@ -1,17 +0,0 @@
# Generated by Django 4.0.2 on 2022-02-18 19:06
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('wol', '0004_remove_device_port_device_port'),
]
operations = [
migrations.AlterUniqueTogether(
name='port',
unique_together={('number', 'name')},
),
]

View File

@@ -1,22 +0,0 @@
# Generated by Django 4.0.2 on 2022-02-18 19:14
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('wol', '0005_alter_port_unique_together'),
]
operations = [
migrations.AlterUniqueTogether(
name='port',
unique_together=set(),
),
migrations.AlterField(
model_name='device',
name='port',
field=models.ManyToManyField(to='wol.Port'),
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 4.0.2 on 2022-02-19 11:32
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('wol', '0006_alter_port_unique_together_alter_device_port'),
]
operations = [
migrations.AlterField(
model_name='device',
name='port',
field=models.ManyToManyField(blank=True, to='wol.Port'),
),
]

View File

@@ -21,6 +21,6 @@ class Websocket(models.Model):
class Settings(models.Model):
enable_notifications = models.BooleanField(default=True)
enable_console_logging = models.BooleanField(default=False)
sort_by = models.SlugField(default="name")
scan_address = models.GenericIPAddressField(null=True, blank=True)
interval = models.PositiveSmallIntegerField(null=True, blank=True)

View File

@@ -6,17 +6,19 @@
let visitors = 0;
let devices = [];
let settings = {}
onMount(() => {
store.subscribe(currentMessage => {
if (currentMessage.type == "init") {
// create placeholder devices
for (let index = 0; index < currentMessage.message.length; index++) {
const element = currentMessage.message[index];
// create devices
for (let index = 0; index < currentMessage.message.devices.length; index++) {
const element = currentMessage.message.devices[index];
devices.push(element);
devices = devices;
devices.sort(compare)
}
devices.sort(compare);
settings = currentMessage.message.settings;
} else if (currentMessage.type == "status") {
// set device array and sort
const index = devices.findIndex(x => x.id == currentMessage.message.id);
@@ -45,17 +47,22 @@
// delete device
const devCol = document.querySelector(`#device-col-${currentMessage.message}`);
devCol.remove();
} else if (currentMessage.type == "scan_network") {
// set scanned network devices
if (!currentMessage.message) {
return
}
settings["scan_network"] = currentMessage.message;
const btnScan = document.querySelector("#btnScan");
const btnScanSpinner = document.querySelector("#btnScanSpinner");
const btnScanText = document.querySelector("#btnScanText");
btnScan.disabled = false;
btnScanSpinner.classList.add("d-none");
btnScanText.innerText = "Scan";
}
})
})
function onSendMessage() {
if (message.length > 0) {
store.sendMessage(message);
message = "";
}
}
function setUp(id) {
const dot = document.querySelector(`#dot-${id}`)
const spinner = document.querySelector(`#spinner-${id}`);
@@ -112,7 +119,7 @@
</script>
<main>
<Navbar visitors={visitors} />
<Navbar settings={settings} visitors={visitors} />
<div class="container mb-3">
<div class="row">
{#each devices as device}

View File

@@ -1,7 +1,6 @@
<script>
import store from '../store.js';
export let device;
console.log(device);
let modalDevice = JSON.parse(JSON.stringify(device));
let customPort = {}

View File

@@ -1,38 +1,79 @@
<script>
import store from '../store.js';
export let visitors;
export let settings;
let addDevice = {}
let addDevice = {
cron: {
enabled: false,
value: ""
}
}
function updateDevice() {
if (Object.keys(addDevice).length < 4) {
function updateDevice(data) {
if (Object.keys(data).length < 4) {
return
}
store.sendMessage({
type: "update_device",
data: addDevice
data: data
})
}
function updateSettings() {
store.sendMessage({
type: "update_settings",
data: settings
})
}
function scanNetwork() {
store.sendMessage({
type: "scan_network"
})
const btnScan = document.querySelector("#btnScan");
const btnScanSpinner = document.querySelector("#btnScanSpinner");
const btnScanText = document.querySelector("#btnScanText");
btnScan.disabled = true;
btnScanSpinner.classList.remove("d-none");
btnScanText.innerText = "Scanning...";
}
function addScan(i) {
document.querySelector(`#btnAdd${i}`).disabled = true;
const dev = settings.scan_network[i];
updateDevice(dev);
}
</script>
<nav class="navbar navbar-expand-sm navbar-light bg-light">
<div class="container-fluid">
<a class="navbar-brand" href="/">
<img src="/favicon.png" alt="Logo" width="24" height="24">
<img src="/favicon.png" alt="Logo" width="24" height="24" class="me-2">
UpSnap
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNavAltMarkup" aria-controls="navbarNavAltMarkup" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNavAltMarkup">
<div class="navbar-nav me-auto">
<a class="nav-link {window.location.pathname == "/" ? 'active' : ''}" href="/">Devices</a>
<a class="nav-link {window.location.pathname == "/settings" ? 'active' : ''}" href="/settings">Settings</a>
</div>
<span class="d-flex">
<button type="button" class="btn btn-light py-2 px-3 me-2" data-bs-toggle="modal" data-bs-target="#addDevice">
Add device<i class="ms-1 fa-solid fa-plus"></i>
</button>
<span class="ms-auto d-flex">
<div class="dropdown">
<button class="btn btn-light dropdown-toggle px-3 me-2 py-2 " type="button" id="dropdownMenuButton1" data-bs-toggle="dropdown" aria-expanded="false">
More
</button>
<ul class="dropdown-menu" aria-labelledby="dropdownMenuButton1">
<li>
<button class="dropdown-item" data-bs-toggle="modal" data-bs-target="#addDevice">
<i class="fa-solid fa-plus me-2"></i>Add device
</button>
</li>
<li>
<button class="dropdown-item" data-bs-toggle="modal" data-bs-target="#Settings">
<i class="fa-solid fa-sliders me-2"></i>Settings
</button>
</li>
</ul>
</div>
<div class="card border-0">
<div class="card-body py-2 px-3">
{visitors} {visitors === 1 ? "Visitor" : "Visitors"}<i class="ms-1 fa-solid {visitors === 1 ? "fa-user" : "fa-user-group"} text-muted"></i>
@@ -44,14 +85,14 @@
</nav>
<div class="modal fade" id="addDevice" tabindex="-1" aria-labelledby="addDeviceLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title fw-bold" id="addDeviceLabel">Add device</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form id="addForm" on:submit|preventDefault={updateDevice}>
<form id="addForm" on:submit|preventDefault={() => updateDevice(addDevice)}>
<div class="row">
<div class="col-sm">
<div class="mb-3">
@@ -80,16 +121,110 @@
</div>
</div>
</div>
<div class="row">
<div class="col-auto ms-auto">
<button type="submit" form="addForm" class="btn btn-outline-success">Add device</button>
</div>
</div>
<h5 class="fw-bold">Network discovery</h5>
{#if !settings.discovery}
<div class="callout callout-danger">
<p class="my-0">To enable this option, please enter your network address above.</p>
</div>
{/if}
<button id="btnScan" class="btn btn-secondary" type="button" on:click={scanNetwork} disabled={!settings.discovery}>
<span id="btnScanSpinner" class="spinner-grow spinner-grow-sm d-none" role="status" aria-hidden="true"></span>
<span id="btnScanText">Scan</span>
</button>
{#if settings.scan_network?.length}
<table class="table">
<thead>
<tr>
<td>Name</td>
<td>IP</td>
<td>Netmask</td>
<td>MAC</td>
<td>Add</td>
</tr>
</thead>
<tbody>
{#each settings.scan_network as device, i}
<tr>
<td>{device.name}</td>
<td>{device.ip}</td>
<td>{device.netmask}</td>
<td>{device.mac}</td>
<td>
<button type="button" id="btnAdd{i}" class="btn btn-outline-secondary py-0" on:click={() => addScan(i)}>
<i class="fa-solid fa-plus fa-sm"></i>
</button>
</td>
</tr>
{/each}
</tbody>
</table>
{/if}
</form>
</div>
</div>
</div>
</div>
<div class="modal fade" id="Settings" tabindex="-1" aria-labelledby="settingsLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title fw-bold" id="settingsLabel">Settings</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form id="settingsForm" on:submit|preventDefault={updateSettings}>
<h5 class="fw-bold">General</h5>
<div class="row">
<div class="col-sm-8">
<div class="mb-3">
<label for="inputNetworkDiscovery" class="form-label">Network discovery address</label>
<input type="text" class="form-control" id="inputNetworkDiscovery" placeholder="192.168.1.0/24" bind:value="{settings.discovery}" pattern="^([01]?\d\d?|2[0-4]\d|25[0-5])(?:\.(?:[01]?\d\d?|2[0-4]\d|25[0-5])){'{'}2{'}'}(?:\.(?:0))(?:/[0-2]\d|/3[0-2])$" required>
</div>
</div>
<div class="col-sm-4">
<div class="mb-3">
<label for="inputIntervalSettings" class="form-label">Interval</label>
<input type="number" class="form-control" id="inputIntervalSettings" bind:value="{settings.interval}" required>
</div>
</div>
</div>
<div class="row">
<div class="col-sm">
<div class="mb-3">
<div class="form-check">
<label class="form-check-label" for="flexCheckDefault">
Enable notifications
</label>
<input class="form-check-input" type="checkbox" value="" id="flexCheckDefault" bind:checked={settings.notifications}>
</div>
</div>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="submit" form="addForm" class="btn btn-outline-success">Add device</button>
<button type="submit" form="settingsForm" class="btn btn-outline-success">Save</button>
</div>
</div>
</div>
</div>
<style lang="scss">
@import "../variables.scss";
.dropdown-item {
&:active {
color: #1e2125;
background-color: #e9ecef;
}
}
.card {
border-radius: 1em;
}
@@ -97,6 +232,18 @@
.btn-light {
background-color: white;
border-radius: 1em;
}
.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-danger {
border-left-color: $danger;
}
}
</style>

View File

@@ -14,6 +14,8 @@ $list-group-bg: transparent;
$modal-content-border-radius: 2em;
$modal-inner-padding: 1.5rem;
$btn-border-radius: 0.5rem;
$dropdown-border-radius: 1em;
$dropdown-border-width: 0px;
// font awesome
$fa-font-path: "webfonts";