Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
# Hub Secret prefix for per-tenant lifecycle markers.
lvms_storage_tenant_config_secret_prefix: "lvms-tenant-config-"

# Namespace for hub-cluster config Secrets — matches OSAC_STORAGE_CONFIG_NAMESPACE
# set in the storage-operations-ig pod spec (downward API metadata.namespace).
lvms_storage_config_namespace: "{{ lookup('env', 'OSAC_STORAGE_CONFIG_NAMESPACE') | default('osac-system', true) }}"

# topolvm StorageClass provisioner name.
lvms_storage_provisioner: "topolvm.io"

# LVMCluster device class name created by osac-installer's configure-lvms.sh hook.
lvms_storage_device_class: "vg1"
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
title: LVMS Storage Provider
description: >
Provisions per-tenant StorageClasses on the hub cluster using the LVMS (LVM Storage)
topolvm provisioner. No external backend or credentials required — LVMS is
installed on the hub by osac-installer when lvms.enabled=true. Hub cluster only;
CaaS guest cluster storage is out of scope.
template_type: storage_provider
implementation_strategy: lvms
capabilities:
provisioning_targets:
- vmaas
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
---
# LVMS ensure_storage_class: create per-tenant labeled StorageClass on the hub cluster.
# Uses the topolvm provisioner backed by the lvms-vg1 VolumeGroup created by
# osac-installer's configure-lvms.sh hook. One StorageClass per tier. Idempotent.

- name: Assert _provider_tiers is defined and non-empty
ansible.builtin.assert:
that:
- _provider_tiers is defined
- _provider_tiers | length > 0
fail_msg: >-
_provider_tiers must be provided by the service role dispatcher.

- name: Validate tenant_name is a valid DNS label
ansible.builtin.fail:
msg: "tenant_name '{{ tenant_name }}' is not a valid DNS label."
when: tenant_name is not regex('^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$')

- name: Build expected StorageClass entries
ansible.builtin.set_fact:
_lvms_expected_sc_entries: >-
{% set entries = [] -%}
{% for tier in _provider_tiers -%}
{% set _ = entries.append({'name': 'osac-' ~ tenant_name ~ '-' ~ tier.name, 'tier': tier.name}) -%}
{% endfor -%}
{{ entries }}
_lvms_expected_sc_names: >-
{% set names = [] -%}
{% for tier in _provider_tiers -%}
{% set _ = names.append('osac-' ~ tenant_name ~ '-' ~ tier.name) -%}
{% endfor -%}
{{ names }}

- name: Check existing tenant StorageClasses
kubernetes.core.k8s_info:
api_version: storage.k8s.io/v1
kind: StorageClass
label_selectors:
- "osac.openshift.io/tenant={{ tenant_name }}"
- "app.kubernetes.io/managed-by=osac-aap"
register: _lvms_sc_check

- name: Determine missing StorageClasses
ansible.builtin.set_fact:
_lvms_missing_sc_names: >-
{{ _lvms_expected_sc_names | difference(
_lvms_sc_check.resources | map(attribute='metadata.name') | list
) }}

- name: Create missing per-tenant StorageClasses
kubernetes.core.k8s:
state: present
definition:
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: "{{ item.name }}"
labels:
osac.openshift.io/tenant: "{{ tenant_name }}"
osac.openshift.io/storage-tier: "{{ item.tier }}"
app.kubernetes.io/managed-by: osac-aap
provisioner: "{{ lvms_storage_provisioner }}"
parameters:
"topolvm.io/device-class": "{{ lvms_storage_device_class }}"
reclaimPolicy: Delete
volumeBindingMode: WaitForFirstConsumer
loop: "{{ _lvms_expected_sc_entries | selectattr('name', 'in', _lvms_missing_sc_names) | list }}"
loop_control:
label: "{{ item.name }}"

- name: Set StorageClass names output
ansible.builtin.set_fact:
storage_provider_storage_class_names: "{{ _lvms_expected_sc_entries }}"
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
---
# LVMS setup: create a per-tenant hub Secret as a lifecycle marker.
# LVMS has no external backend credentials — the Secret exists solely so that
# the storage controller's hubSecretExists() check returns true, allowing it to
# advance from Stage 1 (StorageBackendReady) to Stage 2 (ensure_storage_class).
# Deleted by teardown_backend when the tenant is removed.

- name: Assert _provider_tiers is defined and non-empty
ansible.builtin.assert:
that:
- _provider_tiers is defined
- _provider_tiers | length > 0
fail_msg: >-
_provider_tiers must be provided by the service role dispatcher.
This role should not be called directly — use osac.service.storage_provider.

- name: Validate tenant_name is a valid DNS label
ansible.builtin.fail:
msg: "tenant_name '{{ tenant_name }}' is not a valid DNS label."
when: tenant_name is not regex('^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$')

- name: Create hub lifecycle marker Secret for tenant
kubernetes.core.k8s:
state: present
definition:
apiVersion: v1
kind: Secret
metadata:
name: "{{ lvms_storage_tenant_config_secret_prefix }}{{ tenant_name }}"
namespace: "{{ lvms_storage_config_namespace }}"
labels:
osac.openshift.io/tenant: "{{ tenant_name }}"
app.kubernetes.io/managed-by: osac-aap
stringData:
provider: lvms

- name: Set tenant config output (required by dispatcher)
ansible.builtin.set_fact:
storage_provider_tenant_config:
provider: lvms
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
# LVMS teardown_backend: remove the per-tenant hub lifecycle marker Secret.
# No external backend resources to clean up — LVMS is cluster-native.

- name: Validate tenant_name is a valid DNS label
ansible.builtin.fail:
msg: "tenant_name '{{ tenant_name }}' is not a valid DNS label."
when: tenant_name is not regex('^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$')

- name: Delete hub lifecycle marker Secret
kubernetes.core.k8s:
state: absent
api_version: v1
kind: Secret
name: "{{ lvms_storage_tenant_config_secret_prefix }}{{ tenant_name }}"
namespace: "{{ lvms_storage_config_namespace }}"
register: _lvms_teardown_secret_result
failed_when: false

- name: Warn if Secret deletion was unsuccessful
ansible.builtin.debug:
msg: "Warning: Secret deletion for tenant '{{ tenant_name }}' unsuccessful: {{ _lvms_teardown_secret_result.msg | default('unknown') }}"
when: _lvms_teardown_secret_result.failed | default(false)

- name: Report teardown_backend summary
ansible.builtin.debug:
msg: "Backend teardown for tenant '{{ tenant_name }}' complete."
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
---
# LVMS teardown_cluster_storage: remove per-tenant StorageClasses from the hub cluster.
# Finds SCs by label selector. The target cluster may be unreachable (being destroyed)
# so failures are tolerated and reported rather than fatal.

- name: Validate tenant_name is a valid DNS label
ansible.builtin.fail:
msg: "tenant_name '{{ tenant_name }}' is not a valid DNS label."
when: tenant_name is not regex('^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$')

- name: Clean up tenant StorageClasses
block:
- name: Find tenant StorageClasses by label
kubernetes.core.k8s_info:
api_version: storage.k8s.io/v1
kind: StorageClass
label_selectors:
- "osac.openshift.io/tenant={{ tenant_name }}"
- "app.kubernetes.io/managed-by=osac-aap"
register: _lvms_cleanup_sc_list

- name: Delete tenant StorageClasses
kubernetes.core.k8s:
state: absent
api_version: storage.k8s.io/v1
kind: StorageClass
name: "{{ item.metadata.name }}"
loop: "{{ _lvms_cleanup_sc_list.resources | default([]) }}"
loop_control:
label: "{{ item.metadata.name }}"
ignore_errors: true # noqa: ignore-errors

- name: Report teardown_cluster_storage summary
ansible.builtin.debug:
msg: >-
Teardown of cluster-side resources for tenant '{{ tenant_name }}' complete.
{{ _lvms_cleanup_sc_list.resources | default([]) | length }} StorageClass(es) removed.

rescue:
- name: Warn about teardown_cluster_storage failure
ansible.builtin.debug:
msg: >-
Teardown of cluster-side resources failed for tenant '{{ tenant_name }}'.
This may be expected if the target cluster is being destroyed.
Error: {{ ansible_failed_result.msg | default('unknown') }}
30 changes: 22 additions & 8 deletions osac-installer/charts/osac-prereqs/files/hooks/configure-lvms.sh
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,29 @@ done
echo "Waiting for lvms-operator deployment..."
oc wait --for=condition=Available deploy/lvms-operator -n openshift-storage --timeout=900s

echo "Applying LVMCluster configuration..."
oc apply -f /config/config.yaml
_sc_output=$(oc get sc lvms-vg1 --ignore-not-found -o name 2>&1) \
|| { echo "ERROR: failed to query StorageClasses: ${_sc_output}" >&2; exit 1; }
if [[ -n "${_sc_output}" ]]; then
# lvms-vg1 pre-exists (e.g. MOC, where it was installed by cluster admins).
# Skip both LVMCluster creation AND the default-class annotation: on shared clusters
# another StorageClass (e.g. Ceph) is already the intended default, and annotating
# lvms-vg1 here would silently override that.
echo "lvms-vg1 already exists, skipping LVMCluster creation and annotation."
else
echo "Applying LVMCluster configuration..."
oc apply -f /config/config.yaml

echo "Waiting for lvms-vg1 StorageClass..."
until [[ -n "$(oc get sc --ignore-not-found lvms-vg1 -o name)" ]]; do
sleep 5
done
echo "Waiting for lvms-vg1 StorageClass..."
for _attempt in $(seq 1 120); do
_sc_query=$(oc get sc --ignore-not-found lvms-vg1 -o name 2>&1) \
|| { echo "ERROR: oc get StorageClass failed: ${_sc_query}" >&2; exit 1; }
[[ -n "${_sc_query}" ]] && break
(( _attempt < 120 )) || { echo "ERROR: timed out waiting for lvms-vg1 StorageClass" >&2; exit 1; }
sleep 5
done

echo "Setting lvms-vg1 as default StorageClass..."
oc annotate sc lvms-vg1 storageclass.kubernetes.io/is-default-class=true --overwrite
echo "Setting lvms-vg1 as default StorageClass..."
oc annotate sc lvms-vg1 storageclass.kubernetes.io/is-default-class=true --overwrite
fi

echo "LVMS configuration complete."
125 changes: 125 additions & 0 deletions osac-installer/charts/osac/templates/hooks/register-local-storage.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
{{- if .Values.lvms.enabled }}
apiVersion: batch/v1
kind: Job
metadata:
name: register-local-storage
namespace: {{ .Release.Namespace }}
labels:
{{- include "osac.labels" . | nindent 4 }}
annotations:
"helm.sh/hook": post-install,post-upgrade
"helm.sh/hook-weight": "31"
"helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
spec:
backoffLimit: 5
# waitForFulfillment polls up to 60 × (30s curl + 10s sleep) = 2400s worst case.
# This deadline must exceed that to avoid premature termination.
activeDeadlineSeconds: 3000
template:
metadata:
labels:
{{- include "osac.labels" . | nindent 8 }}
spec:
serviceAccountName: admin
initContainers:
{{- include "osac.waitForFulfillment" . | nindent 6 }}
containers:
- name: register-storage
image: {{ .Values.cliImage }}
command:
- /bin/bash
- -euo
- pipefail
- -c
- |
AUTH_TOKEN=$(< /var/run/secrets/kubernetes.io/serviceaccount/token)
API="https://fulfillment-internal-api:8001/api/private/v1"

# Step 1: Create the local StorageBackend. Capture the ID whether
# this is the first install (2xx) or a re-install (409 = already exists).
BACKEND_RESP=$(mktemp)
BACKEND_CODE=$(curl -skS \
Comment on lines +38 to +41

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@zszabo-rh did you open a ticket for this based on the conversation with Roy here? Just making sure we don't lose track of any future improvements.

If you have created a ticket, please share the ticket-id with me in slack.
If you haven't yet and decide we should track this work, please open a task ticket in Jira, add the Component "Storage" and send it my way. Thanks.

--connect-timeout 5 --max-time 30 \
-o "${BACKEND_RESP}" -w '%{http_code}' \
-X POST "${API}/storage_backends" \
-H "Authorization: Bearer ${AUTH_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"metadata":{"name":"local"},"spec":{"provider":"lvms","endpoint":"n/a","credentials":{"username":"n/a","password":"n/a"}}}')

case "${BACKEND_CODE}" in
2??)
BACKEND_ID=$(python3 -c "import sys,json; print(json.load(open('${BACKEND_RESP}'))['id'])")
echo "StorageBackend 'local' created (id: ${BACKEND_ID})."
;;
409)
echo "StorageBackend 'local' already exists, verifying spec..."
EXISTING=$(curl -skS --connect-timeout 5 --max-time 30 \
"${API}/storage_backends" \
-H "Authorization: Bearer ${AUTH_TOKEN}")
BACKEND_ID=$(echo "${EXISTING}" | python3 -c "import sys,json; items=json.load(sys.stdin).get('items',[]); m=next((i for i in items if i.get('metadata',{}).get('name')=='local'),None); p=m.get('spec',{}).get('provider','') if m else ''; sys.stderr.write('ERROR: backend not found or wrong provider: '+p+'\n') or sys.exit(1) if not m or p!='lvms' else print(m['id'])")
echo "StorageBackend 'local' already registered with provider=lvms (id: ${BACKEND_ID})."
;;
*)
echo "ERROR: Failed to create StorageBackend 'local' (HTTP ${BACKEND_CODE}):" >&2
cat "${BACKEND_RESP}" >&2
rm -f "${BACKEND_RESP}"
exit 1
;;
esac
rm -f "${BACKEND_RESP}"

# Step 2: Create the local StorageTier referencing the backend by ID.
# Protocol 2 = STORAGE_PROTOCOL_BLOCK (LVMS provides block volumes).
TIER_BODY=$(printf '{"metadata":{"name":"local"},"spec":{"backends":[{"backend_id":"%s","protocol":2}]}}' "${BACKEND_ID}")
TIER_RESP=$(mktemp)
TIER_CODE=$(curl -skS \
--connect-timeout 5 --max-time 30 \
-o "${TIER_RESP}" -w '%{http_code}' \
-X POST "${API}/storage_tiers" \
-H "Authorization: Bearer ${AUTH_TOKEN}" \
-H "Content-Type: application/json" \
-d "${TIER_BODY}")

case "${TIER_CODE}" in
2??) echo "StorageTier 'local' created." ;;
409)
echo "StorageTier 'local' already exists, verifying backend reference..."
EXISTING_TIER=$(curl -skS --connect-timeout 5 --max-time 30 \
"${API}/storage_tiers" -H "Authorization: Bearer ${AUTH_TOKEN}")
echo "${EXISTING_TIER}" | python3 -c "import sys,json; items=json.load(sys.stdin).get('items',[]); m=next((t for t in items if t.get('metadata',{}).get('name')=='local'),None); sys.exit(1) if not m else None; ok=next((b for b in m.get('spec',{}).get('backends',[]) if b.get('backend_id')=='${BACKEND_ID}' and b.get('protocol')==2),None); sys.stderr.write('ERROR: tier does not reference backend ${BACKEND_ID}\n') or sys.exit(1) if not ok else print('ok')"
;;
*)
echo "ERROR: Failed to create StorageTier 'local' (HTTP ${TIER_CODE}):" >&2
cat "${TIER_RESP}" >&2
rm -f "${TIER_RESP}"
exit 1
;;
esac
rm -f "${TIER_RESP}"

echo "Local storage registration complete."
env:
- name: HOME
value: /tmp
volumeMounts:
- name: tmp
mountPath: /tmp
resources:
requests:
cpu: 50m
memory: 128Mi
limits:
cpu: 200m
memory: 256Mi
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
seccompProfile:
type: RuntimeDefault
capabilities:
drop: ["ALL"]
volumes:
- name: tmp
emptyDir: {}
restartPolicy: OnFailure
{{- end }}
Loading
Loading