#!/opt/lnxall_app/bin/lua

-- Created by jiaqiang.ye@lnxall.com
-- Simple Local Device Command Batch Executor
-- 2025/03/06

local cjson = require 'cjson'
local posix = require 'posix'
local invoker = require 'invoker'

local gfmt = string.format

local g_iplist         = nil -- local ip list
local g_linecmd        = nil -- shell command to be executed on remote device
local g_file0, g_file1 = nil, nil -- files to be copied to remote device
local g_onhost = nil -- whether the command line is invoked on host

local function badstr(str)
	if type(str) == "string" and #str > 0 then return false end
	return true
end

local function find_localip(ndev)
	local okay, output = invoker.invoke(invoker.OUTPUT, "ip", "neigh")
	if okay ~= 0 or type(output) ~= "string" then
		io.stderr:write("Error, command `ip neigh` has failed.\n")
		io.stderr:flush()
		return nil
	end

	local needle = "dev " .. ndev
	local nip, iptab = 0, {}
	for line in string.gmatch(output, "[^\r\n]+") do
		if string.find(line, needle, 1, true) then
			local ip = string.match(line, "^([%d%.]+)%s")
			if invoker.isipv4(ip) then
				nip = nip + 1
				iptab[nip] = ip
			end
		end
	end

	if nip == 0 then
		io.stderr:write(gfmt("Error, no device found via network device: %s\n", ndev))
		io.stderr:flush()
		return nil
	end
	if nip >= 2 then table.sort(iptab) end
	io.stdout:write("{\n\t\"iplist\":[\n")
	for i, ip in ipairs(iptab) do
		if i >= 2 then
			io.stdout:write(gfmt(",\n\t\t\"%s\"", ip))
		else
			io.stdout:write(gfmt("\t\t\"%s\"", ip))
		end
		io.stdout:flush()
	end
	io.stdout:write("\n\t]\n}\n")
	io.stdout:flush()
	return iptab
end

local function read_localip(iplist)
	local iph = io.open(iplist, "rb")
	if not iph then
		io.stderr:write(gfmt("Error, failed to open '%s'\n", iplist))
		io.stderr:flush()
		return nil
	end

	local ipd = iph:read("*a")
	iph:close(); iph = nil
	if badstr(ipd) then
		io.stderr:write(gfmt("Error, failed to read '%s'\n", iplist))
		io.stderr:flush()
		return nil
	end

	local okay, ipjson = pcall(cjson.decode, ipd)
	local ilist = type(ipjson) == "table" and ipjson["iplist"] or nil
	if not okay or type(ilist) ~= "table" then
		io.stderr:write(gfmt("Error, invalid iplist file: '%s'\n", iplist))
		io.stderr:flush()
		return nil
	end

	return ilist
end

local function check_args(iplist, linecmd, file0, file1)
	if badstr(iplist) or badstr(linecmd) then
		io.stderr:write("Error, invalid command-line arguments\n")
		io.stderr:flush()
		return false
	end

	if invoker.invoke(invoker.NOSTDIO, "ip", "link", "show", "dev", iplist) == 0 then
		g_iplist = find_localip(iplist)
	elseif posix.access(iplist) == 0 then
		g_iplist = read_localip(iplist)
	elseif invoker.isipv4(iplist) then
		g_iplist = { [1] = iplist }
	end

	if type(g_iplist) ~= "table" then
		io.stderr:write("Error, failed to construct IP list.\n")
		io.stderr:flush()
		return false
	end

	local lcmd = invoker.readfile(linecmd, invoker.TRIMEND)	
	if type(lcmd) ~= "string" or #lcmd == 0 or string.find(lcmd, "\r", 1, true) or string.find(lcmd, "\n", 1, true) then
		io.stderr:write(gfmt("Error, invalid one-line command file: '%s'\n", linecmd))
		io.stderr:flush()
		return false
	end
	g_linecmd = lcmd
	local needle = "# BATCH_HOST"
	local lencmd, lenn = string.len(lcmd), string.len(needle)
	if lencmd > lenn and lcmd:sub(lencmd - lenn + 1) == needle then
		g_onhost = true
	end

	if not badstr(file0) then
		if posix.access(file0) ~= 0 then
			io.stderr:write(gfmt("Error, cannot find file: '%s'\n", file0))
			io.stderr:flush()
			return false
		end
		g_file0 = file0
	end

	if not badstr(file1) then
		if posix.access(file1) ~= 0 then
			io.stderr:write(gfmt("Error, cannot find file: '%s'\n", file1))
			io.stderr:flush()
			return false
		end
		g_file1 = file1
	end

	return true
end

local function ssh_copy(ip, file)
	io.stdout:write(gfmt("[%s] Copying '%s' to '%s' ...\n", os.date(), file, ip))
	io.stdout:flush()
	local okay = invoker.invoke(0, "sshpass", "-p", "lnxall123", "scp", "-r",
		"-oConnectTimeout=5", "-oUserKnownHostsFile=/dev/null", "-oStrictHostKeyChecking=no",
		file, gfmt("root@%s:/tmp/", ip))
	return okay == 0
end

local function take_operation(ip)
	if g_file0 and not ssh_copy(ip, g_file0) then
		io.stderr:write(gfmt("Error, failed to copy '%s' to '%s'\n", g_file0, ip))
		io.stderr:flush()
		return false
	end

	if g_file1 and not ssh_copy(ip, g_file1) then
		io.stderr:write(gfmt("Error, failed to copy '%s' to '%s'\n", g_file1, ip))
		io.stderr:flush()
		return false
	end

	local linecmd, okay, needle = nil, nil, "DEVIPADDR"
	if string.find(g_linecmd, needle, 1, true) then
		linecmd = string.gsub(g_linecmd, needle, ip)
	else
		linecmd = g_linecmd
	end

	if g_onhost then
		io.stdout:write(gfmt("[%s] Invoking command on host '%s': %s\n", os.date(), ip, linecmd))
		io.stdout:flush()
		okay = invoker.invoke(0, "sh", "-c", linecmd)
	else
		io.stdout:write(gfmt("[%s] Invoking command on device '%s': %s\n", os.date(), ip, linecmd))
		io.stdout:flush()
		okay = invoker.invoke(0, "sshpass", "-p", "lnxall123", "ssh",
			"-oConnectTimeout=5", "-oUserKnownHostsFile=/dev/null", "-oStrictHostKeyChecking=no",
			gfmt("root@%s", ip), linecmd)
	end
	return okay == 0
end

local function batch_operation()
	local ni = 0
	local sep0 = string.rep("*", 120) .. "\n"
	local sep1 = string.rep("=", 120) .. "\n"
	while true do
		ni = ni + 1
		local ip = g_iplist[ni]
		if ip == nil then break end
		if invoker.isipv4(ip) then
			io.stdout:write(sep0) ; io.stdout:flush()
			take_operation(ip)
			io.stdout:write(sep1) ; io.stdout:flush()
		else
			ip = tostring(ip)
			if type(ip) ~= "string" then ip = "unknown" end
			io.stderr:write(gfmt("Error, invalid IPv4 address: %s\n", ip))
			io.stderr:flush()
			break
		end
	end
end

local function batchlocal_help()
	io.stdout:write("Usage:\n")
	io.stdout:write("\tbatchlocal NETDEV|IPLIST.JSON ONELINE-CMD.SH [COPY_FILE0] [COPY_FILE1]\n")
	io.stdout:write("\tExample: batchlocal eth0 oneline-cmd.sh XXXX0.elf\n")
	io.stdout:write("\tExample: batchlocal iplist.json oneline-cmd.sh XXXX0.elf XXXX1.elf\n")
	io.stdout:write("Note:\n\tContents of `oneline-cmd.sh` could be:\n")
	io.stdout:write("cd /tmp && chmod 755 XXXX0.elf && ./XXXX0.elf -n ; rm -f -v *.elf\n")
	io.stdout:flush()
end

if not check_args(arg[1], arg[2], arg[3], arg[4]) then
	batchlocal_help()
	os.exit(1)
end
batch_operation()
os.exit(0)
