# Load previous state if [ -f "$STATE_DIR/.env" ]; then source "$STATE_DIR/.env" fi {% if storage_boxes and storage_boxes|length > 0 %} echo "" echo "=== Processing Storage Boxes ===" {% for box in storage_boxes %} echo "" echo "Processing storage box: {{ box.name }} (type={{ box.type | default(value='bx11') }})" {# ── Step 1: Create if absent (idempotent: describe-then-create) ───────── #} if hcloud storage-box describe "{{ box.name }}" &>/dev/null; then echo " Storage Box '{{ box.name }}' already exists, skipping creation" else : "${STORAGEBOX_PASSWORD:?STORAGEBOX_PASSWORD must be set (inject from SOPS) before creating {{ box.name }}}" hcloud storage-box create \ --name "{{ box.name }}" \ --type "{{ box.type | default(value='bx11') }}" \ --location "{{ box.location | default(value='fsn1') }}" \ --password "$STORAGEBOX_PASSWORD" \ {%- if box.enable_ssh | default(value=true) %} --enable-ssh \ {%- endif %} {%- if box.enable_samba | default(value=false) %} --enable-samba \ {%- endif %} {%- if box.enable_webdav | default(value=false) %} --enable-webdav \ {%- endif %} {%- if box.enable_zfs | default(value=false) %} --enable-zfs \ {%- endif %} {%- if box.reachable_externally | default(value=false) %} --reachable-externally \ {%- endif %} {%- for key in box.ssh_keys | default(value=[]) %} --ssh-key "{{ key }}" \ {%- endfor %} {%- if box.labels is defined %} {%- for k, v in box.labels %} --label "{{ k }}={{ v }}" \ {%- endfor %} {%- endif %} --output json > "$STATE_DIR/storagebox_{{ box.name }}.json" echo " ✓ Storage Box '{{ box.name }}' created ({{ box.type | default(value='bx11') }})" fi {# ── Step 1b: Reconcile access settings — runs unconditionally (create or already-existed) so protocol toggles in servers.ncl take effect on boxes that already exist, not just at creation. hcloud storage-box update-access-settings applies live, no reboot/downtime. #} _access_out=$(hcloud storage-box update-access-settings "{{ box.name }}" \ --enable-ssh={{ box.enable_ssh | default(value=true) }} \ --enable-samba={{ box.enable_samba | default(value=false) }} \ --enable-webdav={{ box.enable_webdav | default(value=false) }} \ --enable-zfs={{ box.enable_zfs | default(value=false) }} \ --reachable-externally={{ box.reachable_externally | default(value=false) }} 2>&1) if [ $? -eq 0 ]; then echo " ✓ access settings reconciled (ssh={{ box.enable_ssh | default(value=true) }} samba={{ box.enable_samba | default(value=false) }} webdav={{ box.enable_webdav | default(value=false) }} external={{ box.reachable_externally | default(value=false) }})" else echo " ⚠ could not reconcile access settings for {{ box.name }}: $_access_out" fi {# ── Step 2: Delete protection ─────────────────────────────────────────── #} {%- if box.protection is defined and box.protection.delete %} _protect_out=$(hcloud storage-box enable-protection "{{ box.name }}" delete 2>&1) if [ $? -eq 0 ]; then echo " ✓ delete protection enabled" else echo " ⚠ could not set delete protection (already set?): $_protect_out" fi {%- endif %} {# ── Step 3: Subaccounts — per-purpose scoped access (protocol + path + credential). password_env_key is mandatory and explicit (not derived from home_directory) — each subaccount gets its OWN STORAGEBOX_SUB_PASSWORD_ env var, so a leaked SMB credential never also grants the restic SFTP path. #} {%- for sub in box.subaccounts | default(value=[]) %} echo " Subaccount home={{ sub.home_directory }} (readonly={{ sub.readonly | default(value=false) }})" _sub_exists=$(HJSON="$(hcloud storage-box subaccount list "{{ box.name }}" -o json 2>/dev/null)" \ nu -c '$env.HJSON | from json | (any {|s| ($s | get -o home_directory | default "") == "{{ sub.home_directory }}"}) | if $in { "True" } else { "False" }' 2>/dev/null || echo "False") if [ "$_sub_exists" = "True" ]; then echo " subaccount for {{ sub.home_directory }} already exists, skipping" else : "${STORAGEBOX_SUB_PASSWORD_{{ sub.password_env_key }}:?STORAGEBOX_SUB_PASSWORD_{{ sub.password_env_key }} must be set (inject from SOPS) for subaccount {{ sub.home_directory }} on {{ box.name }}}" # stdout+stderr merged into a variable (not streamed) — the nu wrapper that # runs this script only surfaces the bash process's OWN stderr when the # WHOLE script exits non-zero. A single failed subaccount must not abort # the rest of the reconcile, so the script keeps going — which means its # real error text has to be echoed inline (into stdout) or it's silently # dropped by the caller. See qa: storage-box-subaccount-error-swallowed. _create_out=$(hcloud storage-box subaccount create "{{ box.name }}" \ --home-directory "{{ sub.home_directory }}" \ --name "{{ sub.name | default(value=sub.home_directory) }}" \ --password "$STORAGEBOX_SUB_PASSWORD_{{ sub.password_env_key }}" \ {%- if sub.enable_ssh | default(value=true) %} --enable-ssh \ {%- endif %} {%- if sub.enable_samba | default(value=false) %} --enable-samba \ {%- endif %} {%- if sub.enable_webdav | default(value=false) %} --enable-webdav \ {%- endif %} {%- if sub.reachable_externally | default(value=false) %} --reachable-externally \ {%- endif %} {%- if sub.readonly | default(value=false) %} --readonly \ {%- endif %} {%- if sub.description %} --description "{{ sub.description }}" \ {%- endif %} --output json 2>&1) _create_rc=$? if [ $_create_rc -eq 0 ]; then echo "$_create_out" >> "$STATE_DIR/storagebox_{{ box.name }}_subs.json" echo " ✓ subaccount created for {{ sub.home_directory }}" else echo " ❌ subaccount create FAILED for {{ sub.home_directory }}: $_create_out" fi fi {# ── Reconcile subaccount access settings — runs unconditionally, same reasoning as Step 1b. NOTE: the subaccount identifier field name below (tries username, then id) is not verified against a live box — confirm with `hcloud storage-box subaccount list "{{ box.name }}" -o json` on first real run and adjust the extraction if it differs. #} _sub_ident=$(HJSON="$(hcloud storage-box subaccount list "{{ box.name }}" -o json 2>/dev/null)" \ nu -c '$env.HJSON | from json | where home_directory == "{{ sub.home_directory }}" | get -o 0 | if ($in == null) { "" } else { let m = $in; ($m | get -o username | default ($m | get -o id | default "" | into string)) }' 2>/dev/null) if [ -n "$_sub_ident" ]; then _reconcile_out=$(hcloud storage-box subaccount update-access-settings "{{ box.name }}" "$_sub_ident" \ --enable-ssh={{ sub.enable_ssh | default(value=true) }} \ --enable-samba={{ sub.enable_samba | default(value=false) }} \ --enable-webdav={{ sub.enable_webdav | default(value=false) }} \ --reachable-externally={{ sub.reachable_externally | default(value=false) }} \ --readonly={{ sub.readonly | default(value=false) }} 2>&1) if [ $? -eq 0 ]; then echo " ✓ access settings reconciled for {{ sub.home_directory }}" else echo " ⚠ could not reconcile access settings for {{ sub.home_directory }}: $_reconcile_out" fi {# name/description are set only at creation by `subaccount create`; `update` keeps them in sync declaratively for already-existing subaccounts. #} _meta_out=$(hcloud storage-box subaccount update "{{ box.name }}" "$_sub_ident" \ --name "{{ sub.name | default(value=sub.home_directory) }}" \ {%- if sub.description %} --description "{{ sub.description }}" \ {%- endif %} 2>&1) if [ $? -eq 0 ]; then echo " ✓ name/description reconciled for {{ sub.home_directory }}" else echo " ⚠ could not reconcile name/description for {{ sub.home_directory }}: $_meta_out" fi else echo " ⚠ could not resolve subaccount identifier for {{ sub.home_directory }} — skipping reconcile" fi {%- endfor %} _sb_key=$(echo "{{ box.name }}" | tr 'a-z' 'A-Z' | tr -c 'A-Z0-9' '_') echo "STORAGEBOX_${_sb_key}=ready" >> "$STATE_DIR/.env" {% endfor %} echo "" echo "=== Storage Box Deployment Complete ===" {% else %} echo "No storage boxes defined for this configuration." {% endif %}