Skip to content
This repository was archived by the owner on Sep 9, 2026. It is now read-only.
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
2 changes: 2 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,5 @@ jobs:

- name: Run integration tests
run: uv run make test
env:
STORAGE_TESTS_ENABLED: "true"
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,12 @@ context/
# Integration test generated files
tests/integration/kubeconfig-*

# Storage test infrastructure
tests/integration/integration_config.yml
tests/integration/.mock_vms_pid
tests/integration/.storage_env
tests/vast_vms_smoke_test.yml

# CLAUDE.md
CLAUDE.md
.claude/
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,17 @@ spec:
envFrom:
- secretRef:
name: cluster-fulfillment-ig
- secretRef:
name: storage-operations-ig
optional: true
- configMapRef:
name: storage-operations-ig
optional: true
env:
- name: OSAC_STORAGE_CONFIG_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: OSAC_COMPUTE_INSTANCE_OPERATIONS_NAMESPACE_DEFAULT
valueFrom:
fieldRef:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
apiVersion: v1
kind: Pod
metadata:
labels:
ansible_job: ''
spec:
serviceAccountName: osac-sa
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: ansible_job
operator: Exists
topologyKey: kubernetes.io/hostname
containers:
- image: >-
{{ aap_ee_image }}
name: worker
imagePullPolicy: Always
args:
- ansible-runner
- worker
- '--private-data-dir=/runner'
volumeMounts:
- name: kube-api-access
mountPath: /var/run/secrets/kubernetes.io/serviceaccount
readOnly: true
envFrom:
Comment thread
akshaynadkarni marked this conversation as resolved.
- secretRef:
name: storage-operations-ig
- configMapRef:
name: storage-operations-ig
optional: true
env:
- name: OSAC_STORAGE_CONFIG_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
volumes:
- name: kube-api-access
projected:
sources:
- serviceAccountToken:
path: token
expirationSeconds: 3600
- configMap:
name: kube-root-ca.crt
items:
- key: ca.crt
path: ca.crt
- downwardAPI:
items:
- path: namespace
fieldRef:
apiVersion: v1
fieldPath: metadata.namespace
- configMap:
name: openshift-service-ca.crt
items:
- key: service-ca.crt
path: service-ca.crt
defaultMode: 420
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
gateway_settings:

Check warning on line 2 in collections/ansible_collections/osac/config_as_code/roles/aap/vars/controller.yml

View workflow job for this annotation

GitHub Actions / ansible-lint

var-naming[no-role-prefix]

Variables names from within roles should use aap_ as a prefix. (vars: gateway_settings)
gateway_access_token_expiration: 7200

# Create a project from the specified git repo
Expand Down Expand Up @@ -327,6 +327,34 @@
allow_simultaneous: true
ask_variables_on_launch: true
verbosity: 0
# Storage operations use allow_simultaneous: false to prevent concurrent tenant
# provisioning races on VAST resources. This serializes all tenant operations
# through a single AAP worker. Acceptable for current scale; for high-volume
# tenant provisioning, consider per-tenant locking instead.
- name: "{{ aap_prefix }}-create-org"
project: "{{ aap_prefix }}"
organization: "{{ aap_organization_name }}"
job_type: run
playbook: "playbook_osac_configure_tenant_storage.yml"
inventory: "{{ aap_prefix }}-storage-operations"
execution_environment: "{{ aap_prefix }}-ee"
instance_groups:
- "{{ aap_prefix }}-storage-operations-ig"
allow_simultaneous: false
ask_variables_on_launch: true
verbosity: 0
- name: "{{ aap_prefix }}-delete-org"
project: "{{ aap_prefix }}"
organization: "{{ aap_organization_name }}"
job_type: run
playbook: "playbook_osac_delete_tenant_storage.yml"
inventory: "{{ aap_prefix }}-storage-operations"
execution_environment: "{{ aap_prefix }}-ee"
instance_groups:
- "{{ aap_prefix }}-storage-operations-ig"
allow_simultaneous: false
ask_variables_on_launch: true
verbosity: 0

controller_job_template_surveys: # noqa: var-naming[no-role-prefix]
- name: "{{ aap_prefix }}-create-hosted-cluster-post-install"
Expand Down Expand Up @@ -431,6 +459,9 @@
- name: "{{ aap_prefix }}-bare-metal-fulfillment"
description: "Bare Metal Fulfillment Inventory"
organization: "{{ aap_organization_name }}"
- name: "{{ aap_prefix }}-storage-operations"
description: "Storage Operations Inventory"
organization: "{{ aap_organization_name }}"

controller_inventory_sources: # noqa: var-naming[no-role-prefix]
- name: "{{ aap_prefix }}-cluster-fulfillment-is"
Expand Down Expand Up @@ -487,6 +518,15 @@
overwrite: true
overwrite_vars: true
update_cache_timeout: 0
- name: "{{ aap_prefix }}-storage-operations-is"
organization: "{{ aap_organization_name }}"
source: scm
source_project: "{{ aap_prefix }}"
source_path: "inventory/localhost.yml"
inventory: "{{ aap_prefix }}-storage-operations"
overwrite: true
overwrite_vars: true
update_cache_timeout: 0

controller_schedules: # noqa: var-naming[no-role-prefix]
# Sync project every 10min to get the latest updates from the git repository
Expand Down Expand Up @@ -826,3 +866,7 @@
- key: clouds.yaml
path: clouds.yaml
defaultMode: 420

- name: "{{ aap_prefix }}-storage-operations-ig"
is_container_group: true
pod_spec_override: "{{ lookup('ansible.builtin.template', 'storage-operations-ig.j2') }}"
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,7 @@ class TemplateTypeEnum(StrEnum):
cluster = "cluster"
compute_instance = "compute_instance"
network = "network"
storage_provider = "storage_provider"


class NetworkClassCapabilities(Base):
Expand Down Expand Up @@ -536,6 +537,13 @@ def templates(self) -> Generator[BaseTemplate | NetworkClassTemplate, None, None
implementation_strategy=metadata.implementation_strategy,
capabilities=metadata.capabilities or NetworkClassCapabilities(),
)
elif metadata.template_type == TemplateTypeEnum.storage_provider:
# Storage provider roles are not yielded as compute instance or
# network templates — they are dispatched via osac.service.storage_provider.
display.vvv(
f"Skipping storage_provider role '{path.name}' in collection '{self.name}'"
)
continue
else:
yield ComputeInstanceTemplate(**common, spec_defaults=metadata.spec_defaults)
except Exception as e:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
---
argument_specs:
main:
short_description: Storage provider interface role
description: >
Dispatches storage operations to provider-specific template roles.
Validates inputs and enforces the provider and protocol allowlists
before any dynamic role name construction to prevent role-name injection.
options:
storage_provider_tiers:
type: list
required: true
description: >
List of storage tier definitions. Each tier declares its name, protocol,
provider, and optional QoS/quota settings. The dispatcher groups tiers
by provider and dispatches to each provider's template role with the
filtered tier subset.
Example: [{name: default, protocol: nfs, provider: vast,
qos_policy: default-qos,
qos_limits: {static_limits: {max_reads_bw_mbps: 100, max_writes_bw_mbps: 100}}}]
storage_provider_action:
type: str
required: true
choices:
- setup
- ensure_storage_class
- teardown
description: >
Which storage operation to perform. setup provisions provider-side
resources. ensure_storage_class creates K8s Secret and StorageClass
on the target cluster (JIT). teardown removes all provisioned resources.
storage_provider_provisioning_target:
type: str
required: true
choices:
- vmaas
- hcp_control_plane
- hcp_worker_root
- hcp_data_plane
description: >
Provisioning context. vmaas is the default VMaaS path.
CaaS targets (hcp_control_plane, hcp_worker_root, hcp_data_plane)
route to HyperShift integration points (stubbed until HyperShift
storage integration is available).
storage_provider_block_encryption_passphrase:
type: str
required: false
default: ""
description: >
Passphrase for block encryption. Passed from the Tenant CR event payload.
When empty, block StorageClasses are created without host encryption.
storage_provider_snapshots_enabled:
type: bool
required: false
default: true
description: >
Create a VolumeSnapshotClass alongside the StorageClass for K8s-native
snapshot support. Skipped if the VolumeSnapshot CRD is not installed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
---
# Thin wrapper for per-provider dispatch. Called via include_tasks with loop
# because Ansible does not support loop: on include_role directly.
#
# Receives from caller (via loop_var or set_fact):
# _current_provider — the provider name for this iteration (e.g. 'vast')
# _dispatch_action — task file to invoke (setup, ensure_storage_class, teardown)
#
# Uses from play scope:
# storage_provider_tiers — full tier list (filtered here per provider)
# storage_provider_provisioning_target — provisioning target enum

- name: "Compute filtered tiers for current provider"
ansible.builtin.set_fact:
_computed_provider_tiers: >-
{{ (storage_provider_tiers | selectattr('provider', 'equalto', _current_provider) | list)
| selectattr('name', 'in', _requested_tiers) | list
if (_requested_tiers is defined and _requested_tiers | length > 0)
else (storage_provider_tiers | selectattr('provider', 'equalto', _current_provider) | list) }}

- name: "Fail if requested tier filter produced no matches"
when: >-
_requested_tiers is defined and _requested_tiers | length > 0 and
_computed_provider_tiers | length == 0
ansible.builtin.fail:
msg: >-
None of the requested tiers ({{ _requested_tiers | join(', ') }}) match
provider '{{ _current_provider }}' tiers. Check STORAGE_TIERS configuration.

- name: "Dispatch provider storage action"
ansible.builtin.include_role:
name: "osac.templates.{{ _current_provider }}_storage"
tasks_from: "{{ _dispatch_action }}"
Comment thread
akshaynadkarni marked this conversation as resolved.
public: true
vars:
_provider_tiers: "{{ _computed_provider_tiers }}"
_provisioning_target: "{{ storage_provider_provisioning_target }}"

- name: "Accumulate tenant config after dispatch (setup only)"
ansible.builtin.set_fact:
_all_tenant_configs: >-
{{ _all_tenant_configs | combine({_current_provider: storage_provider_tenant_config}) }}
when:
- _dispatch_action == 'setup'
- storage_provider_tenant_config is defined
- storage_provider_tenant_config is mapping

- name: "Accumulate StorageClass names after dispatch (ensure_storage_class only)"
ansible.builtin.set_fact:
_all_sc_names: "{{ _all_sc_names + (storage_provider_storage_class_names | default([])) }}"
when:
- _dispatch_action == 'ensure_storage_class'
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
# Dispatches ensure_storage_class to each unique provider in the tier list.
# Accumulates StorageClass names across all providers into a single list.

- name: Initialize StorageClass name accumulator
ansible.builtin.set_fact:
_all_sc_names: []

- name: Dispatch ensure_storage_class to each provider
block:
- name: Run ensure_storage_class per provider
ansible.builtin.include_tasks: _dispatch_provider.yaml
loop: "{{ _unique_providers }}"
loop_control:
loop_var: _current_provider

- name: Set StorageClass names output (list)
ansible.builtin.set_fact:
storage_provider_storage_class_names: "{{ _all_sc_names }}"
Loading
Loading