truenas_ws.sh 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325
  1. #!/usr/bin/env sh
  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 ]; then
  39. _ws_response=$(midclt -K "$DEPLOY_TRUENAS_APIKEY" call "$1" "$2" "$3")
  40. fi
  41. if [ $# -eq 2 ]; then
  42. _ws_response=$(midclt -K "$DEPLOY_TRUENAS_APIKEY" call "$1" "$2")
  43. fi
  44. if [ $# -eq 1 ]; then
  45. _ws_response=$(midclt -K "$DEPLOY_TRUENAS_APIKEY" call "$1")
  46. fi
  47. _debug "_ws_response" "$_ws_response"
  48. printf "%s" "$_ws_response"
  49. return 0
  50. }
  51. # Upload certificate with webclient api
  52. _ws_upload_cert() {
  53. /usr/bin/env python - <<EOF
  54. import sys
  55. from truenas_api_client import Client
  56. with Client() as c:
  57. ### Login with API key
  58. print("I:Trying to upload new certificate...")
  59. ret = c.call("auth.login_with_api_key", "${DEPLOY_TRUENAS_APIKEY}")
  60. if ret:
  61. ### upload certificate
  62. with open('$1', 'r') as file:
  63. fullchain = file.read()
  64. with open('$2', 'r') as file:
  65. privatekey = file.read()
  66. ret = c.call("certificate.create", {"name": "$3", "create_type": "CERTIFICATE_CREATE_IMPORTED", "certificate": fullchain, "privatekey": privatekey, "passphrase": ""}, job=True)
  67. print("R:" + str(ret["id"]))
  68. sys.exit(0)
  69. else:
  70. print("R:0")
  71. print("E:_ws_upload_cert error!")
  72. sys.exit(7)
  73. EOF
  74. return $?
  75. }
  76. # Check argument is a number
  77. # Usage:
  78. #
  79. # Output:
  80. # n/a
  81. #
  82. # Arguments:
  83. # $1 - Anything
  84. #
  85. # Returns:
  86. # 0: true
  87. # 1: false
  88. _ws_check_jobid() {
  89. case "$1" in
  90. [0-9]*)
  91. return 0
  92. ;;
  93. esac
  94. return 1
  95. }
  96. # Wait for job to finish and return result as JSON
  97. # Usage:
  98. # _ws_result=$(_ws_get_job_result "$_ws_jobid")
  99. # _new_certid=$(printf "%s" "$_ws_result" | jq -r '."id"')
  100. #
  101. # Output:
  102. # JSON result of the job
  103. #
  104. # Arguments:
  105. # $1 - JobID
  106. #
  107. # Returns:
  108. # n/a
  109. _ws_get_job_result() {
  110. while true; do
  111. sleep 2
  112. _ws_response=$(_ws_call "core.get_jobs" "[[\"id\", \"=\", $1]]")
  113. if [ "$(printf "%s" "$_ws_response" | jq -r '.[]."state"')" != "RUNNING" ]; then
  114. _ws_result="$(printf "%s" "$_ws_response" | jq '.[]."result"')"
  115. _debug "_ws_result" "$_ws_result"
  116. printf "%s" "$_ws_result"
  117. _ws_error="$(printf "%s" "$_ws_response" | jq '.[]."error"')"
  118. if [ "$_ws_error" != "null" ]; then
  119. _err "Job $1 failed:"
  120. _err "$_ws_error"
  121. return 7
  122. fi
  123. break
  124. fi
  125. done
  126. return 0
  127. }
  128. ########################
  129. ### Public functions ###
  130. ########################
  131. # truenas_ws_deploy
  132. #
  133. # Deploy new certificate to TrueNAS services
  134. #
  135. # Arguments
  136. # 1: Domain
  137. # 2: Key-File
  138. # 3: Certificate-File
  139. # 4: CA-File
  140. # 5: FullChain-File
  141. # Returns:
  142. # 0: Success
  143. # 1: Missing API Key
  144. # 2: TrueNAS not ready
  145. # 3: Not a JobID
  146. # 4: FTP cert error
  147. # 5: WebUI cert error
  148. # 6: Job error
  149. # 7: WS call error
  150. #
  151. truenas_ws_deploy() {
  152. _domain="$1"
  153. _file_key="$2"
  154. _file_cert="$3"
  155. _file_ca="$4"
  156. _file_fullchain="$5"
  157. _debug _domain "$_domain"
  158. _debug _file_key "$_file_key"
  159. _debug _file_cert "$_file_cert"
  160. _debug _file_ca "$_file_ca"
  161. _debug _file_fullchain "$_file_fullchain"
  162. ########## Environment check
  163. _info "Checking environment variables..."
  164. _getdeployconf DEPLOY_TRUENAS_APIKEY
  165. # Check API Key
  166. if [ -z "$DEPLOY_TRUENAS_APIKEY" ]; then
  167. _err "TrueNAS API key not found, please set the DEPLOY_TRUENAS_APIKEY environment variable."
  168. return 1
  169. fi
  170. _secure_debug2 DEPLOY_TRUENAS_APIKEY "$DEPLOY_TRUENAS_APIKEY"
  171. _info "Environment variables: OK"
  172. ########## Health check
  173. _info "Checking TrueNAS health..."
  174. _ws_response=$(_ws_call "system.ready" | tr '[:lower:]' '[:upper:]')
  175. _ws_ret=$?
  176. if [ $_ws_ret -gt 0 ]; then
  177. _err "Error calling system.ready:"
  178. _err "$_ws_response"
  179. return $_ws_ret
  180. fi
  181. if [ "$_ws_response" != "TRUE" ]; then
  182. _err "TrueNAS is not ready."
  183. _err "Please check environment variables DEPLOY_TRUENAS_APIKEY, DEPLOY_TRUENAS_HOSTNAME and DEPLOY_TRUENAS_PROTOCOL."
  184. _err "Verify API key."
  185. return 2
  186. fi
  187. _savedeployconf DEPLOY_TRUENAS_APIKEY "$DEPLOY_TRUENAS_APIKEY"
  188. _info "TrueNAS health: OK"
  189. ########## System info
  190. _info "Gather system info..."
  191. _ws_response=$(_ws_call "system.info")
  192. _truenas_version=$(printf "%s" "$_ws_response" | jq -r '."version"')
  193. _info "TrueNAS version: $_truenas_version"
  194. ########## Gather current certificate
  195. _info "Gather current WebUI certificate..."
  196. _ws_response="$(_ws_call "system.general.config")"
  197. _ui_certificate_id=$(printf "%s" "$_ws_response" | jq -r '."ui_certificate"."id"')
  198. _ui_certificate_name=$(printf "%s" "$_ws_response" | jq -r '."ui_certificate"."name"')
  199. _info "Current WebUI certificate ID: $_ui_certificate_id"
  200. _info "Current WebUI certificate name: $_ui_certificate_name"
  201. ########## Upload new certificate
  202. _info "Upload new certificate..."
  203. _certname="acme_$(_utc_date | tr -d '\-\:' | tr ' ' '_')"
  204. _info "New WebUI certificate name: $_certname"
  205. _debug _certname "$_certname"
  206. _ws_out=$(_ws_upload_cert "$_file_fullchain" "$_file_key" "$_certname")
  207. echo "$_ws_out" | while IFS= read -r LINE; do
  208. case "$LINE" in
  209. I:*)
  210. _info "${LINE#I:}"
  211. ;;
  212. D:*)
  213. _debug "${LINE#D:}"
  214. ;;
  215. E*)
  216. _err "${LINE#E:}"
  217. ;;
  218. *) ;;
  219. esac
  220. done
  221. _new_certid=$(echo "$_ws_out" | grep 'R:' | cut -d ':' -f 2)
  222. _info "New certificate ID: $_new_certid"
  223. ########## FTP
  224. _info "Replace FTP certificate..."
  225. _ws_response=$(_ws_call "ftp.update" "{\"ssltls_certificate\": $_new_certid}")
  226. _ftp_certid=$(printf "%s" "$_ws_response" | jq -r '."ssltls_certificate"')
  227. if [ "$_ftp_certid" != "$_new_certid" ]; then
  228. _err "Cannot set FTP certificate."
  229. _debug "_ws_response" "$_ws_response"
  230. return 4
  231. fi
  232. ########## ix Apps (SCALE only)
  233. _info "Replace app certificates..."
  234. _ws_response=$(_ws_call "app.query")
  235. for _app_name in $(printf "%s" "$_ws_response" | jq -r '.[]."name"'); do
  236. _info "Checking app $_app_name..."
  237. _ws_response=$(_ws_call "app.config" "$_app_name")
  238. if [ "$(printf "%s" "$_ws_response" | jq -r '."network" | has("certificate_id")')" = "true" ]; then
  239. _info "App has certificate option, setup new certificate..."
  240. _info "App will be redeployed after updating the certificate."
  241. _ws_jobid=$(_ws_call "app.update" "$_app_name" "{\"values\": {\"network\": {\"certificate_id\": $_new_certid}}}")
  242. _debug "_ws_jobid" "$_ws_jobid"
  243. if ! _ws_check_jobid "$_ws_jobid"; then
  244. _err "No JobID returned from websocket method."
  245. return 3
  246. fi
  247. _ws_result=$(_ws_get_job_result "$_ws_jobid")
  248. _ws_ret=$?
  249. if [ $_ws_ret -gt 0 ]; then
  250. return $_ws_ret
  251. fi
  252. _debug "_ws_result" "$_ws_result"
  253. _info "App certificate replaced."
  254. else
  255. _info "App has no certificate option, skipping..."
  256. fi
  257. done
  258. ########## WebUI
  259. _info "Replace WebUI certificate..."
  260. _ws_response=$(_ws_call "system.general.update" "{\"ui_certificate\": $_new_certid}")
  261. _changed_certid=$(printf "%s" "$_ws_response" | jq -r '."ui_certificate"."id"')
  262. if [ "$_changed_certid" != "$_new_certid" ]; then
  263. _err "WebUI certificate change error.."
  264. return 5
  265. else
  266. _info "WebUI certificate replaced."
  267. fi
  268. _info "Restarting WebUI..."
  269. _ws_response=$(_ws_call "system.general.ui_restart")
  270. _info "Waiting for UI restart..."
  271. sleep 6
  272. ########## Certificates
  273. _info "Deleting old certificate..."
  274. _ws_jobid=$(_ws_call "certificate.delete" "$_ui_certificate_id")
  275. if ! _ws_check_jobid "$_ws_jobid"; then
  276. _err "No JobID returned from websocket method."
  277. return 3
  278. fi
  279. _ws_result=$(_ws_get_job_result "$_ws_jobid")
  280. _ws_ret=$?
  281. if [ $_ws_ret -gt 0 ]; then
  282. return $_ws_ret
  283. fi
  284. _info "Have a nice day...bye!"
  285. }