Repository navigation
OSAC-3011: add LVMS local storage provider for dev/CI environments #131
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
091dfbc
fc9a8ea
352d575
77fba73
8614eb4
84ea7e6
1506388
9c2da5d
09cae8f
bce9de0
e591fe6
e161f96
2dc2dbf
9297982
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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." | ||
| 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') }} |
| 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
| --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 }} | ||
Uh oh!
There was an error while loading. Please reload this page.