| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325 |
- #!/usr/bin/env sh
- # TrueNAS deploy script for SCALE/CORE using websocket
- # It is recommend to use a wildcard certificate
- #
- # Websocket Documentation: https://www.truenas.com/docs/api/scale_websocket_api.html
- #
- # Tested with TrueNAS Scale - Electric Eel 24.10
- # Changes certificate in the following services:
- # - Web UI
- # - FTP
- # - iX Apps
- #
- # The following environment variables must be set:
- # ------------------------------------------------
- #
- # # API KEY
- # # Use the folowing URL to create a new API token: <TRUENAS_HOSTNAME OR IP>/ui/apikeys
- # export DEPLOY_TRUENAS_APIKEY="<API_KEY_GENERATED_IN_THE_WEB_UI"
- #
- ### Private functions
- # Call websocket method
- # Usage:
- # _ws_response=$(_ws_call "math.dummycalc" "'{"x": 4, "y": 5}'")
- # _info "$_ws_response"
- #
- # Output:
- # {"z": 9}
- #
- # Arguments:
- # $@ - midclt arguments for call
- #
- # Returns:
- # JSON/JOBID
- _ws_call() {
- _debug "_ws_call arg1" "$1"
- _debug "_ws_call arg2" "$2"
- _debug "_ws_call arg3" "$3"
- if [ $# -eq 3 ]; then
- _ws_response=$(midclt -K "$DEPLOY_TRUENAS_APIKEY" call "$1" "$2" "$3")
- fi
- if [ $# -eq 2 ]; then
- _ws_response=$(midclt -K "$DEPLOY_TRUENAS_APIKEY" call "$1" "$2")
- fi
- if [ $# -eq 1 ]; then
- _ws_response=$(midclt -K "$DEPLOY_TRUENAS_APIKEY" call "$1")
- fi
- _debug "_ws_response" "$_ws_response"
- printf "%s" "$_ws_response"
- return 0
- }
- # Upload certificate with webclient api
- _ws_upload_cert() {
- /usr/bin/env python - <<EOF
- import sys
- from truenas_api_client import Client
- with Client() as c:
- ### Login with API key
- print("I:Trying to upload new certificate...")
- ret = c.call("auth.login_with_api_key", "${DEPLOY_TRUENAS_APIKEY}")
- if ret:
- ### upload certificate
- with open('$1', 'r') as file:
- fullchain = file.read()
- with open('$2', 'r') as file:
- privatekey = file.read()
- ret = c.call("certificate.create", {"name": "$3", "create_type": "CERTIFICATE_CREATE_IMPORTED", "certificate": fullchain, "privatekey": privatekey, "passphrase": ""}, job=True)
- print("R:" + str(ret["id"]))
- sys.exit(0)
- else:
- print("R:0")
- print("E:_ws_upload_cert error!")
- sys.exit(7)
- EOF
- return $?
- }
- # Check argument is a number
- # Usage:
- #
- # Output:
- # n/a
- #
- # Arguments:
- # $1 - Anything
- #
- # Returns:
- # 0: true
- # 1: false
- _ws_check_jobid() {
- case "$1" in
- [0-9]*)
- return 0
- ;;
- esac
- return 1
- }
- # Wait for job to finish and return result as JSON
- # Usage:
- # _ws_result=$(_ws_get_job_result "$_ws_jobid")
- # _new_certid=$(printf "%s" "$_ws_result" | jq -r '."id"')
- #
- # Output:
- # JSON result of the job
- #
- # Arguments:
- # $1 - JobID
- #
- # Returns:
- # n/a
- _ws_get_job_result() {
- while true; do
- sleep 2
- _ws_response=$(_ws_call "core.get_jobs" "[[\"id\", \"=\", $1]]")
- if [ "$(printf "%s" "$_ws_response" | jq -r '.[]."state"')" != "RUNNING" ]; then
- _ws_result="$(printf "%s" "$_ws_response" | jq '.[]."result"')"
- _debug "_ws_result" "$_ws_result"
- printf "%s" "$_ws_result"
- _ws_error="$(printf "%s" "$_ws_response" | jq '.[]."error"')"
- if [ "$_ws_error" != "null" ]; then
- _err "Job $1 failed:"
- _err "$_ws_error"
- return 7
- fi
- break
- fi
- done
- return 0
- }
- ########################
- ### Public functions ###
- ########################
- # truenas_ws_deploy
- #
- # Deploy new certificate to TrueNAS services
- #
- # Arguments
- # 1: Domain
- # 2: Key-File
- # 3: Certificate-File
- # 4: CA-File
- # 5: FullChain-File
- # Returns:
- # 0: Success
- # 1: Missing API Key
- # 2: TrueNAS not ready
- # 3: Not a JobID
- # 4: FTP cert error
- # 5: WebUI cert error
- # 6: Job error
- # 7: WS call error
- #
- truenas_ws_deploy() {
- _domain="$1"
- _file_key="$2"
- _file_cert="$3"
- _file_ca="$4"
- _file_fullchain="$5"
- _debug _domain "$_domain"
- _debug _file_key "$_file_key"
- _debug _file_cert "$_file_cert"
- _debug _file_ca "$_file_ca"
- _debug _file_fullchain "$_file_fullchain"
- ########## Environment check
- _info "Checking environment variables..."
- _getdeployconf DEPLOY_TRUENAS_APIKEY
- # Check API Key
- if [ -z "$DEPLOY_TRUENAS_APIKEY" ]; then
- _err "TrueNAS API key not found, please set the DEPLOY_TRUENAS_APIKEY environment variable."
- return 1
- fi
- _secure_debug2 DEPLOY_TRUENAS_APIKEY "$DEPLOY_TRUENAS_APIKEY"
- _info "Environment variables: OK"
- ########## Health check
- _info "Checking TrueNAS health..."
- _ws_response=$(_ws_call "system.ready" | tr '[:lower:]' '[:upper:]')
- _ws_ret=$?
- if [ $_ws_ret -gt 0 ]; then
- _err "Error calling system.ready:"
- _err "$_ws_response"
- return $_ws_ret
- fi
- if [ "$_ws_response" != "TRUE" ]; then
- _err "TrueNAS is not ready."
- _err "Please check environment variables DEPLOY_TRUENAS_APIKEY, DEPLOY_TRUENAS_HOSTNAME and DEPLOY_TRUENAS_PROTOCOL."
- _err "Verify API key."
- return 2
- fi
- _savedeployconf DEPLOY_TRUENAS_APIKEY "$DEPLOY_TRUENAS_APIKEY"
- _info "TrueNAS health: OK"
- ########## System info
- _info "Gather system info..."
- _ws_response=$(_ws_call "system.info")
- _truenas_version=$(printf "%s" "$_ws_response" | jq -r '."version"')
- _info "TrueNAS version: $_truenas_version"
- ########## Gather current certificate
- _info "Gather current WebUI certificate..."
- _ws_response="$(_ws_call "system.general.config")"
- _ui_certificate_id=$(printf "%s" "$_ws_response" | jq -r '."ui_certificate"."id"')
- _ui_certificate_name=$(printf "%s" "$_ws_response" | jq -r '."ui_certificate"."name"')
- _info "Current WebUI certificate ID: $_ui_certificate_id"
- _info "Current WebUI certificate name: $_ui_certificate_name"
- ########## Upload new certificate
- _info "Upload new certificate..."
- _certname="acme_$(_utc_date | tr -d '\-\:' | tr ' ' '_')"
- _info "New WebUI certificate name: $_certname"
- _debug _certname "$_certname"
- _ws_out=$(_ws_upload_cert "$_file_fullchain" "$_file_key" "$_certname")
- echo "$_ws_out" | while IFS= read -r LINE; do
- case "$LINE" in
- I:*)
- _info "${LINE#I:}"
- ;;
- D:*)
- _debug "${LINE#D:}"
- ;;
- E*)
- _err "${LINE#E:}"
- ;;
- *) ;;
- esac
- done
- _new_certid=$(echo "$_ws_out" | grep 'R:' | cut -d ':' -f 2)
- _info "New certificate ID: $_new_certid"
- ########## FTP
- _info "Replace FTP certificate..."
- _ws_response=$(_ws_call "ftp.update" "{\"ssltls_certificate\": $_new_certid}")
- _ftp_certid=$(printf "%s" "$_ws_response" | jq -r '."ssltls_certificate"')
- if [ "$_ftp_certid" != "$_new_certid" ]; then
- _err "Cannot set FTP certificate."
- _debug "_ws_response" "$_ws_response"
- return 4
- fi
- ########## ix Apps (SCALE only)
- _info "Replace app certificates..."
- _ws_response=$(_ws_call "app.query")
- for _app_name in $(printf "%s" "$_ws_response" | jq -r '.[]."name"'); do
- _info "Checking app $_app_name..."
- _ws_response=$(_ws_call "app.config" "$_app_name")
- if [ "$(printf "%s" "$_ws_response" | jq -r '."network" | has("certificate_id")')" = "true" ]; then
- _info "App has certificate option, setup new certificate..."
- _info "App will be redeployed after updating the certificate."
- _ws_jobid=$(_ws_call "app.update" "$_app_name" "{\"values\": {\"network\": {\"certificate_id\": $_new_certid}}}")
- _debug "_ws_jobid" "$_ws_jobid"
- if ! _ws_check_jobid "$_ws_jobid"; then
- _err "No JobID returned from websocket method."
- return 3
- fi
- _ws_result=$(_ws_get_job_result "$_ws_jobid")
- _ws_ret=$?
- if [ $_ws_ret -gt 0 ]; then
- return $_ws_ret
- fi
- _debug "_ws_result" "$_ws_result"
- _info "App certificate replaced."
- else
- _info "App has no certificate option, skipping..."
- fi
- done
- ########## WebUI
- _info "Replace WebUI certificate..."
- _ws_response=$(_ws_call "system.general.update" "{\"ui_certificate\": $_new_certid}")
- _changed_certid=$(printf "%s" "$_ws_response" | jq -r '."ui_certificate"."id"')
- if [ "$_changed_certid" != "$_new_certid" ]; then
- _err "WebUI certificate change error.."
- return 5
- else
- _info "WebUI certificate replaced."
- fi
- _info "Restarting WebUI..."
- _ws_response=$(_ws_call "system.general.ui_restart")
- _info "Waiting for UI restart..."
- sleep 6
- ########## Certificates
- _info "Deleting old certificate..."
- _ws_jobid=$(_ws_call "certificate.delete" "$_ui_certificate_id")
- if ! _ws_check_jobid "$_ws_jobid"; then
- _err "No JobID returned from websocket method."
- return 3
- fi
- _ws_result=$(_ws_get_job_result "$_ws_jobid")
- _ws_ret=$?
- if [ $_ws_ret -gt 0 ]; then
- return $_ws_ret
- fi
- _info "Have a nice day...bye!"
- }
|