The little things give you away... A collection of various small helper stuff
Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.
 
 
 

228 lignes
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[@]}"