new file mode 100755
@@ -0,0 +1,190 @@
+#!/usr/bin/python -B
+#
+# Author: Christian Storm <christian.storm@siemens.com>
+# Copyright (C) 2022, Siemens AG
+#
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+"""
+Suricatta General HTTP Server Mock
+
+Server mock implementation modeled after the 'General Purpose HTTP Server'.
+"""
+
+import os
+import sys
+import argparse
+import hashlib
+import pathlib
+import logging
+
+try:
+ import bottle
+except ModuleNotFoundError:
+ server_dir = os.path.dirname(os.path.abspath(__file__))
+ sys.exit(f"Install bottle, e.g., 'cd {server_dir}; wget http://bottlepy.org/bottle.py'")
+
+BOTTLE_SCHEME = "http"
+RETRY_BUSY = 10
+
+
+class server(object):
+ firmware_dir = None
+ url = None
+ debug = False
+
+
+def log(message, loglevel=logging.INFO):
+ logcolor = {
+ logging.ERROR: 31,
+ logging.WARN: 33,
+ logging.INFO: 32,
+ logging.DEBUG: 30,
+ }
+ loglevel = logging.ERROR if str(message).startswith(("400", "500")) else loglevel
+ color = logcolor.get(loglevel, 1)
+ print(f"\033[{color}m{message}\033[0m")
+ return message
+
+
+def logresult(func):
+ def decorator(*args, **kwargs):
+ return log(func(*args, **kwargs))
+
+ return decorator
+
+
+def extract_device_name(query):
+ device_name = bottle.request.headers.get("name")
+ if device_name is None and len(query) > 0:
+ device_name = "".join(ch for ch in "".join(map("".join, sorted(query.items()))) if ch.isalnum())
+ return device_name
+
+
+@bottle.error(500)
+@logresult
+def error500(error):
+ bottle.response.set_header("Content-Type", "text/plain; charset=utf-8")
+ bottle.response.status = "500 Oops."
+ return bottle.response.status
+
+
+@bottle.error(404)
+@logresult
+def error404(error):
+ bottle.response.set_header("Content-Type", "text/plain; charset=utf-8")
+ bottle.response.status = "404 Not found."
+ return bottle.response.status
+
+
+@bottle.route("/log", method="PUT")
+@logresult
+def route_log():
+ bottle.response.set_header("Content-Type", "text/plain; charset=utf-8")
+ bottle.response.status = "200 Log received."
+ log(">>> Log received: {}".format((bottle.request.body.read()).decode("UTF-8")))
+ return bottle.response.status
+
+
+@bottle.route("/", method="GET")
+@logresult
+def route_main():
+ if server.debug:
+ headers = ["{}: {}".format(h, bottle.request.headers.get(h)) for h in bottle.request.headers.keys()]
+ log(
+ ">>> {} {}\n{}".format(bottle.request.method, bottle.request.url, "\n".join(headers)),
+ logging.DEBUG,
+ )
+
+ bottle.response.set_header("Content-Type", "text/plain; charset=utf-8")
+
+ device_name = extract_device_name(bottle.request.query)
+ if device_name is None:
+ bottle.response.status = "400 HTTP Header 'name' or 'identify' cfg section error."
+ return bottle.response.status
+
+ firmware = os.path.join(server.firmware_dir, device_name)
+ firmware_relative_path = pathlib.Path(firmware).relative_to(os.getcwd())
+ if not os.path.isfile(firmware) or not os.access(firmware, os.R_OK):
+ log(f">>> Place firmware file at '{firmware_relative_path}' to update device {device_name}.")
+ # Send back the client name ID served just for the client's information.
+ bottle.response.set_header("Served-Client", device_name)
+ bottle.response.status = f"404 No update is available for {device_name}"
+ return bottle.response.status
+ log(f">>> Firmware file found at {firmware_relative_path}.")
+
+ try:
+ with open(firmware, "rb") as file:
+ bottle.response.set_header("Content-Md5", hashlib.md5(file.read()).hexdigest())
+ except FileNotFoundError:
+ bottle.response.status = f"500 Firmware file not found: {firmware_relative_path}"
+ return bottle.response.status
+
+ bottle.response.status = "302 Update available."
+ bottle.response.set_header("Location", f"{server.url}/firmware/{device_name}")
+ return bottle.response.status
+
+
+@bottle.route("/firmware/<filepath:path>")
+def route_download(filepath):
+ if bottle.request.method == "GET":
+ log(f">>> Serving firmware file: {filepath}")
+ return bottle.static_file(filepath, root=server.firmware_dir)
+
+
+def runserver():
+ parser = argparse.ArgumentParser(
+ add_help=True,
+ description="""SWUpdate Suricatta 'General HTTP Server' Mock.""",
+ epilog="""""",
+ )
+ parser.add_argument(
+ "-x",
+ metavar="BOTTLE_HOST",
+ dest="BOTTLE_HOST",
+ help="host to bind to (default: %(default)s)",
+ default="localhost",
+ )
+ parser.add_argument(
+ "-p",
+ metavar="BOTTLE_PORT",
+ dest="BOTTLE_PORT",
+ help="port to bind to (default: %(default)s)",
+ default="8080",
+ )
+ parser.add_argument(
+ "-d",
+ dest="debug",
+ help="Enable debug logging",
+ default=False,
+ action="store_true",
+ )
+ parser.add_argument(
+ "-f",
+ metavar="FIRMWARE_DIR",
+ dest="FIRMWARE_DIR",
+ help="path to firmware files" " directory (default: %(default)s)",
+ default=os.path.join(os.getcwd(), "firmwares"),
+ )
+ args = parser.parse_args()
+
+ global server
+ server.debug = args.debug
+ server.url = f"{BOTTLE_SCHEME}://{args.BOTTLE_HOST}:{args.BOTTLE_PORT}"
+ server.firmware_dir = args.FIRMWARE_DIR
+
+ if not pathlib.Path(server.firmware_dir).is_dir():
+ sys.exit(f"Path {server.firmware_dir} is not an existing directory.")
+
+ log(f"Debug logging: {server.debug}.", logging.INFO)
+ log(f"Serving firmware files from {server.firmware_dir}")
+ bottle.run(host=args.BOTTLE_HOST, port=args.BOTTLE_PORT, debug=False)
+
+
+if __name__ == "__main__":
+ try:
+ runserver()
+ except OSError as e:
+ if e.errno != 98:
+ raise
+ print("ERROR: Address already in use, server already running?")
new file mode 100644
@@ -0,0 +1,613 @@
+--[[
+
+ SWUpdate Suricatta Example Lua Module for the 'General Purpose HTTP Server'.
+
+ Author: Christian Storm <christian.storm@siemens.com>
+ Copyright (C) 2022, Siemens AG
+
+ SPDX-License-Identifier: GPL-2.0-or-later
+--]]
+
+--luacheck: no max line length
+
+local suricatta = require("suricatta")
+
+--[[ >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> ]]
+--[[ Library Functions ]]
+--[[ <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< ]]
+
+--- Simplistic getopt()
+--
+-- Only short options (one dash, one character) are supported.
+-- Unknown options are (silently) ignored.
+-- If an option's required argument is missing, (':', option) is returned.
+--
+--- @param argv string Integer-keyed arguments table
+--- @param optstring string GETOPT(3)-like option string
+--- @return function # Iterator, returning the next (option, optarg) pair
+function getopt(argv, optstring)
+ if type(argv) ~= "table" or type(optstring) ~= "string" then
+ return function() end
+ end
+ for index, value in ipairs(argv) do
+ argv[value] = index
+ end
+ local f = string.gmatch(optstring, "%l:?")
+ return function()
+ while true do
+ local option = f()
+ if not option then
+ return
+ end
+ local optchar = option:sub(1, 1)
+ local param = "-" .. optchar
+ if argv[param] then
+ if option:sub(2, 2) ~= ":" then
+ return optchar, nil
+ end
+ local value = argv[argv[param] + 1]
+ if not value then
+ return ":", optchar
+ end
+ if value:sub(1, 1) == "-" then
+ return ":", optchar
+ end
+ return optchar, value
+ end
+ end
+ end
+end
+
+
+--- Merge two tables' data into one.
+--
+--- @param dest table Destination table, modified
+--- @param source table Table to merge into dest, overruling `dest`'s data if existing
+--- @return table # Merged table
+function table.merge(dest, source)
+ local function istable(t)
+ return type(t) == "table"
+ end
+ for k, v in pairs(source) do
+ if istable(v) and istable(dest[k]) then
+ table.merge(dest[k], v)
+ else
+ dest[k] = v
+ end
+ end
+ return dest
+end
+
+
+--- Escape and trim a String.
+--
+-- The default substitutions table is suitable for escaping
+-- to proper JSON.
+--
+--- @param str string The JSON string to be escaped
+--- @param substs? table<string,string> Substitutions to apply
+--- @return string # The escaped JSON string
+function escape(str, substs)
+ local escapes = '[%z\1-\31"\\]'
+ if not substs then
+ substs = {
+ ['"'] = '"',
+ ["\\"] = "\\\\",
+ ["\b"] = "",
+ ["\f"] = "",
+ ["\n"] = "",
+ ["\r"] = "",
+ ["\t"] = "",
+ }
+ end
+ substs.__index = function(_, c)
+ return string.format("\\u00%02X", string.byte(c))
+ end
+ setmetatable(substs, substs)
+ return ((string.gsub(str, escapes, substs)):match("^%s*(.-)%s*$"):gsub("%s+", " "))
+end
+
+
+--[[ >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> ]]
+--[[ Suricatta General Purpose HTTP Server Module ]]
+--[[ <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< ]]
+
+--- Device state and information.
+--
+--- @class device
+--- @field pstate suricatta.pstate Persistent state ID number
+--- @field id string Device ID
+device = {
+ pstate = nil,
+ id = nil
+}
+
+
+--- Job type "enum".
+--
+--- @class job.type
+--- @type table<string, number>
+jobtype = {
+ INSTALL = 1,
+ DOWNLOAD = 2,
+ BOTH = 3,
+}
+
+
+--- A Job.
+--
+--- @class job
+--- @field md5 string MD5 sum of the artifact
+--- @field url string URL of the artifact
+--- @field typ job.type Type of job
+
+
+--- Configuration for the General Purpose HTTP Server.
+--
+--- @class gs
+--- @field channel table<string, channel> Channels used in this module
+--- @field channel_config suricatta.channel.options Channel options (defaults, shadowed by config file, shadowed by command line arguments)
+--- @field job job Current job information
+--- @field polldelay table Default and temporary delay between two server poll operations in seconds
+gs = {
+ channel = {},
+ channel_config = {},
+ job = {},
+ polldelay = { default = 0, current = 0 },
+}
+
+
+--- Query and handle pending actions on server.
+--
+-- Lua counterpart of `server_op_res_t server_has_pending_action(int *action_id)`.
+--
+-- The suricatta.status return values UPDATE_AVAILABLE and ID_REQUESTED
+-- are handled in `suricatta/suricatta.c`, the others result in SWUpdate
+-- sleeping again.
+--
+--- @param action_id number Current Action ID [unused]
+--- @return number # Action ID [optional]
+--- @return suricatta.status # Suricatta return code
+function has_pending_action(action_id)
+ action_id = action_id
+ gs.polldelay.current = gs.polldelay.default
+ local _, pstate = suricatta.pstate.get()
+ if pstate == suricatta.pstate.INSTALLED then
+ suricatta.notify.warn("An installed update is pending testing, not querying server.")
+ return suricatta.status.NO_UPDATE_AVAILABLE
+ end
+
+ suricatta.notify.trace("Querying %q", gs.channel_config.url)
+ local _, _, data = gs.channel.default.get({
+ url = gs.channel_config.url,
+ format = suricatta.channel.content.NONE,
+ nocheckanswer = true, -- Don't print 404 ERROR() message, see comment below.
+ headers_to_send = { ["name"] = device.id },
+ })
+
+ if data.http_response_code == 404 then
+ -- Note: Returning 404 on no update available is a bit unfortunate
+ -- printing an error message if `nocheckanswer` isn't true.
+ -- Maybe '204 No Content' as not repurposing a general error condition
+ -- would've been a better choice. Then, 204 has to be introduced in
+ -- channel_map_http_code() as known though.
+ if math.random(3) == 1 then
+ -- Deliberately update the server from time to time with my health status.
+ suricatta.notify.trace(
+ "Server queries client data for: %s",
+ data.received_headers["Served-Client"] or "<Unknown>"
+ )
+ return suricatta.status.ID_REQUESTED
+ end
+ suricatta.notify.trace("Server served request for: %s", data.received_headers["Served-Client"] or "<Unknown>")
+ return suricatta.status.NO_UPDATE_AVAILABLE
+ end
+
+ if data.http_response_code == 503 then
+ -- Server is busy serving updates to another client.
+ -- Try again after seconds announced in HTTP header or default value.
+ gs.polldelay.current = tonumber(data.received_headers["Retry-After"]) or gs.polldelay.default
+ suricatta.notify.debug("Server busy, waiting for %ds.", gs.polldelay.current)
+ return suricatta.status.NO_UPDATE_AVAILABLE
+ end
+
+ if data.http_response_code == 302 then
+ -- Returning 302 is, like 404 above, also a bit unfortunate as it
+ -- requires telling curl not to follow the redirection but instead
+ -- treat this as the artifact's URL (see channel initialization
+ -- in server_start()).
+ suricatta.notify.info("Update available, update job enqueued.")
+ gs.job.md5 = data.received_headers["Content-Md5"]
+ gs.job.url = data.received_headers["Location"]
+ return suricatta.status.UPDATE_AVAILABLE
+ end
+
+ suricatta.notify.trace("Unhandled HTTP status code %d.", data.http_response_code)
+ return suricatta.status.NO_UPDATE_AVAILABLE
+end
+suricatta.server.register(has_pending_action, suricatta.server.HAS_PENDING_ACTION)
+
+
+--- Callback to check for update cancellation on server while downloading.
+--
+-- Some servers, e.g., hawkBit, support (remote) update cancellation while
+-- the download phase. This (optional) callback function is registered
+-- as `channel_data_t`'s `dwlwrdata` function for this purpose.
+--
+-- Note: The CALLBACK_PROGRESS and CALLBACK_CHECK_CANCEL callback functions
+-- are both executed under mutual exclusion in suricatta's Lua state which
+-- is suspended in the call to suricatta.{install,download}(). While this is
+-- safe, both callback functions should take care not to starve each other.
+--
+-- WARNING: This function is called as part of the CURLOPT_WRITEFUNCTION
+-- callback as soon as there is data received, i.e., usually *MANY* times.
+-- Since this function and CALLBACK_PROGRESS are contending for the Lua
+-- state lock (and by extension for some others), register and use this
+-- function only if really necessary.
+--
+--- @return suricatta.status # OK to continue downloading, UPDATE_CANCELED to cancel
+function check_cancel_callback()
+ return suricatta.status.OK
+end
+-- suricatta.server.register(check_cancel_callback, suricatta.server.CALLBACK_CHECK_CANCEL)
+
+
+--- Lua equivalent of `sourcetype` as in `include/swupdate_status.h`.
+--
+--- @type table<string, number>
+--- @class sourcetype
+--- @field SOURCE_UNKNOWN number 0
+--- @field SOURCE_WEBSERVER number 1
+--- @field SOURCE_SURICATTA number 2
+--- @field SOURCE_DOWNLOADER number 3
+--- @field SOURCE_LOCAL number 4
+--- @field SOURCE_CHUNKS_DOWNLOADER number 5
+
+
+--- Lua equivalent of `RECOVERY_STATUS` as in `include/swupdate_status.h`.
+--
+--- @type table<string, number>
+--- @class RECOVERY_STATUS
+--- @field IDLE number 0
+--- @field START number 1
+--- @field RUN number 2
+--- @field SUCCESS number 3
+--- @field FAILURE number 4
+--- @field DOWNLOAD number 5
+--- @field DONE number 6
+--- @field SUBPROCESS number 7
+--- @field PROGRESS number 8
+
+
+--- Lua equivalent of `progress_msg` as in `include/progress_ipc.h`.
+--
+--- @class progress_msg
+--- @field magic number SWUpdate IPC magic number
+--- @field status RECOVERY_STATUS Update status
+--- @field dwl_percent number Percent of downloaded data
+--- @field nsteps number Total steps count
+--- @field cur_step number Current step
+--- @field cur_percent number Percent in current step
+--- @field cur_image string Name of the current image to be installed
+--- @field hnd_name string Name of the running handler
+--- @field source sourcetype The source that has triggered the update
+--- @field info string Additional information about the installation
+--- @field jsoninfo table If `info` is JSON, according Lua Table
+
+
+--- Progress thread callback handling progress reporting to remote.
+--
+-- Deliberately just uploading the JSON content while not respecting
+-- the log configuration of the real `server_general.c` implementation.
+-- This callback function is optional.
+--
+-- Note: The CALLBACK_PROGRESS and CALLBACK_CHECK_CANCEL callback functions
+-- are both executed under mutual exclusion in suricatta's Lua state which
+-- is suspended in the call to suricatta.{install,download}(). While this is
+-- safe, both callback functions should take care not to starve each other.
+--
+--- @param message progress_msg The progress message
+--- @return suricatta.status # Suricatta return code
+function progress_callback(message)
+ if not gs.channel.progress then
+ return suricatta.status.OK
+ end
+ local logmessage
+ if message.dwl_percent > 0 and message.dwl_percent <= 100 and message.cur_step == 0 then
+ -- Rate limit progress messages sent to server.
+ if message.dwl_percent % 5 ~= 0 then
+ return suricatta.status.OK
+ end
+ if gs.job.typ == jobtype.INSTALL then
+ logmessage = escape(
+ string.format([[{"message": "File Processing...", "percent": %d}]], message.dwl_percent or 0)
+ )
+ else
+ logmessage = escape(
+ string.format([[{"message": "Downloading...", "percent": %d}]], message.dwl_percent or 0)
+ )
+ end
+ elseif message.dwl_percent == 100 and message.cur_step > 0 then
+ -- Rate limit progress messages sent to server.
+ if message.cur_percent % 5 ~= 0 then
+ return suricatta.status.OK
+ end
+ logmessage = escape(
+ string.format(
+ [[{"message": "Installing artifact %d/%d: '%s' with '%s'...", "percent": %d}]],
+ message.cur_step,
+ message.nsteps or 0,
+ message.cur_image or "<UNKNOWN>",
+ message.hnd_name or "<UNKNOWN>",
+ message.cur_percent or 0
+ )
+ )
+ end
+ if logmessage ~= nil then
+ local res, _, data = gs.channel.progress.put({
+ url = string.format("%s/%s", gs.channel_config.url, "log"),
+ content_type = "application/json",
+ method = suricatta.channel.method.PUT,
+ format = suricatta.channel.content.NONE,
+ request_body = logmessage,
+ })
+ if not res then
+ suricatta.notify.error("HTTP Error %d while uploading log.", tonumber(data.http_response_code) or -1)
+ end
+ end
+ return suricatta.status.OK
+end
+suricatta.server.register(progress_callback, suricatta.server.CALLBACK_PROGRESS)
+
+
+--- Install an update.
+--
+-- Lua counterpart of `server_op_res_t server_install_update(void)`.
+--
+--- @return suricatta.status # Suricatta return code
+function install_update()
+ local res
+
+ suricatta.notify.info("Installing artifact from %q", gs.job.url)
+
+ -- Open progress reporting channel with default options.
+ res, gs.channel.progress = suricatta.channel.open({})
+ if not res then
+ suricatta.notify.error("Cannot initialize progress reporting channel, will not send progress.")
+ gs.channel.progress = nil
+ end
+
+ -- Chose an installation mode as I please...
+ -- Note: `drain_messages` is false, i.e., do not upload strictly all
+ -- progress messages to not artificially lengthen the installation
+ -- process due to progress message network I/O. Hence, only while
+ -- the update operation is in flight, progress messages are offloaded.
+ -- Once the operation has finished, the possibly remaining progress
+ -- messages are discarded. Instead of all progress messages, send a
+ -- final notification for the server's information.
+ res = suricatta.status.OK
+ local url = gs.job.url
+ local destfile
+ if math.random(2) == 2 then
+ suricatta.notify.info(">> Running in download + installation mode.")
+ else
+ suricatta.notify.info(">> Running in download and then local installation mode.")
+ -- Note: suricatta.get_tmpdir() returned path is '/' terminated.
+ destfile = ("%s%s"):format(suricatta.get_tmpdir(), "update.swu")
+ gs.job.typ = jobtype.DOWNLOAD
+ res, _, _ = suricatta.download({ channel = gs.channel.default, url = url, drain_messages = false }, destfile)
+ url = ("file://%s"):format(destfile)
+ gs.job.typ = jobtype.INSTALL
+ if not res then
+ suricatta.notify.error("Error downloading artifact!")
+ end
+ end
+ if res then
+ if not gs.job.typ then
+ gs.job.typ = jobtype.BOTH
+ end
+ res, _, _ = suricatta.install({ channel = gs.channel.default, url = url, drain_messages = false })
+ if destfile then
+ os.remove(destfile)
+ end
+ end
+
+ if gs.channel.progress then
+ local finres, _, findata = gs.channel.progress.put({
+ url = string.format("%s/%s", gs.channel_config.url, "log"),
+ content_type = "application/json",
+ method = suricatta.channel.method.PUT,
+ format = suricatta.channel.content.NONE,
+ request_body = escape([[{"message": "Final Note: Installation done :)"}]]),
+ })
+ if not finres then
+ suricatta.notify.error(
+ "HTTP Error %d while uploading final notification log.",
+ tonumber(findata.http_response_code) or -1
+ )
+ end
+ gs.channel.progress.close()
+ gs.channel.progress = nil
+ end
+
+ gs.job = {}
+
+ if not res then
+ suricatta.notify.error("Error installing artifact!")
+ return suricatta.status.EERR
+ end
+
+ suricatta.notify.info("Update artifact installed successfully.")
+ return suricatta.status.OK
+end
+suricatta.server.register(install_update, suricatta.server.INSTALL_UPDATE)
+
+
+--- Print the help text.
+--
+-- Lua counterpart of `void server_print_help(void)`.
+--
+--- @param defaults suricatta.channel.options Compile-time channel default options ∪ { polldelay = CHANNEL_DEFAULT_POLLING_INTERVAL }
+--- @return suricatta.status # Suricatta return code
+function print_help(defaults)
+ defaults = defaults or {}
+ io.stdout:write(string.format("\t -u <URL> * URL to the server instance, e.g., http://localhost:8080\n"))
+ io.stdout:write(string.format("\t -i <string> * The device ID.\n"))
+ io.stdout:write(string.format("\t -p <int> Polling delay (default: %ds).\n", defaults.polldelay))
+ return suricatta.status.OK
+end
+suricatta.server.register(print_help, suricatta.server.PRINT_HELP)
+
+
+--- Start the Suricatta server.
+--
+-- Lua counterpart of `server_op_res_t server_start(char *fname, int argc, char *argv[])`.
+--
+--- @param defaults table<string, any> Lua suricatta module channel default options
+--- @param argv table[] C's `argv` as Lua Table
+--- @param fconfig table<string, any> SWUpdate configuration file's [suricatta] section as Lua Table ∪ { polldelay = CHANNEL_DEFAULT_POLLING_INTERVAL }
+--- @return suricatta.status # Suricatta return code
+function server_start(defaults, argv, fconfig)
+ -- Use defaults,
+ -- ... shadowed by configuration file values,
+ -- ... shadowed by command line arguments.
+ local configuration = defaults or {}
+ table.merge(configuration, fconfig or {})
+ device.id, configuration.id = configuration.id, nil
+ gs.polldelay.default, configuration.polldelay = configuration.polldelay, nil
+ gs.channel_config = configuration
+ for opt, arg in getopt(argv or {}, "u:i:p:") do
+ if opt == "u" then
+ gs.channel_config.url = tostring(arg)
+ elseif opt == "i" then
+ device.id = tostring(arg)
+ elseif opt == "p" then
+ gs.polldelay.default = tonumber(arg)
+ elseif opt == ":" then
+ io.stderr:write("Missing argument.")
+ print_help(defaults)
+ return suricatta.status.EINIT
+ end
+ end
+ gs.polldelay.current = gs.polldelay.default
+
+ if not gs.channel_config.url or not device.id then
+ suricatta.notify.error("Mandatory configuration parameter missing.")
+ return suricatta.status.EINIT
+ end
+
+ local res
+ res, gs.channel.default = suricatta.channel.open({ url = gs.channel_config.url, nofollow = true })
+ if not res then
+ suricatta.notify.error("Cannot initialize channel.")
+ return suricatta.status.EINIT
+ end
+
+ suricatta.notify.info("Running with device ID %s on %q.", device.id, gs.channel_config.url)
+ return suricatta.status.OK
+end
+suricatta.server.register(server_start, suricatta.server.SERVER_START)
+
+
+--- Stop the Suricatta server.
+--
+-- Lua counterpart of `server_op_res_t server_stop(void)`.
+--
+--- @return suricatta.status # Suricatta return code
+function server_stop()
+ gs.channel_config = {}
+ for channel, _ in pairs(gs.channel) do
+ gs.channel[channel].close()
+ gs.channel[channel] = {}
+ end
+ return suricatta.status.OK
+end
+suricatta.server.register(server_stop, suricatta.server.SERVER_STOP)
+
+
+--- Query the polling interval from remote.
+--
+-- Lua counterpart of `unsigned int server_get_polling_interval(void)`.
+--
+--- @return number # Polling interval in seconds
+function get_polling_interval()
+ -- Not implemented at server side, hence return device-local polling
+ -- interval that is configurable via IPC or the server-announced wait
+ -- time after having received a 503: busy while serving another client.
+ return gs.polldelay.current
+end
+suricatta.server.register(get_polling_interval, suricatta.server.GET_POLLING_INTERVAL)
+
+
+--- Send device configuration/data to remote.
+--
+-- Lua counterpart of `server_op_res_t server_send_target_data(void)`.
+--
+--- @return suricatta.status # Suricatta return code
+function send_target_data()
+ local res, _, data = gs.channel.default.put({
+ url = string.format("%s/%s", gs.channel_config.url, "log"),
+ content_type = "application/json",
+ method = suricatta.channel.method.PUT,
+ format = suricatta.channel.content.NONE,
+ request_body = string.format([[{"message": "I'm %s and I'm fine."}]], tostring(device.id)),
+ })
+ if not res then
+ suricatta.notify.error("HTTP Error %d while uploading target data.", tonumber(data.http_response_code) or -1)
+ end
+ return suricatta.status.OK
+end
+suricatta.server.register(send_target_data, suricatta.server.SEND_TARGET_DATA)
+
+
+--- Lua-alike of `ipc_message` as in `include/network_ipc.h`
+--
+-- Note: Some members are deliberately not passed through to the Lua realm
+-- such as `ipc_message.data.len` as that's handled by the C-to-Lua bridge
+-- transparently.
+-- Also, this is not a direct equivalent as, e.g., the `json` field is not
+-- present in `struct ipc_message`, but rather it's a "sensible" selection.
+-- As another example, CMD_ENABLE is also not passed through and hence not
+-- in `ipc_commands` as it's handled directly in `suricatta/suricatta.c`.
+--
+--- @type table<string, number>
+--- @class ipc_commands
+--- @field ACTIVATION number 0
+--- @field CONFIG number 1
+--- @field GET_STATUS number 3
+--
+--- @class ipc_message
+--- @field magic number SWUpdate IPC magic number
+--- @field commands ipc_commands IPC commands
+--- @field cmd number Command number, one of `ipc_commands`'s values
+--- @field msg string String data sent via IPC
+--- @field json string If `msg` is JSON, JSON as Lua Table
+
+
+--- Handle IPC messages sent to Suricatta Lua module.
+--
+-- Lua counterpart of `server_op_res_t server_ipc(ipc_message *msg)`.
+--
+--- @param message ipc_message The IPC message sent
+--- @return string # IPC reply string
+--- @return suricatta.status # Suricatta return code
+function ipc(message)
+ if not (message or {}).json then
+ return escape([[{ "request": "IPC requests must be JSON formatted" }]], { ['"'] = '"' }),
+ suricatta.status.EBADMSG
+ end
+ message.msg = message.msg or "<NONE>"
+ if message.cmd == message.commands.CONFIG then
+ suricatta.notify.debug("Got IPC configuration message: %s", message.msg)
+ if message.json.polling then
+ gs.polldelay.default = tonumber(message.json.polling) or gs.polldelay.default
+ return escape([[{ "request": "applied" }]], { ['"'] = '"' }), suricatta.status.OK
+ end
+ elseif message.cmd == message.commands.ACTIVATION then
+ suricatta.notify.debug("Got IPC activation message: %s", message.msg)
+ return escape([[{ "request": "inapplicable" }]], { ['"'] = '"' }), suricatta.status.OK
+ end
+ suricatta.notify.warn("Got unknown IPC message: %s", message.msg)
+ return escape([[{ "request": "unknown" }]], { ['"'] = '"' }), suricatta.status.EBADMSG
+end
+suricatta.server.register(ipc, suricatta.server.IPC)
The example suricatta Lua module implementing support for a server mock implementation modeled after the "General Purpose HTTP Server" showcases how a suricatta Lua module could look like. It is documented extensively supporting and extending the suricatta Lua module documentation. Signed-off-by: Christian Storm <christian.storm@siemens.com> --- examples/suricatta/server_general.py | 190 +++++++ examples/suricatta/swupdate_suricatta.lua | 613 ++++++++++++++++++++++ 2 files changed, 803 insertions(+) create mode 100755 examples/suricatta/server_general.py create mode 100644 examples/suricatta/swupdate_suricatta.lua