b8e0ec67d7da42dbdd9837bb11833cdc6b761b31 hiram Mon Jun 8 14:50:45 2026 -0700 eliminate the checkIp and reset STATUS_NAMES to liftStatus refs #31811 diff --git src/hg/utils/otto/userRequests/ottoRequestView.cgi src/hg/utils/otto/userRequests/ottoRequestView.cgi index 9f15674425b..3495e163221 100644 --- src/hg/utils/otto/userRequests/ottoRequestView.cgi +++ src/hg/utils/otto/userRequests/ottoRequestView.cgi @@ -7,90 +7,95 @@ Access is restricted to a single IP (UCSC VPN, 128.114.198.5). Any other REMOTE_ADDR gets a 403. """ import cgi import html import json import os import re import subprocess import sys import time import urllib.parse from datetime import datetime -ALLOWED_IP = '128.114.198.5' TRASH = '/data/apache/trash' TABLE = 'ottoRequest' # Configuration will be set dynamically in main() HGDB_CONF = None DB = None USE_PROFILE = None # Galaxy queue status panel - snapshot is refreshed by ottoRequestWatch.sh # (cron, every 11 minutes), CGI just reads it. CACHE_PATH = '/data/apache/trash/ottoRequestGalaxyStatus.json' CACHE_TTL = 1800 # seconds; older than this -> show "stale" instead # featureBits coverage snapshot - append-only file maintained by # featureBitsSnapshot.py (cron, via ottoRequestWatch.sh). fb.*.txt # values are immutable once an alignment completes so no TTL is needed; # featureBitsPct() falls back to an NFS read on a snapshot miss. FB_SNAPSHOT_PATH = '/data/apache/trash/ottoRequestFeatureBitsPct.json' -# from README.txt in this directory -STATUS_NAMES = { +liftStatus = { 0: 'received by API', 1: 'acknowledged, email sent', 2: 'galaxy job started', 3: 'galaxy done, download started', 4: 'downloaded, track files made', 5: 'symlinks ready, awaiting push', 6: 'push complete', 7: 'ERROR', 8: 'COMPLETE (final email sent)', } +asmStatus = { + 0: 'received by API', + 1: 'acknowledged, email sent', + 2: 'assembly build started', + 3: 'assembly build done', + 4: 'downloaded, track files made', + 5: 'symlinks ready, awaiting push', + 6: 'push complete', + 7: 'ERROR', + 8: 'COMPLETE (final email sent)', +} + + COLS = ['id', 'requestType', 'fromDb', 'toDb', 'email', 'comment', 'requestTime', 'status', 'buildDir', 'completeTime'] # featureBits coverage lookup roots HIVE_GENOMES = '/hive/data/genomes' ASMHUB_ROOT = HIVE_GENOMES + '/asmHubs' # in-process caches; one CGI invocation only, but rows reuse same accessions _buildDirCache = {} _fbPctCache = {} _genarkAsmName = {} # populated up-front by loadGenarkNames() _fbSnapshot = {} # populated up-front by loadFeatureBitsSnapshot() def forbidden(msg): sys.stdout.write("Status: 403 Forbidden\r\n") sys.stdout.write("Content-Type: text/plain; charset=utf-8\r\n\r\n") sys.stdout.write(msg + "\n") sys.exit(0) -def checkIp(): - remote = os.environ.get('REMOTE_ADDR', '') - if remote != ALLOWED_IP: - forbidden(f"Access denied for {remote!r}; this page is restricted.") - - def setDbConfig(use_otto=False): """Set database configuration globals based on config parameter.""" global HGDB_CONF, DB, USE_PROFILE if use_otto: HGDB_CONF = '/data/apache/cgi-bin/otto/.otto.conf' DB = 'hgcentral' USE_PROFILE = False else: HGDB_CONF = '/usr/local/apache/cgi-bin/hg.conf' DB = 'hgcentraltest' USE_PROFILE = True def unescapeMysql(s): """Reverse `hgsql -B` escaping (\\n, \\t, \\\\, \\0). One pass so @@ -130,39 +135,39 @@ ok, out, err = hgsqlRun(sql) if not ok: raise RuntimeError(err.strip() or 'hgsql failed') rows = [] if out.strip(): for line in out.rstrip('\n').split('\n'): rows.append([unescapeMysql(f) for f in line.split('\t')]) return rows def doResetStatus(form): rid = form.getfirst('id', '') stat = form.getfirst('status', '') if not rid.isdigit(): return None, f"bad id: {rid!r}" - if not stat.isdigit() or int(stat) not in STATUS_NAMES: + if not stat.isdigit() or int(stat) not in liftStatus: return None, f"bad status: {stat!r}" sql = (f"UPDATE {TABLE} SET status = {int(stat)} " f"WHERE id = {int(rid)}") ok, _out, err = hgsqlRun(sql) if not ok: return None, err.strip() or 'hgsql update failed' return (f"id={rid} status set to {stat} " - f"({STATUS_NAMES[int(stat)]})"), None + f"({liftStatus[int(stat)]})"), None def loadGenarkNames(accessions): """Populate _genarkAsmName: {gcAccession: asmName} for the given accessions in one bulk hgsql call against the genark table. Lets hubBuildDir() construct paths directly instead of NFS-listdir'ing /hive/data/genomes/asmHubs/.../<XXX>/<XXX>/<XXX>/ to discover the asmName suffix on each accession.""" if not accessions: return quoted = ",".join("'%s'" % a for a in sorted(accessions)) sql = (f"SELECT gcAccession, asmName FROM genark " f"WHERE gcAccession IN ({quoted});") ok, out, _err = hgsqlRun(sql) if not ok or not out.strip(): @@ -339,31 +344,31 @@ f'<b>{galaxyStatus.get("running", "?")}</b> running · ' f'<b>{galaxyStatus.get("queued", "?")}</b> queued · ' f'<b>{galaxyStatus.get("new", "?")}</b> new ' f'<small>(as of {html.escape(galaxyStatus.get("ts", ""))})</small>' f'{staleNote}</div>\n') else: out('<div class="legend">Galaxy queue: ' '<i>status unavailable</i></div>\n') if info: out(f'<div class="banner info">{html.escape(info)}</div>\n') if error: out(f'<div class="banner error">{html.escape(error)}</div>\n') out('<div class="legend">status: ') out(' · '.join(f'<code>{k}</code>={html.escape(v)}' - for k, v in STATUS_NAMES.items())) + for k, v in liftStatus.items())) # Count rows by type for toggle button labels completed_count = sum(1 for r in rows if len(r) > 7 and r[7] == '8') assembly_count = sum(1 for r in rows if len(r) > 1 and r[1] == 'assembly') liftover_count = sum(1 for r in rows if len(r) > 1 and r[1] == 'liftOver') # Config toggle button - switches between test and production databases current_config = 'otto' if use_otto else 'test' switch_config = 'test' if use_otto else 'otto' switch_label = 'Switch to hgwdev' if use_otto else 'Switch to RR' config_url = f'?config={switch_config}' out(f' · <b>{len(rows)}</b> row(s)' f'<a href="{config_url}" class="configBtn">{switch_label}</a>' '<button class="refreshBtn" type="button" ' 'onclick="location.reload()">refresh</button>' f'<button class="toggleBtn" type="button" id="toggleComplete" ' @@ -397,31 +402,31 @@ cls_parts = [] if stnum in (7, 8): cls_parts.append(f's{stnum}') # Add requestType class for toggle filtering if typeIdx < len(r): req_type = r[typeIdx].lower() if req_type in ('assembly', 'liftover'): cls_parts.append(req_type) cls = ' '.join(cls_parts) out(f'<tr class="{cls}">') for i, c in enumerate(COLS): cell = r[i] if i < len(r) else '' if c == 'comment': out(f'<td class="comment">{html.escape(cell)}</td>') elif c == 'status': - label = STATUS_NAMES.get(stnum, '?') + label = liftStatus.get(stnum, '?') out(f'<td><b>{html.escape(cell)}</b> ' f'<small>{html.escape(label)}</small></td>') elif c in ('fromDb', 'toDb') and cell: href = ('https://genome-test.gi.ucsc.edu/cgi-bin/hgTracks?db=' + urllib.parse.quote(cell, safe='')) out(f'<td><a href="{html.escape(href)}" target="_blank">' f'{html.escape(cell)}</a></td>') elif c == 'email' and '@' in cell: user = cell.split('@', 1)[0] out(f'<td title="{html.escape(cell)}">' f'{html.escape(user)}</td>') else: out(f'<td>{html.escape(cell)}</td>') fromAcc = r[fromIdx] if fromIdx < len(r) else '' toAcc = r[toIdx] if toIdx < len(r) else '' @@ -431,31 +436,31 @@ out(f'<td>{html.escape(fwd or "-")} / ' f'{html.escape(rev or "-")}</td>') else: out('<td></td>') elapsed = elapsedStr(r[reqIdx] if reqIdx < len(r) else '', r[doneIdx] if doneIdx < len(r) else '') out(f'<td>{html.escape(elapsed)}</td>') # reset form out('<td><form method="post" ' 'onsubmit="return confirm(\'Reset status of id=' + html.escape(rid) + ' to \' + this.status.value + \'?\')">' '<input type="hidden" name="action" value="resetStatus">' f'<input type="hidden" name="id" value="{html.escape(rid)}">' '<select name="status">') - for k in sorted(STATUS_NAMES): + for k in sorted(liftStatus): sel = ' selected' if k == stnum else '' out(f'<option value="{k}"{sel}>{k}</option>') out('</select> <button type="submit">set</button></form></td>') out('</tr>\n') out('</table>\n') out('<script src="/js/sorttable.js"></script>\n') out('<script>\n' 'function toggleCompleted() {\n' ' var table = document.querySelector("table");\n' ' var btn = document.getElementById("toggleComplete");\n' ' var isHidden = table.classList.contains("hide-complete");\n' ' if (isHidden) {\n' ' table.classList.remove("hide-complete");\n' f' btn.textContent = "hide completed ({completed_count})";\n' ' localStorage.setItem("hideCompleted", "false");\n' @@ -504,31 +509,30 @@ ' if (localStorage.getItem("hideAssembly") === "true") {\n' ' var btn = document.getElementById("toggleAssembly");\n' ' table.classList.add("hide-assembly");\n' ' btn.textContent = "show assembly";\n' ' }\n' ' if (localStorage.getItem("hideLiftover") === "true") {\n' ' var btn = document.getElementById("toggleLiftover");\n' ' table.classList.add("hide-liftover");\n' ' btn.textContent = "show liftOver";\n' ' }\n' '});\n' '</script>\n') out('</body></html>\n') def main(): -# checkIp() # Create FieldStorage once - it consumes stdin and can't be read twice form = cgi.FieldStorage() # Detect configuration from URL parameter use_otto = form.getfirst('config') == 'otto' setDbConfig(use_otto) # POST/Redirect/GET: handle the write, then 303 to a GET of the same URL # so a browser reload doesn't re-submit the form and re-run the UPDATE. if os.environ.get('REQUEST_METHOD', 'GET') == 'POST': action = form.getfirst('action', '') if action == 'resetStatus': info, error = doResetStatus(form) else: