module(..., package.seeall) -- Load libraries require("modelfunctions") require("fs") require("date") require("format") require("posix") require("validator") require("processinfo") -- Set variables local configfile = "/etc/gnats/databases" local processname = "gnats" local packagename = "gnats" local baseurl = "/etc/gnats" --No trailing / local databaseurl = "/var/lib/gnats/" local gnatsopts = " -H dev.alpinelinux.org " local gnatspath = "/usr/libexec/gnats/" gnatsopts = " -H 10.1.59.82 " -- constants local SECT_HEADER = 1 local SECT_SFIELDS = 2 local SECT_MFIELDS = 3 local header = {} local sfields = {} local mfields = {} local fields_single = { ["number"] = "", ["category"] = "", ["synopsis"] = "", ["confidential"] = "", ["severity"] = "", ["priority"] = "", ["responsible"] = "", ["state"] = "", ["quarter"] = "", ["keywords"] = "", ["date_required"] = "", ["class"] = "", ["submitter_id"] = "", ["arrival_date"] = "", ["closed_date"] = "", ["last_modified"] = "", ["originator"] = "", ["release"] = "", ["notify_list"] = "" } local fields_multiple = { ["organization"]="", ["environment"]="", ["description"]="", ["how_to_repeat"]="", ["fix"]="", ["release_note"]="", ["audit_trail"]="", ["unformatted"]="" } local descr = { } -- ################################################################################ -- LOCAL FUNCTIONS -- Function to recursively inserts all filenames in a dir into an array local function recursedir(path, filearray) local k,v for k,v in pairs(posix.dir(path) or {}) do -- Ignore files that begins with a '.' if not string.match(v, "^%.") then local f = path .. "/" .. v -- If subfolder exists, list files in this subfolder if (posix.stat(f).type == "directory") then recursedir(f, filearray) else table.insert(filearray, f) end end end end -- ################################################################################ -- LOCAL GNATS SPECIFIC FUNCTIONS local function process_header(line) local k, v if string.find(line, "^%s*$") then return 1 end k, v = string.match(line, "^([%a%d_.-]+): (.*)$") if k then header[string.lower(k:gsub("-", "_"))] = v end return 0 end local function process_sfields(line) local k, v, i, j, key local found = 0 key, v = string.match(line, "^>([%a-]+):%s*(.*)$") if key then k = string.lower(key:gsub("-", "_")) if fields_single[k] == nil then return 1 end fields_single[k] = v end return 0 end local current_mfield = 0 local function mfield_add_line(line) if fields_multiple[current_mfield] == nil then fields_multiple[current_mfield] = line or "" else fields_multiple[current_mfield] = fields_multiple[current_mfield].."\n"..(line or "") end end local function process_mfields(line) local k, v = string.match(line, "^>([%a-]+):%s*(.*)$") if k then k = string.lower(k:gsub("-", "_")) if fields_multiple[k] then current_mfield = k end else v = line end mfield_add_line(v) return 0 end local function get_array(name) local a = {} local f = assert(io.popen("/usr/bin/query-pr "..format.escapespecialcharacters(gnatsopts).." --valid-values " .. format.escapespecialcharacters(name))) for line in f:lines() do table.insert(a, line) end f:close() return a end local function get_various_info() local f = assert(io.popen("/usr/bin/query-pr "..format.escapespecialcharacters(gnatsopts).." -x -q | wc -l")) local count = f:read("*l") f:close() return count end -- ################################################################################ -- GNATS SPECIFIC FUNCTIONS -- generate an array of select options function get_select_opt(self, name) return get_array(name) end -- generate an array of select options function list_responsible() local a = {} local f = assert(io.popen("/usr/bin/query-pr "..format.escapespecialcharacters(gnatsopts).." --list-responsible ")) for line in f:lines() do table.insert(a, string.match(line,"^(.-):.*$")) end f:close() return a end function summary() -- TODO: this func should return an iterator instead of a table local line local pr = {} local prs = {} local i,k,v local search_opts = "" -- create command line options for quer-pr. For example: -- -- --priority low --class sw-bug --category doc --skip-closed -- -- Note that --skip-closed has value = "" -- for k,v in pairs(search_criteria) do if v then search_opts = search_opts.." --"..k.." "..v end end local f = assert(io.popen("query-pr "..format.escapespecialcharacters(gnatsopts)..format.escapespecialcharacters(search_opts))) i = 0 for line in f:lines() do -- if line is empty, insert to array and reset record if line == "" then prs[i] = pr pr = {} end -- extract the key/value: -- >Number: 24 -- >Category: pending -- >Synopsis: bug report -- >Confidential: no -- >Severity: serious -- >Priority: medium -- >Responsible: gnats-admin -- >State: open -- >Class: sw-bug -- >Submitter-Id: unknown -- >Arrival-Date: Sat Apr 07 14:45:02 +0000 2007 -- >Originator: Nathan Angelacos -- >Release: -- -- >Number: 25 -- ... k,v = line:match(">(.-):%s+(.*)") if k then -- convert to lowercase and replace '-' with '_' -- -- Submitter-Id -> submitter_id local key = string.lower(k:gsub("-", "_")) if key == "number" then i = tonumber(v) end if key == "arrival_date" then local datetable = date.string_to_table(v) if (tonumber(datetable.month) < 10) then datetable.month = "0" .. tostring(datetable.month) end -- if (tonumber(datetable.day) < 10) then datetable.day = "0" .. tostring(datetable.day) end pr['submit_date'] = string.sub(datetable.year,3) .. "/" .. datetable.month .. "/" .. datetable.day pr['submit_date_in_seconds'] = os.time(datetable) end pr[ key ] = v end end local prs_sorted = {} for k,v in pairs(prs) do table.insert(prs_sorted, v) end table.sort(prs_sorted, function(a,b) return (a.submit_date_in_seconds > b.submit_date_in_seconds) end) return prs_sorted end -- validate if "value" is a valid field function validate_field(self, value, field) -- check if for or field is nil if field == nil or value == nil then return nil end -- loop through the array for k,v in pairs(get_array(field)) do if value == v then return value end end return nil end -- validate for insecure chars in field function validate_textfield(value) if value == nil or value == "" then return "" end return string.match(value, "^[-_%a%d@.]*$") end -- just set the private variable function set_search_criteria(self, criteria) search_criteria = criteria end -- append an n valid input message function set_invalid_field(self,field) table.insert(invalid_input, field) end --[==[ -- return a table of invald input fields function get_invalid_inputs() return invalid_input end --]==] --[=[ -- dump the pr function write_pr(id) local cmd = "query-pr -F "..gnatsopts.." "..tostring(id) local f = assert(io.popen(cmd)) io.write(f:read("*all")) f:close() end --]=] -- read pr to header, sfields and mfields function read_pr(self, id) local cmd = "query-pr -F "..format.escapespecialcharacters(gnatsopts).." "..format.escapespecialcharacters(id) local f = assert(io.popen(cmd)) local line local section = SECT_HEADER for line in f:lines() do -- read header if section == SECT_HEADER then section = section + process_header(line) end if section == SECT_SFIELDS then section = section + process_sfields(line) end if section == SECT_MFIELDS then section = section + process_mfields(line) end end return header, fields_single, fields_multiple end -- ################################################################################ -- PUBLIC FUNCTIONS function valid_filename(self, path) local files = {} recursedir(baseurl,files) for k,v in pairs(files) do if (v == path) then return true end end return false, "Not a valid filename!" end function getstatus() local status = {} local value, errtxt = processinfo.package_version(packagename) status.version = cfe({ label="Program version", value=value, errtxt=errtxt, name=packagename }) return cfe({ type="group", value=status, label="GNATS Status" }) -- return modelfunctions.getstatus(processname, packagename, "GNATS Status") end --[[ function startstop_service(action) return modelfunctions.startstop_service(processname, action) end --]] function list_databases() local databases = {} local dbs = format.parse_lines(fs.read_file(configfile) or "") for i,db in ipairs(dbs) do temp = {} temp.name, temp.description, temp.directory = string.match(db, "([^:]+):([^:]+):([^:]+)") databases[#databases+1] = temp end return cfe({ type="structure", value=databases, label="GNATS Databases" }) end function list_database_files(database) local files = {} local dbs = list_databases() for i,db in ipairs(dbs.value) do if db.name == database then if fs.is_dir(db.directory) and validator.is_valid_filename(string.gsub(db.directory, "/+$", ""), databaseurl) then files = fs.find_files_as_array("[^.].*", db.directory.."/gnats-adm") or {} end break end end return cfe({ type="list", value=files, label="GNATS Files", database=database }) end local function validate_database_filename(filename) -- We're allowed to edit files in /var/lib/gnats/xxx/gnats-adm/ if fs.is_file(filename) and string.match(filename, "^"..format.escapemagiccharacters(databaseurl).."[^/]+/gnats%-adm/[^/]+$") then return true end return false end function read_database_file(filename) return modelfunctions.getfiledetails(filename, validate_database_filename) end function update_database_file(filecontents) return modelfunctions.setfiledetails(filecontents, validate_database_filename) end function list_config_files() local files = fs.find_files_as_array("[^.].*", baseurl) or {} return cfe({ type="list", value=files, label="GNATS Files" }) end local function validate_config_filename(filename) -- We're allowed to edit files in /etc/gnats/ and below if fs.is_file(filename) and string.match(filename, "^"..format.escapemagiccharacters(baseurl)) and not string.match(filename, "%.%.") then return true end return false end function read_config_file(filename) return modelfunctions.getfiledetails(filename, validate_config_filename) end function update_config_file(filecontents) return modelfunctions.setfiledetails(filecontents, validate_config_filename) end function getreport() local status = {} local config = getconfig() local value, errtxt = processinfo.package_version(packagename) status.version = cfe({ name = "version", label="Program version", value=value, errtxt=errtxt, }) local files = {} recursedir(baseurl,files) status.configfiles = cfe({ name="configfiles", label="Config files", type="select", descr=table.concat(files,"\n"), option=files, }) status.configfiles.size = #status.configfiles.option status.numbugs = cfe({ name="numbugs", label="Number of open reports", value=get_various_info(), }) local alpinerelease = fs.read_file_as_array("/etc/alpine-release") or {""} status.alpinerelease = cfe({ name="alpinerelease", label="Alpine release", value=alpinerelease[1], }) status.debug = cfe({ name="debug", label="DEBUG output", value=get_various_info(), }) return status end function get_logfile () local file = {} local cmdtxt = "cat /var/log/messages | grep " .. processname local cmd, error = io.popen(cmdtxt ,r) local cmdoutput = cmd:read("*a") cmd:close() file["filename"] = cfe({ name="filename", label="File name", value=cmdtxt, }) file["filecontent"] = cfe({ type="longtext", name="filecontent", label="File content", value=cmdoutput, }) return file end function getconfig() if (fs.is_file(configfile)) then return format.parse_ini_file(fs.read_file(configfile) or "", "") or {} end return {} end function get_filedetails(self,path) if not (valid_filename(self,path)) then return false, {['errtxt'] = "Not a valid filename!"} end local file = {} local filedetails = {} local config = {} local filenameerrtxt if (path) and (fs.is_file(path)) then filedetails = fs.stat(path) config = getconfig(path) else config = {} config.filename = {} config["filename"]["errtxt"]="Config file '".. path .. "' is missing!" end file["filename" .. (num or "")] = cfe({ name="filename" .. (num or ""), label="File name", value=path, errtxt=filenameerrtxt }) file["filesize" .. (num or "")] = cfe({ name="filesize" .. (num or ""), label="File size", value=filedetails.size or 0, }) file["mtime" .. (num or "")] = cfe({ name="mtime" .. (num or ""), label="File date", value=filedetails.mtime or "---", }) file["filecontent" .. (num or "")] = cfe({ type="longtext", name="filecontent" .. (num or ""), label="File content", value=fs.read_file(path) or "", }) -- Sum all errors into one cfe local sumerrors = "" for k,v in pairs(config) do if (config[k]) and (config[k]["errtxt"]) and (config[k]["errtxt"] ~= "") then sumerrors = sumerrors .. config[k]["errtxt"] .. "\n" end end if (sumerrors ~= "") then file["sumerrors" .. (num or "")] = cfe ({ name="sumerrors" .. (num or ""), label = "Configuration errors", errtxt = string.match(sumerrors, "(.-)\n$"), }) end return true, file end function update_filecontent (self, modifications,path) if not (fs.is_file(path)) then return false, "Not a filename" end if (valid_filename(self,path)) then local file_result,err = fs.write_file(path, format.dostounix(modifications)) if (err) and (#err > 0) then return false, err end return true else return false, "Not a valid filename!" end return false, "Something went wrong!" end function sendbug (self, message) if not (message) then return false, "No valid message to send" end if not (string.match(string.lower(tostring(message[1])), "^to:")) or not (string.match(string.lower(tostring(message[2])), "^from:")) then return false, "Message is malformatted." end local cmdtxt = "/usr/bin/which sendmail" local cmd, error = io.popen(cmdtxt) local cmdoutput = cmd:read("*a") cmd:close() if not (cmdoutput) then return false, "Sendmail is not installed!" end local mailtxt = "outgoing_mail" fs.write_file(mailtxt, table.concat(message , "\n")) local cmdtxt = "/usr/sbin/sendmail -oi -t < " .. format.escapespecialcharacters(mailtxt) .. " 2>&1" local cmd, error = io.popen(cmdtxt) local cmdoutput = cmd:read("*a") cmd:close() local cmd, error = io.popen("/bin/rm -f " .. format.escapespecialcharacters(mailtxt)) cmd:close() if (#cmdoutput > 0) then return false, cmdoutput end return true end