Compare commits
2 Commits
Author | SHA1 | Date |
---|---|---|
sqozz | fdcefa7f9e | |
sqozz | 7f6bc4d6f9 |
|
@ -0,0 +1,4 @@
|
|||
UPDATE_SERVER=iot-update-server.lan
|
||||
USER=sqozz
|
||||
PROJECT_NAME=ota_test
|
||||
FIRMWARE_BRANCH=latest
|
|
@ -0,0 +1,68 @@
|
|||
#!/bin/bash -e
|
||||
# This is a helper script to automatically deploy firmware binaries to a remote running esp-ota-update-server
|
||||
# It is configured by a project specific env-file which needs to be sourced before calling this script
|
||||
# TODO:
|
||||
# - implement sanity check for config vars
|
||||
|
||||
## Settings you may want to adjust
|
||||
ESP_UPDATER_FS_PATH=/home/sqozz/esp_ota_updater # FS path to esp-ota-update-server on remote server (no trailing slash!)
|
||||
FIRMWARE_FS_PATH=$ESP_UPDATER_FS_PATH/firmwares/ # FS path to the firmwares. Only used to construct full path (REMOTE_BIN_FILE) later on
|
||||
CONFIG_FS_PATH=$ESP_UPDATER_FS_PATH/config.json # FS path to the config of esp-ota-update-server. Unsed to adjust the hash of the deployed binary
|
||||
LOCAL_BIN_FILE="$PROJECT_NAME.ino.nodemcu.bin" # name of the binary to flash - this is the default for arduino nodemcu binaries
|
||||
|
||||
ssh-add -l 2> /dev/null > /dev/null
|
||||
if [ $? -eq 2 ]; then
|
||||
echo "ssh-agent is highly advisable for this script! Continuing anyway but you're going to be asked for you password quiet often."
|
||||
fi
|
||||
|
||||
echo "=== PROJECT SETTINGS ==="
|
||||
echo "Updateserver: $USER@$UPDATE_SERVER"
|
||||
echo "Project: $PROJECT_NAME"
|
||||
echo "Branch: $FIRMWARE_BRANCH"
|
||||
echo ""
|
||||
|
||||
# calculate local hash
|
||||
echo -n "Calculating hash for $LOCAL_BIN_FILE…"
|
||||
LOCAL_HASH=$(md5sum $LOCAL_BIN_FILE | awk '{ print $1 }')
|
||||
echo -e "\b -> $LOCAL_HASH… done."
|
||||
|
||||
# calculate remote hash
|
||||
BIN_FS_PATH=$PROJECT_NAME/$FIRMWARE_BRANCH/$FIRMWARE_BRANCH.bin
|
||||
REMOTE_BIN_FILE=$FIRMWARE_FS_PATH$BIN_FS_PATH
|
||||
echo -n "Calculating hash for $USER@$UPDATE_SERVER:$REMOTE_BIN_FILE…"
|
||||
REMOTE_HASH=$(ssh $USER@$UPDATE_SERVER "md5sum $REMOTE_BIN_FILE" | awk '{ print $1 }')
|
||||
echo -e "\b -> $REMOTE_HASH… done."
|
||||
|
||||
# only upload if the binary is not the same
|
||||
if [ "$LOCAL_HASH" = "$REMOTE_HASH" ]; then
|
||||
echo "Firmware already exists on update server, skipping upload…"
|
||||
exit 10
|
||||
fi
|
||||
|
||||
echo -n "Uploading $LOCAL_BIN_FILE to $USER@$UPDATE_SERVER:$REMOTE_BIN_FILE… "
|
||||
scp $LOCAL_BIN_FILE $USER@$UPDATE_SERVER:$REMOTE_BIN_FILE > /dev/null
|
||||
echo "done."
|
||||
|
||||
# we should never end up here if the transfer fails. This is just to be 100% sure
|
||||
REMOTE_HASH=$(ssh $USER@$UPDATE_SERVER "md5sum $REMOTE_BIN_FILE" | awk '{ print $1 }')
|
||||
echo "Remote hash after upload: $REMOTE_HASH"
|
||||
if [ "$LOCAL_HASH" != "$REMOTE_HASH" ]; then
|
||||
echo "Remote hash $REMOTE_HASH does not match local hash $LOCAL_HASH after upload!"
|
||||
exit 20
|
||||
fi
|
||||
|
||||
# backup
|
||||
echo -n "Creating config.json backup to config.json.old… "
|
||||
ssh $USER@$UPDATE_SERVER "cp $CONFIG_FS_PATH $CONFIG_FS_PATH.old"
|
||||
echo "done."
|
||||
|
||||
# if you pipe this directly into config.json it will result in a race and an erased/empty config
|
||||
# therefore we write it into a temp-config first
|
||||
echo -n "Creating updated config.json.new… "
|
||||
ssh $USER@$UPDATE_SERVER "cat $CONFIG_FS_PATH | jq '.firmwares.$PROJECT_NAME.$FIRMWARE_BRANCH.hash = \"$REMOTE_HASH\"' > $CONFIG_FS_PATH.new"
|
||||
echo "done."
|
||||
|
||||
# and finally: deploy
|
||||
echo -n "Deploying updated config.json.new to config.json… "
|
||||
ssh $USER@$UPDATE_SERVER "cp $CONFIG_FS_PATH.new $CONFIG_FS_PATH"
|
||||
echo "done."
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"devices" : {
|
||||
"AA:BB:CC:DD:EE:FF" : {
|
||||
"role" : "ota_test",
|
||||
"version" : "latest"
|
||||
}
|
||||
},
|
||||
|
||||
"firmwares" : {
|
||||
"ota_test" : {
|
||||
"latest" : {
|
||||
"filename" : "latest.bin",
|
||||
"hash" : "d41d8cd98f00b204e9800998ecf8427e"
|
||||
},
|
||||
"backup" : {
|
||||
"filename" : "backup.bin",
|
||||
"hash" : ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,34 +1,72 @@
|
|||
#!/usr/bin/env python3
|
||||
import hashlib
|
||||
from bottle import route, run, request, HTTPResponse, response
|
||||
import hashlib
|
||||
import json
|
||||
import pdb
|
||||
import os
|
||||
|
||||
updated = True
|
||||
filename = "firmware.bin"
|
||||
|
||||
@route('/update')
|
||||
def update():
|
||||
def updateRequest():
|
||||
#for header in request.headers.keys():
|
||||
# print("{}: {}".format(header, request.headers.get(header, "")))
|
||||
|
||||
requestor_mac = request.headers.get("X-Esp8266-Sta-Mac")
|
||||
running_fw_md5 = request.headers.get("X-Esp8266-Sketch-Md5", "")
|
||||
print("Update request from {} with fw {}".format(requestor_mac, running_fw_md5))
|
||||
available_fw_md5 = md5(filename)
|
||||
if not "X-Esp8266-Sta-Mac" in request.headers:
|
||||
return "Hello fellow friend! Looking for updates?"
|
||||
|
||||
print("Lates firmware available is {} - ".format(available_fw_md5), end="")
|
||||
if available_fw_md5 != running_fw_md5:
|
||||
print("Differs, updating…")
|
||||
response.set_header('Content-Type', 'application/octet-stream')
|
||||
response.set_header('Content-Disposition', ' attachment; filename={}'.format(filename))
|
||||
with open(filename, "rb") as firmware:
|
||||
fw = firmware.read()
|
||||
response.set_header('Content-Length', '{}'.format(len(fw)))
|
||||
response.set_header('X-MD5', available_fw_md5)
|
||||
return fw
|
||||
else:
|
||||
readConfig()
|
||||
global config
|
||||
requestor_mac = request.headers.get("X-Esp8266-Sta-Mac", "")
|
||||
running_fw_md5 = request.headers.get("X-Esp8266-Sketch-Md5", "")
|
||||
role, version, fw_filename, fw_hash = getProperties(config, requestor_mac)
|
||||
if role == "" and version == "" and fw_filename == "" and fw_hash == "":
|
||||
print("No configuration for {} found. Serving 304 - No Firmware".format(requestor_mac))
|
||||
return nofirmware()
|
||||
|
||||
print("Update request from {} with role {} (running fw hash: {}).".format(requestor_mac, role, running_fw_md5))
|
||||
fw_path = os.path.join("firmwares", role, version, fw_filename)
|
||||
try:
|
||||
latest_fw_md5 = md5(fw_path)
|
||||
except FileNotFoundError:
|
||||
print("Configured firmware not found at {}".format(fw_path))
|
||||
return nofirmware()
|
||||
|
||||
if latest_fw_md5 != fw_hash:
|
||||
print("Configured hash ({}) does not match calculeted one ({})".format(fw_hash, latest_fw_md5))
|
||||
return nofirmware()
|
||||
|
||||
print("Latest firmware for role \"{}\" is \"{}\" ({}) - ".format(role, fw_filename, latest_fw_md5), end="")
|
||||
if latest_fw_md5 == running_fw_md5:
|
||||
print("Same, skipping…")
|
||||
return HTTPResponse(status=304, body="")
|
||||
return nofirmware()
|
||||
|
||||
print("Differs, updating…")
|
||||
response.set_header('Content-Type', 'application/octet-stream')
|
||||
response.set_header('Content-Disposition', ' attachment; filename={}'.format(filename))
|
||||
with open(fw_path, "rb") as fw:
|
||||
firmware = fw.read()
|
||||
response.set_header('Content-Length', '{}'.format(len(firmware)))
|
||||
response.set_header('X-MD5', latest_fw_md5)
|
||||
print("Serving {} bytes to {} with role {}".format(len(firmware), requestor_mac, role))
|
||||
return firmware
|
||||
|
||||
def getProperties(config, mac):
|
||||
configured_devices = config.get("devices", {}).keys()
|
||||
devices = config.get("devices", {})
|
||||
firmwares = config.get("firmwares", {})
|
||||
match = devices.get(mac, {"role":"", "version":""})
|
||||
role = match.get("role", "")
|
||||
version = match.get("version", "")
|
||||
role_firmwares = firmwares.get(role, {})
|
||||
firmware = role_firmwares.get(version, {})
|
||||
firmware_filename = firmware.get("filename", "")
|
||||
firmware_hash = firmware.get("hash", "")
|
||||
return role, version, firmware_filename, firmware_hash
|
||||
|
||||
def nofirmware():
|
||||
return HTTPResponse(status=304, body="")
|
||||
|
||||
def md5(fname):
|
||||
hash_md5 = hashlib.md5()
|
||||
|
@ -37,4 +75,11 @@ def md5(fname):
|
|||
hash_md5.update(chunk)
|
||||
return hash_md5.hexdigest()
|
||||
|
||||
def readConfig(config_name="config.json"):
|
||||
global config
|
||||
config = ""
|
||||
with open(config_name, "r") as conf:
|
||||
config = json.loads(conf.read())
|
||||
|
||||
readConfig()
|
||||
run(host='localhost', port=8080, debug=True)
|
||||
|
|
Loading…
Reference in New Issue