diff --git a/glideinmonitor/indexer/indexer.py b/glideinmonitor/indexer/indexer.py index 57a2af7..19ca953 100644 --- a/glideinmonitor/indexer/indexer.py +++ b/glideinmonitor/indexer/indexer.py @@ -34,6 +34,8 @@ def directory_jobs(start_path): # Iterate through each folder in the starting directory (probably GWMS_Log_Dir) for root, dirs, files in os.walk(start_path): + log("DEBUG", "Indexing directory. Data root %s dirs %s" % (str(root), str(dirs))) + # Child directory level level = root.replace(start_path, '').count(os.sep) diff --git a/glideinmonitor/lib/database.py b/glideinmonitor/lib/database.py index fe88d55..28ddceb 100644 --- a/glideinmonitor/lib/database.py +++ b/glideinmonitor/lib/database.py @@ -253,7 +253,7 @@ def getInfo(self, jobID, given_guid): # Do directory/file names need to be sanitized? if given_guid: - cur.execute("SELECT * FROM file_index WHERE GUID='{}'".format(jobID)) + cur.execute("SELECT * FROM file_index WHERE GUID LIKE '{}'".format(jobID)) else: cur.execute("SELECT * FROM file_index WHERE ID='{}'".format(jobID)) diff --git a/glideinmonitor/lib/logger.py b/glideinmonitor/lib/logger.py index 6f4eea9..fbe5c94 100644 --- a/glideinmonitor/lib/logger.py +++ b/glideinmonitor/lib/logger.py @@ -24,4 +24,4 @@ def log(error_level, message): os.makedirs(log_location_dir) log_location = os.path.join(log_location_dir, datetime.datetime.now().strftime("%Y-%m-%d") + ".txt") with open(log_location, "a") as log_file: - log_file.write(error_level + " - " + str(int(datetime.datetime.now().timestamp())) + " - " + message + "\n") + log_file.write(error_level + " - " + "{:%Y-%m-%d %H:%M:%S%z}".format(datetime.datetime.now()) + " - " + message + "\n") diff --git a/glideinmonitor/webserver/rest_api.py b/glideinmonitor/webserver/rest_api.py index bb87074..8b566d9 100644 --- a/glideinmonitor/webserver/rest_api.py +++ b/glideinmonitor/webserver/rest_api.py @@ -21,6 +21,12 @@ def api_job_file(job_id, given_guid): return path +def api_condorjob_file(factory, feuser, entry_name, condor_job_id): + # Get configuration + + return "/data/glideinmonitor/upload/%s/user_%s/glidein_gfactory_instance/entry_%s/job.%s" % (factory, feuser, entry_name, condor_job_id) + + def api_job_info(job_id, given_guid): # Provides info on a job in a JSON format db = Database() diff --git a/glideinmonitor/webserver/static/condor_jobview.html b/glideinmonitor/webserver/static/condor_jobview.html new file mode 100644 index 0000000..f60946a --- /dev/null +++ b/glideinmonitor/webserver/static/condor_jobview.html @@ -0,0 +1,168 @@ + + + + + GWMS Factory - Condor Job View + + + + + + + + +
+ +
+ +
+

+ Job + {{job_id}} +

+
+
+
+
+

General Information

+ + + + + + + + + + + + + + + +
FileSize (.err + .out){{infoFileSize}}
Entry Name{{infoEntryName}}
Frontend Username{{infoFrontendUsername}}
+
+
+
+

Data Files

+
+
+
+ +
+
+
+
+
+
Full Logs
+ +
+ +
+ +
+
+
+
+
Condor Logs
+ +
+ +
+ +
+ +
+ +
+
+
+
+
+
+
+
+ + +



























+
+ + + + + + + + + + + + + + + +
+ + diff --git a/glideinmonitor/webserver/static/condor_jobview.js b/glideinmonitor/webserver/static/condor_jobview.js new file mode 100644 index 0000000..7dd6b67 --- /dev/null +++ b/glideinmonitor/webserver/static/condor_jobview.js @@ -0,0 +1,217 @@ +/* +* Helper functions +*/ +function var_win(data, name) { + // Open a variable's contents in a new window + let newWindow = window.open("", name, "width=600, height=800"); + newWindow.document.write("
" + data + "
"); + newWindow.document.title = name; +} + + +function var_download(text, filename) { + // Variable downloader + let blob = new Blob([text], {type: "text/plain;charset=utf-8"}); + saveAs(blob, filename); +} + + +function parseTarString(text) { + // Parses a inflated gzip file that's still in a tar? format + // Returns a 2D array containing the name and text of each file + let curr_file = -1; + let content = []; + + const lines = text.split('\n'); + for (let i = 0; i < lines.length; i++) { + // For each line, check if the line is a TAR file header + if (lines[i].includes(String.fromCharCode(0)) && lines[i].includes("ustar")) { + // Increment the index of the content variable + curr_file += 1; + content[curr_file] = ["", ""]; + + // Get the file name from the header + let charStart = 0; + while (lines[i].charCodeAt(charStart) === 0) { + charStart += 1; + } + let begin_removed = lines[i].substr(charStart); + content[curr_file][0] = begin_removed.substr(0, begin_removed.indexOf(String.fromCharCode(0))); + + // Get the last line of content (which is included on the same line of the 0 characters) + content[curr_file][1] = lines[i].substring(1 + lines[i].lastIndexOf(String.fromCharCode(0))) + "\n"; + } else { + content[curr_file][1] += lines[i] + "\n"; + } + } + + return content; +} + + +function parseBase64(startText) { + if (App.JobERR_Content.indexOf(startText) !== -1) { + + let begin = App.JobERR_Content.indexOf("644 -", App.JobERR_Content.indexOf(startText)); + if (begin !== -1) { + begin = App.JobERR_Content.indexOf("644 -", begin) + 6; + let end = App.JobERR_Content.indexOf("=", begin); + let content = App.JobERR_Content.substr(begin, end - begin) + return base64(content); + } + } + return null; +} + + +function activateBtn(id, name, content) { + // Enables a button with click support + $("#" + id).addClass("btn-outline-primary"); + $("#" + id).removeClass("btn-outline-secondary"); + $("#" + id).removeAttr("disabled"); + $("#" + id).click(function () { + btnClick(name, content) + }); +} + + +function btnClick(name, content) { + // On button click, either open or download the content + if ($("#toggleFileAction").parent().hasClass("off")) { + // Download the content + var_download(content, name); + } else { + // View the content + var_win(content, name); + } +} + +/* + * Search System + */ +function performSearch() { + let query = $("#searchInput").val(); + if (query === "") { + $("#searchTable").html(""); + return + } + + let results = []; + + // Iterate over each file + for (let file_index in App.JSON) { + // Got a file, go through the contents + const lines = App.JSON[file_index]["content"].split('\n'); + for (let i = 0; i < lines.length; i++) { + if (lines[i].indexOf(query) !== -1) { + // Found a line that matches the query + results.push("
" + lines[i] + "
"); + } + } + } + + // Now add the results + $("#searchTable").html(results); +} + +/* + * Initial Setup + */ +const App = new Vue({ + el: '#app', + data: { + job_id: null, + content: "p { color : red }", + infoEntryName: null, + infoFileSize: null, + infoFrontendUsername: null, + }, + mounted: function () { + this.$nextTick(init(this)); + } +}); + + +function init(App) { + // Get the Job ID + const urlPath = location.pathname; + var splitData = urlPath.split("/"); + App.job_id = splitData.pop(); + App.infoEntryName = splitData.pop(); + App.infoFrontendUsername = splitData.pop(); + App.infoFactory = splitData.pop(); +} + + +// Get the Output File +xhr_file = new XMLHttpRequest(); +xhr_file.open('GET', '/api/job_download/condor_id/' + App.infoFactory + '/' + App.infoFrontendUsername + '/' + App.infoEntryName + '/' + App.job_id + '.out', true); +xhr_file.responseType = 'arraybuffer'; + +xhr_file.onload = function (e) { + if (this.status === 200) { + // Got it! + App.JobOUT_Name = 'job.' + App.job_id + '.out'; + App.JobOUT_Content = new TextDecoder("utf-8").decode(this.response); + activateBtn("btnJobOUT", App.JobOUT_Name, App.JobOUT_Content); + return + } else { + alert("Error downloading output file") + } +}; + +xhr_file.send(); + + +// Get the Err File itself and parse it +xhr_errfile = new XMLHttpRequest(); +xhr_errfile.open('GET', '/api/job_download/condor_id/' + App.infoFactory + '/' + App.infoFrontendUsername + '/' + App.infoEntryName + '/' + App.job_id + '.err', true); +xhr_errfile.responseType = 'arraybuffer'; + +xhr_errfile.onload = function (e) { + if (this.status === 200) { + // Got it, now process the file + App.JobERR_Name = 'job.' + App.job_id + '.err'; + App.JobERR_Content = new TextDecoder("utf-8").decode(this.response); + activateBtn("btnJobERR", App.JobERR_Name, App.JobERR_Content); + + // Now process the err file for Condor Logs + let fileNamePre = "job." + (App.job_id) + "."; + + if (App.JobERR_Name) { + // Master Log + App.JobCondorMasterLog = parseBase64("MasterLog\n========"); + if (App.JobCondorMasterLog) { + activateBtn("btnMasterLog", fileNamePre + "MasterLog.txt", App.JobCondorMasterLog); + } + + // Startd Log + App.JobCondorStartdLog = parseBase64("StartdLog\n========"); + if (App.JobCondorStartdLog) { + activateBtn("btnStartdLog", fileNamePre + "StartdLog.txt", App.JobCondorStartdLog); + } + + // Starter Log + App.JobCondorStarterLog = parseBase64("StarterLog\n========"); + if (App.JobCondorStarterLog) { + activateBtn("btnStarterLog", fileNamePre + "StarterLog.txt", App.JobCondorStarterLog); + } + + // StartdHistory Log + App.JobCondorStartdHistoryLog = parseBase64("StartdHistoryLog\n========"); + if (App.JobCondorStartdHistoryLog) { + activateBtn("btnStardHistLog", fileNamePre + "StartdHistoryLog.txt", App.JobCondorStartdHistoryLog); + } + + // XML Description Log + App.JobCondorXMLDescLog = parseBase64("=== Encoded XML description of glidein activity ==="); + if (App.JobCondorXMLDescLog) { + activateBtn("btnXMLDescLog", fileNamePre + "XMLDescription.txt", App.JobCondorXMLDescLog); + } + } + } else { + alert("Error downloading err file") + } +}; + +xhr_errfile.send(); diff --git a/glideinmonitor/webserver/webserver.py b/glideinmonitor/webserver/webserver.py index c21aac2..d8a6856 100644 --- a/glideinmonitor/webserver/webserver.py +++ b/glideinmonitor/webserver/webserver.py @@ -1,5 +1,6 @@ import os import sys +import json import datetime import hashlib import argparse @@ -9,6 +10,8 @@ # TODO: explicit import to understand better scope from glideinmonitor.webserver.rest_api import * from glideinmonitor.lib.config import Config +from glideinmonitor.lib.logger import log + app = Flask(__name__) auth = HTTPBasicAuth() @@ -41,6 +44,15 @@ def app_job(job_id): return data +@app.route('/condor_job////') +@auth.login_required +def app_condor_job(factory, feuser, entry_name, condor_job_id): + log("DEBUG", "Serving factory=%s, feuser=%s, entry_name=%s, condor_job_id=%s" % (factory, feuser, entry_name, condor_job_id)) + with open(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'static', 'condor_jobview.html'), 'r') as file: + data = file.read() + return data + + @app.route('/job_guid_reroute/', methods=['POST']) @auth.login_required def app_job_guid(): @@ -90,11 +102,19 @@ def flask_api_job_download_db(job_id): return send_file(api_job_file(job_id, False), as_attachment=True) +@app.route('/api/job_download/condor_id////') +@auth.login_required +def flask_api_condorjob_download_db(factory, feuser, entry_name, condor_job_id): + # Download job for given condor id + return send_file(api_condorjob_file(factory, feuser, entry_name, condor_job_id), as_attachment=True) + + @app.route('/api/job_info/') @auth.login_required def flask_api_job_info(job_guid): # Info for a job given GUID - return api_job_info(job_guid, True) + info = api_job_info(job_guid, True) + return redirect("/job/" + str(json.loads(info)["ID"]), code=303) @app.route('/api/job_info/db_id/')