From d3e4812cbf83d9ed49bc2092db7edf5d9ede9756 Mon Sep 17 00:00:00 2001 From: Wouter R Date: Wed, 17 Jul 2013 17:43:33 +0200 Subject: [PATCH] Implement settings interface. --- src/conf_defaults.lua | 43 +++++++++----- src/test/test_settings.lua | 66 +++++++++++++++++++++ src/util/settings.lua | 115 +++++++++++++++++++++++++++++++++++++ 3 files changed, 209 insertions(+), 15 deletions(-) create mode 100644 src/test/test_settings.lua create mode 100644 src/util/settings.lua diff --git a/src/conf_defaults.lua b/src/conf_defaults.lua index 0b7bdf4..84c62c7 100644 --- a/src/conf_defaults.lua +++ b/src/conf_defaults.lua @@ -1,5 +1,8 @@ local M = {} +--NOTE: proposed notation for baseline configuration (containing defaults as well as type and constraint information) +--the table name is the configuration key; min, max and regex are all optional; type is one of: {bool, int, float, string} + --NOTE: pcall protects from invocation exceptions, which is what we need except --during debugging. This flag replaces them with a normal call so we can inspect stack traces. M.DEBUG_PCALLS = true @@ -7,28 +10,38 @@ M.DEBUG_PCALLS = true --REST responses will contain 'module' and 'function' keys describing what was requested M.API_INCLUDE_ENDPOINT_INFO = false -M.DEFAULT_AP_SSID = "d3d-ap-%MAC_ADDR_TAIL%" -M.DEFAULT_AP_ADDRESS = "192.168.10.1" -M.DEFAULT_AP_NETMASK = "255.255.255.0" +-- was: M.DEFAULT_AP_SSID = "d3d-ap-%MAC_ADDR_TAIL%" +M.apSsid = { + default = 'd3d-ap-%%MAC_ADDR_TAIL%%', + type = 'string', + description = 'Access Point mode SSID', + min = 1, + max = 32 +} + +-- was: M.DEFAULT_AP_ADDRESS = "192.168.10.1" +M.apAddress = { + default = '192.168.10.1', + type = 'string', + description = 'Access Point mode IP address', + regex = '%d+\.%d+\.%d+\.%d+' +} + +-- was: M.DEFAULT_AP_NETMASK = "255.255.255.0" +M.apNetmask = { + default = '255.255.255.0', + type = 'string', + description = 'Access Point mode netmask', + regex = '%d+\.%d+\.%d+\.%d+' +} ---NOTE: proposed notation for baseline configuration (containing defaults as well as type and constraint information) ---the table name is the configuration key; min, max and regex are all optional; type is one of: {int, float, string, ...?} M.temperature = { default = 230, type = 'int', - description = '...xyzzy', + description = '3D printer temperature', min = 0, max = 350 } -M.ssid = { - default = 'd3d-ap-%%MAC_TAIL%%', - type = 'int', --one of: {int, float, string, ...?} - min = 1, - max = 32, - regex = '[a-zA-Z0-9 -=+]+' -} - - return M diff --git a/src/test/test_settings.lua b/src/test/test_settings.lua new file mode 100644 index 0000000..7f1d75f --- /dev/null +++ b/src/test/test_settings.lua @@ -0,0 +1,66 @@ +local s = require('util.settings') +local defaults = require('conf_defaults') + +local uciConfigFile = '/etc/config/wifibox' +local uciConfigFileBackup = '/etc/config/wifibox.orig' + +local M = { + _is_test = true, + _skip = { }, + _wifibox_only = { 'get' } +} + + +function M:_setup() + os.execute('mv -f ' .. uciConfigFile .. ' ' .. uciConfigFileBackup .. ' 2>/dev/null') +end + +function M:_teardown() + os.execute('rm -f ' .. uciConfigFile) + os.execute('mv -f ' .. uciConfigFileBackup .. ' ' .. uciConfigFile .. ' 2>/dev/null') +end + + +function M:test_get() + local realKey, fakeKey = 'apAddress', 'theAnswer' + + assert(not s.exists(fakeKey)) + local fakeValue = s.get(fakeKey) + assert(fakeValue == nil) + + assert(s.exists(realKey)) + local realValue = s.get(realKey) + assert(realValue ~= nil) + assert(realValue == defaults.apAddress.default) +end + +function M:test_set() + local key = 'apAddress' + local goodValue, badValue1, badValue2 = '10.0.0.1', '10.00.1', '10.0.0d.1' + + assert(s.get(key) == defaults.apAddress.default) + assert(s.isDefault(key)) + + assert(s.set(key, goodValue)) + assert(s.get(key) == goodValue) + assert(not s.isDefault(key)) + + assert(s.set(key, badValue1) == nil) + assert(s.get(key) == goodValue) + + assert(s.set(key, badValue2) == nil) + assert(s.get(key) == goodValue) + + assert(s.set(key, nil)) + assert(s.isDefault(key)) +end + +function M:test_setNonExistent() + local fakeKey = 'theAnswer' + + assert(s.get(fakeKey) == nil) + assert(s.set(fakeKey, 42) == nil) + assert(s.get(fakeKey) == nil) +end + +return M diff --git a/src/util/settings.lua b/src/util/settings.lua new file mode 100644 index 0000000..685345f --- /dev/null +++ b/src/util/settings.lua @@ -0,0 +1,115 @@ +--[[ + This settings interface reads and writes its configuration using UCI. + The corresponding config file is /etc/config/wifibox. To have an initial + set of reasonable settings (and allow users to easily return to them), + any key not found in the UCI configuration is looked up in the (immutable) + 'base configuration' (base_config.lua). This file also contains constraints + to check if newly set values are valid. +]]-- +local u = require('util.utils') +local baseconfig = require('conf_defaults') +local uci = require('uci').cursor() + +local M = {} + +local UCI_CONFIG_NAME = 'wifibox' -- the file under /etc/config +local UCI_CONFIG_FILE = '/etc/config/' .. UCI_CONFIG_NAME +local UCI_CONFIG_TYPE = 'settings' -- the section type that will be used in UCI_CONFIG_FILE +local UCI_CONFIG_SECTION = 'general' -- the section name that will be used in UCI_CONFIG_FILE +local ERR_NO_SUCH_KEY = "key does not exist" + + +local function toUciValue(v, type) + if type == 'bool' then return v and '1' or '0' end + return tostring(v) +end + +local function fromUciValue(v, type) + if type == 'bool' then + return (v == '1') and true or false + elseif type == 'float' or type == 'int' then + return tonumber(v) + else + return v + end + +end + +local function isValid(value, baseTable) + local type, min, max, regex = baseTable.type, baseTable.min, baseTable.max, baseTable.regex + + if type == 'bool' then + return isboolean(value) or nil,"invalid bool value" + elseif type == 'int' or type == 'float' then + local ok = isnumber(value) + ok = ok and (type == 'float' or math.floor(value) == value) + if min then ok = ok and value >= min end + if max then ok = ok and value <= max end + return ok or nil,"invalid int/float value" + elseif type == 'string' then + local ok = true + if min then ok = ok and value:len() >= min end + if max then ok = ok and value:len() <= max end + if regex then ok = ok and value:match(regex) ~= nil end + return ok or nil,"invalid string value" + end + + return true +end + +local function getBaseKeyTable(key) + local base = baseconfig[key] + return type(base) == 'table' and base.default ~= nil and base or nil +end + + +function M.get(key) + local base = getBaseKeyTable(key) + + if not base then return nil,ERR_NO_SUCH_KEY end + + local v = base.default + local uciV = fromUciValue(uci:get(UCI_CONFIG_NAME, UCI_CONFIG_SECTION, key)) + + return uciV or v +end + +function M.exists(key) + return getBaseKeyTable(key) ~= nil +end + +function M.isDefault(key) + if not M.exists(key) then return nil,ERR_NO_SUCH_KEY end + return uci:get(UCI_CONFIG_NAME, UCI_CONFIG_SECTION, key) == nil +end + +-- pass nil as value to restore default +function M.set(key, value) + local r = u.create(UCI_CONFIG_FILE) + uci:set(UCI_CONFIG_NAME, UCI_CONFIG_SECTION, UCI_CONFIG_TYPE) + + local base = getBaseKeyTable(key) + if not base then return nil,ERR_NO_SUCH_KEY end + + if M.isDefault(key) and value == nil then return true end -- key is default already + + local current = uci:get(UCI_CONFIG_NAME, UCI_CONFIG_SECTION, key) + + if fromUciValue(current) == value then return true end + + if value ~= nil then + local valid,m = isValid(value, base) + if (valid) then + uci:set(UCI_CONFIG_NAME, UCI_CONFIG_SECTION, key, toUciValue(value, base.type)) + else + return nil,m + end + else + uci:delete(UCI_CONFIG_NAME, UCI_CONFIG_SECTION, key) + end + + uci:commit(UCI_CONFIG_NAME) + return true +end + +return M