The little things give you away... A collection of various small helper stuff
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

228 lines
8.1 KiB

  1. #!/bin/bash
  2. declare -a columns columndefs columnattributes
  3. columns+=('jobid'); columndefs+=('job["job_data"]["ident"]'); columnattributes+=('');
  4. columns+=('url'); columndefs+=('job["job_data"]["url"]'); columnattributes+=('');
  5. columns+=('user'); columndefs+=('job["job_data"]["started_by"]'); columnattributes+=('');
  6. columns+=('pipenick'); columndefs+=('pipelines[job["job_data"]["pipeline_id"]] if job["job_data"]["pipeline_id"] in pipelines else "unknown"'); columnattributes+=('');
  7. columns+=('queued'); columndefs+=('job["job_data"]["queued_at"]'); columnattributes+=('date');
  8. columns+=('started'); columndefs+=('job["job_data"]["started_at"]'); columnattributes+=('date');
  9. columns+=('last active'); columndefs+=('int(job["ts"])'); columnattributes+=('date,coloured');
  10. function valid_column {
  11. local candidate="$1"
  12. local column
  13. for column in "${columns[@]}"
  14. do
  15. [[ "${candidate}" == "${column}" ]] && return 0
  16. done
  17. return 1
  18. }
  19. sortcolumns=()
  20. filter=
  21. filtercaseinsensitive=
  22. nocolours=
  23. notable=
  24. dates= # Whether to use full dates for the time columns rather than expired time strings
  25. while [[ $# -gt 0 ]]
  26. do
  27. if [[ "$1" == "--help" || "$1" == "-h" ]]
  28. then
  29. echo "Usage: archivebot-jobs [options]" >&2
  30. echo "Prints a table of current AB jobs" >&2
  31. echo "Options:" >&2
  32. echo " --help, -h Show this message and exit." >&2
  33. 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
  34. 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
  35. echo " The FILTER has this format: COLUMN{=|<|>|^|*|$|~}VALUE" >&2
  36. 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
  37. echo " --ifilter FILTER, -i Like --filter, but case-insensitive" >&2
  38. echo " --no-colours, --no-colors Don't colourise the last activity column if it's been a while." >&2
  39. echo " --no-table Raw output without feeding through column(1); columns are separated by tabs." >&2
  40. echo " --dates Print dates instead of elapsed times for queued/started/last active columns." >&2
  41. echo "The COLUMNs are the names of each column, printed in capital letters in the first line of the output." >&2
  42. exit 0
  43. elif [[ "$1" == "--sort" || "$1" == "-s" ]]
  44. then
  45. sortcolumns+=("$2")
  46. shift
  47. elif [[ "$1" == "--filter" || "$1" == "-f" ]]
  48. then
  49. filter="$2"
  50. filtercaseinsensitive=
  51. shift
  52. elif [[ "$1" == "--ifilter" || "$1" == "-i" ]]
  53. then
  54. filter="$2"
  55. filtercaseinsensitive=1
  56. shift
  57. elif [[ "$1" == "--no-colours" || "$1" == "--no-colors" ]]
  58. then
  59. nocolours=1
  60. elif [[ "$1" == "--no-table" ]]
  61. then
  62. notable=1
  63. elif [[ "$1" == "--dates" ]]
  64. then
  65. dates=1
  66. else
  67. echo "Unknown option: $1" >&2
  68. exit 1
  69. fi
  70. shift
  71. done
  72. # Validate sortcolumns and filter
  73. if [[ "${filter}" ]]
  74. then
  75. if [[ "${filter}" == *$'\n'* ]]
  76. then
  77. echo "Invalid filter: newlines not allowed" >&2
  78. exit 1
  79. fi
  80. if [[ ! ( "${filter}" == *'='* || "${filter}" == *'<'* || "${filter}" == *'>'* || "${filter}" == *'^'* || "${filter}" == *'*'* || "${filter}" == *'$'* || "${filter}" == *'~'* ) ]]
  81. then
  82. echo "Invalid filter: ${filter}" >&2
  83. exit 1
  84. fi
  85. column="${filter%%[=<>^*$~]*}"
  86. if ! valid_column "${column,,}"
  87. then
  88. echo "Invalid filter column: ${column}" >&2
  89. exit 1
  90. fi
  91. fi
  92. if [[ ${#sortcolumns[@]} -gt 0 ]]
  93. then
  94. for column in "${sortcolumns[@]}"
  95. do
  96. column="${column#-}"
  97. if ! valid_column "${column,,}"
  98. then
  99. echo "Invalid sort column: ${column}" >&2
  100. exit 1
  101. fi
  102. done
  103. else
  104. # Default sort order
  105. sortcolumns+=("jobid")
  106. fi
  107. if [[ "${notable}" ]]
  108. then
  109. column=("cat")
  110. else
  111. column=("column" "-t" $'-s\t')
  112. fi
  113. jobdata="$(curl -s -H "Accept: application/json" "http://dashboard.at.ninjawedding.org/logs/recent?count=1" 2>/dev/null)"
  114. pipelinedata="$(curl -s -H "Accept: application/json" "http://dashboard.at.ninjawedding.org/pipelines" 2>/dev/null)"
  115. if [[ -z "${jobdata}" || -z "${pipelinedata}" ]]
  116. then
  117. echo "Error retrieving job or pipeline data" >&2
  118. exit 1
  119. fi
  120. { echo "${jobdata}"; echo "${pipelinedata}"; echo "${filter}"; } | python3 -c \
  121. '
  122. if True: # For sensible indentation
  123. import datetime
  124. import json
  125. import sys
  126. import time
  127. currentTime = time.time()
  128. def render_date(ts, coloured = False):
  129. global currentTime
  130. diff = currentTime - ts
  131. colourStr = f"\x1b[{0 if diff < 6 * 3600 else 7};31m" if coloured and diff >= 300 else ""
  132. colourEndStr = "\x1b[0m" if colourStr else ""
  133. if "'${dates}'":
  134. return colourStr + datetime.datetime.fromtimestamp(ts).isoformat(sep = " ") + colourEndStr
  135. if diff <= 0:
  136. return "now"
  137. elif diff < 60:
  138. return "<1 min ago"
  139. elif diff < 86400:
  140. return colourStr + (f"{diff // 3600:.0f}h " if diff >= 3600 else "") + f"{(diff % 3600) // 60:.0f}mn ago" + colourEndStr
  141. else:
  142. return colourStr + f"{diff // 86400:.0f}d {(diff % 86400) // 3600:.0f}h ago" + colourEndStr
  143. jobdata = json.loads(sys.stdin.readline())
  144. pipelinedata = json.loads(sys.stdin.readline())
  145. filter = sys.stdin.readline().strip()
  146. pipelines = {p["id"]: p["nickname"] for p in pipelinedata["pipelines"]}
  147. jobs = []
  148. for job in jobdata:
  149. jobs.append({'"$(for i in ${!columns[@]}; do echo '
  150. "'"${columns[$i]}"'": '"${columndefs[$i]}"','; done)"'
  151. })
  152. columns = ('"$(for column in "${columns[@]}"; do echo '"'"${column}"'", '; done)"')
  153. columnAttributes = {'"$(for i in ${!columns[@]}; do echo -n '"'"${columns[$i]}"'": "'"${columnattributes[$i]}"'".split(","), '; done)"'}
  154. # Filter
  155. if filter:
  156. import re
  157. match = re.match(r"^(?P<column>[A-Za-z ]+)(?P<op>[=<>^*$~])(?P<value>.*)$", filter)
  158. filterDict = match.groupdict()
  159. filterDict["column"] = filterDict["column"].lower()
  160. assert filterDict["column"] in columns
  161. compFunc = {
  162. "=": lambda a, b: a == b,
  163. "<": lambda a, b: a < b,
  164. ">": lambda a, b: a > b,
  165. "^": lambda a, b: a.startswith(b),
  166. "*": lambda a, b: b in a,
  167. "$": lambda a, b: a.endswith(b),
  168. "~": lambda a, b: re.search(b, a) is not None,
  169. }[filterDict["op"]]
  170. if isinstance(jobs[0][filterDict["column"]], (int, float)):
  171. filterDict["value"] = float(filterDict["value"])
  172. transform = lambda x: x.lower() if "'${filtercaseinsensitive}'" and isinstance(x, str) else x
  173. jobs = [job for job in jobs if compFunc(transform(job[filterDict["column"]]), transform(filterDict["value"]))]
  174. if not jobs:
  175. sys.exit(0)
  176. # Sort
  177. class reversor: # https://stackoverflow.com/a/56842689
  178. def __init__(self, obj):
  179. self.obj = obj
  180. def __eq__(self, other):
  181. return other.obj == self.obj
  182. def __lt__(self, other):
  183. return other.obj < self.obj
  184. sortColumnsRaw = ('"$(printf "'%s', " "${sortcolumns[@]}")"')
  185. sortColumns = tuple((column[1:] if column.startswith("-") else column).lower() for column in sortColumnsRaw)
  186. sortColumnAsc = tuple(not column.startswith("-") for column in sortColumnsRaw)
  187. assert all(column in columns for column in sortColumns)
  188. if not "'${dates}'":
  189. # Reverse sorting order for columns which have a date attribute since the column will have elapsed time
  190. sortColumnAttrs = tuple(columnAttr for column, columnAttr in columnAttributes.items() if column in sortColumns)
  191. sortColumnAsc = tuple(not columnAsc if "date" in columnAttr else columnAsc for columnAsc, column, columnAttr in zip(sortColumnAsc, sortColumns, sortColumnAttrs))
  192. jobs = sorted(jobs, key = lambda job: tuple(job[column] if columnAsc else reversor(job[column]) for column, columnAsc in zip(sortColumns, sortColumnAsc)))
  193. # Renderers
  194. renderers = {}
  195. for column, columnAttr in columnAttributes.items():
  196. if "date" in columnAttr:
  197. if "coloured" in columnAttr:
  198. renderers[column] = lambda x: render_date(x, coloured = not "'${nocolours}'")
  199. else:
  200. renderers[column] = render_date
  201. # Print
  202. print("\t".join(column.upper() for column in columns))
  203. for job in jobs:
  204. for column in renderers:
  205. job[column] = renderers[column](job[column])
  206. print("\t".join(job[column] for column in columns))
  207. ' | "${column[@]}"