Compare commits

...

2 Commits

Author SHA1 Message Date
sqozz fdcefa7f9e Add autodeploy script 2019-02-06 11:15:14 +01:00
sqozz 7f6bc4d6f9 Add config capabilities 2018-11-17 23:37:43 +01:00
5 changed files with 156 additions and 18 deletions

4
autodeploy.env.example Normal file
View File

@ -0,0 +1,4 @@
UPDATE_SERVER=iot-update-server.lan
USER=sqozz
PROJECT_NAME=ota_test
FIRMWARE_BRANCH=latest

68
autodeploy.sh Normal file
View File

@ -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."

21
config.json Normal file
View File

@ -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" : ""
}
}
}
}

View File

View File

@ -1,34 +1,72 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import hashlib
from bottle import route, run, request, HTTPResponse, response from bottle import route, run, request, HTTPResponse, response
import hashlib
import json
import pdb import pdb
import os
updated = True updated = True
filename = "firmware.bin" filename = "firmware.bin"
@route('/update') @route('/update')
def update(): def updateRequest():
#for header in request.headers.keys(): #for header in request.headers.keys():
# print("{}: {}".format(header, request.headers.get(header, ""))) # print("{}: {}".format(header, request.headers.get(header, "")))
requestor_mac = request.headers.get("X-Esp8266-Sta-Mac") if not "X-Esp8266-Sta-Mac" in request.headers:
running_fw_md5 = request.headers.get("X-Esp8266-Sketch-Md5", "") return "Hello fellow friend! Looking for updates?"
print("Update request from {} with fw {}".format(requestor_mac, running_fw_md5))
available_fw_md5 = md5(filename)
print("Lates firmware available is {} - ".format(available_fw_md5), end="") readConfig()
if available_fw_md5 != running_fw_md5: global config
print("Differs, updating…") requestor_mac = request.headers.get("X-Esp8266-Sta-Mac", "")
response.set_header('Content-Type', 'application/octet-stream') running_fw_md5 = request.headers.get("X-Esp8266-Sketch-Md5", "")
response.set_header('Content-Disposition', ' attachment; filename={}'.format(filename)) role, version, fw_filename, fw_hash = getProperties(config, requestor_mac)
with open(filename, "rb") as firmware: if role == "" and version == "" and fw_filename == "" and fw_hash == "":
fw = firmware.read() print("No configuration for {} found. Serving 304 - No Firmware".format(requestor_mac))
response.set_header('Content-Length', '{}'.format(len(fw))) return nofirmware()
response.set_header('X-MD5', available_fw_md5)
return fw print("Update request from {} with role {} (running fw hash: {}).".format(requestor_mac, role, running_fw_md5))
else: 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…") 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): def md5(fname):
hash_md5 = hashlib.md5() hash_md5 = hashlib.md5()
@ -37,4 +75,11 @@ def md5(fname):
hash_md5.update(chunk) hash_md5.update(chunk)
return hash_md5.hexdigest() 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) run(host='localhost', port=8080, debug=True)