provisioning-catalog/providers/hetzner/templates/hetzner_storage_box.j2

172 lines
8.1 KiB
Django/Jinja

# 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_<key>
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 %}