Implement LED status indicators for v4, store generated firmware images for faster retrieval

This commit is contained in:
Kumi 2018-12-26 00:25:53 +01:00
parent 267b613893
commit 6e6c63bed8
9 changed files with 335 additions and 140 deletions

1
.gitignore vendored
View file

@ -3,3 +3,4 @@ __pycache__
*.pyc
static/static_root
imagebuilder
images/

View file

@ -1,59 +1,119 @@
# Get IP address on VPN bridge interface through some arcane magic
ipaddr(){
if="${1:-br-VPN360}"
result=$(/sbin/ip -o -4 addr show dev "${if}" 2&>/dev/null | /bin/sed 's/^.*inet // ; s/\/...*$//')
/usr/bin/printf %s "${result}"
}
# Disable (broadcast) WiFi device
stopwifi(){
/sbin/uci set wireless.radio0.disabled=1
/sbin/uci commit
/sbin/uci commit
}
# Enable (broadcast) WiFi device
startwifi(){
/sbin/uci set wireless.radio0.disabled=0
/sbin/uci commit
/sbin/wifi
/sbin/wifi
}
# Disable and re-enable (broadcast) WiFi device
restartwifi(){
stopwifi
startwifi
}
. /etc/vpnsecret
# Set power LED brightness
powerled(){
echo $1 > /sys/devices/platform/leds-gpio/leds/gl-ar750:white:power/brightness
}
# Set second LED brightness
led2g(){
echo $1 > /sys/devices/platform/leds-gpio/leds/gl-ar750:white:wlan2g/brightness
}
# Set third LED brightness
led5g(){
echo $1 > /sys/devices/platform/leds-gpio/leds/gl-ar750:white:wlan5g/brightness
}
. /etc/vpnsecret # Source the server authentication secret
# Retrieve hosts file from server
/usr/bin/wget -O/etc/hosts https://admin360.kumi.host/hosts --post-data "secret=$SECRET" --no-check-certificate >/var/log/wget 2>&1
# Prepare for default VPN-WiFi bridge
/sbin/uci set wireless.@wifi-iface[0].network="VPN360"
/sbin/uci commit
# Disable WiFi for as long as there is no bridge providing IP addresses
stopwifi
# Turn off all LEDs
powerled 0
led2g 0
led5g 0
# Launch VPN client
/usr/sbin/openvpn /etc/openvpn/client.conf >/var/log/openvpn &
/bin/sleep 60
# Try for approx. 1 minute to get an IP from the VPN before falling back to DHCP
counter=0
if [ $(ipaddr) ]
then
startwifi
while [ True ]
do
sleep 10
if [ $(ipaddr) ]
then
/usr/bin/wget -O- https://admin360.kumi.host/heartbeat --post-data "secret=$SECRET&ip=$(ipaddr)" --no-check-certificate 2>/var/log/wget | /bin/ash
fi
done
else
/sbin/uci set wireless.@wifi-iface[0].network="DHCP"
/sbin/uci commit
startwifi
/sbin/ip a add 192.168.36.1/24 dev br-DHCP
/sbin/ifconfig br-DHCP down
/sbin/ifconfig br-DHCP up
while [ True ]
do
sleep 10
/usr/bin/wget -O- https://admin360.kumi.host/heartbeat --post-data "secret=$SECRET" --no-check-certificate 2>/var/log/wget | /bin/ash
done
fi
while [ $counter -lt 60 ]
do
powerled $(( counter % 2 )) # Make power LED flash
if [ $(ipaddr) ] # = If connection to the server is working
then
# Turn on LEDs indicating boot completion and connection success
powerled 1
led5g 1
# Enable WiFi as the VPN bridge is now functional
startwifi
# Send a heartbeat to the server every 10 seconds
# This is also used to transfer commands from the server to the device
while [ True ]
do
/bin/sleep 10
# Let's hope there is an IP address on the VPN interface
# If not, this might be a temporary issue (lost network connection or lease expiration)
# We assume that users will reboot the device if it doesn't work for extended periods of time
if [ $(ipaddr) ]
then
/usr/bin/wget -O- https://admin360.kumi.host/heartbeat --post-data "secret=$SECRET&ip=$(ipaddr)" --no-check-certificate 2>/var/log/wget | /bin/ash
fi
done
fi
counter=$(( counter + 1 ))
/bin/sleep 1 # Wait for a second before re-trying
done
# We should only ever get to this point if no VPN connection was established within a minute
# Switch WiFi device to the DHCP bridge
/sbin/uci set wireless.@wifi-iface[0].network="DHCP"
/sbin/uci commit
# Turn on LEDs indicating connection failure and DHCP fallback
powerled 1
led2g 1
# Start WiFi device now bridged to the DHCP and assign server IP
startwifi
/sbin/ip a add 192.168.36.1/24 dev br-DHCP
/sbin/ifconfig br-DHCP down
/sbin/ifconfig br-DHCP up
# Send a heartbeat to the server every 10 seconds
# This is also used to transfer commands from the server to the device
while [ True ]
do
sleep 10
/usr/bin/wget -O- https://admin360.kumi.host/heartbeat --post-data "secret=$SECRET" --no-check-certificate 2>/var/log/wget | /bin/ash
done

View file

@ -17,19 +17,3 @@ config timeserver 'ntp'
list server '2.openwrt.pool.ntp.org'
list server '3.openwrt.pool.ntp.org'
config led
option trigger 'netdev'
option dev 'br-lan'
option mode 'link'
option name 'WAN'
option sysfs 'gl-ar300m:green:lan'
option default '0'
config led
option name 'VPN'
option trigger 'netdev'
option dev 'br-VPN360'
option mode 'link'
option sysfs 'gl-ar300m:green:wlan'
option default '0'

View file

@ -0,0 +1,29 @@
# Generated by Django 2.1.3 on 2018-12-25 22:25
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('manager', '0017_auto_20181223_0936'),
]
operations = [
migrations.AddField(
model_name='device',
name='firmware',
field=models.DateTimeField(blank=True, editable=False, null=True, verbose_name='Firmware Creation Time'),
),
migrations.AddField(
model_name='model',
name='firmware',
field=models.DateTimeField(auto_now=True, verbose_name='Firmware Modification Date'),
),
migrations.AlterField(
model_name='device',
name='wifi',
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='manager.Wifi'),
),
]

View file

@ -0,0 +1,29 @@
# Generated by Django 2.1.3 on 2018-12-25 22:28
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('manager', '0018_auto_20181225_2225'),
]
operations = [
migrations.AlterField(
model_name='device',
name='lastbeat',
field=models.DateTimeField(blank=True, editable=False, null=True, verbose_name='Last Received Timestamp'),
),
migrations.AlterField(
model_name='device',
name='lasttime',
field=models.DateTimeField(blank=True, editable=False, null=True, verbose_name='Last Received IP'),
),
migrations.AlterField(
model_name='device',
name='wifi',
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='manager.Wifi'),
),
]

View file

@ -0,0 +1,19 @@
# Generated by Django 2.1.3 on 2018-12-25 22:28
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('manager', '0019_auto_20181225_2228'),
]
operations = [
migrations.AlterField(
model_name='device',
name='wifi',
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='manager.Wifi'),
),
]

View file

@ -0,0 +1,24 @@
# Generated by Django 2.1.3 on 2018-12-25 22:30
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('manager', '0020_auto_20181225_2228'),
]
operations = [
migrations.AddField(
model_name='device',
name='update',
field=models.BooleanField(default=False, verbose_name='Trigger Firmware Update'),
),
migrations.AlterField(
model_name='device',
name='wifi',
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='manager.Wifi'),
),
]

View file

@ -24,6 +24,7 @@ class Model(models.Model):
name = models.CharField("Model Name", max_length=100, unique=True)
extname = models.CharField("Manufacturer Model Name", max_length=100)
config = models.TextField("OpenWRT Compile Config", blank=True, null=True)
firmware = models.DateTimeField("Firmware Modification Date", auto_now=True)
def __str__(self):
return self.name
@ -48,11 +49,13 @@ class Device(models.Model):
network = models.ForeignKey(Network, on_delete=models.SET_NULL, blank=True, null=True)
wifi = models.OneToOneField(Wifi, on_delete=models.SET_NULL, blank=True, null=True)
curip = models.CharField("Current IP Address", max_length=15, blank=True, null=True)
lasttime = models.DateTimeField("Last Received IP", blank=True, null=True)
lastbeat = models.DateTimeField("Last Received Timestamp", blank=True, null=True)
lasttime = models.DateTimeField("Last Received IP", blank=True, null=True, editable=False)
lastbeat = models.DateTimeField("Last Received Timestamp", blank=True, null=True, editable=False)
secret = models.CharField("Secret", default=getRandom, max_length=128)
password = models.CharField("Device Password", default=getRandom, max_length=128)
vpnconfig = models.TextField("VPN Configuration", blank=True, null=True, editable=False)
firmware = models.DateTimeField("Firmware Creation Time", blank=True, null=True, editable=False)
update = models.BooleanField("Trigger Firmware Update", default=False)
reboot = models.BooleanField("Trigger Reboot", default=False)
def __str__(self):

View file

@ -6,6 +6,7 @@ from django.views.decorators.csrf import csrf_exempt
from django.utils import timezone
from django.core.files import File
from django.db.models.fields.files import FieldFile
from .models import Device, Organization, Network, Model
from distutils.dir_util import copy_tree
@ -18,6 +19,7 @@ import socket
import tempfile
import crypt
import tarfile
import datetime
def index(request):
if request.user.is_authenticated:
@ -47,6 +49,121 @@ def hosts(request):
device.save()
return render(request, "manager/hosts", {"device": device})
def mkfirmware(device, path):
if device.firmware and device.firmware > device.model.firmware and glob.glob("%s/%s.bin" % (path, device.id)):
return True
BEFORE = os.getcwd()
DEVICEDIR = "/opt/vpnmanager/device-config/%i/" % device.model.id
SRCDIR = "/opt/vpnmanager/imagebuilder/%i/" % device.model.id
if glob.glob(SRCDIR + "/.kumilock"):
return False
with open(SRCDIR + "/.kumilock", "w") as lock:
lock.write("")
tempdir = tempfile.TemporaryDirectory()
copy_tree(DEVICEDIR, tempdir.name)
# Write OpenVPN config
with open(tempdir.name + "/etc/openvpn/client.conf", "w") as vpnconf:
vpnconf.write(device.vpnconfig)
# Write secret
with open(tempdir.name + "/etc/vpnsecret", "w") as secret:
secret.write('SECRET="%s"' % device.secret)
# Write password
with open(tempdir.name + "/etc/shadow", "r") as shadow:
password = crypt.crypt(device.password, crypt.mksalt(crypt.METHOD_MD5))
shadowin = shadow.read()
with open(tempdir.name + "/etc/shadow", "w") as shadowout:
shadowout.write(shadowin.replace("$PASSWORD", password))
# Write SSID
with open(tempdir.name + "/etc/config/wireless", "r") as wireless:
wirein = wireless.read()
with open(tempdir.name + "/etc/config/wireless", "w") as wireout:
wire = wirein.replace("$SSID", device.serial)
try:
wire2 = wire.replace("$WIFISSID", device.wifi.ssid)
wire2 = wire2.replace("$WIFIKEY", device.wifi.key)
except:
wire2 = wire.replace("$WIFISSID", "NoSuchWiFi")
wire2 = wire2.replace("$WIFIKEY", "NoSuchKey")
wireout.write(wire2)
'''
# Generate .tar.gz file
with tarfile.open(tempdir.name + ".tar.gz", "w:gz") as tar:
tar.add(tempdir.name, arcname=os.path.sep)
with open(tempdir.name + ".tar.gz", "rb") as download:
response = HttpResponse(download.read(), content_type="application/tar+gzip")
response['Content-Disposition'] = 'inline; filename=' + os.path.basename(device.serial + ".tar.gz")
return response
'''
# Create compilation environment
os.system("rm -rf " + SRCDIR + "/files/")
os.mkdir(SRCDIR + "/files/")
os.system("cp -r " + tempdir.name + "/* " + SRCDIR + "/files/")
tempdir.cleanup()
os.system("rm " + SRCDIR + "/bin/targets/ar71xx/generic/*")
# Build image
os.chdir(SRCDIR)
try:
subprocess.call(["/usr/bin/make"])
except:
os.remove(SRCDIR + "/.kumilock")
os.chdir(BEFORE)
return False
os.chdir(BEFORE)
os.rename(glob.glob(SRCDIR + "/bin/targets/ar71xx/generic/*squashfs-sysupgrade.bin")[0], "%s/%s.bin" % (path, device.id))
os.remove(SRCDIR + "/.kumilock")
os.system("rm -rf " + SRCDIR + "/files/")
os.system("rm " + SRCDIR + "/bin/targets/ar71xx/generic/*")
device.firmware = datetime.datetime.now()
device.save()
return True
@csrf_exempt
def update(request):
FWDIR = "/opt/vpnmanager/images/"
device = get_object_or_404(Device, secret=request.POST.get("secret", ""))
if not mkfirmware(device, FWDIR):
return HttpResponse(status=503)
device.update = False
device.save()
with open("%s/%s.bin" % (FWDIR, device.id), "rb") as download:
response = HttpResponse(download.read(), content_type="application/octet-stream")
response['Content-Disposition'] = 'inline; filename=%s.bin' % device.serial
return response
def ping(request, device_id):
if request.user.is_authenticated:
device = None
@ -141,103 +258,22 @@ def editdevice(request, device_id):
return redirect("/")
def getconfig(request, device_id):
FWDIR = "/opt/vpnmanager/images/"
if not request.user.is_superuser:
return redirect("/")
device = get_object_or_404(Device, id=device_id)
BEFORE = os.getcwd()
DEVICEDIR = "/opt/vpnmanager/device-config/%i/" % device.model.id
SRCDIR = "/opt/vpnmanager/imagebuilder/%i/" % device.model.id
if glob.glob(SRCDIR + "/.kumilock"):
return HttpResponse("Another download is being prepared right now. Please wait for it to finish and try again later.")
with open(SRCDIR + "/.kumilock", "w") as lock:
lock.write("")
tempdir = tempfile.TemporaryDirectory()
copy_tree(DEVICEDIR, tempdir.name)
# Write OpenVPN config
with open(tempdir.name + "/etc/openvpn/client.conf", "w") as vpnconf:
vpnconf.write(device.vpnconfig)
# Write secret
with open(tempdir.name + "/etc/vpnsecret", "w") as secret:
secret.write('SECRET="%s"' % device.secret)
# Write password
with open(tempdir.name + "/etc/shadow", "r") as shadow:
password = crypt.crypt(device.password, crypt.mksalt(crypt.METHOD_MD5))
shadowin = shadow.read()
with open(tempdir.name + "/etc/shadow", "w") as shadowout:
shadowout.write(shadowin.replace("$PASSWORD", password))
# Write SSID
with open(tempdir.name + "/etc/config/wireless", "r") as wireless:
wirein = wireless.read()
with open(tempdir.name + "/etc/config/wireless", "w") as wireout:
wire = wirein.replace("$SSID", device.serial)
try:
wire2 = wire.replace("$WIFISSID", device.wifi.ssid)
wire2 = wire2.replace("$WIFIKEY", device.wifi.key)
except:
wire2 = wire.replace("$WIFISSID", "NoSuchWiFi")
wire2 = wire2.replace("$WIFIKEY", "NoSuchKey")
wireout.write(wire2)
if not mkfirmware(device, FWDIR):
return HttpResponse("Something went wrong generating the firmware image. The server may be busy, please try again later.")
device.update = False
device.save()
'''
# Generate .tar.gz file
with tarfile.open(tempdir.name + ".tar.gz", "w:gz") as tar:
tar.add(tempdir.name, arcname=os.path.sep)
with open(tempdir.name + ".tar.gz", "rb") as download:
response = HttpResponse(download.read(), content_type="application/tar+gzip")
response['Content-Disposition'] = 'inline; filename=' + os.path.basename(device.serial + ".tar.gz")
return response
'''
# Create compilation environment
os.system("rm -rf " + SRCDIR + "/files/")
os.mkdir(SRCDIR + "/files/")
os.system("cp -r " + tempdir.name + "/* " + SRCDIR + "/files/")
tempdir.cleanup()
os.system("rm " + SRCDIR + "/bin/targets/ar71xx/generic/*")
# Build image
os.chdir(SRCDIR)
try:
subprocess.call(["/usr/bin/make"])
except:
os.remove(SRCDIR + "/.kumilock")
os.chdir(BEFORE)
return HttpResponse("Something went wrong building the image file.")
os.chdir(BEFORE)
with open(glob.glob(SRCDIR + "/bin/targets/ar71xx/generic/*squashfs-sysupgrade.bin")[0], "rb") as download:
with open("%s/%s.bin" % (FWDIR, device.id), "rb") as download:
response = HttpResponse(download.read(), content_type="application/octet-stream")
response['Content-Disposition'] = 'inline; filename=' + os.path.basename(device.serial + ".bin")
os.remove(SRCDIR + "/.kumilock")
os.system("rm -rf " + SRCDIR + "/files/")
os.system("rm " + SRCDIR + "/bin/targets/ar71xx/generic/*")
response['Content-Disposition'] = 'inline; filename=%s.bin' % device.serial
return response
def rebootdevice(request, device_id):
@ -246,13 +282,23 @@ def rebootdevice(request, device_id):
for organization in Organization.objects.filter(users=request.user):
device = device or Device.objects.filter(id=device_id, organization=organization)
if not device:
return redirect("/")
if device:
device[0].reboot = True
device[0].save()
device[0].reboot = True
device[0].save()
return redirect("/")
return redirect("/")
def updatedevice(request, device_id):
if request.user.is_staff:
device = None
for organization in Organization.objects.filter(users=request.user):
device = device or Device.objects.filter(id=device_id, organization=organization)
if device:
device[0].update = True
device[0].save()
return redirect("/")
def deletedevice(request, device_id):
if request.user.is_superuser: