#!/bin/bash declare -a columns columndefs columnattributes columns+=('jobid'); columndefs+=('job["job_data"]["ident"]'); columnattributes+=(''); columns+=('url'); columndefs+=('job["job_data"]["url"]'); columnattributes+=(''); columns+=('user'); columndefs+=('job["job_data"]["started_by"]'); columnattributes+=(''); columns+=('pipenick'); columndefs+=('pipelines[job["job_data"]["pipeline_id"]] if job["job_data"]["pipeline_id"] in pipelines else "unknown"'); columnattributes+=(''); columns+=('queued'); columndefs+=('job["job_data"]["queued_at"]'); columnattributes+=('date'); columns+=('started'); columndefs+=('job["job_data"]["started_at"]'); columnattributes+=('date'); columns+=('last active'); columndefs+=('int(job["ts"])'); columnattributes+=('date,coloured'); function valid_column { local candidate="$1" local column for column in "${columns[@]}" do [[ "${candidate}" == "${column}" ]] && return 0 done return 1 } sortcolumns=() filter= filtercaseinsensitive= nocolours= notable= dates= # Whether to use full dates for the time columns rather than expired time strings while [[ $# -gt 0 ]] do if [[ "$1" == "--help" || "$1" == "-h" ]] then echo "Usage: archivebot-jobs [options]" >&2 echo "Prints a table of current AB jobs" >&2 echo "Options:" >&2 echo " --help, -h Show this message and exit." >&2 echo " --sort [-]COLUMN, -s Sort the table by a column (descending if preceded by '-'). This can be used multiple times to refine the sorting." >&2 echo " --filter FILTER, -f Filter the table for rows where a COLUMN has a certain VALUE. If specified multiple times, only the last value is used." >&2 echo " The FILTER has this format: COLUMN{=|<|>|^|*|$|~}VALUE" >&2 echo " = means the value must be exactly as specified; < and > mean it must be less/greater than the specified; ^ and $ mean it must start/end with the specified; * means it must contain the specified; ~ means it must match the specified regex." >&2 echo " --ifilter FILTER, -i Like --filter, but case-insensitive" >&2 echo " --no-colours, --no-colors Don't colourise the last activity column if it's been a while." >&2 echo " --no-table Raw output without feeding through column(1); columns are separated by tabs." >&2 echo " --dates Print dates instead of elapsed times for queued/started/last active columns." >&2 echo "The COLUMNs are the names of each column, printed in capital letters in the first line of the output." >&2 exit 0 elif [[ "$1" == "--sort" || "$1" == "-s" ]] then sortcolumns+=("$2") shift elif [[ "$1" == "--filter" || "$1" == "-f" ]] then filter="$2" filtercaseinsensitive= shift elif [[ "$1" == "--ifilter" || "$1" == "-i" ]] then filter="$2" filtercaseinsensitive=1 shift elif [[ "$1" == "--no-colours" || "$1" == "--no-colors" ]] then nocolours=1 elif [[ "$1" == "--no-table" ]] then notable=1 elif [[ "$1" == "--dates" ]] then dates=1 else echo "Unknown option: $1" >&2 exit 1 fi shift done # Validate sortcolumns and filter if [[ "${filter}" ]] then if [[ "${filter}" == *$'\n'* ]] then echo "Invalid filter: newlines not allowed" >&2 exit 1 fi if [[ ! ( "${filter}" == *'='* || "${filter}" == *'<'* || "${filter}" == *'>'* || "${filter}" == *'^'* || "${filter}" == *'*'* || "${filter}" == *'$'* || "${filter}" == *'~'* ) ]] then echo "Invalid filter: ${filter}" >&2 exit 1 fi column="${filter%%[=<>^*$~]*}" if ! valid_column "${column,,}" then echo "Invalid filter column: ${column}" >&2 exit 1 fi fi if [[ ${#sortcolumns[@]} -gt 0 ]] then for column in "${sortcolumns[@]}" do column="${column#-}" if ! valid_column "${column,,}" then echo "Invalid sort column: ${column}" >&2 exit 1 fi done else # Default sort order sortcolumns+=("jobid") fi if [[ "${notable}" ]] then column=("cat") else column=("column" "-t" $'-s\t') fi jobdata="$(curl -s -H "Accept: application/json" "http://dashboard.at.ninjawedding.org/logs/recent?count=1" 2>/dev/null)" pipelinedata="$(curl -s -H "Accept: application/json" "http://dashboard.at.ninjawedding.org/pipelines" 2>/dev/null)" if [[ -z "${jobdata}" || -z "${pipelinedata}" ]] then echo "Error retrieving job or pipeline data" >&2 exit 1 fi { echo "${jobdata}"; echo "${pipelinedata}"; echo "${filter}"; } | python3 -c \ ' if True: # For sensible indentation import datetime import json import sys import time currentTime = time.time() def render_date(ts, coloured = False): global currentTime diff = currentTime - ts colourStr = f"\x1b[{0 if diff < 6 * 3600 else 7};31m" if coloured and diff >= 300 else "" colourEndStr = "\x1b[0m" if colourStr else "" if "'${dates}'": return colourStr + datetime.datetime.fromtimestamp(ts).isoformat(sep = " ") + colourEndStr if diff <= 0: return "now" elif diff < 60: return "<1 min ago" elif diff < 86400: return colourStr + (f"{diff // 3600:.0f}h " if diff >= 3600 else "") + f"{(diff % 3600) // 60:.0f}mn ago" + colourEndStr else: return colourStr + f"{diff // 86400:.0f}d {(diff % 86400) // 3600:.0f}h ago" + colourEndStr jobdata = json.loads(sys.stdin.readline()) pipelinedata = json.loads(sys.stdin.readline()) filter = sys.stdin.readline().strip() pipelines = {p["id"]: p["nickname"] for p in pipelinedata["pipelines"]} jobs = [] for job in jobdata: jobs.append({'"$(for i in ${!columns[@]}; do echo ' "'"${columns[$i]}"'": '"${columndefs[$i]}"','; done)"' }) columns = ('"$(for column in "${columns[@]}"; do echo '"'"${column}"'", '; done)"') columnAttributes = {'"$(for i in ${!columns[@]}; do echo -n '"'"${columns[$i]}"'": "'"${columnattributes[$i]}"'".split(","), '; done)"'} # Filter if filter: import re match = re.match(r"^(?P[A-Za-z ]+)(?P[=<>^*$~])(?P.*)$", filter) filterDict = match.groupdict() filterDict["column"] = filterDict["column"].lower() assert filterDict["column"] in columns compFunc = { "=": lambda a, b: a == b, "<": lambda a, b: a < b, ">": lambda a, b: a > b, "^": lambda a, b: a.startswith(b), "*": lambda a, b: b in a, "$": lambda a, b: a.endswith(b), "~": lambda a, b: re.search(b, a) is not None, }[filterDict["op"]] if isinstance(jobs[0][filterDict["column"]], (int, float)): filterDict["value"] = float(filterDict["value"]) transform = lambda x: x.lower() if "'${filtercaseinsensitive}'" and isinstance(x, str) else x jobs = [job for job in jobs if compFunc(transform(job[filterDict["column"]]), transform(filterDict["value"]))] if not jobs: sys.exit(0) # Sort class reversor: # https://stackoverflow.com/a/56842689 def __init__(self, obj): self.obj = obj def __eq__(self, other): return other.obj == self.obj def __lt__(self, other): return other.obj < self.obj sortColumnsRaw = ('"$(printf "'%s', " "${sortcolumns[@]}")"') sortColumns = tuple((column[1:] if column.startswith("-") else column).lower() for column in sortColumnsRaw) sortColumnAsc = tuple(not column.startswith("-") for column in sortColumnsRaw) assert all(column in columns for column in sortColumns) if not "'${dates}'": # Reverse sorting order for columns which have a date attribute since the column will have elapsed time sortColumnAttrs = tuple(columnAttr for column, columnAttr in columnAttributes.items() if column in sortColumns) sortColumnAsc = tuple(not columnAsc if "date" in columnAttr else columnAsc for columnAsc, column, columnAttr in zip(sortColumnAsc, sortColumns, sortColumnAttrs)) jobs = sorted(jobs, key = lambda job: tuple(job[column] if columnAsc else reversor(job[column]) for column, columnAsc in zip(sortColumns, sortColumnAsc))) # Renderers renderers = {} for column, columnAttr in columnAttributes.items(): if "date" in columnAttr: if "coloured" in columnAttr: renderers[column] = lambda x: render_date(x, coloured = not "'${nocolours}'") else: renderers[column] = render_date # Print print("\t".join(column.upper() for column in columns)) for job in jobs: for column in renderers: job[column] = renderers[column](job[column]) print("\t".join(job[column] for column in columns)) ' | "${column[@]}"