From 8d5d7531a892d029f84005ffbc44a6d31f710514 Mon Sep 17 00:00:00 2001 From: lemoer Date: Thu, 12 Aug 2021 01:41:37 +0200 Subject: [PATCH] gluon-config-api*: split to packages --- package/features | 5 + .../gluon-config-api-contact-info/Makefile | 16 ++ .../gluon/config-api/parts/contact-info.lua | 5 +- .../Makefile | 6 +- .../config-api/controller/controller.lua | 137 ++++++++++++++++++ .../gluon/config-api/controller/schema.lua | 131 +++++++++++++++++ .../lib/gluon/config-api/parts/wizard.lua | 24 +++ .../lib/gluon/config-api}/www/cgi-bin/api | 0 .../lib/gluon/config-mode/www/cgi-bin/api | 8 + .../gluon-config-api-geo-location/Makefile | 16 ++ .../gluon/config-api/parts/geo-location.lua | 3 - .../lib/gluon/config-api/parts/wizard.lua | 24 +++ .../lib/gluon/config-api/www/cgi-bin/api | 8 + 13 files changed, 373 insertions(+), 10 deletions(-) create mode 100644 package/gluon-config-api-contact-info/Makefile rename package/{gluon-config-api => gluon-config-api-contact-info}/luasrc/lib/gluon/config-api/parts/contact-info.lua (88%) rename package/{gluon-config-api => gluon-config-api-core}/Makefile (71%) create mode 100644 package/gluon-config-api-core/luasrc/lib/gluon/config-api/controller/controller.lua create mode 100644 package/gluon-config-api-core/luasrc/lib/gluon/config-api/controller/schema.lua create mode 100644 package/gluon-config-api-core/luasrc/lib/gluon/config-api/parts/wizard.lua rename package/{gluon-config-api/luasrc/lib/gluon/config-mode => gluon-config-api-core/luasrc/lib/gluon/config-api}/www/cgi-bin/api (100%) mode change 100755 => 100644 create mode 100755 package/gluon-config-api-core/luasrc/lib/gluon/config-mode/www/cgi-bin/api create mode 100644 package/gluon-config-api-geo-location/Makefile rename package/{gluon-config-api => gluon-config-api-geo-location}/luasrc/lib/gluon/config-api/parts/geo-location.lua (96%) create mode 100644 package/gluon-config-api/luasrc/lib/gluon/config-api/parts/wizard.lua create mode 100644 package/gluon-config-api/luasrc/lib/gluon/config-api/www/cgi-bin/api diff --git a/package/features b/package/features index 72887e3a..dff1a886 100644 --- a/package/features +++ b/package/features @@ -31,6 +31,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', diff --git a/package/gluon-config-api-contact-info/Makefile b/package/gluon-config-api-contact-info/Makefile new file mode 100644 index 00000000..419796f7 --- /dev/null +++ b/package/gluon-config-api-contact-info/Makefile @@ -0,0 +1,16 @@ +# Copyright (C) 2021 Leonardo Moerlein +# 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)) diff --git a/package/gluon-config-api/luasrc/lib/gluon/config-api/parts/contact-info.lua b/package/gluon-config-api-contact-info/luasrc/lib/gluon/config-api/parts/contact-info.lua similarity index 88% rename from package/gluon-config-api/luasrc/lib/gluon/config-api/parts/contact-info.lua rename to package/gluon-config-api-contact-info/luasrc/lib/gluon/config-api/parts/contact-info.lua index 4fb83396..7a7fb7df 100644 --- a/package/gluon-config-api/luasrc/lib/gluon/config-api/parts/contact-info.lua +++ b/package/gluon-config-api-contact-info/luasrc/lib/gluon/config-api/parts/contact-info.lua @@ -3,18 +3,15 @@ local M = {} function M.schema(site, platform) return { - type = 'object', properties = { wizard = { - type = 'object', properties = { contact = { type = 'string' } } } - }, - required = { 'wizard' } + } } end diff --git a/package/gluon-config-api/Makefile b/package/gluon-config-api-core/Makefile similarity index 71% rename from package/gluon-config-api/Makefile rename to package/gluon-config-api-core/Makefile index 7ee110d8..3eeacf1a 100644 --- a/package/gluon-config-api/Makefile +++ b/package/gluon-config-api-core/Makefile @@ -3,14 +3,14 @@ include $(TOPDIR)/rules.mk -PKG_NAME:=gluon-config-api +PKG_NAME:=gluon-config-api-core PKG_VERSION:=1 include ../gluon.mk -define Package/gluon-config-api +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)) +$(eval $(call BuildPackageGluon,gluon-config-api-core)) diff --git a/package/gluon-config-api-core/luasrc/lib/gluon/config-api/controller/controller.lua b/package/gluon-config-api-core/luasrc/lib/gluon/config-api/controller/controller.lua new file mode 100644 index 00000000..6bb81aed --- /dev/null +++ b/package/gluon-config-api-core/luasrc/lib/gluon/config-api/controller/controller.lua @@ -0,0 +1,137 @@ +local json = require 'jsonc' +local site = require 'gluon.site' +local util = require 'gluon.util' +local ubus = require 'ubus' +local os = require 'os' +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 +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 parts = load_parts() + +entry({"v1", "config"}, call(function(http, renderer) + if http.request.env.REQUEST_METHOD == 'GET' then + http:header('Content-Type', 'application/json; charset=utf-8') + http:write(json.stringify(config_get(parts), true)) + elseif http.request.env.REQUEST_METHOD == 'POST' then + 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 config = json.parse(request_body) + if not config then + http:status(400, 'Bad Request') + http:header('Content-Type', 'application/json; charset=utf-8') + http:write('{ "status": 400, "error": "Bad JSON in Body" }\n') + http:close() + return + end + + -- Verify schema + + local parser = ucl.parser() + local res, err = parser:parse_string(request_body) + + if not res then + http:status(500, 'Internal Server Error.') + http:header('Content-Type', 'application/json; charset=utf-8') + http:write('{ "status": 500, "error": "Internal UCL Parsing Failed. This should not happen at all." }\n') + http:close() + return + end + + res, err = parser:validate(schema_get(parts)) + if not res then + http:status(400, 'Bad Request') + http:header('Content-Type', 'application/json; charset=utf-8') + http:write('{ "status": 400, "error": "Schema mismatch" }\n') + http:close() + return + end + + -- Apply config + config_set(parts, config) + + -- Write result + + http:write(json.stringify(res, true)) + elseif http.request.env.REQUEST_METHOD == 'OPTIONS' then + local result = json.stringify({ + schema = schema_get(parts), + allowed_methods = {'GET', 'POST', 'OPTIONS'} + }, true) + + -- Content-Length is needed, as the transfer encoding is not chunked for OPTIONS. + http:header('Content-Length', tostring(#result)) + http:header('Content-Type', 'application/json; charset=utf-8') + http:write(result) + else + http:status(501, 'Not Implemented') + http:header('Content-Length', '0') + http:write('Not Implemented\n') + end + + http:close() +end)) diff --git a/package/gluon-config-api-core/luasrc/lib/gluon/config-api/controller/schema.lua b/package/gluon-config-api-core/luasrc/lib/gluon/config-api/controller/schema.lua new file mode 100644 index 00000000..f64d891f --- /dev/null +++ b/package/gluon-config-api-core/luasrc/lib/gluon/config-api/controller/schema.lua @@ -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 diff --git a/package/gluon-config-api-core/luasrc/lib/gluon/config-api/parts/wizard.lua b/package/gluon-config-api-core/luasrc/lib/gluon/config-api/parts/wizard.lua new file mode 100644 index 00000000..08b1343a --- /dev/null +++ b/package/gluon-config-api-core/luasrc/lib/gluon/config-api/parts/wizard.lua @@ -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 diff --git a/package/gluon-config-api/luasrc/lib/gluon/config-mode/www/cgi-bin/api b/package/gluon-config-api-core/luasrc/lib/gluon/config-api/www/cgi-bin/api old mode 100755 new mode 100644 similarity index 100% rename from package/gluon-config-api/luasrc/lib/gluon/config-mode/www/cgi-bin/api rename to package/gluon-config-api-core/luasrc/lib/gluon/config-api/www/cgi-bin/api diff --git a/package/gluon-config-api-core/luasrc/lib/gluon/config-mode/www/cgi-bin/api b/package/gluon-config-api-core/luasrc/lib/gluon/config-mode/www/cgi-bin/api new file mode 100755 index 00000000..a8881b28 --- /dev/null +++ b/package/gluon-config-api-core/luasrc/lib/gluon/config-mode/www/cgi-bin/api @@ -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 +} diff --git a/package/gluon-config-api-geo-location/Makefile b/package/gluon-config-api-geo-location/Makefile new file mode 100644 index 00000000..57c95e65 --- /dev/null +++ b/package/gluon-config-api-geo-location/Makefile @@ -0,0 +1,16 @@ +# Copyright (C) 2021 Leonardo Moerlein +# 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)) diff --git a/package/gluon-config-api/luasrc/lib/gluon/config-api/parts/geo-location.lua b/package/gluon-config-api-geo-location/luasrc/lib/gluon/config-api/parts/geo-location.lua similarity index 96% rename from package/gluon-config-api/luasrc/lib/gluon/config-api/parts/geo-location.lua rename to package/gluon-config-api-geo-location/luasrc/lib/gluon/config-api/parts/geo-location.lua index f4794fde..3ff5e97d 100644 --- a/package/gluon-config-api/luasrc/lib/gluon/config-api/parts/geo-location.lua +++ b/package/gluon-config-api-geo-location/luasrc/lib/gluon/config-api/parts/geo-location.lua @@ -9,10 +9,8 @@ function M.schema(site, platform) end return { - type = 'object', properties = { wizard = { - type = 'object', properties = { location = { type = 'object', @@ -27,7 +25,6 @@ function M.schema(site, platform) } } }, - required = { 'wizard' } } end diff --git a/package/gluon-config-api/luasrc/lib/gluon/config-api/parts/wizard.lua b/package/gluon-config-api/luasrc/lib/gluon/config-api/parts/wizard.lua new file mode 100644 index 00000000..08b1343a --- /dev/null +++ b/package/gluon-config-api/luasrc/lib/gluon/config-api/parts/wizard.lua @@ -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 diff --git a/package/gluon-config-api/luasrc/lib/gluon/config-api/www/cgi-bin/api b/package/gluon-config-api/luasrc/lib/gluon/config-api/www/cgi-bin/api new file mode 100644 index 00000000..a8881b28 --- /dev/null +++ b/package/gluon-config-api/luasrc/lib/gluon/config-api/www/cgi-bin/api @@ -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 +}