-- acf model for /etc/network/interfaces -- Copyright(c) 2007 N. Angelacos - Licensed under terms of GPL2 module (..., package.seeall) require("modelfunctions") fs = require("acf.fs") format = require("acf.format") local servicename = "networking" local filename = "/etc/network/interfaces" local path = "PATH=/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin:/sbin " local array -- iface is a local table with cfes for the various parts of interface definitions -- Depending on the address family and corresponding method, different options are valid local iface = { required = { comment = {type="longtext", label="Comments", seq=2}, auto = {type="boolean", value=false, label="Auto bring-up", seq=3}, name = {label="Interface Name", seq=1}, family = {type="select", label="Address Family", option={"inet", "ipx", "inet6"}, seq=4}, method = {type="select", label="Method", option={"loopback", "static", "manual", "dhcp", "bootp", "ppp", "wvdial", "dynamic", "v4tunnel"}, seq=5}, ['pre-up'] = {type="longtext", label="'pre-up' actions", seq=100}, up = {type="longtext", label="'up' actions", seq=101}, down = {type="longtext", label="'down' actions", seq=102}, ['post-down'] = {type="longtext", label="'post-down' actions", seq=103}, }, family_methods = { inet = {"loopback", "static", "manual", "dhcp", "bootp", "ppp", "wvdial"}, ipx = {"static", "dynamic"}, inet6 = {"loopback", "static", "manual", "v4tunnel"}, }, method_options = { inet = { static = {"address", "netmask", "broadcast", "network", "metric", "gateway", "pointopoint", "media", "hwaddress", "mtu"}, dhcp = {"hostname", "leasehours", "leasetime", "vendor", "client", "hwaddress"}, bootp = {"bootfile", "server", "hwaddr"}, ppp = {"provider"}, wvdial = {"provider"}, }, ipx = { static = {"frame", "netnum"}, dynamic = {"frame"}, }, inet6 ={ static = {"address", "netmask", "gateway", "media", "hwaddress", "mtu"}, v4tunnel = {"address", "netmask", "endpoint", "local", "gateway", "ttl"}, }, }, optional = { address = {label="Address", seq=6}, netmask = {label="Netmask", seq=7}, endpoint = {label="Endpoint address", seq=8}, ['local'] = {label="Local address", seq=9}, broadcast = {label="Broadcast address", seq=10}, network = {label="Network address", seq=11}, metric = {label="Routing metric", seq=12}, gateway = {label="Default gateway", seq=13}, pointopoint = {label="Point-to-point address", seq=14}, media = {label="Medium type", seq=15}, hostname = {label="Hostname", seq=16}, leasehours = {label="Preferred lease time (hours)", seq=17}, leasetime = {label="Preferred lease time (seconds)", seq=18}, vendor = {label="Vendor class identifier", seq=19}, client = {label="Client identifier", seq=20}, hwaddress = {label="Hardware address", seq=21}, mtu = {label="MTU size", seq=22}, bootfile = {label="Boot file", seq=23}, server = {label="Server address", seq=24}, hwaddr = {label="Hardware address", seq=25}, provider = {label="Provider name", seq=26}, frame = {label="Ethernet frame type", seq=27}, netnum = {label="Network number", seq=28}, ttl = {label="TTL setting", seq=29}, }, } -- Create an interface structure with all options local get_blank_iface = function () local f = {} for name,table in pairs(iface.required) do f[name] = cfe(table) end for name,table in pairs(iface.optional) do f[name] = cfe(table) end return cfe({ type="group", value=f, label="Interface description" }) end -- Get a reverse list of options valid for a given family / method local get_options = function (family, method) local optional = {} if iface.method_options[family] and iface.method_options[family][method] then for i,o in ipairs(iface.method_options[family][method]) do optional[o] = true end end return optional end -- return true or false + the index of array entry matching "name" local arrayindex = function (name) if name and #name > 0 then if array == nil then unpack_interfaces () end for k,v in ipairs(array) do if name == v.value.name.value then return true, k end end end return false, 0 end local appendentry = function (self, value, prefix) self = self or "" -- if we already have some values, then append a newline if #self > 0 then self = self .. "\n" end -- strip the prefix local str = string.gsub(value, "^" .. ( prefix or "" ), "") -- and append return self .. str end local expandentry = function (self, prefix) if #self == 0 then return "" end -- force the string to end in a single linefeed self = string.gsub (self, "\r", "") self = string.gsub (self, "[\n]*$", "\n") local strings = {} for line in string.gmatch(self, "(.-)\n") do if #line > 0 then strings[#strings+1] = prefix .. " " .. line end end return table.concat(strings, "\n") end -- This function parses the interfaces file and creates array local unpack_interfaces = function () if array == nil then local filecontent = fs.read_file(filename) -- make sure it has a terminating \n filecontent = string.gsub (filecontent, "([^\n])$", "%1\n") array = {} local comment = "" local auto = {} for line in string.gmatch ( filecontent, "%s*([^\n]*%S)%s*\n?") do -- it can be #, auto, iface, or a parameter if string.match(line, "^#") then comment = appendentry(comment, line , "#%s*" ) elseif string.match(line, "^auto") then local name = string.match(line, "auto%s+(%S+)") auto[name] = true elseif string.match(line, "^iface") then local name, family, method = string.match(line, "%S+%s+(%S+)%s+(%S+)%s+(%S+)") array[#array + 1] = get_blank_iface() local interface = array[#array].value interface.comment.value = comment comment = "" interface.name.value = name or "" interface.family.value = family or "" interface.method.value = method or "" elseif #array then -- it must be some kind of parameter local param, val = string.match(line, "(%S+)%s*(.*)$") if (param) then local interface = array[#array].value if comment ~= "" then interface.comment.value = appendentry(interface.comment.value, comment) comment = "" end if not (interface[param]) then interface[param] = cfe({label=param, errtxt = "Unknown parameter"}) end interface[param].value = appendentry(interface[param].value, val) end end end for i,int in ipairs(array) do if auto[int.value.name.value] then int.value.auto.value = true end end end return array end -- This function takes array and writes the interfaces file (only writing options valid for the family/method combo) local commit_interfaces = function() local str = "" local strings = {} local required = {} for name,value in pairs(iface.required) do -- Check for the ones handled by other means if name~="comment" and name~="name" and name~="family" and name~="method" and name~="auto" then required[name] = true end end for n,int in ipairs(array) do local me = int.value if me.comment.value ~= "" then strings[#strings+1] = expandentry(me.comment.value, "#") end if me.auto.value then strings[#strings+1] = "auto " .. me.name.value end strings[#strings+1] = string.format("iface %s %s %s", me.name.value, me.family.value, me.method.value) local optional = get_options(me.family.value, me.method.value) for name,entry in pairs(me) do if (required[name] or optional[name]) and entry.value ~= "" then strings[#strings+1] = expandentry(entry.value, "\t"..name) end end strings[#strings+1] = "" end local filecontent = table.concat(strings, "\n") fs.write_file(filename, filecontent) end -- Validate the interface (assuming valid family / method, will only validate the appropriate options) local validate_interface = function (def) local success = true local optional = {} -- since the structure is different depending on the family and method, check them first success = modelfunctions.validateselect(def.value.family) and success success = modelfunctions.validateselect(def.value.method) and success if success then -- Validate the method (given the family) local method = cfe(iface.required.method) method.value = def.value.method.value method.option = {} if iface.family_methods[def.value.family.value] then method.option = iface.family_methods[def.value.family.value] end if not modelfunctions.validateselect(method) then def.value.method.errtxt = "Invalid method for this address family" success = false end -- Determine the list of valid options optional = get_options(def.value.family.value, def.value.method.value) end if #def.value.name.value == 0 then def.value.name.errtxt = "The interface must have a name" success = false elseif string.find(def.value.name.value, "%s") then def.value.name.errtxt = "Cannot contain spaces" success = false end -- More validation tests go here --- -- for optional fields, first check optional[name] to see if it should be validated if not success then def.errtxt = "Failed validation" end return success end local list_interfaces = function() local output = {} unpack_interfaces() for i,int in ipairs(array) do output[#output+1] = int.value.name.value end table.sort(output) return output end ------------------------------------------------------------------------------- -- Public Methods ------------------------------------------------------------------------------- get_all_interfaces = function(self, clientdata) unpack_interfaces() return cfe({ type="group", value=array, label="Interfaces" }) end get_iface_by_name = function(self, clientdata) -- if the name is blank, then return a blank iface with error unpack_interfaces() local rc, idx = arrayindex(clientdata.name or "") if rc == false then local ret = get_blank_iface() ret.value.name.value = name ret.value.name.errtxt = "Interface does not exist" return ret end return array[idx] end get_iface = function(self, clientdata) return get_blank_iface() end create_iface = function(self, def) unpack_interfaces() local rc rc = validate_interface( def ) if rc then idx = #array table.insert( array, idx+1, def ) commit_interfaces() else def.errtxt = "Failed to create interface" end return def end update_iface = function(self, def) unpack_interfaces() -- if the def by that name doesn't exist, fail local rc, idx = arrayindex(def.value.name.value or "" ) if rc == false then def.value.name.errtxt = "This is an invalid interface name" else rc = validate_interface( def ) end if rc then array[idx] = def commit_interfaces() else def.errtxt = "Failed to update interface" end return def end get_delete_iface_by_name = function(self, clientdata) local result = {} result.name = cfe({ type="select", value=clientdata.name or "", label="Interface Name", option=list_interfaces() }) return cfe({ type="group", value=result, label="Interface Name" }) end delete_iface_by_name = function(self, deleterequest) local success = modelfunctions.validateselect(deleterequest.value.name) if success then unpack_interfaces() local rc, idx = arrayindex(deleterequest.value.name.value) if rc then table.remove (array, idx ) commit_interfaces() deleterequest.descr = "Interface deleted" else deleterequest.errtxt = "Interface not found" end else deleterequest.errtxt = "Failed to delete interface" end return deleterequest end get_status = function () local status = {} status.filename = cfe({ value=filename, label="Interfaces file" }) status.iproute = cfe({ type="longtext", label="ip route" }) status.ipaddr = cfe({ type="longtext", label="ip addr" }) status.iptunnel = cfe({ type="longtext", label="ip tunnel" }) if not fs.is_file(filename) then status.filename.errtxt = "File not found" end local cmd = path.."ip route" local f = io.popen(cmd) status.iproute.value = f:read("*a") f:close() cmd = path.."ip addr" f = io.popen(cmd) status.ipaddr.value = f:read("*a") f:close() cmd = path.."ip tunnel" f = io.popen(cmd) status.iptunnel.value = f:read("*a") f:close() return cfe({ type="group", value=status, label="Status" }) end get_file = function () return modelfunctions.getfiledetails(filename) end write_file = function (self, newfile) array = nil return modelfunctions.setfiledetails(self, newfile, {filename}) end get_interfaces = function() local cmd = path.."ip addr" local f = io.popen(cmd) local ipaddr = f:read("*a") f:close() -- now parse the result to find the interfaces local retval = {} for line in string.gmatch(ipaddr, "[^\n]*\n?") do if string.find(line, "^%d+:%s+") then local interface=string.match(line, "^%d+:%s+([^:@]+)") retval[#retval+1] = interface end end return cfe({ type="list", value=retval, label="IP Interfaces" }) end get_addresses = function() local cmd = path.."ip addr" local f = io.popen(cmd) local ipaddr = f:read("*a") f:close() -- now parse the result to find the interfaces and IP addresses local retval = {} local interface for line in string.gmatch(ipaddr, "[^\n]*\n?") do if string.find(line, "^%d+:%s+") then interface=string.match(line, "^%d+:%s+([^:@]+)") elseif string.find(line, "^%s*inet%s") then table.insert(retval, {interface=interface, ipaddr=string.match(line, "^%s*inet%s+([%d%.]+)")}) end end return cfe({ type="structure", value=retval, label="Interface IP Addresses" }) end get_ifup_by_name = function(self, clientdata) local result = {} result.name = cfe({ type="select", value=clientdata.name or "", label="Interface Name", option=list_interfaces() }) return cfe({ type="group", value=result, label="Interface Name" }) end ifup_by_name = function (self, ifuprequest) local success = modelfunctions.validateselect(ifuprequest.value.name) if success then name = ifuprequest.value.name.value or "" local cmd = path.."ifup "..format.escapespecialcharacters(name).." 2>&1" local f = io.popen(cmd) ifuprequest.descr = f:read("*a") f:close() if ifuprequest.descr == "" then ifuprequest.descr = "Interface up" end else ifuprequest.errtxt = "Failed ifup" end return ifuprequest end get_ifdown_by_name = function(self, clientdata) local result = {} result.name = cfe({ type="select", value=clientdata.name or "", label="Interface Name", option=list_interfaces() }) return cfe({ type="group", value=result, label="Interface Name" }) end ifdown_by_name = function (self, ifdownrequest) local success = modelfunctions.validateselect(ifdownrequest.value.name) if success then name = ifdownrequest.value.name.value or "" local cmd = path.."ifdown "..format.escapespecialcharacters(name).." 2>&1" local f = io.popen(cmd) ifdownrequest.descr = f:read("*a") f:close() if ifdownrequest.descr == "" then ifdownrequest.descr = "Interface down" end else ifdownrequest.errtxt = "Failed ifup" end return ifdownrequest end get_restartnetworking = function(self, clientdata) local actions = {} actions[1] = "restart" local service = cfe({ type="hidden", value=servicename, label="Service Name" }) local startstop = cfe({ type="group", label="Restart Networking", value={servicename=service}, option=actions }) return startstop end restartnetworking = function(self, startstop) return modelfunctions.startstop_service(startstop, "restart") end