-- acf model for /etc/network/interfaces -- Copyright(c) 2007 N. Angelacos - Licensed under terms of GPL2 module (..., package.seeall) require("modelfunctions") require("fs") require("format") local filename = "/etc/network/interfaces" local path = "PATH=/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin:/sbin " -- iface is a local (private) table with private methods for managing -- the interfaces file. All low-level stuff is done here. It exposes -- the iface.tags, file(raw), and array (parsed), as well as a number -- of functions for managing the interface file. These are in a local -- table because nobody outside the model should know anything about -- the interfaces file (not even where it is - it could be in an LDAP -- directory for all we know) The public module functions are defined -- further below local iface = { tags = { comment = {type="longtext", label="Comments"}, auto = {type="boolean", value=false, label="Auto bring-up"}, name = {label="Interface Name"}, family = {type="select", label="Address Family", option={"inet", "ipx", "inet6"}}, ['pre-up'] = {type="longtext", label="'pre-up' actions"}, up = {type="longtext", label="'up' actions"}, down = {type="longtext", label="'down' actions"}, ['post-down'] = {type="longtext", label="'post-down' actions"} }, method_tag = {type="select", label="Method"}, family_methods = { inet = {"loopback", "static", "manual", "dhcp", "bootp", "ppp", "wvdial"}, ipx = {"static", "dynamic"}, inet6 = {"loopback", "static", "manual", "v4tunnel"} }, method_options = {inet = {loopback = {}, static = {address = {label="Address"}, netmask = {label="Netmask"}, broadcast = {label="Broadcast address"}, network = {label="Network address"}, metric = {label="Routing metric"}, gateway = {label="Default gateway"}, pointopoint = {label="Point-to-point address"}, media = {label="Medium type"}, hwaddress = {label="Hardware address"}, mtu = {label="MTU size"} }, manual = {}, dhcp = {hostname = {label="Hostname"}, leasehours = {label="Preferred lease time (hours)"}, leasetime = {label="Preferred lease time (seconds)"}, vendor = {label="Vendor class identifier"}, client = {label="Client identifier"}, hwaddress = {label="Hardware address"} }, bootp = {bootfile = {label="Boot file"}, server = {label="Server address"}, hwaddr = {label="Hardware address"} }, ppp = {provider = {label="Provider name"} }, wvdial = {provider = {label="Provider name"} }, }, ipx = { static = {frame = {label="Ethernet frame type"}, netnum = {label="Network number"} }, dynamic = {frame = {label="Ethernet frame type"} }, }, inet6 ={loopback = {}, static = {address = {label="Address"}, netmask = {label="Netmask"}, gateway = {label="Default gateway"}, media = {label="Medium type"}, hwaddress = {label="Hardware address"}, mtu = {label="MTU size"} }, manual = {}, v4tunnel = {address = {label="Address"}, netmask = {label="Netmask"}, endpoint = {label="Endpoint address"}, ['local'] = {label="Local address"}, gateway = {label="Default gateway"}, ttl = {label="TTL setting"} }, }, }, -- lowlevel functions will insert file and array here } -- Lowlevel functions - they do things directly to iface. iface.read_file = function () iface.file = cfe({ type="longtext", label=filename }) local file = io.open(filename) local rc = false if file then iface.file.value = file:read("*a") -- make sure it has a terminating \n iface.file.value = string.gsub (iface.file.value, "([^\n])$", "%1\n") file:close() rc = true end return rc end iface.write_file = function () local rc = true fs.write_file(iface.file.label, iface.file.value) return rc end iface.iface_type = function (family, method) local f = {} for name,table in pairs(iface.tags) do f[name] = cfe(table) end f.family.value = family or "" if family and iface.family_methods[family] then f.method = cfe(iface.method_tag) f.method.option = iface.family_methods[family] f.method.value = method or "" if method and iface.method_options[family][method] then for name,table in pairs(iface.method_options[family][method]) do f[name] = cfe(table) end elseif method then f.method.errtxt = "Invalid method" end elseif family then f.family.errtxt = "Invalid family" end return cfe({ type="group", value=f, label="Interface description" }) end -- return true or false + the index of iface.array matching "name" iface.index = function (name) if name and #name > 0 then if iface.array == nil then iface.unpack_interfaces () end for k,v in ipairs(iface.array) do if name == v.value.name.value then return true, k end end end return false, 0 end iface.append = 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 iface.expand = 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 iface.unpack_interfaces = function () if iface.array == nil then iface.read_file() iface.array = {} -- call it array so we don't have to call it iface.array everywhere local array = iface.array local comment = "" local auto = {} for line in string.gmatch ( iface.file.value, "%s*([^\n]*%S)%s*\n?") do -- it can be #, auto, iface, or a parameter if string.match(line, "^#") then comment = iface.append(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] = iface.iface_type(family, method) local interface = array[#array].value interface.comment.value = comment comment = "" interface.name.value = name 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 = iface.append(interface.comment.value, comment) comment = "" end if not (interface[param]) then interface[param] = cfe({label=param, errtxt = "Unknown parameter"}) end interface[param].value = iface.append (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 cfe({ type="list", value=iface.array, label="Interfaces" }) end iface.pack_interfaces = function () local str = "" local strings = {} for n,int in ipairs(iface.array) do local me = int.value if me.comment.value ~= "" then strings[#strings+1] = iface.expand(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) for name,entry in pairs(me) do if name~="comment" and name~="name" and name~="family" and name~="method" and name~="auto" and entry.value ~= "" then strings[#strings+1] = iface.expand(entry.value, "\t"..name) end end strings[#strings+1] = "" end return table.concat(strings, "\n") end iface.commit = function () iface.file.value = iface.pack_interfaces() return iface.write_file() end iface.add_after = function (def, name) -- if the new def.name is already in the table, then fail local rc, idx = iface.index(def.value.name.value) if rc == true then def.value.name.errtxt = "This interface is already defined" rc = false else rc, def = iface.validate( def ) end -- if the name to insert after doesn't exist, just add it to the end if rc then rc, idx = iface.index(name) if rc == false then idx = #iface.array end table.insert( iface.array, idx+1, def ) rc = iface.commit() end if rc == false then def.errtxt = "Failed to create interface" end return def end iface.read = function (name) -- if the name is blank, then return a blank iface with error iface.unpack_interfaces() local rc, idx = iface.index(name) if rc == false then local ret = iface.iface_type() ret.value.name.value = name ret.value.name.errtxt = "Interface does not exist" return ret end return iface.array[idx] end iface.update = function (def) -- if the def by that name doesn't exist, fail local rc, idx = iface.index(def.value.name.value or "" ) if rc == false then def.value.name.errtxt = "This is an invalid interface name" else rc, def = iface.validate ( def ) end if rc then iface.array[idx] = def rc = iface.commit() end if rc == false then def.errtxt = "Failed to update interface" end return def end iface.delete = function (name) local rc, idx = iface.index(name) if rc then table.remove (iface.array, idx ) rc = iface.commit() end local value if rc then value = "Interface deleted" else value = "Interface not found" end return cfe({ value=value, label="Delete result" }) end iface.validate = function (def) local success = true -- since the structure is different depending on the family and method ... local method = "" if def.value.method then method = def.value.method.value end local newdef = iface.iface_type(def.value.family.value, method) for name,option in pairs(newdef.value) do if option.errtxt then success = false end if def.value[name] then option.value = def.value[name].value end end -- Now, start to validate (family and method already validated) if #newdef.value.name.value == 0 then newdef.value.name.errtxt = "The interface must have a name" success = false elseif string.find(newdef.value.name.value, "%s") then newdef.value.name.errtxt = "Cannot contain spaces" success = false end -- More validation tests go here --- -- if not success then newdef.errtxt = "Failed validation" end return success, newdef end ------------------------------------------------------------------------------- -- Public Methods ------------------------------------------------------------------------------- get_all_interfaces = iface.unpack_interfaces get_iface_by_name = iface.read get_iface = iface.iface_type create_iface = iface.add_after update_iface = iface.update delete_iface_by_name = iface.delete 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(format.escapespecialcharacters(cmd)) status.iproute.value = f:read("*a") f:close() cmd = path.."ip addr" f = io.popen(format.escapespecialcharacters(cmd)) status.ipaddr.value = f:read("*a") f:close() cmd = path.."ip tunnel" f = io.popen(format.escapespecialcharacters(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 (newfile) iface.array = nil iface.file = nil return modelfunctions.setfiledetails(newfile, {filename}) end get_interfaces = function() local cmd = path.."ip addr" local f = io.popen(format.escapespecialcharacters(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(format.escapespecialcharacters(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 ifup_by_name = function (name) name = name or "" local cmd = path.."ifup "..name local f = io.popen(format.escapespecialcharacters(cmd)) local cmdresult = f:read("*a") f:close() if cmdresult == "" then cmdresult = "Interface up" end return cfe({ type="longtext", value=cmdresult, label="ifup "..name }) end ifdown_by_name = function (name) name = name or "" local cmd = path.."ifdown "..name local f = io.popen(format.escapespecialcharacters(cmd)) local cmdresult = f:read("*a") f:close() if cmdresult == "" then cmdresult = "Interface down" end return cfe({ type="longtext", value=cmdresult, label="ifdown "..name }) end restartnetworking = function() return modelfunctions.startstop_service("networking", "restart") end