diff options
Diffstat (limited to 'dhcp-model.lua')
-rw-r--r-- | dhcp-model.lua | 1327 |
1 files changed, 470 insertions, 857 deletions
diff --git a/dhcp-model.lua b/dhcp-model.lua index 2dd0831..11c0228 100644 --- a/dhcp-model.lua +++ b/dhcp-model.lua @@ -3,975 +3,588 @@ module (..., package.seeall) --- get additional libraries -require("procps") +require("modelfunctions") require("validator") -require("daemoncontrol") -require("procps") -require("processinfo") local subnet = {} -local cfgdir = "/etc/dhcp/" +local configfile = "/etc/dhcp/dhcpd.conf" local processname = "dhcpd" local packagename = "dhcp" +local leasefile = "/var/lib/dhcp/dhcpd.leases" +local config -- ################################################################################ -- LOCAL FUNCTIONS -function process_status_text(procname) - local t = procps.pidof(procname) - if (t) and (#t > 0) then - return "Enabled" - else - return "Disabled" - end -end --- ################################################################################ --- PUBLIC FUNCTIONS - -function getstatus () - local status = {} - - local value, errtxt = processinfo.package_version(packagename) - status.version = cfe({ name = "version", - label="Program version", - value=value, - errtxt=errtxt, - }) - - status.status = cfe({ name="status", - label="Program status", - value=process_status_text(processname), - }) - - local autostart_sequense, autostart_errtxt = processinfo.process_botsequence(processname) - status.autostart = cfe({ name="autostart", - label="Autostart sequence", - value=autostart_sequense, - errtxt=autostart_errtxt, - }) - - return status +local replacemagiccharacters = function(str) + return string.gsub(str, "[%(%)%.%%%+%-%*%?%[%]%^%$]", "%%%1") end ---- the tokenizer functions - must be dislocated into a library later -tokenizer = {} - -tokenizer.new = function( str, delim ) - local token = {} - token.value = str; - token.delim = delim; - token.pos = 1 - return token -end - -tokenizer.pos = function( value, substr, pos ) - local retval = pos - local done = false - while not done and retval <= #value do - if string.sub( value, retval, retval ) == substr then - done = true - else - retval = retval + 1 - end - end - - return retval -end - -tokenizer.next = function( token ) - if token.pos > #token.value then - return token, nil - end - - local strpos = tokenizer.pos( token.value, token.delim, token.pos ) - retval = string.sub(token.value, token.pos, strpos-1) - if retval == token.delim then - retval = "" - token.pos = token.pos + 1 - else - token.pos = strpos + 1 - end - - return token, retval +local replaceentry = function(file, configentry, newstring) + if newstring then + return string.gsub(file, string.gsub(replacemagiccharacters(table.concat(configentry, "\n")), "\n", "%%s+"), replacemagiccharacters(newstring), 1) + else + return string.gsub(file, string.gsub(replacemagiccharacters(table.concat(configentry, "\n")), "\n", "%%s+").."%s*;%s*\n?", "", 1) + end end ---- -dep_check = function () - - icode = 0 - retval = {} - - lpos = require "posix" - ptr, msg, code = lpos.access("/etc/dhcp") - if ptr == nil then - table.insert(retval, "/etc/dhcp") - icode = icode + 1 - end - - ptr, msg, code = lpos.access("/usr/sbin/dhcpd") - if ptr == nil then - table.insert(retval, "/usr/sbin/dhcpd") - icode = icode + 1 +local parseconfigfile = function(file) + -- first, remove all comments + file = file or "" + lines = {} + for line in string.gmatch(file, "([^\n]*)\n?") do + lines[#lines+1] = string.gsub(line, "#.*$", "") end + file = table.concat(lines, " ") - ptr, msg, code = lpos.access("/etc/init.d/dhcpd") - if ptr == nil then - table.insert(retval, "/etc/init.d/dhcpd") - icode = icode + 1 + -- each line either ends with ';' or with '{'...'}' + -- build an array with one entry per statement, each entry having an array of elements, and possibly a subarray + local config = {} + local stack = {config} + local entry + for word in string.gmatch(file, "(%S+)") do + if word == "{" then + if not entry then + entry = {} + table.insert(stack[#stack], entry) + end + entry.sub = {} + stack[#stack+1] = entry.sub + entry = nil + elseif word == "}" then + stack[#stack] = nil + else + if not entry then + entry = {} + table.insert(stack[#stack], entry) + entry[1] = word + else + entry[#entry+1] = word + end + if string.find(word, ";$") then + entry[#entry] = string.sub(word, 1, -2) + entry = nil + end + end end - - if icode == 0 then - retval = nil - end - - return retval + return config end -config_generate = function() +local validate_host = function( host ) + -- FIXME +-- hostname +-- ip +-- mac + return true, host +end - msg = "" - tmpfilename = os.tmpname() - - -- create tmp config file - local tmpfile = io.open( tmpfilename, "w+" ) - - -- get, validate and write global settings to tmp config file - settings = read_settings() - s_msg, s_fields = validate_settings( settings ) - if #s_msg > 0 then - tmpfile:close() - os.remove( tmpfilename ) - msg = "Configuration Generation Failed!\n\n" .. - "Reason: Error in Global Settings\n" - return msg +local validate_subnet = function( net ) + local success = true + if net.value.subnet.value == "" or string.find(net.value.subnet.value, "[^%w.-]") then + net.value.subnet.errtxt = "Invalid domain name / IPv4 address" + success = false end - - tmpfile:write( "authoritative;\n" ) - tmpfile:write( "ddns-update-style none;\n\n" ) - tmpfile:write( "option local-wpad-server code 252 = text;\n\n" ) - - tmpfile:write( "include \"/etc/dhcp/dhcpd.preconfig\";\n" ) - - if #settings.domainname.value > 0 then - tmpfile:write( "option domain-name \"" .. settings.domainname.value .. "\";\n" ) + if not validator.is_ipv4(net.value.netmask.value) then + net.value.netmask.errtxt = "Invalid IPv4 address" + success = false end - tmpfile:write( "default-lease-time " .. settings.defleasetime.value .. ";\n" ) - tmpfile:write( "max-lease-time " .. settings.maxleasetime.value .. ";\n\n" ) - - -- get, validate and write subnet configurations to tmp config file - tmpfile:write( "###### SUBNET CONFIG BEGIN ######\n\n" ) - subnets = get_subnets() - local numnetworks = 0 - for k,v in ipairs(subnets) do - numnetworks = numnetworks + 1 - net = subnet_read( v ) - sn_msg, sn_fields = validate_network( net ) - if #sn_msg > 0 then - tmpfile:close() - os.remove( tmpfilename ) - msg = "Configuration Generation Failed!\n\n" .. - "Reason: Error in Subnet '" .. v .. "'\n" - return msg - end - - tmpfile:write( "# " .. net.name.value .. "\n" ) - tmpfile:write( "subnet " .. net.subnet.value .. " netmask " .. net.netmask.value .. " {\n" ) - if #net.defleasetime.value > 0 then - tmpfile:write( " default-lease-time " .. net.defleasetime.value .. ";\n" ) - end - if #net.maxleasetime.value > 0 then - tmpfile:write( " max-lease-time " .. net.maxleasetime.value .. ";\n" ) - end - tmpfile:write( " option routers " .. net.gateway.value .. ";\n" ) - dnssrvrs = "" - if #net.dnssrv1.value > 0 then - dnssrvrs = net.dnssrv1.value - end - if #net.dnssrv2.value > 0 then - if #dnssrvrs > 0 then - dnssrvrs = dnssrvrs .. ", " .. net.dnssrv2.value - else - dnssrvrs = net.dnssrv2.value - end - end - if #dnssrvrs > 0 then - tmpfile:write( " option domain-name-servers " .. dnssrvrs .. ";\n" ) - end - if #net.domainname.value > 0 then - tmpfile:write( " option domain-name \"" .. net.domainname.value .. "\";\n" ) - end - if #net.wpad.value > 0 then - tmpfile:write( " option local-wpad-server \"" .. net.wpad.value .. "\\n\";\n" ) - end - spec2_msg = generate_pool( tmpfile, tmpfilename, net ) - if #spec2_msg > 0 then - tmpfile:close() - os.remove( tmpfilename ) - msg = "Configuration Generation Failed!\n\n" .. spec2_msg - return msg - end - --- generate advanced part / drop in - advancedfile = io.open( cfgdir .. net.name.value .. ".advanced", "r" ) - if advancedfile ~= nil then - nxtline = advancedfile:read( "*l" ) - while nxtline ~= nil do - tmpfile:write( " " .. nxtline .. "\n" ) - nxtline = advancedfile:read( "*l" ) - end - advancedfile:close() - end - --- - - tmpfile:write( "}\n\n" ) + if net.value.defleasetime.value ~= "" and not validator.is_integer_in_range(net.value.defleasetime.value, 1800, 86400) then + net.value.defleasetime.errtxt = "Lease time must be: 1800 < x < 86400" + success = false end - tmpfile:write( "###### SUBNET CONFIG END ######\n\n" ) - - if numnetworks <= 0 then - tmpfile:close() - os.remove( tmpfilename ) - msg = "Configuration Generation Failed!\n\n" .. - "Reason: No Subnets defined!\n" - return msg + if net.value.maxleasetime.value ~= "" and not validator.is_integer_in_range(net.value.maxleasetime.value, 1800, 86400) then + net.value.maxleasetime.errtxt = "Lease time must be: 1800 < x < 86400" + success = false end - - msg = generate_hosts( tmpfile ) - if #msg > 0 then - tmpfile:close() - os.remove( tmpfilename ) - return msg + if net.value.routers.value ~= "" then + for router in string.gmatch(net.value.routers.value, "([^,%s]+),?%s*") do + if string.find(router, "[^%w.-]") then + net.value.routers.errtxt = "Invalid domain name / IPv4 address" + success = false + break + end + end end - - tmpfile:write( "include \"/etc/dhcp/dhcpd.postconfig\";\n" ) - - tmpfile:close() - os.rename( tmpfilename, "/etc/dhcp/dhcpd.conf" ) - - -- make sure the master pre/post config files are present - local precfg - local postcfg - precfg = io.open( "/etc/dhcp/dhcpd.preconfig", "r" ) - postcfg = io.open( "/etc/dhcp/dhcpd.postconfig", "r" ) - - if precfg == nil then - precfg = io.open( "/etc/dhcp/dhcpd.preconfig", "w+" ) + if string.find(net.value.domainname.value, "[^%w.-]") then + net.value.domainname.errtxt = "Invalid domain name" + success = false end - precfg:close() - - if postcfg == nil then - postcfg = io.open( "/etc/dhcp/dhcpd.postconfig", "w+" ) + if net.value.domainnameservers.value ~= "" then + for server in string.gmatch(net.value.domainnameservers.value, "([^,%s]+),?%s*") do + if string.find(server, "[^%w.-]") then + net.value.domainnameservers.errtxt = "Invalid domain name / IPv4 address" + success = false + end + end end - postcfg:close() - - return "Configuration Generation Successful!\n" -end - -generate_pool = function( tmpfile, tmpfilename, net ) - if not validator.is_ipv4( net.leaserangestart.value ) or - not validator.is_ipv4( net.leaserangeend.value ) then - if net.unknownclients.value == "allow" then - msg = "Reason: permitted unknown clients but failed to define lease range!\n" - return msg - end - - return "" - end - - --- pool header - tmpfile:write( " pool {\n" ) - if net.unknownclients.value == "allow" then - tmpfile:write( " allow known-clients;\n" ) - tmpfile:write( " allow unknown-clients;\n" ) - else - tmpfile:write( " allow known-clients;\n" ) - tmpfile:write( " deny unknown-clients;\n" ) + if net.value.leaserangestart.value ~= "" and not validator.is_ipv4(net.value.leaserangestart.value) then + net.value.leaserangestart.errtxt = "Invalid IPv4 address" + success = false end - tmpfile:write( " range " .. net.leaserangestart.value .. " " .. net.leaserangeend.value .. ";\n" ) - tmpfile:write( " }\n" ) - - return "" -end - -generate_hosts = function( outfile ) - - local retval = "" - - outfile:write( "\n####### STATIC HOSTS BEGIN ######\n\n" ) - - snets = get_subnets() - for k,v in ipairs(snets) do - msg = generate_hosts_persubnet( outfile, v ) - if #msg > 0 then - return msg + if net.value.leaserangeend.value ~= "" then + if not validator.is_ipv4(net.value.leaserangeend.value) then + net.value.leaserangeend.errtxt = "Invalid IPv4 address" + success = false + elseif net.value.leaserangestart.value == "" then + net.value.leaserangeend.errtxt = "Cannot define range end without range start" + success = false end end - - outfile:write( "###### STATIC HOSTS END ######\n" ) - - retval = generate_hosts_dynamic( outfile ) + success = success and modelfunctions.validateselect(net.value.unknownclients) - return retval + return success, net end -generate_hosts_persubnet = function( outfile, netname ) - - local retval = "" - - local hostsfile = io.open( cfgdir .. netname .. ".static", "r" ) - if hostsfile ~= nil then - local hostsdata = hostsfile:read( "*a" ) - hostsfile:close() - if hostsdata ~= nil then - if #hostsdata <= 0 then - return retval - end - outfile:write( "# " .. netname .. "\n" ) - outfile:write( "group {\n" ) - local done = false - local hosttoken = tokenizer.new( hostsdata, "\n" ) - while not done do - hosttoken, nexthost = tokenizer.next( hosttoken ) - if nexthost ~= nil then - if string.sub( nexthost, 1, 1) ~= "#" then - local spectoken = tokenizer.new( nexthost, ";" ) - spectoken, hostname = tokenizer.next( spectoken ) - spectoken, ip = tokenizer.next( spectoken ) - spectoken, mac = tokenizer.next( spectoken ) - spectoken, comment = tokenizer.next( spectoken ) - outfile:write(" host " .. hostname .. " {\n") - outfile:write(" hardware ethernet " .. mac .. ";\n") - outfile:write(" fixed-address " .. ip .. ";\n") - outfile:write(" }\n") - end - else - done = true - end +local validate_settings = function ( settings ) + local success = true + if settings.value.defleasetime.value ~= "" and not validator.is_integer_in_range(settings.value.defleasetime.value, 1800, 86400) then + settings.value.defleasetime.errtxt = "Out of range 1800 < x < 86400 or not integer" + success = false + end + if settings.value.maxleasetime.value ~= "" and not validator.is_integer_in_range(settings.value.maxleasetime.value, 1800, 86400) then + settings.value.maxleasetime.errtxt = "Out of range 1800 < x < 86400 or not integer" + success = false + end + if string.find(settings.value.domainname.value, "[^%w.-]") then + settings.value.domainname.errtxt = "Invalid domain name" + success = false + end + if settings.value.domainnameservers.value ~= "" then + for server in string.gmatch(settings.value.domainnameservers.value, "([^,%s]+),?%s*") do + if string.find(server, "[^%w.-]") then + settings.value.domainnameservers.errtxt = "Invalid domain name / IPv4 address" + success = false end - outfile:write( "}\n\n" ) - else - retval = "Configuration Generation Failed: Failed to read data from subnet static hosts file for " .. netname end end - return retval + return success, settings end -generate_hosts_dynamic = function( outfile ) - - local retval = "" - - local hostsfile = io.open( cfgdir .. "dhcpd.dynamic", "r" ) - if hostsfile ~= nil then - local hostsdata = hostsfile:read( "*a" ) - hostsfile:close() - if hostsdata ~= nil then - if #hostsdata <= 0 then - return retval - end - outfile:write( "group {\n" ) - local done = false - local hosttoken = tokenizer.new( hostsdata, "\n" ) - while not done do - hosttoken, nexthost = tokenizer.next( hosttoken ) - if nexthost ~= nil then - if string.sub( nexthost, 1, 1) ~= "#" then - local spectoken = tokenizer.new( nexthost, ";" ) - spectoken, hostname = tokenizer.next( spectoken ) - spectoken, mac = tokenizer.next( spectoken ) - spectoken, comment = tokenizer.next( spectoken ) - outfile:write(" host " .. hostname .. " {\n") - outfile:write(" hardware ethernet " .. mac .. ";\n") - outfile:write(" }\n") - end - else - done = true - end - end - outfile:write( "}\n" ) - else - retval = "Configuration Generation Failed: Failed to read data from dynamic hosts file!" +-- Give it the string and the position of the { and it will find the corresponding } +local find_section_end = function(file, section_start) + local i = section_start+1 + local indent = 1 + while indent > 0 and i <= #file do + local char = string.sub(file, i, i) + if char == "}" then indent = indent-1 + elseif char == "{" then indent = indent+1 + elseif char == "#" then i = string.find(file, "\n", i) + elseif char == "'" then i = string.find(file, "'", i) + elseif char == '"' then i = string.find(file, '"', i) end + i=i+1; end - - return retval + return i-1 end -subnet_delete = function( name ) - - local msg = "" +local subnet_write = function(net) + local file = fs.read_file(configfile) + config = config or parseconfigfile(file) - local filename = cfgdir .. name - os.remove( filename .. ".subnet" ) - os.remove( filename .. ".static" ) - os.remove( filename .. ".dynamic" ) - - return msg -end - -advglobal_read = function() + -- First, add or update the submet line + local subnetline = "subnet "..net.value.subnet.value.." netmask "..net.value.netmask.value + local found = false + for i,value in ipairs(config or {}) do + if value[1] == "subnet" and value[2] == net.value.subnet.value then + file = replaceentry(file, value, subnetline) + found = true + end + end + if not found then file = file.."\n"..subnetline.." {\n}" end - preconfig = "" - postconfig = "" - dynamic = "" + -- Now, find the subnet section + local subnet_start = select(2, string.find(file, replacemagiccharacters(subnetline).."%s*{")) + local subnet_end = find_section_end(file, subnet_start) - 1 + subnet_start = string.find(file, "\n", subnet_start) + 1 + local subnetcontent = string.sub(file, subnet_start, subnet_end) - file = io.open( cfgdir .. "dhcpd.preconfig", "r" ) - if file ~= nil then - preconfig = file:read( "*a" ) - if preconfig == nil then - preconfig = "" - end - file:close() + -- Update the subnet data + if net.value.defleasetime.value ~= "" then + net.value.defleasetime.replace = "default-lease-time "..net.value.defleasetime.value end - - file = io.open( cfgdir .. "dhcpd.postconfig", "r" ) - if file ~= nil then - postconfig = file:read( "*a" ) - if postconfig == nil then - postconfig = "" - end - file:close() + if net.value.maxleasetime.value ~= "" then + net.value.maxleasetime.replace = "max-lease-time "..net.value.maxleasetime.value end - - file = io.open( cfgdir .. "dhcpd.dynamic", "r" ) - if file ~= nil then - dynamic = file:read( "*a" ) - if dynamic == nil then - dynamic = "" - end - file:close() + if net.value.routers.value ~= "" then + net.value.routers.replace = "option routers "..net.value.routers.value end - - return cfe({ preconfig = preconfig, postconfig = postconfig, dynamic = dynamic }) -end - -advglobal_update = function( preconfig, postconfig, dynamic ) - - file = io.open( cfgdir .. "dhcpd.preconfig", "wb+" ) - if file ~= nil then - file:write( preconfig ) - file:close() + if net.value.domainname.value ~= "" then + net.value.domainname.replace = 'option domain-name "'..net.value.domainname.value..'"' end - - file = io.open( cfgdir .. "dhcpd.postconfig", "wb+" ) - if file ~= nil then - file:write( postconfig ) - file:close() + if net.value.domainnameservers.value ~= "" then + net.value.domainnameservers.replace = "option domain-name-servers "..net.value.domainnameservers.value end - - file = io.open( cfgdir .. "dhcpd.dynamic", "wb+" ) - if file ~= nil then - file:write( dynamic ) - file:close() + if net.value.leaserangestart.value ~= "" then + net.value.leaserangestart.replace = "range "..net.value.leaserangestart.value end - - return cfe({ preconfig = preconfig, postconfig = postconfig, dynamic = dynamic }) -end - -subnet_read = function( name ) - local filename = cfgdir .. name .. ".subnet" - local net = create_new_net( name, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil ) - - for line in io.lines(filename) do - if (string.sub(line, 1, 15) == "def-lease-time:") then - net.defleasetime.value = string.sub(line, 17) - elseif (string.sub(line, 1, 15) == "max-lease-time:") then - net.maxleasetime.value = string.sub(line, 17) - elseif (string.sub(line, 1, 8) == "gateway:") then - net.gateway.value = string.sub(line, 10) - elseif (string.sub(line, 1, 12) == "domain-name:") then - net.domainname.value = string.sub(line, 14) - elseif (string.sub(line, 1, 10) == "dns-srv-1:") then - net.dnssrv1.value = string.sub(line, 12) - elseif (string.sub(line, 1, 10) == "dns-srv-2:") then - net.dnssrv2.value = string.sub(line, 12) - elseif (string.sub(line, 1, 7) == "subnet:") then - net.subnet.value = string.sub(line, 9) - elseif (string.sub(line, 1, 8) == "netmask:") then - net.netmask.value = string.sub(line, 10) - elseif (string.sub(line, 1, 18) == "lease-range-start:") then - net.leaserangestart.value = string.sub(line, 20) - elseif (string.sub(line, 1, 16) == "lease-range-end:") then - net.leaserangeend.value = string.sub(line, 18) - elseif (string.sub(line, 1, 5) == "wpad:") then - net.wpad.value = string.sub(line, 7) - elseif (string.sub(line, 1, 16) == "unknown-clients:") then - net.unknownclients.value = string.sub(line, 18) - end + if net.value.leaserangeend.value ~= "" then + net.value.leaserangestart.replace = net.value.leaserangestart.replace.." "..net.value.leaserangeend.value end - if net.unknownclients.value ~= "allow" then - net.unknownclients.value = "deny" + if net.value.unknownclients.value ~= "" then + net.value.unknownclients.replace = net.value.unknownclients.value.." unknown-clients" end - - net.statichosts.value = subnet_get_spechosts( name, "static" ) - net.advanced.value = subnet_get_spechosts( name, "advanced" ) - - return net -end -subnet_get_spechosts = function( name, suffix ) - local retval = "" - local filename = cfgdir .. name .. "." .. suffix - if file_exists( filename ) then - local file = io.open( filename, "r" ) - if file ~= nil then - msg = file:read( "*a" ) - if msg ~= nil then - retval = msg + local subnetconfig = parseconfigfile(subnetcontent) + for i,value in ipairs(subnetconfig or {}) do + if value[1] == "default-lease-time" then + subnetcontent = replaceentry(subnetcontent, value, net.value.defleasetime.replace) + net.value.defleasetime.replace = nil + elseif value[1] == "max-lease-time" then + subnetcontent = replaceentry(subnetcontent, value, net.value.maxleasetime.replace) + net.value.maxleasetime.replace = nil + elseif value[1] == "option" then + if value[2] == "routers" then + subnetcontent = replaceentry(subnetcontent, value, net.value.routers.replace) + net.value.routers.replace = nil + elseif value[2] == "domain-name" then + subnetcontent = replaceentry(subnetcontent, value, net.value.domainname.replace) + net.value.domainname.replace = nil + elseif value[2] == "domain-name-servers" then + subnetcontent = replaceentry(subnetcontent, value, net.value.domainnameservers.replace) + net.value.domainnameservers.replace = nil + end + elseif value[1] == "range" then + -- We need to steal the dynamic-bootp status + if value[2] == "dynamic-bootp" and net.value.leaserangestart.replace then + net.value.leaserangestart.replace = string.gsub(net.value.leaserangestart.replace, "range", "%1 "..value[2]) + end + -- Need to use a pool if unknownclients defined + if net.value.unknownclients.replace then + subnetcontent = replaceentry(subnetcontent, value) + else + subnetcontent = replaceentry(subnetcontent, value, net.value.leaserangestart.replace) + net.value.leaserangestart.replace = nil + end + -- We only support one pool per subnet + elseif value[1] == "pool" then + for x,y in ipairs(value.sub or {}) do + if y[2] == "unknown-clients" then + subnetcontent = replaceentry(subnetcontent, y, net.value.unknownclients.replace) + net.value.unknownclients.replace = nil + elseif y[1] == "range" then + -- We need to steal the dynamic-bootp status + if y[2] == "dynamic-bootp" and net.value.leaserangestart.replace then + net.value.leaserangestart.replace = string.gsub(net.value.leaserangestart.replace, "range", "%1 "..y[2]) + end + subnetcontent = replaceentry(subnetcontent, y, net.value.leaserangestart.replace) + net.value.leaserangestart.replace = nil + end + end + if net.value.leaserangestart.replace then + subnetcontent = string.gsub(subnetcontent, "(pool%s*{%s*\n)", "%1"..replacemagiccharacters(net.value.leaserangestart.replace)..";\n") + net.value.leaserangestart.replace = nil + end + if net.value.unknownclients.replace then + subnetcontent = string.gsub(subnetcontent, "(pool%s*{%s*\n)", "%1"..replacemagiccharacters(net.value.unknownclients.replace)..";\n") + net.value.unknownclients.replace = nil end - file:close() end end - - return retval -end -subnet_update_statichosts = function( name, statichosts ) - local msg = ""; - local filename = cfgdir .. name .. ".static" - - file, errmsg = io.open( filename, "wb+" ) - if file == nil then - msg = "Error: Failed to open " .. filename .. "(" .. errmsg .. ")!" - else - file:write( statichosts ) - file:close() + -- add in new lines at the top if they didn't exist + local newlines = {} + newlines[#newlines+1] = net.value.defleasetime.replace + net.value.defleasetime.replace = nil + newlines[#newlines+1] = net.value.maxleasetime.replace + net.value.maxleasetime.replace = nil + newlines[#newlines+1] = net.value.routers.replace + net.value.routers.replace = nil + newlines[#newlines+1] = net.value.domainname.replace + net.value.domainname.replace = nil + newlines[#newlines+1] = net.value.domainnameservers.replace + net.value.domainnameservers.replace = nil + if net.value.leaserangestart.replace and not net.value.unknownclients.replace then + newlines[#newlines+1] = net.value.leaserangestart.replace + net.value.leaserangestart.replace = nil end - - return msg -end - -subnet_update_advanced = function( name, advanced ) - local msg = ""; - local filename = cfgdir .. name .. ".advanced" - - file, errmsg = io.open( filename, "wb+" ) - if file == nil then - msg = "Error: Failed to open " .. filename .. "(" .. errmsg .. ")!" - else - file:write( advanced ) - file:close() + if #newlines > 0 then + for i,line in ipairs(newlines) do newlines[i] = " "..line end + newlines[#newlines+1] = subnetcontent + subnetcontent = table.concat(newlines, ";\n") end - - return msg -end - -read_settings = function() - local filename = cfgdir .. "globalsettings.conf" - local settings = create_new_settings( nil, nil, nil ) - if file_exists( filename ) then - for line in io.lines(filename) do - if (string.sub(line, 1, 15) == "def-lease-time:") then - settings.defleasetime.value = string.sub(line, 17) - elseif (string.sub(line, 1, 15) == "max-lease-time:") then - settings.maxleasetime.value = string.sub(line, 17) - elseif (string.sub(line, 1, 12) == "domain-name:") then - settings.domainname.value = string.sub(line, 14) - end + if net.value.unknownclients.replace then + local temp = " pool {\n "..net.value.unknownclients.replace..";\n" + net.value.unknownclients.replace = nil + if net.value.leaserangestart.replace then + temp = temp .. " " .. net.value.leaserangestart.replace .. ";\n" + net.value.leaserangestart.replace = nil end + subnetcontent = subnetcontent .. temp .. " }\n" end - - return settings + + -- The subnet is updated, put it into the file + file = string.sub(file, 1, subnet_start-1) .. subnetcontent .. string.sub(file, subnet_end+1, -1) + + -- Finally, write out the new file + fs.write_file(configfile, string.gsub(file, "\n*$", "")) + config = nil end -subnet_write = function( net ) - msg, fields = validate_network( net ) - if #msg > 0 then - return cfe({ msg = msg, fields = fields }), net - end - local filename = cfgdir .. net.name.value .. ".subnet" - local file = io.open( filename, "w+" ) - file:write( "def-lease-time: " .. net.defleasetime.value .. "\n" ) - file:write( "max-lease-time: " .. net.maxleasetime.value .. "\n" ) - file:write( "gateway: " .. net.gateway.value .. "\n" ) - file:write( "domain-name: " .. net.domainname.value .. "\n" ) - file:write( "dns-srv-1: " .. net.dnssrv1.value .. "\n" ) - file:write( "dns-srv-2: " .. net.dnssrv2.value .. "\n" ) - file:write( "subnet: " .. net.subnet.value .. "\n" ) - file:write( "netmask: " .. net.netmask.value .. "\n" ) - file:write( "lease-range-start: " .. net.leaserangestart.value .. "\n" ) - file:write( "lease-range-end: " .. net.leaserangeend.value .. "\n" ) - file:write( "wpad: " .. net.wpad.value .. "\n" ) - file:write( "unknown-clients: " .. net.unknownclients.value .. "\n" ) - file:close() - - spec_msg = validate_statichosts( net.statichosts.value ) - if #spec_msg == 0 then - spec_msg = subnet_update_statichosts( net.name.value, net.statichosts.value ) - if #spec_msg > 0 then - msg = spec_msg - table.insert( fields, "statichosts" ) - end - else - msg = spec_msg - table.insert( fields, "statichosts" ) - end +-- ################################################################################ +-- PUBLIC FUNCTIONS - spec_msg = subnet_update_advanced( net.name.value, net.advanced.value ) - if #spec_msg > 0 then - msg = spec_msg - table.insert( fields, "advanced" ) - end - - return cfe({ msg = msg, fields = {}}), net +function startstop_service(action) + return modelfunctions.startstop_service(processname, action) end -validate_statichosts = function( statichosts ) - local line = 1 - local msg = "" - local done = false - hosttoken = tokenizer.new( statichosts, "\n") - while not done do - hosttoken, nexthost = tokenizer.next( hosttoken ) - if nexthost ~= nil then - if string.sub(nexthost, 1, 1) ~= "#" then - fieldtoken = tokenizer.new( nexthost, ";") - fieldtoken, hostname = tokenizer.next( fieldtoken ) - fieldtoken, ip = tokenizer.next( fieldtoken ) - fieldtoken, mac = tokenizer.next( fieldtoken ) - fieldtoken, comment = tokenizer.next( fieldtoken ) - if hostname == nil then - msg = msg .. "Static Hosts: hostname missing on line " .. line .. "!\n" - else - if not is_valid_hostname( hostname ) then - msg = msg .. "Static Hosts: Invalid hostname on line " .. line .. "!\n" + +function getstatus () + return modelfunctions.getstatus(processname, packagename, "DHCP Status") +end + +create_new_subnet = function() + net = { + subnet = cfe({ label="Subnet" }), + netmask = cfe({ label="Netmask" }), + defleasetime = cfe({ label="Default Lease Time" }), + maxleasetime = cfe({ label="Maximum Lease Time" }), + routers = cfe({ label="Routers", descr="Comma-separated addresses" }), + domainname = cfe({ label="Domainname" }), + domainnameservers = cfe({ label="Domain Name Servers", descr="Comma-separated addresses" }), + --wpad = cfe({ label="Web Proxy Auto Discovery" }), + leaserangestart = cfe({ label="Lease Range Start" }), + leaserangeend = cfe({ label="Lease Range End" }), + unknownclients = cfe({ type="select", label="Unknown Clients", option={"", "allow", "deny"} }), + } + + return cfe({ type="group", value=net, label="Subnet" }) +end + +subnet_read = function( name ) + config = config or parseconfigfile(fs.read_file(configfile)) + local net = create_new_subnet() + net.value.subnet.value = name + local pools = 0 + local ranges = 0 + + for j,k in ipairs(config) do + if k[1] == "subnet" and k[2] == name then + net.value.netmask.value = k[4] or "" + for i,value in ipairs(k.sub or {}) do + if value[1] == "default-lease-time" then + net.value.defleasetime.value = value[2] or "" + elseif value[1] == "max-lease-time" then + net.value.maxleasetime.value = value[2] or "" + elseif value[1] == "option" then + if value[2] == "routers" then + net.value.routers.value = table.concat(value, " ", 3) + elseif value[2] == "domain-name" then + net.value.domainname.value = string.sub(value[3] or "", 2, -2) + elseif value[2] == "domain-name-servers" then + net.value.domainnameservers.value = table.concat(value, " ", 3) + --elseif value[2] == "local-wpad-server" then + -- net.value.wpad.value = string.sub(value[3] or "", 2, -2) end - end - if ip == nil then - msg = msg .. "Static Hosts: ip missing on line " .. line .. "!\n" - else - if not validator.is_ipv4( ip ) then - msg = msg .. "Static Hosts: Invalid ip on line " .. line .. "!\n" + elseif value[1] == "range" then + ranges = ranges + 1 + if value[2] == "dynamic-bootp" then + net.value.leaserangestart.value = value[3] or "" + net.value.leaserangeend.value = value[4] or "" + else + net.value.leaserangestart.value = value[2] or "" + net.value.leaserangeend.value = value[3] or "" end - end - if mac == nil then - msg = msg .. "Static Hosts: mac missing on line " .. line .. "!\n" - else - if not validator.is_mac( mac ) then - msg = msg .. "Static Hosts: Invalid mac on line " .. line .. "!\n" + -- We only support one pool per subnet + elseif value[1] == "pool" then + pools = pools + 1 + for x,y in ipairs(value.sub or {}) do + if y[2] == "unknown-clients" then + net.value.unknownclients.value = y[1] + elseif y[1] == "range" then + ranges = ranges + 1 + if y[2] == "dynamic-bootp" then + net.value.leaserangestart.value = y[3] or "" + net.value.leaserangeend.value = y[4] or "" + else + net.value.leaserangestart.value = y[2] or "" + net.value.leaserangeend.value = y[3] or "" + end + end end end end - line = line + 1 - else - done = true + break end end + + if pools > 1 or ranges > 1 then + net.value.subnet.errtxt = "Warning! This subnet contains multiple pool/range definitions. This is not supported by ACF. Saving may break functionality!" + end - return msg + return net end -validate_dynamichosts = function( dynamichosts ) - local line = 1 - msg = "" - local done = false - hosttoken = tokenizer.new( dynamichosts, "\n") - while not done do - hosttoken, nexthost = tokenizer.next( hosttoken ) - if nexthost ~= nil then - if string.sub(nexthost, 1, 1) ~= "#" then - fieldtoken = tokenizer.new( nexthost, ";") - fieldtoken, hostname = tokenizer.next( fieldtoken ) - fieldtoken, mac = tokenizer.next( fieldtoken ) - fieldtoken, comment = tokenizer.next( fieldtoken ) - if hostname == nil then - msg = msg .. "Dynamic Hosts: hostname missing on line " .. line .. "!\n" - else - if not is_valid_hostname( hostname ) then - msg = msg .. "Dynamic Hosts: Invalid hostname on line " .. line .. "!\n" - end - end - if mac == nil then - msg = msg .. "Dynamic Hosts: mac missing on line " .. line .. "!\n" - else - if not validator.is_mac( mac ) then - msg = msg .. "Dynamic Hosts: Invalid mac on line " .. line .. "!\n" - end - end +subnet_update = function( net ) + local success, net = validate_subnet( net ) + if not net.value.subnet.errtxt then + local previous_success = success + success = false + net.value.subnet.errtxt = "This subnet does not exist" + local subnets = get_subnets() + for i,subnet in ipairs(subnets.value) do + if subnet == net.value.subnet.value then + success = previous_success + net.value.subnet.errtxt = nil + break end - line = line + 1 - else - done = true end end - - return msg -end - -update_settings = function ( settings ) - - msg, fields = validate_settings ( settings ) - if #msg > 0 then - return cfe({ msg = msg, fields = fields }), settings + if success then + subnet_write(net) + else + net.errtxt = "Failed to update subnet" end - local filename = cfgdir .. "globalsettings.conf" - local file = io.open( filename, "w+" ) - file:write( "def-lease-time: " .. settings.defleasetime.value .. "\n" ) - file:write( "max-lease-time: " .. settings.maxleasetime.value .. "\n" ) - file:write( "domain-name: " .. settings.domainname.value .. "\n" ) - return cfe({ msg = "", fields = {}}), settings + + return net end - subnet_create = function( net ) - if file_exists( cfgdir .. net.name.value .. ".subnet" ) then - return cfe({ msg = "This subnet already exists!", fields = {}}), net - end - retcode, net = subnet_write( net ) - return retcode, net -end - -_tonumber = function( value ) - ret = tonumber( value ) - if (ret == nil) then - ret = 0 - end - return ret -end - -validate_network = function( net ) - fields = {} - msg = "" - if #net.name.value < 3 then - table.insert(fields, "name") - msg = msg .. "Minimum network name length is 3 characters!\n" - end - if not is_valid_netname( net.name.value ) then - table.insert( fields, "name" ) - msg = msg .. "Invalid network name: allowed characters are: 'a..z', '0..9', '-'\n" - end - if net.name.value == "<new>" then - table.insert(fields, "name") - msg = msg .. "<new> is not a valid network name!\n" - end - if #net.defleasetime.value > 0 then - if not validator.is_integer_in_range(_tonumber(net.defleasetime.value), 1800, 86400) then - table.insert(fields, "defleasetime") - msg = msg .. "Default-Lease-Time must be: 1800 < x < 86400\n" - end - end - if #net.maxleasetime.value > 0 then - if not validator.is_integer_in_range(_tonumber(net.maxleasetime.value), 1800, 86400) then - table.insert(fields, "maxleasetime") - msg = msg .. "Maximum-Lease-Time must be: 1800 < x < 86400\n" - end - end - if not validator.is_ipv4(net.gateway.value) then - table.insert(fields, "gateway") - msg = msg .. "Gateway: invalid IPv4 address!\n" - end - if #net.dnssrv1.value > 0 then - if not validator.is_ipv4(net.dnssrv1.value) then - table.insert(fields, "dnssrv1") - msg = msg .. "DNS Server 1: invalid IPv4 address!\n" - end - end - if not validator.is_ipv4(net.dnssrv2.value) then - if #net.dnssrv2.value > 0 then - table.insert(fields, "dnssrv2") - msg = msg .. "DNS Server 2: invalid IPv4 address!\n" - end - end - if not validator.is_ipv4(net.subnet.value) then - table.insert(fields, "subnet") - msg = msg .. "Subnet: invalid IPv4 address!\n" - end - if not validator.is_ipv4(net.netmask.value) then - table.insert(fields, "netmask") - msg = msg .. "Netmask: invalid IPv4 address!\n" - end - if #net.leaserangestart.value > 0 then - if not validator.is_ipv4(net.leaserangestart.value) then - table.insert(fields, "leaserangestart") - msg = msg .. "Lease-Range-Start: invalid IPv4 address!\n" + local success, net = validate_subnet(net) + if not net.value.subnet.errtxt then + local subnets = get_subnets() + for i,subnet in ipairs(subnets.value) do + if subnet == net.value.subnet.value then + success = false + net.value.subnet.errtxt = "This subnet already exists" + break + end end end - if #net.leaserangeend.value > 0 then - if not validator.is_ipv4(net.leaserangeend.value) then - table.insert(fields, "leaserangeend") - msg = msg .. "Lease-Range-End: invalid IPv4 address!\n" - end + if success then + subnet_write(net) + else + net.errtxt = "Failed to create subnet" end - return msg, fields -end - -file_exists = function( filename ) - retval = false - lpos = require "posix" - ptr, msg, code = lpos.access( filename ) - if ptr ~= nil then - retval = true - end - return retval + return net end -read_file = function ( filename ) - local contents = "" - local line = "" - local file = io.open( filename, "r" ) - if file ~= nil then - line = file:read( "*l" ) - while line ~= nil do - contents = contents .. "\n" .. line - line = file:read( "*l" ) +subnet_delete = function(name) + local file = fs.read_file(configfile) + config = config or parseconfigfile(file) + local cmdresult = cfe({ value="Failed to delete subnet - not found", label="Delete subnet result" }) + local subnets = get_subnets() + for i,subnet in ipairs(subnets.value) do + if subnet == name then + local start, endd = string.find(file, "subnet%s*"..replacemagiccharacters(name).."%s*netmask[^{]*{") + endd = find_section_end(file, endd) + endd = string.find(file, "\n", endd) + file = string.sub(file, 1, start-1) .. string.sub(file, endd+1, -1) + fs.write_file(configfile, string.gsub(file, "\n*$", "")) + config = nil end - file:close() - else - contents = "\n Error: File not found!\n\n" end - return contents + return cmdresult end -is_running = function( process ) - return procps.pidof(process) ~= nil -end - -get_dhcpd_version = function() - local retval = "dhcpd" - local file = io.popen("/usr/sbin/dhcpd --version 2>&1") - if file ~= nil then - local line = file:read( "*a" ) - if #line > 0 then - retval = line +get_subnets = function () + config = config or parseconfigfile(fs.read_file(configfile)) + local retval = {} + for i,entry in ipairs(config) do + if string.lower(entry[1] or "") == "subnet" then + table.insert(retval, entry[2]) end - file:close() end - return retval + return cfe({ type="list", value=retval, label="Subnet list" }) end -service_control = function ( command ) - - local retval = "" - local code = false - local x - local y - - code, retval, x, y = daemoncontrol.daemoncontrol ( "dhcpd", command ) - - return retval -end - -function nonil( value ) - local retval = "" - if value ~= nil then - retval = value - end - - return retval -end - -get_subnets = function () - - local retval = {} - - lpos = require "posix" - files = lpos.dir( "/etc/dhcp" ) - for k,v in ipairs(files) do - if string.sub(v, -7) == ".subnet" then - table.insert(retval, string.sub(v, 1, -8)) +read_settings = function() + config = config or parseconfigfile(fs.read_file(configfile)) + local domainname = cfe({ label="Domain Name" }) + local domainnameservers = cfe({ label="Domain Name Servers", descr="Comma-separated addresses" }) + local defleasetime = cfe({ label="Default Lease Time" }) + local maxleasetime = cfe({ label="Maximum Lease Time" }) + for i,value in ipairs(config) do + if value[1] == "option" then + if value[2] == "domain-name" then + domainname.value = string.sub(value[3] or "", 2, -2) + elseif value[2] == "domain-name-servers" then + domainnameservers.value = table.concat(value, " ", 3) + end + elseif value[1] == "default-lease-time" then + defleasetime.value = value[2] or "" + elseif value[1] == "max-lease-time" then + maxleasetime.value = value[2] or "" end end - return retval + return cfe({ type="group", value={domainname=domainname, domainnameservers=domainnameservers, defleasetime=defleasetime, maxleasetime=maxleasetime}, label = "Global settings" }) end -create_new_net = function( name, defleasetime, maxleasetime, gateway, domainname, dnssrv1, dnssrv2, subnet, netmask, leaserangestart, leaserangeend, wpad, statichosts, unknownclients, dynamichosts, advanced, useadvanced ) - net = { name = { label="Name", value=nonil(name), type="message" }, - defleasetime = { label="Default Lease Time", value=nonil(defleasetime), type="text" }, - maxleasetime = { label="Maximum Lease Time", value=nonil(maxleasetime), type="text" }, - gateway = { label="Gateway", value=nonil(gateway), type="text" }, - domainname = { label="Domainname", value=nonil(domainname), type="text" }, - dnssrv1 = { label="DNS Server 1", value=nonil(dnssrv1), type="text" }, - dnssrv2 = { label="DNS Server 2", value=nonil(dnssrv2), type="text" }, - subnet = { label="Subnet", value=nonil(subnet), type="text" }, - netmask = { label="Netmask", value=nonil(netmask), type="text" }, - leaserangestart = { label="Lease Range Start", value=nonil(leaserangestart), type="text" }, - leaserangeend = { label="Lease Range End", value=nonil(leaserangeend), type="text" }, - wpad = { label="Web Proxy Auto Discovery", value=nonil(wpad), type="text" }, - statichosts = { label="Static Hosts", value=nonil(statichosts), type="text" }, - dynamichosts = { label="Dynamic Hosts", value=nonil(dynamichosts), type="text" }, - unknownclients = { label="Unknown Clients", value=nonil(unknownclients), type="text" }, - advanced = { label="Advanced", value=nonil(advanced), type="text" }, - useadvanced = { label="Use Advanced", value=nonil(useadvanced), type="text" } - } - if net.unknownclients.value ~= "allow" then - net.unknownclients.value = "deny" +update_settings = function ( settings ) + success, settings = validate_settings(settings) + if success then + local file = fs.read_file(configfile) + config = config or parseconfigfile(file) + + -- set up the lines we want to enter + if settings.value.domainname.value ~= "" then + settings.value.domainname.replace = 'option domain-name "'..settings.value.domainname.value..'"' + end + if settings.value.domainnameservers.value ~= "" then + settings.value.domainnameservers.replace = "option domain-name-servers "..settings.value.domainnameservers.value + end + if settings.value.defleasetime.value ~= "" then + settings.value.defleasetime.replace = "default-lease-time "..settings.value.defleasetime.value + end + if settings.value.maxleasetime.value ~= "" then + settings.value.maxleasetime.replace = "max-lease-time "..settings.value.maxleasetime.value + end + + -- replace existing lines + for i,value in ipairs(config) do + if value[1] == "option" then + if value[2] == "domain-name" then + file = replaceentry(file, value, settings.value.domainname.replace) + settings.value.domainname.replace = nil + elseif value[2] == "domain-name-servers" then + file = replaceentry(file, value, settings.value.domainnameservers.replace) + settings.value.domainnameservers.replace = nil + end + elseif value[1] == "default-lease-time" then + file = replaceentry(file, value, settings.value.defleasetime.replace) + settings.value.defleasetime.replace = nil + elseif value[1] == "max-lease-time" then + file = replaceentry(file, value, settings.value.maxleasetime.replace) + settings.value.maxleasetime.replace = nil + end + end + + -- add in new lines at the top if they didn't exist + local newlines = {} + newlines[#newlines+1] = settings.value.domainname.replace + settings.value.domainname.replace = nil + newlines[#newlines+1] = settings.value.domainnameservers.replace + settings.value.domainnameservers.replace = nil + newlines[#newlines+1] = settings.value.defleasetime.replace + settings.value.defleasetime.replace = nil + newlines[#newlines+1] = settings.value.maxleasetime.replace + settings.value.maxleasetime.replace = nil + if #newlines > 0 then + newlines[#newlines+1] = file + file = table.concat(newlines, ";\n") + end + fs.write_file(configfile, string.gsub(file, "\n*$", "")) + config = nil + else + settings.errtxt = "Failed to update global settings" end - - return net -end -create_new_settings = function( defleasetime, maxleasetime, domainname ) - settings = { domainname = { label="Domainname", type="text", value=nonil(domainname) }, - defleasetime = { label="Default Lease Time", type="text", value=nonil(defleasetime) }, - maxleasetime = { label="Maximum Lease Time", type="text", value=nonil(maxleasetime) } - } return settings end -validate_settings = function ( settings ) - - msg = "" - fields = {} - - if not validator.is_integer_in_range(_tonumber(settings.defleasetime.value), 1800, 86400) then - msg = msg .. "Default Lease Time: Out of range 1800 < x < 86400 or not integer\n" - table.insert( fields, "defleasetime" ) - end - if not validator.is_integer_in_range(_tonumber(settings.maxleasetime.value), 1800, 86400) then - msg = msg .. "Maximum Lease Time: Out of range 1800 < x < 86400 or not integer\n" - table.insert( fields, "maxleasetime" ) - end - if not is_valid_hostname( settings.domainname.value ) then - if #settings.domainname.value > 0 then - msg = msg .. "Invalid domainname: valid chars are 'a..z', '0..9', '.', '-'\n" - table.insert( fields, "domainname" ) - end - end - - return msg, fields +getconfigfile = function() + return modelfunctions.getfiledetails(configfile) end -is_valid_hostname = function ( hostname ) - - local retval = true - - name = string.lower( hostname ) - lap = 1 - while lap <= #name do - chr = string.sub(name, lap, lap) - if (chr >= "a" and chr <= "z") or - (chr >= "0" and chr <= "9") or - (chr == ".") or (chr == "-") then - - else - retval = false - end - lap = lap + 1 - end - - return retval +setconfigfile = function(filedetails) + filedetails.value.filename.value = configfile + return modelfunctions.setfiledetails(filedetails) end -is_valid_netname = function ( netname ) - - local retval = true - - name = string.lower( netname ) - lap = 1 - while lap <= #name do - chr = string.sub( name, lap, lap ) - if (chr >= "a" and chr <= "z") or - (chr >= "0" and chr <= "9") or - (chr == "-") then - - else - retval = false - end - lap = lap + 1 - end - - return retval +getleases = function() + return modelfunctions.getfiledetails(leasefile) end - |