Merge 3f739d513c
into e40ed5810d
This commit is contained in:
commit
4a67eafb08
@ -36,6 +36,11 @@ when(_'web-advanced' and _'autoupdater', {
|
||||
'gluon-web-autoupdater',
|
||||
})
|
||||
|
||||
feature('config-api', {
|
||||
'gluon-config-api-contact-info',
|
||||
'gluon-config-api-geo-location',
|
||||
})
|
||||
|
||||
|
||||
when(_'mesh-batman-adv-15', {
|
||||
'gluon-ebtables-limit-arp',
|
||||
|
16
package/gluon-config-api-contact-info/Makefile
Normal file
16
package/gluon-config-api-contact-info/Makefile
Normal file
@ -0,0 +1,16 @@
|
||||
# Copyright (C) 2021 Leonardo Moerlein <me at irrelefant.net>
|
||||
# This is free software, licensed under the Apache 2.0 license.
|
||||
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=gluon-config-api-contact-info
|
||||
PKG_VERSION:=1
|
||||
|
||||
include ../gluon.mk
|
||||
|
||||
define Package/gluon-config-api-contact-info
|
||||
TITLE:=Allows the user to provide contact information to be distributed in the mesh
|
||||
DEPENDS:=+gluon-config-api-core
|
||||
endef
|
||||
|
||||
$(eval $(call BuildPackageGluon,gluon-config-api-contact-info))
|
@ -0,0 +1,32 @@
|
||||
|
||||
local M = {}
|
||||
|
||||
function M.schema(site, platform)
|
||||
return {
|
||||
properties = {
|
||||
wizard = {
|
||||
properties = {
|
||||
contact = {
|
||||
type = 'string'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
function M.set(config, uci)
|
||||
local owner = uci:get_first("gluon-node-info", "owner")
|
||||
|
||||
uci:set("gluon-node-info", owner, "contact", config.wizard.contact)
|
||||
uci:save("gluon-node-info")
|
||||
end
|
||||
|
||||
function M.get(uci, config)
|
||||
local owner = uci:get_first("gluon-node-info", "owner")
|
||||
|
||||
config.wizard = config.wizard or {}
|
||||
config.wizard.contact = uci:get("gluon-node-info", owner, "contact")
|
||||
end
|
||||
|
||||
return M
|
16
package/gluon-config-api-core/Makefile
Normal file
16
package/gluon-config-api-core/Makefile
Normal file
@ -0,0 +1,16 @@
|
||||
# Copyright (C) 2021 Leonardo Moerlein <me at irrelefant.net>
|
||||
# This is free software, licensed under the Apache 2.0 license.
|
||||
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=gluon-config-api-core
|
||||
PKG_VERSION:=1
|
||||
|
||||
include ../gluon.mk
|
||||
|
||||
define Package/gluon-config-api-core
|
||||
TITLE:=Provides a REST API to configure the gluon node
|
||||
DEPENDS:=+gluon-web +uhttpd +libucl +gluon-config-mode-core
|
||||
endef
|
||||
|
||||
$(eval $(call BuildPackageGluon,gluon-config-api-core))
|
@ -0,0 +1,142 @@
|
||||
local os = require 'os'
|
||||
local json = require 'jsonc'
|
||||
local site = require 'gluon.site'
|
||||
local glob = require 'posix.glob'
|
||||
local libgen = require 'posix.libgen'
|
||||
local simpleuci = require 'simple-uci'
|
||||
local schema = dofile('/lib/gluon/config-api/controller/schema.lua')
|
||||
local ucl = require "ucl"
|
||||
|
||||
package 'gluon-config-api'
|
||||
|
||||
function load_parts()
|
||||
local parts = {}
|
||||
for _, f in pairs(glob.glob('/lib/gluon/config-api/parts/*.lua')) do
|
||||
table.insert(parts, dofile(f))
|
||||
end
|
||||
return parts
|
||||
end
|
||||
|
||||
function config_get(parts)
|
||||
local config = {}
|
||||
local uci = simpleuci.cursor()
|
||||
|
||||
for _, part in pairs(parts) do
|
||||
part.get(uci, config)
|
||||
end
|
||||
|
||||
return config
|
||||
end
|
||||
|
||||
function schema_get(parts)
|
||||
local total_schema = {}
|
||||
for _, part in pairs(parts) do
|
||||
total_schema = schema.merge_schemas(total_schema, part.schema(site, nil))
|
||||
end
|
||||
return total_schema
|
||||
end
|
||||
|
||||
function config_set(parts, config)
|
||||
local uci = simpleuci.cursor()
|
||||
|
||||
for _, part in pairs(parts) do
|
||||
part.set(config, uci)
|
||||
end
|
||||
|
||||
-- commit all uci configs
|
||||
os.execute('uci commit')
|
||||
end
|
||||
|
||||
local function pump(src, snk)
|
||||
while true do
|
||||
local chunk, src_err = src()
|
||||
local ret, snk_err = snk(chunk, src_err)
|
||||
|
||||
if not (chunk and ret) then
|
||||
local err = src_err or snk_err
|
||||
if err then
|
||||
return nil, err
|
||||
else
|
||||
return true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function json_response(http, obj)
|
||||
local result = json.stringify(obj, true)
|
||||
http:header('Content-Type', 'application/json; charset=utf-8')
|
||||
-- Content-Length is needed, as the transfer encoding is not chunked for
|
||||
-- http method OPTIONS.
|
||||
http:header('Content-Length', tostring(#result))
|
||||
http:write(result..'\n')
|
||||
end
|
||||
|
||||
local function get_request_body_as_json(http)
|
||||
local request_body = ""
|
||||
pump(http.input, function (data)
|
||||
if data then
|
||||
request_body = request_body .. data
|
||||
end
|
||||
end)
|
||||
|
||||
-- Verify that we really have JSON input. UCL is able to parse other
|
||||
-- config formats as well. Those config formats allow includes and so on.
|
||||
-- This may be a security issue.
|
||||
|
||||
local data = json.parse(request_body)
|
||||
|
||||
if not data then
|
||||
http:status(400, 'Bad Request')
|
||||
json_response(http, { status = 400, error = "Bad JSON in Body" })
|
||||
http:close()
|
||||
return
|
||||
end
|
||||
|
||||
return data
|
||||
end
|
||||
|
||||
local function verify_schema(schema, config)
|
||||
local parser = ucl.parser()
|
||||
local res, err = parser:parse_string(json.stringify(config))
|
||||
|
||||
assert(res, "Internal UCL Parsing Failed. This should not happen at all.")
|
||||
|
||||
res, err = parser:validate(schema)
|
||||
return res
|
||||
end
|
||||
|
||||
entry({"v1", "config"}, call(function(http, renderer)
|
||||
local parts = load_parts()
|
||||
|
||||
if http.request.env.REQUEST_METHOD == 'GET' then
|
||||
json_response(http, config_get(parts))
|
||||
elseif http.request.env.REQUEST_METHOD == 'POST' then
|
||||
local config = get_request_body_as_json(http)
|
||||
|
||||
-- Verify schema
|
||||
if not verify_schema(schema_get(parts), config) then
|
||||
http:status(400, 'Bad Request')
|
||||
json_response(http, { status = 400, error = "Schema mismatch" })
|
||||
http:close()
|
||||
return
|
||||
end
|
||||
|
||||
-- Apply config
|
||||
config_set(parts, config)
|
||||
|
||||
-- Write result
|
||||
json_response(http, { status = 200, error = "Accepted" })
|
||||
elseif http.request.env.REQUEST_METHOD == 'OPTIONS' then
|
||||
json_response(http, {
|
||||
schema = schema_get(parts),
|
||||
allowed_methods = {'GET', 'POST', 'OPTIONS'}
|
||||
})
|
||||
else
|
||||
http:status(501, 'Not Implemented')
|
||||
http:header('Content-Length', '0')
|
||||
http:write('Not Implemented\n')
|
||||
end
|
||||
|
||||
http:close()
|
||||
end))
|
@ -0,0 +1,131 @@
|
||||
|
||||
local util = require 'gluon.util'
|
||||
|
||||
local M = {}
|
||||
|
||||
local function merge_types(Ta, Tb)
|
||||
-- T == nil means "any" type is allowed
|
||||
|
||||
if not Ta then return Tb end
|
||||
if not Tb then return Ta end
|
||||
|
||||
-- convert scalar types to arrays
|
||||
if type(Ta) ~= 'table' then Ta = { Ta } end
|
||||
if type(Tb) ~= 'table' then Tb = { Tb } end
|
||||
|
||||
local Tnew = {}
|
||||
|
||||
for _, t in pairs(Ta) do
|
||||
if util.contains(Tb, t) then
|
||||
table.insert(Tnew, t)
|
||||
end
|
||||
end
|
||||
|
||||
assert(#Tnew > 0, 'ERROR: The schema does not match anything at all.')
|
||||
|
||||
if #Tnew == 1 then
|
||||
return Tnew[1] -- convert to scalar
|
||||
else
|
||||
return Tnew
|
||||
end
|
||||
end
|
||||
|
||||
local function keys(tab)
|
||||
local keys = {}
|
||||
if tab then
|
||||
for k, _ in pairs(tab) do
|
||||
table.insert(keys, k)
|
||||
end
|
||||
end
|
||||
return keys
|
||||
end
|
||||
|
||||
local function merge_array(table1, table2)
|
||||
local values = {}
|
||||
if table1 then
|
||||
for _, v in pairs(table1) do
|
||||
table.insert(values, v)
|
||||
end
|
||||
end
|
||||
if table2 then
|
||||
for _, v in pairs(table2) do
|
||||
if not util.contains(values, v) then
|
||||
table.insert(values, v)
|
||||
end
|
||||
end
|
||||
end
|
||||
return values
|
||||
end
|
||||
|
||||
local function deepcopy(o, seen)
|
||||
seen = seen or {}
|
||||
if o == nil then return nil end
|
||||
if seen[o] then return seen[o] end
|
||||
|
||||
local no
|
||||
if type(o) == 'table' then
|
||||
no = {}
|
||||
seen[o] = no
|
||||
|
||||
for k, v in next, o, nil do
|
||||
no[deepcopy(k, seen)] = deepcopy(v, seen)
|
||||
end
|
||||
setmetatable(no, deepcopy(getmetatable(o), seen))
|
||||
else -- number, string, boolean, etc
|
||||
no = o
|
||||
end
|
||||
return no
|
||||
end
|
||||
|
||||
function M.merge_schemas(schema1, schema2)
|
||||
local merged = {}
|
||||
|
||||
merged.type = merge_types(schema1.type, schema2.type)
|
||||
|
||||
function add_property(pkey, pdef)
|
||||
merged.properties = merged.properties or {}
|
||||
merged.properties[pkey] = pdef
|
||||
end
|
||||
|
||||
if not merged.type or merged.type == 'object' then
|
||||
-- generate merged.properties
|
||||
local properties1 = schema1.properties or {}
|
||||
local properties2 = schema2.properties or {}
|
||||
|
||||
for _, pkey in pairs(merge_array(keys(properties1), keys(properties2))) do
|
||||
local pdef1 = properties1[pkey]
|
||||
local pdef2 = properties2[pkey]
|
||||
|
||||
if pdef1 and pdef2 then
|
||||
add_property(pkey, M.merge_schemas(pdef1, pdef2))
|
||||
elseif pdef1 then
|
||||
add_property(pkey, deepcopy(pdef1))
|
||||
elseif pdef2 then
|
||||
add_property(pkey, deepcopy(pdef2))
|
||||
end
|
||||
end
|
||||
|
||||
-- generate merged.additionalProperties
|
||||
if schema1.additionalProperties and schema2.additionalProperties then
|
||||
merged.additionalProperties = M.merge_schemas(
|
||||
schema1.additionalProperties, schema2.additionalProperties)
|
||||
else
|
||||
merged.additionalProperties = false
|
||||
end
|
||||
|
||||
-- generate merged.required
|
||||
merged.required = merge_array(schema1.required, schema2.required)
|
||||
if #merged.required == 0 then
|
||||
merged.required = nil
|
||||
end
|
||||
end
|
||||
|
||||
-- TODO: implement array
|
||||
|
||||
-- generate merged.default
|
||||
merged.default = schema2.default or schema1.default
|
||||
|
||||
return merged
|
||||
end
|
||||
|
||||
return M
|
@ -0,0 +1,24 @@
|
||||
|
||||
local M = {}
|
||||
|
||||
function M.schema(site, platform)
|
||||
return {
|
||||
type = 'object',
|
||||
properties = {
|
||||
wizard = {
|
||||
type = 'object',
|
||||
additionalProperties = false
|
||||
}
|
||||
},
|
||||
additionalProperties = false,
|
||||
required = { 'wizard' }
|
||||
}
|
||||
end
|
||||
|
||||
function M.set(config, uci)
|
||||
end
|
||||
|
||||
function M.get(uci, config)
|
||||
end
|
||||
|
||||
return M
|
@ -0,0 +1,8 @@
|
||||
#!/usr/bin/lua
|
||||
|
||||
require 'gluon.web.cgi' {
|
||||
base_path = '/lib/gluon/config-api',
|
||||
|
||||
layout_package = 'gluon-config-api',
|
||||
layout_template = 'theme/layout', -- only used for error pages
|
||||
}
|
16
package/gluon-config-api-geo-location/Makefile
Normal file
16
package/gluon-config-api-geo-location/Makefile
Normal file
@ -0,0 +1,16 @@
|
||||
# Copyright (C) 2021 Leonardo Moerlein <me at irrelefant.net>
|
||||
# This is free software, licensed under the Apache 2.0 license.
|
||||
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=gluon-config-api-geo-location
|
||||
PKG_VERSION:=1
|
||||
|
||||
include ../gluon.mk
|
||||
|
||||
define Package/gluon-config-api-geo-location
|
||||
TITLE:=Set geographic location of a node
|
||||
DEPENDS:=+gluon-config-api-core
|
||||
endef
|
||||
|
||||
$(eval $(call BuildPackageGluon,gluon-config-api-geo-location))
|
@ -0,0 +1,61 @@
|
||||
|
||||
local M = {}
|
||||
|
||||
function M.schema(site, platform)
|
||||
local altitude = nil
|
||||
|
||||
if site.config_mode.geo_location.show_altitude(false) then
|
||||
altitude = { type = 'number' }
|
||||
end
|
||||
|
||||
return {
|
||||
properties = {
|
||||
wizard = {
|
||||
properties = {
|
||||
location = {
|
||||
type = 'object',
|
||||
properties = {
|
||||
share_location = { type = 'boolean' },
|
||||
lat = { type = 'number' },
|
||||
lon = { type = 'number' },
|
||||
altitude = altitude
|
||||
},
|
||||
required = { 'lat', 'lon', 'share_location' }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
end
|
||||
|
||||
function M.set(config, uci)
|
||||
local location = uci:get_first("gluon-node-info", "location")
|
||||
local config_location = config.wizard.location or {}
|
||||
|
||||
uci:set("gluon-node-info", location, "share_location",
|
||||
config_location.share_location or false)
|
||||
uci:set("gluon-node-info", location, "latitude", config_location.lat)
|
||||
uci:set("gluon-node-info", location, "longitude", config_location.lon)
|
||||
uci:set("gluon-node-info", location, "altitude", config_location.altitude)
|
||||
|
||||
uci:save("gluon-node-info")
|
||||
end
|
||||
|
||||
function M.get(uci, config)
|
||||
config.wizard = config.wizard or {}
|
||||
|
||||
local location = uci:get_first("gluon-node-info", "location")
|
||||
local lon = uci:get("gluon-node-info", location, "longitude")
|
||||
|
||||
if lon then
|
||||
config.wizard.location = {
|
||||
share_location = uci:get_bool("gluon-node-info", location, "share_location"),
|
||||
lat = tonumber(uci:get("gluon-node-info", location, "latitude")),
|
||||
lon = tonumber(lon),
|
||||
-- if uci:get() returns nil, then altitude will not be present in the result
|
||||
altitude = tonumber(uci:get("gluon-node-info", location, "altitude"))
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
51
package/libucl/Makefile
Normal file
51
package/libucl/Makefile
Normal file
@ -0,0 +1,51 @@
|
||||
#
|
||||
# Copyright (C) 2021 OpenWrt.org
|
||||
#
|
||||
# This is free software, licensed under the GNU General Public License v2.
|
||||
# See /LICENSE for more information.
|
||||
#
|
||||
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=libucl
|
||||
PKG_VERSION:=0.8.1
|
||||
PKG_RELEASE:=1
|
||||
|
||||
PKG_SOURCE_PROTO:=git
|
||||
PKG_SOURCE_URL:=https://github.com/vstakhov/libucl
|
||||
PKG_SOURCE_VERSION:=e6c5d8079b95796099693b0889f07a036f78ad77
|
||||
PKG_MAINTAINER:=Leonardo Mörlein <me@irrelefant.net>
|
||||
|
||||
PKG_LICENSE:=BSD-2-Clause
|
||||
|
||||
PKG_FIXUP:=autoreconf
|
||||
|
||||
include $(INCLUDE_DIR)/package.mk
|
||||
include $(INCLUDE_DIR)/autotools.mk
|
||||
|
||||
CONFIGURE_ARGS += \
|
||||
--enable-lua
|
||||
|
||||
define Package/libucl
|
||||
SECTION:=libs
|
||||
CATEGORY:=Libraries
|
||||
TITLE:=Config Parsing
|
||||
DEPENDS:=+liblua
|
||||
URL:=https://github.com/vstakhov/libucl
|
||||
endef
|
||||
|
||||
define Package/libucl/description
|
||||
Universal configuration library parser.
|
||||
endef
|
||||
|
||||
define Package/libucl/install
|
||||
$(INSTALL_DIR) $(1)/usr/lib
|
||||
$(CP) $(PKG_BUILD_DIR)/src/.libs/libucl.so $(1)/usr/lib/
|
||||
$(CP) $(PKG_BUILD_DIR)/src/.libs/libucl.so.5 $(1)/usr/lib/
|
||||
$(CP) $(PKG_BUILD_DIR)/src/.libs/libucl.so.5.1.0 $(1)/usr/lib/
|
||||
$(INSTALL_DIR) $(1)/usr/lib/lua
|
||||
$(CP) $(PKG_BUILD_DIR)/lua/.libs/ucl.so $(1)/usr/lib/lua/ucl.so
|
||||
endef
|
||||
|
||||
|
||||
$(eval $(call BuildPackage,libucl))
|
Loading…
Reference in New Issue
Block a user