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.

319 lines
8.3 KiB

  1. #!/usr/bin/env bash
  2. # TrueNAS deploy script for SCALE/CORE using websocket
  3. # It is recommend to use a wildcard certificate
  4. #
  5. # Websocket Documentation: https://www.truenas.com/docs/api/scale_websocket_api.html
  6. #
  7. # Tested with TrueNAS Scale - Electric Eel 24.10
  8. # Changes certificate in the following services:
  9. # - Web UI
  10. # - FTP
  11. # - iX Apps
  12. #
  13. # The following environment variables must be set:
  14. # ------------------------------------------------
  15. #
  16. # # API KEY
  17. # # Use the folowing URL to create a new API token: <TRUENAS_HOSTNAME OR IP>/ui/apikeys
  18. # export DEPLOY_TRUENAS_APIKEY="<API_KEY_GENERATED_IN_THE_WEB_UI"
  19. #
  20. ### Private functions
  21. # Call websocket method
  22. # Usage:
  23. # _ws_response=$(_ws_call "math.dummycalc" "'{"x": 4, "y": 5}'")
  24. # _info "$_ws_response"
  25. #
  26. # Output:
  27. # {"z": 9}
  28. #
  29. # Arguments:
  30. # $@ - midclt arguments for call
  31. #
  32. # Returns:
  33. # JSON/JOBID
  34. _ws_call() {
  35. _debug "_ws_call arg1" "$1"
  36. _debug "_ws_call arg2" "$2"
  37. _debug "_ws_call arg3" "$3"
  38. if [ $# -eq 3 ]
  39. then
  40. _ws_response=$(midclt -K "$DEPLOY_TRUENAS_APIKEY" call "$1" "$2" "$3")
  41. fi
  42. if [ $# -eq 2 ]
  43. then
  44. _ws_response=$(midclt -K "$DEPLOY_TRUENAS_APIKEY" call "$1" "$2")
  45. fi
  46. if [ $# -eq 1 ]
  47. then
  48. _ws_response=$(midclt -K "$DEPLOY_TRUENAS_APIKEY" call "$1")
  49. fi
  50. _debug "_ws_response" "$_ws_response"
  51. printf "%s" "$_ws_response"
  52. return 0
  53. }
  54. # Check argument is a number
  55. # Usage:
  56. #
  57. # Output:
  58. # n/a
  59. #
  60. # Arguments:
  61. # $1 - Anything
  62. #
  63. # Returns:
  64. # 0: true
  65. # 1: false
  66. _ws_check_jobid() {
  67. case "$1" in
  68. [0-9]*)
  69. return 0
  70. ;;
  71. esac
  72. return 1
  73. }
  74. # Wait for job to finish and return result as JSON
  75. # Usage:
  76. # _ws_result=$(_ws_get_job_result "$_ws_jobid")
  77. # _new_certid=$(printf "%s" "$_ws_result" | jq -r '."id"')
  78. #
  79. # Output:
  80. # JSON result of the job
  81. #
  82. # Arguments:
  83. # $1 - JobID
  84. #
  85. # Returns:
  86. # n/a
  87. _ws_get_job_result() {
  88. while true
  89. do
  90. sleep 2
  91. _ws_response=$(_ws_call "core.get_jobs" "[[\"id\", \"=\", $1]]")
  92. if [ "$(printf "%s" "$_ws_response" | jq -r '.[]."state"')" != "RUNNING" ]
  93. then
  94. _ws_result="$(printf "%s" "$_ws_response" | jq '.[]."result"')"
  95. _debug "_ws_result" "$_ws_result"
  96. printf "%s" "$_ws_result"
  97. _ws_error="$(printf "%s" "$_ws_response" | jq '.[]."error"')"
  98. if [ "$_ws_error" != "null" ]
  99. then
  100. _err "Job $1 failed:"
  101. _err "$_ws_error"
  102. return 7
  103. fi
  104. break
  105. fi
  106. done
  107. return 0
  108. }
  109. ########################
  110. ### Public functions ###
  111. ########################
  112. # truenas_ws_deploy
  113. #
  114. # Deploy new certificate to TrueNAS services
  115. #
  116. # Arguments
  117. # 1: Domain
  118. # 2: Key-File
  119. # 3: Certificate-File
  120. # 4: CA-File
  121. # 5: FullChain-File
  122. # Returns:
  123. # 0: Success
  124. # 1: Missing API Key
  125. # 2: TrueNAS not ready
  126. # 3: Not a JobID
  127. # 4: FTP cert error
  128. # 5: WebUI cert error
  129. # 6: Job error
  130. # 7: WS call error
  131. # 10: No CORE or SCALE detected
  132. #
  133. truenas_ws_deploy() {
  134. _domain="$1"
  135. _file_key="$2"
  136. _file_cert="$3"
  137. _file_ca="$4"
  138. _file_fullchain="$5"
  139. _debug _domain "$_domain"
  140. _debug _file_key "$_file_key"
  141. _debug _file_cert "$_file_cert"
  142. _debug _file_ca "$_file_ca"
  143. _debug _file_fullchain "$_file_fullchain"
  144. ########## Environment check
  145. _info "Checking environment variables..."
  146. _getdeployconf DEPLOY_TRUENAS_APIKEY
  147. # Check API Key
  148. if [ -z "$DEPLOY_TRUENAS_APIKEY" ]
  149. then
  150. _err "TrueNAS API key not found, please set the DEPLOY_TRUENAS_APIKEY environment variable."
  151. exit 1
  152. fi
  153. _secure_debug2 DEPLOY_TRUENAS_APIKEY "$DEPLOY_TRUENAS_APIKEY"
  154. _info "Environment variables: OK"
  155. ########## Health check
  156. _info "Checking TrueNAS health..."
  157. _ws_response=$(_ws_call "system.ready" | tr '[:lower:]' '[:upper:]')
  158. _ws_ret=$?
  159. if [ $_ws_ret -gt 0 ]
  160. then
  161. _err "Error calling system.ready:"
  162. _err "$_ws_response"
  163. exit $_ws_ret
  164. fi
  165. if [ "$_ws_response" != "TRUE" ]
  166. then
  167. _err "TrueNAS is not ready."
  168. _err "Please check environment variables DEPLOY_TRUENAS_APIKEY, DEPLOY_TRUENAS_HOSTNAME and DEPLOY_TRUENAS_PROTOCOL."
  169. _err "Verify API key."
  170. exit 2
  171. fi
  172. _savedeployconf DEPLOY_TRUENAS_APIKEY "$DEPLOY_TRUENAS_APIKEY"
  173. _info "TrueNAS health: OK"
  174. ########## System info
  175. _info "Gather system info..."
  176. _ws_response=$(_ws_call "system.info")
  177. _truenas_system=$(printf "%s" "$_ws_response" | jq -r '."version"' | cut -d '-' -f 2 | tr '[:lower:]' '[:upper:]')
  178. _truenas_version=$(printf "%s" "$_ws_response" | jq -r '."version"' | cut -d '-' -f 3)
  179. _info "TrueNAS system: $_truenas_system"
  180. _info "TrueNAS version: $_truenas_version"
  181. if [ "$_truenas_system" != "SCALE" ] && [ "$_truenas_system" != "CORE" ]
  182. then
  183. _err "Cannot gather TrueNAS system. Nor CORE oder SCALE detected."
  184. exit 10
  185. fi
  186. ########## Gather current certificate
  187. _info "Gather current WebUI certificate..."
  188. _ws_response="$(_ws_call "system.general.config")"
  189. _ui_certificate_id=$(printf "%s" "$_ws_response" | jq -r '."ui_certificate"."id"')
  190. _ui_certificate_name=$(printf "%s" "$_ws_response" | jq -r '."ui_certificate"."name"')
  191. _info "Current WebUI certificate ID: $_ui_certificate_id"
  192. _info "Current WebUI certificate name: $_ui_certificate_name"
  193. ########## Upload new certificate
  194. _info "Upload new certificate..."
  195. _certname="acme_$(_utc_date | tr -d '\-\:' | tr ' ' '_')"
  196. _debug _certname "$_certname"
  197. _ws_jobid=$(_ws_call "certificate.create" "{\"name\": \"${_certname}\", \"create_type\": \"CERTIFICATE_CREATE_IMPORTED\", \"certificate\": \"$(_json_encode <"$_file_fullchain")\", \"privatekey\": \"$(_json_encode <"$_file_key")\", \"passphrase\": \"\"}")
  198. _debug "_ws_jobid" "$_ws_jobid"
  199. if ! _ws_check_jobid "$_ws_jobid"
  200. then
  201. _err "No JobID returned from websocket method."
  202. exit 3
  203. fi
  204. _ws_result=$(_ws_get_job_result "$_ws_jobid")
  205. _ws_ret=$?
  206. if [ $_ws_ret -gt 0 ]
  207. then
  208. exit $_ws_ret
  209. fi
  210. _debug "_ws_result" "$_ws_result"
  211. _new_certid=$(printf "%s" "$_ws_result" | jq -r '."id"')
  212. _info "New certificate ID: $_new_certid"
  213. ########## FTP
  214. _info "Replace FTP certificate..."
  215. _ws_response=$(_ws_call "ftp.update" "{\"ssltls_certificate\": $_new_certid}")
  216. _ftp_certid=$(printf "%s" "$_ws_response" | jq -r '."ssltls_certificate"')
  217. if [ "$_ftp_certid" != "$_new_certid" ]
  218. then
  219. _err "Cannot set FTP certificate."
  220. _debug "_ws_response" "$_ws_response"
  221. exit 4
  222. fi
  223. ########## ix Apps (SCALE only)
  224. if [ "$_truenas_system" = "SCALE" ]
  225. then
  226. _info "Replace app certificates..."
  227. _ws_response=$(_ws_call "app.query")
  228. for _app_name in $(printf "%s" "$_ws_response" | jq -r '.[]."name"')
  229. do
  230. _info "Checking app $_app_name..."
  231. _ws_response=$(_ws_call "app.config" "$_app_name")
  232. if [ "$(printf "%s" "$_ws_response" | jq -r '."network" | has("certificate_id")')" = "true" ]
  233. then
  234. _info "App has certificate option, setup new certificate..."
  235. _info "App will be redeployed after updating the certificate."
  236. _ws_jobid=$(_ws_call "app.update" "$_app_name" "{\"values\": {\"network\": {\"certificate_id\": $_new_certid}}}")
  237. _debug "_ws_jobid" "$_ws_jobid"
  238. if ! _ws_check_jobid "$_ws_jobid"
  239. then
  240. _err "No JobID returned from websocket method."
  241. exit 3
  242. fi
  243. _ws_result=$(_ws_get_job_result "$_ws_jobid")
  244. _ws_ret=$?
  245. if [ $_ws_ret -gt 0 ]
  246. then
  247. exit $_ws_ret
  248. fi
  249. _debug "_ws_result" "$_ws_result"
  250. _info "App certificate replaced."
  251. else
  252. _info "App has no certificate option, skipping..."
  253. fi
  254. done
  255. fi
  256. ########## WebUI
  257. _info "Replace WebUI certificate..."
  258. _ws_response=$(_ws_call "system.general.update" "{\"ui_certificate\": $_new_certid}")
  259. _changed_certid=$(printf "%s" "$_ws_response" | jq -r '."ui_certificate"."id"')
  260. if [ "$_changed_certid" != "$_new_certid" ]
  261. then
  262. _err "WebUI certificate change error.."
  263. exit 5
  264. else
  265. _info "WebUI certificate replaced."
  266. fi
  267. _info "Restarting WebUI..."
  268. _ws_response=$(_ws_call "system.general.ui_restart")
  269. _info "Waiting for UI restart..."
  270. sleep 6
  271. ########## Certificates
  272. _info "Deleting old certificate..."
  273. _ws_jobid=$(_ws_call "certificate.delete" "$_ui_certificate_id")
  274. if ! _ws_check_jobid "$_ws_jobid"
  275. then
  276. _err "No JobID returned from websocket method."
  277. exit 3
  278. fi
  279. _ws_result=$(_ws_get_job_result "$_ws_jobid")
  280. _ws_ret=$?
  281. if [ $_ws_ret -gt 0 ]
  282. then
  283. exit $_ws_ret
  284. fi
  285. _info "Have a nice day...bye!"
  286. }