mirror of https://github.com/aredn/aredn.git
539 lines
20 KiB
Lua
Executable File
539 lines
20 KiB
Lua
Executable File
#!/usr/bin/lua
|
|
--[[
|
|
|
|
Part of AREDN -- Used for creating Amateur Radio Emergency Data Networks
|
|
Copyright (C) 2021 Tim Wilkinson
|
|
Original Perl Copyright (C) 2015 Conrad Lara
|
|
See Contributors file for additional contributors
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation version 3 of the License.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
Additional Terms:
|
|
|
|
Additional use restrictions exist on the AREDN(TM) trademark and logo.
|
|
See AREDNLicense.txt for more info.
|
|
|
|
Attributions to the AREDN Project must be retained in the source code.
|
|
If importing this code into a new or existing project attribution
|
|
to the AREDN project must be added to the source code.
|
|
|
|
You must not misrepresent the origin of the material contained within.
|
|
|
|
Modified versions must be modified to attribute to the original source
|
|
and be marked in reasonable ways as differentiate it from the original
|
|
version
|
|
|
|
--]]
|
|
|
|
require("nixio")
|
|
require("aredn.hardware")
|
|
require("aredn.http")
|
|
require("aredn.utils")
|
|
local html = require("aredn.html")
|
|
require("uci")
|
|
local aredn_info = require("aredn.info")
|
|
|
|
-- helpers start
|
|
|
|
function mesh_ip_to_hostnames(ip)
|
|
if not ip or ip == "" or ip == "none" then
|
|
return ""
|
|
end
|
|
local pattern = "^" .. ip .. "%s+([%w%-%.]+)"
|
|
local host = "none"
|
|
for line in io.lines("/etc/hosts")
|
|
do
|
|
local host = line:match(pattern)
|
|
if host then
|
|
return host.gsub("%s+", " / ")
|
|
end
|
|
end
|
|
if nixio.fs.stat("/var/run/hosts_olsr.stable") then
|
|
for line in io.lines("/var/run/hosts_olsr.stable")
|
|
do
|
|
local host = line:match(pattern)
|
|
if host then
|
|
host = host:gsub("^dtdlink%.","")
|
|
host = host:gsub("^mid[0-9]*%.","")
|
|
host = host:gsub("%.local.mesh$","")
|
|
return host
|
|
end
|
|
end
|
|
end
|
|
return host
|
|
end
|
|
|
|
function get_default_gw(iface)
|
|
-- wan will route via table 254 default gw
|
|
-- wifi will route via OLSR table 31 default gw
|
|
local rtable = ""
|
|
if iface == "wan" then
|
|
rtable = "ip route list table 254"
|
|
elseif iface == "wifi" then
|
|
rtable = "ip route list table 31"
|
|
else
|
|
return "none"
|
|
end
|
|
|
|
local p = io.popen(rtable)
|
|
if p then
|
|
for line in p:lines()
|
|
do
|
|
local gw = line:match("^default%svia%s([%d%.]+)")
|
|
if gw then
|
|
p:close()
|
|
return gw
|
|
end
|
|
end
|
|
p:close()
|
|
end
|
|
return "none"
|
|
end
|
|
|
|
function get_memavail()
|
|
local f = io.open("/proc/meminfo", "r")
|
|
if f then
|
|
for line in f:lines()
|
|
do
|
|
local memavail = line:match("^MemAvailable:%s+(%d+)%s+kB")
|
|
if memavail then
|
|
f:close()
|
|
return tonumber(memavail)
|
|
end
|
|
end
|
|
f:close()
|
|
end
|
|
return 0
|
|
end
|
|
|
|
function get_wifi_signal(wifiif)
|
|
local signal = -1000
|
|
local noise = -1000
|
|
for mac, station in pairs(iwinfo.nl80211.assoclist(wifiif))
|
|
do
|
|
if station.signal ~= 0 and station.signal > signal then
|
|
signal = station.signal
|
|
end
|
|
if station.noise ~= 0 and station.noise > noise then
|
|
noise = station.noise
|
|
end
|
|
end
|
|
if signal == -1000 or noise == -1000 then
|
|
return "none", "none"
|
|
else
|
|
if signal > 0 then
|
|
signal = (0 - signal)
|
|
end
|
|
if noise > 0 then
|
|
noise = (0 - noise)
|
|
end
|
|
return signal, noise
|
|
end
|
|
end
|
|
|
|
-- helpers end
|
|
|
|
local node = aredn_info.get_nvram("node")
|
|
if node == "" then
|
|
node = "NOCALL"
|
|
end
|
|
local tactical = aredn_info.get_nvram("tactical")
|
|
local config = aredn_info.get_nvram("config")
|
|
if config == "" or nixio.fs.stat("/etc/config.mesh", "type") ~= "dir" then
|
|
config = "not set"
|
|
end
|
|
|
|
local haswifi = aredn.hardware.has_wifi()
|
|
local wifi_iface = aredn.hardware.get_iface_name("wifi")
|
|
local wifi_nr = wifi_iface:match("wlan(%d+)")
|
|
local wifi_disabled = true
|
|
local radio = "radio0"
|
|
if haswifi and wifi_nr then
|
|
wifi_disabled = false
|
|
radio = "radio" .. wifi_nr
|
|
end
|
|
|
|
local cursor = uci.cursor()
|
|
|
|
local wifi_channel
|
|
local wifi_chanbw
|
|
local wifi_freq_range = ""
|
|
local wifi_ssid
|
|
if not wifi_disabled then
|
|
wifi_channel = cursor:get("wireless", radio, "channel")
|
|
wifi_channel = tonumber(wifi_channel) or 0
|
|
wifi_chanbw = tonumber(cursor:get("wireless", radio, "chanbw") or "20")
|
|
local rfchans = aredn.hardware.get_rfchannels(wifi_iface)
|
|
if rfchans and rfchans[1] then
|
|
local basefreq = rfchans[1].frequency
|
|
if basefreq > 3000 and basefreq < 5000 then
|
|
wifi_channel = wifi_channel * 5 + 3000
|
|
elseif basefreq > 900 and basefreq < 2300 then
|
|
wifi_channel = wifi_channel * 5 + 887
|
|
end
|
|
for _, chan in ipairs(rfchans)
|
|
do
|
|
if chan.number == wifi_channel then
|
|
wifi_freq_range = (chan.frequency - wifi_chanbw / 2) .. " - " .. (chan.frequency + wifi_chanbw / 2) .. " MHz"
|
|
break
|
|
end
|
|
end
|
|
end
|
|
|
|
wifi_ssid = "none"
|
|
cursor:foreach("wireless", "wifi-iface",
|
|
function (section)
|
|
if section.network == "wifi" then
|
|
wifi_ssid = section.ssid
|
|
return false
|
|
end
|
|
end
|
|
)
|
|
end
|
|
|
|
local node_desc = cursor:get("system", "@system[0]", "description")
|
|
local lat_lon = "<strong>Location Not Available</strong>"
|
|
local lat = cursor:get("aredn", "@location[0]", "lat")
|
|
local lon = cursor:get("aredn", "@location[0]", "lon")
|
|
if lat and lon then
|
|
lat_lon = string.format("<center><strong>Location: </strong> %s %s</center>", lat, lon)
|
|
end
|
|
|
|
local host_total = 0
|
|
local host_nodes = 0
|
|
if nixio.fs.stat("/var/run/hosts_olsr.stable") then
|
|
for line in io.lines("/var/run/hosts_olsr.stable")
|
|
do
|
|
if line:match("^10%.") and not line:match("%smid%d+%.") then
|
|
host_total = host_total + 1
|
|
local host = line:match("^10%..+%sdtdlink%.")
|
|
if host then
|
|
host_nodes = host_nodes + 1
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- post data
|
|
|
|
if os.getenv("REQUEST_METHOD") == "POST" then
|
|
require('luci.http')
|
|
local request = luci.http.Request(nixio.getenv(),
|
|
function()
|
|
local v = io.read(1024)
|
|
if not v then
|
|
io.close()
|
|
end
|
|
return v
|
|
end
|
|
)
|
|
local css = request:formvalue("css")
|
|
if css and css:match("%.css$") and nixio.fs.stat("/www/" .. css) then
|
|
nixio.fs.unlink("/tmp/web/style.css")
|
|
nixio.fs.symlink("/www/" .. css, "/tmp/web/style.css")
|
|
end
|
|
end
|
|
|
|
-- generate page
|
|
|
|
http_header()
|
|
html.header(node .. " status", true)
|
|
html.print("<body><form method='post' action='/cgi-bin/status' enctype='multipart/form-data'>")
|
|
html.print("<center>")
|
|
|
|
html.alert_banner()
|
|
html.msg_banner()
|
|
|
|
-- page header
|
|
html.print("<h1><big>" .. node)
|
|
if tactical ~= "" then
|
|
html.print(" / " .. tactical)
|
|
end
|
|
html.print("</big></h1>")
|
|
html.print("<center>" .. lat_lon .. "</center>")
|
|
if node_desc then
|
|
html.print("<table id='node_description_display'><tr><td>" .. node_desc .. "</td></tr></table>")
|
|
end
|
|
html.print("<hr>")
|
|
|
|
-- Work out what config mode we're in. We use this to better instruct the
|
|
-- user about the next steps
|
|
local config_mode = nil
|
|
if config == "not set" then
|
|
config_mode = "ram"
|
|
for line in io.lines("/proc/mounts")
|
|
do
|
|
if line:match("overlay") or line:match("ext4") then
|
|
config_mode = "setup"
|
|
break
|
|
end
|
|
end
|
|
end
|
|
|
|
-- nav buttons
|
|
html.navbar_user("status", config_mode)
|
|
|
|
html.print("<input type=hidden name=reload value=reload>")
|
|
|
|
if config_mode then
|
|
html.print("<b><br><br>This node is not yet configured.<br>")
|
|
if config_mode == "setup" then
|
|
html.print("Go to the <a href='setup'>setup page</a> and set your node name and password.<br>")
|
|
html.print("Click Save Changes, <u>even if you didn't make any changes</u>, then the node will reboot.</b>")
|
|
else
|
|
html.print("<br><b>*** WARNING ***</b><p>AREDN is currently running a temporary image.<br>All configuration changes will be lost if you reboot.</p>")
|
|
html.print("Before doing anything else, please go<br>to the <a href='admin'>administration page</a> and upload firmware.<br>")
|
|
end
|
|
html.print("<br><br>")
|
|
html.print("<div style='max-width:540px; text-align:left'>")
|
|
html.print("<p>This device can be configured to either permit or prohibit known encrypted traffic on its RF link. It is up to the user to decide which is appropriate based on how it will be used and the license under which it will be operated. These rules vary by country, frequency, and intended use. You are encouraged to read and understand these rules before going further.</p>")
|
|
html.print("<p>This device is pre-configured with no restrictions as to the type of data being passed.</p>")
|
|
html.print("<p>Follow these steps if <span style=\"text-decoration: underline\">you wish to prohibit</span> known encrypted traffic on the RF link. These instructions will disappear, so copy them for your reference:</p>")
|
|
html.print("<p><ol>")
|
|
html.print("<li>Setup your node name and password as instructed at the top of this page</li>")
|
|
html.print("<li>After you Save Changes allow your node to reboot</li>")
|
|
html.print("<li>Return to the Node Status page and navigate to Setup > Administration</li>")
|
|
html.print("<li>Obtain the blockknownencryption package from the AREDN™ website OR refresh the Package list (node must be connected to the internet)</li>")
|
|
html.print("<li>Install the blockknownencryption package by uploading it or choosing it from the package drop-down list</li>")
|
|
html.print("<li>Wait until the package installs and then reboot your node</li>")
|
|
html.print("</ol></p>")
|
|
html.print("</div>")
|
|
end
|
|
|
|
-- status display
|
|
|
|
local col1 = {}
|
|
local col2 = {}
|
|
|
|
-- left column - network interface info
|
|
|
|
local ip = cursor:get("network", "wifi", "ipaddr")
|
|
local mask = cursor:get("network", "wifi", "netmask")
|
|
local cidr
|
|
if not ip or not mask then
|
|
col1[#col1 + 1] = "<th align=right><nobr>primary address:</nobr></th><td>none<br>"
|
|
else
|
|
cidr = netmask_to_cidr(mask)
|
|
if wifi_disabled then
|
|
col1[#col1 + 1] = "<th align=right><nobr>primary address:</nobr></th><td>" .. ip .. " <small>/ " .. cidr .. "</small><br>"
|
|
else
|
|
wifi_gw = get_default_gw("wifi")
|
|
col1[#col1 + 1] = "<th align=right><nobr>mesh address:</nobr><br><nobr>mesh gateway:</nobr><br><nobr>gateway node:</nobr><br>SSID:<br>channel:<br><nobr>channel width:</nobr><br>frequency range:</br></th><td>" .. ip .. " <small>/ " .. cidr .. "</small><br>" .. wifi_gw .. "<br>" .. mesh_ip_to_hostnames(wifi_gw) .. "<br>" .. wifi_ssid .. "<br>" .. wifi_channel .. "<br>" .. wifi_chanbw .. " MHz<br>" .. wifi_freq_range .. "</td>"
|
|
end
|
|
end
|
|
|
|
ip = cursor:get("network", "lan", "ipaddr")
|
|
mask = cursor:get("network", "lan", "netmask")
|
|
local browser_ip
|
|
local remote_ip = os.getenv("REMOTE_ADDRESS")
|
|
if remote_ip then
|
|
remote_ip = remote_ip.match("::ffff:([%d%.]+)")
|
|
end
|
|
local hide_local = false
|
|
if remote_ip then
|
|
browser_ip = remote_ip
|
|
if not validate_same_subnet(remote_ip, ip, mask) then
|
|
hide_local = true
|
|
end
|
|
end
|
|
if ip:match("^10%.") or not hide_local then
|
|
cidr = netmask_to_cidr(mask)
|
|
local lan_wifi_ssid = "none"
|
|
cursor:foreach("wireless", "wifi-iface",
|
|
function (section)
|
|
if section.network == "lan" then
|
|
lan_wifi_ssid = section.ssid
|
|
return false
|
|
end
|
|
end
|
|
)
|
|
if lan_wifi_ssid ~= "none" then
|
|
col1[#col1 + 1] = "<th align=right><nobr>LAN address:</nobr><br><nobr>LAN AP SSID:</nobr></th><td>" .. ip .. " <small>/ " .. cidr .. "</small><br>" .. lan_wifi_ssid .. "</td>"
|
|
else
|
|
col1[#col1 + 1] = "<th align=right><nobr>LAN address:</nobr></th><td>" .. ip .. " <small>/ " .. cidr .. "</small></td>"
|
|
end
|
|
end
|
|
|
|
local wan_iface = aredn.hardware.get_iface_name("wan")
|
|
if wan_iface and not hide_local then
|
|
local ip, bcast, mask = aredn.hardware.get_interface_ip4(wan_iface)
|
|
if not ip then
|
|
if not wifi_gw or wifi_gw == "none" then
|
|
col1[#col1 + 1] = "<th align=right valign=top><nobr>WAN address:</nobr><br><nobr>default gateway:</nobr></th><td valign=top>none<br>none</td>"
|
|
else
|
|
col1[#col1 + 1] = "<th align=right valign=top><nobr>WAN address:</nobr><br><nobr>default gateway:</nobr><br><nobr>gateway node:</nobr></th><td valign=top>none<br>" .. wifi_gw .. "<br>" .. mesh_ip_to_hostnames(wifi_gw) .. "</td>"
|
|
end
|
|
else
|
|
local wprefix = ""
|
|
local wan_wifi_snr = "none"
|
|
local wan_wifi_ssid
|
|
if wan_iface:match("^wlan%d+$") then
|
|
wprefix = "wifi "
|
|
local s, n = get_wifi_signal(wan_iface)
|
|
if s ~= "none" and n ~= "none" then
|
|
wan_wifi_snr = math.abs(s - n)
|
|
end
|
|
cursor:foreach("wireless", "wifi-iface",
|
|
function (section)
|
|
if section.network == "wan" then
|
|
wan_wifi_ssid = section.ssid
|
|
return false
|
|
end
|
|
end
|
|
)
|
|
if not wan_wifi_ssid then -- if still nil then set default
|
|
wan_wifi_ssid = "none"
|
|
end
|
|
end
|
|
|
|
cidr = netmask_to_cidr(mask)
|
|
wan_gw = get_default_gw("wan")
|
|
if wprefix == "" then -- no wifi wan
|
|
if wan_gw:match("^10%.") or not hide_local then
|
|
if wan_gw:match("^10%.") then
|
|
col1[#col1 + 1] = "<th align=right><nobr><nobr>WAN address:</nobr><br>gateway:<br><nobr>gateway node:</nobr></th><td>" .. ip .. " <small>/ " .. cidr .. "</small><br>" .. wan_gw .. "<br><nobr>" .. mesh_ip_to_hostnames(wan_gw) .. "</nobr></td>"
|
|
else
|
|
col1[#col1 + 1] = "<th align=right><nobr>WAN address:</nobr><br>default gateway:</th><td>" .. ip .. " <small>/ " .. cidr .. "</small><br>" .. wan_gw .. "</td>"
|
|
end
|
|
end
|
|
else -- with wifi wan
|
|
if wan_wifi_ssid ~= "none" and wan_wifi_snr ~= "none" then
|
|
if wan_gw:match("^10%.") or not hide_local then
|
|
if wan_gw:match("^10%.") then
|
|
col1[#col1 + 1] = "<th align=right><nobr>" .. wprefix .. "WAN address:</nobr><br><nobr>SSID | SNR:</nobr><br>gateway:<br><nobr>gateway node:</nobr></th><td>" .. ip .. " <small>/ " .. cidr .. "</small><br><nobr>" .. wan_wifi_ssid .. " | " .. wan_wifi_snr .. " dB<br>" .. wan_gw .. "<br><nobr>" .. mesh_ip_to_hostnames(wan_gw) .. "</nobr></td>"
|
|
else
|
|
col1[#col1 + 1] = "<th align=right><nobr>" .. wprefix .. "WAN address:</nobr><br><nobr>SSID | SNR:</nobr><br>default gateway:</th><td>" .. ip .. " <small>/ " .. cidr .. "</small><br><nobr>" .. wan_wifi_ssid .. " | " .. wan_wifi_snr .. " dB<br>" .. wan_gw .. "</td>"
|
|
end
|
|
end
|
|
else
|
|
if wan_gw:match("^10%.") or not hide_local then
|
|
if wan_gw:match("^10%.") then
|
|
col1[#col1 + 1] = "<th align=right><nobr>" .. wprefix .. "WAN address:</nobr><br>gateway:<br><nobr>gateway node:</nobr></th><td>" .. ip .. " <small>/ " .. cidr .. "</small><br>" .. wan_gw .. "<br><nobr>" .. mesh_ip_to_hostnames(wan_gw) .. "</nobr></td>"
|
|
else
|
|
col1[#col1 + 1] = "<th align=right><nobr>" .. wprefix .. "WAN address:</nobr><br>default gateway:</th><td>" .. ip .. " <small>/ " .. cidr .. "</small><br>" .. wan_gw .. "</td>"
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
if browser_ip then
|
|
col1[#col1 + 1] = "<th align=right><nobr>your address:</nobr></th><td>" .. browser_ip .. "<br><nobr>" .. mesh_ip_to_hostnames(browser_ip) .. "</nobr></td>"
|
|
end
|
|
|
|
-- right column - system info
|
|
|
|
if config == "mesh" and not wifi_disabled then
|
|
col2[#col2 + 1] = "<th align=right valign=middle><nobr>signal|noise|SNR:</nobr></th><td valign=middle><nobr>"
|
|
local s, n = get_wifi_signal(wifi_iface)
|
|
if s == "none" then
|
|
col2[#col2] = col2[#col2] .. "no RF links"
|
|
col2[#col2] = col2[#col2] .. " <button type=button onClick='window.location=\"signal?realtime=1\"' title='Display continuous or archived signal strength on a chart'>Charts</button></nobr></td>"
|
|
else
|
|
col2[#col2] = col2[#col2] .. "<b>" .. s .. " | " .. n .. " | " .. math.abs(s - n) .. " dB</b>"
|
|
col2[#col2] = col2[#col2] .. " <button type=button onClick='window.location=\"signal?realtime=1\"' title='Display continuous or archived signal strength on a chart'>Charts</button></nobr></td>"
|
|
end
|
|
end
|
|
|
|
local azimuth = cursor:get("aredn", "@location[0]", "azimuth")
|
|
if tonumber(azimuth) then
|
|
azimuth = azimuth .. "°"
|
|
else
|
|
azimuth = nil
|
|
end
|
|
local elevation = cursor:get("aredn", "@location[0]", "elevation")
|
|
if tonumber(elevation) then
|
|
elevation = elevation .. "°"
|
|
else
|
|
elevation = nil
|
|
end
|
|
local height = cursor:get("aredn", "@location[0]", "height")
|
|
if tonumber(height) then
|
|
height = height .. "m"
|
|
else
|
|
height = nil
|
|
end
|
|
local antenna
|
|
if not wifi_disabled then
|
|
antenna = aredn.hardware.get_current_antenna(wifi_iface)
|
|
if antenna then
|
|
antenna = antenna.description
|
|
end
|
|
end
|
|
col2[#col2 + 1] = "<th align=right><nobr>firmware version:</nobr><br><nobr>model:</nobr><br>" ..
|
|
(antenna and "<br><nobr>antenna:</nobr>" or "") ..
|
|
(azimuth and "<br><nobr>azimuth:</nobr>" or "") ..
|
|
(elevation and "<br><nobr>elevation:</nobr>" or "") ..
|
|
(height and "<br><nobr>height:</nobr>" or "") ..
|
|
"</th><td>" ..
|
|
read_all("/etc/mesh-release") .. "<br>" .. (aredn.hardware.get_radio() or { name = "unknown" }).name .. "<br>" ..
|
|
(antenna and "<br>" .. antenna or "") ..
|
|
(azimuth and "<br>" .. azimuth or "") ..
|
|
(elevation and "<br>" .. elevation or "") ..
|
|
(height and "<br>" .. height or "") ..
|
|
"</td>"
|
|
|
|
local sysinfo = nixio.sysinfo()
|
|
local uptime = string.format("%d:%02d", math.floor(sysinfo.uptime / 3600) % 24, math.floor(sysinfo.uptime / 60) % 60)
|
|
if sysinfo.uptime >= 172800 then
|
|
uptime = math.floor(sysinfo.uptime / 86400) .. " days, " .. uptime
|
|
elseif sysinfo.uptime >= 86400 then
|
|
uptime = "1 day, " .. uptime
|
|
end
|
|
col2[#col2 + 1] = "<th align=right><nobr>system time:</nobr><br>uptime:</th><td>" .. os.date("%a %b %e %Y") .. " " .. os.date("%T %Z") .. "<br>" .. uptime .. "</td>"
|
|
|
|
local vfs = nixio.fs.statvfs("/overlay")
|
|
local fspace = vfs.bfree * vfs.bsize / 1024
|
|
if fspace < 100 then
|
|
fspace = "<blink><b>" .. fspace .. " KB</b></blink>"
|
|
else
|
|
fspace = fspace .. " KB"
|
|
end
|
|
|
|
local rspace = sysinfo.freeram / 1024
|
|
local mavail = get_memavail()
|
|
if rspace < mavail then
|
|
rspace = mavail
|
|
end
|
|
|
|
if rspace < 500 then
|
|
rspace = "<blink><b>" .. rspace .. " KB</b></blink>"
|
|
else
|
|
rspace = rspace .. " KB"
|
|
end
|
|
|
|
col2[#col2 + 1] = "<th align=right valign=top><nobr>load average:</nobr><br><nobr>available space:</nobr></th><td>" .. string.format("%.2f, %.2f, %.2f", sysinfo.loads[1], sysinfo.loads[2], sysinfo.loads[3]) .. "<br><nobr><em>flash</em> = " .. fspace .. "</nobr><br><nobr><em>memory</em> = " .. rspace .. "</nobr></td>"
|
|
col2[#col2 + 1] = "<th align=right valign=top><nobr>host entries:</nobr></th><td><b>" .. host_nodes .. "</b> nodes / <b>" .. host_total .. "</b> total devices</td>"
|
|
|
|
-- now print the tables
|
|
|
|
html.print("<br><br><table>")
|
|
html.print("<tr><td valign=top><table cellpadding=4>")
|
|
for i, line in ipairs(col1)
|
|
do
|
|
html.print("<tr>" .. line .. "</tr>")
|
|
end
|
|
html.print("</table></td><td valign=top><table cellpadding=4>")
|
|
for i, line in ipairs(col2)
|
|
do
|
|
html.print("<tr>" .. line .. "</tr>")
|
|
end
|
|
html.print("</table></td></tr></table>")
|
|
|
|
-- end
|
|
html.print("</center></form>")
|
|
|
|
html.footer()
|
|
html.print("</body></html>")
|