From ed3d890b1f73e509d5c12f40e528ce728cb129b3 Mon Sep 17 00:00:00 2001
From: David Bauer <blocktrron@users.noreply.github.com>
Date: Tue, 12 Feb 2019 11:00:29 +0100
Subject: [PATCH] gluon-scheduled-domain-switch: add package (#1555)

This package allows to automatically switch to another domain, either
at a given point in time or after the node was offline long enough.
---
 .../package/gluon-scheduled-domain-switch.rst | 38 +++++++++++
 package/gluon-core/check_site.lua             |  9 ---
 .../luasrc/usr/lib/lua/gluon/util.lua         |  9 +++
 .../gluon-scheduled-domain-switch/Makefile    | 13 ++++
 .../check_site.lua                            |  6 ++
 .../upgrade/950-gluon-scheduled-domain-switch | 20 ++++++
 .../luasrc/usr/bin/gluon-check-connection     | 36 ++++++++++
 .../luasrc/usr/bin/gluon-switch-domain        | 67 +++++++++++++++++++
 scripts/check_site.lua                        |  9 +++
 9 files changed, 198 insertions(+), 9 deletions(-)
 create mode 100644 docs/package/gluon-scheduled-domain-switch.rst
 create mode 100644 package/gluon-scheduled-domain-switch/Makefile
 create mode 100644 package/gluon-scheduled-domain-switch/check_site.lua
 create mode 100755 package/gluon-scheduled-domain-switch/luasrc/lib/gluon/upgrade/950-gluon-scheduled-domain-switch
 create mode 100755 package/gluon-scheduled-domain-switch/luasrc/usr/bin/gluon-check-connection
 create mode 100755 package/gluon-scheduled-domain-switch/luasrc/usr/bin/gluon-switch-domain

diff --git a/docs/package/gluon-scheduled-domain-switch.rst b/docs/package/gluon-scheduled-domain-switch.rst
new file mode 100644
index 000000000..aeb0d967a
--- /dev/null
+++ b/docs/package/gluon-scheduled-domain-switch.rst
@@ -0,0 +1,38 @@
+gluon-scheduled-domain-switch
+=============================
+
+This package allows to switch a routers domain at a given point
+in time. This is needed for switching between incompatible transport
+protocols (e.g. 802.11s and IBSS or VXLAN).
+
+Nodes will switch when the defined *switch-time* has passed. In case the node was
+powered off while this was supposed to happen, it might not be able to aquire the
+correct time. In this case, the node will switch after it has not seen any gateway
+for a given period of time.
+
+site.conf
+---------
+All those settings have to be defined exclusively in the domain, not the site.
+
+domain_switch : optional (needed for domains to switch)
+    target_domain :
+        - target domain to switch to
+    switch_after_offline_mins :
+        - amount of time without reachable gateway to switch unconditionally
+    switch_time :
+        - UNIX epoch after which domain will be switched
+    connection_check_targets :
+        - array of IPv6 addresses which are probed to determine if the node is
+	  connected to the mesh
+
+Example::
+
+  domain_switch = {
+    target_domain = 'new_domain',
+    switch_after_offline_mins = 120,
+    switch_time = 1546344000, -- 01.01.2019 - 12:00 UTC
+    connection_check_targets = {
+      '2001:4860:4860::8888',
+      '2001:4860:4860::8844',
+    },
+  },
diff --git a/package/gluon-core/check_site.lua b/package/gluon-core/check_site.lua
index 7017af7e7..ede64cd02 100644
--- a/package/gluon-core/check_site.lua
+++ b/package/gluon-core/check_site.lua
@@ -3,15 +3,6 @@ need_string(in_site({'site_name'}))
 
 -- this_domain() returns nil when multidomain support is disabled
 if this_domain() then
-	function need_domain_name(path)
-		need_string(path)
-		need(path, function(default_domain)
-			local f = io.open(os.getenv('IPKG_INSTROOT') .. '/lib/gluon/domains/' .. default_domain .. '.json')
-			if not f then return false end
-			f:close()
-			return true
-		end, nil, 'be a valid domain name')
-	end
 	need_domain_name(in_site({'default_domain'}))
 
 	need_table(in_domain({'domain_names'}), function(domain)
diff --git a/package/gluon-core/luasrc/usr/lib/lua/gluon/util.lua b/package/gluon-core/luasrc/usr/lib/lua/gluon/util.lua
index e8b7550be..df9be8d6e 100644
--- a/package/gluon-core/luasrc/usr/lib/lua/gluon/util.lua
+++ b/package/gluon-core/luasrc/usr/lib/lua/gluon/util.lua
@@ -253,3 +253,12 @@ function foreach_radio(uci, f)
 		end
 	end
 end
+
+function get_uptime()
+	local uptime_file = readfile("/proc/uptime")
+	if uptime_file == nil then
+		-- Something went wrong reading "/proc/uptime"
+		return nil
+	end
+	return tonumber(uptime_file:match('^[^ ]+'))
+end
diff --git a/package/gluon-scheduled-domain-switch/Makefile b/package/gluon-scheduled-domain-switch/Makefile
new file mode 100644
index 000000000..b1d7f786d
--- /dev/null
+++ b/package/gluon-scheduled-domain-switch/Makefile
@@ -0,0 +1,13 @@
+include $(TOPDIR)/rules.mk
+
+PKG_NAME:=gluon-scheduled-domain-switch
+PKG_VERSION:=1
+
+include ../gluon.mk
+
+define Package/gluon-scheduled-domain-switch
+  TITLE:=Allows scheduled migrations between domains
+  DEPENDS:=+gluon-core @GLUON_MULTIDOMAIN
+endef
+
+$(eval $(call BuildPackageGluon,gluon-scheduled-domain-switch))
diff --git a/package/gluon-scheduled-domain-switch/check_site.lua b/package/gluon-scheduled-domain-switch/check_site.lua
new file mode 100644
index 000000000..d1a2fa212
--- /dev/null
+++ b/package/gluon-scheduled-domain-switch/check_site.lua
@@ -0,0 +1,6 @@
+if need_table(in_domain({'domain_switch'}), check_domain_switch, false) then
+	need_domain_name(in_domain({'domain_switch', 'target_domain'}))
+	need_number(in_domain({'domain_switch', 'switch_after_offline_mins'}))
+	need_number(in_domain({'domain_switch', 'switch_time'}))
+	need_string_array_match(in_domain({'domain_switch', 'connection_check_targets'}), '^[%x:]+$')
+end
diff --git a/package/gluon-scheduled-domain-switch/luasrc/lib/gluon/upgrade/950-gluon-scheduled-domain-switch b/package/gluon-scheduled-domain-switch/luasrc/lib/gluon/upgrade/950-gluon-scheduled-domain-switch
new file mode 100755
index 000000000..a15ed6822
--- /dev/null
+++ b/package/gluon-scheduled-domain-switch/luasrc/lib/gluon/upgrade/950-gluon-scheduled-domain-switch
@@ -0,0 +1,20 @@
+#!/usr/bin/lua
+
+local json = require 'jsonc'
+local site = require 'gluon.site'
+local unistd = require 'posix.unistd'
+
+local cronfile = "/usr/lib/micron.d/gluon-scheduled-domain-switch"
+
+-- Check if domain switch is scheduled
+if site.domain_switch() == nil then
+	-- In case no domain switch is scheduled, remove cronfile
+	os.remove(cronfile)
+	os.exit(0)
+end
+
+-- Only in case domain switch is scheduled
+local f = io.open(cronfile, "w")
+f:write("* * * * *  /usr/bin/gluon-check-connection\n")
+f:write("*/5 * * * *  /usr/bin/gluon-switch-domain\n")
+f:close()
diff --git a/package/gluon-scheduled-domain-switch/luasrc/usr/bin/gluon-check-connection b/package/gluon-scheduled-domain-switch/luasrc/usr/bin/gluon-check-connection
new file mode 100755
index 000000000..508cd17a6
--- /dev/null
+++ b/package/gluon-scheduled-domain-switch/luasrc/usr/bin/gluon-check-connection
@@ -0,0 +1,36 @@
+#!/usr/bin/lua
+
+local unistd = require 'posix.unistd'
+local util = require 'gluon.util'
+local site = require 'gluon.site'
+
+local offline_flag_file = "/tmp/gluon_offline"
+local is_offline = true
+
+-- Check if domain-switch is scheduled
+if site.domain_switch() == nil then
+	-- Switch not applicable for current domain
+	os.exit(0)
+end
+
+-- Check reachability of pre-defined targets
+for _, ip in ipairs(site.domain_switch.connection_check_targets()) do
+	local exit_code = os.execute("ping -c 1 -w 10 " .. ip)
+	if exit_code == 0 then
+		is_offline = false
+		break
+	end
+end
+
+if is_offline then
+	-- Check if we were previously offline
+	if unistd.access(offline_flag_file) then
+		os.exit(0)
+	end
+	-- Create offline flag
+	local f = io.open(offline_flag_file, "w")
+	f:write(tostring(util.get_uptime()))
+	f:close()
+else
+	os.remove(offline_flag_file)
+end
diff --git a/package/gluon-scheduled-domain-switch/luasrc/usr/bin/gluon-switch-domain b/package/gluon-scheduled-domain-switch/luasrc/usr/bin/gluon-switch-domain
new file mode 100755
index 000000000..57fed15a2
--- /dev/null
+++ b/package/gluon-scheduled-domain-switch/luasrc/usr/bin/gluon-switch-domain
@@ -0,0 +1,67 @@
+#!/usr/bin/lua
+
+local uci = require('simple-uci').cursor()
+local unistd = require 'posix.unistd'
+local util = require 'gluon.util'
+local site = require 'gluon.site'
+
+-- Returns true if node was offline long enough to perform domain switch
+function switch_after_min_reached()
+	if not unistd.access("/tmp/gluon_offline") then
+		return false
+	end
+
+	local switch_after_sec = site.domain_switch.switch_after_offline_mins() * 60
+
+	local current_uptime = util.get_uptime()
+	if current_uptime == nil then
+		return false
+	end
+
+	local f = util.readfile("/tmp/gluon_offline")
+	if f == nil then
+		return false
+	end
+	local offline_since = tonumber(f)
+
+	local offline_time_sec = current_uptime - offline_since
+
+	if offline_time_sec > switch_after_sec then
+		return true
+	end
+	return false
+end
+
+-- Returns true in case switch time has passed
+function switch_time_passed()
+	local current_time = os.time()
+	local switch_time = site.domain_switch.switch_time()
+
+	return switch_time < current_time
+end
+
+if site.domain_switch() == nil then
+	-- Switch not applicable for current domain
+	print("No domain switch defined for the current domain.")
+	os.exit(0)
+end
+
+local current_domain = uci:get("gluon", "core", "domain")
+local target_domain = site.domain_switch.target_domain()
+
+if target_domain == current_domain then
+	-- Current and target domain are equal
+	print("Domain '" .. target_domain .. "' equals current domain.")
+	os.exit(1)
+end
+
+if not switch_after_min_reached() and not switch_time_passed() then
+	-- Neither switch-time passed nor switch_after_min reached
+	os.exit(0)
+end
+
+uci:set("gluon", "core", "domain", target_domain)
+uci:commit("gluon")
+
+os.execute("gluon-reconfigure")
+os.execute("reboot")
diff --git a/scripts/check_site.lua b/scripts/check_site.lua
index 41944cb0c..6db3b1c0c 100644
--- a/scripts/check_site.lua
+++ b/scripts/check_site.lua
@@ -305,6 +305,15 @@ function need_array_of(path, array, required)
 	return need_array(path, function(e) need_one_of(e, array) end, required)
 end
 
+function need_domain_name(path)
+	need_string(path)
+	need(path, function(domain_name)
+		local f = io.open(os.getenv('IPKG_INSTROOT') .. '/lib/gluon/domains/' .. domain_name .. '.json')
+		if not f then return false end
+		f:close()
+		return true
+	end, nil, 'be a valid domain name')
+end
 
 local check = assert(loadfile())
 
-- 
GitLab