diff --git a/package/gluon-hoodselector/Makefile b/package/gluon-hoodselector/Makefile new file mode 100644 index 00000000..394deeae --- /dev/null +++ b/package/gluon-hoodselector/Makefile @@ -0,0 +1,24 @@ +include $(TOPDIR)/rules.mk + +PKG_NAME:=gluon-hoodselector + +GLUON_VERSION:=3 +PKG_VERSION:=2 + +include ../gluon.mk + +define Package/gluon-hoodselector + TITLE:=Automatically migrate nodes between domains. + DEPENDS:=+luaposix +libgluonutil +lua-math-polygon +libjson-c +gluon-site +micrond +luabitop @GLUON_MULTIDOMAIN + CONFLICTS:=+gluon-config-mode-domain-select +endef + +define Package/gluon-hoodselector/description + Hoodselector automatically detects in which domain the node is + located based on its geolocation settings. Domains require + bounding boxes defined as polygons or rectangles. Hoodselector + selects a domain from the list of known domains and migrate + towards it without requiring a reboot. +endef + +$(eval $(call BuildPackageGluon,gluon-hoodselector)) diff --git a/package/gluon-hoodselector/check_site.lua b/package/gluon-hoodselector/check_site.lua new file mode 100644 index 00000000..81b9e39e --- /dev/null +++ b/package/gluon-hoodselector/check_site.lua @@ -0,0 +1,26 @@ +local function check_lat_lon_range(pos, range, label) + need({'hoodselector', 'shapes'}, function() + if (type(pos) ~= "number") then + return false + end + if pos > range or pos < -range then + return false + end + return true + end, true, label.." must match a range +/-"..range) +end + +if this_domain() ~= need_string(in_site({'default_domain'})) then + for _, shape in pairs(need_table(in_domain({'hoodselector', 'shapes'}))) do + need({'hoodselector', 'shapes'}, function() + if #shape < 2 then + return false + end + for _, v in ipairs(shape) do + check_lat_lon_range(v.lat, 90.0, "lat") + check_lat_lon_range(v.lon, 180.0, "lon") + end + return true + end, true, "needs to have at least 2 coordinates for rectangular shapes.") + end +end diff --git a/package/gluon-hoodselector/files/usr/lib/micron.d/hoodselector b/package/gluon-hoodselector/files/usr/lib/micron.d/hoodselector new file mode 100644 index 00000000..6e793151 --- /dev/null +++ b/package/gluon-hoodselector/files/usr/lib/micron.d/hoodselector @@ -0,0 +1 @@ +*/2 * * * * /usr/sbin/hoodselector diff --git a/package/gluon-hoodselector/luasrc/usr/lib/lua/hoodselector/util.lua b/package/gluon-hoodselector/luasrc/usr/lib/lua/hoodselector/util.lua new file mode 100644 index 00000000..b93097a6 --- /dev/null +++ b/package/gluon-hoodselector/luasrc/usr/lib/lua/hoodselector/util.lua @@ -0,0 +1,79 @@ +local util = require ('gluon.util') +local math_polygon = require('math-polygon') +local json = require ('jsonc') +local uci = require('simple-uci').cursor() +local site = require ('gluon.site') +local logger = require('posix.syslog') +local M = {} + +function M.log(msg) + io.stdout:write(msg..'\n') + logger.openlog(msg, logger.LOG_PID) +end + +function M.get_domains() + local list = {} + for _, domain_path in ipairs(util.glob('/lib/gluon/domains/*.json')) do + table.insert(list, { + domain_code = domain_path:match('([^/]+)%.json$'), + domain = assert(json.load(domain_path)), + }) + end + return list +end + +-- Return the default domain from the domain list. +-- This method can return the following data: +-- * default domain +function M.get_default_domain(jdomains) + for _, domain in pairs(jdomains) do + if domain.domain_code == site.default_domain() then + return domain + end + end +end + +-- Get Geoposition. +-- This method can return the following data: +-- * table {lat, lon} +function M.get_geolocation() + return { + lat = tonumber(uci:get('gluon-node-info', uci:get_first('gluon-node-info', 'location'), 'latitude')), + lon = tonumber(uci:get('gluon-node-info', uci:get_first('gluon-node-info', 'location'), 'longitude')) + } +end + +-- Return domain from the domain list based on geo position or nil if no geo based domain could be +-- determined. +function M.get_domain_by_geo(jdomains, geo) + for _, domain in pairs(jdomains) do + if domain.domain_code ~= site.default_domain() then + -- Keep record of how many nested shapes we are in, e.g. a polyon with holes. + local nesting = 1 + for _, area in pairs(domain.domain.hoodselector.shapes) do + -- Convert rectangle, defined by to points, into polygon + if #area == 2 then + area = math_polygon.two_point_rec_to_poly(area) + end + if (math_polygon.point_in_polygon(area, geo) == 1) then + nesting = nesting * (-1) + end + end + if nesting == -1 then return domain end + end + end + return nil +end + +function M.set_domain_config(domain) + if uci:get('gluon', 'core', 'domain') ~= domain.domain_code then + uci:set('gluon', 'core', 'domain', domain.domain_code) + uci:commit('gluon') + os.execute('gluon-reconfigure') + M.log('Set domain "'..domain.domain.domain_names[domain.domain_code]..'"') + return true + end + return false +end + +return M diff --git a/package/gluon-hoodselector/luasrc/usr/sbin/hoodselector b/package/gluon-hoodselector/luasrc/usr/sbin/hoodselector new file mode 100755 index 00000000..12125da7 --- /dev/null +++ b/package/gluon-hoodselector/luasrc/usr/sbin/hoodselector @@ -0,0 +1,56 @@ +#!/usr/bin/lua + +local bit = require('bit') +local unistd = require('posix.unistd') +local fcntl = require('posix.fcntl') +local hoodutil = require('hoodselector.util') + +-- PID file to ensure the hoodselector isn't running parallel +local lockfile = '/var/lock/hoodselector.lock' +local lockfd, err = fcntl.open(lockfile, bit.bor(fcntl.O_WRONLY, fcntl.O_CREAT), 384) -- mode 0600 + +if not lockfd then + hoodutil.log(err, '\n') + os.exit(1) +end + +local ok, _ = fcntl.fcntl(lockfd, fcntl.F_SETLK, { + l_start = 0, + l_len = 0, + l_type = fcntl.F_WRLCK, + l_whence = unistd.SEEK_SET, +}) + +if not ok then + io.stderr:write(string.format( + "Unable to lock file %s. Make sure there is no other instance of the hoodselector running.\n", + lockfile + )) + os.exit(1) +end + +-- geolocation mode +-- If we have a location we will try to select the domain corresponding to this location. +-- If no domain for the location has been defined or if we can't determine the node's location, +-- we will select the default domain as last fallback instance. +local geo = hoodutil.get_geolocation() +if geo.lat ~= nil and geo.lon ~= nil then + io.stdout:write('Position found. Enter "geolocation mode" ...\n') + local jdomains = hoodutil.get_domains() + local geo_base_domain = hoodutil.get_domain_by_geo(jdomains, geo) + if geo_base_domain ~= nil then + if hoodutil.set_domain_config(geo_base_domain) then + os.execute("gluon-reload") + hoodutil.log('Domain set by geolocation mode.\n') + end + return + end + io.stdout:write('No domain has been defined for the current position. Continue with default domain mode\n') +else + io.stdout:write('No position found. Continue with default domain mode\n') +end + +-- default domain mode +if hoodutil.set_domain_config(hoodutil.get_default_domain(hoodutil.get_domains())) then + os.execute("gluon-reload") +end